Merge branch 'feat/Phase4-Skilltrees' into dev

This commit is contained in:
2025-11-28 22:03:16 -06:00
99 changed files with 3488 additions and 495 deletions

View File

@@ -0,0 +1,34 @@
# Absolute Zero - Arcanist Cryomancy ultimate
# Ultimate freeze all enemies
ability_id: "absolute_zero"
name: "Absolute Zero"
description: "Lower the temperature to absolute zero, freezing all enemies solid and dealing massive ice damage"
ability_type: "spell"
base_power: 90
damage_type: "ice"
scaling_stat: "intelligence"
scaling_factor: 0.7
mana_cost: 70
cooldown: 6
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "absolute_freeze"
name: "Absolute Zero"
effect_type: "stun"
duration: 2
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "absolute_zero"
- effect_id: "shattered"
name: "Shattered"
effect_type: "dot"
duration: 2
power: 20
stat_affected: null
stacks: 1
max_stacks: 1
source: "absolute_zero"

View File

@@ -0,0 +1,16 @@
# Aimed Shot - Wildstrider Marksmanship ability
# High accuracy ranged attack
ability_id: "aimed_shot"
name: "Aimed Shot"
description: "Take careful aim and fire a precise shot at your target"
ability_type: "attack"
base_power: 18
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.6
mana_cost: 8
cooldown: 1
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Arcane Brilliance - Lorekeeper Arcane Weaving ability
# Intelligence buff
ability_id: "arcane_brilliance"
name: "Arcane Brilliance"
description: "Grant an ally increased intelligence and magical power"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "intelligence"
scaling_factor: 0.4
mana_cost: 10
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "arcane_brilliance_buff"
name: "Arcane Brilliance"
effect_type: "buff"
duration: 5
power: 10
stat_affected: "intelligence"
stacks: 1
max_stacks: 1
source: "arcane_brilliance"

View File

@@ -0,0 +1,25 @@
# Arcane Weakness - Lorekeeper Arcane Weaving ability
# Stat debuff on enemy
ability_id: "arcane_weakness"
name: "Arcane Weakness"
description: "Expose the weaknesses in your enemy's defenses, reducing their resistances"
ability_type: "spell"
base_power: 0
damage_type: "arcane"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 25
cooldown: 3
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "weakened_defenses"
name: "Weakened"
effect_type: "debuff"
duration: 4
power: 25
stat_affected: null
stacks: 1
max_stacks: 1
source: "arcane_weakness"

View File

@@ -0,0 +1,25 @@
# Army of the Dead - Necromancer Raise Dead ultimate
# Summon undead army
ability_id: "army_of_the_dead"
name: "Army of the Dead"
description: "Raise an entire army of undead to overwhelm your enemies"
ability_type: "spell"
base_power: 80
damage_type: "shadow"
scaling_stat: "charisma"
scaling_factor: 0.7
mana_cost: 70
cooldown: 8
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "undead_army"
name: "Army of the Dead"
effect_type: "buff"
duration: 5
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "army_of_the_dead"

View File

@@ -0,0 +1,25 @@
# Bestial Wrath - Wildstrider Beast Companion ability
# Pet damage buff
ability_id: "bestial_wrath"
name: "Bestial Wrath"
description: "Enrage your companion, increasing their damage for 3 turns"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.4
mana_cost: 25
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "enraged_companion"
name: "Enraged Companion"
effect_type: "buff"
duration: 3
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "bestial_wrath"

View File

@@ -0,0 +1,16 @@
# Blessed Sacrifice - Oathkeeper Redemption ability
# Transfer ally wounds to self
ability_id: "blessed_sacrifice"
name: "Blessed Sacrifice"
description: "Take an ally's wounds upon yourself, healing them while damaging yourself"
ability_type: "spell"
base_power: 50
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 25
cooldown: 4
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Blizzard - Arcanist Cryomancy ability
# AoE ice damage with slow
ability_id: "blizzard"
name: "Blizzard"
description: "Summon a devastating blizzard that damages and slows all enemies"
ability_type: "spell"
base_power: 40
damage_type: "ice"
scaling_stat: "intelligence"
scaling_factor: 0.55
mana_cost: 32
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "frostbitten"
name: "Frostbitten"
effect_type: "debuff"
duration: 3
power: 30
stat_affected: null
stacks: 1
max_stacks: 1
source: "blizzard"

View File

@@ -0,0 +1,16 @@
# Cleanse - Oathkeeper Redemption ability
# Remove all debuffs
ability_id: "cleanse"
name: "Cleanse"
description: "Purify an ally, removing all negative effects"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.3
mana_cost: 18
cooldown: 3
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Cleave - Vanguard Weapon Master ability
# AoE attack hitting all enemies
ability_id: "cleave"
name: "Cleave"
description: "Swing your weapon in a wide arc, hitting all enemies"
ability_type: "attack"
base_power: 20
damage_type: "physical"
scaling_stat: "strength"
scaling_factor: 0.5
mana_cost: 15
cooldown: 2
is_aoe: true
target_count: 0
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Confuse - Lorekeeper Illusionist ability
# Random target attacks
ability_id: "confuse"
name: "Confuse"
description: "Confuse your enemy, causing them to attack random targets"
ability_type: "spell"
base_power: 0
damage_type: "arcane"
scaling_stat: "charisma"
scaling_factor: 0.5
mana_cost: 12
cooldown: 2
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "confused"
name: "Confused"
effect_type: "debuff"
duration: 2
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "confuse"

View File

@@ -0,0 +1,25 @@
# Consecrated Ground - Oathkeeper Aegis of Light ability
# Ground buff with damage reduction zone
ability_id: "consecrated_ground"
name: "Consecrated Ground"
description: "Consecrate the ground, creating a zone that reduces damage taken by all allies standing within"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.4
mana_cost: 30
cooldown: 4
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "consecrated_protection"
name: "Consecrated"
effect_type: "buff"
duration: 3
power: 25
stat_affected: null
stacks: 1
max_stacks: 1
source: "consecrated_ground"

View File

@@ -0,0 +1,25 @@
# Consecration - Luminary Radiant Judgment ability
# Ground AoE holy damage
ability_id: "consecration"
name: "Consecration"
description: "Consecrate the ground beneath your feet, dealing holy damage to all nearby enemies"
ability_type: "spell"
base_power: 40
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.55
mana_cost: 28
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "consecrated_ground"
name: "Consecrated"
effect_type: "dot"
duration: 3
power: 10
stat_affected: null
stacks: 1
max_stacks: 1
source: "consecration"

View File

@@ -0,0 +1,16 @@
# Coordinated Attack - Wildstrider Beast Companion ability
# Attack with pet
ability_id: "coordinated_attack"
name: "Coordinated Attack"
description: "Attack in perfect coordination with your companion for bonus damage"
ability_type: "skill"
base_power: 30
damage_type: "physical"
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 18
cooldown: 2
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Corpse Explosion - Necromancer Raise Dead ability
# Detonate corpse/minion AoE
ability_id: "corpse_explosion"
name: "Corpse Explosion"
description: "Detonate a corpse or minion, dealing AoE shadow damage to all nearby enemies"
ability_type: "spell"
base_power: 45
damage_type: "shadow"
scaling_stat: "intelligence"
scaling_factor: 0.55
mana_cost: 28
cooldown: 3
is_aoe: true
target_count: 0
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Coup de Grace - Assassin Blade Specialist ability
# Execute low HP targets
ability_id: "coup_de_grace"
name: "Coup de Grace"
description: "Deliver the killing blow. Instantly kills targets below 25% HP, otherwise deals massive damage"
ability_type: "attack"
base_power: 70
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.6
mana_cost: 40
cooldown: 4
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Curse of Agony - Necromancer Dark Affliction ability
# Heavy shadow DoT
ability_id: "curse_of_agony"
name: "Curse of Agony"
description: "Curse your target with unbearable agony, dealing increasing shadow damage over 5 turns"
ability_type: "spell"
base_power: 10
damage_type: "shadow"
scaling_stat: "intelligence"
scaling_factor: 0.55
mana_cost: 28
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "agony"
name: "Curse of Agony"
effect_type: "dot"
duration: 5
power: 12
stat_affected: null
stacks: 1
max_stacks: 1
source: "curse_of_agony"

View File

@@ -0,0 +1,25 @@
# Death Mark - Assassin Shadow Dancer ability
# Mark target for bonus damage
ability_id: "death_mark"
name: "Death Mark"
description: "Mark your target for death. Your next attack deals 200% damage"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "dexterity"
scaling_factor: 0.0
mana_cost: 30
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "marked_for_death"
name: "Marked for Death"
effect_type: "debuff"
duration: 2
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "death_mark"

View File

@@ -0,0 +1,25 @@
# Death Pact - Necromancer Raise Dead ability
# Sacrifice minion for HP/mana
ability_id: "death_pact"
name: "Death Pact"
description: "Sacrifice one of your minions to restore your health and mana"
ability_type: "spell"
base_power: 50
damage_type: null
scaling_stat: "charisma"
scaling_factor: 0.5
mana_cost: 0
cooldown: 5
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "death_pact_heal"
name: "Death Pact"
effect_type: "hot"
duration: 1
power: 40
stat_affected: null
stacks: 1
max_stacks: 1
source: "death_pact"

View File

@@ -0,0 +1,25 @@
# Divine Aegis - Oathkeeper Aegis of Light ability
# Massive party shield
ability_id: "divine_aegis"
name: "Divine Aegis"
description: "Invoke divine protection to create a powerful shield around all allies"
ability_type: "spell"
base_power: 60
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.6
mana_cost: 45
cooldown: 5
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "divine_aegis_shield"
name: "Divine Aegis"
effect_type: "shield"
duration: 3
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "divine_aegis"

View File

@@ -0,0 +1,34 @@
# Divine Blessing - Oathkeeper Redemption ability
# Stat buff + HoT
ability_id: "divine_blessing"
name: "Divine Blessing"
description: "Bless an ally with divine power, increasing their stats and healing over time"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 35
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "blessed"
name: "Divine Blessing"
effect_type: "buff"
duration: 4
power: 15
stat_affected: null
stacks: 1
max_stacks: 1
source: "divine_blessing"
- effect_id: "blessed_healing"
name: "Blessed Healing"
effect_type: "hot"
duration: 4
power: 10
stat_affected: null
stacks: 1
max_stacks: 1
source: "divine_blessing"

View File

@@ -0,0 +1,16 @@
# Divine Intervention - Luminary Divine Protection ability
# Full heal + cleanse
ability_id: "divine_intervention"
name: "Divine Intervention"
description: "Call upon divine power to fully heal and cleanse an ally of all negative effects"
ability_type: "spell"
base_power: 80
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.6
mana_cost: 45
cooldown: 5
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Divine Storm - Luminary Radiant Judgment ultimate
# Ultimate AoE holy + stun all
ability_id: "divine_storm"
name: "Divine Storm"
description: "Unleash the full fury of the divine, dealing massive holy damage to all enemies and stunning them"
ability_type: "spell"
base_power: 95
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.7
mana_cost: 60
cooldown: 5
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "divine_judgment"
name: "Divine Judgment"
effect_type: "stun"
duration: 1
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "divine_storm"

View File

@@ -0,0 +1,25 @@
# Drain Life - Necromancer Dark Affliction ability
# Shadow damage + self-heal
ability_id: "drain_life"
name: "Drain Life"
description: "Drain the life force from your enemy, dealing shadow damage and healing yourself"
ability_type: "spell"
base_power: 18
damage_type: "shadow"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 12
cooldown: 1
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "life_drain"
name: "Life Drained"
effect_type: "hot"
duration: 1
power: 9
stat_affected: null
stacks: 1
max_stacks: 1
source: "drain_life"

View File

@@ -0,0 +1,34 @@
# Epidemic - Necromancer Dark Affliction ultimate
# Ultimate multi-DoT all enemies
ability_id: "epidemic"
name: "Epidemic"
description: "Unleash a devastating epidemic that afflicts all enemies with multiple diseases"
ability_type: "spell"
base_power: 60
damage_type: "shadow"
scaling_stat: "intelligence"
scaling_factor: 0.7
mana_cost: 60
cooldown: 6
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "epidemic_plague"
name: "Epidemic"
effect_type: "dot"
duration: 5
power: 20
stat_affected: null
stacks: 1
max_stacks: 1
source: "epidemic"
- effect_id: "weakened"
name: "Weakened"
effect_type: "debuff"
duration: 5
power: 25
stat_affected: null
stacks: 1
max_stacks: 1
source: "epidemic"

View File

@@ -0,0 +1,16 @@
# Execute - Vanguard Weapon Master ability
# Bonus damage to low HP targets
ability_id: "execute"
name: "Execute"
description: "Finish off weakened enemies. Deals bonus damage to targets below 30% HP"
ability_type: "attack"
base_power: 60
damage_type: "physical"
scaling_stat: "strength"
scaling_factor: 0.6
mana_cost: 40
cooldown: 3
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Explosive Shot - Wildstrider Marksmanship ability
# Impact AoE damage
ability_id: "explosive_shot"
name: "Explosive Shot"
description: "Fire an explosive arrow that detonates on impact, dealing AoE damage"
ability_type: "attack"
base_power: 55
damage_type: "fire"
scaling_stat: "dexterity"
scaling_factor: 0.55
mana_cost: 38
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "burning_shrapnel"
name: "Burning Shrapnel"
effect_type: "dot"
duration: 2
power: 8
stat_affected: null
stacks: 1
max_stacks: 1
source: "explosive_shot"

View File

@@ -0,0 +1,25 @@
# Firestorm - Arcanist Pyromancy ability
# Massive AoE fire damage
ability_id: "firestorm"
name: "Firestorm"
description: "Call down a storm of fire from the heavens, devastating all enemies"
ability_type: "spell"
base_power: 55
damage_type: "fire"
scaling_stat: "intelligence"
scaling_factor: 0.6
mana_cost: 45
cooldown: 4
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "scorched"
name: "Scorched"
effect_type: "dot"
duration: 2
power: 12
stat_affected: null
stacks: 1
max_stacks: 3
source: "firestorm"

View File

@@ -0,0 +1,16 @@
# Flame Burst - Arcanist Pyromancy ability
# AoE fire burst centered on caster
ability_id: "flame_burst"
name: "Flame Burst"
description: "Release a burst of flames around you, scorching all nearby enemies"
ability_type: "spell"
base_power: 25
damage_type: "fire"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 18
cooldown: 2
is_aoe: true
target_count: 0
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Frozen Orb - Arcanist Cryomancy ability
# AoE freeze with damage
ability_id: "frozen_orb"
name: "Frozen Orb"
description: "Launch a swirling orb of frost that freezes enemies in its path"
ability_type: "spell"
base_power: 28
damage_type: "ice"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 20
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "frozen"
name: "Frozen"
effect_type: "stun"
duration: 1
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "frozen_orb"

View File

@@ -0,0 +1,25 @@
# Glacial Spike - Arcanist Cryomancy ability
# Heavy single target with freeze
ability_id: "glacial_spike"
name: "Glacial Spike"
description: "Impale your target with a massive spike of ice, dealing heavy damage and freezing them"
ability_type: "spell"
base_power: 60
damage_type: "ice"
scaling_stat: "intelligence"
scaling_factor: 0.6
mana_cost: 40
cooldown: 3
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "deep_freeze"
name: "Deep Freeze"
effect_type: "stun"
duration: 2
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "glacial_spike"

View File

@@ -0,0 +1,25 @@
# Guardian Angel - Luminary Divine Protection ability
# Death prevention buff
ability_id: "guardian_angel"
name: "Guardian Angel"
description: "Bless an ally with divine protection that prevents death once"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.4
mana_cost: 35
cooldown: 6
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "guardian_angel_buff"
name: "Guardian Angel"
effect_type: "buff"
duration: 5
power: 1
stat_affected: null
stacks: 1
max_stacks: 1
source: "guardian_angel"

View File

@@ -0,0 +1,25 @@
# Hammer of Justice - Luminary Radiant Judgment ability
# Holy damage + stun
ability_id: "hammer_of_justice"
name: "Hammer of Justice"
description: "Smash your enemy with a divine hammer, dealing holy damage and stunning them"
ability_type: "spell"
base_power: 55
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.6
mana_cost: 38
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "justice_stun"
name: "Judged"
effect_type: "stun"
duration: 2
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "hammer_of_justice"

View File

@@ -0,0 +1,25 @@
# Haste - Lorekeeper Arcane Weaving ability
# Grant extra action
ability_id: "haste"
name: "Haste"
description: "Speed up time around an ally, granting them an extra action"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "intelligence"
scaling_factor: 0.4
mana_cost: 20
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "hasted"
name: "Hasted"
effect_type: "buff"
duration: 2
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "haste"

View File

@@ -7,7 +7,7 @@ description: "Channel divine energy to restore an ally's health"
ability_type: "spell" ability_type: "spell"
base_power: 25 base_power: 25
damage_type: "holy" damage_type: "holy"
scaling_stat: "intelligence" scaling_stat: "wisdom"
scaling_factor: 0.5 scaling_factor: 0.5
mana_cost: 10 mana_cost: 10
cooldown: 0 cooldown: 0

View File

@@ -0,0 +1,25 @@
# Holy Fire - Luminary Radiant Judgment ability
# Holy DoT with reduced healing
ability_id: "holy_fire"
name: "Holy Fire"
description: "Engulf your enemy in holy flames that burn over time and reduce their healing"
ability_type: "spell"
base_power: 25
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 18
cooldown: 2
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "holy_burning"
name: "Holy Fire"
effect_type: "dot"
duration: 3
power: 8
stat_affected: null
stacks: 1
max_stacks: 1
source: "holy_fire"

View File

@@ -0,0 +1,25 @@
# Holy Shield - Luminary Divine Protection ability
# Grant damage absorb shield
ability_id: "holy_shield"
name: "Holy Shield"
description: "Grant an ally a protective barrier of holy light that absorbs damage"
ability_type: "spell"
base_power: 30
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 15
cooldown: 2
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "holy_shield_barrier"
name: "Holy Shield"
effect_type: "shield"
duration: 3
power: 30
stat_affected: null
stacks: 1
max_stacks: 1
source: "holy_shield"

View File

@@ -0,0 +1,25 @@
# Ice Shard - Arcanist Cryomancy ability
# Single target ice damage with slow
ability_id: "ice_shard"
name: "Ice Shard"
description: "Hurl a shard of ice at your enemy, dealing frost damage and slowing them"
ability_type: "spell"
base_power: 20
damage_type: "ice"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 10
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "chilled"
name: "Chilled"
effect_type: "debuff"
duration: 2
power: 20
stat_affected: null
stacks: 1
max_stacks: 3
source: "ice_shard"

View File

@@ -0,0 +1,25 @@
# Inferno - Arcanist Pyromancy ability
# AoE fire DoT
ability_id: "inferno"
name: "Inferno"
description: "Summon a raging inferno that burns all enemies for 3 turns"
ability_type: "spell"
base_power: 35
damage_type: "fire"
scaling_stat: "intelligence"
scaling_factor: 0.55
mana_cost: 30
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "inferno_burn"
name: "Inferno Flames"
effect_type: "dot"
duration: 3
power: 10
stat_affected: null
stacks: 1
max_stacks: 3
source: "inferno"

View File

@@ -0,0 +1,34 @@
# Last Stand - Oathkeeper Aegis of Light ultimate
# Invulnerable + taunt all
ability_id: "last_stand"
name: "Last Stand"
description: "Make your final stand, becoming invulnerable and forcing all enemies to attack you"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "constitution"
scaling_factor: 0.5
mana_cost: 55
cooldown: 8
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "invulnerable"
name: "Invulnerable"
effect_type: "buff"
duration: 3
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "last_stand"
- effect_id: "ultimate_taunt"
name: "Challenged"
effect_type: "debuff"
duration: 3
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "last_stand"

View File

@@ -0,0 +1,25 @@
# Lay on Hands - Oathkeeper Redemption ability
# Touch heal
ability_id: "lay_on_hands"
name: "Lay on Hands"
description: "Place your hands upon an ally to heal their wounds"
ability_type: "spell"
base_power: 25
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 12
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "gentle_healing"
name: "Soothed"
effect_type: "hot"
duration: 2
power: 5
stat_affected: null
stacks: 1
max_stacks: 1
source: "lay_on_hands"

View File

@@ -0,0 +1,25 @@
# Mass Confusion - Lorekeeper Illusionist ability
# AoE confusion
ability_id: "mass_confusion"
name: "Mass Confusion"
description: "Unleash a wave of illusions that confuses all enemies"
ability_type: "spell"
base_power: 0
damage_type: "arcane"
scaling_stat: "charisma"
scaling_factor: 0.55
mana_cost: 35
cooldown: 4
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "mass_confused"
name: "Bewildered"
effect_type: "debuff"
duration: 3
power: 40
stat_affected: null
stacks: 1
max_stacks: 1
source: "mass_confusion"

View File

@@ -0,0 +1,25 @@
# Mass Domination - Lorekeeper Illusionist ultimate
# Mind control all enemies
ability_id: "mass_domination"
name: "Mass Domination"
description: "Dominate the minds of all enemies, forcing them to attack each other"
ability_type: "spell"
base_power: 0
damage_type: "arcane"
scaling_stat: "charisma"
scaling_factor: 0.7
mana_cost: 75
cooldown: 8
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "dominated"
name: "Dominated"
effect_type: "debuff"
duration: 3
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "mass_domination"

View File

@@ -0,0 +1,25 @@
# Mass Enhancement - Lorekeeper Arcane Weaving ability
# AoE stat buff
ability_id: "mass_enhancement"
name: "Mass Enhancement"
description: "Enhance all allies with arcane power, increasing all their stats"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 32
cooldown: 4
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "enhanced"
name: "Enhanced"
effect_type: "buff"
duration: 4
power: 15
stat_affected: null
stacks: 1
max_stacks: 1
source: "mass_enhancement"

View File

@@ -0,0 +1,25 @@
# Mass Heal - Luminary Divine Protection ability
# AoE healing
ability_id: "mass_heal"
name: "Mass Heal"
description: "Channel divine energy to heal all allies"
ability_type: "spell"
base_power: 35
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.55
mana_cost: 30
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "mass_regen"
name: "Divine Healing"
effect_type: "hot"
duration: 2
power: 8
stat_affected: null
stacks: 1
max_stacks: 1
source: "mass_heal"

View File

@@ -0,0 +1,25 @@
# Mesmerize - Lorekeeper Illusionist ability
# Stun for 2 turns
ability_id: "mesmerize"
name: "Mesmerize"
description: "Mesmerize your target with illusions, stunning them for 2 turns"
ability_type: "spell"
base_power: 0
damage_type: "arcane"
scaling_stat: "charisma"
scaling_factor: 0.5
mana_cost: 22
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "mesmerized"
name: "Mesmerized"
effect_type: "stun"
duration: 2
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "mesmerize"

View File

@@ -0,0 +1,25 @@
# Miracle - Oathkeeper Redemption ultimate
# Full party heal + cleanse all
ability_id: "miracle"
name: "Miracle"
description: "Perform a divine miracle that fully heals all allies and cleanses all negative effects"
ability_type: "spell"
base_power: 100
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.7
mana_cost: 70
cooldown: 8
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "miraculous_healing"
name: "Miraculous"
effect_type: "hot"
duration: 3
power: 20
stat_affected: null
stacks: 1
max_stacks: 1
source: "miracle"

View File

@@ -0,0 +1,25 @@
# Mirror Image - Lorekeeper Illusionist ability
# Summon decoys
ability_id: "mirror_image"
name: "Mirror Image"
description: "Create illusory copies of yourself that absorb enemy attacks"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "charisma"
scaling_factor: 0.5
mana_cost: 28
cooldown: 5
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "mirror_images"
name: "Mirror Images"
effect_type: "shield"
duration: 4
power: 40
stat_affected: null
stacks: 3
max_stacks: 3
source: "mirror_image"

View File

@@ -0,0 +1,16 @@
# Multishot - Wildstrider Marksmanship ability
# Hit multiple targets
ability_id: "multishot"
name: "Multishot"
description: "Fire multiple arrows in quick succession, hitting up to 3 targets"
ability_type: "attack"
base_power: 22
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.5
mana_cost: 18
cooldown: 2
is_aoe: true
target_count: 3
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Phantasmal Killer - Lorekeeper Illusionist ability
# Psychic damage + fear
ability_id: "phantasmal_killer"
name: "Phantasmal Killer"
description: "Conjure a nightmarish illusion that terrifies and damages your target"
ability_type: "spell"
base_power: 55
damage_type: "arcane"
scaling_stat: "charisma"
scaling_factor: 0.6
mana_cost: 42
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "terrified"
name: "Terrified"
effect_type: "debuff"
duration: 3
power: 30
stat_affected: null
stacks: 1
max_stacks: 1
source: "phantasmal_killer"

View File

@@ -0,0 +1,25 @@
# Piercing Shot - Wildstrider Marksmanship ability
# Line AoE that pierces through enemies
ability_id: "piercing_shot"
name: "Piercing Shot"
description: "Fire a powerful arrow that pierces through all enemies in a line"
ability_type: "attack"
base_power: 40
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.55
mana_cost: 28
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "armor_pierced"
name: "Armor Pierced"
effect_type: "debuff"
duration: 2
power: 15
stat_affected: null
stacks: 1
max_stacks: 1
source: "piercing_shot"

View File

@@ -0,0 +1,25 @@
# Plague - Necromancer Dark Affliction ability
# Spreading poison DoT
ability_id: "plague"
name: "Plague"
description: "Infect your target with a virulent plague that spreads to nearby enemies"
ability_type: "spell"
base_power: 15
damage_type: "poison"
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 20
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "plagued"
name: "Plagued"
effect_type: "dot"
duration: 4
power: 8
stat_affected: null
stacks: 1
max_stacks: 3
source: "plague"

View File

@@ -0,0 +1,16 @@
# Power Strike - Vanguard Weapon Master ability
# Heavy attack dealing 150% weapon damage
ability_id: "power_strike"
name: "Power Strike"
description: "A heavy attack that deals 150% weapon damage"
ability_type: "attack"
base_power: 15
damage_type: "physical"
scaling_stat: "strength"
scaling_factor: 0.6
mana_cost: 8
cooldown: 1
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Precise Strike - Assassin Blade Specialist ability
# High crit chance attack
ability_id: "precise_strike"
name: "Precise Strike"
description: "A calculated strike aimed at vital points with increased critical chance"
ability_type: "attack"
base_power: 15
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.5
mana_cost: 8
cooldown: 1
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Primal Fury - Wildstrider Beast Companion ability
# Pet AoE attack
ability_id: "primal_fury"
name: "Primal Fury"
description: "Command your companion to unleash a devastating attack on all enemies"
ability_type: "skill"
base_power: 50
damage_type: "physical"
scaling_stat: "wisdom"
scaling_factor: 0.55
mana_cost: 35
cooldown: 4
is_aoe: true
target_count: 0
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Rain of Arrows - Wildstrider Marksmanship ultimate
# Ultimate AoE attack
ability_id: "rain_of_arrows"
name: "Rain of Arrows"
description: "Call down a devastating rain of arrows upon all enemies"
ability_type: "attack"
base_power: 85
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.7
mana_cost: 55
cooldown: 5
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "pinned"
name: "Pinned"
effect_type: "debuff"
duration: 1
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "rain_of_arrows"

View File

@@ -0,0 +1,25 @@
# Raise Ghoul - Necromancer Raise Dead ability
# Summon stronger ghoul
ability_id: "raise_ghoul"
name: "Raise Ghoul"
description: "Raise a powerful ghoul from the dead to serve you"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "charisma"
scaling_factor: 0.55
mana_cost: 22
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "ghoul_minion"
name: "Ghoul"
effect_type: "buff"
duration: 99
power: 35
stat_affected: null
stacks: 1
max_stacks: 1
source: "raise_ghoul"

View File

@@ -0,0 +1,34 @@
# Reality Shift - Lorekeeper Arcane Weaving ultimate
# Massive buff allies + debuff enemies
ability_id: "reality_shift"
name: "Reality Shift"
description: "Alter reality itself, greatly empowering allies while weakening all enemies"
ability_type: "spell"
base_power: 0
damage_type: "arcane"
scaling_stat: "intelligence"
scaling_factor: 0.7
mana_cost: 70
cooldown: 8
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "reality_empowered"
name: "Reality Empowered"
effect_type: "buff"
duration: 5
power: 30
stat_affected: null
stacks: 1
max_stacks: 1
source: "reality_shift"
- effect_id: "reality_weakened"
name: "Reality Distorted"
effect_type: "debuff"
duration: 5
power: 30
stat_affected: null
stacks: 1
max_stacks: 1
source: "reality_shift"

View File

@@ -0,0 +1,25 @@
# Rending Blow - Vanguard Weapon Master ability
# Attack with bleed DoT
ability_id: "rending_blow"
name: "Rending Blow"
description: "Strike with such force that your enemy bleeds for 3 turns"
ability_type: "attack"
base_power: 35
damage_type: "physical"
scaling_stat: "strength"
scaling_factor: 0.5
mana_cost: 25
cooldown: 2
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "bleed"
name: "Bleeding"
effect_type: "dot"
duration: 3
power: 8
stat_affected: null
stacks: 1
max_stacks: 3
source: "rending_blow"

View File

@@ -0,0 +1,16 @@
# Resurrection - Luminary Divine Protection ultimate
# Revive fallen ally
ability_id: "resurrection"
name: "Resurrection"
description: "Call upon the divine to bring a fallen ally back to life with 50% HP and mana"
ability_type: "spell"
base_power: 50
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 60
cooldown: 8
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Riposte - Vanguard Shield Bearer ability
# Counter attack after blocking
ability_id: "riposte"
name: "Riposte"
description: "After blocking an attack, counter with a swift strike"
ability_type: "skill"
base_power: 30
damage_type: "physical"
scaling_stat: "strength"
scaling_factor: 0.5
mana_cost: 20
cooldown: 2
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Shadow Assault - Assassin Shadow Dancer ultimate
# AoE guaranteed crits
ability_id: "shadow_assault"
name: "Shadow Assault"
description: "Become one with the shadows and strike all enemies with guaranteed critical hits"
ability_type: "skill"
base_power: 80
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.7
mana_cost: 55
cooldown: 6
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "shadow_crit"
name: "Shadow Strike"
effect_type: "buff"
duration: 1
power: 100
stat_affected: "crit_chance"
stacks: 1
max_stacks: 1
source: "shadow_assault"

View File

@@ -0,0 +1,16 @@
# Shadowstep - Assassin Shadow Dancer ability
# Teleport and backstab
ability_id: "shadowstep"
name: "Shadowstep"
description: "Vanish into the shadows and reappear behind your target, striking from behind"
ability_type: "skill"
base_power: 18
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.6
mana_cost: 10
cooldown: 2
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Shield of Faith - Oathkeeper Aegis of Light ability
# Shield for self and allies
ability_id: "shield_of_faith"
name: "Shield of Faith"
description: "Create a shield of divine faith that protects you and nearby allies"
ability_type: "spell"
base_power: 35
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 20
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "faith_shield"
name: "Shield of Faith"
effect_type: "shield"
duration: 3
power: 25
stat_affected: null
stacks: 1
max_stacks: 1
source: "shield_of_faith"

View File

@@ -0,0 +1,25 @@
# Shield Wall - Vanguard Shield Bearer ability
# Defensive buff reducing damage
ability_id: "shield_wall"
name: "Shield Wall"
description: "Raise your shield to block incoming attacks, reducing damage by 50% for 3 turns"
ability_type: "defend"
base_power: 0
damage_type: null
scaling_stat: "constitution"
scaling_factor: 0.3
mana_cost: 12
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "shield_wall_buff"
name: "Shield Wall"
effect_type: "buff"
duration: 3
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "shield_wall"

View File

@@ -0,0 +1,16 @@
# Smite - Luminary Radiant Judgment ability
# Holy damage attack
ability_id: "smite"
name: "Smite"
description: "Call down holy light to smite your enemies"
ability_type: "spell"
base_power: 20
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 10
cooldown: 0
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,25 @@
# Smoke Bomb - Assassin Shadow Dancer ability
# Evasion buff
ability_id: "smoke_bomb"
name: "Smoke Bomb"
description: "Throw a smoke bomb, making yourself untargetable for 1 turn"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "dexterity"
scaling_factor: 0.3
mana_cost: 15
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "smoke_screen"
name: "Smoke Screen"
effect_type: "buff"
duration: 1
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "smoke_bomb"

View File

@@ -0,0 +1,34 @@
# Soul Rot - Necromancer Dark Affliction ability
# DoT + reduced healing on target
ability_id: "soul_rot"
name: "Soul Rot"
description: "Rot your target's soul, dealing shadow damage over time and reducing their healing received"
ability_type: "spell"
base_power: 45
damage_type: "shadow"
scaling_stat: "intelligence"
scaling_factor: 0.6
mana_cost: 38
cooldown: 4
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "rotting_soul"
name: "Soul Rot"
effect_type: "dot"
duration: 4
power: 15
stat_affected: null
stacks: 1
max_stacks: 1
source: "soul_rot"
- effect_id: "healing_reduction"
name: "Corrupted"
effect_type: "debuff"
duration: 4
power: 50
stat_affected: null
stacks: 1
max_stacks: 1
source: "soul_rot"

View File

@@ -0,0 +1,25 @@
# Stampede - Wildstrider Beast Companion ultimate
# Summon beast horde AoE
ability_id: "stampede"
name: "Stampede"
description: "Call upon the spirits of the wild to summon a stampede of beasts that tramples all enemies"
ability_type: "skill"
base_power: 90
damage_type: "physical"
scaling_stat: "wisdom"
scaling_factor: 0.7
mana_cost: 60
cooldown: 6
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "trampled"
name: "Trampled"
effect_type: "debuff"
duration: 2
power: 30
stat_affected: null
stacks: 1
max_stacks: 1
source: "stampede"

View File

@@ -0,0 +1,25 @@
# Summon Abomination - Necromancer Raise Dead ability
# Summon powerful abomination
ability_id: "summon_abomination"
name: "Summon Abomination"
description: "Stitch together corpses to create a powerful abomination"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "charisma"
scaling_factor: 0.6
mana_cost: 45
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "abomination_minion"
name: "Abomination"
effect_type: "buff"
duration: 99
power: 60
stat_affected: null
stacks: 1
max_stacks: 1
source: "summon_abomination"

View File

@@ -0,0 +1,25 @@
# Summon Companion - Wildstrider Beast Companion ability
# Summon animal pet
ability_id: "summon_companion"
name: "Summon Companion"
description: "Call your loyal animal companion to fight by your side"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "wisdom"
scaling_factor: 0.5
mana_cost: 15
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "companion_active"
name: "Animal Companion"
effect_type: "buff"
duration: 99
power: 20
stat_affected: null
stacks: 1
max_stacks: 1
source: "summon_companion"

View File

@@ -0,0 +1,25 @@
# Summon Skeleton - Necromancer Raise Dead ability
# Summon skeleton warrior
ability_id: "summon_skeleton"
name: "Summon Skeleton"
description: "Raise a skeleton warrior from the dead to fight for you"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "charisma"
scaling_factor: 0.5
mana_cost: 15
cooldown: 0
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "skeleton_minion"
name: "Skeleton Warrior"
effect_type: "buff"
duration: 99
power: 20
stat_affected: null
stacks: 1
max_stacks: 1
source: "summon_skeleton"

View File

@@ -0,0 +1,25 @@
# Sun Burst - Arcanist Pyromancy ultimate
# Ultimate fire nuke
ability_id: "sun_burst"
name: "Sun Burst"
description: "Channel the power of the sun to unleash a devastating explosion of fire on all enemies"
ability_type: "spell"
base_power: 100
damage_type: "fire"
scaling_stat: "intelligence"
scaling_factor: 0.7
mana_cost: 65
cooldown: 5
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "incinerated"
name: "Incinerated"
effect_type: "dot"
duration: 3
power: 15
stat_affected: null
stacks: 1
max_stacks: 1
source: "sun_burst"

View File

@@ -0,0 +1,25 @@
# Taunt - Oathkeeper Aegis of Light ability
# Force enemies to attack you
ability_id: "taunt"
name: "Taunt"
description: "Force all enemies to focus their attacks on you"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "constitution"
scaling_factor: 0.3
mana_cost: 8
cooldown: 3
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "taunted"
name: "Taunted"
effect_type: "debuff"
duration: 2
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "taunt"

View File

@@ -0,0 +1,25 @@
# Thousand Cuts - Assassin Blade Specialist ultimate
# Multi-hit flurry
ability_id: "thousand_cuts"
name: "Thousand Cuts"
description: "Unleash a flurry of strikes, each with 50% crit chance"
ability_type: "attack"
base_power: 100
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.7
mana_cost: 60
cooldown: 5
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "bleeding_wounds"
name: "Bleeding Wounds"
effect_type: "dot"
duration: 3
power: 15
stat_affected: null
stacks: 1
max_stacks: 5
source: "thousand_cuts"

View File

@@ -0,0 +1,25 @@
# Time Warp - Lorekeeper Arcane Weaving ability
# AoE extra actions
ability_id: "time_warp"
name: "Time Warp"
description: "Bend time itself, granting all allies increased speed"
ability_type: "spell"
base_power: 0
damage_type: null
scaling_stat: "intelligence"
scaling_factor: 0.5
mana_cost: 45
cooldown: 5
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "time_warped"
name: "Time Warped"
effect_type: "buff"
duration: 3
power: 75
stat_affected: null
stacks: 1
max_stacks: 1
source: "time_warp"

View File

@@ -0,0 +1,25 @@
# Titan's Wrath - Vanguard Weapon Master ultimate
# Devastating AoE attack with stun
ability_id: "titans_wrath"
name: "Titan's Wrath"
description: "Unleash a devastating attack that deals 300% weapon damage and stuns all enemies"
ability_type: "attack"
base_power: 100
damage_type: "physical"
scaling_stat: "strength"
scaling_factor: 0.7
mana_cost: 60
cooldown: 5
is_aoe: true
target_count: 0
effects_applied:
- effect_id: "titans_stun"
name: "Staggered"
effect_type: "stun"
duration: 1
power: 0
stat_affected: null
stacks: 1
max_stacks: 1
source: "titans_wrath"

View File

@@ -0,0 +1,25 @@
# Unbreakable - Vanguard Shield Bearer ultimate
# Massive damage reduction
ability_id: "unbreakable"
name: "Unbreakable"
description: "Channel your inner strength to become nearly invulnerable, reducing all damage by 75% for 5 turns"
ability_type: "defend"
base_power: 0
damage_type: null
scaling_stat: "constitution"
scaling_factor: 0.3
mana_cost: 50
cooldown: 6
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "unbreakable_buff"
name: "Unbreakable"
effect_type: "buff"
duration: 5
power: 75
stat_affected: null
stacks: 1
max_stacks: 1
source: "unbreakable"

View File

@@ -0,0 +1,25 @@
# Vanish - Assassin Shadow Dancer ability
# Stealth for 2 turns
ability_id: "vanish"
name: "Vanish"
description: "Disappear into the shadows, becoming invisible for 2 turns and dropping threat"
ability_type: "skill"
base_power: 0
damage_type: null
scaling_stat: "dexterity"
scaling_factor: 0.3
mana_cost: 25
cooldown: 5
is_aoe: false
target_count: 1
effects_applied:
- effect_id: "stealth"
name: "Stealthed"
effect_type: "buff"
duration: 2
power: 100
stat_affected: null
stacks: 1
max_stacks: 1
source: "vanish"

View File

@@ -0,0 +1,16 @@
# Vital Strike - Assassin Blade Specialist ability
# Massive crit damage
ability_id: "vital_strike"
name: "Vital Strike"
description: "Strike a vital organ for massive critical damage"
ability_type: "attack"
base_power: 30
damage_type: "physical"
scaling_stat: "dexterity"
scaling_factor: 0.55
mana_cost: 18
cooldown: 2
is_aoe: false
target_count: 1
effects_applied: []

View File

@@ -0,0 +1,16 @@
# Word of Healing - Oathkeeper Redemption ability
# AoE heal
ability_id: "word_of_healing"
name: "Word of Healing"
description: "Speak a word of power that heals all nearby allies"
ability_type: "spell"
base_power: 40
damage_type: "holy"
scaling_stat: "wisdom"
scaling_factor: 0.55
mana_cost: 30
cooldown: 3
is_aoe: true
target_count: 0
effects_applied: []

View File

@@ -21,6 +21,7 @@ base_stats:
starting_equipment: starting_equipment:
- worn_staff - worn_staff
- cloth_armor - cloth_armor
- health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -21,6 +21,7 @@ base_stats:
starting_equipment: starting_equipment:
- rusty_dagger - rusty_dagger
- cloth_armor - cloth_armor
- health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -21,6 +21,7 @@ base_stats:
starting_equipment: starting_equipment:
- tome - tome
- cloth_armor - cloth_armor
- health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -21,6 +21,7 @@ base_stats:
starting_equipment: starting_equipment:
- rusty_mace - rusty_mace
- cloth_armor - cloth_armor
- health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -21,6 +21,7 @@ base_stats:
starting_equipment: starting_equipment:
- bone_wand - bone_wand
- cloth_armor - cloth_armor
- health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -22,6 +22,7 @@ starting_equipment:
- rusty_sword - rusty_sword
- rusty_shield - rusty_shield
- cloth_armor - cloth_armor
- health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -22,6 +22,7 @@ base_stats:
starting_equipment: starting_equipment:
- rusty_sword - rusty_sword
- cloth_armor - cloth_armor
- health_potion_small
# Starting abilities # Starting abilities
starting_abilities: starting_abilities:

View File

@@ -21,6 +21,7 @@ base_stats:
starting_equipment: starting_equipment:
- rusty_bow - rusty_bow
- cloth_armor - cloth_armor
- health_potion_small
starting_abilities: starting_abilities:
- basic_attack - basic_attack

View File

@@ -1269,9 +1269,9 @@ class CombatService:
character = self.character_service.get_character(char_id, user_id) character = self.character_service.get_character(char_id, user_id)
# Add XP and check for level up # Add XP and check for level up
old_level = character.level leveled_up = character.add_experience(xp_per_player)
character.experience += xp_per_player if leveled_up:
# TODO: Add level up logic based on XP thresholds rewards.level_ups.append(char_id)
# Add gold # Add gold
character.gold += gold_per_player character.gold += gold_per_player
@@ -1279,9 +1279,6 @@ class CombatService:
# Save character # Save character
self.character_service.update_character(character, user_id) self.character_service.update_character(character, user_id)
if character.level > old_level:
rewards.level_ups.append(char_id)
except Exception as e: except Exception as e:
logger.error("Failed to distribute rewards to character", logger.error("Failed to distribute rewards to character",
char_id=char_id, char_id=char_id,

View File

@@ -365,17 +365,162 @@ effects_applied:
### Experience & Leveling ### Experience & Leveling
| Source | XP Gain | **XP Sources:**
|--------|---------|
| Combat victory | Based on enemy difficulty | | Source | XP Gain | Notes |
| Quest completion | Fixed quest reward | |--------|---------|-------|
| Story milestones | Major plot points | | Combat victory | Based on enemy `experience_reward` field | Divided evenly among party members |
| Exploration | Discovering new locations | | Quest completion | Fixed quest reward | Defined in quest data |
| Story milestones | Major plot points | AI-driven narrative rewards |
| Exploration | Discovering new locations | Future enhancement |
**Level Progression:** **Level Progression:**
- XP required increases per level (exponential curve)
- Each level grants +1 skill point The XP requirement for each level follows an exponential curve using the formula:
- Stats may increase based on class
```
XP Required = 100 × (current_level ^ 1.5)
```
| Level | XP Required | Cumulative XP |
|-------|-------------|---------------|
| 1→2 | 100 | 100 |
| 2→3 | 282 | 382 |
| 3→4 | 519 | 901 |
| 4→5 | 800 | 1,701 |
| 5→6 | 1,118 | 2,819 |
| 6→7 | 1,469 | 4,288 |
| 7→8 | 1,849 | 6,137 |
| 8→9 | 2,254 | 8,391 |
| 9→10 | 2,683 | 11,074 |
**Leveling Mechanics:**
- Each level grants **+1 skill point** to spend in skill trees
- Skill points calculated: `level - unlocked_skills.length`
- Overflow XP automatically carries to next level
- Level up triggers automatically when threshold reached
- Base stats remain constant (progression via skill trees & equipment)
**Implementation:**
- Leveling logic lives in `Character` model (`add_experience()`, `level_up()` methods)
- No separate service needed (OOP design pattern)
- See `api/app/models/character.py` lines 312-358
### Skill Trees
**Overview:**
Each character class has **2-3 skill trees** representing different specializations or playstyles. Players earn **1 skill point per level** to unlock skills, which provide permanent bonuses and unlock combat abilities.
**Skill Points:**
```
Available Skill Points = Character Level - Unlocked Skills Count
```
**Skill Tree Structure:**
Each skill tree contains **5 tiers** of increasing power:
| Tier | Description | Typical Effects |
|------|-------------|-----------------|
| **1** | Entry-level skills | Basic abilities, small stat bonuses (+3-5) |
| **2** | Intermediate skills | Enhanced abilities, moderate bonuses (+5-8) |
| **3** | Advanced skills | Powerful abilities, passive effects |
| **4** | Expert skills | Ability enhancements, large bonuses (+10-15) |
| **5** | Ultimate skills | Class-defining abilities, massive bonuses (+20+) |
**Prerequisites:**
Skills have prerequisites that create progression paths:
- Tier 1 skills have **no prerequisites** (open choices)
- Higher tier skills require **specific lower-tier skills**
- Cannot skip tiers (must unlock Tier 1 before Tier 2, etc.)
- Can mix between trees within same class
**Skill Effects:**
Skills provide multiple types of benefits:
1. **Stat Bonuses** - Permanent increases to stats
```yaml
effects:
stat_bonuses:
strength: 10
defense: 5
```
2. **Ability Unlocks** - Grant new combat abilities
```yaml
effects:
abilities:
- shield_bash
- riposte
```
3. **Passive Effects** - Special mechanics
```yaml
effects:
passive_effects:
- stun_resistance
- damage_reflection
```
4. **Ability Enhancements** - Modify existing abilities
```yaml
effects:
ability_enhancements:
fireball:
damage_bonus: 15
mana_cost_reduction: 5
```
5. **Combat Bonuses** - Crit chance, crit multiplier, etc.
```yaml
effects:
combat_bonuses:
crit_chance: 0.1 # +10%
crit_multiplier: 0.5 # +0.5x
```
**Example Progression Path:**
**Vanguard - Shield Bearer Tree:**
```
Level 1: No skills yet (0 points)
Level 2: Unlock "Shield Bash" (Tier 1) → Gain shield bash ability
Level 3: Unlock "Fortify" (Tier 1) → +5 defense bonus
Level 4: Unlock "Shield Wall" (Tier 2, requires Shield Bash) → Shield wall ability
Level 5: Unlock "Iron Skin" (Tier 2, requires Fortify) → +5 constitution
Level 6: Unlock "Guardian's Resolve" (Tier 3) → +10 defense + stun resistance
...
```
**Class Specializations:**
Each class offers distinct playstyles through their trees:
| Class | Tree 1 | Tree 2 | Tree 3 |
|-------|--------|--------|--------|
| **Vanguard** | Shield Bearer (Tank) | Weapon Master (DPS) | - |
| **Arcanist** | Pyromancy (Fire) | Cryomancy (Ice) | Electromancy (Lightning) |
| **Wildstrider** | Beast Mastery | Nature Magic | - |
| **Assassin** | Shadow Arts | Poison Master | - |
| **Luminary** | Holy Magic | Divine Protection | - |
| **Necromancer** | Death Magic | Corpse Summoning | - |
| **Lorekeeper** | Arcane Knowledge | Support Magic | - |
| **Oathkeeper** | Divine Wrath | Holy Shield | - |
**Design Notes:**
- Skill choices are **permanent** (no respec system currently)
- Players can mix skills from different trees within same class
- Some skills are **mutually exclusive** by design (different playstyles)
- Skill point allocation encourages specialization vs. generalization
**Implementation:**
- Skills defined in class YAML files at `api/app/data/classes/*.yaml`
- Character stores only `unlocked_skills: List[str]` (skill IDs)
- Bonuses calculated dynamically via `Character.get_effective_stats()`
- Full documentation: [SKILLS_AND_ABILITIES.md](SKILLS_AND_ABILITIES.md)
### Loot System ### Loot System

View File

@@ -0,0 +1,479 @@
# Skills and Abilities System
## Overview
The game uses two distinct but interconnected systems for character progression and combat:
- **Skills**: Progression system that provides passive bonuses and unlocks abilities
- **Abilities**: Combat actions (attacks, spells, skills) that can be used in battle
**Key Distinction**: Skills are *unlocked through leveling* and provide permanent benefits. Abilities are *combat actions* that characters can execute during battles.
**⚠️ Terminology Note**: The word "skill" and "spell" have specific meanings in this system. See the [Terminology Clarification](#terminology-clarification) section below for important distinctions between Skills (progression nodes), Abilities (combat actions), Spells (a type of ability), and the confusing `AbilityType.SKILL` enum.
---
## Terminology Clarification
It's important to understand three distinct concepts that can be confusing due to overlapping names:
### 1. Skills (Skill Tree Nodes)
**What**: Nodes in the skill tree progression system that you unlock with skill points earned from leveling up.
**Where**: Defined in class YAML files at `/api/app/data/classes/*.yaml`
**Model**: `SkillNode` in [app/models/skills.py](../app/models/skills.py)
**Purpose**: Provide passive stat bonuses and/or unlock combat abilities
**Examples**:
- "Shield Bash" skill node (unlocks the Shield Bash ability)
- "Fortify" skill node (grants +5 defense bonus)
- "Fireball Mastery" skill node (unlocks the Fireball ability)
### 2. Abilities (Combat Actions)
**What**: Actions you can perform during combat - the umbrella term for all combat actions.
**Where**: Defined in separate YAML files at `/api/app/data/abilities/*.yaml`
**Model**: `Ability` in [app/models/abilities.py](../app/models/abilities.py)
**Purpose**: Define combat mechanics - damage, effects, costs, targeting, etc.
**Examples**:
- Fireball ability (deals fire damage)
- Shield Bash ability (stuns enemy)
- Heal ability (restores HP)
- Basic Attack ability (default physical attack)
### 3. Spells (A Type of Ability)
**What**: A **category** of ability that uses magic/arcane power (not a separate system).
**Type Field**: `ability_type: "spell"` in the Ability YAML
**Purpose**: Differentiate magical abilities from physical attacks or special skills
**Examples**:
- Fireball (ability_type: spell)
- Lightning Bolt (ability_type: spell)
- Heal (ability_type: spell)
### Ability Types (AbilityType Enum)
All abilities have an `ability_type` field that categorizes them. From [app/models/enums.py](../app/models/enums.py):
```python
class AbilityType(Enum):
"""Categories of abilities that can be used in combat or exploration."""
ATTACK = "attack" # Basic physical attack
SPELL = "spell" # Magical spell (arcane/divine)
SKILL = "skill" # Special class ability (⚠️ different from skill tree!)
ITEM_USE = "item_use" # Using a consumable item
DEFEND = "defend" # Defensive action
```
**⚠️ Important Confusion to Avoid**: `AbilityType.SKILL` refers to special class abilities used in combat (like a Rogue's "Sneak Attack"). This is **completely different** from "Skills" in the skill tree progression system (`SkillNode`).
### Complete Example: Fireball
To illustrate how all three concepts work together:
**1. Skill Tree Node** (what you unlock with a skill point):
```yaml
# In /api/app/data/classes/arcanist.yaml
- skill_id: fireball_mastery
name: Fireball Mastery
description: Learn to cast the devastating fireball spell
tier: 1
prerequisites: []
effects:
abilities:
- fireball # ← References the ability
```
**2. Ability Definition** (the combat action):
```yaml
# In /api/app/data/abilities/fireball.yaml
ability_id: "fireball"
name: "Fireball"
description: "Hurl a ball of fire at your enemies"
ability_type: "spell" # ← This makes it a SPELL-type ability
base_power: 30
damage_type: "fire"
scaling_stat: "intelligence"
mana_cost: 15
```
**3. The Flow**:
- Player unlocks "Fireball Mastery" **skill** (spends 1 skill point)
- This grants access to the "Fireball" **ability**
- Fireball is categorized as a **spell** (ability_type: spell)
- Player can now use Fireball in combat
### Summary Table
| Term | What It Is | Where Defined | Purpose |
|------|------------|---------------|---------|
| **Skill** (SkillNode) | Progression node | `/api/app/data/classes/*.yaml` | Unlock abilities, grant bonuses |
| **Ability** | Combat action | `/api/app/data/abilities/*.yaml` | Define what you can do in battle |
| **Spell** | Type of ability | `ability_type: "spell"` | Magical/arcane combat actions |
| **AbilityType.SKILL** | Type of ability | `ability_type: "skill"` | Special class abilities (⚠️ not skill tree) |
---
## Architecture
### Skills
**Purpose**: Character progression and passive bonuses
**Data Model**: [app/models/skills.py](../app/models/skills.py)
- `SkillNode`: Individual skill in a tree
- `SkillTree`: Collection of related skills
- `PlayerClass`: Character class with multiple skill trees
**Data Location**: [app/data/classes/*.yaml](../app/data/classes/)
- Skills are embedded within class definitions
- Each class YAML contains 2+ skill trees
- Example: `vanguard.yaml`, `arcanist.yaml`
**Loader**: [app/services/class_loader.py](../app/services/class_loader.py)
- `ClassLoader` reads class YAML files
- Parses skill trees and nodes during class loading
- Caches PlayerClass instances
**Storage on Character**: [app/models/character.py](../app/models/character.py)
```python
unlocked_skills: List[str] = field(default_factory=list) # Just skill IDs
```
### Abilities
**Purpose**: Combat actions and spells
**Data Model**: [app/models/abilities.py](../app/models/abilities.py)
- `Ability`: Complete definition of a combat action
- Includes damage, effects, costs, targeting, etc.
**Data Location**: [app/data/abilities/*.yaml](../app/data/abilities/)
- Each ability is a separate YAML file
- Example: `fireball.yaml`, `shield_bash.yaml`, `heal.yaml`
**Loader**: [app/models/abilities.py](../app/models/abilities.py)
- `AbilityLoader` class (lines 164-238)
- Loads abilities on-demand or in bulk
- Caches Ability instances
**Storage on Character**: Not stored directly
- Character has `player_class.starting_abilities`
- Abilities unlocked through skills are computed dynamically
---
## How Skills Unlock Abilities
### Skill Effects Dictionary
Skills can provide multiple types of benefits through their `effects` dictionary:
```yaml
# From vanguard.yaml
- skill_id: shield_bash
name: Shield Bash
description: Strike an enemy with your shield, dealing minor damage and stunning them
tier: 1
prerequisites: []
effects:
abilities: # Unlocks abilities for combat use
- shield_bash # References /app/data/abilities/shield_bash.yaml
```
```yaml
# Skills can also provide stat bonuses
- skill_id: fortify
name: Fortify
description: Your defensive training grants you enhanced protection
tier: 1
prerequisites: []
effects:
stat_bonuses:
defense: 5 # Passive stat increase
```
```yaml
# Or both at once
- skill_id: perfect_form
name: Perfect Form
description: Your combat technique reaches perfection
tier: 5
prerequisites:
- weapon_mastery
effects:
stat_bonuses:
strength: 20
dexterity: 10
combat_bonuses:
crit_chance: 0.1
crit_multiplier: 0.5
```
### Effect Types
Skills can define effects in several categories:
1. **`abilities`**: List of ability IDs to unlock
2. **`stat_bonuses`**: Direct stat increases (strength, defense, etc.)
3. **`combat_bonuses`**: Combat modifiers (crit_chance, crit_multiplier)
4. **`passive_effects`**: Special passive effects (stun_resistance, etc.)
5. **`ability_enhancements`**: Modify existing abilities
---
## Data Flow: Skill Unlock to Ability Usage
### 1. Character Levels Up
```python
# Character gains a level and skill point
character.level_up()
# character.level is now higher
# character.available_skill_points increases
```
### 2. Player Unlocks a Skill
```python
# Service validates and unlocks the skill
skill_id = "shield_bash"
character.unlocked_skills.append(skill_id)
# Character saves to database
```
### 3. Getting Available Abilities
```python
# Character.get_unlocked_abilities() - character.py:177-194
def get_unlocked_abilities(self) -> List[str]:
# Start with class's built-in abilities
abilities = list(self.player_class.starting_abilities)
# Get all skill nodes from all trees
all_skills = self.player_class.get_all_skills()
# Collect abilities from unlocked skills
for skill in all_skills:
if skill.skill_id in self.unlocked_skills:
abilities.extend(skill.get_unlocked_abilities())
return abilities # Returns list of ability IDs
```
### 4. Extracting Abilities from Skills
```python
# SkillNode.get_unlocked_abilities() - skills.py:73-87
def get_unlocked_abilities(self) -> List[str]:
abilities = []
if "abilities" in self.effects:
ability = self.effects["abilities"]
if isinstance(ability, list):
abilities.extend(ability)
else:
abilities.append(ability)
return abilities
```
### 5. Loading Ability Details for Combat
```python
# During combat initialization
ability_loader = AbilityLoader()
ability_ids = character.get_unlocked_abilities()
# Load full Ability objects
available_abilities = []
for ability_id in ability_ids:
ability = ability_loader.load_ability(ability_id)
if ability:
available_abilities.append(ability)
# Now character can use these abilities in combat
```
### 6. Using an Ability in Combat
```python
# Player selects ability
ability = ability_loader.load_ability("fireball")
# Calculate power with character's stats
effective_stats = character.get_effective_stats()
damage = ability.calculate_power(effective_stats)
# Apply to target
target.current_hp -= damage
# Apply effects (DoT, buffs, debuffs)
effects = ability.get_effects_to_apply()
for effect in effects:
target.active_effects.append(effect)
```
---
## Complete Example: Fireball (Step-by-Step Workflow)
This example shows the complete journey from skill definition through combat usage. For the YAML definitions, see the [Terminology Clarification](#terminology-clarification) section above.
### Step 1: Define the Skill (in class YAML)
See the Fireball Mastery skill node example in the Terminology section.
### Step 2: Define the Ability (separate YAML)
See the Fireball ability YAML example in the Terminology section.
**Key Point**: The skill's `effects.abilities` list contains `fireball`, which references `/api/app/data/abilities/fireball.yaml`.
### Step 3: Character Progression
```python
# Character is created as Arcanist
character = Character(
name="Merlin",
player_class=arcanist_class,
level=1,
unlocked_skills=[], # No skills yet
)
# Starting abilities from class
character.get_unlocked_abilities() # Returns: ["basic_attack"]
# Character reaches level 2, spends skill point
character.unlocked_skills.append("fireball_skill")
# Now has fireball ability
character.get_unlocked_abilities() # Returns: ["basic_attack", "fireball"]
```
### Step 4: Combat Usage
```python
# Combat begins
ability_loader = AbilityLoader()
fireball = ability_loader.load_ability("fireball")
# Character's intelligence is 15
# Effective stats calculation includes all bonuses
effective_stats = character.get_effective_stats() # intelligence = 15
# Calculate damage
damage = fireball.calculate_power(effective_stats)
# damage = 30 + (15 * 0.5) = 30 + 7.5 = 37
# Apply to enemy
enemy.current_hp -= 37
# Apply burning effect
burn_effect = fireball.get_effects_to_apply()[0]
enemy.active_effects.append(burn_effect)
# Enemy will take 5 fire damage per turn for 3 turns
```
---
## Skill Bonuses vs Abilities
### Passive Bonuses
Skills provide **permanent** stat increases calculated by `Character.get_effective_stats()`:
```python
# Character._get_skill_bonuses() - character.py:156-175
def _get_skill_bonuses(self) -> Dict[str, int]:
bonuses: Dict[str, int] = {}
# Get all skill nodes from all trees
all_skills = self.player_class.get_all_skills()
# Sum up bonuses from unlocked skills
for skill in all_skills:
if skill.skill_id in self.unlocked_skills:
skill_bonuses = skill.get_stat_bonuses()
for stat_name, bonus in skill_bonuses.items():
bonuses[stat_name] = bonuses.get(stat_name, 0) + bonus
return bonuses
```
### Extracting Stat Bonuses
```python
# SkillNode.get_stat_bonuses() - skills.py:57-71
def get_stat_bonuses(self) -> Dict[str, int]:
bonuses = {}
for key, value in self.effects.items():
# Look for stat names in effects
if key in ["strength", "dexterity", "constitution",
"intelligence", "wisdom", "charisma"]:
bonuses[key] = value
elif key == "defense" or key == "resistance" or key == "hit_points":
bonuses[key] = value
return bonuses
```
**Note**: The current implementation looks for direct stat keys in `effects`. The newer YAML format uses nested `stat_bonuses` dictionaries. This may need updating.
---
## Key Design Principles
1. **Separation of Concerns**
- Skills = Progression system (what you unlock)
- Abilities = Combat system (what you can do)
2. **Data-Driven Design**
- Both are defined in YAML files
- Game designers can modify without code changes
- Easy to balance and iterate
3. **Loose Coupling**
- Skills reference abilities by ID only
- Abilities are loaded on-demand
- Changes to abilities don't affect skill definitions
4. **Efficient Storage**
- Character only stores skill IDs (not full objects)
- Abilities computed dynamically when needed
- Reduces database payload
5. **Flexible Effects System**
- Skills can unlock multiple abilities
- Skills can provide stat bonuses
- Skills can do both simultaneously
- Room for future effect types
---
## Related Documentation
- [DATA_MODELS.md](DATA_MODELS.md) - Character system and items
- [GAME_SYSTEMS.md](GAME_SYSTEMS.md) - Combat mechanics
- [API_REFERENCE.md](API_REFERENCE.md) - API endpoints for skills
---
## Future Enhancements
Potential improvements to the system:
1. **Skill Levels**: Allow skills to be upgraded multiple times
2. **Ability Enhancements**: Skills that modify existing abilities
3. **Conditional Unlocks**: Require items, quests, or achievements
4. **Skill Synergies**: Bonuses for unlocking related skills
5. **Respec System**: Allow skill point redistribution
6. **Skill Prerequisites Validation**: Runtime validation of skill trees

View File

@@ -729,8 +729,8 @@ app.register_blueprint(combat_bp, url_prefix='/combat')
- ✅ Multiple enemies work - would like to update to allow the player to select which enemy to attack - ✅ Multiple enemies work - would like to update to allow the player to select which enemy to attack
- ✅ Combat state persists (refresh page) - ✅ Combat state persists (refresh page)
- [ ] Spells consume mana - unable to test - [ ] Spells consume mana - Need to test
- [ ] Items can be used in combat - unable to test - [ ] Items can be used in combat - Need to test
**Bug Fixes & Polish:** **Bug Fixes & Polish:**
- Fix any calculation errors - Fix any calculation errors

View File

@@ -1,467 +0,0 @@
## Phase 4B: Skill Trees & Leveling (Week 4)
### Task 4.1: Verify Skill Tree Data (2 hours)
**Objective:** Review skill system
**Files to Review:**
- `/api/app/models/skills.py` - SkillNode, SkillTree, PlayerClass
- `/api/app/data/skills/` - Skill YAML files for all 8 classes
**Verification Checklist:**
- [ ] Skill trees loaded from YAML
- [ ] Each class has 2 skill trees
- [ ] Each tree has 5 tiers
- [ ] Prerequisites work correctly
- [ ] Stat bonuses apply correctly
**Acceptance Criteria:**
- All 8 classes have complete skill trees
- Unlock logic works
- Respec logic implemented
---
### Task 4.2: Create Skill Tree Template (2 days / 16 hours)
**Objective:** Visual skill tree UI
**File:** `/public_web/templates/character/skills.html`
**Layout:**
```
┌─────────────────────────────────────────────────────────────┐
│ CHARACTER SKILL TREES │
├─────────────────────────────────────────────────────────────┤
│ │
│ Skill Points Available: 5 [Respec] ($$$)│
│ │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ TREE 1: Combat │ │ TREE 2: Utility │ │
│ ├────────────────────────┤ ├────────────────────────┤ │
│ │ │ │ │ │
│ │ Tier 5: [⬢] [⬢] │ │ Tier 5: [⬢] [⬢] │ │
│ │ │ │ │ │ │ │ │ │
│ │ Tier 4: [⬢] [⬢] │ │ Tier 4: [⬢] [⬢] │ │
│ │ │ │ │ │ │ │ │ │
│ │ Tier 3: [⬢] [⬢] │ │ Tier 3: [⬢] [⬢] │ │
│ │ │ │ │ │ │ │ │ │
│ │ Tier 2: [✓] [⬢] │ │ Tier 2: [⬢] [✓] │ │
│ │ │ │ │ │ │ │ │ │
│ │ Tier 1: [✓] [✓] │ │ Tier 1: [✓] [✓] │ │
│ │ │ │ │ │
│ └────────────────────────┘ └────────────────────────┘ │
│ │
│ Legend: [✓] Unlocked [⬡] Available [⬢] Locked │
│ │
└─────────────────────────────────────────────────────────────┘
```
**Implementation:**
```html
{% extends "base.html" %}
{% block title %}Skill Trees - {{ character.name }}{% endblock %}
{% block content %}
<div class="skills-container">
<div class="skills-header">
<h1>{{ character.name }}'s Skill Trees</h1>
<div class="skills-info">
<span class="skill-points">Skill Points: <strong>{{ character.skill_points }}</strong></span>
<button class="btn btn-warning btn-respec"
hx-post="/characters/{{ character.character_id }}/skills/respec"
hx-confirm="Respec costs {{ respec_cost }} gold. Continue?"
hx-target=".skills-container"
hx-swap="outerHTML">
Respec ({{ respec_cost }} gold)
</button>
</div>
</div>
<div class="skill-trees-grid">
{% for tree in character.skill_trees %}
<div class="skill-tree">
<h2 class="tree-name">{{ tree.name }}</h2>
<p class="tree-description">{{ tree.description }}</p>
<div class="tree-diagram">
{% for tier in range(5, 0, -1) %}
<div class="skill-tier" data-tier="{{ tier }}">
<span class="tier-label">Tier {{ tier }}</span>
<div class="skill-nodes">
{% for node in tree.get_nodes_by_tier(tier) %}
<div class="skill-node {{ get_node_status(node, character) }}"
data-skill-id="{{ node.skill_id }}"
hx-get="/skills/{{ node.skill_id }}/tooltip"
hx-target="#skill-tooltip"
hx-swap="innerHTML"
hx-trigger="mouseenter">
<div class="node-icon">
{% if node.skill_id in character.unlocked_skills %}
{% elif character.can_unlock(node.skill_id) %}
{% else %}
{% endif %}
</div>
<span class="node-name">{{ node.name }}</span>
{% if character.can_unlock(node.skill_id) and character.skill_points > 0 %}
<button class="btn-unlock"
hx-post="/characters/{{ character.character_id }}/skills/unlock"
hx-vals='{"skill_id": "{{ node.skill_id }}"}'
hx-target=".skills-container"
hx-swap="outerHTML">
Unlock
</button>
{% endif %}
</div>
{# Draw prerequisite lines #}
{% if node.prerequisite_skill_id %}
<div class="prerequisite-line"></div>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{# Skill Tooltip (populated via HTMX) #}
<div id="skill-tooltip" class="skill-tooltip"></div>
</div>
{% endblock %}
```
**Also create `/public_web/templates/character/partials/skill_tooltip.html`:**
```html
<div class="tooltip-content">
<h3 class="skill-name">{{ skill.name }}</h3>
<p class="skill-description">{{ skill.description }}</p>
<div class="skill-bonuses">
<strong>Bonuses:</strong>
<ul>
{% for stat, bonus in skill.stat_bonuses.items() %}
<li>+{{ bonus }} {{ stat|title }}</li>
{% endfor %}
</ul>
</div>
{% if skill.prerequisite_skill_id %}
<p class="prerequisite">
<strong>Requires:</strong> {{ get_skill_name(skill.prerequisite_skill_id) }}
</p>
{% endif %}
</div>
```
**Acceptance Criteria:**
- Dual skill tree layout works
- 5 tiers × 2 nodes per tree displayed
- Locked/available/unlocked states visual
- Prerequisite lines drawn
- Hover shows tooltip
- Mobile responsive
---
### Task 4.3: Skill Unlock HTMX (4 hours)
**Objective:** Click to unlock skills
**File:** `/public_web/app/views/skills.py`
```python
"""
Skill Views
Routes for skill tree UI.
"""
from flask import Blueprint, render_template, request, g
from app.services.api_client import APIClient, APIError
from app.utils.auth import require_auth
from app.utils.logging import get_logger
logger = get_logger(__file__)
skills_bp = Blueprint('skills', __name__)
@skills_bp.route('/<skill_id>/tooltip', methods=['GET'])
@require_auth
def skill_tooltip(skill_id: str):
"""Get skill tooltip (HTMX partial)."""
# Load skill data
# Return rendered tooltip
pass
@skills_bp.route('/characters/<character_id>/skills', methods=['GET'])
@require_auth
def character_skills(character_id: str):
"""Display character skill trees."""
api_client = APIClient()
try:
# Get character
response = api_client.get(f'/characters/{character_id}')
character = response['result']
# Calculate respec cost
respec_cost = character['level'] * 100
return render_template(
'character/skills.html',
character=character,
respec_cost=respec_cost
)
except APIError as e:
logger.error(f"Failed to load skills: {e}")
return render_template('partials/error.html', error=str(e))
@skills_bp.route('/characters/<character_id>/skills/unlock', methods=['POST'])
@require_auth
def unlock_skill(character_id: str):
"""Unlock skill (HTMX endpoint)."""
api_client = APIClient()
skill_id = request.form.get('skill_id')
try:
# Unlock skill via API
response = api_client.post(
f'/characters/{character_id}/skills/unlock',
json={'skill_id': skill_id}
)
# Re-render skill trees
character = response['result']['character']
respec_cost = character['level'] * 100
return render_template(
'character/skills.html',
character=character,
respec_cost=respec_cost
)
except APIError as e:
logger.error(f"Failed to unlock skill: {e}")
return render_template('partials/error.html', error=str(e))
```
**Acceptance Criteria:**
- Click available node unlocks skill
- Skill points decrease
- Stat bonuses apply immediately
- Prerequisites enforced
- UI updates without page reload
---
### Task 4.4: Respec Functionality (4 hours)
**Objective:** Respec button with confirmation
**Implementation:** (in `skills_bp`)
```python
@skills_bp.route('/characters/<character_id>/skills/respec', methods=['POST'])
@require_auth
def respec_skills(character_id: str):
"""Respec all skills."""
api_client = APIClient()
try:
response = api_client.post(f'/characters/{character_id}/skills/respec')
character = response['result']['character']
respec_cost = character['level'] * 100
return render_template(
'character/skills.html',
character=character,
respec_cost=respec_cost,
message="Skills reset! All skill points refunded."
)
except APIError as e:
logger.error(f"Failed to respec: {e}")
return render_template('partials/error.html', error=str(e))
```
**Acceptance Criteria:**
- Respec button costs gold
- Confirmation modal shown
- All skills reset
- Skill points refunded
- Gold deducted
---
### Task 4.5: XP & Leveling System (1 day / 8 hours)
**Objective:** Award XP after combat, level up grants skill points
**File:** `/api/app/services/leveling_service.py`
```python
"""
Leveling Service
Manages XP gain and level ups.
"""
from app.models.character import Character
from app.utils.logging import get_logger
logger = get_logger(__file__)
class LevelingService:
"""Service for XP and leveling."""
@staticmethod
def xp_required_for_level(level: int) -> int:
"""
Calculate XP required for a given level.
Formula: 100 * (level ^ 2)
"""
return 100 * (level ** 2)
@staticmethod
def award_xp(character: Character, xp_amount: int) -> dict:
"""
Award XP to character and check for level up.
Args:
character: Character instance
xp_amount: XP to award
Returns:
Dict with leveled_up, new_level, skill_points_gained
"""
character.experience += xp_amount
leveled_up = False
levels_gained = 0
# Check for level ups (can level multiple times)
while character.experience >= LevelingService.xp_required_for_level(character.level + 1):
character.level += 1
character.skill_points += 1
levels_gained += 1
leveled_up = True
logger.info(f"Character {character.character_id} leveled up to {character.level}")
return {
'leveled_up': leveled_up,
'new_level': character.level if leveled_up else None,
'skill_points_gained': levels_gained,
'xp_gained': xp_amount
}
```
**Update Combat Results Endpoint:**
```python
# In /api/app/api/combat.py
@combat_bp.route('/<combat_id>/results', methods=['GET'])
@require_auth
def get_combat_results(combat_id: str):
"""Get combat results with XP/loot."""
combat_service = CombatService(get_appwrite_service())
encounter = combat_service.get_encounter(combat_id)
if encounter.status != CombatStatus.VICTORY:
return error_response("Combat not won", 400)
# Calculate XP (based on enemy difficulty)
xp_gained = sum(enemy.level * 50 for enemy in encounter.combatants if not enemy.is_player)
# Award XP to character
char_service = get_character_service()
character = char_service.get_character(encounter.character_id, g.user_id)
from app.services.leveling_service import LevelingService
level_result = LevelingService.award_xp(character, xp_gained)
# Award gold
gold_gained = sum(enemy.level * 25 for enemy in encounter.combatants if not enemy.is_player)
character.gold += gold_gained
# Generate loot (TODO: implement loot tables)
loot = []
# Save character
char_service.update_character(character)
return success_response({
'victory': True,
'xp_gained': xp_gained,
'gold_gained': gold_gained,
'loot': loot,
'level_up': level_result
})
```
**Create Level Up Modal Template:**
**File:** `/public_web/templates/game/partials/level_up_modal.html`
```html
<div class="modal-overlay">
<div class="modal-content level-up-modal">
<div class="modal-header">
<h2>🎉 LEVEL UP! 🎉</h2>
</div>
<div class="modal-body">
<p class="level-up-text">
Congratulations! You've reached <strong>Level {{ new_level }}</strong>!
</p>
<div class="level-up-rewards">
<p>You gained:</p>
<ul>
<li>+1 Skill Point</li>
<li>+{{ stat_increases.vitality }} Vitality</li>
<li>+{{ stat_increases.spirit }} Spirit</li>
</ul>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeModal()">Awesome!</button>
<a href="/characters/{{ character_id }}/skills" class="btn btn-secondary">
View Skill Trees
</a>
</div>
</div>
</div>
```
**Acceptance Criteria:**
- XP awarded after combat victory
- Level up triggers at XP threshold
- Skill points granted on level up
- Level up modal shown
- Character stats increase
---

View File

@@ -714,22 +714,18 @@ def view_skills(character_id: str):
api_client = get_api_client() api_client = get_api_client()
try: try:
# Get character data # Get character data (includes full player_class with skill_trees)
response = api_client.get(f"/api/v1/characters/{character_id}") response = api_client.get(f"/api/v1/characters/{character_id}")
character = response.get('result') character = response.get('result')
# Load class data to get skill trees # Player class is already embedded in character response
class_id = character.get('class_id') player_class = character.get('player_class')
player_class = None
if class_id:
response = api_client.get(f"/api/v1/classes/{class_id}")
player_class = response.get('result')
logger.info( logger.info(
"Skill tree viewed", "Skill tree viewed",
user_id=user.get('id'), user_id=user.get('id'),
character_id=character_id character_id=character_id,
class_id=player_class.get('class_id') if player_class else None
) )
return render_template( return render_template(
@@ -752,3 +748,145 @@ def view_skills(character_id: str):
) )
flash('An error occurred while loading the skill tree.', 'error') flash('An error occurred while loading the skill tree.', 'error')
return redirect(url_for('character_views.list_characters')) return redirect(url_for('character_views.list_characters'))
@character_bp.route('/<character_id>/skills/unlock', methods=['POST'])
@require_auth_web
def unlock_skill(character_id: str):
"""
Unlock a skill for a character (HTMX endpoint).
Args:
character_id: ID of the character
Returns:
Re-rendered skills container partial
"""
user = get_current_user()
api_client = get_api_client()
skill_id = request.form.get('skill_id')
if not skill_id:
return _render_skills_page(
api_client, character_id, user,
error="No skill specified"
)
try:
# Call API to unlock skill
api_client.post(
f"/api/v1/characters/{character_id}/skills/unlock",
data={'skill_id': skill_id}
)
logger.info(
"Skill unlocked",
user_id=user.get('id'),
character_id=character_id,
skill_id=skill_id
)
return _render_skills_page(
api_client, character_id, user,
message=f"Skill unlocked!"
)
except APIError as e:
logger.error(
"Failed to unlock skill",
user_id=user.get('id'),
character_id=character_id,
skill_id=skill_id,
error=str(e)
)
return _render_skills_page(
api_client, character_id, user,
error=str(e.message) if hasattr(e, 'message') else "Failed to unlock skill"
)
@character_bp.route('/<character_id>/skills/respec', methods=['POST'])
@require_auth_web
def respec_skills(character_id: str):
"""
Respec all skills for a character (HTMX endpoint).
Args:
character_id: ID of the character
Returns:
Re-rendered skills container partial
"""
user = get_current_user()
api_client = get_api_client()
try:
# Call API to respec skills
response = api_client.post(f"/api/v1/characters/{character_id}/skills/respec")
result = response.get('result', {})
logger.info(
"Skills respec",
user_id=user.get('id'),
character_id=character_id,
cost=result.get('cost')
)
return _render_skills_page(
api_client, character_id, user,
message=f"Skills reset! {result.get('available_points', 0)} skill points refunded."
)
except APIError as e:
logger.error(
"Failed to respec skills",
user_id=user.get('id'),
character_id=character_id,
error=str(e)
)
return _render_skills_page(
api_client, character_id, user,
error=str(e.message) if hasattr(e, 'message') else "Failed to respec skills"
)
def _render_skills_page(api_client, character_id: str, user: dict,
message: str = None, error: str = None):
"""
Helper to render the skills container partial for HTMX updates.
Args:
api_client: API client instance
character_id: Character ID
user: Current user dict
message: Optional success message
error: Optional error message
Returns:
Rendered skills container HTML (partial, no base template)
"""
try:
# Get fresh character data (includes full player_class with skill_trees)
response = api_client.get(f"/api/v1/characters/{character_id}")
character = response.get('result')
# Player class is already embedded in character response
player_class = character.get('player_class')
# Use partial template for HTMX responses (no base.html wrapper)
return render_template(
'character/partials/skills_container.html',
character=character,
player_class=player_class,
message=message,
error=error
)
except APIError as e:
logger.error("Failed to render skills page", error=str(e))
return render_template(
'character/partials/skills_container.html',
character={'character_id': character_id, 'name': 'Unknown', 'unlocked_skills': [], 'available_skill_points': 0, 'level': 1, 'gold': 0},
player_class=None,
error="Failed to load character data"
)

View File

@@ -0,0 +1,531 @@
/**
* Code of Conquest - Skill Tree Stylesheet
* Visual skill tree UI with dual-column layout
*/
/* ===== SKILL TREE CONTAINER ===== */
.skills-container {
max-width: 1400px;
margin: 2rem auto;
padding: 2rem;
}
/* ===== HEADER ===== */
.skills-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1rem;
}
.skills-header h1 {
font-family: var(--font-heading);
font-size: var(--text-3xl);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 2px;
margin: 0;
}
.skills-info {
display: flex;
align-items: center;
gap: 1.5rem;
}
.skill-points-display {
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
border-radius: 8px;
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.skill-points-label {
font-size: var(--text-sm);
color: var(--text-muted);
text-transform: uppercase;
font-weight: 600;
}
.skill-points-value {
font-size: var(--text-2xl);
color: var(--accent-gold);
font-weight: 700;
}
.btn-respec {
background: transparent;
border: 2px solid var(--accent-red);
color: var(--accent-red);
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
font-family: var(--font-body);
font-size: var(--text-sm);
font-weight: 600;
text-transform: uppercase;
transition: all 0.2s ease;
}
.btn-respec:hover {
background: var(--accent-red);
color: var(--text-primary);
}
.btn-respec:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ===== BACK LINK ===== */
.skills-back-link {
margin-bottom: 1rem;
}
.skills-back-link a {
color: var(--text-secondary);
text-decoration: none;
font-size: var(--text-sm);
transition: color 0.2s ease;
}
.skills-back-link a:hover {
color: var(--accent-gold);
}
/* ===== DUAL TREE GRID ===== */
.skill-trees-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
/* ===== INDIVIDUAL SKILL TREE ===== */
.skill-tree {
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
border-radius: 8px;
padding: 1.5rem;
}
.tree-header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--border-primary);
}
.tree-name {
font-family: var(--font-heading);
font-size: var(--text-xl);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 1px;
margin: 0 0 0.5rem 0;
}
.tree-description {
font-size: var(--text-sm);
color: var(--text-secondary);
margin: 0;
line-height: 1.5;
}
/* ===== TREE DIAGRAM (TIERS) ===== */
.tree-diagram {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* ===== SKILL TIER ROW ===== */
.skill-tier {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 0;
position: relative;
}
.tier-label {
font-size: var(--text-xs);
color: var(--text-muted);
text-transform: uppercase;
font-weight: 600;
min-width: 50px;
text-align: right;
}
/* ===== SKILL NODES CONTAINER ===== */
.skill-nodes {
display: flex;
gap: 1rem;
flex: 1;
justify-content: center;
}
/* ===== INDIVIDUAL SKILL NODE ===== */
.skill-node {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
min-width: 120px;
background: var(--bg-input);
border: 2px solid var(--border-primary);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.skill-node:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
/* ===== NODE STATES ===== */
/* Locked (cannot unlock yet) */
.skill-node--locked {
opacity: 0.5;
cursor: not-allowed;
}
.skill-node--locked:hover {
transform: none;
box-shadow: none;
}
.skill-node--locked .node-icon {
background: var(--bg-tertiary);
border-color: var(--border-primary);
color: var(--text-muted);
}
/* Available (can unlock) */
.skill-node--available {
border-color: var(--accent-blue);
box-shadow: 0 0 10px rgba(52, 152, 219, 0.3);
}
.skill-node--available .node-icon {
background: var(--bg-input);
border-color: var(--accent-blue);
color: var(--accent-blue);
}
.skill-node--available:hover {
border-color: var(--accent-gold);
box-shadow: 0 0 15px rgba(243, 156, 18, 0.4);
}
/* Unlocked (already acquired) */
.skill-node--unlocked {
border-color: var(--accent-green);
}
.skill-node--unlocked .node-icon {
background: var(--accent-green);
border-color: var(--accent-green);
color: var(--text-primary);
}
/* ===== NODE ICON ===== */
.node-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid var(--border-primary);
border-radius: 50%;
font-size: var(--text-lg);
font-weight: 700;
background: var(--bg-tertiary);
transition: all 0.2s ease;
}
/* ===== NODE NAME ===== */
.node-name {
font-size: var(--text-xs);
color: var(--text-primary);
text-align: center;
font-weight: 600;
line-height: 1.3;
max-width: 100px;
}
/* ===== UNLOCK BUTTON ===== */
.btn-unlock {
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
background: var(--accent-gold);
border: none;
border-radius: 4px;
padding: 0.25rem 0.75rem;
font-size: var(--text-xs);
font-weight: 600;
color: var(--bg-tertiary);
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
z-index: 10;
}
.btn-unlock:hover {
background: var(--accent-gold-hover);
transform: translateX(-50%) scale(1.05);
}
/* ===== PREREQUISITE LINES ===== */
/* Vertical line from node to previous tier */
.skill-node--has-prereq::before {
content: '';
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: 20px;
background: var(--border-primary);
}
.skill-node--unlocked.skill-node--has-prereq::before,
.skill-node--available.skill-node--has-prereq::before {
background: var(--accent-green);
}
/* ===== LEGEND ===== */
.skills-legend {
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 2rem;
padding: 1rem;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 4px;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: var(--text-sm);
color: var(--text-secondary);
}
.legend-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid var(--border-primary);
border-radius: 50%;
font-size: var(--text-xs);
}
.legend-icon--unlocked {
background: var(--accent-green);
border-color: var(--accent-green);
color: var(--text-primary);
}
.legend-icon--available {
border-color: var(--accent-blue);
color: var(--accent-blue);
}
.legend-icon--locked {
background: var(--bg-tertiary);
color: var(--text-muted);
opacity: 0.5;
}
/* ===== TOOLTIP ===== */
.skill-tooltip {
position: fixed;
z-index: 1000;
background: var(--bg-tertiary);
border: 2px solid var(--border-ornate);
border-radius: 8px;
padding: 1rem;
max-width: 300px;
box-shadow: var(--shadow-lg);
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
}
.skill-tooltip.visible {
opacity: 1;
}
.tooltip-header {
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-primary);
}
.tooltip-name {
font-family: var(--font-heading);
font-size: var(--text-lg);
color: var(--accent-gold);
margin: 0 0 0.25rem 0;
}
.tooltip-tier {
font-size: var(--text-xs);
color: var(--text-muted);
text-transform: uppercase;
}
.tooltip-description {
font-size: var(--text-sm);
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 0.75rem;
}
.tooltip-section {
margin-bottom: 0.75rem;
}
.tooltip-section:last-child {
margin-bottom: 0;
}
.tooltip-section-title {
font-size: var(--text-xs);
color: var(--accent-gold);
text-transform: uppercase;
font-weight: 600;
margin-bottom: 0.25rem;
}
.tooltip-bonuses {
list-style: none;
padding: 0;
margin: 0;
}
.tooltip-bonuses li {
font-size: var(--text-sm);
color: var(--accent-green);
padding: 0.125rem 0;
}
.tooltip-bonuses li::before {
content: '+ ';
}
.tooltip-prerequisite {
font-size: var(--text-sm);
color: var(--accent-red-light);
}
.tooltip-abilities {
list-style: none;
padding: 0;
margin: 0;
}
.tooltip-abilities li {
font-size: var(--text-sm);
color: var(--accent-blue);
padding: 0.125rem 0;
}
/* ===== MESSAGES ===== */
.skills-message {
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
text-align: center;
}
.skills-message--success {
background: rgba(39, 174, 96, 0.2);
border: 1px solid var(--accent-green);
color: var(--accent-green);
}
.skills-message--error {
background: rgba(192, 57, 43, 0.2);
border: 1px solid var(--accent-red);
color: var(--accent-red-light);
}
/* ===== RESPONSIVE ===== */
@media (max-width: 1024px) {
.skill-trees-grid {
grid-template-columns: 1fr;
}
.skills-header {
flex-direction: column;
align-items: flex-start;
}
.skills-info {
width: 100%;
justify-content: space-between;
}
}
@media (max-width: 768px) {
.skills-container {
padding: 1rem;
}
.skill-tree {
padding: 1rem;
}
.skill-nodes {
flex-direction: column;
align-items: center;
}
.skill-node {
min-width: 100%;
max-width: 200px;
}
.tier-label {
display: none;
}
.skills-legend {
flex-wrap: wrap;
gap: 1rem;
}
}
@media (max-width: 480px) {
.skills-header h1 {
font-size: var(--text-xl);
}
.skills-info {
flex-direction: column;
gap: 0.75rem;
}
.skill-points-display {
width: 100%;
justify-content: center;
}
.btn-respec {
width: 100%;
}
}

View File

@@ -0,0 +1,127 @@
{# Skills Container Partial - For HTMX updates #}
{# Calculate available skill points: level minus unlocked skills count #}
{% set skill_points = character.available_skill_points|default(character.level - (character.unlocked_skills|default([])|length), true) %}
<div class="skills-container" id="skills-container">
{# Back Link #}
<div class="skills-back-link">
<a href="{{ url_for('character_views.view_character', character_id=character.character_id) }}">
&larr; Back to {{ character.name }}
</a>
</div>
{# Header #}
<div class="skills-header">
<h1>{{ character.name }}'s Skill Trees</h1>
<div class="skills-info">
<div class="skill-points-display">
<span class="skill-points-label">Skill Points:</span>
<span class="skill-points-value">{{ skill_points }}</span>
</div>
{% set respec_cost = character.level * 100 %}
<button class="btn-respec"
hx-post="{{ url_for('character_views.respec_skills', character_id=character.character_id) }}"
hx-target="#skills-container"
hx-swap="outerHTML"
hx-confirm="Respec will reset all skills and cost {{ respec_cost }} gold. Continue?"
{% if character.gold < respec_cost or not character.unlocked_skills %}disabled{% endif %}>
Respec ({{ respec_cost }} gold)
</button>
</div>
</div>
{# Flash Messages #}
{% if message %}
<div class="skills-message skills-message--success">{{ message }}</div>
{% endif %}
{% if error %}
<div class="skills-message skills-message--error">{{ error }}</div>
{% endif %}
{# Skill Trees Grid #}
{% if player_class and player_class.skill_trees %}
<div class="skill-trees-grid">
{% for tree in player_class.skill_trees %}
<div class="skill-tree" data-tree-id="{{ tree.tree_id }}">
{# Tree Header #}
<div class="tree-header">
<h2 class="tree-name">{{ tree.name }}</h2>
<p class="tree-description">{{ tree.description }}</p>
</div>
{# Tree Diagram (Tiers 5 to 1, top to bottom) #}
<div class="tree-diagram">
{% for tier in range(5, 0, -1) %}
<div class="skill-tier" data-tier="{{ tier }}">
<span class="tier-label">Tier {{ tier }}</span>
<div class="skill-nodes">
{% for node in tree.nodes if node.tier == tier %}
{# Determine node state #}
{% set is_unlocked = node.skill_id in character.unlocked_skills %}
{% set prereqs_met = not node.prerequisites or (node.prerequisites | select('in', character.unlocked_skills) | list | length == node.prerequisites | length) %}
{% set has_lower_tier = tier == 1 or (tree.nodes | selectattr('tier', 'equalto', tier - 1) | selectattr('skill_id', 'in', character.unlocked_skills) | list | length > 0) %}
{% set can_unlock = not is_unlocked and prereqs_met and has_lower_tier and skill_points > 0 %}
{% set has_prereq = node.prerequisites | length > 0 %}
<div class="skill-node {% if is_unlocked %}skill-node--unlocked{% elif can_unlock %}skill-node--available{% else %}skill-node--locked{% endif %}{% if has_prereq %} skill-node--has-prereq{% endif %}"
data-skill-id="{{ node.skill_id }}"
data-tree-id="{{ tree.tree_id }}"
onmouseenter="showTooltip(event, '{{ node.skill_id }}')"
onmouseleave="hideTooltip()">
<div class="node-icon">
{% if is_unlocked %}
&#10003;
{% elif can_unlock %}
&#9671;
{% else %}
&#9670;
{% endif %}
</div>
<span class="node-name">{{ node.name }}</span>
{% if can_unlock %}
<button class="btn-unlock"
hx-post="{{ url_for('character_views.unlock_skill', character_id=character.character_id) }}"
hx-vals='{"skill_id": "{{ node.skill_id }}"}'
hx-target="#skills-container"
hx-swap="outerHTML"
onclick="event.stopPropagation()">
Unlock
</button>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{# Legend #}
<div class="skills-legend">
<div class="legend-item">
<span class="legend-icon legend-icon--unlocked">&#10003;</span>
<span>Unlocked</span>
</div>
<div class="legend-item">
<span class="legend-icon legend-icon--available">&#9671;</span>
<span>Available</span>
</div>
<div class="legend-item">
<span class="legend-icon legend-icon--locked">&#9670;</span>
<span>Locked</span>
</div>
</div>
{% else %}
<div class="skills-message skills-message--error">
Unable to load skill trees for this class.
</div>
{% endif %}
{# Tooltip Container #}
<div id="skill-tooltip" class="skill-tooltip"></div>
</div>

View File

@@ -0,0 +1,151 @@
{% extends "base.html" %}
{% block title %}Skill Trees - {{ character.name }} - Code of Conquest{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/skills.css') }}">
{% endblock %}
{% block content %}
{% include "character/partials/skills_container.html" %}
{# Embed skill data as JSON for tooltips #}
<script>
// Skill data for tooltips (embedded from server)
const skillData = {
{% if player_class and player_class.skill_trees %}
{% for tree in player_class.skill_trees %}
{% for node in tree.nodes %}
"{{ node.skill_id }}": {
name: "{{ node.name }}",
description: "{{ node.description | replace('"', '\\"') | replace('\n', ' ') }}",
tier: {{ node.tier }},
prerequisites: {{ node.prerequisites | tojson }},
effects: {{ node.effects | tojson if node.effects else '{}' }}
}{% if not loop.last %},{% endif %}
{% endfor %}{% if not loop.last %},{% endif %}
{% endfor %}
{% endif %}
};
// Character's unlocked skills
const unlockedSkills = {{ character.unlocked_skills | tojson }};
// Tooltip element
const tooltip = document.getElementById('skill-tooltip');
/**
* Show tooltip for a skill node
*/
function showTooltip(event, skillId) {
const skill = skillData[skillId];
if (!skill) return;
// Build tooltip HTML
let html = `
<div class="tooltip-header">
<h3 class="tooltip-name">${skill.name}</h3>
<span class="tooltip-tier">Tier ${skill.tier}</span>
</div>
<p class="tooltip-description">${skill.description}</p>
`;
// Stat bonuses
if (skill.effects && skill.effects.stat_bonuses) {
const bonuses = skill.effects.stat_bonuses;
const bonusList = Object.entries(bonuses)
.map(([stat, value]) => `<li>+${value} ${formatStatName(stat)}</li>`)
.join('');
html += `
<div class="tooltip-section">
<div class="tooltip-section-title">Stat Bonuses</div>
<ul class="tooltip-bonuses">${bonusList}</ul>
</div>
`;
}
// Abilities unlocked
if (skill.effects && skill.effects.abilities) {
const abilities = skill.effects.abilities;
const abilityList = abilities.map(a => `<li>${formatAbilityName(a)}</li>`).join('');
html += `
<div class="tooltip-section">
<div class="tooltip-section-title">Unlocks Ability</div>
<ul class="tooltip-abilities">${abilityList}</ul>
</div>
`;
}
// Prerequisites
if (skill.prerequisites && skill.prerequisites.length > 0) {
const prereqNames = skill.prerequisites.map(p => {
const prereqSkill = skillData[p];
const met = unlockedSkills.includes(p);
return `<span style="color: ${met ? 'var(--accent-green)' : 'var(--accent-red-light)'}">${prereqSkill ? prereqSkill.name : p}</span>`;
}).join(', ');
html += `
<div class="tooltip-section">
<p class="tooltip-prerequisite"><strong>Requires:</strong> ${prereqNames}</p>
</div>
`;
}
tooltip.innerHTML = html;
// Position tooltip
const rect = event.target.closest('.skill-node').getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
let left = rect.right + 10;
let top = rect.top;
// Check if tooltip goes off right edge
if (left + 300 > window.innerWidth) {
left = rect.left - 310;
}
// Check if tooltip goes off bottom edge
if (top + tooltipRect.height > window.innerHeight) {
top = window.innerHeight - tooltipRect.height - 10;
}
tooltip.style.left = left + 'px';
tooltip.style.top = top + 'px';
tooltip.classList.add('visible');
}
/**
* Hide tooltip
*/
function hideTooltip() {
tooltip.classList.remove('visible');
}
/**
* Format stat name for display
*/
function formatStatName(stat) {
const names = {
strength: 'Strength',
dexterity: 'Dexterity',
constitution: 'Constitution',
intelligence: 'Intelligence',
wisdom: 'Wisdom',
charisma: 'Charisma',
luck: 'Luck',
defense: 'Defense',
resistance: 'Resistance',
mana_points: 'Mana',
hit_points: 'Hit Points'
};
return names[stat] || stat.charAt(0).toUpperCase() + stat.slice(1).replace(/_/g, ' ');
}
/**
* Format ability name for display
*/
function formatAbilityName(ability) {
return ability.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
}
</script>
{% endblock %}

View File

@@ -82,7 +82,7 @@ Displays character stats, resource bars, and action buttons
</div> </div>
</div> </div>
{# Quick Actions (Inventory, Equipment, NPC, Travel) #} {# Quick Actions (Inventory, Equipment, Skills, NPC, Travel) #}
<div class="quick-actions"> <div class="quick-actions">
{# Inventory - Opens modal #} {# Inventory - Opens modal #}
<button class="action-btn action-btn--special" <button class="action-btn action-btn--special"
@@ -103,6 +103,16 @@ Displays character stats, resource bars, and action buttons
⚔️ Equipment & Gear ⚔️ Equipment & Gear
</button> </button>
{# Skill Trees - Direct link to skills page #}
<a class="action-btn action-btn--special"
href="{{ url_for('character_views.view_skills', character_id=character.character_id) }}">
&#127793; Skill Trees
{% set skill_pts = character.available_skill_points|default(character.level - (character.unlocked_skills|default([])|length), true) %}
{% if skill_pts > 0 %}
<span class="action-count action-count--highlight">({{ skill_pts }} pts)</span>
{% endif %}
</a>
{# Talk to NPC - Opens NPC accordion #} {# Talk to NPC - Opens NPC accordion #}
<button class="action-btn action-btn--special" <button class="action-btn action-btn--special"
hx-get="{{ url_for('game.npcs_accordion', session_id=session_id) }}" hx-get="{{ url_for('game.npcs_accordion', session_id=session_id) }}"