diff --git a/resources/lang/en.json b/resources/lang/en.json
index 5dfce1e70..fb35d5134 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -516,6 +516,8 @@
"black": "Black",
"classic_icons_desc": "Lighter outline with near-black interior",
"classic_icons_label": "Classic icons",
+ "classic_names_desc": "Arial bitmap font for player names and troops (off uses the smooth font)",
+ "classic_names_label": "Classic names",
"classic_numbers_desc": "Pixel font for structure level numbers (off uses the smooth font)",
"classic_numbers_label": "Classic level numbers",
"colored": "Colored",
diff --git a/src/client/hud/layers/GraphicsSettingsModal.ts b/src/client/hud/layers/GraphicsSettingsModal.ts
index 22ca56b20..3be6cde09 100644
--- a/src/client/hud/layers/GraphicsSettingsModal.ts
+++ b/src/client/hud/layers/GraphicsSettingsModal.ts
@@ -535,6 +535,14 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
this.patchName({ darkNames: !this.currentDarkNames() });
}
+ private currentClassicNames(): boolean {
+ return this.userSettings.graphicsOverrides().name?.classicFont ?? false;
+ }
+
+ private onToggleClassicNames() {
+ this.patchName({ classicFont: !this.currentClassicNames() });
+ }
+
private onResetClick() {
this.userSettings.setGraphicsOverrides({});
this.requestUpdate();
@@ -550,6 +558,7 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
const hoverGlowAlpha = this.currentHoverGlowAlpha();
const namesColored = !this.currentDarkNames();
const iconSize = this.currentIconSize();
+ const classicNames = this.currentClassicNames();
const classicIcons = this.currentClassicIcons();
const classicNumbers = this.currentClassicNumbers();
const highlightFill = this.currentHighlightFill();
@@ -800,6 +809,25 @@ export class GraphicsSettingsModal extends LitElement implements Controller {
+
+
diff --git a/src/client/render/gl/GraphicsOverrides.ts b/src/client/render/gl/GraphicsOverrides.ts
index 453e371c5..c61adc009 100644
--- a/src/client/render/gl/GraphicsOverrides.ts
+++ b/src/client/render/gl/GraphicsOverrides.ts
@@ -7,6 +7,7 @@ export const GraphicsOverridesSchema = z
nameScaleFactor: z.number(),
cullThreshold: z.number(),
darkNames: z.boolean(),
+ classicFont: z.boolean(),
hoverFadeAlpha: z.number(),
hoverGlowWidth: z.number(),
hoverGlowAlpha: z.number(),
diff --git a/src/client/render/gl/RenderOverrides.ts b/src/client/render/gl/RenderOverrides.ts
index cd71ea9ed..c36e565a1 100644
--- a/src/client/render/gl/RenderOverrides.ts
+++ b/src/client/render/gl/RenderOverrides.ts
@@ -28,6 +28,8 @@ export function applyGraphicsOverrides(
if (overrides.structure?.iconSize !== undefined) {
settings.structure.iconSize = overrides.structure.iconSize;
}
+ // Classic names: Arial bitmap font for names/troops (default false = MSDF).
+ settings.name.classicFont = overrides.name?.classicFont ?? false;
if (overrides.structure?.classicIcons ?? true) {
// Classic look (default): lighter player-colored shape behind a darkened
// player-colored icon glyph (matching the old canvas renderer's
diff --git a/src/client/render/gl/RenderSettings.ts b/src/client/render/gl/RenderSettings.ts
index 6654e50a4..b6c701f31 100644
--- a/src/client/render/gl/RenderSettings.ts
+++ b/src/client/render/gl/RenderSettings.ts
@@ -265,6 +265,8 @@ export interface RenderSettings {
hoverGlowWidth: number;
/** Peak opacity of the hover glow (0 disables it). */
hoverGlowAlpha: number;
+ /** true = Arial bitmap font for names/troops, false = overpass MSDF. */
+ classicFont: boolean;
};
fx: {
shockwaveRingWidth: number;
diff --git a/src/client/render/gl/passes/name-pass/ArialAtlas.ts b/src/client/render/gl/passes/name-pass/ArialAtlas.ts
new file mode 100644
index 000000000..ae7904803
--- /dev/null
+++ b/src/client/render/gl/passes/name-pass/ArialAtlas.ts
@@ -0,0 +1,184 @@
+/**
+ * Runtime Arial bitmap-font atlas generator.
+ *
+ * The "classic" name font is Arial rendered to a coverage atlas (white glyphs
+ * on transparent), as an alternative to the overpass-bold MSDF atlas. There's
+ * no Arial asset to ship and no offline bmfont tooling, so the atlas is built
+ * once at startup with canvas 2D: each glyph is rasterized, its tight bounding
+ * box is found by scanning pixels, and the glyphs are shelf-packed.
+ *
+ * Metrics are emitted with the SAME em size (48) and baseline (36) as the MSDF
+ * atlas, so name sizing, hit-testing, flag offsets and icon alignment are all
+ * unchanged — only the glyph shapes and advances differ. The returned shape is
+ * a ParsedAtlas, so it flows through buildGlyphTables / buildGlyphMetricsTex /
+ * layoutString exactly like the MSDF atlas.
+ */
+
+import type { BMChar, ParsedAtlas } from "./Types";
+
+// Match the MSDF atlas em/base so all downstream sizing stays identical.
+const EM = 48;
+const BASE = 36;
+// Bold to match the weight of the overpass-bold name style.
+const FONT = `bold ${EM}px Arial, "Liberation Sans", sans-serif`;
+
+const ATLAS_W = 1024;
+const PAD = 2; // transparent gutter between packed glyphs
+
+// Codepoint coverage: ASCII + Latin-1 + Latin Extended-A (matches CHAR_RANGE),
+// skipping the C0/C1 control gaps. Covers player names and troop labels.
+function* codepoints(): Generator {
+ for (let c = 32; c <= 383; c++) {
+ if (c >= 127 && c <= 159) continue;
+ yield c;
+ }
+}
+
+interface Measured {
+ code: number;
+ ch: string;
+ advance: number;
+ w: number;
+ h: number;
+ xoffset: number;
+ yoffset: number;
+ // Where the tight bbox landed in the measuring canvas (to re-place it later).
+ srcMinX: number;
+ srcMinY: number;
+}
+
+export function generateArialBitmapAtlas(): {
+ atlas: ParsedAtlas;
+ canvas: HTMLCanvasElement;
+} {
+ // --- measuring pass: rasterize each glyph, find its tight bbox ---
+ const CELL = 80; // > any 48px glyph incl. ascenders/descenders
+ const PEN_X = 8;
+ const PEN_Y = 56; // baseline inside the measuring cell
+ const tmp = document.createElement("canvas");
+ tmp.width = CELL;
+ tmp.height = CELL;
+ const tctx = tmp.getContext("2d", { willReadFrequently: true })!;
+ tctx.font = FONT;
+ tctx.textBaseline = "alphabetic";
+ tctx.textAlign = "left";
+ tctx.fillStyle = "#fff";
+
+ const measured: Measured[] = [];
+ for (const code of codepoints()) {
+ const ch = String.fromCodePoint(code);
+ const advance = Math.ceil(tctx.measureText(ch).width);
+ tctx.clearRect(0, 0, CELL, CELL);
+ tctx.fillText(ch, PEN_X, PEN_Y);
+ const data = tctx.getImageData(0, 0, CELL, CELL).data;
+
+ let minX = CELL;
+ let minY = CELL;
+ let maxX = -1;
+ let maxY = -1;
+ for (let y = 0; y < CELL; y++) {
+ for (let x = 0; x < CELL; x++) {
+ if (data[(y * CELL + x) * 4 + 3] > 8) {
+ if (x < minX) minX = x;
+ if (x > maxX) maxX = x;
+ if (y < minY) minY = y;
+ if (y > maxY) maxY = y;
+ }
+ }
+ }
+
+ if (maxX < 0) {
+ // No ink (e.g. space) — advance only, zero-size glyph.
+ measured.push({
+ code,
+ ch,
+ advance,
+ w: 0,
+ h: 0,
+ xoffset: 0,
+ yoffset: 0,
+ srcMinX: 0,
+ srcMinY: 0,
+ });
+ continue;
+ }
+
+ measured.push({
+ code,
+ ch,
+ advance,
+ w: maxX - minX + 1,
+ h: maxY - minY + 1,
+ xoffset: minX - PEN_X, // glyph left relative to the pen
+ yoffset: BASE - (PEN_Y - minY), // glyph top relative to the line top
+ srcMinX: minX,
+ srcMinY: minY,
+ });
+ }
+
+ // --- shelf packing into a fixed-width atlas ---
+ const chars: BMChar[] = [];
+ const placements: { m: Measured; ax: number; ay: number }[] = [];
+ let x = PAD;
+ let y = PAD;
+ let rowH = 0;
+ for (const m of measured) {
+ if (m.w === 0) {
+ chars.push(toBMChar(m, 0, 0));
+ continue;
+ }
+ if (x + m.w + PAD > ATLAS_W) {
+ x = PAD;
+ y += rowH + PAD;
+ rowH = 0;
+ }
+ placements.push({ m, ax: x, ay: y });
+ chars.push(toBMChar(m, x, y));
+ x += m.w + PAD;
+ if (m.h > rowH) rowH = m.h;
+ }
+ const ATLAS_H = y + rowH + PAD;
+
+ // --- render pass: draw each glyph at its packed slot ---
+ const canvas = document.createElement("canvas");
+ canvas.width = ATLAS_W;
+ canvas.height = ATLAS_H;
+ const actx = canvas.getContext("2d")!;
+ actx.font = FONT;
+ actx.textBaseline = "alphabetic";
+ actx.textAlign = "left";
+ actx.fillStyle = "#fff";
+ for (const { m, ax, ay } of placements) {
+ // Drawing at pen P put the bbox at (P + (srcMin - PEN)); solve for the pen
+ // that lands the tight top-left at (ax, ay).
+ const penX = ax - m.srcMinX + PEN_X;
+ const penY = ay - m.srcMinY + PEN_Y;
+ actx.fillText(m.ch, penX, penY);
+ }
+
+ const atlas: ParsedAtlas = {
+ fontSize: EM,
+ base: BASE,
+ scaleW: ATLAS_W,
+ scaleH: ATLAS_H,
+ distanceRange: 0,
+ chars,
+ kernings: [],
+ };
+ return { atlas, canvas };
+}
+
+function toBMChar(m: Measured, ax: number, ay: number): BMChar {
+ return {
+ id: m.code,
+ char: m.ch,
+ width: m.w,
+ height: m.h,
+ xoffset: m.xoffset,
+ yoffset: m.yoffset,
+ xadvance: m.advance,
+ x: ax,
+ y: ay,
+ page: 0,
+ };
+}
diff --git a/src/client/render/gl/passes/name-pass/TextProgram.ts b/src/client/render/gl/passes/name-pass/TextProgram.ts
index 66ff87ee1..e75671841 100644
--- a/src/client/render/gl/passes/name-pass/TextProgram.ts
+++ b/src/client/render/gl/passes/name-pass/TextProgram.ts
@@ -1,9 +1,16 @@
/**
- * TextProgram — MSDF text rendering (player names + troop counts).
+ * TextProgram — text rendering for player names + troop counts.
*
- * Owns: shader program, uniform locations, MSDF atlas texture (async loaded).
- * Shared textures (glyphMetrics, cursor, strings, playerData) are passed in
- * and bound at draw time but not owned/deleted by this class.
+ * Supports two fonts, switchable per draw via settings.name.classicFont:
+ * - MSDF: the overpass-bold atlas (default), loaded async from CDN.
+ * - classic: an Arial bitmap coverage atlas built at runtime (set via
+ * setArialFont), drawn fill-only (no synthesized outline).
+ * Both fonts share the em size (48) and baseline (36), so name sizing and the
+ * sibling icon/status passes are unaffected by the choice.
+ *
+ * Owns: shader program, uniform locations, the MSDF atlas texture and the
+ * Arial atlas texture. Glyph-metric / cursor / string / player-data textures
+ * are passed in and bound at draw time but not owned here.
*/
import { assetUrl } from "src/core/AssetUrls";
@@ -28,14 +35,26 @@ export class TextProgram {
private program: WebGLProgram;
private textures: TextProgramTextures;
- // Async-loaded MSDF atlas
+ // MSDF atlas (async-loaded)
private atlasTex: WebGLTexture | null = null;
private atlasReady = false;
+ private msdfScaleW: number;
+ private msdfScaleH: number;
+ private distanceRange: number;
+
+ // Arial bitmap atlas (built at runtime, set via setArialFont)
+ private arialAtlasTex: WebGLTexture | null = null;
+ private arialMetricsTex: WebGLTexture | null = null;
+ private arialScaleW = 0;
+ private arialScaleH = 0;
// Uniform locations
private uCamera: WebGLUniformLocation;
private uTime: WebGLUniformLocation;
private uDistRange: WebGLUniformLocation;
+ private uAtlasScaleW: WebGLUniformLocation;
+ private uAtlasScaleH: WebGLUniformLocation;
+ private uClassic: WebGLUniformLocation;
private uLerpSpeed: WebGLUniformLocation;
private uCullThreshold: WebGLUniformLocation;
private uNameScaleFactor: WebGLUniformLocation;
@@ -52,8 +71,6 @@ export class TextProgram {
private uHoverGlowWidth: WebGLUniformLocation;
private uHoverGlowAlpha: WebGLUniformLocation;
- private distanceRange: number;
-
constructor(
gl: WebGL2RenderingContext,
atlas: ParsedAtlas,
@@ -62,6 +79,8 @@ export class TextProgram {
this.gl = gl;
this.textures = textures;
this.distanceRange = atlas.distanceRange;
+ this.msdfScaleW = atlas.scaleW;
+ this.msdfScaleH = atlas.scaleH;
this.program = createProgram(
gl,
@@ -77,25 +96,20 @@ export class TextProgram {
gl.uniform1i(gl.getUniformLocation(this.program, "uStrings"), 3);
gl.uniform1i(gl.getUniformLocation(this.program, "uPlayerData"), 4);
- // Static uniforms
+ // Static uniforms — em size + baseline are shared by both fonts.
gl.uniform1f(
gl.getUniformLocation(this.program, "uFontSize")!,
atlas.fontSize,
);
- gl.uniform1f(
- gl.getUniformLocation(this.program, "uAtlasScaleW")!,
- atlas.scaleW,
- );
- gl.uniform1f(
- gl.getUniformLocation(this.program, "uAtlasScaleH")!,
- atlas.scaleH,
- );
gl.uniform1f(gl.getUniformLocation(this.program, "uBase")!, atlas.base);
// Dynamic uniform locations
this.uCamera = gl.getUniformLocation(this.program, "uCamera")!;
this.uTime = gl.getUniformLocation(this.program, "uTime")!;
this.uDistRange = gl.getUniformLocation(this.program, "uDistRange")!;
+ this.uAtlasScaleW = gl.getUniformLocation(this.program, "uAtlasScaleW")!;
+ this.uAtlasScaleH = gl.getUniformLocation(this.program, "uAtlasScaleH")!;
+ this.uClassic = gl.getUniformLocation(this.program, "uClassic")!;
this.uLerpSpeed = gl.getUniformLocation(this.program, "uLerpSpeed")!;
this.uCullThreshold = gl.getUniformLocation(
this.program,
@@ -142,8 +156,30 @@ export class TextProgram {
this.loadAtlas();
}
- get ready(): boolean {
- return this.atlasReady;
+ /** True when the atlas for the requested font is uploaded and drawable. */
+ isReady(classic: boolean): boolean {
+ return classic ? this.arialAtlasTex !== null : this.atlasReady;
+ }
+
+ /** Install the runtime-built Arial bitmap atlas (coverage mask) + metrics. */
+ setArialFont(
+ canvas: HTMLCanvasElement,
+ metricsTex: WebGLTexture,
+ scaleW: number,
+ scaleH: number,
+ ): void {
+ const gl = this.gl;
+ const tex = gl.createTexture()!;
+ gl.bindTexture(gl.TEXTURE_2D, tex);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
+ this.arialAtlasTex = tex;
+ this.arialMetricsTex = metricsTex;
+ this.arialScaleW = scaleW;
+ this.arialScaleH = scaleH;
}
private loadAtlas(): void {
@@ -172,16 +208,28 @@ export class TextProgram {
ambient: number,
highlightOwnerID: number,
fadeOwnerID: number,
+ classic: boolean,
): void {
- if (!this.atlasReady) return;
+ if (!this.isReady(classic)) return;
const gl = this.gl;
const ns = settings.name;
gl.useProgram(this.program);
+ const atlasTex = classic ? this.arialAtlasTex! : this.atlasTex!;
+ const metricsTex = classic
+ ? this.arialMetricsTex!
+ : this.textures.glyphMetrics;
+ const scaleW = classic ? this.arialScaleW : this.msdfScaleW;
+ const scaleH = classic ? this.arialScaleH : this.msdfScaleH;
+ const distRange = classic ? 0 : this.distanceRange;
+
gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix);
gl.uniform1f(this.uTime, performance.now() / 1000);
- gl.uniform1f(this.uDistRange, this.distanceRange);
+ gl.uniform1f(this.uDistRange, distRange);
+ gl.uniform1f(this.uAtlasScaleW, scaleW);
+ gl.uniform1f(this.uAtlasScaleH, scaleH);
+ gl.uniform1i(this.uClassic, classic ? 1 : 0);
gl.uniform1f(this.uLerpSpeed, ns.lerpSpeed);
gl.uniform1f(this.uCullThreshold, ns.cullThreshold);
gl.uniform1f(this.uNameScaleFactor, ns.nameScaleFactor);
@@ -202,9 +250,9 @@ export class TextProgram {
gl.uniform1f(this.uHoverGlowAlpha, ns.hoverGlowAlpha);
gl.activeTexture(gl.TEXTURE0);
- gl.bindTexture(gl.TEXTURE_2D, this.atlasTex!);
+ gl.bindTexture(gl.TEXTURE_2D, atlasTex);
gl.activeTexture(gl.TEXTURE1);
- gl.bindTexture(gl.TEXTURE_2D, this.textures.glyphMetrics);
+ gl.bindTexture(gl.TEXTURE_2D, metricsTex);
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, this.textures.cursor);
gl.activeTexture(gl.TEXTURE3);
@@ -225,5 +273,6 @@ export class TextProgram {
const gl = this.gl;
gl.deleteProgram(this.program);
if (this.atlasTex) gl.deleteTexture(this.atlasTex);
+ if (this.arialAtlasTex) gl.deleteTexture(this.arialAtlasTex);
}
}
diff --git a/src/client/render/gl/passes/name-pass/index.ts b/src/client/render/gl/passes/name-pass/index.ts
index 6072c82de..358811939 100644
--- a/src/client/render/gl/passes/name-pass/index.ts
+++ b/src/client/render/gl/passes/name-pass/index.ts
@@ -29,6 +29,7 @@ import type { RenderSettings } from "../../RenderSettings";
import { createFullscreenQuad } from "../../utils/GlUtils";
import { renderTroops } from "../../../../Utils";
+import { generateArialBitmapAtlas } from "./ArialAtlas";
import type { GlyphTables } from "./AtlasData";
import {
buildEmojiLookup,
@@ -49,7 +50,7 @@ import { StatusIconProgram } from "./StatusIconProgram";
import { layoutString } from "./TextLayout";
import { TextProgram } from "./TextProgram";
import type { PlayerSlot } from "./Types";
-import { LINES_PER_PLAYER, MAX_CHARS } from "./Types";
+import { CHAR_RANGE, LINES_PER_PLAYER, MAX_CHARS } from "./Types";
// Flag quad aspect ratio — must match FLAG_CELL_W / FLAG_CELL_H in FlagAtlasArray.ts.
const FLAG_ASPECT = 128 / 85;
@@ -73,11 +74,19 @@ export class NamePass {
private statusIconProgram: StatusIconProgram;
private debugProgram: DebugProgram;
- // Atlas + glyph data
+ // Atlas + glyph data. `glyph`/`kernTable` point at the active font; the MSDF
+ // and Arial tables are kept so layout can be recomputed when the font toggles.
private glyph: GlyphTables;
private kernTable: Int8Array;
private fontSize: number;
private fontBase: number;
+ private msdfGlyph: GlyphTables;
+ private msdfKern: Int8Array;
+ private arialGlyph: GlyphTables;
+ private arialKern: Int8Array;
+ private arialMetricsTex: WebGLTexture;
+ // Which font the cursor buffers are currently laid out for.
+ private classicFont = false;
// Player management
private playerByID: Map;
@@ -130,6 +139,8 @@ export class NamePass {
this.fontBase = atlas.base;
this.glyph = buildGlyphTables(atlas.chars);
this.kernTable = buildKernTable(atlas.kernings);
+ this.msdfGlyph = this.glyph;
+ this.msdfKern = this.kernTable;
this.emojiCharToIndex = buildEmojiLookup();
// Runtime flag-image manager (TEXTURE_2D_ARRAY of player flags, fetched
@@ -204,6 +215,25 @@ export class NamePass {
this.playerDataTex,
this.maxPlayers,
);
+
+ // Classic Arial bitmap font: built once at runtime, same em/baseline as the
+ // MSDF atlas so sizing/icons are unchanged. Selected via name.classicFont.
+ const arial = generateArialBitmapAtlas();
+ this.arialGlyph = buildGlyphTables(arial.atlas.chars);
+ this.arialKern = new Int8Array(CHAR_RANGE * CHAR_RANGE); // no kerning
+ this.arialMetricsTex = buildGlyphMetricsTex(gl, arial.atlas);
+ this.textProgram.setArialFont(
+ arial.canvas,
+ this.arialMetricsTex,
+ arial.atlas.scaleW,
+ arial.atlas.scaleH,
+ );
+
+ this.classicFont = settings.name.classicFont;
+ if (this.classicFont) {
+ this.glyph = this.arialGlyph;
+ this.kernTable = this.arialKern;
+ }
}
// -------------------------------------------------------------------------
@@ -643,8 +673,37 @@ export class NamePass {
return 0;
}
+ /**
+ * Switch the active text font when settings.name.classicFont changes, then
+ * re-lay-out every name + troop line — advances differ between the two fonts,
+ * so cursor positions and name half-widths must be recomputed.
+ */
+ private syncFont(): void {
+ const classic = this.settings.name.classicFont;
+ if (classic === this.classicFont) return;
+ this.classicFont = classic;
+ this.glyph = classic ? this.arialGlyph : this.msdfGlyph;
+ this.kernTable = classic ? this.arialKern : this.msdfKern;
+ for (const slot of this.slots.values()) {
+ if (slot.nameLen > 0) {
+ slot.nameHalfWidth = this.uploadStringRow(
+ slot.index * LINES_PER_PLAYER,
+ slot.static.displayName,
+ );
+ }
+ if (slot.troopLen > 0 && slot.lastTroopStr) {
+ this.uploadStringRow(
+ slot.index * LINES_PER_PLAYER + 1,
+ slot.lastTroopStr,
+ );
+ }
+ this.writePlayerDataRow(slot);
+ }
+ }
+
draw(cameraMatrix: Float32Array, ambient: number): void {
- if (!this.textProgram.ready) return;
+ this.syncFont();
+ if (!this.textProgram.isReady(this.classicFont)) return;
if (this.slots.size === 0) return;
const gl = this.gl;
@@ -704,6 +763,7 @@ export class NamePass {
ambient,
this.highlightOwnerID,
fadeOwnerID,
+ this.classicFont,
);
this.statusIconProgram.draw(
cameraMatrix,
@@ -730,6 +790,7 @@ export class NamePass {
this.statusIconProgram.dispose();
this.debugProgram.dispose();
gl.deleteTexture(this.glyphMetricsTex);
+ gl.deleteTexture(this.arialMetricsTex);
gl.deleteTexture(this.cursorTex);
gl.deleteTexture(this.stringTex);
gl.deleteTexture(this.playerDataTex);
diff --git a/src/client/render/gl/render-settings.json b/src/client/render/gl/render-settings.json
index 88125818d..1858961ad 100644
--- a/src/client/render/gl/render-settings.json
+++ b/src/client/render/gl/render-settings.json
@@ -217,7 +217,8 @@
"statusRowOffset": 1.4,
"hoverFadeAlpha": 0.5,
"hoverGlowWidth": 5,
- "hoverGlowAlpha": 0.75
+ "hoverGlowAlpha": 0.75,
+ "classicFont": false
},
"fx": {
"shockwaveRingWidth": 0.04,
diff --git a/src/client/render/gl/shaders/name/name.frag.glsl b/src/client/render/gl/shaders/name/name.frag.glsl
index c1c14610b..1991dc064 100644
--- a/src/client/render/gl/shaders/name/name.frag.glsl
+++ b/src/client/render/gl/shaders/name/name.frag.glsl
@@ -10,6 +10,7 @@ uniform float uOutlineUsePlayerColor;
uniform float uFillUsePlayerColor;
uniform float uHoverGlowWidth; // px the white hover glow extends past the outline
uniform float uHoverGlowAlpha; // peak opacity of the hover glow
+uniform int uClassic; // 1 = Arial bitmap coverage atlas, 0 = MSDF
in vec2 vUV;
in vec4 vPlayerColor; // player territory color (rgb) + alpha
@@ -25,16 +26,24 @@ void main() {
// Degenerate fragment — skip
if (vPlayerColor.a <= 0.0) discard;
+ // Compute fill color: player color, or per-type grayscale shade
+ // (black for human, grayer for nation/bot).
+ vec3 fillColor = mix(vec3(vNameShade), vPlayerColor.rgb, uFillUsePlayerColor);
+
+ // Classic Arial bitmap: the atlas is a coverage mask; tint it with the fill
+ // color, no outline (matching the old DOM-rendered names).
+ if (uClassic == 1) {
+ float coverage = texture(uAtlas, vUV).a;
+ if (coverage <= 0.0) discard;
+ fragColor = vec4(fillColor, vPlayerColor.a * coverage);
+ return;
+ }
+
// Border darkens with night: t² stays dark longer, snaps toward the
// outline color late in the day cycle.
float t = 1.0 - uNightAmbient;
float borderT = t * t;
- // Compute fill color: player color, or per-type grayscale shade
- // (black for human, grayer for nation/bot). Applies in day and night.
- vec3 defaultFill = vec3(vNameShade);
- vec3 fillColor = mix(defaultFill, vPlayerColor.rgb, uFillUsePlayerColor);
-
vec3 msd = texture(uAtlas, vUV).rgb;
float sd = median(msd.r, msd.g, msd.b);