# 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. ## 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 - WGSL 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 - Bayer 8×8 dithering for 3-bit RGB color (8 colors total) - 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 - Bayer 8×8 dithering reduces colors to 3-bit RGB (8 colors) - Final blit pass upscales framebuffer to window using nearest-neighbor sampling - Depth buffer for 3D rendering with proper occlusion **Terrain Height Deformation:** - EXR heightmap files loaded via `exr` crate (single-channel R32Float format) - Height displacement applied in vertex shader - Separate terrain pipeline with texture sampling in vertex stage - TerrainUniforms includes height_scale parameter for tweaking displacement strength - R32Float textures require non-filterable samplers (FilterMode::Nearest) **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 WGSL shaders are stored in the `shaders/` directory: - `shaders/standard.wgsl` - Standard mesh rendering with directional lighting - `shaders/terrain.wgsl` - Terrain rendering with height displacement - `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 (WGSL) with diffuse+ambient lighting - `terrain.rs` - Terrain mesh generation and pipeline creation - `postprocess.rs` - Low-res framebuffer and blit shader for upscaling - `mesh.rs` - Vertex/Mesh structs, plane/cube mesh generation, glTF loading - `heightmap.rs` - EXR heightmap loading using `exr` crate - `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) - **image**: Image loading and processing (includes EXR support) - **half**: Float16 support (dependency of exr) - **kurbo**: Bezier curve evaluation for movement acceleration curves ## Technical Notes ### EXR Heightmap Loading When loading EXR files with the `exr` crate: - 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()` - Create R32Float texture for height data - R32Float is non-filterable, requires `FilterMode::Nearest` sampler ### wgpu Texture Formats - R32Float = single-channel 32-bit float, **non-filterable** - Use `TextureSampleType::Float { filterable: false }` in bind group layout - Use `SamplerBindingType::NonFiltering` for sampler binding - Attempting linear filtering on R32Float causes validation errors ### Multiple Render Pipelines - `Pipeline` enum determines which pipeline to use per DrawCall - Different pipelines can have different shaders, bind group layouts, uniforms - Terrain pipeline: includes height texture binding in vertex stage - Standard pipeline: basic mesh rendering without height displacement - 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 ``` ## Current Implementation Status ### Implemented Features **ECS Architecture:** - ✅ Full ECS conversion completed - ✅ Entity system with EntityManager (spawn/despawn/query) - ✅ Component storages (Transform, Mesh, Physics, Movement, Input, PlayerTag, StateMachine) - ✅ Systems pipeline (input → state machine → physics → physics sync → render) - ✅ No `Rc>` - clean component ownership - ✅ Event bus integrated as complementary to systems **Core Rendering:** - ✅ wgpu renderer with Vulkan backend - ✅ Low-res framebuffer (160×120) with Bayer dithering - ✅ Multiple render pipelines (standard mesh + terrain) - ✅ Directional lighting with diffuse + ambient - ✅ EXR heightmap loading and terrain displacement - ✅ glTF mesh loading - ✅ render_system (ECS-based DrawCall generation) **Input System:** - ✅ Two-layer input pipeline (InputState → InputComponent) - ✅ player_input_system converts raw input to gameplay commands - ✅ SDL event handling in InputState - ✅ Per-entity InputComponent for controllable entities **Camera & Debug:** - ✅ 3D camera with rotation (yaw/pitch) - ✅ Noclip mode for development (in debug/noclip.rs) - ✅ Mouse look with relative mouse mode - ✅ Toggle with 'I' key, 'N' for noclip mode **Physics:** - ✅ rapier3d integration with PhysicsManager singleton - ✅ PhysicsComponent storage (rigidbody/collider handles) - ✅ physics_sync_system (syncs physics → transforms) - ✅ Physics step integrated into game loop - ⚠️ Ground detection not yet implemented - ⚠️ Movement physics not yet connected **State Machines:** - ✅ Generic StateMachine implementation - ✅ StateMachineStorage (ECS component) - ✅ state_machine_system updates all state machines - ✅ Transitions can query ECS components - ⚠️ Player state transitions not yet configured **Player:** - ✅ Player entity spawning function - ✅ Components: Transform, Mesh, Physics, Movement, Input, PlayerTag - ⚠️ Movement system not yet implemented - ⚠️ State machine not yet attached to player - ⚠️ Currently inactive (noclip camera used instead) **Movement Configuration:** - ✅ Horizontal movement config (Bezier acceleration curves) - ✅ Vertical movement config (jump mechanics) - ✅ MovementComponent storage - ⚠️ Movement system not yet implemented - ⚠️ Not yet integrated with physics ### Not Yet Implemented - ❌ Movement system (apply InputComponent → physics velocities) - ❌ Ground detection and collision response - ❌ Player state machine configuration - ❌ Camera follow behavior (tracks player entity) - ❌ Snow deformation compute shaders - ❌ Debug UI system ### Current Focus **ECS migration is complete!** The architecture is now fully entity-component-system based with clean separation of data and logic. The next steps are: 1. Implement movement_system to apply InputComponent to physics 2. Configure player state machine transitions 3. Implement ground detection 4. Add camera follow system 5. Integrate snow deformation The noclip camera mode serves as the primary navigation method for testing. Press 'N' to toggle noclip mode.