1use std::collections::HashMap;
5use crate::character::stats::{StatKind, StatModifier};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct SkillId(pub u64);
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum SkillType {
20 Active,
21 Passive,
22 Toggle,
23 Aura,
24 Reaction,
25 Ultimate,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum Element {
34 Physical,
35 Fire,
36 Ice,
37 Lightning,
38 Holy,
39 Dark,
40 Arcane,
41 Poison,
42 Nature,
43 Wind,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum HealTarget {
48 Self_,
49 SingleAlly,
50 AllAllies,
51 AreaAllies { radius: u32 },
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum BuffTarget {
56 Self_,
57 SingleAlly,
58 SingleEnemy,
59 AllAllies,
60 AllEnemies,
61 Area { radius: u32 },
62}
63
64#[derive(Debug, Clone)]
69pub enum SkillEffect {
70 Damage {
71 base_damage: f32,
72 ratio: f32,
73 element: Element,
74 aoe_radius: f32,
75 pierces: bool,
76 },
77 Heal {
78 base_heal: f32,
79 ratio: f32,
80 target: HealTarget,
81 },
82 Buff {
83 modifiers: Vec<StatModifier>,
84 duration_secs: f32,
85 target: BuffTarget,
86 },
87 Debuff {
88 modifiers: Vec<StatModifier>,
89 duration_secs: f32,
90 target: BuffTarget,
91 },
92 Summon {
93 entity_type: String,
94 count: u32,
95 duration_secs: f32,
96 },
97 Teleport {
98 range: f32,
99 blink: bool, },
101 Zone {
102 radius: f32,
103 duration_secs: f32,
104 tick_interval: f32,
105 tick_effect: Box<SkillEffect>,
106 },
107 Projectile {
108 speed: f32,
109 pierce_count: u32,
110 split_count: u32,
111 element: Element,
112 damage: f32,
113 },
114 Chain {
115 max_targets: u32,
116 jump_range: f32,
117 damage_reduction: f32,
118 element: Element,
119 base_damage: f32,
120 },
121 Shield {
122 absorb_amount: f32,
123 duration_secs: f32,
124 },
125 Drain {
126 stat: DrainTarget,
127 amount: f32,
128 return_fraction: f32,
129 },
130 Composite(Vec<SkillEffect>),
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum DrainTarget {
135 Hp,
136 Mp,
137 Stamina,
138}
139
140impl SkillEffect {
141 pub fn is_damaging(&self) -> bool {
142 matches!(self, SkillEffect::Damage { .. } | SkillEffect::Projectile { .. } | SkillEffect::Chain { .. })
143 }
144
145 pub fn is_healing(&self) -> bool {
146 matches!(self, SkillEffect::Heal { .. })
147 }
148}
149
150#[derive(Debug, Clone, Default)]
155pub struct SkillCost {
156 pub mana: f32,
157 pub stamina: f32,
158 pub hp: f32,
159 pub cooldown_secs: f32,
160 pub cast_time_secs: f32,
161 pub channel_time_secs: f32,
162 pub skill_point_cost: u32,
163}
164
165impl SkillCost {
166 pub fn free() -> Self {
167 Self::default()
168 }
169
170 pub fn mana_cost(mana: f32, cooldown: f32) -> Self {
171 Self { mana, cooldown_secs: cooldown, ..Default::default() }
172 }
173
174 pub fn stamina_cost(stamina: f32, cooldown: f32) -> Self {
175 Self { stamina, cooldown_secs: cooldown, ..Default::default() }
176 }
177
178 pub fn with_cast_time(mut self, cast_time: f32) -> Self {
179 self.cast_time_secs = cast_time;
180 self
181 }
182}
183
184#[derive(Debug, Clone)]
189pub enum SkillRequirement {
190 Level(u32),
191 SkillRank { skill_id: SkillId, min_rank: u32 },
192 Stat { kind: StatKind, min_value: f32 },
193 ClassArchetype(String),
194}
195
196impl SkillRequirement {
197 pub fn check_level(&self, level: u32) -> bool {
198 match self {
199 SkillRequirement::Level(required) => level >= *required,
200 _ => true, }
202 }
203}
204
205#[derive(Debug, Clone)]
210pub struct Skill {
211 pub id: SkillId,
212 pub name: String,
213 pub description: String,
214 pub icon_char: char,
215 pub skill_type: SkillType,
216 pub max_rank: u32,
217 pub requirements: Vec<SkillRequirement>,
218 pub effects_per_rank: Vec<SkillEffect>,
219 pub cost_per_rank: Vec<SkillCost>,
220 pub passive_modifiers: Vec<StatModifier>,
221 pub tags: Vec<String>,
222}
223
224impl Skill {
225 pub fn new(id: SkillId, name: impl Into<String>, skill_type: SkillType) -> Self {
226 Self {
227 id,
228 name: name.into(),
229 description: String::new(),
230 icon_char: '*',
231 skill_type,
232 max_rank: 5,
233 requirements: Vec::new(),
234 effects_per_rank: Vec::new(),
235 cost_per_rank: Vec::new(),
236 passive_modifiers: Vec::new(),
237 tags: Vec::new(),
238 }
239 }
240
241 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
242 self.description = desc.into();
243 self
244 }
245
246 pub fn with_icon(mut self, c: char) -> Self {
247 self.icon_char = c;
248 self
249 }
250
251 pub fn with_max_rank(mut self, rank: u32) -> Self {
252 self.max_rank = rank;
253 self
254 }
255
256 pub fn add_requirement(mut self, req: SkillRequirement) -> Self {
257 self.requirements.push(req);
258 self
259 }
260
261 pub fn add_rank_effect(mut self, effect: SkillEffect) -> Self {
262 self.effects_per_rank.push(effect);
263 self
264 }
265
266 pub fn add_rank_cost(mut self, cost: SkillCost) -> Self {
267 self.cost_per_rank.push(cost);
268 self
269 }
270
271 pub fn add_passive(mut self, modifier: StatModifier) -> Self {
272 self.passive_modifiers.push(modifier);
273 self
274 }
275
276 pub fn add_tag(mut self, tag: impl Into<String>) -> Self {
277 self.tags.push(tag.into());
278 self
279 }
280
281 pub fn effect_at_rank(&self, rank: u32) -> Option<&SkillEffect> {
282 let idx = (rank as usize).saturating_sub(1).min(self.effects_per_rank.len().saturating_sub(1));
283 self.effects_per_rank.get(idx)
284 }
285
286 pub fn cost_at_rank(&self, rank: u32) -> Option<&SkillCost> {
287 let idx = (rank as usize).saturating_sub(1).min(self.cost_per_rank.len().saturating_sub(1));
288 self.cost_per_rank.get(idx)
289 }
290
291 pub fn cooldown_at_rank(&self, rank: u32) -> f32 {
292 self.cost_at_rank(rank).map(|c| c.cooldown_secs).unwrap_or(0.0)
293 }
294}
295
296#[derive(Debug, Clone)]
301pub struct SkillNode {
302 pub skill: Skill,
303 pub position: (u8, u8),
304 pub unlocked: bool,
305 pub rank: u32,
306 pub prereqs: Vec<usize>, }
308
309impl SkillNode {
310 pub fn new(skill: Skill, position: (u8, u8)) -> Self {
311 Self { skill, position, unlocked: false, rank: 0, prereqs: Vec::new() }
312 }
313
314 pub fn with_prereqs(mut self, prereqs: Vec<usize>) -> Self {
315 self.prereqs = prereqs;
316 self
317 }
318
319 pub fn is_available(&self, tree: &SkillTree) -> bool {
320 if self.prereqs.is_empty() { return true; }
321 self.prereqs.iter().all(|&idx| {
322 tree.nodes.get(idx).map(|n| n.rank > 0).unwrap_or(false)
323 })
324 }
325}
326
327#[derive(Debug, Clone)]
332pub struct SkillTree {
333 pub name: String,
334 pub nodes: Vec<SkillNode>,
335 pub connections: Vec<(usize, usize)>,
336}
337
338impl SkillTree {
339 pub fn new(name: impl Into<String>) -> Self {
340 Self { name: name.into(), nodes: Vec::new(), connections: Vec::new() }
341 }
342
343 pub fn add_node(mut self, node: SkillNode) -> Self {
344 self.nodes.push(node);
345 self
346 }
347
348 pub fn add_connection(mut self, from: usize, to: usize) -> Self {
349 self.connections.push((from, to));
350 self
351 }
352
353 pub fn total_points_spent(&self) -> u32 {
354 self.nodes.iter().map(|n| n.rank).sum()
355 }
356
357 pub fn find_by_id(&self, id: SkillId) -> Option<(usize, &SkillNode)> {
358 self.nodes.iter().enumerate().find(|(_, n)| n.skill.id == id)
359 }
360
361 pub fn find_by_id_mut(&mut self, id: SkillId) -> Option<(usize, &mut SkillNode)> {
362 self.nodes.iter_mut().enumerate().find(|(_, n)| n.skill.id == id)
363 }
364
365 pub fn available_nodes(&self) -> Vec<usize> {
366 (0..self.nodes.len())
367 .filter(|&i| self.nodes[i].is_available(self))
368 .collect()
369 }
370}
371
372#[derive(Debug, Clone, Default)]
377pub struct SkillBook {
378 pub known: HashMap<SkillId, (Skill, u32)>, }
380
381impl SkillBook {
382 pub fn new() -> Self {
383 Self { known: HashMap::new() }
384 }
385
386 pub fn learn(&mut self, skill: Skill) -> bool {
387 if self.known.contains_key(&skill.id) { return false; }
388 self.known.insert(skill.id, (skill, 1));
389 true
390 }
391
392 pub fn upgrade(&mut self, skill_id: SkillId) -> bool {
393 if let Some((skill, rank)) = self.known.get_mut(&skill_id) {
394 if *rank < skill.max_rank {
395 *rank += 1;
396 return true;
397 }
398 }
399 false
400 }
401
402 pub fn forget(&mut self, skill_id: SkillId) -> Option<Skill> {
403 self.known.remove(&skill_id).map(|(s, _)| s)
404 }
405
406 pub fn rank_of(&self, skill_id: SkillId) -> u32 {
407 self.known.get(&skill_id).map(|(_, r)| *r).unwrap_or(0)
408 }
409
410 pub fn knows(&self, skill_id: SkillId) -> bool {
411 self.known.contains_key(&skill_id)
412 }
413
414 pub fn all_skills(&self) -> impl Iterator<Item = (&Skill, u32)> {
415 self.known.values().map(|(s, r)| (s, *r))
416 }
417
418 pub fn passive_skills(&self) -> impl Iterator<Item = (&Skill, u32)> {
419 self.known.values()
420 .filter(|(s, _)| s.skill_type == SkillType::Passive)
421 .map(|(s, r)| (s, *r))
422 }
423
424 pub fn active_skills(&self) -> impl Iterator<Item = (&Skill, u32)> {
425 self.known.values()
426 .filter(|(s, _)| s.skill_type == SkillType::Active || s.skill_type == SkillType::Ultimate)
427 .map(|(s, r)| (s, *r))
428 }
429
430 pub fn can_afford_upgrade(&self, skill_id: SkillId, skill_points: u32) -> bool {
431 if let Some((skill, rank)) = self.known.get(&skill_id) {
432 if *rank >= skill.max_rank { return false; }
433 let cost = skill.cost_at_rank(*rank + 1)
434 .map(|c| c.skill_point_cost)
435 .unwrap_or(1);
436 skill_points >= cost
437 } else {
438 false
439 }
440 }
441}
442
443#[derive(Debug, Clone)]
448pub struct Ability {
449 pub skill_id: SkillId,
450 pub hotkey: u8,
451 pub override_icon: Option<char>,
452 pub override_name: Option<String>,
453}
454
455impl Ability {
456 pub fn new(skill_id: SkillId, hotkey: u8) -> Self {
457 Self { skill_id, hotkey, override_icon: None, override_name: None }
458 }
459}
460
461#[derive(Debug, Clone)]
466pub struct AbilityBar {
467 pub slots: [Option<Ability>; 12],
468}
469
470impl AbilityBar {
471 pub fn new() -> Self {
472 Self { slots: [const { None }; 12] }
473 }
474
475 pub fn bind(&mut self, slot: usize, ability: Ability) -> Option<Ability> {
476 if slot >= 12 { return None; }
477 let old = self.slots[slot].take();
478 self.slots[slot] = Some(ability);
479 old
480 }
481
482 pub fn unbind(&mut self, slot: usize) -> Option<Ability> {
483 if slot >= 12 { return None; }
484 self.slots[slot].take()
485 }
486
487 pub fn get(&self, slot: usize) -> Option<&Ability> {
488 self.slots.get(slot).and_then(|s| s.as_ref())
489 }
490
491 pub fn find_by_skill(&self, skill_id: SkillId) -> Option<usize> {
492 self.slots.iter().position(|s| s.as_ref().map(|a| a.skill_id) == Some(skill_id))
493 }
494
495 pub fn occupied_count(&self) -> usize {
496 self.slots.iter().filter(|s| s.is_some()).count()
497 }
498}
499
500impl Default for AbilityBar {
501 fn default() -> Self {
502 Self::new()
503 }
504}
505
506#[derive(Debug, Clone, Default)]
511pub struct CooldownTracker {
512 pub timers: HashMap<SkillId, f32>,
513}
514
515impl CooldownTracker {
516 pub fn new() -> Self {
517 Self { timers: HashMap::new() }
518 }
519
520 pub fn start(&mut self, skill_id: SkillId, duration: f32) {
521 self.timers.insert(skill_id, duration);
522 }
523
524 pub fn remaining(&self, skill_id: SkillId) -> f32 {
525 *self.timers.get(&skill_id).unwrap_or(&0.0)
526 }
527
528 pub fn is_ready(&self, skill_id: SkillId) -> bool {
529 self.remaining(skill_id) <= 0.0
530 }
531
532 pub fn tick(&mut self, dt: f32) {
533 for timer in self.timers.values_mut() {
534 *timer = (*timer - dt).max(0.0);
535 }
536 }
537
538 pub fn reduce(&mut self, skill_id: SkillId, amount: f32) {
539 if let Some(t) = self.timers.get_mut(&skill_id) {
540 *t = (*t - amount).max(0.0);
541 }
542 }
543
544 pub fn reset(&mut self, skill_id: SkillId) {
545 self.timers.remove(&skill_id);
546 }
547
548 pub fn reset_all(&mut self) {
549 self.timers.clear();
550 }
551
552 pub fn apply_cdr(&mut self, cdr_percent: f32) {
553 let mult = (1.0 - cdr_percent / 100.0).max(0.0);
555 for timer in self.timers.values_mut() {
556 *timer *= mult;
557 }
558 }
559}
560
561#[derive(Debug, Clone)]
566pub struct Combo {
567 pub name: String,
568 pub trigger_sequence: Vec<SkillId>,
569 pub bonus_effect: SkillEffect,
570 pub window_ms: f32,
571 pub reset_on_damage: bool,
572}
573
574impl Combo {
575 pub fn new(name: impl Into<String>, sequence: Vec<SkillId>, bonus: SkillEffect, window_ms: f32) -> Self {
576 Self {
577 name: name.into(),
578 trigger_sequence: sequence,
579 bonus_effect: bonus,
580 window_ms,
581 reset_on_damage: false,
582 }
583 }
584
585 pub fn matches(&self, recent: &[SkillId]) -> bool {
586 if recent.len() < self.trigger_sequence.len() { return false; }
587 let start = recent.len() - self.trigger_sequence.len();
588 &recent[start..] == self.trigger_sequence.as_slice()
589 }
590}
591
592#[derive(Debug, Clone)]
593pub struct ComboSystem {
594 pub combos: Vec<Combo>,
595 pub recent_skills: Vec<SkillId>,
596 pub last_skill_time: f32,
597 pub current_time: f32,
598 pub max_history: usize,
599}
600
601impl ComboSystem {
602 pub fn new() -> Self {
603 Self {
604 combos: Vec::new(),
605 recent_skills: Vec::new(),
606 last_skill_time: 0.0,
607 current_time: 0.0,
608 max_history: 8,
609 }
610 }
611
612 pub fn add_combo(&mut self, combo: Combo) {
613 self.combos.push(combo);
614 }
615
616 pub fn tick(&mut self, dt: f32) {
617 self.current_time += dt;
618 }
619
620 pub fn register_skill_use(&mut self, skill_id: SkillId) {
621 if let Some(last) = self.recent_skills.last() {
623 let _ = last;
624 let elapsed_ms = (self.current_time - self.last_skill_time) * 1000.0;
625 let max_window = self.combos.iter().map(|c| c.window_ms).fold(0.0f32, f32::max);
626 if elapsed_ms > max_window && max_window > 0.0 {
627 self.recent_skills.clear();
628 }
629 }
630 self.recent_skills.push(skill_id);
631 self.last_skill_time = self.current_time;
632 if self.recent_skills.len() > self.max_history {
633 self.recent_skills.remove(0);
634 }
635 }
636
637 pub fn check_combos(&self) -> Vec<&Combo> {
638 let elapsed_ms = (self.current_time - self.last_skill_time) * 1000.0;
639 self.combos.iter()
640 .filter(|c| {
641 c.matches(&self.recent_skills) && elapsed_ms <= c.window_ms
642 })
643 .collect()
644 }
645
646 pub fn reset(&mut self) {
647 self.recent_skills.clear();
648 }
649}
650
651impl Default for ComboSystem {
652 fn default() -> Self {
653 Self::new()
654 }
655}
656
657pub struct SkillPresets;
662
663impl SkillPresets {
664 pub fn warrior_tree() -> SkillTree {
665 let slash = Skill::new(SkillId(1001), "Power Slash", SkillType::Active)
666 .with_description("A powerful slash dealing heavy physical damage.")
667 .with_icon('/')
668 .with_max_rank(5)
669 .add_rank_effect(SkillEffect::Damage { base_damage: 30.0, ratio: 1.5, element: Element::Physical, aoe_radius: 0.0, pierces: false })
670 .add_rank_cost(SkillCost::stamina_cost(20.0, 4.0))
671 .add_rank_effect(SkillEffect::Damage { base_damage: 45.0, ratio: 1.7, element: Element::Physical, aoe_radius: 0.0, pierces: false })
672 .add_rank_cost(SkillCost::stamina_cost(20.0, 3.8))
673 .add_rank_effect(SkillEffect::Damage { base_damage: 60.0, ratio: 1.9, element: Element::Physical, aoe_radius: 0.0, pierces: false })
674 .add_rank_cost(SkillCost::stamina_cost(22.0, 3.5))
675 .add_rank_effect(SkillEffect::Damage { base_damage: 80.0, ratio: 2.1, element: Element::Physical, aoe_radius: 0.0, pierces: false })
676 .add_rank_cost(SkillCost::stamina_cost(25.0, 3.2))
677 .add_rank_effect(SkillEffect::Damage { base_damage: 100.0, ratio: 2.5, element: Element::Physical, aoe_radius: 0.0, pierces: false })
678 .add_rank_cost(SkillCost::stamina_cost(30.0, 3.0))
679 .add_tag("melee");
680
681 let whirlwind = Skill::new(SkillId(1002), "Whirlwind", SkillType::Active)
682 .with_description("Spin and deal AoE damage to all nearby enemies.")
683 .with_icon('✦')
684 .with_max_rank(5)
685 .add_requirement(SkillRequirement::SkillRank { skill_id: SkillId(1001), min_rank: 2 })
686 .add_rank_effect(SkillEffect::Damage { base_damage: 20.0, ratio: 1.0, element: Element::Physical, aoe_radius: 3.0, pierces: false })
687 .add_rank_cost(SkillCost::stamina_cost(35.0, 8.0))
688 .add_rank_effect(SkillEffect::Damage { base_damage: 30.0, ratio: 1.2, element: Element::Physical, aoe_radius: 3.5, pierces: false })
689 .add_rank_cost(SkillCost::stamina_cost(35.0, 7.5))
690 .add_rank_effect(SkillEffect::Damage { base_damage: 45.0, ratio: 1.4, element: Element::Physical, aoe_radius: 4.0, pierces: false })
691 .add_rank_cost(SkillCost::stamina_cost(38.0, 7.0))
692 .add_rank_effect(SkillEffect::Damage { base_damage: 60.0, ratio: 1.6, element: Element::Physical, aoe_radius: 4.5, pierces: false })
693 .add_rank_cost(SkillCost::stamina_cost(40.0, 6.5))
694 .add_rank_effect(SkillEffect::Damage { base_damage: 80.0, ratio: 2.0, element: Element::Physical, aoe_radius: 5.0, pierces: false })
695 .add_rank_cost(SkillCost::stamina_cost(45.0, 6.0))
696 .add_tag("melee").add_tag("aoe");
697
698 let battle_cry = Skill::new(SkillId(1003), "Battle Cry", SkillType::Active)
699 .with_description("Rally allies, granting bonus attack for 30 seconds.")
700 .with_icon('!')
701 .with_max_rank(3)
702 .add_rank_effect(SkillEffect::Buff {
703 modifiers: vec![StatModifier::percent("battle_cry", StatKind::PhysicalAttack, 0.1)],
704 duration_secs: 30.0,
705 target: BuffTarget::AllAllies,
706 })
707 .add_rank_cost(SkillCost::stamina_cost(30.0, 60.0))
708 .add_tag("support");
709
710 let iron_skin = Skill::new(SkillId(1004), "Iron Skin", SkillType::Passive)
711 .with_description("Passive increase to Defense.")
712 .with_icon('Ω')
713 .with_max_rank(5)
714 .add_passive(StatModifier::flat("iron_skin", StatKind::Defense, 5.0));
715
716 let berserker_rage = Skill::new(SkillId(1005), "Berserker Rage", SkillType::Toggle)
717 .with_description("Enter a rage state: more damage, less defense.")
718 .with_icon('Ψ')
719 .with_max_rank(1)
720 .add_requirement(SkillRequirement::Level(10))
721 .add_rank_effect(SkillEffect::Composite(vec![
722 SkillEffect::Buff {
723 modifiers: vec![StatModifier::percent("berserk", StatKind::PhysicalAttack, 0.3)],
724 duration_secs: f32::MAX,
725 target: BuffTarget::Self_,
726 },
727 SkillEffect::Debuff {
728 modifiers: vec![StatModifier::percent("berserk_def", StatKind::Defense, -0.2)],
729 duration_secs: f32::MAX,
730 target: BuffTarget::Self_,
731 },
732 ]))
733 .add_rank_cost(SkillCost::free());
734
735 SkillTree::new("Warrior")
736 .add_node(SkillNode::new(slash, (2, 0)))
737 .add_node(SkillNode::new(whirlwind, (2, 1)).with_prereqs(vec![0]))
738 .add_node(SkillNode::new(battle_cry, (1, 1)))
739 .add_node(SkillNode::new(iron_skin, (3, 0)))
740 .add_node(SkillNode::new(berserker_rage, (2, 2)).with_prereqs(vec![1]))
741 .add_connection(0, 1)
742 .add_connection(1, 4)
743 }
744
745 pub fn mage_tree() -> SkillTree {
746 let fireball = Skill::new(SkillId(2001), "Fireball", SkillType::Active)
747 .with_description("Hurl a flaming orb at your enemies.")
748 .with_icon('o')
749 .with_max_rank(5)
750 .add_rank_effect(SkillEffect::Projectile { speed: 15.0, pierce_count: 0, split_count: 0, element: Element::Fire, damage: 40.0 })
751 .add_rank_cost(SkillCost::mana_cost(25.0, 3.0).with_cast_time(0.8))
752 .add_rank_effect(SkillEffect::Projectile { speed: 15.0, pierce_count: 0, split_count: 0, element: Element::Fire, damage: 60.0 })
753 .add_rank_cost(SkillCost::mana_cost(25.0, 2.8).with_cast_time(0.75))
754 .add_rank_effect(SkillEffect::Projectile { speed: 17.0, pierce_count: 0, split_count: 1, element: Element::Fire, damage: 80.0 })
755 .add_rank_cost(SkillCost::mana_cost(28.0, 2.5).with_cast_time(0.7))
756 .add_rank_effect(SkillEffect::Projectile { speed: 17.0, pierce_count: 0, split_count: 1, element: Element::Fire, damage: 100.0 })
757 .add_rank_cost(SkillCost::mana_cost(30.0, 2.3).with_cast_time(0.65))
758 .add_rank_effect(SkillEffect::Projectile { speed: 20.0, pierce_count: 0, split_count: 2, element: Element::Fire, damage: 130.0 })
759 .add_rank_cost(SkillCost::mana_cost(35.0, 2.0).with_cast_time(0.6))
760 .add_tag("fire").add_tag("ranged");
761
762 let ice_shard = Skill::new(SkillId(2002), "Ice Shard", SkillType::Active)
763 .with_description("Launch a shard of ice that pierces through enemies.")
764 .with_icon('*')
765 .with_max_rank(5)
766 .add_rank_effect(SkillEffect::Projectile { speed: 20.0, pierce_count: 2, split_count: 0, element: Element::Ice, damage: 30.0 })
767 .add_rank_cost(SkillCost::mana_cost(20.0, 2.5))
768 .add_rank_effect(SkillEffect::Projectile { speed: 20.0, pierce_count: 3, split_count: 0, element: Element::Ice, damage: 45.0 })
769 .add_rank_cost(SkillCost::mana_cost(22.0, 2.3))
770 .add_rank_effect(SkillEffect::Projectile { speed: 22.0, pierce_count: 3, split_count: 0, element: Element::Ice, damage: 60.0 })
771 .add_rank_cost(SkillCost::mana_cost(24.0, 2.1))
772 .add_rank_effect(SkillEffect::Projectile { speed: 22.0, pierce_count: 4, split_count: 0, element: Element::Ice, damage: 80.0 })
773 .add_rank_cost(SkillCost::mana_cost(26.0, 1.9))
774 .add_rank_effect(SkillEffect::Projectile { speed: 25.0, pierce_count: 5, split_count: 0, element: Element::Ice, damage: 100.0 })
775 .add_rank_cost(SkillCost::mana_cost(30.0, 1.7))
776 .add_tag("ice").add_tag("ranged");
777
778 let arcane_shield = Skill::new(SkillId(2003), "Arcane Shield", SkillType::Active)
779 .with_description("Create a barrier that absorbs incoming damage.")
780 .with_icon('Ω')
781 .with_max_rank(3)
782 .add_rank_effect(SkillEffect::Shield { absorb_amount: 100.0, duration_secs: 10.0 })
783 .add_rank_cost(SkillCost::mana_cost(40.0, 30.0).with_cast_time(0.5))
784 .add_rank_effect(SkillEffect::Shield { absorb_amount: 175.0, duration_secs: 12.0 })
785 .add_rank_cost(SkillCost::mana_cost(40.0, 28.0).with_cast_time(0.4))
786 .add_rank_effect(SkillEffect::Shield { absorb_amount: 280.0, duration_secs: 15.0 })
787 .add_rank_cost(SkillCost::mana_cost(45.0, 25.0).with_cast_time(0.3));
788
789 let mana_mastery = Skill::new(SkillId(2004), "Mana Mastery", SkillType::Passive)
790 .with_description("Reduces mana cost of all spells.")
791 .with_icon('M')
792 .with_max_rank(5)
793 .add_passive(StatModifier::percent("mana_mastery", StatKind::MaxMp, 0.05));
794
795 let blink = Skill::new(SkillId(2005), "Blink", SkillType::Active)
796 .with_description("Instantly teleport a short distance.")
797 .with_icon('→')
798 .with_max_rank(3)
799 .add_requirement(SkillRequirement::Level(8))
800 .add_rank_effect(SkillEffect::Teleport { range: 8.0, blink: true })
801 .add_rank_cost(SkillCost::mana_cost(30.0, 15.0))
802 .add_rank_effect(SkillEffect::Teleport { range: 12.0, blink: true })
803 .add_rank_cost(SkillCost::mana_cost(28.0, 12.0))
804 .add_rank_effect(SkillEffect::Teleport { range: 16.0, blink: true })
805 .add_rank_cost(SkillCost::mana_cost(25.0, 10.0));
806
807 let chain_lightning = Skill::new(SkillId(2006), "Chain Lightning", SkillType::Active)
808 .with_description("Lightning that jumps between enemies.")
809 .with_icon('~')
810 .with_max_rank(5)
811 .add_requirement(SkillRequirement::Level(15))
812 .add_rank_effect(SkillEffect::Chain { max_targets: 3, jump_range: 5.0, damage_reduction: 0.2, element: Element::Lightning, base_damage: 50.0 })
813 .add_rank_cost(SkillCost::mana_cost(45.0, 8.0).with_cast_time(1.0))
814 .add_rank_effect(SkillEffect::Chain { max_targets: 4, jump_range: 5.5, damage_reduction: 0.18, element: Element::Lightning, base_damage: 70.0 })
815 .add_rank_cost(SkillCost::mana_cost(47.0, 7.5).with_cast_time(0.9))
816 .add_rank_effect(SkillEffect::Chain { max_targets: 5, jump_range: 6.0, damage_reduction: 0.15, element: Element::Lightning, base_damage: 90.0 })
817 .add_rank_cost(SkillCost::mana_cost(50.0, 7.0).with_cast_time(0.8))
818 .add_rank_effect(SkillEffect::Chain { max_targets: 6, jump_range: 6.5, damage_reduction: 0.12, element: Element::Lightning, base_damage: 115.0 })
819 .add_rank_cost(SkillCost::mana_cost(53.0, 6.5).with_cast_time(0.75))
820 .add_rank_effect(SkillEffect::Chain { max_targets: 8, jump_range: 7.0, damage_reduction: 0.10, element: Element::Lightning, base_damage: 145.0 })
821 .add_rank_cost(SkillCost::mana_cost(58.0, 6.0).with_cast_time(0.7))
822 .add_tag("lightning").add_tag("aoe");
823
824 SkillTree::new("Mage")
825 .add_node(SkillNode::new(fireball, (1, 0)))
826 .add_node(SkillNode::new(ice_shard, (3, 0)))
827 .add_node(SkillNode::new(arcane_shield, (2, 0)))
828 .add_node(SkillNode::new(mana_mastery, (2, 1)))
829 .add_node(SkillNode::new(blink, (2, 2)))
830 .add_node(SkillNode::new(chain_lightning, (1, 2)).with_prereqs(vec![0]))
831 .add_connection(0, 5)
832 .add_connection(2, 3)
833 .add_connection(3, 4)
834 }
835
836 pub fn rogue_tree() -> SkillTree {
837 let backstab = Skill::new(SkillId(3001), "Backstab", SkillType::Active)
838 .with_description("Deal massive damage from stealth or behind the target.")
839 .with_icon('↑')
840 .with_max_rank(5)
841 .add_rank_effect(SkillEffect::Damage { base_damage: 50.0, ratio: 2.0, element: Element::Physical, aoe_radius: 0.0, pierces: false })
842 .add_rank_cost(SkillCost::stamina_cost(25.0, 6.0))
843 .add_rank_effect(SkillEffect::Damage { base_damage: 70.0, ratio: 2.3, element: Element::Physical, aoe_radius: 0.0, pierces: false })
844 .add_rank_cost(SkillCost::stamina_cost(25.0, 5.5))
845 .add_rank_effect(SkillEffect::Damage { base_damage: 95.0, ratio: 2.6, element: Element::Physical, aoe_radius: 0.0, pierces: false })
846 .add_rank_cost(SkillCost::stamina_cost(27.0, 5.0))
847 .add_rank_effect(SkillEffect::Damage { base_damage: 125.0, ratio: 3.0, element: Element::Physical, aoe_radius: 0.0, pierces: false })
848 .add_rank_cost(SkillCost::stamina_cost(28.0, 4.5))
849 .add_rank_effect(SkillEffect::Damage { base_damage: 160.0, ratio: 3.5, element: Element::Physical, aoe_radius: 0.0, pierces: false })
850 .add_rank_cost(SkillCost::stamina_cost(30.0, 4.0))
851 .add_tag("melee").add_tag("stealth");
852
853 let poison_blade = Skill::new(SkillId(3002), "Poison Blade", SkillType::Active)
854 .with_description("Coat your blade in poison, applying DoT.")
855 .with_icon('¥')
856 .with_max_rank(5)
857 .add_rank_effect(SkillEffect::Zone {
858 radius: 0.0,
859 duration_secs: 10.0,
860 tick_interval: 1.0,
861 tick_effect: Box::new(SkillEffect::Damage { base_damage: 8.0, ratio: 0.3, element: Element::Poison, aoe_radius: 0.0, pierces: false }),
862 })
863 .add_rank_cost(SkillCost::stamina_cost(15.0, 12.0))
864 .add_tag("poison");
865
866 let evasion_skill = Skill::new(SkillId(3003), "Evasion", SkillType::Passive)
867 .with_description("Permanently increases evasion.")
868 .with_icon('E')
869 .with_max_rank(5)
870 .add_passive(StatModifier::flat("evasion_skill", StatKind::Evasion, 3.0));
871
872 let shadowstep = Skill::new(SkillId(3004), "Shadowstep", SkillType::Active)
873 .with_description("Teleport behind a target enemy.")
874 .with_icon('↓')
875 .with_max_rank(3)
876 .add_requirement(SkillRequirement::Level(12))
877 .add_rank_effect(SkillEffect::Teleport { range: 6.0, blink: true })
878 .add_rank_cost(SkillCost::stamina_cost(35.0, 20.0))
879 .add_tag("movement");
880
881 SkillTree::new("Rogue")
882 .add_node(SkillNode::new(backstab, (2, 0)))
883 .add_node(SkillNode::new(poison_blade, (1, 0)))
884 .add_node(SkillNode::new(evasion_skill, (3, 0)))
885 .add_node(SkillNode::new(shadowstep, (2, 1)).with_prereqs(vec![0]))
886 .add_connection(0, 3)
887 }
888
889 pub fn healer_tree() -> SkillTree {
890 let holy_light = Skill::new(SkillId(4001), "Holy Light", SkillType::Active)
891 .with_description("Call down a beam of healing light on a target.")
892 .with_icon('+')
893 .with_max_rank(5)
894 .add_rank_effect(SkillEffect::Heal { base_heal: 60.0, ratio: 1.5, target: HealTarget::SingleAlly })
895 .add_rank_cost(SkillCost::mana_cost(30.0, 4.0).with_cast_time(1.0))
896 .add_rank_effect(SkillEffect::Heal { base_heal: 90.0, ratio: 1.7, target: HealTarget::SingleAlly })
897 .add_rank_cost(SkillCost::mana_cost(30.0, 3.8).with_cast_time(0.9))
898 .add_rank_effect(SkillEffect::Heal { base_heal: 120.0, ratio: 2.0, target: HealTarget::SingleAlly })
899 .add_rank_cost(SkillCost::mana_cost(32.0, 3.5).with_cast_time(0.8))
900 .add_rank_effect(SkillEffect::Heal { base_heal: 160.0, ratio: 2.3, target: HealTarget::SingleAlly })
901 .add_rank_cost(SkillCost::mana_cost(35.0, 3.3).with_cast_time(0.7))
902 .add_rank_effect(SkillEffect::Heal { base_heal: 200.0, ratio: 2.8, target: HealTarget::SingleAlly })
903 .add_rank_cost(SkillCost::mana_cost(38.0, 3.0).with_cast_time(0.6));
904
905 let renew = Skill::new(SkillId(4002), "Renew", SkillType::Active)
906 .with_description("Apply a regeneration aura to an ally.")
907 .with_icon('R')
908 .with_max_rank(5)
909 .add_rank_effect(SkillEffect::Zone {
910 radius: 0.0,
911 duration_secs: 15.0,
912 tick_interval: 1.0,
913 tick_effect: Box::new(SkillEffect::Heal { base_heal: 10.0, ratio: 0.2, target: HealTarget::SingleAlly }),
914 })
915 .add_rank_cost(SkillCost::mana_cost(20.0, 15.0));
916
917 let mass_heal = Skill::new(SkillId(4003), "Mass Heal", SkillType::Active)
918 .with_description("Heal all allies in range.")
919 .with_icon('H')
920 .with_max_rank(3)
921 .add_requirement(SkillRequirement::Level(15))
922 .add_rank_effect(SkillEffect::Heal { base_heal: 80.0, ratio: 1.2, target: HealTarget::AllAllies })
923 .add_rank_cost(SkillCost::mana_cost(70.0, 30.0).with_cast_time(2.0))
924 .add_rank_effect(SkillEffect::Heal { base_heal: 120.0, ratio: 1.5, target: HealTarget::AllAllies })
925 .add_rank_cost(SkillCost::mana_cost(70.0, 28.0).with_cast_time(1.8))
926 .add_rank_effect(SkillEffect::Heal { base_heal: 180.0, ratio: 2.0, target: HealTarget::AllAllies })
927 .add_rank_cost(SkillCost::mana_cost(75.0, 25.0).with_cast_time(1.5));
928
929 let divine_favor = Skill::new(SkillId(4004), "Divine Favor", SkillType::Passive)
930 .with_description("Increases healing output.")
931 .with_icon('†')
932 .with_max_rank(5)
933 .add_passive(StatModifier::flat("divine_favor", StatKind::Wisdom, 2.0));
934
935 SkillTree::new("Healer")
936 .add_node(SkillNode::new(holy_light, (2, 0)))
937 .add_node(SkillNode::new(renew, (1, 0)))
938 .add_node(SkillNode::new(mass_heal, (2, 1)).with_prereqs(vec![0]))
939 .add_node(SkillNode::new(divine_favor, (3, 0)))
940 .add_connection(0, 2)
941 }
942
943 pub fn summoner_tree() -> SkillTree {
944 let summon_wolf = Skill::new(SkillId(5001), "Summon Wolf", SkillType::Active)
945 .with_description("Summon a wolf companion to fight for you.")
946 .with_icon('W')
947 .with_max_rank(5)
948 .add_rank_effect(SkillEffect::Summon { entity_type: "wolf".to_string(), count: 1, duration_secs: 60.0 })
949 .add_rank_cost(SkillCost::mana_cost(50.0, 30.0).with_cast_time(2.0))
950 .add_rank_effect(SkillEffect::Summon { entity_type: "wolf".to_string(), count: 1, duration_secs: 90.0 })
951 .add_rank_cost(SkillCost::mana_cost(50.0, 28.0).with_cast_time(1.8))
952 .add_rank_effect(SkillEffect::Summon { entity_type: "dire_wolf".to_string(), count: 1, duration_secs: 120.0 })
953 .add_rank_cost(SkillCost::mana_cost(55.0, 25.0).with_cast_time(1.5))
954 .add_rank_effect(SkillEffect::Summon { entity_type: "dire_wolf".to_string(), count: 2, duration_secs: 150.0 })
955 .add_rank_cost(SkillCost::mana_cost(60.0, 23.0).with_cast_time(1.3))
956 .add_rank_effect(SkillEffect::Summon { entity_type: "shadow_wolf".to_string(), count: 2, duration_secs: 180.0 })
957 .add_rank_cost(SkillCost::mana_cost(70.0, 20.0).with_cast_time(1.0));
958
959 let summon_golem = Skill::new(SkillId(5002), "Summon Stone Golem", SkillType::Active)
960 .with_description("Summon a powerful stone golem tank.")
961 .with_icon('G')
962 .with_max_rank(3)
963 .add_requirement(SkillRequirement::Level(10))
964 .add_rank_effect(SkillEffect::Summon { entity_type: "stone_golem".to_string(), count: 1, duration_secs: 120.0 })
965 .add_rank_cost(SkillCost::mana_cost(80.0, 60.0).with_cast_time(3.0))
966 .add_rank_effect(SkillEffect::Summon { entity_type: "iron_golem".to_string(), count: 1, duration_secs: 150.0 })
967 .add_rank_cost(SkillCost::mana_cost(85.0, 55.0).with_cast_time(2.5))
968 .add_rank_effect(SkillEffect::Summon { entity_type: "crystal_golem".to_string(), count: 1, duration_secs: 200.0 })
969 .add_rank_cost(SkillCost::mana_cost(90.0, 50.0).with_cast_time(2.0));
970
971 let bond = Skill::new(SkillId(5003), "Empathic Bond", SkillType::Passive)
972 .with_description("Your summons gain more of your stats.")
973 .with_icon('∞')
974 .with_max_rank(5)
975 .add_passive(StatModifier::flat("bond", StatKind::Charisma, 3.0));
976
977 SkillTree::new("Summoner")
978 .add_node(SkillNode::new(summon_wolf, (1, 0)))
979 .add_node(SkillNode::new(summon_golem, (3, 0)))
980 .add_node(SkillNode::new(bond, (2, 0)))
981 .add_connection(0, 1)
982 }
983
984 pub fn warrior_combos() -> Vec<Combo> {
986 vec![
987 Combo::new(
988 "Unstoppable Force",
989 vec![SkillId(1001), SkillId(1001), SkillId(1002)],
990 SkillEffect::Damage { base_damage: 200.0, ratio: 3.0, element: Element::Physical, aoe_radius: 5.0, pierces: true },
991 2000.0,
992 ),
993 Combo::new(
994 "Warcry Slash",
995 vec![SkillId(1003), SkillId(1001)],
996 SkillEffect::Damage { base_damage: 50.0, ratio: 1.5, element: Element::Physical, aoe_radius: 0.0, pierces: false },
997 1500.0,
998 ),
999 ]
1000 }
1001
1002 pub fn mage_combos() -> Vec<Combo> {
1003 vec![
1004 Combo::new(
1005 "Frozen Inferno",
1006 vec![SkillId(2002), SkillId(2001)],
1007 SkillEffect::Damage { base_damage: 120.0, ratio: 2.5, element: Element::Arcane, aoe_radius: 3.0, pierces: false },
1008 1500.0,
1009 ),
1010 ]
1011 }
1012}
1013
1014#[cfg(test)]
1019mod tests {
1020 use super::*;
1021
1022 #[test]
1023 fn test_skill_id_equality() {
1024 assert_eq!(SkillId(1), SkillId(1));
1025 assert_ne!(SkillId(1), SkillId(2));
1026 }
1027
1028 #[test]
1029 fn test_skill_effect_at_rank() {
1030 let tree = SkillPresets::warrior_tree();
1031 let node = &tree.nodes[0]; let eff = node.skill.effect_at_rank(1);
1033 assert!(eff.is_some());
1034 assert!(eff.unwrap().is_damaging());
1035 }
1036
1037 #[test]
1038 fn test_skill_book_learn() {
1039 let mut book = SkillBook::new();
1040 let skill = Skill::new(SkillId(1), "Test", SkillType::Active);
1041 assert!(book.learn(skill.clone()));
1042 assert!(!book.learn(skill)); }
1044
1045 #[test]
1046 fn test_skill_book_upgrade() {
1047 let mut book = SkillBook::new();
1048 let skill = Skill::new(SkillId(1), "Test", SkillType::Active).with_max_rank(3);
1049 book.learn(skill);
1050 assert!(book.upgrade(SkillId(1)));
1051 assert_eq!(book.rank_of(SkillId(1)), 2);
1052 }
1053
1054 #[test]
1055 fn test_skill_book_max_rank() {
1056 let mut book = SkillBook::new();
1057 let skill = Skill::new(SkillId(1), "Test", SkillType::Active).with_max_rank(1);
1058 book.learn(skill);
1059 assert!(!book.upgrade(SkillId(1))); }
1061
1062 #[test]
1063 fn test_ability_bar_bind_unbind() {
1064 let mut bar = AbilityBar::new();
1065 bar.bind(0, Ability::new(SkillId(1), 0));
1066 assert!(bar.get(0).is_some());
1067 bar.unbind(0);
1068 assert!(bar.get(0).is_none());
1069 }
1070
1071 #[test]
1072 fn test_cooldown_tracker() {
1073 let mut tracker = CooldownTracker::new();
1074 tracker.start(SkillId(1), 5.0);
1075 assert!(!tracker.is_ready(SkillId(1)));
1076 tracker.tick(3.0);
1077 assert!((tracker.remaining(SkillId(1)) - 2.0).abs() < 0.001);
1078 tracker.tick(2.0);
1079 assert!(tracker.is_ready(SkillId(1)));
1080 }
1081
1082 #[test]
1083 fn test_cooldown_tracker_reduce() {
1084 let mut tracker = CooldownTracker::new();
1085 tracker.start(SkillId(1), 10.0);
1086 tracker.reduce(SkillId(1), 5.0);
1087 assert!((tracker.remaining(SkillId(1)) - 5.0).abs() < 0.001);
1088 }
1089
1090 #[test]
1091 fn test_combo_matches() {
1092 let combo = Combo::new(
1093 "Test",
1094 vec![SkillId(1), SkillId(2), SkillId(3)],
1095 SkillEffect::Damage { base_damage: 100.0, ratio: 1.0, element: Element::Physical, aoe_radius: 0.0, pierces: false },
1096 2000.0,
1097 );
1098 let seq = vec![SkillId(5), SkillId(1), SkillId(2), SkillId(3)];
1099 assert!(combo.matches(&seq));
1100 let bad_seq = vec![SkillId(1), SkillId(2)];
1101 assert!(!combo.matches(&bad_seq));
1102 }
1103
1104 #[test]
1105 fn test_combo_system_detects_combo() {
1106 let mut sys = ComboSystem::new();
1107 sys.add_combo(Combo::new(
1108 "Test",
1109 vec![SkillId(1001), SkillId(1002)],
1110 SkillEffect::Damage { base_damage: 50.0, ratio: 1.0, element: Element::Physical, aoe_radius: 0.0, pierces: false },
1111 2000.0,
1112 ));
1113 sys.register_skill_use(SkillId(1001));
1114 sys.register_skill_use(SkillId(1002));
1115 let found = sys.check_combos();
1116 assert_eq!(found.len(), 1);
1117 }
1118
1119 #[test]
1120 fn test_skill_tree_warrior_available() {
1121 let tree = SkillPresets::warrior_tree();
1122 let available = tree.available_nodes();
1123 assert!(!available.is_empty());
1124 }
1125
1126 #[test]
1127 fn test_skill_tree_total_points() {
1128 let tree = SkillPresets::mage_tree();
1129 assert_eq!(tree.total_points_spent(), 0);
1130 }
1131}