Skip to content

Audit fixes + gameplay/UX rounds + World Richness + Material Identity passes - #8

Draft
LundstedtAdam wants to merge 79 commits into
masterfrom
storyline
Draft

Audit fixes + gameplay/UX rounds + World Richness + Material Identity passes#8
LundstedtAdam wants to merge 79 commits into
masterfrom
storyline

Conversation

@LundstedtAdam

@LundstedtAdam LundstedtAdam commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Implements the prioritized action plan from the full code audit, plus eight follow-up rounds.

Round 1 — audit fixes

Bug fixes

  • Cargo capacity lost on reload: setShipUpgrades now derives backpackCapacity from the cargo tier.
  • Resource duplication: upgradeShip and craft persist inventory/items immediately.
  • Ground drops lost on reload: backpack-overflow drops now persist per body.
  • Stuck input after Alt-Tab: keyboard/mouse state clears on window blur and pointer-lock loss.
  • Player embedded in terrain on long falls: vertical collision is swept in sub-voxel steps.

Performance

  • Ship telemetry moved out of per-frame zustand writes into a non-reactive shipTelemetry object.
  • Crosshair aim raycast capped to digging reach and the 3×3×3 chunk neighbourhood.
  • VoxelHUD compass and DiscoveryPanel sensor polls only setState when the visible reading changed.

UX & tests

  • First-time control-hints overlay per mode; renderer-init-failure overlay instead of a black screen.
  • vitest + jsdom test suite; findNearby* scanners collapsed into one generic helper; eslint underscore convention.

Round 2 — gameplay & UX improvements

  • Settings everywhere + FOV: gear button in every HUD (flight/landed/on-foot), plus a persisted FOV slider (60–100°).
  • Minecraft-style water: non-solid, swimmable, translucent double-sided surface mesh separate from opaque terrain, placeable-into, underwater tint overlay.

Round 3 — Settings panel: unreachable rows, dead taps, and settings that silently did nothing

  • No scroll container anywhere in the panel's chain — rows past the viewport were unreachable on small screens. Fixed by splitting into a fixed header + scrolling .settings-body.
  • A global touchend double-tap-zoom guard suppressed the native click on any second tap within 300ms anywhere on the page. Now scoped to taps that also land within ~40px of each other.
  • "Mouse/touch sensitivity" only affected the on-foot look, never the ship's mouse-drag orbit or virtual joystick — now scales both, relative to the default value.
  • "Invert pitch" was applied in every flight input path but not to the on-foot look.
  • The FOV setting was silently overridden every frame during descent/ascent and reset to a fixed 70 on the legacy pre-disembark surface view — all three now use the player's FOV as their base, preserving the existing cinematic curves.

Round 4 — hardcoded body names leaking through in English mode

Three HUD components rendered sceneMode.planet/sceneMode.target directly instead of translating it, so the raw internal canonical id (the Swedish name used throughout the data layer, e.g. "Jorden") was shown as-is regardless of the active language — SurfaceHUD.tsx, VoxelHUD.tsx, DescentOverlay.tsx. All three now go through useT()'s name() helper. Also filled a related gap: BODY_NAMES was missing Pluto/Charon, plus a completeness test that fails loudly if a future body is added without a matching translation entry.

Round 5 — scan range was gated by distance to a POI's anchor point, not its footprint

A modular POI (structures.ts) can span up to POI_MAX_HALF_EXTENT (32 voxels) from its cell-hash anchor, assembled from 4–6 jigsaw-connected 8×8 modules — but the scanner's proximity check (findNearbyPOI/findNearbyWarLorePOI) only ever measured distance to that anchor point, never the structure's actual generated extent. SCAN_RANGE is 14, so a player standing in a peripheral module of a large ruin (or anywhere the anchor itself happened to fall inside solid terrain) could be physically on/inside the structure and still never get close enough to the anchor to trigger a scan.

  • findNearbyPOI/findNearbyWarLorePOI now correct the raw anchor-distance into a distance-to-footprint (0 once the player is standing anywhere inside it), reusing the same memoized generatePOI() result the terrain stamper already built for that cell — a guaranteed cache hit by scan time, no measurable added cost.
  • findNearbyDeepSite (Layer 2 chambers) had the same class of bug at a smaller scale (chamber radii 8–18 can exceed SCAN_RANGE from the center point alone) — distance is now clamped to the actual carved cylinder shape instead.
  • Both corrections only ever reduce the reported distance versus the old formula, so nothing previously scannable becomes unscannable — this can only newly enable scans that previously failed near a structure's edges.
  • 5 new regression tests cover both corrections against the real generated footprint/chamber shape, not hardcoded magic numbers.

Round 6 — Minecraft-style grid inventory + passive extractor/condenser production with offline progression

Grid inventory: the backpack sheet now renders a Minecraft-style grid of item slots (ItemSlot.tsx) — a coloured icon swatch reusing the existing RESOURCE_COLOR/CRAFTED_COLOR tables, badged with the stack count — instead of a scrolling text list. Tapping a slot selects it and reveals a Drop action for that stack; the underlying Partial> data model is unchanged (it already was a stack map, just rendered as text before).

Passive production: two new placeable structures, extractor and condenser, accumulate a resource over time while powered:

  • An extractor is assigned the ore vein the player was aiming at when placed (falling back to the same direction-scan the orbital-scanner UI already uses to find the nearest vein); a condenser draws atmospheric volatiles and is gated to zero output on airless bodies via the existing hasAtmosphere check, so it never draws power there either.
  • planetPower allocation is now priority-ordered — refinery, then extractor, then condenser — each consuming from whatever generation is left after the previous kind, so refining (the later stage of the chain) isn't starved by earlier stages.
  • Production ticks once a second alongside the existing refineTick, depositing into the structure's own capacity-limited storage and spilling overflow to the ground exactly like mining overflow does. Extractors/condensers reuse the silo sheet UI (walk up, open, withdraw) rather than requiring the player to mine the block to collect their yield.
  • Extractors cost one Mining Drill (the previously-unused 11.2 crafted item), giving it a purpose in the crafting chain.

Offline progression: each body now persists a lastActive wall-clock timestamp (localforage, same pattern as existing per-body keys). When a body's structures are loaded, the elapsed real-world time since lastActive is applied as a single production catch-up (applyOfflineProduction), capped at 72h and clamped to zero if the elapsed time is negative (clock wound backward).

  • Known tradeoff: there is no trusted clock available client-side, so this cannot be made fully tamper-proof. The 72h cap bounds a single "jump" to a plausible away-from-the-game absence, but a determined player could still repeat the exploit by winding the device clock forward repeatedly and reloading. The residual risk is bounded (not eliminated) by each producer's own storage capacity acting as a ceiling, and by offline catch-up never spilling excess to the ground (unlike the online tick) — it's silently capped instead, so there's nothing to gain by maximizing overflow.
  • 8 new tests cover fractional-progress accumulation, power gating (including the atmosphere gate), ground-drop overflow on the online tick, and the offline-catch-up cap/clamp/negative-elapsed behavior.

Verified: npm run build (tsc strict + vite), npm run lint, npm test (52 tests) — all green.

Round 7 — World Richness: procedural environmental density across the voxel walking layer (8 phases)

A large content/systems pass increasing exploration density and per-planet identity on foot, built entirely by extending existing systems (no parallel architecture, no redesign of the chunk/mesh/streaming pipeline). Every phase reuses the codebase's own established primitives: cell-hash deterministic placement (findNearestCellSpec/craters/POIs), the VoxelScatter.tsx instancing pattern, and fbm2/valueNoise2/valueNoise3.

Phase 1 — per-planet terrain identity from already-authored data. BiomeProfile had detailFreq/detailAmp, microFreq/microAmp, and cellNoiseFreq/cellNoiseAmp fields authored per body but only ever consumed by the GPU orbital shader — voxel worldgen ignored them, so every body sharing an archetype had an identical height-field shape. Wired into columnHeight as extra octaves and into layerBlock's subsoil/rock threshold (a lateral perturbation so strata bands wave across cliffs instead of forming a flat plane).

Phase 2 — ground decoration density (VoxelScatter.tsx). Two new scatter kinds (blade, a hand-built crossed-quad billboard; branch, a pre-rotated cylinder). Every body now layers: fine abiotic grit (always), a universal "ore tell" fleck only where a vein actually surfaces (reuses oreAt directly, never lies about what's below), the existing per-archetype feature layer, and any authored accents — additive instead of the old either/or selection, so curated bodies (Mars, Europa, …) gain density rather than losing their archetype baseline. The 12 bodies with no authored props got one distinguishing accent each.

Phase 3 — POI structural variety + micro-discovery channel (structures.ts). Two new module kinds (bridge, an elevated gangway; collapsed, a room whose walls/roof have already failed) and a new outpost POI type. A new cache POI type (budget [1,1], 4-6 voxels) is the micro-discovery channel — a universal MICRO_DISCOVERY spec concatenated onto every body's POIs (same "universal + per-body" composition resourceProfiles.ts's FUNDAMENTALS already uses), giving frequent small cosmetic finds without any per-instance authoring.

Phase 4 — cave/terrain 3D richness. New fbm3 noise primitive. A worm-tunnel carve (two independent fbm3 fields intersected near their midpoint) is additive to the existing blobby-cavern test, giving winding tunnels alongside caverns. A rarer, cliff-gated overhang carve (4-sample slope probe) allows shallow undercuts on steep faces only. The rock-layering jitter from Phase 1 is the same mechanism used here.

Phase 5 — latitude-based biome transitions. A flat-world latitude proxy (latitudeOf, pure function of world-Z distance from the equator) drives two things: a shader-side blend toward each body's own (previously unused) colorPolar near the "poles" — a pure TSL addition to voxelMaterial.ts, never touching the per-block palette or the mesher/worker transfer contract — and a density falloff on Earth's grass/flora scatter layers.

Phase 6 — hydrology: lakes, rivers, wetlands, waterfalls. Procedural LakeSpec (cell-hash basins, gated by archetype — ordinary lakes on Earth, sparser methane lakes tinted differently on Titan/dune) and a new river LandmarkSpec kind (a short polyline, canyon-style trench per segment). A new localWaterCeilingAt computes the higher of the global sea and any active lake/river's own natural rim height per column, used for the fill decision, for surfaceHeightAt's spawn-safety check, and for VoxelScatter's underwater/shoreline checks. Shoreline dressing reuses the waterAdjacent filter (abiotic everywhere water exists; Earth-only reeds). Waterfalls are never cell-hash scattered like other emitters — RiverWaterfalls.tsx derives them automatically from a river's own polyline wherever consecutive points' natural height drops enough, so they can't drift out of sync with the river geometry.

Phase 7 — procedural trees (cosmetic, superseded on Earth by Round 8). A parameterized (not full L-system) prefab — tapered trunk + optional canopy sphere, both InstancedMesh — placed with the same cell-hash/ring-shell/LOD pattern as VoxelScatter, in a new sibling VoxelTrees.tsx. A low-frequency forest-coverage mask clusters trees into groves instead of a uniform carpet. Dead trees skip the canopy; fallen trees reuse the trunk geometry reoriented.

Phase 8 — ambient wildlife. Small instanced creatures (birds, ground critters, ambiguous "drifters") with simple procedural circling/bobbing motion, modeled on Emitters.tsx's existing column-emitter pattern. A new AudioManager.playWildlifeCall synthesizes two calls (no sample assets — same oscillator/noise-buffer technique as the existing playSurfaceTexture), scheduled by a parallel driver in VoxelAudio.tsx alongside (never replacing) the existing texture scheduler.

Narrative-integrity decision (confirmed with the user before implementation): the game already has a rule that no body has "confirmed" life (LifeStatus has no such value; a validator enforces this over all lore text). Phases 2/7/8's flora/fauna-like content is gated by a single shared lifeAmbiguous() (moved into contentProfiles.ts, the canonical narrative-data module, so all three phases read the same source of truth): real biology only on Earth; sparse, deliberately ambiguous life-like content only on the handful of bodies already flagged theoretical/inconclusive in their science notes (Mars, Titan, Venus, Europa, Ganymede); every other body stays abiotic-only.

Explicitly out of scope, with rationale: the orbital/pre-landing GPU terrain shader (different rendering path, not the "voxel world" the objective named); full connected river-network hydraulic simulation (bounded 2-4 point polylines only); visible tree branch geometry/exposed roots/hollow trunks (a canopy sphere already reads as "tree" at voxel-game distances; a third instanced part wasn't judged worth the complexity this round).

64 new tests across 6 new/extended test files cover the deterministic logic in every phase (noise primitives, height-field perturbation, cave carving, lake/river carving and water fill, POI generation, narrative gating for scatter/trees/wildlife).

Manual in-game playtesting has not been done for this round — this environment cannot run the WebGPU renderer interactively, so no phase's visual result, frame time, or exact placement (the one demonstration river added to Earth in particular) has been inspected live. Recommended before considering Round 7 fully closed.

Round 8 — Material Identity: procedural texture atlas + choppable voxel trees

Every voxel previously carried a single flat vertex color as its only material signal. This round gives every block a real, texture-based material identity — extending the existing chunk/mesh/worldgen pipeline, not replacing it — and turns Earth's forest from cosmetic scenery into a real, choppable part of the terrain.

Texture atlas (textureAtlas.ts, new). A small (128×128) atlas is generated once as a deterministic raw pixel buffer — seeded procedural math (reusing the existing valueNoise2 primitive), not Canvas2D/OffscreenCanvas, so it builds correctly under the vitest/jsdom test environment and stays importable inside the mesher worker without pulling three's DataTexture code into that bundle (confirmed via the worker chunk's build output size). 17 material tiles (grass top/side, dirt, stone, sand, ice, ice-glow, lava, sulphur, bark top/side, leaves, metal, panel, glass, ore-fleck, build) each get their own procedural pattern — fleck noise, high-frequency crack lines for stone, vertical streaks for bark, clustered noise for leaves. getBlockFaceTiles(id) maps every block id to a {top, side, bottom} tile triple — grass gets a distinct green top / dirt-blended side / dirt bottom, wood gets bark sides vs. end-grain caps, everything else is uniform.

Pipeline threading. greedyMesh.ts's emitQuad — which already computes face direction right where it reads the palette — now also emits a materialUV vertex attribute (raw, unwrapped per-quad-corner coordinates, not normalized 0-1), so a greedy-merged multi-voxel quad repeats the texture once per voxel instead of stretching one tile across it. The attribute threads through the existing MeshResult/worker transfer contract (voxelTypes.ts, mesher.worker.ts, mesher.ts) exactly like positions/normals/colors already do. voxelMaterial.ts samples the atlas via a new TSL texture() node and multiplies it against the existing per-vertex biome-tint/AO color — texture is now the primary material signal, biome color is a diluted (TINT_STRENGTH = 0.4) overlay on natural terrain/tree blocks only; ore veins and player-built structures keep their existing full-strength tint unchanged (that saturation was already deliberate, so built things read as artificial).

Choppable voxel trees. Two new block ids, WOOD_LOG and LEAVES. A new trees.ts generates a deterministic trunk-plus-canopy voxel assembly (reusing structures.ts's SVoxel/BuiltStructure contract and seeded PRNG — not routed through the POI room-jigsaw machinery, which doesn't fit a tree), and a new stampTrees() in worldGen.ts places them on Earth using the same cell-hash/forest-coverage-mask tuning the old cosmetic forest used. Mining a trunk voxel (ChunkManager.tsx's mineTick) now cascades upward through the whole log column, yielding one 'wood' resource per log via the existing mining pipeline; afterward a new treeDecay.ts scans the felled column's own bounding box for any LEAVES voxel left with no WOOD_LOG within a small radius and silently removes it — bounded to that tree's own footprint, so no general voxel-grid flood-fill was needed. Earth's old purely-cosmetic, non-interactic InstancedMesh forest (treeProfiles.ts/VoxelTrees.tsx) is retired; the separate "ambiguous alien growth" decorative variant on theoretical/inconclusive bodies is untouched, since those are deliberately non-interactive by narrative design.

Explicitly out of scope, with rationale: per-archetype texture variants (a "dirt" tile looks the same on every body today; only the biome tint overlay varies) — not requested and would meaningfully grow the atlas/authoring surface for a cosmetic-only gain; a general voxel-grid BFS for leaf decay — the bounded per-tree scan is provably sufficient since a chop can only ever disconnect leaves belonging to that same tree.

26 new tests across 3 new test files (textureAtlas.test.ts, trees.test.ts, treeDecay.test.ts) plus extensions to greedyMesh.test.ts, worldGen.test.ts, and treeProfiles.test.ts cover tile-mapping correctness, per-voxel-unit UV tiling under greedy merging, tree-generation determinism and bounds, and leaf-decay adjacency logic.

Manual in-game/visual playtesting has not been done for this round — this environment cannot render WebGPU live, so the atlas's on-screen appearance, whether materials actually read as distinct at a glance, chop/felling game feel, and any frame-rate impact from the added texture sample have not been inspected on a real device. Recommended before considering Round 8 fully closed.

Round 8 fix 1 — grass/weed scatter renders as solid quads, not cut-out foliage

Reported after Round 8: VoxelScatter.tsx's crossedQuadGeometry (the blade prop kind — grass tufts/weeds/reeds, layered onto most bodies' ground clutter) had no UV attribute and its material had no alpha texture, so the billboard rendered as a flat solid colored rectangle instead of a foliage silhouette. Three other rendering issues were reported alongside this one (UV stretching on terrain, missing ambient occlusion, floating scatter/wildlife) — each was checked directly against the code before touching anything, and none were actually present: the per-voxel-unit UV tiling and AO baking added earlier in Round 8 are intact and already covered by passing regression tests, and the height-sampling functions (landHeightAt/columnHeight) are pure functions of world position with zero dependency on chunk-mesh generation state, so the hypothesized race condition can't exist in this codebase. Per the user's decision, only the confirmed scatter issue was fixed, to avoid risking regressions in already-correct, already-tested code.

Fix: a new deterministic alpha-cutout grass-blade texture (textureAtlas.ts's getBladeAlphaTexture — three tapered strands narrowing root-to-tip, same raw-pixel-buffer generation pattern as the material atlas, no Canvas/DOM), a standard UV attribute on the crossed-quad geometry (extracted to a new scatterGeometry.ts for unit-testability, mirroring the existing Emitters.tsx/emitterPoints.ts split), and alphaTest-based cutout wired onto the blade material only — every other scatter kind (crystal/spire/fungus/slab/branch) is intentionally solid 3D geometry, not a billboard, and is untouched.

6 new tests (textureAtlas.test.ts, scatterGeometry.test.ts) cover the texture's determinism, its cutout (non-uniform alpha) and root-to-tip taper, and the geometry's UV mapping. Not visually verified on a live WebGPU device.

Round 8 fix 2 — sun light silently dropped by the WebGPU node renderer (the real cause of a flat, dark, textureless surface)

A live screenshot from an actual device (iOS Safari) showed the on-foot Earth surface rendering as one flat, dark, textureless wash — no visible grass/dirt/stone material variation anywhere, even in open areas. Investigated two hypotheses purely by reading code:

  • Silent WebGPU→WebGL2 fallback on that device — ruled out. Three.js's hybrid renderer runs the identical shared node-graph machinery (custom attributes, DataTexture, texture() sampling) under both backends; this wouldn't by itself produce a flat/dark result, and this codebase's only renderer-failure path is a full-screen takeover, clearly not engaged since the game is fully playable.
  • A real texture-pipeline bug — ruled out on inspection: DataTexture colorSpace/mipmap defaults and the materialUV/atlas-tile UV math all check out correct.

Actual root cause: SolarSystem.tsx registers each classic-three light class it uses with the WebGPU renderer's node library (library.addLight(NodeClass, LightClass) — the node system looks up a light's shader-node handler by light.constructor, and an unregistered class is dropped with a console warning, every frame). AmbientLight and PointLight were registered; DirectionalLightVoxelScene.tsx's SunLight, the voxel surface's only directional key light, and the sole source of directional shading, the normal-jitter effect, and shadows — never was. This is a pre-existing gap dating to the original Phase 9.2 voxel pass, not something this session's texture-atlas work introduced; nothing caught it before because no prior round's commit was ever checked against a live device.

With only flat ambient light reaching the terrain, the newly-added texture atlas and baked AO are both still present in the mesh data, but with zero directional falloff or specular response to give them contrast, everything reads as one flat, dim wash — exactly matching the screenshot.

Fix: one-line addition to the existing registration block in SolarSystem.tsx

library.addLight(DirectionalLightNode, DirectionalLight);

DirectionalLightNode is a named export from three/webgpu, same import path as the two already-registered light-node classes. DirectionalLight is used nowhere else in the codebase, so this has no other call sites to affect.

Round 8 fix 3 — settings highlight, sun/bloom green glow + crash on quality change, grass texture orientation

Reported next, together with a Minecraft screenshot as a visual-clarity reference:

  • Settings panel never highlights the active option. src/ui/SettingsPanel.tsx's quality/language/game-mode pickers correctly apply an active class, and styles.css defines a distinct look for it — but a later, equal-specificity rule (.settings-panel .setting-row button) always won the cascade tie and painted every pill button identically. Fixed by scoping the active rule under .settings-panel to raise its specificity so it always wins regardless of declaration order.
  • Changing quality settings washed the screen green, then crashed. Two independent bugs: (1) the sun's deliberately-HDR core material used an extreme, unevenly-scaled multiplier (3.5/2.8/2.0, "so the bloom pass makes it glow") — bloom is off on Low (invisible) and turns on at Medium+, at which point ACES filmic tonemapping's well-known hue-shift-toward-green artifact on very bright, unevenly-scaled highlights kicks in (this exact bug class was already hit and fixed for the ship model's emissive glow elsewhere in this repo); retuned to a lower-magnitude warm-white value matching this codebase's own established sunlight color. (2) Effects.tsx rebuilt its entire postprocessing GPU pipeline (PostProcessing + PassNode +, when bloom is on, BloomNode's ~11 render targets) from scratch on every quality change without ever disposing the previous instance — a leak that eventually hits a WebGPU resource-exhaustion crash. Now the pipeline only rebuilds (with proper disposal) when bloom/chromatic-aberration structurally turn on or off; ordinary quality-tier tuning updates the existing reactive GPU uniforms in place instead (bloom's strength/radius/threshold are already uniform()-wrapped internally; added an equivalent uniform for chromatic aberration, mirroring this file's existing heatUniform pattern).
  • Grass block side faces were misoriented — found while checking the texture atlas against the Minecraft reference. Two real, provable-by-tracing-the-code bugs, not subjective tuning: textureAtlas.ts's grass/dirt row mapping was backwards relative to how DataTexture's flipY=false and the UV emission actually place texture-space v=0/v=1 on a rendered voxel face, so grass showed at the bottom and dirt at the top; and east/west faces (d===0 in greedyMesh.ts) use the opposite sweep-axis pairing from north/south faces (d===2), so the gradient ran horizontally instead of vertically on two of every grass block's four side faces. Fixed both, and narrowed the transition band for a crisper grass/dirt edge closer to the reference image.

1 new regression test (greedyMesh.test.ts) proves the east/west UV-axis fix by isolating vertices with a nonzero X normal and asserting the vertical texture axis — not the horizontal one — scales with a vertical voxel merge.

Round 8 fix 4 — green glow persisted on High/Ultra (two more HDR sources)

Fix 3's sun-core retune was real but incomplete — the user confirmed the glow was still present specifically on High/Ultra. Two more uncapped/extreme HDR "sun glow" sources were found, both untouched by fix 3:

  • VoxelSky.tsx/Sky.tsx's sky-dome sun disc (identical code in both files, deliberately kept separate per this project's voxel/orbital rendering split): peaked at 8.6× magnitude — over 4× higher than the already-fixed sun mesh, and covering a full-screen sky dome rather than a small sphere. Reduced to 2.6× peak in both.
  • Sun.tsx's corona inner ring: an even more extreme, unevenly-scaled ratio (1:0.55:0.2) than the original problematic sun-core value, at 2.4× magnitude — left untouched in fix 3 as "not the primary source," which undersold its contribution. Reduced to 1.6× at a much less extreme 1:0.8:0.6 ratio.

This explains the High/Ultra-specific symptom: quality.ts's bloomThreshold drops (0.85 → 0.75 → 0.65) and bloomStrength rises (0.25 → 0.3 → 0.35) from medium to ultra, so higher tiers admit more of this previously-unfixed HDR into bloom and amplify it harder. No other extreme/uncapped HDR source was found elsewhere in the scene (terrainMaterial.ts, Clouds.tsx, AsteroidBelt.tsx, and the planet materials in materials.ts are all non-HDR or already texture-bounded to ≤1).

Pure numeric constant changes, no logic/test impact.


Attempted a live visual check via Playwright + the pre-installed Chromium against a local preview build for fixes 2-4 — real WebGPU is active in this sandbox (three.js's own "WebGPU is experimental on this platform" log confirms it engaged, not a WebGL fallback), but its software GPU implementation fails outright on this app's buffer sizes (createBuffer failed... too large for the implementation), so the canvas never renders anything here — an environment limitation, not a code issue, unrelated to any of these fixes. All four fixes still need confirmation on real hardware. If the glow persists after fix 4, the most useful next report is exactly which object still shows it (sky vs. sun disc vs. corona) rather than another blind sweep.

Verified (all fixes): npx tsc --noEmit, npm run lint, npx vite build, npm test (119 tests) — all green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGDzMFuEEA5rqcHcgTHXid

claude added 30 commits June 24, 2026 23:09
…ndable bodies

Phase 9 baseline for surface quality on all landable bodies. Three fixes:

1. Flat shading -> analytic normals. New scene/terrain.ts builds a procedural
   height field per body and recomputes the surface normal analytically from
   the field's tangential gradient (not screen-space derivatives), so slopes,
   valleys and peaks light correctly. Geometry is displaced from the macro
   field; micro detail rides on top as a normal/colour bump.

2. Low geometry -> quality-scaled terrainSegments (96/160/224/320) for landable
   bodies, replacing the coarse planet/moon segment counts up close.

3. Flat colour -> per-body micro-detail keyed to physical character: rocky
   (grain/cracks/boulders), icy (crystalline + subsurface hint), volcanic
   (lava + ash + hot-spot emissive), sandy (dune ripples), and subtle,
   texture-led terrain for Earth (land relief, mountain grain, animated water).

Adds terrain profiles to all 14 existing landable bodies and introduces Pluto
+ Charon (real J2000 elements), rendered fully procedurally with no base map.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWCU8bNWRMR7nZswEn6zVa
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWCU8bNWRMR7nZswEn6zVa
useTexture's onLoad callback receives no texture when a body has no maps
(procedural Pluto passes an empty url record), so the unguarded
tex.anisotropy assignment threw 'Cannot set properties of undefined',
which unmounted the whole R3F tree -> flash-then-black on every device.
Guard the assignment, matching Moon.tsx. Verified in a headless WebGL2
(no-WebGPU) browser run: scene renders, fallback activates silently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWCU8bNWRMR7nZswEn6zVa
The touch-device hamburger/drawer (top bar with hamburger toggle, slide-in
drawer holding the time-scale slider and Pause/Tour/Orbits/Reset/Settings)
was absent from this branch's HUD; on coarse-pointer devices the desktop
control bar was shown instead. Restore the hamburger HUD and its CSS,
gated on (hover:none) and (pointer:coarse) so desktop is unchanged.
Verified in an emulated touch browser: bar visible, drawer opens/closes,
all five actions present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWCU8bNWRMR7nZswEn6zVa
…in branch

Integrates the Phase-8 ship/descent/surface-exploration feature (ship
piloting, atmospheric descent, walkable surface scene, touch controls,
ship/surface/descent HUDs, surface audio, spaceship model) and the
spread-out orbital scale onto this branch, on top of the terrain-shading
overhaul. Brought in via squash so history stays linear.

Scale: adopt the spread distances (Mercury 200 .. Neptune 2300, Pluto
2800); verified every moon system clears neighbouring planet orbits
(tightest gap Jupiter->Saturn ~38u), so moons no longer cross planets.

Conflicts resolved:
- bodies.ts: kept both the new scale and per-body terrain profiles;
  de-duplicated Pluto/Charon (the auto-merge left two) keeping the
  procedural terrain-profiled version, repositioned to distance 2800.
- quality.ts: kept both terrainSegments (orbital shading) and
  terrainResolution/terrainNoiseOctaves (surface meshes).
- HUD.tsx/styles.css: took the determined-ride versions (full ship HUD +
  hamburger drawer, superset of the hamburger I had restored).
- netlify.toml: kept this branch's (Node 22 + SPA redirect).

Verified: tsc clean, vite build clean, and a headless WebGL2 (no-WebGPU)
run renders the solar scene, shows the hamburger on touch, and enters
ship mode (telemetry HUD) with no fatal errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWCU8bNWRMR7nZswEn6zVa
The belt constants were still on the pre-port scale (184-222), which after
the scale change sat at Mercury's orbit. Reposition to 645-755 (centered
~700), between Mars (~620) and Jupiter (~950) and inside Jupiter's inner
moon shell (~762) so nothing crosses orbits.

Rework for a believable belt within the quality presets:
- Three size/detail tiers (LOD): abundant tiny low-poly dust, fewer
  medium rocks, rare large moon-sized high-poly bodies. Poly count scales
  with size so big rocks read up close while the many small ones stay cheap.
- Wide, random, non-uniform size distribution (dust to ~5u moonlets);
  irregular lumpy shapes via direction-based vertex displacement (crack-free),
  with multiple shape variants per tier.
- Individual slow tumbling on the visible (medium/large) tiers only, so the
  belt feels alive without per-frame cost on the thousands of dust rocks.
- Whole-belt orbital drift on the sim clock; density scales with preset.

A few instanced draw calls; at medium (~2000) only ~440 rocks update per
frame (~86k tris), comfortably within the 60fps mobile budget. Verified:
tsc + build clean, headless WebGL2 run renders with no fatal errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWCU8bNWRMR7nZswEn6zVa
1. Camera distance: the chase cam used a slow lerp that fell behind at high
   thrust, so the ship receded ('zoom-out'). Velocity-compensate the follow
   (rigidly track the ship's motion, then ease the residual) so distance stays
   constant and the ship is always clearly framed; add only a subtle (12%)
   throttle-scaled pull-back and a gentler speed FOV (75->68).

2. Throttle curve: replace the pow(1.3) shaping with a four-zone piecewise map
   - slider 0-25/25-50/50-75% -> thrust 0-10/10-20/20-30% (fine low-end),
   75-100% -> 30-100% (power band). Linear within zones, continuous at every
   boundary (no stepping). Verified numerically.

3. Throttle indicator: new upper-center HUD readout showing throttle % with the
   four response zones marked and the last quarter tinted as the power band;
   fill tracks the lever smoothly and turns warm in that zone.

4. Power-band effects (75-100%): smooth (smoothstep) screen shake in ShipCamera
   and a pulsing warm orange-red edge vignette, both scaling 0 at 75% -> full at
   100%. Shake respects prefers-reduced-motion; the glow keeps the center clear
   so it reads as exciting, not disorienting.

Store now holds the raw lever position so zones/indicator/effects key off the
slider quarters. Verified: tsc + build clean; headless run enters ship mode,
renders the indicator (4 zones), no heat glow at idle, no fatal errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWCU8bNWRMR7nZswEn6zVa
In the chromatic-aberration path the composed output took red and blue from
the offset samples but green from the heat-tinted color, so the warm
reentryTint (1.0,0.6,0.3) only affected the green channel — the descent heat
glow rendered green. Build the aberrated image first, then apply the heat
tint across all channels (as the non-aberration path already did), so re-entry
now glows warm orange. Verified: tsc + build clean, render pipeline OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWCU8bNWRMR7nZswEn6zVa
…orld

First sub-phase of Phase 9 (Voxel Surface Exploration). Adds the on-foot mode
skeleton so the player can step off the landed ship into a voxel version of the
same body and back, ahead of the real chunked engine landing in 9.1.

- store: new SceneMode `voxel` plus `disembark()` (surface->voxel) and
  `boardShip()` (voxel->surface) actions. Launch/ascent stays reachable from
  the surface view, so the loop is land -> disembark -> explore -> board ->
  Launch -> ascend.
- VoxelScene: placeholder instanced-cube ground lit from the shared biome
  profile (colour/ambient/sun/sky horizon) so the voxel world reads as the same
  body. Owns its own rendering and scene backdrop; no terrain/ imports, per the
  separate-systems constraint. First-person placeholder camera.
- scene routing: both on-foot modes hide the space scene (planets, stars, sun,
  belts, labels); render VoxelScene when on foot.
- UI: Disembark button on the surface HUD; new VoxelHUD with a Board ship
  action. Surface HUD bottom row generalised to an actions group.

Verified: tsc + vite build clean. New/edited files are lint-clean (4 remaining
eslint errors are pre-existing in untouched descent/ship files).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
Replaces the 9.0 placeholder ground with a real chunked voxel engine, dynamic
from the first chunk (live edits + re-mesh), on the high-performance pipeline.

- voxelTypes: 32³ chunk constants, bit-packed Uint32Array voxel layout, and a
  strict discriminated-union worker contract (MeshRequest/MeshResult) with
  concrete TypedArray payloads + transfer-list helpers (zero-copy hand-off).
- chunk: authoritative main-thread voxel storage with rev/dirty tracking and a
  sparse session edit overlay (reapplied after regen — basis for persistence).
- noise + worldGen: dependency-free CPU value-noise FBM; per-body heightfield +
  caves into the packed array, derived from the shared BiomeProfile so every
  body generates a distinct world from the same algorithm. Deterministic seed.
- greedyMesh (pure) + mesher.worker: greedy face merging with baked per-corner
  ambient occlusion and vertex pooling (no per-quad GC); worker returns geometry
  via Transferable Objects. Pure mesher is unit-tested off the worker.
- mesher (main): typed worker pool + BufferGeometry assembly.
- ChunkManager: quality-scaled streaming around the camera (ring-nearest, budget-
  throttled worldgen + meshing), far-chunk unload keeping edited data, and a
  live right-click dig that re-meshes only the affected chunk + neighbours.
- VoxelScene: streams chunks at the world origin with biome lighting/backdrop;
  temporary left-drag + WASD debug fly camera until 9.3 first-person controls.
- quality: per-preset voxel view radius, worker count, and mesh budget.

Verified: tsc + vite build clean (worker emits its own chunk); new files lint-
clean. 12/12 runtime logic tests pass (single-voxel 6 faces, 2x2x2 greedy merge
to 6 quads, enclosed=0 faces, worldgen solidity/empty-sky, real chunk meshes to
~2.2k tris, determinism). 60fps streaming is the manual browser step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
Makes the voxel world read as the same body as the Phase 8 surface (colour,
sky, fog, light) while clearly not looking like vanilla Minecraft.

- voxelMaterial (TSL/WebGPU MeshStandardNodeMaterial): albedo carries the
  mesher's baked ambient occlusion; low-amplitude world-space normal jitter
  breaks the flat voxel faces so they catch light unevenly; biome fog using the
  same fogColor/density and 0.005 falloff as terrainMaterial.ts, keyed off
  camera (view-space) distance since chunks aren't centred.
- VoxelSky: voxel-owned sky dome mirroring the Phase 8 biome gradient + sun
  disc (zenith/horizon/sun from BiomeProfile), follows the camera so the
  streamed world never reaches the dome edge. Independent code per the
  separate-rendering-systems constraint.
- SunLight: directional key light matching the biome sun, following the camera
  so its shadow frustum tracks the player; casts voxel shadows gated by the
  quality preset (mapSize/enabled). Replaces the static placeholder light.
- ChunkManager now uses createVoxelMaterial(getBiome(planet)); meshes already
  cast/receive shadows.

Verified: tsc + vite build clean (all TSL nodes — attribute, positionWorld,
positionView, mx_fractal_noise_vec3 — resolve); new files lint-clean. Visual
parity vs Phase 8 Mars and shadow quality are the manual browser step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
…ntrols

Replaces the debug fly camera with a real on-foot controller.

- voxelPhysics: per-body surface gravity (real values, radius-proxy fallback)
  plus grip/speed by terrain archetype — icy Europa slippery, Martian dust
  slows, rock firm (variation layer 5).
- player: AABB swept against the voxel grid, per-axis snap-to-contact collision
  with auto step-up over 1-voxel lips; ground friction; fixed-speed jump so
  lower gravity jumps higher and hangs longer (Moon floaty, Earth normal).
  Exposes VoxelApi (isSolid + edit) the ChunkManager fulfils.
- voxelControls: shared input singleton; desktop pointer-lock mouse look +
  WASD/Space/Shift.
- VoxelTouchControls (mobile-first): left joystick to move, right-side drag to
  look, jump button — writes the same input the controller reads.
- PlayerController: steps the player, drives camera + a visible first-person
  hand/tool, spawns on the surface; ChunkManager now serves collision queries
  (generate-on-demand, solid below floor) and aims digs from screen centre under
  pointer lock.

Verified: tsc + vite build clean; new files lint-clean. 12/12 player-physics
runtime tests pass — rests on floor (no tunnelling), stops at walls, steps up a
1-voxel lip, jumps and lands, and low-gravity apex ≈ 6x Earth's. Touch/desktop
feel is the manual browser step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
Every landable body is now explorable with a matching voxel biome, built from
6 archetypes differentiated by per-body colour/atmosphere/physics (no unique
system per body).

- voxelTypes: expanded block set (grass, sand, water, ice, glowing ice, lava,
  sulphur); palette stride is now RGBA (rgb + emissive).
- mesh pipeline: greedyMesh/mesher carry a per-vertex emissive channel; the
  voxel material self-illuminates lava and glowing ice (reads in shadow/caves),
  so Io and Europa glow without washing out lit surfaces.
- voxelBiomes: 6 archetypes (rock, regolith, earth, ice, lava, dune) with a
  16-body mapping; per-body palette pulls colours from the shared BiomeProfile;
  per-archetype terrain params (seas, dunes, craters, glow depth, lava level,
  cave size).
- worldGen: archetype-aware block layering plus natural features — Earth seas
  fill the lowlands, Titan dunes ripple, the Moon/Mercury get impact craters
  (bowl + rim), Europa grows luminous ice veins and larger caverns, Io floods
  deep cavern lava lakes. Spawn stays above any sea/lava surface.

Verified: tsc + vite build clean; voxel files lint-clean. 18/18 voxel logic
tests pass (mesh integrity, greedy merge, worldgen, determinism, and each
archetype's signature blocks: Earth water, Io lava, Europa ice+glow, Titan
sand, mapped Ganymede terrain). Player-physics tests still green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
Gives each body a signature ambient particle + sound layer (variation layer 4).

- voxelWeather + VoxelWeather: per-archetype weather particles in a camera-
  following box that fall/drift and wrap — Mars red dust storm, Titan methane
  drizzle, Io ash embers (additive), icy-body snow/crystals, faint Earth motes;
  airless regolith bodies get none. Count scales with a new quality budget.
- AudioManager: per-material footsteps (dust/sand/rock/ice/grass/water) and a
  resonant cave drone with setCaveAmount.
- VoxelAudio: reuses the per-body surface ambience (wind/rumble + scheduled
  ice/lava/geyser textures) for the voxel world; resets the cave drone on exit.
- PlayerController drives footsteps (per stride, material underfoot via the new
  VoxelApi.blockAt) and the underground swell (how far the eye sits below the
  surface column).
- worldGen: ice tops are ICE (so footsteps read crisp); ice palette brightened.

Verified: tsc + vite build clean; new files lint-clean. 18/18 voxel logic tests
and player-physics tests still pass after the worldgen/palette tweaks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
Closes the on-foot loop (land -> disembark -> explore -> return -> ascend).

- VoxelHUD: a compass showing heading plus a marker that points back to the
  ship (the disembark point at the world origin) with live distance, fed by a
  non-reactive player-telemetry singleton polled via rAF (no 60fps React
  re-render of the scene). Board ship returns to the Phase 8 surface.
- VoxelTransition: a brief full-screen wash in the body's sky colour when
  crossing between the surface and the voxel world, so the swap reads as an
  atmospheric continuation while the first chunks stream in — skipped entirely
  under prefers-reduced-motion.
- PlayerController publishes position/yaw telemetry each frame.

Perf pass: per-preset budgets confirmed (view radius 2–5, 1–4 workers, mesh
budget 2–6, particles 180–1500, shadows off on low); weather is camera-local
and frustum-exempt, far chunks unload, and the HUD/compass avoid scene
re-renders.

Verified: tsc + vite build clean; new files lint-clean. Voxel (18) and player
(12) logic tests still pass. End-to-end feel is the manual browser step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
Adds variation layer 3 — one InstancedMesh of props per body, placed
deterministically on the terrain so bodies sharing an archetype feel distinct:
dead boulders on the Moon/Mercury, sulphur crystals on Io, glowing ice spires
on Europa, fungal blooms on Titan, scattered rocks on Mars/Earth. Airless still
gets rocks (Moon's signature). Colours come from the biome; ice spires and
sulphur/fungus carry a faint emissive.

- scatterProfiles: per-archetype prop kind, colour/emissive, density, scale,
  cell size.
- VoxelScatter: a single InstancedMesh (one draw call, frustum-exempt, casts/
  receives shadows) filled from hashed scatter cells around the player on the
  real land surface (skips sea/lava cells); rebuilt only when the player crosses
  a cell, nearest-first up to the quality cap. New voxelScatter quality budget
  (120–1100). Exposes landHeightAt for placement.

Portability fix: renamed voxelWeather.ts -> weatherProfiles.ts and the new
scatter profiles -> scatterProfiles.ts so they no longer differ from their
VoxelWeather.tsx / VoxelScatter.tsx components by case alone (broke on
case-insensitive filesystems; only built here because Linux is case-sensitive).

Verified: tsc + vite build clean (no case-path warning); voxel files lint-clean.
22/22 voxel logic tests pass (incl. per-archetype scatter kinds + deterministic
landHeightAt); player-physics tests still green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
…itivity

Real NASA surface gravity:
- voxelPhysics now stores real m/s² per body (Mercury 3.7 … Earth 9.81 … Phobos
  0.006) and converts to an Earth-relative multiplier (g/9.81), so relative feel
  matches reality — Moon floaty, Jupiter moons light, Titan light. Tiny moons
  (Phobos/Deimos/Miranda/Charon) are floored at 0.04 so they feel nearly
  weightless without launching the player out of the streamed area each jump.

Fix 1 — landing on moons:
- findNearestLandable ranked ALL bodies including gas giants, so near Jupiter/
  Saturn/etc. the giant always shadowed its (landable) moons and the Land button
  targeted the unlandable giant. Now gas-giant planets are excluded as
  candidates while their moons still rank, so every landable moon is reachable.
  Also fixed a latent moon-distance bug (used ship Y instead of Y relative to
  the moon).

Fix 2 — digging + block highlight:
- Unified tap/click dig at the crosshair for both touch (tap on the look layer)
  and desktop (left-click while pointer-locked, or right-click); first desktop
  click captures the pointer. ChunkManager raycasts from screen centre each
  frame to drive a subtle wireframe highlight on the aimed voxel (within reach)
  and api.dig() breaks it. Added a centre crosshair.

Fix 3 — look sensitivity:
- Raised the base look speed and added a lookSensitivity multiplier (store
  ControlConfig, default 1.5) applied to both mouse and touch look, with a
  "Look sensitivity (on foot)" slider in the settings panel.

Verified: tsc + vite build clean; lint clean. 30 voxel logic tests pass
(incl. gravity multipliers: Earth 1.0, Moon 0.165, Mars 0.378, Io 0.183, Titan
0.138, Phobos floored) + 12 player tests. The descent/landing path can't be
bundled headlessly (ephemeris imports three/webgpu), so moon landing is the
manual browser step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
Per request, digging is now an explicit action rather than tap/left-click:
- Touch: a new DIG button (next to JUMP) breaks the aimed voxel; the look layer
  is back to look-only (no accidental digs while turning).
- Desktop: left-click only captures the pointer for mouse look; right-click
  digs the voxel under the crosshair.

The crosshair + wireframe highlight on the targeted voxel are unchanged.

Verified: tsc + vite build clean; lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
Joystick, JUMP and DIG sat too close to the edges. Pulled them in (scoped to
the voxel controls, so the ship HUD layout is unchanged) for a more natural
thumb position.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
Touch-dragging the controls was triggering the native text-selection highlight
and the iOS/Android copy/paste callout + magnifier. Disable user-select and the
touch callout on the body (it's a fullscreen game), keep transparent tap
highlight, and re-enable selection on form inputs so the settings date/text
fields still work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
Vision statement plus all phases 0–9 (completed) and 10–14 (planned) with
brief descriptions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U13sXB2AGoakPiMQZpay4V
Space flight (Elite Dangerous virtual joystick):
- New virtualStick module: mouse deltas accumulate into a self-centering
  2D stick with radial clamp and frame-rate-independent spring decay
- Mouse now flies the ship (yaw weaker than pitch for cinematic banking);
  right-drag orbits the chase camera
- Gamepad uses a normalized radial dead zone with an S-curve (no axial
  snapping); roll keeps a 1D shaped axis
- Flight-assist damping retuned to ~0.985/frame
- Fine control (Shift or settings toggle) caps thrust at 25% and softens
  rotation; keyboard throttle moved off Shift to W/S

First-person surface (Minecraft slipperiness + cubic look):
- Player physics rewritten to the multiplicative slipperiness model on a
  fixed 20 tps timestep: ground friction = S * 0.91, accel = (0.6/S)^3,
  fixed air drag for predictable jump arcs; fixed gravity-independent jump
- Per-material slipperiness (rock 0.6, ice 0.98, sand 0.7, volcanic 0.55,
  methane/Titan 0.85)
- Cubic mouse/touch look sensitivity (~0.15deg/px at default)
- Touch move joystick gets the radial dead zone + S-curve; no camera roll

Camera: chase cam uses THREE.MathUtils.damp and a sphere-cast against
planets to avoid clipping into terrain.

Settings: mouse/touch sensitivity slider, fine-control toggle, gamepad
dead zone range 0.05-0.20; removed unused lookSensitivity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XWEJ5TcS2ggwbcUKByZcL
Add src/voxel/contentProfiles.ts — a per-body CONTENT lookup mirroring the
existing getBiome/getScatter pattern, keyed by canonical body names. Defines
PropSpec/LandmarkSpec/POISpec/EmitterSpec and getContent(); only props are
consumed now (landmarks/pois/emitters land in 10.1–10.4).

Refactor VoxelScatter into per-kind PropLayer InstancedMeshes so a body can mix
several distinct scatter props, driven by getContent(body).props with the
instance budget split across layers. Bodies without authored props fall back to
the archetype scatter (no regression). Add a 'slab' geometry kind.

Author Mars's three science-grounded props: wind-sculpted ventifacts, layered
sedimentary outcrops, and impact-exposed subsurface water-ice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XWEJ5TcS2ggwbcUKByZcL
Add a landmark height-field modifier pass (landmarkDelta) to columnHeight,
modeled on the existing crater system: each landmark is a deterministic analytic
shape (volcano cone + caldera, basin/lake bowl, canyon/ridge linear feature,
crater bowl+rim) anchored at a body-relative world position. Landmarks are
threaded through VoxelTerrainParams from getContent(body).landmarks, so scatter,
spawn and collision all see the same modified terrain.

Raise ChunkManager's vertMax to include the tallest volcano/ridge landmark so
peaks like Olympus Mons aren't vertically clipped.

Author showcase landmarks: Olympus Mons + Valles Marineris (Mars), Loki Patera
lava lake (Io), Sputnik Planitia + Wright Mons (Pluto).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XWEJ5TcS2ggwbcUKByZcL
Add src/voxel/emitterProfiles.ts (per-kind visuals) and src/voxel/Emitters.tsx:
- ColumnEmitterField: deterministically-placed column emitters around the player
  (Mars dust devils with rising swirl, Io fumaroles, Triton geysers, Titan
  methane bubbles), rebuilt on cell crossing, quality-scaled via voxelParticles.
- VacuumDust: Moon footstep dust that launches from the feet when moving and
  falls in a perfect parabola under the body's low gravity (no air drag).
Drive emitters from getContent(body).emitters; wire <Emitters/> into VoxelScene.
Author emitters for Mars, Io, Triton, Titan, and the Moon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XWEJ5TcS2ggwbcUKByZcL
Add src/voxel/structures.ts: a deterministic POI generator that assembles voxel
modules (room/tower/dome/tank/pipe) Jigsaw-style on a coarse grid, carves
doorways between connected modules, then runs a procedural damage pass — height-
biased collapse plus environmental reclamation (dust/ice/sulphur/sand creeping
in) — so no two instances are alike. Seeded + cached so a POI spanning chunk
seams stamps identically.

Add built-material blocks METAL/PANEL/GLASS (+ palette colours). Thread pois
through VoxelTerrainParams and stamp overlapping POIs into chunks at generation
time in worldGen, so they mesh and collide for free. Raise ChunkManager vertMax
to clear POI height above the surface.

Author showcase POIs with layered narrative-stratigraphy story text: Mars rusted
atmospheric processor + frozen botanical dome; Io geothermal tap with fused
blast doors; Pluto launch relay being entombed by nitrogen ice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XWEJ5TcS2ggwbcUKByZcL
Add a curiosity-driven discovery system (no quest markers). A short-range sensor
hints that something is near (direction + distance only); walking up and scanning
(E key or on-screen button) reveals the POI's layered narrative-stratigraphy
story and records it to a knowledge journal.

Store gains persisted discovery state (discovered/journal/mysteryClues) saved to
localStorage, plus recordDiscovery(). A cross-body 'signal' mystery: the Mars
processor, Io geothermal tap and Pluto relay each carry a clue fragment; once
three converge, a resolution entry is written. worldGen.findNearbyPOI reverses
the deterministic POI placement so the scan and the rendered ruin always agree.

New DiscoveryPanel UI (sensor hint, scan readout, journal) wired into App;
EN/SV strings added. Knowledge persists across reloads; world edits do not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XWEJ5TcS2ggwbcUKByZcL
Author content for the remaining 13 landable bodies: each gets a defining
procedural landmark (Caloris Basin, Maxwell Montes, Tycho, Stickney, Conamara
Chaos, Galileo Regio, Valhalla, Verona Rupes, Serenity Chasma, Kraken Mare,
Cantaloupe Terrain, the elevator anchor) and a modular POI with layered story
text (sheared solar array, Venera/habitat wreck, flooded transit hub, tether
station, buried caches, sub-ice drill, torn magnetics station, empty cryo
facility, cliffside outpost, landslide facility, listening post, aerostat wreck,
heritage dome). Custom science-grounded prop sets added for the Moon, Mercury
and Europa; others use the archetype scatter.

Seed a second lingering mystery ('manufacturer', Titan) and fix the mystery
resolver to count clues per-mystery so the 'signal' thread resolves correctly
after three convergent clues.

Add distance-based LOD thinning to the prop layer so far rings cost less.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XWEJ5TcS2ggwbcUKByZcL
Replace the always-visible right-side body picker (BodyPicker) with a single
hamburger navigation drawer used on all viewports. The drawer now has two clearly
separated sections:
  1. Actions — time scale, Pause, Tour, Orbits, Reset, Settings, Fly
  2. Bodies  — the full planet list for quick camera navigation

Delete BodyPicker and its right-edge styles; unify the former mobile-only drawer
into the primary menu (no separate desktop action bar). Body buttons are 44px+
tap targets in a thumb-reachable drawer. Add menu/actions/fly i18n strings (EN/SV).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XWEJ5TcS2ggwbcUKByZcL
Three HUD components rendered sceneMode.planet/sceneMode.target
directly instead of translating it, so the raw internal canonical id
(the Swedish name used throughout the data layer, e.g. "Jorden") was
shown as-is regardless of the active language:

- SurfaceHUD.tsx: "JORDEN — SURFACE" instead of "EARTH — SURFACE"
- VoxelHUD.tsx: "JORDEN — ON FOOT" instead of "EARTH — ON FOOT"
- DescentOverlay.tsx: the descent target readout

All three now go through useT()'s name() helper, the same pattern
already used correctly in ShipHUD/InfoPanel/Labels. The bug only
surfaced visibly on the 5 bodies whose Swedish and English names
differ (Mercury, Earth, Saturn, Neptune, Moon) — every other body's
id happens to be spelled the same in both languages, which is why it
went unnoticed until landing on Earth specifically.

Also fills a related gap: BODY_NAMES was missing Pluto/Charon (silently
harmless today since bodyName() falls back to the id itself, and those
two ids are already correct in both languages, but the table should be
the single source of truth rather than relying on that coincidence).

Added a completeness test that fails loudly if any future body is
added to systems/bodies.ts without a matching BODY_NAMES entry, plus
basic bodyName() translation coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGDzMFuEEA5rqcHcgTHXid
@LundstedtAdam LundstedtAdam changed the title Audit fixes + gameplay/UX rounds: persistence, perf, settings everywhere, Minecraft water, Settings panel bugs Audit fixes + gameplay/UX rounds: persistence, perf, settings everywhere, Minecraft water, Settings panel bugs, body-name localization Jul 3, 2026
…s footprint

A modular POI (structures.ts) can span up to POI_MAX_HALF_EXTENT (32
voxels) from its cell-hash anchor, assembled from 4-6 jigsaw-connected
8x8 modules — but the scanner's proximity check only ever measured
distance to that anchor point, never the structure's actual generated
extent. SCAN_RANGE is 14, so a player standing in a peripheral module
of a large ruin (or anywhere the anchor itself happened to fall inside
solid terrain) could be physically inside/on the structure and still
never get close enough to the anchor to trigger a scan.

findNearbyPOI/findNearbyWarLorePOI now correct the raw anchor-distance
into a distance-to-footprint (0 once the player is standing anywhere
inside it), via a new correctPOIDistance step that looks up the exact
same generatePOI() result the terrain stamper already built for that
cell — a guaranteed cache hit by the time a player is close enough to
scan, so this adds no measurable cost. findNearestCellSpec now returns
the winning cell's (gx,gz) so callers can re-derive that lookup, and
poiInstanceSeed centralizes the per-instance seed formula previously
duplicated only in stampPOIs.

findNearbyDeepSite (Layer 2 chambers) had the same class of bug at a
smaller scale — chamber radii (8-18) can exceed SCAN_RANGE from the
chamber's vertical-center point alone. Distance is now clamped to the
actual carved cylinder shape (radius + depth band) instead of the
center point.

Both fixes only ever reduce the reported distance relative to the old
formula, so anything already scannable stays scannable — this can only
newly enable scans that previously failed near a structure's edges.

Verified: tsc --noEmit, eslint, vite build, and 5 new regression tests
(9 total in worldGen.test.ts, 45 across the suite) — all pass. Manual
in-game playtesting (walking to a peripheral module of a large ruin
and confirming the Scan button lights up) has NOT been done and is
still recommended before considering this closed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGDzMFuEEA5rqcHcgTHXid
@LundstedtAdam LundstedtAdam changed the title Audit fixes + gameplay/UX rounds: persistence, perf, settings everywhere, Minecraft water, Settings panel bugs, body-name localization Audit fixes + gameplay/UX rounds: persistence, perf, settings, water, body-name localization, scan-range fix Jul 4, 2026
claude added 2 commits July 4, 2026 11:18
Replace the text-list backpack rendering with a grid of ItemSlot
icons (colour swatch + stack count, reusing RESOURCE_COLOR/
CRAFTED_COLOR) and tap-to-select + drop detail row. SiloSheet is
left on the old row-based rendering for now.
Adds two new placeable structures (extractor, condenser) that
passively accumulate a resource while powered, fixed at placement
time — extractors take the ore vein the player aimed at (or the
nearest one found by the existing orbital-scanner direction scan),
condensers draw atmospheric volatiles and are gated to zero output on
airless bodies via the existing hasAtmosphere check. Power allocation
in planetPower is now priority-ordered: refinery, then extractor,
then condenser, each drawing from what's left after the previous
kind.

Production ticks once a second alongside refineTick, depositing into
the structure's own capacity-limited storage (spilling overflow to
the ground like mining does) and reusing the silo sheet UI so players
can walk up and withdraw what's accumulated instead of having to mine
the block.

Offline progression: each body now persists a lastActive timestamp,
and on loading a body's structures the elapsed real-world time is
applied as a single catch-up (applyOfflineProduction), capped at 72h
and clamped to zero for a backward-wound clock. This bounds a single
jump but can't fully close the client-only-clock exploit — a
determined player can still repeat it by winding the clock forward
repeatedly; storage capacity is the ultimate ceiling.

Extractors also give the previously-unused Mining Drill crafted item
(11.2) a purpose as their item cost.
@LundstedtAdam LundstedtAdam changed the title Audit fixes + gameplay/UX rounds: persistence, perf, settings, water, body-name localization, scan-range fix Audit fixes + gameplay/UX rounds: persistence, perf, settings, water, localization, scan-range, grid inventory, offline progression Jul 4, 2026
claude added 11 commits July 4, 2026 15:55
Phases 10 and 11 were still listed as "Planned" despite being almost
entirely built. This brings the roadmap in line with actual commit
history and PR state rather than the original planning-time text:

- Marks Phase 10 done and adds Phase 10.5 (8-act mystery/narrative
  system) as its own entry, noting its dependency on Phase 10 and its
  naming coincidence with an unrelated commit also labeled "Phase 10.5".
- Breaks Phase 11 into its actual 11.0-11.7 sub-phases with status per
  the real commit history, including the later Hyperdrive->Quantum Drive
  rename and the unlabeled but substantive 11.6/11.7 work folded into
  PR #8. 11.6 is marked "Implemented, pending playtest" rather than
  done, since manual verification is in progress.
- Adds a Branches & PRs section recording storyline as the single
  active branch and PRs #1-7 as closed/superseded.
- Records PR #8 as cross-cutting technical audit work that was never
  assigned a phase number, with a note on where to track it.

No code or commit history changed - this is a status-tracking update
only.
BiomeProfile already authors detailFreq/detailAmp and microFreq/microAmp
per body, but only the GPU orbital shader consumed them — voxel worldgen
ignored them entirely, so every body in an archetype shared the exact
same roll/mountain height-field shape. columnHeight now layers these in
as a secondary fbm2 octave (detail) and a finest single-octave bump
(micro), giving each planet its own fine terrain texture for free.

Guarded on amplitude > 0 so bodies/tests without these fields are
unaffected (byte-identical height field to before).
Every body now layers fine abiotic grit (loose stones, always-on) under
the existing per-archetype feature scatter, and Earth additionally gets
dense grass tufts ('blade', a hand-built crossed-quad billboard) plus
small flower/mushroom accents ('fungus' reused at flower scale). This
replaces the old "authored props XOR archetype fallback" selection with
an additive stack (clutter + feature + any authored accents), so the 4
already-curated bodies (Mars/Manen/Merkurius/Europa) gain density
instead of losing their archetype baseline.

The 12 bodies that previously had no authored props at all now get one
distinguishing accent prop each, reusing existing scatter kinds with
body-specific colour/scale.

Life-like ground clutter (grass/flowers/ambiguous alien growths) is
gated by the game's existing no-confirmed-life rule: Earth is
unconditional real biology; Mars/Titan/Venus/Europa/Ganymede (the
bodies whose contentProfiles.ts science note lifeStatus is
'theoretical'/'inconclusive') get sparse, low-density ambiguous
growths tinted from their own palette; every other body stays
abiotic-only. This reads scienceNotes directly rather than duplicating
the classification, so it can't drift from the narrative data.

Also adds the waterAdjacent scatter-placement filter (mechanism only,
unused until the hydrology phase) and a new scatterProfiles.test.ts
covering the narrative gating.
Two new POITypes reusing the existing jigsaw/damage/cache pipeline in
structures.ts entirely:

- 'outpost': adds two new ModuleKinds ('bridge', an elevated gangway
  fragment with support pillars and rails; 'collapsed', a room whose
  walls/roof have already failed independent of the shared damage()
  roll) so not every ruin reads as one of the same 4 archetypes.
- 'cache': the micro-discovery channel — a single small debris/crate
  cluster (budget [1,1], 4-6 voxels, no floor pad), placed on every
  body via a new universal MICRO_DISCOVERY POISpec concatenated onto
  each body's authored pois[] in voxelBiomes.ts (same "universal +
  per-body" composition FUNDAMENTALS already uses for ore veins,
  avoiding 16 near-identical per-body copies). No story/clue/act, so
  the journal just logs "found: Debris Cache" per instance without
  needing hand-authored prose - reuses stampPOIs/findNearbyPOI/
  recordDiscovery verbatim.

buildModule() now takes the shared rng stream (threaded from build())
to drive 'cache' layout and 'collapsed' wall-voxel dropout.

New structures.test.ts covers determinism and POI_MAX_HALF_EXTENT
compliance for both new types.
…re tells

- noise.ts: new fbm3 (3D analogue of the existing fbm2).
- worldGen.ts generateChunk: worm-tunnel carve intersects two independent
  fbm3 fields near their midpoint, additive to the existing blobby-cavern
  test (never replaces it) — gives winding tunnel-like voids alongside
  caverns. A separate, rarer overhang/alcove carve is gated to steep
  cliff faces only (4-sample columnHeight slope probe, once per column)
  and allowed to breach the previously-untouchable depth<=2 skin there,
  giving occasional shallow undercuts on cliffs without affecting
  interior caves or flat ground.
- layerBlock: the subsoil/rock (and ice/rock) depth threshold is now
  perturbed by a lateral valueNoise2 term sourced from the body's own
  cellNoiseFreq/cellNoiseAmp (BiomeProfile, previously GPU-shader-only,
  same idle-data reuse as Phase 1) — strata boundaries wave across a
  cliff face instead of forming a flat plane. 0 on bodies that author
  cellNoiseAmp=0 (Earth), so their layering is unaffected.
- New universal "ore tell" scatter layer (oreGlint in scatterProfiles.ts,
  oreTell filter in VoxelScatter.tsx): a rare glinting mineral fleck
  placed only where oreAt() (now exported) confirms a vein surfaces
  within a few voxels — reuses the exact same vein data the orbital
  scanner already agrees on, so the hint never lies about what's below.

New noise.test.ts and worldGen.test.ts coverage for fbm3, the worm-tunnel
carve (isolated via an impossible cavern threshold), and the layered-rock
jitter (isolated via a flat, cave-free synthetic terrain).
Adds a flat-world latitude proxy (latitudeOf/LATITUDE_SPAN in
voxelBiomes.ts, pure function of world-Z distance from the equator —
these are flat voxel worlds, not literal spheres) shared by two
systems:

- voxelMaterial.ts: the terrain/water shader now blends albedo toward
  the body's own BiomeProfile.colorPolar near the "poles" (a gradual
  TSL smoothstep band keyed on positionWorld.z, applied before the
  existing fog mix). This is a pure shader-graph addition — it never
  touches the per-block palette Float32Array or the mesher/worker
  transfer contract, which only support one flat colour per block id
  per body and can't vary by position without much larger surgery.
  colorPolar was already authored per body but, like Phase 1/4's
  detail/micro/cellNoise fields, only ever consumed by the GPU orbital
  shader — never the voxel walking layer. Skipped entirely (zero cost)
  on bodies that didn't author a distinct polar colour.
- VoxelScatter.tsx: Earth's grass/flora ground-clutter layers
  (scatterProfiles.ts) now thin out toward the poles via a new
  latitudeFalloff flag, a gradual per-instance density falloff rather
  than a uniform carpet end to end.

New voxelBiomes.test.ts (latitudeOf monotonicity/bounds) and an
extra scatterProfiles.test.ts case confirming the flora layers are
flagged for falloff.
Local water - independent of the body's single global sea level:

- LakeSpec (contentProfiles.ts): a cell-hash-scattered basin (same
  placement mechanism as craters/POIs), gated by archetype (earth gets
  ordinary lakes, dune gets sparser methane lakes tinted differently
  in the palette) rather than hand-authored per body.
- 'river' LandmarkSpec kind: a short polyline (2-4 points), each
  consecutive pair a canyon-style trench segment with end tapering.
  One demonstration river added to Jorden.
- worldGen.ts: a new localWaterCeilingAt() computes, per column, the
  higher of the global sea and any active lake/river's own natural rim
  height (h + carve-depth at that column - no separate rim sampling
  needed). Used for the liquid-fill decision in generateChunk, for
  surfaceHeightAt's spawn-safety check (so a lake landing on the
  disembark origin can't spawn the player underwater), and now for
  VoxelScatter's underwater/shoreline checks too (previously those only
  knew about the global sea, so ground clutter could render underwater
  inside a new lake, and shoreline dressing couldn't find a lake's
  shore at all).

Shoreline/wetland dressing (scatterProfiles.ts, using Phase 2's
waterAdjacent filter): abiotic wet-mineral dressing on every body
(silently places nothing where there's no water at all), plus
Earth-only reeds - kept off ambiguous-life bodies since "reeds" is a
much less deniable life claim than the vague growths used elsewhere.

Waterfalls: never cell-hash scattered like other emitters (an
EmitterSpec model mismatch for a feature that must sit at one exact
point) - RiverWaterfalls.tsx instead derives them automatically from
a river's own polyline wherever consecutive points' natural terrain
height drops more than 8 voxels, so a waterfall can never drift out of
sync with its river's geometry. Shares Points/material setup with
Emitters.tsx via a new emitterPoints.ts module (also fixes a
react-refresh lint warning from exporting a non-component helper out
of a component file).

New worldGen.test.ts coverage: lake/river height-field dip, water fill
without a global sea, no regression when a body has no lakes, and
river trench tapering.

Known limitation, not addressed this round: full connected river
networks with realistic hydraulic flow remain explicitly out of scope
(see the plan) - this is bounded polylines only. The one demonstration
river's exact visual placement has not been playtested.
treeProfiles.ts + VoxelTrees.tsx add a parameterized (not full L-system)
tree prefab: a tapered trunk cylinder plus an optional canopy sphere,
both InstancedMesh, placed with the exact same cell-hash/ring-shell/LOD
streaming pattern VoxelScatter.tsx already uses (new sibling component
rather than extending VoxelScatter, to avoid destabilizing the working
scatter system with a multi-part "compound prop" concept it wasn't
built for).

Per-tree seed drives: age variation (height range), dead trees (skip
the canopy instance), fallen trees (same trunk geometry, reoriented
rather than a separate part), and a low-frequency forest-coverage mask
(fbm2) that clusters trees into groves with clearings instead of a
uniform carpet. New quality.voxelTrees budget follows the existing
voxelScatter/voxelParticles shape (flat per-tier ceiling).

Narrative gating matches Phase 2's ground clutter exactly, now shared
via a single lifeAmbiguous() moved from scatterProfiles.ts into
contentProfiles.ts (the canonical narrative-data module) so ground
clutter, trees, and (next) wildlife can't drift from each other or
from the game's actual science-integrity data: real forest on Earth,
sparse ambiguous growths (tinted from the body's own palette) on
bodies whose life question is still open, nothing anywhere else.

Scope note (documented in treeProfiles.ts): no visible branch geometry,
exposed roots, or hollow trunks this round — a canopy sphere on a trunk
reads as "tree" at voxel-game distances, and a third instanced part
for branches alone wasn't judged worth the added complexity.

New treeProfiles.test.ts covers the narrative gating.
wildlifeProfiles.ts + VoxelWildlife.tsx add small instanced creatures
with simple procedural circling/bobbing motion (no skeletal animation):
real birds + ground critters on Earth, one sparse deliberately-ambiguous
"drifter" layer (never a clear animal silhouette) on bodies whose life
question is still open, nothing anywhere else. Modeled closely on
Emitters.tsx's existing ColumnEmitterField (cell-hash anchors rebuilt
only on cell-crossing, per-instance motion computed every frame) but
drives an InstancedMesh of small solid shapes instead of point sprites.
Skipped entirely on the lowest quality tier — ambient creatures are
flavour, not core gameplay.

Narrative gating reuses the same lifeAmbiguous() moved to
contentProfiles.ts in Phase 7, so ground clutter, trees, and wildlife
all read from one shared, single source of truth and can't drift from
each other or from the game's actual science-integrity data.

Audio: a new AudioManager.playWildlifeCall() synthesizes two calls (no
sample assets, same oscillator/noise-buffer technique as the existing
playSurfaceTexture) — a bright chirp for Earth, a slow ambiguous pitch
wobble elsewhere — scheduled by a parallel setTimeout-with-jitter
driver in VoxelAudio.tsx that runs alongside (never replacing) the
existing ice/volcanic/geyser texture scheduler, gated to the same
bodies the visual layer appears on.

New wildlifeProfiles.test.ts covers the narrative gating. This closes
out the 8-phase World Richness plan's content phases; a final
verification + self-review pass follows.
Documents the 8-phase procedural-richness pass across the voxel
walking layer as PR #8's Round 8 (technical audit & hardening
section) — genuinely new capability rather than maintenance, but
still not a numbered phase since it deepens the existing Phase 9-11
voxel world rather than opening a new roadmap arc.
@LundstedtAdam LundstedtAdam changed the title Audit fixes + gameplay/UX rounds: persistence, perf, settings, water, localization, scan-range, grid inventory, offline progression Audit fixes + gameplay/UX rounds + World Richness pass Jul 4, 2026
claude added 2 commits July 4, 2026 20:33
Replaces flat-vertex-color-only block identity with a real procedural
texture atlas (deterministic pixel buffer, no canvas/DOM) sampled per
voxel face via a new materialUV vertex attribute threaded through the
greedy mesher and worker pipeline. Biome palette tint is diluted to a
partial-strength overlay on natural terrain/tree blocks so the atlas
texture is the primary material identity, not just color.

Adds two new block ids (WOOD_LOG, LEAVES) and a voxel-embedded tree
generator (trees.ts) stamped into Earth's terrain by worldGen.ts,
replacing the old purely-cosmetic InstancedMesh forest. Trees are
choppable: mining a trunk voxel cascades upward through the whole log
column, yields wood via the existing resource pipeline, and triggers
leaf-disconnection decay (treeDecay.ts) for any leaves left without a
nearby log — all reusing the existing mining/edit/persistence path.

Not visually verified on a live WebGPU device (this environment can't
render); tsc/lint/build/vitest all green (112 tests).
Records the texture-atlas/material-face-mapping system and the switch
from cosmetic to voxel-embedded choppable trees on Earth, alongside PR #8's
existing round history.
@LundstedtAdam LundstedtAdam changed the title Audit fixes + gameplay/UX rounds + World Richness pass Audit fixes + gameplay/UX rounds + World Richness + Material Identity passes Jul 4, 2026
claude added 8 commits July 5, 2026 07:39
VoxelScatter.tsx's crossedQuadGeometry (the 'blade' prop kind used for
grass tufts/weeds/reeds) had no UV attribute and its material had no
alpha texture, so the billboard rendered as a flat solid rectangle
instead of a foliage silhouette.

Adds a deterministic alpha-cutout grass-blade texture (textureAtlas.ts's
getBladeAlphaTexture, same raw-pixel-buffer pattern as the existing
material atlas — no Canvas/DOM), gives the crossed-quad geometry a
standard UV attribute (extracted to scatterGeometry.ts for testability,
mirroring the existing Emitters.tsx/emitterPoints.ts split), and wires
the blade material to alphaTest-cutout against it.

Other three reported issues (UV stretching on terrain, missing AO,
floating scatter/wildlife) were checked against the code and found not
to be present — already correct and covered by passing tests — so left
untouched per investigation + user decision.

Not visually verified on a live WebGPU device; tsc/lint/build/vitest
all green (118 tests).
SolarSystem.tsx registers each classic-three light class with the
WebGPU renderer's node library (nodeLibrary.getLightNodeClass keys by
constructor, and an unregistered class is dropped entirely with a
console warning, per-frame). AmbientLight and PointLight were
registered; DirectionalLight never was, so VoxelScene.tsx's SunLight —
the terrain's only directional key light, and the sole source of
directional shading, the normal-jitter effect, and shadows on the
voxel surface — has been silently doing nothing since it was added.

With only flat ambient light contributing, the surface reads as one
flat, dim wash regardless of the texture atlas or baked AO underneath,
exactly matching a live screenshot from an actual device. This is a
pre-existing gap (not introduced by the Material Identity pass) that
nothing caught because no prior round was checked against a live
WebGPU device.

Attempted a live visual check via Playwright/Chromium against a local
preview build: real WebGPU is active in this sandbox, but its software
GPU implementation fails on this app's buffer sizes entirely
("createBuffer failed... too large for the implementation"), so the
canvas never renders here regardless of this fix — an environment
limitation, not a code issue. tsc/lint/build/vitest all green (118
tests, no logic changed). Still needs confirmation on real hardware.
…ientation

- styles.css: the active-state pill button rule (.seg button.active) had
  identical CSS specificity to a later, more generic rule
  (.settings-panel .setting-row button), so the generic rule always won
  the cascade tie and painted every quality/language/game-mode button
  the same regardless of selection. Scoped the active rule under
  .settings-panel to raise its specificity so it always wins.

- Sun.tsx: the sun's HDR core material used an extreme, unevenly-scaled
  multiplier (3.5/2.8/2.0) "so the bloom pass makes it glow". Bloom is
  off on Low quality (invisible) and turns on at Medium+, at which
  point ACES filmic tonemapping's known hue-shift-toward-green artifact
  on very bright, unevenly-scaled highlights kicks in — this exact bug
  class was already hit and fixed for the ship model's emissive glow.
  Retuned to a lower-magnitude, warm-white value matching this
  codebase's own established sunlight color convention.

- Effects.tsx: the whole postprocessing GPU pipeline (PostProcessing +
  PassNode +, when bloom is on, BloomNode's ~11 render targets) was
  rebuilt from scratch on every single quality change, and the previous
  instance's GPU resources were never disposed — a leak that eventually
  hits a WebGPU resource-exhaustion crash. Now only rebuilds (with
  proper disposal) when bloom/chromatic-aberration structurally turn on
  or off; ordinary quality-tier tuning updates the existing reactive
  uniforms in place instead (bloom's strength/radius/threshold are
  already uniform()-wrapped internally; added an equivalent uniform for
  chromatic aberration, mirroring this file's existing heatUniform).

- greedyMesh.ts + textureAtlas.ts: found while reviewing grass_side
  against a Minecraft-clarity reference. Two real orientation bugs, not
  rendering yet since the sun/light fixes just landed: (1) grass_side's
  row-to-grass/dirt mapping was inverted relative to how DataTexture's
  flipY=false + the UV emission actually place v=0/v=1 on a voxel face,
  so grass showed at the bottom and dirt at the top; (2) east/west faces
  (d===0) use the opposite (u,v) sweep-axis pairing from north/south
  faces (d===2), so the directional gradient ran horizontally instead
  of vertically on two of every grass block's four side faces. Fixed
  both, plus narrowed the transition band for a crisper edge.

tsc/lint/build/vitest all green (119 tests, 1 new regression test for
the d===0 UV orientation). Attempted a live Playwright/Chromium check
again; this sandbox's software WebGPU still can't allocate this app's
buffers at all ("createBuffer failed... too large for the
implementation"), unrelated to any of these fixes — still needs
confirmation on real hardware.
The previous fix only retuned Sun.tsx's core sphere material. Two more
uncapped/extreme HDR "sun glow" sources were still feeding bloom:

- VoxelSky.tsx and Sky.tsx's sky-dome sun disc (identical code in both,
  deliberately separate per this project's voxel/orbital rendering
  split) peaked at 8.6x magnitude — over 4x higher than the already-
  fixed sun mesh, and covering a full-screen sky dome rather than a
  small sphere. Reduced to 2.6x peak in both files.
- Sun.tsx's corona inner ring used an even more extreme, unevenly-
  scaled ratio (1:0.55:0.2) at 2.4x magnitude than the original
  problematic sun-core value — previously left untouched as "not the
  primary source", which undersold its contribution. Reduced to 1.6x
  at a much less extreme 1:0.8:0.6 ratio.

quality.ts's bloomThreshold drops (0.85 -> 0.75 -> 0.65) and
bloomStrength rises (0.25 -> 0.3 -> 0.35) from medium to ultra, so
higher tiers admit more of this previously-unfixed HDR into bloom and
amplify it harder — explaining why the glow was specifically reported
on High/Ultra after the first fix.

tsc/lint/build/vitest all green (119 tests, pure numeric constants, no
logic change). Still needs confirmation on real hardware — this is a
second tuning pass without the ability to render locally; if the glow
persists, the next step is pinpointing exactly which object (sky vs.
sun disc vs. corona) still shows it rather than continuing to guess.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants