diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..0abe598 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,84 @@ +name: Bug Report +description: Report a bug or unexpected behavior +labels: ["bug"] +type: "Task" +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report this bug! + + - type: textarea + id: description + attributes: + label: Description + description: Describe the bug using the "In order to" format + placeholder: | + In order to [achieve some goal/benefit], + as a [type of user], + I need [the bug to be fixed/expected behavior]. + validations: + required: true + + - type: textarea + id: current-behavior + attributes: + label: Current Behavior + description: Describe what currently happens using Given/When/Then format + placeholder: | + ```gherkin + Scenario: Bug occurs + Given [initial context/state] + When [action is taken] + Then [unexpected outcome/bug behavior] + ``` + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: Describe what should happen using Given/When/Then format + placeholder: | + ```gherkin + Scenario: Expected behavior + Given [initial context/state] + When [action is taken] + Then [expected outcome] + ``` + validations: + required: true + + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to Reproduce + description: Provide steps to reproduce the bug + placeholder: | + 1. Go to '...' + 2. Run command '...' + 3. See error + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: Provide relevant environment details + placeholder: | + - OS: [e.g., Ubuntu 24.04, macOS 14.0] + - Version: [e.g., v0.5.0] + - Rust version: [e.g., 1.75.0] + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Any additional information, logs, or screenshots + placeholder: Add any other context, error logs, or screenshots here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/epic.yml b/.github/ISSUE_TEMPLATE/epic.yml new file mode 100644 index 0000000..55a3e6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic.yml @@ -0,0 +1,94 @@ +name: Epic +description: Define a large body of work spanning multiple features +labels: ["epic"] +type: "Epic" +body: + - type: markdown + attributes: + value: | + Thanks for creating an epic! + + - type: textarea + id: description + attributes: + label: Description + description: Describe the epic and its business value + placeholder: | + In order to [achieve major goal/strategic benefit], + as [stakeholder/user type], + we need [high-level capability or outcome]. + + **Business Value:** + [Why is this important? What problem does it solve?] + validations: + required: true + + - type: textarea + id: scope + attributes: + label: Scope + description: High-level scope and boundaries of this epic + placeholder: | + **In Scope:** + - [What's included] + - [Major capabilities] + + **Out of Scope:** + - [What's explicitly excluded] + - [Future considerations] + validations: + required: true + + - type: textarea + id: success-criteria + attributes: + label: Success Criteria + description: How will we know this epic is successful? + placeholder: | + ```gherkin + Scenario: Epic is complete + Given [all features are implemented] + When [users interact with the system] + Then [desired outcomes are achieved] + ``` + + **Metrics:** + - [Measurable success metric 1] + - [Measurable success metric 2] + validations: + required: true + + - type: textarea + id: features + attributes: + label: Features & Tasks + description: List of features or tasks that make up this epic + placeholder: | + - [ ] #[issue-number] - [Feature/Task name] + - [ ] #[issue-number] - [Feature/Task name] + - [ ] #[issue-number] - [Feature/Task name] + validations: + required: false + + - type: textarea + id: dependencies + attributes: + label: Dependencies & Risks + description: External dependencies, blockers, or risks + placeholder: | + **Dependencies:** + - [External system/team/resource] + + **Risks:** + - [Potential risk or blocker] + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Any additional information, mockups, or references + placeholder: Add any other context, diagrams, or references here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..17b55be --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,50 @@ +name: Feature +description: Request a new feature or enhancement +labels: ["enhancement"] +type: "Feature" +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to suggest a feature! + + - type: textarea + id: description + attributes: + label: Description + description: Describe the feature using the "In order to" format + placeholder: | + In order to [achieve some goal/benefit], + as a [type of user], + I want [some feature/capability]. + validations: + required: true + + - type: textarea + id: acceptance-criteria + attributes: + label: Acceptance Criteria + description: Define the acceptance criteria using Given/When/Then format + placeholder: | + ```gherkin + Scenario: [Scenario name] + Given [initial context/state] + When [action is taken] + Then [expected outcome] + + Scenario: [Another scenario name] + Given [initial context/state] + When [action is taken] + Then [expected outcome] + ``` + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Any additional information, mockups, or references + placeholder: Add any other context, screenshots, or references here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/support.yml b/.github/ISSUE_TEMPLATE/support.yml new file mode 100644 index 0000000..9c26e50 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support.yml @@ -0,0 +1,92 @@ +name: Support Request +description: Get help with using the application or troubleshooting issues +labels: ["support"] +type: "Support" +body: + - type: markdown + attributes: + value: | + Thanks for reaching out! We'll help you as soon as possible. + + - type: textarea + id: description + attributes: + label: Description + description: Describe what you're trying to do and what help you need + placeholder: | + In order to [achieve some goal], + as a [type of user], + I need help with [specific issue or question]. + validations: + required: true + + - type: textarea + id: current-situation + attributes: + label: Current Situation + description: What's happening now or what you've already tried + placeholder: | + ```gherkin + Scenario: Current situation + Given [what I have/where I am] + When [what I do/try] + Then [what happens] + ``` + + **What I've tried:** + - [Thing 1] + - [Thing 2] + validations: + required: true + + - type: textarea + id: expected-outcome + attributes: + label: Expected Outcome + description: What are you trying to accomplish? + placeholder: | + ```gherkin + Scenario: What I'm trying to achieve + Given [starting point] + When [action I want to take] + Then [desired result] + ``` + validations: + required: true + + - type: dropdown + id: priority + attributes: + label: Priority + description: How urgent is this issue? + options: + - Low - I can work around this + - Medium - This is blocking some work + - High - This is blocking critical work + - Critical - System is down or unusable + default: 0 + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: System information that might be relevant + placeholder: | + - OS: [e.g., Ubuntu 24.04, macOS 14.0, Windows 11] + - Version: [e.g., v0.5.0] + - Browser: [if web-based] + - Other relevant details: [e.g., network setup, deployment environment] + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Logs, screenshots, error messages, or other helpful information + placeholder: | + Add any error messages, logs, screenshots, or other context here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 0000000..3930818 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,57 @@ +name: Task +description: Create a work item or implementation task +labels: ["task"] +type: "Task" +body: + - type: markdown + attributes: + value: | + Thanks for creating a task! + + - type: textarea + id: description + attributes: + label: Description + description: Describe what needs to be done + placeholder: | + In order to [achieve some goal/benefit], + as a [type of user/developer], + I need [specific work to be completed]. + validations: + required: true + + - type: textarea + id: acceptance-criteria + attributes: + label: Acceptance Criteria + description: Define when this task is complete using Given/When/Then format + placeholder: | + ```gherkin + Scenario: Task is complete + Given [initial state] + When [implementation is done] + Then [expected result] + ``` + validations: + required: true + + - type: textarea + id: technical-notes + attributes: + label: Technical Notes + description: Implementation details, dependencies, or technical considerations + placeholder: | + - Dependencies: [any prerequisites or blocking issues] + - Approach: [high-level implementation approach] + - Files affected: [key files or modules to change] + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: Any additional information or references + placeholder: Add any other context here. + validations: + required: false diff --git a/crates/libmarathon/src/platform/desktop/executor.rs b/crates/libmarathon/src/platform/desktop/executor.rs index 811268a..e20653a 100644 --- a/crates/libmarathon/src/platform/desktop/executor.rs +++ b/crates/libmarathon/src/platform/desktop/executor.rs @@ -178,12 +178,13 @@ impl AppHandler { set_scale_factor(scale_factor); // Create window entity with all required components (use logical size) + // Convert physical pixels to logical pixels using proper floating-point division + let logical_width = (physical_size.width as f64 / scale_factor) as f32; + let logical_height = (physical_size.height as f64 / scale_factor) as f32; + let mut window = bevy::window::Window { title: "Marathon".to_string(), - resolution: WindowResolution::new( - physical_size.width / scale_factor as u32, - physical_size.height / scale_factor as u32, - ), + resolution: WindowResolution::new(logical_width, logical_height), mode: WindowMode::Windowed, position: WindowPosition::Automatic, focused: true, diff --git a/crates/libmarathon/src/platform/ios/executor.rs b/crates/libmarathon/src/platform/ios/executor.rs index 7125a98..89fd43d 100644 --- a/crates/libmarathon/src/platform/ios/executor.rs +++ b/crates/libmarathon/src/platform/ios/executor.rs @@ -124,13 +124,14 @@ impl AppHandler { info!("iOS scale factor: {}", scale_factor); // Create window entity with all required components + // Convert physical pixels to logical pixels using proper floating-point division + let logical_width = (physical_size.width as f64 / scale_factor) as f32; + let logical_height = (physical_size.height as f64 / scale_factor) as f32; + let mut window = bevy::window::Window { title: "Marathon".to_string(), - resolution: WindowResolution::new( - physical_size.width / scale_factor as u32, - physical_size.height / scale_factor as u32, - ), - mode: WindowMode::Windowed, + resolution: WindowResolution::new(logical_width, logical_height), + mode: WindowMode::BorderlessFullscreen, position: WindowPosition::Automatic, focused: true, ..Default::default() diff --git a/docs/rfcs/0005-spatial-audio-system.md b/docs/rfcs/0005-spatial-audio-system.md new file mode 100644 index 0000000..3d95852 --- /dev/null +++ b/docs/rfcs/0005-spatial-audio-system.md @@ -0,0 +1,597 @@ +# RFC 0005: Spatial Audio System + +**Status:** Draft +**Authors:** Sienna +**Created:** 2025-12-14 + +## Abstract + +This RFC proposes vendoring Firewheel (audio graph engine) and Steam Audio (spatial simulation) into Marathon's engine layer, providing a bus-based mixing architecture with developer tooling for professional-grade audio mixing. The system handles spatial 3D audio, real-time environmental simulation, and provides egui-based debugging tools for mix engineering. + +## Motivation + +The current engine lacks audio infrastructure entirely. Aspen's design requirements specify professional-grade spatial audio with "hyper-dense" soundscapes, which requires more than bevy_audio's basic playback capabilities. + +### Current Options and Trade-offs + +**bevy_audio** — Bevy's built-in audio system provides basic playback but no spatial simulation, no mixing infrastructure, and no developer tooling. Insufficient for professional audio work. + +**Middleware (FMOD/Wwise)** — Industry-standard solutions with excellent tooling, but introduce external editors, licensing complexity, poor Rust integration, and proprietary formats. The friction of context-switching between middleware and game engine slows iteration. + +**bevy_seedling + bevy_steam_audio** — Rust-native solutions that integrate Firewheel (lock-free audio graph) and Steam Audio (binaural spatial simulation). These are the right foundation, but as external dependencies they lag Bevy version updates and don't integrate with Marathon's conventions. + +### Solution: Vendor and Integrate + +Vendor Firewheel and Steam Audio as first-party Marathon code. This provides: + +1. **Version control** — Update Bevy on our schedule, not waiting for upstream +2. **Simplification** — Strip unused features (LUFS analyzer nodes, input stream handling, complex pool abstractions) +3. **Integration** — Use Marathon's logging, diagnostics, and patterns throughout +4. **Tooling** — Build egui mixer panels and spatial visualization that understand Marathon's conventions + +The maintenance burden (porting upstream improvements manually) is accepted in exchange for control and simplicity. + +### Requirements + +From Aspen's audio direction: + +1. **3D spatial audio** — Sounds positioned in world space, processed through HRTF binaural simulation +2. **Hyper-dense soundscapes** — Hundreds of potential sources with intelligent prioritization and culling +3. **Professional mixing** — Bus-based architecture with EQ, metering, and preset management +4. **Real-time simulation** — Steam Audio for distance attenuation, occlusion, and (future) reverb + +From Marathon's engine requirements: + +5. **First-party code** — Vendored into Marathon, not external dependencies +6. **Developer tooling** — egui mixer panel and spatial debug visualization +7. **Performance** — Handle 64+ simultaneous voices at <2ms audio thread latency + +## Architecture + +The audio system forms a pipeline from game world to speaker output: + +``` +[Game Layer: AudioSource + Transform components] + ↓ +[Bevy Integration: Position sync, asset loading, component lifecycle] + ↓ +[Bus Mixer: SFX, Ambient, Music, UI, Voice → Master] + ↓ +[Firewheel: Lock-free audio graph, real-time thread] + ↓ +[Steam Audio: HRTF binaural, distance attenuation, occlusion] + ↓ +[cpal: Audio I/O] → Speakers/Headphones +``` + +**Game Layer** — Games spawn entities with `AudioSource` and `Transform` components. The engine handles everything else. + +**Bevy Integration** — Synchronizes ECS state with the audio graph. When transforms change, update Steam Audio parameters. When entities spawn, create corresponding Firewheel nodes. + +**Bus Mixer** — Categorical organization (SFX, Ambient, Music, UI, Voice). Each bus has gain, EQ, sends. Master bus applies limiting and metering. + +**Firewheel** — Lock-free audio graph engine running on a dedicated real-time thread. Processes ~512-sample buffers at 48kHz (~10ms per buffer). + +**Steam Audio** — Spatial simulation providing HRTF convolution, distance attenuation, air absorption, and occlusion detection. + +## Audio Source Lifecycle + +When a game spawns an entity with `AudioSource` and `Transform`: + +1. **Detection** — `Added` query in Bevy system detects new entity +2. **Node Creation** — Create Firewheel nodes: sampler → Steam Audio processor → gain → bus input +3. **Graph Connection** — Connect nodes in the audio graph (lock-free operation) +4. **Parameter Sync** — Every frame, `Changed` updates Steam Audio position/direction atomics +5. **Audio Thread Processing** — Real-time thread reads atomics, processes buffers, outputs spatialized audio +6. **Despawn** — `RemovedComponents` disconnects and removes nodes + +The critical design is **two-thread architecture with lock-free communication**: + +- **Game thread (60Hz)** — Updates parameters via atomics, never blocks audio +- **Audio thread (100Hz)** — Reads atomics, processes buffers, never blocks on game state + +The audio thread always has valid parameters (possibly slightly stale). This prevents glitches from game thread hitches. + +## Spatial Audio Processing + +Steam Audio transforms mono sources into binaural stereo through four stages: + +**1. Distance Attenuation** — Volume falloff based on distance. Curve is configurable (linear, logarithmic, custom) between min distance (full volume) and max distance (silent). + +**2. Air Absorption** — Frequency-dependent filtering. High frequencies attenuate faster over distance. Modeled with 3-band filter (low/mid/high). This is why distant thunder rumbles but nearby thunder cracks. + +**3. Occlusion** — Ray-casting determines if geometry blocks the direct path. Occlusion factor (0.0 = fully blocked, 1.0 = clear) applies low-pass filtering to simulate transmission through materials. + +**4. HRTF Convolution** — Convolves the mono source with Head-Related Transfer Functions for the source's direction. Creates stereo output that the brain interprets as coming from that direction in space. Uses Steam Audio's default HRTF measured from dummy head, with support for custom HRTFs via SOFA files. + +The result: a mono sound positioned in world space becomes stereo that sounds like it's coming from that position when heard through headphones. + +## Bus Mixer + +Instead of mixing hundreds of sources individually, we mix categorical buses: + +- **SFX Bus** — Footsteps, hammer hits, impacts +- **Ambient Bus** — Wind, birds, streams, background loops +- **Music Bus** — Background music stems +- **UI Bus** — Button clicks, menu sounds +- **Voice Bus** — Dialogue, narration (future) + +Each bus provides: + +- **3-band EQ** — Low shelf, mid bell, high shelf for tonal shaping +- **Send levels** — Route to effect buses (reverb, delay) +- **Fader** — Gain control, -∞ to +12dB +- **Solo/Mute** — Isolation for debugging +- **Metering** — Real-time peak and RMS + +**Master Bus** adds: + +- **Limiter** — Peak limiting at -0.3dB ceiling (prevents clipping) +- **LUFS Meter** — Integrated loudness (target: -14 LUFS for streaming platforms) + +Sound engineers think in categories ("SFX is too loud") rather than individual sources. This architecture matches that mental model. + +## Source Prioritization + +With potentially 200+ sources in a dense scene, we can only render ~64 voices before mix becomes muddy and CPU maxes out. Priority system determines which sources actually play: + +**Culling pipeline:** + +1. **Distance cull** — Sources beyond max distance are culled immediately +2. **Amplitude cull** — Sources below audibility threshold are culled +3. **Priority scoring** — Score remaining sources based on: + - Bus type (Voice > UI > SFX > Music > Ambient) + - Distance (closer = higher priority) + - Amplitude (louder = higher priority) + - Recency (recently started sounds get boost to preserve transients) +4. **Sort by priority** — Highest to lowest +5. **Take top N** — Render top 64, cull the rest + +Result: In crowded scenes, important sounds stay clear. Ambient background gracefully fades when foreground action demands attention. + +## Soundscape Zones + +For hyper-dense ambient audio, individual point sources are insufficient. Soundscape zones define regions that activate layered sounds when the listener enters: + +**SoundscapeZone component:** + +- **Shape** — Sphere, box, or cylinder +- **Layers** — Multiple sound sources (e.g., wind rustling leaves, distant birds, occasional creaks) +- **Fade distance** — Crossfade range when entering/exiting (prevents popping) +- **Priority** — When zones overlap, higher priority wins + +**Layer types:** + +- **Spatial** — Positioned in world, processed through Steam Audio +- **Non-spatial** — Plays directly to bus without spatialization (ambient beds) +- **Randomized** — Plays occasionally with random timing (birds, creaks) + +The system automatically activates/deactivates zones based on listener position and crossfades between overlapping zones. + +## Developer Tooling + +### Spatial Debug Visualization + +Egui-based debug mode renders active audio sources as translucent spheres in world space. Uses Bevy gizmos for simplicity (not volumetric shaders). + +Each visualization shows: + +- **Position** — Sphere centered on source's world position +- **Falloff range** — Two concentric spheres (min distance / max distance) +- **Amplitude** — Brightness pulses with current amplitude +- **Bus category** — Color-coded by bus (SFX = blue, Ambient = green, Music = purple, UI = yellow, Voice = orange) +- **Occlusion** — Ray from source to listener (green = clear, red = occluded) + +**Selection workflow:** + +1. Click on sphere gizmo +2. Inspector panel shows: amplitude (animated bar), occlusion %, distance, bus, spatial config +3. Action buttons: "Solo This Bus", "Open in Mixer", "Play Solo" + +**Pause and inspect:** + +1. Pause game +2. Visualizations freeze in place +3. Click sources to inspect state +4. Adjust mixer +5. Resume to hear changes + +The tight loop (hear problem → see problem → fix problem) is what separates professional tooling from guesswork. + +### Mixer Panel + +Egui panel resembling a hardware mixing console. Each bus gets a channel strip: + +``` +┌────────┐ +│ SFX │ +├────────┤ +│ EQ │ — 3-band controls (collapsed by default) +│ Sends │ — Reverb/delay levels +│ [S][M] │ — Solo/mute buttons +│ ▮▮▮▯▯▯ │ — Peak meter +│ ┃████┃ │ — Fader +│ -2.0dB │ — Gain readout +└────────┘ +``` + +**Master section:** + +``` +┌──────────┐ +│ MASTER │ +├──────────┤ +│ Limiter │ +│ [ON] -0.3│ +├──────────┤ +│ ▮▮▮▮▮▯▯▯ │ — Stereo meter +│ ┃█████┃ │ — Fader +│ 0.0dB │ +├──────────┤ +│LUFS: -14 │ — Integrated loudness +└──────────┘ +``` + +**Preset system:** + +- Save complete mixer state (all fader positions, EQ settings, sends) +- Serialize to JSON (version-controllable alongside assets) +- A/B toggle for comparison + +## Data Structures + +### Core Components + +```rust +/// Positioned audio source in world space +#[derive(Component)] +pub struct AudioSource { + pub sample: Handle, + pub bus: AudioBus, + pub gain: f32, + pub looping: bool, + pub spatial: SpatialConfig, +} + +#[derive(Clone)] +pub struct SpatialConfig { + pub min_distance: f32, + pub max_distance: f32, + pub falloff: FalloffCurve, + pub occlusion_enabled: bool, +} + +#[derive(Component)] +pub struct AudioListener; + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum AudioBus { + #[default] + Sfx, + Ambient, + Music, + Ui, + Voice, +} +``` + +### Soundscape Zones + +```rust +#[derive(Component)] +pub struct SoundscapeZone { + pub layers: Vec, + pub shape: ZoneShape, + pub fade_distance: f32, +} + +pub struct SoundscapeLayer { + pub sample: Handle, + pub gain: f32, + pub spatial: bool, +} + +pub enum ZoneShape { + Sphere { radius: f32 }, + Box { half_extents: Vec3 }, + Cylinder { radius: f32, height: f32 }, +} +``` + +### Mixer State + +```rust +#[derive(Resource)] +pub struct MixerState { + pub buses: HashMap, + pub effects: Vec, + pub master: MasterState, +} + +pub struct BusState { + pub gain_db: f32, + pub muted: bool, + pub soloed: bool, + pub eq: BusEq, + pub sends: Vec, + + // Read-only, updated from audio thread via atomics + pub peak_l: f32, + pub peak_r: f32, + pub rms_l: f32, + pub rms_r: f32, +} + +pub struct MasterState { + pub gain_db: f32, + pub limiter_enabled: bool, + pub limiter_ceiling_db: f32, + pub peak_l: f32, + pub peak_r: f32, + pub lufs_integrated: f32, +} +``` + +### Debug Visualization + +```rust +#[derive(Resource, Default)] +pub struct AudioDebugState { + pub listener: Vec3, + pub sources: Vec, +} + +pub struct AudioSourceDebug { + pub entity: Entity, + pub position: Vec3, + pub bus: AudioBus, + pub amplitude: f32, + pub min_distance: f32, + pub max_distance: f32, + pub occlusion: f32, +} + +#[derive(Resource)] +pub struct AudioDebugSettings { + pub enabled: bool, + pub show_falloff_ranges: bool, + pub show_occlusion_rays: bool, + pub min_amplitude_threshold: f32, + pub bus_colors: HashMap, +} +``` + +## Implementation Phases + +### Phase 1: Vendor Firewheel + +- Fork Firewheel into `crates/libmarathon/src/audio/firewheel/` +- Strip unused features (LUFS nodes, input streams, complex pooling) +- Adapt to Marathon's logging patterns +- Verify basic playback works + +**Critical files:** +- NEW: `crates/libmarathon/src/audio/firewheel/` (vendored) +- NEW: `crates/libmarathon/src/audio/mod.rs` (module root) + +### Phase 2: Vendor Steam Audio + +- Vendor Steam Audio Rust bindings (audionimbus) into `crates/libmarathon/src/audio/steam_audio/` +- Create Firewheel node wrapper for Steam Audio processing +- Implement HRTF initialization (default dummy head HRTF) +- Test binaural output with positioned source + +**Critical files:** +- NEW: `crates/libmarathon/src/audio/steam_audio/` (vendored bindings) +- NEW: `crates/libmarathon/src/audio/steam_audio_node.rs` (Firewheel integration) + +### Phase 3: Bevy Integration + +- Create `AudioSource`, `AudioListener` components +- Implement position sync system (Transform → Steam Audio atomics) +- Implement component lifecycle (Added/Removed → node creation/cleanup) +- Asset loading (decode audio files into Firewheel samples) + +**Critical files:** +- NEW: `crates/libmarathon/src/audio/components.rs` +- NEW: `crates/libmarathon/src/audio/systems.rs` +- NEW: `crates/libmarathon/src/audio/assets.rs` + +### Phase 4: Bus Mixer + +- Create `MixerState` resource with bus hierarchy +- Implement bus Firewheel nodes with gain/EQ/sends +- Connect all sources to appropriate buses +- Add master bus with limiting + +**Critical files:** +- NEW: `crates/libmarathon/src/audio/mixer.rs` +- NEW: `crates/libmarathon/src/audio/buses.rs` + +### Phase 5: Prioritization and Culling + +- Implement priority scoring system +- Add distance and amplitude culling +- Enforce voice limit (64 simultaneous) +- Test with 200+ sources + +**Critical files:** +- NEW: `crates/libmarathon/src/audio/prioritization.rs` +- MODIFY: `crates/libmarathon/src/audio/systems.rs` + +### Phase 6: Debug Visualization + +- Implement gizmo rendering for active sources +- Add selection raycasting +- Create inspector panel (egui) +- Add amplitude animation to visualizations + +**Critical files:** +- NEW: `crates/libmarathon/src/audio/debug.rs` +- NEW: `crates/libmarathon/src/audio/debug_ui.rs` + +### Phase 7: Mixer Panel + +- Implement egui mixer panel with channel strips +- Add EQ controls (collapsed by default) +- Add solo/mute buttons +- Implement metering (peak/RMS from audio thread) +- Add preset save/load + +**Critical files:** +- NEW: `crates/libmarathon/src/audio/mixer_ui.rs` +- NEW: `crates/libmarathon/src/audio/presets.rs` + +### Phase 8: Soundscape Zones + +- Implement `SoundscapeZone` component +- Add zone activation system (listener position → zone enable/disable) +- Implement crossfading between zones +- Add randomized layer playback + +**Critical files:** +- NEW: `crates/libmarathon/src/audio/soundscape.rs` + +## Design Decisions + +### Why Vendor Instead of Depend? + +**Decision:** Copy bevy_seedling and bevy_steam_audio source into Marathon, adapt to Marathon's patterns. + +**Rationale:** + +External dependencies create version lag (we wait for Bevy compatibility updates), include unused features (bloat), and don't match Marathon's conventions (logging, diagnostics). Vendoring provides control (update Bevy on our schedule), simplicity (strip unused code), and integration (use Marathon's patterns). + +**Trade-off:** Maintenance burden (manually port upstream improvements) in exchange for control and integration. + +### Why Real-Time Simulation (Not Baked)? + +**Decision:** Use Steam Audio's real-time simulation initially. Add baking as optimization later if needed. + +**Rationale:** + +Baked audio pre-computes reverb at probe points, saving CPU but requiring build steps and consuming memory (~hundreds of MB for dense grids). Baked data doesn't handle dynamic geometry. + +Aspen has construction and terrain modification — players actively change the world. Baked data would need frequent rebuilding. Real-time handles this naturally and allows immediate iteration. + +**Risk:** CPU cost. Steam Audio's real-time reverb is expensive. + +**Mitigation:** If we hit performance limits, add baked reverb for static geometry while keeping real-time for dynamic elements. + +**Trade-off:** Higher CPU usage in exchange for dynamic world support and faster iteration. + +### Why Bus-Based Mixing? + +**Decision:** Expose mixing controls at bus level (5 buses), not individual sources (200+ sources). + +**Rationale:** + +Sound engineers think categorically ("ambient is too loud") not granularly ("this specific wind loop at position X is too loud"). With 200+ sources, individual control is cognitively impossible. + +Industry tools (FMOD, Wwise, Pro Tools) all use bus/group architecture. This matches existing mental models. + +Individual source control is still available through inspector panel for debugging. + +**Trade-off:** Less granular control in exchange for cognitive manageability. + +### Why Gizmos (Not Volumetric Shaders)? + +**Decision:** Use Bevy gizmos for debug visualization initially. Consider custom shaders later if needed. + +**Rationale:** + +True volumetric hazes (ray-marched spheres) would look better but are expensive and complex. Gizmos are simple, performant, already available in Bevy. + +Visualization is for development tooling, not player-facing graphics. Functional clarity > visual polish. + +**Trade-off:** Less visual polish in exchange for implementation simplicity. + +## Open Questions + +### HRTF Personalization + +Steam Audio supports custom HRTFs from SOFA files. Personalized HRTFs (measured from individual's ears) provide dramatically better spatial accuracy than generic HRTFs. + +Should we expose this as accessibility option? Some players may have hearing differences that make generic HRTFs ineffective. + +**Challenge:** SOFA files must be measured or generated. Services exist (HRTF from ear photos) but quality varies. + +### Head Tracking on iPad + +AirPods Pro provide head orientation via Core Motion. Combined with spatial audio, head tracking anchors sounds in world space as you turn your head, improving externalization. + +Should we integrate head tracking? Aspen uses fixed camera in city builder — listener position is camera, not player's head. Does head tracking make sense? Would it be disorienting if sounds move when you physically turn your head while visual camera stays fixed? + +**Possible approaches:** +- Disable entirely +- Optional for users who want it +- Hybrid with reduced sensitivity + +### Reverb Strategy + +Options: +- **Real-time ray-traced** — Handles dynamic geometry, CPU-expensive +- **Baked probe-based** — Cheap at runtime, requires build step, doesn't handle dynamics +- **Parametric (FDN)** — Middle ground, cheaper than convolution, less accurate + +For Aspen's construction-heavy gameplay, which is right? + +**Possible hybrid:** Baked for large static geometry (terrain), real-time for buildings (player constructs), parametric for fallback. + +### Network Sync for Audio + +In parallel multiplayer, should audio events sync between peers? + +- **If yes:** Need to network events, handle latency, avoid doubling when both hear local and remote +- **If no:** Each player hears only own actions, may feel disconnected + +**Possible middle ground:** Sync "world state" sounds (ambient zones, persistent audio) but not transients (footsteps, UI). + +### Music System + +This RFC focuses on spatial SFX/ambient. Music requires: + +- Stem-based playback (mix layers independently) +- Adaptive mixing (respond to game state) +- Crossfading between tracks +- Beat-synced events +- Horizontal re-sequencing + +Should we include basic music playback here (non-spatial, single track, simple crossfade) and defer adaptive system, or wait and design music holistically? + +## Success Metrics + +- **Spatial accuracy:** Blindfolded playtesters point toward sources within 15° error. Front/back confusion <10%. +- **Mix quality:** Professional mix rated "better" by >80% of A/B test participants. +- **Tooling effectiveness:** Sound engineer identifies and fixes deliberate mix problem within 5 minutes using debug visualization. +- **Performance:** 64 simultaneous voices with Steam Audio HRTF at <2ms audio thread time on M1 iPad. No audible glitches. + +## Testing Strategy + +### Unit Tests + +- `test_audio_graph_lifecycle()` — Verify node creation/cleanup on entity spawn/despawn +- `test_bus_routing()` — Verify sources route to correct buses +- `test_prioritization()` — Verify priority scoring and culling +- `test_spatial_config()` — Verify Steam Audio parameter sync from Transform + +### Integration Tests + +- `test_dense_soundscape()` — Spawn 200 sources, verify 64 voice limit enforced +- `test_zone_activation()` — Listener enters/exits zone, verify layers activate/deactivate +- `test_mixer_preset()` — Save/load preset, verify state restoration +- `test_occlusion()` — Place geometry between source and listener, verify occlusion applied + +### Performance Tests + +- 64 active voices: <2ms audio thread latency +- 200 total sources: Priority culling to 64 in <1ms +- Rejoin bandwidth (not applicable to audio) + +## References + +- [Firewheel](https://github.com/BillyDM/firewheel) — Lock-free audio graph engine +- [bevy_seedling](https://github.com/CorvusPrudens/bevy_seedling) — Reference for Firewheel integration patterns +- [audionimbus](https://github.com/MaxenceMaire/audionimbus) — Rust bindings for Steam Audio +- [Steam Audio Documentation](https://valvesoftware.github.io/steam-audio/) — Spatial simulation reference +- Aspen Style Guide, Section 6: Audio Direction diff --git a/docs/rfcs/0006-agent-simulation-architecture.md b/docs/rfcs/0006-agent-simulation-architecture.md new file mode 100644 index 0000000..c660726 --- /dev/null +++ b/docs/rfcs/0006-agent-simulation-architecture.md @@ -0,0 +1,1716 @@ +# RFC: Agent Simulation Architecture + +**Status:** Draft +**Author:** Sienna +**Date:** December 2024 +**Scope:** NPC behavior, lifecycle, scheduling, world interaction + +--- + +## 1. Overview + +Aspen simulates a small village of up to 500 inhabitants—people, animals, vehicles, buildings—all living their lives in real-time. There's no pause button, no fast-forward. Time flows, and everyone in it flows with it. + +The village baker wakes before dawn, walks to work, bakes bread, serves customers, takes lunch with her spouse, and returns home in the evening. When she catches a cold, she stays home; her apprentice covers the shop. When her child starts school, her mornings shift. When her mother ages and needs care, her priorities change again. These aren't scripted events—they emerge from simple rules about how people, places, and relationships interact. + +This RFC describes how that emergence happens. + +### 1.1 The Challenge + +We want Dwarf Fortress depth on iPad hardware. That means: + +- **500 entities existing simultaneously**, each with position, state, and history +- **No teleportation**—everyone is always somewhere, moving through space +- **Real-time simulation** with no pausing, running alongside heavy graphics +- **Emergent behavior** from generic rules, not hand-coded special cases +- **Multiplayer synchronization** across 2-4 peers with no central server + +The solution is to separate concerns by **temporal scale**. Not everything changes at the same rate. A villager's life stage changes yearly; their daily schedule changes daily; their moment-to-moment actions change in real-time. By layering these concerns, we can update slow things slowly and fast things only when necessary. + +### 1.2 Design Principles + +**Generic composition over special cases.** The same systems that make a baker bake should make a cat groom, a car rust, and a building decay. We build primitives (needs, lifecycles, capabilities) and compose them differently for different entities. + +**Emergence over authorship.** We build generic rules and let interactions emerge—including ones we never anticipated. The famous Dwarf Fortress "cat death bug" illustrates this perfectly: Toady One never wrote code for cats dying from alcohol. He wrote "substances exist on surfaces," "contact transfers substances to body parts," "grooming consumes substances," and "alcohol is toxic scaled by body mass." A player spilled beer in a tavern. Cats walked through it, groomed their paws, and died of alcohol poisoning. It was a *bug*—but it emerged inevitably from correct generic systems. That's the power of emergence: the simulation does things you never explicitly designed, because the rules are true enough to combine in unexpected ways. + +**Consequences without failure.** Life has texture—sickness disrupts plans, relationships strain, businesses struggle. But there are no game-over states. A sick baker means the apprentice steps up, the spouse brings soup, the schedule adapts. The simulation bends; it doesn't break. + +**Existence is cheap; cognition is expensive.** Every entity is always somewhere, always moving, always present in the world. But only a few need to *think* at any moment. We do level-of-detail on minds, not bodies. + +### 1.3 Key Abstractions + +Several concepts recur throughout the architecture: + +| Abstraction | What It Is | Why It Matters | +|-------------|-----------|----------------| +| **Life Arc** | State machine of major life stages | Defines what's *possible* for an entity right now | +| **Schedule Authority** | Who controls an entity's time | Children don't set their own bedtime | +| **Institution** | Shared context coordinating multiple entities | The bakery coordinates its workers without them talking | +| **Condition** | Temporary modifier affecting all layers | Sickness changes schedule, capabilities, behavior, and spreads through substrate | +| **Relationship** | Emergent bond enabling coordination | Spouses can have dinner together without negotiating in real-time | + +--- + +## 2. The Four-Layer Architecture + +The simulation operates across four layers, each running at a different timescale. Higher layers constrain lower layers: your life arc determines what schedules are possible; your schedule determines what behaviors are appropriate; your behaviors interact with the substrate. + +```mermaid +flowchart TB + subgraph arc["LIFE ARC — months/years"] + ARC_DESC["What's possible for this entity?
Roles, capabilities, autonomy level"] + end + + subgraph sched["DAILY SCHEDULE — once per day"] + SCHED_DESC["Where should I be? What category of activity?
Generated from roles, authority, relationships, personality"] + end + + subgraph bt["BEHAVIOR TREE — real-time"] + BT_DESC["What exactly should I do right now?
Reactive to world state, needs, nearby entities"] + end + + subgraph sub["WORLD SUBSTRATE — continuous"] + SUB_DESC["Environmental state affecting everyone
Substances, temperature, sound, contagion"] + end + + arc --> |"constrains"| sched + sched --> |"constrains"| bt + bt <--> |"reads & writes"| sub + sub --> |"triggers"| bt +``` + +Think of it as nested contexts. Martha's life arc says "you're an adult, partnered, working as a baker." That context generates her schedule: "5am wake, 6am-2pm work, 6pm dinner with spouse." The schedule context activates her work behavior tree: "customers waiting? serve them. Stock low? bake more." Her actions affect the substrate (oven heats up, flour gets on floor), and the substrate affects her back (smoke from burned bread triggers coughing). + +### 2.1 Life Arc + +The life arc is a state machine representing major life stages. It changes rarely—on birthdays, marriages, deaths, career changes—and when it does, the whole entity transforms. New roles become available; old ones close off. The child who couldn't hold a job becomes an adolescent who can apprentice becomes an adult who can master a craft. + +```mermaid +stateDiagram-v2 + [*] --> Child + Child --> Adolescent: age 13 + Adolescent --> Adult: age 18 + Adult --> Elder: age 65 + Elder --> [*]: death + + state Adult { + [*] --> Single + Single --> Courting: meets someone + Courting --> Partnered: commitment + Partnered --> Parent: has child + Parent --> Partnered: children grown + Partnered --> Single: separation + } +``` + +But life arcs aren't just for people. The same abstraction applies to anything that changes state over time: + +```mermaid +flowchart LR + subgraph person["Person"] + P1[Child] --> P2[Adolescent] --> P3[Adult] --> P4[Elder] + end + + subgraph vehicle["Vehicle"] + V1[New] --> V2[Used] --> V3[Worn] --> V4[Junked] + end + + subgraph building["Building"] + B1[Constructed] --> B2[Maintained] --> B3[Dilapidated] --> B4[Ruined] + end + + subgraph relationship["Relationship"] + R1[Strangers] --> R2[Acquaintance] --> R3[Friendly] --> R4[Close] + end +``` + +The life arc is the slowest-changing layer, but it's also the most consequential. When Martha's life arc transitions from "Adult/Single" to "Adult/Partnered," her whole world shifts: new shared commitments appear in her schedule, new behaviors unlock (care for partner), new needs emerge (intimacy, coordination). One state change ripples through every other layer. + +#### Autonomy Gradient + +One critical thing the life arc controls is **autonomy**—who has authority over this entity's time and decisions. + +| Life Stage | Schedule Authority | Range | Commerce | Can Refuse? | +|------------|-------------------|-------|----------|-------------| +| Child (0-12) | External (parents/school) | Near home | No | No | +| Adolescent (13-17) | Partial (school imposed, leisure self-directed) | Wider | Limited | Yes, with conflict | +| Adult (18+) | Autonomous (self-directed) | Unlimited | Full | N/A | + +A child doesn't generate their own schedule—their parents and school do. They have blocks of free time, but bedtime isn't negotiable. An adolescent gains more control, can refuse (though refusal has consequences), and has a wider range of independent movement. Adults are fully autonomous, though they may *commit* their time to employers, partners, and children. + +This autonomy gradient is how children work in the simulation: not as special-cased entities, but as regular entities whose life arc state grants limited autonomy. + +### 2.2 Daily Schedule + +Each game-day morning, entities generate (or receive) a schedule: a timeline of where to be and what broad category of activity to do. The schedule doesn't specify exact actions—"bake sourdough at 7:15am"—but rather activity blocks: "Work at Bakery, 6am-2pm." + +```mermaid +gantt + title Martha's Daily Schedule (Baker) + dateFormat HH:mm + axisFormat %H:%M + + section Home + Wake & Routine :05:00, 30m + Breakfast :05:30, 15m + + section Commute + Walk to Bakery :05:45, 15m + + section Bakery + Work :06:00, 120m + Break :08:00, 30m + Work :08:30, 330m + + section Commute + Walk Home :14:00, 15m + + section Home + Lunch :14:15, 45m + Leisure :15:00, 180m + Dinner w/ Spouse :18:00, 60m + Evening :19:00, 180m + Sleep :22:00, 420m +``` + +Compare this to a child's schedule, which is largely *imposed* rather than generated: + +```mermaid +gantt + title Tommy's Daily Schedule (Child, age 8) + dateFormat HH:mm + axisFormat %H:%M + + section Home (Parent-Imposed) + Wake :07:00, 30m + Breakfast :07:30, 30m + + section Commute + Walk to School :08:00, 30m + + section School (Institution-Imposed) + Morning Classes :08:30, 210m + Lunch :12:00, 30m + Afternoon Classes :12:30, 150m + + section Commute + Walk Home :15:00, 30m + + section Home + Free Play (SELF) :15:30, 150m + Dinner :18:00, 60m + Homework/Evening :19:00, 90m + Bedtime Routine :20:30, 30m + Sleep :21:00, 600m +``` + +Tommy's schedule has one self-directed block: Free Play. Everything else comes from parents (wake time, meals, bedtime) or institutions (school). His behavior tree only runs freely during that 2.5-hour window; the rest of the time, he's following externally-imposed activities. + +#### Schedule Authority + +Who generates or imposes the schedule depends on the entity's **schedule authority**, which flows from their life arc and relationships: + +```mermaid +flowchart TB + subgraph authority["Who Sets the Schedule?"] + AUT["Autonomous
──────────
Adult generates own schedule
from roles, personality, needs"] + + EXT["External
──────────
Parent/institution imposes schedule
Entity follows with limited agency"] + + PART["Partial
──────────
Some blocks imposed (work, school)
Other blocks self-directed"] + end + + AUT --> |"commits to"| EMPLOYER["Employer
(work hours)"] + AUT --> |"commits to"| PARTNER["Partner
(shared activities)"] + AUT --> |"commits to"| CHILDREN["Children
(parenting duties)"] + + EXT --> |"imposed by"| PARENT["Parent"] + EXT --> |"imposed by"| SCHOOL["School"] + + PART --> |"work hours from"| JOB["Job"] + PART --> |"school hours from"| INST["Institution"] + PART --> |"free time is"| SELF["Self-directed"] +``` + +An adult with a job has **partial** authority: their employer claims certain hours, but evenings and weekends are autonomous. An adult with a partner has autonomous authority but **shared commitments** that appear in both schedules (dinner together, date night). + +#### Shared Commitments + +When entities have relationships, they can form commitments that appear in both schedules without real-time negotiation: + +```mermaid +sequenceDiagram + participant M as Martha's Schedule Gen + participant C as Shared Commitment + participant D as David's Schedule Gen + + Note over C: "Dinner together, daily, 6pm, at home" + + M->>C: Query my commitments + C-->>M: Dinner 6pm with David + M->>M: Block 6pm-7pm: Dinner (Home) + + D->>C: Query my commitments + C-->>D: Dinner 6pm with Martha + D->>D: Block 6pm-7pm: Dinner (Home) + + Note over M,D: Both schedules now include dinner
No real-time coordination needed +``` + +The commitment was established earlier (when the relationship reached sufficient depth) and now it simply *exists* in the schedule data. Both entities will be at the same place at the same time doing the same activity—not because they negotiated, but because they both read from the same shared commitment. + +### 2.3 Behavior Tree + +When the schedule says "Work," what does that actually mean? The behavior tree answers: "Given that I'm at work, and given the current state of the world, what exactly should I do right now?" + +The behavior tree is a reactive decision-making system. It evaluates conditions (customers waiting? batch in progress? needs urgent?) and selects appropriate actions. Unlike schedules, which are generated once per day, behavior trees tick continuously for nearby entities. + +```mermaid +flowchart TB + ROOT[Work Activity] --> URGENT{Urgent Need?} + URGENT -->|yes| HANDLE[Handle Need] + URGENT -->|no| CUST{Customers?} + + CUST -->|yes| SERVE[Serve Customer] + CUST -->|no| BATCH{Batch in Progress?} + + BATCH -->|yes| CONTINUE[Continue Batch] + BATCH -->|no| STOCK{Stock Low?} + + STOCK -->|yes| START[Start New Batch] + STOCK -->|no| IDLE[Idle Tasks] + + CONTINUE --> PHASE{Which Phase?} + PHASE -->|mixing| MIX[Mix Dough] + PHASE -->|rising| WAIT[Wait for Rise] + PHASE -->|shaping| SHAPE[Shape Loaves] + PHASE -->|baking| TEND[Tend Oven] + PHASE -->|done| UNLOAD[Unload & Stock] + + IDLE --> CLEAN[Clean] + IDLE --> ORGANIZE[Organize] + IDLE --> WANDER[Wait] +``` + +Martha's behavior tree doesn't know it's 7:23am or that she's been working for 83 minutes. It only knows: "I'm in a Work activity block. There's a batch rising. No customers. My needs are fine." From those inputs, it selects: "Wait for rise to complete." + +The schedule says *where* and *when*. The behavior tree says *what* and *how*. + +#### Conditions Modify Behavior + +When Martha has the flu, her behavior tree still runs—but its priorities shift. A **condition** is a temporary modifier that affects how the tree evaluates: + +```mermaid +flowchart TB + subgraph normal["Normal State"] + N_ROOT[Activity] --> N_NEEDS{Needs Urgent?} + N_NEEDS -->|rarely| N_HANDLE[Handle] + N_NEEDS -->|usually no| N_WORK[Continue Activity] + end + + subgraph sick["With Flu Condition"] + S_ROOT[Activity] --> S_NEEDS{Needs Urgent?} + S_NEEDS -->|"always (rest)"| S_HANDLE[Rest/Sleep] + S_NEEDS -.->|blocked| S_WORK[Continue Activity] + end + + FLU[Flu Condition] -->|"modifies
need thresholds"| sick + FLU -->|"overrides
schedule"| SCHED[Work → Rest at Home] + FLU -->|"reduces
capabilities"| CAP[Movement slowed
Work disabled] +``` + +The flu doesn't require special-case code in Martha's behavior tree. It's a condition that: +- Overrides her schedule (Work blocks become Rest at Home blocks) +- Modifies her need thresholds (Rest need is always "urgent") +- Reduces her capabilities (can't do Work actions, movement slowed) + +Her BT runs the same logic, but with different inputs, it produces different outputs: stay in bed, sleep, accept soup from spouse. + +### 2.4 World Substrate + +The bottom layer is the world itself: physical space filled with substances, temperatures, sounds, smells, and other environmental state. The substrate doesn't belong to any entity—it's shared context that affects everyone passing through. + +```mermaid +flowchart LR + subgraph substrate["World Substrate"] + PUDDLE["Alcohol Puddle
(trigger volume)"] + TEMP["Temperature Zone
(near oven)"] + SOUND["Sound Emitter
(church bells)"] + CONTAGION["Contagion Cloud
(sick villager)"] + end + + subgraph effects["Generic Effects"] + PUDDLE -->|"contact transfer"| PAWS["Substance on paws"] + TEMP -->|"comfort modifier"| COMFORT["Comfort need affected"] + SOUND -->|"wake event"| WAKE["Entities react"] + CONTAGION -->|"condition transfer"| SICK["May get sick"] + end +``` + +The substrate is where emergence happens. The Dwarf Fortress cat bug emerged from generic rules that were each individually correct: + +1. **Substances exist on surfaces.** A spilled drink creates a puddle with substance: alcohol. +2. **Contact transfers substances.** Walking through a puddle transfers substance to feet/paws. +3. **Grooming consumes substances.** Cats groom dirty paws, moving substance from paws to stomach. +4. **Consumed substances affect entities.** Alcohol in stomach creates Intoxication condition, scaled by body mass. + +A cat weighs 4kg. A dwarf weighs 60kg. Same amount of alcohol, very different effects. The cat gets a severe Intoxication condition; the dwarf barely notices. Toady never wrote cat-alcohol code—the death emerged from generic rules combining in ways he hadn't anticipated. We *want* that kind of emergence (though perhaps with fewer cat casualties). + +```mermaid +sequenceDiagram + participant W as World Substrate + participant C as Cat + participant B as Cat's Body State + participant BT as Cat's Behavior Tree + + Note over W: Alcohol puddle exists at position (x,y) + + C->>W: Path intersects puddle + W->>B: Transfer: Alcohol → Paws + + B->>BT: Trigger: Dirty paws + BT->>BT: Select: Groom action + BT->>B: Consume: Paws → Stomach + + B->>B: Apply effect: Alcohol × (1/body_mass) + B->>C: Add condition: Severe Intoxication + + Note over C: Cat now has modified
behavior and capabilities +``` + +#### Substrate Layers + +The substrate tracks multiple environmental factors, each with its own rules for propagation and effect: + +| Layer | Examples | Propagation | Effect on Entities | +|-------|----------|-------------|-------------------| +| **Substances** | Alcohol, water, mud, blood | Surface contact, evaporation | Contact transfer, consumption effects | +| **Temperature** | Oven heat, winter cold, fire | Radiation, convection | Comfort needs, damage, clothing requirements | +| **Light** | Sunlight, lamplight, darkness | Line-of-sight | Visibility, mood, sleep pressure | +| **Sound** | Speech, bells, construction | Distance falloff, occlusion | Wake events, communication, mood | +| **Air Quality** | Smoke, dust, perfume | Diffusion, ventilation | Health, comfort, attraction/repulsion | +| **Contagion** | Flu virus, rumors, moods | Proximity, contact | Condition transfer | + +The substrate is the lowest layer but in some ways the most important. It's where the world becomes *physical*—where actions have consequences that ripple out to affect everyone nearby. + +--- +| Light | Natural and artificial illumination | Visibility, mood, sleep schedules | +| Sound | Ambient noise, conversations, alerts | Wake events, mood, communication range | +| Air Quality | Smoke, dust, pleasant/unpleasant odors | Health, comfort, attraction/repulsion | +| Contagion | Disease vectors on surfaces and in air | Condition transmission | + +--- + +## 3. Entity Composition + +Entities are composed from orthogonal components. Not everything has agency; not everything moves; not everything ages. The composition determines what systems apply to an entity. + +### 3.1 Component Categories + +```mermaid +flowchart TB + subgraph Universal["Universal (all entities)"] + LC[Lifecycle
state machine over time] + end + + subgraph Physical["Physical (exists in space)"] + PP[PhysicalPresence
position, bounds, movement] + end + + subgraph Agency["Agency (makes decisions)"] + N[Needs
biological/psychological drives] + I[Instincts
species-typical behaviors] + R[Roles
acquired responsibilities] + AU[Autonomy
schedule authority level] + end + + subgraph Social["Social (coordinates with others)"] + REL[Relationships
bonds with other entities] + MEM[Membership
belongs to institutions] + end + + subgraph Interaction["Interaction (used by others)"] + O[Operable
provides capabilities] + ST[Stateful
tracks internal state] + end + + subgraph Institution["Institution (coordinates others)"] + GOV[Governance
sets schedules, rules] + RES[Resources
shared inventory, equipment] + REP[Reputation
collective standing] + end +``` + +### 3.2 Entity Examples + +Different entity types compose different components: + +```mermaid +flowchart LR + subgraph person[" "] + direction TB + P[👤 Person] + P --> P_LC[Lifecycle] + P --> P_PP[PhysicalPresence] + P --> P_N[Needs] + P --> P_I[Instincts] + P --> P_R[Roles] + P --> P_AU[Autonomy] + P --> P_REL[Relationships] + P --> P_MEM[Membership] + end + + subgraph deer[" "] + direction TB + D[🦌 Wild Deer] + D --> D_LC[Lifecycle] + D --> D_PP[PhysicalPresence] + D --> D_N[Needs] + D --> D_I[Instincts] + end + + subgraph cat[" "] + direction TB + C[🐱 Pet Cat] + C --> C_LC[Lifecycle] + C --> C_PP[PhysicalPresence] + C --> C_N[Needs] + C --> C_I[Instincts] + C --> C_R[Roles: Mouser?] + C --> C_MEM[Membership: Household] + end + + subgraph car[" "] + direction TB + V[🚗 Car] + V --> V_LC[Lifecycle] + V --> V_PP[PhysicalPresence] + V --> V_O[Operable] + V --> V_ST[Stateful: fuel, wear] + end + + subgraph bakery[" "] + direction TB + B[🏪 Bakery] + B --> B_LC[Lifecycle] + B --> B_PP[PhysicalPresence] + B --> B_O[Operable: stations] + B --> B_ST[Stateful: inventory] + B --> B_GOV[Governance] + B --> B_RES[Resources] + B --> B_REP[Reputation] + end +``` + +| Entity | Components | Notable Characteristics | +|--------|-----------|------------------------| +| **Adult Person** | Lifecycle, PhysicalPresence, Needs, Instincts, Roles, Autonomy (full), Relationships, Membership | Full agency, can hold jobs, form relationships | +| **Child** | Lifecycle, PhysicalPresence, Needs, Instincts, Autonomy (limited), Relationships, Membership | External schedule authority, limited range | +| **Wild Deer** | Lifecycle, PhysicalPresence, Needs, Instincts | No roles, no relationships, pure survival behaviors | +| **Pet Cat** | Lifecycle, PhysicalPresence, Needs, Instincts, Roles?, Membership | May have mouser role, belongs to household | +| **Car** | Lifecycle, PhysicalPresence, Operable, Stateful | No agency—operated by others | +| **Bakery** | Lifecycle, PhysicalPresence, Operable, Stateful, Governance, Resources, Reputation | Institution that coordinates workers | +| **School** | Lifecycle, PhysicalPresence, Governance, Resources, Reputation | Institution with schedule authority over students | +| **Household** | Governance, Resources | Virtual institution (no physical presence of its own) | + +### 3.3 Institutions + +Institutions are entities that coordinate multiple other entities. They provide shared context, impose schedules, and aggregate state. + +```mermaid +flowchart TB + subgraph bakery["The Bakery (Institution)"] + GOV["Governance
───────────
Operating hours: 6am-2pm
Worker schedule templates
Policies: breaks, roles"] + RES["Resources
───────────
Inventory: flour, sugar...
Equipment: ovens, counters
Money: cash drawer"] + REP["Reputation
───────────
Quality rating: 4.2
Customer loyalty
Community standing"] + STATE["Operational State
───────────
Status: Open
Customer queue: 3
Active batches: 2"] + end + + subgraph workers["Workers"] + M[Martha
Owner/Baker] + A[Alex
Apprentice] + J[Jamie
Counter Staff] + end + + M -->|"Membership
role: owner"| bakery + A -->|"Membership
role: apprentice"| bakery + J -->|"Membership
role: employee"| bakery + + bakery -->|"provides schedule
template"| M + bakery -->|"provides schedule
template"| A + bakery -->|"provides schedule
template"| J + + bakery -->|"shared blackboard
for BT decisions"| STATE +``` + +#### Institution Types + +| Institution Type | Provides | Schedule Authority Over | Example | +|-----------------|----------|------------------------|---------| +| **Workplace** | Job roles, schedule templates, shared resources | Employees (work hours) | Bakery, Farm, Clinic | +| **School** | Curriculum, schedule, social environment | Students (school hours) | Elementary School | +| **Household** | Shared living space, family roles, resources | Children (most hours), shared meals | The Miller Home | +| **Religious** | Community, rituals, moral framework | Members (worship times) | Church, Temple | +| **Government** | Laws, services, civic identity | Citizens (jury duty, etc.) | Town Council | + +#### Institution Lifecycle + +Institutions have their own life arcs: + +```mermaid +stateDiagram-v2 + [*] --> Founded: entrepreneur starts + Founded --> Struggling: early days + Struggling --> Established: gains customers + Struggling --> Closed: fails + Established --> Thriving: grows reputation + Established --> Declining: loses relevance + Thriving --> Landmark: becomes institution + Declining --> Struggling: attempts revival + Declining --> Closed: gives up + Landmark --> Declining: world changes + Closed --> [*] +``` + +When an institution's state changes, it affects all members: +- Bakery becomes "Struggling" → workers' schedules reduce, stress increases +- School becomes "Thriving" → better facilities, more students, hiring +- Household loses income-earner → schedule authority may shift, roles redistribute + +### 3.4 Relationships + +Relationships are not entities themselves—they emerge from repeated interactions between entities. But they have state that affects both parties. + +```mermaid +flowchart TB + subgraph rel["Relationship: Martha ↔ David (Spouses)"] + BOND["Bond
───────────
Type: Romantic
Strength: 0.85
Duration: 12 years"] + COORD["Coordination Level
───────────
Level: Cohabiting
Shared schedule context
Joint decisions"] + COMMIT["Commitments
───────────
Daily dinner together
Weekly date night
Shared childcare"] + HISTORY["History
───────────
Recent interactions
Conflict patterns
Support given/received"] + end + + M[Martha] <-->|"relationship"| rel + D[David] <-->|"relationship"| rel + + rel -->|"enables"| SCHED["Shared schedule
generation"] + rel -->|"triggers"| CARE["Care behaviors
when sick"] + rel -->|"modifies"| MOOD["Mood from
proximity"] +``` + +#### Relationship Properties + +```rust +struct Relationship { + parties: (Entity, Entity), + bond_type: BondType, + bond_strength: f32, // 0.0 = strangers, 1.0 = deeply bonded + started_at: GameTime, + + coordination_level: CoordinationLevel, + shared_commitments: Vec, + interaction_history: RingBuffer, + + // Emotional state + warmth: f32, // current positive feeling + tension: f32, // unresolved conflict +} + +enum BondType { + Romantic, + Familial { relation: FamilyRelation }, + Friendship, + Professional, + Caretaking, // pet-owner, elder care +} + +enum CoordinationLevel { + None, // strangers, no coordination + AdHoc, // can propose one-off activities + Recurring, // can establish patterns (friends) + Cohabiting, // share schedule generation context + Dependent, // one party's schedule authority over other +} +``` + +#### Relationship Evolution + +```mermaid +stateDiagram-v2 + [*] --> Strangers + Strangers --> Acquaintance: meet + Acquaintance --> Friendly: positive interactions + Acquaintance --> Strangers: drift apart + Friendly --> Close: sustained contact + Friendly --> Acquaintance: reduced contact + Close --> Bonded: deep investment + Close --> Friendly: life changes + + note right of Acquaintance: AdHoc coordination + note right of Friendly: Recurring coordination + note right of Close: May cohabit + note right of Bonded: Deep coordination + + state romantic <> + Friendly --> romantic: attraction + romantic --> Courting + Courting --> Partnered: commitment + Partnered --> Bonded: marriage/equivalent +``` + +#### How Relationships Affect Simulation + +| Layer | Relationship Effect | +|-------|-------------------| +| **Life Arc** | Enables transitions (Courting → Partnered → Parent) | +| **Schedule** | Shared commitments appear in both schedules; coordination protocol | +| **Behavior Tree** | Care behaviors activate ("is partner sick?"); presence affects mood | +| **Substrate** | Proximity triggers (comfort from nearby loved one) | + +### 3.5 Conditions + +Conditions are temporary states that modify an entity across all layers. They have duration, severity, and systematic effects. + +```mermaid +flowchart TB + subgraph condition["Condition: Flu"] + META["Metadata
───────────
Severity: 0.6
Started: Day 45
Expected: 5 days"] + + subgraph effects["Effects by Layer"] + E_SCHED["Schedule Override
Work → Rest at home"] + E_NEED["Need Modifiers
Rest: +50% rate
Hunger: -30% rate"] + E_CAP["Capability Modifiers
Work: disabled
Movement: 0.5x speed"] + E_BT["Behavior Modifiers
Rest priority: urgent
Seek comfort: active"] + end + + CONTAGION["Contagion
───────────
Creates trigger volumes
Near-contact transmission
Decays over time"] + end + + condition -->|"applied to"| MARTHA[Martha] + + MARTHA -->|"schedule regenerates"| NEW_SCHED["Stays home,
rest blocks"] + MARTHA -->|"BT priorities shift"| NEW_BT["Rest always wins,
seek soup/comfort"] + MARTHA -->|"emits"| VOLUME["Contagion
trigger volume"] + + VOLUME -->|"may infect"| DAVID[David
spouse] + DAVID -->|"BT responds"| CARE["Care for partner
behaviors activate"] +``` + +#### Condition Types + +| Condition | Duration | Schedule Effect | Capability Effect | Social Effect | +|-----------|----------|-----------------|-------------------|---------------| +| **Flu** | 3-7 days | Work → Rest | Movement slowed, work disabled | Contagious, triggers care | +| **Pregnancy** | 9 months | Progressive work reduction | Gradual capability changes | Relationship deepening | +| **Grief** | Weeks-months | Social withdrawal | Work quality reduced | Others offer support | +| **Intoxication** | Hours | May miss commitments | Coordination impaired | Social disinhibition | +| **Injury** | Variable | May require rest | Specific capability loss | May trigger care | +| **Joy** | Days | More social scheduling | Quality bonus | Mood is contagious | + +#### Condition Structure + +```rust +struct Condition { + condition_type: ConditionType, + severity: f32, // 0.0-1.0 + started_at: GameTime, + expected_duration: Option, + + // Effects + need_modifiers: Vec, + capability_modifiers: Vec, + schedule_overrides: Vec, + behavior_modifiers: Vec, + + // Spread + contagion: Option, +} + +struct NeedModifier { + need: NeedType, + rate_multiplier: f32, // how fast need grows + threshold_modifier: f32, // when "urgent" triggers +} + +struct CapabilityModifier { + capability: CapabilityType, + available: bool, // can they do it at all? + quality_multiplier: f32, // how well? +} + +struct ScheduleOverride { + replaces: ActivityCategory, + with_activity: ActivityCategory, + with_location: Option, +} +``` + +### 3.6 The Wild Deer (Non-Sapient Entity) + +Wild animals demonstrate a simplified entity that lacks social components entirely. + +```mermaid +flowchart TB + subgraph deer["Wild Deer Components"] + LC["Lifecycle
───────────
Fawn → Adult → Elder
Seasonal breeding"] + PP["PhysicalPresence
───────────
Position in world
Herd membership
Movement speed"] + N["Needs
───────────
Hunger: grazing
Thirst: water sources
Rest: safe spots
Safety: flee threats"] + I["Instincts
───────────
Flee predators/humans
Herd behavior
Seasonal migration
Territorial marking"] + end + + subgraph absent["NOT Present"] + NO_R[No Roles] + NO_AU[No Autonomy system] + NO_REL[No Relationships] + NO_MEM[No Membership] + end + + deer --> MODE + + subgraph MODE["Behavioral Mode (simplified BT)"] + direction LR + DANGER{Threat?} -->|yes| FLEE[Flee] + DANGER -->|no| REST_Q{Tired?} + REST_Q -->|yes| REST[Rest] + REST_Q -->|no| HUNGRY{Hungry?} + HUNGRY -->|yes| GRAZE[Graze] + HUNGRY -->|no| SEASON{Migration?} + SEASON -->|yes| MIGRATE[Migrate] + SEASON -->|no| WANDER[Wander] + end +``` + +#### Deer Don't Have Schedules + +Unlike sapient entities, deer operate purely on **modes** driven by needs and instincts: + +| Mode | Trigger | Behavior | +|------|---------|----------| +| **Flee** | Threat detected (predator, player, loud noise) | Sprint away, rejoin herd at safe distance | +| **Graze** | Hunger need elevated | Move slowly through foliage, consuming plants | +| **Rest** | Rest need elevated, safety confirmed | Find sheltered spot, lie down | +| **Migrate** | Seasonal instinct + environmental cues | Move toward seasonal grounds as herd | +| **Wander** | No pressing needs | Drift with herd, light foraging | + +#### Substrate Interaction + +Deer interact with the world substrate without schedules or roles: + +- **Consume foliage:** Grazing depletes plant resources (your garden!) +- **Create sounds:** Movement alerts predators and players +- **Leave traces:** Scent trails, tracks, droppings +- **Respond to substrate:** Flee from fire, smoke, loud sounds + +This is covered more in Section 6 (Performance Architecture) under simulation tiers. + +--- + +## 4. Tools and Capabilities + +Behaviors require capabilities. "Bake bread" requires heat application and transformation. The behavior doesn't care whether heat comes from a wood-fired oven, a gas range, or a magical flame—it just needs the `Heat` capability with sufficient parameters. + +Tools are entities that provide capabilities to their operators. This abstraction lets us: +- Add new tools without changing behaviors +- Have behaviors gracefully degrade when ideal tools aren't available +- Model tool quality affecting output quality +- Share tools across multiple entities (institution resources) + +### 4.1 Capability Types + +```mermaid +flowchart TB + subgraph capabilities["Capability Categories"] + direction TB + + MOV["Movement
───────────
speed_multiplier: f32
capacity: usize
terrain: Vec"] + + COMM["Communication
───────────
range: CommRange
async_ok: bool"] + + TRANS["Transformation
───────────
from: ResourceType
to: ResourceType
quality_modifier: f32"] + + FORCE["Force
───────────
magnitude: f32
precision: f32"] + + STOR["Storage
───────────
capacity: usize
preservation: Vec"] + + INFO["Information
───────────
domain: InfoDomain"] + + PROT["Protection
───────────
from: Hazard
amount: f32"] + + HEAT["Heat
───────────
temperature: f32
control: f32"] + end +``` + +```rust +enum Capability { + Movement { speed_multiplier: f32, capacity: usize, terrain: Vec }, + Communication { range: CommRange, async_ok: bool }, + Transformation { from: ResourceType, to: ResourceType, quality_modifier: f32 }, + Force { magnitude: f32, precision: f32 }, + Storage { capacity: usize, preservation: Vec }, + Information { domain: InformationDomain }, + Protection { from: EnvironmentalHazard, amount: f32 }, + Heat { temperature: f32, control: f32 }, +} +``` + +### 4.2 Operator Modes + +Using a tool affects the operator in different ways: + +```mermaid +flowchart LR + subgraph modes["Operator Modes"] + direction TB + + INH["🚗 Inhabit
───────────
Inside the tool
Your movement = its movement
May block other actions"] + + WLD["🔨 Wield
───────────
Holding the tool
Can still move (maybe limited)
Occupies hands"] + + STN["🔥 Station
───────────
Standing at the tool
Tool is stationary
Periodic attention needed"] + + WR["🧥 Wear
───────────
Passive benefit
No active attention
Slot-based"] + end + + CAR[Car] --> INH + HAMMER[Hammer] --> WLD + PHONE[Phone] --> WLD + OVEN[Oven] --> STN + WORKBENCH[Workbench] --> STN + COAT[Coat] --> WR + GLASSES[Glasses] --> WR +``` + +```rust +enum OperatorMode { + /// Inside the tool—your movement is its movement (car, elevator) + Inhabit { + position_binding: PositionBinding, + blocks_other_actions: bool + }, + + /// Holding the tool—can still move, maybe limited (phone, hammer) + Wield { + hands_required: u8, + mobility: Mobility + }, + + /// Standing near the tool—tool is stationary (oven, workbench) + Station { + interaction_range: f32, + attention: Attention + }, + + /// Wearing the tool—passive benefit (coat, glasses) + Wear { + slot: WearSlot, + passive: bool + }, +} +``` + +### 4.3 Tools and Institutions + +When tools belong to an institution, they're part of the institution's **Resources** component. Multiple workers can use them, and their state is shared. + +```mermaid +flowchart TB + subgraph bakery["Bakery (Institution)"] + RES["Resources Component"] + + subgraph equipment["Shared Equipment"] + OVEN1["Oven #1
Status: In Use
Batch: Sourdough"] + OVEN2["Oven #2
Status: Available"] + COUNTER["Counter
Status: Martha using"] + PREP["Prep Station
Status: Alex using"] + end + + RES --> equipment + end + + subgraph workers["Workers query availability"] + M["Martha's BT:
Need oven?
→ Check institution"] + A["Alex's BT:
Need prep station?
→ Check institution"] + end + + M -->|"queries"| RES + A -->|"queries"| RES + + RES -->|"Oven #2 available"| M + RES -->|"Prep in use, wait"| A +``` + +This solves the "multiple workers, shared tools" problem: +1. Tools belong to the Institution's Resources +2. Worker BTs query the Institution for available tools +3. Institution tracks who's using what +4. No direct coordination between workers needed—the Institution mediates + +### 4.4 Example Tools + +| Tool | Capabilities | Operator Mode | Institution? | +|------|-------------|---------------|--------------| +| Car | Movement (10× speed, 4 capacity), Storage | Inhabit (blocks other actions) | Household or personal | +| Phone | Communication (global, async), Information | Wield (1 hand, full mobility) | Personal | +| Oven | Heat (high temp, good control), Transformation (raw → cooked) | Station (periodic attention) | Bakery | +| Hammer | Force (high magnitude, low precision) | Wield (1 hand, stationary while using) | Workshop or personal | +| Coat | Protection (cold, 0.8) | Wear (body slot, passive) | Personal | +| Cash Register | Information (transactions), Storage (money) | Station (full attention) | Shop | +| Plow | Transformation (soil → tilled), Force | Inhabit (with draft animal) | Farm | +| Loom | Transformation (thread → cloth) | Station (full attention) | Workshop | + +### 4.5 Tool Quality and Output + +Tool quality affects the output of behaviors that use them. This compounds with skill and input quality: + +```mermaid +flowchart LR + subgraph inputs["Inputs"] + SKILL["Skill Tier
Martha: Master (1.3×)"] + TOOL["Tool Quality
Oven: Fine (1.15×)"] + MAT["Material Quality
Flour: Good (1.1×)"] + end + + subgraph calc["Calculation"] + BASE["Base Quality: 1.0"] + inputs --> MULT["Multiply"] + BASE --> MULT + MULT --> FINAL["Final: 1.0 × 1.3 × 1.15 × 1.1
= 1.65 (Excellent)"] + end + + subgraph output["Output"] + FINAL --> BREAD["🍞 Excellent Bread
Higher value
Better reputation effect"] + end +``` + +```rust +fn calculate_output_quality( + base: f32, + skill: &Skill, + tool: &Tool, + materials: &[Material], + conditions: &[Condition], +) -> f32 { + let skill_mod = skill.quality_modifier(); + let tool_mod = tool.quality_modifier(); + let material_mod = materials.iter() + .map(|m| m.quality) + .sum::() / materials.len() as f32; + let condition_mod = conditions.iter() + .filter_map(|c| c.capability_modifier_for(CapabilityType::Work)) + .map(|m| m.quality_multiplier) + .product::(); + + base * skill_mod * tool_mod * material_mod * condition_mod +} +``` + +--- + +## 5. Production Workflows + +Complex activities like baking bread unfold across multiple actions and time spans. The system tracks in-progress work through **batch entities** that accumulate state across phases. + +### 5.1 Workflow Phases + +```mermaid +flowchart LR + subgraph prep["Preparation"] + D[Decide Batch] --> G[Gather Ingredients] + G --> P[Prepare Workspace] + P --> S[Start Batch] + end + + subgraph production["Production"] + S --> M[Mix Dough] + M --> R["Rise
(60 min)"] + R --> SH[Shape Loaves] + SH --> B["Bake
(45 min)"] + end + + subgraph completion["Completion"] + B --> U[Unload] + U --> ST[Stock Display] + end + + subgraph quality["Quality Accumulates"] + SK[Skill Tier] --> Q[Final Quality] + TQ[Tool Quality] --> Q + IQ[Ingredient Quality] --> Q + Q --> ST + end +``` + +### 5.2 Batch Entity + +A batch is itself an entity—it has lifecycle, state, and exists in the world (on a prep table, in an oven, on a cooling rack). + +```mermaid +stateDiagram-v2 + [*] --> NeedsMixing: batch created + NeedsMixing --> Rising: mixed + Rising --> NeedsShaping: rise complete + NeedsShaping --> NeedsBaking: shaped + NeedsBaking --> Baking: put in oven + Baking --> Done: bake complete + Done --> [*]: stocked or sold + + note right of Rising: Timer-based transition + note right of Baking: Timer-based transition +``` + +```rust +struct Batch { + recipe: RecipeId, + phase: BatchPhase, + phase_complete_at: Option, + quality_accumulator: f32, + quantity: u32, + location: Entity, // prep table, oven, cooling rack + assigned_worker: Option, + ingredients_used: Vec<(ResourceType, u32, f32)>, // type, amount, quality +} + +enum BatchPhase { + NeedsMixing, + Rising, + NeedsShaping, + NeedsBaking, + Baking, + Done, +} +``` + +### 5.3 Interruptible Work + +Batches persist across interruptions. If Martha starts mixing, gets interrupted by a customer, and returns—the batch is still there, waiting. + +```mermaid +sequenceDiagram + participant M as Martha + participant B as Batch + participant I as Institution + participant C as Customer + + M->>B: Start mixing + B->>B: Phase: NeedsMixing → Rising + Note over B: Timer starts: 60 min + + C->>I: Arrives at bakery + I->>M: Customer waiting (BT interrupt) + M->>M: BT: Customer > Batch in progress + M->>C: Serve customer + + Note over B: Still rising... + + M->>M: Customer done, BT re-evaluates + B-->>M: Phase complete signal + M->>B: Continue: Shape loaves +``` + +The institution's shared state lets any available worker continue a batch: + +```mermaid +sequenceDiagram + participant M as Martha + participant B as Batch + participant I as Institution + participant A as Alex (Apprentice) + + M->>B: Start batch, begin rising + M->>M: Break time (schedule) + M->>I: Release batch assignment + + Note over B: Rising complete + B->>I: Needs attention signal + + I->>A: Batch needs shaping + Note over A: BT: Available worker + A->>B: Continue: Shape loaves + Note over B: Quality modified by Alex's skill +``` + +### 5.4 Multi-Step Recipes + +Some recipes require coordination across multiple batches or workers: + +```mermaid +flowchart TB + subgraph cake["Wedding Cake Production"] + subgraph layer1["Layer Batches (parallel)"] + L1[Chocolate Layer] + L2[Vanilla Layer] + L3[Red Velvet Layer] + end + + subgraph frosting["Frosting Batch"] + F[Buttercream] + end + + subgraph assembly["Assembly (sequential)"] + layer1 --> STACK[Stack Layers] + frosting --> FROST[Apply Frosting] + STACK --> FROST + FROST --> DEC[Decorate] + end + + subgraph quality["Quality Factors"] + L1 --> Q[Final Quality] + L2 --> Q + L3 --> Q + F --> Q + DEC_SKILL[Decorator Skill] --> Q + end + end +``` + +The system handles this through **batch dependencies**: + +```rust +struct Recipe { + id: RecipeId, + name: String, + phases: Vec, + dependencies: Vec, + required_skills: Vec<(SkillType, SkillTier)>, + required_capabilities: Vec, +} + +struct BatchDependency { + /// This batch requires these other batches to complete first + requires: Vec, + /// How the inputs combine + combination: CombinationType, +} + +enum CombinationType { + Sequential, // one after another + Parallel, // can happen simultaneously + Assembly, // combine outputs into one +} +``` + +--- + +## 6. Performance Architecture + +With 500 entities and no teleportation, we need aggressive optimization on cognition while maintaining physical presence for all entities. The key insight is that *existing* is cheap, but *thinking* is expensive. Every entity is always somewhere, but only a few need to make decisions at any moment. + +### 6.1 Simulation Tiers + +Entities fall into different simulation tiers based on their relationship to players and their cognitive complexity. + +```mermaid +flowchart TB + subgraph tiers["Simulation Tiers"] + direction TB + + ATT["🎮 ATTACHED (1-4)
Players control these directly"] + + NEAR["👁️ NEARBY (~30-50)
Visible to players
Full behavior trees
Real-time decisions"] + + BACK["🌫️ BACKGROUND (~400)
Following schedules
Path interpolation
No active decisions"] + + WILD["🦌 WILDLIFE (~50+)
Mode-based behavior
Herd movement
Simplified needs"] + end + + ATT --> NEAR + NEAR --> BACK + BACK --> NEAR + + WILD -.->|"flee trigger"| NEAR +``` + +| Tier | Count | Movement | Cognition | Substrate | Update Rate | +|------|-------|----------|-----------|-----------|-------------| +| **Attached** | 1-4 | Player input | Player-driven | Full | Every frame | +| **Nearby** | ~30-50 | Full pathfinding | Full BT ticks | Full | Every frame | +| **Background** | ~400 | Path interpolation | Schedule-following | Batched | On wake events | +| **Wildlife** | ~50+ | Herd interpolation | Mode-based | Sampled | Every few seconds | + +#### Tier Transitions + +Entities move between tiers based on proximity to players. When a player approaches a background villager, that villager promotes to nearby tier and begins active decision-making. When players leave, the villager demotes back to background. + +The transition is seamless because the entity's *state* is always current—their position along their path, their current schedule block, their needs levels. Promotion just means "start running the behavior tree" rather than "reconstruct who this person is." + +```mermaid +sequenceDiagram + participant P as Player + participant V as Villager (Background) + participant S as Simulation + + Note over V: Following schedule via interpolation + P->>S: Moves toward V + S->>S: Distance check: P within 50m of V + S->>V: Promote to Nearby + Note over V: BT activates, evaluates current situation + V->>V: "I'm in Work block, at bakery, batch needs tending" + V->>V: Begin active baking behavior + + P->>S: Moves away + S->>S: Distance check: P beyond 80m of V + S->>V: Demote to Background + Note over V: BT suspends, resume interpolation +``` + +The hysteresis (promote at 50m, demote at 80m) prevents thrashing when players are near the boundary. + +### 6.2 The Wildlife Tier + +Wild animals like deer need a different approach. They're not following schedules—they don't have appointments or jobs. But they're also not complex enough to warrant full behavior trees. + +Wildlife uses **mode-based behavior**: a simplified decision system that picks one of a few behavioral modes based on needs and instincts, then follows that mode until something changes. + +```mermaid +stateDiagram-v2 + [*] --> Grazing: default + + Grazing --> Fleeing: threat detected + Grazing --> Resting: rest need high + Grazing --> Drinking: thirst need high + Grazing --> Migrating: seasonal trigger + + Fleeing --> Grazing: threat gone, safe distance + Resting --> Grazing: rested enough + Drinking --> Grazing: thirst satisfied + Migrating --> Grazing: reached destination + + Resting --> Fleeing: threat detected + Drinking --> Fleeing: threat detected +``` + +**Mode evaluation happens infrequently**—every 10-30 seconds, or when a significant event occurs (threat appears, need crosses threshold). Between evaluations, the animal simply continues its current mode behavior. + +**Herd movement** further reduces computation. Rather than pathfinding for each deer individually, we pathfind for the herd centroid and have individuals maintain positions relative to the herd using simple flocking rules. One pathfinding operation serves a dozen animals. + +#### Wildlife and Players + +When a player approaches wildlife, the animals don't promote to "nearby" in the same way villagers do. Instead, the player's presence triggers a **threat evaluation**: + +1. Player enters detection range (species-specific, ~30-50m for deer) +2. Wildlife mode immediately evaluates → switches to Fleeing +3. Flee pathfinding runs once, animals scatter +4. Once at safe distance, mode returns to previous + +This is much cheaper than full BT promotion because: +- No complex decision-making—just "threat? flee" +- Flee paths are simple (away from threat) rather than goal-seeking +- No need to maintain the wildlife at nearby-tier cognition + +#### Wildlife and Substrate + +Wildlife interacts with the world substrate, but in a sampled way: + +- **Grazing depletes foliage** at their current location (your garden suffers) +- **Movement creates trigger volumes** that predators and players can detect +- **Seasonal state** affects migration instincts globally + +These interactions happen when mode transitions occur or on a slow timer, not continuously. + +### 6.3 Discrete Event Simulation + +The traditional game loop polls every entity every frame: "Do you have anything to do?" This is wasteful—most entities most of the time answer "no, still walking to the bakery." + +Instead, we use **discrete event simulation**: compute when interesting things will happen, schedule wake-up events, and sleep until then. The simulation advances by processing events in time order rather than by checking everyone constantly. + +```mermaid +flowchart LR + subgraph once["Computed Once (at path start)"] + PATH["Compute A* path"] --> QUERY["Query spatial index:
What will I intersect?"] + QUERY --> SCHEDULE["Schedule wake events:
• Puddle at t=2.3s
• Door at t=4.1s
• Arrive at t=5.0s"] + end + + subgraph runtime["Per Frame"] + TIME["Current time"] --> QUEUE{"Wake queue:
anything ready?"} + QUEUE -->|"yes"| WAKE["Wake entity,
run BT tick"] + QUEUE -->|"no"| SKIP["Skip"] + WAKE --> NEXT["Schedule next wake"] + end +``` + +When Martha starts walking from home to the bakery: + +1. Pathfinding computes her route +2. The system raycasts the path against trigger volumes (doors, puddles, zone boundaries) +3. Wake events are scheduled: "Martha enters bakery zone at 5:52am" +4. Martha sleeps—her position interpolates along the path, but no code runs for her +5. At 5:52am, the wake event fires, her BT activates: "I'm at work, what should I do?" + +This means 400 background villagers might generate only a few dozen wake events per second total, rather than 400 updates per frame. + +### 6.4 Wake Reasons + +Entities wake for different reasons, and the reason affects what happens: + +| Wake Reason | Trigger | Response | +|-------------|---------|----------| +| **Path Complete** | Arrived at destination | BT evaluates: what now? | +| **Schedule Event** | Time for next activity | Possibly generate new path, update activity | +| **Trigger Volume** | Crossed into a zone | Apply zone effects (enter puddle, enter building) | +| **External Interrupt** | Player approached, someone spoke to them | Promote tier if needed, respond to stimulus | +| **Condition Change** | Got sick, condition ended | Regenerate schedule, modify BT priorities | +| **Institution Signal** | Bakery needs attention, customer arrived | Check if this entity should respond | + +```mermaid +sequenceDiagram + participant W as Wake Queue + participant E as Entity (sleeping) + participant BT as Behavior Tree + participant S as Schedule + + Note over E: Interpolating position... + W->>E: Wake: ScheduleEvent (Work block starts) + E->>S: What's my current block? + S-->>E: Work at Bakery, 6:00-14:00 + E->>BT: Evaluate work behaviors + BT-->>E: Start batch (stock is low) + E->>W: Schedule next wake: batch needs attention in 60min + Note over E: Returns to sleep, position interpolates +``` + +### 6.5 Institution as Shared Blackboard + +When multiple entities work at the same institution, they need to coordinate without direct communication. The institution's state acts as a **shared blackboard**—a place where information is posted that any worker can read. + +```mermaid +flowchart TB + subgraph bakery["Bakery Institution State (Blackboard)"] + STATUS["Status: Open"] + QUEUE["Customer Queue: 3 waiting"] + BATCHES["Active Batches:
• Sourdough (Rising, 20min left)
• Baguettes (Baking, 5min left)"] + STOCK["Stock Levels: Low on croissants"] + EQUIPMENT["Equipment:
• Oven 1: In use
• Oven 2: Available"] + end + + M["Martha's BT"] -->|"reads"| bakery + A["Alex's BT"] -->|"reads"| bakery + J["Jamie's BT"] -->|"reads"| bakery + + M -->|"writes"| BATCHES + A -->|"writes"| BATCHES + J -->|"writes"| QUEUE +``` + +When Martha's BT runs, it doesn't ask "what is Alex doing?" It asks the bakery: "are there customers? is stock low? are batches in progress?" The answers come from the shared state, which Alex and Jamie also read and write. + +This is how multiple workers naturally coordinate without explicit communication: +- Jamie sees customers in queue → serves them +- Martha sees low stock → starts a batch +- Alex sees batch needs shaping → shapes it + +No one tells anyone what to do. They all read the same blackboard and respond to what needs doing. + +### 6.6 Background Entity Fidelity + +Background entities aren't "paused"—they're still living their lives, just without active decision-making. Their fidelity comes from: + +**Schedule adherence:** A background baker still arrives at work at 6am, takes breaks, goes home. Their position interpolates along pre-computed paths between schedule waypoints. + +**Need simulation:** Needs still tick (slowly, in batched updates). If a background entity's hunger becomes critical, it triggers a wake event to handle it. + +**Condition progression:** Sickness still runs its course. A background entity can get better (or worse) without being in nearby tier. + +**Institution participation:** Background workers still contribute to institution state. Martha being at the bakery (even as background) means the bakery has its master baker present—affecting quality, capacity, customer satisfaction. + +The goal is that when a player approaches any entity, that entity's state is *plausible*—they're where they should be, doing what they should be doing, with needs and conditions that make sense for their day so far. + +--- + +## 7. Synchronization and Networking + +Aspen uses peer-to-peer networking with CRDTs—there's no central server deciding what's true. Each player's device simulates the world, and they synchronize state to stay consistent. This creates interesting challenges for agent simulation: how do 500 villagers stay coherent across 2-4 peers without a server arbitrating? + +The good news is that the four-layer architecture maps well to different sync strategies. Slow-changing state syncs directly; fast-changing state uses determinism. + +### 7.1 Sync Strategy by Layer + +```mermaid +flowchart TB + subgraph layers["Simulation Layers"] + LA["Life Arc
───────────
Changes: monthly
Sync: CRDT state"] + + SCHED["Daily Schedule
───────────
Changes: daily
Sync: CRDT state"] + + BT["Behavior Tree
───────────
Changes: per-second
Sync: Outcomes as state"] + + SUB["World Substrate
───────────
Changes: continuous
Sync: Mixed"] + end + + subgraph strategy["Sync Strategy"] + LA -->|"Last-Writer-Wins"| CRDT1["CRDT Register"] + SCHED -->|"Last-Writer-Wins"| CRDT2["CRDT Register"] + BT -->|"Decisions become state"| CRDT3["CRDT Updates"] + SUB -->|"Spatial partitioning"| HYBRID["Ownership + CRDTs"] + end +``` + +| Layer | Change Rate | Sync Method | Conflict Resolution | +|-------|-------------|-------------|---------------------| +| **Life Arc** | Rare (monthly) | CRDT register | Last-writer-wins; conflicts are rare | +| **Schedule** | Daily | CRDT register | Regenerate from same inputs | +| **Behavior Tree** | Real-time | Outcomes as CRDT state | Entity owner is authoritative | +| **Substrate** | Continuous | Spatial ownership | Owner broadcasts; others apply | + +### 7.2 What Syncs Directly + +Some state is small, changes rarely, and syncs trivially as CRDT registers: + +**Life arc state:** Martha's life arc (Adult/Partnered/Baker) is a single enum. When it transitions, the new state syncs. Conflicts are nearly impossible—life arc transitions are triggered by game events (birthday, marriage) that themselves sync. + +**Relationships:** Bond strength, coordination level, shared commitments. These change slowly through accumulated interactions. CRDT counters work well for bond strength; registers for discrete state. + +**Institution state:** The bakery's reputation, operating hours, worker roster. Changes infrequently, syncs as registers. + +**Conditions:** Martha has the flu. This is a register—condition type, severity, start time, expected duration. When she recovers, the condition is removed. + +**Schedules:** Each entity's daily schedule is generated once per game-day. The schedule itself is data—a list of (time, activity, location) tuples. It syncs as a register, regenerated each morning. + +### 7.3 Behavior Tree Outcomes + +Behavior trees tick locally and produce **outcomes**—state changes that sync like any other state. We're not syncing the tree traversal; we're syncing what the entity decided to do. + +```mermaid +sequenceDiagram + participant P1 as Peer 1 (simulating Martha) + participant CRDT as Shared State + participant P2 as Peer 2 + + Note over P1: Martha's BT ticks + P1->>P1: Evaluate: stock low, start batch + P1->>CRDT: Update: Martha.current_action = StartBatch + P1->>CRDT: Update: Bakery.batches += Sourdough + CRDT->>P2: Sync state changes + + Note over P2: Peer 2 sees Martha is now
working on sourdough batch +``` + +When Martha's BT decides "start a sourdough batch," that decision produces state changes: +- Martha's current action updates +- A new batch entity is created +- Bakery inventory decrements (flour, yeast consumed) +- Equipment status updates (mixer in use) + +All of these are normal CRDT state changes. Peer 2 doesn't run Martha's BT independently—they receive the outcome and update their local view. + +This is simpler and more robust than trying to achieve deterministic BT evaluation across peers. The BT is just local decision-making; the results are what matter for synchronization. + +### 7.4 Wake Queue Synchronization + +The wake queue is trickier. Each peer maintains its own queue of (time, entity, reason) events. If the queues diverge, entities will wake at different times and make different decisions. + +**Solution: Derive wake events from synced state.** + +Wake events aren't arbitrary—they're computed from paths, schedules, and trigger volumes. If two peers have: +- The same path for Martha (derived from same schedule) +- The same trigger volumes in the world (synced substrate) +- The same entity positions (interpolated from synced paths) + +Then they'll compute the same wake events. + +```mermaid +flowchart LR + subgraph synced["Synced State"] + SCHED["Schedule:
Martha goes to bakery at 5:45"] + MAP["World:
Trigger volumes, paths"] + end + + subgraph derived["Derived (not synced)"] + PATH["Path computation:
Home → Bakery"] + WAKE["Wake events:
• Enter bakery zone at 5:52"] + end + + synced --> derived + + subgraph peers["Both Peers"] + P1["Peer 1: computes same path,
same wake events"] + P2["Peer 2: computes same path,
same wake events"] + end + + derived --> peers +``` + +The wake queue itself never syncs—it's locally computed from synced inputs. + +### 7.5 Spatial Ownership and the Substrate + +The world substrate (puddles, temperatures, trigger volumes) changes frequently and needs spatial partitioning. We use the same ownership model as the terrain and buildings: + +**Tiles have owners.** The peer who owns a tile is authoritative for substrate state on that tile. When Martha spills a drink, the owner of that tile creates the puddle and broadcasts its existence. + +**Ownership follows players.** Tiles near a player are owned by that player's peer. When players are apart, they own different regions. When players are together, one peer owns the shared space (typically whoever arrived first or has priority). + +**Substrate changes broadcast.** When an owner creates, modifies, or removes substrate state, they send a delta to other peers. Others apply it without question—the owner is authoritative. + +```mermaid +sequenceDiagram + participant P1 as Peer 1 (owns tile) + participant P2 as Peer 2 + participant SUB as Substrate + + Note over P1: Martha (P1's villager) spills drink + P1->>SUB: Create puddle at (x, y) + P1->>P2: Broadcast: Puddle created at (x, y) + P2->>SUB: Apply: Create puddle at (x, y) + + Note over P1,P2: Both peers now have puddle
Cat pathfinding will intersect it +``` + +### 7.6 Nearby Tier and Entity Ownership + +What happens when Peer 1's player is near a villager, but Peer 2's player is far away? + +**Entities have owners.** Like tiles, entities are owned by one peer at a time. The owner runs the BT and syncs outcomes. Ownership typically follows proximity—if your player is nearest to Martha, you own Martha. + +When Peer 1 owns Martha: +- Peer 1 runs Martha's BT at full fidelity +- BT decisions produce state changes that sync to Peer 2 +- Peer 2 sees Martha's actions but doesn't run her BT + +When players are together, one peer owns the nearby villagers. When apart, ownership naturally distributes—each peer owns the villagers near their player. + +```mermaid +sequenceDiagram + participant P1 as Peer 1 (owns Martha) + participant M as Martha's State (CRDT) + participant P2 as Peer 2 + + Note over P1: Player 1 near Martha + P1->>P1: Run Martha's BT + P1->>M: Update: current_action = ServingCustomer + M->>P2: Sync + + Note over P2: Peer 2 sees Martha serving
but didn't run the decision +``` + +Ownership can transfer when players move: +1. Peer 1's player walks away from Martha +2. Peer 2's player approaches Martha +3. Ownership transfers: Peer 2 now runs Martha's BT +4. Martha's state is already synced—Peer 2 picks up where Peer 1 left off + +### 7.7 Institution State Across Peers + +Institution state (bakery's customer queue, active batches, inventory) is shared by multiple entities and both peers might have workers there. + +**Institution state lives in CRDTs:** +- Customer queue: CRDT sequence (ordered list) +- Inventory: CRDT map of item → count +- Active batches: CRDT map of batch_id → batch state +- Equipment status: CRDT map of equipment_id → (in_use, user) + +When Martha (Peer 1's nearby villager) starts a batch, Peer 1 updates the CRDT. The change propagates to Peer 2. Alex (Peer 2's nearby villager) sees the new batch and doesn't duplicate the work. + +```mermaid +sequenceDiagram + participant P1 as Peer 1 + participant CRDT as Bakery State (CRDT) + participant P2 as Peer 2 + + Note over P1: Martha's BT: Stock is low, start batch + P1->>CRDT: Add batch: Sourdough (NeedsMixing) + CRDT->>P2: Sync: New batch added + + Note over P2: Alex's BT: Stock is low, but...
batch already started, assist instead + P2->>CRDT: Update batch: Alex assisting +``` + +The institution blackboard pattern works across peers because the blackboard is the CRDT—both peers read and write the same shared state. + +--- + +## 8. Open Questions + +Several design questions remain unresolved: + +### 8.1 Child Agency Transitions + +The RFC establishes that children have external schedule authority that gradually shifts toward autonomy through adolescence. But the specific mechanics need design: + +- At what ages do specific autonomy capabilities unlock? +- How do parent-child conflicts resolve? (Teenager refuses bedtime) +- What happens when parents are absent? (Orphans, neglect) +- How do custody arrangements work after separation? + +### 8.2 Relationship Formation + +Relationships evolve through interaction, but the specific mechanics of relationship formation need work: + +- What triggers the transition from Acquaintance to Friendly? +- How does romantic attraction emerge? (Personality compatibility? Proximity? Random chance?) +- How do negative relationships form? (Rivals, enemies) +- Can relationships form between background-tier entities, or only when nearby? + +### 8.3 Institution Founding and Collapse + +Institutions have lifecycles, but who founds them and how? + +- Can villagers autonomously start businesses? +- What triggers institution collapse? (Owner dies, bankruptcy, obsolescence) +- How do institutions transfer ownership? +- Can players found institutions, or only villagers? + +### 8.4 Wildlife Ecology + +The wildlife tier handles individual animals, but ecosystems need more thought: + +- How do predator-prey relationships work? +- Do populations grow and shrink over time? +- How does wildlife interact with villager activities? (Hunting, farming, domestication) +- Seasonal migration at population scale + +### 8.5 Condition Spread + +Contagious conditions spread through the substrate, but the dynamics need tuning: + +- How quickly do diseases spread through a population? +- Are there immune responses, vaccination concepts? +- How do villagers respond behaviorally to epidemics? (Avoidance, quarantine) +- Can conditions affect institutions? (Bakery closed due to illness) + +### 8.6 Schedule Conflicts + +The coordination protocol handles shared commitments, but conflicts can still arise: + +- What if two commitments overlap? (Work shift vs child's school event) +- How do emergencies override schedules? (Fire, injury, death in family) +- How much personality affects willingness to reschedule? +- Can villagers develop resentment from repeated schedule conflicts? + +--- + +## 9. Summary + +The agent simulation architecture separates concerns by temporal scale: + +| Layer | Scope | Update Rate | Sync Strategy | +|-------|-------|-------------|---------------| +| **Life Arc** | What's possible | Monthly | CRDT register | +| **Daily Schedule** | Where and when | Daily | CRDT register | +| **Behavior Tree** | What to do now | Real-time | Outcomes as CRDT state | +| **World Substrate** | Environmental state | Continuous | Spatial ownership | + +**Entities compose from orthogonal components** rather than inheriting from class hierarchies: +- Universal: Lifecycle +- Physical: PhysicalPresence +- Agency: Needs, Instincts, Roles, Autonomy +- Social: Relationships, Membership +- Interaction: Operable, Stateful +- Institution: Governance, Resources, Reputation + +**Institutions coordinate multiple entities** through shared blackboard state. Workers don't communicate directly—they read and write shared state (customer queue, inventory, active batches) and respond to what needs doing. + +**Relationships emerge from interaction** and enable coordination. Bond strength unlocks coordination levels, from ad-hoc activities to cohabiting schedules. Shared commitments appear in both parties' schedules without real-time negotiation. + +**Conditions temporarily modify all layers**—schedule overrides, need modifiers, capability reductions, behavior priorities. Sickness isn't a special case; it's a condition that affects Martha like any other condition affects any other entity. + +**Performance comes from discrete event simulation**: compute wake events up front, sleep until they happen, promote to full cognition only when players are nearby. Four simulation tiers handle different entity types: +- Attached (players): full control +- Nearby (visible villagers): full BT +- Background (distant villagers): interpolation + wake events +- Wildlife (animals): mode-based + herd movement + +**Networking syncs outcomes, not process.** Behavior trees tick locally and produce state changes that sync via CRDT—we don't try to make peers walk the same tree in lockstep. The wake queue is derived from synced state (schedules, paths, triggers). Institutions are CRDT-backed shared blackboards that work across peers. + +The goal is Dwarf Fortress depth with iPad sustainability—rich emergent behavior from simple generic rules, not hand-coded special cases. A cat dies from alcohol poisoning not because we wrote cat-alcohol logic, but because substances transfer on contact, grooming consumes substances, and alcohol affects small bodies strongly. Martha coordinates dinner with her spouse not through complex dialogue, but through shared commitments resolved during schedule generation. + +**Life has texture—consequences without failure.** Sickness disrupts schedules and triggers care from loved ones. Institutions struggle and thrive. Relationships deepen and fray. Children grow into autonomy. The village feels alive because its inhabitants are living, not performing.