use serde::{Deserialize, Serialize};
#[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(f64),
}
#[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: f64,
}
impl Default for FingererAggregate {
fn default() -> Self {
Self {
flat_fps: 0.0,
add_percent: 0.0,
mul_factor: 1.0,
}
}
}
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 *= 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,
}
}
#[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, 1.0);
}
#[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![ModifierEffect::MulFactor(2.0)]),
perm(vec![ModifierEffect::MulFactor(2.0)]),
];
let a = FingererAggregate::rebuild(&mods);
assert!((a.mul_factor - 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),
ModifierEffect::MulFactor(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 - 2.0).abs() < 1e-9);
}
#[test]
fn source_ids_are_stable() {
assert_eq!(ModifierSource::GreenCoin.id(), "green_coin");
assert_eq!(ModifierSource::PurpleCoin.id(), "purple_coin");
}
}