diff --git a/docs/plans/noble-singing-origami.md b/docs/plans/noble-singing-origami.md deleted file mode 100644 index d976485..0000000 --- a/docs/plans/noble-singing-origami.md +++ /dev/null @@ -1,360 +0,0 @@ -# Phase 5 Quest System - NPC Integration Plan - -## Summary - -Implement a quest-centric system where quests define their NPC givers, and NPCs automatically offer eligible quests during conversations using a probability roll + AI selection approach. - -## Key Design Decisions - -| Decision | Choice | Rationale | -|----------|--------|-----------| -| Data ownership | Quest-centric | Quests define `quest_giver_npc_ids`. Adding new quests doesn't touch NPC files. | -| Offer trigger | Probability + AI select | Location-based roll first, then AI naturally weaves selected quest into dialogue. | -| Lore integration | Stub service | Create `LoreService` interface with mock data now; swap to Weaviate in Phase 6. | - -## Architecture Overview - -``` -NPC Conversation Flow with Quest Integration: - -POST /api/v1/npcs/{npc_id}/talk - │ - ▼ -┌─────────────────────────────┐ -│ 1. Load Context │ -│ - NPC, Character, Location│ -└────────────┬────────────────┘ - │ - ▼ -┌─────────────────────────────┐ -│ 2. Quest Eligibility Check │ -│ QuestEligibilityService │ -│ - Find quests where NPC │ -│ is quest_giver │ -│ - Filter by char level, │ -│ completed, active, etc │ -└────────────┬────────────────┘ - │ - ▼ -┌─────────────────────────────┐ -│ 3. Probability Roll │ -│ - Town: 30%, Tavern: 35% │ -│ - Wilderness: 5% │ -│ - If fail → no offer │ -└────────────┬────────────────┘ - │ (if success) - ▼ -┌─────────────────────────────┐ -│ 4. Get Lore Context (stub) │ -│ LoreService.get_context()│ -│ - Quest embedded lore │ -│ - Mock regional lore │ -└────────────┬────────────────┘ - │ - ▼ -┌─────────────────────────────┐ -│ 5. Build AI Prompt │ -│ - NPC persona + knowledge│ -│ - Quest offering context │ -│ - Lore context │ -└────────────┬────────────────┘ - │ - ▼ -┌─────────────────────────────┐ -│ 6. AI Generates Dialogue │ -│ Naturally weaves quest │ -│ offer into conversation │ -│ Includes [QUEST_OFFER:id]│ -└────────────┬────────────────┘ - │ - ▼ -┌─────────────────────────────┐ -│ 7. Parse Response │ -│ Extract quest_offered │ -│ Return to frontend │ -└─────────────────────────────┘ -``` - ---- - -## YAML Schema Designs - -### Quest YAML (Quest-Centric Approach) - -```yaml -# /api/app/data/quests/easy/cellar_rats.yaml -quest_id: quest_cellar_rats -name: "Rat Problem in the Cellar" -description: | - Giant rats have infested the Rusty Anchor's cellar. -difficulty: easy - -# NPC Quest Givers (quest defines this, not NPC) -quest_giver_npc_ids: - - npc_grom_ironbeard -quest_giver_name: "Grom Ironbeard" # Display fallback - -# Location context -location_id: crossville_tavern -region_id: crossville - -# Offering conditions -offering_triggers: - location_types: ["tavern"] - min_character_level: 1 - max_character_level: 5 - required_quests_completed: [] - probability_weights: - tavern: 0.35 - town: 0.20 - -# NPC-specific offer dialogue (keyed by NPC ID) -npc_offer_dialogues: - npc_grom_ironbeard: - dialogue: | - *leans in conspiratorially* Got a problem, friend. Giant rats - in me cellar. Been scaring off customers. 50 gold for whoever - clears 'em out. - conditions: - min_relationship: 30 - required_flags: [] - forbidden_flags: ["refused_rat_quest"] - -# What NPCs know about this quest (for AI context) -npc_quest_knowledge: - npc_grom_ironbeard: - - "The rats started appearing about a week ago" - - "They seem bigger than normal rats" - - "Old smuggling tunnels under the cellar" - -# Embedded lore (used before Weaviate exists) -lore_context: - backstory: | - The cellar connects to old smuggling tunnels from Captain - Morgath's days. Recent earthquakes may have reopened them. - world_connections: - - "The earthquakes also disturbed the Old Mines" - regional_hints: - - "Smuggling was common 50 years ago in Crossville" - -# Narrative hooks for natural dialogue -dialogue_templates: - narrative_hooks: - - "mentions unusual scratching sounds from below" - - "complains about spoiled food supplies" - - "nervously glances toward the cellar door" - -# Objectives -objectives: - - objective_id: kill_rats - description: "Clear out the giant rats (0/10)" - objective_type: kill - required_progress: 10 - target_enemy_type: giant_rat - -# Rewards -rewards: - gold: 50 - experience: 100 - items: [] - relationship_bonuses: - npc_grom_ironbeard: 10 - unlocks_quests: ["quest_tunnel_mystery"] - -# Completion -completion_dialogue: - npc_grom_ironbeard: | - *actually smiles* Well done! Rats are gone, cellar's safe. - Here's your coin. Drink's on the house tonight. -``` - -### NPC YAML (Simplified - No Quest Lists) - -NPCs no longer need `quest_giver_for` since quests define their givers. - -```yaml -# /api/app/data/npcs/crossville/npc_grom_ironbeard.yaml -npc_id: npc_grom_ironbeard -name: Grom Ironbeard -role: bartender -location_id: crossville_tavern -image_url: /static/images/npcs/crossville/grom_ironbeard.png - -# All other existing fields remain unchanged -personality: { ... } -appearance: { ... } -knowledge: { ... } -dialogue_hooks: { ... } -# NO quest_giver_for field needed -``` - ---- - -## Implementation Tasks - -### Group 1: Quest Data Models (3 tasks) - -| ID | Task | File | Notes | -|----|------|------|-------| -| 5.1 | Create Quest dataclass | `/api/app/models/quest.py` | Include all fields from YAML schema | -| 5.2 | Create QuestObjective, QuestReward, QuestTriggers | `/api/app/models/quest.py` | Objective types: kill, collect, travel, interact, discover | -| 5.3 | Add `to_dict()`/`from_dict()` serialization | `/api/app/models/quest.py` | Test round-trip JSON serialization | - -### Group 2: Quest Loading Service (3 tasks) - -| ID | Task | File | Notes | -|----|------|------|-------| -| 5.4 | Create `/api/app/data/quests/` directory structure | directories | `easy/`, `medium/`, `hard/`, `epic/` | -| 5.5 | Implement QuestService (loader) | `/api/app/services/quest_service.py` | Follow `npc_loader.py` pattern with caching | -| 5.6 | Write 5 example quests | `/api/app/data/quests/*.yaml` | 2 easy, 2 medium, 1 hard | - -### Group 3: Quest Eligibility Service (3 tasks) - -| ID | Task | File | Notes | -|----|------|------|-------| -| 5.7 | Create QuestEligibilityService | `/api/app/services/quest_eligibility_service.py` | Core eligibility logic | -| 5.8 | Implement `get_quests_for_npc()` | same file | Find quests where NPC is quest_giver | -| 5.9 | Implement probability roll + filtering | same file | Location-based weights, level check, status check | - -### Group 4: Lore Service Stub (2 tasks) - -| ID | Task | File | Notes | -|----|------|------|-------| -| 5.10 | Create LoreService interface | `/api/app/services/lore_service.py` | Abstract interface for Phase 6 | -| 5.11 | Implement MockLoreService | same file | Returns quest's embedded lore_context | - -### Group 5: AI Prompt Integration (3 tasks) - -| ID | Task | File | Notes | -|----|------|------|-------| -| 5.12 | Add quest offering section to template | `/api/app/ai/templates/npc_dialogue.j2` | Quest context + natural weaving instructions | -| 5.13 | Add lore context section to template | same file | Filtered lore for NPC knowledge | -| 5.14 | Implement quest offer parsing | `/api/app/ai/response_parser.py` | Extract `[QUEST_OFFER:quest_id]` markers | - -### Group 6: NPC API Integration (4 tasks) - -| ID | Task | File | Notes | -|----|------|------|-------| -| 5.15 | Integrate eligibility check into `talk_to_npc` | `/api/app/api/npcs.py` | Check before building AI context | -| 5.16 | Add quest context to AI task | `/api/app/tasks/ai_tasks.py` | Modify `_process_npc_dialogue_task` | -| 5.17 | Handle quest_offered in response | `/api/app/api/npcs.py` | Parse and include in API response | -| 5.18 | Remove `quest_giver_for` from NPC model | `/api/app/models/npc.py` | Clean up old field if exists | - -### Group 7: Quest Accept/Manage Endpoints (4 tasks) - -| ID | Task | File | Notes | -|----|------|------|-------| -| 5.19 | Create quests blueprint | `/api/app/api/quests.py` | Register in `__init__.py` | -| 5.20 | Implement `POST /api/v1/quests/accept` | same file | Add to active_quests, update relationship | -| 5.21 | Implement `POST /api/v1/quests/decline` | same file | Set `refused_{quest_id}` flag | -| 5.22 | Implement `GET /api/v1/characters/{id}/quests` | same file | List active and completed quests | - -### Group 8: Testing & Validation (3 tasks) - -| ID | Task | File | Notes | -|----|------|------|-------| -| 5.23 | Unit tests for Quest models | `/api/tests/test_quest_models.py` | Serialization, validation | -| 5.24 | Unit tests for QuestEligibilityService | `/api/tests/test_quest_eligibility.py` | Filtering logic | -| 5.25 | Integration test: full quest offer flow | `/api/tests/test_quest_integration.py` | NPC talk → offer → accept | - ---- - -## Critical Files to Read Before Implementation - -| File | Reason | -|------|--------| -| `/api/app/services/npc_loader.py` | Pattern for YAML loading with caching | -| `/api/app/models/npc.py` | Current NPC dataclass structure | -| `/api/app/api/npcs.py` | Current `talk_to_npc` endpoint implementation | -| `/api/app/tasks/ai_tasks.py` | `_process_npc_dialogue_task` function (lines 667-795) | -| `/api/app/ai/templates/npc_dialogue.j2` | Current prompt template structure | -| `/api/app/models/character.py` | `active_quests`, `completed_quests` fields | - ---- - -## Data Flow Summary - -``` -1. Player talks to NPC → POST /api/v1/npcs/{npc_id}/talk - -2. Backend: - a. QuestService.get_quests_for_npc(npc_id) - → Find quests where npc_id in quest_giver_npc_ids - - b. QuestEligibilityService.filter_eligible(quests, character) - → Remove: already active, completed, wrong level, flags block - - c. Probability roll based on location - → 35% chance in tavern, 5% in wilderness, etc. - - d. If roll succeeds + eligible quests exist: - → Pick quest (first eligible or AI-selected if multiple) - → Build QuestOfferContext with dialogue + lore - - e. Add quest context to AI prompt - - f. AI generates dialogue, naturally mentions quest - → Includes [QUEST_OFFER:quest_cellar_rats] if offering - -3. Parse response, return to frontend: - { - "dialogue": "NPC's natural dialogue...", - "quest_offered": { - "quest_id": "quest_cellar_rats", - "name": "Rat Problem", - "description": "...", - "rewards": {...} - } - } - -4. Frontend shows quest offer UI - → Player clicks Accept - -5. POST /api/v1/quests/accept - → Add to character.active_quests - → Update NPC relationship (+5) - → Return acceptance dialogue -``` - ---- - -## Phase 6 Integration Points - -When implementing Phase 6 (Weaviate lore), these touchpoints enable integration: - -1. **LoreService interface** - Replace `MockLoreService` with `WeaviateLoreService` -2. **Quest.lore_context** - Supplement embedded lore with Weaviate queries -3. **NPC dialogue template** - Lore section already prepared -4. **Knowledge filtering** - `LoreService.filter_for_npc()` method exists - ---- - -## NPC YAML Migration - -Existing NPC files need these changes: - -**Remove (if exists):** -- `quest_giver_for: [...]` - No longer needed - -**Keep unchanged:** -- `location_id` - Required -- `image_url` - Required -- All other fields - Unchanged - -The `quest_giver_for` field in `npc_mayor_aldric.yaml` will be removed since quests now define their givers. - ---- - -## Success Criteria - -- [ ] Quest YAML schema implemented and validated -- [ ] QuestService loads quests from YAML with caching -- [ ] QuestEligibilityService filters correctly by all conditions -- [ ] Probability roll works per location type -- [ ] AI prompt includes quest context when offering -- [ ] AI naturally weaves quest offers into dialogue -- [ ] Quest offer parsing extracts `[QUEST_OFFER:id]` correctly -- [ ] Accept endpoint adds quest to active_quests -- [ ] Max 2 active quests enforced -- [ ] Relationship bonus applied on quest completion -- [ ] LoreService stub returns embedded lore_context diff --git a/public_web/app/views/game_views.py b/public_web/app/views/game_views.py index d7e3793..21e8df0 100644 --- a/public_web/app/views/game_views.py +++ b/public_web/app/views/game_views.py @@ -1323,6 +1323,10 @@ def inventory_equip(session_id: str): if not item_id: return '
No item selected
', 400 + if not slot: + logger.warning("equip_missing_slot", item_id=item_id) + return '
No equipment slot specified
', 400 + try: # Get session to find character session_response = client.get(f'/api/v1/sessions/{session_id}') @@ -1333,17 +1337,14 @@ def inventory_equip(session_id: str): return '
No character found
', 400 # Equip the item via API - payload = {'item_id': item_id} - if slot: - payload['slot'] = slot - + payload = {'item_id': item_id, 'slot': slot} client.post(f'/api/v1/characters/{character_id}/inventory/equip', payload) # Return updated character panel return redirect(url_for('game.character_panel', session_id=session_id)) except APIError as e: - logger.error("failed_to_equip_item", session_id=session_id, item_id=item_id, error=str(e)) + logger.error("failed_to_equip_item", session_id=session_id, item_id=item_id, slot=slot, error=str(e)) return f'
Failed to equip item: {e}
', 500 diff --git a/public_web/templates/game/partials/equipment_modal.html b/public_web/templates/game/partials/equipment_modal.html index f4a17a1..e532d5d 100644 --- a/public_web/templates/game/partials/equipment_modal.html +++ b/public_web/templates/game/partials/equipment_modal.html @@ -12,14 +12,17 @@ Displays character's equipped gear and inventory summary {# Modal Body #}