demo'd old inventory area in room.html to make way for new content (hex table now centered in view); old test suite now targets Role card in #id_tray cells where appropriate, or skips Sig card select until aforementioned new feature deployed; new scripts & jasmine tests too; removed one irrelevant test case from apps.epic.tests.ITs.test_views.SelectRoleViewTest

This commit is contained in:
Disco DeDisco
2026-03-30 16:42:23 -04:00
parent 299a806862
commit 8b006be138
12 changed files with 553 additions and 374 deletions

View File

@@ -3,6 +3,11 @@ var RoleSelect = (function () {
// ahead of the fetch response doesn't get overridden by Tray.open().
var _turnChangedBeforeFetch = false;
// Set to true while placeCard animation is running. handleTurnChanged
// defers its work until the animation completes.
var _animationPending = false;
var _pendingTurnChange = null;
var ROLES = [
{ code: "PC", name: "Player", element: "Fire" },
{ code: "BC", name: "Builder", element: "Stone" },
@@ -27,18 +32,10 @@ var RoleSelect = (function () {
if (backdrop) backdrop.remove();
}
function selectRole(roleCode, cardEl) {
function selectRole(roleCode) {
_turnChangedBeforeFetch = false; // fresh selection, reset the race flag
var invCard = cardEl.cloneNode(true);
invCard.classList.add("flipped");
// strip old event listeners from the clone by replacing with a clean copy
var clean = invCard.cloneNode(true);
closeFan();
var invSlot = document.getElementById("id_inv_role_card");
if (invSlot) invSlot.appendChild(clean);
// Immediately lock the stack — do not wait for WS turn_changed
var stack = document.querySelector(".card-stack[data-starter-roles]");
if (stack) {
@@ -60,24 +57,27 @@ var RoleSelect = (function () {
}).then(function (response) {
if (!response.ok) {
// Server rejected (role already taken) — undo optimistic update
if (invSlot && invSlot.contains(clean)) invSlot.removeChild(clean);
if (stack) {
stack.dataset.starterRoles = stack.dataset.starterRoles
.split(",").filter(function (r) { return r.trim() !== roleCode; }).join(",");
}
openFan();
} else {
// Place role card in tray grid and open the tray
var grid = document.getElementById("id_tray_grid");
if (grid) {
var trayCard = document.createElement("div");
trayCard.className = "tray-cell tray-role-card";
trayCard.dataset.role = roleCode;
grid.insertBefore(trayCard, grid.firstChild);
}
// Only open if turn_changed hasn't already arrived and closed it.
if (typeof Tray !== "undefined" && !_turnChangedBeforeFetch) {
Tray.open();
// Always animate the role card into the tray, even if turn_changed
// already arrived. placeCard opens the tray, arcs the card in,
// then force-closes — so the user always sees their role card land.
// If turn_changed arrived before the fetch, handleTurnChanged already
// ran; _pendingTurnChange will be null and onComplete is a no-op.
if (typeof Tray !== "undefined") {
_animationPending = true;
Tray.placeCard(roleCode, function () {
_animationPending = false;
if (_pendingTurnChange) {
var ev = _pendingTurnChange;
_pendingTurnChange = null;
handleTurnChanged(ev);
}
});
}
}
});
@@ -135,7 +135,7 @@ var RoleSelect = (function () {
"Start round 1 as<br>" + role.name + " (" + role.code + ") …?",
function () { // confirm
card.classList.remove("guard-active");
selectRole(role.code, card);
selectRole(role.code);
},
function () { // dismiss (NVM / outside click)
card.classList.remove("guard-active");
@@ -165,9 +165,13 @@ var RoleSelect = (function () {
}
function handleTurnChanged(event) {
// If a placeCard animation is running, defer until it completes.
if (_animationPending) {
_pendingTurnChange = event;
return;
}
var active = String(event.detail.active_slot);
var invSlot = document.getElementById("id_inv_role_card");
if (invSlot) invSlot.innerHTML = "";
// Force-close tray instantly so it never obscures the next player's card-stack.
// Also set the race flag so the fetch .then() doesn't re-open if it arrives late.
@@ -220,8 +224,13 @@ var RoleSelect = (function () {
}
return {
openFan: openFan,
closeFan: closeFan,
setReload: function (fn) { _reload = fn; },
openFan: openFan,
closeFan: closeFan,
setReload: function (fn) { _reload = fn; },
// Testing hook — resets animation-pause state between Jasmine specs
_testReset: function () {
_animationPending = false;
_pendingTurnChange = null;
},
};
}());

View File

@@ -224,6 +224,37 @@ var Tray = (function () {
});
}
// _arcIn — add .arc-in to cardEl, wait for animationend, remove it, call onComplete.
function _arcIn(cardEl, onComplete) {
cardEl.classList.add('arc-in');
cardEl.addEventListener('animationend', function handler() {
cardEl.removeEventListener('animationend', handler);
cardEl.classList.remove('arc-in');
if (onComplete) onComplete();
});
}
// placeCard(roleCode, onComplete) — mark the first tray cell with the role,
// open the tray, arc-in the cell, then force-close. Calls onComplete after.
// The grid always contains exactly 8 .tray-cell elements (from the template);
// the first one receives .tray-role-card and data-role instead of a new element
// being inserted, so the cell count never changes.
function placeCard(roleCode, onComplete) {
if (!_grid) { if (onComplete) onComplete(); return; }
var firstCell = _grid.querySelector('.tray-cell');
if (!firstCell) { if (onComplete) onComplete(); return; }
firstCell.classList.add('tray-role-card');
firstCell.dataset.role = roleCode;
firstCell.textContent = roleCode;
open();
_arcIn(firstCell, function () {
forceClose();
if (onComplete) onComplete();
});
}
function _startDrag(clientX, clientY) {
_dragHandled = false;
if (_wrap) _wrap.classList.add('tray-dragging');
@@ -428,6 +459,14 @@ var Tray = (function () {
_onBtnClick = null;
}
_cancelPendingHide();
// Clear any role-card state from tray cells (Jasmine afterEach)
if (_grid) {
_grid.querySelectorAll('.tray-cell').forEach(function (el) {
el.classList.remove('tray-role-card', 'arc-in');
el.textContent = '';
delete el.dataset.role;
});
}
_wrap = null;
_btn = null;
_tray = null;
@@ -446,6 +485,7 @@ var Tray = (function () {
close: close,
forceClose: forceClose,
isOpen: isOpen,
placeCard: placeCard,
reset: reset,
_testSetLandscape: function (v) { _landscapeOverride = v; },
};

View File

@@ -655,43 +655,6 @@ class SelectRoleViewTest(TestCase):
)
class RevealPhaseRenderingTest(TestCase):
def setUp(self):
self.founder = User.objects.create(email="founder@test.io")
self.room = Room.objects.create(name="Test Room", owner=self.founder)
gamers = [self.founder]
for i in range(2, 7):
gamers.append(User.objects.create(email=f"g{i}@test.io"))
roles = ["PC", "BC", "SC", "AC", "NC", "EC"]
for i, (gamer, role) in enumerate(zip(gamers, roles), start=1):
TableSeat.objects.create(
room=self.room, gamer=gamer, slot_number=i,
role=role, role_revealed=True,
)
self.room.gate_status = Room.OPEN
self.room.table_status = Room.SIG_SELECT
self.room.save()
self.client.force_login(self.founder)
def test_face_up_role_cards_rendered_when_sig_select(self):
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
)
self.assertContains(response, "face-up")
def test_inv_role_card_slot_present(self):
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
)
self.assertContains(response, "id_inv_role_card")
def test_partner_indicator_present_when_sig_select(self):
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
)
self.assertContains(response, "partner-indicator")
class RoomActionsViewTest(TestCase):
def setUp(self):
self.owner = User.objects.create(email="owner@test.io")