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 {