# 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). ## 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 **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 ```