//! The `Tower` entity and its loadout. The `TowerBehavior` trait (TP modifications) lives in
//! `crate::behavior::tower`.
//!
//! `weapon_levels` is the tower's *persistent* state — which `WeaponID`s are equipped and at
//! what tier. It deliberately does NOT store `Box<dyn ProjectileBehavior>` instances: those
//! are ephemeral (cheap, cloned onto every shot) and carry no identity, so there'd be no way
//! to answer "does the tower already have Impact, and at what tier" from them. The actual
//! behavior instances are materialized fresh from `WeaponRegistry` at fire time — see
//! `Tower::loadout` + `systems::tick_tower`.
use std::collections::{HashMap, VecDeque};
use macroquad::math::Vec2;
use crate::{
behavior::tower::TowerBehavior,
registry::{WeaponID, WeaponRegistry},
types::DamageDelivery,
world::{CENTER_X, CENTER_Y},
};
pub struct DpsMeter {
window: VecDeque<(f32, f32)>, // (timestamp, amount)
span: f32,
}
impl DpsMeter {
pub fn new(span: f32) -> Self {
Self {
window: VecDeque::new(),
span,
}
}
pub fn record(&mut self, now: f32, amount: f32) {
self.window.push_back((now, amount));
while self
.window
.front()
.is_some_and(|&(t, _)| now - t > self.span)
{
self.window.pop_front();
}
}
pub fn dps(&self) -> f32 {
self.window.iter().map(|&(_, a)| a).sum::<f32>() / self.span
}
}
/// Split + combined damage tracking: `hit` and `dot` are independent meters (so "how hard
/// does my weapon hit" and "how much is poison carrying" can be read separately), and
/// `combined_dps` is just their sum — no third meter needed since it's pure arithmetic on
/// the other two, not a third stream of events to record.
pub struct DamageMeters {
pub hit: DpsMeter,
pub dot: DpsMeter,
}
impl DamageMeters {
pub fn new(span: f32) -> Self {
Self {
hit: DpsMeter::new(span),
dot: DpsMeter::new(span),
}
}
pub fn record(&mut self, delivery: DamageDelivery, now: f32, amount: f32) {
match delivery {
DamageDelivery::Hit => self.hit.record(now, amount),
DamageDelivery::Dot => self.dot.record(now, amount),
}
}
pub fn combined_dps(&self) -> f32 {
self.hit.dps() + self.dot.dps()
}
}
pub struct Tower {
pub pos: Vec2,
pub angle: f32,
pub cooldown: f32,
pub weapon_levels: HashMap<WeaponID, u32>,
pub mods: Vec<Box<dyn TowerBehavior>>,
pub meters: DamageMeters,
}
pub fn tower_location() -> Vec2 {
Vec2::new(CENTER_X, CENTER_Y)
}
impl Tower {
pub fn new(registry: &WeaponRegistry) -> Self {
let mut base = Tower {
pos: tower_location(),
angle: 0.0,
cooldown: 0.0,
weapon_levels: HashMap::new(),
mods: vec![],
meters: DamageMeters::new(3.0), // trailing 3s window
};
let impact = registry.id_of("impact").unwrap();
let coating = registry.id_of("poison_coating").unwrap();
base.equip(impact);
base.equip(coating);
base
}
/// The one hook a weapon-crate pick needs: if the tower already has `id`, bump its
/// tier in place; otherwise add it fresh at tier 1. No separate "add" vs "upgrade" path
/// needed since both are just "this slot's tier goes up by one". Statuses (poison, etc.)
/// are NOT a separate equip slot here — they're applied by whichever `ProjectileBehavior`
/// carries them (e.g. `PoisonCoating`), so equipping one is the same `equip` call as any
/// other weapon.
pub fn equip(&mut self, id: WeaponID) {
*self.weapon_levels.entry(id).or_insert(0) += 1;
}
/// Every equipped weapon's `(id, tier)`, ready to hand to `WeaponRegistry::make` to
/// materialize this frame's actual `Box<dyn ProjectileBehavior>` list.
pub fn loadout(&self) -> impl Iterator<Item = (WeaponID, u32)> + '_ {
self.weapon_levels.iter().map(|(&id, &tier)| (id, tier))
}
}