1use std::collections::HashMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum StatKind {
12 Strength,
14 Dexterity,
15 Intelligence,
16 Vitality,
17 Wisdom,
18 Charisma,
19 Luck,
20 Constitution,
21 Agility,
22 Endurance,
23 Perception,
24 Willpower,
25 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 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#[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 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#[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#[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 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#[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 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 pub fn apply_modifiers(&mut self, registry: &ModifierRegistry) {
302 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 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 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 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 pub fn physical_attack(&self, weapon_damage: f32) -> f32 {
334 self.get(StatKind::Strength) * 2.0 + weapon_damage
335 }
336
337 pub fn magical_attack(&self, spell_power: f32) -> f32 {
339 self.get(StatKind::Intelligence) * 2.0 + spell_power
340 }
341
342 pub fn defense(&self, armor_rating: f32) -> f32 {
344 self.get(StatKind::Constitution) + armor_rating
345 }
346
347 pub fn magic_resist(&self, magic_armor: f32) -> f32 {
349 self.get(StatKind::Willpower) * 0.5 + magic_armor
350 }
351
352 pub fn speed(&self) -> f32 {
354 self.get(StatKind::Dexterity) * 0.5 + self.get(StatKind::Agility) * 0.5
355 }
356
357 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 pub fn crit_multiplier(&self) -> f32 {
365 1.5 + self.get(StatKind::Strength) * 0.01
366 }
367
368 pub fn evasion(&self) -> f32 {
370 self.get(StatKind::Dexterity) * 0.3 + self.get(StatKind::Agility) * 0.2
371 }
372
373 pub fn accuracy(&self) -> f32 {
375 self.get(StatKind::Perception) * 0.5 + self.get(StatKind::Dexterity) * 0.2
376 }
377
378 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 pub fn hp_regen(&self) -> f32 {
386 self.get(StatKind::Vitality) * 0.02 + self.get(StatKind::Regeneration)
387 }
388
389 pub fn mp_regen(&self) -> f32 {
391 self.get(StatKind::Wisdom) * 0.05 + self.get(StatKind::ManaRegen)
392 }
393
394 pub fn move_speed(&self) -> f32 {
396 100.0 + self.get(StatKind::Agility) * 2.0 + self.get(StatKind::MoveSpeed)
397 }
398
399 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#[derive(Debug, Clone)]
416pub struct ResourcePool {
417 pub current: f32,
418 pub max: f32,
419 pub regen_rate: f32,
420 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 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 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 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 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 pub fn set_current(&mut self, val: f32) {
488 self.current = val.clamp(0.0, self.max);
489 }
490
491 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#[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#[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 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#[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 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#[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 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#[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#[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; sv.multiplier = 2.0;
902 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); assert!((pool.current - 50.0).abs() < f32::EPSILON);
986 pool.tick(3.0); 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(®);
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 assert!(res.hp.current > res.hp.max - 10.0);
1037 }
1038}