use serde::{Deserialize, Serialize};
use crate::bignum::Mag;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum ModifierSource {
GreenCoin,
PurpleCoin,
}
impl ModifierSource {
pub fn id(self) -> &'static str {
match self {
Self::GreenCoin => "green_coin",
Self::PurpleCoin => "purple_coin",
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub enum ModifierEffect {
FlatFps(f64),
AddPercent(f64),
MulFactor(Mag),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum ModifierDuration {
Permanent,
Ticks(u32),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Modifier {
pub source: ModifierSource,
pub effects: Vec<ModifierEffect>,
pub duration: ModifierDuration,
#[serde(default)]
pub created_at_tick: u64,
}
impl Modifier {
pub fn strength(&self) -> f32 {
const FADE_TICKS: f32 = 30.0; match self.duration {
ModifierDuration::Permanent => 1.0,
ModifierDuration::Ticks(n) => {
let remaining = n as f32;
if remaining >= FADE_TICKS {
1.0
} else {
let t = (remaining / FADE_TICKS).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FingererAggregate {
pub flat_fps: f64,
pub add_percent: f64,
pub mul_factor: Mag,
}
impl Default for FingererAggregate {
fn default() -> Self {
Self {
flat_fps: 0.0,
add_percent: 0.0,
mul_factor: Mag::ONE,
}
}
}
impl FingererAggregate {
pub fn rebuild(modifiers: &[Modifier]) -> Self {
let mut a = Self::default();
for m in modifiers {
for e in &m.effects {
match *e {
ModifierEffect::FlatFps(v) => a.flat_fps += v,
ModifierEffect::AddPercent(v) => a.add_percent += v,
ModifierEffect::MulFactor(v) => a.mul_factor = a.mul_factor.mul(v),
}
}
}
a
}
}
#[cfg(test)]
mod tests {
use super::*;
fn perm(effects: Vec<ModifierEffect>) -> Modifier {
Modifier {
source: ModifierSource::GreenCoin,
effects,
duration: ModifierDuration::Permanent,
created_at_tick: 0,
}
}
fn mul(v: f64) -> ModifierEffect {
ModifierEffect::MulFactor(Mag::from_f64(v))
}
#[test]
fn empty_aggregate_is_identity() {
let a = FingererAggregate::rebuild(&[]);
assert_eq!(a.flat_fps, 0.0);
assert_eq!(a.add_percent, 0.0);
assert_eq!(a.mul_factor, Mag::ONE);
}
#[test]
fn add_percent_sums_across_modifiers() {
let mods = vec![
perm(vec![ModifierEffect::AddPercent(0.10)]),
perm(vec![ModifierEffect::AddPercent(0.10)]),
];
let a = FingererAggregate::rebuild(&mods);
assert!((a.add_percent - 0.20).abs() < 1e-9);
}
#[test]
fn mul_factor_multiplies_across_modifiers() {
let mods = vec![perm(vec![mul(2.0)]), perm(vec![mul(2.0)])];
let a = FingererAggregate::rebuild(&mods);
assert!((a.mul_factor.to_f64() - 4.0).abs() < 1e-9);
}
#[test]
fn flat_fps_sums_across_modifiers() {
let mods = vec![
perm(vec![ModifierEffect::FlatFps(50.0)]),
perm(vec![ModifierEffect::FlatFps(75.0)]),
];
let a = FingererAggregate::rebuild(&mods);
assert!((a.flat_fps - 125.0).abs() < 1e-9);
}
#[test]
fn mixed_effects_on_same_modifier_all_apply() {
let mods = vec![perm(vec![
ModifierEffect::FlatFps(50.0),
ModifierEffect::AddPercent(0.10),
mul(2.0),
])];
let a = FingererAggregate::rebuild(&mods);
assert!((a.flat_fps - 50.0).abs() < 1e-9);
assert!((a.add_percent - 0.10).abs() < 1e-9);
assert!((a.mul_factor.to_f64() - 2.0).abs() < 1e-9);
}
#[test]
fn mul_factor_stack_survives_billions() {
let mods: Vec<_> = (0..10_000).map(|_| perm(vec![mul(1.5)])).collect();
let a = FingererAggregate::rebuild(&mods);
let expected = (1.5_f64).log10() * 10_000.0;
assert!((a.mul_factor.log10 - expected).abs() < 1e-6);
}
#[test]
fn source_ids_are_stable() {
assert_eq!(ModifierSource::GreenCoin.id(), "green_coin");
assert_eq!(ModifierSource::PurpleCoin.id(), "purple_coin");
}
}