Update
This commit is contained in:
@@ -10,6 +10,8 @@ from ..services.helpers import get_game_message
|
||||
from ..core.security import create_access_token, hash_password, verify_password, get_current_user
|
||||
from ..services.models import UserRegister, UserLogin
|
||||
from .. import database as db
|
||||
from ..items import items_manager
|
||||
from ..services.helpers import calculate_player_capacity, enrich_character_data
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["authentication"])
|
||||
|
||||
@@ -110,23 +112,7 @@ async def login(user: UserLogin):
|
||||
"is_premium": account.get("premium_expires_at") is not None,
|
||||
},
|
||||
"characters": [
|
||||
{
|
||||
"id": char["id"],
|
||||
"name": char["name"],
|
||||
"level": char["level"],
|
||||
"xp": char["xp"],
|
||||
"hp": char["hp"],
|
||||
"max_hp": char["max_hp"],
|
||||
"stamina": char["stamina"],
|
||||
"max_stamina": char["max_stamina"],
|
||||
"strength": char["strength"],
|
||||
"agility": char["agility"],
|
||||
"endurance": char["endurance"],
|
||||
"intellect": char["intellect"],
|
||||
"avatar_data": char.get("avatar_data"),
|
||||
"last_played_at": char.get("last_played_at"),
|
||||
"location_id": char["location_id"],
|
||||
}
|
||||
await enrich_character_data(char, items_manager)
|
||||
for char in characters
|
||||
],
|
||||
"needs_character_creation": len(characters) == 0
|
||||
@@ -188,17 +174,7 @@ async def get_account(current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"last_login_at": account.get("last_login_at"),
|
||||
},
|
||||
"characters": [
|
||||
{
|
||||
"id": char["id"],
|
||||
"name": char["name"],
|
||||
"level": char["level"],
|
||||
"xp": char["xp"],
|
||||
"hp": char["hp"],
|
||||
"max_hp": char["max_hp"],
|
||||
"location_id": char["location_id"],
|
||||
"avatar_data": char.get("avatar_data"),
|
||||
"last_played_at": char.get("last_played_at"),
|
||||
}
|
||||
await enrich_character_data(char, items_manager)
|
||||
for char in characters
|
||||
]
|
||||
}
|
||||
@@ -373,17 +349,7 @@ async def steam_login(steam_data: Dict[str, Any]):
|
||||
"last_login_at": account.get("last_login_at")
|
||||
},
|
||||
"characters": [
|
||||
{
|
||||
"id": char["id"],
|
||||
"name": char["name"],
|
||||
"level": char["level"],
|
||||
"xp": char["xp"],
|
||||
"hp": char["hp"],
|
||||
"max_hp": char["max_hp"],
|
||||
"location_id": char["location_id"],
|
||||
"avatar_data": char.get("avatar_data"),
|
||||
"last_played_at": char.get("last_played_at")
|
||||
}
|
||||
await enrich_character_data(char, items_manager)
|
||||
for char in characters
|
||||
],
|
||||
"needs_character_creation": len(characters) == 0
|
||||
|
||||
@@ -5,7 +5,8 @@ Handles character creation, selection, and deletion.
|
||||
from fastapi import APIRouter, HTTPException, Depends, status, Request
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
|
||||
from ..services.helpers import get_game_message
|
||||
from ..items import items_manager
|
||||
from ..services.helpers import enrich_character_data, get_game_message
|
||||
|
||||
from ..core.security import decode_token, create_access_token, security
|
||||
from ..services.models import CharacterCreate, CharacterSelect
|
||||
@@ -13,7 +14,6 @@ from .. import database as db
|
||||
|
||||
router = APIRouter(prefix="/api/characters", tags=["characters"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_characters(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||
"""List all characters for the logged-in account"""
|
||||
@@ -31,20 +31,7 @@ async def list_characters(credentials: HTTPAuthorizationCredentials = Depends(se
|
||||
|
||||
return {
|
||||
"characters": [
|
||||
{
|
||||
"id": char["id"],
|
||||
"name": char["name"],
|
||||
"level": char["level"],
|
||||
"xp": char["xp"],
|
||||
"hp": char["hp"],
|
||||
"max_hp": char["max_hp"],
|
||||
"stamina": char["stamina"],
|
||||
"max_stamina": char["max_stamina"],
|
||||
"avatar_data": char.get("avatar_data"),
|
||||
"location_id": char["location_id"],
|
||||
"created_at": char["created_at"],
|
||||
"last_played_at": char.get("last_played_at"),
|
||||
}
|
||||
await enrich_character_data(char, items_manager)
|
||||
for char in characters
|
||||
]
|
||||
}
|
||||
|
||||
@@ -464,9 +464,12 @@ async def combat_action(
|
||||
new_progress[obj['target']] = current_count + 1
|
||||
progres_changed = True
|
||||
|
||||
|
||||
if progres_changed:
|
||||
# Check completion
|
||||
all_done = True
|
||||
progress_str = ""
|
||||
|
||||
for obj in objectives:
|
||||
target = obj['target']
|
||||
req_count = obj['count']
|
||||
@@ -476,8 +479,10 @@ async def combat_action(
|
||||
if obj['type'] == 'kill_count':
|
||||
if curr < req_count:
|
||||
all_done = False
|
||||
# Capture progress string for the notification (if this was the target updated)
|
||||
if target == combat['npc_id']:
|
||||
progress_str = f" ({curr}/{req_count})"
|
||||
elif obj['type'] == 'item_delivery':
|
||||
# For mixed quests, we can't complete purely on kills.
|
||||
pass
|
||||
|
||||
await db.update_quest_progress(player['id'], q_record['quest_id'], new_progress, 'active')
|
||||
@@ -486,7 +491,7 @@ async def combat_action(
|
||||
messages.append(create_combat_message(
|
||||
"quest_update",
|
||||
origin="system",
|
||||
message=f"Quest updated: {get_locale_string(q_def['title'], locale)}"
|
||||
message=f"{get_locale_string(q_def['title'], locale)}{progress_str}"
|
||||
))
|
||||
quest_updated = True
|
||||
|
||||
@@ -501,62 +506,6 @@ async def combat_action(
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update quest progress: {e}")
|
||||
# -----------------------------
|
||||
|
||||
for q_record in active_quests:
|
||||
if q_record['status'] != 'active':
|
||||
continue
|
||||
|
||||
q_def = all_quests_def.get(q_record['quest_id'])
|
||||
if not q_def: continue
|
||||
|
||||
objectives = q_def.get('objectives', [])
|
||||
current_progress = q_record.get('progress') or {}
|
||||
new_progress = current_progress.copy()
|
||||
progres_changed = False
|
||||
|
||||
for obj in objectives:
|
||||
if obj['type'] == 'kill_count' and obj['target'] == combat['npc_id']:
|
||||
current_count = current_progress.get(obj['target'], 0)
|
||||
if current_count < obj['count']:
|
||||
new_progress[obj['target']] = current_count + 1
|
||||
progres_changed = True
|
||||
|
||||
if progres_changed:
|
||||
# Check completion
|
||||
all_done = True
|
||||
for obj in objectives:
|
||||
target = obj['target']
|
||||
req_count = obj['count']
|
||||
curr = new_progress.get(target, 0)
|
||||
|
||||
# Simple check for now (ignoring items for kill quests)
|
||||
if obj['type'] == 'kill_count':
|
||||
if curr < req_count:
|
||||
all_done = False
|
||||
elif obj['type'] == 'item_delivery':
|
||||
# Items are checked at hand-in usually
|
||||
# But we need to know if we should mark as completed "ready to turn in" or just "objectives met"
|
||||
# For mixed quests, we can't complete purely on kills.
|
||||
# Let's just update progress.
|
||||
pass
|
||||
|
||||
# We generally don't auto-complete quests, user has to hand in.
|
||||
# But we can update the progress in DB.
|
||||
new_status = 'active'
|
||||
# If we wanted to support auto-complete, we'd do it here. Use 'active'.
|
||||
|
||||
await db.update_quest_progress(player['id'], q_record['quest_id'], new_progress, new_status)
|
||||
|
||||
# Notify user
|
||||
messages.append(create_combat_message(
|
||||
"quest_update",
|
||||
origin="system",
|
||||
message=f"Quest updated: {get_locale_string(q_def['title'], locale)}"
|
||||
))
|
||||
quest_updated = True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update quest progress: {e}")
|
||||
# -----------------------------
|
||||
|
||||
|
||||
|
||||
@@ -502,6 +502,10 @@ async def get_current_location(request: Request, current_user: dict = Depends(ge
|
||||
# Format interactables for response with cooldown info
|
||||
interactables_data = []
|
||||
for interactable in location.interactables:
|
||||
# Check if locked
|
||||
if getattr(interactable, 'locked', False):
|
||||
continue
|
||||
|
||||
actions_data = []
|
||||
for action in interactable.actions:
|
||||
# Check cooldown status for this specific action
|
||||
@@ -556,6 +560,10 @@ async def get_current_location(request: Request, current_user: dict = Depends(ge
|
||||
destination_id = location.exits[direction]
|
||||
destination_loc = LOCATIONS.get(destination_id)
|
||||
|
||||
# Check if destination is locked
|
||||
if destination_loc and getattr(destination_loc, 'locked', False):
|
||||
continue
|
||||
|
||||
if destination_loc:
|
||||
# Calculate real distance using coordinates
|
||||
distance = calculate_distance(
|
||||
|
||||
@@ -3,10 +3,12 @@ from typing import Dict, List, Any, Optional
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from ..core.websockets import manager
|
||||
from ..core.security import get_current_user
|
||||
from .. import database as db
|
||||
from .. import game_logic
|
||||
from ..items import ItemsManager
|
||||
from ..services.helpers import get_locale_string
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/quests",
|
||||
@@ -14,19 +16,110 @@ router = APIRouter(
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
# Request Models
|
||||
class HistoryParams:
|
||||
page: int = 1
|
||||
page_size: int = 20
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Dependencies
|
||||
QUESTS_DATA = {}
|
||||
NPCS_DATA = {}
|
||||
LOCATIONS_DATA = {}
|
||||
|
||||
def init_router_dependencies(items_manager: ItemsManager, quests_data=None, npcs_data=None):
|
||||
global ITEMS_MANAGER, QUESTS_DATA, NPCS_DATA
|
||||
def init_router_dependencies(items_manager: ItemsManager, quests_data=None, npcs_data=None, locations_data=None):
|
||||
global ITEMS_MANAGER, QUESTS_DATA, NPCS_DATA, LOCATIONS_DATA
|
||||
ITEMS_MANAGER = items_manager
|
||||
if quests_data:
|
||||
QUESTS_DATA = quests_data
|
||||
if npcs_data:
|
||||
NPCS_DATA = npcs_data
|
||||
if locations_data:
|
||||
LOCATIONS_DATA = locations_data
|
||||
|
||||
@router.get("/history")
|
||||
async def get_quest_history_endpoint(
|
||||
page: int = 1,
|
||||
limit: int = 20,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get completed quest history with pagination"""
|
||||
character_id = current_user['id']
|
||||
history = await db.get_quest_history(character_id, page=page, page_size=limit)
|
||||
|
||||
# Enrich with quest definitions
|
||||
enriched_data = []
|
||||
for entry in history['data']:
|
||||
quest_def = QUESTS_DATA.get(entry['quest_id'])
|
||||
if quest_def:
|
||||
# Merge entry data with quest def
|
||||
item = dict(entry)
|
||||
item['title'] = quest_def.get('title')
|
||||
item['description'] = quest_def.get('description')
|
||||
item['type'] = quest_def.get('type')
|
||||
item['objectives'] = quest_def.get('objectives') # Fix: Copy objectives
|
||||
|
||||
# Enrich with giver info
|
||||
if quest_def.get('giver_id'):
|
||||
giver = NPCS_DATA.get(quest_def['giver_id'])
|
||||
if giver:
|
||||
item['giver_name'] = giver.get('name')
|
||||
item['giver_image'] = giver.get('image')
|
||||
|
||||
# Get Location Name
|
||||
if giver.get('location_id'):
|
||||
loc = LOCATIONS_DATA.get(giver['location_id'])
|
||||
if loc:
|
||||
item['giver_location_name'] = loc.name
|
||||
else:
|
||||
item['giver_location_name'] = giver['location_id']
|
||||
|
||||
enriched_data.append(item)
|
||||
else:
|
||||
# Fallback if quest def removed?
|
||||
enriched_data.append(entry)
|
||||
|
||||
# 2nd pass: Enrich objectives and rewards for all items in enriched_data
|
||||
final_data = []
|
||||
for q_data in enriched_data:
|
||||
# ENRICH OBJECTIVES WITH NAMES
|
||||
if 'objectives' in q_data:
|
||||
enriched_objs = []
|
||||
for obj in q_data['objectives']:
|
||||
new_obj = dict(obj)
|
||||
target = obj.get('target')
|
||||
if obj.get('type') == 'kill_count':
|
||||
npc = NPCS_DATA.get(target)
|
||||
if npc:
|
||||
new_obj['target_name'] = npc.get('name')
|
||||
else:
|
||||
logger.warning(f"NPC not found for target: {target}")
|
||||
elif obj.get('type') == 'item_delivery':
|
||||
item = ITEMS_MANAGER.get_item(target)
|
||||
if item:
|
||||
new_obj['target_name'] = item.name
|
||||
else:
|
||||
logger.warning(f"Item not found for target: {target}")
|
||||
enriched_objs.append(new_obj)
|
||||
q_data['objectives'] = enriched_objs
|
||||
|
||||
# ENRICH REWARDS WITH NAMES
|
||||
# For history, rewards might be stored in 'rewards' json.
|
||||
if 'rewards' in q_data and 'items' in q_data['rewards']:
|
||||
enriched_items = {}
|
||||
for item_id, qty in q_data['rewards']['items'].items():
|
||||
item = ITEMS_MANAGER.get_item(item_id)
|
||||
name = item.name if item else item_id
|
||||
enriched_items[item_id] = {'qty': qty, 'name': name}
|
||||
|
||||
q_data['reward_items_details'] = enriched_items
|
||||
|
||||
final_data.append(q_data)
|
||||
|
||||
history['data'] = final_data
|
||||
return history
|
||||
|
||||
|
||||
@router.get("/active")
|
||||
async def get_active_quests(current_user: dict = Depends(get_current_user)):
|
||||
@@ -34,18 +127,8 @@ async def get_active_quests(current_user: dict = Depends(get_current_user)):
|
||||
character_id = current_user['id']
|
||||
quests = await db.get_character_quests(character_id)
|
||||
|
||||
# Filter for active or completed but not yet turned in?
|
||||
# Usually "active" means in progress.
|
||||
# We want to return detailed info merged with static data
|
||||
|
||||
result = []
|
||||
for q in quests:
|
||||
# If it's a repeatable quest that is on cooldown, maybe don't show it as active?
|
||||
# But we want to show history?
|
||||
# Let's filter by status="active" or "completed" (ready to turn in?)
|
||||
# Wait, if status is "completed", it means it's done.
|
||||
# For repeatable quests, "completed" means it's in cooldown.
|
||||
|
||||
quest_def = QUESTS_DATA.get(q['quest_id'])
|
||||
if not quest_def:
|
||||
continue
|
||||
@@ -63,6 +146,58 @@ async def get_active_quests(current_user: dict = Depends(get_current_user)):
|
||||
else:
|
||||
q_data['on_cooldown'] = False
|
||||
|
||||
# Global Quest Progress
|
||||
if quest_def.get('type') == 'global':
|
||||
g_quest = await db.get_global_quest(q['quest_id'])
|
||||
if g_quest:
|
||||
q_data['global_progress'] = g_quest.get('global_progress', {})
|
||||
q_data['global_is_completed'] = g_quest.get('is_completed', False)
|
||||
|
||||
# Enrich with giver info
|
||||
if quest_def.get('giver_id'):
|
||||
giver = NPCS_DATA.get(quest_def['giver_id'])
|
||||
if giver:
|
||||
q_data['giver_name'] = giver.get('name')
|
||||
q_data['giver_image'] = giver.get('image')
|
||||
|
||||
# Get Location Name
|
||||
if giver.get('location_id'):
|
||||
loc = LOCATIONS_DATA.get(giver['location_id'])
|
||||
if loc:
|
||||
q_data['giver_location_name'] = loc.name
|
||||
else:
|
||||
q_data['giver_location_name'] = giver['location_id']
|
||||
|
||||
# ENRICH OBJECTIVES WITH NAMES
|
||||
if 'objectives' in q_data:
|
||||
enriched_objs = []
|
||||
for obj in q_data['objectives']:
|
||||
new_obj = dict(obj)
|
||||
target = obj.get('target')
|
||||
if obj.get('type') == 'kill_count':
|
||||
npc = NPCS_DATA.get(target)
|
||||
if npc:
|
||||
new_obj['target_name'] = npc.get('name')
|
||||
elif obj.get('type') == 'item_delivery':
|
||||
item = ITEMS_MANAGER.get_item(target)
|
||||
if item:
|
||||
new_obj['target_name'] = item.name
|
||||
enriched_objs.append(new_obj)
|
||||
q_data['objectives'] = enriched_objs
|
||||
|
||||
# ENRICH REWARDS WITH NAMES
|
||||
if 'rewards' in q_data and 'items' in q_data['rewards']:
|
||||
enriched_items = {}
|
||||
for item_id, qty in q_data['rewards']['items'].items():
|
||||
item = ITEMS_MANAGER.get_item(item_id)
|
||||
name = item.name if item else item_id
|
||||
enriched_items[item_id] = {'qty': qty, 'name': name}
|
||||
|
||||
# Store back in a way frontend can use, or just replace items dict?
|
||||
# Frontend currently iterates entries of items.
|
||||
# Let's add a new field 'reward_items_details'
|
||||
q_data['reward_items_details'] = enriched_items
|
||||
|
||||
result.append(q_data)
|
||||
|
||||
return result
|
||||
@@ -194,7 +329,19 @@ async def hand_in_quest(quest_id: str, current_user: dict = Depends(get_current_
|
||||
|
||||
if inv_item:
|
||||
available = inv_item['quantity']
|
||||
needed = required_count - current_count
|
||||
needed = required_count - current_count # Personal needed (to match max count)
|
||||
|
||||
# GLOBAL CAP CHECK
|
||||
is_global = quest_def.get('type') == 'global'
|
||||
if is_global:
|
||||
global_quest = await db.get_global_quest(quest_id)
|
||||
global_prog = global_quest.get('global_progress', {}) if global_quest else {}
|
||||
global_current_val = global_prog.get(target, 0)
|
||||
global_remaining = max(0, required_count - global_current_val)
|
||||
|
||||
# Cap needed by global remaining
|
||||
needed = min(needed, global_remaining)
|
||||
|
||||
to_take = min(available, needed)
|
||||
|
||||
if to_take > 0:
|
||||
@@ -207,13 +354,50 @@ async def hand_in_quest(quest_id: str, current_user: dict = Depends(get_current_
|
||||
items_deducted.append(f"{target} x{to_take}")
|
||||
|
||||
# Global Quest Logic
|
||||
if quest_def.get('type') == 'global':
|
||||
# Update global counters
|
||||
global_quest = await db.get_global_quest(quest_id)
|
||||
global_prog = global_quest['global_progress'] if global_quest else {}
|
||||
global_current = global_prog.get(target, 0)
|
||||
global_prog[target] = global_current + to_take
|
||||
if is_global:
|
||||
# Re-fetch or use existing? We need to be careful with race conditions slightly,
|
||||
# but safe enough for now to just update.
|
||||
# We already fetched 'global_prog' above.
|
||||
|
||||
# Add contribution
|
||||
new_global = global_current_val + to_take
|
||||
global_prog[target] = new_global
|
||||
await db.update_global_quest(quest_id, global_prog)
|
||||
|
||||
# Check for global completion
|
||||
is_global_complete = True
|
||||
for obj in objectives:
|
||||
t = obj['target']
|
||||
req = obj['count']
|
||||
# Check cached updated prog
|
||||
if global_prog.get(t, 0) < req:
|
||||
is_global_complete = False
|
||||
break
|
||||
|
||||
if is_global_complete:
|
||||
# Finish global quest!
|
||||
await finish_global_quest(quest_id, quest_def)
|
||||
|
||||
# RETURN IMMEDIATELY to prevent double rewards/deletion logic
|
||||
# We construct a success response here.
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Global Quest Completed!",
|
||||
"is_completed": True,
|
||||
"items_deducted": items_deducted,
|
||||
"rewards": ["See Global Rewards"], # Placeholders, real rewards via finish_global_quest/websocket
|
||||
"completion_text": quest_def.get("completion_text", "Global Quest Finished!"),
|
||||
"quest_update": {
|
||||
**quest_def,
|
||||
"quest_id": quest_id,
|
||||
"status": "completed",
|
||||
"progress": updated_progress,
|
||||
"on_cooldown": quest_def.get('repeatable'),
|
||||
}
|
||||
}
|
||||
else:
|
||||
# Prevent individual completion if global is not done
|
||||
all_completed = False
|
||||
|
||||
if new_count < required_count:
|
||||
all_completed = False
|
||||
@@ -227,23 +411,38 @@ async def hand_in_quest(quest_id: str, current_user: dict = Depends(get_current_
|
||||
if current_count < required_count:
|
||||
all_completed = False
|
||||
|
||||
# Save progress
|
||||
status = "active"
|
||||
if all_completed:
|
||||
status = "completed"
|
||||
|
||||
await db.update_quest_progress(character_id, quest_id, updated_progress, status)
|
||||
|
||||
# If completed, giving rewards
|
||||
# WEIGHT CHECK FOR REWARDS
|
||||
rewards_msg = []
|
||||
if all_completed:
|
||||
rewards = quest_def.get('rewards', {})
|
||||
reward_weight = 0.0
|
||||
|
||||
if 'items' in rewards:
|
||||
for item_id, qty in rewards['items'].items():
|
||||
item_def = ITEMS_MANAGER.get_item(item_id)
|
||||
if item_def:
|
||||
reward_weight += item_def.weight * qty
|
||||
|
||||
# Calculate current weight
|
||||
# Calculate current weight and capacity
|
||||
from ..services.helpers import calculate_player_capacity
|
||||
inventory = await db.get_inventory(character_id)
|
||||
current_weight, capacity, _, _ = await calculate_player_capacity(inventory, ITEMS_MANAGER)
|
||||
|
||||
if current_weight + reward_weight > capacity:
|
||||
# Rollback? The items for delivery were already removed above!
|
||||
# Ideally we should check weight BEFORE deducting delivery items.
|
||||
# converting this to a "check before action" logic is hard because delivery logic is stateful.
|
||||
# However, delivery items REDUCE weight. So we are likely safe unless rewards are heavier than delivered items.
|
||||
# BUT, if we error here, we technically leave the quest in "partially delivered" state, which is fine.
|
||||
# The user can just clear inventory and try again.
|
||||
|
||||
raise HTTPException(status_code=400, detail=f"Not enough inventory space for rewards! (Overweight by {current_weight + reward_weight - capacity:.1f})")
|
||||
|
||||
# Give Rewards
|
||||
# XP
|
||||
if 'xp' in rewards:
|
||||
xp_gained = rewards['xp']
|
||||
# We use current_user['xp'] but optimally we should fetch fresh player data if we want to be safe
|
||||
# For simplicity and performance, assuming current_user is fresh enough (it's from dependency)
|
||||
new_xp = current_user['xp'] + xp_gained
|
||||
await db.update_player(character_id, xp=new_xp)
|
||||
rewards_msg.append(f"{xp_gained} XP")
|
||||
@@ -262,7 +461,10 @@ async def hand_in_quest(quest_id: str, current_user: dict = Depends(get_current_
|
||||
if 'items' in rewards:
|
||||
for item_id, qty in rewards['items'].items():
|
||||
await db.add_item_to_inventory(character_id, item_id, qty)
|
||||
rewards_msg.append(f"{item_id} x{qty}") # Should assume name resolution on frontend or here
|
||||
# Resolve name
|
||||
idev = ITEMS_MANAGER.get_item(item_id)
|
||||
name = idev.name if idev else item_id
|
||||
rewards_msg.append(f"{name} x{qty}")
|
||||
|
||||
# Set cooldown if repeatable
|
||||
if quest_def.get('repeatable'):
|
||||
@@ -270,6 +472,44 @@ async def hand_in_quest(quest_id: str, current_user: dict = Depends(get_current_
|
||||
expires = time.time() + (cooldown_hours * 3600)
|
||||
await db.set_quest_cooldown(character_id, quest_id, expires)
|
||||
|
||||
# LOG HISTORY
|
||||
await db.log_quest_completion(
|
||||
character_id=character_id,
|
||||
quest_id=quest_id,
|
||||
started_at=quest_record.get('started_at') or time.time(),
|
||||
rewards=quest_def.get('rewards', {})
|
||||
)
|
||||
|
||||
# REMOVE FROM ACTIVE QUESTS (DELETE)
|
||||
await db.delete_character_quest(character_id, quest_id)
|
||||
|
||||
status = "completed"
|
||||
|
||||
else:
|
||||
# Not completed, just update progress
|
||||
status = "active"
|
||||
await db.update_quest_progress(character_id, quest_id, updated_progress, status)
|
||||
|
||||
# ENRICH OBJECTIVES FOR RESPONSE
|
||||
enriched_objs = []
|
||||
for obj in objectives:
|
||||
new_obj = dict(obj)
|
||||
target = obj.get('target')
|
||||
|
||||
# Add current progress
|
||||
new_obj['current'] = updated_progress.get(target, 0)
|
||||
|
||||
# Add names
|
||||
if obj.get('type') == 'kill_count':
|
||||
npc = NPCS_DATA.get(target)
|
||||
if npc:
|
||||
new_obj['target_name'] = npc.get('name')
|
||||
elif obj.get('type') == 'item_delivery':
|
||||
item = ITEMS_MANAGER.get_item(target)
|
||||
if item:
|
||||
new_obj['target_name'] = item.name
|
||||
enriched_objs.append(new_obj)
|
||||
|
||||
response = {
|
||||
"success": True,
|
||||
"progress": updated_progress,
|
||||
@@ -281,6 +521,7 @@ async def hand_in_quest(quest_id: str, current_user: dict = Depends(get_current_
|
||||
"quest_id": quest_id,
|
||||
"status": status,
|
||||
"progress": updated_progress,
|
||||
"objectives": enriched_objs,
|
||||
"on_cooldown": all_completed and quest_def.get('repeatable'),
|
||||
# other fields as needed
|
||||
}
|
||||
@@ -300,3 +541,78 @@ async def get_global_quest_progress(quest_id: str):
|
||||
if not quest:
|
||||
return {"progress": {}}
|
||||
return quest
|
||||
|
||||
|
||||
async def finish_global_quest(quest_id: str, quest_def: Dict):
|
||||
"""
|
||||
Handle global quest completion:
|
||||
1. Mark global quest as completed
|
||||
2. Unlock content (In-Memory)
|
||||
3. Distribute rewards to all participants
|
||||
4. Broadcast completion
|
||||
"""
|
||||
logger.info(f"🌍 Finishing Global Quest: {quest_id}")
|
||||
|
||||
# 1. Mark as completed in DB
|
||||
await db.mark_global_quest_completed(quest_id)
|
||||
|
||||
# 2. Unlock content (In-Memory)
|
||||
unlocks = []
|
||||
|
||||
# Unlock Locations
|
||||
for loc in LOCATIONS_DATA.values():
|
||||
if loc.unlocked_by == quest_id:
|
||||
loc.locked = False
|
||||
unlocks.append({"type": "location", "name": get_locale_string(loc.name, 'en'), "id": loc.id})
|
||||
|
||||
# Unlock interactables
|
||||
for inter in loc.interactables:
|
||||
if inter.unlocked_by == quest_id:
|
||||
inter.locked = False
|
||||
unlocks.append({"type": "interactable", "name": get_locale_string(inter.name, 'en'), "location": loc.id})
|
||||
|
||||
# 3. Distribute Rewards to participants
|
||||
participants = await db.get_all_quest_participants(quest_id)
|
||||
total_xp_pool = quest_def.get('rewards', {}).get('xp', 0)
|
||||
|
||||
total_required = 0
|
||||
for obj in quest_def.get('objectives', []):
|
||||
total_required += obj.get('count', 0)
|
||||
|
||||
for p in participants:
|
||||
# Calculate user contribution
|
||||
user_progress = p.get('progress', {})
|
||||
user_contribution = 0
|
||||
for obj in quest_def.get('objectives', []):
|
||||
target = obj['target']
|
||||
user_contribution += user_progress.get(target, 0)
|
||||
|
||||
if user_contribution > 0 and total_required > 0:
|
||||
percentage = user_contribution / total_required
|
||||
xp_reward = int(total_xp_pool * percentage)
|
||||
|
||||
if xp_reward > 0:
|
||||
# Give XP
|
||||
char = await db.get_player_by_id(p['character_id'])
|
||||
if char:
|
||||
new_xp = char['xp'] + xp_reward
|
||||
await db.update_player(p['character_id'], xp=new_xp)
|
||||
|
||||
# Mark as completed (delete from active) and log history
|
||||
await db.delete_character_quest(p['character_id'], quest_id)
|
||||
await db.log_quest_completion(
|
||||
character_id=p['character_id'],
|
||||
quest_id=quest_id,
|
||||
started_at=p['started_at'],
|
||||
rewards={"xp": xp_reward, "note": f"Contribution: {percentage*100:.1f}%"}
|
||||
)
|
||||
|
||||
# 4. Broadcast
|
||||
await manager.broadcast({
|
||||
"type": "global_quest_completed",
|
||||
"quest_id": quest_id,
|
||||
"title": get_locale_string(quest_def.get('title', 'Global Quest'), 'en'),
|
||||
"outcome": {
|
||||
"unlocks": unlocks
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user