Summary of Fixes
Issue 1: Slot Name Mismatch - Equipment modal used armor, accessory but API uses chest, accessory_1 - Updated to all 8 API slots: weapon, off_hand, helmet, chest, gloves, boots, accessory_1, accessory_2 Issue 2: HTMX Request Not Firing (the real blocker) - onclick=closeModal() was removing the button from DOM before HTMX could send the request - Changed to hx-on::after-request=closeModal() so modal closes after the request completes
This commit is contained in:
@@ -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
|
|
||||||
@@ -1323,6 +1323,10 @@ def inventory_equip(session_id: str):
|
|||||||
if not item_id:
|
if not item_id:
|
||||||
return '<div class="error">No item selected</div>', 400
|
return '<div class="error">No item selected</div>', 400
|
||||||
|
|
||||||
|
if not slot:
|
||||||
|
logger.warning("equip_missing_slot", item_id=item_id)
|
||||||
|
return '<div class="error">No equipment slot specified</div>', 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get session to find character
|
# Get session to find character
|
||||||
session_response = client.get(f'/api/v1/sessions/{session_id}')
|
session_response = client.get(f'/api/v1/sessions/{session_id}')
|
||||||
@@ -1333,17 +1337,14 @@ def inventory_equip(session_id: str):
|
|||||||
return '<div class="error">No character found</div>', 400
|
return '<div class="error">No character found</div>', 400
|
||||||
|
|
||||||
# Equip the item via API
|
# Equip the item via API
|
||||||
payload = {'item_id': item_id}
|
payload = {'item_id': item_id, 'slot': slot}
|
||||||
if slot:
|
|
||||||
payload['slot'] = slot
|
|
||||||
|
|
||||||
client.post(f'/api/v1/characters/{character_id}/inventory/equip', payload)
|
client.post(f'/api/v1/characters/{character_id}/inventory/equip', payload)
|
||||||
|
|
||||||
# Return updated character panel
|
# Return updated character panel
|
||||||
return redirect(url_for('game.character_panel', session_id=session_id))
|
return redirect(url_for('game.character_panel', session_id=session_id))
|
||||||
|
|
||||||
except APIError as e:
|
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'<div class="error">Failed to equip item: {e}</div>', 500
|
return f'<div class="error">Failed to equip item: {e}</div>', 500
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,17 @@ Displays character's equipped gear and inventory summary
|
|||||||
|
|
||||||
{# Modal Body #}
|
{# Modal Body #}
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{# Equipment Grid #}
|
{# Equipment Grid - All 8 slots matching API #}
|
||||||
<div class="equipment-grid">
|
<div class="equipment-grid">
|
||||||
{% set slots = [
|
{% set slots = [
|
||||||
('weapon', 'Weapon'),
|
('weapon', 'Weapon'),
|
||||||
('armor', 'Armor'),
|
('off_hand', 'Off-Hand'),
|
||||||
('helmet', 'Helmet'),
|
('helmet', 'Helmet'),
|
||||||
|
('chest', 'Chest Armor'),
|
||||||
|
('gloves', 'Gloves'),
|
||||||
('boots', 'Boots'),
|
('boots', 'Boots'),
|
||||||
('accessory', 'Accessory')
|
('accessory_1', 'Accessory 1'),
|
||||||
|
('accessory_2', 'Accessory 2')
|
||||||
] %}
|
] %}
|
||||||
|
|
||||||
{% for slot_id, slot_name in slots %}
|
{% for slot_id, slot_name in slots %}
|
||||||
@@ -34,11 +37,15 @@ Displays character's equipped gear and inventory summary
|
|||||||
{# Equipped Item #}
|
{# Equipped Item #}
|
||||||
<div class="slot-item">
|
<div class="slot-item">
|
||||||
<div class="slot-icon">
|
<div class="slot-icon">
|
||||||
{% if item.item_type == 'weapon' %}⚔️
|
{# Icon based on slot_id since item_type doesn't distinguish armor slots #}
|
||||||
{% elif item.item_type == 'armor' %}🛡️
|
{% if slot_id == 'weapon' %}⚔️
|
||||||
{% elif item.item_type == 'helmet' %}⛑️
|
{% elif slot_id == 'off_hand' %}🛡️
|
||||||
{% elif item.item_type == 'boots' %}👢
|
{% elif slot_id == 'helmet' %}⛑️
|
||||||
{% elif item.item_type == 'accessory' %}💍
|
{% elif slot_id == 'chest' %}🎽
|
||||||
|
{% elif slot_id == 'gloves' %}🧤
|
||||||
|
{% elif slot_id == 'boots' %}👢
|
||||||
|
{% elif slot_id == 'accessory_1' %}💍
|
||||||
|
{% elif slot_id == 'accessory_2' %}📿
|
||||||
{% else %}📦{% endif %}
|
{% else %}📦{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="slot-details">
|
<div class="slot-details">
|
||||||
@@ -63,10 +70,13 @@ Displays character's equipped gear and inventory summary
|
|||||||
<div class="slot-empty">
|
<div class="slot-empty">
|
||||||
<div class="slot-icon slot-icon--empty">
|
<div class="slot-icon slot-icon--empty">
|
||||||
{% if slot_id == 'weapon' %}⚔️
|
{% if slot_id == 'weapon' %}⚔️
|
||||||
{% elif slot_id == 'armor' %}🛡️
|
{% elif slot_id == 'off_hand' %}🛡️
|
||||||
{% elif slot_id == 'helmet' %}⛑️
|
{% elif slot_id == 'helmet' %}⛑️
|
||||||
|
{% elif slot_id == 'chest' %}🎽
|
||||||
|
{% elif slot_id == 'gloves' %}🧤
|
||||||
{% elif slot_id == 'boots' %}👢
|
{% elif slot_id == 'boots' %}👢
|
||||||
{% elif slot_id == 'accessory' %}💍
|
{% elif slot_id == 'accessory_1' %}💍
|
||||||
|
{% elif slot_id == 'accessory_2' %}📿
|
||||||
{% else %}📦{% endif %}
|
{% else %}📦{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="slot-empty-text">Empty</div>
|
<div class="slot-empty-text">Empty</div>
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ Partial template loaded via HTMX when an item is selected
|
|||||||
hx-vals='{"item_id": "{{ item.item_id }}"{% if suggested_slot %}, "slot": "{{ suggested_slot }}"{% endif %}}'
|
hx-vals='{"item_id": "{{ item.item_id }}"{% if suggested_slot %}, "slot": "{{ suggested_slot }}"{% endif %}}'
|
||||||
hx-target="#character-panel"
|
hx-target="#character-panel"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
onclick="closeModal()">
|
hx-on::after-request="closeModal()">
|
||||||
Equip
|
Equip
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user