feat(backend): Implement base framework for Perks, Skills, and Derived Stats
This commit is contained in:
@@ -79,8 +79,9 @@ class InitiateCombatRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CombatActionRequest(BaseModel):
|
class CombatActionRequest(BaseModel):
|
||||||
action: str # 'attack', 'defend', 'flee', 'use_item'
|
action: str # 'attack', 'skill', 'flee', 'use_item'
|
||||||
item_id: Optional[str] = None # For use_item action
|
item_id: Optional[str] = None # For use_item action
|
||||||
|
skill_id: Optional[str] = None # For skill action
|
||||||
|
|
||||||
|
|
||||||
class PvPCombatInitiateRequest(BaseModel):
|
class PvPCombatInitiateRequest(BaseModel):
|
||||||
|
|||||||
178
api/services/skills.py
Normal file
178
api/services/skills.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
"""
|
||||||
|
Skills service - loads skill definitions and provides skill availability logic.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class Skill:
|
||||||
|
"""Represents a combat skill."""
|
||||||
|
def __init__(self, skill_id: str, data: Dict[str, Any]):
|
||||||
|
self.id = skill_id
|
||||||
|
self.name = data.get('name', skill_id)
|
||||||
|
self.description = data.get('description', '')
|
||||||
|
self.icon = data.get('icon', '⚔️')
|
||||||
|
self.stat_requirement = data.get('stat_requirement', 'strength')
|
||||||
|
self.stat_threshold = data.get('stat_threshold', 0)
|
||||||
|
self.level_requirement = data.get('level_requirement', 1)
|
||||||
|
self.cooldown = data.get('cooldown', 3)
|
||||||
|
self.stamina_cost = data.get('stamina_cost', 5)
|
||||||
|
self.effects = data.get('effects', {})
|
||||||
|
|
||||||
|
|
||||||
|
class SkillsManager:
|
||||||
|
"""Loads and manages skill definitions from JSON."""
|
||||||
|
|
||||||
|
def __init__(self, gamedata_path: str = "./gamedata"):
|
||||||
|
self.gamedata_path = Path(gamedata_path)
|
||||||
|
self.skills: Dict[str, Skill] = {}
|
||||||
|
self.load_skills()
|
||||||
|
|
||||||
|
def load_skills(self):
|
||||||
|
"""Load skills from skills.json."""
|
||||||
|
json_path = self.gamedata_path / 'skills.json'
|
||||||
|
try:
|
||||||
|
with open(json_path, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
for skill_id, skill_data in data.get('skills', {}).items():
|
||||||
|
self.skills[skill_id] = Skill(skill_id, skill_data)
|
||||||
|
|
||||||
|
print(f"⚔️ Loaded {len(self.skills)} skills")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("⚠️ skills.json not found")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Error loading skills.json: {e}")
|
||||||
|
|
||||||
|
def get_skill(self, skill_id: str) -> Optional[Skill]:
|
||||||
|
"""Get a skill by ID."""
|
||||||
|
return self.skills.get(skill_id)
|
||||||
|
|
||||||
|
def get_all_skills(self) -> Dict[str, Skill]:
|
||||||
|
"""Get all skills."""
|
||||||
|
return self.skills
|
||||||
|
|
||||||
|
def get_available_skills(self, character: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get all skills available to a character based on their stats and level.
|
||||||
|
Returns list of skill dicts with availability info.
|
||||||
|
"""
|
||||||
|
available = []
|
||||||
|
for skill_id, skill in self.skills.items():
|
||||||
|
stat_value = character.get(skill.stat_requirement, 0)
|
||||||
|
level = character.get('level', 1)
|
||||||
|
|
||||||
|
unlocked = (stat_value >= skill.stat_threshold and level >= skill.level_requirement)
|
||||||
|
|
||||||
|
skill_info = {
|
||||||
|
"id": skill.id,
|
||||||
|
"name": skill.name,
|
||||||
|
"description": skill.description,
|
||||||
|
"icon": skill.icon,
|
||||||
|
"stat_requirement": skill.stat_requirement,
|
||||||
|
"stat_threshold": skill.stat_threshold,
|
||||||
|
"level_requirement": skill.level_requirement,
|
||||||
|
"cooldown": skill.cooldown,
|
||||||
|
"stamina_cost": skill.stamina_cost,
|
||||||
|
"unlocked": unlocked,
|
||||||
|
"effects": skill.effects,
|
||||||
|
}
|
||||||
|
available.append(skill_info)
|
||||||
|
|
||||||
|
return available
|
||||||
|
|
||||||
|
|
||||||
|
class Perk:
|
||||||
|
"""Represents a passive perk."""
|
||||||
|
def __init__(self, perk_id: str, data: Dict[str, Any]):
|
||||||
|
self.id = perk_id
|
||||||
|
self.name = data.get('name', perk_id)
|
||||||
|
self.description = data.get('description', '')
|
||||||
|
self.icon = data.get('icon', '⭐')
|
||||||
|
self.requirements = data.get('requirements', {})
|
||||||
|
self.effects = data.get('effects', {})
|
||||||
|
|
||||||
|
|
||||||
|
class PerksManager:
|
||||||
|
"""Loads and manages perk definitions from JSON."""
|
||||||
|
|
||||||
|
def __init__(self, gamedata_path: str = "./gamedata"):
|
||||||
|
self.gamedata_path = Path(gamedata_path)
|
||||||
|
self.perks: Dict[str, Perk] = {}
|
||||||
|
self.load_perks()
|
||||||
|
|
||||||
|
def load_perks(self):
|
||||||
|
"""Load perks from perks.json."""
|
||||||
|
json_path = self.gamedata_path / 'perks.json'
|
||||||
|
try:
|
||||||
|
with open(json_path, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
for perk_id, perk_data in data.get('perks', {}).items():
|
||||||
|
self.perks[perk_id] = Perk(perk_id, perk_data)
|
||||||
|
|
||||||
|
print(f"⭐ Loaded {len(self.perks)} perks")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("⚠️ perks.json not found")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Error loading perks.json: {e}")
|
||||||
|
|
||||||
|
def get_perk(self, perk_id: str) -> Optional[Perk]:
|
||||||
|
"""Get a perk by ID."""
|
||||||
|
return self.perks.get(perk_id)
|
||||||
|
|
||||||
|
def get_all_perks(self) -> Dict[str, Perk]:
|
||||||
|
"""Get all perks."""
|
||||||
|
return self.perks
|
||||||
|
|
||||||
|
def check_requirements(self, perk: Perk, character: Dict[str, Any]) -> bool:
|
||||||
|
"""Check if a character meets a perk's requirements."""
|
||||||
|
for req_key, req_value in perk.requirements.items():
|
||||||
|
if req_key.endswith('_max'):
|
||||||
|
# Max constraint (e.g., endurance_max: 8 means END must be ≤ 8)
|
||||||
|
stat_name = req_key.replace('_max', '')
|
||||||
|
if character.get(stat_name, 0) > req_value:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# Min constraint
|
||||||
|
if character.get(req_key, 0) < req_value:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_available_perks(self, character: Dict[str, Any], owned_perk_ids: List[str]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get all perks with availability status for a character.
|
||||||
|
"""
|
||||||
|
available = []
|
||||||
|
for perk_id, perk in self.perks.items():
|
||||||
|
meets_requirements = self.check_requirements(perk, character)
|
||||||
|
owned = perk_id in owned_perk_ids
|
||||||
|
|
||||||
|
perk_info = {
|
||||||
|
"id": perk.id,
|
||||||
|
"name": perk.name,
|
||||||
|
"description": perk.description,
|
||||||
|
"icon": perk.icon,
|
||||||
|
"requirements": perk.requirements,
|
||||||
|
"effects": perk.effects,
|
||||||
|
"meets_requirements": meets_requirements,
|
||||||
|
"owned": owned,
|
||||||
|
}
|
||||||
|
available.append(perk_info)
|
||||||
|
|
||||||
|
return available
|
||||||
|
|
||||||
|
|
||||||
|
# Perk points per level
|
||||||
|
PERK_POINT_INTERVAL = 5 # Every 5 levels
|
||||||
|
|
||||||
|
|
||||||
|
def get_total_perk_points(level: int) -> int:
|
||||||
|
"""Calculate total perk points available for a given level."""
|
||||||
|
return level // PERK_POINT_INTERVAL
|
||||||
|
|
||||||
|
|
||||||
|
# Global instances
|
||||||
|
skills_manager = SkillsManager()
|
||||||
|
perks_manager = PerksManager()
|
||||||
225
api/services/stats.py
Normal file
225
api/services/stats.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
"""
|
||||||
|
Central stat calculation service.
|
||||||
|
All derived stats are computed here from base attributes + equipment + buffs.
|
||||||
|
Results are cached in Redis and invalidated on any change.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
from .. import database as db
|
||||||
|
from ..items import items_manager as ITEMS_MANAGER
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Game Config (raise per expansion) ───
|
||||||
|
STAT_CAP = 50 # Max base stat points per attribute
|
||||||
|
MAX_LEVEL = 60 # Max character level
|
||||||
|
POINTS_PER_LEVEL = 1 # Stat points granted per level
|
||||||
|
|
||||||
|
|
||||||
|
async def calculate_derived_stats(character_id: int, redis_mgr=None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Calculate all derived stats for a character.
|
||||||
|
Checks Redis cache first; if miss, computes from DB and caches result.
|
||||||
|
|
||||||
|
Returns dict with all derived stat values.
|
||||||
|
"""
|
||||||
|
# 1. Check Redis cache
|
||||||
|
if redis_mgr and redis_mgr.redis_client:
|
||||||
|
try:
|
||||||
|
cached = await redis_mgr.redis_client.get(f"stats:{character_id}")
|
||||||
|
if cached:
|
||||||
|
return json.loads(cached)
|
||||||
|
except Exception:
|
||||||
|
pass # Graceful degradation — recalculate if Redis fails
|
||||||
|
|
||||||
|
# 2. Fetch data from DB
|
||||||
|
char = await db.get_player_by_id(character_id)
|
||||||
|
if not char:
|
||||||
|
return _empty_stats()
|
||||||
|
|
||||||
|
equipment = await db.get_all_equipment(character_id)
|
||||||
|
effects = await db.get_player_effects(character_id)
|
||||||
|
|
||||||
|
# 3. Fetch owned perks
|
||||||
|
owned_perks = await db.get_character_perks(character_id)
|
||||||
|
owned_perk_ids = [row['perk_id'] for row in owned_perks]
|
||||||
|
|
||||||
|
# 4. Compute derived stats
|
||||||
|
stats = _compute_stats(char, equipment, effects, owned_perk_ids)
|
||||||
|
|
||||||
|
# 5. Cache in Redis (5 min TTL)
|
||||||
|
if redis_mgr and redis_mgr.redis_client:
|
||||||
|
try:
|
||||||
|
await redis_mgr.redis_client.setex(
|
||||||
|
f"stats:{character_id}", 300, json.dumps(stats)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_stats(char: Dict[str, Any], equipment: Dict[str, Any], effects: List[Dict], perk_ids: List[str] = None) -> Dict[str, Any]:
|
||||||
|
"""Pure computation of derived stats from base data."""
|
||||||
|
strength = char.get('strength', 0)
|
||||||
|
agility = char.get('agility', 0)
|
||||||
|
endurance = char.get('endurance', 0)
|
||||||
|
intellect = char.get('intellect', 0)
|
||||||
|
level = char.get('level', 1)
|
||||||
|
if perk_ids is None:
|
||||||
|
perk_ids = []
|
||||||
|
|
||||||
|
# ─── Base derived stats from attributes ───
|
||||||
|
attack_power = 5 + int(strength * 1.5) + level
|
||||||
|
crit_chance = 0.05 + (agility * 0.005)
|
||||||
|
crit_damage = 1.5 + (strength * 0.01)
|
||||||
|
dodge_chance = min(0.25, 0.02 + (agility * 0.005)) # Cap 25%
|
||||||
|
flee_chance_base = 0.4 + (agility * 0.01)
|
||||||
|
max_hp = 30 + (endurance * 5) + (level * 3)
|
||||||
|
max_stamina = 20 + (endurance * 2) + level
|
||||||
|
status_resistance = endurance * 0.01
|
||||||
|
block_chance = 0.0
|
||||||
|
item_effectiveness = 1.0 + (intellect * 0.02)
|
||||||
|
xp_bonus = 1.0 + (intellect * 0.01)
|
||||||
|
loot_quality = 1.0 + (intellect * 0.005)
|
||||||
|
crafting_bonus = intellect * 0.01
|
||||||
|
carry_weight = 10.0 + (strength * 0.5)
|
||||||
|
|
||||||
|
# ─── Equipment bonuses ───
|
||||||
|
total_armor = 0
|
||||||
|
weapon_crit = 0.0
|
||||||
|
weapon_damage_min = 0
|
||||||
|
weapon_damage_max = 0
|
||||||
|
has_shield = False
|
||||||
|
|
||||||
|
for slot, item_data in equipment.items():
|
||||||
|
if not item_data or not item_data.get('item_id'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get inventory item to find the item definition
|
||||||
|
inv_item_sync = item_data # equipment dict already has item_id reference
|
||||||
|
item_def = ITEMS_MANAGER.get_item(inv_item_sync.get('item_id', ''))
|
||||||
|
|
||||||
|
# Try to get item_id from the inventory item if the direct lookup failed
|
||||||
|
if not item_def:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item_def.stats:
|
||||||
|
total_armor += item_def.stats.get('armor', 0)
|
||||||
|
weapon_crit += item_def.stats.get('crit_chance', 0)
|
||||||
|
|
||||||
|
if slot == 'weapon':
|
||||||
|
weapon_damage_min = item_def.stats.get('damage_min', 0)
|
||||||
|
weapon_damage_max = item_def.stats.get('damage_max', 0)
|
||||||
|
|
||||||
|
if slot == 'offhand':
|
||||||
|
has_shield = True
|
||||||
|
|
||||||
|
# Apply equipment to derived stats
|
||||||
|
crit_chance += weapon_crit
|
||||||
|
armor_reduction = total_armor / (total_armor + 50) if total_armor > 0 else 0.0
|
||||||
|
|
||||||
|
if has_shield:
|
||||||
|
block_chance = strength * 0.003
|
||||||
|
|
||||||
|
# ─── Buff effects ───
|
||||||
|
for effect in effects:
|
||||||
|
effect_name = effect.get('effect_name', '')
|
||||||
|
value = effect.get('value', 0)
|
||||||
|
# Future: apply buff modifiers here
|
||||||
|
|
||||||
|
# ─── Perk passive bonuses ───
|
||||||
|
if 'thick_skin' in perk_ids:
|
||||||
|
max_hp = int(max_hp * 1.10) # +10% max HP
|
||||||
|
if 'lucky_strike' in perk_ids:
|
||||||
|
crit_chance += 0.05 # +5% crit chance
|
||||||
|
if 'quick_learner' in perk_ids:
|
||||||
|
xp_bonus *= 1.15 # +15% XP
|
||||||
|
if 'glass_cannon' in perk_ids:
|
||||||
|
attack_power = int(attack_power * 1.30) # +30% attack
|
||||||
|
max_hp = int(max_hp * 0.80) # -20% HP
|
||||||
|
if 'survivor' in perk_ids:
|
||||||
|
max_hp = int(max_hp * 1.02) # Small HP boost from regen perk
|
||||||
|
if 'scavenger' in perk_ids:
|
||||||
|
loot_quality *= 1.10 # +10% loot quality
|
||||||
|
if 'fleet_footed' in perk_ids:
|
||||||
|
# Travel stamina reduction tracked for movement system
|
||||||
|
pass
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
# Core combat
|
||||||
|
"attack_power": attack_power,
|
||||||
|
"crit_chance": round(crit_chance, 4),
|
||||||
|
"crit_damage": round(crit_damage, 2),
|
||||||
|
"dodge_chance": round(dodge_chance, 4),
|
||||||
|
"flee_chance_base": round(flee_chance_base, 2),
|
||||||
|
# Vitals
|
||||||
|
"max_hp": max_hp,
|
||||||
|
"max_stamina": max_stamina,
|
||||||
|
# Defense
|
||||||
|
"total_armor": total_armor,
|
||||||
|
"armor_reduction": round(armor_reduction, 4),
|
||||||
|
"block_chance": round(block_chance, 4),
|
||||||
|
"status_resistance": round(status_resistance, 4),
|
||||||
|
# Utility
|
||||||
|
"item_effectiveness": round(item_effectiveness, 2),
|
||||||
|
"xp_bonus": round(xp_bonus, 2),
|
||||||
|
"loot_quality": round(loot_quality, 3),
|
||||||
|
"crafting_bonus": round(crafting_bonus, 2),
|
||||||
|
"carry_weight": round(carry_weight, 1),
|
||||||
|
# Weapon info
|
||||||
|
"weapon_damage_min": weapon_damage_min,
|
||||||
|
"weapon_damage_max": weapon_damage_max,
|
||||||
|
"has_shield": has_shield,
|
||||||
|
# Perk flags
|
||||||
|
"has_last_stand": 'last_stand' in perk_ids,
|
||||||
|
"has_resilient": 'resilient' in perk_ids,
|
||||||
|
"has_iron_fist": 'iron_fist' in perk_ids,
|
||||||
|
"has_heavy_hitter": 'heavy_hitter' in perk_ids,
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_stats() -> Dict[str, Any]:
|
||||||
|
"""Default stats for error cases."""
|
||||||
|
return {
|
||||||
|
"attack_power": 5,
|
||||||
|
"crit_chance": 0.05,
|
||||||
|
"crit_damage": 1.5,
|
||||||
|
"dodge_chance": 0.02,
|
||||||
|
"flee_chance_base": 0.4,
|
||||||
|
"max_hp": 30,
|
||||||
|
"max_stamina": 20,
|
||||||
|
"total_armor": 0,
|
||||||
|
"armor_reduction": 0.0,
|
||||||
|
"block_chance": 0.0,
|
||||||
|
"status_resistance": 0.0,
|
||||||
|
"item_effectiveness": 1.0,
|
||||||
|
"xp_bonus": 1.0,
|
||||||
|
"loot_quality": 1.0,
|
||||||
|
"crafting_bonus": 0.0,
|
||||||
|
"carry_weight": 10.0,
|
||||||
|
"weapon_damage_min": 0,
|
||||||
|
"weapon_damage_max": 0,
|
||||||
|
"has_shield": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def invalidate_stats_cache(character_id: int, redis_mgr=None):
|
||||||
|
"""
|
||||||
|
Delete cached stats for a character. Call this whenever:
|
||||||
|
- Equipment changes (equip/unequip/break)
|
||||||
|
- Stat points allocated
|
||||||
|
- Level up
|
||||||
|
- Buff applied/expired
|
||||||
|
"""
|
||||||
|
if redis_mgr and redis_mgr.redis_client:
|
||||||
|
try:
|
||||||
|
await redis_mgr.redis_client.delete(f"stats:{character_id}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_flee_chance(flee_chance_base: float, enemy_level: int) -> float:
|
||||||
|
"""Calculate actual flee chance against a specific enemy."""
|
||||||
|
return max(0.1, min(0.9, flee_chance_base - (enemy_level * 0.02)))
|
||||||
194
gamedata/perks.json
Normal file
194
gamedata/perks.json
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
{
|
||||||
|
"perks": {
|
||||||
|
"heavy_hitter": {
|
||||||
|
"name": {
|
||||||
|
"en": "Heavy Hitter",
|
||||||
|
"es": "Golpe Pesado"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "+10% damage with two-handed weapons",
|
||||||
|
"es": "+10% de daño con armas a dos manos"
|
||||||
|
},
|
||||||
|
"icon": "🔨",
|
||||||
|
"requirements": {
|
||||||
|
"strength": 10
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"two_handed_damage_bonus": 0.1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"iron_fist": {
|
||||||
|
"name": {
|
||||||
|
"en": "Iron Fist",
|
||||||
|
"es": "Puño de Hierro"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Unarmed attacks deal STR × 1 damage",
|
||||||
|
"es": "Los ataques sin armas hacen STR × 1 de daño"
|
||||||
|
},
|
||||||
|
"icon": "👊",
|
||||||
|
"requirements": {
|
||||||
|
"strength": 20
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"unarmed_str_scaling": 1.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fleet_footed": {
|
||||||
|
"name": {
|
||||||
|
"en": "Fleet Footed",
|
||||||
|
"es": "Pies Ligeros"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "-20% stamina cost on travel",
|
||||||
|
"es": "-20% de coste de aguante al viajar"
|
||||||
|
},
|
||||||
|
"icon": "🏃",
|
||||||
|
"requirements": {
|
||||||
|
"agility": 10
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"travel_stamina_reduction": 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lucky_strike": {
|
||||||
|
"name": {
|
||||||
|
"en": "Lucky Strike",
|
||||||
|
"es": "Golpe de Suerte"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "+5% crit chance",
|
||||||
|
"es": "+5% de probabilidad de crítico"
|
||||||
|
},
|
||||||
|
"icon": "🍀",
|
||||||
|
"requirements": {
|
||||||
|
"agility": 20
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"crit_chance_bonus": 0.05
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"thick_skin": {
|
||||||
|
"name": {
|
||||||
|
"en": "Thick Skin",
|
||||||
|
"es": "Piel Gruesa"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "+10% max HP",
|
||||||
|
"es": "+10% de vida máxima"
|
||||||
|
},
|
||||||
|
"icon": "🛡️",
|
||||||
|
"requirements": {
|
||||||
|
"endurance": 10
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"max_hp_bonus_percent": 0.1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resilient": {
|
||||||
|
"name": {
|
||||||
|
"en": "Resilient",
|
||||||
|
"es": "Resistente"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Status effects last 1 fewer turn (min 1)",
|
||||||
|
"es": "Los efectos de estado duran 1 turno menos (mín 1)"
|
||||||
|
},
|
||||||
|
"icon": "💪",
|
||||||
|
"requirements": {
|
||||||
|
"endurance": 20
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"status_duration_reduction": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quick_learner": {
|
||||||
|
"name": {
|
||||||
|
"en": "Quick Learner",
|
||||||
|
"es": "Aprendiz Rápido"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "+15% XP gain",
|
||||||
|
"es": "+15% de experiencia ganada"
|
||||||
|
},
|
||||||
|
"icon": "📖",
|
||||||
|
"requirements": {
|
||||||
|
"intellect": 10
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"xp_bonus": 0.15
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scavenger": {
|
||||||
|
"name": {
|
||||||
|
"en": "Scavenger",
|
||||||
|
"es": "Carroñero"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "+1 quantity on consumable/resource drops",
|
||||||
|
"es": "+1 cantidad en drops de consumibles/recursos"
|
||||||
|
},
|
||||||
|
"icon": "🦅",
|
||||||
|
"requirements": {
|
||||||
|
"intellect": 20
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"consumable_loot_bonus": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"survivor": {
|
||||||
|
"name": {
|
||||||
|
"en": "Survivor",
|
||||||
|
"es": "Superviviente"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Heal 2% max HP every combat turn",
|
||||||
|
"es": "Cura 2% de vida máxima cada turno de combate"
|
||||||
|
},
|
||||||
|
"icon": "❤️🩹",
|
||||||
|
"requirements": {
|
||||||
|
"endurance": 15,
|
||||||
|
"agility": 10
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"combat_regen_percent": 0.02
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"glass_cannon": {
|
||||||
|
"name": {
|
||||||
|
"en": "Glass Cannon",
|
||||||
|
"es": "Cañón de Cristal"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "+30% damage, -20% max HP",
|
||||||
|
"es": "+30% de daño, -20% de vida máxima"
|
||||||
|
},
|
||||||
|
"icon": "💣",
|
||||||
|
"requirements": {
|
||||||
|
"strength": 20,
|
||||||
|
"endurance_max": 8
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"damage_bonus": 0.3,
|
||||||
|
"max_hp_penalty_percent": 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"last_stand": {
|
||||||
|
"name": {
|
||||||
|
"en": "Last Stand",
|
||||||
|
"es": "Última Resistencia"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Once per combat, survive lethal damage with 1 HP",
|
||||||
|
"es": "Una vez por combate, sobrevive daño letal con 1 de vida"
|
||||||
|
},
|
||||||
|
"icon": "💀",
|
||||||
|
"requirements": {
|
||||||
|
"endurance": 30
|
||||||
|
},
|
||||||
|
"effects": {
|
||||||
|
"cheat_death": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
332
gamedata/skills.json
Normal file
332
gamedata/skills.json
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
{
|
||||||
|
"skills": {
|
||||||
|
"power_strike": {
|
||||||
|
"name": {
|
||||||
|
"en": "Power Strike",
|
||||||
|
"es": "Golpe Poderoso"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "A devastating blow with 20% chance to stun",
|
||||||
|
"es": "Un golpe devastador con 20% de probabilidad de aturdir"
|
||||||
|
},
|
||||||
|
"icon": "💥",
|
||||||
|
"stat_requirement": "strength",
|
||||||
|
"stat_threshold": 8,
|
||||||
|
"level_requirement": 5,
|
||||||
|
"cooldown": 3,
|
||||||
|
"stamina_cost": 5,
|
||||||
|
"effects": {
|
||||||
|
"damage_multiplier": 1.8,
|
||||||
|
"stun_chance": 0.2,
|
||||||
|
"stun_duration": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"crushing_blow": {
|
||||||
|
"name": {
|
||||||
|
"en": "Crushing Blow",
|
||||||
|
"es": "Golpe Aplastante"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Heavy strike that ignores 50% of enemy armor",
|
||||||
|
"es": "Golpe pesado que ignora el 50% de la armadura enemiga"
|
||||||
|
},
|
||||||
|
"icon": "🔨",
|
||||||
|
"stat_requirement": "strength",
|
||||||
|
"stat_threshold": 15,
|
||||||
|
"level_requirement": 12,
|
||||||
|
"cooldown": 4,
|
||||||
|
"stamina_cost": 7,
|
||||||
|
"effects": {
|
||||||
|
"damage_multiplier": 1.5,
|
||||||
|
"armor_penetration": 0.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"berserker_rage": {
|
||||||
|
"name": {
|
||||||
|
"en": "Berserker Rage",
|
||||||
|
"es": "Furia Berserker"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "+50% damage for 3 turns, but +25% damage taken",
|
||||||
|
"es": "+50% de daño durante 3 turnos, pero +25% de daño recibido"
|
||||||
|
},
|
||||||
|
"icon": "🔥",
|
||||||
|
"stat_requirement": "strength",
|
||||||
|
"stat_threshold": 25,
|
||||||
|
"level_requirement": 20,
|
||||||
|
"cooldown": 6,
|
||||||
|
"stamina_cost": 10,
|
||||||
|
"effects": {
|
||||||
|
"buff": "berserker_rage",
|
||||||
|
"buff_duration": 3,
|
||||||
|
"damage_bonus": 0.5,
|
||||||
|
"damage_taken_increase": 0.25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
"name": {
|
||||||
|
"en": "Execution",
|
||||||
|
"es": "Ejecución"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "300% damage if target below 25% HP, else 100%",
|
||||||
|
"es": "300% de daño si el objetivo está por debajo del 25% de vida, si no 100%"
|
||||||
|
},
|
||||||
|
"icon": "⚰️",
|
||||||
|
"stat_requirement": "strength",
|
||||||
|
"stat_threshold": 40,
|
||||||
|
"level_requirement": 35,
|
||||||
|
"cooldown": 8,
|
||||||
|
"stamina_cost": 8,
|
||||||
|
"effects": {
|
||||||
|
"damage_multiplier": 1.0,
|
||||||
|
"execute_threshold": 0.25,
|
||||||
|
"execute_multiplier": 3.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quick_slash": {
|
||||||
|
"name": {
|
||||||
|
"en": "Quick Slash",
|
||||||
|
"es": "Tajo Rápido"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Attack twice at 60% power each",
|
||||||
|
"es": "Ataca dos veces al 60% de poder cada una"
|
||||||
|
},
|
||||||
|
"icon": "🗡️",
|
||||||
|
"stat_requirement": "agility",
|
||||||
|
"stat_threshold": 8,
|
||||||
|
"level_requirement": 5,
|
||||||
|
"cooldown": 2,
|
||||||
|
"stamina_cost": 3,
|
||||||
|
"effects": {
|
||||||
|
"hits": 2,
|
||||||
|
"damage_multiplier": 0.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"evade": {
|
||||||
|
"name": {
|
||||||
|
"en": "Evade",
|
||||||
|
"es": "Evadir"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Guaranteed dodge on the next incoming attack",
|
||||||
|
"es": "Esquivar garantizado en el próximo ataque recibido"
|
||||||
|
},
|
||||||
|
"icon": "🏃",
|
||||||
|
"stat_requirement": "agility",
|
||||||
|
"stat_threshold": 15,
|
||||||
|
"level_requirement": 12,
|
||||||
|
"cooldown": 3,
|
||||||
|
"stamina_cost": 4,
|
||||||
|
"effects": {
|
||||||
|
"buff": "evade",
|
||||||
|
"buff_duration": 1,
|
||||||
|
"guaranteed_dodge": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"poisoned_blade": {
|
||||||
|
"name": {
|
||||||
|
"en": "Poisoned Blade",
|
||||||
|
"es": "Hoja Envenenada"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "80% damage + poison (3 dmg/turn for 4 turns)",
|
||||||
|
"es": "80% de daño + veneno (3 de daño/turno durante 4 turnos)"
|
||||||
|
},
|
||||||
|
"icon": "🧪",
|
||||||
|
"stat_requirement": "agility",
|
||||||
|
"stat_threshold": 25,
|
||||||
|
"level_requirement": 20,
|
||||||
|
"cooldown": 5,
|
||||||
|
"stamina_cost": 6,
|
||||||
|
"effects": {
|
||||||
|
"damage_multiplier": 0.8,
|
||||||
|
"poison_damage": 3,
|
||||||
|
"poison_duration": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shadow_strike": {
|
||||||
|
"name": {
|
||||||
|
"en": "Shadow Strike",
|
||||||
|
"es": "Golpe Sombrío"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "250% damage, guaranteed critical hit",
|
||||||
|
"es": "250% de daño, golpe crítico garantizado"
|
||||||
|
},
|
||||||
|
"icon": "🌑",
|
||||||
|
"stat_requirement": "agility",
|
||||||
|
"stat_threshold": 40,
|
||||||
|
"level_requirement": 35,
|
||||||
|
"cooldown": 7,
|
||||||
|
"stamina_cost": 8,
|
||||||
|
"effects": {
|
||||||
|
"damage_multiplier": 2.5,
|
||||||
|
"guaranteed_crit": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fortify": {
|
||||||
|
"name": {
|
||||||
|
"en": "Fortify",
|
||||||
|
"es": "Fortificar"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Reduce incoming damage by 60% for 2 turns",
|
||||||
|
"es": "Reduce el daño recibido en un 60% durante 2 turnos"
|
||||||
|
},
|
||||||
|
"icon": "🛡️",
|
||||||
|
"stat_requirement": "endurance",
|
||||||
|
"stat_threshold": 8,
|
||||||
|
"level_requirement": 5,
|
||||||
|
"cooldown": 3,
|
||||||
|
"stamina_cost": 4,
|
||||||
|
"effects": {
|
||||||
|
"buff": "fortify",
|
||||||
|
"buff_duration": 2,
|
||||||
|
"damage_reduction": 0.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"second_wind": {
|
||||||
|
"name": {
|
||||||
|
"en": "Second Wind",
|
||||||
|
"es": "Segundo Aliento"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Restore 20% of max HP",
|
||||||
|
"es": "Restaura el 20% de la vida máxima"
|
||||||
|
},
|
||||||
|
"icon": "💚",
|
||||||
|
"stat_requirement": "endurance",
|
||||||
|
"stat_threshold": 15,
|
||||||
|
"level_requirement": 12,
|
||||||
|
"cooldown": 5,
|
||||||
|
"stamina_cost": 6,
|
||||||
|
"effects": {
|
||||||
|
"heal_percent": 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"iron_skin": {
|
||||||
|
"name": {
|
||||||
|
"en": "Iron Skin",
|
||||||
|
"es": "Piel de Hierro"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Immune to status effects for 3 turns",
|
||||||
|
"es": "Inmune a efectos de estado durante 3 turnos"
|
||||||
|
},
|
||||||
|
"icon": "🪨",
|
||||||
|
"stat_requirement": "endurance",
|
||||||
|
"stat_threshold": 25,
|
||||||
|
"level_requirement": 20,
|
||||||
|
"cooldown": 6,
|
||||||
|
"stamina_cost": 8,
|
||||||
|
"effects": {
|
||||||
|
"buff": "iron_skin",
|
||||||
|
"buff_duration": 3,
|
||||||
|
"status_immunity": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"adrenaline_rush": {
|
||||||
|
"name": {
|
||||||
|
"en": "Adrenaline Rush",
|
||||||
|
"es": "Subida de Adrenalina"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Restore 30% of max stamina (free to use)",
|
||||||
|
"es": "Restaura el 30% del aguante máximo (sin coste)"
|
||||||
|
},
|
||||||
|
"icon": "⚡",
|
||||||
|
"stat_requirement": "endurance",
|
||||||
|
"stat_threshold": 40,
|
||||||
|
"level_requirement": 35,
|
||||||
|
"cooldown": 8,
|
||||||
|
"stamina_cost": 0,
|
||||||
|
"effects": {
|
||||||
|
"stamina_restore_percent": 0.3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"analyze": {
|
||||||
|
"name": {
|
||||||
|
"en": "Analyze",
|
||||||
|
"es": "Analizar"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Reveal enemy HP%, next attack, and weakness",
|
||||||
|
"es": "Revela el % de vida del enemigo, su próximo ataque y debilidad"
|
||||||
|
},
|
||||||
|
"icon": "🔍",
|
||||||
|
"stat_requirement": "intellect",
|
||||||
|
"stat_threshold": 8,
|
||||||
|
"level_requirement": 5,
|
||||||
|
"cooldown": 2,
|
||||||
|
"stamina_cost": 2,
|
||||||
|
"effects": {
|
||||||
|
"reveal_hp": true,
|
||||||
|
"reveal_intent": true,
|
||||||
|
"mark_analyzed": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exploit_weakness": {
|
||||||
|
"name": {
|
||||||
|
"en": "Exploit Weakness",
|
||||||
|
"es": "Explotar Debilidad"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "200% damage if Analyze was used this combat",
|
||||||
|
"es": "200% de daño si se usó Analizar en este combate"
|
||||||
|
},
|
||||||
|
"icon": "🎯",
|
||||||
|
"stat_requirement": "intellect",
|
||||||
|
"stat_threshold": 15,
|
||||||
|
"level_requirement": 12,
|
||||||
|
"cooldown": 4,
|
||||||
|
"stamina_cost": 5,
|
||||||
|
"effects": {
|
||||||
|
"damage_multiplier": 2.0,
|
||||||
|
"requires_analyzed": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"drain_life": {
|
||||||
|
"name": {
|
||||||
|
"en": "Drain Life",
|
||||||
|
"es": "Drenar Vida"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "100% damage, heal for 50% of damage dealt",
|
||||||
|
"es": "100% de daño, cura el 50% del daño causado"
|
||||||
|
},
|
||||||
|
"icon": "🩸",
|
||||||
|
"stat_requirement": "intellect",
|
||||||
|
"stat_threshold": 25,
|
||||||
|
"level_requirement": 20,
|
||||||
|
"cooldown": 5,
|
||||||
|
"stamina_cost": 6,
|
||||||
|
"effects": {
|
||||||
|
"damage_multiplier": 1.0,
|
||||||
|
"lifesteal": 0.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foresight": {
|
||||||
|
"name": {
|
||||||
|
"en": "Foresight",
|
||||||
|
"es": "Premonición"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Enemy's next 2 attacks automatically miss",
|
||||||
|
"es": "Los próximos 2 ataques del enemigo fallan automáticamente"
|
||||||
|
},
|
||||||
|
"icon": "👁️",
|
||||||
|
"stat_requirement": "intellect",
|
||||||
|
"stat_threshold": 40,
|
||||||
|
"level_requirement": 35,
|
||||||
|
"cooldown": 7,
|
||||||
|
"stamina_cost": 7,
|
||||||
|
"effects": {
|
||||||
|
"buff": "foresight",
|
||||||
|
"buff_duration": 2,
|
||||||
|
"enemy_miss": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user