Merge branch 'feat/Phase4-Skilltrees' into dev
This commit is contained in:
34
api/app/data/abilities/absolute_zero.yaml
Normal file
34
api/app/data/abilities/absolute_zero.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/aimed_shot.yaml
Normal file
16
api/app/data/abilities/aimed_shot.yaml
Normal 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: []
|
||||||
25
api/app/data/abilities/arcane_brilliance.yaml
Normal file
25
api/app/data/abilities/arcane_brilliance.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/arcane_weakness.yaml
Normal file
25
api/app/data/abilities/arcane_weakness.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/army_of_the_dead.yaml
Normal file
25
api/app/data/abilities/army_of_the_dead.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/bestial_wrath.yaml
Normal file
25
api/app/data/abilities/bestial_wrath.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/blessed_sacrifice.yaml
Normal file
16
api/app/data/abilities/blessed_sacrifice.yaml
Normal 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: []
|
||||||
25
api/app/data/abilities/blizzard.yaml
Normal file
25
api/app/data/abilities/blizzard.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/cleanse.yaml
Normal file
16
api/app/data/abilities/cleanse.yaml
Normal 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: []
|
||||||
16
api/app/data/abilities/cleave.yaml
Normal file
16
api/app/data/abilities/cleave.yaml
Normal 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: []
|
||||||
25
api/app/data/abilities/confuse.yaml
Normal file
25
api/app/data/abilities/confuse.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/consecrated_ground.yaml
Normal file
25
api/app/data/abilities/consecrated_ground.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/consecration.yaml
Normal file
25
api/app/data/abilities/consecration.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/coordinated_attack.yaml
Normal file
16
api/app/data/abilities/coordinated_attack.yaml
Normal 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: []
|
||||||
16
api/app/data/abilities/corpse_explosion.yaml
Normal file
16
api/app/data/abilities/corpse_explosion.yaml
Normal 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: []
|
||||||
16
api/app/data/abilities/coup_de_grace.yaml
Normal file
16
api/app/data/abilities/coup_de_grace.yaml
Normal 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: []
|
||||||
25
api/app/data/abilities/curse_of_agony.yaml
Normal file
25
api/app/data/abilities/curse_of_agony.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/death_mark.yaml
Normal file
25
api/app/data/abilities/death_mark.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/death_pact.yaml
Normal file
25
api/app/data/abilities/death_pact.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/divine_aegis.yaml
Normal file
25
api/app/data/abilities/divine_aegis.yaml
Normal 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"
|
||||||
34
api/app/data/abilities/divine_blessing.yaml
Normal file
34
api/app/data/abilities/divine_blessing.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/divine_intervention.yaml
Normal file
16
api/app/data/abilities/divine_intervention.yaml
Normal 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: []
|
||||||
25
api/app/data/abilities/divine_storm.yaml
Normal file
25
api/app/data/abilities/divine_storm.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/drain_life.yaml
Normal file
25
api/app/data/abilities/drain_life.yaml
Normal 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"
|
||||||
34
api/app/data/abilities/epidemic.yaml
Normal file
34
api/app/data/abilities/epidemic.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/execute.yaml
Normal file
16
api/app/data/abilities/execute.yaml
Normal 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: []
|
||||||
25
api/app/data/abilities/explosive_shot.yaml
Normal file
25
api/app/data/abilities/explosive_shot.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/firestorm.yaml
Normal file
25
api/app/data/abilities/firestorm.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/flame_burst.yaml
Normal file
16
api/app/data/abilities/flame_burst.yaml
Normal 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: []
|
||||||
25
api/app/data/abilities/frozen_orb.yaml
Normal file
25
api/app/data/abilities/frozen_orb.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/glacial_spike.yaml
Normal file
25
api/app/data/abilities/glacial_spike.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/guardian_angel.yaml
Normal file
25
api/app/data/abilities/guardian_angel.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/hammer_of_justice.yaml
Normal file
25
api/app/data/abilities/hammer_of_justice.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/haste.yaml
Normal file
25
api/app/data/abilities/haste.yaml
Normal 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"
|
||||||
@@ -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
|
||||||
|
|||||||
25
api/app/data/abilities/holy_fire.yaml
Normal file
25
api/app/data/abilities/holy_fire.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/holy_shield.yaml
Normal file
25
api/app/data/abilities/holy_shield.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/ice_shard.yaml
Normal file
25
api/app/data/abilities/ice_shard.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/inferno.yaml
Normal file
25
api/app/data/abilities/inferno.yaml
Normal 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"
|
||||||
34
api/app/data/abilities/last_stand.yaml
Normal file
34
api/app/data/abilities/last_stand.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/lay_on_hands.yaml
Normal file
25
api/app/data/abilities/lay_on_hands.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/mass_confusion.yaml
Normal file
25
api/app/data/abilities/mass_confusion.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/mass_domination.yaml
Normal file
25
api/app/data/abilities/mass_domination.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/mass_enhancement.yaml
Normal file
25
api/app/data/abilities/mass_enhancement.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/mass_heal.yaml
Normal file
25
api/app/data/abilities/mass_heal.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/mesmerize.yaml
Normal file
25
api/app/data/abilities/mesmerize.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/miracle.yaml
Normal file
25
api/app/data/abilities/miracle.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/mirror_image.yaml
Normal file
25
api/app/data/abilities/mirror_image.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/multishot.yaml
Normal file
16
api/app/data/abilities/multishot.yaml
Normal 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: []
|
||||||
25
api/app/data/abilities/phantasmal_killer.yaml
Normal file
25
api/app/data/abilities/phantasmal_killer.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/piercing_shot.yaml
Normal file
25
api/app/data/abilities/piercing_shot.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/plague.yaml
Normal file
25
api/app/data/abilities/plague.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/power_strike.yaml
Normal file
16
api/app/data/abilities/power_strike.yaml
Normal 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: []
|
||||||
16
api/app/data/abilities/precise_strike.yaml
Normal file
16
api/app/data/abilities/precise_strike.yaml
Normal 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: []
|
||||||
16
api/app/data/abilities/primal_fury.yaml
Normal file
16
api/app/data/abilities/primal_fury.yaml
Normal 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: []
|
||||||
25
api/app/data/abilities/rain_of_arrows.yaml
Normal file
25
api/app/data/abilities/rain_of_arrows.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/raise_ghoul.yaml
Normal file
25
api/app/data/abilities/raise_ghoul.yaml
Normal 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"
|
||||||
34
api/app/data/abilities/reality_shift.yaml
Normal file
34
api/app/data/abilities/reality_shift.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/rending_blow.yaml
Normal file
25
api/app/data/abilities/rending_blow.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/resurrection.yaml
Normal file
16
api/app/data/abilities/resurrection.yaml
Normal 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: []
|
||||||
16
api/app/data/abilities/riposte.yaml
Normal file
16
api/app/data/abilities/riposte.yaml
Normal 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: []
|
||||||
25
api/app/data/abilities/shadow_assault.yaml
Normal file
25
api/app/data/abilities/shadow_assault.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/shadowstep.yaml
Normal file
16
api/app/data/abilities/shadowstep.yaml
Normal 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: []
|
||||||
25
api/app/data/abilities/shield_of_faith.yaml
Normal file
25
api/app/data/abilities/shield_of_faith.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/shield_wall.yaml
Normal file
25
api/app/data/abilities/shield_wall.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/smite.yaml
Normal file
16
api/app/data/abilities/smite.yaml
Normal 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: []
|
||||||
25
api/app/data/abilities/smoke_bomb.yaml
Normal file
25
api/app/data/abilities/smoke_bomb.yaml
Normal 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"
|
||||||
34
api/app/data/abilities/soul_rot.yaml
Normal file
34
api/app/data/abilities/soul_rot.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/stampede.yaml
Normal file
25
api/app/data/abilities/stampede.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/summon_abomination.yaml
Normal file
25
api/app/data/abilities/summon_abomination.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/summon_companion.yaml
Normal file
25
api/app/data/abilities/summon_companion.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/summon_skeleton.yaml
Normal file
25
api/app/data/abilities/summon_skeleton.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/sun_burst.yaml
Normal file
25
api/app/data/abilities/sun_burst.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/taunt.yaml
Normal file
25
api/app/data/abilities/taunt.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/thousand_cuts.yaml
Normal file
25
api/app/data/abilities/thousand_cuts.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/time_warp.yaml
Normal file
25
api/app/data/abilities/time_warp.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/titans_wrath.yaml
Normal file
25
api/app/data/abilities/titans_wrath.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/unbreakable.yaml
Normal file
25
api/app/data/abilities/unbreakable.yaml
Normal 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"
|
||||||
25
api/app/data/abilities/vanish.yaml
Normal file
25
api/app/data/abilities/vanish.yaml
Normal 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"
|
||||||
16
api/app/data/abilities/vital_strike.yaml
Normal file
16
api/app/data/abilities/vital_strike.yaml
Normal 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: []
|
||||||
16
api/app/data/abilities/word_of_healing.yaml
Normal file
16
api/app/data/abilities/word_of_healing.yaml
Normal 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: []
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
479
api/docs/SKILLS_AND_ABILITIES.md
Normal file
479
api/docs/SKILLS_AND_ABILITIES.md
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
467
docs/PHASE4b.md
467
docs/PHASE4b.md
@@ -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
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
|
|||||||
531
public_web/static/css/skills.css
Normal file
531
public_web/static/css/skills.css
Normal 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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
127
public_web/templates/character/partials/skills_container.html
Normal file
127
public_web/templates/character/partials/skills_container.html
Normal 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) }}">
|
||||||
|
← 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 %}
|
||||||
|
✓
|
||||||
|
{% elif can_unlock %}
|
||||||
|
◇
|
||||||
|
{% else %}
|
||||||
|
◆
|
||||||
|
{% 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">✓</span>
|
||||||
|
<span>Unlocked</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-icon legend-icon--available">◇</span>
|
||||||
|
<span>Available</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-icon legend-icon--locked">◆</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>
|
||||||
151
public_web/templates/character/skills.html
Normal file
151
public_web/templates/character/skills.html
Normal 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 %}
|
||||||
@@ -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) }}">
|
||||||
|
🌱 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) }}"
|
||||||
|
|||||||
Reference in New Issue
Block a user