diff --git a/src/client/controllers/BuildPreviewController.ts b/src/client/controllers/BuildPreviewController.ts index 26a55b1b3..b9f771df8 100644 --- a/src/client/controllers/BuildPreviewController.ts +++ b/src/client/controllers/BuildPreviewController.ts @@ -52,6 +52,16 @@ export class BuildPreviewController implements Controller { // frame with the current cursor world position. private lastGhostData: GhostPreviewData | null = null; + // Static inputs for the nuke trajectory preview (source silo + threatening + // SAMs). Recomputed in the throttled renderGhost path; cursorLoop rebuilds + // the Bezier each frame with the live cursor position as the destination so + // the arc tracks the cursor smoothly instead of snapping tile-to-tile. + private nukeTrajectoryStatic: { + srcX: number; + srcY: number; + sams: SAMInfo[]; + } | null = null; + constructor( private game: GameView, private eventBus: EventBus, @@ -78,16 +88,45 @@ export class BuildPreviewController implements Controller { // integer tile coord centers on that tile), so we subtract 0.5 here to // place the icon exactly under the cursor. const cursorLoop = () => { - if (this.lastGhostData !== null) { + const ghost = this.lastGhostData; + const traj = this.nukeTrajectoryStatic; + if (ghost !== null || traj !== null) { const w = this.transformHandler.screenToWorldCoordinatesFloat( this.mousePos.x, this.mousePos.y, ); - this.view.updateGhostPreview({ - ...this.lastGhostData, - tileX: w.x - 0.5, - tileY: w.y - 0.5, - }); + if (ghost !== null) { + // The range circle (defense post / SAM / nuke radius) normally + // follows the cursor, so smooth it the same way as the icon. When + // upgrading, the circle is anchored to the existing structure's tile + // (stationary, correctly snapped) — leave it alone in that case. + const radiusFollowsCursor = !( + ghost.canUpgrade && ghost.upgradeTargetTile !== null + ); + this.view.updateGhostPreview({ + ...ghost, + tileX: w.x - 0.5, + tileY: w.y - 0.5, + ...(radiusFollowsCursor + ? { radiusTileX: w.x - 0.5, radiusTileY: w.y - 0.5 } + : {}), + }); + } + if (traj !== null) { + // Rebuild the arc with the live cursor as the destination (same + // tile-center convention as the icon: shader adds +0.5). + this.view.updateNukeTrajectory( + buildNukeTrajectory( + traj.srcX, + traj.srcY, + w.x - 0.5, + w.y - 0.5, + this.game.height(), + this.uiState.rocketDirectionUp, + traj.sams, + ), + ); + } } requestAnimationFrame(cursorLoop); }; @@ -228,17 +267,17 @@ export class BuildPreviewController implements Controller { */ private updateNukeTrajectoryPreview(tileRef: TileRef | undefined): void { if (!this.ghostUnit || tileRef === undefined) { - this.view.updateNukeTrajectory(null); + this.clearNukeTrajectory(); return; } const type = this.ghostUnit.buildableUnit.type; if (type !== UnitType.AtomBomb && type !== UnitType.HydrogenBomb) { - this.view.updateNukeTrajectory(null); + this.clearNukeTrajectory(); return; } const myPlayer = this.game.myPlayer(); if (!myPlayer) { - this.view.updateNukeTrajectory(null); + this.clearNukeTrajectory(); return; } @@ -246,7 +285,7 @@ export class BuildPreviewController implements Controller { .units(UnitType.MissileSilo) .filter((u) => u.isActive()); if (silos.length === 0) { - this.view.updateNukeTrajectory(null); + this.clearNukeTrajectory(); return; } @@ -285,17 +324,14 @@ export class BuildPreviewController implements Controller { }); } - this.view.updateNukeTrajectory( - buildNukeTrajectory( - srcX, - srcY, - dstX, - dstY, - this.game.height(), - this.uiState.rocketDirectionUp, - sams, - ), - ); + // Stash the static inputs; cursorLoop rebuilds the Bezier each frame with + // the live cursor as the destination so the arc tracks smoothly. + this.nukeTrajectoryStatic = { srcX, srcY, sams }; + } + + private clearNukeTrajectory(): void { + this.nukeTrajectoryStatic = null; + this.view.updateNukeTrajectory(null); } private buildGhostPreviewData( @@ -446,7 +482,7 @@ export class BuildPreviewController implements Controller { this.ghostUnit = null; this.lastGhostData = null; this.view.updateGhostPreview(null); - this.view.updateNukeTrajectory(null); + this.clearNukeTrajectory(); } private removeGhostStructure() {