feat(backend): Integrate Derived Stats into combat, loot, and crafting mechanics

This commit is contained in:
Joan
2026-02-25 10:05:14 +01:00
parent 185781d168
commit fd94387d54
10 changed files with 727 additions and 101 deletions

View File

@@ -17,6 +17,7 @@ from .. import database as db
from ..items import ItemsManager
from .. import game_logic
from ..core.websockets import manager
from ..services.stats import STAT_CAP, invalidate_stats_cache
logger = logging.getLogger(__name__)
@@ -457,19 +458,28 @@ async def spend_stat_point(
if stat not in valid_stats:
raise HTTPException(status_code=400, detail=f"Invalid stat. Must be one of: {', '.join(valid_stats)}")
# Check stat cap
if player[stat] >= STAT_CAP:
raise HTTPException(status_code=400, detail=f"{stat.capitalize()} is already at maximum ({STAT_CAP})")
# Update the stat and decrease unspent points
update_data = {
stat: player[stat] + 1,
'unspent_points': player['unspent_points'] - 1
}
# Endurance increases max HP
# Endurance increases max HP and max stamina
if stat == 'endurance':
update_data['max_hp'] = player['max_hp'] + 5
update_data['hp'] = min(player['hp'] + 5, update_data['max_hp']) # Also heal by 5
update_data['max_stamina'] = player['max_stamina'] + 2
update_data['stamina'] = min(player['stamina'] + 2, update_data['max_stamina']) # Also restore by 2
await db.update_character(current_user['id'], **update_data)
# Invalidate cached derived stats
await invalidate_stats_cache(current_user['id'], redis_manager)
return {
"success": True,
"message": f"Increased {stat} by 1!",
@@ -1550,4 +1560,139 @@ async def drop_item(
return {
"success": True,
"message": get_game_message('dropped_item_success', locale, emoji=item_def.emoji, name=get_locale_string(item_def.name, locale), qty=quantity)
}
}
@router.get("/api/game/character-sheet")
async def get_character_sheet(current_user: dict = Depends(get_current_user)):
"""Get the full character sheet with base stats, derived stats, skills, and perks."""
from ..services.stats import calculate_derived_stats
from ..services.skills import skills_manager, perks_manager, get_total_perk_points
player = current_user
character_id = player['id']
# Get derived stats
derived = await calculate_derived_stats(character_id, redis_manager)
# Get available skills
available_skills = skills_manager.get_available_skills(player)
# Get owned perks
owned_perks_rows = await db.get_character_perks(character_id)
owned_perk_ids = [row['perk_id'] for row in owned_perks_rows]
# Get all perks with availability
all_perks = perks_manager.get_available_perks(player, owned_perk_ids)
# Calculate perk points
total_perk_points = get_total_perk_points(player['level'])
used_perk_points = len(owned_perk_ids)
available_perk_points = total_perk_points - used_perk_points
return {
"base_stats": {
"strength": player['strength'],
"agility": player['agility'],
"endurance": player['endurance'],
"intellect": player['intellect'],
"unspent_points": player['unspent_points'],
"stat_cap": STAT_CAP,
},
"derived_stats": derived,
"skills": available_skills,
"perks": {
"available_points": available_perk_points,
"total_points": total_perk_points,
"used_points": used_perk_points,
"all_perks": all_perks,
},
"character": {
"name": player['name'],
"level": player['level'],
"xp": player['xp'],
"hp": player['hp'],
"max_hp": player['max_hp'],
"stamina": player['stamina'],
"max_stamina": player['max_stamina'],
"avatar_data": player.get('avatar_data'),
}
}
@router.post("/api/game/select_perk")
async def select_perk(
perk_id: str,
current_user: dict = Depends(get_current_user)
):
"""Select a perk for the character."""
from ..services.skills import perks_manager, get_total_perk_points
player = current_user
character_id = player['id']
# Check perk exists
perk = perks_manager.get_perk(perk_id)
if not perk:
raise HTTPException(status_code=404, detail="Perk not found")
# Check perk points available
owned_perks = await db.get_character_perks(character_id)
owned_perk_ids = [row['perk_id'] for row in owned_perks]
total_points = get_total_perk_points(player['level'])
used_points = len(owned_perk_ids)
if used_points >= total_points:
raise HTTPException(status_code=400, detail="No perk points available")
# Check if already owned
if perk_id in owned_perk_ids:
raise HTTPException(status_code=400, detail="Perk already selected")
# Check requirements
if not perks_manager.check_requirements(perk, player):
raise HTTPException(status_code=400, detail="Requirements not met for this perk")
# Add perk
success = await db.add_character_perk(character_id, perk_id)
if not success:
raise HTTPException(status_code=400, detail="Failed to select perk")
# Invalidate stats cache (perks affect derived stats)
await invalidate_stats_cache(character_id, redis_manager)
return {
"success": True,
"message": f"Perk '{perk_id}' selected!",
"perk": {
"id": perk.id,
"name": perk.name,
"description": perk.description,
"icon": perk.icon,
}
}
@router.get("/api/game/available-skills")
async def get_available_skills(current_user: dict = Depends(get_current_user)):
"""Get available skills for the combat UI abilities dropdown."""
from ..services.skills import skills_manager
from .. import database as db
player = current_user
all_skills = skills_manager.get_available_skills(player)
# Check cooldowns
effects = await db.get_player_effects(player['id'])
cooldowns = {eff['source']: eff['ticks_remaining'] for eff in effects if eff.get('effect_type') == 'cooldown'}
# Only return unlocked skills for the combat dropdown
unlocked = []
for s in all_skills:
if s['unlocked']:
cd_source = f"cd:{s['id']}"
s['current_cooldown'] = cooldowns.get(cd_source, 0)
unlocked.append(s)
return {"skills": unlocked}