Problem: HUD theming stops at CSS variables, in-world HUD ignores the theme, in-world text is DOM, captions are a flag nothing renders, and settings expose six ids. Verified on main: packages/react/src/hudTheme.ts emits color/border/radius/font variables only; no border-image or FontFace in packages/react/src; packages/shell/src/world/WorldHud.tsx and floatTextStyle.ts use literal colors; SETTING_IDS in packages/core/src/settings/settingsModel.ts:83-90 has six entries.
Tasks, one PR each, in order. Every task except 4 needs before/after shots: use bun run shoot --fixture <name> for react blocks (register new fixtures in PREVIEW_FIXTURES) and bun run shoot <game> --mode play for the world HUD.
-
HUD skin. New packages/react/src/hudSkin.ts: HudSkin { frame?: { image: string; slice: [number, number, number, number]; scale?: number; fill?: boolean }; slot?: same shape; bar?: { fill: string; track: string; cap?: string }; font?: { family: string; src: string; weight?: number; pixel?: boolean }; cursor?: string }, hudSkinVars(skin) -> CSS variables (--jg-frame-image, --jg-frame-slice, ...), useHudFont(skin) that loads via new FontFace and returns loaded. Extend HudTheme with skin?: HudSkin and hudThemeVars to emit the skin variables; image-rendering: pixelated when font.pixel. Then make HudFrame, HudPanel, the bars in bars.tsx, InventoryGrid slots, action bars, Window, StartScreen, PauseMenu consume the variables (border-image on frames, background images on bars) with the current look as the fallback when no skin is set. Draw two test skins procedurally with canvas in a fixture (a painted cartoon frame and a thin dark overlay), register a HudSkinPreview fixture showing the inventory and vitals blocks in both. Tests on hudSkinVars. CHANGELOG Added.
-
World HUD reads the theme. In packages/shell/src/world/WorldHud.tsx, floatTextStyle.ts, pings, telegraphs: replace literal colors with HudTheme tokens read from the provider (add a useHudTheme hook in packages/react/src/hudTheme.ts if missing). Shots.
-
SDF world labels. Add troika-three-text to packages/shell (MIT, note in CREDITS.md). New packages/shell/src/render/WorldLabel.tsx with { text; anchor: [x,y,z] | entityId; worldSize; billboard?; occlude?; fadeRange?: [near, far]; color?; font? }; migrate nameplates and floating combat text to it behind nameplates.renderer?: "sdf" | "dom" (default sdf). Shots.
-
Captions. New packages/core/src/ui/captions.ts: createCaptionQueue() with push({ text; speaker?; startMs; endMs }), active(nowMs), snapshot/restore; packages/react/src/captions.tsx Captions block honoring AccessibilityState.captions and text scale; wire ctx.game.audio.play with a caption field on SoundDef to push automatically. Fixture and tests.
-
Settings surface. Add to SETTING_IDS: graphics.renderScale, graphics.frameCap, graphics.fullscreen, graphics.post.ao, graphics.post.bloom, graphics.post.dof, accessibility.reducedMotion, accessibility.highContrast, accessibility.textScale, accessibility.colorblind, language. Add accessibility and language categories to packages/shell/src/settings/settingsController.ts; apply render scale to the canvas DPR, frame cap in FrameDriver (skip sim when the frame arrives early, using ctx.sim accumulation), fullscreen via requestFullscreen on the shell root, post toggles through the existing PostProcessingConfig. SettingsScreen in packages/react renders the new categories. Shots of the screen. Closes #1688.
Rules: wait for bun run agent:bootstrap --check; branch claude/<slug> off origin/main; claim comment first; one task per PR with Refs #1688; bun run gen after export changes (JSDoc on every export; create* factories need snapshot/restore); run bun run check-hud-tokens; CHANGELOG bullet; check-types and test on core, react, shell as touched plus check-stateful-ratchet, check-doc-symbols, check-orphan-ratchet; merge origin/main before pushing; shots via bun run shoot --fixture and bun run shoot <game>; squash auto-merge; fix CI on the same branch.
Problem: HUD theming stops at CSS variables, in-world HUD ignores the theme, in-world text is DOM, captions are a flag nothing renders, and settings expose six ids. Verified on main:
packages/react/src/hudTheme.tsemits color/border/radius/font variables only; noborder-imageorFontFaceinpackages/react/src;packages/shell/src/world/WorldHud.tsxandfloatTextStyle.tsuse literal colors;SETTING_IDSinpackages/core/src/settings/settingsModel.ts:83-90has six entries.Tasks, one PR each, in order. Every task except 4 needs before/after shots: use
bun run shoot --fixture <name>for react blocks (register new fixtures inPREVIEW_FIXTURES) andbun run shoot <game> --mode playfor the world HUD.HUD skin. New
packages/react/src/hudSkin.ts:HudSkin { frame?: { image: string; slice: [number, number, number, number]; scale?: number; fill?: boolean }; slot?: same shape; bar?: { fill: string; track: string; cap?: string }; font?: { family: string; src: string; weight?: number; pixel?: boolean }; cursor?: string },hudSkinVars(skin) -> CSS variables(--jg-frame-image,--jg-frame-slice, ...),useHudFont(skin)that loads vianew FontFaceand returnsloaded. ExtendHudThemewithskin?: HudSkinandhudThemeVarsto emit the skin variables;image-rendering: pixelatedwhenfont.pixel. Then makeHudFrame,HudPanel, the bars inbars.tsx,InventoryGridslots, action bars,Window,StartScreen,PauseMenuconsume the variables (border-imageon frames, background images on bars) with the current look as the fallback when no skin is set. Draw two test skins procedurally with canvas in a fixture (a painted cartoon frame and a thin dark overlay), register aHudSkinPreviewfixture showing the inventory and vitals blocks in both. Tests onhudSkinVars. CHANGELOG Added.World HUD reads the theme. In
packages/shell/src/world/WorldHud.tsx,floatTextStyle.ts, pings, telegraphs: replace literal colors withHudThemetokens read from the provider (add auseHudThemehook inpackages/react/src/hudTheme.tsif missing). Shots.SDF world labels. Add
troika-three-texttopackages/shell(MIT, note inCREDITS.md). Newpackages/shell/src/render/WorldLabel.tsxwith{ text; anchor: [x,y,z] | entityId; worldSize; billboard?; occlude?; fadeRange?: [near, far]; color?; font? }; migrate nameplates and floating combat text to it behindnameplates.renderer?: "sdf" | "dom"(defaultsdf). Shots.Captions. New
packages/core/src/ui/captions.ts:createCaptionQueue()withpush({ text; speaker?; startMs; endMs }),active(nowMs), snapshot/restore;packages/react/src/captions.tsxCaptionsblock honoringAccessibilityState.captionsand text scale; wirectx.game.audio.playwith acaptionfield onSoundDefto push automatically. Fixture and tests.Settings surface. Add to
SETTING_IDS:graphics.renderScale,graphics.frameCap,graphics.fullscreen,graphics.post.ao,graphics.post.bloom,graphics.post.dof,accessibility.reducedMotion,accessibility.highContrast,accessibility.textScale,accessibility.colorblind,language. Addaccessibilityandlanguagecategories topackages/shell/src/settings/settingsController.ts; apply render scale to the canvas DPR, frame cap inFrameDriver(skip sim when the frame arrives early, usingctx.simaccumulation), fullscreen viarequestFullscreenon the shell root, post toggles through the existingPostProcessingConfig.SettingsScreeninpackages/reactrenders the new categories. Shots of the screen.Closes #1688.Rules: wait for
bun run agent:bootstrap --check; branchclaude/<slug>offorigin/main; claim comment first; one task per PR withRefs #1688;bun run genafter export changes (JSDoc on every export;create*factories need snapshot/restore); runbun run check-hud-tokens; CHANGELOG bullet;check-typesandteston core, react, shell as touched pluscheck-stateful-ratchet,check-doc-symbols,check-orphan-ratchet; mergeorigin/mainbefore pushing; shots viabun run shoot --fixtureandbun run shoot <game>; squash auto-merge; fix CI on the same branch.