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

@@ -34,6 +34,8 @@ from app.services.combat_repository import (
get_combat_repository,
CombatRepository
)
from app.services.quest_service import get_quest_service
from app.models.quest import ObjectiveType
from app.utils.logging import get_logger
logger = get_logger(__file__)
@@ -1290,6 +1292,9 @@ class CombatService:
items=len(rewards.items),
level_ups=rewards.level_ups)
# Update quest progress for kill objectives
self._update_quest_kill_progress(encounter, session, user_id)
return rewards
def _build_loot_context(self, encounter: CombatEncounter) -> LootContext:
@@ -1339,6 +1344,114 @@ class CombatService:
# Helper Methods
# =========================================================================
def _update_quest_kill_progress(
self,
encounter: CombatEncounter,
session,
user_id: str
) -> None:
"""
Update quest progress for kill objectives based on defeated enemies.
Scans all defeated enemies in the encounter, identifies their types,
and updates any active quests that have kill objectives matching
those enemy types.
Args:
encounter: Completed combat encounter
session: Game session
user_id: User ID for character updates
"""
# Collect killed enemy types and counts
killed_enemy_types: Dict[str, int] = {}
for combatant in encounter.combatants:
if not combatant.is_player and combatant.is_dead():
enemy_id = combatant.combatant_id.split("_")[0]
enemy = self.enemy_loader.load_enemy(enemy_id)
if enemy:
# Use enemy_id as the type identifier (matches target_enemy_type in quests)
killed_enemy_types[enemy_id] = killed_enemy_types.get(enemy_id, 0) + 1
if not killed_enemy_types:
return # No enemies killed
# Get character
if session.is_solo():
char_id = session.solo_character_id
else:
# For multiplayer, we'd need to update all players
# For now, just handle solo
return
try:
character = self.character_service.get_character(char_id, user_id)
if not character:
return
# Get active quests
active_quests = getattr(character, 'active_quests', [])
if not active_quests:
return
quest_service = get_quest_service()
quest_states = getattr(character, 'quest_states', {})
updated = False
for quest_id in active_quests:
quest = quest_service.load_quest(quest_id)
if not quest:
continue
quest_state = quest_states.get(quest_id, {})
objectives_progress = quest_state.get('objectives_progress', {})
for objective in quest.objectives:
# Check if this is a kill objective with a matching enemy type
if objective.objective_type != ObjectiveType.KILL:
continue
target_enemy = objective.target_enemy_type
if not target_enemy or target_enemy not in killed_enemy_types:
continue
# Update progress for this objective
kill_count = killed_enemy_types[target_enemy]
current_progress = objectives_progress.get(objective.objective_id, 0)
new_progress = min(
current_progress + kill_count,
objective.required_progress
)
if new_progress > current_progress:
objectives_progress[objective.objective_id] = new_progress
updated = True
logger.info(
"Quest kill progress updated",
character_id=char_id,
quest_id=quest_id,
objective_id=objective.objective_id,
enemy_type=target_enemy,
kills=kill_count,
progress=f"{new_progress}/{objective.required_progress}"
)
# Update quest state
if quest_id in quest_states:
quest_states[quest_id]['objectives_progress'] = objectives_progress
# Save character if any quests were updated
if updated:
character.quest_states = quest_states
self.character_service.update_character(character, user_id)
except Exception as e:
logger.error(
"Failed to update quest kill progress",
char_id=char_id,
error=str(e)
)
def _create_combatant_from_character(
self,
character: Character