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 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 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 pub fn set_current(&mut self, val: f32) {
501 self.current = val.clamp(0.0, self.max);
502 }
503
504 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#[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#[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 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#[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 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#[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 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#[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#[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; sv.multiplier = 2.0;
915 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); assert!((pool.current - 50.0).abs() < f32::EPSILON);
999 pool.tick(3.0); 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(®);
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 assert!(res.hp.current > res.hp.max - 10.0);
1050 }
1051}