td

BehaviorSystem
Login

BehaviorSystem

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:

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

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