Move indicator fx for warships. (#2871)

Been playing this game for months, and after i feel myself enough
experienced about the game, decided to start contributing to the
project. This is my first one!

## Description:

Move indicator fx for warships. Listening MoveWarshipIntentEvent to draw
the fx. Code is basic, respectly layered and written regarding project's
structure.


![warship](https://github.com/user-attachments/assets/7e286e2b-2331-40a3-b62b-ad03dceef676)

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

420coder

---------

Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
This commit is contained in:
opressorMk2
2026-01-14 12:15:40 +00:00
committed by GitHub
co-authored by Ryan
parent 24774b4804
commit c40faecb95
3 changed files with 95 additions and 1 deletions
+1 -1
View File
@@ -258,7 +258,7 @@ export function createRenderer(
new UILayer(game, eventBus, transformHandler), new UILayer(game, eventBus, transformHandler),
new NukeTrajectoryPreviewLayer(game, eventBus, transformHandler, uiState), new NukeTrajectoryPreviewLayer(game, eventBus, transformHandler, uiState),
new StructureIconsLayer(game, eventBus, uiState, transformHandler), new StructureIconsLayer(game, eventBus, uiState, transformHandler),
new DynamicUILayer(game, transformHandler), new DynamicUILayer(game, transformHandler, eventBus),
new NameLayer(game, transformHandler, eventBus), new NameLayer(game, transformHandler, eventBus),
eventsDisplay, eventsDisplay,
chatDisplay, chatDisplay,
@@ -1,4 +1,5 @@
import { renderNumber } from "src/client/Utils"; import { renderNumber } from "src/client/Utils";
import { EventBus } from "src/core/EventBus";
import { UnitType } from "src/core/game/Game"; import { UnitType } from "src/core/game/Game";
import { import {
BonusEventUpdate, BonusEventUpdate,
@@ -6,7 +7,9 @@ import {
GameUpdateType, GameUpdateType,
} from "src/core/game/GameUpdates"; } from "src/core/game/GameUpdates";
import type { GameView, UnitView } from "../../../core/game/GameView"; import type { GameView, UnitView } from "../../../core/game/GameView";
import { MoveWarshipIntentEvent } from "../../Transport";
import { TransformHandler } from "../TransformHandler"; import { TransformHandler } from "../TransformHandler";
import { MoveIndicatorUI } from "../ui/MoveIndicatorUI";
import { NavalTarget } from "../ui/NavalTarget"; import { NavalTarget } from "../ui/NavalTarget";
import { NukeTelegraph } from "../ui/NukeTelegraph"; import { NukeTelegraph } from "../ui/NukeTelegraph";
import { TextIndicator } from "../ui/TextIndicator"; import { TextIndicator } from "../ui/TextIndicator";
@@ -24,8 +27,18 @@ export class DynamicUILayer implements Layer {
constructor( constructor(
private readonly game: GameView, private readonly game: GameView,
private transformHandler: TransformHandler, 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 { shouldTransform(): boolean {
return false; return false;
} }
+81
View File
@@ -0,0 +1,81 @@
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.worldToScreenCoordinates(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;
}
}