Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
14 changes: 4 additions & 10 deletions src/client/graphics/GameRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import { SpawnTimer } from "./layers/SpawnTimer";
import { StructureIconsLayer } from "./layers/StructureIconsLayer";
import { StructureLayer } from "./layers/StructureLayer";
import { TeamStats } from "./layers/TeamStats";
import { TerrainLayer } from "./layers/TerrainLayer";
import { TerritoryLayer } from "./layers/TerritoryLayer";
import { UILayer } from "./layers/UILayer";
import { UnitDisplay } from "./layers/UnitDisplay";
Expand Down Expand Up @@ -248,7 +247,6 @@ 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[] = [
new TerrainLayer(game, transformHandler),
new TerritoryLayer(game, eventBus, transformHandler, userSettings),
new RailroadLayer(game, eventBus, transformHandler),
structureLayer,
Expand Down Expand Up @@ -315,7 +313,8 @@ export class GameRenderer {
private layers: Layer[],
private performanceOverlay: PerformanceOverlay,
) {
const context = canvas.getContext("2d", { alpha: false });
// Keep the main canvas transparent; the WebGPU territory canvas renders the background.
const context = canvas.getContext("2d", { alpha: true });
if (context === null) throw new Error("2d context not supported");
this.context = context;
}
Expand Down Expand Up @@ -363,13 +362,8 @@ export class GameRenderer {
renderGame() {
FrameProfiler.clear();
const start = performance.now();
// Set background
this.context.fillStyle = this.game
.config()
.theme()
.backgroundColor()
.toHex();
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Clear overlay canvas to transparent; the territory WebGPU canvas draws the base.
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);

const handleTransformState = (
needsTransform: boolean,
Expand Down
73 changes: 73 additions & 0 deletions src/client/graphics/HoverInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { UnitType } from "../../core/game/Game";
import { TileRef } from "../../core/game/GameMap";
import { GameView, PlayerView, UnitView } from "../../core/game/GameView";

export type HoverInfo = {
player: PlayerView | null;
unit: UnitView | null;
isWilderness: boolean;
isIrradiatedWilderness: boolean;
};

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) {
return (a: UnitView, b: UnitView) => {
const distA = euclideanDistWorld(coord, a.tile(), game);
const distB = euclideanDistWorld(coord, b.tile(), game);
return distA - distB;
};
}

export function getHoverInfo(
game: GameView,
worldCoord: { x: number; y: number },
): HoverInfo {
const info: HoverInfo = {
player: null,
unit: null,
isWilderness: false,
isIrradiatedWilderness: false,
};

if (!game.isValidCoord(worldCoord.x, worldCoord.y)) {
return info;
}

const tile = game.ref(worldCoord.x, worldCoord.y);
const owner = game.owner(tile);

if (owner && owner.isPlayer()) {
info.player = owner as PlayerView;
return info;
}

if (owner && !owner.isPlayer() && game.isLand(tile)) {
info.isIrradiatedWilderness = game.hasFallout(tile);
info.isWilderness = !info.isIrradiatedWilderness;
return info;
}

if (!game.isLand(tile)) {
const units = game
.units(UnitType.Warship, UnitType.TradeShip, UnitType.TransportShip)
.filter((u) => euclideanDistWorld(worldCoord, u.tile(), game) < 50)
.sort(distSortUnitWorld(worldCoord, game));

if (units.length > 0) {
info.unit = units[0];
}
}

return info;
}
4 changes: 4 additions & 0 deletions src/client/graphics/TransformHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export class TransformHandler {
return this._boundingRect;
}

viewOffset(): { x: number; y: number } {
return { x: this.offsetX, y: this.offsetY };
}

width(): number {
return this.boundingRect().width;
}
Expand Down
58 changes: 10 additions & 48 deletions src/client/graphics/layers/PlayerInfoOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ import {
PlayerProfile,
PlayerType,
Relation,
Unit,
UnitType,
} from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
import { AllianceView } from "../../../core/game/GameUpdates";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { ContextMenuEvent, MouseMoveEvent } from "../../InputHandler";
Expand All @@ -20,6 +18,7 @@ import {
renderTroops,
translateText,
} from "../../Utils";
import { getHoverInfo } from "../HoverInfo";
import { getFirstPlacePlayer, getPlayerIcons } from "../PlayerIcons";
import { TransformHandler } from "../TransformHandler";
import { Layer } from "./Layer";
Expand All @@ -33,26 +32,6 @@ import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url";
import portIcon from "/images/PortIcon.svg?url";
import samLauncherIcon from "/images/SamLauncherIconWhite.svg?url";

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) {
return (a: Unit | UnitView, b: Unit | UnitView) => {
const distA = euclideanDistWorld(coord, a.tile(), game);
const distB = euclideanDistWorld(coord, b.tile(), game);
return distA - distB;
};
}

@customElement("player-info-overlay")
export class PlayerInfoOverlay extends LitElement implements Layer {
@property({ type: Object })
Expand Down Expand Up @@ -119,38 +98,21 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
public maybeShow(x: number, y: number) {
this.hide();
const worldCoord = this.transform.screenToWorldCoordinates(x, y);
if (!this.game.isValidCoord(worldCoord.x, worldCoord.y)) {
return;
}
const info = getHoverInfo(this.game, worldCoord);

const tile = this.game.ref(worldCoord.x, worldCoord.y);
if (!tile) return;

const owner = this.game.owner(tile);

if (owner && owner.isPlayer()) {
this.player = owner as PlayerView;
if (info.player) {
this.player = info.player;
this.player.profile().then((p) => {
this.playerProfile = p;
});
this.setVisible(true);
} else if (owner && !owner.isPlayer() && this.game.isLand(tile)) {
if (this.game.hasFallout(tile)) {
this.isIrradiatedWilderness = true;
} else {
this.isWilderness = true;
}
} else if (info.isWilderness || info.isIrradiatedWilderness) {
this.isWilderness = info.isWilderness;
this.isIrradiatedWilderness = info.isIrradiatedWilderness;
this.setVisible(true);
} else if (info.unit) {
this.unit = info.unit;
this.setVisible(true);
Comment on lines +101 to 115
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard async profile load from stale hover.

If the mouse moves fast, an older profile() promise can overwrite the current player’s profile.

🛠️ Suggested fix
@@
   `@state`()
   private playerProfile: PlayerProfile | null = null;
+
+  private profileRequestId = 0;
@@
     if (info.player) {
-      this.player = info.player;
-      this.player.profile().then((p) => {
-        this.playerProfile = p;
-      });
+      const player = info.player;
+      this.player = player;
+      this.playerProfile = null;
+      const requestId = ++this.profileRequestId;
+      player.profile().then((p) => {
+        if (this.player === player && requestId === this.profileRequestId) {
+          this.playerProfile = p;
+        }
+      });
       this.setVisible(true);
     } else if (info.isWilderness || info.isIrradiatedWilderness) {
🤖 Prompt for AI Agents
In `@src/client/graphics/layers/PlayerInfoOverlay.ts` around lines 101 - 115, When
loading the async profile in PlayerInfoOverlay, guard against stale promises by
capturing the specific player reference before awaiting profile() and only
assigning the result if the overlay still references that same player; in the
block where you set this.player and call this.player.profile().then(...), change
to save the player in a local const (e.g. const player = info.player), set
this.player = player, then in the .then callback check if this.player === player
before setting this.playerProfile and calling this.setVisible(true) (or move
setVisible earlier as needed) so an older promise cannot overwrite a newer
hover's profile.

} else if (!this.game.isLand(tile)) {
const units = this.game
.units(UnitType.Warship, UnitType.TradeShip, UnitType.TransportShip)
.filter((u) => euclideanDistWorld(worldCoord, u.tile(), this.game) < 50)
.sort(distSortUnitWorld(worldCoord, this.game));

if (units.length > 0) {
this.unit = units[0];
this.setVisible(true);
}
}
}

Expand Down
77 changes: 0 additions & 77 deletions src/client/graphics/layers/TerrainLayer.ts

This file was deleted.

Loading
Loading