td

tower.rs at [1c4f2f89eb]
Login

tower.rs at [1c4f2f89eb]

File src/tower.rs artifact d7da16b987 part of check-in 1c4f2f89eb


//! 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))
    }
}