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