Home · Architecture · Combat · Extensions · Known gaps · Source index
Extension guides
Source snapshot: 1c4f2f89ebf6c70e81dccb412394be06a69fa932.
These guides describe extension points present in the source at that snapshot. They do not assume systems described outside src/ and Cargo.toml. Follow the links into the exact source, and check the warnings before relying on a hook at runtime. For terminology and a module-by-module map, see Source index and glossary.
Extension matrix
| Extension | Definition point | Registration or attachment point | Runtime state |
|---|---|---|---|
| Weapon/projectile behavior | ProjectileBehavior |
WeaponRegistry::new, then the tower loadout |
Hit, wall-hit, tick, and lifetime-expiry hooks are dispatched; on_fire is not |
| Enemy kind | EnemyKind |
EnemyRegistry::new |
Factory works; the wave spawner still selects only "blob" |
| Enemy passive | EnemyBehavior |
Push into an enemy's behaviors vector |
Tick and death hooks are dispatched |
| Status | StatusBehavior |
Push on HitContext.status, queue ApplyStatus, or register in StatusRegistry |
Status ticking and removal work; registry lookup is not used by gameplay |
| Damage type | DamageBehavior and DamageID |
DamageRegistry::new |
IDs, per-kind accumulation, and resistance lookup work; registered compute factories are not called |
| Ground pool | GroundPool and PoolBehavior |
Constructor plus CommandBuffer::spawn_pool |
Spawn command works; pool pass is not called, TTL is not consumed, and pools are not drawn |
Add a weapon or projectile behavior
Use this path for a behavior that should be materialized onto every projectile from an equipped weapon entry. Existing examples are Impact and Poison Coating.
Steps
- Create
src/weapons/<name>.rs. - Define a concrete,
Clonetype.ProjectileBehavioruses boxed dynamic cloning, so a behavior placed inVec<Box<dyn ProjectileBehavior>>must satisfy the blanket clone implementation generated inbehavior/mod.rs. - Add a constructor such as
create(tier: u32, ...) -> Box<dyn ProjectileBehavior>. Derive all tier-scaled values there. The registry passes the current tier wheneversystems::tick_towercreates a shot. - Implement the relevant hooks:
on_hit_enemycan add flat/increased/more damage toHitContext.damage, append statuses toHitContext.status, and queue commands.on_hit_wallcan return a reflected velocity or request piercing.on_tickcan mutate the projectile and query nearby enemies throughTickContext.on_expirecan react to the suppliedExpireReason.
- Return a deliberate
HitResponse.PASSmeans no pierce and no reflection. After all behavior responses are folded, the projectile is consumed unless at least one behavior requested pierce or supplied reflection. - Override
priority()when ordering matters. Behaviors are sorted for each new projectile. The available conventional tiers arePRE,MODIFY,DEFAULT,IMPACT, andPOST; lower values run first. Impact usesIMPACTso earlier modifiers can populate the accumulator. - Export the module from
src/weapons/mod.rs. - Register a stable internal name, display name, registry priority, and factory closure in
WeaponRegistry::new. Resolve any requiredDamageIDonce before moving it into the closure, as the existing constructors do. - Decide how the tower obtains the weapon:
- Every registered weapon appears in
WeaponRegistry::all(), so it can be rolled by the weapon crate. - To make it part of the initial loadout, resolve the ID and call
equipinTower::new.
- Every registered weapon appears in
- Verify hit behavior, projectile retention, and tier upgrades. Choosing an already-equipped ID increments its tier; the next shot is rebuilt from the registry with that new tier.
Runtime warnings
ProjectileBehavior::on_fireis declared, andFireContextexists, buttick_towernever constructs aFireContextor dispatcheson_fire. Fan-shot, multishot, or other spawn-shaping behavior will not run without new runtime wiring.TowerBehaviorduplicates projectile-style hooks andTower.modsstores such objects, but no system dispatches tower mods.ExpireReasondeclaresLifetime,OutOfBounds, andConsumed; current projectile code dispatcheson_expireonly for natural lifetime expiry. Enemy hits and wall hits may remove a projectile without an expiry callback, and no out-of-bounds expiry path exists.- Registry
priorityis stored separately from behaviorpriority. Projectile ordering usesProjectileBehavior::priority(); the registry's stored priority is not used for that sort or for crate offers. - Collision checks happen after movement and select one overlapping enemy through
first_hit; there is no swept collision test.
Add an enemy kind
An enemy kind is a factory for a complete Enemy: stats, drawing callback, statuses, resistances, and passive behavior list. Blob is the only registered kind in this snapshot.
Steps
- Create
src/enemies/<name>.rsand export it fromsrc/enemies/mod.rs. - Define a kind type and implement
EnemyKind. - In
create(tier), construct everyEnemyfield:- use
id: 0;World::spawn_enemyallocates the live ID; - use
kind: EnemyID::PLACEHOLDER;EnemyRegistry::make_kindstamps the registered kind ID; - initialize
hpandmax_hpconsistently; - provide position, velocity, radius, XP, death animation, source/killer state, status and resistance collections, passive behaviors, and draw callback.
- use
- Implement
animation()as the draw callback returned in the enemy template. - Attach movement and other passives to
behaviorsin the intended order. - Register the kind in
EnemyRegistry::newwith a stable name, display name, priority, and atierfactory calling your kind'screate. - Ensure every spawn path calls
make_kind(id, tier), not plainmake, so the concrete enemy receives its realEnemyID. - Add an explicit spawn-selection path. The current wave system resolves
"blob"by name on every spawn; registration alone does not make another kind appear. - Verify direct spawning, movement, damage, death hooks, corpse data, XP, and any replication behavior.
Runtime warnings
tick_waveis hard-coded to"blob". There is no wave table, random kind selection, or registry iteration in spawning.EnemyRegistrystores display names and priorities, but no current UI or spawn policy consumes them.TransformEntity::Enemyreplication rebuilds a template from the dead enemy's kind and tier, then replaces the template's behavior list with a clone of the source list. Kind-defined fresh behavior state can therefore be overwritten during replication.- Reaching the center emits
DespawnEnemy; it does not damage a tower or player because no tower-health path exists.
Add an enemy passive
Use an EnemyBehavior for stateful per-enemy logic such as movement, periodic actions, or reactions to death. Existing examples are SeekCenter, ZigZag, and SplitOnDeath.
Steps
- Define a
Clonebehavior type in an appropriatesrc/enemies/module. - Add a constructor returning
Box<dyn EnemyBehavior>. Put per-instance counters and configuration on the struct. - Implement
on_tick,on_death, or both:- mutate only the current enemy directly;
- read other enemies through
c.query; - affect other entities by queueing through
c.cmd; - use
c.rngrather than a global random source; - use
c.dtfor simulation-time updates.
- Push the behavior into the kind's
Enemy.behaviorsvector. - Test both ordinary enemies and replicated children if the passive can coexist with
SplitOnDeath.
Runtime warnings
- Tick and death dispatch are active, but
EnemyBehavior::priority()is never read and enemy behavior vectors are never sorted. Execution follows vector insertion order. - Death behavior runs in
death_sweep, after projectile/status damage commands have been applied. Commands emitted by death passives are applied after that pass. - A damage command that kills an enemy emits an
EnemyKilledevent immediately, anddeath_sweepemits anotherEnemyKilledevent for the same dead enemy. In this snapshot, that can duplicate event-side death animation and XP grants.
Add a status
A status is a cloneable object stored on an enemy and ticked once per enemy pass. PeriodDamage is the implemented model.
Steps
- Define a
Clonetype implementingStatusBehavior. - Store all status-local timing and attribution data on the status itself. In particular, capture the applying
Sourcewhen attribution must survive later ticks: the generic enemy tick context usesSource::None. - Implement
on_tick(&mut self, enemy, context) -> bool.- Queue damage with
c.cmd.damage(...)andDamageDelivery::Dotrather than reducing HP directly. - Queue effects on other entities through the command buffer.
- Return
truewhen the status should be removed; returnfalseto retain it.
- Queue damage with
- Choose an application route:
- append a boxed status to
HitContext.status; projectile finalization turns each item intoCommand::ApplyStatus; - call
c.cmd.apply_status(target, status)from another behavior; or - register a factory in
StatusRegistry::newfor future generic construction.
- append a boxed status to
- If the status needs a damage kind, resolve its
DamageIDwhen constructing the status factory or applying behavior. - Test application, ticking, expiration boundary, source attribution, and multiple simultaneous instances.
Runtime warnings
StatusRegistryis constructed and stored onWorld, but gameplay never callsstatus_registry.make,id_of, orall. Registering a status does not make it obtainable or applied.StatusBehavior::priority()is declared but never used; statuses execute in vector insertion order.- There is no stacking, replacement, deduplication, dispel, or status identity policy. Every application pushes another boxed object.
- Statuses currently receive only tick hooks. There are no apply/remove, damage-taken, or death hooks.
- A movement modifier can mutate the enemy during
on_tick, but current movement passives run before statuses and can set velocity first; define the intended ordering explicitly if adding slow effects.
Add a damage type
Current combat uses typed DamageID keys in DamageMap and Enemy.resists. DamageBehavior and its registry factory describe a target-dependent magnitude rule, but the live damage path does not invoke that rule.
Steps
- Define a concrete type in
src/behavior/damage.rsand implementDamageBehavior::compute(base, target). - Register a stable internal name, display name, priority, and factory in
DamageRegistry::new. - Resolve the resulting
DamageIDby name wherever a weapon, status, or pool is constructed. Prefer resolving once in a registry constructor and capturing the copyable ID in a factory closure. - Add typed damage to
HitContext.damageor a newDamageMapwith:flat(id, amount)for base damage;increased(id, pct)for additive increased/reduced modifiers;more(id, pct)for independently multiplicative more/less modifiers.
- Put resistance values under the same
DamageIDin each relevant enemy'sresistsmap. Missing entries mean zero resistance. - Verify mixed-type hits, zero and nonzero resistance, negative or extreme modifiers, and DPS attribution.
Runtime warnings
Enemy::apply_damagecallsDamageEffects::compute()and applies resistance directly. It does not obtain or call the registeredDamageBehavior. A customDamageBehavior::computehas no runtime effect until the damage application path is wired to the registry.DamageRegistry::make, its factory argument, display name, and priority are currently unused in combat.- The legacy
types::DamageType = i32alias andbehavior::damage::Damage::{PHYSICAL, ACID}integer constants are separate from the activeDamageIDregistry keys. Do not use them as substitutes for aDamageID. - Resistance is not clamped. Values above
1.0produce negative damage, and negative resistance amplifies damage.
Add a ground pool
A ground pool is a stationary entity carrying one or more PoolBehavior objects. Existing constructors produce acid and slow instances in src/pool.rs; their behaviors are in src/pools/acid.rs and src/pools/slow.rs.
Steps
- Create
src/pools/<name>.rsand export it fromsrc/pools/mod.rs. - Define a
Clonebehavior type with its potency, cadence, or other per-pool state. - Implement
PoolBehavior::on_tick.- Query occupants with
c.query.enemies_in_radius(pool.pos, pool.radius). - Queue damage or status application by each returned
EnemyRef.id. - Multiply continuous rates by
c.dt. - Use
pool.source/c.sourcefor attribution.
- Query occupants with
- Add a constructor on
GroundPool, or construct the struct at the caller, setting position, radius, TTL, source, and behaviors. - Spawn it from another behavior through
c.cmd.spawn_pool(pool). The command application path appends it toWorld.pools. - Add and verify runtime lifecycle wiring described below before expecting the behavior to run.
Required runtime wiring and current gaps
systems::pool_passexists and correctly snapshots enemies, rebuilds the grid, creates a tick context, and dispatches each pool. However,step_worldnever callspool_pass. As written, no pool behavior ticks.GroundPool.ttlis never decremented and expired pools are never removed.- No drawing code renders
World.pools. AcidTickcomputes local damage and iterates targets but queues no damage command.SlowTickiterates targets but applies no status. Both are scaffolds, not working effects.- No existing weapon or other live behavior calls
spawn_pool, so pool constructors are not reached during normal play. - There is no pool registry or pool ID. Pools are configured instances created directly in code.
Verification checklist
Run checks against the source snapshot after implementing an extension:
cargo checkfor trait bounds, object safety, and module registration.- Confirm the factory can be resolved by its stable registry name.
- Confirm the content is reachable from a live spawn/equip/application path, not merely registered.
- Confirm the relevant dispatcher is called from
step_world. - Confirm commands are applied after the pass and events are delivered as expected.
- Confirm source attribution (
TowerversusNone) andDamageDeliveryproduce the expected meter bucket. - Confirm clone behavior for projectiles, enemy replication, statuses, and pools.
- Confirm expiration/removal behavior and test any gap called out in this page.
Home · Architecture · Combat · Extensions · Known gaps · Source index