diff --git a/src/client/Transport.ts b/src/client/Transport.ts index eba9b82b0..b6f9bd9e4 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -132,6 +132,10 @@ export class CancelAttackIntentEvent implements GameEvent { ) {} } +export class CancelBoatIntentEvent implements GameEvent { + constructor(public readonly unitID: number) {} +} + export class SendSetTargetTroopRatioEvent implements GameEvent { constructor(public readonly ratio: number) {} } @@ -221,6 +225,10 @@ export class Transport { this.eventBus.on(CancelAttackIntentEvent, (e) => this.onCancelAttackIntentEvent(e), ); + this.eventBus.on(CancelBoatIntentEvent, (e) => + this.onCancelBoatIntentEvent(e), + ); + this.eventBus.on(MoveWarshipIntentEvent, (e) => { this.onMoveWarshipEvent(e); }); @@ -568,6 +576,14 @@ export class Transport { }); } + private onCancelBoatIntentEvent(event: CancelBoatIntentEvent) { + this.sendIntent({ + type: "cancel_boat", + clientID: this.lobbyConfig.clientID, + unitID: event.unitID, + }); + } + private onMoveWarshipEvent(event: MoveWarshipIntentEvent) { this.sendIntent({ type: "move_warship", diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index 89a2d4d15..c8452c505 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -26,6 +26,7 @@ import { import { ClientID } from "../../../core/Schemas"; import { CancelAttackIntentEvent, + CancelBoatIntentEvent, SendAllianceReplyIntentEvent, } from "../../Transport"; import { Layer } from "./Layer"; @@ -380,6 +381,12 @@ export class EventsDisplay extends LitElement implements Layer { this.eventBus.emit(new CancelAttackIntentEvent(myPlayer.id(), id)); } + emitBoatCancelIntent(id: number) { + const myPlayer = this.game.playerByClientID(this.clientID); + if (!myPlayer) return; + this.eventBus.emit(new CancelBoatIntentEvent(id)); + } + emitGoToPlayerEvent(attackerID: number) { const attacker = this.game.playerBySmallID(attackerID) as PlayerView; if (!attacker) return; @@ -572,25 +579,29 @@ export class EventsDisplay extends LitElement implements Layer { } private renderBoats() { - if (this.outgoingBoats.length === 0) { - return html``; - } - return html` ${this.outgoingBoats.length > 0 ? html` - + ${this.outgoingBoats.map( - (boats) => html` + (boat) => html` + ${!boat.retreating() + ? html`` + : "(retreating...)"} `, )} diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 174732941..c6d46a0b3 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -20,6 +20,7 @@ export type Intent = | AttackIntent | CancelAttackIntent | BoatAttackIntent + | CancelBoatIntent | AllianceRequestIntent | AllianceRequestReplyIntent | BreakAllianceIntent @@ -37,6 +38,7 @@ export type AttackIntent = z.infer; export type CancelAttackIntent = z.infer; export type SpawnIntent = z.infer; export type BoatAttackIntent = z.infer; +export type CancelBoatIntent = z.infer; export type AllianceRequestIntent = z.infer; export type AllianceRequestReplyIntent = z.infer< typeof AllianceRequestReplyIntentSchema @@ -196,6 +198,7 @@ const BaseIntentSchema = z.object({ "cancel_attack", "spawn", "boat", + "cancel_boat", "name", "targetPlayer", "emoji", @@ -294,6 +297,11 @@ export const CancelAttackIntentSchema = BaseIntentSchema.extend({ attackID: z.string(), }); +export const CancelBoatIntentSchema = BaseIntentSchema.extend({ + type: z.literal("cancel_boat"), + unitID: z.number(), +}); + export const MoveWarshipIntentSchema = BaseIntentSchema.extend({ type: z.literal("move_warship"), unitId: z.number(), @@ -318,6 +326,7 @@ const IntentSchema = z.union([ CancelAttackIntentSchema, SpawnIntentSchema, BoatAttackIntentSchema, + CancelBoatIntentSchema, AllianceRequestIntentSchema, AllianceRequestReplyIntentSchema, BreakAllianceIntentSchema, diff --git a/src/core/execution/BoatRetreatExecution.ts b/src/core/execution/BoatRetreatExecution.ts new file mode 100644 index 000000000..bcef746a4 --- /dev/null +++ b/src/core/execution/BoatRetreatExecution.ts @@ -0,0 +1,59 @@ +import { consolex } from "../Consolex"; +import { Execution, Game, Player, PlayerID, UnitType } from "../game/Game"; + +export class BoatRetreatExecution implements Execution { + private active = true; + private player: Player | undefined; + constructor( + private playerID: PlayerID, + private unitID: number, + ) {} + + init(mg: Game, ticks: number): void { + if (!mg.hasPlayer(this.playerID)) { + console.warn(`BoatRetreatExecution: Player ${this.playerID} not found`); + this.active = false; + return; + } + this.player = mg.player(this.playerID); + } + + tick(ticks: number): void { + if (!this.player) { + console.warn(`BoatRetreatExecution: Player ${this.playerID} not found`); + this.active = false; + return; + } + + const unit = this.player + .units() + .find( + (unit) => + unit.id() === this.unitID && unit.type() === UnitType.TransportShip, + ); + + if (!unit) { + consolex.warn(`Didn't find outgoing boat with id ${this.unitID}`); + this.active = false; + return; + } + + unit.orderBoatRetreat(); + this.active = false; + } + + owner(): Player { + if (this.player === undefined) { + throw new Error("Not initialized"); + } + return this.player; + } + + isActive(): boolean { + return this.active; + } + + activeDuringSpawnPhase(): boolean { + return false; + } +} diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index ab66789c1..230784921 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -7,6 +7,7 @@ import { AllianceRequestExecution } from "./alliance/AllianceRequestExecution"; import { AllianceRequestReplyExecution } from "./alliance/AllianceRequestReplyExecution"; import { BreakAllianceExecution } from "./alliance/BreakAllianceExecution"; import { AttackExecution } from "./AttackExecution"; +import { BoatRetreatExecution } from "./BoatRetreatExecution"; import { BotSpawner } from "./BotSpawner"; import { ConstructionExecution } from "./ConstructionExecution"; import { DonateGoldExecution } from "./DonateGoldExecution"; @@ -59,6 +60,8 @@ export class Executor { } case "cancel_attack": return new RetreatExecution(playerID, intent.attackID); + case "cancel_boat": + return new BoatRetreatExecution(playerID, intent.unitID); case "move_warship": return new MoveWarshipExecution(intent.unitId, intent.tile); case "spawn": diff --git a/src/core/execution/RetreatExecution.ts b/src/core/execution/RetreatExecution.ts index bfb865aa0..c40929adc 100644 --- a/src/core/execution/RetreatExecution.ts +++ b/src/core/execution/RetreatExecution.ts @@ -15,7 +15,7 @@ export class RetreatExecution implements Execution { init(mg: Game, ticks: number): void { if (!mg.hasPlayer(this.playerID)) { - console.warn(`RetreatExecution: player ${this.player.id()} not found`); + console.warn(`RetreatExecution: player ${this.playerID} not found`); return; } this.mg = mg; diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 97f2034a0..027f022c4 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -165,6 +165,10 @@ export class TransportShipExecution implements Execution { } this.lastMove = ticks; + if (this.boat.retreating()) { + this.dst = this.src!; // src is guaranteed to be set at this point + } + const result = this.pathFinder.nextTile(this.boat.tile(), this.dst); switch (result.type) { case PathFindResultType.Completed: diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 2d4c8bcec..1eace413d 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -348,6 +348,8 @@ export interface Unit { // Health hasHealth(): boolean; + retreating(): boolean; + orderBoatRetreat(): void; health(): number; modifyHealth(delta: number): void; diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index 94e0c01c5..bb080a694 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -73,6 +73,7 @@ export interface UnitUpdate { pos: TileRef; lastPos: TileRef; isActive: boolean; + retreating: boolean; targetUnitId?: number; // Only for trade ships targetTile?: TileRef; // Only for nukes health?: number; diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index de56f49b6..d1396ff7a 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -78,6 +78,12 @@ export class UnitView { troops(): number { return this.data.troops; } + retreating(): boolean { + if (this.type() !== UnitType.TransportShip) { + throw Error("Must be a transport ship"); + } + return this.data.retreating; + } tile(): TileRef { return this.data.pos; } diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index a86e6c309..916c62438 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -18,6 +18,7 @@ export class UnitImpl implements Unit { private _targetUnit: Unit | undefined; private _health: bigint; private _lastTile: TileRef; + private _retreating: boolean = false; private _targetedBySAM = false; private _lastSetSafeFromPirates: number; // Only for trade ships private _constructionType: UnitType | undefined; @@ -73,6 +74,7 @@ export class UnitImpl implements Unit { ownerID: this._owner.smallID(), lastOwnerID: this._lastOwner?.smallID(), isActive: this._active, + retreating: this._retreating, pos: this._tile, lastPos: this._lastTile, health: this.hasHealth() ? Number(this._health) : undefined, @@ -171,6 +173,17 @@ export class UnitImpl implements Unit { return this._active; } + retreating(): boolean { + return this._retreating; + } + + orderBoatRetreat() { + if (this.type() !== UnitType.TransportShip) { + throw new Error(`Cannot retreat ${this.type()}`); + } + this._retreating = true; + } + constructionType(): UnitType | null { if (this.type() !== UnitType.Construction) { throw new Error(`Cannot get construction type on ${this.type()}`);