td

context.rs at [1c4f2f89eb]
Login

context.rs at [1c4f2f89eb]

File src/context.rs artifact c89b4fe4e8 part of check-in 1c4f2f89eb


//! Behavior contexts — one shared core, several event-specific faces.
//!
//! Every hook needs the same capabilities: write commands, read the world, roll dice, know
//! who fired, know the time. Factor that into `BehaviorCtx`; each specific context embeds it
//! and `Deref`s to it, so `c.cmd` / `c.query` / `c.rng` work flat no matter which context you
//! hold. Only the *tail* fields differ per event.

use std::collections::HashMap;

use macroquad::math::Vec2;

use crate::{
    behavior::status::StatusBehavior, command::CommandBuffer, registry::DamageID, rng::Rng,
    spatial::SpatialQuery, types::Source,
};

pub struct BehaviorCtx<'a> {
    pub cmd: &'a mut CommandBuffer,
    pub query: &'a SpatialQuery<'a>,
    pub rng: &'a mut Rng,
    pub source: Source,
    pub now: f32,
}

/// One damage kind's contribution within a single hit, PoE-style: a flat base amount, a
/// pool of "increased/reduced" percentages that all sum together, and a running product of
/// "more/less" factors that each apply independently on top of the increased/reduced total.
///
/// `increased`/`reduced` mods are cheap and meant to stack freely (ten small +5% increased
/// mods behave like one +50%). `more`/`less` mods are meant to be rare and impactful (each
/// one is its own multiplier, so two "50% more" mods compound to 2.25x, not 2x) — that's the
/// whole point of keeping them as two separate accumulators instead of one.
///
/// `Default` is the single place "untouched" is defined for every field, so `flat`,
/// `increased`, and `more` all share the same `.entry(id).or_default()` init path — no
/// per-method special-casing, which is exactly what caused the earlier zeroing bug.
#[derive(Clone, Copy, Debug)]
pub struct DamageEffects {
    flat: f32,
    increased: f32,
    more: f32,
}

impl Default for DamageEffects {
    fn default() -> Self {
        Self {
            flat: 0.0,
            increased: 0.0,
            more: 1.0, // neutral for a running *product*, unlike the other two fields
        }
    }
}

impl DamageEffects {
    /// `flat * (1 + increased) * more`, floored at 0 so reductions can zero out a hit but
    /// never push it negative into healing.
    pub fn compute(&self) -> f32 {
        (self.flat * (1.0 + self.increased).max(0.0) * self.more).max(0.0)
    }
}

#[derive(Default, Clone, Debug)]
pub struct DamageMap {
    pub damage_map: HashMap<DamageID, DamageEffects>,
}

impl DamageMap {
    /// Add flat base damage of `id`'s kind.
    pub fn flat(&mut self, id: DamageID, amount: f32) -> &mut Self {
        self.damage_map.entry(id).or_default().flat += amount;
        self
    }

    /// PoE-style "increased/reduced": every mod on a kind sums into one pool before being
    /// applied once. `0.2` = +20% increased, `-0.3` = -30% reduced. Order never matters.
    pub fn increased(&mut self, id: DamageID, pct: f32) {
        self.damage_map.entry(id).or_default().increased += pct;
    }

    /// PoE-style "more/less": each call is its own independent multiplicative factor,
    /// applied on top of every other "more" mod rather than summed with them. `0.5` = 50%
    /// more, `-0.2` = 20% less. Each factor is floored at 0 before multiplying in, so one
    /// extreme "-150% less" zeroes the total instead of flipping the sign and un-zeroing it
    /// when combined with another "less" mod.
    pub fn more(&mut self, id: DamageID, pct: f32) {
        let e = self.damage_map.entry(id).or_default();
        e.more *= (1.0 + pct).max(0.0);
    }

    pub fn resolve(&self) -> f32 {
        self.damage_map.values().map(DamageEffects::compute).sum()
    }
}

pub struct HitContext<'a> {
    pub core: BehaviorCtx<'a>,
    pub hit_pos: Vec2,
    pub normal: Vec2,
    pub damage: DamageMap,
    pub status: Vec<Box<dyn StatusBehavior>>,
}

impl<'a> HitContext<'a> {
    pub fn new(core: BehaviorCtx<'a>, hit_pos: Vec2, normal: Vec2) -> HitContext<'a> {
        HitContext {
            core,
            hit_pos,
            normal,
            damage: DamageMap::default(),
            status: Vec::new(),
        }
    }
}

/// Firing geometry, available before anything is in flight: muzzle, aim direction, and the
/// locked target. Spawn-shaping behaviors (fan, multishot) live here.
pub struct FireContext<'a> {
    pub core: BehaviorCtx<'a>,
    pub origin: Vec2,
    pub aim: Vec2,
    pub target: Option<crate::spatial::EnemyRef>,
}

/// `dt` — and critically the *sim* dt, so in-flight behaviors freeze when a panel pauses the
/// game. UI uses real dt; this is the other half of the two-clock rule.
pub struct TickContext<'a> {
    pub core: BehaviorCtx<'a>,
    pub dt: f32,
}

/// `reason` lets a behavior trigger on one kind of death only (e.g. only on natural lifetime
/// expiry, not on consume-after-hit or fly-off-screen).
pub struct ExpireContext<'a> {
    pub core: BehaviorCtx<'a>,
    pub reason: ExpireReason,
}

/// Delivered to tower/enemy on-death hooks: where it died and who gets the kill.
pub struct DeathContext<'a> {
    pub core: BehaviorCtx<'a>,
    pub pos: Vec2,
    pub killer: Source,
}

/// Generic bus delivery for SP passives — the `GameEvent` itself is the payload, so the
/// context is just the core.
pub struct EventContext<'a> {
    pub core: BehaviorCtx<'a>,
}

#[derive(PartialEq, Eq)]
pub enum ExpireReason {
    Lifetime,
    OutOfBounds,
    Consumed,
}

/// Deref glue, written once via macro instead of six hand-copied impls. This is what makes
/// `c.cmd.spawn_pool(...)` work directly on any context.
macro_rules! impl_ctx_deref {
    ($t:ident) => {
        impl<'a> std::ops::Deref for $t<'a> {
            type Target = BehaviorCtx<'a>;
            fn deref(&self) -> &Self::Target {
                &self.core
            }
        }
        impl<'a> std::ops::DerefMut for $t<'a> {
            fn deref_mut(&mut self) -> &mut Self::Target {
                &mut self.core
            }
        }
    };
}
impl_ctx_deref!(HitContext);
impl_ctx_deref!(FireContext);
impl_ctx_deref!(TickContext);
impl_ctx_deref!(ExpireContext);
impl_ctx_deref!(DeathContext);
impl_ctx_deref!(EventContext);