diff --git a/resources/images/DonateIconWhite.png b/resources/images/DonateGoldIconWhite.png similarity index 100% rename from resources/images/DonateIconWhite.png rename to resources/images/DonateGoldIconWhite.png diff --git a/resources/images/DonateIconWhite.svg b/resources/images/DonateGoldIconWhite.svg similarity index 100% rename from resources/images/DonateIconWhite.svg rename to resources/images/DonateGoldIconWhite.svg diff --git a/resources/images/DonateTroopIconWhite.svg b/resources/images/DonateTroopIconWhite.svg new file mode 100644 index 000000000..2921ec8cd --- /dev/null +++ b/resources/images/DonateTroopIconWhite.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/extra/DonateTroopIconWhite.svg b/resources/images/extra/DonateTroopIconWhite.svg new file mode 100644 index 000000000..b3883037c --- /dev/null +++ b/resources/images/extra/DonateTroopIconWhite.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/extra/DonateTroopIconWhiteUnder_2.svg b/resources/images/extra/DonateTroopIconWhiteUnder_2.svg new file mode 100644 index 000000000..4ccec88b4 --- /dev/null +++ b/resources/images/extra/DonateTroopIconWhiteUnder_2.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/extra/DonateTroopIconWhiteUnder_3.svg b/resources/images/extra/DonateTroopIconWhiteUnder_3.svg new file mode 100644 index 000000000..130c329c7 --- /dev/null +++ b/resources/images/extra/DonateTroopIconWhiteUnder_3.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/extra/DonateTroopIconWhite_3.svg b/resources/images/extra/DonateTroopIconWhite_3.svg new file mode 100644 index 000000000..4c2b82ec2 --- /dev/null +++ b/resources/images/extra/DonateTroopIconWhite_3.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/extra/gunAndSoldier.svg b/resources/images/extra/gunAndSoldier.svg new file mode 100644 index 000000000..bbdc734be --- /dev/null +++ b/resources/images/extra/gunAndSoldier.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/client/Transport.ts b/src/client/Transport.ts index a4f0db599..3d1eb885d 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -96,7 +96,15 @@ export class SendEmojiIntentEvent implements GameEvent { ) {} } -export class SendDonateIntentEvent implements GameEvent { +export class SendDonateGoldIntentEvent implements GameEvent { + constructor( + public readonly sender: PlayerView, + public readonly recipient: PlayerView, + public readonly gold: number | null, + ) {} +} + +export class SendDonateTroopsIntentEvent implements GameEvent { constructor( public readonly sender: PlayerView, public readonly recipient: PlayerView, @@ -187,7 +195,12 @@ export class Transport { this.onSendTargetPlayerIntent(e), ); this.eventBus.on(SendEmojiIntentEvent, (e) => this.onSendEmojiIntent(e)); - this.eventBus.on(SendDonateIntentEvent, (e) => this.onSendDonateIntent(e)); + this.eventBus.on(SendDonateGoldIntentEvent, (e) => + this.onSendDonateGoldIntent(e), + ); + this.eventBus.on(SendDonateTroopsIntentEvent, (e) => + this.onSendDonateTroopIntent(e), + ); this.eventBus.on(SendEmbargoIntentEvent, (e) => this.onSendEmbargoIntent(e), ); @@ -425,9 +438,18 @@ export class Transport { }); } - private onSendDonateIntent(event: SendDonateIntentEvent) { + private onSendDonateGoldIntent(event: SendDonateGoldIntentEvent) { this.sendIntent({ - type: "donate", + type: "donate_gold", + clientID: this.lobbyConfig.clientID, + recipient: event.recipient.id(), + gold: event.gold, + }); + } + + private onSendDonateTroopIntent(event: SendDonateTroopsIntentEvent) { + this.sendIntent({ + type: "donate_troops", clientID: this.lobbyConfig.clientID, recipient: event.recipient.id(), troops: event.troops, diff --git a/src/client/graphics/layers/PlayerPanel.ts b/src/client/graphics/layers/PlayerPanel.ts index a6c153a42..0e34b6508 100644 --- a/src/client/graphics/layers/PlayerPanel.ts +++ b/src/client/graphics/layers/PlayerPanel.ts @@ -15,13 +15,15 @@ import { TileRef } from "../../../core/game/GameMap"; import { renderNumber, renderTroops } from "../../Utils"; import targetIcon from "../../../../resources/images/TargetIconWhite.svg"; import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg"; -import donateIcon from "../../../../resources/images/DonateIconWhite.svg"; +import donateTroopIcon from "../../../../resources/images/DonateTroopIconWhite.svg"; +import donateGoldIcon from "../../../../resources/images/DonateGoldIconWhite.svg"; import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg"; import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg"; import { SendAllianceRequestIntentEvent, SendBreakAllianceIntentEvent, - SendDonateIntentEvent, + SendDonateGoldIntentEvent, + SendDonateTroopsIntentEvent, SendEmojiIntentEvent, SendTargetPlayerIntentEvent, SendEmbargoIntentEvent, @@ -77,9 +79,23 @@ export class PlayerPanel extends LitElement implements Layer { this.hide(); } - private handleDonateClick(e: Event, myPlayer: PlayerView, other: PlayerView) { + private handleDonateTroopClick( + e: Event, + myPlayer: PlayerView, + other: PlayerView, + ) { e.stopPropagation(); - this.eventBus.emit(new SendDonateIntentEvent(myPlayer, other, null)); + this.eventBus.emit(new SendDonateTroopsIntentEvent(myPlayer, other, null)); + this.hide(); + } + + private handleDonateGoldClick( + e: Event, + myPlayer: PlayerView, + other: PlayerView, + ) { + e.stopPropagation(); + this.eventBus.emit(new SendDonateGoldIntentEvent(myPlayer, other, null)); this.hide(); } @@ -302,12 +318,24 @@ export class PlayerPanel extends LitElement implements Layer { : ""} ${canDonate ? html`` + : ""} + ${canDonate + ? html`` : ""} ${canSendEmoji diff --git a/src/client/graphics/layers/RadialMenu.ts b/src/client/graphics/layers/RadialMenu.ts index 168a4918b..2fabcaaec 100644 --- a/src/client/graphics/layers/RadialMenu.ts +++ b/src/client/graphics/layers/RadialMenu.ts @@ -18,7 +18,8 @@ import { SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, - SendDonateIntentEvent, + SendDonateTroopsIntentEvent, + SendDonateGoldIntentEvent, SendEmojiIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent, diff --git a/src/client/styles.css b/src/client/styles.css index 724760f7b..3d2e5c99f 100644 --- a/src/client/styles.css +++ b/src/client/styles.css @@ -360,8 +360,8 @@ label.option-card:hover { } #helpModal .donate-icon { - mask: url("../../resources/images/DonateIconWhite.svg") no-repeat center / - cover; + mask: url("../../resources/images/DonateTroopIconWhite.svg") no-repeat + center / cover; } #helpModal .build-icon { diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 8c15d083e..002273531 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -23,7 +23,8 @@ export type Intent = | BreakAllianceIntent | TargetPlayerIntent | EmojiIntent - | DonateIntent + | DonateGoldIntent + | DonateTroopsIntent | TargetTroopRatioIntent | BuildUnitIntent | EmbargoIntent @@ -40,7 +41,8 @@ export type AllianceRequestReplyIntent = z.infer< export type BreakAllianceIntent = z.infer; export type TargetPlayerIntent = z.infer; export type EmojiIntent = z.infer; -export type DonateIntent = z.infer; +export type DonateGoldIntent = z.infer; +export type DonateTroopsIntent = z.infer; export type EmbargoIntent = z.infer; export type TargetTroopRatioIntent = z.infer< typeof TargetTroopRatioIntentSchema @@ -232,8 +234,14 @@ export const EmbargoIntentSchema = BaseIntentSchema.extend({ action: z.union([z.literal("start"), z.literal("stop")]), }); -export const DonateIntentSchema = BaseIntentSchema.extend({ - type: z.literal("donate"), +export const DonateGoldIntentSchema = BaseIntentSchema.extend({ + type: z.literal("donate_gold"), + recipient: ID, + gold: z.number().nullable(), +}); + +export const DonateTroopIntentSchema = BaseIntentSchema.extend({ + type: z.literal("donate_troops"), recipient: ID, troops: z.number().nullable(), }); @@ -271,7 +279,8 @@ const IntentSchema = z.union([ BreakAllianceIntentSchema, TargetPlayerIntentSchema, EmojiIntentSchema, - DonateIntentSchema, + DonateGoldIntentSchema, + DonateTroopIntentSchema, TargetTroopRatioIntentSchema, BuildUnitIntentSchema, EmbargoIntentSchema, diff --git a/src/core/execution/DonateGoldExecution.ts b/src/core/execution/DonateGoldExecution.ts new file mode 100644 index 000000000..9a921e76b --- /dev/null +++ b/src/core/execution/DonateGoldExecution.ts @@ -0,0 +1,58 @@ +import { consolex } from "../Consolex"; +import { Execution, Game, Player, PlayerID, Gold } from "../game/Game"; + +export class DonateGoldExecution implements Execution { + private sender: Player; + private recipient: Player; + + private active = true; + + constructor( + private senderID: PlayerID, + private recipientID: PlayerID, + private gold: number | null, + ) {} + + init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this.senderID)) { + console.warn(`DonateExecution: sender ${this.senderID} not found`); + this.active = false; + return; + } + if (!mg.hasPlayer(this.recipientID)) { + console.warn(`DonateExecution recipient ${this.recipientID} not found`); + this.active = false; + return; + } + + this.sender = mg.player(this.senderID); + this.recipient = mg.player(this.recipientID); + if (this.gold == null) { + this.gold = Math.round(this.sender.gold() / 3); + } + } + + tick(ticks: number): void { + if (this.sender.canDonate(this.recipient)) { + this.sender.donateGold(this.recipient, this.gold); + this.recipient.updateRelation(this.sender, 50); + } else { + consolex.warn( + `cannot send gold from ${this.sender.name()} to ${this.recipient.name()}`, + ); + } + this.active = false; + } + + owner(): Player { + return null; + } + + isActive(): boolean { + return this.active; + } + + activeDuringSpawnPhase(): boolean { + return false; + } +} diff --git a/src/core/execution/DonateExecution.ts b/src/core/execution/DonateTroopExecution.ts similarity index 86% rename from src/core/execution/DonateExecution.ts rename to src/core/execution/DonateTroopExecution.ts index 70012b0e3..324ed07a8 100644 --- a/src/core/execution/DonateExecution.ts +++ b/src/core/execution/DonateTroopExecution.ts @@ -1,7 +1,7 @@ import { consolex } from "../Consolex"; -import { Execution, Game, Player, PlayerID } from "../game/Game"; +import { Execution, Game, Player, PlayerID, Gold } from "../game/Game"; -export class DonateExecution implements Execution { +export class DonateTroopsExecution implements Execution { private sender: Player; private recipient: Player; @@ -34,7 +34,7 @@ export class DonateExecution implements Execution { tick(ticks: number): void { if (this.sender.canDonate(this.recipient)) { - this.sender.donate(this.recipient, this.troops); + this.sender.donateTroops(this.recipient, this.troops); this.recipient.updateRelation(this.sender, 50); } else { consolex.warn( diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index d5ccc4c56..630c642f4 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -29,7 +29,8 @@ import { AllianceRequestReplyExecution } from "./alliance/AllianceRequestReplyEx import { BreakAllianceExecution } from "./alliance/BreakAllianceExecution"; import { TargetPlayerExecution } from "./TargetPlayerExecution"; import { EmojiExecution } from "./EmojiExecution"; -import { DonateExecution } from "./DonateExecution"; +import { DonateTroopsExecution } from "./DonateTroopExecution"; +import { DonateGoldExecution } from "./DonateGoldExecution"; import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution"; import { ConstructionExecution } from "./ConstructionExecution"; import { fixProfaneUsername, isProfaneUsername } from "../validations/username"; @@ -111,8 +112,14 @@ export class Executor { return new TargetPlayerExecution(playerID, intent.target); case "emoji": return new EmojiExecution(playerID, intent.recipient, intent.emoji); - case "donate": - return new DonateExecution(playerID, intent.recipient, intent.troops); + case "donate_troops": + return new DonateTroopsExecution( + playerID, + intent.recipient, + intent.troops, + ); + case "donate_gold": + return new DonateGoldExecution(playerID, intent.recipient, intent.gold); case "troop_ratio": return new SetTargetTroopRatioExecution(playerID, intent.ratio); case "embargo": diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 09613874b..7d96afd7e 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -384,7 +384,8 @@ export interface Player { // Donation canDonate(recipient: Player): boolean; - donate(recipient: Player, troops: number): void; + donateTroops(recipient: Player, troops: number): void; + donateGold(recipient: Player, gold: number): void; // Embargo hasEmbargoAgainst(other: Player): boolean; diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index e3ae5acf0..d84e0a1ae 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -39,7 +39,7 @@ import { import { CellString, GameImpl } from "./GameImpl"; import { UnitImpl } from "./UnitImpl"; import { MessageType } from "./Game"; -import { renderTroops } from "../../client/Utils"; +import { renderTroops, renderNumber } from "../../client/Utils"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; import { andFN, manhattanDistFN, TileRef } from "./GameMap"; import { AttackImpl } from "./AttackImpl"; @@ -523,7 +523,7 @@ export class PlayerImpl implements Player { return true; } - donate(recipient: Player, troops: number): void { + donateTroops(recipient: Player, troops: number): void { this.sentDonations.push(new Donation(recipient, this.mg.ticks())); recipient.addTroops(this.removeTroops(troops)); this.mg.displayMessage( @@ -537,6 +537,20 @@ export class PlayerImpl implements Player { recipient.id(), ); } + donateGold(recipient: Player, gold: number): void { + this.sentDonations.push(new Donation(recipient, this.mg.ticks())); + recipient.addGold(this.removeGold(gold)); + this.mg.displayMessage( + `Sent ${renderNumber(gold)} gold to ${recipient.name()}`, + MessageType.INFO, + this.id(), + ); + this.mg.displayMessage( + `Recieved ${renderNumber(gold)} gold from ${this.name()}`, + MessageType.SUCCESS, + recipient.id(), + ); + } hasEmbargoAgainst(other: Player): boolean { return this.embargoes.has(other.id()); @@ -588,13 +602,13 @@ export class PlayerImpl implements Player { this._gold += toInt(toAdd); } - removeGold(toRemove: Gold): void { - if (toRemove > this._gold) { - throw Error( - `Player ${this} does not enough gold (${toRemove} vs ${this._gold}))`, - ); + removeGold(toRemove: Gold): number { + if (toRemove <= 1) { + return 0; } - this._gold -= toInt(toRemove); + const actualRemoved = minInt(this._gold, toInt(toRemove)); + this._gold -= actualRemoved; + return Number(actualRemoved); } population(): number {