Files
Code_of_Conquest/api/app/api/npcs.py

431 lines
14 KiB
Python

"""
NPC API Blueprint
This module provides API endpoints for NPC interactions:
- Get NPC details
- Talk to NPC (queues AI dialogue generation)
- Get NPCs at location
All endpoints require authentication and enforce ownership validation.
"""
from datetime import datetime, timezone
from flask import Blueprint, request
from app.services.session_service import get_session_service, SessionNotFound
from app.services.character_service import get_character_service, CharacterNotFound
from app.services.npc_loader import get_npc_loader
from app.services.location_loader import get_location_loader
from app.tasks.ai_tasks import enqueue_ai_task, TaskType
from app.utils.response import (
success_response,
accepted_response,
error_response,
not_found_response,
validation_error_response
)
from app.utils.auth import require_auth, get_current_user
from app.utils.logging import get_logger
# Initialize logger
logger = get_logger(__file__)
# Create blueprint
npcs_bp = Blueprint('npcs', __name__)
@npcs_bp.route('/api/v1/npcs/<npc_id>', methods=['GET'])
@require_auth
def get_npc_details(npc_id: str):
"""
Get NPC details with knowledge filtered by character interaction state.
Path params:
npc_id: NPC ID to get details for
Query params:
character_id: Optional character ID for filtering revealed secrets
Returns:
JSON response with NPC details
"""
try:
user = get_current_user()
character_id = request.args.get('character_id')
# Load NPC
npc_loader = get_npc_loader()
npc = npc_loader.load_npc(npc_id)
if not npc:
return not_found_response("NPC not found")
npc_data = npc.to_dict()
# Filter knowledge based on character interaction state
if character_id:
try:
character_service = get_character_service()
character = character_service.get_character(character_id, user.id)
if character:
# Get revealed secrets based on conditions
revealed = character_service.check_npc_secret_conditions(character, npc)
# Build available knowledge (public + revealed)
available_knowledge = []
if npc.knowledge:
available_knowledge.extend(npc.knowledge.public)
available_knowledge.extend(revealed)
npc_data["available_knowledge"] = available_knowledge
# Remove secret knowledge from response
if npc_data.get("knowledge"):
npc_data["knowledge"]["secret"] = []
npc_data["knowledge"]["will_share_if"] = []
# Add interaction summary
interaction = character.npc_interactions.get(npc_id, {})
npc_data["interaction_summary"] = {
"interaction_count": interaction.get("interaction_count", 0),
"relationship_level": interaction.get("relationship_level", 50),
"first_met": interaction.get("first_met"),
}
except CharacterNotFound:
logger.debug("Character not found for NPC filter", character_id=character_id)
return success_response(npc_data)
except Exception as e:
logger.error("Failed to get NPC details",
npc_id=npc_id,
error=str(e))
return error_response("Failed to get NPC", 500)
@npcs_bp.route('/api/v1/npcs/<npc_id>/talk', methods=['POST'])
@require_auth
def talk_to_npc(npc_id: str):
"""
Initiate conversation with an NPC.
Validates NPC is at current location, updates interaction state,
and queues AI dialogue generation task.
Path params:
npc_id: NPC ID to talk to
Request body:
session_id: Active session ID
topic: Conversation topic/opener (default: "greeting")
player_response: What the player says to the NPC (overrides topic if provided)
Returns:
JSON response with job_id for polling result
"""
try:
user = get_current_user()
data = request.get_json()
session_id = data.get('session_id')
# player_response overrides topic for bidirectional dialogue
player_response = data.get('player_response')
topic = player_response if player_response else data.get('topic', 'greeting')
if not session_id:
return validation_error_response("session_id is required")
# Get session
session_service = get_session_service()
session = session_service.get_session(session_id, user.id)
# Load NPC
npc_loader = get_npc_loader()
npc = npc_loader.load_npc(npc_id)
if not npc:
return not_found_response("NPC not found")
# Validate NPC is at current location
if npc.location_id != session.game_state.current_location:
logger.warning("NPC not at current location",
npc_id=npc_id,
npc_location=npc.location_id,
current_location=session.game_state.current_location)
return error_response("NPC is not at your current location", 400)
# Get character
character_service = get_character_service()
character = character_service.get_character(session.solo_character_id, user.id)
# Get or create interaction state
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
interaction = character.npc_interactions.get(npc_id, {})
if not interaction:
# First meeting
interaction = {
"npc_id": npc_id,
"first_met": now,
"last_interaction": now,
"interaction_count": 1,
"revealed_secrets": [],
"relationship_level": 50,
"custom_flags": {},
}
else:
# Update existing interaction
interaction["last_interaction"] = now
interaction["interaction_count"] = interaction.get("interaction_count", 0) + 1
# Check for newly revealed secrets
revealed = character_service.check_npc_secret_conditions(character, npc)
# Update character with new interaction state
character_service.update_npc_interaction(
character.character_id,
user.id,
npc_id,
interaction
)
# Build NPC knowledge for AI context
npc_knowledge = []
if npc.knowledge:
npc_knowledge.extend(npc.knowledge.public)
npc_knowledge.extend(revealed)
# Get previous dialogue history for context (last 3 exchanges)
previous_dialogue = character_service.get_npc_dialogue_history(
character.character_id,
user.id,
npc_id,
limit=3
)
# Prepare AI context
task_context = {
"session_id": session_id,
"character_id": character.character_id,
"character": character.to_story_dict(),
"npc": npc.to_story_dict(),
"npc_full": npc.to_dict(), # Full NPC data for reference
"conversation_topic": topic,
"game_state": session.game_state.to_dict(),
"npc_knowledge": npc_knowledge,
"revealed_secrets": revealed,
"interaction_count": interaction["interaction_count"],
"relationship_level": interaction.get("relationship_level", 50),
"previous_dialogue": previous_dialogue, # Pass conversation history
}
# Enqueue AI task
result = enqueue_ai_task(
task_type=TaskType.NPC_DIALOGUE,
user_id=user.id,
context=task_context,
priority="normal",
session_id=session_id,
character_id=character.character_id
)
logger.info("NPC dialogue task queued",
user_id=user.id,
npc_id=npc_id,
job_id=result.get('job_id'),
interaction_count=interaction["interaction_count"])
return accepted_response({
"job_id": result.get('job_id'),
"status": "queued",
"message": f"Starting conversation with {npc.name}...",
"npc_name": npc.name,
"npc_role": npc.role,
})
except SessionNotFound:
return not_found_response("Session not found")
except CharacterNotFound:
return not_found_response("Character not found")
except Exception as e:
logger.error("Failed to talk to NPC",
npc_id=npc_id,
error=str(e))
return error_response("Failed to start conversation", 500)
@npcs_bp.route('/api/v1/npcs/at-location/<location_id>', methods=['GET'])
@require_auth
def get_npcs_at_location(location_id: str):
"""
Get all NPCs at a specific location.
Path params:
location_id: Location ID to get NPCs for
Returns:
JSON response with list of NPCs at location
"""
try:
npc_loader = get_npc_loader()
npcs = npc_loader.get_npcs_at_location(location_id)
npcs_list = []
for npc in npcs:
npcs_list.append({
"npc_id": npc.npc_id,
"name": npc.name,
"role": npc.role,
"appearance": npc.appearance.brief,
"tags": npc.tags,
"image_url": npc.image_url,
})
return success_response({
"location_id": location_id,
"npcs": npcs_list,
})
except Exception as e:
logger.error("Failed to get NPCs at location",
location_id=location_id,
error=str(e))
return error_response("Failed to get NPCs", 500)
@npcs_bp.route('/api/v1/npcs/<npc_id>/relationship', methods=['POST'])
@require_auth
def adjust_npc_relationship(npc_id: str):
"""
Adjust relationship level with an NPC.
Path params:
npc_id: NPC ID
Request body:
character_id: Character ID
adjustment: Amount to add/subtract (can be negative)
Returns:
JSON response with updated relationship level
"""
try:
user = get_current_user()
data = request.get_json()
character_id = data.get('character_id')
adjustment = data.get('adjustment', 0)
if not character_id:
return validation_error_response("character_id is required")
if not isinstance(adjustment, int):
return validation_error_response("adjustment must be an integer")
# Validate NPC exists
npc_loader = get_npc_loader()
npc = npc_loader.load_npc(npc_id)
if not npc:
return not_found_response("NPC not found")
# Adjust relationship
character_service = get_character_service()
character = character_service.adjust_npc_relationship(
character_id,
user.id,
npc_id,
adjustment
)
new_level = character.npc_interactions.get(npc_id, {}).get("relationship_level", 50)
logger.info("NPC relationship adjusted",
npc_id=npc_id,
character_id=character_id,
adjustment=adjustment,
new_level=new_level)
return success_response({
"npc_id": npc_id,
"relationship_level": new_level,
})
except CharacterNotFound:
return not_found_response("Character not found")
except Exception as e:
logger.error("Failed to adjust NPC relationship",
npc_id=npc_id,
error=str(e))
return error_response("Failed to adjust relationship", 500)
@npcs_bp.route('/api/v1/npcs/<npc_id>/flag', methods=['POST'])
@require_auth
def set_npc_flag(npc_id: str):
"""
Set a custom flag on NPC interaction (e.g., "helped_with_rats": true).
Path params:
npc_id: NPC ID
Request body:
character_id: Character ID
flag_name: Name of the flag
flag_value: Value to set
Returns:
JSON response confirming flag was set
"""
try:
user = get_current_user()
data = request.get_json()
character_id = data.get('character_id')
flag_name = data.get('flag_name')
flag_value = data.get('flag_value')
if not character_id:
return validation_error_response("character_id is required")
if not flag_name:
return validation_error_response("flag_name is required")
# Validate NPC exists
npc_loader = get_npc_loader()
npc = npc_loader.load_npc(npc_id)
if not npc:
return not_found_response("NPC not found")
# Set flag
character_service = get_character_service()
character_service.set_npc_custom_flag(
character_id,
user.id,
npc_id,
flag_name,
flag_value
)
logger.info("NPC flag set",
npc_id=npc_id,
character_id=character_id,
flag_name=flag_name)
return success_response({
"npc_id": npc_id,
"flag_name": flag_name,
"flag_value": flag_value,
})
except CharacterNotFound:
return not_found_response("Character not found")
except Exception as e:
logger.error("Failed to set NPC flag",
npc_id=npc_id,
error=str(e))
return error_response("Failed to set flag", 500)