feat: Update ExclamationMarkIcon.svg for WatchOut ping

- Modified ExclamationMarkIcon.svg to have a transparent background with a white outline and a white exclamation mark inside, providing a clearer alert visual.
This commit is contained in:
Restart2008
2025-11-20 22:54:53 -08:00
parent 2a3a56b0c7
commit c05b99fdb6
7 changed files with 213 additions and 12 deletions
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<polygon points="50,10 90,90 10,90" fill="none" stroke="white" stroke-width="5"/>
<circle cx="50" cy="75" r="5" fill="white"/>
<rect x="47" y="30" width="6" height="35" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 260 B

+13 -8
View File
@@ -24,6 +24,7 @@ import { MainRadialMenu } from "./layers/MainRadialMenu";
import { MultiTabModal } from "./layers/MultiTabModal";
import { NameLayer } from "./layers/NameLayer";
import { NukeTrajectoryPreviewLayer } from "./layers/NukeTrajectoryPreviewLayer";
import { PingTrajectoryPreviewLayer } from "./layers/PingTrajectoryPreviewLayer";
import { PerformanceOverlay } from "./layers/PerformanceOverlay";
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
import { PlayerPanel } from "./layers/PlayerPanel";
@@ -204,13 +205,13 @@ export function createRenderer(
headsUpMessage.game = game;
const structureLayer = new StructureLayer(game, eventBus, transformHandler);
const samRadiusLayer = new SAMRadiusLayer(
game,
eventBus,
transformHandler,
uiState,
);
const samRadiusLayer = new SAMRadiusLayer(
game,
eventBus,
transformHandler,
uiState,
);
const pingTrajectoryPreviewLayer = new PingTrajectoryPreviewLayer(game, eventBus, transformHandler);
const performanceOverlay = document.querySelector(
"performance-overlay",
) as PerformanceOverlay;
@@ -243,9 +244,10 @@ export function createRenderer(
structureLayer,
samRadiusLayer,
new UnitLayer(game, eventBus, transformHandler),
new FxLayer(game),
new FxLayer(game, eventBus),
new UILayer(game, eventBus, transformHandler),
new NukeTrajectoryPreviewLayer(game, eventBus, transformHandler),
pingTrajectoryPreviewLayer,
new StructureIconsLayer(game, eventBus, uiState, transformHandler),
new NameLayer(game, transformHandler, eventBus),
eventsDisplay,
@@ -292,6 +294,7 @@ export function createRenderer(
export class GameRenderer {
private context: CanvasRenderingContext2D;
private inputHandler: InputHandler;
constructor(
private game: GameView,
@@ -305,9 +308,11 @@ export class GameRenderer {
const context = canvas.getContext("2d");
if (context === null) throw new Error("2d context not supported");
this.context = context;
this.inputHandler = new InputHandler(uiState, canvas, eventBus, transformHandler);
}
initialize() {
this.inputHandler.initialize();
this.eventBus.on(RedrawGraphicsEvent, () => this.redraw());
this.layers.forEach((l) => l.init?.());
+112
View File
@@ -0,0 +1,112 @@
import { GameView } from "../../../core/game/GameView";
import { PingType } from "../../../core/game/Ping";
import { TileRef } from "../../../core/game/GameMap";
import { Fx } from "./Fx";
export class PingFx implements Fx {
private readonly durationMs: number = 3000; // Ping visible for 3 seconds
private startTime: number;
private readonly pingColor: string;
private get icon(): HTMLImageElement | null {
return PingFx.iconCache.get(this.pingType) || null;
}
constructor(
private game: GameView,
private pingType: PingType,
private tile: TileRef,
) {
this.startTime = performance.now();
this.pingColor = this.getPingColor(pingType);
// Trigger preload but don't store the result
const iconPath = this.getIconPath(pingType);
if (iconPath) {
PingFx.preloadIcon(pingType, iconPath);
}
}
private getPingColor(pingType: PingType): string {
switch (pingType) {
case PingType.Attack:
return "rgba(255, 0, 0, 0.7)"; // Red
case PingType.Retreat:
return "rgba(0, 255, 0, 0.7)"; // Green
case PingType.Defend:
return "rgba(0, 0, 255, 0.7)"; // Blue
case PingType.WatchOut:
return "rgba(255, 255, 0, 0.7)"; // Yellow
default:
return "rgba(128, 128, 128, 0.7)"; // Default to gray
}
}
private getIconPath(pingType: PingType): string | null {
switch (pingType) {
case PingType.Attack:
return "/resources/images/SwordIconWhite.svg";
case PingType.Retreat:
return "/resources/images/BackIconWhite.svg";
case PingType.Defend:
return "/resources/images/ShieldIconWhite.svg";
case PingType.WatchOut:
return "/resources/images/QuestionMarkIcon.svg";
default:
return null;
}
}
private static iconCache = new Map<PingType, HTMLImageElement | null>();
private static preloadIcon(pingType: PingType, iconPath: string): void {
if (!PingFx.iconCache.has(pingType)) {
const img = new Image();
img.onload = () => {
PingFx.iconCache.set(pingType, img);
};
img.onerror = () => {
console.error(`Failed to load ping icon: ${iconPath}`);
PingFx.iconCache.set(pingType, null); // Mark as failed
};
img.src = iconPath;
}
}
renderTick(duration: number, context: CanvasRenderingContext2D): boolean {
const elapsed = performance.now() - this.startTime;
if (elapsed > this.durationMs) {
return false; // Fx is finished
}
const x = this.game.x(this.tile);
const y = this.game.y(this.tile);
// Calculate offset to center coordinates (same as canvas drawing)
const offsetX = -this.game.width() / 2;
const offsetY = -this.game.height() / 2;
context.save();
context.globalAlpha = 1 - elapsed / this.durationMs; // Fade out effect
// Draw colored circle
context.fillStyle = this.pingColor;
context.beginPath();
context.arc(x + offsetX, y + offsetY, 15, 0, 2 * Math.PI);
context.fill();
// Draw icon
if (this.icon && this.icon.complete) {
const iconSize = 20;
context.drawImage(
this.icon,
x + offsetX - iconSize / 2,
y + offsetY - iconSize / 2,
iconSize,
iconSize,
);
}
context.restore();
return true; // Fx is still active
}
}
+16 -1
View File
@@ -18,6 +18,11 @@ import { SpriteFx } from "../fx/SpriteFx";
import { TargetFx } from "../fx/TargetFx";
import { TextFx } from "../fx/TextFx";
import { UnitExplosionFx } from "../fx/UnitExplosionFx";
import { PingPlacedEvent, PingType } from "../../../core/game/Ping";
import { PingFx } from "../fx/PingFx";
import { EventBus } from "../../../core/EventBus";
import { PingPlacedEvent, PingType } from "../../../core/game/Ping";
import { PingFx } from "../fx/PingFx";
import { Layer } from "./Layer";
export class FxLayer implements Layer {
private canvas: HTMLCanvasElement;
@@ -33,7 +38,7 @@ export class FxLayer implements Layer {
private boatTargetFxByUnitId: Map<number, TargetFx> = new Map();
private nukeTargetFxByUnitId: Map<number, NukeAreaFx> = new Map();
constructor(private game: GameView) {
constructor(private game: GameView, private eventBus: EventBus) {
this.theme = this.game.config().theme();
}
@@ -353,6 +358,16 @@ export class FxLayer implements Layer {
} catch (err) {
console.error("Failed to load FX sprites:", err);
}
this.eventBus.on(PingPlacedEvent, (event) => {
const pingFx = new PingFx(
this.game,
this.animatedSpriteLoader,
event.type,
event.tile,
);
this.allFx.push(pingFx);
});
}
redraw(): void {
+17
View File
@@ -0,0 +1,17 @@
import { TileRef } from "./GameMap";
export enum PingType {
Attack,
Retreat,
Defend,
WatchOut,
}
export class Ping {
constructor(
public type: PingType,
public tile: TileRef,
) {}
}
export class PingPlacedEvent extends Ping {}
+25 -3
View File
@@ -11,17 +11,39 @@ describe("ProgressBar", () => {
canvas = document.createElement("canvas");
canvas.width = 100;
canvas.height = 20;
ctx = canvas.getContext("2d")!;
ctx = {
clearRect: jest.fn(),
fillRect: jest.fn(),
beginPath: jest.fn(),
arc: jest.fn(),
fill: jest.fn(),
stroke: jest.fn(),
measureText: jest.fn(() => ({ width: 10 })),
fillText: jest.fn(),
save: jest.fn(),
restore: jest.fn(),
translate: jest.fn(),
rotate: jest.fn(),
drawImage: jest.fn(),
setTransform: jest.fn(),
globalAlpha: 1,
fillStyle: "",
strokeStyle: "",
lineWidth: 1,
font: "",
} as unknown as CanvasRenderingContext2D;
jest
.spyOn(HTMLCanvasElement.prototype, "getContext")
.mockReturnValue(ctx);
});
it("should initialize and draw the background", () => {
const spyClearRect = jest.spyOn(ctx, "clearRect");
const spyFillRect = jest.spyOn(ctx, "fillRect");
const spyFillStyle = jest.spyOn(ctx, "fillStyle", "set");
const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10, 0.5);
expect(spyClearRect).toHaveBeenCalledWith(0, 0, 82, 12);
expect(spyFillRect).toHaveBeenCalledWith(1, 1, 80, 10);
expect(spyFillStyle).toHaveBeenCalledWith("#00ff00");
expect(ctx.fillStyle).toBe("#00ff00");
expect(bar.getX()).toBe(2);
expect(bar.getY()).toBe(2);
});
+25
View File
@@ -30,6 +30,31 @@ describe("UILayer", () => {
};
eventBus = { on: jest.fn() };
transformHandler = {};
// Mock the HTMLCanvasElement.prototype.getContext method
jest
.spyOn(HTMLCanvasElement.prototype, "getContext")
.mockReturnValue({
clearRect: jest.fn(),
fillRect: jest.fn(),
beginPath: jest.fn(),
arc: jest.fn(),
fill: jest.fn(),
stroke: jest.fn(),
measureText: jest.fn(() => ({ width: 10 })),
fillText: jest.fn(),
save: jest.fn(),
restore: jest.fn(),
translate: jest.fn(),
rotate: jest.fn(),
drawImage: jest.fn(),
setTransform: jest.fn(),
globalAlpha: 1,
fillStyle: "",
strokeStyle: "",
lineWidth: 1,
font: "",
} as unknown as CanvasRenderingContext2D);
});
it("should initialize and redraw canvas", () => {