Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/client/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,21 @@ export class RefreshGraphicsEvent implements GameEvent {}

export class TogglePerformanceOverlayEvent implements GameEvent {}

export class ToggleTerritoryWebGLEvent implements GameEvent {}

export class TerritoryWebGLStatusEvent implements GameEvent {
constructor(
public readonly enabled: boolean,
public readonly active: boolean,
public readonly supported: boolean,
public readonly message?: string,
) {}
}

export class ToggleTerritoryWebGLDebugBordersEvent implements GameEvent {
constructor(public readonly enabled: boolean) {}
}

export class ToggleStructureEvent implements GameEvent {
constructor(public readonly structureTypes: UnitType[] | null) {}
}
Expand Down
43 changes: 43 additions & 0 deletions src/client/graphics/FrameProfiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export class FrameProfiler {
private static timings: Record<string, number> = {};

/**
* Clear all accumulated timings for the current frame.
*/
static clear(): void {
this.timings = {};
}

/**
* Record a duration (in ms) for a named span.
*/
static record(name: string, duration: number): void {
if (!Number.isFinite(duration)) return;
this.timings[name] = (this.timings[name] ?? 0) + duration;
}

/**
* Convenience helper to start a span.
* Returns a high-resolution timestamp to be passed into end().
*/
static start(): number {
return performance.now();
}

/**
* Convenience helper to end a span started with start().
*/
static end(name: string, startTime: number): void {
const duration = performance.now() - startTime;
this.record(name, duration);
}

/**
* Consume and reset all timings collected so far.
*/
static consume(): Record<string, number> {
const copy = { ...this.timings };
this.timings = {};
return copy;
}
}
32 changes: 31 additions & 1 deletion src/client/graphics/GameRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GameView } from "../../core/game/GameView";
import { UserSettings } from "../../core/game/UserSettings";
import { GameStartingModal } from "../GameStartingModal";
import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler";
import { FrameProfiler } from "./FrameProfiler";
import { TransformHandler } from "./TransformHandler";
import { UIState } from "./UIState";
import { AdTimer } from "./layers/AdTimer";
Expand Down Expand Up @@ -36,6 +37,7 @@ import { StructureLayer } from "./layers/StructureLayer";
import { TeamStats } from "./layers/TeamStats";
import { TerrainLayer } from "./layers/TerrainLayer";
import { TerritoryLayer } from "./layers/TerritoryLayer";
import { TerritoryWebGLStatus } from "./layers/TerritoryWebGLStatus";
import { UILayer } from "./layers/UILayer";
import { UnitDisplay } from "./layers/UnitDisplay";
import { UnitLayer } from "./layers/UnitLayer";
Expand Down Expand Up @@ -219,6 +221,18 @@ export function createRenderer(
performanceOverlay.eventBus = eventBus;
performanceOverlay.userSettings = userSettings;

let territoryWebGLStatus = document.querySelector(
"territory-webgl-status",
) as TerritoryWebGLStatus;
if (!(territoryWebGLStatus instanceof TerritoryWebGLStatus)) {
territoryWebGLStatus = document.createElement(
"territory-webgl-status",
) as TerritoryWebGLStatus;
document.body.appendChild(territoryWebGLStatus);
}
territoryWebGLStatus.eventBus = eventBus;
territoryWebGLStatus.userSettings = userSettings;

const alertFrame = document.querySelector("alert-frame") as AlertFrame;
if (!(alertFrame instanceof AlertFrame)) {
console.error("alert frame not found");
Expand All @@ -236,6 +250,7 @@ export function createRenderer(
// Try to group layers by the return value of shouldTransform.
// Not grouping the layers may cause excessive calls to context.save() and context.restore().
const layers: Layer[] = [
territoryWebGLStatus,
new TerrainLayer(game, transformHandler),
new TerritoryLayer(game, eventBus, transformHandler, userSettings),
new RailroadLayer(game, transformHandler),
Expand Down Expand Up @@ -343,7 +358,9 @@ export class GameRenderer {
}

renderGame() {
FrameProfiler.clear();
const start = performance.now();
const layerDurations: Record<string, number> = {};
// Set background
this.context.fillStyle = this.game
.config()
Expand Down Expand Up @@ -375,15 +392,28 @@ export class GameRenderer {
needsTransform,
isTransformActive,
);

const layerStart = FrameProfiler.start();
layer.renderLayer?.(this.context);
const layerDuration = performance.now() - layerStart;

const name = layer.constructor?.name ?? "UnknownLayer";
// Accumulate time in case a layer renders multiple times per frame
layerDurations[name] = (layerDurations[name] ?? 0) + layerDuration;
}
handleTransformState(false, isTransformActive); // Ensure context is clean after rendering
this.transformHandler.resetChanged();

requestAnimationFrame(() => this.renderGame());
const duration = performance.now() - start;

this.performanceOverlay.updateFrameMetrics(duration);
const internalTimings = FrameProfiler.consume();
const combinedDurations: Record<string, number> = { ...internalTimings };
for (const [name, value] of Object.entries(layerDurations)) {
combinedDurations[name] = (combinedDurations[name] ?? 0) + value;
}

this.performanceOverlay.updateFrameMetrics(duration, combinedDurations);

if (duration > 50) {
console.warn(
Expand Down
70 changes: 70 additions & 0 deletions src/client/graphics/HoverTargetResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { UnitType } from "../../core/game/Game";
import { TileRef } from "../../core/game/GameMap";
import { GameView, PlayerView, UnitView } from "../../core/game/GameView";

const HOVER_UNIT_TYPES: UnitType[] = [
UnitType.Warship,
UnitType.TradeShip,
UnitType.TransportShip,
];
const HOVER_DISTANCE_PX = 5;

function euclideanDistWorld(
coord: { x: number; y: number },
tileRef: TileRef,
game: GameView,
): number {
const x = game.x(tileRef);
const y = game.y(tileRef);
const dx = coord.x - x;
const dy = coord.y - y;
return Math.sqrt(dx * dx + dy * dy);
}

function distSortUnitWorld(
coord: { x: number; y: number },
game: GameView,
): (a: UnitView, b: UnitView) => number {
return (a, b) => {
const distA = euclideanDistWorld(coord, a.tile(), game);
const distB = euclideanDistWorld(coord, b.tile(), game);
return distA - distB;
};
}

export interface HoverTargetResolution {
player: PlayerView | null;
unit: UnitView | null;
}

export function resolveHoverTarget(
game: GameView,
worldCoord: { x: number; y: number },
): HoverTargetResolution {
const tile = game.ref(worldCoord.x, worldCoord.y);
if (!tile) {
return { player: null, unit: null };
}

const owner = game.owner(tile);
if (owner && owner.isPlayer()) {
return { player: owner as PlayerView, unit: null };
}

if (game.isLand(tile)) {
return { player: null, unit: null };
}

const units = game
.units(...HOVER_UNIT_TYPES)
.filter(
(u) => euclideanDistWorld(worldCoord, u.tile(), game) < HOVER_DISTANCE_PX,
)
.sort(distSortUnitWorld(worldCoord, game));

if (units.length > 0) {
return { player: units[0].owner(), unit: units[0] };
}

return { player: null, unit: null };
}
36 changes: 36 additions & 0 deletions src/client/graphics/layers/BorderRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { TileRef } from "../../../core/game/GameMap";
import { PlayerView } from "../../../core/game/GameView";

export interface BorderRenderer {
setAlternativeView(enabled: boolean): void;
setHoveredPlayerId(playerSmallId: number | null): void;
drawsOwnBorders(): boolean;

updateBorder(
tile: TileRef,
owner: PlayerView | null,
isBorder: boolean,
isDefended: boolean,
hasFallout: boolean,
): void;

clearTile(tile: TileRef): void;

render(context: CanvasRenderingContext2D): void;
}

export class NullBorderRenderer implements BorderRenderer {
drawsOwnBorders(): boolean {
return false;
}

setAlternativeView() {}

setHoveredPlayerId() {}

updateBorder() {}

clearTile() {}

render() {}
}
Loading
Loading