Pre-menu-integration snapshot: combat, crafting, status effects, gamedata updates
This commit is contained in:
@@ -208,8 +208,18 @@ async def execute_attack(
|
||||
broken_armor = []
|
||||
|
||||
if is_pvp:
|
||||
is_defending = False
|
||||
target_effects = await db.get_player_effects(target['id'])
|
||||
defending_effect = next((e for e in target_effects if e['effect_name'] == 'defending'), None)
|
||||
if defending_effect:
|
||||
is_defending = True
|
||||
reduction = defending_effect.get('value', 50) / 100
|
||||
damage = max(1, int(damage * (1 - reduction)))
|
||||
messages.append(create_combat_message("damage_reduced", origin="enemy", reduction=int(reduction * 100)))
|
||||
await db.remove_effect(target['id'], 'defending')
|
||||
|
||||
# PvP: use equipment-based armor reduction + durability
|
||||
armor_absorbed, broken_armor = await reduce_armor_func(target['id'], damage)
|
||||
armor_absorbed, broken_armor = await reduce_armor_func(target['id'], damage, is_defending)
|
||||
actual_damage = max(1, damage - armor_absorbed)
|
||||
else:
|
||||
# PvE: use NPC's flat defense value
|
||||
@@ -279,6 +289,54 @@ async def execute_attack(
|
||||
}
|
||||
|
||||
|
||||
async def execute_defend(
|
||||
player_id: int,
|
||||
player: dict,
|
||||
player_stats: dict,
|
||||
is_pvp: bool,
|
||||
locale: str = 'en'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a defend action.
|
||||
Reduces incoming damage by 50% for the next turn, but increases durability loss.
|
||||
|
||||
Returns: {
|
||||
messages: list
|
||||
}
|
||||
"""
|
||||
from .. import database as db
|
||||
from .helpers import create_combat_message, get_game_message
|
||||
|
||||
messages = []
|
||||
|
||||
# 5% Stamina restore
|
||||
stamina_restore = max(5, int(player_stats.get('max_stamina', 100) * 0.05))
|
||||
new_stamina = min(player_stats.get('max_stamina', 100), player.get('stamina', 100) + stamina_restore)
|
||||
await db.update_player(player_id, stamina=new_stamina)
|
||||
|
||||
# Add defending effect
|
||||
await db.add_effect(
|
||||
player_id=player_id,
|
||||
effect_name="defending",
|
||||
effect_icon="🛡️",
|
||||
effect_type="buff",
|
||||
ticks_remaining=1,
|
||||
persist_after_combat=False,
|
||||
source="combat_defend",
|
||||
value=50 # 50% reduction
|
||||
)
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"player_defend",
|
||||
origin="player"
|
||||
))
|
||||
|
||||
return {
|
||||
'messages': messages,
|
||||
'stamina_restored': stamina_restore,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SKILL ACTION
|
||||
# ============================================================================
|
||||
@@ -294,6 +352,7 @@ async def execute_skill(
|
||||
items_manager: ItemsManager,
|
||||
reduce_armor_func,
|
||||
redis_manager=None,
|
||||
locale: str = 'en'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a skill action. Validates requirements, deducts stamina, applies effects.
|
||||
@@ -402,6 +461,17 @@ async def execute_skill(
|
||||
if effects.get('guaranteed_crit'):
|
||||
damage = int(damage * player_stats.get('crit_damage', 1.5))
|
||||
|
||||
is_defending = False
|
||||
if is_pvp:
|
||||
target_effects = await db.get_player_effects(target['id'])
|
||||
defending_effect = next((e for e in target_effects if e['effect_name'] == 'defending'), None)
|
||||
if defending_effect:
|
||||
is_defending = True
|
||||
reduction = defending_effect.get('value', 50) / 100
|
||||
damage = max(1, int(damage * (1 - reduction)))
|
||||
messages.append(create_combat_message("damage_reduced", origin="enemy", reduction=int(reduction * 100)))
|
||||
await db.remove_effect(target['id'], 'defending')
|
||||
|
||||
# Multi-hit
|
||||
num_hits = effects.get('hits', 1)
|
||||
total_damage = 0
|
||||
@@ -412,7 +482,7 @@ async def execute_skill(
|
||||
|
||||
if is_pvp:
|
||||
# PvP: armor from equipment
|
||||
absorbed, broken_armor = await reduce_armor_func(target['id'], hit_dmg)
|
||||
absorbed, broken_armor = await reduce_armor_func(target['id'], hit_dmg, is_defending)
|
||||
total_armor_absorbed += absorbed
|
||||
for broken in broken_armor:
|
||||
messages.append(create_combat_message(
|
||||
@@ -450,8 +520,11 @@ async def execute_skill(
|
||||
"skill_heal", origin="player", heal=heal_amount, skill_icon="🩸"
|
||||
))
|
||||
|
||||
from .helpers import calculate_dynamic_status_damage
|
||||
# Poison DoT
|
||||
if 'poison_damage' in effects:
|
||||
poison_dmg = calculate_dynamic_status_damage(effects, 'poison', target)
|
||||
if poison_dmg is not None:
|
||||
poison_dur = effects.get('poison_duration', 3)
|
||||
if is_pvp:
|
||||
# PvP: add as player effect
|
||||
await db.add_effect(
|
||||
@@ -459,14 +532,14 @@ async def execute_skill(
|
||||
effect_name="Poison",
|
||||
effect_icon="🧪",
|
||||
effect_type="damage",
|
||||
damage_per_tick=effects['poison_damage'],
|
||||
ticks_remaining=effects['poison_duration'],
|
||||
damage_per_tick=poison_dmg,
|
||||
ticks_remaining=poison_dur,
|
||||
persist_after_combat=True,
|
||||
source=f"skill_poison:{skill.id}"
|
||||
)
|
||||
else:
|
||||
# PvE: add to npc_status_effects string
|
||||
poison_str = f"poison:{effects['poison_damage']}:{effects['poison_duration']}"
|
||||
poison_str = f"poison:{poison_dmg}:{poison_dur}"
|
||||
existing = combat_state.get('npc_status_effects', '') or ''
|
||||
if existing:
|
||||
existing += '|' + poison_str
|
||||
@@ -476,7 +549,38 @@ async def execute_skill(
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"skill_effect", origin="player",
|
||||
message=f"🧪 Poisoned! ({effects['poison_damage']} dmg/turn)"
|
||||
message=f"🧪 Poisoned! ({poison_dmg} dmg/turn)"
|
||||
))
|
||||
|
||||
# Burn DoT
|
||||
burn_dmg = calculate_dynamic_status_damage(effects, 'burn', target)
|
||||
if burn_dmg is not None:
|
||||
burn_dur = effects.get('burn_duration', 3)
|
||||
if is_pvp:
|
||||
# PvP: add as player effect
|
||||
await db.add_effect(
|
||||
player_id=target['id'],
|
||||
effect_name="Burning",
|
||||
effect_icon="🔥",
|
||||
effect_type="damage",
|
||||
damage_per_tick=burn_dmg,
|
||||
ticks_remaining=burn_dur,
|
||||
persist_after_combat=True,
|
||||
source=f"skill_burn:{skill.id}"
|
||||
)
|
||||
else:
|
||||
# PvE: add to npc_status_effects string
|
||||
burn_str = f"burning:{burn_dmg}:{burn_dur}"
|
||||
existing = combat_state.get('npc_status_effects', '') or ''
|
||||
if existing:
|
||||
existing += '|' + burn_str
|
||||
else:
|
||||
existing = burn_str
|
||||
await db.update_combat(player_id, {'npc_status_effects': existing})
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"skill_effect", origin="player",
|
||||
message=f"🔥 Burning! ({burn_dmg} dmg/turn)"
|
||||
))
|
||||
|
||||
# Stun chance
|
||||
@@ -506,7 +610,7 @@ async def execute_skill(
|
||||
await db.update_combat(player_id, {'npc_status_effects': existing})
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"skill_effect", origin="player", message="💫 Stunned!"
|
||||
"skill_effect", origin="player", message=get_game_message('stunned_status', locale)
|
||||
))
|
||||
|
||||
# Weapon durability
|
||||
@@ -721,7 +825,13 @@ async def execute_use_item(
|
||||
# 6. Status effect on target (burn from molotov etc.) — PvE only
|
||||
status_effect = combat_effects.get('status') if not is_pvp else None
|
||||
if status_effect and not target_defeated:
|
||||
npc_status = f"{status_effect['name']}:{status_effect.get('damage_per_tick', 0)}:{status_effect.get('ticks', 1)}"
|
||||
dmg = status_effect.get('damage_per_tick', 0)
|
||||
if 'damage_percent' in status_effect:
|
||||
max_hp = target.get('npc_max_hp', target.get('max_hp', 100))
|
||||
base_dmg = max_hp * status_effect['damage_percent']
|
||||
dmg = random.randint(max(1, int(base_dmg * 0.8)), max(1, int(base_dmg * 1.2)))
|
||||
|
||||
npc_status = f"{status_effect['name']}:{dmg}:{status_effect.get('ticks', 1)}"
|
||||
await db.update_combat(player_id, {'npc_status_effects': npc_status})
|
||||
messages.append(create_combat_message(
|
||||
"effect_applied", origin="player",
|
||||
@@ -1123,6 +1233,7 @@ async def execute_npc_turn(
|
||||
npc_def,
|
||||
reduce_armor_func,
|
||||
redis_manager=None,
|
||||
locale: str = 'en'
|
||||
) -> Tuple[List[dict], bool]:
|
||||
"""
|
||||
Execute the NPC's turn with buff-aware damage reduction.
|
||||
@@ -1145,7 +1256,7 @@ async def execute_npc_turn(
|
||||
|
||||
from ..game_logic import npc_attack
|
||||
messages, player_defeated = await npc_attack(
|
||||
player_id, combat, npc_def, reduce_armor_func, player_stats=stats
|
||||
player_id, combat, npc_def, reduce_armor_func, player_stats=stats, locale=locale
|
||||
)
|
||||
|
||||
return messages, player_defeated
|
||||
|
||||
@@ -3,11 +3,27 @@ Helper utilities for game calculations and common operations.
|
||||
Contains distance calculations, stamina costs, capacity calculations, etc.
|
||||
"""
|
||||
import math
|
||||
from typing import Tuple, List, Dict, Any, Union
|
||||
import random
|
||||
from typing import Tuple, List, Dict, Any, Union, Optional
|
||||
from .. import database as db
|
||||
from ..items import ItemsManager
|
||||
|
||||
|
||||
def calculate_dynamic_status_damage(effects: dict, prefix: str, target: dict) -> Optional[int]:
|
||||
"""Helper to calculate status damage based on percentage over max HP."""
|
||||
if f'{prefix}_percent' in effects:
|
||||
target_max_hp = target.get('max_hp') or target.get('npc_max_hp', 100)
|
||||
pct = effects[f'{prefix}_percent']
|
||||
base_dmg = target_max_hp * pct
|
||||
# +/- 20% deviation
|
||||
min_dmg = max(1, int(base_dmg * 0.8))
|
||||
max_dmg = max(1, int(base_dmg * 1.2))
|
||||
return random.randint(min_dmg, max_dmg)
|
||||
elif f'{prefix}_damage' in effects:
|
||||
return effects[f'{prefix}_damage']
|
||||
return None
|
||||
|
||||
|
||||
def get_locale_string(value: Union[str, Dict[str, str]], lang: str = 'en') -> str:
|
||||
"""Helper to safely get string from i18n object or string."""
|
||||
if isinstance(value, dict):
|
||||
@@ -54,6 +70,8 @@ GAME_MESSAGES = {
|
||||
'pvp_combat_started_broadcast': {'en': "⚔️ PvP combat started between {attacker} and {defender}!", 'es': "⚔️ ¡Combate PvP iniciado entre {attacker} y {defender}!"},
|
||||
'flee_success_text': {'en': "{name} fled from combat!", 'es': "¡{name} huyó del combate!"},
|
||||
'flee_fail_text': {'en': "{name} tried to flee but failed!", 'es': "¡{name} intentó huir pero falló!"},
|
||||
'stunned_status': {'en': "💫 Stunned!", 'es': "💫 ¡Aturdido!"},
|
||||
'npc_stunned_cannot_act': {'en': "💫 {npc_name} is stunned and cannot act!", 'es': "💫 ¡{npc_name} está aturdido y no puede actuar!"},
|
||||
|
||||
# Loot
|
||||
'corpse_name_npc': {'en': "{name} Corpse", 'es': "Cadáver de {name}"},
|
||||
@@ -101,12 +119,16 @@ GAME_MESSAGES = {
|
||||
'item_used': {'en': "Used {name}", 'es': "Usado {name}"},
|
||||
'no_item': {'en': "You don't have this item", 'es': "No tienes este objeto"},
|
||||
'cannot_use': {'en': "This item cannot be used", 'es': "Este objeto no se puede usar"},
|
||||
'cannot_equip_combat': {'en': "Cannot change equipment during combat", 'es': "No puedes cambiar de equipamiento durante el combate"},
|
||||
'cured': {'en': "Cured", 'es': "Curado"},
|
||||
|
||||
# Status Effects
|
||||
'statusDamage': {'en': "You took {damage} damage from status effects", 'es': "Has recibido {damage} de daño por efectos de estado"},
|
||||
'statusHeal': {'en': "You recovered {heal} HP from status effects", 'es': "Has recuperado {heal} vida por efectos de estado"},
|
||||
'diedFromStatus': {'en': "You died from status effects", 'es': "Has muerto por efectos de estado"},
|
||||
|
||||
# Combat Warnings
|
||||
'enemy_charging': {'en': "⚠️ {enemy} is gathering strength for a massive attack!", 'es': "⚠️ ¡{enemy} está reuniendo fuerzas para un ataque masivo!"},
|
||||
}
|
||||
|
||||
def get_game_message(key: str, lang: str = 'en', **kwargs) -> str:
|
||||
@@ -140,6 +162,36 @@ def translate_travel_message(direction: str, location_name: str, lang: str = 'en
|
||||
|
||||
|
||||
import json
|
||||
from typing import List, Dict, Any, Tuple
|
||||
from .. import database as db
|
||||
|
||||
async def get_resolved_player_effects(player_id: int, in_combat: bool = False) -> List[Dict]:
|
||||
"""Helper to fetch and format active player effects for combat payloads."""
|
||||
from ..services.skills import skills_manager
|
||||
from ..services.status_effects import status_effects_manager
|
||||
|
||||
player_effects = []
|
||||
all_effects = await db.get_player_effects(player_id)
|
||||
for eff in all_effects:
|
||||
if eff.get('effect_type') == 'cooldown':
|
||||
continue
|
||||
resolved = status_effects_manager.resolve_player_effect(
|
||||
eff.get('effect_name', ''),
|
||||
eff.get('effect_icon', '⚡'),
|
||||
eff.get('source', ''),
|
||||
skills_manager,
|
||||
in_combat=in_combat
|
||||
)
|
||||
player_effects.append({
|
||||
'name': resolved['name'],
|
||||
'effect_name': eff.get('effect_name', ''), # Needed for frontend state tracking
|
||||
'icon': resolved['icon'],
|
||||
'ticks_remaining': eff.get('ticks_remaining', 0),
|
||||
'damage_per_tick': eff.get('damage_per_tick', 0), # Needed for logic
|
||||
'type': eff.get('effect_type', 'buff'),
|
||||
'description': resolved['description'],
|
||||
})
|
||||
return player_effects
|
||||
|
||||
def create_combat_message(message_type: str, origin: str = "neutral", **data) -> dict:
|
||||
"""Create a structured combat message object.
|
||||
@@ -274,7 +326,7 @@ async def calculate_player_capacity(inventory: List[Dict[str, Any]], items_manag
|
||||
return current_weight, max_weight, current_volume, max_volume
|
||||
|
||||
|
||||
async def reduce_armor_durability(player_id: int, damage_taken: int, items_manager: ItemsManager) -> Tuple[int, List[Dict[str, Any]]]:
|
||||
async def reduce_armor_durability(player_id: int, damage_taken: int, items_manager: ItemsManager, is_defending: bool = False) -> Tuple[int, List[Dict[str, Any]]]:
|
||||
"""
|
||||
Reduce durability of equipped armor pieces when taking damage.
|
||||
Returns: (armor_damage_absorbed, broken_armor_pieces)
|
||||
@@ -311,7 +363,7 @@ async def reduce_armor_durability(player_id: int, damage_taken: int, items_manag
|
||||
armor_absorbed = min(damage_taken // 2, total_armor)
|
||||
|
||||
# Calculate durability loss for each armor piece
|
||||
base_reduction_rate = 0.1
|
||||
base_reduction_rate = 0.2 if is_defending else 0.1
|
||||
broken_armor = []
|
||||
|
||||
for armor in equipped_armor:
|
||||
|
||||
@@ -60,6 +60,10 @@ class SkillsManager:
|
||||
"""
|
||||
available = []
|
||||
for skill_id, skill in self.skills.items():
|
||||
# Skip NPC-only skills (assumed to be those with 0 stat threshold and level 1 requirement)
|
||||
if (skill.stat_threshold <= 0 and skill.level_requirement <= 1) or getattr(skill, 'npc_only', False):
|
||||
continue
|
||||
|
||||
stat_value = character.get(skill.stat_requirement, 0)
|
||||
level = character.get('level', 1)
|
||||
|
||||
|
||||
@@ -36,7 +36,30 @@ async def calculate_derived_stats(character_id: int, redis_mgr=None) -> Dict[str
|
||||
if not char:
|
||||
return _empty_stats()
|
||||
|
||||
equipment = await db.get_all_equipment(character_id)
|
||||
raw_equipment = await db.get_all_equipment(character_id)
|
||||
enriched_equipment = {}
|
||||
|
||||
for slot, item_data in raw_equipment.items():
|
||||
if not item_data or not item_data.get('item_id'):
|
||||
continue
|
||||
|
||||
inv_item = await db.get_inventory_item_by_id(item_data['item_id'])
|
||||
if not inv_item:
|
||||
continue
|
||||
|
||||
enriched_item = {
|
||||
'item_id': inv_item['item_id'], # String ID
|
||||
'inventory_id': item_data['item_id']
|
||||
}
|
||||
|
||||
unique_item_id = inv_item.get('unique_item_id')
|
||||
if unique_item_id:
|
||||
unique_item = await db.get_unique_item(unique_item_id)
|
||||
if unique_item and unique_item.get('unique_stats'):
|
||||
enriched_item['unique_stats'] = unique_item['unique_stats']
|
||||
|
||||
enriched_equipment[slot] = enriched_item
|
||||
|
||||
effects = await db.get_player_effects(character_id)
|
||||
|
||||
# 3. Fetch owned perks
|
||||
@@ -44,7 +67,7 @@ async def calculate_derived_stats(character_id: int, redis_mgr=None) -> Dict[str
|
||||
owned_perk_ids = [row['perk_id'] for row in owned_perks]
|
||||
|
||||
# 4. Compute derived stats
|
||||
stats = _compute_stats(char, equipment, effects, owned_perk_ids)
|
||||
stats = _compute_stats(char, enriched_equipment, effects, owned_perk_ids)
|
||||
|
||||
# 5. Cache in Redis (5 min TTL)
|
||||
if redis_mgr and redis_mgr.redis_client:
|
||||
@@ -95,21 +118,29 @@ def _compute_stats(char: Dict[str, Any], equipment: Dict[str, Any], effects: Lis
|
||||
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', ''))
|
||||
item_id_str = item_data.get('item_id', '')
|
||||
item_def = ITEMS_MANAGER.get_item(item_id_str)
|
||||
|
||||
# Try to get item_id from the inventory item if the direct lookup failed
|
||||
if not item_def:
|
||||
continue
|
||||
|
||||
# Merge base stats and unique stats
|
||||
merged_stats = {}
|
||||
if item_def.stats:
|
||||
total_armor += item_def.stats.get('armor', 0)
|
||||
weapon_crit += item_def.stats.get('crit_chance', 0)
|
||||
merged_stats.update(item_def.stats)
|
||||
if item_data.get('unique_stats'):
|
||||
merged_stats.update(item_data['unique_stats'])
|
||||
|
||||
if merged_stats:
|
||||
total_armor += merged_stats.get('armor', 0)
|
||||
weapon_crit += merged_stats.get('crit_chance', 0)
|
||||
max_hp += merged_stats.get('max_hp', 0)
|
||||
max_stamina += merged_stats.get('max_stamina', 0)
|
||||
carry_weight += merged_stats.get('weight_capacity', 0)
|
||||
|
||||
if slot == 'weapon':
|
||||
weapon_damage_min = item_def.stats.get('damage_min', 0)
|
||||
weapon_damage_max = item_def.stats.get('damage_max', 0)
|
||||
weapon_damage_min = merged_stats.get('damage_min', 0)
|
||||
weapon_damage_max = merged_stats.get('damage_max', 0)
|
||||
|
||||
if slot == 'offhand':
|
||||
has_shield = True
|
||||
@@ -218,6 +249,28 @@ async def invalidate_stats_cache(character_id: int, redis_mgr=None):
|
||||
await redis_mgr.redis_client.delete(f"stats:{character_id}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Sync derived max_hp and max_stamina to the database characters table
|
||||
try:
|
||||
derived = await calculate_derived_stats(character_id, redis_mgr)
|
||||
char = await db.get_player_by_id(character_id)
|
||||
if char:
|
||||
new_max_hp = derived.get('max_hp', char['max_hp'])
|
||||
new_max_stamina = derived.get('max_stamina', char['max_stamina'])
|
||||
|
||||
if new_max_hp != char['max_hp'] or new_max_stamina != char['max_stamina']:
|
||||
new_hp = min(char['hp'], new_max_hp)
|
||||
new_stamina = min(char['stamina'], new_max_stamina)
|
||||
await db.update_player(
|
||||
character_id,
|
||||
max_hp=new_max_hp,
|
||||
max_stamina=new_max_stamina,
|
||||
hp=new_hp,
|
||||
stamina=new_stamina
|
||||
)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(f"Failed to sync derived stats to DB for {character_id}: {e}")
|
||||
|
||||
|
||||
def get_flee_chance(flee_chance_base: float, enemy_level: int) -> float:
|
||||
|
||||
101
api/services/status_effects.py
Normal file
101
api/services/status_effects.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Status Effects Manager.
|
||||
Loads status effect definitions from gamedata/status_effects.json.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class StatusEffect:
|
||||
"""Represents a status effect definition."""
|
||||
def __init__(self, effect_id: str, data: Dict[str, Any]):
|
||||
self.id = effect_id
|
||||
self.icon = data.get('icon', '⚡')
|
||||
self.name = data.get('name', effect_id.capitalize())
|
||||
self.description = data.get('description', effect_id.capitalize())
|
||||
self.type = data.get('type', 'debuff')
|
||||
|
||||
|
||||
class StatusEffectsManager:
|
||||
"""Manages status effect definitions loaded from JSON."""
|
||||
|
||||
def __init__(self, gamedata_path: str = "./gamedata"):
|
||||
self.effects: Dict[str, StatusEffect] = {}
|
||||
filepath = os.path.join(gamedata_path, 'status_effects.json')
|
||||
self._load(filepath)
|
||||
|
||||
def _load(self, filepath: str):
|
||||
"""Load status effects from a JSON file."""
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
for effect_id, effect_data in data.get('effects', {}).items():
|
||||
self.effects[effect_id] = StatusEffect(effect_id, effect_data)
|
||||
print(f"✨ Loaded {len(self.effects)} status effects")
|
||||
except FileNotFoundError:
|
||||
print("⚠️ status_effects.json not found")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error loading status_effects.json: {e}")
|
||||
|
||||
def get_effect(self, effect_id: str) -> Optional[StatusEffect]:
|
||||
"""Get a status effect by its ID."""
|
||||
return self.effects.get(effect_id)
|
||||
|
||||
def get_effect_info(self, effect_id: str) -> Dict[str, Any]:
|
||||
"""Get effect info dict for API responses. Returns a fallback if not found."""
|
||||
effect = self.effects.get(effect_id)
|
||||
if effect:
|
||||
return {
|
||||
'name': effect.name,
|
||||
'icon': effect.icon,
|
||||
'description': effect.description,
|
||||
'type': effect.type,
|
||||
}
|
||||
# Fallback for unknown effects
|
||||
return {
|
||||
'name': {'en': effect_id.capitalize(), 'es': effect_id.capitalize()},
|
||||
'icon': '⚡',
|
||||
'description': {'en': effect_id.capitalize(), 'es': effect_id.capitalize()},
|
||||
'type': 'debuff',
|
||||
}
|
||||
|
||||
def resolve_player_effect(self, effect_name: str, effect_icon: str, source: str, skills_manager=None, in_combat: bool = True) -> Dict[str, Any]:
|
||||
"""
|
||||
Resolve translated name and description for a player effect.
|
||||
Tries skill source first, then status_effects.json, then fallback.
|
||||
"""
|
||||
translated_name = effect_name
|
||||
translated_desc = ''
|
||||
|
||||
# 1. Try to get from skill source (e.g., "skill:fortify")
|
||||
if source.startswith('skill:') and skills_manager:
|
||||
skill_id = source.split(':', 1)[1]
|
||||
skill_def = skills_manager.get_skill(skill_id)
|
||||
if skill_def:
|
||||
translated_name = skill_def.name
|
||||
translated_desc = skill_def.description
|
||||
|
||||
# 2. Try to get from status_effects.json by lowercased effect name
|
||||
if not translated_desc:
|
||||
effect_key = effect_name.lower()
|
||||
effect = self.effects.get(effect_key)
|
||||
if effect:
|
||||
translated_name = effect.name
|
||||
translated_desc = effect.description
|
||||
|
||||
# 3. Fallback: wrap the raw name as a translatable dict
|
||||
if not translated_desc:
|
||||
translated_desc = {'en': effect_name, 'es': effect_name}
|
||||
if isinstance(translated_name, str):
|
||||
translated_name = {'en': translated_name, 'es': translated_name}
|
||||
|
||||
return {
|
||||
'name': translated_name,
|
||||
'icon': effect_icon or '⚡',
|
||||
'description': translated_desc,
|
||||
}
|
||||
|
||||
|
||||
# Module-level singleton
|
||||
status_effects_manager = StatusEffectsManager()
|
||||
Reference in New Issue
Block a user