feat(backend): Integrate Derived Stats into combat, loot, and crafting mechanics
This commit is contained in:
@@ -208,12 +208,17 @@ async def interact_with_object(
|
||||
items_dropped = []
|
||||
damage_taken = outcome.damage_taken
|
||||
|
||||
# Calculate current capacity
|
||||
# Calculate current capacity and fetch derived stats
|
||||
from api.services.helpers import calculate_player_capacity
|
||||
from api.services.stats import calculate_derived_stats
|
||||
from api.items import items_manager as ITEMS_MANAGER
|
||||
|
||||
inventory = await db.get_inventory(player_id)
|
||||
current_weight, max_weight, current_volume, max_volume = await calculate_player_capacity(inventory, ITEMS_MANAGER)
|
||||
|
||||
stats = await calculate_derived_stats(player_id)
|
||||
loot_quality = stats.get('loot_quality', 1.0)
|
||||
|
||||
# Add items to inventory (or drop if over capacity)
|
||||
for item_id, quantity in outcome.items_reward.items():
|
||||
item = items_manager.get_item(item_id)
|
||||
@@ -258,7 +263,12 @@ async def interact_with_object(
|
||||
await db.drop_item_to_world(item_id, 1, player['location_id'], unique_item_id=unique_item_id)
|
||||
items_dropped.append(f"{emoji} {item_name}")
|
||||
else:
|
||||
# Stackable items - process as before
|
||||
# Stackable items - apply loot quality bonus for resources and consumables
|
||||
if getattr(item, 'category', item.type) in ['resource', 'consumable'] and loot_quality > 1.0:
|
||||
bonus_chance = loot_quality - 1.0
|
||||
if random.random() < bonus_chance:
|
||||
quantity += 1
|
||||
|
||||
item_weight = item.weight * quantity
|
||||
item_volume = item.volume * quantity
|
||||
|
||||
@@ -312,6 +322,15 @@ async def use_item(player_id: int, item_id: str, items_manager, locale: str = 'e
|
||||
if not player:
|
||||
return {"success": False, "message": "Player not found"}
|
||||
|
||||
# Get derived stats for item effectiveness
|
||||
# In some paths redis_manager might not be injected, so we attempt to fetch it from websockets module if needed,
|
||||
# or let stats service fetch without cache
|
||||
from api.services.stats import calculate_derived_stats
|
||||
import api.core.websockets as ws
|
||||
redis_mgr = getattr(ws.manager, 'redis_manager', None)
|
||||
stats = await calculate_derived_stats(player['id'], redis_mgr)
|
||||
item_effectiveness = stats.get('item_effectiveness', 1.0)
|
||||
|
||||
# Check if player has the item
|
||||
inventory = await db.get_inventory(player_id)
|
||||
item_entry = None
|
||||
@@ -385,7 +404,8 @@ async def use_item(player_id: int, item_id: str, items_manager, locale: str = 'e
|
||||
|
||||
# 3. Direct Healing (Legacy/Instant)
|
||||
if 'hp_restore' in item.effects:
|
||||
hp_restore = item.effects['hp_restore']
|
||||
base_hp_restore = item.effects['hp_restore']
|
||||
hp_restore = int(base_hp_restore * item_effectiveness)
|
||||
old_hp = player['hp']
|
||||
new_hp = min(player['max_hp'], old_hp + hp_restore)
|
||||
actual_restored = new_hp - old_hp
|
||||
@@ -395,7 +415,8 @@ async def use_item(player_id: int, item_id: str, items_manager, locale: str = 'e
|
||||
effects_msg.append(f"+{actual_restored} HP")
|
||||
|
||||
if 'stamina_restore' in item.effects:
|
||||
stamina_restore = item.effects['stamina_restore']
|
||||
base_stamina_restore = item.effects['stamina_restore']
|
||||
stamina_restore = int(base_stamina_restore * item_effectiveness)
|
||||
old_stamina = player['stamina']
|
||||
new_stamina = min(player['max_stamina'], old_stamina + stamina_restore)
|
||||
actual_restored = new_stamina - old_stamina
|
||||
@@ -532,6 +553,14 @@ async def check_and_apply_level_up(player_id: int) -> Dict[str, Any]:
|
||||
unspent_points=new_unspent_points
|
||||
)
|
||||
|
||||
# Invalidate cached derived stats (level affects max_hp, max_stamina, attack_power, etc.)
|
||||
from api.services.stats import invalidate_stats_cache
|
||||
try:
|
||||
from api.core.websockets import manager as ws_manager
|
||||
await invalidate_stats_cache(player_id, getattr(ws_manager, 'redis_manager', None))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"leveled_up": True,
|
||||
"new_level": current_level,
|
||||
@@ -588,7 +617,7 @@ def generate_npc_intent(npc_def, combat_state: dict) -> dict:
|
||||
return intent
|
||||
|
||||
|
||||
async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -> Tuple[List[dict], bool]:
|
||||
async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func, player_stats: dict = None) -> Tuple[List[dict], bool]:
|
||||
"""
|
||||
Execute NPC turn based on PREVIOUS intent, then generate NEXT intent.
|
||||
Returns: (messages_list, player_defeated)
|
||||
@@ -603,21 +632,38 @@ async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -
|
||||
npc_hp = combat['npc_hp']
|
||||
npc_max_hp = combat['npc_max_hp']
|
||||
npc_status_str = combat.get('npc_status_effects', '')
|
||||
is_stunned = False
|
||||
|
||||
if npc_status_str:
|
||||
# Parse status: "bleeding:5:3" (name:dmg:ticks) or multiple "bleeding:5:3|burning:2:2"
|
||||
# Parse status: "bleeding:5:3" (name:dmg:ticks) or "stun:1"
|
||||
# Handling multiple effects separated by |
|
||||
effects_list = npc_status_str.split('|')
|
||||
active_effects = []
|
||||
npc_damage_taken = 0
|
||||
npc_healing_received = 0
|
||||
is_stunned = False
|
||||
|
||||
for effect_str in effects_list:
|
||||
if not effect_str: continue
|
||||
try:
|
||||
parts = effect_str.split(':')
|
||||
name = parts[0]
|
||||
|
||||
if name == 'stun' and len(parts) >= 2:
|
||||
ticks = int(parts[1])
|
||||
if ticks > 0:
|
||||
is_stunned = True
|
||||
messages.append(create_combat_message(
|
||||
"skill_effect",
|
||||
origin="enemy",
|
||||
message=f"💫 {npc_def.name} is stunned and cannot act!"
|
||||
))
|
||||
ticks -= 1
|
||||
if ticks > 0:
|
||||
active_effects.append(f"stun:{ticks}")
|
||||
continue
|
||||
|
||||
if len(parts) >= 3:
|
||||
name = parts[0]
|
||||
dmg = int(parts[1])
|
||||
ticks = int(parts[2])
|
||||
|
||||
@@ -698,7 +744,7 @@ async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -
|
||||
actual_damage = 0
|
||||
|
||||
# EXECUTE INTENT
|
||||
if npc_hp > 0: # Only attack if alive
|
||||
if npc_hp > 0 and not is_stunned: # Only attack if alive and not stunned
|
||||
if intent_type == 'defend':
|
||||
# NPC defends - heals 5% HP
|
||||
heal_amount = int(combat['npc_max_hp'] * 0.05)
|
||||
@@ -765,28 +811,65 @@ async def npc_attack(player_id: int, combat: dict, npc_def, reduce_armor_func) -
|
||||
# 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:
|
||||
# Check for dodge
|
||||
dodged = False
|
||||
if player_stats and 'dodge_chance' in player_stats:
|
||||
if random.random() < player_stats['dodge_chance']:
|
||||
dodged = True
|
||||
messages.append(create_combat_message(
|
||||
"item_broken",
|
||||
origin="player",
|
||||
item_name=armor['name'],
|
||||
emoji=armor['emoji']
|
||||
"combat_dodge",
|
||||
origin="player"
|
||||
))
|
||||
# Prevent damage calculation
|
||||
actual_damage = 0
|
||||
new_player_hp = player['hp']
|
||||
|
||||
# Check for block (if shield is equipped)
|
||||
blocked = False
|
||||
if not dodged and player_stats and player_stats.get('has_shield', False):
|
||||
if random.random() < player_stats.get('block_chance', 0):
|
||||
blocked = True
|
||||
messages.append(create_combat_message(
|
||||
"combat_block",
|
||||
origin="player"
|
||||
))
|
||||
# Apply blocked effect (damage reduced significantly or nullified)
|
||||
npc_damage = max(1, int(npc_damage * 0.2)) # Block mitigates 80% damage
|
||||
|
||||
await db.update_player(player_id, hp=new_player_hp)
|
||||
if not dodged:
|
||||
# Calculate armor durability loss based on PRE-reduction damage
|
||||
armor_absorbed, broken_armor = await reduce_armor_func(player_id, npc_damage)
|
||||
|
||||
# If player_stats provides a percentage reduction, apply it instead of raw absorption
|
||||
if player_stats and player_stats.get('armor_reduction', 0) > 0:
|
||||
pct_reduction = player_stats['armor_reduction']
|
||||
actual_damage = max(1, int(npc_damage * (1 - pct_reduction)))
|
||||
# Still show "armor_absorbed" conceptually for UI logs, though it's % based now
|
||||
armor_absorbed_visual = npc_damage - actual_damage
|
||||
else:
|
||||
actual_damage = max(1, npc_damage - armor_absorbed)
|
||||
armor_absorbed_visual = 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=actual_damage,
|
||||
armor_absorbed=armor_absorbed_visual
|
||||
))
|
||||
|
||||
if broken_armor and not dodged:
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user