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,110 @@
{% extends "base.html" %}
{% block title %}Dev Tools - Code of Conquest{% endblock %}
{% block extra_head %}
<style>
.dev-banner {
background: #dc2626;
color: white;
padding: 0.5rem 1rem;
text-align: center;
font-weight: bold;
position: sticky;
top: 0;
z-index: 100;
}
.dev-container {
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
.dev-section {
background: rgba(30, 30, 40, 0.9);
border: 1px solid #4a4a5a;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.dev-section h2 {
color: #f59e0b;
margin-top: 0;
margin-bottom: 1rem;
border-bottom: 1px solid #4a4a5a;
padding-bottom: 0.5rem;
}
.dev-link {
display: block;
background: #3b82f6;
color: white;
padding: 1rem 1.5rem;
border-radius: 6px;
text-decoration: none;
margin-bottom: 0.5rem;
transition: background 0.2s;
}
.dev-link:hover {
background: #2563eb;
}
.dev-link-disabled {
background: #4a4a5a;
cursor: not-allowed;
opacity: 0.6;
}
.dev-link small {
display: block;
font-size: 0.85rem;
opacity: 0.8;
margin-top: 0.25rem;
}
</style>
{% endblock %}
{% block content %}
<div class="dev-banner">
DEV MODE - Testing Tools (Not available in production)
</div>
<div class="dev-container">
<h1>Development Testing Tools</h1>
<div class="dev-section">
<h2>Story System</h2>
<a href="{{ url_for('dev.story_hub') }}" class="dev-link">
Story Gameplay Tester
<small>Create sessions, test actions, view AI responses</small>
</a>
</div>
<div class="dev-section">
<h2>Quest System</h2>
<span class="dev-link dev-link-disabled">
Quest Tester (Coming Soon)
<small>Test quest offering, acceptance, and completion</small>
</span>
</div>
<div class="dev-section">
<h2>API Debug</h2>
<span class="dev-link dev-link-disabled">
API Inspector (Coming Soon)
<small>View raw API requests and responses</small>
</span>
</div>
<div class="dev-section">
<h2>Quick Links</h2>
<p style="color: #9ca3af; margin: 0;">
<strong>API Docs:</strong> <a href="http://localhost:5000/api/v1/docs" target="_blank" style="color: #60a5fa;">localhost:5000/api/v1/docs</a><br>
<strong>Characters:</strong> <a href="{{ url_for('character_views.list_characters') }}" style="color: #60a5fa;">Character List</a>
</p>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{# DM response after job completes #}
<div class="dm-response-content">
{{ dm_response | safe }}
</div>
{# Debug info #}
<div class="debug-panel" style="margin-top: 1rem; padding: 0.5rem; font-size: 0.7rem;">
<div class="debug-toggle" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">
[+] Raw API Response
</div>
<div style="display: none; margin-top: 0.5rem; white-space: pre; color: #a3e635; max-height: 150px; overflow: auto;">
{{ raw_result | tojson(indent=2) }}
</div>
</div>
{# Trigger state and history refresh #}
<div hx-get="{{ url_for('dev.get_state', session_id=session_id) }}"
hx-trigger="load"
hx-target="#state-content"
hx-swap="innerHTML"
style="display: none;"></div>
<div hx-get="{{ url_for('dev.get_history', session_id=session_id) }}"
hx-trigger="load"
hx-target="#history-content"
hx-swap="innerHTML"
style="display: none;"></div>

View File

@@ -0,0 +1,21 @@
{# History entries partial #}
{% if history %}
{% for entry in history|reverse %}
<div class="history-entry">
<div class="history-turn">Turn {{ entry.turn }}</div>
<div class="history-action">{{ entry.action }}</div>
<div class="history-response">{{ entry.dm_response[:150] }}{% if entry.dm_response|length > 150 %}...{% endif %}</div>
</div>
{% endfor %}
{% if pagination and pagination.has_more %}
<button hx-get="{{ url_for('dev.get_history', session_id=session_id) }}?offset={{ pagination.offset + pagination.limit }}"
hx-target="#history-content"
hx-swap="innerHTML"
style="width: 100%; padding: 0.5rem; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.8rem;">
Load More
</button>
{% endif %}
{% else %}
<p style="color: #9ca3af; text-align: center; font-size: 0.85rem;">No history yet.</p>
{% endif %}

View File

@@ -0,0 +1,29 @@
{# Job status polling partial - polls every 2 seconds until complete #}
<div class="loading"
hx-get="{{ url_for('dev.job_status', job_id=job_id) }}?session_id={{ session_id }}"
hx-trigger="load delay:2s"
hx-swap="outerHTML">
<div style="margin-bottom: 0.5rem;">
<span class="spinner"></span>
Processing your action...
</div>
<div style="font-size: 0.75rem; color: #9ca3af;">
Job: {{ job_id[:8] }}... | Status: {{ status }}
</div>
</div>
<style>
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #4a4a5a;
border-top-color: #60a5fa;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>

View File

@@ -0,0 +1,129 @@
{#
NPC Dialogue partial - displays conversation history with current exchange.
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-conversation">
<div class="npc-conversation-header">
<span class="npc-conversation-title">Conversation with {{ npc_name }}</span>
</div>
<div class="npc-conversation-history">
{# Show previous exchanges #}
{% if conversation_history %}
{% for exchange in conversation_history %}
<div class="dialogue-exchange dialogue-exchange-past">
<div class="dialogue-player">
<span class="dialogue-speaker">{{ character_name }}:</span>
<span class="dialogue-text">{{ exchange.player_line }}</span>
</div>
<div class="dialogue-npc">
<span class="dialogue-speaker">{{ npc_name }}:</span>
<span class="dialogue-text">{{ exchange.npc_response }}</span>
</div>
</div>
{% endfor %}
{% endif %}
{# Show current exchange (highlighted) #}
<div class="dialogue-exchange dialogue-exchange-current">
<div class="dialogue-player">
<span class="dialogue-speaker">{{ character_name }}:</span>
<span class="dialogue-text">{{ player_line }}</span>
</div>
<div class="dialogue-npc">
<span class="dialogue-speaker">{{ npc_name }}:</span>
<span class="dialogue-text">{{ dialogue }}</span>
</div>
</div>
</div>
</div>
<style>
.npc-conversation {
background: #1a1a2a;
border-radius: 6px;
overflow: hidden;
}
.npc-conversation-header {
background: #2a2a3a;
padding: 0.75rem 1rem;
border-bottom: 1px solid #4a4a5a;
}
.npc-conversation-title {
color: #f59e0b;
font-weight: 600;
font-size: 0.95rem;
}
.npc-conversation-history {
padding: 1rem;
max-height: 400px;
overflow-y: auto;
}
.dialogue-exchange {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #3a3a4a;
}
.dialogue-exchange:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.dialogue-exchange-past {
opacity: 0.7;
}
.dialogue-exchange-current {
background: #2a2a3a;
margin: 0 -1rem;
padding: 1rem;
border-radius: 0;
opacity: 1;
}
.dialogue-player {
margin-bottom: 0.5rem;
}
.dialogue-npc {
padding-left: 0.5rem;
border-left: 2px solid #f59e0b;
}
.dialogue-speaker {
font-weight: 600;
margin-right: 0.5rem;
}
.dialogue-player .dialogue-speaker {
color: #60a5fa;
}
.dialogue-npc .dialogue-speaker {
color: #f59e0b;
}
.dialogue-text {
color: #e5e7eb;
line-height: 1.5;
white-space: pre-wrap;
}
.dialogue-exchange-past .dialogue-text {
color: #9ca3af;
}
</style>

View File

@@ -0,0 +1,32 @@
{# Session state partial #}
<div class="state-item">
<div class="state-label">Session ID</div>
<div class="state-value">{{ session_id[:12] }}...</div>
</div>
<div class="state-item">
<div class="state-label">Turn</div>
<div class="state-value">{{ session.turn_number }}</div>
</div>
<div class="state-item">
<div class="state-label">Location</div>
<div class="state-value">{{ session.game_state.current_location }}</div>
</div>
<div class="state-item">
<div class="state-label">Type</div>
<div class="state-value">{{ session.game_state.location_type }}</div>
</div>
<div class="state-item">
<div class="state-label">Active Quests</div>
<div class="state-value">{{ session.game_state.active_quests|length }}</div>
</div>
{% if session.game_state.active_quests %}
<div class="state-item" style="margin-top: 0.5rem;">
<div class="state-label">Quest IDs</div>
<div class="state-value" style="font-size: 0.75rem;">
{% for quest_id in session.game_state.active_quests %}
{{ quest_id[:10] }}...{% if not loop.last %}, {% endif %}
{% endfor %}
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,31 @@
{# Travel Modal Partial - displays available travel destinations #}
<div class="modal-overlay" onclick="this.remove()">
<div class="modal-content" onclick="event.stopPropagation()">
<h3>Travel to...</h3>
{% if locations %}
{% for location in locations %}
<button class="location-btn"
hx-post="{{ url_for('dev.do_travel', session_id=session_id) }}"
hx-vals='{"location_id": "{{ location.location_id }}"}'
hx-target="#dm-response"
hx-swap="innerHTML">
<div class="location-name">{{ location.name }}</div>
<div class="location-type">{{ location.location_type | default('Unknown') }}</div>
{% if location.description %}
<div class="location-desc" style="font-size: 0.75rem; color: #6b7280; margin-top: 0.25rem;">
{{ location.description[:80] }}{% if location.description|length > 80 %}...{% endif %}
</div>
{% endif %}
</button>
{% endfor %}
{% else %}
<p style="color: #9ca3af; text-align: center;">No locations available to travel to.</p>
<p style="color: #6b7280; font-size: 0.85rem; text-align: center;">
Discover new locations by exploring and talking to NPCs.
</p>
{% endif %}
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">Cancel</button>
</div>
</div>

View File

@@ -0,0 +1,199 @@
{% extends "base.html" %}
{% block title %}Story Tester - Dev Tools{% endblock %}
{% block extra_head %}
<style>
.dev-banner {
background: #dc2626;
color: white;
padding: 0.5rem 1rem;
text-align: center;
font-weight: bold;
position: sticky;
top: 0;
z-index: 100;
}
.story-container {
max-width: 900px;
margin: 2rem auto;
padding: 0 1rem;
}
.story-section {
background: rgba(30, 30, 40, 0.9);
border: 1px solid #4a4a5a;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.story-section h2 {
color: #f59e0b;
margin-top: 0;
margin-bottom: 1rem;
border-bottom: 1px solid #4a4a5a;
padding-bottom: 0.5rem;
}
.character-select {
width: 100%;
padding: 0.75rem;
background: #2a2a3a;
border: 1px solid #4a4a5a;
border-radius: 6px;
color: white;
font-size: 1rem;
margin-bottom: 1rem;
}
.btn-create {
background: #10b981;
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
.btn-create:hover {
background: #059669;
}
.btn-create:disabled {
background: #4a4a5a;
cursor: not-allowed;
}
.session-list {
list-style: none;
padding: 0;
margin: 0;
}
.session-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: #2a2a3a;
border-radius: 6px;
margin-bottom: 0.5rem;
}
.session-item a {
color: #60a5fa;
text-decoration: none;
}
.session-item a:hover {
text-decoration: underline;
}
.session-meta {
color: #9ca3af;
font-size: 0.85rem;
}
.error {
background: #7f1d1d;
color: #fecaca;
padding: 1rem;
border-radius: 6px;
margin-bottom: 1rem;
}
.no-characters {
color: #9ca3af;
text-align: center;
padding: 2rem;
}
.no-characters a {
color: #60a5fa;
}
#create-result {
margin-top: 1rem;
}
.success {
background: #065f46;
color: #a7f3d0;
padding: 1rem;
border-radius: 6px;
}
</style>
{% endblock %}
{% block content %}
<div class="dev-banner">
DEV MODE - Story Gameplay Tester
</div>
<div class="story-container">
<h1>Story System Tester</h1>
<p style="color: #9ca3af;"><a href="{{ url_for('dev.index') }}" style="color: #60a5fa;">&larr; Back to Dev Tools</a></p>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<div class="story-section">
<h2>Create New Session</h2>
{% if characters %}
<form hx-post="{{ url_for('dev.create_session') }}"
hx-target="#create-result"
hx-swap="innerHTML">
<select name="character_id" class="character-select" required>
<option value="">-- Select a Character --</option>
{% for char in characters %}
<option value="{{ char.character_id }}">
{{ char.name }} ({{ char.class_name }} Lvl {{ char.level }})
</option>
{% endfor %}
</select>
<button type="submit" class="btn-create">
Create Story Session
</button>
</form>
<div id="create-result"></div>
{% else %}
<div class="no-characters">
<p>No characters found. <a href="{{ url_for('character_views.create_origin') }}">Create a character</a> first.</p>
</div>
{% endif %}
</div>
<div class="story-section">
<h2>Existing Sessions</h2>
{% if sessions %}
<ul class="session-list">
{% for session in sessions %}
<li class="session-item">
<div>
<a href="{{ url_for('dev.story_session', session_id=session.session_id) }}">
Session {{ session.session_id[:8] }}...
</a>
<div class="session-meta">
Turn {{ session.turn_number }} | {{ session.game_state.current_location }}
</div>
</div>
<span class="session-meta">
{{ session.character_id[:8] }}...
</span>
</li>
{% endfor %}
</ul>
{% else %}
<p style="color: #9ca3af; text-align: center;">No active sessions. Create one above.</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,669 @@
{% extends "base.html" %}
{% block title %}Story Session - Dev Tools{% endblock %}
{% block extra_head %}
<style>
.dev-banner {
background: #dc2626;
color: white;
padding: 0.5rem 1rem;
text-align: center;
font-weight: bold;
position: sticky;
top: 0;
z-index: 100;
}
.session-container {
max-width: 1200px;
margin: 1rem auto;
padding: 0 1rem;
display: grid;
grid-template-columns: 250px 1fr 300px;
gap: 1rem;
}
@media (max-width: 1024px) {
.session-container {
grid-template-columns: 1fr;
}
}
.panel {
background: rgba(30, 30, 40, 0.9);
border: 1px solid #4a4a5a;
border-radius: 8px;
padding: 1rem;
}
.panel h3 {
color: #f59e0b;
margin-top: 0;
margin-bottom: 1rem;
font-size: 1rem;
border-bottom: 1px solid #4a4a5a;
padding-bottom: 0.5rem;
}
/* Left sidebar - State */
.state-panel {
font-size: 0.85rem;
}
.state-item {
margin-bottom: 0.75rem;
}
.state-label {
color: #9ca3af;
font-size: 0.75rem;
text-transform: uppercase;
}
.state-value {
color: white;
font-weight: 500;
}
/* Main area */
.main-panel {
min-height: 500px;
}
#dm-response {
background: #1a1a2a;
border-radius: 6px;
padding: 1.5rem;
min-height: 200px;
line-height: 1.6;
white-space: pre-wrap;
margin-bottom: 1rem;
}
.actions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.5rem;
margin-bottom: 1rem;
}
.action-btn {
background: #3b82f6;
color: white;
padding: 0.75rem 1rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
text-align: left;
transition: background 0.2s;
}
.action-btn:hover {
background: #2563eb;
}
.action-btn:disabled {
background: #4a4a5a;
cursor: wait;
}
.action-btn.action-premium {
background: #8b5cf6;
}
.action-btn.action-premium:hover {
background: #7c3aed;
}
.action-btn.action-elite {
background: #f59e0b;
}
.action-btn.action-elite:hover {
background: #d97706;
}
/* Right sidebar - History */
.history-panel {
max-height: 600px;
overflow-y: auto;
}
.history-entry {
padding: 0.75rem;
background: #2a2a3a;
border-radius: 6px;
margin-bottom: 0.5rem;
font-size: 0.85rem;
}
.history-turn {
color: #f59e0b;
font-weight: bold;
margin-bottom: 0.25rem;
}
.history-action {
color: #60a5fa;
margin-bottom: 0.5rem;
}
.history-response {
color: #d1d5db;
white-space: pre-wrap;
max-height: 100px;
overflow: hidden;
text-overflow: ellipsis;
}
/* Debug panel */
.debug-panel {
margin-top: 1rem;
padding: 1rem;
background: #1a1a2a;
border-radius: 6px;
font-family: monospace;
font-size: 0.75rem;
}
.debug-toggle {
color: #9ca3af;
cursor: pointer;
user-select: none;
}
.debug-content {
margin-top: 0.5rem;
max-height: 200px;
overflow: auto;
white-space: pre;
color: #a3e635;
}
.loading {
text-align: center;
padding: 1rem;
color: #60a5fa;
}
.error {
background: #7f1d1d;
color: #fecaca;
padding: 0.75rem;
border-radius: 6px;
}
.btn-refresh {
background: #6366f1;
color: white;
padding: 0.25rem 0.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.75rem;
float: right;
}
/* NPC Sidebar */
.npc-section {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #4a4a5a;
}
.npc-section h4 {
color: #f59e0b;
font-size: 0.85rem;
margin: 0 0 0.5rem 0;
}
.npc-card {
cursor: pointer;
padding: 0.5rem;
margin: 0.25rem 0;
background: #2a2a3a;
border-radius: 4px;
border: 1px solid transparent;
transition: all 0.2s;
}
.npc-card:hover {
background: #3a3a4a;
border-color: #5a5a6a;
}
.npc-name {
font-weight: 500;
color: #e5e7eb;
font-size: 0.9rem;
}
.npc-role {
font-size: 0.75rem;
color: #9ca3af;
}
.npc-empty {
color: #6b7280;
font-size: 0.8rem;
font-style: italic;
}
/* Travel Section */
.travel-section {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #4a4a5a;
}
.btn-travel {
width: 100%;
background: #059669;
color: white;
padding: 0.75rem 1rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s;
}
.btn-travel:hover {
background: #047857;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: #1a1a2a;
border: 1px solid #4a4a5a;
border-radius: 8px;
padding: 1.5rem;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-content h3 {
color: #f59e0b;
margin-top: 0;
margin-bottom: 1rem;
}
.location-btn {
width: 100%;
padding: 0.75rem;
margin: 0.5rem 0;
background: #2a2a3a;
border: 1px solid #4a4a5a;
border-radius: 6px;
color: white;
cursor: pointer;
text-align: left;
transition: all 0.2s;
}
.location-btn:hover {
background: #3a3a4a;
border-color: #5a5a6a;
}
.location-name {
font-weight: 500;
color: #e5e7eb;
}
.location-type {
font-size: 0.8rem;
color: #9ca3af;
}
.modal-close {
margin-top: 1rem;
padding: 0.5rem 1rem;
background: #6b7280;
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
}
.modal-close:hover {
background: #4b5563;
}
/* NPC Dialogue Result */
.npc-dialogue {
background: #2a2a3a;
border-left: 3px solid #f59e0b;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 0 6px 6px 0;
}
.npc-dialogue-header {
color: #f59e0b;
font-weight: 500;
margin-bottom: 0.5rem;
}
/* NPC Chat Form Styles */
.npc-card-wrapper {
margin: 0.25rem 0;
background: #2a2a3a;
border-radius: 4px;
border: 1px solid transparent;
transition: all 0.2s;
}
.npc-card-wrapper:hover {
background: #3a3a4a;
border-color: #5a5a6a;
}
.npc-card-header {
cursor: pointer;
padding: 0.5rem;
}
.npc-chat-form {
padding: 0.5rem;
padding-top: 0;
border-top: 1px solid #4a4a5a;
}
.npc-chat-input {
width: 100%;
padding: 0.5rem;
margin-bottom: 0.5rem;
background: #1a1a2a;
border: 1px solid #4a4a5a;
border-radius: 4px;
color: #e5e7eb;
font-size: 0.85rem;
}
.npc-chat-input:focus {
outline: none;
border-color: #f59e0b;
}
.npc-chat-input::placeholder {
color: #6b7280;
}
.npc-chat-buttons {
display: flex;
gap: 0.5rem;
}
.btn-npc-send {
flex: 1;
padding: 0.4rem 0.75rem;
background: #f59e0b;
border: none;
border-radius: 4px;
color: #1a1a2a;
font-weight: 500;
font-size: 0.8rem;
cursor: pointer;
transition: background 0.2s;
}
.btn-npc-send:hover {
background: #d97706;
}
.btn-npc-send:disabled {
background: #4a4a5a;
color: #9ca3af;
cursor: wait;
}
.btn-npc-greet {
padding: 0.4rem 0.75rem;
background: #3b3b4b;
border: 1px solid #5a5a6a;
border-radius: 4px;
color: #e5e7eb;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-npc-greet:hover {
background: #4b4b5b;
border-color: #6a6a7a;
}
</style>
{% endblock %}
{% block content %}
<div class="dev-banner">
DEV MODE - Session {{ session_id[:8] }}...
</div>
<div class="session-container">
<!-- Left sidebar: State -->
<div class="panel state-panel">
<h3>
Session State
<button class="btn-refresh"
hx-get="{{ url_for('dev.get_state', session_id=session_id) }}"
hx-target="#state-content"
hx-swap="innerHTML">
Refresh
</button>
</h3>
<div id="state-content">
<div class="state-item">
<div class="state-label">Session ID</div>
<div class="state-value">{{ session_id[:12] }}...</div>
</div>
<div class="state-item">
<div class="state-label">Turn</div>
<div class="state-value">{{ session.turn_number }}</div>
</div>
<div class="state-item">
<div class="state-label">Location</div>
<div class="state-value">{{ session.game_state.current_location }}</div>
</div>
<div class="state-item">
<div class="state-label">Type</div>
<div class="state-value">{{ session.game_state.location_type }}</div>
</div>
<div class="state-item">
<div class="state-label">Active Quests</div>
<div class="state-value">{{ session.game_state.active_quests|length }}</div>
</div>
</div>
<!-- NPC Section -->
<div class="npc-section">
<h4>NPCs Here</h4>
<div id="npc-list">
{% if npcs_present %}
{% for npc in npcs_present %}
<div class="npc-card-wrapper">
<div class="npc-card-header"
onclick="toggleNpcChat('{{ npc.npc_id }}')">
<div class="npc-name">{{ npc.name }}</div>
<div class="npc-role">{{ npc.role }}</div>
</div>
<div class="npc-chat-form" id="npc-chat-{{ npc.npc_id }}" style="display: none;">
<form hx-post="{{ url_for('dev.talk_to_npc', session_id=session_id, npc_id=npc.npc_id) }}"
hx-target="#dm-response"
hx-swap="innerHTML">
<input type="text"
name="player_response"
placeholder="Say something..."
class="npc-chat-input"
maxlength="500"
autocomplete="off">
<div class="npc-chat-buttons">
<button type="submit" class="btn-npc-send" hx-disabled-elt="this">Send</button>
<button type="button"
class="btn-npc-greet"
hx-post="{{ url_for('dev.talk_to_npc', session_id=session_id, npc_id=npc.npc_id) }}"
hx-vals='{"topic": "greeting"}'
hx-target="#dm-response"
hx-swap="innerHTML"
hx-disabled-elt="this">Greet</button>
</div>
</form>
</div>
</div>
{% endfor %}
{% else %}
<p class="npc-empty">No one here to talk to.</p>
{% endif %}
</div>
</div>
<!-- Travel Section -->
<div class="travel-section">
<button class="btn-travel"
hx-get="{{ url_for('dev.travel_modal', session_id=session_id) }}"
hx-target="#modal-container"
hx-swap="innerHTML">
Travel to...
</button>
</div>
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #4a4a5a;">
<a href="{{ url_for('dev.story_hub') }}" style="color: #60a5fa; font-size: 0.85rem;">&larr; Back to Sessions</a>
</div>
</div>
<!-- Main area: DM Response & Actions -->
<div class="panel main-panel">
<h3>Story Gameplay</h3>
<!-- DM Response Area -->
<div id="dm-response">
{% if history %}
{{ history[-1].dm_response if history else 'Take an action to begin your adventure...' }}
{% else %}
Take an action to begin your adventure...
{% endif %}
</div>
<!-- Action Buttons -->
<div class="actions-grid" id="action-buttons">
{% if session.available_actions %}
{% for action in session.available_actions %}
<button class="action-btn {% if action.category == 'special' %}action-elite{% elif action.category in ['gather_info', 'travel'] and action.prompt_id in ['investigate_suspicious', 'follow_lead', 'make_camp'] %}action-premium{% endif %}"
hx-post="{{ url_for('dev.take_action', session_id=session_id) }}"
hx-vals='{"action_type": "button", "prompt_id": "{{ action.prompt_id }}"}'
hx-target="#dm-response"
hx-swap="innerHTML"
hx-disabled-elt="this"
title="{{ action.description }}">
{{ action.display_text }}
</button>
{% endfor %}
{% else %}
<p style="color: #9ca3af; text-align: center; grid-column: 1 / -1;">
No actions available for your tier/location. Try changing location or upgrading tier.
</p>
{% endif %}
</div>
<!-- Debug Panel -->
<div class="debug-panel">
<div class="debug-toggle" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">
[+] Debug Info (click to toggle)
</div>
<div class="debug-content" style="display: none;">
Session ID: {{ session_id }}
Character ID: {{ session.character_id }}
Turn: {{ session.turn_number }}
Game State: {{ session.game_state | tojson }}
</div>
</div>
</div>
<!-- Right sidebar: History -->
<div class="panel history-panel">
<h3>
History
<button class="btn-refresh"
hx-get="{{ url_for('dev.get_history', session_id=session_id) }}"
hx-target="#history-content"
hx-swap="innerHTML">
Refresh
</button>
</h3>
<div id="history-content">
{% if history %}
{% for entry in history|reverse %}
<div class="history-entry">
<div class="history-turn">Turn {{ entry.turn }}</div>
<div class="history-action">{{ entry.action }}</div>
<div class="history-response">{{ entry.dm_response[:150] }}{% if entry.dm_response|length > 150 %}...{% endif %}</div>
</div>
{% endfor %}
{% else %}
<p style="color: #9ca3af; text-align: center; font-size: 0.85rem;">No history yet.</p>
{% endif %}
</div>
</div>
</div>
<!-- Modal Container for Travel/NPC dialogs -->
<div id="modal-container"></div>
<script>
// Toggle NPC chat form visibility
function toggleNpcChat(npcId) {
const chatForm = document.getElementById('npc-chat-' + npcId);
if (!chatForm) return;
// Close all other NPC chat forms
document.querySelectorAll('.npc-chat-form').forEach(form => {
if (form.id !== 'npc-chat-' + npcId) {
form.style.display = 'none';
}
});
// Toggle the clicked NPC's chat form
if (chatForm.style.display === 'none') {
chatForm.style.display = 'block';
// Focus the input field
const input = chatForm.querySelector('.npc-chat-input');
if (input) {
input.focus();
}
} else {
chatForm.style.display = 'none';
}
}
// Clear input after successful form submission
document.body.addEventListener('htmx:afterRequest', function(event) {
// Check if this was an NPC chat form submission
const form = event.detail.elt.closest('.npc-chat-form form');
if (form && event.detail.successful) {
const input = form.querySelector('.npc-chat-input');
if (input) {
input.value = '';
}
}
});
</script>
{% endblock %}