td

ExtensionGuides
Login

ExtensionGuides

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

  1. Create src/weapons/<name>.rs.
  2. Define a concrete, Clone type. ProjectileBehavior uses boxed dynamic cloning, so a behavior placed in Vec<Box<dyn ProjectileBehavior>> must satisfy the blanket clone implementation generated in behavior/mod.rs.
  3. Add a constructor such as create(tier: u32, ...) -> Box<dyn ProjectileBehavior>. Derive all tier-scaled values there. The registry passes the current tier whenever systems::tick_tower creates a shot.
  4. Implement the relevant hooks:
    • on_hit_enemy can add flat/increased/more damage to HitContext.damage, append statuses to HitContext.status, and queue commands.
    • on_hit_wall can return a reflected velocity or request piercing.
    • on_tick can mutate the projectile and query nearby enemies through TickContext.
    • on_expire can react to the supplied ExpireReason.
  5. Return a deliberate HitResponse. PASS means 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.
  6. Override priority() when ordering matters. Behaviors are sorted for each new projectile. The available conventional tiers are PRE, MODIFY, DEFAULT, IMPACT, and POST; lower values run first. Impact uses IMPACT so earlier modifiers can populate the accumulator.
  7. Export the module from src/weapons/mod.rs.
  8. Register a stable internal name, display name, registry priority, and factory closure in WeaponRegistry::new. Resolve any required DamageID once before moving it into the closure, as the existing constructors do.
  9. 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 equip in Tower::new.
  10. 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

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

  1. Create src/enemies/<name>.rs and export it from src/enemies/mod.rs.
  2. Define a kind type and implement EnemyKind.
  3. In create(tier), construct every Enemy field:
    • use id: 0; World::spawn_enemy allocates the live ID;
    • use kind: EnemyID::PLACEHOLDER; EnemyRegistry::make_kind stamps the registered kind ID;
    • initialize hp and max_hp consistently;
    • provide position, velocity, radius, XP, death animation, source/killer state, status and resistance collections, passive behaviors, and draw callback.
  4. Implement animation() as the draw callback returned in the enemy template.
  5. Attach movement and other passives to behaviors in the intended order.
  6. Register the kind in EnemyRegistry::new with a stable name, display name, priority, and a tier factory calling your kind's create.
  7. Ensure every spawn path calls make_kind(id, tier), not plain make, so the concrete enemy receives its real EnemyID.
  8. 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.
  9. Verify direct spawning, movement, damage, death hooks, corpse data, XP, and any replication behavior.

Runtime warnings

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

  1. Define a Clone behavior type in an appropriate src/enemies/ module.
  2. Add a constructor returning Box<dyn EnemyBehavior>. Put per-instance counters and configuration on the struct.
  3. 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.rng rather than a global random source;
    • use c.dt for simulation-time updates.
  4. Push the behavior into the kind's Enemy.behaviors vector.
  5. Test both ordinary enemies and replicated children if the passive can coexist with SplitOnDeath.

Runtime warnings

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

  1. Define a Clone type implementing StatusBehavior.
  2. Store all status-local timing and attribution data on the status itself. In particular, capture the applying Source when attribution must survive later ticks: the generic enemy tick context uses Source::None.
  3. Implement on_tick(&mut self, enemy, context) -> bool.
    • Queue damage with c.cmd.damage(...) and DamageDelivery::Dot rather than reducing HP directly.
    • Queue effects on other entities through the command buffer.
    • Return true when the status should be removed; return false to retain it.
  4. Choose an application route:
    • append a boxed status to HitContext.status; projectile finalization turns each item into Command::ApplyStatus;
    • call c.cmd.apply_status(target, status) from another behavior; or
    • register a factory in StatusRegistry::new for future generic construction.
  5. If the status needs a damage kind, resolve its DamageID when constructing the status factory or applying behavior.
  6. Test application, ticking, expiration boundary, source attribution, and multiple simultaneous instances.

Runtime warnings

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

  1. Define a concrete type in src/behavior/damage.rs and implement DamageBehavior::compute(base, target).
  2. Register a stable internal name, display name, priority, and factory in DamageRegistry::new.
  3. Resolve the resulting DamageID by 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.
  4. Add typed damage to HitContext.damage or a new DamageMap with:
    • flat(id, amount) for base damage;
    • increased(id, pct) for additive increased/reduced modifiers;
    • more(id, pct) for independently multiplicative more/less modifiers.
  5. Put resistance values under the same DamageID in each relevant enemy's resists map. Missing entries mean zero resistance.
  6. Verify mixed-type hits, zero and nonzero resistance, negative or extreme modifiers, and DPS attribution.

Runtime warnings

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

  1. Create src/pools/<name>.rs and export it from src/pools/mod.rs.
  2. Define a Clone behavior type with its potency, cadence, or other per-pool state.
  3. 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.source for attribution.
  4. Add a constructor on GroundPool, or construct the struct at the caller, setting position, radius, TTL, source, and behaviors.
  5. Spawn it from another behavior through c.cmd.spawn_pool(pool). The command application path appends it to World.pools.
  6. Add and verify runtime lifecycle wiring described below before expecting the behavior to run.

Required runtime wiring and current gaps

Verification checklist

Run checks against the source snapshot after implementing an extension:

  1. cargo check for trait bounds, object safety, and module registration.
  2. Confirm the factory can be resolved by its stable registry name.
  3. Confirm the content is reachable from a live spawn/equip/application path, not merely registered.
  4. Confirm the relevant dispatcher is called from step_world.
  5. Confirm commands are applied after the pass and events are delivered as expected.
  6. Confirm source attribution (Tower versus None) and DamageDelivery produce the expected meter bucket.
  7. Confirm clone behavior for projectiles, enemy replication, statuses, and pools.
  8. Confirm expiration/removal behavior and test any gap called out in this page.

Home · Architecture · Combat · Extensions · Known gaps · Source index