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()); }