1use crate::context::StatContext;
7use crate::numeric::StatNumeric;
8use crate::resolver::StatResolver;
9use crate::stat_id::StatId;
10use serde::{Deserialize, Serialize};
11
12#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
14pub enum TriggerCondition {
15 Below(f64),
17 Above(f64),
19 Empty,
21 Full,
23}
24
25#[derive(Serialize, Deserialize, Debug, Clone)]
27pub struct ThresholdTrigger {
28 pub condition: TriggerCondition,
29 pub event_name: String,
30}
31
32#[derive(Serialize, Deserialize, Debug, Clone)]
34pub struct TimeEffect {
35 pub name: String,
37 pub amount_per_tick: f64,
40 pub ticks_remaining: u32,
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub struct ResourceEvent {
47 pub event_name: String,
48}
49
50#[derive(Debug, Clone)]
52pub struct ResourcePool {
53 pub max_stat_id: StatId,
55 pub current_value: f64,
57 pub active_effects: Vec<TimeEffect>,
59 pub triggers: Vec<ThresholdTrigger>,
61}
62
63impl ResourcePool {
64 pub fn new(max_stat_id: StatId, resolver: &mut StatResolver, context: &StatContext) -> Self {
66 let max_val = resolver
67 .resolve(&max_stat_id, context)
68 .map(|r| r.value.to_f64())
69 .unwrap_or(0.0);
70
71 Self {
72 max_stat_id,
73 current_value: max_val,
74 active_effects: Vec::new(),
75 triggers: Vec::new(),
76 }
77 }
78
79 pub fn add_effect(&mut self, effect: TimeEffect) {
81 self.active_effects.push(effect);
82 }
83
84 pub fn add_trigger(&mut self, trigger: ThresholdTrigger) {
86 self.triggers.push(trigger);
87 }
88
89 pub fn apply_damage(
92 &mut self,
93 amount: f64,
94 resolver: &mut StatResolver,
95 context: &StatContext,
96 ) -> Vec<ResourceEvent> {
97 self.change_value(-amount, resolver, context)
99 }
100
101 pub fn apply_heal(
104 &mut self,
105 amount: f64,
106 resolver: &mut StatResolver,
107 context: &StatContext,
108 ) -> Vec<ResourceEvent> {
109 self.change_value(amount, resolver, context)
110 }
111
112 pub fn tick(
115 &mut self,
116 resolver: &mut StatResolver,
117 context: &StatContext,
118 ) -> Vec<ResourceEvent> {
119 let mut total_change = 0.0;
120
121 self.active_effects.retain_mut(|effect| {
122 if effect.ticks_remaining > 0 {
123 total_change += effect.amount_per_tick;
124 effect.ticks_remaining -= 1;
125 effect.ticks_remaining > 0
126 } else {
127 false
128 }
129 });
130
131 if total_change != 0.0 {
132 self.change_value(total_change, resolver, context)
133 } else {
134 self.change_value(0.0, resolver, context)
138 }
139 }
140
141 fn change_value(
143 &mut self,
144 delta: f64,
145 resolver: &mut StatResolver,
146 context: &StatContext,
147 ) -> Vec<ResourceEvent> {
148 let max_val = resolver
149 .resolve(&self.max_stat_id, context)
150 .map(|r| r.value.to_f64())
151 .unwrap_or(0.0);
152
153 self.current_value = (self.current_value + delta).clamp(0.0, max_val);
154 self.check_triggers(resolver, context)
155 }
156
157 fn check_triggers(
159 &self,
160 resolver: &mut StatResolver,
161 context: &StatContext,
162 ) -> Vec<ResourceEvent> {
163 let mut events = Vec::new();
164 let max_val = resolver
165 .resolve(&self.max_stat_id, context)
166 .map(|r| r.value.to_f64())
167 .unwrap_or(0.0);
168
169 for trigger in &self.triggers {
170 let is_met = match trigger.condition {
171 TriggerCondition::Below(threshold) => self.current_value < threshold,
172 TriggerCondition::Above(threshold) => self.current_value > threshold,
173 TriggerCondition::Empty => self.current_value <= 0.0,
174 TriggerCondition::Full => self.current_value >= max_val,
175 };
176
177 if is_met {
178 events.push(ResourceEvent {
179 event_name: trigger.event_name.clone(),
180 });
181 }
182 }
183
184 events
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::source::ConstantSource;
192
193 #[test]
194 fn test_resource_pool_tick_and_triggers() {
195 let mut resolver = StatResolver::new();
196 let context = StatContext::new();
197 let max_hp_id = StatId::from("MAX_HP");
198
199 resolver.register_source(max_hp_id.clone(), Box::new(ConstantSource(100.0)));
201
202 let mut pool = ResourcePool::new(max_hp_id, &mut resolver, &context);
203 assert_eq!(pool.current_value, 100.0);
204
205 pool.add_trigger(ThresholdTrigger {
207 condition: TriggerCondition::Empty,
208 event_name: "DEATH".to_string(),
209 });
210
211 pool.add_effect(TimeEffect {
213 name: "Poison".to_string(),
214 amount_per_tick: -10.0,
215 ticks_remaining: 3,
216 });
217
218 let events = pool.tick(&mut resolver, &context);
220 assert_eq!(pool.current_value, 90.0);
221 assert!(events.is_empty());
222
223 pool.tick(&mut resolver, &context);
225 assert_eq!(pool.current_value, 80.0);
226
227 pool.tick(&mut resolver, &context);
229 assert_eq!(pool.current_value, 70.0);
230
231 pool.tick(&mut resolver, &context);
233 assert_eq!(pool.current_value, 70.0); let events = pool.apply_damage(100.0, &mut resolver, &context);
237 assert_eq!(pool.current_value, 0.0);
238 assert_eq!(events.len(), 1);
239 assert_eq!(events[0].event_name, "DEATH");
240 }
241
242 #[test]
243 fn test_resource_pool_healing_and_full() {
244 let mut resolver = StatResolver::new();
245 let context = StatContext::new();
246 let max_hp_id = StatId::from("MAX_HP");
247
248 resolver.register_source(max_hp_id.clone(), Box::new(ConstantSource(100.0)));
249
250 let mut pool = ResourcePool::new(max_hp_id, &mut resolver, &context);
251 pool.apply_damage(50.0, &mut resolver, &context); assert_eq!(pool.current_value, 50.0);
253
254 pool.add_trigger(ThresholdTrigger {
255 condition: TriggerCondition::Full,
256 event_name: "FULLY_HEALED".to_string(),
257 });
258
259 pool.add_effect(TimeEffect {
261 name: "Regen".to_string(),
262 amount_per_tick: 20.0,
263 ticks_remaining: 3,
264 });
265
266 pool.tick(&mut resolver, &context);
268 pool.tick(&mut resolver, &context);
270 let events = pool.tick(&mut resolver, &context);
272 assert_eq!(pool.current_value, 100.0);
273
274 assert_eq!(events.len(), 1);
275 assert_eq!(events[0].event_name, "FULLY_HEALED");
276 }
277}