agent update
This commit is contained in:
10
.pi/skills/self-gating-systems/SKILL.md
Normal file
10
.pi/skills/self-gating-systems/SKILL.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Intent-Based Architecture Skill
|
||||||
|
|
||||||
|
Read the architecture document at `docs/self-gating-systems.md` (resolve relative to the project root `/home/jonas/projects/snow_trail_sdl/`) and follow its patterns when:
|
||||||
|
|
||||||
|
- Adding cross-system communication (use intents, not direct function calls)
|
||||||
|
- Adding new systems to the main loop
|
||||||
|
- Changing how game modes work (editor, dialog, gameplay)
|
||||||
|
- Working with camera, input, or mode-switching logic
|
||||||
|
|
||||||
|
Key rule: **systems communicate through typed intent data in shared queues** — producers insert intents, consumers process and remove them. No system calls another system's functions directly.
|
||||||
135
docs/self-gating-systems.md
Normal file
135
docs/self-gating-systems.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# Intent-Based Architecture
|
||||||
|
|
||||||
|
Systems communicate through **intents** — one-frame typed data in shared queues — not through direct function calls. This decouples producers from consumers: the system that wants something to happen does not know or care which system processes it.
|
||||||
|
|
||||||
|
## The Pattern
|
||||||
|
|
||||||
|
```
|
||||||
|
Producer World (shared state) Consumer
|
||||||
|
──────── ──────────────────── ────────
|
||||||
|
detects dialog change
|
||||||
|
↓
|
||||||
|
insert(camera, CameraTransitionIntent)
|
||||||
|
↓
|
||||||
|
camera_transition_intents
|
||||||
|
camera_intent_system reads it
|
||||||
|
sets up CameraTransition component
|
||||||
|
removes intent
|
||||||
|
```
|
||||||
|
|
||||||
|
An intent is ephemeral (one frame). The state change it triggers (a component mutation) persists.
|
||||||
|
|
||||||
|
## Intent Types
|
||||||
|
|
||||||
|
Intents are plain structs stored in `Storage<T>` on `World`, keyed by target entity:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// components/intent.rs
|
||||||
|
pub struct FollowPlayerIntent;
|
||||||
|
pub struct StopFollowingIntent;
|
||||||
|
pub struct CameraTransitionIntent { pub duration: f32 }
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// world.rs
|
||||||
|
pub follow_player_intents: Storage<FollowPlayerIntent>,
|
||||||
|
pub stop_following_intents: Storage<StopFollowingIntent>,
|
||||||
|
pub camera_transition_intents: Storage<CameraTransitionIntent>,
|
||||||
|
```
|
||||||
|
|
||||||
|
Adding a new intent = one struct + one storage field. Nothing else changes.
|
||||||
|
|
||||||
|
## Producing Intents
|
||||||
|
|
||||||
|
Any code with access to the storage can submit. The entity is the target:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Event handler wants camera to follow player — doesn't call camera functions
|
||||||
|
world.follow_player_intents.insert(camera_entity, FollowPlayerIntent);
|
||||||
|
|
||||||
|
// Dialog system detects state change — doesn't know how transitions work
|
||||||
|
world.camera_transition_intents.insert(camera_entity, CameraTransitionIntent { duration: 0.8 });
|
||||||
|
```
|
||||||
|
|
||||||
|
Producers don't import consumer modules. They only know about the intent type and the storage.
|
||||||
|
|
||||||
|
## Consuming Intents
|
||||||
|
|
||||||
|
A consumer system reads, acts, and removes:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn camera_intent_system(world: &mut World) {
|
||||||
|
// 1. Read
|
||||||
|
let follow_entities = world.follow_player_intents.all();
|
||||||
|
for entity in follow_entities {
|
||||||
|
// 2. Act — all the follow setup logic lives here
|
||||||
|
start_camera_following(world, entity);
|
||||||
|
// 3. Remove (consume)
|
||||||
|
world.follow_player_intents.remove(entity);
|
||||||
|
}
|
||||||
|
// ... same for stop_following_intents, camera_transition_intents
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The consumer owns the implementation. `start_camera_following` is a private function inside the camera module — no other module can call it directly.
|
||||||
|
|
||||||
|
## Why This Matters
|
||||||
|
|
||||||
|
**Decoupling.** The dialog system doesn't import camera functions. It inserts a `CameraTransitionIntent`. If the camera system changes how transitions work, the dialog system is unaffected.
|
||||||
|
|
||||||
|
**Multiple producers, same path.** Editor toggle, dialog state changes, and future cutscene systems all produce the same `CameraTransitionIntent`. They all go through the same processing. No special cases.
|
||||||
|
|
||||||
|
**Testability.** To test camera transitions: insert a `CameraTransitionIntent`, call `camera_intent_system`, check the result. No need to simulate dialog state or editor toggles.
|
||||||
|
|
||||||
|
**Additive behavior.** To add a new reaction to `StopFollowingIntent` (e.g., play a sound), write a new system that reads the intent before the camera system consumes it. No existing code changes.
|
||||||
|
|
||||||
|
## Execution Order
|
||||||
|
|
||||||
|
The main loop is a flat pipeline. Order encodes causality:
|
||||||
|
|
||||||
|
```
|
||||||
|
Input + intent generation (camera_input, player_input, dialog_transition_detect)
|
||||||
|
↓
|
||||||
|
Intent processing (camera_intent_system)
|
||||||
|
↓
|
||||||
|
Camera behavior (noclip, dialog_camera, follow, transition, ground_clamp)
|
||||||
|
↓
|
||||||
|
Editor overlay (UI only — not a game system)
|
||||||
|
↓
|
||||||
|
Fixed-step physics (state_machine, physics, triggers, dialog)
|
||||||
|
↓
|
||||||
|
Per-frame systems (state_machine, rotate, trees, spotlights, snow)
|
||||||
|
↓
|
||||||
|
Render
|
||||||
|
```
|
||||||
|
|
||||||
|
Moving a system changes the data flow. The order is the contract.
|
||||||
|
|
||||||
|
## Systems Also Self-Gate
|
||||||
|
|
||||||
|
Because intents express what should happen, systems naturally have nothing to do when their data is absent:
|
||||||
|
|
||||||
|
- `camera_follow_system` — no `FollowComponent` = no work
|
||||||
|
- `dialog_camera_system` — no active bubbles = no work
|
||||||
|
- `player_input_system` — camera not following = no player input
|
||||||
|
- `camera_noclip_system` — camera has `FollowComponent` = skip
|
||||||
|
|
||||||
|
The main loop doesn't branch on mode flags. Systems check their own data.
|
||||||
|
|
||||||
|
## When to Use Intents vs. Direct Mutation
|
||||||
|
|
||||||
|
| Situation | Approach |
|
||||||
|
|---|---|
|
||||||
|
| One system wants another to do something | Intent |
|
||||||
|
| A system updating its own components | Direct mutation |
|
||||||
|
| Per-frame continuous computation | Components + tick system |
|
||||||
|
| Persistent state (is the camera following?) | Component (`FollowComponent`) |
|
||||||
|
| One-shot request (start following) | Intent (`FollowPlayerIntent`) |
|
||||||
|
|
||||||
|
## Adding New Intents
|
||||||
|
|
||||||
|
1. Define the struct in `components/intent.rs`
|
||||||
|
2. Add a `Storage<T>` field to `World` (+ `new()` + `despawn()`)
|
||||||
|
3. Producers insert into the storage
|
||||||
|
4. A consumer system reads, acts, and removes
|
||||||
|
5. Done — no other code changes
|
||||||
Reference in New Issue
Block a user