Audit fixes + gameplay/UX rounds + World Richness + Material Identity passes - #8
Draft
LundstedtAdam wants to merge 79 commits into
Draft
Audit fixes + gameplay/UX rounds + World Richness + Material Identity passes#8LundstedtAdam wants to merge 79 commits into
LundstedtAdam wants to merge 79 commits into
Conversation
…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
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
…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
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.
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.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the prioritized action plan from the full code audit, plus eight follow-up rounds.
Round 1 — audit fixes
Bug fixes
setShipUpgradesnow derivesbackpackCapacityfrom the cargo tier.upgradeShipandcraftpersist inventory/items immediately.Performance
shipTelemetryobject.UX & tests
findNearby*scanners collapsed into one generic helper; eslint underscore convention.Round 2 — gameplay & UX improvements
Round 3 — Settings panel: unreachable rows, dead taps, and settings that silently did nothing
.settings-body.touchenddouble-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.Round 4 — hardcoded body names leaking through in English mode
Three HUD components rendered
sceneMode.planet/sceneMode.targetdirectly 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 throughuseT()'sname()helper. Also filled a related gap:BODY_NAMESwas 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 toPOI_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_RANGEis 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/findNearbyWarLorePOInow correct the raw anchor-distance into a distance-to-footprint (0 once the player is standing anywhere inside it), reusing the same memoizedgeneratePOI()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 exceedSCAN_RANGEfrom the center point alone) — distance is now clamped to the actual carved cylinder shape instead.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 existingRESOURCE_COLOR/CRAFTED_COLORtables, 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 underlyingPartial>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:
hasAtmospherecheck, so it never draws power there either.planetPowerallocation 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.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.Offline progression: each body now persists a
lastActivewall-clock timestamp (localforage, same pattern as existing per-body keys). When a body's structures are loaded, the elapsed real-world time sincelastActiveis applied as a single production catch-up (applyOfflineProduction), capped at 72h and clamped to zero if the elapsed time is negative (clock wound backward).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), theVoxelScatter.tsxinstancing pattern, andfbm2/valueNoise2/valueNoise3.Phase 1 — per-planet terrain identity from already-authored data.
BiomeProfilehaddetailFreq/detailAmp,microFreq/microAmp, andcellNoiseFreq/cellNoiseAmpfields 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 intocolumnHeightas extra octaves and intolayerBlock'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 (reusesoreAtdirectly, 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 newoutpostPOI type. A newcachePOI type (budget[1,1], 4-6 voxels) is the micro-discovery channel — a universalMICRO_DISCOVERYspec concatenated onto every body's POIs (same "universal + per-body" compositionresourceProfiles.ts'sFUNDAMENTALSalready uses), giving frequent small cosmetic finds without any per-instance authoring.Phase 4 — cave/terrain 3D richness. New
fbm3noise primitive. A worm-tunnel carve (two independentfbm3fields 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)colorPolarnear the "poles" — a pure TSL addition tovoxelMaterial.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 newriverLandmarkSpeckind (a short polyline, canyon-style trench per segment). A newlocalWaterCeilingAtcomputes the higher of the global sea and any active lake/river's own natural rim height per column, used for the fill decision, forsurfaceHeightAt's spawn-safety check, and forVoxelScatter's underwater/shoreline checks. Shoreline dressing reuses thewaterAdjacentfilter (abiotic everywhere water exists; Earth-only reeds). Waterfalls are never cell-hash scattered like other emitters —RiverWaterfalls.tsxderives 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 asVoxelScatter, in a new siblingVoxelTrees.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 newAudioManager.playWildlifeCallsynthesizes two calls (no sample assets — same oscillator/noise-buffer technique as the existingplaySurfaceTexture), scheduled by a parallel driver inVoxelAudio.tsxalongside (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 (
LifeStatushas 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 sharedlifeAmbiguous()(moved intocontentProfiles.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 flaggedtheoretical/inconclusivein 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 existingvalueNoise2primitive), not Canvas2D/OffscreenCanvas, so it builds correctly under the vitest/jsdom test environment and stays importable inside the mesher worker without pullingthree'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'semitQuad— which already computes face direction right where it reads the palette — now also emits amaterialUVvertex 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 existingMeshResult/worker transfer contract (voxelTypes.ts,mesher.worker.ts,mesher.ts) exactly likepositions/normals/colorsalready do.voxelMaterial.tssamples the atlas via a new TSLtexture()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_LOGandLEAVES. A newtrees.tsgenerates a deterministic trunk-plus-canopy voxel assembly (reusingstructures.ts'sSVoxel/BuiltStructurecontract and seeded PRNG — not routed through the POI room-jigsaw machinery, which doesn't fit a tree), and a newstampTrees()inworldGen.tsplaces them on Earth using the same cell-hash/forest-coverage-mask tuning the old cosmetic forest used. Mining a trunk voxel (ChunkManager.tsx'smineTick) now cascades upward through the whole log column, yielding one'wood'resource per log via the existing mining pipeline; afterward a newtreeDecay.tsscans the felled column's own bounding box for anyLEAVESvoxel left with noWOOD_LOGwithin 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-interacticInstancedMeshforest (treeProfiles.ts/VoxelTrees.tsx) is retired; the separate "ambiguous alien growth" decorative variant ontheoretical/inconclusivebodies 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 togreedyMesh.test.ts,worldGen.test.ts, andtreeProfiles.test.tscover 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'scrossedQuadGeometry(thebladeprop 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'sgetBladeAlphaTexture— 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 newscatterGeometry.tsfor unit-testability, mirroring the existingEmitters.tsx/emitterPoints.tssplit), andalphaTest-based cutout wired onto thebladematerial 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:
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.DataTexturecolorSpace/mipmap defaults and thematerialUV/atlas-tile UV math all check out correct.Actual root cause:
SolarSystem.tsxregisters 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 bylight.constructor, and an unregistered class is dropped with a console warning, every frame).AmbientLightandPointLightwere registered;DirectionalLight—VoxelScene.tsx'sSunLight, 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—DirectionalLightNodeis a named export fromthree/webgpu, same import path as the two already-registered light-node classes.DirectionalLightis 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:
src/ui/SettingsPanel.tsx's quality/language/game-mode pickers correctly apply anactiveclass, andstyles.cssdefines 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-panelto raise its specificity so it always wins regardless of declaration order.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.tsxrebuilt 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 alreadyuniform()-wrapped internally; added an equivalent uniform for chromatic aberration, mirroring this file's existingheatUniformpattern).textureAtlas.ts's grass/dirt row mapping was backwards relative to howDataTexture'sflipY=falseand the UV emission actually place texture-spacev=0/v=1on a rendered voxel face, so grass showed at the bottom and dirt at the top; and east/west faces (d===0ingreedyMesh.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'sbloomThresholddrops (0.85 → 0.75 → 0.65) andbloomStrengthrises (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 inmaterials.tsare 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