td

TowersWeaponsProgression
Login

TowersWeaponsProgression

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

Towers, weapons, and progression

Tower runtime

Live. The sole tower is created at (450, 450) with zero cooldown and a three-second damage-meter window. Every frame it aims at the enemy with minimum center-to-center distance. When cooldown is non-positive and the enemy vector is nonempty, it:

  1. Resets cooldown to 1 / 4.2 seconds.
  2. Builds a velocity of 300 units/s along the current angle.
  3. Allocates a projectile ID.
  4. Materializes every equipped (WeaponID, tier) through WeaponRegistry.
  5. Sorts the resulting behaviors by ProjectileBehavior::priority.
  6. Pushes one radius-5 projectile with source Tower and lifetime 1000 seconds.

Risk. Target selection does not filter hp <= 0, so an enemy killed earlier in the frame but not yet swept can still influence aim and permit firing.

Risk. The cooldown emits at most one shot per frame and resets rather than carrying negative remainder. Low frame rates therefore lose shots instead of catching up.

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

Persistent loadout, ephemeral behaviors

Live. Tower.weapon_levels is a HashMap<WeaponID, u32>. It is the persistent loadout; boxed projectile behaviors are newly constructed for every shot.

crate pick / initial equip
  -> weapon_levels[id] += 1

next shot
  -> for each (id, tier)
       WeaponRegistry::make(id, tier)
  -> sort behavior instances by behavior priority
  -> attach to projectile

This means a crate upgrade affects the next shot with no rebuild operation. The initial tower already equips both registered weapons at tier 1.

Weapon Initial state Behavior priority Implemented tier effect
Impact Tier 1 IMPACT = 100 Physical flat = tier; increased = 1% × (tier − 1)
Poison Coating Tier 1 DEFAULT = 0 Poison amount = 3 + 1.5 × tier over nominal 5 s

Poison runs before Impact because of priority, but it only appends a status while Impact appends damage, so current results do not depend on their relative execution beyond that guarantee.

Risk. Loadout iteration begins in a standard HashMap, whose order is not a replay-stable content ordering. Sorting fixes current cross-priority order, but equal-priority weapons inherit that arbitrary starting order.

Sources: src/tower.rs, src/registry.rs, src/weapons/impact.rs, src/weapons/poison.rs.

Weapon and content registries

Live. The generic registry interns string names into dense, type-distinct IDs and stores display name, integer priority, factory, and an all list. Separate marker types prevent mixing weapon, enemy, damage, and status IDs at compile time.

Registry Seeded entries Current use
Weapon impact, poison_coating Loadout creation and crate display/offers
Enemy blob Wave and split construction
Damage physical, poison IDs supplied to weapons/statuses; factories themselves unused
Status poison Constructed and stored, but live poison bypasses it

Partial. Registry definition priority is readable through Registry::priority, but firing sorts the created behavior's priority instead. The status registry's poison creates a fixed Source::None status and is not requested by current gameplay.

Risk. Re-registering a name overwrites its definition but appends the same typed ID to all again. There is no duplicate guard. Crate rolling assumes the weapon catalog is nonempty; current seeding satisfies that assumption.

Sources: src/registry.rs, src/world.rs.

XP and levels

Live. Only ResourceKind::Wp is connected, and it is treated as player XP. Required XP is:

required(level) = round(10.0 * 1.6^level)

The player starts at level 0, XP 0, next requirement 10, and no pending picks. Adding XP loops across as many thresholds as the award crosses; each threshold increments both level and level_ups.

Resource State
Wp Live: calls Player::add_xp
Sp Unwired: command arm is empty
Tp Unwired: command arm is empty

XP comes from EnemyKilled(Corpse) event handling. The duplicate event risk can multiply awards; see CombatPipeline.

Sources: src/world.rs, src/types.rs.

Weapon crate state machine

Live. After death_sweep, if picks are pending, the player did not manually close a crate, and the screen is Playing, the world rolls three offers and opens the crate.

Offers are sampled independently with replacement:

offer[i] = all_weapons[parent_rng.next_u32() % all_weapons.len()]

With two registered weapons and three cards, duplicates are expected and valid.

Input While crate open
Left / Right Move cursor within cards 0–2
Enter Pick selected weapon
Space Close without consuming a level-up

Picking calls tower.equip(id), consumes one pending level, and resets player status to None. If more picks remain, dismiss_weapon_crate immediately rolls another crate; otherwise it returns to Playing.

Closing sets Screen::Playing and PlayerStatus::ManuallyClosed without consuming the pending pick. Automatic reopening is then suppressed. Pressing Space during normal play calls open_weapon_crate; if a pick remains, this reopens it. A successful later pick resets status.

Live but surprising. Space during normal play cannot create a free crate because open_weapon_crate returns when no level-up is pending.

Risk. Simulation is not paused while the crate is displayed. main calls step_world for every screen state before drawing and handling the crate, despite the “two-clock/pause” comments in context code. Enemies, projectiles, waves, deaths, and level gains continue behind the overlay.

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

DPS meters

Live. Tower-attributed Damage commands record their resolved amount by delivery type:

Meter Receives
Hit DamageDelivery::Hit
DoT DamageDelivery::Dot
Total Hit DPS + DoT DPS

Each meter stores (world.now, amount) samples. On every new record it removes samples older than three seconds, then reports sum / 3. Rendering displays all three values.

Risk. Old samples are pruned only when another damage record arrives. If combat stops, displayed DPS can remain nonzero indefinitely.

Risk. The denominator is always three seconds, including the first seconds of play, and damage is not capped to remaining enemy HP. These are valid implementation choices but should not be read as instantaneous DPS or effective HP removed.

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

Tower and fire extension hooks

Hook/storage State Evidence
ProjectileBehavior::on_fire Unwired Defined, no dispatcher/call site
FireContext Unwired Defined with origin, aim, target; never constructed
TowerBehavior hit/tick/expire hooks Unwired Trait exists; no dispatch functions
Tower.mods Unwired Initialized empty and never read by systems
Multishot/fan shot shaping Unwired Tower directly pushes exactly one projectile

The projectile hooks after launch are live, but the pre-flight and tower-modification layers are scaffolding only. See IncompleteSystems.

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