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 15:15:40 +03:00
committed by GitHub
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 NukeTrajectoryPreviewLayer(game, eventBus, transformHandler, uiState),
new StructureIconsLayer(game, eventBus, uiState, transformHandler),
new DynamicUILayer(game, transformHandler),
new DynamicUILayer(game, transformHandler, eventBus),
new NameLayer(game, transformHandler, eventBus),
eventsDisplay,
chatDisplay,
@@ -1,4 +1,5 @@
import { renderNumber } from "src/client/Utils";
import { EventBus } from "src/core/EventBus";
import { UnitType } from "src/core/game/Game";
import {
BonusEventUpdate,
@@ -6,7 +7,9 @@ import {
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";
@@ -24,8 +27,18 @@ export class DynamicUILayer implements Layer {
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;
}
+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;
}
}