first commit
This commit is contained in:
186
public_web/templates/game/partials/character_panel.html
Normal file
186
public_web/templates/game/partials/character_panel.html
Normal file
@@ -0,0 +1,186 @@
|
||||
{#
|
||||
Character Panel - Left sidebar
|
||||
Displays character stats, resource bars, and action buttons
|
||||
#}
|
||||
<div class="character-panel">
|
||||
{# Character Header #}
|
||||
<div class="character-header">
|
||||
<div class="character-name">{{ character.name }}</div>
|
||||
<div class="character-info">
|
||||
<span class="character-class">{{ character.class_name }}</span>
|
||||
<span class="character-level">Level {{ character.level }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Resource Bars #}
|
||||
<div class="resource-bars">
|
||||
{# HP Bar #}
|
||||
<div class="resource-bar resource-bar--hp">
|
||||
<div class="resource-bar-label">
|
||||
<span class="resource-bar-name">HP</span>
|
||||
<span class="resource-bar-value">{{ character.current_hp }} / {{ character.max_hp }}</span>
|
||||
</div>
|
||||
<div class="resource-bar-track">
|
||||
{% set hp_percent = (character.current_hp / character.max_hp * 100)|int %}
|
||||
<div class="resource-bar-fill" style="width: {{ hp_percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# MP Bar #}
|
||||
<div class="resource-bar resource-bar--mp">
|
||||
<div class="resource-bar-label">
|
||||
<span class="resource-bar-name">MP</span>
|
||||
<span class="resource-bar-value">{{ character.current_mp }} / {{ character.max_mp }}</span>
|
||||
</div>
|
||||
<div class="resource-bar-track">
|
||||
{% set mp_percent = (character.current_mp / character.max_mp * 100)|int %}
|
||||
<div class="resource-bar-fill" style="width: {{ mp_percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Stats Accordion (Collapsed by default) #}
|
||||
<div class="panel-accordion collapsed" data-panel-accordion="stats">
|
||||
<button class="panel-accordion-header" onclick="togglePanelAccordion(this)">
|
||||
<span>Stats</span>
|
||||
<span class="panel-accordion-icon">▼</span>
|
||||
</button>
|
||||
<div class="panel-accordion-content">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-abbr">STR</div>
|
||||
<div class="stat-value">{{ character.stats.strength }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-abbr">DEX</div>
|
||||
<div class="stat-value">{{ character.stats.dexterity }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-abbr">CON</div>
|
||||
<div class="stat-value">{{ character.stats.constitution }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-abbr">INT</div>
|
||||
<div class="stat-value">{{ character.stats.intelligence }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-abbr">WIS</div>
|
||||
<div class="stat-value">{{ character.stats.wisdom }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-abbr">CHA</div>
|
||||
<div class="stat-value">{{ character.stats.charisma }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Quick Actions (Equipment, NPC, Travel) #}
|
||||
<div class="quick-actions">
|
||||
{# Equipment & Gear - Opens modal #}
|
||||
<button class="action-btn action-btn--special"
|
||||
hx-get="{{ url_for('game.equipment_modal', session_id=session_id) }}"
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML">
|
||||
⚔️ Equipment & Gear
|
||||
</button>
|
||||
|
||||
{# Talk to NPC - Opens NPC accordion #}
|
||||
<button class="action-btn action-btn--special"
|
||||
hx-get="{{ url_for('game.npcs_accordion', session_id=session_id) }}"
|
||||
hx-target="#accordion-npcs"
|
||||
hx-swap="innerHTML"
|
||||
onclick="document.querySelector('[data-accordion=npcs]').classList.remove('collapsed')">
|
||||
💬 Talk to NPC...
|
||||
</button>
|
||||
|
||||
{# Travel - Opens modal #}
|
||||
<button class="action-btn action-btn--special"
|
||||
hx-get="{{ url_for('game.travel_modal', session_id=session_id) }}"
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML">
|
||||
🗺️ Travel to...
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Actions Section #}
|
||||
<div class="actions-section">
|
||||
<div class="actions-title">Actions</div>
|
||||
|
||||
{# Free Tier Actions #}
|
||||
<div class="actions-tier">
|
||||
<div class="actions-tier-label">Free Actions</div>
|
||||
<div class="actions-list">
|
||||
{% for action in actions.free %}
|
||||
{% set available = action.context == ['any'] or location.location_type in action.context %}
|
||||
<button class="action-btn action-btn--free"
|
||||
hx-post="{{ url_for('game.take_action', session_id=session_id) }}"
|
||||
hx-vals='{"action_type": "button", "prompt_id": "{{ action.prompt_id }}"}'
|
||||
hx-target="#narrative-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-disabled-elt="this"
|
||||
{% if not available %}disabled title="Not available in this location"{% endif %}>
|
||||
<span class="action-btn-text">{{ action.display_text }}</span>
|
||||
{% if action.cooldown %}
|
||||
<span class="action-btn-cooldown" title="{{ action.cooldown }} turn cooldown">⏱</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Premium Tier Actions #}
|
||||
<div class="actions-tier">
|
||||
<div class="actions-tier-label">Premium Actions</div>
|
||||
<div class="actions-list">
|
||||
{% for action in actions.premium %}
|
||||
{% set available = action.context == ['any'] or location.location_type in action.context %}
|
||||
{% set locked = user_tier not in ['premium', 'elite'] %}
|
||||
<button class="action-btn {% if locked %}action-btn--locked{% else %}action-btn--premium{% endif %}"
|
||||
{% if not locked %}
|
||||
hx-post="{{ url_for('game.take_action', session_id=session_id) }}"
|
||||
hx-vals='{"action_type": "button", "prompt_id": "{{ action.prompt_id }}"}'
|
||||
hx-target="#narrative-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-disabled-elt="this"
|
||||
{% endif %}
|
||||
{% if locked %}disabled title="Requires Premium tier"{% elif not available %}disabled title="Not available in this location"{% endif %}>
|
||||
<span class="action-btn-text">{{ action.display_text }}</span>
|
||||
{% if locked %}
|
||||
<span class="action-btn-lock">🔒</span>
|
||||
{% elif action.cooldown %}
|
||||
<span class="action-btn-cooldown" title="{{ action.cooldown }} turn cooldown">⏱</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Elite Tier Actions #}
|
||||
<div class="actions-tier">
|
||||
<div class="actions-tier-label">Elite Actions</div>
|
||||
<div class="actions-list">
|
||||
{% for action in actions.elite %}
|
||||
{% set available = action.context == ['any'] or location.location_type in action.context %}
|
||||
{% set locked = user_tier != 'elite' %}
|
||||
<button class="action-btn {% if locked %}action-btn--locked{% else %}action-btn--elite{% endif %}"
|
||||
{% if not locked %}
|
||||
hx-post="{{ url_for('game.take_action', session_id=session_id) }}"
|
||||
hx-vals='{"action_type": "button", "prompt_id": "{{ action.prompt_id }}"}'
|
||||
hx-target="#narrative-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-disabled-elt="this"
|
||||
{% endif %}
|
||||
{% if locked %}disabled title="Requires Elite tier"{% elif not available %}disabled title="Not available in this location"{% endif %}>
|
||||
<span class="action-btn-text">{{ action.display_text }}</span>
|
||||
{% if locked %}
|
||||
<span class="action-btn-lock">🔒</span>
|
||||
{% elif action.cooldown %}
|
||||
<span class="action-btn-cooldown" title="{{ action.cooldown }} turn cooldown">⏱</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
26
public_web/templates/game/partials/dm_response.html
Normal file
26
public_web/templates/game/partials/dm_response.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{#
|
||||
DM Response Partial
|
||||
Shows the completed DM response and triggers sidebar refreshes
|
||||
#}
|
||||
<div class="dm-response">
|
||||
{{ dm_response }}
|
||||
</div>
|
||||
|
||||
{# Hidden triggers to refresh sidebars after action completes #}
|
||||
<div hx-get="{{ url_for('game.history_accordion', session_id=session_id) }}"
|
||||
hx-trigger="load"
|
||||
hx-target="#accordion-history"
|
||||
hx-swap="innerHTML"
|
||||
style="display: none;"></div>
|
||||
|
||||
<div hx-get="{{ url_for('game.npcs_accordion', session_id=session_id) }}"
|
||||
hx-trigger="load"
|
||||
hx-target="#accordion-npcs"
|
||||
hx-swap="innerHTML"
|
||||
style="display: none;"></div>
|
||||
|
||||
<div hx-get="{{ url_for('game.character_panel', session_id=session_id) }}"
|
||||
hx-trigger="load"
|
||||
hx-target="#character-panel"
|
||||
hx-swap="innerHTML"
|
||||
style="display: none;"></div>
|
||||
108
public_web/templates/game/partials/equipment_modal.html
Normal file
108
public_web/templates/game/partials/equipment_modal.html
Normal file
@@ -0,0 +1,108 @@
|
||||
{#
|
||||
Equipment Modal
|
||||
Displays character's equipped gear and inventory summary
|
||||
#}
|
||||
<div class="modal-overlay" onclick="if(event.target === this) closeModal()">
|
||||
<div class="modal-content modal-content--md">
|
||||
{# Modal Header #}
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Equipment & Gear</h3>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
|
||||
{# Modal Body #}
|
||||
<div class="modal-body">
|
||||
{# Equipment Grid #}
|
||||
<div class="equipment-grid">
|
||||
{% set slots = [
|
||||
('weapon', 'Weapon'),
|
||||
('armor', 'Armor'),
|
||||
('helmet', 'Helmet'),
|
||||
('boots', 'Boots'),
|
||||
('accessory', 'Accessory')
|
||||
] %}
|
||||
|
||||
{% for slot_id, slot_name in slots %}
|
||||
{% set item = character.equipped.get(slot_id) %}
|
||||
<div class="equipment-slot {% if item %}equipment-slot--equipped{% else %}equipment-slot--empty{% endif %}"
|
||||
data-slot="{{ slot_id }}">
|
||||
<div class="slot-header">
|
||||
<span class="slot-label">{{ slot_name }}</span>
|
||||
</div>
|
||||
|
||||
{% if item %}
|
||||
{# Equipped Item #}
|
||||
<div class="slot-item">
|
||||
<div class="slot-icon">
|
||||
{% if item.item_type == 'weapon' %}⚔️
|
||||
{% elif item.item_type == 'armor' %}🛡️
|
||||
{% elif item.item_type == 'helmet' %}⛑️
|
||||
{% elif item.item_type == 'boots' %}👢
|
||||
{% elif item.item_type == 'accessory' %}💍
|
||||
{% else %}📦{% endif %}
|
||||
</div>
|
||||
<div class="slot-details">
|
||||
<div class="slot-item-name">{{ item.name }}</div>
|
||||
<div class="slot-stats">
|
||||
{% if item.damage %}
|
||||
<span class="stat-damage">{{ item.damage }} DMG</span>
|
||||
{% endif %}
|
||||
{% if item.defense %}
|
||||
<span class="stat-defense">{{ item.defense }} DEF</span>
|
||||
{% endif %}
|
||||
{% if item.stat_bonuses %}
|
||||
{% for stat, bonus in item.stat_bonuses.items() %}
|
||||
<span class="stat-bonus">+{{ bonus }} {{ stat[:3].upper() }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{# Empty Slot #}
|
||||
<div class="slot-empty">
|
||||
<div class="slot-icon slot-icon--empty">
|
||||
{% if slot_id == 'weapon' %}⚔️
|
||||
{% elif slot_id == 'armor' %}🛡️
|
||||
{% elif slot_id == 'helmet' %}⛑️
|
||||
{% elif slot_id == 'boots' %}👢
|
||||
{% elif slot_id == 'accessory' %}💍
|
||||
{% else %}📦{% endif %}
|
||||
</div>
|
||||
<div class="slot-empty-text">Empty</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Equipment Summary #}
|
||||
<div class="equipment-summary">
|
||||
<div class="summary-title">Total Bonuses</div>
|
||||
<div class="summary-stats">
|
||||
{% set total_bonuses = {} %}
|
||||
{% for slot_id, item in character.equipped.items() %}
|
||||
{% if item and item.stat_bonuses %}
|
||||
{% for stat, bonus in item.stat_bonuses.items() %}
|
||||
{% if total_bonuses.update({stat: total_bonuses.get(stat, 0) + bonus}) %}{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if total_bonuses %}
|
||||
{% for stat, bonus in total_bonuses.items() %}
|
||||
<span class="summary-stat">+{{ bonus }} {{ stat[:3].upper() }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="summary-none">No stat bonuses</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Modal Footer #}
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn--secondary" onclick="closeModal()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
27
public_web/templates/game/partials/job_polling.html
Normal file
27
public_web/templates/game/partials/job_polling.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{#
|
||||
Job Polling Partial
|
||||
Shows loading state while waiting for AI response, auto-polls for completion
|
||||
#}
|
||||
{% if player_action %}
|
||||
<div class="player-action-echo">
|
||||
<span class="player-action-label">Your action:</span>
|
||||
<span class="player-action-text">{{ player_action }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="loading-state"
|
||||
hx-get="{{ url_for('game.poll_job', session_id=session_id, job_id=job_id) }}"
|
||||
hx-trigger="load delay:1s"
|
||||
hx-swap="innerHTML"
|
||||
hx-target="#narrative-content">
|
||||
<div class="loading-spinner-large"></div>
|
||||
<p class="loading-text">
|
||||
{% if status == 'queued' %}
|
||||
Awaiting the Dungeon Master...
|
||||
{% elif status == 'processing' %}
|
||||
The Dungeon Master considers your action...
|
||||
{% else %}
|
||||
Processing...
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
66
public_web/templates/game/partials/narrative_panel.html
Normal file
66
public_web/templates/game/partials/narrative_panel.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{#
|
||||
Narrative Panel - Middle section
|
||||
Displays location header, ambient details, and DM response
|
||||
#}
|
||||
<div class="narrative-panel">
|
||||
{# Location Header #}
|
||||
<div class="location-header">
|
||||
<div class="location-top">
|
||||
<span class="location-type-badge location-type-badge--{{ location.location_type }}">
|
||||
{{ location.location_type }}
|
||||
</span>
|
||||
<h2 class="location-name">{{ location.name }}</h2>
|
||||
</div>
|
||||
<div class="location-meta">
|
||||
<span class="location-region">{{ location.region }}</span>
|
||||
<span class="turn-counter">Turn {{ session.turn_number }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Ambient Details (Collapsible) #}
|
||||
{% if location.ambient_description %}
|
||||
<div class="ambient-section">
|
||||
<button class="ambient-toggle" onclick="toggleAmbient()">
|
||||
<span>Ambient Details</span>
|
||||
<span class="ambient-icon">▼</span>
|
||||
</button>
|
||||
<div class="ambient-content">
|
||||
{{ location.ambient_description }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# DM Response Area #}
|
||||
<div class="narrative-content" id="narrative-content">
|
||||
<div class="dm-response">
|
||||
{{ dm_response }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Player Input Area #}
|
||||
<div class="player-input-area">
|
||||
<form class="player-input-form"
|
||||
hx-post="{{ url_for('game.take_action', session_id=session_id) }}"
|
||||
hx-target="#narrative-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-disabled-elt="find button">
|
||||
<label for="player-action" class="player-input-label">What will you do?</label>
|
||||
<div class="player-input-row">
|
||||
<input type="hidden" name="action_type" value="text">
|
||||
<textarea id="player-action"
|
||||
name="action_text"
|
||||
class="player-input-textarea"
|
||||
placeholder="Describe your action... (e.g., 'I draw my sword and approach the tavern keeper')"
|
||||
rows="2"
|
||||
required></textarea>
|
||||
<button type="submit" class="player-input-submit">
|
||||
<span class="submit-text">Act</span>
|
||||
<span class="submit-icon">→</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="player-input-hint">
|
||||
Press Enter to submit, Shift+Enter for new line
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
127
public_web/templates/game/partials/npc_chat_modal.html
Normal file
127
public_web/templates/game/partials/npc_chat_modal.html
Normal file
@@ -0,0 +1,127 @@
|
||||
{#
|
||||
NPC Chat Modal (Expanded)
|
||||
Shows NPC profile with portrait, relationship meter, and conversation interface
|
||||
#}
|
||||
<div class="modal-overlay" onclick="if(event.target === this) closeModal()">
|
||||
<div class="modal-content modal-content--lg">
|
||||
{# Modal Header #}
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">{{ npc.name }}</h3>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
|
||||
{# Modal Body - Two Column Layout #}
|
||||
<div class="modal-body npc-modal-body">
|
||||
{# Left Column: NPC Profile #}
|
||||
<div class="npc-profile">
|
||||
{# NPC Portrait #}
|
||||
<div class="npc-portrait">
|
||||
{% if npc.image_url %}
|
||||
<img src="{{ npc.image_url }}" alt="{{ npc.name }}">
|
||||
{% else %}
|
||||
<div class="npc-portrait-placeholder">
|
||||
{{ npc.name[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# NPC Info #}
|
||||
<div class="npc-profile-info">
|
||||
<div class="npc-profile-role">{{ npc.role }}</div>
|
||||
{% if npc.appearance %}
|
||||
<div class="npc-profile-appearance">{{ npc.appearance }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# NPC Tags #}
|
||||
{% if npc.tags %}
|
||||
<div class="npc-profile-tags">
|
||||
{% for tag in npc.tags %}
|
||||
<span class="npc-profile-tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Relationship Meter #}
|
||||
<div class="npc-relationship">
|
||||
<div class="relationship-header">
|
||||
<span class="relationship-label">Relationship</span>
|
||||
<span class="relationship-value">{{ relationship_level|default(50) }}/100</span>
|
||||
</div>
|
||||
<div class="relationship-bar">
|
||||
<div class="relationship-fill" style="width: {{ relationship_level|default(50) }}%"></div>
|
||||
</div>
|
||||
<div class="relationship-status">
|
||||
{% set level = relationship_level|default(50) %}
|
||||
{% if level >= 80 %}
|
||||
<span class="status-friendly">Trusted Ally</span>
|
||||
{% elif level >= 60 %}
|
||||
<span class="status-friendly">Friendly</span>
|
||||
{% elif level >= 40 %}
|
||||
<span class="status-neutral">Neutral</span>
|
||||
{% elif level >= 20 %}
|
||||
<span class="status-unfriendly">Wary</span>
|
||||
{% else %}
|
||||
<span class="status-unfriendly">Hostile</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Interaction Stats #}
|
||||
<div class="npc-interaction-stats">
|
||||
<div class="interaction-stat">
|
||||
<span class="stat-label">Conversations</span>
|
||||
<span class="stat-value">{{ interaction_count|default(0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Right Column: Conversation #}
|
||||
<div class="npc-conversation">
|
||||
{# Conversation History #}
|
||||
<div class="chat-history" id="chat-history-{{ npc.npc_id }}">
|
||||
{% if conversation_history %}
|
||||
{% for msg in conversation_history %}
|
||||
<div class="chat-message chat-message--{{ msg.speaker }}">
|
||||
<strong>{{ msg.speaker_name }}:</strong> {{ msg.text }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="chat-empty-state">
|
||||
Start a conversation with {{ npc.name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Chat Input #}
|
||||
<form class="chat-input-form"
|
||||
hx-post="{{ url_for('game.talk_to_npc', session_id=session_id, npc_id=npc.npc_id) }}"
|
||||
hx-target="#chat-history-{{ npc.npc_id }}"
|
||||
hx-swap="beforeend">
|
||||
<input type="text"
|
||||
name="player_response"
|
||||
class="chat-input"
|
||||
placeholder="Say something..."
|
||||
autocomplete="off"
|
||||
autofocus>
|
||||
<button type="submit" class="chat-send-btn">Send</button>
|
||||
<button type="button"
|
||||
class="chat-greet-btn"
|
||||
hx-post="{{ url_for('game.talk_to_npc', session_id=session_id, npc_id=npc.npc_id) }}"
|
||||
hx-vals='{"topic": "greeting"}'
|
||||
hx-target="#chat-history-{{ npc.npc_id }}"
|
||||
hx-swap="beforeend">
|
||||
Greet
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Modal Footer #}
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn--secondary" onclick="closeModal()">
|
||||
End Conversation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,59 @@
|
||||
{#
|
||||
NPC Dialogue Response partial - displays conversation with current exchange.
|
||||
Used when job polling returns NPC dialogue results.
|
||||
|
||||
Expected context:
|
||||
- npc_name: Name of the NPC
|
||||
- character_name: Name of the player character
|
||||
- conversation_history: List of previous exchanges [{player_line, npc_response}, ...]
|
||||
- player_line: What the player just said
|
||||
- dialogue: NPC's current response
|
||||
- session_id: For any follow-up actions
|
||||
#}
|
||||
|
||||
<div class="npc-dialogue-response">
|
||||
<div class="npc-dialogue-header">
|
||||
<span class="npc-dialogue-title">{{ npc_name }} says:</span>
|
||||
</div>
|
||||
|
||||
<div class="npc-dialogue-content">
|
||||
{# Show conversation history if present #}
|
||||
{% if conversation_history %}
|
||||
<div class="conversation-history">
|
||||
{% for exchange in conversation_history[-3:] %}
|
||||
<div class="history-exchange">
|
||||
<div class="history-player">
|
||||
<span class="speaker player">{{ character_name }}:</span>
|
||||
<span class="text">{{ exchange.player_line }}</span>
|
||||
</div>
|
||||
<div class="history-npc">
|
||||
<span class="speaker npc">{{ npc_name }}:</span>
|
||||
<span class="text">{{ exchange.npc_response }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Show current exchange #}
|
||||
<div class="current-exchange">
|
||||
{% if player_line %}
|
||||
<div class="player-message">
|
||||
<span class="speaker player">{{ character_name }}:</span>
|
||||
<span class="text">{{ player_line }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="npc-message">
|
||||
<span class="speaker npc">{{ npc_name }}:</span>
|
||||
<span class="text">{{ dialogue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Trigger sidebar refreshes after NPC dialogue #}
|
||||
<div hx-get="{{ url_for('game.npcs_accordion', session_id=session_id) }}"
|
||||
hx-trigger="load"
|
||||
hx-target="#accordion-npcs"
|
||||
hx-swap="innerHTML"
|
||||
style="display: none;"></div>
|
||||
30
public_web/templates/game/partials/sidebar_history.html
Normal file
30
public_web/templates/game/partials/sidebar_history.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{#
|
||||
History Accordion Content
|
||||
Shows previous turns with actions and DM responses
|
||||
#}
|
||||
{% if history %}
|
||||
<div class="history-list">
|
||||
{% for entry in history %}
|
||||
<div class="history-item">
|
||||
<div class="history-item-header">
|
||||
<span class="history-turn">Turn {{ entry.turn }}</span>
|
||||
</div>
|
||||
<div class="history-action">{{ entry.action }}</div>
|
||||
<div class="history-response">{{ entry.dm_response|truncate(150) }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="history-load-more">
|
||||
<button class="btn-load-more"
|
||||
hx-get="{{ url_for('game.history_accordion', session_id=session_id) }}?offset={{ history|length }}"
|
||||
hx-target="#accordion-history"
|
||||
hx-swap="beforeend">
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="quest-empty">
|
||||
No history yet. Take your first action!
|
||||
</div>
|
||||
{% endif %}
|
||||
51
public_web/templates/game/partials/sidebar_map.html
Normal file
51
public_web/templates/game/partials/sidebar_map.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{#
|
||||
Map Accordion Content
|
||||
Shows discovered locations grouped by region
|
||||
#}
|
||||
{% if discovered_locations %}
|
||||
{# Group locations by region #}
|
||||
{% set regions = {} %}
|
||||
{% for loc in discovered_locations %}
|
||||
{% set region = loc.region %}
|
||||
{% if region not in regions %}
|
||||
{% set _ = regions.update({region: []}) %}
|
||||
{% endif %}
|
||||
{% set _ = regions[region].append(loc) %}
|
||||
{% endfor %}
|
||||
|
||||
{% for region_name, locations in regions.items() %}
|
||||
<div class="map-region">
|
||||
<div class="map-region-name">{{ region_name }}</div>
|
||||
<div class="map-locations">
|
||||
{% for loc in locations %}
|
||||
<div class="map-location {% if loc.is_current %}current{% endif %}"
|
||||
{% if not loc.is_current %}
|
||||
hx-post="{{ url_for('game.do_travel', session_id=session_id) }}"
|
||||
hx-vals='{"location_id": "{{ loc.location_id }}"}'
|
||||
hx-target="#narrative-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Travel to {{ loc.name }}?"
|
||||
{% endif %}>
|
||||
<span class="map-location-icon">
|
||||
{% if loc.location_type == 'town' %}🏘️
|
||||
{% elif loc.location_type == 'tavern' %}🍺
|
||||
{% elif loc.location_type == 'wilderness' %}🌲
|
||||
{% elif loc.location_type == 'dungeon' %}⚔️
|
||||
{% elif loc.location_type == 'ruins' %}🏚️
|
||||
{% else %}📍
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="map-location-name">{{ loc.name }}</span>
|
||||
<span class="map-location-type">
|
||||
{% if loc.is_current %}(here){% else %}{{ loc.location_type }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="quest-empty">
|
||||
No locations discovered yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
29
public_web/templates/game/partials/sidebar_npcs.html
Normal file
29
public_web/templates/game/partials/sidebar_npcs.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{#
|
||||
NPCs Accordion Content
|
||||
Shows NPCs at current location with click to chat
|
||||
#}
|
||||
{% if npcs %}
|
||||
<div class="npc-list">
|
||||
{% for npc in npcs %}
|
||||
<div class="npc-item"
|
||||
hx-get="{{ url_for('game.npc_chat_modal', session_id=session_id, npc_id=npc.npc_id) }}"
|
||||
hx-target="#modal-container"
|
||||
hx-swap="innerHTML">
|
||||
<div class="npc-name">{{ npc.name }}</div>
|
||||
<div class="npc-role">{{ npc.role }}</div>
|
||||
<div class="npc-appearance">{{ npc.appearance }}</div>
|
||||
{% if npc.tags %}
|
||||
<div class="npc-tags">
|
||||
{% for tag in npc.tags %}
|
||||
<span class="npc-tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="quest-empty">
|
||||
No NPCs at this location.
|
||||
</div>
|
||||
{% endif %}
|
||||
36
public_web/templates/game/partials/sidebar_quests.html
Normal file
36
public_web/templates/game/partials/sidebar_quests.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{#
|
||||
Quests Accordion Content
|
||||
Shows active quests with objectives and progress
|
||||
#}
|
||||
{% if quests %}
|
||||
<div class="quest-list">
|
||||
{% for quest in quests %}
|
||||
<div class="quest-item">
|
||||
<div class="quest-header">
|
||||
<span class="quest-name">{{ quest.name }}</span>
|
||||
<span class="quest-difficulty quest-difficulty--{{ quest.difficulty }}">
|
||||
{{ quest.difficulty }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="quest-giver">From: {{ quest.quest_giver }}</div>
|
||||
<div class="quest-objectives">
|
||||
{% for objective in quest.objectives %}
|
||||
<div class="quest-objective">
|
||||
<span class="quest-objective-check {% if objective.completed %}completed{% endif %}">
|
||||
{% if objective.completed %}✓{% endif %}
|
||||
</span>
|
||||
<span class="quest-objective-text">{{ objective.description }}</span>
|
||||
{% if objective.required > 1 %}
|
||||
<span class="quest-objective-progress">{{ objective.current }}/{{ objective.required }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="quest-empty">
|
||||
No active quests. Talk to NPCs to find adventures!
|
||||
</div>
|
||||
{% endif %}
|
||||
51
public_web/templates/game/partials/travel_modal.html
Normal file
51
public_web/templates/game/partials/travel_modal.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{#
|
||||
Travel Modal
|
||||
Shows available destinations for travel
|
||||
#}
|
||||
<div class="modal-overlay" onclick="if(event.target === this) closeModal()">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">🗺️ Travel</h3>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1rem; font-size: var(--text-sm);">
|
||||
Choose your destination:
|
||||
</p>
|
||||
{% if destinations %}
|
||||
<div class="travel-destinations">
|
||||
{% for loc in destinations %}
|
||||
<button class="travel-destination"
|
||||
hx-post="{{ url_for('game.do_travel', session_id=session_id) }}"
|
||||
hx-vals='{"location_id": "{{ loc.location_id }}"}'
|
||||
hx-target="#narrative-content"
|
||||
hx-swap="innerHTML">
|
||||
<div class="travel-destination-name">
|
||||
{% if loc.location_type == 'town' %}🏘️
|
||||
{% elif loc.location_type == 'tavern' %}🍺
|
||||
{% elif loc.location_type == 'wilderness' %}🌲
|
||||
{% elif loc.location_type == 'dungeon' %}⚔️
|
||||
{% elif loc.location_type == 'ruins' %}🏚️
|
||||
{% else %}📍
|
||||
{% endif %}
|
||||
{{ loc.name }}
|
||||
</div>
|
||||
<div class="travel-destination-meta">
|
||||
{{ loc.location_type|capitalize }} • {{ loc.region }}
|
||||
</div>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="quest-empty">
|
||||
No other locations discovered yet. Explore to find new places!
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal()" style="width: auto; padding: 0.5rem 1rem;">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user