bevy_gauge 0.2.2

bevy_gauge - a flexible stats system for Bevy
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
use bevy::platform::collections::HashMap;
use evalexpr::{ContextWithMutableVariables, Value, IterateVariablesContext, Context};

use super::prelude::*;
use dashmap::DashMap;

/// Defines how modifiers are combined, typically for a specific part of a stat.
///
/// When multiple modifiers apply to the same stat part (e.g., multiple sources of "increased damage"),
/// `ModType` determines if their effects are additive or multiplicative.
#[derive(PartialEq, Debug, Clone, Default)]
pub enum ModType {
    /// Modifiers are summed together. This is common for "increased" or "added" effects.
    /// For example, +10% damage and +20% damage result in +30% damage.
    #[default]
    Add,
    /// Modifiers are multiplied together. This is common for "more" or "less" effects.
    /// For example, a 10% "more" multiplier (1.1x) and a 20% "more" multiplier (1.2x)
    /// result in a total multiplier of 1.1 * 1.2 = 1.32x (or 32% more).
    Mul,
}

/// The core internal enum representing different kinds of stat structures and behaviors.
/// Each variant dictates how a stat stores its data, processes modifiers, and calculates its final value.
/// This is primarily used internally by the stat system based on configurations provided in `Config`.
#[derive(Debug, Clone)]
pub(crate) enum StatType {
    /// Simple numeric value with no complex modification rules.
    /// Example: Current resource values that are modified directly.
    Flat(Flat),
    /// Value that can be modified by additive or multiplicative modifiers.
    /// Example: Damage that can be increased by percentage modifiers
    Modifiable(Modifiable),
    /// Stat composed of multiple parts combined through an expression.
    /// Example: Final damage = base * (1 + increased) * (1 + more)
    Complex(Complex),
    /// Stat that can be filtered and queried by tags.
    /// Example: "Increased fire damage with axes" combines fire and axe modifiers
    Tagged(Tagged),
}

impl Stat for StatType {
    fn new(path: &StatPath) -> Self {
        let stat_type_name = Konfig::get_stat_type(path.name);
        match stat_type_name.as_str() {
            "Flat" => StatType::Flat(Flat::new(path)),
            "Modifiable" => StatType::Modifiable(Modifiable::new(path)),
            "Complex" => StatType::Complex(Complex::new(path)),
            "Tagged" => StatType::Tagged(Tagged::new(path)),
            _ => panic!("Invalid stat type: {}", stat_type_name),
        }
    }

    fn initialize(&self, path: &StatPath, stats: &mut Stats) {
        match self {
            StatType::Flat(flat) => flat.initialize(path, stats),
            StatType::Modifiable(modifiable) => modifiable.initialize(path, stats),
            StatType::Complex(complex) => complex.initialize(path, stats),
            StatType::Tagged(tagged) => tagged.initialize(path, stats),
        }
    }

    fn add_modifier(&mut self, path: &StatPath, modifier: ModifierType) {
        match self {
            StatType::Flat(flat) => flat.add_modifier(path, modifier),
            StatType::Modifiable(modifiable) => modifiable.add_modifier(path, modifier),
            StatType::Complex(complex) => complex.add_modifier(path, modifier),
            StatType::Tagged(tagged) => tagged.add_modifier(path, modifier),
        }
    }

    fn remove_modifier(&mut self, path: &StatPath, modifier: &ModifierType) {
        match self {
            StatType::Flat(flat) => flat.remove_modifier(path, modifier),
            StatType::Modifiable(modifiable) => modifiable.remove_modifier(path, modifier),
            StatType::Complex(complex) => complex.remove_modifier(path, modifier),
            StatType::Tagged(tagged) => tagged.remove_modifier(path, modifier),
        }
    }
    
    fn evaluate(&self, path: &StatPath, stats: &Stats) -> f32 {
        match self {
            StatType::Flat(flat) => flat.0,
            StatType::Modifiable(modifiable) => modifiable.evaluate(path, stats),
            StatType::Complex(complex) => complex.evaluate(path, stats),
            StatType::Tagged(tagged) => tagged.evaluate(path, stats),
        }
    }

    fn clear_internal_cache(&mut self, path: &StatPath) -> Vec<String> {
        match self {
            StatType::Flat(_) => Vec::new(), /* No internal cache for Flat */
            StatType::Modifiable(_) => Vec::new(), /* No internal cache for Modifiable */
            StatType::Complex(_) => Vec::new(), /* No internal cache for Complex currently, or it's handled by parts */
            StatType::Tagged(tagged) => tagged.clear_internal_cache(path),
        }
    }
}

/// The simplest stat type - just holds a single numeric value.
/// 
/// # Examples
/// 
/// - Base health: Direct numeric value
/// - Resource costs: Simple numbers that get modified directly
/// - Level requirements: Plain numeric values
/// 
/// # Modification Behavior
/// 
/// - Only accepts literal number modifications
/// - Modifications directly add to or subtract from the base value
/// - No support for percentage or complex modifications
#[derive(Debug, Clone)]
pub(crate) struct Flat(f32);

impl Stat for Flat {
    fn new(_path: &StatPath) -> Self { Self(0.0) }

    fn add_modifier(&mut self, _path: &StatPath, modifier: ModifierType) {
        if let ModifierType::Literal(value) = modifier {
            self.0 += value;
        }
    }

    fn remove_modifier(&mut self, _path: &StatPath, modifier: &ModifierType) {
        if let ModifierType::Literal(value) = modifier {
            self.0 -= value;
        }
    }

    fn set(&mut self, _path: &StatPath, value: f32) { self.0 = value; }

    fn evaluate(&self, _path: &StatPath, _stats: &Stats) -> f32 { self.0 }
}

/// A stat that can be modified by either additive or multiplicative modifiers.
/// 
/// # Fields
/// 
/// * `relationship`: Determines if modifiers are added or multiplied
/// * `base`: The starting value before any modifiers
/// * `mods`: List of expressions that modify the base value
/// 
/// # Examples
/// 
/// Additive (relationship = Add):
/// ```text
/// base = 100
/// mod1 = +50% (0.5)
/// mod2 = +30% (0.3)
/// final = 100 * (1 + 0.5 + 0.3) = 180
/// ```
/// 
/// Multiplicative (relationship = Mul):
/// ```text
/// base = 100
/// mod1 = 50% more (1.5)
/// mod2 = 30% more (1.3)
/// final = 100 * 1.5 * 1.3 = 195
/// ```
#[derive(Debug, Clone)]
pub(crate) struct Modifiable {
    pub(crate) relationship: ModType,
    pub(crate) base: f32,
    pub(crate) mods: Vec<Expression>,
}

impl Stat for Modifiable {
    fn new(path: &StatPath) -> Self {
        let relationship = Konfig::get_relationship_type(path.name);
        let base = if relationship == ModType::Mul { 1.0 } else { 0.0 };
        Self { relationship, base, mods: Vec::new() }
    }

    fn add_modifier(&mut self, _path: &StatPath, modifier: ModifierType) {
        match modifier {
            ModifierType::Literal(vals) => { 
                match self.relationship {
                    ModType::Add => self.base += vals,
                    ModType::Mul => {
                        // For multiplicative stats, convert percentage to multiplier (0.4 -> 1.4)
                        let multiplier = vals + 1.0;
                        // If base is 0.0 from a previous Add context or uninitialized for Mul, treat this literal as the new base for multiplication
                        if self.base == 0.0 && self.mods.is_empty() { 
                            self.base = multiplier;
                        } else {
                            self.base *= multiplier;
                        }
                    }
                }
            }
            ModifierType::Expression(expression) => self.mods.push(expression.clone()),
        }
    }

    fn remove_modifier(&mut self, _path: &StatPath, modifier: &ModifierType) {
        match modifier {
            ModifierType::Literal(vals) => { 
                match self.relationship {
                    ModType::Add => self.base -= vals,
                    ModType::Mul => {
                        // For multiplicative stats, convert percentage to multiplier (0.4 -> 1.4)
                        let multiplier = vals + 1.0;
                        self.base /= multiplier;
                    }
                }
            }
            ModifierType::Expression(expression) => {
                if let Some(pos) = self.mods.iter().position(|e| e == expression) {
                    self.mods.remove(pos);
                }
            }
        }
    }

    fn evaluate(&self, _path: &StatPath, stats: &Stats) -> f32 {
        let computed: Vec<f32> = self.mods.iter()
            .map(|expr| {
                // Use Stats::evaluate_expression which handles missing variables properly
                stats.evaluate_expression(&expr.definition, None).unwrap_or(0.0)
            })
            .collect();
        
        let result = match self.relationship {
            ModType::Add => self.base + computed.iter().sum::<f32>(),
            ModType::Mul => {
                // For multiplicative stats, add 1.0 to each modifier (converts percentages to multipliers)
                // and start with 1.0 if there are no modifiers
                let multiplier = if computed.is_empty() {
                    1.0
                } else {
                    computed.iter().map(|v| v + 1.0).product::<f32>()
                };
                self.base * multiplier
            },
        };
        result
    }
}

/// A stat composed of multiple parts that are combined through an expression.
/// 
/// # Fields
/// 
/// * `total`: Expression that defines how to combine the parts
/// * `modifier_steps`: Named parts that can each be modified independently
/// 
/// # Example
/// 
/// Final damage calculation:
/// ```text
/// parts:
///   - base_damage: Flat value
///   - increased_damage: Sum of all "increased" modifiers
///   - more_damage: Product of all "more" modifiers
/// total = "base_damage * (1 + increased_damage) * more_damage"
/// ```
/// 
/// Each part can have its own modifiers and they're combined according to the expression.
#[derive(Debug, Clone)]
pub(crate) struct Complex {
    pub(crate) total: Expression,
    pub(crate) modifier_steps: HashMap<String, Modifiable>,
}

impl Stat for Complex {
    fn new(path: &StatPath) -> Self {
        let total_expression_str = Konfig::get_total_expression(path.name);
        let compiled_expression = Expression::new(&total_expression_str).unwrap_or_else(|e| panic!("Failed to compile total_expression for {}: {} - Error: {}", path.name, total_expression_str, e));

        let mut modifier_steps = HashMap::new();
        for part in compiled_expression.compiled.iter_identifiers() {
            let part_path = &StatPath::parse(part);
            let step = Modifiable::new(part_path);
            modifier_steps.insert(part.to_string(), step);
        }

        Self {
            total: compiled_expression,
            modifier_steps,
        }
    }

    fn initialize(&self, path: &StatPath, stats: &mut Stats) {
        for part in self.total.compiled.iter_identifiers() {
            let part_path = format!("{}.{}", path.name, part);
            stats.add_dependent(&part_path, DependentType::LocalStat(path.name.to_string()));
        }
    }

    fn add_modifier(&mut self, path: &StatPath, modifier: ModifierType) {
        let Some(part_key) = path.part else { return };
        let part = self.modifier_steps.get_mut(part_key).unwrap();
        part.add_modifier(path, modifier);
    }

    fn remove_modifier(&mut self, path: &StatPath, modifier: &ModifierType) {
        let Some(part_key) = path.part else { return };
        let part = self.modifier_steps.get_mut(part_key).unwrap();
        part.remove_modifier(path, modifier);
    }
    
    fn evaluate(&self, path: &StatPath, stats: &Stats) -> f32 {
        if let Some(part_key) = path.part {
            let Some(part) = self.modifier_steps.get(part_key) else { return 0.0 };
            let part_total = part.evaluate(path, stats);
            stats.set_cached(path.full_path, part_total);
            return part_total;
        } else {
            let mut expression_context = evalexpr::HashMapContext::new();

            for (part_name, _modifiable_part_definition) in &self.modifier_steps {
                let part_full_path_str = format!("{}.{}", path.name, part_name);
                let part_value = stats.evaluate_by_string(&part_full_path_str);
                expression_context.set_value(part_name.clone(), evalexpr::Value::Float(part_value as f64))
                    .map_err(|e| StatError::Internal{details: format!("Failed to set part '{}' in Complex eval context: {}", part_name, e)})
                    .unwrap();
            }
            
            let main_cache_context = stats.get_context();
            for (var_key, var_val) in main_cache_context.iter_variables() {
                if expression_context.get_value(&var_key).is_none() {
                    expression_context.set_value(var_key.clone().into(), var_val.clone())
                        .map_err(|e| StatError::Internal{details: format!("Failed to merge var '{}' in Complex eval context: {}", var_key, e)})
                        .unwrap();
                }
            }
            
            let total_expr_str = self.total.definition.as_str();
            let total = self.total.compiled
                .eval_with_context(&expression_context)
                .map_err(|e| StatError::ExpressionError { expression: total_expr_str.to_string(), details: e.to_string() })
                .unwrap()
                .as_number()
                .unwrap_or(0.0) as f32;

            stats.set_cached(&path.full_path, total);
            return total;
        }
    }
}

/// A stat that supports tag-based filtering and querying.
/// 
/// Tagged stats handle cases where modifiers can apply broadly but need to be
/// queried specifically. For example, "increased fire damage" applies to all weapons,
/// but we query it as "increased fire damage with axes" for specific calculations.
/// 
/// # Fields
/// 
/// * `total`: Expression combining all parts after tag filtering
/// * `modifier_steps`: Map of parts to their tagged modifiers
/// * `query_cache`: Thread-safe cache of previously computed queries
/// 
/// # Caching
/// 
/// Uses DashMap for thread-safe caching without blocking. Cache entries are
/// invalidated when relevant modifiers change. The cache is eventually consistent,
/// meaning duplicate work might happen but results will be correct.
/// 
/// # Example
/// 
/// ```text
/// // Adding modifiers:
/// "50% increased fire damage" -> applies to all weapons
/// "30% increased damage with axes" -> applies to all damage types
/// 
/// // Querying:
/// "increased fire damage with axes" -> combines both modifiers
/// ```
#[derive(Debug, Clone)]
pub(crate) struct Tagged {
    pub(crate) total: Expression,
    pub(crate) modifier_steps: HashMap<String, TaggedEntry>,
    pub(crate) query_tracker: DashMap<(String, u32), ()>, // Track queries made, but don't cache results
}

/// Entry in a Tagged stat's modifier map, storing modifiers for a specific combination of tags.
/// The u32 key represents the tag bits, and the Modifiable contains the actual stat modifications.
#[derive(Debug, Clone)]
pub(crate) struct TaggedEntry(pub HashMap<u32, Modifiable>);

impl TaggedEntry {
    fn new() -> Self {
        Self(HashMap::new())
    }
}

impl Tagged {
    fn evaluate_part(&self, part: &str, tag: u32, stats: &Stats) -> f32 {
        let Some(tagged_entry) = self.modifier_steps.get(part) else {
            return 0.0;
        };

        let mod_type = Konfig::get_relationship_type(part);

        let mut relevant_mod_values = vec![0.0];
        for (mod_tag_key, modifiable_stat_for_tag) in &tagged_entry.0 {            
            // Check if a permissive modifier applies to a strict query
            // Permissive modifiers start as u32::MAX and have category bits cleared, then specific bits set
            // Strict queries are just the combination of specific bits (e.g., FIRE | AXE = 65)
            // A permissive modifier applies if the strict query "satisfies" what the modifier requires
            let modifier_applies = if tag == 0 {
                true // tag 0 means "match everything"
            } else if *mod_tag_key == u32::MAX {
                false // u32::MAX means no valid tags were resolved, shouldn't match anything
            } else {
                // For permissive tags: check if the strict query has all the bits that the permissive modifier requires
                // The permissive modifier has the required bits set and "don't care" bits as 1
                // We need to check if (query & modifier) == query, meaning the modifier covers the query
                (tag & mod_tag_key) == tag
            };
            
            if modifier_applies {
                let mod_value = modifiable_stat_for_tag.evaluate(&StatPath::parse(""), stats);
                relevant_mod_values.push(mod_value);
            }
        }
        
        let final_value = match mod_type {
            ModType::Add => relevant_mod_values.iter().sum(),
            ModType::Mul => {
                // For multiplicative stats, add 1.0 to each modifier (converts percentages to multipliers)
                relevant_mod_values.iter().map(|v| v + 1.0).product()
            },
        };
        final_value
    }


}

impl Stat for Tagged {
    fn new(path: &StatPath) -> Self {
        let total_expression_str = Konfig::get_total_expression(path.name);
        let compiled_expression = Expression::new(&total_expression_str).unwrap_or_else(|e| panic!("Failed to compile total_expression for {}: {} - Error: {}", path.name, total_expression_str, e));

        let mut modifier_steps = HashMap::new();
        for part in compiled_expression.compiled.iter_identifiers() {
            let step = TaggedEntry::new();
            modifier_steps.insert(part.to_string(), step);
        }

        Self {
            total: compiled_expression,
            modifier_steps,
            query_tracker: DashMap::new(),
        }
    }

    fn add_modifier(&mut self, path: &StatPath, modifier: ModifierType) {
        let Some(tag) = path.tag else { return };
        let Some(part) = path.part else { return };

        let step_map = self.modifier_steps.entry(part.to_string())
            .or_insert(TaggedEntry(HashMap::new()));
        let step = step_map.0.entry(tag).or_insert(Modifiable::new(path));
        step.add_modifier(path, modifier);

        // Note: We can't call invalidate_dependent_cache_entries here because we don't have access to Stats
        // The invalidation will be handled by the StatsMutator when it calls clear_internal_cache_for_path
    }

    fn remove_modifier(&mut self, path: &StatPath, modifier: &ModifierType) {
        let Some(tag) = path.tag else { return };
        let Some(part) = path.part else { return };

        if let Some(step_map) = self.modifier_steps.get_mut(part) {
            if let Some(step) = step_map.0.get_mut(&tag) {
                step.remove_modifier(path, modifier);
            }
        }

        // Note: We can't call invalidate_dependent_cache_entries here because we don't have access to Stats
        // The invalidation will be handled by the StatsMutator when it calls clear_internal_cache_for_path
    }
    
    fn evaluate(&self, path: &StatPath, stats: &Stats) -> f32 {
        if let (Some(part_name), Some(tag_val)) = (&path.part, path.tag) {
            // Track this query for future invalidation
            let query_key = (part_name.to_string(), tag_val);
            self.query_tracker.insert(query_key, ());
            
            // Always compute the value fresh (Stats component will handle caching)
            let value = self.evaluate_part(part_name, tag_val, stats);
            return value;
        } 
        // There is no (part.part.is_some() && path.tag.is_none()) case, because a tag is required for a tagged stat.
        else if path.part.is_none() && path.tag.is_some() {
            let tag_val = path.tag.unwrap();
            let mut context = stats.cached_stats.context().clone();
            for (part_name_in_total_expr, _step_definition) in &self.modifier_steps {
                let part_value = self.evaluate_part(part_name_in_total_expr, tag_val, stats);
                context.set_value(part_name_in_total_expr.to_string(), Value::Float(part_value as f64)).unwrap();
            }
            let total_val = self.total.evaluate(&context);
            stats.set_cached(&path.full_path, total_val);
            return total_val;
        }
        0.0
    }

    fn clear_internal_cache(&mut self, path: &StatPath) -> Vec<String> {
        let mut paths_to_invalidate = Vec::new();
        
        if let Some(tag) = path.tag {
            // Find all tracked queries that would be affected by this tag change
            self.query_tracker.retain(|(part, query_tag_from_key), _| {
                let should_invalidate = if *query_tag_from_key == 0 {
                    true // query tag 0 means "match everything", so any change affects it
                } else if tag == u32::MAX {
                    false // u32::MAX means no valid tags, shouldn't affect anything
                } else {
                    // Check if the affected permissive modifier would apply to this tracked query
                    (*query_tag_from_key & tag) == *query_tag_from_key
                };
                
                if should_invalidate {
                    // Build the full path for this query to invalidate in Stats cache
                    let full_path = format!("{}.{}.{}", path.name, part, query_tag_from_key);
                    paths_to_invalidate.push(full_path);
                }
                
                !should_invalidate // retain returns true for items to keep, false for items to remove
            });
        } else {
            // If no specific tag, clear all tracked queries for this stat
            self.query_tracker.clear();
        }
        
        paths_to_invalidate
    }
}