use crate::bonus::{apply_compiled_bonuses, compile_bonus, Bonus, CompiledBonus};
use crate::resolver::StatResolver;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum StackBehavior {
Refresh,
Accumulate { reset_duration: bool },
Independent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusEffect {
pub id: String,
pub name: String,
pub bonuses: Vec<Bonus>,
pub max_stacks: u32,
pub stack_behavior: StackBehavior,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EffectTrigger {
pub event: String,
pub condition: Option<crate::condition::ConditionDef>,
pub effect: StatusEffect,
pub duration_ticks: Option<u32>,
pub stacks: u32,
}
#[derive(Debug, Clone)]
pub struct ActiveStatusEffect {
pub effect: StatusEffect,
pub current_stacks: u32,
pub duration_ticks: Option<u32>, pub compiled_bonuses: Vec<CompiledBonus<crate::numeric::StatValue>>,
}
impl ActiveStatusEffect {
pub fn new(effect: StatusEffect, duration_ticks: Option<u32>, stacks: u32) -> Self {
let compiled = effect
.bonuses
.iter()
.map(compile_bonus::<crate::numeric::StatValue>)
.collect();
Self {
effect,
current_stacks: stacks,
duration_ticks,
compiled_bonuses: compiled,
}
}
}
pub struct StatusEffectManager {
pub active_effects: Vec<ActiveStatusEffect>,
pub triggers: Vec<EffectTrigger>,
dirty: bool,
active_resolver: Option<StatResolver>,
}
impl Default for StatusEffectManager {
fn default() -> Self {
Self::new()
}
}
impl StatusEffectManager {
pub fn new() -> Self {
Self {
active_effects: Vec::new(),
triggers: Vec::new(),
dirty: true,
active_resolver: None,
}
}
pub fn register_trigger(&mut self, trigger: EffectTrigger) {
self.triggers.push(trigger);
}
pub fn fire_event(&mut self, event_name: &str, context: &crate::context::StatContext) {
let triggers_to_fire: Vec<(StatusEffect, Option<u32>, u32)> = self
.triggers
.iter()
.filter(|t| t.event == event_name)
.filter(|t| {
if let Some(ref cond) = t.condition {
cond.evaluate(context)
} else {
true
}
})
.map(|t| (t.effect.clone(), t.duration_ticks, t.stacks))
.collect();
for (effect, duration, stacks) in triggers_to_fire {
self.add_status_effect(effect, duration, stacks);
}
}
pub fn add_status_effect(
&mut self,
effect: StatusEffect,
duration_ticks: Option<u32>,
stacks: u32,
) {
let effect_id = effect.id.clone();
if effect.stack_behavior != StackBehavior::Independent {
if let Some(existing) = self
.active_effects
.iter_mut()
.find(|s| s.effect.id == effect_id)
{
match existing.effect.stack_behavior {
StackBehavior::Refresh => {
existing.duration_ticks = duration_ticks;
}
StackBehavior::Accumulate { reset_duration } => {
existing.current_stacks =
(existing.current_stacks + stacks).min(existing.effect.max_stacks);
if reset_duration {
existing.duration_ticks = duration_ticks;
}
}
_ => {}
}
self.dirty = true;
return;
}
}
let stacks = stacks.min(effect.max_stacks).max(1);
self.active_effects
.push(ActiveStatusEffect::new(effect, duration_ticks, stacks));
self.dirty = true;
}
pub fn tick(&mut self) {
let mut expired = false;
self.active_effects.retain_mut(|status| {
if let Some(ref mut ticks) = status.duration_ticks {
if *ticks > 0 {
*ticks -= 1;
if *ticks == 0 {
expired = true;
return false; }
true } else {
expired = true;
false }
} else {
true }
});
if expired {
self.dirty = true;
}
}
pub fn remove_status_effect(&mut self, effect_id: &str) {
let original_len = self.active_effects.len();
self.active_effects.retain(|s| s.effect.id != effect_id);
if self.active_effects.len() != original_len {
self.dirty = true;
}
}
pub fn get_active_resolver<'a>(
&'a mut self,
base_resolver: &StatResolver,
) -> &'a mut StatResolver {
if self.dirty || self.active_resolver.is_none() {
let mut fork = base_resolver.fork();
for status in &self.active_effects {
for _ in 0..status.current_stacks {
apply_compiled_bonuses(&mut fork, &status.compiled_bonuses);
}
}
self.active_resolver = Some(fork);
self.dirty = false;
}
self.active_resolver.as_mut().unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::StatContext;
use crate::numeric::StatNumeric;
use crate::source::ConstantSource;
use crate::stat_id::StatId;
#[test]
fn test_status_effect_manager_buffs() {
let mut base_resolver = StatResolver::new();
let atk_id = StatId::from("ATK");
base_resolver.register_source(atk_id.clone(), Box::new(ConstantSource(100.0)));
let mut status_manager = StatusEffectManager::new();
let context = StatContext::new();
let atk = status_manager
.get_active_resolver(&base_resolver)
.resolve(&atk_id, &context)
.unwrap();
assert_eq!(atk.value.to_f64(), 100.0);
let buff = StatusEffect {
id: "BUFF_WARCRY".to_string(),
name: "Warcry".to_string(),
bonuses: vec![Bonus::add(atk_id.clone())
.flat(50.0)
.in_phase(crate::transform::TransformPhase::Additive)],
max_stacks: 1,
stack_behavior: StackBehavior::Refresh,
};
status_manager.add_status_effect(buff, Some(2), 1);
let atk = status_manager
.get_active_resolver(&base_resolver)
.resolve(&atk_id, &context)
.unwrap();
assert_eq!(atk.value.to_f64(), 150.0);
status_manager.tick();
let atk = status_manager
.get_active_resolver(&base_resolver)
.resolve(&atk_id, &context)
.unwrap();
assert_eq!(atk.value.to_f64(), 150.0);
status_manager.tick();
let atk = status_manager
.get_active_resolver(&base_resolver)
.resolve(&atk_id, &context)
.unwrap();
assert_eq!(atk.value.to_f64(), 100.0); }
#[test]
fn test_status_effect_manager_accumulation() {
let mut base_resolver = StatResolver::new();
let def_id = StatId::from("DEF");
base_resolver.register_source(def_id.clone(), Box::new(ConstantSource(50.0)));
let mut status_manager = StatusEffectManager::new();
let context = StatContext::new();
let debuff = StatusEffect {
id: "SUNDER_ARMOR".to_string(),
name: "Sunder Armor".to_string(),
bonuses: vec![Bonus::add(def_id.clone())
.flat(-10.0)
.in_phase(crate::transform::TransformPhase::Additive)],
max_stacks: 5,
stack_behavior: StackBehavior::Accumulate {
reset_duration: true,
},
};
status_manager.add_status_effect(debuff.clone(), Some(3), 1);
let def = status_manager
.get_active_resolver(&base_resolver)
.resolve(&def_id, &context)
.unwrap();
assert_eq!(def.value.to_f64(), 40.0);
status_manager.add_status_effect(debuff.clone(), Some(3), 1);
let def = status_manager
.get_active_resolver(&base_resolver)
.resolve(&def_id, &context)
.unwrap();
assert_eq!(def.value.to_f64(), 30.0); }
#[test]
fn test_status_effect_manager_triggers() {
let mut base_resolver = StatResolver::new();
let hp_id = StatId::from("HP");
let def_id = StatId::from("DEF");
base_resolver.register_source(hp_id.clone(), Box::new(ConstantSource(100.0)));
base_resolver.register_source(def_id.clone(), Box::new(ConstantSource(50.0)));
let mut status_manager = StatusEffectManager::new();
let shield_effect = StatusEffect {
id: "EFFECT_SHIELD".to_string(),
name: "Shield".to_string(),
bonuses: vec![Bonus::add(def_id.clone())
.flat(30.0)
.in_phase(crate::transform::TransformPhase::Additive)],
max_stacks: 1,
stack_behavior: StackBehavior::Refresh,
};
let condition = crate::condition::ConditionDef::LessThan {
key: "current_hp".to_string(),
value: 30.0,
};
let trigger = EffectTrigger {
event: "on_damage_taken".to_string(),
condition: Some(condition),
effect: shield_effect,
duration_ticks: Some(3),
stacks: 1,
};
status_manager.register_trigger(trigger);
let mut context = StatContext::new();
context.set("current_hp", 70.0);
status_manager.fire_event("on_damage_taken", &context);
let def = status_manager
.get_active_resolver(&base_resolver)
.resolve(&def_id, &context)
.unwrap();
assert_eq!(def.value.to_f64(), 50.0);
context.set("current_hp", 20.0);
status_manager.fire_event("on_damage_taken", &context);
let def = status_manager
.get_active_resolver(&base_resolver)
.resolve(&def_id, &context)
.unwrap();
assert_eq!(def.value.to_f64(), 80.0); }
}