From 2fec1e994e391e29c685f69b467f0785b7809c17 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sat, 16 May 2026 18:51:34 -0700 Subject: [PATCH] retire DynamicUILayer, restore warship UX on WebGL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DynamicUILayer was a canvas2D mix of: bonus-event gold/troops popups (already duplicated by WebGL BonusPopupPass), nuke/transport telegraph indicators (duplicated by WebGL passes), and a warship move-indicator chevron drawn via MoveIndicatorUI. Delete the layer outright along with its three orphan UI helpers (MoveIndicatorUI, NavalTarget, NukeTelegraph). That deletion uncovered a pre-existing bug from the "migrate away from canvas" commit: warship select/move no longer worked. The deleted UnitLayer had owned the click flow that emits MoveWarshipIntentEvent. Re-add the flow inside UILayer (which already tracks selected / multi-selected warships for its selection box): MouseUpEvent → move-multi → move-single → select-nearest, plus shift+drag box complete and select-all hotkey. Wire MoveWarshipIntentEvent → view.showMoveIndicator(tx, ty, ownerID) in mountWebGLDebugRenderer so the WebGL MoveIndicatorPass draws the converging-chevron animation at the move target, colored by the warship's owner. mountWebGLDebugRenderer now takes gameView + eventBus to resolve the owner and subscribe. --- src/client/ClientGameRunner.ts | 19 +++ src/client/graphics/GameRenderer.ts | 2 - src/client/graphics/layers/DynamicUILayer.ts | 165 ------------------ src/client/graphics/layers/UILayer.ts | 171 +++++++++++++++++++ src/client/graphics/ui/MoveIndicatorUI.ts | 81 --------- src/client/graphics/ui/NavalTarget.ts | 132 -------------- src/client/graphics/ui/NukeTelegraph.ts | 113 ------------ 7 files changed, 190 insertions(+), 493 deletions(-) delete mode 100644 src/client/graphics/layers/DynamicUILayer.ts delete mode 100644 src/client/graphics/ui/MoveIndicatorUI.ts delete mode 100644 src/client/graphics/ui/NavalTarget.ts delete mode 100644 src/client/graphics/ui/NukeTelegraph.ts diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 711fe43d7..47933bd73 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -47,6 +47,7 @@ import { import { endGame, startGame, startTime } from "./LocalPersistantStats"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { + MoveWarshipIntentEvent, SendAllianceExtensionIntentEvent, SendAllianceRequestIntentEvent, SendAttackIntentEvent, @@ -231,6 +232,8 @@ export function joinLobby( function mountWebGLDebugRenderer( terrainMap: TerrainMapData, transformHandler: import("./graphics/TransformHandler").TransformHandler, + gameView: GameView, + eventBus: EventBus, ): { builder: WebGLFrameBuilder; syncCamera: () => void } { const gameMap = terrainMap.gameMap; const mapWidth = gameMap.width(); @@ -332,6 +335,20 @@ function mountWebGLDebugRenderer( (window as unknown as { __webglView?: unknown }).__webglView = view; + // Move-target chevrons: when the player issues a warship move, show the + // animated chevron pass at the target tile. The renderer needs the target's + // tile x/y and the warship's owner smallID (so the chevrons use the right + // color). + eventBus.on(MoveWarshipIntentEvent, (e) => { + const tile = e.tile; + const tx = gameView.x(tile); + const ty = gameView.y(tile); + // Resolve owner via the first unit in the move set. + const firstUnit = gameView.unit(e.unitIds[0]); + if (firstUnit === undefined) return; + view.showMoveIndicator(tx, ty, firstUnit.owner().smallID()); + }); + return { builder: new WebGLFrameBuilder(view), syncCamera }; } @@ -389,6 +406,8 @@ async function createClientGame( const { builder: webglBuilder, syncCamera } = mountWebGLDebugRenderer( gameMap, gameRenderer.transformHandler, + gameView, + eventBus, ); gameRenderer.onPreRender = syncCamera; diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index a25bff278..2ad7dcdbe 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -13,7 +13,6 @@ import { BuildMenu } from "./layers/BuildMenu"; import { ChatDisplay } from "./layers/ChatDisplay"; import { ChatModal } from "./layers/ChatModal"; import { ControlPanel } from "./layers/ControlPanel"; -import { DynamicUILayer } from "./layers/DynamicUILayer"; import { EmojiTable } from "./layers/EmojiTable"; import { EventsDisplay } from "./layers/EventsDisplay"; import { GameLeftSidebar } from "./layers/GameLeftSidebar"; @@ -265,7 +264,6 @@ export function createRenderer( const layers: Layer[] = [ new UILayer(game, eventBus, transformHandler), new StructureIconsLayer(game, eventBus, uiState, transformHandler), - new DynamicUILayer(game, transformHandler, eventBus), new NameLayer(game, transformHandler, eventBus), new AttackingTroopsOverlay(game, transformHandler, eventBus, userSettings), eventsDisplay, diff --git a/src/client/graphics/layers/DynamicUILayer.ts b/src/client/graphics/layers/DynamicUILayer.ts deleted file mode 100644 index a6200e360..000000000 --- a/src/client/graphics/layers/DynamicUILayer.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { renderNumber } from "src/client/Utils"; -import { EventBus } from "src/core/EventBus"; -import { UnitType } from "src/core/game/Game"; -import { BonusEventUpdate, GameUpdateType } from "src/core/game/GameUpdates"; -import type { GameView, UnitView } from "../../../core/game/GameView"; -import { MoveWarshipIntentEvent } from "../../Transport"; -import { TransformHandler } from "../TransformHandler"; -import { MoveIndicatorUI } from "../ui/MoveIndicatorUI"; -import { NavalTarget } from "../ui/NavalTarget"; -import { NukeTelegraph } from "../ui/NukeTelegraph"; -import { TextIndicator } from "../ui/TextIndicator"; -import { UIElement } from "../ui/UIElement"; -import { Layer } from "./Layer"; - -const TEXT_OFFSET_Y = -5; -const TEXT_STACK_SPACING = 8; - -export class DynamicUILayer implements Layer { - private readonly uiElements: Array = []; - private lastRefresh = Date.now(); - - constructor( - private readonly game: GameView, - private transformHandler: TransformHandler, - private eventBus: EventBus, - ) {} - - init() { - // Listen for warship move clicks for MoveIndicatorUI - this.eventBus.on(MoveWarshipIntentEvent, (e) => { - const x = this.game.x(e.tile); - const y = this.game.y(e.tile); - this.uiElements.push(new MoveIndicatorUI(this.transformHandler, x, y)); - }); - } - - shouldTransform(): boolean { - return false; - } - - tick() { - if (!this.game.config().userSettings()?.fxLayer()) { - return; - } - - const updates = this.game.updatesSinceLastTick(); - if (!updates) return; - - updates[GameUpdateType.Unit]?.forEach((unit) => { - const unitView = this.game.unit(unit.id); - if (!unitView) return; - this.onUnitEvent(unitView); - }); - - updates[GameUpdateType.BonusEvent]?.forEach((bonusEvent) => { - if (bonusEvent === undefined) return; - this.onBonusEvent(bonusEvent); - }); - } - - onBonusEvent(bonus: BonusEventUpdate) { - // Only display text fx for the current player - if (this.game.player(bonus.player) !== this.game.myPlayer()) { - return; - } - const tile = bonus.tile; - const x = this.game.x(tile); - let y = this.game.y(tile) + TEXT_OFFSET_Y; - const gold = bonus.gold; - const troops = bonus.troops; - - if (gold !== 0) { - this.addNumber(gold, x, y, 1000, 10); - y += TEXT_STACK_SPACING; // increase y so the next popup starts below - } - - if (troops !== 0) { - this.addNumber(troops, x, y, 1000, 10); - } - } - - onUnitEvent(unit: UnitView) { - switch (unit.type()) { - case UnitType.HydrogenBomb: - case UnitType.AtomBomb: { - this.onBombEvent(unit); - break; - } - case UnitType.TransportShip: { - this.onTransportShipEvent(unit); - break; - } - } - } - - onBombEvent(unit: UnitView) { - const myPlayer = this.game.myPlayer(); - if (!myPlayer) { - return; - } - if ( - this.createdThisTick(unit) && - (unit.owner() === myPlayer || unit.owner().isOnSameTeam(myPlayer)) - ) { - const target = new NukeTelegraph(this.transformHandler, this.game, unit); - this.uiElements.push(target); - } - } - - onTransportShipEvent(unit: UnitView) { - const myPlayer = this.game.myPlayer(); - if (!myPlayer) { - return; - } - if (this.createdThisTick(unit) && unit.owner() === myPlayer) { - const target = new NavalTarget(this.transformHandler, this.game, unit); - this.uiElements.push(target); - } - } - - renderLayer(context: CanvasRenderingContext2D) { - const now = Date.now(); - const dt = now - this.lastRefresh; - this.lastRefresh = now; - if (this.game.config().userSettings()?.fxLayer()) { - this.renderUIElements(context, dt); - } - } - - renderUIElements(context: CanvasRenderingContext2D, delta: number) { - for (let i = this.uiElements.length - 1; i >= 0; i--) { - if (!this.uiElements[i].render(context, delta)) { - this.uiElements.splice(i, 1); - } - } - } - - private createdThisTick(unit: UnitView): boolean { - return unit.createdAt() === this.game.ticks(); - } - - private addNumber( - num: bigint | number, - x: number, - y: number, - duration: number, - riseDistance: number, - ) { - if (BigInt(num) === 0n) return; // Don't show anything for 0 - const absNum = - typeof num === "bigint" ? (num < 0n ? -num : num) : Math.abs(num); - const shortened = renderNumber(absNum, 0); - const sign = num >= 0 ? "+" : "-"; - this.uiElements.push( - new TextIndicator( - this.transformHandler, - `${sign} ${shortened}`, - x, - y, - duration, - riseDistance, - ), - ); - } -} diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts index 1338baad4..0b1aafe36 100644 --- a/src/client/graphics/layers/UILayer.ts +++ b/src/client/graphics/layers/UILayer.ts @@ -1,18 +1,27 @@ import { Colord } from "colord"; import { Theme } from "src/core/configuration/Theme"; +import { Cell } from "src/core/game/Game"; import { EventBus } from "../../../core/EventBus"; import { UnitType } from "../../../core/game/Game"; +import { TileRef } from "../../../core/game/GameMap"; import { GameView, UnitView } from "../../../core/game/GameView"; import { CloseViewEvent, + ContextMenuEvent, + MouseUpEvent, + SelectAllWarshipsEvent, + TouchEvent, UnitSelectionEvent, WarshipSelectionBoxCancelEvent, WarshipSelectionBoxCompleteEvent, WarshipSelectionBoxUpdateEvent, } from "../../InputHandler"; +import { MoveWarshipIntentEvent } from "../../Transport"; import { TransformHandler } from "../TransformHandler"; import { Layer } from "./Layer"; +const WARSHIP_SELECTION_RADIUS = 10; + /** * Layer responsible for drawing UI elements that overlay the game. * Currently: warship selection boxes + drag-rectangle selection. @@ -116,9 +125,171 @@ export class UILayer implements Layer { this.eventBus.on(WarshipSelectionBoxCompleteEvent, clearBox); this.eventBus.on(WarshipSelectionBoxCancelEvent, clearBox); this.eventBus.on(CloseViewEvent, clearBox); + + // Warship select/move click flow (previously in the deleted UnitLayer). + this.eventBus.on(MouseUpEvent, (e) => this.onMouseUp(e)); + this.eventBus.on(TouchEvent, (e) => this.onTouch(e)); + this.eventBus.on(WarshipSelectionBoxCompleteEvent, (e) => + this.onSelectionBoxComplete(e), + ); + this.eventBus.on(SelectAllWarshipsEvent, () => this.onSelectAllWarships()); + this.redraw(); } + /** + * Find player-owned warships near the given cell, sorted by distance. + */ + private findWarshipsNearCell(clickRef: TileRef): UnitView[] { + const myPlayer = this.game.myPlayer(); + if (!myPlayer) return []; + return this.game + .units(UnitType.Warship) + .filter( + (unit) => + unit.isActive() && + unit.owner() === myPlayer && + this.game.manhattanDist(unit.tile(), clickRef) <= + WARSHIP_SELECTION_RADIUS, + ) + .sort( + (a, b) => + this.game.manhattanDist(a.tile(), clickRef) - + this.game.manhattanDist(b.tile(), clickRef), + ); + } + + /** + * Resolve a left-click in the world: + * - multi-selected warships present + clicked water → move them all + * - single selected warship + clicked water → move it, then deselect + * - otherwise → if there's a nearby warship, select the closest one + */ + private onMouseUp( + event: MouseUpEvent, + clickRef?: TileRef, + nearbyWarships?: UnitView[], + ) { + if (clickRef === undefined) { + const cell = this.transformHandler.screenToWorldCoordinates( + event.x, + event.y, + ); + if (!this.game.isValidCoord(cell.x, cell.y)) return; + clickRef = this.game.ref(cell.x, cell.y); + } + if (!this.game.isWater(clickRef)) return; + + if (this.multiSelectedWarships.length > 0) { + const myPlayer = this.game.myPlayer(); + const activeIds = this.multiSelectedWarships + .filter((u) => u.isActive() && u.owner() === myPlayer) + .map((u) => u.id()); + + if (activeIds.length > 0) { + this.eventBus.emit(new MoveWarshipIntentEvent(activeIds, clickRef)); + } + this.eventBus.emit(new UnitSelectionEvent(null, false)); + return; + } + + if (this.selectedUnit) { + this.eventBus.emit( + new MoveWarshipIntentEvent([this.selectedUnit.id()], clickRef), + ); + this.eventBus.emit(new UnitSelectionEvent(this.selectedUnit, false)); + return; + } + + nearbyWarships ??= this.findWarshipsNearCell(clickRef); + if (nearbyWarships.length > 0) { + this.eventBus.emit(new UnitSelectionEvent(nearbyWarships[0], true)); + } + } + + /** + * Touch handler mirroring mouse-up. On dry land with no selection, falls + * back to opening the radial menu. + */ + private onTouch(event: TouchEvent) { + const cell = this.transformHandler.screenToWorldCoordinates( + event.x, + event.y, + ); + if (!this.game.isValidCoord(cell.x, cell.y)) return; + + const clickRef = this.game.ref(cell.x, cell.y); + if (this.game.inSpawnPhase()) { + if (!this.game.isWater(clickRef)) { + this.eventBus.emit(new MouseUpEvent(event.x, event.y)); + } + return; + } + if (!this.game.isWater(clickRef)) { + this.eventBus.emit(new ContextMenuEvent(event.x, event.y)); + return; + } + if (this.selectedUnit || this.multiSelectedWarships.length > 0) { + this.onMouseUp(new MouseUpEvent(event.x, event.y), clickRef); + return; + } + const nearbyWarships = this.findWarshipsNearCell(clickRef); + if (nearbyWarships.length > 0) { + this.onMouseUp( + new MouseUpEvent(event.x, event.y), + clickRef, + nearbyWarships, + ); + } else { + this.eventBus.emit(new ContextMenuEvent(event.x, event.y)); + } + } + + /** + * Resolve a shift+drag selection box: gather all player-owned warships + * whose screen position falls inside the rectangle. + */ + private onSelectionBoxComplete(event: WarshipSelectionBoxCompleteEvent) { + const x1 = Math.min(event.startX, event.endX); + const y1 = Math.min(event.startY, event.endY); + const x2 = Math.max(event.startX, event.endX); + const y2 = Math.max(event.startY, event.endY); + + const myPlayer = this.game.myPlayer(); + if (!myPlayer) return; + + const selected = this.game.units(UnitType.Warship).filter((unit) => { + if (!unit.isActive() || unit.owner() !== myPlayer) return false; + const screen = this.transformHandler.worldToScreenCoordinates( + new Cell(this.game.x(unit.tile()), this.game.y(unit.tile())), + ); + return ( + screen.x >= x1 && screen.x <= x2 && screen.y >= y1 && screen.y <= y2 + ); + }); + + // Clear single selection if we got a box selection + if (selected.length > 0 && this.selectedUnit) { + this.eventBus.emit(new UnitSelectionEvent(this.selectedUnit, false)); + } + this.eventBus.emit(new UnitSelectionEvent(null, true, selected)); + } + + private onSelectAllWarships() { + const myPlayer = this.game.myPlayer(); + if (!myPlayer) return; + + const allWarships = this.game + .units(UnitType.Warship) + .filter((u) => u.isActive() && u.owner() === myPlayer); + if (allWarships.length === 0) return; + + if (this.selectedUnit) { + this.eventBus.emit(new UnitSelectionEvent(this.selectedUnit, false)); + } + this.eventBus.emit(new UnitSelectionEvent(null, true, allWarships)); + } + renderLayer(context: CanvasRenderingContext2D) { context.drawImage( this.canvas, diff --git a/src/client/graphics/ui/MoveIndicatorUI.ts b/src/client/graphics/ui/MoveIndicatorUI.ts deleted file mode 100644 index 8da36b7d4..000000000 --- a/src/client/graphics/ui/MoveIndicatorUI.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Cell } from "src/core/game/Game"; -import { TransformHandler } from "../TransformHandler"; -import { UIElement } from "./UIElement"; - -/** - * move indicator fx for warship, similar to moba games. - */ -export class MoveIndicatorUI implements UIElement { - private lifeTime = 0; - private readonly duration = 800; // ms - private readonly startRadius = 13; // starting distance from center (screen pixels) - private readonly chevronSize = 5; // size in screen pixels - private readonly cell: Cell; - - constructor( - private transformHandler: TransformHandler, - public x: number, - public y: number, - ) { - this.cell = new Cell(this.x + 0.5, this.y + 0.5); - } - - render(ctx: CanvasRenderingContext2D, delta: number): boolean { - this.lifeTime += delta; - if (this.lifeTime >= this.duration) return false; - - const t = this.lifeTime / this.duration; - const alpha = 1 - t; // fade out - - // Scale with zoom level (same pattern as NavalTarget) - const transformScale = this.transformHandler.scale; - const scale = transformScale > 10 ? 1 + (transformScale - 10) / 10 : 1; - - const radius = this.startRadius * scale * (1 - t * 0.7); // converge inward - const chevronSize = this.chevronSize * scale; - - // Get screen coordinates - const screenPos = this.transformHandler.worldToCanvasCoordinates(this.cell); - const centerX = screenPos.x; - const centerY = screenPos.y; - - ctx.save(); - ctx.globalAlpha = alpha; - ctx.strokeStyle = "#ff0000"; - ctx.lineWidth = 2 * scale; - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - - // pre calculation of offsets - const tipOffset = chevronSize * 0.4; - const wingOffset = chevronSize * 0.6; - const width = chevronSize; - - ctx.beginPath(); - - // Top (pointing down) - ctx.moveTo(centerX - width, centerY - radius - wingOffset); - ctx.lineTo(centerX, centerY - radius + tipOffset); - ctx.lineTo(centerX + width, centerY - radius - wingOffset); - - // Bottom (pointing up) - ctx.moveTo(centerX - width, centerY + radius + wingOffset); - ctx.lineTo(centerX, centerY + radius - tipOffset); - ctx.lineTo(centerX + width, centerY + radius + wingOffset); - - // Left (pointing right) - ctx.moveTo(centerX - radius - wingOffset, centerY - width); - ctx.lineTo(centerX - radius + tipOffset, centerY); - ctx.lineTo(centerX - radius - wingOffset, centerY + width); - - // Right (pointing left) - ctx.moveTo(centerX + radius + wingOffset, centerY - width); - ctx.lineTo(centerX + radius - tipOffset, centerY); - ctx.lineTo(centerX + radius + wingOffset, centerY + width); - - ctx.stroke(); - - ctx.restore(); - return true; - } -} diff --git a/src/client/graphics/ui/NavalTarget.ts b/src/client/graphics/ui/NavalTarget.ts deleted file mode 100644 index 0e88487df..000000000 --- a/src/client/graphics/ui/NavalTarget.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Cell, UnitType } from "src/core/game/Game"; -import { GameView, UnitView } from "src/core/game/GameView"; -import { TransformHandler } from "../TransformHandler"; -import { UIElement } from "./UIElement"; - -const BASE_ALPHA = 0.9; -const SHADOW_OFFSET_Y = 2; - -/** - * Draw a simple zoom-aware target - */ -export class Target implements UIElement { - private offset = 0; - private readonly rotationSpeed = 20; - private readonly dashSize: number; - private readonly outerRadius: number; - private readonly cell: Cell; - private readonly animationDuration = 150; - private animationElapsedTime = 0; - protected ended: boolean = false; - protected lifeTime: number = 0; - - constructor( - private transformHandler: TransformHandler, - public x: number, - public y: number, - private radius: number, - ) { - this.outerRadius = radius * 2 - 4; - // 2 dashes per circle, with a 10 pixel gap - this.dashSize = Math.PI * this.outerRadius - 10; - this.cell = new Cell(this.x + 0.5, this.y + 0.5); - } - render(ctx: CanvasRenderingContext2D, delta: number): boolean { - this.lifeTime += delta; - - if (this.ended) { - this.animationElapsedTime += delta; - if (this.animationElapsedTime >= this.animationDuration) return false; - } - - let t: number; - if (this.ended) { - // end animation - t = Math.max(0, 1 - this.animationElapsedTime / this.animationDuration); - } else { - t = 1; // No start fade feels more reactive - } - const alpha = Math.max(0, Math.min(1, BASE_ALPHA * t)); - - const screenPos = this.transformHandler.worldToCanvasCoordinates(this.cell); - screenPos.x = Math.round(screenPos.x); - screenPos.y = Math.round(screenPos.y); - const transformScale = this.transformHandler.scale; - const scale = transformScale > 10 ? 1 + (transformScale - 10) / 10 : 1; - this.offset += this.rotationSpeed * (delta / 1000); - - ctx.save(); - ctx.globalAlpha = alpha; - ctx.lineWidth = 1; - ctx.strokeStyle = `rgba(255,0,0,${alpha})`; - - this.drawInnerRing(ctx, screenPos.x, screenPos.y, scale); - this.drawOuterRing(ctx, screenPos.x, screenPos.y, scale); - - ctx.restore(); - return true; - } - - private drawInnerRing( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - scale: number, - ) { - ctx.beginPath(); - ctx.lineWidth = 2; - ctx.lineDashOffset = this.offset * scale; - ctx.setLineDash([8 * scale, 8 * scale]); - ctx.arc(x, y, this.radius * scale, 0, Math.PI * 2); - ctx.stroke(); - } - - private drawOuterRing( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - scale: number, - ) { - ctx.beginPath(); - ctx.lineWidth = 4 * scale; - ctx.lineDashOffset = (-this.offset / 2) * scale; - ctx.setLineDash([this.dashSize * scale, 10 * scale]); - ctx.arc(x, y, this.outerRadius * scale, 0, Math.PI * 2); - ctx.stroke(); - - // Small shadow under the outer circle - ctx.beginPath(); - ctx.strokeStyle = `rgba(0,0,0,0.2)`; - ctx.arc(x, y + SHADOW_OFFSET_Y, this.outerRadius * scale, 0, Math.PI * 2); - ctx.stroke(); - } -} - -/** - * Bind a target to a naval invasion - */ -export class NavalTarget extends Target { - constructor( - transformHandler: TransformHandler, - readonly game: GameView, - private unit: UnitView, - ) { - const tile = unit.targetTile(); - if (tile === undefined) { - throw new Error("NavalTarget requires a target tile"); - } - super(transformHandler, game.x(tile), game.y(tile), 10); - } - - render(ctx: CanvasRenderingContext2D, delta: number): boolean { - if ( - !this.ended && - (!this.unit.isActive() || - (this.unit.type() === UnitType.TransportShip && - this.unit.transportShipState().isRetreating)) - ) { - this.ended = true; - } - return super.render(ctx, delta); - } -} diff --git a/src/client/graphics/ui/NukeTelegraph.ts b/src/client/graphics/ui/NukeTelegraph.ts deleted file mode 100644 index 1d54a8567..000000000 --- a/src/client/graphics/ui/NukeTelegraph.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Cell } from "src/core/game/Game"; -import { GameView, UnitView } from "src/core/game/GameView"; -import { TransformHandler } from "../TransformHandler"; -import { UIElement } from "./UIElement"; - -const OUTER_EXPAND = 20; -const FILL_ALPHA_OFFSET = 0.6; - -/** - * Draw an area with two disks - */ -export class CircleArea implements UIElement { - private offset = 0; - private readonly dashSize: number; - private readonly rotationSpeed = 20; - private readonly baseAlpha = 0.9; - private readonly cell: Cell; - private readonly animationDuration = 150; - protected ended: boolean = false; - protected lifeTime: number = 0; - - constructor( - private transformHandler: TransformHandler, - public x: number, - public y: number, - private innerDiameter: number, - private outerDiameter: number, - ) { - this.cell = new Cell(this.x + 0.5, this.y + 0.5); - // Compute a dash length that produces N dashes around the circle - const numDash = Math.max(1, Math.floor(this.outerDiameter / 3)); - this.dashSize = (Math.PI / numDash) * this.outerDiameter; - } - render(ctx: CanvasRenderingContext2D, delta: number): boolean { - this.lifeTime += delta; - - if (this.ended && this.lifeTime >= this.animationDuration) return false; - let t: number; - if (this.ended) { - t = Math.max(0, 1 - this.lifeTime / this.animationDuration); - } else { - t = Math.min(1, this.lifeTime / this.animationDuration); - } - const alpha = Math.max(0, Math.min(1, this.baseAlpha * t)); - const scale = this.transformHandler.scale; - - const innerDiameter = - (this.innerDiameter / 2) * (1 - t) + this.innerDiameter * t; - const screenPos = this.transformHandler.worldToCanvasCoordinates(this.cell); - screenPos.x = Math.round(screenPos.x); - screenPos.y = Math.round(screenPos.y); - - ctx.save(); - ctx.globalAlpha = alpha; - ctx.lineWidth = 2; - ctx.strokeStyle = `rgba(255,0,0,${alpha})`; - ctx.fillStyle = `rgba(255,0,0,${Math.max(0, alpha - FILL_ALPHA_OFFSET)})`; - - // Inner circle - ctx.beginPath(); - ctx.lineWidth = 1; - ctx.arc(screenPos.x, screenPos.y, innerDiameter * scale, 0, Math.PI * 2); - ctx.stroke(); - ctx.fill(); - - // Outer circle - this.offset += this.rotationSpeed * (delta / 1000); - ctx.beginPath(); - ctx.strokeStyle = `rgba(255,0,0,${alpha})`; - ctx.lineWidth = Math.max(2, 1 * scale); - ctx.lineDashOffset = this.offset * scale; - ctx.setLineDash([this.dashSize * scale]); - const outerDiameter = - (this.outerDiameter + OUTER_EXPAND) * (1 - t) + this.outerDiameter * t; - ctx.arc(screenPos.x, screenPos.y, outerDiameter * scale, 0, Math.PI * 2); - ctx.stroke(); - - ctx.restore(); - return true; - } -} - -/** - * Bind a nuke destination to an area - */ -export class NukeTelegraph extends CircleArea { - constructor( - transformHandler: TransformHandler, - private readonly game: GameView, - private nuke: UnitView, - ) { - const tile = nuke.targetTile(); - if (tile === undefined) { - throw new Error("NukeArea requires a target tile"); - } - const magnitude = game.config().nukeMagnitudes(nuke.type()); - super( - transformHandler, - game.x(tile), - game.y(tile), - magnitude.inner, - magnitude.outer, - ); - } - - render(ctx: CanvasRenderingContext2D, delta: number): boolean { - if (!this.ended && !this.nuke.isActive()) { - this.ended = true; - this.lifeTime = 0; // reset lifetime to reuse animation logic - } - return super.render(ctx, delta); - } -}