Compare commits
2 Commits
c00288e256
...
74f63a7721
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74f63a7721 | ||
|
|
bd3d7fc7bd |
@@ -18,11 +18,11 @@ var RoleSelect = (function () {
|
||||
var _postTrayDelay = 3000;
|
||||
|
||||
var ROLES = [
|
||||
{ code: "PC", name: "Player", element: "Fire" },
|
||||
{ code: "BC", name: "Builder", element: "Stone" },
|
||||
{ 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: "AC", name: "Alchemist", element: "Water" },
|
||||
{ code: "BC", name: "Builder", element: "Stone" },
|
||||
{ code: "EC", name: "Economist", element: "Space" },
|
||||
];
|
||||
|
||||
@@ -154,7 +154,7 @@ var RoleSelect = (function () {
|
||||
|
||||
var back = document.createElement("div");
|
||||
back.className = "card-back";
|
||||
back.textContent = "?";
|
||||
back.textContent = "ROLE";
|
||||
|
||||
var front = document.createElement("div");
|
||||
front.className = "card-front";
|
||||
@@ -177,7 +177,7 @@ var RoleSelect = (function () {
|
||||
card.classList.add("guard-active");
|
||||
window.showGuard(
|
||||
card,
|
||||
"Start round 1 as<br>" + role.name + " (" + role.code + ") …?",
|
||||
"Start round 1<br>as " + role.name + " (" + role.code + ") …?",
|
||||
function () { // confirm
|
||||
card.classList.remove("guard-active");
|
||||
selectRole(role.code);
|
||||
@@ -208,6 +208,10 @@ var RoleSelect = (function () {
|
||||
function handleAllRolesFilled() {
|
||||
var wrap = document.getElementById('id_pick_sigs_wrap');
|
||||
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() {
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
if (!scene || !container) return;
|
||||
var w = container.clientWidth, h = container.clientHeight;
|
||||
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') {
|
||||
|
||||
@@ -762,6 +762,7 @@ class RoomViewAllRolesFilledTest(TestCase):
|
||||
response = self.client.get(reverse("epic:room", kwargs={"room_id": self.room.id}))
|
||||
parsed = self.lxml.fromstring(response.content)
|
||||
[_] = parsed.cssselect("#id_pick_sigs_btn")
|
||||
self.assertEqual(parsed.cssselect(".card-stack"), [])
|
||||
|
||||
def test_pick_sigs_btn_hidden_during_role_select(self):
|
||||
# Clear one role — still mid-pick, wrap must be hidden
|
||||
|
||||
@@ -189,6 +189,8 @@ def _role_select_context(room, user):
|
||||
starter_roles = list(
|
||||
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"])}
|
||||
assigned_seats = (
|
||||
sorted(
|
||||
|
||||
@@ -846,4 +846,53 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
|
||||
"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 2–6); 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,
|
||||
))
|
||||
|
||||
|
||||
|
||||
@@ -451,6 +451,7 @@ body {
|
||||
font-size: 0.85rem;
|
||||
color: rgba(var(--secUser), 0.9);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.guard-actions {
|
||||
|
||||
@@ -624,12 +624,25 @@ html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: aut
|
||||
background: rgba(var(--terUser), 1);
|
||||
cursor: default;
|
||||
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"] {
|
||||
cursor: pointer;
|
||||
border-color: rgba(var(--terUser), 1);
|
||||
border: 2px solid rgba(var(--quiUser), 1);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -640,10 +653,11 @@ html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: aut
|
||||
}
|
||||
|
||||
// ─── Card dimensions ───────────────────────────────────────────────────────
|
||||
// Role cards are landscape format — wider than tall — and the largest card type.
|
||||
// Sig cards (half this size) will be layered on top during SIG_SELECT.
|
||||
$card-w: 160px;
|
||||
$card-h: 110px;
|
||||
// Base size matches the card-stack footprint; --table-scale (set by scaleTable()
|
||||
// in room.js) stretches both the grid and individual cards to stay in sync with
|
||||
// the scene transform. Fallback of 1 keeps the fan functional if JS hasn't run.
|
||||
$card-w: 90px;
|
||||
$card-h: 60px;
|
||||
|
||||
// ─── Role select modal ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -662,27 +676,17 @@ $card-h: 110px;
|
||||
#id_role_select {
|
||||
// Always a 3×2 grid — 6 landscape cards in a row would overflow any viewport.
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, $card-w);
|
||||
grid-template-columns: repeat(3, calc(#{$card-w} * var(--table-scale, 1)));
|
||||
gap: 1rem;
|
||||
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 {
|
||||
width: $card-w;
|
||||
height: $card-h;
|
||||
width: calc(#{$card-w} * var(--table-scale, 1));
|
||||
height: calc(#{$card-h} * var(--table-scale, 1));
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
@@ -707,7 +711,8 @@ $card-h: 110px;
|
||||
|
||||
.card-back {
|
||||
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);
|
||||
background: rgba(var(--terUser), 1);
|
||||
border: 2px solid rgba(var(--quiUser), 1);
|
||||
@@ -719,7 +724,7 @@ $card-h: 110px;
|
||||
text-align: center;
|
||||
|
||||
.card-role-name {
|
||||
font-size: 0.75rem;
|
||||
font-size: calc(0.66rem * var(--table-scale, 1));
|
||||
color: rgba(var(--quaUser), 1);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
{% include "apps/gameboard/_partials/_gatekeeper.html" %}
|
||||
{% endif %}
|
||||
{% 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_grip"></div>
|
||||
<button id="id_tray_btn" aria-label="Open seat tray">
|
||||
|
||||
@@ -84,7 +84,8 @@
|
||||
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';
|
||||
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.transform = 'translate(-50%, calc(-100% - 0.5rem))';
|
||||
} else {
|
||||
@@ -123,7 +124,11 @@
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!portal.classList.contains('active')) 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();
|
||||
}, true);
|
||||
// Intercept [data-confirm] buttons (capture phase, before form submits)
|
||||
|
||||
Reference in New Issue
Block a user