Skip to main content

proof_engine/character/
stats.rs

1// src/character/stats.rs
2// Character stats, leveling, resource pools, and modifiers.
3
4use std::collections::HashMap;
5
6// ---------------------------------------------------------------------------
7// StatKind — 30+ distinct statistics
8// ---------------------------------------------------------------------------
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum StatKind {
12    // Primary
13    Strength,
14    Dexterity,
15    Intelligence,
16    Vitality,
17    Wisdom,
18    Charisma,
19    Luck,
20    Constitution,
21    Agility,
22    Endurance,
23    Perception,
24    Willpower,
25    // Combat derived (also addressable directly for bonuses)
26    MaxHp,
27    MaxMp,
28    MaxStamina,
29    PhysicalAttack,
30    MagicalAttack,
31    Defense,
32    MagicResist,
33    Speed,
34    CritChance,
35    CritMultiplier,
36    Evasion,
37    Accuracy,
38    BlockChance,
39    ArmorPenetration,
40    MagicPenetration,
41    // Utility
42    MoveSpeed,
43    AttackSpeed,
44    CastSpeed,
45    LifeSteal,
46    ManaSteal,
47    Tenacity,
48    CooldownReduction,
49    GoldFind,
50    MagicFind,
51    ExpBonus,
52    Thorns,
53    Regeneration,
54    ManaRegen,
55}
56
57impl StatKind {
58    pub fn all_primary() -> &'static [StatKind] {
59        &[
60            StatKind::Strength,
61            StatKind::Dexterity,
62            StatKind::Intelligence,
63            StatKind::Vitality,
64            StatKind::Wisdom,
65            StatKind::Charisma,
66            StatKind::Luck,
67            StatKind::Constitution,
68            StatKind::Agility,
69            StatKind::Endurance,
70            StatKind::Perception,
71            StatKind::Willpower,
72        ]
73    }
74
75    pub fn display_name(&self) -> &'static str {
76        match self {
77            StatKind::Strength => "Strength",
78            StatKind::Dexterity => "Dexterity",
79            StatKind::Intelligence => "Intelligence",
80            StatKind::Vitality => "Vitality",
81            StatKind::Wisdom => "Wisdom",
82            StatKind::Charisma => "Charisma",
83            StatKind::Luck => "Luck",
84            StatKind::Constitution => "Constitution",
85            StatKind::Agility => "Agility",
86            StatKind::Endurance => "Endurance",
87            StatKind::Perception => "Perception",
88            StatKind::Willpower => "Willpower",
89            StatKind::MaxHp => "Max HP",
90            StatKind::MaxMp => "Max MP",
91            StatKind::MaxStamina => "Max Stamina",
92            StatKind::PhysicalAttack => "Physical Attack",
93            StatKind::MagicalAttack => "Magical Attack",
94            StatKind::Defense => "Defense",
95            StatKind::MagicResist => "Magic Resist",
96            StatKind::Speed => "Speed",
97            StatKind::CritChance => "Crit Chance",
98            StatKind::CritMultiplier => "Crit Multiplier",
99            StatKind::Evasion => "Evasion",
100            StatKind::Accuracy => "Accuracy",
101            StatKind::BlockChance => "Block Chance",
102            StatKind::ArmorPenetration => "Armor Penetration",
103            StatKind::MagicPenetration => "Magic Penetration",
104            StatKind::MoveSpeed => "Move Speed",
105            StatKind::AttackSpeed => "Attack Speed",
106            StatKind::CastSpeed => "Cast Speed",
107            StatKind::LifeSteal => "Life Steal",
108            StatKind::ManaSteal => "Mana Steal",
109            StatKind::Tenacity => "Tenacity",
110            StatKind::CooldownReduction => "Cooldown Reduction",
111            StatKind::GoldFind => "Gold Find",
112            StatKind::MagicFind => "Magic Find",
113            StatKind::ExpBonus => "EXP Bonus",
114            StatKind::Thorns => "Thorns",
115            StatKind::Regeneration => "HP Regeneration",
116            StatKind::ManaRegen => "MP Regeneration",
117        }
118    }
119}
120
121// ---------------------------------------------------------------------------
122// StatValue — a single stat with layered bonuses
123// ---------------------------------------------------------------------------
124
125#[derive(Debug, Clone, PartialEq)]
126pub struct StatValue {
127    pub base: f32,
128    pub flat_bonus: f32,
129    pub percent_bonus: f32,
130    pub multiplier: f32,
131}
132
133impl StatValue {
134    pub fn new(base: f32) -> Self {
135        Self {
136            base,
137            flat_bonus: 0.0,
138            percent_bonus: 0.0,
139            multiplier: 1.0,
140        }
141    }
142
143    /// Final = (base + flat_bonus) * (1 + percent_bonus) * multiplier
144    pub fn final_value(&self) -> f32 {
145        (self.base + self.flat_bonus) * (1.0 + self.percent_bonus) * self.multiplier
146    }
147
148    pub fn reset_bonuses(&mut self) {
149        self.flat_bonus = 0.0;
150        self.percent_bonus = 0.0;
151        self.multiplier = 1.0;
152    }
153}
154
155impl Default for StatValue {
156    fn default() -> Self {
157        Self::new(0.0)
158    }
159}
160
161// ---------------------------------------------------------------------------
162// ModifierKind + StatModifier
163// ---------------------------------------------------------------------------
164
165#[derive(Debug, Clone, PartialEq)]
166pub enum ModifierKind {
167    FlatAdd,
168    PercentAdd,
169    FlatMult,
170    Override,
171}
172
173#[derive(Debug, Clone)]
174pub struct StatModifier {
175    pub source: String,
176    pub stat: StatKind,
177    pub value: f32,
178    pub kind: ModifierKind,
179}
180
181impl StatModifier {
182    pub fn flat(source: impl Into<String>, stat: StatKind, value: f32) -> Self {
183        Self { source: source.into(), stat, value, kind: ModifierKind::FlatAdd }
184    }
185
186    pub fn percent(source: impl Into<String>, stat: StatKind, value: f32) -> Self {
187        Self { source: source.into(), stat, value, kind: ModifierKind::PercentAdd }
188    }
189
190    pub fn mult(source: impl Into<String>, stat: StatKind, value: f32) -> Self {
191        Self { source: source.into(), stat, value, kind: ModifierKind::FlatMult }
192    }
193
194    pub fn override_val(source: impl Into<String>, stat: StatKind, value: f32) -> Self {
195        Self { source: source.into(), stat, value, kind: ModifierKind::Override }
196    }
197}
198
199// ---------------------------------------------------------------------------
200// ModifierRegistry — tracks all active modifiers and recomputes stats
201// ---------------------------------------------------------------------------
202
203#[derive(Debug, Clone, Default)]
204pub struct ModifierRegistry {
205    modifiers: Vec<StatModifier>,
206}
207
208impl ModifierRegistry {
209    pub fn new() -> Self {
210        Self { modifiers: Vec::new() }
211    }
212
213    pub fn add(&mut self, modifier: StatModifier) {
214        self.modifiers.push(modifier);
215    }
216
217    pub fn remove_by_source(&mut self, source: &str) {
218        self.modifiers.retain(|m| m.source != source);
219    }
220
221    pub fn remove_by_source_and_stat(&mut self, source: &str, stat: StatKind) {
222        self.modifiers.retain(|m| !(m.source == source && m.stat == stat));
223    }
224
225    pub fn clear(&mut self) {
226        self.modifiers.clear();
227    }
228
229    pub fn iter(&self) -> impl Iterator<Item = &StatModifier> {
230        self.modifiers.iter()
231    }
232
233    pub fn count(&self) -> usize {
234        self.modifiers.len()
235    }
236
237    /// Apply all modifiers for a given stat to a StatValue.
238    pub fn apply_to(&self, stat: StatKind, sv: &mut StatValue) {
239        sv.reset_bonuses();
240        let mut override_val: Option<f32> = None;
241        for m in &self.modifiers {
242            if m.stat != stat { continue; }
243            match m.kind {
244                ModifierKind::FlatAdd => sv.flat_bonus += m.value,
245                ModifierKind::PercentAdd => sv.percent_bonus += m.value,
246                ModifierKind::FlatMult => sv.multiplier *= m.value,
247                ModifierKind::Override => override_val = Some(m.value),
248            }
249        }
250        if let Some(ov) = override_val {
251            sv.base = ov;
252            sv.flat_bonus = 0.0;
253            sv.percent_bonus = 0.0;
254            sv.multiplier = 1.0;
255        }
256    }
257}
258
259// ---------------------------------------------------------------------------
260// StatSheet — the full set of stats for one character
261// ---------------------------------------------------------------------------
262
263#[derive(Debug, Clone)]
264pub struct StatSheet {
265    pub stats: HashMap<StatKind, StatValue>,
266}
267
268impl StatSheet {
269    pub fn new() -> Self {
270        let mut stats = HashMap::new();
271        // Initialise every primary stat to 10
272        for &kind in StatKind::all_primary() {
273            stats.insert(kind, StatValue::new(10.0));
274        }
275        Self { stats }
276    }
277
278    pub fn with_base(mut self, kind: StatKind, base: f32) -> Self {
279        self.stats.insert(kind, StatValue::new(base));
280        self
281    }
282
283    pub fn get(&self, kind: StatKind) -> f32 {
284        self.stats.get(&kind).map(|sv| sv.final_value()).unwrap_or(0.0)
285    }
286
287    pub fn get_mut(&mut self, kind: StatKind) -> &mut StatValue {
288        self.stats.entry(kind).or_insert_with(|| StatValue::new(0.0))
289    }
290
291    pub fn set_base(&mut self, kind: StatKind, base: f32) {
292        self.stats.entry(kind).or_insert_with(|| StatValue::new(0.0)).base = base;
293    }
294
295    pub fn add_base(&mut self, kind: StatKind, delta: f32) {
296        let sv = self.stats.entry(kind).or_insert_with(|| StatValue::new(0.0));
297        sv.base += delta;
298    }
299
300    /// Reapply all modifiers from the registry.
301    pub fn apply_modifiers(&mut self, registry: &ModifierRegistry) {
302        // Collect keys first to avoid borrow conflicts
303        let keys: Vec<StatKind> = self.stats.keys().copied().collect();
304        for key in keys {
305            if let Some(sv) = self.stats.get_mut(&key) {
306                registry.apply_to(key, sv);
307            }
308        }
309    }
310
311    /// Compute derived stat: MaxHP
312    pub fn max_hp(&self, level: u32) -> f32 {
313        self.get(StatKind::Vitality) * 10.0
314            + self.get(StatKind::Constitution) * 5.0
315            + level as f32 * 20.0
316    }
317
318    /// Compute derived stat: MaxMP
319    pub fn max_mp(&self, level: u32) -> f32 {
320        self.get(StatKind::Intelligence) * 8.0
321            + self.get(StatKind::Wisdom) * 4.0
322            + level as f32 * 10.0
323    }
324
325    /// Compute derived stat: MaxStamina
326    pub fn max_stamina(&self, level: u32) -> f32 {
327        self.get(StatKind::Endurance) * 6.0
328            + self.get(StatKind::Constitution) * 3.0
329            + level as f32 * 5.0
330    }
331
332    /// Physical Attack (weapon_damage is passed in from equipment)
333    pub fn physical_attack(&self, weapon_damage: f32) -> f32 {
334        self.get(StatKind::Strength) * 2.0 + weapon_damage
335    }
336
337    /// Magical Attack (spell_power from equipment/skills)
338    pub fn magical_attack(&self, spell_power: f32) -> f32 {
339        self.get(StatKind::Intelligence) * 2.0 + spell_power
340    }
341
342    /// Defense
343    pub fn defense(&self, armor_rating: f32) -> f32 {
344        self.get(StatKind::Constitution) + armor_rating
345    }
346
347    /// Magic Resist
348    pub fn magic_resist(&self, magic_armor: f32) -> f32 {
349        self.get(StatKind::Willpower) * 0.5 + magic_armor
350    }
351
352    /// Speed
353    pub fn speed(&self) -> f32 {
354        self.get(StatKind::Dexterity) * 0.5 + self.get(StatKind::Agility) * 0.5
355    }
356
357    /// Crit chance (capped at 75%)
358    pub fn crit_chance(&self) -> f32 {
359        let raw = self.get(StatKind::Luck) * 0.1 + self.get(StatKind::Dexterity) * 0.05;
360        raw.min(75.0)
361    }
362
363    /// Crit multiplier
364    pub fn crit_multiplier(&self) -> f32 {
365        1.5 + self.get(StatKind::Strength) * 0.01
366    }
367
368    /// Evasion
369    pub fn evasion(&self) -> f32 {
370        self.get(StatKind::Dexterity) * 0.3 + self.get(StatKind::Agility) * 0.2
371    }
372
373    /// Accuracy
374    pub fn accuracy(&self) -> f32 {
375        self.get(StatKind::Perception) * 0.5 + self.get(StatKind::Dexterity) * 0.2
376    }
377
378    /// Block chance (capped at 50%)
379    pub fn block_chance(&self) -> f32 {
380        let raw = self.get(StatKind::Constitution) * 0.1 + self.get(StatKind::Strength) * 0.05;
381        raw.min(50.0)
382    }
383
384    /// HP regen per second
385    pub fn hp_regen(&self) -> f32 {
386        self.get(StatKind::Vitality) * 0.02 + self.get(StatKind::Regeneration)
387    }
388
389    /// MP regen per second
390    pub fn mp_regen(&self) -> f32 {
391        self.get(StatKind::Wisdom) * 0.05 + self.get(StatKind::ManaRegen)
392    }
393
394    /// Move speed (base 100 units/s)
395    pub fn move_speed(&self) -> f32 {
396        100.0 + self.get(StatKind::Agility) * 2.0 + self.get(StatKind::MoveSpeed)
397    }
398
399    /// Attack speed (1.0 = base, higher is faster)
400    pub fn attack_speed(&self) -> f32 {
401        1.0 + self.get(StatKind::Dexterity) * 0.01 + self.get(StatKind::AttackSpeed)
402    }
403}
404
405impl Default for StatSheet {
406    fn default() -> Self {
407        Self::new()
408    }
409}
410
411// ---------------------------------------------------------------------------
412// ResourcePool — HP / MP / Stamina
413// ---------------------------------------------------------------------------
414
415#[derive(Debug, Clone)]
416pub struct ResourcePool {
417    pub current: f32,
418    pub max: f32,
419    pub regen_rate: f32,
420    /// Seconds after taking damage before regen resumes
421    pub regen_delay: f32,
422    regen_timer: f32,
423}
424
425impl ResourcePool {
426    pub fn new(max: f32, regen_rate: f32, regen_delay: f32) -> Self {
427        Self {
428            current: max,
429            max,
430            regen_rate,
431            regen_delay,
432            regen_timer: 0.0,
433        }
434    }
435
436    pub fn full(&self) -> bool {
437        self.current >= self.max
438    }
439
440    pub fn empty(&self) -> bool {
441        self.current <= 0.0
442    }
443
444    pub fn fraction(&self) -> f32 {
445        if self.max <= 0.0 { 0.0 } else { (self.current / self.max).clamp(0.0, 1.0) }
446    }
447
448    /// Drain amount, returns actual amount drained (clamped to available).
449    pub fn drain(&mut self, amount: f32) -> f32 {
450        let drained = amount.min(self.current).max(0.0);
451        self.current -= drained;
452        self.regen_timer = self.regen_delay;
453        drained
454    }
455
456    /// Restore amount, returns actual amount restored (clamped to max).
457    pub fn restore(&mut self, amount: f32) -> f32 {
458        let before = self.current;
459        self.current = (self.current + amount).min(self.max);
460        self.current - before
461    }
462
463    /// Set max and optionally scale current proportionally.
464    pub fn set_max(&mut self, new_max: f32, scale_current: bool) {
465        if scale_current && self.max > 0.0 {
466            let ratio = self.current / self.max;
467            self.max = new_max.max(1.0);
468            self.current = (self.max * ratio).min(self.max);
469        } else {
470            self.max = new_max.max(1.0);
471            self.current = self.current.min(self.max);
472        }
473    }
474
475    /// Tick regeneration by dt seconds.
476    pub fn tick(&mut self, dt: f32) {
477        // Clamp active timer to current regen_delay so that lowering
478        // regen_delay at runtime takes effect immediately.
479        self.regen_timer = self.regen_timer.min(self.regen_delay);
480
481        if self.regen_timer > 0.0 {
482            self.regen_timer -= dt;
483            if self.regen_timer >= 0.0 {
484                return;
485            }
486            // Timer expired mid-tick — regen for the leftover time
487            let leftover = -self.regen_timer;
488            self.regen_timer = 0.0;
489            if !self.full() {
490                self.current = (self.current + self.regen_rate * leftover).min(self.max);
491            }
492            return;
493        }
494        if !self.full() {
495            self.current = (self.current + self.regen_rate * dt).min(self.max);
496        }
497    }
498
499    /// Force set current (clamps to [0, max]).
500    pub fn set_current(&mut self, val: f32) {
501        self.current = val.clamp(0.0, self.max);
502    }
503
504    /// Instant fill to max.
505    pub fn fill(&mut self) {
506        self.current = self.max;
507    }
508}
509
510impl Default for ResourcePool {
511    fn default() -> Self {
512        Self::new(100.0, 1.0, 5.0)
513    }
514}
515
516// ---------------------------------------------------------------------------
517// XpCurve — experience requirements per level
518// ---------------------------------------------------------------------------
519
520#[derive(Debug, Clone)]
521pub enum XpCurve {
522    Linear { base: u64, increment: u64 },
523    Quadratic { base: u64, factor: f64 },
524    Exponential { base: u64, exponent: f64 },
525    Custom(Vec<u64>),
526}
527
528impl XpCurve {
529    pub fn xp_for_level(&self, level: u32) -> u64 {
530        let lvl = level as u64;
531        match self {
532            XpCurve::Linear { base, increment } => base + increment * (lvl.saturating_sub(1)),
533            XpCurve::Quadratic { base, factor } => {
534                (*base as f64 * (*factor).powf(lvl as f64 - 1.0)) as u64
535            }
536            XpCurve::Exponential { base, exponent } => {
537                (*base as f64 * (lvl as f64).powf(*exponent)) as u64
538            }
539            XpCurve::Custom(table) => {
540                let idx = (level as usize).saturating_sub(1);
541                table.get(idx).copied().unwrap_or(u64::MAX)
542            }
543        }
544    }
545
546    pub fn total_xp_to_level(&self, target_level: u32) -> u64 {
547        (1..target_level).map(|l| self.xp_for_level(l)).sum()
548    }
549}
550
551impl Default for XpCurve {
552    fn default() -> Self {
553        XpCurve::Quadratic { base: 100, factor: 1.5 }
554    }
555}
556
557// ---------------------------------------------------------------------------
558// LevelData — tracks XP and level progression
559// ---------------------------------------------------------------------------
560
561#[derive(Debug, Clone)]
562pub struct LevelData {
563    pub level: u32,
564    pub xp: u64,
565    pub xp_to_next: u64,
566    pub stat_points: u32,
567    pub skill_points: u32,
568    pub curve: XpCurve,
569    pub max_level: u32,
570}
571
572impl LevelData {
573    pub fn new(curve: XpCurve, max_level: u32) -> Self {
574        let xp_to_next = curve.xp_for_level(1);
575        Self {
576            level: 1,
577            xp: 0,
578            xp_to_next,
579            stat_points: 0,
580            skill_points: 0,
581            curve,
582            max_level,
583        }
584    }
585
586    /// Add XP and return number of levels gained.
587    pub fn add_xp(&mut self, amount: u64) -> u32 {
588        if self.level >= self.max_level { return 0; }
589        self.xp += amount;
590        let mut levels_gained = 0u32;
591        while self.level < self.max_level && self.xp >= self.xp_to_next {
592            self.xp -= self.xp_to_next;
593            self.level += 1;
594            levels_gained += 1;
595            self.xp_to_next = self.curve.xp_for_level(self.level);
596        }
597        if self.level >= self.max_level {
598            self.xp = 0;
599            self.xp_to_next = 0;
600        }
601        levels_gained
602    }
603
604    pub fn level_up(&mut self, stat_points_per_level: u32, skill_points_per_level: u32) {
605        self.stat_points += stat_points_per_level;
606        self.skill_points += skill_points_per_level;
607    }
608
609    pub fn spend_stat_point(&mut self) -> bool {
610        if self.stat_points > 0 {
611            self.stat_points -= 1;
612            true
613        } else {
614            false
615        }
616    }
617
618    pub fn spend_skill_point(&mut self) -> bool {
619        if self.skill_points > 0 {
620            self.skill_points -= 1;
621            true
622        } else {
623            false
624        }
625    }
626
627    pub fn xp_progress_fraction(&self) -> f32 {
628        if self.xp_to_next == 0 { return 1.0; }
629        (self.xp as f64 / self.xp_to_next as f64) as f32
630    }
631}
632
633impl Default for LevelData {
634    fn default() -> Self {
635        Self::new(XpCurve::default(), 100)
636    }
637}
638
639// ---------------------------------------------------------------------------
640// StatGrowth — per-level stat increases for a class archetype
641// ---------------------------------------------------------------------------
642
643#[derive(Debug, Clone)]
644pub struct StatGrowth {
645    pub growths: HashMap<StatKind, f32>,
646}
647
648impl StatGrowth {
649    pub fn new() -> Self {
650        Self { growths: HashMap::new() }
651    }
652
653    pub fn set(mut self, kind: StatKind, per_level: f32) -> Self {
654        self.growths.insert(kind, per_level);
655        self
656    }
657
658    pub fn apply_to(&self, sheet: &mut StatSheet) {
659        for (&kind, &amount) in &self.growths {
660            sheet.add_base(kind, amount);
661        }
662    }
663
664    /// Warrior growth template
665    pub fn warrior() -> Self {
666        Self::new()
667            .set(StatKind::Strength, 3.0)
668            .set(StatKind::Constitution, 2.0)
669            .set(StatKind::Vitality, 2.0)
670            .set(StatKind::Endurance, 1.5)
671            .set(StatKind::Agility, 0.5)
672            .set(StatKind::Dexterity, 1.0)
673    }
674
675    pub fn mage() -> Self {
676        Self::new()
677            .set(StatKind::Intelligence, 4.0)
678            .set(StatKind::Wisdom, 2.5)
679            .set(StatKind::Willpower, 2.0)
680            .set(StatKind::Vitality, 1.0)
681            .set(StatKind::Charisma, 0.5)
682    }
683
684    pub fn rogue() -> Self {
685        Self::new()
686            .set(StatKind::Dexterity, 3.5)
687            .set(StatKind::Agility, 3.0)
688            .set(StatKind::Perception, 2.0)
689            .set(StatKind::Luck, 1.5)
690            .set(StatKind::Strength, 1.0)
691    }
692
693    pub fn healer() -> Self {
694        Self::new()
695            .set(StatKind::Wisdom, 3.5)
696            .set(StatKind::Intelligence, 2.0)
697            .set(StatKind::Charisma, 2.5)
698            .set(StatKind::Vitality, 2.0)
699            .set(StatKind::Willpower, 1.5)
700    }
701
702    pub fn ranger() -> Self {
703        Self::new()
704            .set(StatKind::Dexterity, 3.0)
705            .set(StatKind::Perception, 3.0)
706            .set(StatKind::Agility, 2.0)
707            .set(StatKind::Strength, 1.5)
708            .set(StatKind::Endurance, 1.0)
709    }
710}
711
712impl Default for StatGrowth {
713    fn default() -> Self {
714        Self::new()
715            .set(StatKind::Vitality, 1.0)
716            .set(StatKind::Strength, 1.0)
717            .set(StatKind::Dexterity, 1.0)
718    }
719}
720
721// ---------------------------------------------------------------------------
722// StatPreset — predefined stat spreads for common archetypes
723// ---------------------------------------------------------------------------
724
725#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
726pub enum ClassArchetype {
727    Warrior,
728    Mage,
729    Rogue,
730    Healer,
731    Ranger,
732    Summoner,
733    Paladin,
734    Necromancer,
735    Berserker,
736    Elementalist,
737}
738
739pub struct StatPreset;
740
741impl StatPreset {
742    pub fn for_class(class: ClassArchetype, level: u32) -> StatSheet {
743        let mut sheet = StatSheet::new();
744        let growth = Self::growth_for(class);
745        for _ in 1..level {
746            growth.apply_to(&mut sheet);
747        }
748        // Set base spread
749        match class {
750            ClassArchetype::Warrior => {
751                sheet.set_base(StatKind::Strength, 16.0);
752                sheet.set_base(StatKind::Constitution, 14.0);
753                sheet.set_base(StatKind::Vitality, 14.0);
754                sheet.set_base(StatKind::Endurance, 13.0);
755                sheet.set_base(StatKind::Intelligence, 8.0);
756                sheet.set_base(StatKind::Wisdom, 8.0);
757            }
758            ClassArchetype::Mage => {
759                sheet.set_base(StatKind::Intelligence, 18.0);
760                sheet.set_base(StatKind::Wisdom, 14.0);
761                sheet.set_base(StatKind::Willpower, 13.0);
762                sheet.set_base(StatKind::Strength, 6.0);
763                sheet.set_base(StatKind::Constitution, 8.0);
764            }
765            ClassArchetype::Rogue => {
766                sheet.set_base(StatKind::Dexterity, 17.0);
767                sheet.set_base(StatKind::Agility, 16.0);
768                sheet.set_base(StatKind::Perception, 14.0);
769                sheet.set_base(StatKind::Luck, 13.0);
770                sheet.set_base(StatKind::Strength, 10.0);
771            }
772            ClassArchetype::Healer => {
773                sheet.set_base(StatKind::Wisdom, 18.0);
774                sheet.set_base(StatKind::Charisma, 15.0);
775                sheet.set_base(StatKind::Intelligence, 13.0);
776                sheet.set_base(StatKind::Vitality, 12.0);
777                sheet.set_base(StatKind::Willpower, 12.0);
778            }
779            ClassArchetype::Ranger => {
780                sheet.set_base(StatKind::Dexterity, 16.0);
781                sheet.set_base(StatKind::Perception, 15.0);
782                sheet.set_base(StatKind::Agility, 14.0);
783                sheet.set_base(StatKind::Strength, 12.0);
784                sheet.set_base(StatKind::Endurance, 12.0);
785            }
786            ClassArchetype::Summoner => {
787                sheet.set_base(StatKind::Intelligence, 16.0);
788                sheet.set_base(StatKind::Charisma, 17.0);
789                sheet.set_base(StatKind::Wisdom, 13.0);
790                sheet.set_base(StatKind::Willpower, 12.0);
791            }
792            ClassArchetype::Paladin => {
793                sheet.set_base(StatKind::Strength, 14.0);
794                sheet.set_base(StatKind::Constitution, 15.0);
795                sheet.set_base(StatKind::Charisma, 13.0);
796                sheet.set_base(StatKind::Wisdom, 12.0);
797                sheet.set_base(StatKind::Vitality, 13.0);
798            }
799            ClassArchetype::Necromancer => {
800                sheet.set_base(StatKind::Intelligence, 16.0);
801                sheet.set_base(StatKind::Willpower, 15.0);
802                sheet.set_base(StatKind::Wisdom, 12.0);
803                sheet.set_base(StatKind::Charisma, 8.0);
804                sheet.set_base(StatKind::Endurance, 11.0);
805            }
806            ClassArchetype::Berserker => {
807                sheet.set_base(StatKind::Strength, 18.0);
808                sheet.set_base(StatKind::Endurance, 16.0);
809                sheet.set_base(StatKind::Vitality, 14.0);
810                sheet.set_base(StatKind::Agility, 12.0);
811                sheet.set_base(StatKind::Constitution, 10.0);
812            }
813            ClassArchetype::Elementalist => {
814                sheet.set_base(StatKind::Intelligence, 17.0);
815                sheet.set_base(StatKind::Wisdom, 15.0);
816                sheet.set_base(StatKind::Agility, 12.0);
817                sheet.set_base(StatKind::Perception, 11.0);
818                sheet.set_base(StatKind::Willpower, 13.0);
819            }
820        }
821        sheet
822    }
823
824    pub fn growth_for(class: ClassArchetype) -> StatGrowth {
825        match class {
826            ClassArchetype::Warrior => StatGrowth::warrior(),
827            ClassArchetype::Mage => StatGrowth::mage(),
828            ClassArchetype::Rogue => StatGrowth::rogue(),
829            ClassArchetype::Healer => StatGrowth::healer(),
830            ClassArchetype::Ranger => StatGrowth::ranger(),
831            ClassArchetype::Summoner => StatGrowth::new()
832                .set(StatKind::Intelligence, 2.5)
833                .set(StatKind::Charisma, 3.0)
834                .set(StatKind::Wisdom, 2.0),
835            ClassArchetype::Paladin => StatGrowth::new()
836                .set(StatKind::Strength, 2.0)
837                .set(StatKind::Constitution, 2.5)
838                .set(StatKind::Wisdom, 1.5)
839                .set(StatKind::Vitality, 2.0),
840            ClassArchetype::Necromancer => StatGrowth::new()
841                .set(StatKind::Intelligence, 3.0)
842                .set(StatKind::Willpower, 2.5)
843                .set(StatKind::Wisdom, 1.5),
844            ClassArchetype::Berserker => StatGrowth::new()
845                .set(StatKind::Strength, 4.0)
846                .set(StatKind::Endurance, 2.5)
847                .set(StatKind::Vitality, 2.0)
848                .set(StatKind::Agility, 1.0),
849            ClassArchetype::Elementalist => StatGrowth::new()
850                .set(StatKind::Intelligence, 3.5)
851                .set(StatKind::Wisdom, 2.0)
852                .set(StatKind::Agility, 1.5),
853        }
854    }
855}
856
857// ---------------------------------------------------------------------------
858// AllResources — convenience wrapper for HP / MP / Stamina
859// ---------------------------------------------------------------------------
860
861#[derive(Debug, Clone)]
862pub struct AllResources {
863    pub hp: ResourcePool,
864    pub mp: ResourcePool,
865    pub stamina: ResourcePool,
866}
867
868impl AllResources {
869    pub fn from_sheet(sheet: &StatSheet, level: u32) -> Self {
870        let max_hp = sheet.max_hp(level);
871        let max_mp = sheet.max_mp(level);
872        let max_st = sheet.max_stamina(level);
873        Self {
874            hp: ResourcePool::new(max_hp, sheet.hp_regen(), 5.0),
875            mp: ResourcePool::new(max_mp, sheet.mp_regen(), 3.0),
876            stamina: ResourcePool::new(max_st, 10.0, 1.0),
877        }
878    }
879
880    pub fn tick(&mut self, dt: f32) {
881        self.hp.tick(dt);
882        self.mp.tick(dt);
883        self.stamina.tick(dt);
884    }
885
886    pub fn is_alive(&self) -> bool {
887        self.hp.current > 0.0
888    }
889}
890
891impl Default for AllResources {
892    fn default() -> Self {
893        Self {
894            hp: ResourcePool::new(100.0, 1.0, 5.0),
895            mp: ResourcePool::new(50.0, 2.0, 3.0),
896            stamina: ResourcePool::new(100.0, 10.0, 1.0),
897        }
898    }
899}
900
901// ---------------------------------------------------------------------------
902// Tests
903// ---------------------------------------------------------------------------
904
905#[cfg(test)]
906mod tests {
907    use super::*;
908
909    #[test]
910    fn test_stat_value_final() {
911        let mut sv = StatValue::new(10.0);
912        sv.flat_bonus = 5.0;
913        sv.percent_bonus = 0.5; // +50%
914        sv.multiplier = 2.0;
915        // (10 + 5) * (1 + 0.5) * 2.0 = 15 * 1.5 * 2 = 45
916        assert!((sv.final_value() - 45.0).abs() < f32::EPSILON);
917    }
918
919    #[test]
920    fn test_modifier_registry_flat_add() {
921        let mut reg = ModifierRegistry::new();
922        reg.add(StatModifier::flat("sword", StatKind::Strength, 10.0));
923        let mut sv = StatValue::new(20.0);
924        reg.apply_to(StatKind::Strength, &mut sv);
925        assert!((sv.final_value() - 30.0).abs() < f32::EPSILON);
926    }
927
928    #[test]
929    fn test_modifier_registry_remove_source() {
930        let mut reg = ModifierRegistry::new();
931        reg.add(StatModifier::flat("enchant", StatKind::Dexterity, 5.0));
932        reg.remove_by_source("enchant");
933        assert_eq!(reg.count(), 0);
934    }
935
936    #[test]
937    fn test_modifier_override() {
938        let mut reg = ModifierRegistry::new();
939        reg.add(StatModifier::override_val("cap", StatKind::CritChance, 75.0));
940        let mut sv = StatValue::new(99.0);
941        reg.apply_to(StatKind::CritChance, &mut sv);
942        assert!((sv.final_value() - 75.0).abs() < f32::EPSILON);
943    }
944
945    #[test]
946    fn test_stat_sheet_derived_hp() {
947        let sheet = StatPreset::for_class(ClassArchetype::Warrior, 1);
948        let hp = sheet.max_hp(10);
949        assert!(hp > 0.0);
950    }
951
952    #[test]
953    fn test_crit_chance_cap() {
954        let mut sheet = StatSheet::new();
955        sheet.set_base(StatKind::Luck, 1000.0);
956        assert!(sheet.crit_chance() <= 75.0);
957    }
958
959    #[test]
960    fn test_resource_pool_drain_restore() {
961        let mut pool = ResourcePool::new(100.0, 5.0, 0.0);
962        let drained = pool.drain(30.0);
963        assert!((drained - 30.0).abs() < f32::EPSILON);
964        assert!((pool.current - 70.0).abs() < f32::EPSILON);
965        let restored = pool.restore(20.0);
966        assert!((restored - 20.0).abs() < f32::EPSILON);
967        assert!((pool.current - 90.0).abs() < f32::EPSILON);
968    }
969
970    #[test]
971    fn test_resource_pool_overflow() {
972        let mut pool = ResourcePool::new(100.0, 5.0, 0.0);
973        pool.restore(999.0);
974        assert!((pool.current - 100.0).abs() < f32::EPSILON);
975    }
976
977    #[test]
978    fn test_resource_pool_underflow() {
979        let mut pool = ResourcePool::new(100.0, 5.0, 0.0);
980        let drained = pool.drain(999.0);
981        assert!((drained - 100.0).abs() < f32::EPSILON);
982        assert!((pool.current).abs() < f32::EPSILON);
983    }
984
985    #[test]
986    fn test_resource_pool_regen() {
987        let mut pool = ResourcePool::new(100.0, 10.0, 0.0);
988        pool.drain(50.0);
989        pool.tick(1.0);
990        assert!((pool.current - 60.0).abs() < f32::EPSILON);
991    }
992
993    #[test]
994    fn test_resource_pool_regen_delay() {
995        let mut pool = ResourcePool::new(100.0, 10.0, 5.0);
996        pool.drain(50.0);
997        pool.tick(3.0); // still in delay
998        assert!((pool.current - 50.0).abs() < f32::EPSILON);
999        pool.tick(3.0); // delay expires, regen kicks in
1000        assert!(pool.current > 50.0);
1001    }
1002
1003    #[test]
1004    fn test_xp_curve_quadratic() {
1005        let curve = XpCurve::Quadratic { base: 100, factor: 1.5 };
1006        let l1 = curve.xp_for_level(1);
1007        let l2 = curve.xp_for_level(2);
1008        assert!(l2 > l1);
1009    }
1010
1011    #[test]
1012    fn test_level_data_add_xp() {
1013        let mut ld = LevelData::new(XpCurve::Linear { base: 100, increment: 50 }, 100);
1014        let levs = ld.add_xp(100);
1015        assert_eq!(levs, 1);
1016        assert_eq!(ld.level, 2);
1017    }
1018
1019    #[test]
1020    fn test_level_data_multi_level() {
1021        let mut ld = LevelData::new(XpCurve::Linear { base: 10, increment: 0 }, 100);
1022        let levs = ld.add_xp(100);
1023        assert!(levs >= 10);
1024    }
1025
1026    #[test]
1027    fn test_stat_preset_warrior() {
1028        let sheet = StatPreset::for_class(ClassArchetype::Warrior, 10);
1029        assert!(sheet.get(StatKind::Strength) > 10.0);
1030    }
1031
1032    #[test]
1033    fn test_stat_sheet_apply_modifiers() {
1034        let mut sheet = StatSheet::new();
1035        let mut reg = ModifierRegistry::new();
1036        reg.add(StatModifier::flat("test", StatKind::Strength, 100.0));
1037        sheet.apply_modifiers(&reg);
1038        assert!(sheet.get(StatKind::Strength) > 100.0);
1039    }
1040
1041    #[test]
1042    fn test_all_resources_tick() {
1043        let sheet = StatSheet::new();
1044        let mut res = AllResources::from_sheet(&sheet, 1);
1045        res.hp.drain(10.0);
1046        res.hp.regen_delay = 0.0;
1047        res.tick(1.0);
1048        // Should have regenerated some HP
1049        assert!(res.hp.current > res.hp.max - 10.0);
1050    }
1051}