diff --git a/CLAUDE.md b/CLAUDE.md index e293e66..b7356af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,578 +1,15 @@ # CLAUDE.md -This file provides guidance to Claude Code when working with code in this repository. - -## Project Overview - -This is a pure Rust game project using SDL3 for windowing/input, wgpu for rendering, rapier3d for physics, and a low-res retro aesthetic with dithering. This is a migration from the Godot-based snow_trail project, implementing the same snow deformation system and character controller without engine dependencies. - -**Content Creation:** Blender 5.0 is used for terrain modeling and asset export (glTF meshes + EXR heightmaps). +Pure Rust game: SDL3 windowing, wgpu rendering, rapier3d physics, low-res retro aesthetic with dithering. Content created in Blender 5.0 (glTF meshes + EXR heightmaps). ## Code Style -**Code Documentation Guidelines**: -- **NO inline comments unless ABSOLUTELY necessary** -- Code must be self-documenting through clear naming and structure -- Use doc comments (`///`) only for public APIs and complex algorithms -- Avoid obvious comments that restate what the code does -- Let the code speak for itself +- **NO inline comments unless ABSOLUTELY necessary** — code must be self-documenting +- Doc comments (`///`) only for public APIs and complex algorithms +- Run `cargo fmt` before committing (`brace_style = "AlwaysNextLine"`, `control_brace_style = "AlwaysNextLine"`) +- **NO inline paths** — always add `use` statements at the top of files, never inline +- **NO `use` statements inside functions or impl blocks** — all `use` must be at the file (module) level -**Formatting:** -- All code must follow the project's `rustfmt.toml` configuration -- Always run `cargo fmt` before committing to ensure consistent formatting -- Current rustfmt settings: brace_style = "AlwaysNextLine", control_brace_style = "AlwaysNextLine" -- **NO inline paths** - always add `use` statements at the top of files (e.g., `use std::rc::Rc;` instead of `std::rc::Rc` inline in code) -- **NO inline `use` statements in functions** - all `use` statements must be at the file level (module top), not inside function bodies or impl blocks - -## Architecture Decisions - -### ECS Architecture - -The project uses a **pure ECS (Entity Component System)** architecture: - -**Entities:** -- Just IDs (`EntityHandle = u64`) -- Managed by `EntityManager` (spawn/despawn) -- No data themselves - just containers for components - -**Components:** -- Pure data structures stored in component storages -- Each storage is a `HashMap` -- No `Rc>` - clean ownership model -- Components: Transform, Mesh, Physics, Movement, Input, PlayerTag, StateMachine - -**Systems:** -- Functions that query entities with specific component combinations -- Run each frame in defined order -- Read from and write to component storages -- Examples: `player_input_system`, `state_machine_system`, `physics_sync_system`, `render_system` - -**Component Storages (World-owned):** -- `TransformStorage` - Position, rotation, scale -- `MeshStorage` - Mesh data + render pipeline -- `PhysicsStorage` - Rapier3d rigidbody/collider handles -- `MovementStorage` - Movement config + state -- `InputStorage` - Gameplay input commands -- `PlayerTagStorage` - Marker for player entities -- `StateMachineStorage` - Behavior state machines -- All storages owned by single `World` struct for clean ownership - -**Key Benefits:** -- No `Rc>` needed - components are just data -- Clear data flow through systems -- Easy to add/remove components at runtime -- Testable - systems are pure functions -- StateMachine integrates as a component for complex behaviors -- EventBus remains for irregular events and cross-system messaging - -### SDL3 vs SDL2 -We are using SDL3 (latest stable bindings) rather than SDL2. SDL3 provides: -- Modern GPU API integration -- Better input handling -- Active development and future-proofing - -As of December 2025, SDL3 Rust bindings are usable but still maturing: -- `sdl3` crate: v0.16.2 (high-level bindings) -- `sdl3-sys` crate: v0.5.11 (low-level FFI) -- Some features may be incomplete, but core functionality is stable - -### wgpu for Rendering - -**Using wgpu instead of OpenGL:** -- Modern GPU API abstraction (Vulkan/Metal/DX12/OpenGL backends) -- Better cross-platform support -- WESL shader language (WebGPU Shading Language) -- Type-safe API with explicit resource management -- Low-res framebuffer rendering with 3-bit RGB dithering (retro aesthetic) - -**Rendering Architecture:** -- wgpu for 3D mesh rendering with custom shaders -- Low-resolution framebuffer (160×120) upscaled to window size -- Multiple rendering pipelines: standard meshes and terrain -- Separate bind groups for different material types - -### Future: Debug UI -- Debug UI system not yet implemented -- Will be used for real-time parameter tweaking (replacing Godot's exported properties) -- Current debugging relies on println! and recompilation - -## Physics Integration - -Using rapier3d for 3D physics: -- Character controller implemented manually (no built-in CharacterBody equivalent) -- Ground detection via raycasting with QueryPipeline -- Manual rigidbody velocity application -- State machine for movement states (Idle, Walking, Jumping, Falling) - -## Input Handling - -**Two-Layer Input Pipeline:** - -**Layer 1: Raw Input (`utility/input.rs` - `InputState`):** -- Global singleton for SDL event handling -- Tracks raw hardware state (W/A/S/D pressed, mouse delta, etc.) -- Handles SDL events via `handle_event()` method -- Manages global state (mouse capture, quit request, noclip mode) -- Lives in main event loop - -**Layer 2: Gameplay Commands (`components/input.rs` - `InputComponent`):** -- Per-entity ECS component -- Stores processed gameplay commands (move_direction, jump_pressed) -- Filled by `player_input_system()` which reads `InputState` -- Used by movement systems to control entities -- Decouples input source from entity control - -**Input Flow:** -``` -SDL Events → InputState → player_input_system() → InputComponent → movement_system() -``` - -**Current Input Layout:** -- `W/A/S/D`: Movement (converted to Vec3 direction in InputComponent) -- `Space`: Jump (sets jump_pressed in InputComponent) -- `Shift`: Speed boost (for noclip camera) -- `I`: Toggle mouse capture (lock/unlock cursor) -- `Escape`: Quit game -- `N`: Toggle noclip mode -- Mouse motion: Camera look (yaw/pitch) - -## Rendering Pipeline - -**wgpu Rendering System:** -- Low-res framebuffer (160×120) renders to texture -- Final blit pass upscales framebuffer to window using nearest-neighbor sampling -- Depth buffer for 3D rendering with proper occlusion - -**Terrain Rendering:** -- glTF mesh exported from Blender 5.0 with baked height values in vertices -- No runtime displacement in shader - vertices rendered directly -- Separate terrain pipeline for terrain-specific rendering -- Terrain mesh has heights pre-baked during export for optimal performance - -**Terrain Physics:** -- EXR heightmap files loaded via `exr` crate (single-channel R32Float format) -- Heightmap loaded directly into rapier3d heightfield collider -- No runtime sampling or computation - instant loading -- Both glTF and EXR exported from same Blender terrain, guaranteed to match - -**Lighting Model:** -- Directional light (like Godot's DirectionalLight3D) -- Diffuse + ambient lighting (basic Phong model, no specular) -- Light direction is uniform across entire scene -- No attenuation or distance falloff -- Dithering applied after lighting calculations - -## Migration from Godot - -This project ports the snow_trail Godot project (located at `~/shared/projects/snow_trail`) to pure Rust: - -**What carries over:** -- Snow deformation compute shader logic (GLSL can be reused with minor adjustments) -- Character controller state machine architecture -- Movement physics parameters -- Camera follow behavior - -**What changes:** -- No `Base` pattern → **Pure ECS with EntityHandle + Components** -- No Godot scene tree → **Entity-Component-System architecture** -- No exported properties → Components with data (debug UI planned for future) -- rapier3d RigidBodyHandle in PhysicsComponent instead of Gd -- Manual ground detection instead of CharacterBody3D.is_on_floor() -- **Component storages** (TransformStorage, MeshStorage, etc.) instead of Godot nodes -- **Systems** (player_input_system, state_machine_system, etc.) instead of _process() -- **No `Rc>`** - components are just data in hashmaps -- Event bus implemented from scratch (complementary to systems) -- State machine implemented from scratch (integrates as ECS component) - -## Build Commands - -```bash -cargo build -cargo build --release -cargo check -cargo test -cargo run -cargo fmt -``` - -## Shader Files - -WESL shaders are stored in the `src/shaders/` directory: -- `src/shaders/standard.wesl` - Standard mesh rendering with directional lighting -- `src/shaders/terrain.wesl` - Terrain rendering with shadow mapping (no displacement) -- `src/shaders/blit.wgsl` - Fullscreen blit for upscaling low-res framebuffer - -Shaders are loaded at runtime via `std::fs::read_to_string()`, allowing hot-reloading by restarting the application. - -## Module Structure - -**Core:** -- `main.rs` - SDL3 event loop, game loop orchestration, system execution order -- `entity.rs` - EntityManager for entity lifecycle (spawn/despawn/query) -- `world.rs` - World struct that owns all component storages and EntityManager - -**ECS Components (`components/`):** -- `input.rs` - InputComponent (gameplay commands) -- `mesh.rs` - MeshComponent (mesh + pipeline) -- `movement.rs` - MovementComponent (movement config/state) -- `physics.rs` - PhysicsComponent (rigidbody/collider handles) -- `player_tag.rs` - PlayerTag marker component -- `state_machine.rs` - (empty, StateMachine defined in state.rs) -- Note: Component *storages* are defined in `world.rs`, not in component files - -**ECS Systems (`systems/`):** -- `input.rs` - player_input_system (InputState → InputComponent) -- `state_machine.rs` - state_machine_system (updates all state machines) -- `physics_sync.rs` - physics_sync_system (physics → transforms) -- `render.rs` - render_system (queries entities, generates DrawCalls) - -**Rendering:** -- `render.rs` - wgpu renderer, pipelines, bind groups, DrawCall execution -- `shader.rs` - Standard mesh shader (WESL) with diffuse+ambient lighting -- `terrain.rs` - Terrain entity spawning, glTF loading, EXR heightmap → physics collider -- `postprocess.rs` - Low-res framebuffer and blit shader for upscaling -- `mesh.rs` - Vertex/Mesh structs, plane/cube mesh generation, glTF loading -- `draw.rs` - DrawManager (legacy, kept for compatibility) - -**Game Logic:** -- `player.rs` - Player entity spawning function -- `camera.rs` - 3D camera with rotation and follow behavior -- `movement.rs` - Movement configuration and state structs -- `state.rs` - Generic StateMachine implementation -- `physics.rs` - PhysicsManager singleton (rapier3d world) - -**Utilities:** -- `utility/input.rs` - InputState (raw SDL input handling) -- `utility/time.rs` - Time singleton (game time tracking) -- `utility/transform.rs` - Transform struct (position/rotation/scale data type) - -**Debug:** -- `debug/noclip.rs` - Noclip camera controller for development - -**Other:** -- `event.rs` - Type-safe event bus (complementary to ECS for irregular events) -- `picking.rs` - Ray casting for mouse picking (unused currently) - -## Dependencies Rationale - -- **sdl3**: Windowing, input events, and platform integration -- **wgpu**: Modern GPU API abstraction for rendering (Vulkan/Metal/DX12 backends) -- **pollster**: Simple blocking executor for async wgpu initialization -- **rapier3d**: Fast physics engine with good Rust integration -- **glam**: Fast vector/matrix math library (vec3, mat4, quaternions) -- **nalgebra**: Linear algebra for rapier3d integration (Isometry3 conversions) -- **bytemuck**: Safe byte casting for GPU buffer uploads (Pod/Zeroable for vertex data) -- **anyhow**: Ergonomic error handling -- **gltf**: Loading 3D models in glTF format -- **exr**: Loading EXR heightmap files (single-channel float data for physics colliders) -- **kurbo**: Bezier curve evaluation for movement acceleration curves - -## Technical Notes - -### EXR Heightmap Loading (Physics Only) -When loading EXR files with the `exr` crate for physics colliders: -- Must import traits: `use exr::prelude::{ReadChannels, ReadLayers};` -- Use builder pattern: `.no_deep_data().largest_resolution_level().all_channels().all_layers().all_attributes().from_file(path)` -- Extract float data: `channel.sample_data.values_as_f32().collect()` -- Convert to nalgebra DMatrix for rapier3d heightfield collider -- No GPU texture creation - physics data only - -### Blender Export Workflow -**Using Blender 5.0** for terrain creation and export: -- Export terrain as **glTF** with baked height values in mesh vertices -- Export same terrain as **EXR** heightmap (single-channel R32Float) -- Both files represent the same terrain data, guaranteeing visual/physics sync -- glTF used for rendering (vertices rendered directly, no shader displacement) -- EXR used for physics (loaded into rapier3d heightfield collider) - -### Multiple Render Pipelines -- `Pipeline` enum determines which pipeline to use per DrawCall -- Different pipelines can have different shaders, bind group layouts, uniforms -- Terrain pipeline: shadow-mapped rendering with line hatching shading -- Standard pipeline: basic mesh rendering with diffuse lighting -- Each pipeline writes to its own uniform buffer before rendering - -### ECS Component Storages - -**Pattern:** -All component storages are owned by the `World` struct: - -```rust -pub struct World { - pub entities: EntityManager, - pub transforms: TransformStorage, - pub meshes: MeshStorage, - pub physics: PhysicsStorage, - pub movements: MovementStorage, - pub inputs: InputStorage, - pub player_tags: PlayerTagStorage, - pub state_machines: StateMachineStorage, -} - -pub struct TransformStorage { - pub components: HashMap, -} - -impl TransformStorage { - pub fn insert(&mut self, entity: EntityHandle, component: Transform) { } - pub fn get(&self, entity: EntityHandle) -> Option<&Transform> { } - pub fn with_mut(&mut self, entity: EntityHandle, f: F) -> Option { } - pub fn remove(&mut self, entity: EntityHandle) { } - pub fn all(&self) -> Vec { } -} -``` - -**Key Features:** -- No `Rc>` needed - clean ownership model -- World owns all component data - explicit ownership -- Instance methods instead of static methods -- Systems receive `&mut World` - clear data dependencies -- Easy to test - can create multiple worlds -- Safe lookups return `Option` - -**Example Usage:** -```rust -// Create world and entity -let mut world = World::new(); -let entity = world.spawn(); - -// Insert components via world -world.transforms.insert(entity, Transform::IDENTITY); -world.meshes.insert(entity, MeshComponent { ... }); - -// Query and update via world -world.transforms.with_mut(entity, |transform| { - transform.position += velocity * delta; -}); - -// Systems receive world -pub fn my_system(world: &mut World) { - for entity in world.player_tags.all() { - if let Some(input) = world.inputs.get(entity) { - // Process input... - } - } -} - -// Cleanup -world.despawn(entity); // Removes from all storages -``` - -### State Machine as ECS Component - -StateMachine integrates into ECS as a component for complex entity behaviors: - -**State Machine Pattern:** -- StateMachine owns all states via `HashMap>` -- TypeId-based state identification -- Transition conditions are simple closures (can capture entity ID) -- State callbacks receive `&mut World` and can access any component -- Updated by `state_machine_system()` each frame using safe remove/insert pattern - -**Integration Example:** -```rust -// Create state machine for entity -let mut sm = StateMachine::new(Box::new(IdleState { entity })); -sm.add_state(WalkingState { entity }); - -// Transitions can capture entity for checking -sm.add_transition::(move || { - // Note: transitions run before update, so they don't access world - false // Placeholder - implement proper transition logic -}); - -// Insert into world -world.state_machines.insert(entity, sm); - -// States receive world when updated -impl State for IdleState { - fn on_state_update(&mut self, world: &mut World, delta: f32) { - // States can access any component via world - if let Some(input) = world.inputs.get(self.entity) { - // React to input... - } - } -} -``` - -**State Machine System (Safe Pattern):** -```rust -pub fn state_machine_system(world: &mut World, delta: f32) { - let entities: Vec<_> = world.state_machines.all(); - - for entity in entities { - // Temporarily remove state machine to avoid borrow conflicts - if let Some(mut state_machine) = world.state_machines.components.remove(&entity) { - state_machine.update(world, delta); // States can now safely access world - world.state_machines.components.insert(entity, state_machine); - } - } -} -``` - -### Event System (event.rs) - -**Complementary to ECS:** -- Events handle irregular, one-time occurrences -- Systems handle regular per-frame updates -- Events enable cross-system messaging without tight coupling - -**Event Bus Features:** -- No `Clone` requirement on events (fire-and-forget) -- `FnMut` handlers allow stateful callbacks -- Global `add_listener()` and `emit()` functions -- Handlers can access ECS components directly via storages - -**ECS Integration:** -```rust -#[derive(Debug)] -struct FootstepEvent { position: Vec3, force: f32 } -impl Event for FootstepEvent {} - -// System emits event -pub fn foot_contact_system(world: &World) { - for player in world.player_tags.all() { - if is_on_ground(player) { - let pos = world.transforms.get(player).unwrap().position; - emit(&FootstepEvent { position: pos, force: 10.0 }); - } - } -} - -// Event handler (global listener, not part of World) -add_listener(|event: &FootstepEvent| { - snow_terrain::deform_at_position(event.position, event.force); -}); -``` - -**When to use:** -- ✅ One-time events (collision, death, pickup) -- ✅ Cross-system communication (audio, VFX triggers) -- ✅ Spawning/despawning entities -- ❌ Regular updates (use systems instead) - -### ECS Systems - -**System Execution Order (main.rs game loop):** -```rust -let mut world = World::new(); - -'running: loop { - // 1. SDL Events → InputState - for event in event_pump.poll_iter() { - input_state.handle_event(&event); - } - - // 2. InputState → InputComponent (player_input_system) - player_input_system(&mut world, &input_state); - - // 3. Update state machines (state_machine_system) - state_machine_system(&mut world, delta); - - // 4. Simulate physics (PhysicsManager) - PhysicsManager::physics_step(); - - // 5. Sync physics → transforms (physics_sync_system) - physics_sync_system(&mut world); - - // 6. Render (render_system) - let draw_calls = render_system(&world); - render::render(&camera, &draw_calls, time); - - // 7. Cleanup - input_state.clear_just_pressed(); -} -``` - -**System Patterns:** - -**Query Pattern:** -```rust -pub fn my_system(world: &mut World) { - let entities = world.my_storage.all(); // All entities with this component - - for entity in entities { - world.my_storage.with_mut(entity, |component| { - // Process component - }); - } -} -``` - -**Multi-Component Query:** -```rust -pub fn movement_system(world: &mut World) { - for entity in world.player_tags.all() { - if let Some(input) = world.inputs.get(entity) { - if let Some(movement) = world.movements.get(entity) { - world.transforms.with_mut(entity, |transform| { - // Update position based on input + movement - }); - } - } - } -} -``` - -### Movement System (movement.rs) -Configuration and state for character movement physics: - -**Horizontal Movement:** -- `HorizontalMovementConfig`: Parameters for ground movement (acceleration, damping, speed limits) -- `HorizontalMovementState`: Runtime state (input direction, flooring status, surface normal) -- Uses Bezier curves (kurbo::CubicBez) for smooth acceleration ramps -- Separate damping for walking vs idle states - -**Vertical Movement:** -- `VerticalMovementConfig`: Jump parameters (height, duration, air control) -- `VerticalMovementState`: Jump execution tracking (progress, peak detection, abort state) -- Bezier curves for jump height progression over time -- Peak detection allows early jump termination with smooth falloff - -**Key Features:** -- Physics parameters tuned to match Godot prototype -- Curve-based interpolation for responsive feel -- State tracking for ground detection and jump execution -- Configurable air control and momentum limits -- Integration with Time singleton for execution timing - -**Usage Pattern:** -```rust -let config = HorizontalMovementConfig::new(); -let mut state = HorizontalMovementState::new(); - -state.move_input = Vec3::new(input.x, 0.0, input.z); -state.forward_direction = camera.forward(); -state.is_floored = ground_check.is_grounded; - -// Apply movement physics using config + state -``` - -### Time System (utility/time.rs) -Global game time tracking using OnceLock singleton: - -**Implementation:** -```rust -static GAME_START: OnceLock = OnceLock::new(); - -pub struct Time; -impl Time { - pub fn init() { GAME_START.get_or_init(Instant::now); } - pub fn get_time_elapsed() -> f32 { /* ... */ } -} -``` - -**Key Features:** -- Thread-safe singleton using std::sync::OnceLock -- Single initialization point (call Time::init() at startup) -- Returns elapsed time as f32 seconds -- Used for animation, jump timing, and time-based effects -- Zero-cost after initialization (static lookup) - -**Usage:** -```rust -Time::init(); // In main() before game loop - -let time = Time::get_time_elapsed(); // Anywhere in code -``` +## Architecture +Pure ECS: entities are IDs, components are plain data in `HashMap` storages, systems are functions receiving `&mut World`. No `Rc>`.