2026-03-02 13:57:03 -05:00
|
|
|
{% load compress %}
|
|
|
|
|
{% load static %}
|
2026-03-07 15:05:49 -05:00
|
|
|
|
2026-03-02 13:57:03 -05:00
|
|
|
|
2026-01-13 20:58:05 -05:00
|
|
|
<!DOCTYPE html>
|
2026-03-02 13:57:03 -05:00
|
|
|
<html lang="en">
|
2026-01-13 20:58:05 -05:00
|
|
|
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<meta name="author" content="Disco DeDisco">
|
|
|
|
|
<meta name="robots" content="noindex, nofollow">
|
2026-01-29 15:21:54 -05:00
|
|
|
<title>Earthman RPG | {% block title_text %}{% endblock title_text %}</title>
|
2026-03-02 13:57:03 -05:00
|
|
|
{% compress css %}
|
|
|
|
|
<link type="text/x-scss" rel="stylesheet" href="{% static 'scss/core.scss' %}">
|
|
|
|
|
{% endcompress %}
|
2026-03-09 14:40:34 -04:00
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
2026-01-13 20:58:05 -05:00
|
|
|
</head>
|
|
|
|
|
|
2026-03-06 18:14:01 -05:00
|
|
|
<body class="{{ user_palette }} {{ page_class|default:'' }}">
|
2026-01-13 20:58:05 -05:00
|
|
|
<div class="container">
|
2026-03-07 15:05:49 -05:00
|
|
|
{% include "core/_partials/_navbar.html" %}
|
2026-01-30 17:23:07 -05:00
|
|
|
|
2026-04-04 13:49:48 -04:00
|
|
|
<div class="row">
|
|
|
|
|
<div class="col-lg-6">
|
|
|
|
|
<h2 >{% block header_text %}{% endblock header_text %}</h2>
|
|
|
|
|
{% block extra_header %}
|
|
|
|
|
{% endblock extra_header %}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-30 17:23:07 -05:00
|
|
|
{% if messages %}
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="col-md-12">
|
|
|
|
|
{% for message in messages %}
|
|
|
|
|
{% if message.level_tag == 'success' %}
|
|
|
|
|
<div class="alert alert-success">{{ message }}</div>
|
|
|
|
|
{% else %}
|
|
|
|
|
<div class="alert alert-warning">{{ message }}</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
2026-01-29 15:21:54 -05:00
|
|
|
|
2026-02-08 21:43:58 -05:00
|
|
|
{% block content %}
|
|
|
|
|
{% endblock content %}
|
2026-01-13 20:58:05 -05:00
|
|
|
|
|
|
|
|
</div>
|
2026-03-07 15:05:49 -05:00
|
|
|
|
|
|
|
|
{% include "core/_partials/_footer.html" %}
|
2026-01-25 22:40:57 -05:00
|
|
|
|
2026-03-15 01:17:09 -04:00
|
|
|
{% if user.is_authenticated %}
|
|
|
|
|
<button id="id_kit_btn" data-kit-url="{% url 'kit_bag' %}" aria-label="Open Kit Bag">
|
|
|
|
|
<i class="fa-solid fa-briefcase"></i>
|
|
|
|
|
</button>
|
|
|
|
|
{% endif %}
|
|
|
|
|
<dialog id="id_kit_bag_dialog"></dialog>
|
|
|
|
|
|
2026-03-23 19:31:57 -04:00
|
|
|
<div id="id_guard_portal">
|
|
|
|
|
<span class="guard-message"></span>
|
|
|
|
|
<div class="guard-actions">
|
|
|
|
|
<button class="btn btn-confirm guard-yes" type="button">OK</button>
|
|
|
|
|
<button class="btn btn-cancel guard-no" type="button">NVM</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-04 15:13:16 -05:00
|
|
|
{% block scripts %}
|
|
|
|
|
{% endblock scripts %}
|
2026-03-23 19:31:57 -04:00
|
|
|
<script>
|
|
|
|
|
(function () {
|
|
|
|
|
var portal = null;
|
|
|
|
|
var _cb = null;
|
|
|
|
|
var _onDismiss = null;
|
|
|
|
|
|
2026-04-05 16:54:03 -04:00
|
|
|
function show(anchor, message, callback, onDismiss, options) {
|
2026-03-23 19:31:57 -04:00
|
|
|
if (!portal) return;
|
2026-04-05 16:54:03 -04:00
|
|
|
options = options || {};
|
2026-03-23 19:31:57 -04:00
|
|
|
_cb = callback;
|
|
|
|
|
_onDismiss = onDismiss || null;
|
|
|
|
|
portal.querySelector('.guard-message').innerHTML = message;
|
|
|
|
|
portal.classList.add('active');
|
|
|
|
|
var rect = anchor.getBoundingClientRect();
|
|
|
|
|
var pw = portal.offsetWidth;
|
|
|
|
|
var rawLeft = rect.left + rect.width / 2;
|
|
|
|
|
var cleft = Math.max(pw / 2 + 8, Math.min(rawLeft, window.innerWidth - pw / 2 - 8));
|
|
|
|
|
portal.style.left = Math.round(cleft) + 'px';
|
2026-04-05 01:23:20 -04:00
|
|
|
var cardCenterY = rect.top + rect.height / 2;
|
2026-04-05 16:54:03 -04:00
|
|
|
// Default: upper half → below (avoids viewport top edge for navbar/fixed buttons).
|
|
|
|
|
// invertY: upper half → above (for modal grids where tooltip should fly away from centre).
|
|
|
|
|
var showBelow = (cardCenterY < window.innerHeight / 2);
|
|
|
|
|
if (options.invertY) showBelow = !showBelow;
|
|
|
|
|
if (showBelow) {
|
2026-03-23 19:31:57 -04:00
|
|
|
portal.style.top = Math.round(rect.bottom) + 'px';
|
|
|
|
|
portal.style.transform = 'translate(-50%, 0.5rem)';
|
2026-04-05 16:00:52 -04:00
|
|
|
} else {
|
|
|
|
|
portal.style.top = Math.round(rect.top) + 'px';
|
|
|
|
|
portal.style.transform = 'translate(-50%, calc(-100% - 0.5rem))';
|
2026-03-23 19:31:57 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dismiss() {
|
|
|
|
|
if (!portal) return;
|
|
|
|
|
var od = _onDismiss;
|
|
|
|
|
portal.classList.remove('active');
|
|
|
|
|
_cb = null;
|
|
|
|
|
_onDismiss = null;
|
|
|
|
|
if (od) od();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function doConfirm() {
|
|
|
|
|
var cb = _cb;
|
|
|
|
|
portal.classList.remove('active');
|
|
|
|
|
_cb = null;
|
|
|
|
|
_onDismiss = null;
|
|
|
|
|
if (cb) cb();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
|
|
|
portal = document.getElementById('id_guard_portal');
|
|
|
|
|
if (!portal) return;
|
|
|
|
|
portal.querySelector('.guard-yes').addEventListener('click', doConfirm);
|
|
|
|
|
portal.querySelector('.guard-no').addEventListener('click', dismiss);
|
|
|
|
|
document.addEventListener('keydown', function (e) {
|
|
|
|
|
if (e.key === 'Escape') dismiss();
|
|
|
|
|
});
|
|
|
|
|
// Outside-click to dismiss — capture phase + stopPropagation
|
|
|
|
|
// prevents the click from cascading to backdrop listeners (e.g. closeFan)
|
|
|
|
|
document.addEventListener('click', function (e) {
|
|
|
|
|
if (!portal.classList.contains('active')) return;
|
|
|
|
|
if (portal.contains(e.target)) return;
|
2026-04-05 01:23:20 -04:00
|
|
|
// If clicking a card, let the event through so the card's
|
|
|
|
|
// own handler immediately opens the guard on the new target.
|
|
|
|
|
// For any other outside click, stop propagation to prevent
|
|
|
|
|
// the backdrop from also closing the fan.
|
|
|
|
|
if (!e.target.closest('.card')) e.stopPropagation();
|
2026-03-23 19:31:57 -04:00
|
|
|
dismiss();
|
|
|
|
|
}, true);
|
|
|
|
|
// Intercept [data-confirm] buttons (capture phase, before form submits)
|
|
|
|
|
document.addEventListener('click', function (e) {
|
|
|
|
|
var btn = e.target.closest('[data-confirm]');
|
|
|
|
|
if (!btn) return;
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
var form = btn.closest('form');
|
|
|
|
|
show(btn, btn.dataset.confirm, function () {
|
|
|
|
|
if (form) form.submit();
|
2026-04-05 16:00:52 -04:00
|
|
|
else if (btn.dataset.href) window.location.href = btn.dataset.href;
|
2026-03-23 19:31:57 -04:00
|
|
|
});
|
|
|
|
|
}, true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
window.showGuard = show;
|
|
|
|
|
}());
|
|
|
|
|
</script>
|
2026-03-04 15:13:16 -05:00
|
|
|
<script src="{% static "vendor/htmx.min.js" %}"></script>
|
Django Channels role-select sprint: turn_changed, roles_revealed, role_select_start consumer handlers; WS URL changed from room_slug to room_id UUID; TableSeat model - room, gamer, slot_number, role, role_revealed, seat_position fields; Room.table_status field with ROLE_SELECT, SIG_SELECT, IN_GAME choices; migration 0006_table_status_and_table_seat; pick_roles and select_role views; _role_select_context helper; _notify_turn_changed, _notify_roles_revealed, _notify_role_select_start notifiers; all gate-mutation views now call _notify_gate_update; ChannelsFunctionalTest base class with serve_static, screenshot, dump helpers; SQLite TEST NAME set to file path for ChannelsLiveServerTestCase; InMemoryChannelLayer added to test CHANNEL_LAYERS settings; FT 5 and FT 6 now passing - active seat arc and turn advance via WS, no page refresh; room.js, gatekeeper.js, role-select.js added to apps/epic/static; applets.js, game-kit.js, dashboard.js, wallet.js relocated to app-scoped static dirs; room.html: hex table, table-seat arcs, card-stack, inventory panel, role-card hand, WS scripts; _room.scss: room-shell flex layout, .table-hex polygon clip-path, .table-seat and .seat-card-arc, .card-stack eligible/ineligible states, .card flip animation, .inv-role-card stacked hand, .role-select-backdrop; gear btn and room menu always position: fixed; 375 tests, 0 skipped
2026-03-17 00:24:23 -04:00
|
|
|
<script src="{% static "apps/applets/applets.js" %}"></script>
|
|
|
|
|
<script src="{% static "apps/dashboard/game-kit.js" %}"></script>
|
2026-03-04 15:13:16 -05:00
|
|
|
<script>
|
2026-04-02 14:51:08 -04:00
|
|
|
document.cookie = 'user_tz=' + Intl.DateTimeFormat().resolvedOptions().timeZone + '; path=/; SameSite=Lax';
|
2026-03-04 15:13:16 -05:00
|
|
|
document.body.addEventListener('htmx:configRequest', function(evt) {
|
|
|
|
|
evt.detail.headers['X-CSRFToken'] = getCookie('csrftoken');
|
|
|
|
|
});
|
|
|
|
|
function getCookie(name) {
|
|
|
|
|
let val = null;
|
|
|
|
|
if (document.cookie) {
|
|
|
|
|
for (let c of document.cookie.split(';')) {
|
|
|
|
|
c = c.trim();
|
|
|
|
|
if (c.startsWith(name + '=')) {
|
|
|
|
|
val = decodeURIComponent(c.substring(name.length + 1));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return val;
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
2026-01-03 19:20:41 -05:00
|
|
|
</html>
|