Compare commits

..

2 Commits

9 changed files with 99 additions and 30 deletions

View File

@@ -18,11 +18,11 @@ var RoleSelect = (function () {
var _postTrayDelay = 3000; var _postTrayDelay = 3000;
var ROLES = [ var ROLES = [
{ code: "PC", name: "Player", element: "Fire" },
{ code: "BC", name: "Builder", element: "Stone" },
{ code: "SC", name: "Shepherd", element: "Air" }, { code: "SC", name: "Shepherd", element: "Air" },
{ code: "AC", name: "Alchemist", element: "Water" }, { code: "PC", name: "Player", element: "Fire" },
{ code: "NC", name: "Narrator", element: "Time" }, { code: "NC", name: "Narrator", element: "Time" },
{ code: "AC", name: "Alchemist", element: "Water" },
{ code: "BC", name: "Builder", element: "Stone" },
{ code: "EC", name: "Economist", element: "Space" }, { code: "EC", name: "Economist", element: "Space" },
]; ];
@@ -154,7 +154,7 @@ var RoleSelect = (function () {
var back = document.createElement("div"); var back = document.createElement("div");
back.className = "card-back"; back.className = "card-back";
back.textContent = "?"; back.textContent = "ROLE";
var front = document.createElement("div"); var front = document.createElement("div");
front.className = "card-front"; front.className = "card-front";
@@ -177,7 +177,7 @@ var RoleSelect = (function () {
card.classList.add("guard-active"); card.classList.add("guard-active");
window.showGuard( window.showGuard(
card, card,
"Start round 1 as<br>" + role.name + " (" + role.code + ") …?", "Start round 1<br>as " + role.name + " (" + role.code + ") …?",
function () { // confirm function () { // confirm
card.classList.remove("guard-active"); card.classList.remove("guard-active");
selectRole(role.code); selectRole(role.code);
@@ -208,6 +208,10 @@ var RoleSelect = (function () {
function handleAllRolesFilled() { function handleAllRolesFilled() {
var wrap = document.getElementById('id_pick_sigs_wrap'); var wrap = document.getElementById('id_pick_sigs_wrap');
if (wrap) wrap.style.display = ''; if (wrap) wrap.style.display = '';
var stack = document.querySelector('.card-stack');
if (stack) stack.remove();
var trayWrap = document.getElementById('id_tray_wrap');
if (trayWrap) trayWrap.classList.remove('role-select-phase');
} }
function handleSigSelectStarted() { function handleSigSelectStarted() {

View File

@@ -7,7 +7,9 @@
if (!scene || !container) return; if (!scene || !container) return;
var w = container.clientWidth, h = container.clientHeight; var w = container.clientWidth, h = container.clientHeight;
if (!w || !h) return; if (!w || !h) return;
scene.style.transform = 'scale(' + Math.min(w / SCENE_W, h / SCENE_H) + ')'; var scale = Math.min(w / SCENE_W, h / SCENE_H);
scene.style.transform = 'scale(' + scale + ')';
document.documentElement.style.setProperty('--table-scale', scale);
} }
if (document.readyState === 'loading') { if (document.readyState === 'loading') {

View File

@@ -762,6 +762,7 @@ class RoomViewAllRolesFilledTest(TestCase):
response = self.client.get(reverse("epic:room", kwargs={"room_id": self.room.id})) response = self.client.get(reverse("epic:room", kwargs={"room_id": self.room.id}))
parsed = self.lxml.fromstring(response.content) parsed = self.lxml.fromstring(response.content)
[_] = parsed.cssselect("#id_pick_sigs_btn") [_] = parsed.cssselect("#id_pick_sigs_btn")
self.assertEqual(parsed.cssselect(".card-stack"), [])
def test_pick_sigs_btn_hidden_during_role_select(self): def test_pick_sigs_btn_hidden_during_role_select(self):
# Clear one role — still mid-pick, wrap must be hidden # Clear one role — still mid-pick, wrap must be hidden

View File

@@ -189,6 +189,8 @@ def _role_select_context(room, user):
starter_roles = list( starter_roles = list(
room.table_seats.exclude(role__isnull=True).values_list("role", flat=True) room.table_seats.exclude(role__isnull=True).values_list("role", flat=True)
) )
if len(starter_roles) == 6:
card_stack_state = None
_action_order = {r: i for i, r in enumerate(["PC", "NC", "EC", "SC", "AC", "BC"])} _action_order = {r: i for i, r in enumerate(["PC", "NC", "EC", "SC", "AC", "BC"])}
assigned_seats = ( assigned_seats = (
sorted( sorted(

View File

@@ -846,4 +846,53 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
"Tray should be closed after turn advances" "Tray should be closed after turn advances"
)) ))
# ------------------------------------------------------------------ #
# Test 7 — PICK SIGS appears + card stack removed on last role #
# ------------------------------------------------------------------ #
def test_pick_sigs_appears_and_card_stack_removed_on_last_role(self):
"""When the sixth and final role is confirmed, the all_roles_filled
WS event makes the PICK SIGS button visible and removes the card
stack from the DOM entirely."""
emails = [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
]
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Last Role Test", owner=founder)
_fill_room_via_orm(room, emails)
room.table_status = Room.ROLE_SELECT
room.save()
# Pre-assign 5 roles (slots 26); founder (slot 1) is the final picker.
pre_assigned = {2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room,
gamer=slot.gamer,
slot_number=slot.slot_number,
role=pre_assigned.get(slot.slot_number),
)
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
))
# Founder picks the last remaining role (PC — the only card in the fan).
self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard()
# PICK SIGS wrap must become visible via the all_roles_filled WS event.
self.wait_for(lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_pick_sigs_wrap").get_attribute("style"),
))
# Card stack must be removed from the DOM entirely.
self.wait_for(lambda: self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, ".card-stack")), 0,
))

View File

@@ -451,6 +451,7 @@ body {
font-size: 0.85rem; font-size: 0.85rem;
color: rgba(var(--secUser), 0.9); color: rgba(var(--secUser), 0.9);
text-align: center; text-align: center;
white-space: nowrap;
} }
.guard-actions { .guard-actions {

View File

@@ -624,12 +624,25 @@ html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: aut
background: rgba(var(--terUser), 1); background: rgba(var(--terUser), 1);
cursor: default; cursor: default;
transition: box-shadow 0.2s ease; transition: box-shadow 0.2s ease;
position: relative;
&::before {
content: "ROLE";
font-size: 0.6rem;
letter-spacing: 0.14em;
color: rgba(var(--quiUser), 1);
}
.fa-ban {
position: absolute;
font-size: 1.4rem;
}
&[data-state="eligible"] { &[data-state="eligible"] {
cursor: pointer; cursor: pointer;
border-color: rgba(var(--terUser), 1); border: 2px solid rgba(var(--quiUser), 1);
box-shadow: box-shadow:
0 0 0.6rem rgba(var(--ninUser), 0.6), 0 0 0.6rem rgba(var(--ninUser), 1),
0 0 1.6rem rgba(var(--secUser), 0.25); 0 0 1.6rem rgba(var(--secUser), 0.25);
} }
@@ -640,10 +653,11 @@ html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: aut
} }
// ─── Card dimensions ─────────────────────────────────────────────────────── // ─── Card dimensions ───────────────────────────────────────────────────────
// Role cards are landscape format — wider than tall — and the largest card type. // Base size matches the card-stack footprint; --table-scale (set by scaleTable()
// Sig cards (half this size) will be layered on top during SIG_SELECT. // in room.js) stretches both the grid and individual cards to stay in sync with
$card-w: 160px; // the scene transform. Fallback of 1 keeps the fan functional if JS hasn't run.
$card-h: 110px; $card-w: 90px;
$card-h: 60px;
// ─── Role select modal ───────────────────────────────────────────────────── // ─── Role select modal ─────────────────────────────────────────────────────
@@ -662,27 +676,17 @@ $card-h: 110px;
#id_role_select { #id_role_select {
// Always a 3×2 grid — 6 landscape cards in a row would overflow any viewport. // Always a 3×2 grid — 6 landscape cards in a row would overflow any viewport.
display: grid; display: grid;
grid-template-columns: repeat(3, $card-w); grid-template-columns: repeat(3, calc(#{$card-w} * var(--table-scale, 1)));
gap: 1rem; gap: 1rem;
pointer-events: none; pointer-events: none;
// Narrow portrait: scale cards down so the 3-col grid still fits
@media (max-width: 600px) {
grid-template-columns: repeat(3, 110px);
gap: 0.75rem;
.card {
width: 110px;
height: 75px;
}
}
} }
// ─── Card component ──────────────────────────────────────────────────────── // ─── Card component ────────────────────────────────────────────────────────
.card { .card {
width: $card-w; width: calc(#{$card-w} * var(--table-scale, 1));
height: $card-h; height: calc(#{$card-h} * var(--table-scale, 1));
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
pointer-events: auto; pointer-events: auto;
@@ -707,7 +711,8 @@ $card-h: 110px;
.card-back { .card-back {
transform: rotateY(0deg); transform: rotateY(0deg);
font-size: 1.5rem; font-size: calc(0.66rem * var(--table-scale, 1));
letter-spacing: 0.14em;
color: rgba(var(--quiUser), 1); color: rgba(var(--quiUser), 1);
background: rgba(var(--terUser), 1); background: rgba(var(--terUser), 1);
border: 2px solid rgba(var(--quiUser), 1); border: 2px solid rgba(var(--quiUser), 1);
@@ -719,7 +724,7 @@ $card-h: 110px;
text-align: center; text-align: center;
.card-role-name { .card-role-name {
font-size: 0.75rem; font-size: calc(0.66rem * var(--table-scale, 1));
color: rgba(var(--quaUser), 1); color: rgba(var(--quaUser), 1);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;

View File

@@ -95,7 +95,7 @@
{% include "apps/gameboard/_partials/_gatekeeper.html" %} {% include "apps/gameboard/_partials/_gatekeeper.html" %}
{% endif %} {% endif %}
{% if room.table_status %} {% if room.table_status %}
<div id="id_tray_wrap"{% if room.table_status == "ROLE_SELECT" %} class="role-select-phase"{% endif %}> <div id="id_tray_wrap"{% if room.table_status == "ROLE_SELECT" and starter_roles|length < 6 %} class="role-select-phase"{% endif %}>
<div id="id_tray_handle"> <div id="id_tray_handle">
<div id="id_tray_grip"></div> <div id="id_tray_grip"></div>
<button id="id_tray_btn" aria-label="Open seat tray"> <button id="id_tray_btn" aria-label="Open seat tray">

View File

@@ -84,7 +84,8 @@
var rawLeft = rect.left + rect.width / 2; var rawLeft = rect.left + rect.width / 2;
var cleft = Math.max(pw / 2 + 8, Math.min(rawLeft, window.innerWidth - pw / 2 - 8)); var cleft = Math.max(pw / 2 + 8, Math.min(rawLeft, window.innerWidth - pw / 2 - 8));
portal.style.left = Math.round(cleft) + 'px'; portal.style.left = Math.round(cleft) + 'px';
if (rect.top > 120) { var cardCenterY = rect.top + rect.height / 2;
if (cardCenterY < window.innerHeight / 2) {
portal.style.top = Math.round(rect.top) + 'px'; portal.style.top = Math.round(rect.top) + 'px';
portal.style.transform = 'translate(-50%, calc(-100% - 0.5rem))'; portal.style.transform = 'translate(-50%, calc(-100% - 0.5rem))';
} else { } else {
@@ -123,7 +124,11 @@
document.addEventListener('click', function (e) { document.addEventListener('click', function (e) {
if (!portal.classList.contains('active')) return; if (!portal.classList.contains('active')) return;
if (portal.contains(e.target)) return; if (portal.contains(e.target)) return;
e.stopPropagation(); // 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();
dismiss(); dismiss();
}, true); }, true);
// Intercept [data-confirm] buttons (capture phase, before form submits) // Intercept [data-confirm] buttons (capture phase, before form submits)