From 745017aee2ef8258c79e0eacd39b3a25aacc931b Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Tue, 1 Apr 2025 02:51:05 +0900 Subject: [PATCH] Added Gold & Troop donate button (#373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors Changed the icon for the troop donation button, as it looked like it was donating gold. I replaced it with a soldier icon to better reflect its function. Additionally, I updated the icon below it to clearly represent gold donation instead. ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri The cat is because of an extension I have put in. Please ignore it. スクリーンショット 2025-03-30 10 13 06 スクリーンショット 2025-03-30 10 18 10 --------- Co-authored-by: Evan --- ...eIconWhite.png => DonateGoldIconWhite.png} | Bin ...eIconWhite.svg => DonateGoldIconWhite.svg} | 0 resources/images/DonateTroopIconWhite.svg | 36 +++++++++++ .../images/extra/DonateTroopIconWhite.svg | 16 +++++ .../extra/DonateTroopIconWhiteUnder_2.svg | 36 +++++++++++ .../extra/DonateTroopIconWhiteUnder_3.svg | 20 ++++++ .../images/extra/DonateTroopIconWhite_3.svg | 20 ++++++ resources/images/extra/gunAndSoldier.svg | 30 +++++++++ src/client/Transport.ts | 30 +++++++-- src/client/graphics/layers/PlayerPanel.ts | 40 ++++++++++-- src/client/graphics/layers/RadialMenu.ts | 3 +- src/client/styles.css | 4 +- src/core/Schemas.ts | 19 ++++-- src/core/execution/DonateGoldExecution.ts | 58 ++++++++++++++++++ ...teExecution.ts => DonateTroopExecution.ts} | 6 +- src/core/execution/ExecutionManager.ts | 13 +++- src/core/game/Game.ts | 3 +- src/core/game/PlayerImpl.ts | 30 ++++++--- 18 files changed, 331 insertions(+), 33 deletions(-) rename resources/images/{DonateIconWhite.png => DonateGoldIconWhite.png} (100%) rename resources/images/{DonateIconWhite.svg => DonateGoldIconWhite.svg} (100%) create mode 100644 resources/images/DonateTroopIconWhite.svg create mode 100644 resources/images/extra/DonateTroopIconWhite.svg create mode 100644 resources/images/extra/DonateTroopIconWhiteUnder_2.svg create mode 100644 resources/images/extra/DonateTroopIconWhiteUnder_3.svg create mode 100644 resources/images/extra/DonateTroopIconWhite_3.svg create mode 100644 resources/images/extra/gunAndSoldier.svg create mode 100644 src/core/execution/DonateGoldExecution.ts rename src/core/execution/{DonateExecution.ts => DonateTroopExecution.ts} (86%) 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 {