panopticon skill
This commit is contained in:
24
.pi/skills/panopticon/SKILL.md
Normal file
24
.pi/skills/panopticon/SKILL.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: panopticon
|
||||
description: >-
|
||||
Auto-generated project overview for snow_trail. Structure, conventions,
|
||||
and recent activity. Updated nightly by Panopticon.
|
||||
---
|
||||
|
||||
# snow_trail — Project Overview
|
||||
|
||||
Pure Rust retro-aesthetic 3D game using SDL3 windowing, wgpu rendering, and rapier3d physics. Implements a pure ECS architecture with intent-based cross-system communication, state machines via TypeId polymorphism, and compute-shader snow deformation. Content authored in Blender 5.0 (glTF meshes + EXR heightmaps).
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- **Language:** Rust (stable)
|
||||
- **Key dependencies:** sdl3, wgpu, rapier3d, glam, bladeink
|
||||
- **Build:** `cargo build --release`
|
||||
- **Test:** `cargo test`
|
||||
- **Entry point:** `src/main.rs::main()`
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Structure](structure.md) — modules, types, data flow, dependencies
|
||||
- [Guide](guide.md) — conventions, patterns, anti-patterns, testing
|
||||
- [Changelog](changelog.md) — recent changes, active areas, stability
|
||||
83
.pi/skills/panopticon/changelog.md
Normal file
83
.pi/skills/panopticon/changelog.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Changelog
|
||||
|
||||
*Last updated: Friday, April 3, 2026*
|
||||
|
||||
## Active Areas
|
||||
|
||||
| Area | Changes (30d) | Status |
|
||||
|------|---|---|
|
||||
| Main loop & systems | 9 churn | Active — intent-based architecture solidifying |
|
||||
| Dialog system | 10 churn | Active — projectiles, camera, rendering |
|
||||
| Render core | 15 churn | Active — text pipeline, font atlas, globals |
|
||||
| World state | 8 churn | Active — intent storage, component management |
|
||||
| Player mechanics | 6 churn | Stable — character bundle, player states |
|
||||
| Shader system | 4 churn | Active — gizmo lines, dissolve, snow light |
|
||||
|
||||
## Recent Changes
|
||||
|
||||
### Intent-Based Architecture
|
||||
- **Completed major refactor** (3 weeks ago): Systems now communicate through one-frame `Storage<T>` intents instead of direct function calls.
|
||||
- **Intent types**: `FollowPlayerIntent`, `StopFollowingIntent`, `CameraTransitionIntent`, `SpawnParticleIntent`
|
||||
- **Consumer pattern**: Systems read intents from world storage, act on them, then remove them. Producers don't import consumer modules.
|
||||
- **Documented pattern** in `docs/intent-based-architecture.md` with full execution order contract.
|
||||
|
||||
### Dialog System Suite
|
||||
- **Dialog state machine**: Ink-based story progression with display timer, projectile phases, parry mechanics.
|
||||
- **Projectile system**: Spawns physics-driven projectiles with parry windows; detects hits vs. evasion vs. parries (I/J/L buttons).
|
||||
- **Dialog camera**: Auto-frames all dialog participants; transitions smoothly between follow-cam and dialog-cam via intent; computes centroid + spread.
|
||||
- **Bubble rendering**: Billboard-based text with word-wrap, dynamic sizing (tail height → body dimensions), border/fill colors, RGBA text atlas.
|
||||
|
||||
### Text & Font Rendering
|
||||
- **Font atlas pipeline**: Pre-rasterized glyphs (16×8 grid, 124px cells) from Departure Mono font.
|
||||
- **Text measurement**: Character-width aware layout with line wrapping against max bubble width.
|
||||
- **Vertex-based text**: Custom `TextVertex` (position, UV) fed into dedicated text pipeline with view-proj uniform.
|
||||
|
||||
### Particle System
|
||||
- **Intent-driven spawning**: `SpawnParticleIntent` queues particle emissions; `particle_intent_system` transfers into persistent `ParticleBuffers`.
|
||||
- **Configurable emitters**: Burst count, lifetime range, speed range, directional spread (cone), gravity, size range, color gradient.
|
||||
- **Snow particle integration**: Spawned continuously at camera position for screen-fill effect.
|
||||
|
||||
### Render Pipeline
|
||||
- **Global renderer via thread-local**: `render::global` module centralizes `Device`/`Queue` access for loaders and systems; long-term goal is explicit `RenderContext` params.
|
||||
- **Gizmo shader**: Fragment shader outputs world-normal as axis color (X=red, Y=green, Z=blue) for entity transform editing.
|
||||
- **Snow light accumulation**: Persistent texture tracking deformation; blue-noise dithering maps snow brightness into tile pattern (brightness > threshold → dither step).
|
||||
- **Dissolve shader**: Blue-noise masked alpha cutoff; controlled by `enable_dissolve` uniform.
|
||||
|
||||
### Tree Occlusion & Instances
|
||||
- **Tree dissolve system**: Per-instance dissolve amounts lerp toward targets at configurable speed.
|
||||
- **Occlusion detection**: Instances between camera and player dissolve based on perpendicular distance to camera-to-player ray.
|
||||
- **Instance buffer sync**: Tree instances updated into GPU buffer after dissolve state changes.
|
||||
|
||||
### Trigger & Event Flow
|
||||
- **Sphere-based triggers**: Continuous state tracking (Idle ↔ Inside); fires `TriggerEvent` with `Entered`/`Exited` kinds.
|
||||
- **Activation filtering**: `TriggerFilter::Player` specifies which entity types can activate.
|
||||
- **Event queue**: Events collected in `world.trigger_events` and consumed by dialog system to spawn bubbles.
|
||||
|
||||
### Player State Machine
|
||||
- **State hierarchy**: Idle, Walking, Jumping, Falling, Leaping (dash), Rolling.
|
||||
- **Transition logic**: Grounded checks, input presence, airtime duration.
|
||||
- **Physics per-state**: Damping on enter (Idle), horizontal input application (Walking), vertical velocity control (Jumping).
|
||||
|
||||
### Editor & Inspector
|
||||
- **Dear ImGui integration**: Inspector window with player state display, entity transform gizmos.
|
||||
- **Entity selection**: Right-click picking; gizmo renders for selected entity when editor active.
|
||||
- **UI mode gating**: Systems check `editor.wants_keyboard()`/`wants_mouse()` to suppress game input during inspector focus.
|
||||
|
||||
## Stability
|
||||
|
||||
| Area | Last Changed | Status |
|
||||
|---|---|---|
|
||||
| Physics manager | 80+ days | Stable |
|
||||
| Camera follow | 30 days | Stable — intent system reduced coupling |
|
||||
| Movement component | 30+ days | Stable |
|
||||
| Mesh loader | 30+ days | Stable |
|
||||
| Spotlight sync | 30 days | Stable |
|
||||
| State machine physics | 30 days | Stable — core transitions unchanged |
|
||||
| Debug colliders | 30+ days | Stable |
|
||||
|
||||
## Open Threads
|
||||
|
||||
- **Global renderer thread-local**: Marked for gradual migration to explicit `RenderContext` parameters on systems/loaders. Current implementation works; refactor is low-priority.
|
||||
- **Text rendering completeness**: Font atlas + text pipeline in place; scaling and color support functional. No known issues.
|
||||
- **Dialog parry mechanics**: Projectile system complete; integration with Ink outcomes working. Gameplay tuning (PARRY_WINDOW_RADIUS, HIT_RADIUS) ongoing.
|
||||
- **Tree occlusion performance**: Occlusion system runs every frame; no visibility culling yet. Scalability unknown for >100 trees.
|
||||
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
|
||||
163
.pi/skills/panopticon/structure.md
Normal file
163
.pi/skills/panopticon/structure.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Structure
|
||||
|
||||
## Modules
|
||||
|
||||
### `src/entity.rs`
|
||||
Manages entity lifecycle with `EntityHandle` (u64 alias) and `EntityManager` tracking alive entities. Entities are opaque IDs; all data lives in component storages on `World`.
|
||||
|
||||
### `src/world.rs`
|
||||
Core ECS container holding `EntityManager`, 20+ typed `Storage<T>` collections (one per component type), and intent queues (`follow_player_intents`, `stop_following_intents`, `camera_transition_intents`). One-frame intents are inserted and consumed within a single frame. World also holds singleton state: `snow_layer`, `debug_mode`, `gizmo_mesh`.
|
||||
|
||||
### `src/components/`
|
||||
Component types define entity data. Key storages:
|
||||
- **Transforms** (`Transform`): position, rotation, scale
|
||||
- **Physics** (`PhysicsComponent`): rapier3d rigidbody/collider handles
|
||||
- **Movement** (`MovementComponent`): walking speed, acceleration, damping state
|
||||
- **Jump** (`JumpComponent`): jump height, air control, jump curve
|
||||
- **State machines** (stored per-entity): `IdleState`, `WalkingState`, `JumpingState`, `FallingState`, `LeapingState`, `RollingState`
|
||||
- **Input** (`InputComponent`): current move direction, jump/parry key states
|
||||
- **Camera** (`CameraComponent`): FOV, aspect, yaw/pitch; `CameraTransition` for animated transitions
|
||||
- **Dialog** (`DialogBubbleComponent`, `DialogProjectileComponent`, `DialogSourceComponent`): Ink story state, projectile tracking, outcome events
|
||||
- **Rendering** (`MeshComponent`): mesh reference, pipeline, instance buffer, dissolve/snow-light flags
|
||||
- **Misc**: `FollowComponent`, `RotateComponent`, `DissolveComponent`, `TriggerComponent`, `ParticleEmitterConfig`
|
||||
|
||||
### `src/states/state.rs`
|
||||
Per-entity state machine: `StateMachine` holds current `TypeId`, registered state types, and transitions. `State` trait defines lifecycle (`on_enter`, `on_physics_update`, `on_exit`). Transitions are condition predicates checked each update. Player entity uses this for locomotion states.
|
||||
|
||||
### `src/systems/`
|
||||
Flat list of update functions called in main loop. No cross-system coupling; all communication via world state and intents.
|
||||
- **Camera**: `camera_input_system` → generates intents; `camera_intent_system` consumes them; `camera_follow_system` (follows player), `camera_noclip_system`, `camera_transition_system`, `camera_ground_clamp_system`
|
||||
- **Input**: `player_input_system` reads SDL3 `InputState`, writes `InputComponent`
|
||||
- **Physics**: `state_machine_physics_system` (fixed-step), `PhysicsManager::physics_step()`, `physics_sync_system` (copies rapier bodies back to transforms), `trigger_system` (AABB overlap → events)
|
||||
- **Dialog**: `dialog_system` (ticks story state), `dialog_projectile_system` (moves projectiles), `dialog_camera_system` (focuses camera on speaker), `dialog_bubble_render_system` (generates billboard/text draw calls)
|
||||
- **Rendering**: `render_system` collects `DrawCall` from meshes/transforms; `spotlight_sync_system` syncs light positions to shader uniform
|
||||
- **State machine**: `state_machine_system` (per-frame), `state_machine_physics_system` (fixed-step) tick state lifecycle
|
||||
- **Trees**: `tree_occlusion_system` (culls trees behind camera), `tree_dissolve_update_system` (animates dissolve), `tree_instance_buffer_update_system` (writes GPU buffer)
|
||||
- **Snow**: `snow_system` deforms snow layer at physics contacts; `particle_intent_system`/`particle_update_system` manage particle emitters
|
||||
- **Rotate**: `rotate_system` rotates entities by delta
|
||||
|
||||
### `src/bundles/`
|
||||
Factory functions to spawn pre-configured entity groups:
|
||||
- `PlayerBundle`: player character with all locomotion components
|
||||
- `TestCharBundle`: test NPC
|
||||
- `CameraBundle`: camera entity
|
||||
- `TerrainBundle`: terrain mesh + collider
|
||||
- `SpotlightBundle` + `spawn_spotlights()`: light entities from scene data
|
||||
|
||||
### `src/render/`
|
||||
GPU rendering pipeline via wgpu. Singleton `Renderer` stored in thread-local (global.rs).
|
||||
- **`Renderer`**: wgpu device/queue/surface, framebuffer, pipelines, bind groups, shadow map texture, spotlight data
|
||||
- **`DrawCall`** (types.rs): vertex/index buffers, model matrix, pipeline enum, instance buffer, entity ref
|
||||
- **Pipelines** (pipeline.rs): `create_main_pipeline()`, `create_snow_clipmap_pipeline()`, `create_wireframe_pipeline()`, `create_shadow_pipeline()`, `create_debug_lines_pipeline()`
|
||||
- **`Uniforms`** (types.rs): model/view/projection, 4 spotlights, camera position, height scale, player position, time, tile scale, debug mode
|
||||
- **Shadow**: `render_shadow_pass()` renders scene depth to shadow map from each spotlight
|
||||
- **Snow**: `SnowLayer` deforms snow heightfield via compute; `ClipmapConfig` manages multi-level clipmap grid; `deform_at_position()` marks terrain changed
|
||||
- **Snow light**: `SnowLightAccumulation` ping-pong texture accumulates light contributions from spotlights onto snow surface
|
||||
- **Billboard pipeline** + **Text pipeline**: render dialog bubbles and text overlays
|
||||
- **Particle pipeline**: billboarded particles with per-instance color/velocity
|
||||
- **Font atlas**: pre-rasterized glyph texture from embedded font file
|
||||
|
||||
### `src/loaders/`
|
||||
Load scene data from glTF files:
|
||||
- **`scene.rs`** `Space::load_space()`: loads meshes, lights, player spawn, test char spawn from single glTF
|
||||
- **`mesh.rs`** `Mesh::load_gltf_with_instances()`: parses glTF buffers, vertex/index data, multi-instance mesh batches; `InstanceData` (position/rotation/scale/dissolve); `Vertex` (position/normal/UV)
|
||||
- **`lights.rs`**: extracts spotlight transforms/params from glTF nodes
|
||||
- **`empty.rs`**: extracts named empty (spawn point) transforms from glTF
|
||||
- **`heightmap.rs`**: loads EXR heightfield texture for terrain collision
|
||||
- **`terrain.rs`**: builds rapier heightfield collider from EXR matrix
|
||||
|
||||
### `src/physics.rs`
|
||||
Thread-local `PhysicsManager` wrapping rapier3d. `physics_step()` runs one integration step; `add_rigidbody()`, `add_collider()` register bodies; `raycast()` queries. `HeightfieldData` caches terrain height matrix.
|
||||
|
||||
### `src/utility/`
|
||||
- **`transform.rs`** `Transform`: matrix conversions to/from nalgebra `Isometry3`; getters/setters for position/rotation/scale
|
||||
- **`input.rs`** `InputState`: SDL3 key states (WASD, Space, Shift, Ctrl) and mouse delta; `handle_event()` updates state; `clear_just_pressed()` resets one-frame flags
|
||||
- **`time.rs`**: `Time::get_time_elapsed()` returns seconds since init (static Instant)
|
||||
|
||||
### `src/debug/`
|
||||
- **`mode.rs`** `DebugMode` enum: None, Normals, UV, Depth, Wireframe, Colliders, ShadowMap, SnowLight; `cycle()` steps through
|
||||
- **`collider_debug.rs`**: renders rapier collider AABBs as line meshes
|
||||
- **`gizmo.rs`**: renders 3D transform gizmo (position/rotation/scale) for editor
|
||||
|
||||
### `src/editor/`
|
||||
- **`inspector.rs`** `Inspector`: wraps Dear ImGui context; `render()` draws frame to texture; `build_ui()` draws entity inspector panels
|
||||
- **`mod.rs`** `EditorState`: manages editor active state, selected entity, mouse capture; `editor_loop()` calls inspector, handles picking
|
||||
|
||||
### `src/picking.rs`, `src/postprocess.rs`, `src/texture.rs`, `src/paths.rs`
|
||||
Utility modules: ray casting for mouse pick, fullscreen blit/framebuffer downsampling, dither/flowmap texture loading, paths to asset files.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. **Initialization** (`main.rs` `init()`): SDL3 window → wgpu renderer (Vulkan) → World creation → load scene (Space from glTF) → spawn bundles (player, terrain, camera, lights) → initialize physics
|
||||
2. **Main loop** (`main.rs` `main()` with 60 Hz fixed physics + variable-rate graphics):
|
||||
- **Per-frame** (delta): Input events → `player_input_system` (fills `InputComponent`) + `camera_input_system` (generates intents)
|
||||
- **Intent processing**: `camera_intent_system` consumes intents, updates `CameraComponent`/`CameraTransition`
|
||||
- **Camera systems**: follow player, noclip, transition, clamp to ground
|
||||
- **Fixed physics** (1/60s accumulator):
|
||||
- `state_machine_physics_system`: tick state's `on_physics_update()`
|
||||
- `PhysicsManager::physics_step()`: rapier integration
|
||||
- `physics_sync_system`: copy rigidbody poses to `Transform`
|
||||
- `trigger_system`: detect collisions, emit events
|
||||
- `dialog_system`, `dialog_projectile_system`: Ink story tick, projectile movement
|
||||
- **Per-frame systems**: `state_machine_system` (non-physics update), rotate, particle, tree dissolve, snow deformation, spotlight sync
|
||||
- **Render collection**: `render_system` → `Vec<DrawCall>` from all meshes; snow layer adds clipmap draw calls; debug adds collider/gizmo calls
|
||||
- **Submission**: `submit_frame()` renders draw calls, dialog bubbles/text, particles to framebuffer; blit to screen; optional ImGui overlay
|
||||
3. **Frame cleanup**: `InputState::clear_just_pressed()` (flags reset for next frame)
|
||||
|
||||
## Key Types
|
||||
|
||||
| Type | Module | Description |
|
||||
|------|--------|-------------|
|
||||
| `EntityHandle` | entity | u64 opaque entity ID |
|
||||
| `Storage<T>` | world | HashMap storage for per-entity component data |
|
||||
| `World` | world | ECS container: entities, 20+ storages, intent queues, singleton state |
|
||||
| `Transform` | utility/transform | Position (Vec3), rotation (Quat), scale (Vec3); matrix conversions |
|
||||
| `StateMachine` | states/state | Per-entity state machine: current state TypeId, registered states, transitions |
|
||||
| `State` trait | states/state | Lifecycle: `on_enter`, `on_physics_update`, `on_exit`, `on_update` |
|
||||
| `PhysicsComponent` | components/physics | Rapier3d rigidbody + optional collider handles |
|
||||
| `MovementComponent` | components/movement | Walking speed, acceleration, damping, context (floored, last floor time) |
|
||||
| `JumpComponent` | components/jump | Jump height, duration, air control, context (in progress, origin height) |
|
||||
| `CameraComponent` | components/camera | FOV, aspect, yaw/pitch angles, is_active flag |
|
||||
| `InputComponent` | components/input | Current frame: move direction, jump/parry key states (flags) |
|
||||
| `MeshComponent` | components/mesh | Mesh ref, pipeline enum, instance buffer, dissolve/snow-light flags |
|
||||
| `DialogBubbleComponent` | components/dialog | Ink story, current text, dialog phase (displaying/projectile in flight), parry button |
|
||||
| `DrawCall` | render/types | GPU command: vertex/index buffers, model matrix, pipeline, instance count, entity ID |
|
||||
| `Uniforms` | render/types | Per-frame shader data: matrices, spotlight array, debug flags |
|
||||
| `Renderer` | render/mod | GPU state: device, queue, surface, all pipelines, framebuffer, texture samplers |
|
||||
| `Space` | loaders/scene | Loaded scene: mesh batches, spotlight data, spawn positions |
|
||||
| `Mesh` | loaders/mesh | GPU vertex/index buffers, AABB, CPU vertex data for physics |
|
||||
| `SnowLayer` | render/snow | Snow heightfield: deform bind groups, depth texture, clipmap grid levels |
|
||||
| `SnowLightAccumulation` | render/snow_light | Ping-pong textures + pipeline accumulating spotlight contributions onto snow |
|
||||
| `InputState` | utility/input | SDL3 event state: key flags, mouse delta, relative mode |
|
||||
| `PhysicsManager` | physics | Rapier3d bodies/colliders/pipeline; thread-local singleton |
|
||||
| `DebugMode` | debug/mode | Enum: None, Normals, UV, Depth, Wireframe, Colliders, ShadowMap, SnowLight |
|
||||
|
||||
## Entry Points
|
||||
|
||||
1. **`main()` in src/main.rs**
|
||||
- Calls `init()` → initializes SDL3, wgpu, loads world from scene glTF, spawns entities
|
||||
- Runs infinite loop: event poll → systems (camera/input/physics/render) → frame submit → sleep to 60 Hz
|
||||
2. **`Game::init()` in src/main.rs**
|
||||
- SDL3 window + Vulkan surface
|
||||
- `Renderer::new()` initializes all GPU pipelines and textures
|
||||
- `init_world()` loads space, spawns bundles, sets up terrain/snow/lights
|
||||
3. **`World::new()` in src/world.rs**
|
||||
- Creates empty storages for all 20+ component types and intent queues
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Engine**: wgpu (GPU), rapier3d (physics), glam (math), nalgebra (nalgebra for physics conversions), SDL3 (windowing/input)
|
||||
- **Content**: bladeink (Ink story scripting), image crate (EXR heightmaps), gltf (scene loading)
|
||||
- **Editor**: Dear ImGui via imgui crate + SDL3 integration
|
||||
- **Internal**: All systems read/write `World`; systems are called in fixed sequence from main loop; no circular dependencies
|
||||
- **Thread-local singletons**: `Renderer` (render/global.rs), `PhysicsManager` (physics.rs), `GLOBAL_RENDERER`, `GLOBAL_PHYSICS`
|
||||
|
||||
## Initialization Order
|
||||
|
||||
1. SDL3 init → window creation → Vulkan adapter/device
|
||||
2. `Renderer::new()` → wgpu device/queue, create all pipelines, load textures (dither, flowmap, font atlas, shadow map, blue noise)
|
||||
3. Load scene from glTF → meshes, lights, spawns
|
||||
4. Spawn bundles: player (with input/movement/jump/state machine), terrain (mesh + heightfield collider), camera (follows player), lights (spotlights)
|
||||
5. Initialize snow layer (deform by tree positions)
|
||||
6. Initialize snow light accumulation (bind to spotlight data)
|
||||
7. Main loop: process events → run systems in sequence → submit frame
|
||||
Reference in New Issue
Block a user