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,93 @@
{% extends "base.html" %}
{% block title %}Forgot Password - Code of Conquest{% endblock %}
{% block content %}
<div class="auth-container">
<h1 class="page-title">Forgot Password</h1>
<p class="page-subtitle">Recover your account access</p>
<div class="decorative-line"></div>
<!-- Message display area -->
<div id="forgot-password-messages"></div>
<form
id="forgot-password-form"
hx-post="{{ api_base_url }}/api/v1/auth/forgot-password"
hx-ext="json-enc"
hx-target="#forgot-password-messages"
hx-swap="innerHTML"
>
<div class="form-group">
<label class="form-label" for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="adventurer@realm.com"
required
autocomplete="email"
>
<span id="email-error" class="field-error"></span>
</div>
<button type="submit" class="btn btn-primary">
Send Reset Link
<span class="htmx-indicator loading-spinner"></span>
</button>
<button type="button" class="btn btn-secondary" onclick="window.location.href='{{ url_for('auth_views.login') }}'">
Back to Login
</button>
</form>
<div class="form-links">
<div class="divider">or</div>
<a href="{{ url_for('auth_views.register') }}" class="form-link">Don't have an account? Register here</a>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Handle form submission response
document.getElementById('forgot-password-form').addEventListener('htmx:afterSwap', function(event) {
const response = event.detail.xhr.responseText;
try {
const data = JSON.parse(response);
// Check if request was successful
if (data.status === 200) {
// Show success message
document.getElementById('forgot-password-messages').innerHTML =
'<div class="success-message">' +
'If an account exists with this email, you will receive a password reset link shortly. ' +
'Please check your inbox and spam folder.' +
'</div>';
// Clear form
document.getElementById('forgot-password-form').reset();
} else {
// Display error message
if (data.error && data.error.details && data.error.details.email) {
document.getElementById('email-error').textContent = data.error.details.email;
} else if (data.error && data.error.message) {
document.getElementById('forgot-password-messages').innerHTML =
'<div class="error-message">' + data.error.message + '</div>';
}
}
} catch (e) {
console.error('Error parsing response:', e);
}
});
// Clear errors when user starts typing
document.getElementById('email').addEventListener('input', function() {
document.getElementById('email-error').textContent = '';
document.getElementById('forgot-password-messages').innerHTML = '';
});
</script>
{% endblock %}

View File

@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Login - Code of Conquest{% endblock %}
{% block content %}
<div class="auth-container">
<h1 class="page-title">Login</h1>
<p class="page-subtitle">Enter the realm, brave adventurer</p>
<div class="decorative-line"></div>
<!-- Error display area -->
{% if error %}
<div class="error-message">{{ error }}</div>
{% endif %}
<form
id="login-form"
method="POST"
action="{{ url_for('auth_views.login') }}"
>
<div class="form-group">
<label class="form-label" for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="adventurer@realm.com"
required
autocomplete="email"
>
</div>
<div class="form-group">
<label class="form-label" for="password">Password</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="Enter your secret passphrase"
required
autocomplete="current-password"
>
</div>
<div class="checkbox-group">
<input type="checkbox" id="remember_me" name="remember_me" class="checkbox-input">
<label for="remember_me" class="checkbox-label">Remember me for 30 days</label>
</div>
<button type="submit" class="btn btn-primary">
Enter the Realm
<span class="htmx-indicator loading-spinner"></span>
</button>
</form>
<div class="form-links">
<a href="{{ url_for('auth_views.forgot_password') }}" class="form-link">Forgot your password?</a>
<div class="divider">or</div>
<a href="{{ url_for('auth_views.register') }}" class="form-link">Don't have an account? Register here</a>
</div>
</div>
{% endblock %}
{% block scripts %}
{% endblock %}

View File

@@ -0,0 +1,230 @@
{% extends "base.html" %}
{% block title %}Register - Code of Conquest{% endblock %}
{% block content %}
<div class="auth-container">
<h1 class="page-title">Register</h1>
<p class="page-subtitle">Begin your epic journey</p>
<div class="decorative-line"></div>
<!-- Error display area for HTMX responses -->
<div id="register-errors"></div>
<form
id="register-form"
hx-post="{{ api_base_url }}/api/v1/auth/register"
hx-ext="json-enc"
hx-target="#register-errors"
hx-swap="innerHTML"
>
<div class="form-group">
<label class="form-label" for="name">Character Name</label>
<input
type="text"
id="name"
name="name"
class="form-input"
placeholder="Enter your hero's name"
required
minlength="3"
maxlength="50"
autocomplete="name"
>
<span id="name-error" class="field-error"></span>
</div>
<div class="form-group">
<label class="form-label" for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="adventurer@realm.com"
required
autocomplete="email"
>
<span id="email-error" class="field-error"></span>
</div>
<div class="form-group">
<label class="form-label" for="password">Password</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="Create a strong passphrase"
required
autocomplete="new-password"
>
<div class="password-strength">
<div class="strength-bar">
<div id="strength-fill" class="strength-fill"></div>
</div>
<span id="strength-text" class="strength-text">Enter a password to see strength</span>
</div>
<span id="password-error" class="field-error"></span>
</div>
<div class="form-group">
<label class="form-label" for="confirm-password">Confirm Password</label>
<input
type="password"
id="confirm-password"
name="confirm_password"
class="form-input"
placeholder="Re-enter your passphrase"
required
autocomplete="new-password"
>
<span id="confirm-password-error" class="field-error"></span>
</div>
<button type="submit" class="btn btn-primary">
Begin Adventure
<span class="htmx-indicator loading-spinner"></span>
</button>
</form>
<div class="form-links">
<a href="{{ url_for('auth_views.login') }}" class="form-link">Already have an account? Login here</a>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Password strength indicator
function updatePasswordStrength(password) {
const fill = document.getElementById('strength-fill');
const text = document.getElementById('strength-text');
if (!password) {
fill.className = 'strength-fill';
text.textContent = 'Enter a password to see strength';
text.style.color = 'var(--text-muted)';
return;
}
let strength = 0;
// Check length
if (password.length >= 8) strength++;
if (password.length >= 12) strength++;
// Check for uppercase
if (/[A-Z]/.test(password)) strength++;
// Check for lowercase
if (/[a-z]/.test(password)) strength++;
// Check for numbers
if (/[0-9]/.test(password)) strength++;
// Check for special characters
if (/[^A-Za-z0-9]/.test(password)) strength++;
if (strength <= 2) {
fill.className = 'strength-fill strength-weak';
text.textContent = 'Weak - Add more complexity';
text.style.color = 'var(--accent-red)';
} else if (strength <= 4) {
fill.className = 'strength-fill strength-medium';
text.textContent = 'Medium - Almost there';
text.style.color = 'var(--accent-gold)';
} else {
fill.className = 'strength-fill strength-strong';
text.textContent = 'Strong - Excellent password';
text.style.color = 'var(--accent-green)';
}
}
// Attach password strength checker
document.getElementById('password').addEventListener('input', function() {
updatePasswordStrength(this.value);
});
// Validate password confirmation
document.getElementById('confirm-password').addEventListener('input', function() {
const password = document.getElementById('password').value;
const confirmPassword = this.value;
const errorSpan = document.getElementById('confirm-password-error');
if (confirmPassword && password !== confirmPassword) {
errorSpan.textContent = 'Passwords do not match';
} else {
errorSpan.textContent = '';
}
});
// Handle HTMX response
document.body.addEventListener('htmx:afterRequest', function(event) {
// Only handle register form
if (!event.detail.elt || event.detail.elt.id !== 'register-form') return;
try {
const xhr = event.detail.xhr;
// Check if registration was successful
if (xhr.status === 201) {
const data = JSON.parse(xhr.responseText);
// Show success message
document.getElementById('register-errors').innerHTML = '<div class="success-message">' +
'Registration successful! Please check your email to verify your account.' +
'</div>';
// Clear form
document.getElementById('register-form').reset();
updatePasswordStrength('');
// Redirect to login after delay
setTimeout(function() {
window.location.href = '{{ url_for("auth_views.login") }}';
}, 2000);
} else {
// Handle error
const data = JSON.parse(xhr.responseText);
let errorHtml = '<div class="error-message">';
if (data.error && data.error.details) {
for (const [field, message] of Object.entries(data.error.details)) {
errorHtml += `<strong>${field}:</strong> ${message}<br>`;
// Also show inline error
const errorSpan = document.getElementById(field + '-error');
if (errorSpan) {
errorSpan.textContent = message;
}
}
} else if (data.error && data.error.message) {
errorHtml += data.error.message;
} else {
errorHtml += 'An error occurred. Please try again.';
}
errorHtml += '</div>';
document.getElementById('register-errors').innerHTML = errorHtml;
}
} catch (e) {
console.error('Error parsing response:', e);
document.getElementById('register-errors').innerHTML =
'<div class="error-message">An unexpected error occurred.</div>';
}
});
// Clear errors when user starts typing
document.querySelectorAll('.form-input').forEach(input => {
input.addEventListener('input', function() {
const fieldName = this.name;
const errorSpan = document.getElementById(fieldName + '-error');
if (errorSpan) {
errorSpan.textContent = '';
}
// Clear general errors
document.getElementById('register-errors').innerHTML = '';
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,203 @@
{% extends "base.html" %}
{% block title %}Reset Password - Code of Conquest{% endblock %}
{% block content %}
<div class="auth-container">
<h1 class="page-title">Reset Password</h1>
<p class="page-subtitle">Create a new password for your account</p>
<div class="decorative-line"></div>
<!-- Message display area -->
<div id="reset-password-messages"></div>
<form
id="reset-password-form"
hx-post="{{ api_base_url }}/api/v1/auth/reset-password"
hx-ext="json-enc"
hx-target="#reset-password-messages"
hx-swap="innerHTML"
>
<!-- Hidden fields for user_id and secret from URL -->
<input type="hidden" name="user_id" value="{{ user_id }}">
<input type="hidden" name="secret" value="{{ secret }}">
<div class="form-group">
<label class="form-label" for="password">New Password</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="Create a strong passphrase"
required
autocomplete="new-password"
>
<div class="password-strength">
<div class="strength-bar">
<div id="strength-fill" class="strength-fill"></div>
</div>
<span id="strength-text" class="strength-text">Enter a password to see strength</span>
</div>
<span id="password-error" class="field-error"></span>
</div>
<div class="form-group">
<label class="form-label" for="confirm-password">Confirm New Password</label>
<input
type="password"
id="confirm-password"
name="confirm_password"
class="form-input"
placeholder="Re-enter your passphrase"
required
autocomplete="new-password"
>
<span id="confirm-password-error" class="field-error"></span>
</div>
<button type="submit" class="btn btn-primary">
Reset Password
<span class="htmx-indicator loading-spinner"></span>
</button>
<button type="button" class="btn btn-secondary" onclick="window.location.href='{{ url_for('auth_views.login') }}'">
Back to Login
</button>
</form>
</div>
{% endblock %}
{% block scripts %}
<script>
// Password strength indicator
function updatePasswordStrength(password) {
const fill = document.getElementById('strength-fill');
const text = document.getElementById('strength-text');
if (!password) {
fill.className = 'strength-fill';
text.textContent = 'Enter a password to see strength';
text.style.color = 'var(--text-muted)';
return;
}
let strength = 0;
// Check length
if (password.length >= 8) strength++;
if (password.length >= 12) strength++;
// Check for uppercase
if (/[A-Z]/.test(password)) strength++;
// Check for lowercase
if (/[a-z]/.test(password)) strength++;
// Check for numbers
if (/[0-9]/.test(password)) strength++;
// Check for special characters
if (/[^A-Za-z0-9]/.test(password)) strength++;
if (strength <= 2) {
fill.className = 'strength-fill strength-weak';
text.textContent = 'Weak - Add more complexity';
text.style.color = 'var(--accent-red)';
} else if (strength <= 4) {
fill.className = 'strength-fill strength-medium';
text.textContent = 'Medium - Almost there';
text.style.color = 'var(--accent-gold)';
} else {
fill.className = 'strength-fill strength-strong';
text.textContent = 'Strong - Excellent password';
text.style.color = 'var(--accent-green)';
}
}
// Attach password strength checker
document.getElementById('password').addEventListener('input', function() {
updatePasswordStrength(this.value);
});
// Validate password confirmation
document.getElementById('confirm-password').addEventListener('input', function() {
const password = document.getElementById('password').value;
const confirmPassword = this.value;
const errorSpan = document.getElementById('confirm-password-error');
if (confirmPassword && password !== confirmPassword) {
errorSpan.textContent = 'Passwords do not match';
} else {
errorSpan.textContent = '';
}
});
// Validate before submit
document.getElementById('reset-password-form').addEventListener('submit', function(e) {
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirm-password').value;
if (password !== confirmPassword) {
e.preventDefault();
document.getElementById('confirm-password-error').textContent = 'Passwords do not match';
return false;
}
});
// Handle form submission response
document.getElementById('reset-password-form').addEventListener('htmx:afterSwap', function(event) {
const response = event.detail.xhr.responseText;
try {
const data = JSON.parse(response);
// Check if reset was successful
if (data.status === 200) {
// Show success message
document.getElementById('reset-password-messages').innerHTML =
'<div class="success-message">' +
'Password reset successful! Redirecting to login...' +
'</div>';
// Redirect to login after delay
setTimeout(function() {
window.location.href = '{{ url_for("auth_views.login") }}';
}, 2000);
} else {
// Display error message
if (data.error && data.error.details) {
let errorHtml = '<div class="error-message">';
for (const [field, message] of Object.entries(data.error.details)) {
errorHtml += `<strong>${field}:</strong> ${message}<br>`;
const errorSpan = document.getElementById(field + '-error');
if (errorSpan) {
errorSpan.textContent = message;
}
}
errorHtml += '</div>';
document.getElementById('reset-password-messages').innerHTML = errorHtml;
} else if (data.error && data.error.message) {
document.getElementById('reset-password-messages').innerHTML =
'<div class="error-message">' + data.error.message + '</div>';
}
}
} catch (e) {
console.error('Error parsing response:', e);
}
});
// Clear errors when user starts typing
document.querySelectorAll('.form-input').forEach(input => {
input.addEventListener('input', function() {
const fieldName = this.name;
const errorSpan = document.getElementById(fieldName + '-error');
if (errorSpan) {
errorSpan.textContent = '';
}
document.getElementById('reset-password-messages').innerHTML = '';
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block title %}Verify Email - Code of Conquest{% endblock %}
{% block content %}
<div class="auth-container">
<h1 class="page-title">Email Verification</h1>
<p class="page-subtitle">Confirming your email address</p>
<div class="decorative-line"></div>
<div class="text-center">
<div class="success-message">
Your email has been verified successfully!
</div>
<p class="mt-2 mb-2" style="color: var(--text-secondary);">
You can now log in to your account and begin your adventure.
</p>
<button class="btn btn-primary" onclick="window.location.href='{{ url_for('auth_views.login') }}'">
Go to Login
</button>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Code of Conquest - An AI-powered Dungeons & Dragons adventure game">
<title>{% block title %}Code of Conquest{% endblock %}</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Lato:wght@300;400;700&display=swap" rel="stylesheet">
<!-- Main CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
<!-- HTMX for dynamic interactions -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- HTMX JSON encoding extension -->
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/json-enc.js"></script>
{% block extra_head %}{% endblock %}
</head>
<body>
<!-- Header -->
<header class="header">
<div class="header-content">
<a href="/" class="logo">⚔️ Code of Conquest</a>
{% if current_user %}
<nav class="header-nav">
<span class="user-greeting">Welcome, {{ current_user.name or current_user.email }}!</span>
<form method="POST" action="{{ url_for('auth_views.logout') }}" class="logout-form">
<button type="submit" class="btn-link">Logout</button>
</form>
</nav>
{% endif %}
</div>
</header>
<!-- Main Content -->
<main>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages-container">
{% for category, message in messages %}
<div class="flash-message flash-{{ category }}">
{{ message }}
<button class="flash-close" onclick="this.parentElement.remove()">&times;</button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- Page Content -->
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="footer">
<p class="footer-text">
&copy; 2025 Code of Conquest. All rights reserved. | May your adventures be legendary.
</p>
</footer>
<!-- JavaScript -->
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,364 @@
{% extends "base.html" %}
{% block title %}Choose Your Class - Code of Conquest{% endblock %}
{% block content %}
<div class="creation-container">
<!-- Progress Indicator -->
<div class="creation-progress">
<div class="progress-step completed">
<div class="step-number"></div>
<div class="step-label">Origin</div>
</div>
<div class="progress-line"></div>
<div class="progress-step active">
<div class="step-number">2</div>
<div class="step-label">Class</div>
</div>
<div class="progress-line"></div>
<div class="progress-step">
<div class="step-number">3</div>
<div class="step-label">Customize</div>
</div>
<div class="progress-line"></div>
<div class="progress-step">
<div class="step-number">4</div>
<div class="step-label">Confirm</div>
</div>
</div>
<!-- Page Header -->
<h1 class="page-title">Choose Your Class</h1>
<p class="page-subtitle">Select your fighting style and role in combat</p>
<div class="decorative-line"></div>
<!-- Class Selection Form -->
<form method="POST" action="{{ url_for('character_views.create_class') }}" id="class-form">
<div class="class-grid">
{% for player_class in classes %}
<div class="class-card" data-class-id="{{ player_class.class_id }}">
<input
type="radio"
name="class_id"
value="{{ player_class.class_id }}"
id="class-{{ player_class.class_id }}"
class="class-radio"
required
>
<label for="class-{{ player_class.class_id }}" class="class-label">
<div class="class-header">
<h3 class="class-title">{{ player_class.name }}</h3>
</div>
<div class="class-description">
<p>{{ player_class.description }}</p>
</div>
<!-- Base Stats -->
<div class="class-stats">
<div class="stats-label">Base Stats</div>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-name">STR</span>
<span class="stat-value">{{ player_class.base_stats.strength }}</span>
</div>
<div class="stat-item">
<span class="stat-name">DEX</span>
<span class="stat-value">{{ player_class.base_stats.dexterity }}</span>
</div>
<div class="stat-item">
<span class="stat-name">CON</span>
<span class="stat-value">{{ player_class.base_stats.constitution }}</span>
</div>
<div class="stat-item">
<span class="stat-name">INT</span>
<span class="stat-value">{{ player_class.base_stats.intelligence }}</span>
</div>
<div class="stat-item">
<span class="stat-name">WIS</span>
<span class="stat-value">{{ player_class.base_stats.wisdom }}</span>
</div>
<div class="stat-item">
<span class="stat-name">CHA</span>
<span class="stat-value">{{ player_class.base_stats.charisma }}</span>
</div>
</div>
</div>
<!-- Skill Trees -->
<div class="class-trees">
<div class="trees-label">Available Specializations</div>
<p class="trees-hint">You'll choose your path as you level up</p>
<div class="tree-list">
{% for tree in player_class.skill_trees %}
<div class="tree-item">
<span class="tree-icon">🌲</span>
<span class="tree-name">{{ tree }}</span>
</div>
{% endfor %}
</div>
</div>
</label>
</div>
{% endfor %}
</div>
<!-- Navigation Buttons -->
<div class="creation-nav">
<a href="{{ url_for('character_views.create_origin') }}" class="btn btn-secondary">
← Back to Origin
</a>
<button type="submit" class="btn btn-primary">
Next: Customize →
</button>
</div>
</form>
</div>
<style>
/* ===== PROGRESS INDICATOR ===== */
.creation-progress {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 2rem;
padding: 1.5rem 0;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
min-width: 80px;
}
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
color: var(--text-muted);
font-family: var(--font-heading);
font-weight: 600;
font-size: var(--text-sm);
}
.progress-step.completed .step-number {
background: var(--accent-gold);
border-color: var(--accent-gold);
color: var(--bg-primary);
}
.progress-step.active .step-number {
background: var(--bg-primary);
border-color: var(--accent-gold);
color: var(--accent-gold);
box-shadow: 0 0 10px rgba(243, 156, 18, 0.3);
}
.step-label {
font-size: var(--text-xs);
color: var(--text-muted);
text-transform: uppercase;
font-weight: 600;
text-align: center;
}
.progress-step.active .step-label {
color: var(--accent-gold);
}
.progress-step.completed .step-label {
color: var(--text-secondary);
}
.progress-line {
width: 60px;
height: 2px;
background: var(--border-primary);
margin: 0 0.5rem;
margin-bottom: 1.5rem;
}
.progress-step.completed ~ .progress-line {
background: var(--accent-gold);
}
/* ===== CLASS GRID ===== */
.class-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
/* ===== CLASS CARDS ===== */
.class-card {
position: relative;
}
.class-radio {
position: absolute;
opacity: 0;
pointer-events: none;
}
.class-label {
display: block;
padding: 1.5rem;
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
height: 100%;
}
.class-label:hover {
border-color: var(--accent-gold);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.class-radio:checked + .class-label {
border-color: var(--accent-gold);
box-shadow: var(--shadow-glow);
background: linear-gradient(135deg, var(--bg-secondary) 0%, rgba(243, 156, 18, 0.1) 100%);
}
.class-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-primary);
}
.class-title {
font-family: var(--font-heading);
font-size: var(--text-xl);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 0.25rem;
}
.class-description {
margin-bottom: 1rem;
color: var(--text-secondary);
font-size: var(--text-sm);
line-height: 1.5;
}
/* ===== STATS SECTION ===== */
.class-stats {
margin-bottom: 1rem;
padding: 1rem;
background: var(--bg-input);
border-radius: 4px;
}
.stats-label {
font-family: var(--font-heading);
font-size: var(--text-sm);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 0.75rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem;
background: var(--bg-secondary);
border-radius: 4px;
border: 1px solid var(--border-primary);
}
.stat-name {
font-size: var(--text-xs);
color: var(--text-muted);
text-transform: uppercase;
font-weight: 600;
margin-bottom: 0.25rem;
}
.stat-value {
font-family: var(--font-heading);
font-size: var(--text-lg);
color: var(--text-primary);
font-weight: 600;
}
/* ===== SKILL TREES SECTION ===== */
.class-trees {
margin-bottom: 1rem;
}
.trees-label {
font-family: var(--font-heading);
font-size: var(--text-sm);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 0.25rem;
}
.trees-hint {
font-size: var(--text-xs);
color: var(--text-muted);
font-style: italic;
margin-bottom: 0.5rem;
margin-top: 0;
}
.tree-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.tree-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--bg-input);
border-radius: 4px;
color: var(--text-secondary);
font-size: var(--text-sm);
}
.tree-icon {
font-size: var(--text-base);
}
.tree-name {
font-weight: 500;
}
/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
.class-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,597 @@
{% extends "base.html" %}
{% block title %}Confirm Your Character - Code of Conquest{% endblock %}
{% block content %}
<div class="creation-container">
<!-- Progress Indicator -->
<div class="creation-progress">
<div class="progress-step completed">
<div class="step-number"></div>
<div class="step-label">Origin</div>
</div>
<div class="progress-line"></div>
<div class="progress-step completed">
<div class="step-number"></div>
<div class="step-label">Class</div>
</div>
<div class="progress-line"></div>
<div class="progress-step completed">
<div class="step-number"></div>
<div class="step-label">Customize</div>
</div>
<div class="progress-line"></div>
<div class="progress-step active">
<div class="step-number">4</div>
<div class="step-label">Confirm</div>
</div>
</div>
<!-- Page Header -->
<h1 class="page-title">Your Hero Awaits</h1>
<p class="page-subtitle">Review your character and begin your adventure</p>
<div class="decorative-line"></div>
<!-- Character Summary Card -->
<div class="confirm-card">
<!-- Character Header -->
<div class="character-header">
<div class="character-name-display">
<span class="name-label">Hero Name:</span>
<h2 class="character-name">{{ character_name }}</h2>
</div>
<div class="character-class-origin">
<span class="class-badge">{{ player_class.name }}</span>
<span class="origin-badge">{{ origin.name }}</span>
</div>
</div>
<div class="decorative-line"></div>
<!-- Two Column Layout -->
<div class="confirm-content">
<!-- Left Column: Character Details -->
<div class="details-column">
<!-- Origin Story -->
<div class="detail-section">
<h3 class="section-title">Your Origin</h3>
<p class="origin-story">{{ origin.description }}</p>
</div>
<!-- Starting Location -->
<div class="detail-section">
<h3 class="section-title">Starting Location</h3>
<div class="location-info">
<div class="location-name">{{ origin.starting_location.name }}</div>
<p class="location-description">{{ origin.starting_location.description }}</p>
</div>
</div>
<!-- Starting Bonus -->
{% if origin.starting_bonus %}
<div class="detail-section">
<h3 class="section-title">Starting Bonus</h3>
<div class="bonus-item">
<span class="bonus-icon"></span>
<div>
<div class="bonus-text"><strong>{{ origin.starting_bonus.trait }}</strong></div>
<div class="bonus-description">{{ origin.starting_bonus.description }}</div>
<div class="bonus-effect">Effect: {{ origin.starting_bonus.effect }}</div>
</div>
</div>
</div>
{% endif %}
</div>
<!-- Right Column: Class & Stats -->
<div class="stats-column">
<!-- Class Info -->
<div class="detail-section">
<h3 class="section-title">{{ player_class.name }}</h3>
<p class="class-description">{{ player_class.description }}</p>
</div>
<!-- Base Stats -->
<div class="detail-section">
<h3 class="section-title">Base Attributes</h3>
<div class="stats-grid">
<div class="stat-box">
<div class="stat-name">Strength</div>
<div class="stat-value">{{ player_class.base_stats.strength }}</div>
</div>
<div class="stat-box">
<div class="stat-name">Dexterity</div>
<div class="stat-value">{{ player_class.base_stats.dexterity }}</div>
</div>
<div class="stat-box">
<div class="stat-name">Constitution</div>
<div class="stat-value">{{ player_class.base_stats.constitution }}</div>
</div>
<div class="stat-box">
<div class="stat-name">Intelligence</div>
<div class="stat-value">{{ player_class.base_stats.intelligence }}</div>
</div>
<div class="stat-box">
<div class="stat-name">Wisdom</div>
<div class="stat-value">{{ player_class.base_stats.wisdom }}</div>
</div>
<div class="stat-box">
<div class="stat-name">Charisma</div>
<div class="stat-value">{{ player_class.base_stats.charisma }}</div>
</div>
</div>
</div>
<!-- Skill Trees -->
<div class="detail-section">
<h3 class="section-title">Available Specializations</h3>
<p class="spec-note">Choose your path as you level up</p>
<div class="tree-list">
{% for tree in player_class.skill_trees %}
<div class="tree-preview">
<div class="tree-header">
<span class="tree-icon">🌲</span>
<span class="tree-name">{{ tree.name if tree is mapping else tree }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Starting Equipment -->
{% if player_class.starting_equipment %}
<div class="detail-section">
<h3 class="section-title">Starting Equipment</h3>
<div class="gear-list">
{% for item_id in player_class.starting_equipment %}
<div class="gear-item">⚔️ {{ item_id|replace('_', ' ')|title }}</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
<div class="decorative-line"></div>
<!-- Final Confirmation -->
<div class="confirmation-section">
<div class="warning-box">
<div class="warning-icon">⚠️</div>
<div class="warning-content">
<strong>Ready to begin?</strong>
<p>Once created, your character's class and origin cannot be changed. You can create additional characters based on your subscription tier.</p>
</div>
</div>
<form method="POST" action="{{ url_for('character_views.create_confirm') }}" id="confirm-form">
<!-- Navigation Buttons -->
<div class="creation-nav">
<a href="{{ url_for('character_views.create_customize') }}" class="btn btn-secondary">
← Back to Customize
</a>
<button type="submit" class="btn btn-primary btn-create">
Create Character & Begin Adventure ⚔️
</button>
</div>
</form>
</div>
</div>
</div>
<style>
/* ===== PROGRESS INDICATOR ===== */
.creation-progress {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 2rem;
padding: 1.5rem 0;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
min-width: 80px;
}
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
color: var(--text-muted);
font-family: var(--font-heading);
font-weight: 600;
font-size: var(--text-sm);
}
.progress-step.completed .step-number {
background: var(--accent-gold);
border-color: var(--accent-gold);
color: var(--bg-primary);
}
.progress-step.active .step-number {
background: var(--bg-primary);
border-color: var(--accent-gold);
color: var(--accent-gold);
box-shadow: 0 0 10px rgba(243, 156, 18, 0.3);
}
.step-label {
font-size: var(--text-xs);
color: var(--text-muted);
text-transform: uppercase;
font-weight: 600;
text-align: center;
}
.progress-step.active .step-label {
color: var(--accent-gold);
}
.progress-step.completed .step-label {
color: var(--text-secondary);
}
.progress-line {
width: 60px;
height: 2px;
background: var(--border-primary);
margin: 0 0.5rem;
margin-bottom: 1.5rem;
}
.progress-step.completed ~ .progress-line {
background: var(--accent-gold);
}
/* ===== CONFIRM CARD ===== */
.confirm-card {
background: var(--bg-secondary);
border: 2px solid var(--border-ornate);
border-radius: 8px;
padding: 2.5rem;
box-shadow: var(--shadow-lg);
position: relative;
}
/* Ornate corners */
.confirm-card::before,
.confirm-card::after {
content: '';
position: absolute;
width: 40px;
height: 40px;
border: 2px solid var(--accent-gold);
}
.confirm-card::before {
top: -2px;
left: -2px;
border-right: none;
border-bottom: none;
}
.confirm-card::after {
bottom: -2px;
right: -2px;
border-left: none;
border-top: none;
}
/* ===== CHARACTER HEADER ===== */
.character-header {
text-align: center;
margin-bottom: 2rem;
}
.character-name-display {
margin-bottom: 1rem;
}
.name-label {
display: block;
font-family: var(--font-heading);
font-size: var(--text-sm);
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 0.5rem;
}
.character-name {
font-family: var(--font-heading);
font-size: var(--text-4xl);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
margin: 0;
}
.character-class-origin {
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
}
.class-badge,
.origin-badge {
padding: 0.5rem 1.5rem;
border-radius: 20px;
font-size: var(--text-sm);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.class-badge {
background: linear-gradient(135deg, var(--accent-gold) 0%, var(--accent-gold-hover) 100%);
color: var(--bg-primary);
}
.origin-badge {
background: var(--bg-input);
border: 1px solid var(--border-primary);
color: var(--text-primary);
}
/* ===== CONFIRM CONTENT LAYOUT ===== */
.confirm-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin: 2rem 0;
}
/* ===== DETAIL SECTIONS ===== */
.detail-section {
margin-bottom: 2rem;
}
.detail-section:last-child {
margin-bottom: 0;
}
.section-title {
font-family: var(--font-heading);
font-size: var(--text-lg);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-primary);
}
/* ===== ORIGIN STORY ===== */
.origin-story {
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 0;
}
/* ===== LOCATION INFO ===== */
.location-name {
font-size: var(--text-lg);
color: var(--text-primary);
font-weight: 600;
margin-bottom: 0.5rem;
}
.location-description {
color: var(--text-secondary);
line-height: 1.6;
}
/* ===== BONUS LIST ===== */
.bonus-list {
list-style: none;
padding: 0;
margin: 0;
}
.bonus-item {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem;
background: var(--bg-input);
border-radius: 4px;
}
.bonus-icon {
font-size: var(--text-xl);
flex-shrink: 0;
}
.bonus-text {
color: var(--text-primary);
font-weight: 500;
margin-bottom: 0.25rem;
}
.bonus-description {
color: var(--text-secondary);
font-size: var(--text-sm);
margin: 0.25rem 0;
}
.bonus-effect {
color: var(--text-muted);
font-size: var(--text-xs);
font-style: italic;
}
/* ===== CLASS INFO ===== */
.class-description {
color: var(--text-secondary);
line-height: 1.6;
}
/* ===== STATS GRID ===== */
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.stat-box {
padding: 1rem;
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: 4px;
text-align: center;
}
.stat-name {
font-size: var(--text-xs);
color: var(--text-muted);
text-transform: uppercase;
font-weight: 600;
margin-bottom: 0.5rem;
}
.stat-value {
font-family: var(--font-heading);
font-size: var(--text-2xl);
color: var(--accent-gold);
font-weight: 700;
}
/* ===== SKILL TREES ===== */
.spec-note {
font-size: var(--text-sm);
color: var(--text-muted);
font-style: italic;
margin-bottom: 1rem;
}
.tree-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.tree-preview {
padding: 1rem;
background: var(--bg-input);
border-radius: 4px;
border-left: 3px solid var(--accent-gold);
}
.tree-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.tree-icon {
font-size: var(--text-lg);
}
.tree-name {
font-weight: 600;
color: var(--text-primary);
}
.tree-description {
font-size: var(--text-sm);
color: var(--text-secondary);
line-height: 1.5;
}
/* ===== STARTING GEAR ===== */
.gear-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.gear-item {
padding: 0.75rem;
background: var(--bg-input);
border-radius: 4px;
color: var(--text-primary);
}
/* ===== WARNING BOX ===== */
.confirmation-section {
margin-top: 2rem;
}
.warning-box {
display: flex;
gap: 1rem;
padding: 1.5rem;
background: rgba(243, 156, 18, 0.1);
border: 2px solid var(--accent-gold);
border-radius: 4px;
margin-bottom: 2rem;
}
.warning-icon {
font-size: var(--text-3xl);
flex-shrink: 0;
}
.warning-content {
font-size: var(--text-sm);
color: var(--text-primary);
line-height: 1.6;
}
.warning-content strong {
display: block;
font-size: var(--text-base);
color: var(--accent-gold);
margin-bottom: 0.5rem;
}
/* ===== CREATE BUTTON ===== */
.btn-create {
font-size: var(--text-lg);
padding: 1rem 2rem;
}
/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
.confirm-card {
padding: 1.5rem;
}
.character-name {
font-size: var(--text-2xl);
}
.confirm-content {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(3, 1fr);
}
.character-class-origin {
flex-direction: column;
gap: 0.5rem;
}
.warning-box {
flex-direction: column;
text-align: center;
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,425 @@
{% extends "base.html" %}
{% block title %}Customize Your Character - Code of Conquest{% endblock %}
{% block content %}
<div class="creation-container">
<!-- Progress Indicator -->
<div class="creation-progress">
<div class="progress-step completed">
<div class="step-number"></div>
<div class="step-label">Origin</div>
</div>
<div class="progress-line"></div>
<div class="progress-step completed">
<div class="step-number"></div>
<div class="step-label">Class</div>
</div>
<div class="progress-line"></div>
<div class="progress-step active">
<div class="step-number">3</div>
<div class="step-label">Customize</div>
</div>
<div class="progress-line"></div>
<div class="progress-step">
<div class="step-number">4</div>
<div class="step-label">Confirm</div>
</div>
</div>
<!-- Page Header -->
<h1 class="page-title">Name Your Hero</h1>
<p class="page-subtitle">What shall they call you in the halls of legend?</p>
<div class="decorative-line"></div>
<!-- Customization Form -->
<form method="POST" action="{{ url_for('character_views.create_customize') }}" id="customize-form">
<div class="customize-content">
<!-- Character Summary Panel -->
<div class="summary-panel">
<h3 class="panel-title">Your Character So Far</h3>
<div class="summary-section">
<div class="summary-label">Origin</div>
<div class="summary-value">{{ origin.name }}</div>
<p class="summary-description">{{ origin.description[:100] }}...</p>
</div>
<div class="summary-section">
<div class="summary-label">Class</div>
<div class="summary-value">{{ player_class.name }}</div>
<p class="summary-description">{{ player_class.description[:100] }}...</p>
</div>
<div class="summary-section">
<div class="summary-label">Starting Location</div>
<div class="summary-value">{{ origin.starting_location.name }}</div>
<p class="summary-description">{{ origin.starting_location.description }}</p>
</div>
<div class="summary-section">
<div class="summary-label">Base Stats</div>
<div class="stats-compact">
<span class="stat-compact">STR {{ player_class.base_stats.strength }}</span>
<span class="stat-compact">DEX {{ player_class.base_stats.dexterity }}</span>
<span class="stat-compact">CON {{ player_class.base_stats.constitution }}</span>
<span class="stat-compact">INT {{ player_class.base_stats.intelligence }}</span>
<span class="stat-compact">WIS {{ player_class.base_stats.wisdom }}</span>
<span class="stat-compact">CHA {{ player_class.base_stats.charisma }}</span>
</div>
</div>
</div>
<!-- Name Input Panel -->
<div class="name-panel">
<h3 class="panel-title">Choose Your Name</h3>
<div class="form-group">
<label class="form-label" for="character-name">Character Name</label>
<input
type="text"
id="character-name"
name="name"
class="form-input"
placeholder="Enter your character's name"
minlength="3"
maxlength="30"
required
autofocus
>
<span class="form-help">3-30 characters</span>
</div>
<div class="name-suggestions">
<p class="suggestions-label">Need inspiration? Try these:</p>
<div class="suggestion-buttons">
<button type="button" class="suggestion-btn" onclick="setName('Aldric the Brave')">Aldric the Brave</button>
<button type="button" class="suggestion-btn" onclick="setName('Lyra Shadowstep')">Lyra Shadowstep</button>
<button type="button" class="suggestion-btn" onclick="setName('Theron Ironheart')">Theron Ironheart</button>
<button type="button" class="suggestion-btn" onclick="setName('Mira Stormborn')">Mira Stormborn</button>
<button type="button" class="suggestion-btn" onclick="setName('Kael Nightwhisper')">Kael Nightwhisper</button>
<button type="button" class="suggestion-btn" onclick="setName('Aria Firehand')">Aria Firehand</button>
</div>
</div>
<div class="info-box">
<div class="info-icon"></div>
<div class="info-content">
<strong>Character Creation Tips:</strong>
<ul>
<li>Your name will be visible to other players</li>
<li>Choose a name that fits the fantasy RPG setting</li>
<li>You can always create more characters later</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Navigation Buttons -->
<div class="creation-nav">
<a href="{{ url_for('character_views.create_class') }}" class="btn btn-secondary">
← Back to Class
</a>
<button type="submit" class="btn btn-primary">
Next: Confirm →
</button>
</div>
</form>
</div>
<style>
/* ===== PROGRESS INDICATOR ===== */
.creation-progress {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 2rem;
padding: 1.5rem 0;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
min-width: 80px;
}
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
color: var(--text-muted);
font-family: var(--font-heading);
font-weight: 600;
font-size: var(--text-sm);
}
.progress-step.completed .step-number {
background: var(--accent-gold);
border-color: var(--accent-gold);
color: var(--bg-primary);
}
.progress-step.active .step-number {
background: var(--bg-primary);
border-color: var(--accent-gold);
color: var(--accent-gold);
box-shadow: 0 0 10px rgba(243, 156, 18, 0.3);
}
.step-label {
font-size: var(--text-xs);
color: var(--text-muted);
text-transform: uppercase;
font-weight: 600;
text-align: center;
}
.progress-step.active .step-label {
color: var(--accent-gold);
}
.progress-step.completed .step-label {
color: var(--text-secondary);
}
.progress-line {
width: 60px;
height: 2px;
background: var(--border-primary);
margin: 0 0.5rem;
margin-bottom: 1.5rem;
}
.progress-step.completed ~ .progress-line {
background: var(--accent-gold);
}
/* ===== CUSTOMIZE CONTENT LAYOUT ===== */
.customize-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
/* ===== PANELS ===== */
.summary-panel,
.name-panel {
background: var(--bg-secondary);
border: 2px solid var(--border-ornate);
border-radius: 8px;
padding: 2rem;
box-shadow: var(--shadow-md);
}
.panel-title {
font-family: var(--font-heading);
font-size: var(--text-xl);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid var(--border-primary);
}
/* ===== SUMMARY SECTIONS ===== */
.summary-section {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-primary);
}
.summary-section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.summary-label {
font-family: var(--font-heading);
font-size: var(--text-sm);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 0.5rem;
}
.summary-value {
font-size: var(--text-lg);
color: var(--text-primary);
font-weight: 600;
margin-bottom: 0.5rem;
}
.summary-description {
font-size: var(--text-sm);
color: var(--text-secondary);
line-height: 1.5;
}
/* ===== COMPACT STATS ===== */
.stats-compact {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.stat-compact {
padding: 0.5rem 0.75rem;
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: 4px;
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--text-primary);
}
/* ===== NAME INPUT ===== */
.form-group {
margin-bottom: 2rem;
}
.form-label {
display: block;
font-family: var(--font-heading);
font-size: var(--text-sm);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 0.5rem;
}
.form-input {
width: 100%;
padding: 0.75rem 1rem;
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: 4px;
color: var(--text-primary);
font-family: var(--font-body);
font-size: var(--text-base);
transition: all 0.3s ease;
}
.form-input:focus {
outline: none;
border-color: var(--accent-gold);
box-shadow: 0 0 10px rgba(243, 156, 18, 0.2);
}
.form-help {
display: block;
margin-top: 0.25rem;
font-size: var(--text-xs);
color: var(--text-muted);
}
/* ===== NAME SUGGESTIONS ===== */
.name-suggestions {
margin-bottom: 2rem;
}
.suggestions-label {
font-size: var(--text-sm);
color: var(--text-secondary);
margin-bottom: 0.75rem;
}
.suggestion-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
.suggestion-btn {
padding: 0.5rem 1rem;
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: 4px;
color: var(--text-secondary);
font-family: var(--font-body);
font-size: var(--text-sm);
cursor: pointer;
transition: all 0.3s ease;
}
.suggestion-btn:hover {
border-color: var(--accent-gold);
color: var(--accent-gold);
background: rgba(243, 156, 18, 0.1);
}
/* ===== INFO BOX ===== */
.info-box {
display: flex;
gap: 1rem;
padding: 1rem;
background: rgba(52, 152, 219, 0.1);
border-left: 4px solid var(--accent-blue);
border-radius: 4px;
}
.info-icon {
font-size: var(--text-2xl);
flex-shrink: 0;
}
.info-content {
font-size: var(--text-sm);
color: var(--text-secondary);
line-height: 1.6;
}
.info-content strong {
color: var(--text-primary);
display: block;
margin-bottom: 0.5rem;
}
.info-content ul {
margin: 0;
padding-left: 1.5rem;
}
.info-content li {
margin-bottom: 0.25rem;
}
/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
.customize-content {
grid-template-columns: 1fr;
}
.suggestion-buttons {
grid-template-columns: 1fr;
}
.stats-compact {
gap: 0.5rem;
}
.stat-compact {
font-size: var(--text-xs);
padding: 0.4rem 0.6rem;
}
}
</style>
<script>
function setName(name) {
document.getElementById('character-name').value = name;
document.getElementById('character-name').focus();
}
</script>
{% endblock %}

View File

@@ -0,0 +1,387 @@
{% extends "base.html" %}
{% block title %}Choose Your Origin - Code of Conquest{% endblock %}
{% block content %}
<div class="creation-container">
<!-- Progress Indicator -->
<div class="creation-progress">
<div class="progress-step active">
<div class="step-number">1</div>
<div class="step-label">Origin</div>
</div>
<div class="progress-line"></div>
<div class="progress-step">
<div class="step-number">2</div>
<div class="step-label">Class</div>
</div>
<div class="progress-line"></div>
<div class="progress-step">
<div class="step-number">3</div>
<div class="step-label">Customize</div>
</div>
<div class="progress-line"></div>
<div class="progress-step">
<div class="step-number">4</div>
<div class="step-label">Confirm</div>
</div>
</div>
<!-- Page Header -->
<h1 class="page-title">Choose Your Origin</h1>
<p class="page-subtitle">Every hero has a beginning. Where does your story start?</p>
<div class="decorative-line"></div>
<!-- Origin Selection Form -->
<form method="POST" action="{{ url_for('character_views.create_origin') }}" id="origin-form">
<div class="origin-grid">
{% for origin in origins %}
<div class="origin-card" data-origin-id="{{ origin.id }}">
<input
type="radio"
name="origin_id"
value="{{ origin.id }}"
id="origin-{{ origin.id }}"
class="origin-radio"
required
>
<label for="origin-{{ origin.id }}" class="origin-label">
<div class="origin-header">
<h3 class="origin-title">{{ origin.name }}</h3>
</div>
<div class="origin-description">
<p class="description-preview">
{{ origin.description[:180] }}{% if origin.description|length > 180 %}...{% endif %}
</p>
{% if origin.description|length > 180 %}
<div class="description-full" style="display: none;">
<p>{{ origin.description }}</p>
</div>
<button type="button" class="expand-btn" onclick="toggleDescription(this)">
<span class="expand-text">Read More</span>
<span class="expand-icon"></span>
</button>
{% endif %}
</div>
<div class="origin-details">
<div class="detail-item">
<span class="detail-label">Starting Location:</span>
<span class="detail-value">{{ origin.starting_location.name }}</span>
</div>
{% if origin.starting_bonus %}
<div class="detail-item">
<span class="detail-label">Starting Bonus:</span>
<div class="bonus-display">
<strong>{{ origin.starting_bonus.trait }}</strong>
<p class="bonus-description">{{ origin.starting_bonus.description }}</p>
</div>
</div>
{% endif %}
</div>
</label>
</div>
{% endfor %}
</div>
<!-- Navigation Buttons -->
<div class="creation-nav">
<a href="{{ url_for('character_views.list_characters') }}" class="btn btn-secondary">
Cancel
</a>
<button type="submit" class="btn btn-primary">
Next: Choose Class →
</button>
</div>
</form>
</div>
<style>
/* ===== CHARACTER CREATION CONTAINER ===== */
.creation-container {
max-width: 1200px;
margin: 2rem auto;
padding: 2rem;
}
/* ===== PROGRESS INDICATOR ===== */
.creation-progress {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 3rem;
gap: 1rem;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--bg-input);
border: 2px solid var(--border-primary);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-heading);
font-weight: 600;
color: var(--text-muted);
transition: all 0.3s ease;
}
.step-label {
font-size: var(--text-xs);
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.3s ease;
}
.progress-step.active .step-number {
background: var(--accent-gold);
border-color: var(--accent-gold);
color: var(--bg-primary);
box-shadow: var(--shadow-glow);
}
.progress-step.active .step-label {
color: var(--accent-gold);
font-weight: 600;
}
.progress-step.completed .step-number {
background: var(--accent-green);
border-color: var(--accent-green);
color: var(--bg-primary);
}
.progress-step.completed .step-label {
color: var(--accent-green);
}
.progress-line {
width: 60px;
height: 2px;
background: var(--border-primary);
}
/* ===== ORIGIN GRID ===== */
.origin-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
/* ===== ORIGIN CARDS ===== */
.origin-card {
position: relative;
}
.origin-radio {
position: absolute;
opacity: 0;
pointer-events: none;
}
.origin-label {
display: block;
padding: 2rem;
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
height: 100%;
}
.origin-label:hover {
border-color: var(--accent-gold);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.origin-radio:checked + .origin-label {
border-color: var(--accent-gold);
box-shadow: var(--shadow-glow);
background: linear-gradient(135deg, var(--bg-secondary) 0%, rgba(243, 156, 18, 0.1) 100%);
}
.origin-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-primary);
}
.origin-title {
font-family: var(--font-heading);
font-size: var(--text-xl);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 1px;
}
.origin-description {
margin-bottom: 1rem;
color: var(--text-secondary);
line-height: 1.6;
}
.description-preview {
margin: 0;
}
.description-full {
margin-top: 0.5rem;
}
.description-full p {
margin: 0;
}
.expand-btn {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.5rem;
padding: 0.5rem 0;
background: none;
border: none;
color: var(--accent-gold);
font-size: var(--text-sm);
cursor: pointer;
transition: all 0.3s ease;
}
.expand-btn:hover {
color: var(--accent-gold-hover);
}
.expand-icon {
font-size: var(--text-xs);
transition: transform 0.3s ease;
}
.expand-btn.expanded .expand-icon {
transform: rotate(180deg);
}
.origin-details {
margin-bottom: 1.5rem;
}
.detail-item {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}
.detail-label {
font-family: var(--font-heading);
font-size: var(--text-sm);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 0.25rem;
}
.detail-value {
color: var(--text-primary);
font-weight: 500;
}
.bonus-display {
margin-top: 0.5rem;
padding: 0.75rem;
background: var(--bg-input);
border-radius: 4px;
}
.bonus-display strong {
color: var(--accent-gold);
display: block;
margin-bottom: 0.25rem;
}
.bonus-description {
color: var(--text-secondary);
font-size: var(--text-sm);
margin: 0.25rem 0;
}
/* ===== CREATION NAVIGATION ===== */
.creation-nav {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-top: 2rem;
}
.creation-nav .btn {
min-width: 200px;
}
/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
.creation-container {
padding: 1rem;
}
.origin-grid {
grid-template-columns: 1fr;
}
.creation-progress {
gap: 0.5rem;
}
.progress-line {
width: 30px;
}
.step-label {
display: none;
}
.creation-nav {
flex-direction: column;
}
.creation-nav .btn {
width: 100%;
}
}
</style>
<script>
function toggleDescription(button) {
const card = button.closest('.origin-card');
const preview = card.querySelector('.description-preview');
const full = card.querySelector('.description-full');
const expandText = button.querySelector('.expand-text');
if (full.style.display === 'none') {
// Expand
preview.style.display = 'none';
full.style.display = 'block';
expandText.textContent = 'Read Less';
button.classList.add('expanded');
} else {
// Collapse
preview.style.display = 'block';
full.style.display = 'none';
expandText.textContent = 'Read More';
button.classList.remove('expanded');
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,485 @@
{% extends "base.html" %}
{% block title %}{{ character.name }} - Code of Conquest{% endblock %}
{% block content %}
<div class="character-detail-container">
<!-- Header -->
<div class="detail-header">
<div class="header-left">
<h1 class="character-name">{{ character.name }}</h1>
<p class="character-subtitle">
Level {{ character.level }} {{ character.player_class.name }}
</p>
</div>
<div class="header-right">
<a href="{{ url_for('character_views.list_characters') }}" class="btn btn-secondary">
← Back to Characters
</a>
</div>
</div>
<div class="decorative-line"></div>
<!-- Main Content Grid -->
<div class="detail-grid">
<!-- Left Column: Stats & Info -->
<div class="detail-section">
<h2 class="section-title">Character Information</h2>
<div class="info-card">
<div class="info-row">
<span class="info-label">Class:</span>
<span class="info-value">{{ character.player_class.name }}</span>
</div>
<div class="info-row">
<span class="info-label">Origin:</span>
<span class="info-value">{{ character.origin_name }}</span>
</div>
<div class="info-row">
<span class="info-label">Level:</span>
<span class="info-value">{{ character.level }}</span>
</div>
<div class="info-row">
<span class="info-label">Experience:</span>
<span class="info-value">{{ character.experience }} XP</span>
</div>
<div class="info-row">
<span class="info-label">Gold:</span>
<span class="info-value gold">{{ character.gold }} 💰</span>
</div>
<div class="info-row">
<span class="info-label">Skill Points:</span>
<span class="info-value">{{ character.available_skill_points }}</span>
</div>
</div>
<!-- Base Stats -->
<h3 class="subsection-title">Base Statistics</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-name">STR</span>
<span class="stat-value">{{ character.base_stats.strength }}</span>
</div>
<div class="stat-item">
<span class="stat-name">DEX</span>
<span class="stat-value">{{ character.base_stats.dexterity }}</span>
</div>
<div class="stat-item">
<span class="stat-name">CON</span>
<span class="stat-value">{{ character.base_stats.constitution }}</span>
</div>
<div class="stat-item">
<span class="stat-name">INT</span>
<span class="stat-value">{{ character.base_stats.intelligence }}</span>
</div>
<div class="stat-item">
<span class="stat-name">WIS</span>
<span class="stat-value">{{ character.base_stats.wisdom }}</span>
</div>
<div class="stat-item">
<span class="stat-name">CHA</span>
<span class="stat-value">{{ character.base_stats.charisma }}</span>
</div>
</div>
<!-- Derived Stats -->
<h3 class="subsection-title">Derived Statistics</h3>
<div class="info-card">
<div class="info-row">
<span class="info-label">Hit Points:</span>
<span class="info-value">{{ character.current_hp }} / {{ character.max_hp }}</span>
</div>
<div class="info-row">
<span class="info-label">Mana Points:</span>
<span class="info-value">{{ character.base_stats.mana_points }}</span>
</div>
<div class="info-row">
<span class="info-label">Defense:</span>
<span class="info-value">{{ character.base_stats.defense }}</span>
</div>
<div class="info-row">
<span class="info-label">Resistance:</span>
<span class="info-value">{{ character.base_stats.resistance }}</span>
</div>
</div>
</div>
<!-- Right Column: Skills, Inventory, etc -->
<div class="detail-section">
<!-- Unlocked Skills -->
<h2 class="section-title">Unlocked Skills</h2>
{% if character.unlocked_skills %}
<div class="skills-list">
{% for skill_id in character.unlocked_skills %}
<div class="skill-badge">{{ skill_id }}</div>
{% endfor %}
</div>
{% else %}
<p class="empty-text">No skills unlocked yet.</p>
{% endif %}
<a href="{{ url_for('character_views.view_skills', character_id=character.character_id) }}" class="btn btn-primary btn-block">
Manage Skills
</a>
<!-- Equipment -->
<h2 class="section-title">Equipment</h2>
{% if character.equipped %}
<div class="equipment-list">
{% for slot, item in character.equipped.items() %}
<div class="equipment-item">
<span class="equipment-slot">{{ slot|title }}:</span>
<span class="equipment-name">{{ item.name }}</span>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty-text">No equipment equipped.</p>
{% endif %}
<!-- Inventory -->
<h2 class="section-title">Inventory</h2>
{% if character.inventory %}
<div class="inventory-list">
{% for item in character.inventory %}
<div class="inventory-item">
<span class="item-name">{{ item.name }}</span>
<span class="item-type">{{ item.item_type }}</span>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty-text">Inventory is empty.</p>
{% endif %}
<!-- Active Quests -->
<h2 class="section-title">Active Quests</h2>
{% if character.active_quests %}
<div class="quests-list">
{% for quest_id in character.active_quests %}
<div class="quest-item">{{ quest_id }}</div>
{% endfor %}
</div>
{% else %}
<p class="empty-text">No active quests.</p>
{% endif %}
</div>
</div>
<!-- Origin Story -->
<div class="origin-story-section">
<h2 class="section-title">Origin: {{ character.origin.name }}</h2>
<p class="origin-description">{{ character.origin.description }}</p>
<div class="starting-location">
<h3 class="subsection-title">Starting Location</h3>
<p><strong>{{ character.origin.starting_location.name }}</strong> ({{ character.origin.starting_location.region }})</p>
<p class="location-description">{{ character.origin.starting_location.description }}</p>
</div>
</div>
<!-- Actions -->
<div class="character-actions">
<form method="POST" action="{{ url_for('character_views.delete_character', character_id=character.character_id) }}"
onsubmit="return confirm('Are you sure you want to delete {{ character.name }}? This cannot be undone.');">
<button type="submit" class="btn btn-danger">
Delete Character
</button>
</form>
</div>
</div>
<style>
/* ===== CHARACTER DETAIL CONTAINER ===== */
.character-detail-container {
max-width: 1400px;
margin: 2rem auto;
padding: 2rem;
}
/* ===== HEADER ===== */
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
gap: 2rem;
}
.character-name {
font-family: var(--font-heading);
font-size: var(--text-3xl);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 2px;
margin: 0 0 0.5rem 0;
}
.character-subtitle {
font-size: var(--text-lg);
color: var(--text-secondary);
margin: 0;
}
/* ===== GRID LAYOUT ===== */
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
.detail-section {
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
border-radius: 8px;
padding: 1.5rem;
}
/* ===== SECTION TITLES ===== */
.section-title {
font-family: var(--font-heading);
font-size: var(--text-xl);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 1px;
margin: 0 0 1rem 0;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--border-primary);
}
.subsection-title {
font-family: var(--font-heading);
font-size: var(--text-lg);
color: var(--text-primary);
margin: 1.5rem 0 1rem 0;
text-transform: uppercase;
letter-spacing: 1px;
}
/* ===== INFO CARD ===== */
.info-card {
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: 4px;
padding: 1rem;
margin-bottom: 1.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid var(--bg-secondary);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-size: var(--text-sm);
color: var(--text-muted);
font-weight: 600;
}
.info-value {
font-size: var(--text-sm);
color: var(--text-primary);
}
.info-value.gold {
color: var(--accent-gold);
font-weight: 600;
}
/* ===== STATS GRID ===== */
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat-item {
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: 4px;
padding: 1rem;
text-align: center;
}
.stat-name {
display: block;
font-size: var(--text-xs);
color: var(--text-muted);
font-weight: 600;
margin-bottom: 0.5rem;
text-transform: uppercase;
}
.stat-value {
display: block;
font-size: var(--text-2xl);
color: var(--accent-gold);
font-weight: 700;
}
/* ===== SKILLS ===== */
.skills-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.skill-badge {
background: var(--bg-input);
border: 1px solid var(--accent-gold);
border-radius: 12px;
padding: 0.25rem 0.75rem;
font-size: var(--text-xs);
color: var(--accent-gold);
text-transform: uppercase;
font-weight: 600;
}
/* ===== EQUIPMENT & INVENTORY ===== */
.equipment-list,
.inventory-list {
margin-bottom: 1rem;
}
.equipment-item,
.inventory-item {
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: 4px;
padding: 0.75rem;
margin-bottom: 0.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.equipment-slot,
.item-type {
font-size: var(--text-xs);
color: var(--text-muted);
text-transform: uppercase;
font-weight: 600;
}
.equipment-name,
.item-name {
color: var(--text-primary);
}
/* ===== QUESTS ===== */
.quests-list {
margin-bottom: 1rem;
}
.quest-item {
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: 4px;
padding: 0.75rem;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
/* ===== ORIGIN STORY ===== */
.origin-story-section {
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.origin-description {
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.narrative-hooks {
list-style-type: none;
padding: 0;
margin-bottom: 1.5rem;
}
.narrative-hooks li {
background: var(--bg-input);
border-left: 3px solid var(--accent-gold);
padding: 0.75rem;
margin-bottom: 0.5rem;
color: var(--text-secondary);
}
.starting-location {
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: 4px;
padding: 1rem;
}
.location-description {
color: var(--text-secondary);
font-size: var(--text-sm);
margin: 0.5rem 0 0 0;
}
/* ===== EMPTY STATE ===== */
.empty-text {
color: var(--text-muted);
font-style: italic;
margin: 0.5rem 0 1rem 0;
}
/* ===== BUTTONS ===== */
.btn-block {
display: block;
width: 100%;
margin-bottom: 1.5rem;
}
.character-actions {
text-align: center;
}
.btn-danger {
background: transparent;
border: 2px solid var(--accent-red);
color: var(--accent-red);
}
.btn-danger:hover {
background: var(--accent-red);
color: var(--text-primary);
}
/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
.character-detail-container {
padding: 1rem;
}
.detail-header {
flex-direction: column;
align-items: flex-start;
}
.detail-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,455 @@
{% extends "base.html" %}
{% block title %}Your Characters - Code of Conquest{% endblock %}
{% block content %}
<div class="characters-container">
<div class="characters-header">
<div class="header-left">
<h1 class="page-title">Your Characters</h1>
<p class="page-subtitle">
{{ characters|length }} of {{ max_characters }} characters
<span class="tier-badge">{{ current_tier|upper }}</span>
</p>
</div>
<div class="header-right">
{% if can_create %}
<a href="{{ url_for('character_views.create_origin') }}" class="btn btn-primary">
⚔️ Create New Character
</a>
{% else %}
<button class="btn btn-primary" disabled title="Character limit reached for {{ current_tier }} tier">
Character Limit Reached
</button>
{% endif %}
</div>
</div>
<div class="decorative-line"></div>
{% if characters %}
<div class="characters-grid">
{% for character in characters %}
<div class="character-card">
<div class="character-card-header">
<h3 class="character-name">{{ character.name }}</h3>
<span class="character-level">Level {{ character.level }}</span>
</div>
<div class="character-info">
<div class="info-row">
<span class="info-label">Class:</span>
<span class="info-value">{{ character.class_name or character.class|title }}</span>
</div>
<div class="info-row">
<span class="info-label">Origin:</span>
<span class="info-value">{{ character.origin|replace('_', ' ')|title }}</span>
</div>
<div class="info-row">
<span class="info-label">Gold:</span>
<span class="info-value gold">{{ character.gold }} 💰</span>
</div>
<div class="info-row">
<span class="info-label">Experience:</span>
<span class="info-value">{{ character.experience }}</span>
</div>
</div>
{# Sessions Section #}
<div class="character-sessions">
{% if character.sessions %}
<div class="sessions-header">
<span class="sessions-label">Active Sessions:</span>
</div>
<div class="sessions-list">
{% for sess in character.sessions[:3] %}
<a href="{{ url_for('game.play_session', session_id=sess.session_id) }}" class="session-link">
<span class="session-turn">Turn {{ sess.turn_number or 0 }}</span>
<span class="session-status {{ sess.status or 'active' }}">{{ sess.status or 'active' }}</span>
</a>
{% endfor %}
{% if character.sessions|length > 3 %}
<span class="sessions-more">+{{ character.sessions|length - 3 }} more</span>
{% endif %}
</div>
{% endif %}
</div>
<div class="character-actions">
{# Primary Play Action #}
{% if character.sessions %}
<a href="{{ url_for('game.play_session', session_id=character.sessions[0].session_id) }}" class="btn btn-primary btn-sm">
Continue Playing
</a>
<form method="POST" action="{{ url_for('character_views.create_session', character_id=character.character_id) }}" style="display: inline;">
<button type="submit" class="btn btn-secondary btn-sm" title="Start a new adventure">
New Session
</button>
</form>
{% else %}
<form method="POST" action="{{ url_for('character_views.create_session', character_id=character.character_id) }}" style="display: inline;">
<button type="submit" class="btn btn-primary btn-sm">
Start Adventure
</button>
</form>
{% endif %}
{# Secondary Actions #}
<a href="{{ url_for('character_views.view_character', character_id=character.character_id) }}" class="btn btn-secondary btn-sm">
Details
</a>
<a href="{{ url_for('character_views.view_skills', character_id=character.character_id) }}" class="btn btn-secondary btn-sm">
Skills
</a>
<form method="POST" action="{{ url_for('character_views.delete_character', character_id=character.character_id) }}" onsubmit="return confirm('Are you sure you want to delete {{ character.name }}? This cannot be undone.');" style="display: inline;">
<button type="submit" class="btn btn-danger btn-sm">
Delete
</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div class="empty-icon">⚔️</div>
<h2 class="empty-title">No Characters Yet</h2>
<p class="empty-message">Begin your adventure by creating your first character!</p>
<a href="{{ url_for('character_views.create_origin') }}" class="btn btn-primary btn-lg">
Create Your First Character
</a>
</div>
{% endif %}
</div>
<style>
/* ===== CHARACTERS CONTAINER ===== */
.characters-container {
max-width: 1400px;
margin: 2rem auto;
padding: 2rem;
}
/* ===== HEADER ===== */
.characters-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
gap: 2rem;
}
.header-left {
flex: 1;
}
.page-subtitle {
display: flex;
align-items: center;
gap: 1rem;
}
.tier-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
background: var(--accent-gold);
color: var(--bg-primary);
border-radius: 12px;
font-size: var(--text-xs);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
/* ===== CHARACTERS GRID ===== */
.characters-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
/* ===== CHARACTER CARDS ===== */
.character-card {
background: var(--bg-secondary);
border: 2px solid var(--border-primary);
border-radius: 8px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.character-card:hover {
border-color: var(--accent-gold);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.character-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--border-primary);
}
.character-name {
font-family: var(--font-heading);
font-size: var(--text-xl);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 1px;
margin: 0;
}
.character-level {
padding: 0.25rem 0.75rem;
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: 12px;
font-size: var(--text-sm);
font-weight: 600;
color: var(--text-primary);
}
/* ===== CHARACTER INFO ===== */
.character-info {
margin-bottom: 1rem;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid var(--bg-input);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-size: var(--text-sm);
color: var(--text-muted);
font-weight: 600;
}
.info-value {
font-size: var(--text-sm);
color: var(--text-primary);
}
.info-value.gold {
color: var(--accent-gold);
font-weight: 600;
}
/* ===== STATS COMPACT ===== */
.character-stats-compact {
margin-bottom: 1rem;
padding: 1rem;
background: var(--bg-input);
border-radius: 4px;
}
.stat-mini {
display: flex;
align-items: center;
gap: 0.75rem;
}
.stat-label {
font-size: var(--text-sm);
color: var(--text-muted);
font-weight: 600;
min-width: 30px;
}
.stat-bar {
flex: 1;
height: 8px;
background: var(--bg-secondary);
border-radius: 4px;
overflow: hidden;
border: 1px solid var(--border-primary);
}
.stat-fill {
display: block;
height: 100%;
background: linear-gradient(90deg, var(--accent-red-light) 0%, var(--accent-green) 100%);
transition: width 0.3s ease;
}
.stat-text {
font-size: var(--text-xs);
color: var(--text-secondary);
font-family: var(--font-mono);
min-width: 60px;
text-align: right;
}
/* ===== CHARACTER SESSIONS ===== */
.character-sessions {
margin-bottom: 1rem;
min-height: 1rem;
}
.sessions-header {
margin-bottom: 0.5rem;
}
.sessions-label {
font-size: var(--text-sm);
color: var(--text-muted);
font-weight: 600;
}
.sessions-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.session-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.75rem;
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: 4px;
text-decoration: none;
transition: all 0.2s ease;
}
.session-link:hover {
border-color: var(--accent-gold);
background: var(--bg-secondary);
}
.session-turn {
font-size: var(--text-xs);
color: var(--text-primary);
font-weight: 600;
}
.session-status {
font-size: var(--text-xs);
padding: 0.15rem 0.4rem;
border-radius: 3px;
text-transform: uppercase;
font-weight: 600;
}
.session-status.active {
background: var(--accent-green);
color: var(--bg-primary);
}
.session-status.paused {
background: var(--accent-gold);
color: var(--bg-primary);
}
.session-status.ended {
background: var(--text-muted);
color: var(--bg-primary);
}
.sessions-more {
font-size: var(--text-xs);
color: var(--text-muted);
padding: 0.35rem 0.5rem;
}
/* ===== CHARACTER ACTIONS ===== */
.character-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: var(--text-sm);
}
.btn-danger {
background: transparent;
border: 2px solid var(--accent-red);
color: var(--accent-red);
}
.btn-danger:hover {
background: var(--accent-red);
color: var(--text-primary);
}
/* ===== EMPTY STATE ===== */
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: var(--bg-secondary);
border: 2px dashed var(--border-primary);
border-radius: 8px;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-title {
font-family: var(--font-heading);
font-size: var(--text-2xl);
color: var(--accent-gold);
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 1rem;
}
.empty-message {
font-size: var(--text-lg);
color: var(--text-secondary);
margin-bottom: 2rem;
}
.btn-lg {
padding: 1rem 2rem;
font-size: var(--text-lg);
}
/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
.characters-container {
padding: 1rem;
}
.characters-header {
flex-direction: column;
align-items: flex-start;
}
.header-right {
width: 100%;
}
.header-right .btn {
width: 100%;
}
.characters-grid {
grid-template-columns: 1fr;
}
.character-actions {
flex-direction: column;
}
.character-actions .btn {
width: 100%;
}
}
</style>
{% endblock %}

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

View File

@@ -0,0 +1,147 @@
{% extends "base.html" %}
{% block title %}Lost in the Void - Code of Conquest{% endblock %}
{% block content %}
<div class="error-container">
<div class="error-content">
<!-- Error Code -->
<div class="error-code">404</div>
<!-- Error Title -->
<h1 class="error-title">Lost in the Void</h1>
<!-- Flavor Text -->
<div class="error-description">
<p class="lead">The path you seek has vanished into the mists...</p>
<p>This page has either been consumed by ancient magic, moved to another realm, or never existed in the first place.</p>
</div>
<!-- Decorative Divider -->
<div class="error-divider">⚔️</div>
<!-- Navigation Options -->
<div class="error-actions">
<a href="/" class="btn btn-primary">
Return to the Realm
</a>
<a href="{{ url_for('character_views.list_characters') }}" class="btn btn-secondary">
View Your Characters
</a>
</div>
<!-- Additional Help -->
<div class="error-help">
<p class="help-text">
If you believe this path should exist, consult the ancient scrolls (check your URL) or
<a href="/" class="text-link">return home</a> to begin your journey anew.
</p>
</div>
</div>
</div>
<style>
.error-container {
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh - 200px);
padding: 2rem;
}
.error-content {
max-width: 600px;
text-align: center;
background: var(--bg-secondary);
padding: 3rem 2rem;
border-radius: 8px;
border: 2px solid var(--border-ornate);
box-shadow: var(--shadow-lg);
}
.error-code {
font-family: var(--font-heading);
font-size: 8rem;
font-weight: 700;
color: var(--accent-gold);
text-shadow: 0 0 30px rgba(243, 156, 18, 0.5);
line-height: 1;
margin-bottom: 1rem;
}
.error-title {
font-family: var(--font-heading);
font-size: var(--text-4xl);
color: var(--text-primary);
margin-bottom: 1.5rem;
text-transform: uppercase;
letter-spacing: 2px;
}
.error-description {
margin-bottom: 2rem;
}
.error-description .lead {
font-size: var(--text-xl);
color: var(--accent-gold);
font-style: italic;
margin-bottom: 1rem;
}
.error-description p {
color: var(--text-secondary);
line-height: 1.8;
}
.error-divider {
font-size: var(--text-2xl);
color: var(--border-ornate);
margin: 2rem 0;
}
.error-actions {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.error-actions .btn {
width: 100%;
}
.error-help {
padding-top: 2rem;
border-top: 1px solid var(--border-primary);
}
.help-text {
font-size: var(--text-sm);
color: var(--text-muted);
line-height: 1.6;
}
.text-link {
color: var(--accent-gold);
text-decoration: none;
transition: color 0.2s;
}
.text-link:hover {
color: var(--accent-gold-hover);
text-decoration: underline;
}
@media (min-width: 640px) {
.error-actions {
flex-direction: row;
justify-content: center;
}
.error-actions .btn {
width: auto;
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,201 @@
{% extends "base.html" %}
{% block title %}Arcane Disruption - Code of Conquest{% endblock %}
{% block content %}
<div class="error-container">
<div class="error-content">
<!-- Error Code -->
<div class="error-code">500</div>
<!-- Error Title -->
<h1 class="error-title">Arcane Disruption</h1>
<!-- Flavor Text -->
<div class="error-description">
<p class="lead">The magical wards have been breached!</p>
<p>A powerful enchantment has gone awry, causing a disturbance in the realm's core magic. Our court wizards are investigating the anomaly and working to restore balance.</p>
</div>
<!-- Decorative Divider -->
<div class="error-divider">🔮</div>
<!-- Navigation Options -->
<div class="error-actions">
<a href="/" class="btn btn-primary">
Return to Safety
</a>
<a href="javascript:window.location.reload()" class="btn btn-secondary">
Attempt Restoration
</a>
</div>
<!-- Additional Help -->
<div class="error-help">
<p class="help-text">
If this disruption persists, the kingdom's mages (our support team) may need to intervene.
Please try again in a few moments, or <a href="/" class="text-link">retreat to the main hall</a>.
</p>
</div>
<!-- Technical Details (for development) -->
{% if config.get('app', {}).get('debug', False) %}
<div class="error-debug">
<details>
<summary class="debug-summary">Arcane Runes (Debug Info)</summary>
<div class="debug-content">
<p><strong>Spell Component:</strong> Internal Server Error</p>
<p><strong>Magical Signature:</strong> {{ request.url }}</p>
<p><strong>Timestamp:</strong> {{ moment().format('YYYY-MM-DD HH:mm:ss') if moment else 'Unknown' }}</p>
</div>
</details>
</div>
{% endif %}
</div>
</div>
<style>
.error-container {
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh - 200px);
padding: 2rem;
}
.error-content {
max-width: 600px;
text-align: center;
background: var(--bg-secondary);
padding: 3rem 2rem;
border-radius: 8px;
border: 2px solid var(--border-ornate);
box-shadow: var(--shadow-lg);
}
.error-code {
font-family: var(--font-heading);
font-size: 8rem;
font-weight: 700;
color: var(--accent-red-light);
text-shadow: 0 0 30px rgba(231, 76, 60, 0.5);
line-height: 1;
margin-bottom: 1rem;
}
.error-title {
font-family: var(--font-heading);
font-size: var(--text-4xl);
color: var(--text-primary);
margin-bottom: 1.5rem;
text-transform: uppercase;
letter-spacing: 2px;
}
.error-description {
margin-bottom: 2rem;
}
.error-description .lead {
font-size: var(--text-xl);
color: var(--accent-red-light);
font-style: italic;
margin-bottom: 1rem;
}
.error-description p {
color: var(--text-secondary);
line-height: 1.8;
}
.error-divider {
font-size: var(--text-2xl);
color: var(--border-ornate);
margin: 2rem 0;
}
.error-actions {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.error-actions .btn {
width: 100%;
}
.error-help {
padding-top: 2rem;
border-top: 1px solid var(--border-primary);
}
.help-text {
font-size: var(--text-sm);
color: var(--text-muted);
line-height: 1.6;
}
.text-link {
color: var(--accent-gold);
text-decoration: none;
transition: color 0.2s;
}
.text-link:hover {
color: var(--accent-gold-hover);
text-decoration: underline;
}
.error-debug {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid var(--border-primary);
}
.debug-summary {
cursor: pointer;
color: var(--text-muted);
font-size: var(--text-sm);
font-family: var(--font-mono);
padding: 0.5rem;
background: var(--bg-tertiary);
border-radius: 4px;
user-select: none;
}
.debug-summary:hover {
color: var(--text-secondary);
}
.debug-content {
margin-top: 1rem;
padding: 1rem;
background: var(--bg-tertiary);
border-radius: 4px;
text-align: left;
font-family: var(--font-mono);
font-size: var(--text-sm);
}
.debug-content p {
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.debug-content strong {
color: var(--accent-gold);
}
@media (min-width: 640px) {
.error-actions {
flex-direction: row;
justify-content: center;
}
.error-actions .btn {
width: auto;
}
}
</style>
{% endblock %}

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