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        if self.regen_timer > 0.0 {
478            self.regen_timer -= dt;
479            return;
480        }
481        if !self.full() {
482            self.current = (self.current + self.regen_rate * dt).min(self.max);
483        }
484    }
485
486    /// Force set current (clamps to [0, max]).
487    pub fn set_current(&mut self, val: f32) {
488        self.current = val.clamp(0.0, self.max);
489    }
490
491    /// Instant fill to max.
492    pub fn fill(&mut self) {
493        self.current = self.max;
494    }
495}
496
497impl Default for ResourcePool {
498    fn default() -> Self {
499        Self::new(100.0, 1.0, 5.0)
500    }
501}
502
503// ---------------------------------------------------------------------------
504// XpCurve — experience requirements per level
505// ---------------------------------------------------------------------------
506
507#[derive(Debug, Clone)]
508pub enum XpCurve {
509    Linear { base: u64, increment: u64 },
510    Quadratic { base: u64, factor: f64 },
511    Exponential { base: u64, exponent: f64 },
512    Custom(Vec<u64>),
513}
514
515impl XpCurve {
516    pub fn xp_for_level(&self, level: u32) -> u64 {
517        let lvl = level as u64;
518        match self {
519            XpCurve::Linear { base, increment } => base + increment * (lvl.saturating_sub(1)),
520            XpCurve::Quadratic { base, factor } => {
521                (*base as f64 * (*factor).powf(lvl as f64 - 1.0)) as u64
522            }
523            XpCurve::Exponential { base, exponent } => {
524                (*base as f64 * (lvl as f64).powf(*exponent)) as u64
525            }
526            XpCurve::Custom(table) => {
527                let idx = (level as usize).saturating_sub(1);
528                table.get(idx).copied().unwrap_or(u64::MAX)
529            }
530        }
531    }
532
533    pub fn total_xp_to_level(&self, target_level: u32) -> u64 {
534        (1..target_level).map(|l| self.xp_for_level(l)).sum()
535    }
536}
537
538impl Default for XpCurve {
539    fn default() -> Self {
540        XpCurve::Quadratic { base: 100, factor: 1.5 }
541    }
542}
543
544// ---------------------------------------------------------------------------
545// LevelData — tracks XP and level progression
546// ---------------------------------------------------------------------------
547
548#[derive(Debug, Clone)]
549pub struct LevelData {
550    pub level: u32,
551    pub xp: u64,
552    pub xp_to_next: u64,
553    pub stat_points: u32,
554    pub skill_points: u32,
555    pub curve: XpCurve,
556    pub max_level: u32,
557}
558
559impl LevelData {
560    pub fn new(curve: XpCurve, max_level: u32) -> Self {
561        let xp_to_next = curve.xp_for_level(1);
562        Self {
563            level: 1,
564            xp: 0,
565            xp_to_next,
566            stat_points: 0,
567            skill_points: 0,
568            curve,
569            max_level,
570        }
571    }
572
573    /// Add XP and return number of levels gained.
574    pub fn add_xp(&mut self, amount: u64) -> u32 {
575        if self.level >= self.max_level { return 0; }
576        self.xp += amount;
577        let mut levels_gained = 0u32;
578        while self.level < self.max_level && self.xp >= self.xp_to_next {
579            self.xp -= self.xp_to_next;
580            self.level += 1;
581            levels_gained += 1;
582            self.xp_to_next = self.curve.xp_for_level(self.level);
583        }
584        if self.level >= self.max_level {
585            self.xp = 0;
586            self.xp_to_next = 0;
587        }
588        levels_gained
589    }
590
591    pub fn level_up(&mut self, stat_points_per_level: u32, skill_points_per_level: u32) {
592        self.stat_points += stat_points_per_level;
593        self.skill_points += skill_points_per_level;
594    }
595
596    pub fn spend_stat_point(&mut self) -> bool {
597        if self.stat_points > 0 {
598            self.stat_points -= 1;
599            true
600        } else {
601            false
602        }
603    }
604
605    pub fn spend_skill_point(&mut self) -> bool {
606        if self.skill_points > 0 {
607            self.skill_points -= 1;
608            true
609        } else {
610            false
611        }
612    }
613
614    pub fn xp_progress_fraction(&self) -> f32 {
615        if self.xp_to_next == 0 { return 1.0; }
616        (self.xp as f64 / self.xp_to_next as f64) as f32
617    }
618}
619
620impl Default for LevelData {
621    fn default() -> Self {
622        Self::new(XpCurve::default(), 100)
623    }
624}
625
626// ---------------------------------------------------------------------------
627// StatGrowth — per-level stat increases for a class archetype
628// ---------------------------------------------------------------------------
629
630#[derive(Debug, Clone)]
631pub struct StatGrowth {
632    pub growths: HashMap<StatKind, f32>,
633}
634
635impl StatGrowth {
636    pub fn new() -> Self {
637        Self { growths: HashMap::new() }
638    }
639
640    pub fn set(mut self, kind: StatKind, per_level: f32) -> Self {
641        self.growths.insert(kind, per_level);
642        self
643    }
644
645    pub fn apply_to(&self, sheet: &mut StatSheet) {
646        for (&kind, &amount) in &self.growths {
647            sheet.add_base(kind, amount);
648        }
649    }
650
651    /// Warrior growth template
652    pub fn warrior() -> Self {
653        Self::new()
654            .set(StatKind::Strength, 3.0)
655            .set(StatKind::Constitution, 2.0)
656            .set(StatKind::Vitality, 2.0)
657            .set(StatKind::Endurance, 1.5)
658            .set(StatKind::Agility, 0.5)
659            .set(StatKind::Dexterity, 1.0)
660    }
661
662    pub fn mage() -> Self {
663        Self::new()
664            .set(StatKind::Intelligence, 4.0)
665            .set(StatKind::Wisdom, 2.5)
666            .set(StatKind::Willpower, 2.0)
667            .set(StatKind::Vitality, 1.0)
668            .set(StatKind::Charisma, 0.5)
669    }
670
671    pub fn rogue() -> Self {
672        Self::new()
673            .set(StatKind::Dexterity, 3.5)
674            .set(StatKind::Agility, 3.0)
675            .set(StatKind::Perception, 2.0)
676            .set(StatKind::Luck, 1.5)
677            .set(StatKind::Strength, 1.0)
678    }
679
680    pub fn healer() -> Self {
681        Self::new()
682            .set(StatKind::Wisdom, 3.5)
683            .set(StatKind::Intelligence, 2.0)
684            .set(StatKind::Charisma, 2.5)
685            .set(StatKind::Vitality, 2.0)
686            .set(StatKind::Willpower, 1.5)
687    }
688
689    pub fn ranger() -> Self {
690        Self::new()
691            .set(StatKind::Dexterity, 3.0)
692            .set(StatKind::Perception, 3.0)
693            .set(StatKind::Agility, 2.0)
694            .set(StatKind::Strength, 1.5)
695            .set(StatKind::Endurance, 1.0)
696    }
697}
698
699impl Default for StatGrowth {
700    fn default() -> Self {
701        Self::new()
702            .set(StatKind::Vitality, 1.0)
703            .set(StatKind::Strength, 1.0)
704            .set(StatKind::Dexterity, 1.0)
705    }
706}
707
708// ---------------------------------------------------------------------------
709// StatPreset — predefined stat spreads for common archetypes
710// ---------------------------------------------------------------------------
711
712#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
713pub enum ClassArchetype {
714    Warrior,
715    Mage,
716    Rogue,
717    Healer,
718    Ranger,
719    Summoner,
720    Paladin,
721    Necromancer,
722    Berserker,
723    Elementalist,
724}
725
726pub struct StatPreset;
727
728impl StatPreset {
729    pub fn for_class(class: ClassArchetype, level: u32) -> StatSheet {
730        let mut sheet = StatSheet::new();
731        let growth = Self::growth_for(class);
732        for _ in 1..level {
733            growth.apply_to(&mut sheet);
734        }
735        // Set base spread
736        match class {
737            ClassArchetype::Warrior => {
738                sheet.set_base(StatKind::Strength, 16.0);
739                sheet.set_base(StatKind::Constitution, 14.0);
740                sheet.set_base(StatKind::Vitality, 14.0);
741                sheet.set_base(StatKind::Endurance, 13.0);
742                sheet.set_base(StatKind::Intelligence, 8.0);
743                sheet.set_base(StatKind::Wisdom, 8.0);
744            }
745            ClassArchetype::Mage => {
746                sheet.set_base(StatKind::Intelligence, 18.0);
747                sheet.set_base(StatKind::Wisdom, 14.0);
748                sheet.set_base(StatKind::Willpower, 13.0);
749                sheet.set_base(StatKind::Strength, 6.0);
750                sheet.set_base(StatKind::Constitution, 8.0);
751            }
752            ClassArchetype::Rogue => {
753                sheet.set_base(StatKind::Dexterity, 17.0);
754                sheet.set_base(StatKind::Agility, 16.0);
755                sheet.set_base(StatKind::Perception, 14.0);
756                sheet.set_base(StatKind::Luck, 13.0);
757                sheet.set_base(StatKind::Strength, 10.0);
758            }
759            ClassArchetype::Healer => {
760                sheet.set_base(StatKind::Wisdom, 18.0);
761                sheet.set_base(StatKind::Charisma, 15.0);
762                sheet.set_base(StatKind::Intelligence, 13.0);
763                sheet.set_base(StatKind::Vitality, 12.0);
764                sheet.set_base(StatKind::Willpower, 12.0);
765            }
766            ClassArchetype::Ranger => {
767                sheet.set_base(StatKind::Dexterity, 16.0);
768                sheet.set_base(StatKind::Perception, 15.0);
769                sheet.set_base(StatKind::Agility, 14.0);
770                sheet.set_base(StatKind::Strength, 12.0);
771                sheet.set_base(StatKind::Endurance, 12.0);
772            }
773            ClassArchetype::Summoner => {
774                sheet.set_base(StatKind::Intelligence, 16.0);
775                sheet.set_base(StatKind::Charisma, 17.0);
776                sheet.set_base(StatKind::Wisdom, 13.0);
777                sheet.set_base(StatKind::Willpower, 12.0);
778            }
779            ClassArchetype::Paladin => {
780                sheet.set_base(StatKind::Strength, 14.0);
781                sheet.set_base(StatKind::Constitution, 15.0);
782                sheet.set_base(StatKind::Charisma, 13.0);
783                sheet.set_base(StatKind::Wisdom, 12.0);
784                sheet.set_base(StatKind::Vitality, 13.0);
785            }
786            ClassArchetype::Necromancer => {
787                sheet.set_base(StatKind::Intelligence, 16.0);
788                sheet.set_base(StatKind::Willpower, 15.0);
789                sheet.set_base(StatKind::Wisdom, 12.0);
790                sheet.set_base(StatKind::Charisma, 8.0);
791                sheet.set_base(StatKind::Endurance, 11.0);
792            }
793            ClassArchetype::Berserker => {
794                sheet.set_base(StatKind::Strength, 18.0);
795                sheet.set_base(StatKind::Endurance, 16.0);
796                sheet.set_base(StatKind::Vitality, 14.0);
797                sheet.set_base(StatKind::Agility, 12.0);
798                sheet.set_base(StatKind::Constitution, 10.0);
799            }
800            ClassArchetype::Elementalist => {
801                sheet.set_base(StatKind::Intelligence, 17.0);
802                sheet.set_base(StatKind::Wisdom, 15.0);
803                sheet.set_base(StatKind::Agility, 12.0);
804                sheet.set_base(StatKind::Perception, 11.0);
805                sheet.set_base(StatKind::Willpower, 13.0);
806            }
807        }
808        sheet
809    }
810
811    pub fn growth_for(class: ClassArchetype) -> StatGrowth {
812        match class {
813            ClassArchetype::Warrior => StatGrowth::warrior(),
814            ClassArchetype::Mage => StatGrowth::mage(),
815            ClassArchetype::Rogue => StatGrowth::rogue(),
816            ClassArchetype::Healer => StatGrowth::healer(),
817            ClassArchetype::Ranger => StatGrowth::ranger(),
818            ClassArchetype::Summoner => StatGrowth::new()
819                .set(StatKind::Intelligence, 2.5)
820                .set(StatKind::Charisma, 3.0)
821                .set(StatKind::Wisdom, 2.0),
822            ClassArchetype::Paladin => StatGrowth::new()
823                .set(StatKind::Strength, 2.0)
824                .set(StatKind::Constitution, 2.5)
825                .set(StatKind::Wisdom, 1.5)
826                .set(StatKind::Vitality, 2.0),
827            ClassArchetype::Necromancer => StatGrowth::new()
828                .set(StatKind::Intelligence, 3.0)
829                .set(StatKind::Willpower, 2.5)
830                .set(StatKind::Wisdom, 1.5),
831            ClassArchetype::Berserker => StatGrowth::new()
832                .set(StatKind::Strength, 4.0)
833                .set(StatKind::Endurance, 2.5)
834                .set(StatKind::Vitality, 2.0)
835                .set(StatKind::Agility, 1.0),
836            ClassArchetype::Elementalist => StatGrowth::new()
837                .set(StatKind::Intelligence, 3.5)
838                .set(StatKind::Wisdom, 2.0)
839                .set(StatKind::Agility, 1.5),
840        }
841    }
842}
843
844// ---------------------------------------------------------------------------
845// AllResources — convenience wrapper for HP / MP / Stamina
846// ---------------------------------------------------------------------------
847
848#[derive(Debug, Clone)]
849pub struct AllResources {
850    pub hp: ResourcePool,
851    pub mp: ResourcePool,
852    pub stamina: ResourcePool,
853}
854
855impl AllResources {
856    pub fn from_sheet(sheet: &StatSheet, level: u32) -> Self {
857        let max_hp = sheet.max_hp(level);
858        let max_mp = sheet.max_mp(level);
859        let max_st = sheet.max_stamina(level);
860        Self {
861            hp: ResourcePool::new(max_hp, sheet.hp_regen(), 5.0),
862            mp: ResourcePool::new(max_mp, sheet.mp_regen(), 3.0),
863            stamina: ResourcePool::new(max_st, 10.0, 1.0),
864        }
865    }
866
867    pub fn tick(&mut self, dt: f32) {
868        self.hp.tick(dt);
869        self.mp.tick(dt);
870        self.stamina.tick(dt);
871    }
872
873    pub fn is_alive(&self) -> bool {
874        self.hp.current > 0.0
875    }
876}
877
878impl Default for AllResources {
879    fn default() -> Self {
880        Self {
881            hp: ResourcePool::new(100.0, 1.0, 5.0),
882            mp: ResourcePool::new(50.0, 2.0, 3.0),
883            stamina: ResourcePool::new(100.0, 10.0, 1.0),
884        }
885    }
886}
887
888// ---------------------------------------------------------------------------
889// Tests
890// ---------------------------------------------------------------------------
891
892#[cfg(test)]
893mod tests {
894    use super::*;
895
896    #[test]
897    fn test_stat_value_final() {
898        let mut sv = StatValue::new(10.0);
899        sv.flat_bonus = 5.0;
900        sv.percent_bonus = 0.5; // +50%
901        sv.multiplier = 2.0;
902        // (10 + 5) * (1 + 0.5) * 2.0 = 15 * 1.5 * 2 = 45
903        assert!((sv.final_value() - 45.0).abs() < f32::EPSILON);
904    }
905
906    #[test]
907    fn test_modifier_registry_flat_add() {
908        let mut reg = ModifierRegistry::new();
909        reg.add(StatModifier::flat("sword", StatKind::Strength, 10.0));
910        let mut sv = StatValue::new(20.0);
911        reg.apply_to(StatKind::Strength, &mut sv);
912        assert!((sv.final_value() - 30.0).abs() < f32::EPSILON);
913    }
914
915    #[test]
916    fn test_modifier_registry_remove_source() {
917        let mut reg = ModifierRegistry::new();
918        reg.add(StatModifier::flat("enchant", StatKind::Dexterity, 5.0));
919        reg.remove_by_source("enchant");
920        assert_eq!(reg.count(), 0);
921    }
922
923    #[test]
924    fn test_modifier_override() {
925        let mut reg = ModifierRegistry::new();
926        reg.add(StatModifier::override_val("cap", StatKind::CritChance, 75.0));
927        let mut sv = StatValue::new(99.0);
928        reg.apply_to(StatKind::CritChance, &mut sv);
929        assert!((sv.final_value() - 75.0).abs() < f32::EPSILON);
930    }
931
932    #[test]
933    fn test_stat_sheet_derived_hp() {
934        let sheet = StatPreset::for_class(ClassArchetype::Warrior, 1);
935        let hp = sheet.max_hp(10);
936        assert!(hp > 0.0);
937    }
938
939    #[test]
940    fn test_crit_chance_cap() {
941        let mut sheet = StatSheet::new();
942        sheet.set_base(StatKind::Luck, 1000.0);
943        assert!(sheet.crit_chance() <= 75.0);
944    }
945
946    #[test]
947    fn test_resource_pool_drain_restore() {
948        let mut pool = ResourcePool::new(100.0, 5.0, 0.0);
949        let drained = pool.drain(30.0);
950        assert!((drained - 30.0).abs() < f32::EPSILON);
951        assert!((pool.current - 70.0).abs() < f32::EPSILON);
952        let restored = pool.restore(20.0);
953        assert!((restored - 20.0).abs() < f32::EPSILON);
954        assert!((pool.current - 90.0).abs() < f32::EPSILON);
955    }
956
957    #[test]
958    fn test_resource_pool_overflow() {
959        let mut pool = ResourcePool::new(100.0, 5.0, 0.0);
960        pool.restore(999.0);
961        assert!((pool.current - 100.0).abs() < f32::EPSILON);
962    }
963
964    #[test]
965    fn test_resource_pool_underflow() {
966        let mut pool = ResourcePool::new(100.0, 5.0, 0.0);
967        let drained = pool.drain(999.0);
968        assert!((drained - 100.0).abs() < f32::EPSILON);
969        assert!((pool.current).abs() < f32::EPSILON);
970    }
971
972    #[test]
973    fn test_resource_pool_regen() {
974        let mut pool = ResourcePool::new(100.0, 10.0, 0.0);
975        pool.drain(50.0);
976        pool.tick(1.0);
977        assert!((pool.current - 60.0).abs() < f32::EPSILON);
978    }
979
980    #[test]
981    fn test_resource_pool_regen_delay() {
982        let mut pool = ResourcePool::new(100.0, 10.0, 5.0);
983        pool.drain(50.0);
984        pool.tick(3.0); // still in delay
985        assert!((pool.current - 50.0).abs() < f32::EPSILON);
986        pool.tick(3.0); // delay expires, regen kicks in
987        assert!(pool.current > 50.0);
988    }
989
990    #[test]
991    fn test_xp_curve_quadratic() {
992        let curve = XpCurve::Quadratic { base: 100, factor: 1.5 };
993        let l1 = curve.xp_for_level(1);
994        let l2 = curve.xp_for_level(2);
995        assert!(l2 > l1);
996    }
997
998    #[test]
999    fn test_level_data_add_xp() {
1000        let mut ld = LevelData::new(XpCurve::Linear { base: 100, increment: 50 }, 100);
1001        let levs = ld.add_xp(100);
1002        assert_eq!(levs, 1);
1003        assert_eq!(ld.level, 2);
1004    }
1005
1006    #[test]
1007    fn test_level_data_multi_level() {
1008        let mut ld = LevelData::new(XpCurve::Linear { base: 10, increment: 0 }, 100);
1009        let levs = ld.add_xp(100);
1010        assert!(levs >= 10);
1011    }
1012
1013    #[test]
1014    fn test_stat_preset_warrior() {
1015        let sheet = StatPreset::for_class(ClassArchetype::Warrior, 10);
1016        assert!(sheet.get(StatKind::Strength) > 10.0);
1017    }
1018
1019    #[test]
1020    fn test_stat_sheet_apply_modifiers() {
1021        let mut sheet = StatSheet::new();
1022        let mut reg = ModifierRegistry::new();
1023        reg.add(StatModifier::flat("test", StatKind::Strength, 100.0));
1024        sheet.apply_modifiers(&reg);
1025        assert!(sheet.get(StatKind::Strength) > 100.0);
1026    }
1027
1028    #[test]
1029    fn test_all_resources_tick() {
1030        let sheet = StatSheet::new();
1031        let mut res = AllResources::from_sheet(&sheet, 1);
1032        res.hp.drain(10.0);
1033        res.hp.regen_delay = 0.0;
1034        res.tick(1.0);
1035        // Should have regenerated some HP
1036        assert!(res.hp.current > res.hp.max - 10.0);
1037    }
1038}