panopticon skill
This commit is contained in:
354
.pi/skills/panopticon/guide.md
Normal file
354
.pi/skills/panopticon/guide.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# 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<T>`). 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<EntityHandle> = 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<EntityHandle, String> {
|
||||
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::<FallingState>();
|
||||
state_machine.register_state(|w: &mut World| &mut w.falling_states);
|
||||
state_machine.add_transition::<FallingState, IdleState>(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::<IdleState, WalkingState>(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<Spotlight> {
|
||||
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<Spotlight> { .. }
|
||||
```
|
||||
|
||||
### 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<Transform>,
|
||||
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
|
||||
Reference in New Issue
Block a user