From 0a2b6ed9a6a9b112ea9fce0b442613ac6644c6bd Mon Sep 17 00:00:00 2001
From: Abdallah Bahrawi <140177728+abdallahbahrawi1@users.noreply.github.com>
Date: Sat, 25 Oct 2025 23:59:40 +0300
Subject: [PATCH] Implement Stop/Start trading with all (#2278) fixes #2275
Added global Start/Stop trading; use your **player panel** to trigger
it.
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
regression is found:
abodcraft1
---
resources/lang/en.json | 2 ++
src/client/Transport.ts | 15 ++++++++
src/client/graphics/layers/PlayerPanel.ts | 42 +++++++++++++++++++++++
src/core/GameRunner.ts | 1 +
src/core/Schemas.ts | 8 +++++
src/core/configuration/Config.ts | 1 +
src/core/configuration/DefaultConfig.ts | 3 ++
src/core/execution/EmbargoAllExecution.ts | 38 ++++++++++++++++++++
src/core/execution/ExecutionManager.ts | 3 ++
src/core/game/Game.ts | 3 ++
src/core/game/PlayerImpl.ts | 23 +++++++++++++
11 files changed, 139 insertions(+)
create mode 100644 src/core/execution/EmbargoAllExecution.ts
diff --git a/resources/lang/en.json b/resources/lang/en.json
index 57a2c9de5..1f20d7d22 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -611,6 +611,8 @@
"nuke": "Nukes sent by them to you",
"start_trade": "Start Trading",
"stop_trade": "Stop Trading",
+ "stop_trade_all": "Stop Trading with All",
+ "start_trade_all": "Start Trading with All",
"alliances": "Alliances",
"flag": "Flag",
"chat": "Chat",
diff --git a/src/client/Transport.ts b/src/client/Transport.ts
index b5d9bf9b1..ac039f0da 100644
--- a/src/client/Transport.ts
+++ b/src/client/Transport.ts
@@ -132,6 +132,10 @@ export class SendEmbargoIntentEvent implements GameEvent {
) {}
}
+export class SendEmbargoAllIntentEvent implements GameEvent {
+ constructor(public readonly action: "start" | "stop") {}
+}
+
export class SendDeleteUnitIntentEvent implements GameEvent {
constructor(public readonly unitId: number) {}
}
@@ -226,6 +230,9 @@ export class Transport {
this.eventBus.on(SendEmbargoIntentEvent, (e) =>
this.onSendEmbargoIntent(e),
);
+ this.eventBus.on(SendEmbargoAllIntentEvent, (e) =>
+ this.onSendEmbargoAllIntent(e),
+ );
this.eventBus.on(BuildUnitIntentEvent, (e) => this.onBuildUnitIntent(e));
this.eventBus.on(PauseGameEvent, (e) => this.onPauseGameEvent(e));
@@ -528,6 +535,14 @@ export class Transport {
});
}
+ private onSendEmbargoAllIntent(event: SendEmbargoAllIntentEvent) {
+ this.sendIntent({
+ type: "embargo_all",
+ clientID: this.lobbyConfig.clientID,
+ action: event.action,
+ });
+ }
+
private onBuildUnitIntent(event: BuildUnitIntentEvent) {
this.sendIntent({
type: "build_unit",
diff --git a/src/client/graphics/layers/PlayerPanel.ts b/src/client/graphics/layers/PlayerPanel.ts
index 5ff2640a9..79a6d83ad 100644
--- a/src/client/graphics/layers/PlayerPanel.ts
+++ b/src/client/graphics/layers/PlayerPanel.ts
@@ -28,6 +28,7 @@ import { CloseViewEvent, MouseUpEvent } from "../../InputHandler";
import {
SendAllianceRequestIntentEvent,
SendBreakAllianceIntentEvent,
+ SendEmbargoAllIntentEvent,
SendEmbargoIntentEvent,
SendEmojiIntentEvent,
SendTargetPlayerIntentEvent,
@@ -223,6 +224,16 @@ export class PlayerPanel extends LitElement implements Layer {
this.hide();
}
+ private onStopTradingAllClick(e: Event) {
+ e.stopPropagation();
+ this.eventBus.emit(new SendEmbargoAllIntentEvent("start"));
+ }
+
+ private onStartTradingAllClick(e: Event) {
+ e.stopPropagation();
+ this.eventBus.emit(new SendEmbargoAllIntentEvent("stop"));
+ }
+
private handleEmojiClick(e: Event, myPlayer: PlayerView, other: PlayerView) {
e.stopPropagation();
this.emojiTable.showTable((emoji: string) => {
@@ -709,6 +720,37 @@ export class PlayerPanel extends LitElement implements Layer {
})
: ""}
+
+ ${other === my
+ ? html`
+ ${actionButton({
+ onClick: (e: MouseEvent) => this.onStopTradingAllClick(e),
+ icon: stopTradingIcon,
+ iconAlt: "Stop Trading With All",
+ title: !this.actions?.canEmbargoAll
+ ? `${translateText("player_panel.stop_trade_all")} - ${translateText("cooldown")}`
+ : translateText("player_panel.stop_trade_all"),
+ label: !this.actions?.canEmbargoAll
+ ? `${translateText("player_panel.stop_trade_all")} ⏳`
+ : translateText("player_panel.stop_trade_all"),
+ type: "yellow",
+ disabled: !this.actions?.canEmbargoAll,
+ })}
+ ${actionButton({
+ onClick: (e: MouseEvent) => this.onStartTradingAllClick(e),
+ icon: startTradingIcon,
+ iconAlt: "Start Trading With All",
+ title: !this.actions?.canEmbargoAll
+ ? `${translateText("player_panel.start_trade_all")} - ${translateText("cooldown")}`
+ : translateText("player_panel.start_trade_all"),
+ label: !this.actions?.canEmbargoAll
+ ? `${translateText("player_panel.start_trade_all")} ⏳`
+ : translateText("player_panel.start_trade_all"),
+ type: "green",
+ disabled: !this.actions?.canEmbargoAll,
+ })}
+
`
+ : ""}
`;
}
diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts
index 3a309359f..55b214877 100644
--- a/src/core/GameRunner.ts
+++ b/src/core/GameRunner.ts
@@ -189,6 +189,7 @@ export class GameRunner {
canAttack: tile !== null && player.canAttack(tile),
buildableUnits: player.buildableUnits(tile),
canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers),
+ canEmbargoAll: player.canEmbargoAll(),
} as PlayerActions;
if (tile !== null && this.game.hasOwner(tile)) {
diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts
index b13f8ac17..9f2dc53c7 100644
--- a/src/core/Schemas.ts
+++ b/src/core/Schemas.ts
@@ -43,6 +43,7 @@ export type Intent =
| QuickChatIntent
| MoveWarshipIntent
| MarkDisconnectedIntent
+ | EmbargoAllIntent
| UpgradeStructureIntent
| DeleteUnitIntent
| KickPlayerIntent;
@@ -51,6 +52,7 @@ export type AttackIntent = z.infer;
export type CancelAttackIntent = z.infer;
export type SpawnIntent = z.infer;
export type BoatAttackIntent = z.infer;
+export type EmbargoAllIntent = z.infer;
export type CancelBoatIntent = z.infer;
export type AllianceRequestIntent = z.infer;
export type AllianceRequestReplyIntent = z.infer<
@@ -275,6 +277,11 @@ export const EmbargoIntentSchema = BaseIntentSchema.extend({
action: z.union([z.literal("start"), z.literal("stop")]),
});
+export const EmbargoAllIntentSchema = BaseIntentSchema.extend({
+ type: z.literal("embargo_all"),
+ action: z.union([z.literal("start"), z.literal("stop")]),
+});
+
export const DonateGoldIntentSchema = BaseIntentSchema.extend({
type: z.literal("donate_gold"),
recipient: ID,
@@ -354,6 +361,7 @@ const IntentSchema = z.discriminatedUnion("type", [
BuildUnitIntentSchema,
UpgradeStructureIntentSchema,
EmbargoIntentSchema,
+ EmbargoAllIntentSchema,
MoveWarshipIntentSchema,
QuickChatIntentSchema,
AllianceExtensionIntentSchema,
diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts
index baea027af..e9efb64c3 100644
--- a/src/core/configuration/Config.ts
+++ b/src/core/configuration/Config.ts
@@ -130,6 +130,7 @@ export interface Config {
emojiMessageCooldown(): Tick;
emojiMessageDuration(): Tick;
donateCooldown(): Tick;
+ embargoAllCooldown(): Tick;
deleteUnitCooldown(): Tick;
defaultDonationAmount(sender: Player): number;
unitInfo(type: UnitType): UnitInfo;
diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts
index 8ddd390f7..722a3b8f8 100644
--- a/src/core/configuration/DefaultConfig.ts
+++ b/src/core/configuration/DefaultConfig.ts
@@ -570,6 +570,9 @@ export class DefaultConfig implements Config {
donateCooldown(): Tick {
return 10 * 10;
}
+ embargoAllCooldown(): Tick {
+ return 10 * 10;
+ }
deleteUnitCooldown(): Tick {
return 5 * 10;
}
diff --git a/src/core/execution/EmbargoAllExecution.ts b/src/core/execution/EmbargoAllExecution.ts
new file mode 100644
index 000000000..4a0fb731a
--- /dev/null
+++ b/src/core/execution/EmbargoAllExecution.ts
@@ -0,0 +1,38 @@
+import { Execution, Game, Player, PlayerType } from "../game/Game";
+
+export class EmbargoAllExecution implements Execution {
+ constructor(
+ private readonly player: Player,
+ private readonly action: "start" | "stop",
+ ) {}
+
+ init(mg: Game, _: number): void {
+ if (!this.player.canEmbargoAll()) {
+ return;
+ }
+ const me = this.player;
+ for (const p of mg.players()) {
+ if (p.id() === me.id()) continue;
+ if (p.type() === PlayerType.Bot) continue;
+ if (me.isOnSameTeam(p)) continue;
+
+ if (this.action === "start") {
+ if (!me.hasEmbargoAgainst(p)) me.addEmbargo(p, false);
+ } else {
+ if (me.hasEmbargoAgainst(p)) me.stopEmbargo(p);
+ }
+ }
+
+ this.player.recordEmbargoAll();
+ }
+
+ tick(_: number): void {}
+
+ isActive(): boolean {
+ return false;
+ }
+
+ activeDuringSpawnPhase(): boolean {
+ return false;
+ }
+}
diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts
index 67bd7c92d..6fadf0d6e 100644
--- a/src/core/execution/ExecutionManager.ts
+++ b/src/core/execution/ExecutionManager.ts
@@ -13,6 +13,7 @@ import { ConstructionExecution } from "./ConstructionExecution";
import { DeleteUnitExecution } from "./DeleteUnitExecution";
import { DonateGoldExecution } from "./DonateGoldExecution";
import { DonateTroopsExecution } from "./DonateTroopExecution";
+import { EmbargoAllExecution } from "./EmbargoAllExecution";
import { EmbargoExecution } from "./EmbargoExecution";
import { EmojiExecution } from "./EmojiExecution";
import { FakeHumanExecution } from "./FakeHumanExecution";
@@ -100,6 +101,8 @@ export class Executor {
return new DonateGoldExecution(player, intent.recipient, intent.gold);
case "embargo":
return new EmbargoExecution(player, intent.targetID, intent.action);
+ case "embargo_all":
+ return new EmbargoAllExecution(player, intent.action);
case "build_unit":
return new ConstructionExecution(player, intent.unit, intent.tile);
case "allianceExtension": {
diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts
index 8ae3f92b8..6cd9d2d94 100644
--- a/src/core/game/Game.ts
+++ b/src/core/game/Game.ts
@@ -621,6 +621,8 @@ export interface Player {
donateGold(recipient: Player, gold: Gold): boolean;
canDeleteUnit(): boolean;
recordDeleteUnit(): void;
+ canEmbargoAll(): boolean;
+ recordEmbargoAll(): void;
// Embargo
hasEmbargoAgainst(other: Player): boolean;
@@ -743,6 +745,7 @@ export interface PlayerActions {
canAttack: boolean;
buildableUnits: BuildableUnit[];
canSendEmojiAllPlayers: boolean;
+ canEmbargoAll?: boolean;
interaction?: PlayerInteraction;
}
diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts
index 75a3afcf9..7d52ec28d 100644
--- a/src/core/game/PlayerImpl.ts
+++ b/src/core/game/PlayerImpl.ts
@@ -94,6 +94,7 @@ export class PlayerImpl implements Player {
private relations = new Map();
private lastDeleteUnitTick: Tick = -1;
+ private lastEmbargoAllTick: Tick = -1;
public _incomingAttacks: Attack[] = [];
public _outgoingAttacks: Attack[] = [];
@@ -689,6 +690,28 @@ export class PlayerImpl implements Player {
this.lastDeleteUnitTick = this.mg.ticks();
}
+ canEmbargoAll(): boolean {
+ // Cooldown gate
+ if (
+ this.mg.ticks() - this.lastEmbargoAllTick <
+ this.mg.config().embargoAllCooldown()
+ ) {
+ return false;
+ }
+ // At least one eligible player exists
+ for (const p of this.mg.players()) {
+ if (p.id() === this.id()) continue;
+ if (p.type() === PlayerType.Bot) continue;
+ if (this.isOnSameTeam(p)) continue;
+ return true;
+ }
+ return false;
+ }
+
+ recordEmbargoAll(): void {
+ this.lastEmbargoAllTick = this.mg.ticks();
+ }
+
hasEmbargoAgainst(other: Player): boolean {
return this.embargoes.has(other.id());
}