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:
@@ -66,6 +66,8 @@ class Character:
|
||||
|
||||
# Quests and exploration
|
||||
active_quests: List[str] = field(default_factory=list)
|
||||
completed_quests: List[str] = field(default_factory=list)
|
||||
quest_states: Dict[str, Dict] = field(default_factory=dict) # quest_id -> CharacterQuestState.to_dict()
|
||||
discovered_locations: List[str] = field(default_factory=list)
|
||||
current_location: Optional[str] = None # Set to origin starting location on creation
|
||||
|
||||
@@ -378,6 +380,8 @@ class Character:
|
||||
"equipped": {slot: item.to_dict() for slot, item in self.equipped.items()},
|
||||
"gold": self.gold,
|
||||
"active_quests": self.active_quests,
|
||||
"completed_quests": self.completed_quests,
|
||||
"quest_states": self.quest_states,
|
||||
"discovered_locations": self.discovered_locations,
|
||||
"current_location": self.current_location,
|
||||
"npc_interactions": self.npc_interactions,
|
||||
@@ -467,6 +471,8 @@ class Character:
|
||||
equipped=equipped,
|
||||
gold=data.get("gold", 0),
|
||||
active_quests=data.get("active_quests", []),
|
||||
completed_quests=data.get("completed_quests", []),
|
||||
quest_states=data.get("quest_states", {}),
|
||||
discovered_locations=data.get("discovered_locations", []),
|
||||
current_location=data.get("current_location"),
|
||||
npc_interactions=data.get("npc_interactions", {}),
|
||||
|
||||
649
api/app/models/quest.py
Normal file
649
api/app/models/quest.py
Normal file
@@ -0,0 +1,649 @@
|
||||
"""
|
||||
Quest data models for the quest system.
|
||||
|
||||
This module defines Quest and related dataclasses for the YAML-driven quest
|
||||
system. Quests are loaded from YAML files and define their own NPC givers,
|
||||
objectives, rewards, and offering conditions.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Any, Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ObjectiveType(Enum):
|
||||
"""Types of quest objectives."""
|
||||
KILL = "kill"
|
||||
COLLECT = "collect"
|
||||
TRAVEL = "travel"
|
||||
INTERACT = "interact"
|
||||
DISCOVER = "discover"
|
||||
|
||||
|
||||
class QuestDifficulty(Enum):
|
||||
"""Quest difficulty levels."""
|
||||
EASY = "easy"
|
||||
MEDIUM = "medium"
|
||||
HARD = "hard"
|
||||
EPIC = "epic"
|
||||
|
||||
|
||||
class QuestStatus(Enum):
|
||||
"""Status of a quest for a character."""
|
||||
AVAILABLE = "available"
|
||||
ACTIVE = "active"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuestObjective:
|
||||
"""
|
||||
A single objective within a quest.
|
||||
|
||||
Objectives track player progress toward quest completion. Each objective
|
||||
has a type (kill, collect, etc.) and a required progress amount.
|
||||
|
||||
Attributes:
|
||||
objective_id: Unique identifier within the quest
|
||||
description: Player-facing description (e.g., "Kill 10 giant rats")
|
||||
objective_type: Type of objective (kill, collect, travel, etc.)
|
||||
required_progress: Target amount to complete (e.g., 10 for 10 kills)
|
||||
current_progress: Current progress (tracked on character's quest state)
|
||||
target_enemy_type: For kill objectives - enemy type to kill
|
||||
target_item_id: For collect objectives - item to collect
|
||||
target_location_id: For travel/discover objectives - location to reach
|
||||
target_npc_id: For interact objectives - NPC to interact with
|
||||
"""
|
||||
|
||||
objective_id: str
|
||||
description: str
|
||||
objective_type: ObjectiveType
|
||||
required_progress: int = 1
|
||||
current_progress: int = 0
|
||||
target_enemy_type: Optional[str] = None
|
||||
target_item_id: Optional[str] = None
|
||||
target_location_id: Optional[str] = None
|
||||
target_npc_id: Optional[str] = None
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
"""Check if this objective is complete."""
|
||||
return self.current_progress >= self.required_progress
|
||||
|
||||
@property
|
||||
def progress_text(self) -> str:
|
||||
"""Get formatted progress text (e.g., '5/10')."""
|
||||
return f"{self.current_progress}/{self.required_progress}"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize objective to dictionary."""
|
||||
return {
|
||||
"objective_id": self.objective_id,
|
||||
"description": self.description,
|
||||
"objective_type": self.objective_type.value,
|
||||
"required_progress": self.required_progress,
|
||||
"current_progress": self.current_progress,
|
||||
"target_enemy_type": self.target_enemy_type,
|
||||
"target_item_id": self.target_item_id,
|
||||
"target_location_id": self.target_location_id,
|
||||
"target_npc_id": self.target_npc_id,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'QuestObjective':
|
||||
"""Deserialize objective from dictionary."""
|
||||
return cls(
|
||||
objective_id=data["objective_id"],
|
||||
description=data["description"],
|
||||
objective_type=ObjectiveType(data["objective_type"]),
|
||||
required_progress=data.get("required_progress", 1),
|
||||
current_progress=data.get("current_progress", 0),
|
||||
target_enemy_type=data.get("target_enemy_type"),
|
||||
target_item_id=data.get("target_item_id"),
|
||||
target_location_id=data.get("target_location_id"),
|
||||
target_npc_id=data.get("target_npc_id"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuestReward:
|
||||
"""
|
||||
Rewards granted upon quest completion.
|
||||
|
||||
Attributes:
|
||||
gold: Gold amount
|
||||
experience: XP amount
|
||||
items: List of item IDs to grant
|
||||
relationship_bonuses: NPC relationship increases {npc_id: amount}
|
||||
unlocks_quests: Quest IDs that become available after completion
|
||||
reveals_locations: Location IDs revealed upon completion
|
||||
"""
|
||||
|
||||
gold: int = 0
|
||||
experience: int = 0
|
||||
items: List[str] = field(default_factory=list)
|
||||
relationship_bonuses: Dict[str, int] = field(default_factory=dict)
|
||||
unlocks_quests: List[str] = field(default_factory=list)
|
||||
reveals_locations: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize rewards to dictionary."""
|
||||
return {
|
||||
"gold": self.gold,
|
||||
"experience": self.experience,
|
||||
"items": self.items,
|
||||
"relationship_bonuses": self.relationship_bonuses,
|
||||
"unlocks_quests": self.unlocks_quests,
|
||||
"reveals_locations": self.reveals_locations,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'QuestReward':
|
||||
"""Deserialize rewards from dictionary."""
|
||||
return cls(
|
||||
gold=data.get("gold", 0),
|
||||
experience=data.get("experience", 0),
|
||||
items=data.get("items", []),
|
||||
relationship_bonuses=data.get("relationship_bonuses", {}),
|
||||
unlocks_quests=data.get("unlocks_quests", []),
|
||||
reveals_locations=data.get("reveals_locations", []),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuestOfferConditions:
|
||||
"""
|
||||
Conditions required for an NPC to offer this quest.
|
||||
|
||||
These are checked per-NPC to determine if they can offer the quest
|
||||
to a specific character based on relationship and flags.
|
||||
|
||||
Attributes:
|
||||
min_relationship: Minimum relationship level required (0-100)
|
||||
required_flags: Custom flags that must be set on character's NPC interaction
|
||||
forbidden_flags: Custom flags that must NOT be set (e.g., "refused_this_quest")
|
||||
"""
|
||||
|
||||
min_relationship: int = 0
|
||||
required_flags: List[str] = field(default_factory=list)
|
||||
forbidden_flags: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize conditions to dictionary."""
|
||||
return {
|
||||
"min_relationship": self.min_relationship,
|
||||
"required_flags": self.required_flags,
|
||||
"forbidden_flags": self.forbidden_flags,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'QuestOfferConditions':
|
||||
"""Deserialize conditions from dictionary."""
|
||||
return cls(
|
||||
min_relationship=data.get("min_relationship", 0),
|
||||
required_flags=data.get("required_flags", []),
|
||||
forbidden_flags=data.get("forbidden_flags", []),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NPCOfferDialogue:
|
||||
"""
|
||||
NPC-specific dialogue for offering a quest.
|
||||
|
||||
Each NPC can have custom dialogue and conditions for offering a quest,
|
||||
allowing different NPCs to present the same quest differently.
|
||||
|
||||
Attributes:
|
||||
dialogue: The custom offer dialogue for this NPC
|
||||
conditions: Conditions for this NPC to offer the quest
|
||||
"""
|
||||
|
||||
dialogue: str
|
||||
conditions: QuestOfferConditions = field(default_factory=QuestOfferConditions)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize to dictionary."""
|
||||
return {
|
||||
"dialogue": self.dialogue,
|
||||
"conditions": self.conditions.to_dict(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'NPCOfferDialogue':
|
||||
"""Deserialize from dictionary."""
|
||||
conditions = QuestOfferConditions()
|
||||
if data.get("conditions"):
|
||||
conditions = QuestOfferConditions.from_dict(data["conditions"])
|
||||
return cls(
|
||||
dialogue=data.get("dialogue", ""),
|
||||
conditions=conditions,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuestOfferingTriggers:
|
||||
"""
|
||||
Conditions for when a quest can be offered.
|
||||
|
||||
Controls the probability and eligibility for quest offering based on
|
||||
location, character level, and prerequisite quests.
|
||||
|
||||
Attributes:
|
||||
location_types: Location types where quest can be offered (e.g., ["tavern", "town"])
|
||||
specific_locations: Specific location IDs (if more restrictive than types)
|
||||
min_character_level: Minimum character level to receive quest
|
||||
max_character_level: Maximum character level (optional upper bound)
|
||||
required_quests_completed: Quest IDs that must be completed first
|
||||
probability_weights: Probability by location type (e.g., {"tavern": 0.35})
|
||||
"""
|
||||
|
||||
location_types: List[str] = field(default_factory=list)
|
||||
specific_locations: List[str] = field(default_factory=list)
|
||||
min_character_level: int = 1
|
||||
max_character_level: int = 100
|
||||
required_quests_completed: List[str] = field(default_factory=list)
|
||||
probability_weights: Dict[str, float] = field(default_factory=dict)
|
||||
|
||||
def get_probability(self, location_type: str) -> float:
|
||||
"""
|
||||
Get the offering probability for a location type.
|
||||
|
||||
Args:
|
||||
location_type: Type of location (e.g., "tavern", "town", "wilderness")
|
||||
|
||||
Returns:
|
||||
Probability between 0.0 and 1.0
|
||||
"""
|
||||
return self.probability_weights.get(location_type, 0.0)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize triggers to dictionary."""
|
||||
return {
|
||||
"location_types": self.location_types,
|
||||
"specific_locations": self.specific_locations,
|
||||
"min_character_level": self.min_character_level,
|
||||
"max_character_level": self.max_character_level,
|
||||
"required_quests_completed": self.required_quests_completed,
|
||||
"probability_weights": self.probability_weights,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'QuestOfferingTriggers':
|
||||
"""Deserialize triggers from dictionary."""
|
||||
return cls(
|
||||
location_types=data.get("location_types", []),
|
||||
specific_locations=data.get("specific_locations", []),
|
||||
min_character_level=data.get("min_character_level", 1),
|
||||
max_character_level=data.get("max_character_level", 100),
|
||||
required_quests_completed=data.get("required_quests_completed", []),
|
||||
probability_weights=data.get("probability_weights", {}),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuestDialogueTemplates:
|
||||
"""
|
||||
Dialogue templates for natural quest offering.
|
||||
|
||||
Provides narrative hooks and hints that the AI can use to naturally
|
||||
weave quest offers into conversation.
|
||||
|
||||
Attributes:
|
||||
narrative_hooks: Phrases the NPC might mention (e.g., "mentions scratching sounds")
|
||||
ambient_hints: Environmental descriptions (e.g., "You notice scratch marks")
|
||||
"""
|
||||
|
||||
narrative_hooks: List[str] = field(default_factory=list)
|
||||
ambient_hints: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize to dictionary."""
|
||||
return {
|
||||
"narrative_hooks": self.narrative_hooks,
|
||||
"ambient_hints": self.ambient_hints,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'QuestDialogueTemplates':
|
||||
"""Deserialize from dictionary."""
|
||||
return cls(
|
||||
narrative_hooks=data.get("narrative_hooks", []),
|
||||
ambient_hints=data.get("ambient_hints", []),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuestLoreContext:
|
||||
"""
|
||||
Embedded lore context for quest offering.
|
||||
|
||||
Provides backstory and world context that the AI can reference when
|
||||
discussing the quest with players. This serves as a stub until the
|
||||
Weaviate vector database is implemented in Phase 6.
|
||||
|
||||
Attributes:
|
||||
backstory: Background story for this quest
|
||||
world_connections: How this quest connects to world events
|
||||
regional_hints: Local/regional information related to the quest
|
||||
"""
|
||||
|
||||
backstory: str = ""
|
||||
world_connections: List[str] = field(default_factory=list)
|
||||
regional_hints: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize to dictionary."""
|
||||
return {
|
||||
"backstory": self.backstory,
|
||||
"world_connections": self.world_connections,
|
||||
"regional_hints": self.regional_hints,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'QuestLoreContext':
|
||||
"""Deserialize from dictionary."""
|
||||
return cls(
|
||||
backstory=data.get("backstory", ""),
|
||||
world_connections=data.get("world_connections", []),
|
||||
regional_hints=data.get("regional_hints", []),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Quest:
|
||||
"""
|
||||
Complete quest definition loaded from YAML.
|
||||
|
||||
Quests are the central data structure for the quest system. They define
|
||||
their own NPC givers (quest-centric design), objectives, rewards, and
|
||||
offering conditions.
|
||||
|
||||
Attributes:
|
||||
quest_id: Unique identifier (e.g., "quest_cellar_rats")
|
||||
name: Display name (e.g., "Rat Problem in the Cellar")
|
||||
description: Full quest description
|
||||
difficulty: Quest difficulty level
|
||||
quest_giver_npc_ids: NPCs who can offer this quest
|
||||
quest_giver_name: Display name fallback for quest giver
|
||||
location_id: Primary location for this quest
|
||||
region_id: Region this quest belongs to
|
||||
objectives: List of objectives to complete
|
||||
rewards: Rewards for completion
|
||||
offering_triggers: When/where quest can be offered
|
||||
npc_offer_dialogues: NPC-specific offer dialogue {npc_id: NPCOfferDialogue}
|
||||
npc_quest_knowledge: What NPCs know about this quest {npc_id: [facts]}
|
||||
lore_context: Embedded lore for AI context
|
||||
dialogue_templates: Narrative hooks for natural offering
|
||||
completion_dialogue: NPC dialogue upon completion {npc_id: dialogue}
|
||||
tags: Metadata tags for filtering
|
||||
"""
|
||||
|
||||
quest_id: str
|
||||
name: str
|
||||
description: str
|
||||
difficulty: QuestDifficulty
|
||||
quest_giver_npc_ids: List[str]
|
||||
quest_giver_name: str = ""
|
||||
location_id: str = ""
|
||||
region_id: str = ""
|
||||
objectives: List[QuestObjective] = field(default_factory=list)
|
||||
rewards: QuestReward = field(default_factory=QuestReward)
|
||||
offering_triggers: QuestOfferingTriggers = field(default_factory=QuestOfferingTriggers)
|
||||
npc_offer_dialogues: Dict[str, NPCOfferDialogue] = field(default_factory=dict)
|
||||
npc_quest_knowledge: Dict[str, List[str]] = field(default_factory=dict)
|
||||
lore_context: QuestLoreContext = field(default_factory=QuestLoreContext)
|
||||
dialogue_templates: QuestDialogueTemplates = field(default_factory=QuestDialogueTemplates)
|
||||
completion_dialogue: Dict[str, str] = field(default_factory=dict)
|
||||
tags: List[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
"""Check if all objectives are complete."""
|
||||
return all(obj.is_complete for obj in self.objectives)
|
||||
|
||||
def get_offer_dialogue(self, npc_id: str) -> str:
|
||||
"""
|
||||
Get the offer dialogue for a specific NPC.
|
||||
|
||||
Args:
|
||||
npc_id: The NPC's identifier
|
||||
|
||||
Returns:
|
||||
Custom dialogue if defined, otherwise empty string
|
||||
"""
|
||||
if npc_id in self.npc_offer_dialogues:
|
||||
return self.npc_offer_dialogues[npc_id].dialogue
|
||||
return ""
|
||||
|
||||
def get_offer_conditions(self, npc_id: str) -> QuestOfferConditions:
|
||||
"""
|
||||
Get the offer conditions for a specific NPC.
|
||||
|
||||
Args:
|
||||
npc_id: The NPC's identifier
|
||||
|
||||
Returns:
|
||||
Conditions if defined, otherwise default conditions
|
||||
"""
|
||||
if npc_id in self.npc_offer_dialogues:
|
||||
return self.npc_offer_dialogues[npc_id].conditions
|
||||
return QuestOfferConditions()
|
||||
|
||||
def get_npc_knowledge(self, npc_id: str) -> List[str]:
|
||||
"""
|
||||
Get what an NPC knows about this quest.
|
||||
|
||||
Args:
|
||||
npc_id: The NPC's identifier
|
||||
|
||||
Returns:
|
||||
List of facts the NPC knows about the quest
|
||||
"""
|
||||
return self.npc_quest_knowledge.get(npc_id, [])
|
||||
|
||||
def get_completion_dialogue(self, npc_id: str) -> str:
|
||||
"""
|
||||
Get the completion dialogue for a specific NPC.
|
||||
|
||||
Args:
|
||||
npc_id: The NPC's identifier
|
||||
|
||||
Returns:
|
||||
Custom completion dialogue if defined, otherwise empty string
|
||||
"""
|
||||
return self.completion_dialogue.get(npc_id, "")
|
||||
|
||||
def can_npc_offer(self, npc_id: str) -> bool:
|
||||
"""
|
||||
Check if an NPC can offer this quest.
|
||||
|
||||
Args:
|
||||
npc_id: The NPC's identifier
|
||||
|
||||
Returns:
|
||||
True if NPC is in quest_giver_npc_ids
|
||||
"""
|
||||
return npc_id in self.quest_giver_npc_ids
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize quest to dictionary."""
|
||||
return {
|
||||
"quest_id": self.quest_id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"difficulty": self.difficulty.value,
|
||||
"quest_giver_npc_ids": self.quest_giver_npc_ids,
|
||||
"quest_giver_name": self.quest_giver_name,
|
||||
"location_id": self.location_id,
|
||||
"region_id": self.region_id,
|
||||
"objectives": [obj.to_dict() for obj in self.objectives],
|
||||
"rewards": self.rewards.to_dict(),
|
||||
"offering_triggers": self.offering_triggers.to_dict(),
|
||||
"npc_offer_dialogues": {
|
||||
npc_id: dialogue.to_dict()
|
||||
for npc_id, dialogue in self.npc_offer_dialogues.items()
|
||||
},
|
||||
"npc_quest_knowledge": self.npc_quest_knowledge,
|
||||
"lore_context": self.lore_context.to_dict(),
|
||||
"dialogue_templates": self.dialogue_templates.to_dict(),
|
||||
"completion_dialogue": self.completion_dialogue,
|
||||
"tags": self.tags,
|
||||
}
|
||||
|
||||
def to_offer_dict(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Serialize quest for offering UI display.
|
||||
|
||||
Returns a trimmed version suitable for displaying to players
|
||||
when a quest is being offered.
|
||||
|
||||
Returns:
|
||||
Dictionary with quest offer display data
|
||||
"""
|
||||
return {
|
||||
"quest_id": self.quest_id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"difficulty": self.difficulty.value,
|
||||
"quest_giver_name": self.quest_giver_name,
|
||||
"objectives": [
|
||||
{"description": obj.description, "progress_text": obj.progress_text}
|
||||
for obj in self.objectives
|
||||
],
|
||||
"rewards": {
|
||||
"gold": self.rewards.gold,
|
||||
"experience": self.rewards.experience,
|
||||
"items": self.rewards.items,
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Quest':
|
||||
"""Deserialize quest from dictionary."""
|
||||
# Parse objectives
|
||||
objectives = [
|
||||
QuestObjective.from_dict(obj)
|
||||
for obj in data.get("objectives", [])
|
||||
]
|
||||
|
||||
# Parse rewards
|
||||
rewards = QuestReward()
|
||||
if data.get("rewards"):
|
||||
rewards = QuestReward.from_dict(data["rewards"])
|
||||
|
||||
# Parse offering triggers
|
||||
offering_triggers = QuestOfferingTriggers()
|
||||
if data.get("offering_triggers"):
|
||||
offering_triggers = QuestOfferingTriggers.from_dict(data["offering_triggers"])
|
||||
|
||||
# Parse NPC offer dialogues
|
||||
npc_offer_dialogues = {}
|
||||
for npc_id, dialogue_data in data.get("npc_offer_dialogues", {}).items():
|
||||
npc_offer_dialogues[npc_id] = NPCOfferDialogue.from_dict(dialogue_data)
|
||||
|
||||
# Parse lore context
|
||||
lore_context = QuestLoreContext()
|
||||
if data.get("lore_context"):
|
||||
lore_context = QuestLoreContext.from_dict(data["lore_context"])
|
||||
|
||||
# Parse dialogue templates
|
||||
dialogue_templates = QuestDialogueTemplates()
|
||||
if data.get("dialogue_templates"):
|
||||
dialogue_templates = QuestDialogueTemplates.from_dict(data["dialogue_templates"])
|
||||
|
||||
return cls(
|
||||
quest_id=data["quest_id"],
|
||||
name=data["name"],
|
||||
description=data.get("description", ""),
|
||||
difficulty=QuestDifficulty(data.get("difficulty", "easy")),
|
||||
quest_giver_npc_ids=data.get("quest_giver_npc_ids", []),
|
||||
quest_giver_name=data.get("quest_giver_name", ""),
|
||||
location_id=data.get("location_id", ""),
|
||||
region_id=data.get("region_id", ""),
|
||||
objectives=objectives,
|
||||
rewards=rewards,
|
||||
offering_triggers=offering_triggers,
|
||||
npc_offer_dialogues=npc_offer_dialogues,
|
||||
npc_quest_knowledge=data.get("npc_quest_knowledge", {}),
|
||||
lore_context=lore_context,
|
||||
dialogue_templates=dialogue_templates,
|
||||
completion_dialogue=data.get("completion_dialogue", {}),
|
||||
tags=data.get("tags", []),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the quest."""
|
||||
return f"Quest({self.quest_id}, {self.name}, {self.difficulty.value})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CharacterQuestState:
|
||||
"""
|
||||
Tracks a character's progress on an active quest.
|
||||
|
||||
Stored in character's quest tracking data to persist progress
|
||||
across sessions.
|
||||
|
||||
Attributes:
|
||||
quest_id: The quest being tracked
|
||||
status: Current quest status
|
||||
accepted_at: ISO timestamp when quest was accepted
|
||||
objectives_progress: Progress on each objective {objective_id: current_progress}
|
||||
completed_at: ISO timestamp when quest was completed (if completed)
|
||||
"""
|
||||
|
||||
quest_id: str
|
||||
status: QuestStatus = QuestStatus.ACTIVE
|
||||
accepted_at: str = ""
|
||||
objectives_progress: Dict[str, int] = field(default_factory=dict)
|
||||
completed_at: Optional[str] = None
|
||||
|
||||
def update_progress(self, objective_id: str, amount: int = 1) -> None:
|
||||
"""
|
||||
Update progress on an objective.
|
||||
|
||||
Args:
|
||||
objective_id: The objective to update
|
||||
amount: Amount to add to progress
|
||||
"""
|
||||
current = self.objectives_progress.get(objective_id, 0)
|
||||
self.objectives_progress[objective_id] = current + amount
|
||||
|
||||
def get_progress(self, objective_id: str) -> int:
|
||||
"""
|
||||
Get current progress on an objective.
|
||||
|
||||
Args:
|
||||
objective_id: The objective to check
|
||||
|
||||
Returns:
|
||||
Current progress amount
|
||||
"""
|
||||
return self.objectives_progress.get(objective_id, 0)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize to dictionary."""
|
||||
return {
|
||||
"quest_id": self.quest_id,
|
||||
"status": self.status.value,
|
||||
"accepted_at": self.accepted_at,
|
||||
"objectives_progress": self.objectives_progress,
|
||||
"completed_at": self.completed_at,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'CharacterQuestState':
|
||||
"""Deserialize from dictionary."""
|
||||
return cls(
|
||||
quest_id=data["quest_id"],
|
||||
status=QuestStatus(data.get("status", "active")),
|
||||
accepted_at=data.get("accepted_at", ""),
|
||||
objectives_progress=data.get("objectives_progress", {}),
|
||||
completed_at=data.get("completed_at"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation."""
|
||||
return f"CharacterQuestState({self.quest_id}, {self.status.value})"
|
||||
Reference in New Issue
Block a user