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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user