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