# Guide ## Conventions ### Naming - **Bundles**: `*Bundle` suffix (e.g., `PlayerBundle`, `TerrainBundle`) - **Components**: `*Component` suffix (e.g., `MeshComponent`, `MovementComponent`) - **States**: Plain names without suffix (e.g., `IdleState`, `WalkingState`); impl `State` trait - **Intents**: `*Intent` suffix for one-frame events (e.g., `CameraTransitionIntent`, `FollowPlayerIntent`) - **Systems**: `*_system` suffix for functions (e.g., `player_input_system`, `trigger_system`) - **Storage containers**: lowercase plural (e.g., `world.transforms`, `world.movements`) - **Tags** (marker components): bare unit type in `Storage<()>` (e.g., `player_tags`, `bubble_tags`) ### Formatting - Run `cargo fmt` with `brace_style = "AlwaysNextLine"`, `control_brace_style = "AlwaysNextLine"` - **NO inline comments** unless absolutely necessary—code must be self-documenting - Doc comments (`///`) **only** for public APIs and complex algorithms - **NO inline paths** in code—always use `use` statements at file level - **NO `use` statements inside functions or impl blocks**—all imports at module level ### Imports - Group imports: standard library, external crates, internal modules (in that order) - Use fully-qualified module paths in `use` statements; never nest unnecessarily - Example: ```rust use crate::components::{CameraComponent, FollowComponent}; use crate::world::World; use glam::Vec3; ``` ## Patterns ### Intent-Based Communication (One-Frame Queues) **Why**: Systems don't call each other directly. This decouples producer from consumer and maintains a flat pipeline. **How**: Intent is a simple struct in a queue (`Vec`). Producer inserts, consumer reads and removes. ```rust // components/intent.rs pub struct CameraTransitionIntent { pub duration: f32, } // systems/camera.rs - consumer pub fn camera_intent_system(world: &mut World) { let transition_entities: Vec = world.camera_transition_intents.all(); for entity in transition_entities { let duration = world .camera_transition_intents .get(entity) .map(|i| i.duration) .unwrap_or(0.5); start_camera_transition(world, entity, duration); world.camera_transition_intents.remove(entity); // consumed } } ``` ### Bundle Pattern (Entity Factory) **Why**: Encapsulate all initialization logic for related components into one spawnable unit. ```rust // bundles/player.rs pub struct PlayerBundle { pub position: Vec3, } impl Bundle for PlayerBundle { fn spawn(self, world: &mut World) -> Result { let entity = world.spawn(); // Physics let rigidbody = RigidBodyBuilder::kinematic_position_based() .translation(self.position.into()) .build(); let rigidbody_handle = PhysicsManager::add_rigidbody(rigidbody); world.physics.insert(entity, PhysicsComponent { rigidbody: rigidbody_handle, .. }); // State machine with transitions let mut state_machine = StateMachine::new::(); state_machine.register_state(|w: &mut World| &mut w.falling_states); state_machine.add_transition::(move |world| { is_grounded(world, entity_id) && !has_input(world, entity_id) }); world.state_machines.insert(entity, state_machine); // Tags world.player_tags.insert(entity, ()); Ok(entity) } } ``` ### State Machine with Type-Safe Transitions **Why**: Encapsulate state logic (on_enter, on_exit, physics_update) and guard transitions with closures. ```rust // states/player_states.rs - implement State trait impl State for IdleState { fn tick_time(&mut self, delta: f32) { self.time_in_state += delta; } fn on_enter(&mut self, world: &mut World, entity: EntityHandle) { // Apply damping on enter world.physics.with(entity, |physics| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rb| { let current_velocity = *rb.linvel(); rb.set_linvel(Vector::new(0.0, current_velocity.y, 0.0), true); }); }); } fn on_physics_update(&mut self, world: &mut World, entity: EntityHandle, _delta: f32) { // Ground snapping let current_y = world.physics.with(entity, |p| { PhysicsManager::with_rigidbody_mut(p.rigidbody, |rb| rb.translation().y) }).flatten(); // ... } } // bundles/player.rs - register transitions state_machine.add_transition::(move |world| { world .inputs .with(entity_id, |i| i.move_direction.length() > 0.01) .unwrap_or(false) }); ``` ### Storage.with/with_mut Pattern (Closure-Based Access) **Why**: Avoids holding mutable references across multiple accesses; satisfies borrow checker. ```rust // systems/spotlight_sync.rs pub fn spotlight_sync_system(world: &World) -> Vec { let mut spotlights = Vec::new(); for entity in world.spotlights.all() { // Closure captures immutably—safe to call multiple times if let Some(spotlight_component) = world.spotlights.get(entity) { if let Some(transform) = world.transforms.get(entity) { let position = transform.position + spotlight_component.offset; spotlights.push(Spotlight::new(position, ..)); } } } spotlights } // With mutation world.movements.with_mut(entity, |movement| { movement.movement_context.is_floored = true; }); ``` ### System Function Signature **Why**: Pass only the storages (or immutable `World`) that the system needs. Makes data dependencies explicit. ```rust // Good: explicit dependencies pub fn camera_follow_system(world: &mut World) { let camera_entities: Vec<_> = world.follows.all(); for camera_entity in camera_entities { // Access what's needed if let Some(follow) = world.follows.get(camera_entity) { world.transforms.with_mut(camera_entity, |t| { t.position = target_position + new_offset; }); } } } // For read-only systems pub fn spotlight_sync_system(world: &World) -> Vec { .. } ``` ### Default Trait for Configuration Components **Why**: Sensible defaults allow bundle code to be cleaner; override what's needed. ```rust // components/jump.rs impl Default for JumpComponent { fn default() -> Self { Self { jump_height: 5.0, jump_duration: 0.5, air_control_force: 100.0, max_air_momentum: 3.0, air_damping_active: 0.95, air_damping_passive: 0.9, jump_curve: CubicBez::new((0.0, 0.0), (0.4, 1.0), (0.6, 1.0), (1.0, 0.0)), jump_context: JumpContext::default(), } } } // Usage in bundle world.jumps.insert(entity, JumpComponent::default()); ``` ### Physics Closure Pattern **Why**: Avoids lifetime tangles when borrowing rigidbody from PhysicsManager static storage. ```rust // states/player_states.rs world.physics.with(entity, |physics| { PhysicsManager::with_rigidbody_mut(physics.rigidbody, |rigidbody| { let vel = *rigidbody.linvel(); rigidbody.set_linvel(Vector::new(vel.x, 0.0, vel.z), true); }); }); ``` ## Anti-Patterns ### Direct System-to-System Calls **Don't do this:** ```rust // ❌ Bad: tightly coupled, hard to debug fn system_a(world: &mut World) { system_b_logic(world); // Hidden dependency } ``` **Do this instead:** ```rust // ✅ Good: intent in queue, flat pipeline pub fn system_a(world: &mut World) { world.some_intents.insert(entity, MyIntent { .. }); } pub fn system_b(world: &mut World) { for entity in world.some_intents.all() { // process world.some_intents.remove(entity); } } ``` ### Holding Mutable References Across Multiple Storage Accesses **Don't do this:** ```rust // ❌ Bad: won't compile (borrow checker) let mut movement = world.movements.get_mut(entity).unwrap(); let mut transform = world.transforms.get_mut(entity).unwrap(); movement.foo = transform.position.x; ``` **Do this instead:** ```rust // ✅ Good: use closures world.movements.with_mut(entity, |movement| { world.transforms.with(entity, |transform| { movement.foo = transform.position.x; }); }); ``` ### Inline Paths in Code **Don't do this:** ```rust // ❌ Bad let velocity = *crate::physics::PhysicsManager::with_rigidbody_mut(..); ``` **Do this instead:** ```rust // ✅ Good: use statements at top use crate::physics::PhysicsManager; // ... in code let velocity = *PhysicsManager::with_rigidbody_mut(..); ``` ### Global or Context-Based Component Queries **Don't do this:** ```rust // ❌ Bad: loses intent, repeats queries for entity in world.meshes.all() { if let Some(mesh) = world.meshes.get(entity) { .. } } for entity in world.meshes.all() { if let Some(mesh) = world.meshes.get(entity) { .. } } ``` **Do this instead:** ```rust // ✅ Good: query once, iterate clearly let entities: Vec<_> = world.meshes.all(); for entity in entities { if let Some(mesh_comp) = world.meshes.get(entity) { // process } } ``` ### Overly Generic (World/&mut World) Parameters **Don't do this:** ```rust // ❌ Bad: implicit dependencies fn update_position(world: &mut World, entity: EntityHandle) { // What do we actually need? } ``` **Do this instead:** ```rust // ✅ Good: explicit dependencies fn update_position( transforms: &mut Storage, entity: EntityHandle, ) { transforms.with_mut(entity, |t| t.position.y += 1.0); } ``` ## Testing ### Structure Tests are inline with source (`#[cfg(test)] mod tests`). Focus on: - Component initialization and state transitions - Bundle spawning and validation - Physics helper calculations - Intent queue ordering ### What to Test - **State transitions**: Verify conditions gate transitions correctly ```rust #[test] fn test_idle_to_walking_transition() { let world = setup_test_world(); world.inputs.with_mut(player, |i| i.move_direction = Vec3::X); assert!(should_transition_to_walking(&world, player)); } ``` - **Bundle spawning**: Ensure all components are inserted - **Default values**: Verify sensible defaults in `Default` impls - **Physics calculations**: Unit-test slope calculations, terrain queries ### Running Tests ```bash cargo test cargo test --lib # Unit tests only cargo test -- --nocapture # Show output ``` ## References - See **CLAUDE.md** in project root for authoritative style rules (no inline comments, doc comments for public APIs only) - See **docs/self-gating-systems.md** for detailed intent-based pipeline patterns - See `src/states/player_states.rs` for canonical State impl examples - See `src/bundles/player.rs` for complete Bundle pattern with state machine setup