⚠️ Restored to the live tree 2026-08-26 — one required field is missing and one whole vocabulary is absent.
⚠️ max_active_effectsis conditionally REQUIRED whenever effects are declared, and this 1700-line document mentions it zero times. Omit it and the pack is refused.
⚠️ Thefor_eachcollection vocabulary is missing, and described as "future" while it ships. Measured occurrence counts in this file:all_agents0,switch0,delay0,sample0.for_each: all_agentsis the load-bearing surface for expressing a declarative step over simultaneous agent submissions, and it is not mentioned once — both PRD-0001 Trial O executors reached it only by readingsrc/townlet/effects/collections.py. Tracked ashamlet-7eadeb214c.
⚠️ temporal.*does NOT work in effects — the single reconciled account (resolved at source 2026-08-26, because this doc andexpressions.mddisagreed):
- The effects executor passes
temporal={}(effects/executor.py:43,:739), sotemporal.anythingraises here at runtime.- More fundamentally, no schema builder emits a
temporal.*key at all —build_expression_schema(universe/compilers/vfs.py:136-152) emits onlybar.*,vfs.*,self.vfs.*,target.vfs.*— so it fails at compile time first.- Only the item spawn-rule path populates it, with the single key
tick(items/manager.py:624,:630).- ✅ Correct working form: the bare
tickvariable, the reserved engine-written step counter (universe/compilers/vfs.py:130-132). Nottemporal.tick.Tracked as
hamlet-7eadeb214c(comment 262 carries themax_active_effectsandtemporal.*findings).Note
sqrt/sin/cosfail at type-check, not parse — they are absent fromFUNCTION_SPECS. Seeexpressions.md, which carries a banner of its own.
Purpose: Declarative effect definitions with command pipeline language for HAMLET simulation behavior
When to Read: Working with effects system, affordance interactions, item behaviors, temporal mechanics, or any state mutation logic
AI-Friendly Summary: The Effects System is HAMLET's foundational command pipeline language for all simulation behavior. It provides declarative, reusable effect definitions (buffs/debuffs) that attach to agents, items, affordances, or global world state. Each effect has lifecycle hooks (on_spawn, on_tick, on_despawn, on_interrupt) that execute command pipelines for state mutation. Effects support four reapply policies (stack, renew, merge, replace) and use VFS expression language for all value computations. Commands include modify, spawn_effect, spawn_item, if/else conditionals, and for_each loops. All commands are compile-time validated for path correctness and type safety. Effects are the bedrock of World Compiler (T0 Pillar 3) - bars, VFS, cascades, items, and affordances all use effects for mutation logic.
Reading Strategy:
- Quick Reference: Jump to "Field Reference" for specific field documentation
- Examples: See "Effect Examples" section for real patterns
- Command Types: Read "Command Pipeline Language" for all available operations
- First-Time Users: Read "Overview" → "File Structure" → "Reapply Policies" → "Command Language" → "Examples"
Related Documents:
docs/zzz. archive/plans/archive/vfs_uplift/2025-11-19-effects-system-design.md- Complete design documentdocs/plans/vfs_uplift/2025-11-19-task-3-1-effects-dtos.md- Implementation plandocs/config-schemas/variables.md- VFS variable definitionsdocs/config-schemas/affordances.md- Affordance configuration (uses effects)- Expression language: See
docs/plans/vfs_uplift/2025-11-19-task-1-2-expression-parser.md
Scope: Optional pack-scope effects declaration, conventionally <config_pack>/effects.yaml.
The bare effect_definitions shape is recognized by content; fragments may live in nested
.yaml/.yml documents. Effect identifiers must be unique across the scope, with both origins
reported on collision. See declaration discovery.
Status: Phase 3 Complete + Phase 4 Command Extensions Implemented
Pattern: Effects is the foundational command language for ALL simulation behavior. All behavioral parameters must be explicitly specified (no-defaults principle) to ensure reproducibility.
The Effects System is HAMLET's declarative command pipeline language that provides a unified way to express all simulation behavior: affordance interactions, item dynamics, temporal changes, meter cascades, and agent state mutations.
Effects are reusable, time-limited modifications to entity state - similar to buffs and debuffs in games like World of Warcraft or status effects in tabletop RPGs. They:
- Attach to agents, items, affordances, or global world state
- Execute commands at lifecycle stages (spawn, tick, despawn)
- Have explicit duration (auto-despawn when timer expires)
- Support stacking policies (stack, renew, merge, replace)
- Are compiled once and reused many times
Examples:
- Agent buffs: "caffeinated" (energy regen), "inspired" (mood boost), "wet" (hygiene penalty)
- Agent debuffs: "poisoned" (health damage), "exhausted" (energy drain), "sick" (all stats reduced)
- Item states: "spoiled" (food decay), "rusty" (tool degradation), "enchanted" (bonus effects)
- World state: "nighttime" (global visibility modifier), "rush_hour" (affordance availability)
- Reusable Effect Catalog: Define effects once, reference by ID everywhere
- Command Pipeline Execution: Ordered, explicit commands at lifecycle stages
- VFS Expression Language: Pure functional expressions for all value computations
- Compile-Time Validation: Type checking for paths, references, and expressions
- Scope-Aware Context: Effects attach to global/agent/item/affordance with appropriate access
- No Hidden Defaults: All parameters explicit (duration, intensity, policy)
Effects is the bedrock of the World Compiler (T0 Pillar 3):
- Compiled First: Before bars, VFS, cascades, items, affordances
- Used By All: All other World components use effects for mutation logic
- Type-Safe: Expression ASTs compiled at load time, validated against VFS schema
- GPU-Native: Executes on GPU tensors via CommandExecutor
- Declarative Configuration: Change simulation behavior without code changes
- Reproducibility: Effects part of compiled world with provenance tracking
- Composability: Mix and match effects across affordances, items, and agents
- Pedagogical Value: Expose complex behaviors (stacking, merging, timing) as teaching moments
- Performance: Pre-compiled expression ASTs, vectorized execution
version: "1.0"
effect_definitions:
- id: string # Unique effect identifier
description: string # Human-readable description (optional)
scope: global|agent|item|affordance # Where effect attaches
duration: int # Ticks until auto-despawn (REQUIRED)
intensity: float # Default intensity multiplier (default: 1.0)
reapply_policy: stack|renew|merge|replace # Reapply behavior (REQUIRED)
observable: bool # Whether agents can observe this effect (default: true)
# Lifecycle command pipelines
on_spawn: CommandConfig[] # Execute when effect spawned
on_tick: CommandConfig[] # Execute each step while active
on_despawn: CommandConfig[] # Execute before removal
on_interrupt: CommandConfig[] # Execute on forced removalRequired Fields:
id: Unique identifier for referencing this effectscope: Determines attachment point and available contextduration: Lifecycle length (auto-despawn when reaches 0)reapply_policy: Behavior when same effect spawned on same target- All command pipeline fields (
on_spawn,on_tick,on_despawn,on_interrupt) must be present (can be empty lists)
Optional Fields:
description: Documentation stringintensity: Default 1.0 (can be overridden at spawn time)observable: Default true (visible in agent observations)
Type: str
Required: Yes
Example: version: "1.0"
Effects schema version. Always "1.0" for current implementation.
Validation: Must be "1.0"
Type: list[EffectDefinition]
Required: Yes (can be empty list)
Example: See "Effect Examples" section
List of effect definitions. Each effect is a reusable simulation behavior with lifecycle hooks.
Type: str
Required: Yes
Example: id: "energy_regen"
Unique identifier for this effect. Used to spawn effects via spawn_effect(effect_id=...) or reference in affordance/item configs.
Validation:
- Must be unique across all effects in catalog
- Recommended naming: lowercase with underscores
- Must match regex:
^[a-z_][a-z0-9_]*$
Use Cases:
- Affordances spawn effects when interacted with
- Items apply effects when used
- Other effects spawn secondary effects
- Cascades trigger conditional effects
- Temporal systems apply periodic effects
Type: str
Required: No
Example: description: "Regenerates energy over time"
Human-readable description of effect behavior. Used for documentation and debugging.
Best Practices:
- Describe the observable behavior, not implementation details
- Mention key parameters (duration, intensity impact)
- Note any special interactions or side effects
Type: "global" | "agent" | "item" | "affordance"
Required: Yes
Example: scope: agent
Determines where the effect can attach and what context is available to commands.
Scope Semantics:
agent - Effect attaches to individual agents
- Access:
target.bar.*,target.vfs.*,target.position - Storage: Per-agent effect list
[num_agents] - Examples:
energy_regen,poison,caffeinated,hungry - Typical Duration: 1-100 ticks
item - Effect attaches to item instances
- Access:
target.vfs.*,target.position,target.holder_agent - Storage: Per-item effect list
[num_items] - Examples:
item_decay,flaming,cursed,enchanted - Typical Duration: 10-1000 ticks (long-lived items)
global - Effect attaches to world state
- Access:
global.vfs.*,global.bar.*(if global bars exist) - Storage: Single global effect list
- Examples:
day_cycle,nighttime,heatwave,rush_hour - Typical Duration: 100-10000 ticks (persistent world state)
affordance - Effect attaches to affordance instances
- Access:
target.vfs.*,target.position,target.availability - Storage: Per-affordance effect list
[num_affordances] - Examples:
broken,locked,occupied,on_cooldown - Typical Duration: 5-50 ticks (temporary state changes)
Path Resolution:
target.*resolves to the entity the effect is attached to (scope-dependent)global.*always resolves to world stateself.*resolves to the effect instance itself (for effect-local state)temporal.*provides current tick, time-of-day, etc.
Type: int (positive)
Required: Yes
Example: duration: 20
Number of ticks before effect auto-despawns. Counts down each time effect_manager.tick() is called.
Lifecycle:
- Spawn at tick N:
duration_remaining = duration,elapsed_ticks = 0 - Each tick:
duration_remaining -= 1,elapsed_ticks += 1 - Execute
on_tickcommands whileduration_remaining > 0 - When
duration_remaining == 0: Executeon_despawn, remove from active effects
Special Values:
duration: 1- Instant effect (only on_spawn executes, then immediate despawn)
Validation: Must be positive integer
Interaction with Reapply Policies:
renewpolicy resetsduration_remainingtodurationmergepolicy does NOT affect duration (only intensity)stackcreates new instance with own durationreplacedespawns old (ending its duration) and spawns new
Type: float
Required: No (default: 1.0)
Example: intensity: 1.0
Default intensity multiplier for effect strength. Can be overridden when spawning via spawn_effect(..., intensity=2.5).
Usage in Commands:
on_tick:
- modify: target.bar.energy
value: "target.bar.energy + (0.05 * intensity)" # intensity is available as variableTypical Patterns:
- Scaling Effects: Multiply all value changes by intensity
- Threshold Effects: Use intensity in conditionals (
if intensity > 2.0 then ...) - Diminishing Returns:
sqrt(intensity)orlog(1 + intensity)
Interaction with Reapply Policies:
stack: Each instance has own intensity (can differ)renew: Keeps existing intensity (or uses new if higher)merge:intensity += new_intensity(cumulative stacking)replace: Old intensity discarded, new intensity used
Validation: Must be non-negative float
Type: "stack" | "renew" | "merge" | "replace"
Required: Yes
Example: reapply_policy: renew
Determines behavior when spawn_effect(effect_id) is called on entity that already has this effect active.
Policy Details:
Behavior: Each application creates a new, independent effect instance
reapply_policy: stack
# Timeline:
# Tick 1: spawn_effect("health_boost", duration=5) → Instance A (expires tick 6)
# Tick 3: spawn_effect("health_boost", duration=5) → Instance B (expires tick 8)
# Result: Both instances tick independently, both execute commandsUse Cases:
- Food digestion cycles (each meal has own timer)
- Damage-over-time stacking (multiple poison sources)
- Buff stacking (multiple temporary stat boosts)
Performance: O(num_instances) - can create many instances if spammed
Command Execution: All instances execute their commands (effects compound!)
Behavior: Reset duration_remaining to duration, keep single instance
reapply_policy: renew
# Timeline:
# Tick 1: spawn_effect("energy_regen", duration=20) → Instance A (expires tick 21)
# Tick 15: spawn_effect("energy_regen", duration=20) → Instance A refreshed (now expires tick 35)
# Result: Single instance, timer extendedUse Cases:
- "Well fed" status (eating extends the buff)
- Regeneration effects (activity refreshes the healing)
- Temporary buffs that extend with repeated application
Performance: O(1) - always single instance
Command Execution: Single execution per tick (no compounding)
Implementation Detail: Intensity can optionally be updated to max(old, new)
Behavior: Accumulate intensity, keep single instance
reapply_policy: merge
# Timeline:
# Tick 1: spawn_effect("poison", intensity=1.0, duration=20) → Instance A (intensity=1.0)
# Tick 5: spawn_effect("poison", intensity=0.5, duration=20) → Instance A (intensity=1.5)
# Result: Single instance, stronger effectUse Cases:
- Cumulative drug dosage (multiple applications stack strength)
- Poison stacking (multiple sources increase damage rate)
- Debuff accumulation (repeated failures worsen penalty)
Performance: O(1) - always single instance
Command Execution: Single execution per tick, but with higher intensity (commands scale by intensity)
Best Practice: Use intensity in expressions for proper scaling:
on_tick:
- modify: target.bar.health
value: "target.bar.health - (0.02 * intensity)" # Scales with mergesBehavior: Despawn existing instance (execute on_despawn), spawn new instance (execute on_spawn)
reapply_policy: replace
# Timeline:
# Tick 1: spawn_effect("currently_eating", duration=10) → Instance A
# Tick 5: spawn_effect("currently_eating", duration=10) → Instance A despawned, Instance B spawned
# Result: Only newest instance existsUse Cases:
- "Currently doing X" status (only one action at a time)
- Replacing buffs (new overwrites old completely)
- State transitions (entering new state exits previous)
Performance: O(1) - always single instance
Command Execution: Old effect's on_despawn executes, new effect's on_spawn executes
Important: Despawn is explicit (not silent) - useful for cleanup logic
Type: bool
Required: No (default: true)
Example: observable: true
Whether agents can observe this effect in their observations.
True - Effect visible to agent:
observable: true
# Examples: "hungry", "wet", "caffeinated", "injured"
# Agent should know about these for decision-makingFalse - Effect hidden from agent:
observable: false
# Examples: "in_trouble_at_work", "cursed", "monitored", "marked_for_promotion"
# Agent cannot directly observe, but may infer from consequencesFuture Integration: Observable effects will be added to observation spec by VFS observation builder.
Pedagogical Value: Hidden effects teach agents to infer latent state from consequences (e.g., "Why is my boss mad? Oh, I must be in trouble")
Type: list[CommandConfig]
Required: Yes (can be empty list)
Example:
on_spawn:
- modify: target.bar.health
value: "target.bar.health + 0.2"Command pipeline executed immediately when effect spawned. Executes before first tick.
Execution Order:
- Effect instance created
on_spawncommands execute- Effect added to active effects
- (Later)
on_tickcommands execute each step
Typical Uses:
- Instant stat changes (health boost, energy penalty)
- Set initial flags (
target.vfs.is_buffed = true) - Trigger secondary effects (
spawn_effect: "visual_glow") - Initialization logic for complex effects
Performance: Executes once per spawn (not every tick)
Empty List: Valid pattern for effects with no spawn logic (all behavior in on_tick)
Type: list[CommandConfig]
Required: Yes (can be empty list)
Example:
on_tick:
- modify: target.bar.energy
value: "target.bar.energy + (0.05 * intensity)"Command pipeline executed every tick while effect active. Core effect behavior.
Execution Order:
effect_manager.tick()called (inenv.step()after action execution)- For each active effect: execute
on_tickcommands - Decrement
duration_remaining - If
duration_remaining == 0, executeon_despawnand remove
Typical Uses:
- Regeneration/degeneration (bars change over time)
- Periodic checks (if conditions then trigger)
- State updates (modify VFS variables based on current state)
- Temporal logic (day/night cycles, time-based state changes)
Performance: Executes every tick for every active effect - keep command lists short!
Empty List: Valid for instant-only effects (all logic in on_spawn)
Important: Effects tick AFTER action execution and AFTER cascades, so they can react to current state.
Type: list[CommandConfig]
Required: Yes (can be empty list)
Example:
on_despawn:
- modify: target.vfs.is_buffed
value: falseCommand pipeline executed immediately before effect removed from active effects.
Execution Triggers:
duration_remainingreaches 0 (natural expiration)reapply_policy: replacetriggered (old effect replaced)- Entity destroyed (cleanup on death/removal)
Typical Uses:
- Cleanup flags (
target.vfs.is_buffed = false) - Reversal of on_spawn changes (if not handled by duration naturally)
- Final effects (penalty for buff expiring, reward for surviving duration)
- Logging/analytics (record effect completion)
Performance: Executes once per despawn (not every tick)
Empty List: Valid pattern for effects with no cleanup logic
Important: on_despawn executes even if effect interrupted (unlike some game engines that only execute on natural expiration)
Type: list[CommandConfig]
Required: Yes (can be empty list)
Example:
on_interrupt:
- modify: target.vfs.interrupted
value: trueCommand pipeline executed when effect forcibly removed before natural expiration.
Execution Triggers:
- Manual despawn via API (future feature)
- Entity death/removal while effect active
- Effect cancelled by game mechanic
Difference from on_despawn:
on_despawn: Executes for ALL removals (natural + forced)on_interrupt: Executes ONLY for forced removals
Typical Uses:
- Different cleanup for interrupted vs completed effects
- Penalties for premature termination
- Analytics (track interruption rate)
Performance: Executes once per interruption (not every tick)
Empty List: Most effects have empty on_interrupt (use on_despawn for all cleanup)
Effects execute commands at lifecycle stages (on_spawn, on_tick, on_despawn, on_interrupt). Commands are typed, validated at compile-time, and executed on GPU tensors.
Implemented Commands:
- ✅
modify- Set variable/bar value using expression - ✅
spawn_effect- Spawn another effect - ✅
spawn_item- Create item instance - ✅
if- Conditional execution - ✅
for_each- Iterate over collections
All command types are fully implemented and production-ready.
Purpose: Set target variable/bar to new value computed from expression
Schema:
- modify: <path>
value: <expression>Fields:
modify(string, REQUIRED): Path to target variable (e.g.,target.bar.energy,global.vfs.is_night)value(string, REQUIRED): Expression string evaluating to new value
Path Resolution:
target.*: Entity effect is attached to (scope-dependent)target.bar.energy: Agent's energy bar (agent scope)target.vfs.durability: Item's durability variable (item scope)
global.*: World stateglobal.vfs.is_night: Global boolean flagglobal.bar.economy: Global economy meter (if exists)
self.*: Effect instance state (for effect-local variables)temporal.*: Time contexttemporal.tick: Current environment step counttemporal.time_of_day: Hour in 24-hour cycle
Expression Language: Effects use HAMLET's VFS expression language (pure functional, type-safe):
Operators:
- Arithmetic:
+,-,*,/,%,**(power) - Comparison:
<,<=,>,>=,==,!= - Logical:
and,or,not - Ternary:
<cond> if <test> else <alt>
Functions:
clamp(value, min, max): Clamp value to rangemin(a, b): Minimum of two valuesmax(a, b): Maximum of two valuesabs(x): Absolute valuesqrt(x): Square rootsin(x),cos(x): Trigonometric functions
Variables Available in Expressions:
intensity: Effect instance intensity- All paths:
target.bar.*,target.vfs.*,global.vfs.*,temporal.*
Type Safety:
- Expressions type-checked at compile-time
- Path must exist in VFS schema (validated by CommandCompiler)
- Expression return type must match target path type
- Type mismatches cause compilation errors (fail-fast)
Examples:
# Simple increment (WRONG - doesn't clamp)
- modify: target.bar.energy
value: "target.bar.energy + 0.05"
# Clamped increment (CORRECT - bars bounded [0,1])
- modify: target.bar.energy
value: "clamp(target.bar.energy + (0.05 * intensity), 0.0, 1.0)"
# Conditional modification
- modify: target.bar.health
value: "target.bar.health - 0.1 if target.vfs.is_poisoned else target.bar.health"
# Temporal logic
- modify: global.vfs.is_night
value: "temporal.tick % 24 >= 18"
# Complex computation
- modify: target.bar.mood
value: "clamp(target.bar.mood + (sqrt(intensity) * 0.02), 0.0, 1.0)"Performance:
- Expressions compiled to AST at world load time (one-time cost)
- AST evaluated on GPU tensors each execution (vectorized)
- Pre-compilation eliminates runtime parsing overhead
Purpose: Spawn another effect (cascading effects, secondary triggers)
Schema:
- spawn_effect: <effect_id>
target: <expression> # Optional, default: "self"
intensity: <float> # Optional, default: 1.0Fields:
spawn_effect(string, REQUIRED): Effect ID from catalogtarget(expression, OPTIONAL): Target entity for new effect (default:"self")intensity(float, OPTIONAL): Intensity multiplier for spawned effect (default: 1.0)
Target Resolution:
"self": Apply to entity this effect is on (most common)"target": Apply to interaction target (for affordance effects)- Expression: Dynamic target selection (future: nearby agents, held items)
Use Cases:
- Chained Effects: Poisoned → spawn "nauseous" after 5 ticks
- Secondary Effects: Eating food → spawn "digesting" effect
- Conditional Triggers: If health < 0.2 → spawn "critical_condition"
- Visual Effects: Buff applied → spawn "visual_glow" effect
Examples:
# Simple cascade - poison causes nausea
- id: "poison"
on_spawn:
- spawn_effect: "nauseous"
target: "self"
intensity: 1.0
# Conditional spawn - low energy triggers weakness
- id: "energy_monitor"
on_tick:
- if: "target.bar.energy < 0.2"
then:
- spawn_effect: "weakness"
target: "self"
intensity: 2.0
else: []
# Intensity scaling - stronger poison spawns stronger nausea
- id: "poison_severe"
on_spawn:
- spawn_effect: "nauseous"
target: "self"
intensity: "intensity * 1.5" # Scale by poison intensityValidation:
- Effect ID must exist in catalog (compile-time check)
- Target expression must evaluate to valid entity
- Circular dependencies detected (effect A spawns B spawns A)
Performance:
- Spawning creates new effect instance (O(1) operation)
- Reapply policy of spawned effect determines behavior
Purpose: Create item instance in world (loot drops, crafting, resource generation)
Schema:
- spawn_item: <item_type_id>
position: <expression> # Optional, location to spawnFields:
spawn_item(string, REQUIRED): Item type ID from item catalogposition(expression, OPTIONAL): Where to spawn item
Position Resolution:
"target.position": At entity's current position"random": Random valid position in world (future)- Expression: Computed position (e.g.,
"target.position + [1, 0]")
Use Cases:
- Loot Drops: Enemy death → spawn "gold_coin" at position
- Resource Generation: Tree effect → spawn "apple" periodically
- Crafting Results: Crafting effect completes → spawn finished item
- Environmental Spawns: Rain effect → spawn "puddle" items
Examples:
# Drop loot on death
- id: "enemy_death"
on_despawn:
- spawn_item: "gold_coin"
position: "target.position"
# Periodic resource generation
- id: "apple_tree"
on_tick:
- if: "temporal.tick % 100 == 0"
then:
- spawn_item: "apple"
position: "target.position"
else: []
# Multiple item spawn
- id: "treasure_chest"
on_spawn:
- spawn_item: "gold_coin"
position: "target.position"
- spawn_item: "health_potion"
position: "target.position"Validation:
- Item type must exist in item catalog (compile-time check)
- Position must be valid coordinate (runtime check)
Performance:
- Creates new item instance (O(1) operation)
- Items have their own lifecycle (duration, decay effects)
Purpose: Conditional command execution based on runtime state
Schema:
- if: <boolean_expression>
then:
- <command>
- <command>
else:
- <command>Fields:
if(expression, REQUIRED): Boolean expression (condition)then(list[Command], REQUIRED): Commands to execute if trueelse(list[Command], OPTIONAL): Commands to execute if false (default: empty)
Condition Expressions: Must evaluate to boolean type. Use comparison operators and logical combinators.
Use Cases:
- Threshold Effects: If health < 0.2, apply crisis penalties
- State-Dependent Behavior: If night, reduce visibility
- Resource Checks: If has currency, unlock premium effects
- Conditional Spawning: If poisoned, spawn nauseous effect
Examples:
# Simple threshold check
- if: "target.bar.energy < 0.2"
then:
- modify: target.vfs.in_crisis
value: true
- spawn_effect: "energy_crash"
target: "self"
else:
- modify: target.vfs.in_crisis
value: false
# Multiple conditions
- if: "target.bar.energy < 0.2 and target.bar.health < 0.3"
then:
- spawn_effect: "critical_condition"
target: "self"
intensity: 3.0
else: []
# Nested conditionals
- if: "target.bar.health < 0.5"
then:
- if: "target.bar.health < 0.2"
then:
- spawn_effect: "near_death"
target: "self"
else:
- spawn_effect: "injured"
target: "self"
else: []
# Time-based logic
- if: "temporal.tick % 24 >= 18"
then:
- modify: global.vfs.is_night
value: true
else:
- modify: global.vfs.is_night
value: falseValidation:
- Condition must be boolean type (compile-time check)
- Both then and else branches type-checked
- Nested conditionals supported (arbitrary depth)
Performance:
- Condition evaluated once per execution
- Only executed branch runs (no wasted computation)
- Pre-compiled ASTs for condition and branches
Purpose: Iterate over collections and execute commands for each element
Schema:
- for_each: <collection_expression>
as: <iterator_variable>
do:
- <command using iterator>
- <command using iterator>Fields:
for_each(expression, REQUIRED): Collection to iterate (list, tensor)as(string, REQUIRED): Variable name for current elementdo(list[Command], REQUIRED): Commands to execute for each element
Collection Sources:
nearby_agents: Agents within radius (future)held_items: Items in inventory (future)active_effects: Effects on entity (future)- Static lists:
[1, 2, 3]or VFS array variables
Iterator Variable:
Available inside do commands as scoped variable. Access via name specified in as.
Use Cases:
- Area Effects: Apply effect to all nearby agents
- Inventory Processing: Consume all food items
- Batch Operations: Modify multiple VFS variables
- Relationship Systems: Update all social connections
Examples:
# Apply poison to nearby agents (future)
- for_each: "nearby_agents(radius=2)"
as: "agent"
do:
- spawn_effect: "poison"
target: "agent"
intensity: 0.5
# Consume all food items (future)
- for_each: "target.inventory.items"
as: "item"
do:
- if: "item.vfs.type == 'food'"
then:
- modify: "target.bar.energy"
value: "target.bar.energy + item.vfs.nutrition"
- spawn_effect: "item_consumed"
target: "item"
else: []
# Batch VFS updates
- for_each: "[1, 2, 3, 4, 5]"
as: "index"
do:
- modify: "target.vfs.multiplier"
value: "target.vfs.multiplier * index"Validation:
- Collection expression must evaluate to iterable type
- Iterator variable scoped to
doblock only - Commands type-checked with iterator variable in context
Performance:
- Loops execute sequentially (not vectorized)
- Keep collections small for performance
- Prefer batch operations over loops when possible
Current Limitations:
- Limited collection sources in Phase 3/4
- No nested for_each loops (future enhancement)
- No break/continue statements (execute all iterations)
Real examples demonstrating common patterns and use cases:
- id: "energy_regen"
description: "Regenerates energy over time, timer refreshes when reapplied"
scope: agent
duration: 20
intensity: 1.0
reapply_policy: renew
observable: true
on_spawn: []
on_tick:
- modify: target.bar.energy
value: "clamp(target.bar.energy + (0.05 * intensity), 0.0, 1.0)"
on_despawn: []
on_interrupt: []Behavior:
- Regenerates 5% energy per tick for 20 ticks
- If reapplied while active, resets duration to 20 (extends buff)
- Observable by agent (shows as "energy_regen" status)
- No spawn/despawn logic (all behavior in on_tick)
Use Case: Food consumption grants sustained energy recovery
Key Pattern: RENEW policy for single-instance buffs that extend on reapplication
- id: "health_boost"
description: "Instant health restoration, multiple uses stack"
scope: agent
duration: 1
intensity: 1.0
reapply_policy: stack
observable: true
on_spawn:
- modify: target.bar.health
value: "clamp(target.bar.health + (0.2 * intensity), 0.0, 1.0)"
on_tick: []
on_despawn: []
on_interrupt: []Behavior:
- Instant +20% health when spawned
- Duration 1 means immediate despawn after spawn (no ticks)
- Each application stacks (multiple boosts possible)
- All logic in on_spawn (instant effect pattern)
Use Case: Consumable healing items (each use gives separate heal)
Key Pattern: Duration 1 + on_spawn logic for instant effects
- id: "poison"
description: "Deals damage over time, intensity stacks when reapplied"
scope: agent
duration: 20
intensity: 1.0
reapply_policy: merge
observable: true
on_spawn: []
on_tick:
- modify: target.bar.health
value: "clamp(target.bar.health - (0.02 * intensity), 0.0, 1.0)"
on_despawn: []
on_interrupt: []Behavior:
- Deals 2% health damage per tick
- If reapplied, intensity accumulates (1.0 → 1.5 → 2.0)
- Single instance with escalating damage
- Duration not affected by reapplication (keeps counting down)
Use Case: Multiple poison sources stack severity (not duration)
Key Pattern: MERGE policy + intensity scaling for accumulating debuffs
Important: Note how intensity scales the damage - crucial for MERGE policy!
- id: "caffeinated"
description: "Energy boost with crash effect on expiration"
scope: agent
duration: 30
intensity: 1.0
reapply_policy: renew
observable: true
on_spawn:
- modify: target.vfs.is_caffeinated
value: true
- modify: target.bar.energy
value: "clamp(target.bar.energy + 0.3, 0.0, 1.0)"
on_tick:
- modify: target.bar.mood
value: "clamp(target.bar.mood + (0.01 * intensity), 0.0, 1.0)"
on_despawn:
- modify: target.vfs.is_caffeinated
value: false
- spawn_effect: "caffeine_crash"
target: "self"
intensity: 1.0
on_interrupt:
- modify: target.vfs.is_caffeinated
value: falseBehavior:
- Instant energy boost on application
- Gradual mood improvement while active
- Sets VFS flag for other systems to check
- Spawns crash effect on natural expiration
- Cleans up flag even if interrupted
Use Case: Coffee consumption with realistic crash mechanics
Key Pattern: on_spawn (instant), on_tick (sustained), on_despawn (consequence)
- id: "wet"
description: "Reduces hygiene when wet, spawns cold if too long"
scope: agent
duration: 50
intensity: 1.0
reapply_policy: renew
observable: true
on_spawn:
- modify: target.vfs.is_wet
value: true
on_tick:
- modify: target.bar.hygiene
value: "clamp(target.bar.hygiene - (0.01 * intensity), 0.0, 1.0)"
- if: "effect.elapsed_ticks > 30"
then:
- spawn_effect: "cold"
target: "self"
intensity: 1.0
else: []
on_despawn:
- modify: target.vfs.is_wet
value: false
on_interrupt:
- modify: target.vfs.is_wet
value: falseBehavior:
- Sets wet flag, reduces hygiene over time
- If wet for >30 ticks, spawns cold effect
- Renew policy extends duration (stay wet longer if re-exposed)
- Cleanup handled in both on_despawn and on_interrupt
Use Case: Environmental effects with conditional escalation
Key Pattern: Conditional spawning based on effect lifetime
Note: Uses effect.elapsed_ticks to track how long effect has been active
- id: "global_day_cycle"
description: "Updates global day/night state based on time"
scope: global
duration: 10000
intensity: 1.0
reapply_policy: stack
observable: false
on_spawn: []
on_tick:
- modify: global.vfs.is_night
value: "temporal.tick % 24 >= 18 and temporal.tick % 24 < 6"
- modify: global.vfs.time_of_day
value: "temporal.tick % 24"
on_despawn: []
on_interrupt: []Behavior:
- Sets global
is_nightflag based on time-of-day - Updates every tick using temporal context
- Duration 10000 for long-lived world state effect
- Not observable (global state, not agent-specific status)
Use Case: Environment spawns this on startup for day/night cycles
Key Pattern: Global scope + temporal expressions for world state
Note: Uses temporal.tick for time-based logic, global.vfs.* for world state
- id: "item_decay"
description: "Food items spoil over time, become inedible"
scope: item
duration: 200
intensity: 1.0
reapply_policy: stack
observable: false
on_spawn: []
on_tick:
- modify: target.vfs.freshness
value: "clamp(target.vfs.freshness - (0.005 * intensity), 0.0, 1.0)"
- if: "target.vfs.freshness < 0.1"
then:
- modify: target.vfs.is_spoiled
value: true
- spawn_effect: "spoiled"
target: "self"
intensity: 1.0
else: []
on_despawn: []
on_interrupt: []Behavior:
- Reduces item freshness over time
- When freshness drops below 10%, marks as spoiled
- Spawns "spoiled" effect for additional consequences
- Stack policy allows multiple decay effects (compounding decay)
Use Case: Perishable food items in simulation
Key Pattern: Item scope + conditional state transitions
Note: Items have their own VFS variables (target.vfs.*)
- id: "on_cooldown"
description: "Prevents affordance reuse for duration"
scope: affordance
duration: 10
intensity: 1.0
reapply_policy: renew
observable: true
on_spawn:
- modify: target.vfs.available
value: false
on_tick: []
on_despawn:
- modify: target.vfs.available
value: true
on_interrupt:
- modify: target.vfs.available
value: trueBehavior:
- Marks affordance as unavailable on spawn
- Lasts 10 ticks (cooldown period)
- Renew policy extends cooldown if triggered again
- Restores availability on despawn/interrupt
Use Case: Prevent spam-clicking affordances, enforce cooldown periods
Key Pattern: Affordance scope + availability flag manipulation
Note: AffordanceEngine can check target.vfs.available before allowing interaction
- id: "inspired"
description: "Boosts mood and spawns creativity effect"
scope: agent
duration: 40
intensity: 1.0
reapply_policy: replace
observable: true
on_spawn:
- modify: target.bar.mood
value: "clamp(target.bar.mood + 0.2, 0.0, 1.0)"
- spawn_effect: "creative"
target: "self"
intensity: 1.5
on_tick:
- modify: target.bar.mood
value: "clamp(target.bar.mood + (0.005 * intensity), 0.0, 1.0)"
on_despawn:
- modify: target.vfs.was_recently_inspired
value: true
on_interrupt: []Behavior:
- Instant mood boost on spawn
- Spawns secondary "creative" effect with higher intensity
- Sustained mood gain while active
- Leaves flag when expires (for analytics or future effects)
- Replace policy: Only one inspiration at a time
Use Case: Art/music affordances grant inspiration with cascading benefits
Key Pattern: Effect spawning effects for complex behaviors
# Future example - requires nearby_agents() implementation
- id: "healing_aura"
description: "Heals nearby agents in radius"
scope: agent
duration: 20
intensity: 1.0
reapply_policy: stack
observable: true
on_spawn: []
on_tick:
- for_each: "nearby_agents(radius=2)"
as: "nearby"
do:
- modify: "nearby.bar.health"
value: "clamp(nearby.bar.health + (0.02 * intensity), 0.0, 1.0)"
on_despawn: []
on_interrupt: []Behavior:
- Each tick, finds agents within radius 2
- Heals each nearby agent by 2% health
- Stack policy allows multiple auras (compounding heals)
- Works on others, not self
Use Case: Support abilities, area buffs, social effects
Key Pattern: for_each loop for multi-target effects
Note: Requires spatial query functions (future feature)
Effects are compiled by the World Compiler as part of universe compilation:
effects.yaml → EffectsConfig (Pydantic) → EffectCatalog (runtime) → EffectManager (execution)
↓ ↓ ↓ ↓
Parse YAML Validate schema Compile commands Execute commands
Type-check paths Pre-compile ASTs Update GPU tensors
Schema Validation (Pydantic):
- All required fields present
- Field types correct (int, float, string, enum)
- Enum values valid (scope, reapply_policy)
- List structures correct (on_spawn, on_tick, on_despawn, on_interrupt)
Path Validation (CommandCompiler):
modifypaths exist in VFS schema- Paths accessible from effect scope
- Path types match expression return types
Expression Validation (ExpressionParser + TypeChecker):
- Expressions parse correctly (valid syntax)
- Types inferred correctly (type checker validates operations)
- Return type matches target path type
- Variables referenced exist in context (target, global, temporal, intensity)
Reference Validation:
spawn_effecteffect_id exists in catalogspawn_itemitem_type exists in item catalog- Circular dependencies detected (effect A spawns B spawns A)
Invalid Path:
TypeCheckError: Path 'target.bar.invalid' not found in schema.
Available: ['target.bar.energy', 'target.bar.health', ...]
Type Mismatch:
TypeCheckError: Type mismatch for path 'target.bar.energy':
expected float, got bool
Expression Syntax Error:
ParseError: Invalid expression syntax at position 15:
'target.bar.energy +' (expected operand)
Missing Effect Reference:
ReferenceError: Effect 'nonexistent_effect' not found in catalog
(referenced in spawn_effect command)
Pre-Compilation:
- Expression strings → AST at world load time (one-time cost)
- ASTs stored in CommandNode.value_ast (reused every execution)
- Eliminates runtime parsing overhead
Vectorized Execution:
- CommandExecutor evaluates ASTs on GPU tensors
- Entire batch of agents processed in parallel
- Single CUDA kernel for expression evaluation
Scoped Collections:
- Effects stored in scope-specific collections (agent_effects, global_effects, etc.)
- Prevents O(all_effects) searches for scoped operations
- Typical overhead: <1% of step time for 10-50 active effects
Affordances spawn effects when interacted with:
# affordances.yaml
affordances:
- name: "BED"
interactions:
on_start:
- spawn_effect: "energy_regen"
target: "self"
intensity: 1.5Integration: AffordanceEngine validates spawn_effect references exist in EffectCatalog
Items apply effects when used/equipped/consumed:
# items.yaml
items:
item_types:
- id: "healing_potion"
interactions:
on_use:
- spawn_effect: "health_boost"
target: "user"
intensity: 2.0Integration: Items use same command language as effects
Cascades can conditionally spawn effects:
# cascades.yaml (future)
cascades:
- source: energy
targets:
- target: health
condition: "source < 0.2"
on_trigger:
- spawn_effect: "energy_crash"Effects read/write supported variables from the canonical pack-scope roster. The compiler supplies engine roles; authors do not declare readers or writers.
variables:
version: '1.0'
evaluation_mode: mark_and_sweep
debug_logging: false
extents: {}
item_profiles: []
declarations:
- id: is_caffeinated
scope: agent
type: bool
lifetime: episode
semantic_type: custom
initial_value: false
exposed_to: []See variables for scope and consumer restrictions. A registered symbol is not a promise that every effect execution context supports every scope.
Effect IDs:
- Lowercase with underscores:
energy_regen,health_boost - Descriptive of behavior, not source:
caffeinated(notdrank_coffee) - Verb forms for actions:
regenerating,bleeding - Noun forms for states:
poisoned,buffed,cursed
Descriptions:
- Active voice: "Regenerates energy over time" (not "Energy is regenerated")
- Mention key parameters: "Deals 2% health damage per tick"
- Note interactions: "Stacks with other poison sources"
Instant Effects: duration: 1
- All logic in
on_spawn - Empty
on_tickandon_despawn - Use for consumables, instant stat changes
Short-Term Effects: duration: 5-20
- Typical agent buffs/debuffs
- Food digestion, temporary stat boosts
- Balance: Long enough to matter, short enough to require reapplication
Medium-Term Effects: duration: 50-200
- Item decay, slow transformations
- Multi-tick processes (crafting, building)
Long-Term Effects: duration: 500-5000
- Global world state (day/night cycles)
- Persistent item enchantments
- Slow environmental changes
Avoid: Very short durations (<5) for on_tick effects (high overhead, little benefit)
Linear Scaling (simple, predictable):
value: "target.bar.energy + (0.05 * intensity)"Diminishing Returns (prevents overpowering stacks):
value: "target.bar.energy + (0.05 * sqrt(intensity))"Threshold Effects (discrete breakpoints):
value: "target.bar.energy + (0.1 if intensity > 2.0 else 0.05)"Avoid: Hardcoded values that don't scale with intensity (breaks MERGE policy!)
Use stack when:
- Each application is independent (food digestion)
- Want cumulative effects (multiple damage sources)
- Duration should NOT reset
Use renew when:
- Want single instance with extended duration (buffs)
- Don't want intensity stacking (power cap)
- Encourage frequent reapplication (stay fed)
Use merge when:
- Want intensity stacking but single instance (poison)
- Severity should accumulate (cumulative dosage)
- Duration should NOT reset
Use replace when:
- Only one instance should exist (current action)
- Old effect should be interrupted (state transition)
- Want explicit on_despawn for old effect
Minimize on_tick Commands:
- Executes every tick for every active effect
- Keep command lists short (1-3 commands ideal)
- Complex logic should be in expressions, not multiple commands
Use Clamping:
- Always clamp bar modifications:
clamp(value, 0.0, 1.0) - Prevents out-of-range values, reduces downstream checks
Batch Related Modifications:
- Modify multiple bars in single effect rather than spawning multiple effects
- Reduces active effect count, improves cache locality
Avoid Redundant Effects:
- Don't spawn effects unnecessarily
- Use renew/merge policies to prevent duplicate instances
"Interesting Failures":
- Low energy causing health damage (teaches resource management)
- Buff stacking leading to overpowered agents (teaches balance)
- Poison merging causing rapid death (teaches threat prioritization)
Observable vs Hidden Effects:
- Make causal effects observable (agent should know they're poisoned)
- Make latent states hidden (agent infers "boss is mad" from consequences)
- Teaches agents to model unobservable state from observations
Reapply Policy Lessons:
- Compare
stackvsrenewin curriculum - Show emergent behavior from different policies
- Let students experiment with policy changes without code changes
Q: Effect not executing commands
- Verify effect spawned correctly (
effect_manager.get_all_active_effects()) - Check
duration_remaining > 0(may have already expired) - Ensure
effect_manager.tick()called in env.step() - Verify commands are in correct lifecycle hook (on_spawn vs on_tick)
Q: Type check error for valid path
- Check VFS schema includes the path (the canonical variables declaration)
- Verify path accessible from effect scope
- Ensure expression return type matches path type (bool vs float)
Q: Effect spawned but not ticking
- Check scope matches target entity
- Verify
current_steppassed totick() - Ensure
env_statepassed for context building
Q: Intensity not scaling properly
- Verify expressions multiply by
intensityvariable - Check merge policy used if expecting intensity stacking
- Ensure intensity passed at spawn time (defaults to 1.0)
Q: Effects despawning too early/late
- Check
durationfield matches intended tick count - Verify
duration: 1used for instant effects (common mistake:duration: 0) - Remember tick occurs AFTER commands execute
Missing Required Field:
# ERROR: Missing reapply_policy
- id: "energy_regen"
scope: agent
duration: 20
# reapply_policy: renew # REQUIRED!Fix: Add reapply_policy: <policy> to every effect definition
Invalid Scope:
# ERROR: Invalid scope value
- id: "buff"
scope: "player" # Should be: agent, item, global, or affordanceFix: Use one of the four valid scope values
Type Mismatch in Expression:
# ERROR: Boolean assigned to float path
- modify: target.bar.energy # Type: float
value: "target.bar.energy > 0.5" # Returns: boolFix: Ensure expression returns correct type (use ternary for conditionals)
Path Not Found:
# ERROR: Path doesn't exist in VFS schema
- modify: target.bar.mana # If mana not defined in bars.yaml
value: "0.5"Fix: Add variable to VFS schema or correct path typo
- Design Document:
docs/zzz. archive/plans/archive/vfs_uplift/2025-11-19-effects-system-design.md- Complete architecture - Implementation Plan:
docs/plans/vfs_uplift/2025-11-19-task-3-1-effects-dtos.md- DTOs and catalog - VFS Variables:
docs/config-schemas/variables.md- Variable system that effects modify - Affordances:
docs/config-schemas/affordances.md- How affordances spawn effects - Expression Language:
docs/plans/vfs_uplift/2025-11-19-task-1-2-expression-parser.md- Expression syntax and semantics - Training Config:
docs/config-schemas/training.md- How to run experiments with effects
Status: Phase 3 Complete + Phase 4 Command Extensions Implemented Version: 1.0 Last Updated: 2025-11-21