Implement Stop/Start trading with all (#2278)

## Description:
fixes #2275 

Added global Start/Stop trading; use your **player panel** to trigger
it.

<img width="370" height="540" alt="Screenshot 2025-10-23 184447"
src="https://github.com/user-attachments/assets/c3b7967e-ffdd-4f37-ba67-b60a602278ce"
/>

## Please complete the following:

- [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

## Please put your Discord username so you can be contacted if a bug or
regression is found:

abodcraft1
This commit is contained in:
Abdallah Bahrawi
2025-10-25 23:59:40 +03:00
committed by GitHub
parent 913e814b59
commit 380eab5d12
11 changed files with 139 additions and 0 deletions
+2
View File
@@ -606,6 +606,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",
+15
View File
@@ -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",
+42
View File
@@ -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 {
})
: ""}
</div>
${other === my
? html`<div class="grid auto-cols-fr grid-flow-col gap-1">
${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,
})}
</div>`
: ""}
</div>
`;
}
+1
View File
@@ -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)) {
+8
View File
@@ -43,6 +43,7 @@ export type Intent =
| QuickChatIntent
| MoveWarshipIntent
| MarkDisconnectedIntent
| EmbargoAllIntent
| UpgradeStructureIntent
| DeleteUnitIntent
| KickPlayerIntent;
@@ -51,6 +52,7 @@ export type AttackIntent = z.infer<typeof AttackIntentSchema>;
export type CancelAttackIntent = z.infer<typeof CancelAttackIntentSchema>;
export type SpawnIntent = z.infer<typeof SpawnIntentSchema>;
export type BoatAttackIntent = z.infer<typeof BoatAttackIntentSchema>;
export type EmbargoAllIntent = z.infer<typeof EmbargoAllIntentSchema>;
export type CancelBoatIntent = z.infer<typeof CancelBoatIntentSchema>;
export type AllianceRequestIntent = z.infer<typeof AllianceRequestIntentSchema>;
export type AllianceRequestReplyIntent = z.infer<
@@ -276,6 +278,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,
@@ -355,6 +362,7 @@ const IntentSchema = z.discriminatedUnion("type", [
BuildUnitIntentSchema,
UpgradeStructureIntentSchema,
EmbargoIntentSchema,
EmbargoAllIntentSchema,
MoveWarshipIntentSchema,
QuickChatIntentSchema,
AllianceExtensionIntentSchema,
+1
View File
@@ -131,6 +131,7 @@ export interface Config {
emojiMessageCooldown(): Tick;
emojiMessageDuration(): Tick;
donateCooldown(): Tick;
embargoAllCooldown(): Tick;
deletionMarkDuration(): Tick;
deleteUnitCooldown(): Tick;
defaultDonationAmount(sender: Player): number;
+3
View File
@@ -573,6 +573,9 @@ export class DefaultConfig implements Config {
donateCooldown(): Tick {
return 10 * 10;
}
embargoAllCooldown(): Tick {
return 10 * 10;
}
deletionMarkDuration(): Tick {
return 15 * 10;
}
+38
View File
@@ -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;
}
}
+3
View File
@@ -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": {
+3
View File
@@ -624,6 +624,8 @@ export interface Player {
donateGold(recipient: Player, gold: Gold): boolean;
canDeleteUnit(): boolean;
recordDeleteUnit(): void;
canEmbargoAll(): boolean;
recordEmbargoAll(): void;
// Embargo
hasEmbargoAgainst(other: Player): boolean;
@@ -746,6 +748,7 @@ export interface PlayerActions {
canAttack: boolean;
buildableUnits: BuildableUnit[];
canSendEmojiAllPlayers: boolean;
canEmbargoAll?: boolean;
interaction?: PlayerInteraction;
}
+23
View File
@@ -94,6 +94,7 @@ export class PlayerImpl implements Player {
private relations = new Map<Player, number>();
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());
}