td

CombatPipeline
Login

CombatPipeline

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

Combat pipeline

This page describes the executable combat path in the current Rust source. Labels mean:

Label Meaning
Live Called by the main loop and mutates or renders current play
Partial Some implementation exists, but important behavior is absent
Unwired Defined but not reached by the current runtime
Risk Live behavior with a correctness or maintenance hazard

Frame order

Live. Macroquad clamps simulation dt to 0.05 seconds, reads unclamped wall-clock time only for the crate UI, and steps the world even when a weapon crate is open.

main loop
  get_frame_time().min(0.05)
  step_world
    now += dt
    tick_wave
    tick_enemies
    tick_tower
    tick_projectiles
    death_sweep
    animation_system
  draw_simulation
  weapons_crate UI, if open
  apply one UI action, if any
  next_frame

Sources: src/main.rs, src/systems.rs.

Deferred command boundary

Live. Enemy, projectile, status, and death behavior passes use run_system. Each pass:

fork world RNG
  -> collect EnemySnapshot
  -> rebuild Grid
  -> run hooks against snapshot/query
  -> append Commands
  -> apply_commands to live World in insertion order
  -> deliver generated GameEvents in waves

The command buffer is the write side of behavior execution. It supports projectile, pool, enemy, and animation spawning; damage; status application; despawn; resource grants; events; and enemy replication. Application is deliberately non-reentrant: newly caused deaths are not dispatched recursively inside apply_commands.

Command Current application
SpawnProjectile Allocates a fresh entity ID, then pushes
SpawnPool Pushes a pool without an entity ID
SpawnEnemy Allocates a fresh entity ID, then pushes
Damage Resolves DamageMap, applies resistance, records tower DPS, may emit EnemyKilled
ApplyStatus Appends a boxed status to the target
Despawn Removes matching enemies and projectiles
GrantResource(Wp) Adds player XP; Sp and Tp do nothing
Emit Queues a game event
SpawnAnimation Allocates an ID and pushes
Replicate(Enemy) Clones the live enemy's mutated behaviors into a fresh registry template

Sources: src/command.rs, src/world.rs, src/context.rs.

Projectile hit flow

Live. The tower aims at the nearest vector-distance enemy, creates one projectile at the tower position when its cooldown expires, and materializes a fresh behavior list from the tower loadout. Behaviors are sorted by their own priority.

projectile behavior tick
  -> integrate position
  -> SpatialQuery::first_hit
       yes: compute enemy surface normal
            run every on_hit_enemy hook
            fold reflect/pierce responses
            queue one Damage command from HitContext.damage
            queue each accumulated status
       no:  test arena wall
            run every on_hit_wall hook
  -> test lifetime and run on_expire(Lifetime)
  -> retain unless marked consumed/expired
  -> apply queued commands after all projectiles

dispatch_enemy_hit runs every behavior even when an earlier behavior requests reflection or piercing. The last non-None reflection wins; any behavior requesting pierce keeps the projectile unless a reflection is present. With no reflect and no pierce, the projectile is consumed. Damage and statuses are still queued for that hit.

Risk. The collision snapshot is fixed for the entire projectile pass. Multiple projectiles can select the same enemy before queued damage is applied. Commands are then applied in order against the live enemy, including one that is already at or below zero HP.

Sources: src/systems.rs, src/behavior/projectile.rs, src/spatial.rs, src/geometry.rs.

DamageMap

Live. A hit carries a map keyed by typed DamageID. Each kind has independent accumulators:

kind damage = max(0, flat * max(0, 1 + sum(increased)) * product(max(0, 1 + more)))
hit total   = sum(all kind damage)

TowerBaseWeapon contributes physical flat damage equal to its tier and 1% * (tier - 1) increased physical damage. At application, the enemy handles each kind separately:

damage_done = computed_kind_damage * (1 - enemy_resistance_for_kind)
enemy.hp   -= damage_done

Risk. Resistance is not clamped. A resistance above 1 makes damage_done negative and therefore heals; negative resistance amplifies damage. Overkill is not clamped to remaining HP, so meters record the full resolved amount.

Partial. DamageBehavior factories for Physical and Poison are registered, but combat never calls DamageBehavior::compute; live resolution is DamageEffects::compute plus the enemy resistance map.

Sources: src/context.rs, src/enemy.rs, src/weapons/impact.rs, src/behavior/damage.rs, src/registry.rs.

Poison damage over time

Live. Every Poison Coating hit appends an independent PeriodDamage status. Tier t configures total-rate amount 3 + 1.5t, interval 1 second, and lifetime 5 seconds. Despite the interval name, damage is continuous every simulation frame:

frame DoT = (amount / interval) * dt
duration += dt
queue Damage(target, poison map, captured projectile source, Dot)
expire status when duration > lifetime

The projectile source is captured when the status is created, so tower-applied poison remains attributed to Source::Tower during the generic enemy tick, whose context source is None. This feeds DoT DPS and kill attribution.

Risk. Expiration uses duration > lifetime after queuing damage, so a status also queues damage on the first frame that moves it beyond five seconds. Repeated hits stack statuses without merging, refreshing, or a cap.

Sources: src/weapons/poison.rs, src/behavior/status.rs, src/types.rs.

Death and event delivery

Live. A Damage command that leaves an enemy dead immediately appends GameEvent::EnemyKilled(Corpse). Event delivery handles that event by spawning a 2.5-second death animation and granting the corpse XP. Events may generate commands and further events for up to eight waves.

Later in the same frame, death_sweep finds every non-alive enemy, dispatches its live on_death behaviors, and emits another EnemyKilled(Corpse). After that pass it removes dead enemies and may open a crate for queued level-ups.

Damage kills enemy
  -> EnemyKilled event #1
     -> animation + XP
  ...
death_sweep sees same dead enemy
  -> on_death (for example split)
  -> EnemyKilled event #2
     -> animation + XP
  -> remove enemy

Risk: duplicate kill rewards. A normal command-buffer kill emits at least two kill events: one in apply_commands and one in death_sweep. Multiple queued damage commands against the already-dead target can each return Killed and emit additional events. Each event grants full XP and creates a death animation. There is no “death handled” flag or event deduplication.

The event cascade silently stops after eight waves; remaining events are dropped without the suggested log or assertion.

Sources: src/world.rs, src/systems.rs, src/enemy.rs.

Expiry and hooks

Path Status
Projectile on_tick Live
Projectile on_hit_enemy / on_hit_wall Live
Projectile on_expire(Lifetime) Live, with current lifetime fixed at 1000 seconds
ExpireReason::OutOfBounds / Consumed Unwired
Projectile on_fire Unwired; no dispatcher is called
Tower behavior hooks and tower.mods Unwired
Pool behavior pass Unwired; see IncompleteSystems

See also EnemiesAndWaves, TowersWeaponsProgression, and SpatialGeometryRng.

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