Skip to content
Merged
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
7 changes: 6 additions & 1 deletion src/client/controllers/BuildPreviewController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "../../core/game/Game";
import { TileRef } from "../../core/game/GameMap";
import { GameView } from "../../core/game/GameView";
import { UserSettings } from "../../core/game/UserSettings";
import { Controller } from "../Controller";
import {
ConfirmGhostStructureEvent,
Expand Down Expand Up @@ -57,6 +58,7 @@ export class BuildPreviewController implements Controller {
public uiState: UIState,
private transformHandler: TransformHandler,
private view: WebGLGameView,
private userSettings: UserSettings,
) {}

init() {
Expand Down Expand Up @@ -335,13 +337,16 @@ export class BuildPreviewController implements Controller {
break;
}

const cost = u.cost;
return {
ghostType: u.type,
tileX: this.game.x(tileRef),
tileY: this.game.y(tileRef),
canBuild: u.canBuild !== false,
canUpgrade: u.canUpgrade !== false,
cost: Number(u.cost),
cost: Number(cost),
showCost: this.userSettings.cursorCostLabel(),
canAfford: myPlayer.gold() >= cost,
ghostRailPaths: u.ghostRailPaths,
overlappingRailroads: u.overlappingRailroads,
ownerID: myPlayer.smallID(),
Expand Down
9 changes: 8 additions & 1 deletion src/client/hud/GameRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,14 @@ export function createRenderer(

const layers: Controller[] = [
new WarshipSelectionController(game, eventBus, transformHandler, view),
new BuildPreviewController(game, eventBus, uiState, transformHandler, view),
new BuildPreviewController(
game,
eventBus,
uiState,
transformHandler,
view,
userSettings,
),
new HoverHighlightController(game, eventBus, transformHandler, view),
new StructureHighlightController(eventBus, view),
new AttackingTroopsOverlay(game, transformHandler, eventBus, userSettings),
Expand Down
33 changes: 22 additions & 11 deletions src/client/render/gl/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import type { RadialMenuItem } from "./Events";
import { BarPass } from "./passes/BarPass";
import { BorderComputePass } from "./passes/BorderComputePass";
import { BorderStampPass } from "./passes/BorderStampPass";
import { ConquestPopupPass } from "./passes/ConquestPopupPass";
import { CoordinateGridPass } from "./passes/CoordinateGridPass";
import { CrosshairPass } from "./passes/CrosshairPass";
import { FalloutBloomPass } from "./passes/FalloutBloomPass";
Expand All @@ -56,6 +55,7 @@ import { TerrainPass } from "./passes/TerrainPass";
import { TerritoryPass } from "./passes/TerritoryPass";
import { TrailPass } from "./passes/TrailPass";
import { UnitPass } from "./passes/UnitPass";
import { WorldTextPass } from "./passes/WorldTextPass";
import { createRenderSettings, type RenderSettings } from "./RenderSettings";
import { AffiliationPalette } from "./utils/Affiliation";
import { buildTerrainRGBA, getPaletteSize } from "./utils/ColorUtils";
Expand Down Expand Up @@ -114,7 +114,7 @@ export class GPURenderer {
private crosshairPass: CrosshairPass;
private railroadPass: RailroadPass;
private barPass: BarPass;
private conquestPopupPass: ConquestPopupPass;
private worldTextPass: WorldTextPass;
private radialMenuPass: RadialMenuPass;
private selectionBoxPass: SelectionBoxPass;
private moveIndicatorPass: MoveIndicatorPass;
Expand Down Expand Up @@ -399,8 +399,8 @@ export class GPURenderer {
this.namePass = new NamePass(gl, header, paletteData, this.settings);
this.fxPass = new FxPass(gl, header, this.settings);
this.barPass = new BarPass(gl, header, this.settings);
this.conquestPopupPass = new ConquestPopupPass(gl, this.settings);
this.conquestPopupPass.setMapWidth(this.mapW);
this.worldTextPass = new WorldTextPass(gl, this.settings);
this.worldTextPass.setMapWidth(this.mapW);
this.radialMenuPass = new RadialMenuPass(gl);
this.selectionBoxPass = new SelectionBoxPass(gl);
this.moveIndicatorPass = new MoveIndicatorPass(gl, this.settings);
Expand Down Expand Up @@ -730,7 +730,7 @@ export class GPURenderer {
applyConquestEvents(events: ConquestFx[]): void {
if (events.length > 0) {
this.fxPass.applyConquestEvents(events);
this.conquestPopupPass.applyConquestEvents(events);
this.worldTextPass.applyConquestEvents(events);
}
}

Expand All @@ -741,7 +741,7 @@ export class GPURenderer {
this.localPlayerID > 0
? events.filter((e) => e.smallID === this.localPlayerID)
: events;
if (filtered.length > 0) this.conquestPopupPass.applyBonusEvents(filtered);
if (filtered.length > 0) this.worldTextPass.applyBonusEvents(filtered);
}

updateAttackRings(rings: AttackRingInput[]): void {
Expand All @@ -750,18 +750,29 @@ export class GPURenderer {

clearFx(): void {
this.fxPass.clear();
this.conquestPopupPass.clear();
this.worldTextPass.clear();
}
setFxTimeFn(fn: () => number): void {
this.fxPass.setTimeFn(fn);
this.conquestPopupPass.setTimeFn(fn);
this.worldTextPass.setTimeFn(fn);
}

updateGhostPreview(data: GhostPreviewData | null): void {
this.structurePass.updateGhostPreview(data);
this.railroadPass.updateGhostPreview(data);
this.rangeCirclePass.updateGhostPreview(data);
this.crosshairPass.updateGhostPreview(data);
this.worldTextPass.setGhostCostLabel(
data && data.showCost && data.cost > 0
? {
tileX: data.tileX,
tileY: data.tileY,
cost: data.cost,
canAfford: data.canAfford,
canPlace: data.canBuild || data.canUpgrade,
}
: null,
);
this.samGhostVisible =
data !== null && SAM_RADIUS_GHOST_TYPES.has(data.ghostType);
this.samRadiusPass.setVisible(
Expand Down Expand Up @@ -1162,8 +1173,8 @@ export class GPURenderer {
this.fxPass.draw(cam, zoom);
}

this.conquestPopupPass.tick();
this.conquestPopupPass.draw(cam, zoom);
this.worldTextPass.tick();
this.worldTextPass.draw(cam, zoom);

if (this.gridView) this.coordinateGridPass.draw(cam, zoom);
if (pe.name && !this.gridView)
Expand Down Expand Up @@ -1203,7 +1214,7 @@ export class GPURenderer {
this.unitPass.dispose();
this.namePass.dispose();
this.fxPass.dispose();
this.conquestPopupPass.dispose();
this.worldTextPass.dispose();
this.radialMenuPass.dispose();
this.selectionBoxPass.dispose();
this.moveIndicatorPass.dispose();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/**
* ConquestPopupPass — MSDF-rendered floating text popups.
* WorldTextPass — MSDF-rendered text in world space.
*
* Renders two kinds of popups using the same MSDF atlas as NamePass:
* - Conquest popups: "+ 500" gold text at conquered player locations (static position, fade only)
* - Bonus popups: "+ 45K" income text at port tiles (rises upward + fades)
* One pass, one MSDF atlas, several callers:
* - Conquest popups: "+ 500" gold text at conquered player locations (fade only)
* - Bonus popups: "+ 45K" income text at port tiles (rises upward + fades)
* - Ghost cost label: persistent build-cost number under the ghost cursor
*/

import type { BonusEvent, ConquestFx } from "../../types";
Expand All @@ -16,8 +17,9 @@ import { layoutString } from "./name-pass/TextLayout";
import { CHAR_RANGE, MAX_CHARS } from "./name-pass/Types";

import { assetUrl } from "src/core/AssetUrls";
import fragSrc from "../shaders/conquest-popup/conquest-popup.frag.glsl?raw";
import vertSrc from "../shaders/conquest-popup/conquest-popup.vert.glsl?raw";
import { renderNumber } from "../../../Utils";
import fragSrc from "../shaders/world-text/world-text.frag.glsl?raw";
import vertSrc from "../shaders/world-text/world-text.vert.glsl?raw";

const atlasUrl = assetUrl("atlases/msdf-atlas.png");

Expand All @@ -36,6 +38,12 @@ const CONQUEST_Y_OFFSET = 8;
/** World-space font size for conquest popups. */
const CONQUEST_SCALE = 6;
const CONQUEST_OUTLINE_WIDTH = 2.0;
/** Tiles below the ghost icon center for the cost label. */
const GHOST_COST_Y_OFFSET = 3;
/** World-space font size — smaller than popups so it sits unobtrusively under the icon. */
const GHOST_COST_SCALE = 4;
/** Matches player-name outline width for a consistent UI look. */
const GHOST_COST_OUTLINE_WIDTH = 1.4;
Comment on lines +41 to +46
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.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Move ghost-cost tuning values into RenderSettings.

The new constants on Line 41, Line 43, and Line 45 are per-pass tuning values but are hardcoded in the pass.

As per coding guidelines All per-pass tuning constants must be defined in render-settings.json and accessed through RenderSettings, not hardcoded in pass classes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/render/gl/passes/ConquestPopupPass.ts` around lines 40 - 45, Move
the per-pass tuning constants GHOST_COST_Y_OFFSET, GHOST_COST_SCALE, and
GHOST_COST_OUTLINE_WIDTH out of ConquestPopupPass and into render-settings.json
as new keys (e.g., ghostCostYOff, ghostCostScale, ghostCostOutlineWidth), then
access them through the RenderSettings accessor used elsewhere (e.g.,
RenderSettings.<property> or the existing getter pattern) inside
ConquestPopupPass instead of the hardcoded values so the pass reads
RenderSettings.ghostCostYOff, RenderSettings.ghostCostScale and
RenderSettings.ghostCostOutlineWidth at runtime.


// ---------------------------------------------------------------------------
// Active popup tracking
Expand All @@ -62,10 +70,10 @@ function formatGold(gold: number): string {
}

// ---------------------------------------------------------------------------
// ConquestPopupPass
// WorldTextPass
// ---------------------------------------------------------------------------

export class ConquestPopupPass {
export class WorldTextPass {
private gl: WebGL2RenderingContext;
private program: WebGLProgram;
private maxInstances = 512;
Expand Down Expand Up @@ -101,6 +109,16 @@ export class ConquestPopupPass {
// Active popups (both conquest and bonus, unified)
private active: ActivePopup[] = [];

// Persistent ghost-cost label (separate from popup lifecycle; doesn't fade).
private ghostCostLabel: {
x: number;
y: number;
text: string;
colorR: number;
colorG: number;
colorB: number;
} | null = null;

// Settings reference
private settings: RenderSettings;

Expand Down Expand Up @@ -277,12 +295,56 @@ export class ConquestPopupPass {
}
}

/**
* Set or clear the ghost-cost label rendered under the build cursor.
* `null` clears it. Called from Renderer.updateGhostPreview.
*/
setGhostCostLabel(
label: {
tileX: number;
tileY: number;
cost: number;
canAfford: boolean;
canPlace: boolean;
} | null,
): void {
if (label === null) {
this.ghostCostLabel = null;
return;
}
// Color precedence: red (can't afford) > gray (can't place here) > white (OK).
let r = 1,
g = 1,
b = 1;
if (!label.canAfford) {
g = 0.3;
b = 0.3;
} else if (!label.canPlace) {
r = 0.6;
g = 0.6;
b = 0.6;
}
// The vertex shader adds +0.5 to (x, y) for tile-center alignment, so we
// pass raw tile coords here — same convention as the other popup entries.
this.ghostCostLabel = {
x: label.tileX,
y: label.tileY + GHOST_COST_Y_OFFSET,
text: renderNumber(label.cost),
colorR: r,
colorG: g,
colorB: b,
};
}

// -------------------------------------------------------------------------
// Tick — cull expired, rebuild instance buffer
// -------------------------------------------------------------------------

tick(): void {
if (this.active.length === 0) return;
if (this.active.length === 0 && this.ghostCostLabel === null) {
this.instanceCount = 0;
return;
}
const now = this.now();

// Remove expired popups (swap-remove)
Expand Down Expand Up @@ -340,6 +402,38 @@ export class ConquestPopupPass {
}
}

// Ghost cost label — persistent, no fade or rise. layoutString already
// centers cursors around 0, so passing the tile coord places the text
// centered on the tile (vertex shader adds the +0.5 tile-center offset).
const label = this.ghostCostLabel;
if (label) {
layoutString(
label.text,
this.glyph,
this.kernTable,
this.charCodes,
this.cursors,
);
const len = Math.min(label.text.length, MAX_CHARS);
for (let i = 0; i < len; i++) {
if (this.charCodes[i] === 0) continue;
if (count >= this.maxInstances) this.growBuffer();

const off = count * FLOATS_PER_INSTANCE;
this.instanceData[off + 0] = label.x;
this.instanceData[off + 1] = label.y;
this.instanceData[off + 2] = this.cursors[i];
this.instanceData[off + 3] = this.charCodes[i];
this.instanceData[off + 4] = 1;
this.instanceData[off + 5] = label.colorR;
this.instanceData[off + 6] = label.colorG;
this.instanceData[off + 7] = label.colorB;
this.instanceData[off + 8] = GHOST_COST_SCALE;
this.instanceData[off + 9] = GHOST_COST_OUTLINE_WIDTH;
count++;
}
}

this.instanceCount = count;
}

Expand Down
4 changes: 4 additions & 0 deletions src/client/render/types/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ export interface GhostPreviewData {
canBuild: boolean; // Valid placement?
canUpgrade: boolean; // Upgrading existing structure?
cost: number; // Gold cost
/** Whether to render the cost label under the ghost (user setting). */
showCost: boolean;
/** True if the player has enough gold to afford this build (drives label color). */
canAfford: boolean;
ghostRailPaths: TileRef[][]; // TileRef paths (City/Port only)
overlappingRailroads: TileRef[]; // TileRefs containing rails in snap zone
ownerID: number; // Player's smallID (for color)
Expand Down
Loading