From 986f0b61bf976be03fc571ac9eb16b00c2165fe8 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 3 Jun 2026 21:40:00 -0700 Subject: [PATCH] Fix WorldTextPass labels scaling with device-pixel-ratio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attack troop labels, the ghost-cost chip, and the bonusPopup minScreenScale floor were dividing by `zoom`, which Camera.ts stores as device-pixels-per-world-unit (canvasW = cssWidth * dpr). The result was constant device-pixel size — labels rendered ~2x larger on DPR=1 displays than on retina. Multiply each screen-relative scale by dpr so the on-screen size stays constant in CSS pixels. Retune the now-correct sizes: attack labels 34 -> 17, ghost-cost screenScale 30 -> 18, screenYOffset 50 -> 25. --- src/client/render/gl/passes/WorldTextPass.ts | 19 ++++++++++++++----- src/client/render/gl/render-settings.json | 4 ++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/client/render/gl/passes/WorldTextPass.ts b/src/client/render/gl/passes/WorldTextPass.ts index 4a37dcbd2..0a6b43540 100644 --- a/src/client/render/gl/passes/WorldTextPass.ts +++ b/src/client/render/gl/passes/WorldTextPass.ts @@ -44,7 +44,7 @@ const GHOST_COST_OUTLINE_WIDTH = 1.4; * zoom each frame so the on-screen label size stays constant regardless of * how far the camera is zoomed. */ -const ATTACK_LABEL_SCREEN_SCALE = 34.0; +const ATTACK_LABEL_SCREEN_SCALE = 17.0; const ATTACK_LABEL_OUTLINE_WIDTH = 1.2; // --------------------------------------------------------------------------- @@ -397,6 +397,9 @@ export class WorldTextPass { private rebuildInstances(now: number, zoom: number): void { let count = 0; + // canvasW in Camera is cssWidth*dpr, so `zoom` is device-px-per-world-unit. + // Multiply screen-relative scales by dpr to keep a constant CSS-pixel size. + const dpr = window.devicePixelRatio || 1; for (const popup of this.active) { const elapsed = now - popup.startMs; @@ -442,7 +445,8 @@ export class WorldTextPass { // Attack troop labels — persistent, no fade. Controller interpolates // positions before pushing. Scale is divided by zoom so the label keeps // a constant on-screen size regardless of how zoomed-in the camera is. - const attackScale = ATTACK_LABEL_SCREEN_SCALE / Math.max(zoom, 0.0001); + const attackScale = + (ATTACK_LABEL_SCREEN_SCALE * dpr) / Math.max(zoom, 0.0001); for (const label of this.attackTroopLabels) { layoutString( label.text, @@ -478,8 +482,9 @@ export class WorldTextPass { const label = this.ghostCostLabel; if (label) { const invZoom = 1 / Math.max(zoom, 0.0001); - const ghostScale = this.settings.ghostCost.screenScale * invZoom; - const ghostY = label.y + this.settings.ghostCost.screenYOffset * invZoom; + const ghostScale = this.settings.ghostCost.screenScale * dpr * invZoom; + const ghostY = + label.y + this.settings.ghostCost.screenYOffset * dpr * invZoom; layoutString( label.text, this.glyph, @@ -536,7 +541,11 @@ export class WorldTextPass { gl.useProgram(this.program); gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix); gl.uniform1f(this.uZoom, zoom); - gl.uniform1f(this.uMinScreenScale, this.settings.bonusPopup.minScreenScale); + const dpr = window.devicePixelRatio || 1; + gl.uniform1f( + this.uMinScreenScale, + this.settings.bonusPopup.minScreenScale * dpr, + ); gl.uniform1f(this.uDistRange, this.distanceRange); gl.activeTexture(gl.TEXTURE0); diff --git a/src/client/render/gl/render-settings.json b/src/client/render/gl/render-settings.json index 3a7777bf2..988138317 100644 --- a/src/client/render/gl/render-settings.json +++ b/src/client/render/gl/render-settings.json @@ -270,8 +270,8 @@ "cullZoom": 0.3 }, "ghostCost": { - "screenScale": 30, - "screenYOffset": 50 + "screenScale": 18, + "screenYOffset": 25 }, "spawnOverlay": { "highlightRadius": 9,