Home · Architecture · Combat · Extensions · Known gaps · Source index
Behavior System
Core pattern
The behavior layer in src/behavior/ uses boxed, object-safe traits attached to runtime entities. A macro generates companion clone traits and Clone for Box<dyn Trait> implementations, allowing projectiles, enemies, statuses, pools, and tower modifications to clone their heterogeneous behavior vectors.
Dispatch temporarily takes an entity's behavior vector, invokes every hook with &mut access to the entity, then restores the vector. This avoids borrowing entity.behaviors and the whole entity mutably at the same time.
take(entity.behaviors)
-> invoke hooks in vector order
-> fold responses / mutate context
-> restore(entity.behaviors)
Behavior families
| Family | Hooks | Current reachability |
|---|---|---|
ProjectileBehavior |
fire, enemy hit, wall hit, tick, expire, priority | Hit/wall/tick/lifetime expire live; fire unwired |
EnemyBehavior |
tick, death, priority | Tick and death live; priority declared but not used for sorting |
StatusBehavior |
tick, priority | Tick live; priority declared but statuses are not sorted |
PoolBehavior |
tick | Dispatch exists; entire pool pass unwired |
TowerBehavior |
fire/hit/wall/tick/expire | Entire family unwired; Tower.mods unused |
DamageBehavior |
pure compute(base,target) |
Implementations and registry factories exist; combat never invokes them |
Sources: src/behavior/projectile.rs, src/behavior/enemy.rs, src/behavior/status.rs, src/behavior/pool.rs, src/behavior/tower.rs, and src/behavior/damage.rs.
Contexts
All event contexts embed BehaviorCtx, which provides:
| Capability | Field |
|---|---|
| Deferred writes | mutable CommandBuffer |
| Read-only world view | SpatialQuery over enemy snapshot/grid |
| Deterministic randomness | mutable pass-local Rng |
| Attribution | Source |
| Simulation timestamp | now |
Event-specific tails add only relevant data:
| Context | Additional fields | Status |
|---|---|---|
HitContext |
hit position, normal, damage map, pending statuses | Live |
FireContext |
origin, aim, optional target | Unwired |
TickContext |
dt |
Live |
ExpireContext |
expiration reason | Lifetime live; out-of-bounds/consumed unwired |
DeathContext |
position, killer | Live |
EventContext |
no tail | Live in centralized event handler |
Deref/DerefMut implementations make core fields appear directly on specialized contexts. See src/context.rs.
Projectile response fold
Every enemy-hit or wall-hit hook runs; dispatch does not short-circuit. Responses are folded as follows:
- Any
pierce = truesurvives unless reflection is present. - The last non-
Nonereflection vector wins. - Reflection keeps the projectile and replaces velocity.
- With no reflection, the projectile is consumed unless at least one hook requested pierce.
Behavior order matters because all hooks share mutable projectile/context state. Tower-created projectile behaviors are sorted by numeric priority:
| Tier | Value | Intended role |
|---|---|---|
| PRE | -200 | Targeting/setup |
| MODIFY | -100 | Accumulator modifiers |
| DEFAULT | 0 | General effects |
| IMPACT | 100 | Read/finalize accumulated damage |
| POST | 200 | Reactions/cleanup |
The current Impact behavior runs at IMPACT; Poison Coating runs at DEFAULT. Registry definitions also store priority, but shot sorting calls each behavior object's priority(), not the registry's stored value.
Damage and status accumulation
HitContext.damage maps each DamageID to:
max(0, flat × max(0, 1 + sum(increased)) × product(max(0, 1 + each more)))
Contributions resolve per damage kind and are summed by DamageMap::resolve, though enemy application iterates per kind directly to apply kind-specific resistance. flat and increased are additive; more compounds. Final values cannot be negative before resistance.
HitContext.status accumulates boxed statuses. After all projectile hooks, the projectile system queues one ApplyStatus command per entry. Poison statuses capture the applying projectile's source so later DoT ticks retain tower attribution even though the generic enemy tick context uses Source::None.
Concrete live behaviors
- src/weapons/impact.rs: adds tier-scaled physical flat and increased damage.
- src/weapons/poison.rs: applies a five-second poison
PeriodDamage. - src/enemies/movement.rs:
SeekCenter;ZigZagcurrently duplicates it and is not registered. - src/enemies/behaviors.rs: deterministic split replication on death.
- src/behavior/status.rs: frame-proportional periodic damage and expiry.
The acid and slow implementations in src/pools/ only iterate query results; they currently enqueue no damage or status even if pool dispatch is manually called.
Home · Architecture · Combat · Extensions · Known gaps · Source index