From 422072675eabf43cae9a472d5165c4e8875c7db2 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:21:08 +0100 Subject: [PATCH] core(game): derive plan-driven state from motion plans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove `markUnitPlanDriven` from `Game`/`GameImpl` - Centralize “maybe emit UnitUpdate” behind `GameImpl.onUnitMoved` - Record trade ship motion plan before the first move to avoid redundant per-step unit updates --- src/core/execution/TradeShipExecution.ts | 62 +++++++++++++++--------- src/core/game/Game.ts | 5 -- src/core/game/GameImpl.ts | 17 +++++-- src/core/game/UnitImpl.ts | 5 +- 4 files changed, 51 insertions(+), 38 deletions(-) diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index 9b6d932b9..0538d42e5 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -48,9 +48,6 @@ export class TradeShipExecution implements Execution { targetUnit: this._dstPort, lastSetSafeFromPirates: ticks, }); - // This unit can move immediately, but plan-driven units don't emit per-step Unit updates. - // Mark it plan-driven up-front so its first move doesn't generate redundant traffic. - this.mg.markUnitPlanDriven(this.tradeShip.id()); this.mg.stats().boatSendTrade(this.origOwner, this._dstPort.owner()); } @@ -109,12 +106,49 @@ export class TradeShipExecution implements Execution { return; } - const result = this.pathFinder.next(curTile, this._dstPort.tile()); + const dst = this._dstPort.tile(); + const result = this.pathFinder.next(curTile, dst); switch (result.status) { case PathStatus.PENDING: + if (dst !== this.motionPlanDst) { + this.motionPlanId++; + const from = curTile; + const path = this.pathFinder.findPath(from, dst) ?? [from]; + if (path.length === 0 || path[0] !== from) { + path.unshift(from); + } + + this.mg.recordMotionPlan({ + kind: "grid", + unitId: this.tradeShip.id(), + planId: this.motionPlanId, + startTick: ticks + 1, + ticksPerStep: 1, + path, + }); + this.motionPlanDst = dst; + } break; case PathStatus.NEXT: + if (dst !== this.motionPlanDst) { + this.motionPlanId++; + const from = result.node; + const path = this.pathFinder.findPath(from, dst) ?? [from]; + if (path.length === 0 || path[0] !== from) { + path.unshift(from); + } + + this.mg.recordMotionPlan({ + kind: "grid", + unitId: this.tradeShip.id(), + planId: this.motionPlanId, + startTick: ticks + 1, + ticksPerStep: 1, + path, + }); + this.motionPlanDst = dst; + } // Update safeFromPirates status if (this.mg.isWater(result.node) && this.mg.isShoreline(result.node)) { this.tradeShip.setSafeFromPirates(); @@ -133,26 +167,6 @@ export class TradeShipExecution implements Execution { this.active = false; return; } - - const dst = this._dstPort.tile(); - if (dst !== this.motionPlanDst) { - this.motionPlanId++; - const from = this.tradeShip.tile(); - const path = this.pathFinder.findPath(from, dst) ?? [from]; - if (path.length === 0 || path[0] !== from) { - path.unshift(from); - } - - this.mg.recordMotionPlan({ - kind: "grid", - unitId: this.tradeShip.id(), - planId: this.motionPlanId, - startTick: ticks + 1, - ticksPerStep: 1, - path, - }); - this.motionPlanDst = dst; - } } private complete() { diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 619da8211..0fa214165 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -768,11 +768,6 @@ export interface Game extends GameMap { inSpawnPhase(): boolean; executeNextTick(): GameUpdates; drainPackedTileUpdates(): Uint32Array; - /** - * Marks a unit as "plan-driven" so its per-tile `Unit` updates can be suppressed. - * The client is expected to advance the unit position via motion plans. - */ - markUnitPlanDriven(unitId: number): void; recordMotionPlan(record: MotionPlanRecord): void; drainPackedMotionPlans(): Uint32Array | null; setWinner(winner: Player | Team, allPlayersStats: AllPlayersStats): void; diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 91cf123ed..de576ceff 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -433,10 +433,6 @@ export class GameImpl implements Game { return packed; } - markUnitPlanDriven(unitId: number): void { - this.planDrivenUnitIds.add(unitId); - } - recordMotionPlan(record: MotionPlanRecord): void { switch (record.kind) { case "grid": @@ -452,10 +448,21 @@ export class GameImpl implements Game { this.motionPlanRecords.push(record); } - isUnitPlanDriven(unitId: number): boolean { + private isUnitPlanDriven(unitId: number): boolean { return this.planDrivenUnitIds.has(unitId); } + maybeAddUnitUpdate(unit: Unit): void { + if (!this.isUnitPlanDriven(unit.id())) { + this.addUpdate(unit.toUpdate()); + } + } + + onUnitMoved(unit: Unit): void { + this.updateUnitTile(unit); + this.maybeAddUnitUpdate(unit); + } + drainPackedMotionPlans(): Uint32Array | null { const records = this.motionPlanRecords; if (records.length === 0) { diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 930cc6142..161e4aa7f 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -159,10 +159,7 @@ export class UnitImpl implements Unit { } this._lastTile = this._tile; this._tile = tile; - this.mg.updateUnitTile(this); - if (!this.mg.isUnitPlanDriven(this._id)) { - this.mg.addUpdate(this.toUpdate()); - } + this.mg.onUnitMoved(this); } setTroops(troops: number): void {