diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..03520a0 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,140 @@ +# 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.