928 lines
31 KiB
Markdown
928 lines
31 KiB
Markdown
# Quest System
|
|
|
|
**Status:** Planned
|
|
**Phase:** 4 (AI Integration + Story Progression)
|
|
**Timeline:** Week 9 of Phase 4 (Days 13-14)
|
|
**Last Updated:** November 16, 2025
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
The Quest System provides structured objectives and rewards for players during their solo story progression sessions. Quests are defined in YAML files and offered to players by the AI Dungeon Master based on context-aware triggers and location-based probability.
|
|
|
|
**Key Principles:**
|
|
- **YAML-driven design** - Quests defined in data files, no code changes needed
|
|
- **Context-aware offering** - AI analyzes narrative context to offer relevant quests
|
|
- **Location-based triggers** - Different areas have different quest probabilities
|
|
- **Max 2 active quests** - Prevents player overwhelm
|
|
- **Random but meaningful** - Quest offering feels natural, not forced
|
|
- **Rewarding progression** - Quests provide gold, XP, and items
|
|
|
|
---
|
|
|
|
## Quest Structure
|
|
|
|
### Quest Components
|
|
|
|
A quest consists of:
|
|
|
|
1. **Metadata** - Name, description, difficulty, quest giver
|
|
2. **Objectives** - Specific goals to complete (ordered or unordered)
|
|
3. **Rewards** - Gold, XP, items awarded upon completion
|
|
4. **Offering Triggers** - Context and location requirements
|
|
5. **Narrative Hooks** - Story fragments for AI to use in offering
|
|
|
|
---
|
|
|
|
## Data Models
|
|
|
|
### Quest Dataclass
|
|
|
|
```python
|
|
@dataclass
|
|
class Quest:
|
|
"""Represents a quest with objectives and rewards."""
|
|
|
|
quest_id: str # Unique identifier (e.g., "quest_rats_tavern")
|
|
name: str # Display name (e.g., "Rat Problem")
|
|
description: str # Full quest description
|
|
quest_giver: str # NPC or source (e.g., "Tavern Keeper")
|
|
difficulty: str # "easy", "medium", "hard", "epic"
|
|
objectives: List[QuestObjective] # List of objectives to complete
|
|
rewards: QuestReward # Rewards for completion
|
|
offering_triggers: QuestTriggers # When/where quest can be offered
|
|
narrative_hooks: List[str] # Story snippets for AI to use
|
|
status: str = "available" # "available", "active", "completed", "failed"
|
|
progress: Dict[str, Any] = field(default_factory=dict) # Objective progress tracking
|
|
|
|
def is_complete(self) -> bool:
|
|
"""Check if all objectives are completed."""
|
|
return all(obj.completed for obj in self.objectives)
|
|
|
|
def get_next_objective(self) -> Optional[QuestObjective]:
|
|
"""Get the next incomplete objective."""
|
|
for obj in self.objectives:
|
|
if not obj.completed:
|
|
return obj
|
|
return None
|
|
|
|
def update_progress(self, objective_id: str, progress_value: int) -> None:
|
|
"""Update progress for a specific objective."""
|
|
for obj in self.objectives:
|
|
if obj.objective_id == objective_id:
|
|
obj.current_progress = min(progress_value, obj.required_progress)
|
|
if obj.current_progress >= obj.required_progress:
|
|
obj.completed = True
|
|
break
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Serialize to dictionary for JSON storage."""
|
|
return {
|
|
"quest_id": self.quest_id,
|
|
"name": self.name,
|
|
"description": self.description,
|
|
"quest_giver": self.quest_giver,
|
|
"difficulty": self.difficulty,
|
|
"objectives": [obj.to_dict() for obj in self.objectives],
|
|
"rewards": self.rewards.to_dict(),
|
|
"offering_triggers": self.offering_triggers.to_dict(),
|
|
"narrative_hooks": self.narrative_hooks,
|
|
"status": self.status,
|
|
"progress": self.progress
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'Quest':
|
|
"""Deserialize from dictionary."""
|
|
return cls(
|
|
quest_id=data["quest_id"],
|
|
name=data["name"],
|
|
description=data["description"],
|
|
quest_giver=data["quest_giver"],
|
|
difficulty=data["difficulty"],
|
|
objectives=[QuestObjective.from_dict(obj) for obj in data["objectives"]],
|
|
rewards=QuestReward.from_dict(data["rewards"]),
|
|
offering_triggers=QuestTriggers.from_dict(data["offering_triggers"]),
|
|
narrative_hooks=data["narrative_hooks"],
|
|
status=data.get("status", "available"),
|
|
progress=data.get("progress", {})
|
|
)
|
|
```
|
|
|
|
### QuestObjective Dataclass
|
|
|
|
```python
|
|
@dataclass
|
|
class QuestObjective:
|
|
"""Represents a single objective within a quest."""
|
|
|
|
objective_id: str # Unique ID (e.g., "kill_rats")
|
|
description: str # Player-facing description
|
|
objective_type: str # "kill", "collect", "travel", "interact", "discover"
|
|
required_progress: int # Target value (e.g., 10 rats)
|
|
current_progress: int = 0 # Current value (e.g., 5 rats killed)
|
|
completed: bool = False # Objective completion status
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"objective_id": self.objective_id,
|
|
"description": self.description,
|
|
"objective_type": self.objective_type,
|
|
"required_progress": self.required_progress,
|
|
"current_progress": self.current_progress,
|
|
"completed": self.completed
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'QuestObjective':
|
|
return cls(
|
|
objective_id=data["objective_id"],
|
|
description=data["description"],
|
|
objective_type=data["objective_type"],
|
|
required_progress=data["required_progress"],
|
|
current_progress=data.get("current_progress", 0),
|
|
completed=data.get("completed", False)
|
|
)
|
|
```
|
|
|
|
### QuestReward Dataclass
|
|
|
|
```python
|
|
@dataclass
|
|
class QuestReward:
|
|
"""Rewards granted upon quest completion."""
|
|
|
|
gold: int = 0 # Gold reward
|
|
experience: int = 0 # XP reward
|
|
items: List[str] = field(default_factory=list) # Item IDs to grant
|
|
reputation: Optional[str] = None # Reputation faction (future feature)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"gold": self.gold,
|
|
"experience": self.experience,
|
|
"items": self.items,
|
|
"reputation": self.reputation
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'QuestReward':
|
|
return cls(
|
|
gold=data.get("gold", 0),
|
|
experience=data.get("experience", 0),
|
|
items=data.get("items", []),
|
|
reputation=data.get("reputation")
|
|
)
|
|
```
|
|
|
|
### QuestTriggers Dataclass
|
|
|
|
```python
|
|
@dataclass
|
|
class QuestTriggers:
|
|
"""Defines when and where a quest can be offered."""
|
|
|
|
location_types: List[str] # ["town", "wilderness", "dungeon"] or ["any"]
|
|
specific_locations: List[str] # Specific location IDs or empty for any
|
|
min_character_level: int = 1 # Minimum level required
|
|
max_character_level: int = 100 # Maximum level (for scaling)
|
|
required_quests_completed: List[str] = field(default_factory=list) # Quest prerequisites
|
|
probability_weights: Dict[str, float] = field(default_factory=dict) # Location-specific chances
|
|
|
|
def get_offer_probability(self, location_type: str) -> float:
|
|
"""Get the probability of offering this quest at location type."""
|
|
return self.probability_weights.get(location_type, 0.0)
|
|
|
|
def can_offer(self, character_level: int, location: str, location_type: str, completed_quests: List[str]) -> bool:
|
|
"""Check if quest can be offered to this character at this location."""
|
|
|
|
# Check level requirements
|
|
if character_level < self.min_character_level or character_level > self.max_character_level:
|
|
return False
|
|
|
|
# Check quest prerequisites
|
|
for required_quest in self.required_quests_completed:
|
|
if required_quest not in completed_quests:
|
|
return False
|
|
|
|
# Check location type
|
|
if "any" not in self.location_types and location_type not in self.location_types:
|
|
return False
|
|
|
|
# Check specific location (if specified)
|
|
if self.specific_locations and location not in self.specific_locations:
|
|
return False
|
|
|
|
return True
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
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]) -> 'QuestTriggers':
|
|
return cls(
|
|
location_types=data["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", {})
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## YAML Quest Definitions
|
|
|
|
Quests are stored in `/app/data/quests/` organized by difficulty:
|
|
|
|
```
|
|
/app/data/quests/
|
|
├── easy/
|
|
│ ├── rat_problem.yaml
|
|
│ ├── delivery_run.yaml
|
|
│ └── missing_cat.yaml
|
|
├── medium/
|
|
│ ├── bandit_camp.yaml
|
|
│ ├── haunted_ruins.yaml
|
|
│ └── merchant_escort.yaml
|
|
├── hard/
|
|
│ ├── dragon_lair.yaml
|
|
│ ├── necromancer_tower.yaml
|
|
│ └── lost_artifact.yaml
|
|
└── epic/
|
|
├── demon_invasion.yaml
|
|
└── ancient_prophecy.yaml
|
|
```
|
|
|
|
### Example Quest YAML: Rat Problem (Easy)
|
|
|
|
**File:** `/app/data/quests/easy/rat_problem.yaml`
|
|
|
|
```yaml
|
|
quest_id: "quest_rats_tavern"
|
|
name: "Rat Problem"
|
|
description: "The local tavern is overrun with giant rats. The tavern keeper needs someone to clear them out before they scare away all the customers."
|
|
quest_giver: "Tavern Keeper"
|
|
difficulty: "easy"
|
|
|
|
objectives:
|
|
- objective_id: "kill_rats"
|
|
description: "Kill 10 giant rats in the tavern basement"
|
|
objective_type: "kill"
|
|
required_progress: 10
|
|
|
|
rewards:
|
|
gold: 50
|
|
experience: 100
|
|
items: []
|
|
|
|
offering_triggers:
|
|
location_types: ["town"]
|
|
specific_locations: [] # Any town
|
|
min_character_level: 1
|
|
max_character_level: 3
|
|
required_quests_completed: []
|
|
probability_weights:
|
|
town: 0.30 # 30% chance in towns
|
|
wilderness: 0.0 # 0% chance in wilderness
|
|
dungeon: 0.0 # 0% chance in dungeons
|
|
|
|
narrative_hooks:
|
|
- "The tavern keeper frantically waves you over, mentioning strange noises from the basement."
|
|
- "You overhear patrons complaining about rat infestations ruining the food supplies."
|
|
- "A desperate-looking innkeeper approaches you, begging for help with a pest problem."
|
|
```
|
|
|
|
### Example Quest YAML: Bandit Camp (Medium)
|
|
|
|
**File:** `/app/data/quests/medium/bandit_camp.yaml`
|
|
|
|
```yaml
|
|
quest_id: "quest_bandit_camp"
|
|
name: "Clear the Bandit Camp"
|
|
description: "A group of bandits has been raiding merchant caravans along the main road. The town guard wants someone to clear out their camp in the nearby woods."
|
|
quest_giver: "Captain of the Guard"
|
|
difficulty: "medium"
|
|
|
|
objectives:
|
|
- objective_id: "find_camp"
|
|
description: "Locate the bandit camp in the forest"
|
|
objective_type: "discover"
|
|
required_progress: 1
|
|
|
|
- objective_id: "defeat_bandits"
|
|
description: "Defeat the bandit leader and their gang"
|
|
objective_type: "kill"
|
|
required_progress: 1
|
|
|
|
- objective_id: "return_goods"
|
|
description: "Return stolen goods to the town"
|
|
objective_type: "interact"
|
|
required_progress: 1
|
|
|
|
rewards:
|
|
gold: 200
|
|
experience: 500
|
|
items: ["iron_sword", "leather_armor"]
|
|
|
|
offering_triggers:
|
|
location_types: ["town"]
|
|
specific_locations: []
|
|
min_character_level: 3
|
|
max_character_level: 7
|
|
required_quests_completed: []
|
|
probability_weights:
|
|
town: 0.20
|
|
wilderness: 0.05
|
|
dungeon: 0.0
|
|
|
|
narrative_hooks:
|
|
- "The captain of the guard summons you, speaking urgently about increased bandit activity."
|
|
- "A merchant tells you tales of lost caravans and a hidden camp somewhere in the eastern woods."
|
|
- "Wanted posters line the town walls, offering rewards for dealing with the bandit menace."
|
|
```
|
|
|
|
### Example Quest YAML: Dragon's Lair (Hard)
|
|
|
|
**File:** `/app/data/quests/hard/dragon_lair.yaml`
|
|
|
|
```yaml
|
|
quest_id: "quest_dragon_lair"
|
|
name: "The Dragon's Lair"
|
|
description: "An ancient red dragon has awakened in the mountains and is terrorizing the region. The kingdom offers a substantial reward for anyone brave enough to slay the beast."
|
|
quest_giver: "Royal Herald"
|
|
difficulty: "hard"
|
|
|
|
objectives:
|
|
- objective_id: "gather_info"
|
|
description: "Gather information about the dragon's lair"
|
|
objective_type: "interact"
|
|
required_progress: 3
|
|
|
|
- objective_id: "find_lair"
|
|
description: "Locate the dragon's lair in the mountains"
|
|
objective_type: "discover"
|
|
required_progress: 1
|
|
|
|
- objective_id: "slay_dragon"
|
|
description: "Defeat the ancient red dragon"
|
|
objective_type: "kill"
|
|
required_progress: 1
|
|
|
|
- objective_id: "claim_hoard"
|
|
description: "Claim a portion of the dragon's hoard"
|
|
objective_type: "collect"
|
|
required_progress: 1
|
|
|
|
rewards:
|
|
gold: 5000
|
|
experience: 10000
|
|
items: ["dragon_scale_armor", "flaming_longsword", "ring_of_fire_resistance"]
|
|
|
|
offering_triggers:
|
|
location_types: ["town"]
|
|
specific_locations: ["capital_city", "mountain_fortress"]
|
|
min_character_level: 10
|
|
max_character_level: 100
|
|
required_quests_completed: []
|
|
probability_weights:
|
|
town: 0.10
|
|
wilderness: 0.02
|
|
dungeon: 0.0
|
|
|
|
narrative_hooks:
|
|
- "A royal herald announces a call to arms - a dragon threatens the kingdom!"
|
|
- "You hear tales of entire villages razed by dragonfire in the northern mountains."
|
|
- "The king's messenger seeks experienced adventurers for a quest of utmost danger."
|
|
```
|
|
|
|
---
|
|
|
|
## Quest Offering Logic
|
|
|
|
### When Quests Are Offered
|
|
|
|
Quests are checked for offering after each story turn action, using a two-stage process:
|
|
|
|
#### Stage 1: Location-Based Probability Roll
|
|
|
|
```python
|
|
def roll_for_quest_offering(location_type: str) -> bool:
|
|
"""Roll to see if any quest should be offered this turn."""
|
|
|
|
base_probabilities = {
|
|
"town": 0.30, # 30% chance in towns/cities
|
|
"tavern": 0.35, # 35% chance in taverns (special location)
|
|
"wilderness": 0.05, # 5% chance in wilderness
|
|
"dungeon": 0.10, # 10% chance in dungeons
|
|
}
|
|
|
|
chance = base_probabilities.get(location_type, 0.05)
|
|
return random.random() < chance
|
|
```
|
|
|
|
#### Stage 2: Context-Aware Selection
|
|
|
|
If the roll succeeds, the AI Dungeon Master analyzes the recent narrative context to select an appropriate quest:
|
|
|
|
```python
|
|
def select_quest_from_context(
|
|
available_quests: List[Quest],
|
|
character: Character,
|
|
session: GameSession,
|
|
location: str,
|
|
location_type: str
|
|
) -> Optional[Quest]:
|
|
"""Use AI to select a contextually appropriate quest."""
|
|
|
|
# Filter quests by eligibility
|
|
eligible_quests = [
|
|
q for q in available_quests
|
|
if q.offering_triggers.can_offer(
|
|
character.level,
|
|
location,
|
|
location_type,
|
|
character.completed_quests
|
|
)
|
|
]
|
|
|
|
if not eligible_quests:
|
|
return None
|
|
|
|
# Build context for AI decision
|
|
context = {
|
|
"character_name": character.name,
|
|
"character_level": character.level,
|
|
"location": location,
|
|
"recent_actions": session.conversation_history[-3:],
|
|
"available_quests": [
|
|
{
|
|
"quest_id": q.quest_id,
|
|
"name": q.name,
|
|
"narrative_hooks": q.narrative_hooks,
|
|
"difficulty": q.difficulty
|
|
}
|
|
for q in eligible_quests
|
|
]
|
|
}
|
|
|
|
# Ask AI to select most fitting quest (or none)
|
|
prompt = render_quest_selection_prompt(context)
|
|
ai_response = call_ai_api(prompt)
|
|
|
|
selected_quest_id = parse_quest_selection(ai_response)
|
|
|
|
if selected_quest_id:
|
|
return next(q for q in eligible_quests if q.quest_id == selected_quest_id)
|
|
|
|
return None
|
|
```
|
|
|
|
### Quest Offering Flow Diagram
|
|
|
|
```
|
|
After Story Turn Action
|
|
│
|
|
▼
|
|
┌─────────────────────────┐
|
|
│ Check Active Quests │
|
|
│ If >= 2, skip offering │
|
|
└────────┬────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────┐
|
|
│ Location-Based Roll │
|
|
│ - Town: 30% │
|
|
│ - Tavern: 35% │
|
|
│ - Wilderness: 5% │
|
|
│ - Dungeon: 10% │
|
|
└────────┬────────────────┘
|
|
│
|
|
▼
|
|
Roll Success?
|
|
│
|
|
┌────┴────┐
|
|
No Yes
|
|
│ │
|
|
│ ▼
|
|
│ ┌─────────────────────────┐
|
|
│ │ Filter Eligible Quests │
|
|
│ │ - Level requirements │
|
|
│ │ - Location match │
|
|
│ │ - Prerequisites met │
|
|
│ └────────┬────────────────┘
|
|
│ │
|
|
│ ▼
|
|
│ Any Eligible?
|
|
│ │
|
|
│ ┌────┴────┐
|
|
│ No Yes
|
|
│ │ │
|
|
│ │ ▼
|
|
│ │ ┌─────────────────────────┐
|
|
│ │ │ AI Context Analysis │
|
|
│ │ │ Select fitting quest │
|
|
│ │ │ from narrative context │
|
|
│ │ └────────┬────────────────┘
|
|
│ │ │
|
|
│ │ ▼
|
|
│ │ Quest Selected?
|
|
│ │ │
|
|
│ │ ┌────┴────┐
|
|
│ │ No Yes
|
|
│ │ │ │
|
|
│ │ │ ▼
|
|
│ │ │ ┌─────────────────────────┐
|
|
│ │ │ │ Offer Quest to Player │
|
|
│ │ │ │ Display narrative hook │
|
|
│ │ │ └─────────────────────────┘
|
|
│ │ │
|
|
└────────┴────────┴──► No Quest Offered
|
|
```
|
|
|
|
---
|
|
|
|
## Quest Tracking and Completion
|
|
|
|
### Accepting a Quest
|
|
|
|
**Endpoint:** `POST /api/v1/quests/accept`
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"character_id": "char_abc123",
|
|
"quest_id": "quest_rats_tavern"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"quest": {
|
|
"quest_id": "quest_rats_tavern",
|
|
"name": "Rat Problem",
|
|
"description": "The local tavern is overrun with giant rats...",
|
|
"objectives": [
|
|
{
|
|
"objective_id": "kill_rats",
|
|
"description": "Kill 10 giant rats in the tavern basement",
|
|
"current_progress": 0,
|
|
"required_progress": 10,
|
|
"completed": false
|
|
}
|
|
],
|
|
"status": "active"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Updating Quest Progress
|
|
|
|
Quest progress is updated automatically during combat or story actions:
|
|
|
|
```python
|
|
def update_quest_progress(character: Character, event_type: str, event_data: Dict) -> List[str]:
|
|
"""Update quest progress based on game events."""
|
|
|
|
updated_quests = []
|
|
|
|
for quest_id in character.active_quests:
|
|
quest = load_quest(quest_id)
|
|
|
|
for objective in quest.objectives:
|
|
if objective.completed:
|
|
continue
|
|
|
|
# Match event to objective type
|
|
if objective.objective_type == "kill" and event_type == "enemy_killed":
|
|
if matches_objective(objective, event_data):
|
|
quest.update_progress(objective.objective_id, objective.current_progress + 1)
|
|
updated_quests.append(quest_id)
|
|
|
|
elif objective.objective_type == "collect" and event_type == "item_obtained":
|
|
if matches_objective(objective, event_data):
|
|
quest.update_progress(objective.objective_id, objective.current_progress + 1)
|
|
updated_quests.append(quest_id)
|
|
|
|
elif objective.objective_type == "discover" and event_type == "location_discovered":
|
|
if matches_objective(objective, event_data):
|
|
quest.update_progress(objective.objective_id, 1)
|
|
updated_quests.append(quest_id)
|
|
|
|
elif objective.objective_type == "interact" and event_type == "npc_interaction":
|
|
if matches_objective(objective, event_data):
|
|
quest.update_progress(objective.objective_id, objective.current_progress + 1)
|
|
updated_quests.append(quest_id)
|
|
|
|
# Check if quest is complete
|
|
if quest.is_complete():
|
|
complete_quest(character, quest)
|
|
|
|
return updated_quests
|
|
```
|
|
|
|
### Completing a Quest
|
|
|
|
**Endpoint:** `POST /api/v1/quests/complete`
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"character_id": "char_abc123",
|
|
"quest_id": "quest_rats_tavern"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"quest_completed": true,
|
|
"rewards": {
|
|
"gold": 50,
|
|
"experience": 100,
|
|
"items": [],
|
|
"level_up": false
|
|
},
|
|
"message": "You have completed the Rat Problem quest! The tavern keeper thanks you profusely."
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Quest Service
|
|
|
|
### QuestService Class
|
|
|
|
```python
|
|
class QuestService:
|
|
"""Service for managing quests."""
|
|
|
|
def __init__(self):
|
|
self.quest_cache: Dict[str, Quest] = {}
|
|
self._load_all_quests()
|
|
|
|
def _load_all_quests(self) -> None:
|
|
"""Load all quests from YAML files."""
|
|
quest_dirs = [
|
|
"app/data/quests/easy/",
|
|
"app/data/quests/medium/",
|
|
"app/data/quests/hard/",
|
|
"app/data/quests/epic/"
|
|
]
|
|
|
|
for quest_dir in quest_dirs:
|
|
for yaml_file in glob.glob(f"{quest_dir}*.yaml"):
|
|
quest = self._load_quest_from_yaml(yaml_file)
|
|
self.quest_cache[quest.quest_id] = quest
|
|
|
|
def _load_quest_from_yaml(self, filepath: str) -> Quest:
|
|
"""Load a single quest from YAML file."""
|
|
with open(filepath, 'r') as f:
|
|
data = yaml.safe_load(f)
|
|
return Quest.from_dict(data)
|
|
|
|
def get_quest(self, quest_id: str) -> Optional[Quest]:
|
|
"""Get quest by ID."""
|
|
return self.quest_cache.get(quest_id)
|
|
|
|
def get_available_quests(self, character: Character, location: str, location_type: str) -> List[Quest]:
|
|
"""Get all quests available to character at location."""
|
|
return [
|
|
quest for quest in self.quest_cache.values()
|
|
if quest.offering_triggers.can_offer(
|
|
character.level,
|
|
location,
|
|
location_type,
|
|
character.completed_quests
|
|
)
|
|
]
|
|
|
|
def accept_quest(self, character_id: str, quest_id: str) -> bool:
|
|
"""Accept a quest for a character."""
|
|
character = get_character(character_id)
|
|
|
|
# Check quest limit
|
|
if len(character.active_quests) >= 2:
|
|
raise QuestLimitExceeded("You can only have 2 active quests at a time")
|
|
|
|
# Add quest to active quests
|
|
if quest_id not in character.active_quests:
|
|
character.active_quests.append(quest_id)
|
|
update_character(character)
|
|
return True
|
|
|
|
return False
|
|
|
|
def complete_quest(self, character_id: str, quest_id: str) -> QuestReward:
|
|
"""Complete a quest and grant rewards."""
|
|
character = get_character(character_id)
|
|
quest = self.get_quest(quest_id)
|
|
|
|
if not quest or not quest.is_complete():
|
|
raise QuestNotComplete("Quest objectives not complete")
|
|
|
|
# Grant rewards
|
|
character.gold += quest.rewards.gold
|
|
character.experience += quest.rewards.experience
|
|
|
|
for item_id in quest.rewards.items:
|
|
item = load_item(item_id)
|
|
character.inventory.append(item)
|
|
|
|
# Move quest to completed
|
|
character.active_quests.remove(quest_id)
|
|
character.completed_quests.append(quest_id)
|
|
|
|
# Check for level up
|
|
leveled_up = check_level_up(character)
|
|
|
|
update_character(character)
|
|
|
|
return quest.rewards
|
|
```
|
|
|
|
---
|
|
|
|
## UI Integration
|
|
|
|
### Quest Tracker (Sidebar)
|
|
|
|
```
|
|
┌─────────────────────────┐
|
|
│ 🎯 Active Quests (1/2) │
|
|
├─────────────────────────┤
|
|
│ │
|
|
│ Rat Problem │
|
|
│ ─────────────── │
|
|
│ ✅ Kill 10 giant rats │
|
|
│ (7/10) │
|
|
│ │
|
|
│ [View Details] │
|
|
│ │
|
|
└─────────────────────────┘
|
|
```
|
|
|
|
### Quest Offering Modal
|
|
|
|
When a quest is offered during a story turn:
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ Quest Offered! │
|
|
├─────────────────────────────────────────┤
|
|
│ │
|
|
│ 🎯 Rat Problem │
|
|
│ │
|
|
│ The tavern keeper frantically waves │
|
|
│ you over, mentioning strange noises │
|
|
│ from the basement. │
|
|
│ │
|
|
│ "Please, you must help! Giant rats │
|
|
│ have overrun my cellar. I'll pay you │
|
|
│ well to clear them out!" │
|
|
│ │
|
|
│ Difficulty: Easy │
|
|
│ Rewards: 50 gold, 100 XP │
|
|
│ │
|
|
│ Objectives: │
|
|
│ • Kill 10 giant rats │
|
|
│ │
|
|
│ [Accept Quest] [Decline] │
|
|
│ │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
### Quest Detail View
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ Rat Problem [Close] │
|
|
├─────────────────────────────────────────┤
|
|
│ │
|
|
│ Quest Giver: Tavern Keeper │
|
|
│ Difficulty: Easy │
|
|
│ │
|
|
│ Description: │
|
|
│ The local tavern is overrun with giant │
|
|
│ rats. The tavern keeper needs someone │
|
|
│ to clear them out before they scare │
|
|
│ away all the customers. │
|
|
│ │
|
|
│ Objectives: │
|
|
│ ✅ Kill 10 giant rats (7/10) │
|
|
│ │
|
|
│ Rewards: │
|
|
│ 💰 50 gold │
|
|
│ ⭐ 100 XP │
|
|
│ │
|
|
│ [Abandon Quest] │
|
|
│ │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Timeline
|
|
|
|
### Week 9 of Phase 4 (Days 13-14)
|
|
|
|
**Day 13: Quest Data Models & Service**
|
|
- Create Quest, QuestObjective, QuestReward, QuestTriggers dataclasses
|
|
- Create QuestService (load from YAML, manage state)
|
|
- Write 5-10 example quest YAML files (2 easy, 2 medium, 1 hard)
|
|
- Unit tests for quest loading and progression logic
|
|
|
|
**Day 14: Quest Offering & Integration**
|
|
- Implement quest offering logic (context-aware + location-based)
|
|
- Integrate quest offering into story turn flow
|
|
- Quest acceptance/completion API endpoints
|
|
- Quest progress tracking during combat/story events
|
|
- Quest UI components (tracker, offering modal, detail view)
|
|
- Integration testing
|
|
|
|
---
|
|
|
|
## Testing Criteria
|
|
|
|
### Unit Tests
|
|
- ✅ Quest loading from YAML
|
|
- ✅ Quest.is_complete() logic
|
|
- ✅ Quest.update_progress() logic
|
|
- ✅ QuestTriggers.can_offer() filtering
|
|
- ✅ QuestService.get_available_quests() filtering
|
|
|
|
### Integration Tests
|
|
- ✅ Quest offered during story turn
|
|
- ✅ Accept quest (add to active_quests)
|
|
- ✅ Quest limit enforced (max 2 active)
|
|
- ✅ Quest progress updates during combat
|
|
- ✅ Complete quest and receive rewards
|
|
- ✅ Level up from quest XP
|
|
- ✅ Abandon quest
|
|
|
|
### Manual Testing
|
|
- ✅ Full quest flow (offer → accept → progress → complete)
|
|
- ✅ Multiple quests active simultaneously
|
|
- ✅ Quest offering feels natural in narrative
|
|
- ✅ Context-aware quest selection works
|
|
- ✅ Location-based probabilities feel right
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
- ✅ Quest data models implemented and tested
|
|
- ✅ QuestService loads quests from YAML files
|
|
- ✅ Quest offering logic integrated into story turns
|
|
- ✅ Context-aware quest selection working
|
|
- ✅ Location-based probability rolls functioning
|
|
- ✅ Max 2 active quests enforced
|
|
- ✅ Quest acceptance and tracking functional
|
|
- ✅ Quest progress updates automatically
|
|
- ✅ Quest completion and rewards granted
|
|
- ✅ Quest UI components implemented
|
|
- ✅ At least 5 example quests defined in YAML
|
|
|
|
---
|
|
|
|
## Future Enhancements (Post-MVP)
|
|
|
|
### Phase 13+
|
|
- **Quest chains**: Multi-quest storylines with prerequisites
|
|
- **Repeatable quests**: Daily/weekly quests for ongoing rewards
|
|
- **Dynamic quest generation**: AI creates custom quests on-the-fly
|
|
- **Quest sharing**: Multiplayer party quests
|
|
- **Quest journal**: Full quest log with completed quest history
|
|
- **Quest dialogue trees**: Branching conversations with quest givers
|
|
- **Time-limited quests**: Quests that expire after X turns
|
|
- **Reputation system**: Quest completion affects faction standing
|
|
- **Quest difficulty scaling**: Rewards scale with character level
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- **[STORY_PROGRESSION.md](STORY_PROGRESSION.md)** - Turn-based story gameplay system
|
|
- **[DATA_MODELS.md](DATA_MODELS.md)** - Quest data model specifications
|
|
- **[GAME_SYSTEMS.md](GAME_SYSTEMS.md)** - Combat integration with quests
|
|
- **[API_REFERENCE.md](API_REFERENCE.md)** - Quest API endpoints
|
|
- **[ROADMAP.md](ROADMAP.md)** - Phase 4 timeline
|
|
|
|
---
|
|
|
|
**Document Version:** 1.0
|
|
**Created:** November 16, 2025
|
|
**Last Updated:** November 16, 2025
|