# voxel-game architecture Functional-core / imperative-shell split. Every game-logic transformation is a pure function `(value, value) -> value`; the shell threads them through the GPU + winit + JS bridges each tick. ## Module map ``` src/ ├── lib.rs module declarations + run() ├── main.rs native binary entry ├── app.rs IMPERATIVE SHELL — App + winit handler + tick(). │ Composes sim/ + net/ + render/ through bridges. ├── bridges.rs THE ONLY MUTABLE-STATE OWNER. thread_local cells │ for touch input, game status, network, settings, │ plus the wasm-bindgen JS interface. Typed │ accessor functions hide the RefCells. ├── camera.rs Camera + InputState + KbHeld (pure value types). ├── world.rs World + Chunk + Block + raycast + natural_surface_y. ├── mesh.rs Vertex + build_chunk_mesh (greedy meshing + AO); │ emit_oriented_box + name_hash for remote players. ├── proto.rs Wire format for the multiplayer server. ├── shader.wgsl Terrain + sky + outline pipelines (WGSL). ├── post.wgsl Post-process pass-through. ├── shader_source.rs Assembles shader source by prepending Rust- │ derived constants (DAY_PERIOD, SUN_OFFSET) to │ shader.wgsl — one source of truth shared with │ sim::lighting. │ ├── sim/ PURE SIMULATION CORE. No GPU, no winit, no JS. │ ├── mod.rs re-exports │ ├── body.rs PlayerBody { feet, velocity, hp, alive, ... } │ ├── collision.rs AABB + sweep_axis(world, feet, delta, axis) → │ │ (Vec3, bool) │ ├── edit.rs apply_edit(world, EditRec) + chunks_for_edit │ ├── event.rs SimEvent { Landed, VoidDeath, BlockEdited } │ ├── input.rs TouchBridge + merge_held(kb, bridge) → KbHeld │ ├── lighting.rs ⭐ SHARED SUN/SKY TRUTH ⭐ │ │ sun_direction(t), day_strength, twilight_amount, │ │ LightingFrame::at(t), is_in_direct_sun(world, p, t). │ │ Constants DAY_PERIOD / SUN_OFFSET exported to the │ │ WGSL shader via shader_source.rs so the visual │ │ sun direction can never disagree with the │ │ mechanical one consumed by future mob burn / │ │ plant growth / shade pathfinding. │ ├── physics.rs step_movement(world, body, MoveInput) → │ │ MoveOutcome { body', [SimEvent] } │ ├── spawn.rs find_safe_spawn, fall_damage │ └── visibility.rs compute_visible_chunks (frustum + radial cull) │ ├── net/ PURE NETWORK PARSING │ └── mod.rs parse_inbox([raw line]) → [NetEvent] │ └── render/ GPU SHELL — wgpu device, surface, pipelines ├── mod.rs Renderer struct + new() + all draw methods ├── pipelines.rs Pipeline + bind-group-layout factories ├── scene_target.rs depth / scene-color / post-bind-group helpers └── uniform.rs CameraUniform (matches WGSL camera.frame) + OutlineVertex + ChunkBuffers ``` ## Tick pipeline `app::App::tick()` is one frame, in this order: ``` dt + scaled shader_time ↓ (paused?) clear inputs, render last frame, return ↓ drain touch bridge → per-tick input snapshot ↓ honor respawn-request one-shot ↓ drain net inbox → parse_inbox → fold NetEvents into world + remote-player map ↓ send Hello on (re)connection ↓ apply mouse look to camera (yaw/pitch) ↓ sim::step_movement(world, body, MoveInput) → (body', [SimEvent]) ↓ fold sim events into damage ↓ camera.position = body.feet + EYE_HEIGHT * Y ↓ raycast → break/place → maybe broadcast edit ↓ apply damage; periodic state broadcast ↓ render_frame(settings, outline) → Renderer ``` The shell only mediates between pure morphisms and side-effecting boundaries (GPU upload, network outbox, JS thread-locals). ## The lighting invariant **Why this matters:** in Minecraft-class games it's common for the visual shadow renderer and the "mob burns in sunlight" logic to use *different* sun directions / different occlusion tests, and they drift. A mob can stand in visible shade and burn anyway. We avoid that by construction: ``` DAY_PERIOD, SUN_OFFSET (defined once in sim::lighting) │ ┌───────────┴───────────┐ ▼ ▼ sim::lighting::sun_direction(t) shader_source::wgsl_constants_header() │ │ (prepended to shader.wgsl) ▼ ▼ is_in_direct_sun(world, p, t) shader.wgsl::sun_direction(t) │ │ ▼ ▼ mob burn, plant growth, AI pathing visual shading (terrain + sky) ``` When real shadows land in the shader, they ray-march the same voxel grid using the same `sun_direction(t)`. Visual shadow edges will agree with mechanical sun edges by construction. ## Test coverage (53 tests as of this writing) - `mesh`: 8 — winding correctness, greedy meshing, AO darkening, oriented-box winding - `sim::body`: 3 — damage, immunity-when-dead, respawn reset - `sim::collision`: 3 — air sweep, ground snap, no-clip invariant - `sim::edit`: 2 — u8↔Block roundtrip + garbage fallback - `sim::input`: 5 — merge_held release semantics (the joystick-stick bug) - `sim::lighting`: 4 — night below horizon, lit at noon, occluded under overhead, ramp shapes - `sim::physics`: 4 — gravity, jump-only-when-grounded, Landed event, void death, dead-body-doesn't-move - `sim::spawn`: 7 — fall damage boundaries; spawn anchored to natural surface - `net`: 4 — malformed dropped, Welcome / Edit / order - `world`: 4 — terrain determinism, raycast hits/misses - `shader_source`: 2 — header contains constants, terrain source contains body - `app`: (no unit tests — orchestrator; coverage comes from the pure pieces it composes) Running: `cargo test --lib` from the repo root.