Added trading and quests, checkpoint push
This commit is contained in:
302
api/routers/quests.py
Normal file
302
api/routers/quests.py
Normal file
@@ -0,0 +1,302 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||
from typing import Dict, List, Any, Optional
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from ..core.security import get_current_user
|
||||
from .. import database as db
|
||||
from .. import game_logic
|
||||
from ..items import ItemsManager
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/quests",
|
||||
tags=["quests"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Dependencies
|
||||
QUESTS_DATA = {}
|
||||
NPCS_DATA = {}
|
||||
|
||||
def init_router_dependencies(items_manager: ItemsManager, quests_data=None, npcs_data=None):
|
||||
global ITEMS_MANAGER, QUESTS_DATA, NPCS_DATA
|
||||
ITEMS_MANAGER = items_manager
|
||||
if quests_data:
|
||||
QUESTS_DATA = quests_data
|
||||
if npcs_data:
|
||||
NPCS_DATA = npcs_data
|
||||
|
||||
@router.get("/active")
|
||||
async def get_active_quests(current_user: dict = Depends(get_current_user)):
|
||||
"""Get all active quests for the character"""
|
||||
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
|
||||
|
||||
# Enrich with static data
|
||||
q_data = dict(q)
|
||||
q_data['start_at'] = q['started_at'] # Consistency
|
||||
q_data.update(quest_def)
|
||||
|
||||
# Calculate cooldown status for repeatable quests
|
||||
if quest_def.get('repeatable') and q['cooldown_expires_at']:
|
||||
if time.time() < q['cooldown_expires_at']:
|
||||
q_data['on_cooldown'] = True
|
||||
q_data['cooldown_remaining'] = int(q['cooldown_expires_at'] - time.time())
|
||||
else:
|
||||
q_data['on_cooldown'] = False
|
||||
|
||||
result.append(q_data)
|
||||
|
||||
return result
|
||||
|
||||
@router.get("/available")
|
||||
async def get_available_quests(current_user: dict = Depends(get_current_user)):
|
||||
"""Get quests available to be started at current location"""
|
||||
character_id = current_user['id']
|
||||
location_id = current_user['location_id']
|
||||
|
||||
# 1. Identify NPCs at this location
|
||||
local_npcs = [
|
||||
npc_id for npc_id, npc in NPCS_DATA.items()
|
||||
if npc.get('location_id') == location_id
|
||||
]
|
||||
|
||||
if not local_npcs:
|
||||
return []
|
||||
|
||||
# 2. Get quests offered by these NPCs
|
||||
potential_quests = []
|
||||
for q_id, q_def in QUESTS_DATA.items():
|
||||
if q_def.get('giver_id') in local_npcs:
|
||||
potential_quests.append(q_def)
|
||||
|
||||
# 3. Filter out active/completed non-repeatable quests
|
||||
# We need to check DB state
|
||||
available = []
|
||||
|
||||
# Bulk fetch might be better but loop is fine for now
|
||||
for q_def in potential_quests:
|
||||
q_id = q_def['quest_id']
|
||||
existing = await db.get_character_quest(character_id, q_id)
|
||||
|
||||
if not existing:
|
||||
# Never started -> Available
|
||||
available.append(q_def)
|
||||
else:
|
||||
# Exists
|
||||
if existing['status'] == 'active':
|
||||
continue # Already active
|
||||
|
||||
if existing['status'] == 'completed':
|
||||
if q_def.get('repeatable'):
|
||||
# Check cooldown
|
||||
expires = existing.get('cooldown_expires_at')
|
||||
if not expires or time.time() >= expires:
|
||||
available.append(q_def)
|
||||
else:
|
||||
continue # Completed and not repeatable
|
||||
|
||||
if existing['status'] == 'failed':
|
||||
available.append(q_def) # Can retry?
|
||||
|
||||
return available
|
||||
|
||||
@router.post("/accept/{quest_id}")
|
||||
async def accept_quest(quest_id: str, current_user: dict = Depends(get_current_user)):
|
||||
"""Accept a quest"""
|
||||
character_id = current_user['id']
|
||||
quest_def = QUESTS_DATA.get(quest_id)
|
||||
|
||||
if not quest_def:
|
||||
raise HTTPException(status_code=404, detail="Quest not found")
|
||||
|
||||
# Check if repeatable & cooldown
|
||||
existing = await db.get_character_quest(character_id, quest_id)
|
||||
if existing:
|
||||
if not quest_def.get('repeatable'):
|
||||
raise HTTPException(status_code=400, detail="Quest already completed or active")
|
||||
|
||||
# Check cooldown
|
||||
if existing.get('cooldown_expires_at') and time.time() < existing['cooldown_expires_at']:
|
||||
remaining = int(existing['cooldown_expires_at'] - time.time())
|
||||
raise HTTPException(status_code=400, detail=f"Quest on cooldown for {remaining}s")
|
||||
|
||||
if existing['status'] == 'active':
|
||||
raise HTTPException(status_code=400, detail="Quest already active")
|
||||
|
||||
# Accept quest
|
||||
await db.accept_quest(character_id, quest_id)
|
||||
|
||||
# Return updated quest data for frontend
|
||||
updated_q_data = dict(quest_def)
|
||||
updated_q_data['status'] = 'active'
|
||||
updated_q_data['start_at'] = int(time.time())
|
||||
updated_q_data['progress'] = {} # New quest
|
||||
|
||||
return {"success": True, "message": "Quest accepted", "quest": updated_q_data}
|
||||
|
||||
@router.post("/hand_in/{quest_id}")
|
||||
async def hand_in_quest(quest_id: str, current_user: dict = Depends(get_current_user)):
|
||||
"""
|
||||
Hand in items or check completion for a quest.
|
||||
Automatically deducts items from inventory for delivery objectives.
|
||||
"""
|
||||
character_id = current_user['id']
|
||||
quest_def = QUESTS_DATA.get(quest_id)
|
||||
|
||||
if not quest_def:
|
||||
raise HTTPException(status_code=404, detail="Quest not found")
|
||||
|
||||
quest_record = await db.get_character_quest(character_id, quest_id)
|
||||
if not quest_record or quest_record['status'] != 'active':
|
||||
raise HTTPException(status_code=400, detail="Quest not active")
|
||||
|
||||
current_progress = quest_record.get('progress') or {}
|
||||
objectives = quest_def.get('objectives', [])
|
||||
|
||||
updated_progress = current_progress.copy()
|
||||
items_deducted = []
|
||||
all_completed = True
|
||||
|
||||
# Iterate objectives
|
||||
for obj in objectives:
|
||||
obj_type = obj['type']
|
||||
target = obj['target']
|
||||
required_count = obj['count']
|
||||
|
||||
current_count = current_progress.get(target, 0)
|
||||
|
||||
if current_count >= required_count:
|
||||
continue # Already done
|
||||
|
||||
if obj_type == 'item_delivery':
|
||||
# Check inventory
|
||||
inventory = await db.get_inventory(character_id)
|
||||
inv_item = next((i for i in inventory if i['item_id'] == target), None)
|
||||
|
||||
if inv_item:
|
||||
available = inv_item['quantity']
|
||||
needed = required_count - current_count
|
||||
to_take = min(available, needed)
|
||||
|
||||
if to_take > 0:
|
||||
# Remove from inventory
|
||||
await db.remove_item_from_inventory(character_id, target, to_take)
|
||||
|
||||
# Update progress
|
||||
new_count = current_count + to_take
|
||||
updated_progress[target] = new_count
|
||||
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
|
||||
await db.update_global_quest(quest_id, global_prog)
|
||||
|
||||
if new_count < required_count:
|
||||
all_completed = False
|
||||
else:
|
||||
all_completed = False
|
||||
else:
|
||||
all_completed = False
|
||||
|
||||
elif obj_type == 'kill_count':
|
||||
# Check if kill count is met (updated via other events usually)
|
||||
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
|
||||
rewards_msg = []
|
||||
if all_completed:
|
||||
rewards = quest_def.get('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")
|
||||
|
||||
# Check for level up
|
||||
try:
|
||||
level_up_result = await game_logic.check_and_apply_level_up(character_id)
|
||||
if level_up_result and level_up_result.get('leveled_up'):
|
||||
new_level = level_up_result['new_level']
|
||||
stats_gained = level_up_result['levels_gained']
|
||||
rewards_msg.append(f"Level Up! (Lvl {new_level}) +{stats_gained} Stat Points")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check level up in quest hand-in: {e}")
|
||||
|
||||
# Items
|
||||
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
|
||||
|
||||
# Set cooldown if repeatable
|
||||
if quest_def.get('repeatable'):
|
||||
cooldown_hours = quest_def.get('cooldown_hours', 24)
|
||||
expires = time.time() + (cooldown_hours * 3600)
|
||||
await db.set_quest_cooldown(character_id, quest_id, expires)
|
||||
|
||||
response = {
|
||||
"success": True,
|
||||
"progress": updated_progress,
|
||||
"is_completed": all_completed,
|
||||
"items_deducted": items_deducted,
|
||||
"message": "Progress updated",
|
||||
"quest_update": {
|
||||
**quest_def,
|
||||
"quest_id": quest_id,
|
||||
"status": status,
|
||||
"progress": updated_progress,
|
||||
"on_cooldown": all_completed and quest_def.get('repeatable'),
|
||||
# other fields as needed
|
||||
}
|
||||
}
|
||||
|
||||
if all_completed:
|
||||
response["message"] = "Quest Completed!"
|
||||
response["rewards"] = rewards_msg
|
||||
response["completion_text"] = quest_def.get("completion_text", {})
|
||||
|
||||
return response
|
||||
|
||||
# Also exposing global quest state
|
||||
@router.get("/global/{quest_id}")
|
||||
async def get_global_quest_progress(quest_id: str):
|
||||
quest = await db.get_global_quest(quest_id)
|
||||
if not quest:
|
||||
return {"progress": {}}
|
||||
return quest
|
||||
Reference in New Issue
Block a user