feat: Implement Phase 5 Quest System (100% complete)

Add YAML-driven quest system with context-aware offering:

Core Implementation:
- Quest data models (Quest, QuestObjective, QuestReward, QuestTriggers)
- QuestService for YAML loading and caching
- QuestEligibilityService with level, location, and probability filtering
- LoreService stub (MockLoreService) ready for Phase 6 Weaviate integration

Quest Content:
- 5 example quests across difficulty tiers (2 easy, 2 medium, 1 hard)
- Quest-centric design: quests define their NPC givers
- Location-based probability weights for natural quest offering

AI Integration:
- Quest offering section in npc_dialogue.j2 template
- Response parser extracts [QUEST_OFFER:quest_id] markers
- AI naturally weaves quest offers into NPC conversations

API Endpoints:
- POST /api/v1/quests/accept - Accept quest offer
- POST /api/v1/quests/decline - Decline quest offer
- POST /api/v1/quests/progress - Update objective progress
- POST /api/v1/quests/complete - Complete quest, claim rewards
- POST /api/v1/quests/abandon - Abandon active quest
- GET /api/v1/characters/{id}/quests - List character quests
- GET /api/v1/quests/{quest_id} - Get quest details

Frontend:
- Quest tracker sidebar with HTMX integration
- Quest offer modal for accept/decline flow
- Quest detail modal for viewing progress
- Combat service integration for kill objective tracking

Testing:
- Unit tests for Quest models and serialization
- Integration tests for full quest lifecycle
- Comprehensive test coverage for eligibility service

Documentation:
- Reorganized docs into /docs/phases/ structure
- Added Phase 5-12 planning documents
- Updated ROADMAP.md with new structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-29 15:42:55 -06:00
parent e7e329e6ed
commit df26abd207
42 changed files with 8421 additions and 2227 deletions

View File

@@ -4,8 +4,11 @@ Response parser for AI narrative responses.
This module handles AI response parsing. Game state changes (items, gold, XP)
are now handled exclusively through predetermined dice check outcomes in
action templates, not through AI-generated JSON.
Quest offers are extracted from NPC dialogue using [QUEST_OFFER:quest_id] markers.
"""
import re
from dataclasses import dataclass, field
from typing import Any, Optional
@@ -158,3 +161,83 @@ def _parse_game_actions(data: dict[str, Any]) -> GameStateChanges:
changes.location_change = data.get("location_change")
return changes
# ============================================================================
# NPC Dialogue Parsing
# ============================================================================
@dataclass
class ParsedNPCDialogue:
"""
Parsed NPC dialogue response with quest offer extraction.
When NPCs offer quests during conversation, they include a
[QUEST_OFFER:quest_id] marker that signals the UI to show
a quest accept/decline modal.
Attributes:
dialogue: The cleaned dialogue text (marker removed)
quest_offered: Quest ID if a quest was offered, None otherwise
raw_response: The original response text
"""
dialogue: str
quest_offered: Optional[str] = None
raw_response: str = ""
# Regex pattern for quest offer markers
# Matches: [QUEST_OFFER:quest_id] or [QUEST_OFFER: quest_id]
QUEST_OFFER_PATTERN = re.compile(r'\[QUEST_OFFER:\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\]')
def parse_npc_dialogue(response_text: str) -> ParsedNPCDialogue:
"""
Parse an NPC dialogue response, extracting quest offer markers.
The AI is instructed to include [QUEST_OFFER:quest_id] on its own line
when offering a quest. This function extracts the marker and returns
the cleaned dialogue.
Args:
response_text: The raw AI dialogue response
Returns:
ParsedNPCDialogue with cleaned dialogue and optional quest_offered
Example:
>>> response = '''*leans in* Got a problem, friend.
... [QUEST_OFFER:quest_cellar_rats]
... Giant rats in me cellar.'''
>>> result = parse_npc_dialogue(response)
>>> result.quest_offered
'quest_cellar_rats'
>>> '[QUEST_OFFER' in result.dialogue
False
"""
logger.debug("Parsing NPC dialogue", response_length=len(response_text))
quest_offered = None
dialogue = response_text.strip()
# Search for quest offer marker
match = QUEST_OFFER_PATTERN.search(dialogue)
if match:
quest_offered = match.group(1)
# Remove the marker from the dialogue
dialogue = QUEST_OFFER_PATTERN.sub('', dialogue)
# Clean up any extra whitespace/newlines left behind
dialogue = re.sub(r'\n\s*\n', '\n\n', dialogue)
dialogue = dialogue.strip()
logger.info(
"Quest offer extracted from dialogue",
quest_id=quest_offered,
)
return ParsedNPCDialogue(
dialogue=dialogue,
quest_offered=quest_offered,
raw_response=response_text,
)