first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

View 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>

View 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>

View 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()">&times;</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>

View 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>

View 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>

View 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()">&times;</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>

View File

@@ -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>

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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()">&times;</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>

View File

@@ -0,0 +1,152 @@
{% extends "base.html" %}
{% block title %}Playing - Code of Conquest{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/play.css') }}">
{% endblock %}
{% block content %}
<div class="play-container">
{# ===== LEFT SIDEBAR - Character Panel ===== #}
<aside class="play-panel play-sidebar play-sidebar--left" id="character-panel">
{% include "game/partials/character_panel.html" %}
</aside>
{# ===== MIDDLE - Narrative Panel ===== #}
<section class="play-panel play-main">
{% include "game/partials/narrative_panel.html" %}
</section>
{# ===== RIGHT SIDEBAR - Accordions ===== #}
<aside class="play-panel play-sidebar play-sidebar--right accordion-panel">
{# History Accordion #}
<div class="accordion" data-accordion="history">
<button class="accordion-header" onclick="toggleAccordion(this)">
<span>History <span class="accordion-count">({{ history|length }})</span></span>
<span class="accordion-icon"></span>
</button>
<div class="accordion-content" id="accordion-history">
{% include "game/partials/sidebar_history.html" %}
</div>
</div>
{# Quests Accordion #}
<div class="accordion collapsed" data-accordion="quests">
<button class="accordion-header" onclick="toggleAccordion(this)">
<span>Quests <span class="accordion-count">({{ quests|length }})</span></span>
<span class="accordion-icon"></span>
</button>
<div class="accordion-content" id="accordion-quests">
{% include "game/partials/sidebar_quests.html" %}
</div>
</div>
{# NPCs Accordion #}
<div class="accordion collapsed" data-accordion="npcs">
<button class="accordion-header" onclick="toggleAccordion(this)">
<span>NPCs Here <span class="accordion-count">({{ npcs|length }})</span></span>
<span class="accordion-icon"></span>
</button>
<div class="accordion-content" id="accordion-npcs">
{% include "game/partials/sidebar_npcs.html" %}
</div>
</div>
{# Map Accordion #}
<div class="accordion collapsed" data-accordion="map">
<button class="accordion-header" onclick="toggleAccordion(this)">
<span>Map <span class="accordion-count">({{ discovered_locations|length }})</span></span>
<span class="accordion-icon"></span>
</button>
<div class="accordion-content" id="accordion-map">
{% include "game/partials/sidebar_map.html" %}
</div>
</div>
</aside>
</div>
{# Modal Container #}
<div id="modal-container"></div>
{% endblock %}
{% block scripts %}
<script>
// Accordion Toggle (right sidebar)
function toggleAccordion(button) {
const accordion = button.closest('.accordion');
accordion.classList.toggle('collapsed');
}
// Panel Accordion Toggle (character panel)
function togglePanelAccordion(button) {
const accordion = button.closest('.panel-accordion');
accordion.classList.toggle('collapsed');
}
// Toggle Ambient Details
function toggleAmbient() {
const section = document.querySelector('.ambient-section');
section.classList.toggle('collapsed');
}
// Close Modal
function closeModal() {
const modal = document.querySelector('.modal-overlay');
if (modal) {
modal.remove();
}
}
// Close modal on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeModal();
}
});
// Close modal on overlay click
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal-overlay')) {
closeModal();
}
});
// Clear chat input after submission
document.body.addEventListener('htmx:afterSwap', function(e) {
// Clear chat input if it was a chat form submission
if (e.target.closest('.chat-history')) {
const form = document.querySelector('.chat-input-form');
if (form) {
const input = form.querySelector('.chat-input');
if (input) {
input.value = '';
input.focus();
}
}
}
// Clear player action input after submission
if (e.target.id === 'narrative-content') {
const textarea = document.querySelector('.player-input-textarea');
if (textarea) {
textarea.value = '';
textarea.focus();
}
}
});
// Player input: Enter to submit, Shift+Enter for new line
document.addEventListener('keydown', function(e) {
if (e.target.classList.contains('player-input-textarea')) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
const form = e.target.closest('form');
if (form && e.target.value.trim()) {
htmx.trigger(form, 'submit');
}
}
}
});
</script>
{% endblock %}