Backup before cleanup
This commit is contained in:
@@ -6,6 +6,7 @@ import asyncio
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from api.services.helpers import get_game_message
|
||||
from .services.constants import PVP_TURN_TIMEOUT
|
||||
import os
|
||||
import fcntl
|
||||
@@ -401,7 +402,7 @@ async def check_pvp_combat_timers(manager=None):
|
||||
await db.update_pvp_combat(combat['id'], {
|
||||
'turn': new_turn,
|
||||
'turn_started_at': time.time(),
|
||||
'last_action': f"Turn timeout - {current_turn}'s turn skipped|{time.time()}"
|
||||
'last_action': f"turn_timeout:{current_turn}|{time.time()}"
|
||||
})
|
||||
|
||||
processed += 1
|
||||
@@ -423,10 +424,16 @@ async def check_pvp_combat_timers(manager=None):
|
||||
"turn": new_turn,
|
||||
"time_remaining": time_remaining,
|
||||
"turn_timeout": "skipped",
|
||||
"last_action": f"Turn timeout - {current_turn}'s turn skipped"
|
||||
"last_action": f"turn_timeout:{current_turn}"
|
||||
},
|
||||
"is_pvp": True,
|
||||
"message": f"⏱️ Turn skipped due to timeout!"
|
||||
"messages": [
|
||||
{
|
||||
"type": "combat_timeout",
|
||||
"origin": "system",
|
||||
"timestamp": time.time()
|
||||
}
|
||||
]
|
||||
},
|
||||
"timestamp": time.time()
|
||||
}
|
||||
@@ -519,6 +526,8 @@ async def cleanup_interactable_cooldowns(manager=None, world_locations=None):
|
||||
"data": {
|
||||
"instance_id": cooldown_info['instance_id'],
|
||||
"action_id": cooldown_info['action_id'],
|
||||
"name": cooldown_info['name'],
|
||||
"action_name": cooldown_info['action_name'],
|
||||
"message": f"{cooldown_info['action_name']} is ready on {cooldown_info['name']}"
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
@@ -638,7 +647,7 @@ async def process_status_effects(manager=None):
|
||||
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(300) # Wait 5 minutes
|
||||
await asyncio.sleep(60) # Wait 1 minute (requested by user)
|
||||
|
||||
start_time = time.time()
|
||||
logger.info("Running status effects processor...")
|
||||
@@ -658,28 +667,39 @@ async def process_status_effects(manager=None):
|
||||
|
||||
for player_id in affected_players:
|
||||
try:
|
||||
# Get current status effects (after decrement)
|
||||
effects = await db.get_player_status_effects(player_id)
|
||||
# Get current status effects (after decrement), INCLUDING expired (0 ticks)
|
||||
effects = await db.get_player_status_effects(player_id, min_ticks=0)
|
||||
|
||||
if not effects:
|
||||
continue
|
||||
|
||||
# Calculate total damage
|
||||
from api.game_logic import calculate_status_damage
|
||||
total_damage = calculate_status_damage(effects)
|
||||
# Prepare detailed effects data for frontend
|
||||
effects_data = [
|
||||
{
|
||||
"name": e['effect_name'],
|
||||
"ticks_remaining": e['ticks_remaining'],
|
||||
"effect_icon": e.get('effect_icon')
|
||||
}
|
||||
for e in effects
|
||||
]
|
||||
|
||||
if total_damage > 0:
|
||||
damage_dealt += total_damage
|
||||
# Calculate total impact (positive = damage, negative = healing)
|
||||
from api.game_logic import calculate_status_impact
|
||||
total_impact = calculate_status_impact(effects)
|
||||
|
||||
if total_impact > 0:
|
||||
# DAMAGE LOGIC
|
||||
damage_dealt += total_impact
|
||||
player = await db.get_player_by_id(player_id)
|
||||
|
||||
if not player or player['is_dead']:
|
||||
continue
|
||||
|
||||
new_hp = max(0, player['hp'] - total_damage)
|
||||
new_hp = max(0, player['hp'] - total_impact)
|
||||
|
||||
# Check if player died from status effects
|
||||
if new_hp <= 0:
|
||||
await db.update_player(player_id, {'hp': 0, 'is_dead': True})
|
||||
await db.update_player(player_id, hp=0, is_dead=True)
|
||||
deaths += 1
|
||||
|
||||
# Only create corpse if player has items
|
||||
@@ -701,6 +721,7 @@ async def process_status_effects(manager=None):
|
||||
# Notify player of death
|
||||
if manager:
|
||||
from datetime import datetime
|
||||
locale = player.get('locale', 'en')
|
||||
await manager.send_personal_message(
|
||||
player_id,
|
||||
{
|
||||
@@ -708,7 +729,7 @@ async def process_status_effects(manager=None):
|
||||
"data": {
|
||||
"hp": 0,
|
||||
"is_dead": True,
|
||||
"message": "You died from status effects"
|
||||
"message": get_game_message('diedFromStatus', locale)
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
@@ -717,10 +738,11 @@ async def process_status_effects(manager=None):
|
||||
logger.info(f"Player {player['name']} (ID: {player_id}) died from status effects")
|
||||
else:
|
||||
# Apply damage and notify player
|
||||
await db.update_player(player_id, {'hp': new_hp})
|
||||
await db.update_player(player_id, hp=new_hp)
|
||||
|
||||
if manager:
|
||||
from datetime import datetime
|
||||
locale = player.get('locale', 'en')
|
||||
await manager.send_personal_message(
|
||||
player_id,
|
||||
{
|
||||
@@ -728,8 +750,44 @@ async def process_status_effects(manager=None):
|
||||
"data": {
|
||||
"hp": new_hp,
|
||||
"max_hp": player['max_hp'],
|
||||
"damage": total_damage,
|
||||
"message": f"You took {total_damage} damage from status effects"
|
||||
"damage": total_impact,
|
||||
"message": get_game_message('statusDamage', locale, damage=total_impact),
|
||||
"effects": effects_data
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
elif total_impact < 0:
|
||||
# HEALING LOGIC
|
||||
heal_amount = abs(total_impact)
|
||||
player = await db.get_player_by_id(player_id)
|
||||
|
||||
if not player or player['is_dead']:
|
||||
continue
|
||||
|
||||
# Don't heal if already full
|
||||
if player['hp'] >= player['max_hp']:
|
||||
continue
|
||||
|
||||
new_hp = min(player['max_hp'], player['hp'] + heal_amount)
|
||||
real_heal = new_hp - player['hp']
|
||||
|
||||
if real_heal > 0:
|
||||
await db.update_player(player_id, hp=new_hp)
|
||||
|
||||
if manager:
|
||||
from datetime import datetime
|
||||
locale = player.get('locale', 'en')
|
||||
await manager.send_personal_message(
|
||||
player_id,
|
||||
{
|
||||
"type": "status_effect_heal",
|
||||
"data": {
|
||||
"hp": new_hp,
|
||||
"max_hp": player['max_hp'],
|
||||
"heal": real_heal,
|
||||
"message": get_game_message('statusHeal', locale, heal=real_heal),
|
||||
"effects": effects_data
|
||||
},
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
@@ -738,10 +796,13 @@ async def process_status_effects(manager=None):
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing status effects for player {player_id}: {e}")
|
||||
|
||||
# CLEANUP: Remove expired effects now that we've notified the user
|
||||
await db.clean_expired_status_effects()
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(
|
||||
f"Processed status effects for {len(affected_players)} players "
|
||||
f"({damage_dealt} total damage, {deaths} deaths) in {elapsed:.3f}s"
|
||||
f"({damage_dealt} damage, {deaths} deaths) in {elapsed:.3f}s"
|
||||
)
|
||||
|
||||
# Warn if taking too long (potential scaling issue)
|
||||
|
||||
226
api/database.py
226
api/database.py
@@ -262,8 +262,12 @@ player_status_effects = Table(
|
||||
Column("character_id", Integer, ForeignKey("characters.id", ondelete="CASCADE"), nullable=False),
|
||||
Column("effect_name", String(50), nullable=False),
|
||||
Column("effect_icon", String(10), nullable=False),
|
||||
Column("effect_type", String(20), default="damage"), # 'damage', 'buff', 'debuff'
|
||||
Column("damage_per_tick", Integer, nullable=False, default=0),
|
||||
Column("value", Integer, default=0), # Generic value (buff %, damage, etc.)
|
||||
Column("ticks_remaining", Integer, nullable=False),
|
||||
Column("persist_after_combat", Boolean, default=False), # Keep after combat ends
|
||||
Column("source", String(50), nullable=True), # 'item:molotov', 'action:defend'
|
||||
Column("applied_at", Float, nullable=False),
|
||||
)
|
||||
|
||||
@@ -2030,18 +2034,99 @@ async def remove_empty_npc_corpses() -> int:
|
||||
# STATUS EFFECTS FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
async def get_player_status_effects(player_id: int):
|
||||
async def add_effect(
|
||||
player_id: int,
|
||||
effect_name: str,
|
||||
effect_icon: str,
|
||||
ticks_remaining: int,
|
||||
effect_type: str = "damage",
|
||||
damage_per_tick: int = 0,
|
||||
value: int = 0,
|
||||
persist_after_combat: bool = False,
|
||||
source: str = None
|
||||
) -> int:
|
||||
"""
|
||||
Add a status effect to a player.
|
||||
If the effect already exists, it refreshes the duration (ticks_remaining).
|
||||
Returns the effect ID.
|
||||
"""
|
||||
async with DatabaseSession() as session:
|
||||
# Check if effect already exists
|
||||
result = await session.execute(
|
||||
select(player_status_effects).where(
|
||||
and_(
|
||||
player_status_effects.c.character_id == player_id,
|
||||
player_status_effects.c.effect_name == effect_name
|
||||
)
|
||||
)
|
||||
)
|
||||
existing_effect = result.first()
|
||||
|
||||
if existing_effect:
|
||||
# Refresh duration
|
||||
await session.execute(
|
||||
update(player_status_effects).where(
|
||||
player_status_effects.c.id == existing_effect.id
|
||||
).values(
|
||||
ticks_remaining=ticks_remaining,
|
||||
applied_at=time.time()
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
return existing_effect.id
|
||||
else:
|
||||
# Insert new effect
|
||||
stmt = insert(player_status_effects).values(
|
||||
character_id=player_id,
|
||||
effect_name=effect_name,
|
||||
effect_icon=effect_icon,
|
||||
effect_type=effect_type,
|
||||
damage_per_tick=damage_per_tick,
|
||||
value=value,
|
||||
ticks_remaining=ticks_remaining,
|
||||
persist_after_combat=persist_after_combat,
|
||||
source=source,
|
||||
applied_at=time.time()
|
||||
).returning(player_status_effects.c.id)
|
||||
result = await session.execute(stmt)
|
||||
row = result.first()
|
||||
await session.commit()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
async def get_player_effects(player_id: int, min_ticks: int = 1) -> List[Dict[str, Any]]:
|
||||
"""Get all active status effects for a player."""
|
||||
async with DatabaseSession() as session:
|
||||
result = await session.execute(
|
||||
select(player_status_effects).where(
|
||||
and_(
|
||||
player_status_effects.c.character_id == player_id,
|
||||
player_status_effects.c.ticks_remaining > 0
|
||||
player_status_effects.c.ticks_remaining >= min_ticks
|
||||
)
|
||||
)
|
||||
)
|
||||
return [row._asdict() for row in result.fetchall()]
|
||||
return [dict(row._mapping) for row in result.fetchall()]
|
||||
|
||||
|
||||
# Alias for backward compatibility
|
||||
async def get_player_status_effects(player_id: int, min_ticks: int = 1):
|
||||
"""Alias for get_player_effects for backward compatibility."""
|
||||
return await get_player_effects(player_id, min_ticks)
|
||||
|
||||
|
||||
async def remove_effect(player_id: int, effect_name: str) -> bool:
|
||||
"""Remove a specific effect from a player by name."""
|
||||
async with DatabaseSession() as session:
|
||||
await session.execute(
|
||||
delete(player_status_effects).where(
|
||||
and_(
|
||||
player_status_effects.c.character_id == player_id,
|
||||
player_status_effects.c.effect_name == effect_name
|
||||
)
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
return True
|
||||
|
||||
|
||||
async def remove_all_status_effects(player_id: int):
|
||||
@@ -2052,36 +2137,141 @@ async def remove_all_status_effects(player_id: int):
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def decrement_all_status_effect_ticks():
|
||||
"""
|
||||
Decrement ticks for all active status effects and return affected player IDs.
|
||||
Used by background processor.
|
||||
"""
|
||||
async def clean_expired_status_effects():
|
||||
"""Remove all status effects with <= 0 ticks."""
|
||||
async with DatabaseSession() as session:
|
||||
# Get player IDs with effects before updating
|
||||
from sqlalchemy import distinct
|
||||
result = await session.execute(
|
||||
select(distinct(player_status_effects.c.character_id)).where(
|
||||
player_status_effects.c.ticks_remaining > 0
|
||||
await session.execute(
|
||||
delete(player_status_effects).where(player_status_effects.c.ticks_remaining <= 0)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def remove_non_persistent_effects(player_id: int):
|
||||
"""Remove effects where persist_after_combat is False. Called when combat ends."""
|
||||
async with DatabaseSession() as session:
|
||||
await session.execute(
|
||||
delete(player_status_effects).where(
|
||||
and_(
|
||||
player_status_effects.c.character_id == player_id,
|
||||
player_status_effects.c.persist_after_combat == False
|
||||
)
|
||||
)
|
||||
)
|
||||
affected_players = [row[0] for row in result.fetchall()]
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def tick_player_effects(player_id: int) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Decrement ticks and return effects that were applied this tick.
|
||||
Used during combat when player receives a turn.
|
||||
Returns list of effects with their current state (before tick was applied).
|
||||
"""
|
||||
async with DatabaseSession() as session:
|
||||
# Get effects before decrementing
|
||||
result = await session.execute(
|
||||
select(player_status_effects).where(
|
||||
and_(
|
||||
player_status_effects.c.character_id == player_id,
|
||||
player_status_effects.c.ticks_remaining > 0
|
||||
)
|
||||
)
|
||||
)
|
||||
effects = [dict(row._mapping) for row in result.fetchall()]
|
||||
|
||||
if not effects:
|
||||
return []
|
||||
|
||||
# Decrement ticks
|
||||
await session.execute(
|
||||
update(player_status_effects).where(
|
||||
player_status_effects.c.ticks_remaining > 0
|
||||
and_(
|
||||
player_status_effects.c.character_id == player_id,
|
||||
player_status_effects.c.ticks_remaining > 0
|
||||
)
|
||||
).values(ticks_remaining=player_status_effects.c.ticks_remaining - 1)
|
||||
)
|
||||
|
||||
# Remove expired effects
|
||||
await session.execute(
|
||||
delete(player_status_effects).where(player_status_effects.c.ticks_remaining <= 0)
|
||||
delete(player_status_effects).where(
|
||||
and_(
|
||||
player_status_effects.c.character_id == player_id,
|
||||
player_status_effects.c.ticks_remaining <= 0
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
return affected_players
|
||||
return effects
|
||||
|
||||
|
||||
async def decrement_all_status_effect_ticks():
|
||||
"""
|
||||
Decrement ticks for all active status effects and return affected player IDs.
|
||||
Used by background processor. Only processes players NOT in combat.
|
||||
"""
|
||||
async with DatabaseSession() as session:
|
||||
from sqlalchemy import distinct
|
||||
|
||||
# Get all players with active effects
|
||||
result = await session.execute(
|
||||
select(distinct(player_status_effects.c.character_id)).where(
|
||||
player_status_effects.c.ticks_remaining > 0
|
||||
)
|
||||
)
|
||||
all_players = [row[0] for row in result.fetchall()]
|
||||
|
||||
# Filter out players in combat - they process effects on turn
|
||||
players_to_process = []
|
||||
for pid in all_players:
|
||||
if not await is_player_in_combat(pid):
|
||||
players_to_process.append(pid)
|
||||
|
||||
if not players_to_process:
|
||||
return []
|
||||
|
||||
# Decrement ticks only for players not in combat
|
||||
for pid in players_to_process:
|
||||
await session.execute(
|
||||
update(player_status_effects).where(
|
||||
and_(
|
||||
player_status_effects.c.character_id == pid,
|
||||
player_status_effects.c.ticks_remaining > 0
|
||||
)
|
||||
).values(ticks_remaining=player_status_effects.c.ticks_remaining - 1)
|
||||
)
|
||||
|
||||
# NOTE: We do NOT remove expired effects here anymore.
|
||||
# They will be processed by the background task (to apply final tick)
|
||||
# and then cleaned up via clean_expired_status_effects()
|
||||
|
||||
await session.commit()
|
||||
return players_to_process
|
||||
|
||||
|
||||
async def is_player_in_combat(player_id: int) -> bool:
|
||||
"""Check if player is in any active combat (PvE or PvP)."""
|
||||
async with DatabaseSession() as session:
|
||||
# Check PvE combat
|
||||
pve = await session.execute(
|
||||
select(active_combats.c.id).where(active_combats.c.character_id == player_id)
|
||||
)
|
||||
if pve.first():
|
||||
return True
|
||||
|
||||
# Check PvP combat
|
||||
pvp = await session.execute(
|
||||
select(pvp_combats.c.id).where(
|
||||
or_(
|
||||
pvp_combats.c.attacker_character_id == player_id,
|
||||
pvp_combats.c.defender_character_id == player_id
|
||||
)
|
||||
)
|
||||
)
|
||||
if pvp.first():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -335,6 +335,55 @@ async def use_item(player_id: int, item_id: str, items_manager, locale: str = 'e
|
||||
effects = {}
|
||||
effects_msg = []
|
||||
|
||||
# 1. Apply Status Effects (e.g. Regeneration from Bandage)
|
||||
if 'status_effect' in item.effects:
|
||||
status_data = item.effects['status_effect']
|
||||
|
||||
# Check if effect already exists
|
||||
current_effects = await db.get_player_effects(player_id)
|
||||
effect_name = status_data['name']
|
||||
|
||||
# Handle potential dict/string difference in validation (db stores as string usually)
|
||||
# But we need to compare with what's in the DB.
|
||||
# DB get_player_effects returns list of dicts with 'effect_name' key.
|
||||
|
||||
is_active = False
|
||||
for effect in current_effects:
|
||||
# Simple string comparison should suffice as both should be localized keys or raw strings
|
||||
if effect['effect_name'] == effect_name:
|
||||
is_active = True
|
||||
break
|
||||
|
||||
if is_active:
|
||||
return {"success": False, "message": get_game_message('effect_already_active', locale)}
|
||||
|
||||
await db.add_effect(
|
||||
player_id=player['id'],
|
||||
effect_name=status_data['name'],
|
||||
effect_icon=status_data.get('icon', '✨'),
|
||||
effect_type=status_data.get('type', 'buff'),
|
||||
damage_per_tick=status_data.get('damage_per_tick', 0),
|
||||
value=status_data.get('value', 0),
|
||||
ticks_remaining=status_data.get('ticks', 3),
|
||||
persist_after_combat=True, # Consumable effects usually persist
|
||||
source=f"item:{item.id}"
|
||||
)
|
||||
effects['status_applied'] = status_data['name']
|
||||
effects_msg.append(f"Applied {get_locale_string(status_data['name'], locale) if isinstance(status_data['name'], dict) else status_data['name']}")
|
||||
|
||||
# 2. Cure Status Effects
|
||||
if 'cures' in item.effects:
|
||||
cures = item.effects['cures']
|
||||
cured_list = []
|
||||
for cure_effect in cures:
|
||||
if await db.remove_effect(player['id'], cure_effect):
|
||||
cured_list.append(cure_effect)
|
||||
|
||||
if cured_list:
|
||||
effects['cured'] = cured_list
|
||||
effects_msg.append(f"{get_game_message('cured', locale)}: {', '.join(cured_list)}")
|
||||
|
||||
# 3. Direct Healing (Legacy/Instant)
|
||||
if 'hp_restore' in item.effects:
|
||||
hp_restore = item.effects['hp_restore']
|
||||
old_hp = player['hp']
|
||||
@@ -496,15 +545,17 @@ async def check_and_apply_level_up(player_id: int) -> Dict[str, Any]:
|
||||
# STATUS EFFECTS UTILITIES
|
||||
# ============================================================================
|
||||
|
||||
def calculate_status_damage(effects: list) -> int:
|
||||
def calculate_status_impact(effects: list) -> int:
|
||||
"""
|
||||
Calculate total damage from all status effects.
|
||||
Calculate total impact from all status effects.
|
||||
Positive value = Damage
|
||||
Negative value = Healing
|
||||
|
||||
Args:
|
||||
effects: List of status effect dicts
|
||||
|
||||
Returns:
|
||||
Total damage per tick
|
||||
Total impact per tick
|
||||
"""
|
||||
return sum(effect.get('damage_per_tick', 0) for effect in effects)
|
||||
|
||||
@@ -513,8 +564,6 @@ def calculate_status_damage(effects: list) -> int:
|
||||
# COMBAT UTILITIES
|
||||
# ============================================================================
|
||||
|
||||
return message, player_defeated
|
||||
|
||||
|
||||
def generate_npc_intent(npc_def, combat_state: dict) -> dict:
|
||||
"""
|
||||
@@ -548,6 +597,96 @@ async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -
|
||||
if not player:
|
||||
return [], True
|
||||
|
||||
messages = []
|
||||
|
||||
# 1. PROCESS NPC STATUS EFFECTS
|
||||
npc_hp = combat['npc_hp']
|
||||
npc_max_hp = combat['npc_max_hp']
|
||||
npc_status_str = combat.get('npc_status_effects', '')
|
||||
|
||||
if npc_status_str:
|
||||
# Parse status: "bleeding:5:3" (name:dmg:ticks) or multiple "bleeding:5:3|burning:2:2"
|
||||
# Handling multiple effects separated by |
|
||||
effects_list = npc_status_str.split('|')
|
||||
active_effects = []
|
||||
npc_damage_taken = 0
|
||||
npc_healing_received = 0
|
||||
|
||||
for effect_str in effects_list:
|
||||
if not effect_str: continue
|
||||
try:
|
||||
parts = effect_str.split(':')
|
||||
if len(parts) >= 3:
|
||||
name = parts[0]
|
||||
dmg = int(parts[1])
|
||||
ticks = int(parts[2])
|
||||
|
||||
# Apply effect
|
||||
if ticks > 0:
|
||||
if dmg > 0:
|
||||
npc_damage_taken += dmg
|
||||
messages.append(create_combat_message(
|
||||
"effect_damage",
|
||||
origin="enemy",
|
||||
damage=dmg,
|
||||
effect_name=name,
|
||||
npc_name=npc_def.name
|
||||
))
|
||||
elif dmg < 0:
|
||||
heal = abs(dmg)
|
||||
npc_healing_received += heal
|
||||
messages.append(create_combat_message(
|
||||
"effect_heal", # Check if this message type exists or fallback
|
||||
origin="enemy",
|
||||
heal=heal,
|
||||
effect_name=name,
|
||||
npc_name=npc_def.name
|
||||
))
|
||||
|
||||
# Decrement tick
|
||||
ticks -= 1
|
||||
if ticks > 0:
|
||||
active_effects.append(f"{name}:{dmg}:{ticks}")
|
||||
except Exception as e:
|
||||
print(f"Error parsing NPC status: {e}")
|
||||
|
||||
# Update NPC active effects
|
||||
new_status_str = "|".join(active_effects)
|
||||
if new_status_str != npc_status_str:
|
||||
await db.update_combat(player_id, {'npc_status_effects': new_status_str})
|
||||
|
||||
# Apply Total Damage/Healing
|
||||
if npc_damage_taken > 0:
|
||||
npc_hp = max(0, npc_hp - npc_damage_taken)
|
||||
|
||||
if npc_healing_received > 0:
|
||||
npc_hp = min(npc_max_hp, npc_hp + npc_healing_received)
|
||||
|
||||
# Update NPC HP in DB
|
||||
await db.update_combat(player_id, {'npc_hp': npc_hp})
|
||||
|
||||
# Check if NPC died from effects
|
||||
if npc_hp <= 0:
|
||||
messages.append(create_combat_message(
|
||||
"victory",
|
||||
origin="neutral",
|
||||
npc_name=npc_def.name
|
||||
))
|
||||
# Award XP/Loot logic handled in combat route mostly, but we need to signal it.
|
||||
# Returning true for player_defeated is definitely WRONG here if NPC died.
|
||||
# The router usually handles "victory" check after action.
|
||||
# But here this is triggered during NPC turn (which happens after Player turn).
|
||||
# If NPC dies on its OWN turn, we need to handle it.
|
||||
# However, typically NPC dies on Player turn.
|
||||
# If NPC dies from bleeding on its turn, the player wins.
|
||||
# We need to signal this back to router.
|
||||
# But the current return signature is (messages, player_defeated).
|
||||
# We might need to handle the win logic here or update signature.
|
||||
# For now, let's update HP and let the flow continue.
|
||||
# Wait, if NPC is dead, it shouldn't attack!
|
||||
# returning here prevents NPC from attacking if it died from status effects
|
||||
return messages, False
|
||||
|
||||
# Parse current intent (stored in DB as string or JSON, assuming simple string for now or we parse it)
|
||||
current_intent_str = combat.get('npc_intent', 'attack')
|
||||
# Handle legacy/null
|
||||
@@ -556,84 +695,98 @@ async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -
|
||||
|
||||
intent_type = current_intent_str
|
||||
|
||||
messages = []
|
||||
actual_damage = 0
|
||||
|
||||
# EXECUTE INTENT
|
||||
if intent_type == 'defend':
|
||||
# NPC defends - heals 5% HP
|
||||
heal_amount = int(combat['npc_max_hp'] * 0.05)
|
||||
new_npc_hp = min(combat['npc_max_hp'], combat['npc_hp'] + heal_amount)
|
||||
await db.update_combat(player_id, {'npc_hp': new_npc_hp})
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"enemy_defend",
|
||||
origin="enemy",
|
||||
npc_name=npc_def.name,
|
||||
heal=heal_amount
|
||||
))
|
||||
|
||||
elif intent_type == 'special':
|
||||
# Strong attack (1.5x damage)
|
||||
npc_damage = int(random.randint(npc_def.damage_min, npc_def.damage_max) * 1.5)
|
||||
armor_absorbed, broken_armor = await reduce_armor_func(player_id, npc_damage)
|
||||
actual_damage = max(1, npc_damage - armor_absorbed)
|
||||
new_player_hp = max(0, player['hp'] - actual_damage)
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"enemy_special",
|
||||
origin="enemy",
|
||||
npc_name=npc_def.name,
|
||||
damage=npc_damage,
|
||||
armor_absorbed=armor_absorbed
|
||||
))
|
||||
if npc_hp > 0: # Only attack if alive
|
||||
if intent_type == 'defend':
|
||||
# NPC defends - heals 5% HP
|
||||
heal_amount = int(combat['npc_max_hp'] * 0.05)
|
||||
new_npc_hp = min(combat['npc_max_hp'], combat['npc_hp'] + heal_amount)
|
||||
await db.update_combat(player_id, {'npc_hp': new_npc_hp})
|
||||
|
||||
if broken_armor:
|
||||
for armor in broken_armor:
|
||||
messages.append(create_combat_message(
|
||||
"item_broken",
|
||||
origin="player",
|
||||
item_name=armor['name'],
|
||||
emoji=armor['emoji']
|
||||
))
|
||||
|
||||
await db.update_player(player_id, hp=new_player_hp)
|
||||
|
||||
else: # Default 'attack'
|
||||
npc_damage = random.randint(npc_def.damage_min, npc_def.damage_max)
|
||||
|
||||
# Enrage bonus if NPC is below 30% HP
|
||||
is_enraged = combat['npc_hp'] / combat['npc_max_hp'] < 0.3
|
||||
if is_enraged:
|
||||
npc_damage = int(npc_damage * 1.5)
|
||||
messages.append(create_combat_message(
|
||||
"enemy_enraged",
|
||||
origin="enemy",
|
||||
npc_name=npc_def.name
|
||||
"enemy_defend",
|
||||
origin="enemy",
|
||||
npc_name=npc_def.name,
|
||||
heal=heal_amount
|
||||
))
|
||||
|
||||
armor_absorbed, broken_armor = await reduce_armor_func(player_id, npc_damage)
|
||||
actual_damage = max(1, npc_damage - armor_absorbed)
|
||||
new_player_hp = max(0, player['hp'] - actual_damage)
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"enemy_attack",
|
||||
origin="enemy",
|
||||
npc_name=npc_def.name,
|
||||
damage=npc_damage,
|
||||
armor_absorbed=armor_absorbed
|
||||
))
|
||||
elif intent_type == 'special':
|
||||
# Strong attack (1.5x damage)
|
||||
npc_damage = int(random.randint(npc_def.damage_min, npc_def.damage_max) * 1.5)
|
||||
armor_absorbed, broken_armor = await reduce_armor_func(player_id, npc_damage)
|
||||
actual_damage = max(1, npc_damage - armor_absorbed)
|
||||
new_player_hp = max(0, player['hp'] - actual_damage)
|
||||
|
||||
if broken_armor:
|
||||
for armor in broken_armor:
|
||||
messages.append(create_combat_message(
|
||||
"item_broken",
|
||||
origin="player",
|
||||
item_name=armor['name'],
|
||||
emoji=armor['emoji']
|
||||
))
|
||||
messages.append(create_combat_message(
|
||||
"enemy_special",
|
||||
origin="enemy",
|
||||
npc_name=npc_def.name,
|
||||
damage=npc_damage,
|
||||
armor_absorbed=armor_absorbed
|
||||
))
|
||||
|
||||
await db.update_player(player_id, hp=new_player_hp)
|
||||
if broken_armor:
|
||||
for armor in broken_armor:
|
||||
messages.append(create_combat_message(
|
||||
"item_broken",
|
||||
origin="player",
|
||||
item_name=armor['name'],
|
||||
emoji=armor['emoji']
|
||||
))
|
||||
|
||||
await db.update_player(player_id, hp=new_player_hp)
|
||||
|
||||
else: # Default 'attack'
|
||||
npc_damage = random.randint(npc_def.damage_min, npc_def.damage_max)
|
||||
|
||||
# Enrage bonus if NPC is below 30% HP
|
||||
is_enraged = combat['npc_hp'] / combat['npc_max_hp'] < 0.3
|
||||
if is_enraged:
|
||||
npc_damage = int(npc_damage * 1.5)
|
||||
messages.append(create_combat_message(
|
||||
"enemy_enraged",
|
||||
origin="enemy",
|
||||
npc_name=npc_def.name
|
||||
))
|
||||
|
||||
# Check if player is defending (reduces damage by value%)
|
||||
player_effects = await db.get_player_effects(player_id)
|
||||
defending_effect = next((e for e in player_effects if e['effect_name'] == 'defending'), None)
|
||||
if defending_effect:
|
||||
reduction = defending_effect.get('value', 50) / 100 # Default 50% reduction
|
||||
npc_damage = int(npc_damage * (1 - reduction))
|
||||
messages.append(create_combat_message(
|
||||
"damage_reduced",
|
||||
origin="player",
|
||||
reduction=int(reduction * 100)
|
||||
))
|
||||
# Remove defending effect after use
|
||||
await db.remove_effect(player_id, 'defending')
|
||||
|
||||
armor_absorbed, broken_armor = await reduce_armor_func(player_id, npc_damage)
|
||||
actual_damage = max(1, npc_damage - armor_absorbed)
|
||||
new_player_hp = max(0, player['hp'] - actual_damage)
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"enemy_attack",
|
||||
origin="enemy",
|
||||
npc_name=npc_def.name,
|
||||
damage=npc_damage,
|
||||
armor_absorbed=armor_absorbed
|
||||
))
|
||||
|
||||
if broken_armor:
|
||||
for armor in broken_armor:
|
||||
messages.append(create_combat_message(
|
||||
"item_broken",
|
||||
origin="player",
|
||||
item_name=armor['name'],
|
||||
emoji=armor['emoji']
|
||||
))
|
||||
|
||||
await db.update_player(player_id, hp=new_player_hp)
|
||||
|
||||
# GENERATE NEXT INTENT
|
||||
|
||||
|
||||
12
api/items.py
12
api/items.py
@@ -45,6 +45,10 @@ class Item:
|
||||
uncraft_yield: list = None # Materials yielded from uncrafting (before loss chance)
|
||||
uncraft_loss_chance: float = 0.3 # Chance to lose materials when uncrafting (0.3 = 30%)
|
||||
uncraft_tools: list = None # Tools required for uncrafting
|
||||
# Combat system
|
||||
combat_usable: bool = False # Can be used during combat
|
||||
combat_only: bool = False # Can ONLY be used during combat
|
||||
combat_effects: Dict[str, Any] = None # Effects applied in combat (damage, status)
|
||||
|
||||
def __post_init__(self):
|
||||
if self.stats is None:
|
||||
@@ -65,7 +69,8 @@ class Item:
|
||||
self.uncraft_yield = []
|
||||
if self.uncraft_tools is None:
|
||||
self.uncraft_tools = []
|
||||
self.craft_materials = []
|
||||
if self.combat_effects is None:
|
||||
self.combat_effects = {}
|
||||
|
||||
|
||||
class ItemsManager:
|
||||
@@ -129,7 +134,10 @@ class ItemsManager:
|
||||
uncraftable=item_data.get('uncraftable', False),
|
||||
uncraft_yield=item_data.get('uncraft_yield', []),
|
||||
uncraft_loss_chance=item_data.get('uncraft_loss_chance', 0.3),
|
||||
uncraft_tools=item_data.get('uncraft_tools', [])
|
||||
uncraft_tools=item_data.get('uncraft_tools', []),
|
||||
combat_usable=item_data.get('combat_usable', is_consumable), # Default: consumables are combat usable
|
||||
combat_only=item_data.get('combat_only', False),
|
||||
combat_effects=item_data.get('combat_effects', {})
|
||||
)
|
||||
self.items[item_id] = item
|
||||
|
||||
|
||||
@@ -249,7 +249,59 @@ async def combat_action(
|
||||
|
||||
messages = []
|
||||
combat_over = False
|
||||
player_won = False
|
||||
|
||||
# Process status effects (bleeding, etc.) before action
|
||||
active_effects = await db.tick_player_effects(player['id'])
|
||||
|
||||
# Process status effects before action
|
||||
if active_effects:
|
||||
from ..game_logic import calculate_status_impact
|
||||
total_impact = calculate_status_impact(active_effects)
|
||||
|
||||
if total_impact > 0:
|
||||
# DAMAGE
|
||||
damage = total_impact
|
||||
new_hp = max(0, player['hp'] - damage)
|
||||
await db.update_player_hp(player['id'], new_hp)
|
||||
player['hp'] = new_hp # Update local reference
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"effect_damage",
|
||||
origin="player",
|
||||
damage=damage,
|
||||
effect_name="status effects"
|
||||
))
|
||||
|
||||
if new_hp <= 0:
|
||||
# Player died from effects
|
||||
await db.remove_non_persistent_effects(player['id'])
|
||||
await db.end_combat(player['id'])
|
||||
|
||||
return {
|
||||
"player": player,
|
||||
"combat": None,
|
||||
"messages": messages + [create_combat_message("died", origin="player", message="You died from status effects!")],
|
||||
"active_effects": [],
|
||||
"round": combat['round']
|
||||
}
|
||||
elif total_impact < 0:
|
||||
# HEALING
|
||||
heal = abs(total_impact)
|
||||
new_hp = min(player['max_hp'], player['hp'] + heal)
|
||||
actual_heal = new_hp - player['hp']
|
||||
|
||||
if actual_heal > 0:
|
||||
await db.update_player_hp(player['id'], new_hp)
|
||||
player['hp'] = new_hp
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"effect_heal",
|
||||
origin="player",
|
||||
heal=actual_heal,
|
||||
effect_name="status effects"
|
||||
))
|
||||
|
||||
|
||||
|
||||
if req.action == 'attack':
|
||||
# Calculate player damage
|
||||
@@ -382,6 +434,9 @@ async def combat_action(
|
||||
loot_remaining=json.dumps(corpse_loot_dicts)
|
||||
)
|
||||
|
||||
|
||||
|
||||
await db.remove_non_persistent_effects(player['id'])
|
||||
await db.end_combat(player['id'])
|
||||
|
||||
# Update Redis: Delete combat state cache
|
||||
@@ -456,6 +511,7 @@ async def combat_action(
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
|
||||
await db.remove_non_persistent_effects(player['id'])
|
||||
await db.end_combat(player['id'])
|
||||
|
||||
# Broadcast to location that player fled from combat
|
||||
@@ -557,6 +613,7 @@ async def combat_action(
|
||||
await session.execute(stmt)
|
||||
await session.commit()
|
||||
|
||||
await db.remove_non_persistent_effects(player['id'])
|
||||
await db.end_combat(player['id'])
|
||||
|
||||
# Broadcast to location that player died (and corpse if created)
|
||||
@@ -584,6 +641,249 @@ async def combat_action(
|
||||
await db.update_player_statistics(player['id'], failed_flees=1, damage_taken=npc_damage, increment=True)
|
||||
await db.update_combat(player['id'], {'turn': 'player', 'turn_started_at': time.time()})
|
||||
|
||||
elif req.action == 'defend':
|
||||
# Apply "defending" status effect - reduces incoming damage by 50% for 1 turn
|
||||
await db.add_effect(
|
||||
player_id=player['id'],
|
||||
effect_name='defending',
|
||||
effect_icon='🛡️',
|
||||
effect_type='buff',
|
||||
value=50, # 50% damage reduction
|
||||
ticks_remaining=1,
|
||||
persist_after_combat=False,
|
||||
source='action:defend'
|
||||
)
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"defend",
|
||||
origin="player",
|
||||
message=get_game_message('defend_text', locale, name=player['name'])
|
||||
))
|
||||
|
||||
# NPC's turn after defend
|
||||
npc_attack_messages, player_defeated = await game_logic.npc_attack(
|
||||
player['id'],
|
||||
{'npc_hp': combat['npc_hp'], 'npc_max_hp': combat['npc_max_hp']},
|
||||
npc_def,
|
||||
reduce_armor_durability
|
||||
)
|
||||
messages.extend(npc_attack_messages)
|
||||
if player_defeated:
|
||||
await db.remove_non_persistent_effects(player['id'])
|
||||
combat_over = True
|
||||
|
||||
elif req.action == 'use_item':
|
||||
combat_over = False
|
||||
# Validate item_id provided
|
||||
if not req.item_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="item_id required for use_item action"
|
||||
)
|
||||
|
||||
# Get the item from inventory
|
||||
player_inventory = await db.get_inventory(player['id'])
|
||||
inv_item = None
|
||||
for item in player_inventory:
|
||||
if item['item_id'] == req.item_id:
|
||||
inv_item = item
|
||||
break
|
||||
|
||||
if not inv_item:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Item not found in inventory"
|
||||
)
|
||||
|
||||
# Get item definition
|
||||
item_def = ITEMS_MANAGER.get_item(req.item_id)
|
||||
if not item_def:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unknown item"
|
||||
)
|
||||
|
||||
# Check if item is combat usable
|
||||
if not item_def.combat_usable:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="This item cannot be used in combat"
|
||||
)
|
||||
|
||||
# Apply item effects
|
||||
item_name = get_locale_string(item_def.name, locale)
|
||||
effects_applied = []
|
||||
|
||||
# 1. Apply Status Effects (e.g. Regeneration from Bandage)
|
||||
if item_def.effects.get('status_effect'):
|
||||
status_data = item_def.effects['status_effect']
|
||||
await db.add_effect(
|
||||
player_id=player['id'],
|
||||
effect_name=status_data['name'],
|
||||
effect_icon=status_data.get('icon', '✨'),
|
||||
effect_type=status_data.get('type', 'buff'),
|
||||
damage_per_tick=status_data.get('damage_per_tick', 0),
|
||||
value=status_data.get('value', 0),
|
||||
ticks_remaining=status_data.get('ticks', 3),
|
||||
persist_after_combat=True, # Consumable effects usually persist
|
||||
source=f"item:{item_def.id}"
|
||||
)
|
||||
effects_applied.append(f"Applied {status_data['name']}")
|
||||
|
||||
# 2. Cure Status Effects
|
||||
if item_def.effects.get('cures'):
|
||||
cures = item_def.effects['cures']
|
||||
for cure_effect in cures:
|
||||
if await db.remove_effect(player['id'], cure_effect):
|
||||
effects_applied.append(f"Cured {cure_effect}")
|
||||
|
||||
# 3. Handle Direct healing (legacy/instant)
|
||||
if item_def.effects.get('hp_restore') and item_def.effects['hp_restore'] > 0:
|
||||
hp_restore = item_def.effects['hp_restore']
|
||||
old_hp = player['hp']
|
||||
new_hp = min(player.get('max_hp', 100), old_hp + hp_restore)
|
||||
actual_restored = new_hp - old_hp
|
||||
if actual_restored > 0:
|
||||
await db.update_player_hp(player['id'], new_hp)
|
||||
effects_applied.append(f"+{actual_restored} HP")
|
||||
|
||||
if item_def.effects.get('stamina_restore'):
|
||||
stamina_restore = item_def.effects['stamina_restore']
|
||||
old_stamina = player['stamina']
|
||||
new_stamina = min(player.get('max_stamina', 100), old_stamina + stamina_restore)
|
||||
actual_restored = new_stamina - old_stamina
|
||||
if actual_restored > 0:
|
||||
await db.update_player_stamina(player['id'], new_stamina)
|
||||
effects_applied.append(f"+{actual_restored} Stamina")
|
||||
|
||||
# Handle combat effects (throwables)
|
||||
combat_effects = item_def.combat_effects or {}
|
||||
|
||||
# Direct damage from throwable
|
||||
if combat_effects.get('damage_min') and combat_effects.get('damage_max'):
|
||||
damage = random.randint(combat_effects['damage_min'], combat_effects['damage_max'])
|
||||
new_npc_hp = max(0, combat['npc_hp'] - damage)
|
||||
effects_applied.append(f"{damage} damage")
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"item_damage",
|
||||
origin="player",
|
||||
damage=damage,
|
||||
item_name=item_name
|
||||
))
|
||||
|
||||
# Check if NPC is defeated
|
||||
if new_npc_hp <= 0:
|
||||
messages.append(create_combat_message(
|
||||
"victory",
|
||||
origin="neutral",
|
||||
npc_name=npc_def.name
|
||||
))
|
||||
combat_over = True
|
||||
player_won = True
|
||||
|
||||
# Award XP
|
||||
xp_gained = npc_def.xp_reward
|
||||
new_xp = player['xp'] + xp_gained
|
||||
messages.append(create_combat_message(
|
||||
"xp_gain",
|
||||
origin="player",
|
||||
amount=xp_gained
|
||||
))
|
||||
await db.update_player(player['id'], xp=new_xp)
|
||||
await db.update_player_statistics(player['id'], enemies_killed=1, damage_dealt=damage, increment=True)
|
||||
|
||||
# Check for level up
|
||||
level_up_result = await game_logic.check_and_apply_level_up(player['id'])
|
||||
if level_up_result['leveled_up']:
|
||||
messages.append(create_combat_message(
|
||||
"level_up",
|
||||
origin="player",
|
||||
level=level_up_result['new_level'],
|
||||
stat_points=level_up_result['levels_gained']
|
||||
))
|
||||
|
||||
# Create corpse with loot
|
||||
import json as json_module
|
||||
corpse_loot = npc_def.corpse_loot if hasattr(npc_def, 'corpse_loot') else []
|
||||
corpse_loot_dicts = []
|
||||
for loot in corpse_loot:
|
||||
if hasattr(loot, '__dict__'):
|
||||
corpse_loot_dicts.append({
|
||||
'item_id': loot.item_id,
|
||||
'quantity_min': loot.quantity_min,
|
||||
'quantity_max': loot.quantity_max,
|
||||
'required_tool': loot.required_tool
|
||||
})
|
||||
else:
|
||||
corpse_loot_dicts.append(loot)
|
||||
await db.create_npc_corpse(
|
||||
npc_id=combat['npc_id'],
|
||||
location_id=player['location_id'],
|
||||
loot_remaining=json_module.dumps(corpse_loot_dicts)
|
||||
)
|
||||
await db.remove_non_persistent_effects(player['id'])
|
||||
await db.end_combat(player['id'])
|
||||
else:
|
||||
# Update NPC HP
|
||||
await db.update_combat(player['id'], {'npc_hp': new_npc_hp})
|
||||
|
||||
# Apply status effect from item (e.g., burning from molotov)
|
||||
status_effect = combat_effects.get('status')
|
||||
if status_effect and not combat_over:
|
||||
# Apply to NPC via combat status (simplified - NPC status stored in combat record)
|
||||
npc_status = f"{status_effect['name']}:{status_effect.get('damage_per_tick', 0)}:{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",
|
||||
effect_name=status_effect['name'],
|
||||
effect_icon=status_effect.get('icon', '🔥'),
|
||||
target="enemy"
|
||||
))
|
||||
|
||||
# Consume the item
|
||||
await db.remove_item_from_inventory(player['id'], req.item_id, 1)
|
||||
await db.update_player_statistics(player['id'], items_used=1, increment=True)
|
||||
|
||||
# Add item used message
|
||||
effects_str = f" ({', '.join(effects_applied)})" if effects_applied else ""
|
||||
|
||||
# Calculate total restored amounts for frontend floating text
|
||||
hp_restored_val = 0
|
||||
stamina_restored_val = 0
|
||||
|
||||
if item_def.effects.get('hp_restore'):
|
||||
hp_restored_val = min(player.get('max_hp', 100), old_hp + item_def.effects['hp_restore']) - old_hp
|
||||
|
||||
if item_def.effects.get('stamina_restore'):
|
||||
stamina_restored_val = min(player.get('max_stamina', 100), old_stamina + item_def.effects['stamina_restore']) - old_stamina
|
||||
|
||||
messages.append(create_combat_message(
|
||||
"item_used",
|
||||
origin="player",
|
||||
item_name=item_name,
|
||||
effects=effects_str,
|
||||
hp_restore=hp_restored_val if hp_restored_val > 0 else None,
|
||||
stamina_restore=stamina_restored_val if stamina_restored_val > 0 else None
|
||||
))
|
||||
|
||||
# NPC's turn after using item (if combat not over)
|
||||
if not combat_over:
|
||||
npc_attack_messages, player_defeated = await game_logic.npc_attack(
|
||||
player['id'],
|
||||
{'npc_hp': combat['npc_hp'], 'npc_max_hp': combat['npc_max_hp']},
|
||||
npc_def,
|
||||
reduce_armor_durability
|
||||
)
|
||||
messages.extend(npc_attack_messages)
|
||||
|
||||
if player_defeated:
|
||||
await db.remove_non_persistent_effects(player['id'])
|
||||
combat_over = True
|
||||
|
||||
|
||||
# Get updated combat state if not over
|
||||
updated_combat = None
|
||||
if not combat_over:
|
||||
|
||||
@@ -586,6 +586,7 @@ async def get_repairable_items(current_user: dict = Depends(get_current_user)):
|
||||
# Check if player has this tool (find one with highest durability)
|
||||
tool_found = False
|
||||
tool_durability = 0
|
||||
tool_max_durability = 0
|
||||
best_tool_unique = None
|
||||
|
||||
for check_item in inventory:
|
||||
@@ -596,6 +597,7 @@ async def get_repairable_items(current_user: dict = Depends(get_current_user)):
|
||||
best_tool_unique = unique
|
||||
tool_found = True
|
||||
tool_durability = unique.get('durability', 0)
|
||||
tool_max_durability = unique.get('max_durability', 100)
|
||||
|
||||
|
||||
tools_info.append({
|
||||
@@ -604,7 +606,8 @@ async def get_repairable_items(current_user: dict = Depends(get_current_user)):
|
||||
'emoji': tool_def.emoji if tool_def else '🔧',
|
||||
'durability_cost': durability_cost,
|
||||
'has_tool': tool_found,
|
||||
'tool_durability': tool_durability
|
||||
'tool_durability': tool_durability,
|
||||
'tool_max_durability': tool_max_durability
|
||||
})
|
||||
if not tool_found:
|
||||
has_tools = False
|
||||
@@ -633,7 +636,7 @@ async def get_repairable_items(current_user: dict = Depends(get_current_user)):
|
||||
})
|
||||
|
||||
# Sort: repairable items first (can_repair=True), then by durability percent (lowest first), then by name
|
||||
repairable_items.sort(key=lambda x: (not x['can_repair'], -x['durability_percent'], x['name']))
|
||||
repairable_items.sort(key=lambda x: (not x['can_repair'], -x['durability_percent'], str(x['name'])))
|
||||
|
||||
return {'repairable_items': repairable_items}
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ async def _get_enriched_inventory(player_id: int):
|
||||
"unique_stats": unique_stats,
|
||||
"hp_restore": item.effects.get('hp_restore') if item.effects else None,
|
||||
"stamina_restore": item.effects.get('stamina_restore') if item.effects else None,
|
||||
"effects": item.effects,
|
||||
"damage_min": item.stats.get('damage_min') if item.stats else None,
|
||||
"damage_max": item.stats.get('damage_max') if item.stats else None,
|
||||
"stats": item.stats,
|
||||
@@ -183,6 +184,10 @@ async def get_game_state(current_user: dict = Depends(get_current_user)):
|
||||
if not player:
|
||||
raise HTTPException(status_code=404, detail="Player not found")
|
||||
|
||||
# Get player status effects
|
||||
status_effects = await db.get_player_effects(player_id)
|
||||
player['status_effects'] = status_effects
|
||||
|
||||
# Get location
|
||||
location = LOCATIONS.get(player['location_id'])
|
||||
|
||||
@@ -274,6 +279,7 @@ async def get_game_state(current_user: dict = Depends(get_current_user)):
|
||||
"tier": tier if tier is not None else None,
|
||||
"hp_restore": item.effects.get('hp_restore') if item.effects else None,
|
||||
"stamina_restore": item.effects.get('stamina_restore') if item.effects else None,
|
||||
"effects": item.effects,
|
||||
"damage_min": item.stats.get('damage_min') if item.stats else None,
|
||||
"damage_max": item.stats.get('damage_max') if item.stats else None
|
||||
})
|
||||
@@ -325,6 +331,10 @@ async def get_player_profile(current_user: dict = Depends(get_current_user)):
|
||||
player = await db.get_player_by_id(player_id)
|
||||
if not player:
|
||||
raise HTTPException(status_code=404, detail="Player not found")
|
||||
|
||||
# Get player status effects
|
||||
status_effects = await db.get_player_effects(player_id)
|
||||
player['status_effects'] = status_effects
|
||||
|
||||
# Get capacity metrics (weight/volume) using the helper function
|
||||
# We don't need the inventory array itself, just the capacity calculations
|
||||
|
||||
@@ -96,7 +96,13 @@ GAME_MESSAGES = {
|
||||
# Item Usage
|
||||
'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_use': {'en': "This item cannot be used", 'es': "Este objeto no se puede usar"},
|
||||
'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"},
|
||||
}
|
||||
|
||||
def get_game_message(key: str, lang: str = 'en', **kwargs) -> str:
|
||||
|
||||
@@ -79,7 +79,8 @@ class InitiateCombatRequest(BaseModel):
|
||||
|
||||
|
||||
class CombatActionRequest(BaseModel):
|
||||
action: str # 'attack', 'defend', 'flee'
|
||||
action: str # 'attack', 'defend', 'flee', 'use_item'
|
||||
item_id: Optional[str] = None # For use_item action
|
||||
|
||||
|
||||
class PvPCombatInitiateRequest(BaseModel):
|
||||
@@ -91,7 +92,8 @@ class PvPAcknowledgeRequest(BaseModel):
|
||||
|
||||
|
||||
class PvPCombatActionRequest(BaseModel):
|
||||
action: str # 'attack', 'defend', 'flee'
|
||||
action: str # 'attack', 'defend', 'flee', 'use_item'
|
||||
item_id: Optional[str] = None # For use_item action
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user