simplified CLAUDE.md

This commit is contained in:
Jonas H
2026-03-03 19:32:01 +01:00
parent 0e5d99d31c
commit 53a8276a3c

579
CLAUDE.md
View File

@@ -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<EntityHandle, ComponentData>`
- No `Rc<RefCell<>>` - 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<RefCell<>>` 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<Node3D>` 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<RigidBody3D>
- 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<RefCell<>>`** - 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<EntityHandle, Transform>,
}
impl TransformStorage {
pub fn insert(&mut self, entity: EntityHandle, component: Transform) { }
pub fn get(&self, entity: EntityHandle) -> Option<&Transform> { }
pub fn with_mut<F, R>(&mut self, entity: EntityHandle, f: F) -> Option<R> { }
pub fn remove(&mut self, entity: EntityHandle) { }
pub fn all(&self) -> Vec<EntityHandle> { }
}
```
**Key Features:**
- No `Rc<RefCell<>>` 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, Box<dyn State>>`
- 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::<IdleState, WalkingState>(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<Instant> = 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<EntityHandle, T>` storages, systems are functions receiving `&mut World`. No `Rc<RefCell<>>`.