btn-primary label renames + stage-card polarity color refinements — two interleaved threads from one session, committing together since both touch sig + sea stage cards ; LABEL RENAMES: PICK SIGS → SCAN SIGS (room.html #id_pick_sigs_btn), PICK SKY → CAST SKY (room.html #id_pick_sky_btn × 2), PICK SEA → DRAW SEA (room.html #id_pick_sea_btn), TAKE SIG → SAVE SIG (sig-select.js _takeSigBtn.textContent × 2 callsites + section comment) — Element IDs (id_pick_sky_btn etc.), URL names (epic:pick_sigs, epic:pick_sky), and Python state enums (TableStatus.PICK_SKY, PICK_SEA, SIG_SELECT) intentionally retained as stable identifiers; the renamed text is purely the .btn-primary user-facing label ; FT + IT mentions of the old labels swept in test_game_room_select_{sig,sky,sea,role}.py, test_billboard.py, setup_sea_session.py mgmt cmd, apps/epic/{views,utils,models,tasks,tests/integrated/test_views}.py, SigSelectSpec.js, sky_overlay/sea_overlay/dashboard/sky.html, _card-deck.scss, _sky.scss — all docstring/comment references updated for cascade-grep cleanliness ; STAGE-CARD COLOR + CLASS REFINEMENTS (earlier in session): sig-stage card text colour split per polarity — gravity gets --terUser on .fan-card-name + .fan-card-reversal-{name,qualifier} + .sig-qualifier-{above,below}, levity gets --quiUser on the same five slots; all selectors prefixed w. .sig-stage-card to match the 0,4,0 specificity of the default .sig-stage .sig-stage-card .fan-card-face .sig-qualifier-* rule (without the prefix the polarity overrides lose the cascade — .sig-qualifier-below was visibly stuck on the default --quiUser) ; .stat-face-label gets polarity-inverse colours — gravity stat-block bg is --secUser (opposite of card's --priUser) so the label takes --quiUser to stay legible; levity is the symmetric flip (label = --terUser on --priUser stat-block bg) ; levity card title/qualifier drop-shadow swapped from rgba(0,0,0,…) → rgba(255,255,255,…) — dark drop reads as harsh smudge against the inverted-frame levity --secUser bg; applied to both sig-overlay[data-polarity="levity"] stage card AND sea-stage--levity via $_sea-title-shadow-levity (former shared $_sea-title-shadow split into per-polarity {levity,gravity} variants) ; reversal-face class/content alignment so each .fan-card-reversal-* class always carries its semantic content — DOM order per arcana type controls visual layout after the 180° SPIN (DOM-second appears visually on top): Major → title in .fan-card-reversal-name @ DOM-second (visually top after spin), qualifier in .fan-card-reversal-qualifier @ DOM-first; Non-major → title in .fan-card-reversal-name @ DOM-first (visually bottom after spin), qualifier in .fan-card-reversal-qualifier @ DOM-second (preserves the original "qualifier word reads first after spin" layout for Middle/Minor arcana — e.g. "Relieving / Eight of Crowns" not "Eight of Crowns / Relieving") ; _tarot_fan.html renders per-arcana DOM order directly (Django template branches handle both layouts); sig + sea overlays render a fixed two-<p> skeleton (one DOM order) so stage-card.js's populator dynamically rewrites the two <p>s' className per arcana — Major/override branch flips DOM-second to .fan-card-reversal-name + content, DOM-first to .fan-card-reversal-qualifier; non-major branch keeps DOM-first as .fan-card-reversal-name + title, DOM-second as .fan-card-reversal-qualifier + reversalQualifier-or-polarity-fallback ; SigSelectSpec.js + SeaDealSpec.js fixtures + Major reversed-face assertion updated for the new semantic — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -624,7 +624,7 @@ class Character(models.Model):
|
|||||||
"""A gamer's player-character for one seat in one game session.
|
"""A gamer's player-character for one seat in one game session.
|
||||||
|
|
||||||
Lifecycle:
|
Lifecycle:
|
||||||
- Created (draft) when gamer opens PICK SKY overlay.
|
- Created (draft) when gamer opens CAST SKY overlay.
|
||||||
- confirmed_at set on confirm → locked.
|
- confirmed_at set on confirm → locked.
|
||||||
- retired_at set on retirement → archived (seat may hold a new Character).
|
- retired_at set on retirement → archived (seat may hold a new Character).
|
||||||
|
|
||||||
@@ -647,7 +647,7 @@ class Character(models.Model):
|
|||||||
TableSeat, on_delete=models.CASCADE, related_name='characters',
|
TableSeat, on_delete=models.CASCADE, related_name='characters',
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── significator (set at PICK SKY) ────────────────────────────────────
|
# ── significator (set at CAST SKY) ────────────────────────────────────
|
||||||
significator = models.ForeignKey(
|
significator = models.ForeignKey(
|
||||||
TarotCard, null=True, blank=True,
|
TarotCard, null=True, blank=True,
|
||||||
on_delete=models.SET_NULL, related_name='character_significators',
|
on_delete=models.SET_NULL, related_name='character_significators',
|
||||||
@@ -665,7 +665,7 @@ class Character(models.Model):
|
|||||||
# ── computed sky snapshot (full PySwiss response) ───────────────────
|
# ── computed sky snapshot (full PySwiss response) ───────────────────
|
||||||
chart_data = models.JSONField(null=True, blank=True)
|
chart_data = models.JSONField(null=True, blank=True)
|
||||||
|
|
||||||
# ── celtic cross spread (added at PICK SEA) ───────────────────────────
|
# ── celtic cross spread (added at DRAW SEA) ───────────────────────────
|
||||||
celtic_cross = models.JSONField(null=True, blank=True)
|
celtic_cross = models.JSONField(null=True, blank=True)
|
||||||
|
|
||||||
# ── lifecycle ─────────────────────────────────────────────────────────
|
# ── lifecycle ─────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ var SigSelect = (function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── TAKE SIG / WAIT NVM button ─────────────────────────────────────────
|
// ── SAVE SIG / WAIT NVM button ─────────────────────────────────────────
|
||||||
|
|
||||||
function _onTakeSigClick() {
|
function _onTakeSigClick() {
|
||||||
if (_isReady) {
|
if (_isReady) {
|
||||||
@@ -337,7 +337,7 @@ var SigSelect = (function () {
|
|||||||
_countdownTimer = null;
|
_countdownTimer = null;
|
||||||
if (_takeSigBtn) _takeSigBtn.style.fontSize = '';
|
if (_takeSigBtn) _takeSigBtn.style.fontSize = '';
|
||||||
}
|
}
|
||||||
if (_takeSigBtn) _takeSigBtn.textContent = 'TAKE SIG';
|
if (_takeSigBtn) _takeSigBtn.textContent = 'SAVE SIG';
|
||||||
_stopWaitNoGlow();
|
_stopWaitNoGlow();
|
||||||
_stopCountdownGlow();
|
_stopCountdownGlow();
|
||||||
}
|
}
|
||||||
@@ -367,7 +367,7 @@ var SigSelect = (function () {
|
|||||||
_takeSigBtn.id = 'id_take_sig_btn';
|
_takeSigBtn.id = 'id_take_sig_btn';
|
||||||
_takeSigBtn.className = 'btn btn-primary sig-take-sig-btn';
|
_takeSigBtn.className = 'btn btn-primary sig-take-sig-btn';
|
||||||
_takeSigBtn.type = 'button';
|
_takeSigBtn.type = 'button';
|
||||||
_takeSigBtn.textContent = 'TAKE SIG';
|
_takeSigBtn.textContent = 'SAVE SIG';
|
||||||
_takeSigBtn.addEventListener('click', _onTakeSigClick);
|
_takeSigBtn.addEventListener('click', _onTakeSigClick);
|
||||||
stage.appendChild(_takeSigBtn);
|
stage.appendChild(_takeSigBtn);
|
||||||
}
|
}
|
||||||
@@ -495,7 +495,7 @@ var SigSelect = (function () {
|
|||||||
function _showWaitingMsg(pendingPolarity) {
|
function _showWaitingMsg(pendingPolarity) {
|
||||||
if (document.getElementById('id_hex_waiting_msg')) return;
|
if (document.getElementById('id_hex_waiting_msg')) return;
|
||||||
// If the OTHER polarity finished before our tray sequence completed,
|
// If the OTHER polarity finished before our tray sequence completed,
|
||||||
// pick_sky_available will already have fired and revealed PICK SKY.
|
// pick_sky_available will already have fired and revealed CAST SKY.
|
||||||
// In that case skip the waiting msg so the two don't co-exist.
|
// In that case skip the waiting msg so the two don't co-exist.
|
||||||
var pickSkyBtn = document.getElementById('id_pick_sky_btn');
|
var pickSkyBtn = document.getElementById('id_pick_sky_btn');
|
||||||
if (pickSkyBtn && pickSkyBtn.style.display !== 'none') return;
|
if (pickSkyBtn && pickSkyBtn.style.display !== 'none') return;
|
||||||
@@ -539,7 +539,7 @@ var SigSelect = (function () {
|
|||||||
// the overlay vanishes; overlay dismissal + waiting msg run last.
|
// the overlay vanishes; overlay dismissal + waiting msg run last.
|
||||||
// User sees: stage card → tray slides in → sig fades into the tray
|
// User sees: stage card → tray slides in → sig fades into the tray
|
||||||
// cell → tray slides out → 2s pause → overlay dismisses → table hex
|
// cell → tray slides out → 2s pause → overlay dismisses → table hex
|
||||||
// w. waiting msg (or PICK SKY btn if both polarities are done).
|
// w. waiting msg (or CAST SKY btn if both polarities are done).
|
||||||
function _settle() {
|
function _settle() {
|
||||||
_dismissSigOverlay();
|
_dismissSigOverlay();
|
||||||
_showWaitingMsg(pendingPolarity);
|
_showWaitingMsg(pendingPolarity);
|
||||||
@@ -625,7 +625,7 @@ var SigSelect = (function () {
|
|||||||
userRole = overlay.dataset.userRole;
|
userRole = overlay.dataset.userRole;
|
||||||
userPolarity= overlay.dataset.polarity;
|
userPolarity= overlay.dataset.polarity;
|
||||||
|
|
||||||
// PICK SKY btn is rendered hidden during SIG_SELECT; reveal on pick_sky_available
|
// CAST SKY btn is rendered hidden during SIG_SELECT; reveal on pick_sky_available
|
||||||
var pickSkyBtn = document.getElementById('id_pick_sky_btn');
|
var pickSkyBtn = document.getElementById('id_pick_sky_btn');
|
||||||
if (pickSkyBtn) {
|
if (pickSkyBtn) {
|
||||||
pickSkyBtn.addEventListener('click', function () {
|
pickSkyBtn.addEventListener('click', function () {
|
||||||
|
|||||||
@@ -143,26 +143,42 @@ var StageCard = (function () {
|
|||||||
if (qBelow) qBelow.textContent = isMajor ? qualifier : '';
|
if (qBelow) qBelow.textContent = isMajor ? qualifier : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reversal face — four cases:
|
// Reversal face — class always matches semantic content:
|
||||||
// Polarity-split: full reversal title in qualifier slot (top-after-spin), name slot empty
|
// .fan-card-reversal-name → title-like text
|
||||||
// Major: title (with comma) in qualifier slot, qualifier in name slot
|
// .fan-card-reversal-qualifier → qualifier-like text
|
||||||
// Non-major + reversal_qual: reversal_qualifier in qualifier slot, title in name slot
|
// DOM order controls visual layout after the 180° SPIN (DOM-second appears
|
||||||
// Non-major no reversal_qual: fall back to current polarity's qualifier
|
// visually on top). Sig + sea skeletons render a fixed slot pair so we
|
||||||
var rQual = stageCard.querySelector('.fan-card-reversal-qualifier');
|
// assign classes per-arcana here so each branch lands in the right slot:
|
||||||
var rName = stageCard.querySelector('.fan-card-reversal-name');
|
// Major / polarity-split — title on top → .name carried by DOM-second
|
||||||
if (rQual && rName) {
|
// Non-major — qualifier on top → .qualifier carried by DOM-second
|
||||||
|
var slots = stageCard.querySelectorAll('.fan-card-face-reversal > p');
|
||||||
|
if (slots.length === 2) {
|
||||||
|
var bottomEl = slots[0]; // DOM-first → visually bottom after spin
|
||||||
|
var topEl = slots[1]; // DOM-second → visually top after spin
|
||||||
|
// Wipe any squeeze-class artifacts from a prior populate call before
|
||||||
|
// re-stamping the base class — keeps the slot's CSS predictable.
|
||||||
|
bottomEl.className = '';
|
||||||
|
topEl.className = '';
|
||||||
|
|
||||||
if (reversalOverride) {
|
if (reversalOverride) {
|
||||||
_setTitle(rQual, reversalOverride, card);
|
// Polarity-split: single-line title on TOP; qualifier slot empty.
|
||||||
rName.textContent = '';
|
topEl.className = 'fan-card-reversal-name';
|
||||||
|
_setTitle(topEl, reversalOverride, card);
|
||||||
|
bottomEl.className = 'fan-card-reversal-qualifier';
|
||||||
|
bottomEl.textContent = '';
|
||||||
} else if (isMajor) {
|
} else if (isMajor) {
|
||||||
_setTitle(rQual, title + ',', card);
|
// Major: title-with-comma on TOP, qualifier on BOTTOM.
|
||||||
rName.textContent = qualifier;
|
topEl.className = 'fan-card-reversal-name';
|
||||||
} else if (reversalQualifier) {
|
_setTitle(topEl, title + ',', card);
|
||||||
rQual.textContent = reversalQualifier;
|
bottomEl.className = 'fan-card-reversal-qualifier';
|
||||||
_setTitle(rName, title, card);
|
bottomEl.textContent = qualifier;
|
||||||
} else {
|
} else {
|
||||||
rQual.textContent = qualifier;
|
// Non-major: qualifier on TOP, title on BOTTOM (inverted from
|
||||||
_setTitle(rName, title, card);
|
// upright order by design — qualifier word reads first after spin).
|
||||||
|
topEl.className = 'fan-card-reversal-qualifier';
|
||||||
|
topEl.textContent = reversalQualifier || qualifier;
|
||||||
|
bottomEl.className = 'fan-card-reversal-name';
|
||||||
|
_setTitle(bottomEl, title, card);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Countdown scheduler for the polarity-room TAKE SIG gate.
|
Countdown scheduler for the polarity-room SAVE SIG gate.
|
||||||
|
|
||||||
Uses threading.Timer so no separate Celery worker is needed in development.
|
Uses threading.Timer so no separate Celery worker is needed in development.
|
||||||
Single-process only — swap for a Celery task if production uses multiple
|
Single-process only — swap for a Celery task if production uses multiple
|
||||||
|
|||||||
@@ -879,7 +879,7 @@ class SelectRoleMultiSeatTest(TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class RoomViewAllRolesFilledTest(TestCase):
|
class RoomViewAllRolesFilledTest(TestCase):
|
||||||
"""Room view in ROLE_SELECT with all seats assigned shows PICK SIGS button."""
|
"""Room view in ROLE_SELECT with all seats assigned shows SCAN SIGS button."""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
import lxml.html
|
import lxml.html
|
||||||
self.lxml = lxml.html
|
self.lxml = lxml.html
|
||||||
@@ -1785,7 +1785,7 @@ class SigConfirmViewTest(TestCase):
|
|||||||
# ── SKY_SELECT rendering ──────────────────────────────────────────────────────
|
# ── SKY_SELECT rendering ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
class PickSkyRenderingTest(TestCase):
|
class PickSkyRenderingTest(TestCase):
|
||||||
"""Room page at SKY_SELECT renders PICK SKY btn and sig card in tray cell 2."""
|
"""Room page at SKY_SELECT renders CAST SKY btn and sig card in tray cell 2."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
|
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
|
||||||
@@ -1865,7 +1865,7 @@ class PickSkyRenderingTest(TestCase):
|
|||||||
self.assertEqual(founder.sky_birth_tz, "America/New_York")
|
self.assertEqual(founder.sky_birth_tz, "America/New_York")
|
||||||
|
|
||||||
def test_no_sky_delete_btn_in_blank_sky_select_modal(self):
|
def test_no_sky_delete_btn_in_blank_sky_select_modal(self):
|
||||||
"""A fresh PICK SKY modal (no preview wheel rendered yet) must not
|
"""A fresh CAST SKY modal (no preview wheel rendered yet) must not
|
||||||
carry the DEL btn — it would otherwise float in the empty wheel area
|
carry the DEL btn — it would otherwise float in the empty wheel area
|
||||||
suggesting there's something to delete when the user has only seen
|
suggesting there's something to delete when the user has only seen
|
||||||
the form. The JS schedulePreview success handler is the contract that
|
the form. The JS schedulePreview success handler is the contract that
|
||||||
@@ -1881,7 +1881,7 @@ class PickSkyRenderingTest(TestCase):
|
|||||||
# ── SEA_SELECT rendering ──────────────────────────────────────────────────────
|
# ── SEA_SELECT rendering ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
class PickSeaRenderingTest(TestCase):
|
class PickSeaRenderingTest(TestCase):
|
||||||
"""At SKY_SELECT, a confirmed Character swaps PICK SKY → PICK SEA + sea overlay."""
|
"""At SKY_SELECT, a confirmed Character swaps CAST SKY → DRAW SEA + sea overlay."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
|
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from apps.epic.models import Room, RoomInvite
|
|||||||
|
|
||||||
# ── Game-wide constants ────────────────────────────────────────────────────
|
# ── Game-wide constants ────────────────────────────────────────────────────
|
||||||
# Reversal probability applied to any card pulled from a stack, anywhere in
|
# Reversal probability applied to any card pulled from a stack, anywhere in
|
||||||
# the game (PICK SEA initially; future phases — gameplay draws etc. — will
|
# the game (DRAW SEA initially; future phases — gameplay draws etc. — will
|
||||||
# share this single source of truth). Stub for a future per-user profile
|
# share this single source of truth). Stub for a future per-user profile
|
||||||
# override: callers MUST go through stack_reversal_probability(user, room)
|
# override: callers MUST go through stack_reversal_probability(user, room)
|
||||||
# rather than referencing the constant directly so the user-config hookup is
|
# rather than referencing the constant directly so the user-config hookup is
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ def room_view(request, room_id):
|
|||||||
ctx = _role_select_context(room, request.user)
|
ctx = _role_select_context(room, request.user)
|
||||||
ctx["room"] = room
|
ctx["room"] = room
|
||||||
ctx["page_class"] = "page-gameboard"
|
ctx["page_class"] = "page-gameboard"
|
||||||
# Reversal-rate hint label under PICK SEA's SPREAD select — same helper as
|
# Reversal-rate hint label under DRAW SEA's SPREAD select — same helper as
|
||||||
# sea_partial so the value tracks any future per-user override automatically.
|
# sea_partial so the value tracks any future per-user override automatically.
|
||||||
ctx["stack_reversal_pct"] = int(round(stack_reversal_probability(request.user, room) * 100))
|
ctx["stack_reversal_pct"] = int(round(stack_reversal_probability(request.user, room) * 100))
|
||||||
return render(request, "apps/gameboard/room.html", ctx)
|
return render(request, "apps/gameboard/room.html", ctx)
|
||||||
@@ -1223,7 +1223,7 @@ def sky_save(request, room_id):
|
|||||||
@login_required
|
@login_required
|
||||||
def sky_delete(request, room_id):
|
def sky_delete(request, room_id):
|
||||||
"""Purge the requesting gamer's Character on this seat — both unconfirmed
|
"""Purge the requesting gamer's Character on this seat — both unconfirmed
|
||||||
drafts AND confirmed rows. The in-room PICK SKY DEL targets this so SAVE
|
drafts AND confirmed rows. The in-room CAST SKY DEL targets this so SAVE
|
||||||
SKY → DEL → refresh truly drops the saved sky for the seat. The User
|
SKY → DEL → refresh truly drops the saved sky for the seat. The User
|
||||||
model's sky_chart_data is intentionally untouched (Dashsky / My Sky
|
model's sky_chart_data is intentionally untouched (Dashsky / My Sky
|
||||||
applet's DEL handles that separately)."""
|
applet's DEL handles that separately)."""
|
||||||
@@ -1239,7 +1239,7 @@ def sky_delete(request, room_id):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def sea_deck(request, room_id):
|
def sea_deck(request, room_id):
|
||||||
"""Shuffled deck lists (levity + gravity halves) for PICK SEA draw.
|
"""Shuffled deck lists (levity + gravity halves) for DRAW SEA draw.
|
||||||
|
|
||||||
Excludes all Significators already claimed by seated gamers.
|
Excludes all Significators already claimed by seated gamers.
|
||||||
Returns {levity: [{id, name, arcana, suit, number, levity_qualifier,
|
Returns {levity: [{id, name, arcana, suit, number, levity_qualifier,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Management command for manual single-gamer PICK SEA testing.
|
Management command for manual single-gamer DRAW SEA testing.
|
||||||
|
|
||||||
Creates a room at SKY_SELECT with one seated gamer whose sky is already
|
Creates a room at SKY_SELECT with one seated gamer whose sky is already
|
||||||
confirmed, so the PICK SEA overlay is immediately visible on page load.
|
confirmed, so the DRAW SEA overlay is immediately visible on page load.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python src/manage.py setup_sea_session
|
python src/manage.py setup_sea_session
|
||||||
@@ -32,7 +32,7 @@ def _make_session(user):
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Set up a SKY_SELECT room with sky confirmed and print a PICK SEA URL"
|
help = "Set up a SKY_SELECT room with sky confirmed and print a DRAW SEA URL"
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument("--base-url", default="http://localhost:8000")
|
parser.add_argument("--base-url", default="http://localhost:8000")
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ class BillscrollGearMenuTest(FunctionalTest):
|
|||||||
FT: the billscroll page has a gear menu that filters events by label.
|
FT: the billscroll page has a gear menu that filters events by label.
|
||||||
|
|
||||||
Frame = all regular (non-struck) drama entries.
|
Frame = all regular (non-struck) drama entries.
|
||||||
Redact = struck-through (retracted) entries, e.g. a WAIT NVM after TAKE SIG.
|
Redact = struck-through (retracted) entries, e.g. a WAIT NVM after SAVE SIG.
|
||||||
|
|
||||||
Scenario (one gamer, Role + Sig events):
|
Scenario (one gamer, Role + Sig events):
|
||||||
1. Both labels checked by default — all events visible.
|
1. Both labels checked by default — all events visible.
|
||||||
|
|||||||
@@ -843,12 +843,12 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
|
|||||||
))
|
))
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Test 7 — PICK SIGS appears + card stack removed on last role #
|
# Test 7 — SCAN SIGS appears + card stack removed on last role #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def test_pick_sigs_appears_and_card_stack_removed_on_last_role(self):
|
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
|
"""When the sixth and final role is confirmed, the all_roles_filled
|
||||||
WS event makes the PICK SIGS button visible and removes the card
|
WS event makes the SCAN SIGS button visible and removes the card
|
||||||
stack from the DOM entirely."""
|
stack from the DOM entirely."""
|
||||||
emails = [
|
emails = [
|
||||||
"founder@test.io", "amigo@test.io", "bud@test.io",
|
"founder@test.io", "amigo@test.io", "bud@test.io",
|
||||||
@@ -883,7 +883,7 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
|
|||||||
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
||||||
self.confirm_guard()
|
self.confirm_guard()
|
||||||
|
|
||||||
# PICK SIGS wrap must become visible via the all_roles_filled WS event.
|
# SCAN SIGS wrap must become visible via the all_roles_filled WS event.
|
||||||
self.wait_for(lambda: self.assertFalse(
|
self.wait_for(lambda: self.assertFalse(
|
||||||
self.browser.find_element(By.ID, "id_pick_sigs_wrap").get_attribute("style"),
|
self.browser.find_element(By.ID, "id_pick_sigs_wrap").get_attribute("style"),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Functional tests for the PICK SEA overlay — Celtic Cross draw."""
|
"""Functional tests for the DRAW SEA overlay — Celtic Cross draw."""
|
||||||
|
|
||||||
from django.test import tag
|
from django.test import tag
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -39,7 +39,7 @@ def _make_sky_confirmed_room(live_server_url, user, earthman):
|
|||||||
@tag("channels")
|
@tag("channels")
|
||||||
class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
|
class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
|
||||||
"""After sky confirm, the sky overlay closes and the room reloads to the
|
"""After sky confirm, the sky overlay closes and the room reloads to the
|
||||||
table hex w. the PICK SEA btn visible — the gamer must opt into the sea
|
table hex w. the DRAW SEA btn visible — the gamer must opt into the sea
|
||||||
overlay rather than be auto-launched into it."""
|
overlay rather than be auto-launched into it."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -92,22 +92,22 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
def test_pick_sea_btn_visible_after_sky_confirm(self):
|
def test_pick_sea_btn_visible_after_sky_confirm(self):
|
||||||
"""Confirming sky reloads the room to the hex w. PICK SEA replacing
|
"""Confirming sky reloads the room to the hex w. DRAW SEA replacing
|
||||||
PICK SKY; the sea overlay is NOT auto-opened."""
|
CAST SKY; the sea overlay is NOT auto-opened."""
|
||||||
self.create_pre_authenticated_session("founder@test.io")
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
self.browser.get(self.room_url)
|
self.browser.get(self.room_url)
|
||||||
|
|
||||||
# Sky not yet confirmed — PICK SKY btn present.
|
# Sky not yet confirmed — CAST SKY btn present.
|
||||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
|
||||||
|
|
||||||
self._confirm_sky()
|
self._confirm_sky()
|
||||||
|
|
||||||
# Page reloads → hex shows PICK SEA in place of PICK SKY.
|
# Page reloads → hex shows DRAW SEA in place of CAST SKY.
|
||||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn"))
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn"))
|
||||||
self.assertEqual(self.browser.find_elements(By.ID, "id_pick_sky_btn"), [])
|
self.assertEqual(self.browser.find_elements(By.ID, "id_pick_sky_btn"), [])
|
||||||
|
|
||||||
# Sea overlay is NOT auto-opened — it only appears once the gamer
|
# Sea overlay is NOT auto-opened — it only appears once the gamer
|
||||||
# clicks PICK SEA.
|
# clicks DRAW SEA.
|
||||||
has_sea_open = self.browser.execute_script(
|
has_sea_open = self.browser.execute_script(
|
||||||
"return document.documentElement.classList.contains('sea-open');"
|
"return document.documentElement.classList.contains('sea-open');"
|
||||||
)
|
)
|
||||||
@@ -126,7 +126,7 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
|
|||||||
self.assertTrue(not sky or not sky[0].is_displayed())
|
self.assertTrue(not sky or not sky[0].is_displayed())
|
||||||
|
|
||||||
def test_clicking_pick_sea_btn_opens_sea_overlay(self):
|
def test_clicking_pick_sea_btn_opens_sea_overlay(self):
|
||||||
"""The gamer's explicit click on PICK SEA is what opens the sea overlay."""
|
"""The gamer's explicit click on DRAW SEA is what opens the sea overlay."""
|
||||||
self.create_pre_authenticated_session("founder@test.io")
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
self.browser.get(self.room_url)
|
self.browser.get(self.room_url)
|
||||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
|
||||||
@@ -134,7 +134,7 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
|
|||||||
self._confirm_sky()
|
self._confirm_sky()
|
||||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn"))
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn"))
|
||||||
|
|
||||||
# On slow CI, the PICK SEA btn parses into the DOM before the inline
|
# On slow CI, the DRAW SEA btn parses into the DOM before the inline
|
||||||
# `<script>` at the bottom of _sea_overlay.html has bound `openSea` to
|
# `<script>` at the bottom of _sea_overlay.html has bound `openSea` to
|
||||||
# it; a one-shot click can land before the handler exists. Retry click
|
# it; a one-shot click can land before the handler exists. Retry click
|
||||||
# + assert together via wait_for so the race resolves naturally.
|
# + assert together via wait_for so the race resolves naturally.
|
||||||
@@ -150,7 +150,7 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
|
|||||||
self.assertTrue(sea_overlay.is_displayed())
|
self.assertTrue(sea_overlay.is_displayed())
|
||||||
|
|
||||||
|
|
||||||
# ── Helpers for PICK SEA deal tests ──────────────────────────────────────────
|
# ── Helpers for DRAW SEA deal tests ──────────────────────────────────────────
|
||||||
|
|
||||||
def _seed_earthman_cards(earthman, count=20):
|
def _seed_earthman_cards(earthman, count=20):
|
||||||
"""Seed enough Middle Arcana cards for the deck piles."""
|
"""Seed enough Middle Arcana cards for the deck piles."""
|
||||||
@@ -167,7 +167,7 @@ def _seed_earthman_cards(earthman, count=20):
|
|||||||
|
|
||||||
|
|
||||||
def _make_sea_ready_room(earthman):
|
def _make_sea_ready_room(earthman):
|
||||||
"""Create a SKY_SELECT room with a confirmed Character ready for PICK SEA.
|
"""Create a SKY_SELECT room with a confirmed Character ready for DRAW SEA.
|
||||||
|
|
||||||
Returns (room, gamer, seat, char, room_url).
|
Returns (room, gamer, seat, char, room_url).
|
||||||
"""
|
"""
|
||||||
@@ -199,7 +199,7 @@ def _make_sea_ready_room(earthman):
|
|||||||
|
|
||||||
@tag("channels")
|
@tag("channels")
|
||||||
class PickSeaDealTest(ChannelsFunctionalTest):
|
class PickSeaDealTest(ChannelsFunctionalTest):
|
||||||
"""PICK SEA deck stacks, OK btn interaction, card draw, and LOCK HAND."""
|
"""DRAW SEA deck stacks, OK btn interaction, card draw, and LOCK HAND."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|||||||
@@ -325,25 +325,25 @@ class SigSelectThemeTest(FunctionalTest):
|
|||||||
self.assertEqual(corr.text, "")
|
self.assertEqual(corr.text, "")
|
||||||
|
|
||||||
|
|
||||||
# ── TAKE SIG / WAIT NVM — ready gate ──────────────────────────────────────────
|
# ── SAVE SIG / WAIT NVM — ready gate ──────────────────────────────────────────
|
||||||
#
|
#
|
||||||
# TAKE SIG (.btn.btn-primary) appears at the bottom-left corner of the card
|
# SAVE SIG (.btn.btn-primary) appears at the bottom-left corner of the card
|
||||||
# stage preview once a gamer has clicked OK on a card (SigReservation exists).
|
# stage preview once a gamer has clicked OK on a card (SigReservation exists).
|
||||||
# Clicking it sets the gamer's status to ready and changes the btn to WAIT NVM.
|
# Clicking it sets the gamer's status to ready and changes the btn to WAIT NVM.
|
||||||
# WAIT NVM cancels the ready status and reverts back to TAKE SIG.
|
# WAIT NVM cancels the ready status and reverts back to SAVE SIG.
|
||||||
#
|
#
|
||||||
# When all three gamers in a polarity WS room are ready, a 12-second countdown
|
# When all three gamers in a polarity WS room are ready, a 12-second countdown
|
||||||
# starts. Any WAIT NVM during the countdown cancels it; the saved remaining time
|
# starts. Any WAIT NVM during the countdown cancels it; the saved remaining time
|
||||||
# is resumed when all three are ready again. When the countdown completes
|
# is resumed when all three are ready again. When the countdown completes
|
||||||
# (client POSTs sig_confirm) the polarity group returns to the table hex.
|
# (client POSTs sig_confirm) the polarity group returns to the table hex.
|
||||||
# When both polarity groups have confirmed, PICK SKY btn appears in the hex
|
# When both polarity groups have confirmed, CAST SKY btn appears in the hex
|
||||||
# center for all six gamers.
|
# center for all six gamers.
|
||||||
#
|
#
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
class SigReadyGateTest(FunctionalTest):
|
class SigReadyGateTest(FunctionalTest):
|
||||||
"""Single-browser tests for TAKE SIG / WAIT NVM btn."""
|
"""Single-browser tests for SAVE SIG / WAIT NVM btn."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
@@ -377,10 +377,10 @@ class SigReadyGateTest(FunctionalTest):
|
|||||||
)
|
)
|
||||||
ok_btn.click()
|
ok_btn.click()
|
||||||
|
|
||||||
# ── SRG1: TAKE SIG btn not visible before OK ──────────────────────── #
|
# ── SRG1: SAVE SIG btn not visible before OK ──────────────────────── #
|
||||||
|
|
||||||
def test_take_sig_btn_not_visible_before_ok_click(self):
|
def test_take_sig_btn_not_visible_before_ok_click(self):
|
||||||
"""TAKE SIG must be absent until the gamer has OK'd a card."""
|
"""SAVE SIG must be absent until the gamer has OK'd a card."""
|
||||||
room = self._setup_sig_room()
|
room = self._setup_sig_room()
|
||||||
self.create_pre_authenticated_session("founder@test.io")
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||||
@@ -389,7 +389,7 @@ class SigReadyGateTest(FunctionalTest):
|
|||||||
take_sig_btns = self.browser.find_elements(By.ID, "id_take_sig_btn")
|
take_sig_btns = self.browser.find_elements(By.ID, "id_take_sig_btn")
|
||||||
self.assertEqual(len(take_sig_btns), 0)
|
self.assertEqual(len(take_sig_btns), 0)
|
||||||
|
|
||||||
# ── SRG2: TAKE SIG btn appears after OK ──────────────────────────── #
|
# ── SRG2: SAVE SIG btn appears after OK ──────────────────────────── #
|
||||||
|
|
||||||
def test_take_sig_btn_appears_after_ok_click(self):
|
def test_take_sig_btn_appears_after_ok_click(self):
|
||||||
room = self._setup_sig_room()
|
room = self._setup_sig_room()
|
||||||
@@ -401,9 +401,9 @@ class SigReadyGateTest(FunctionalTest):
|
|||||||
take_sig_btn = self.wait_for(
|
take_sig_btn = self.wait_for(
|
||||||
lambda: self.browser.find_element(By.ID, "id_take_sig_btn")
|
lambda: self.browser.find_element(By.ID, "id_take_sig_btn")
|
||||||
)
|
)
|
||||||
self.assertIn("TAKE SIG", take_sig_btn.text.upper())
|
self.assertIn("SAVE SIG", take_sig_btn.text.upper())
|
||||||
|
|
||||||
# ── SRG3: TAKE SIG → WAIT NVM ─────────────────────────────────────── #
|
# ── SRG3: SAVE SIG → WAIT NVM ─────────────────────────────────────── #
|
||||||
|
|
||||||
def test_take_sig_btn_becomes_wait_no_after_click(self):
|
def test_take_sig_btn_becomes_wait_no_after_click(self):
|
||||||
room = self._setup_sig_room()
|
room = self._setup_sig_room()
|
||||||
@@ -429,7 +429,7 @@ class SigReadyGateTest(FunctionalTest):
|
|||||||
).get_attribute("class")
|
).get_attribute("class")
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── SRG4: WAIT NVM → TAKE SIG ─────────────────────────────────────── #
|
# ── SRG4: WAIT NVM → SAVE SIG ─────────────────────────────────────── #
|
||||||
|
|
||||||
def test_wait_no_reverts_to_take_sig(self):
|
def test_wait_no_reverts_to_take_sig(self):
|
||||||
room = self._setup_sig_room()
|
room = self._setup_sig_room()
|
||||||
@@ -446,11 +446,11 @@ class SigReadyGateTest(FunctionalTest):
|
|||||||
By.ID, "id_take_sig_btn").text.upper()
|
By.ID, "id_take_sig_btn").text.upper()
|
||||||
)
|
)
|
||||||
btn = self.browser.find_element(By.ID, "id_take_sig_btn")
|
btn = self.browser.find_element(By.ID, "id_take_sig_btn")
|
||||||
btn.click() # → TAKE SIG again
|
btn.click() # → SAVE SIG again
|
||||||
|
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertIn(
|
lambda: self.assertIn(
|
||||||
"TAKE SIG",
|
"SAVE SIG",
|
||||||
self.browser.find_element(By.ID, "id_take_sig_btn").text.upper(),
|
self.browser.find_element(By.ID, "id_take_sig_btn").text.upper(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -458,7 +458,7 @@ class SigReadyGateTest(FunctionalTest):
|
|||||||
|
|
||||||
@tag("channels")
|
@tag("channels")
|
||||||
class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
|
class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
|
||||||
"""Multi-browser WebSocket tests for the polarity-room countdown and PICK SKY."""
|
"""Multi-browser WebSocket tests for the polarity-room countdown and CAST SKY."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
@@ -546,7 +546,7 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
|
|||||||
|
|
||||||
@tag("channels")
|
@tag("channels")
|
||||||
def test_countdown_element_appears_when_all_three_levity_gamers_ready(self):
|
def test_countdown_element_appears_when_all_three_levity_gamers_ready(self):
|
||||||
"""When PC, NC, and SC each click TAKE SIG the countdown becomes visible."""
|
"""When PC, NC, and SC each click SAVE SIG the countdown becomes visible."""
|
||||||
room, emails = self._setup_sig_select_room()
|
room, emails = self._setup_sig_select_room()
|
||||||
levity_emails = [emails[0], emails[1], emails[3]] # PC, NC, SC
|
levity_emails = [emails[0], emails[1], emails[3]] # PC, NC, SC
|
||||||
browsers = []
|
browsers = []
|
||||||
@@ -556,7 +556,7 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
|
|||||||
b.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
b.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||||
browsers.append(b)
|
browsers.append(b)
|
||||||
|
|
||||||
# Each levity gamer OK's a card then clicks TAKE SIG
|
# Each levity gamer OK's a card then clicks SAVE SIG
|
||||||
for b in browsers:
|
for b in browsers:
|
||||||
self._ok_card_in_browser(b)
|
self._ok_card_in_browser(b)
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
@@ -616,12 +616,12 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
|
|||||||
for b in browsers:
|
for b in browsers:
|
||||||
b.quit()
|
b.quit()
|
||||||
|
|
||||||
# ── SRG7: PICK SKY btn appears after both polarity groups confirm ─── #
|
# ── SRG7: CAST SKY btn appears after both polarity groups confirm ─── #
|
||||||
|
|
||||||
@tag("channels")
|
@tag("channels")
|
||||||
def test_pick_sky_btn_appears_in_hex_after_both_groups_confirm(self):
|
def test_pick_sky_btn_appears_in_hex_after_both_groups_confirm(self):
|
||||||
"""Once both levity and gravity countdowns complete, all six browsers
|
"""Once both levity and gravity countdowns complete, all six browsers
|
||||||
see the PICK SKY btn in the table hex center."""
|
see the CAST SKY btn in the table hex center."""
|
||||||
# This test drives the full flow end-to-end but uses ORM shortcuts
|
# This test drives the full flow end-to-end but uses ORM shortcuts
|
||||||
# to set all-ready state for one polarity, letting the other complete
|
# to set all-ready state for one polarity, letting the other complete
|
||||||
# via the UI, to keep execution time manageable.
|
# via the UI, to keep execution time manageable.
|
||||||
@@ -647,7 +647,7 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
|
|||||||
b.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
b.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||||
browsers.append(b)
|
browsers.append(b)
|
||||||
|
|
||||||
# All levity gamers OK and TAKE SIG
|
# All levity gamers OK and SAVE SIG
|
||||||
for b in browsers:
|
for b in browsers:
|
||||||
self._ok_card_in_browser(b)
|
self._ok_card_in_browser(b)
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
@@ -655,7 +655,7 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
|
|||||||
)
|
)
|
||||||
b.find_element(By.ID, "id_take_sig_btn").click()
|
b.find_element(By.ID, "id_take_sig_btn").click()
|
||||||
|
|
||||||
# Wait for countdown to expire or be confirmed; PICK SKY appears in hex
|
# Wait for countdown to expire or be confirmed; CAST SKY appears in hex
|
||||||
# countdown is 12 s so use wait_for_slow (MAX_WAIT=10 is not enough)
|
# countdown is 12 s so use wait_for_slow (MAX_WAIT=10 is not enough)
|
||||||
for b in browsers:
|
for b in browsers:
|
||||||
self.wait_for_slow(
|
self.wait_for_slow(
|
||||||
@@ -711,7 +711,7 @@ class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
|
|||||||
# class PickSkyTrayFlowTest(FunctionalTest):
|
# class PickSkyTrayFlowTest(FunctionalTest):
|
||||||
#
|
#
|
||||||
# def test_pick_sky_btn_opens_tray_with_sig_card_in_slot_2(self):
|
# def test_pick_sky_btn_opens_tray_with_sig_card_in_slot_2(self):
|
||||||
# """Clicking PICK SKY opens #id_tray; tray cell 2 shows the gamer's
|
# """Clicking CAST SKY opens #id_tray; tray cell 2 shows the gamer's
|
||||||
# sig card icon (Blank.svg placeholder until card-specific icons land)."""
|
# sig card icon (Blank.svg placeholder until card-specific icons land)."""
|
||||||
# ...
|
# ...
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Functional tests for the PICK SKY overlay — natal chart entry."""
|
"""Functional tests for the CAST SKY overlay — natal chart entry."""
|
||||||
|
|
||||||
import json as _json
|
import json as _json
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ def _make_sky_select_room():
|
|||||||
|
|
||||||
|
|
||||||
class PickSkyLocalStorageTest(FunctionalTest):
|
class PickSkyLocalStorageTest(FunctionalTest):
|
||||||
"""PICK SKY form fields persist to localStorage."""
|
"""CAST SKY form fields persist to localStorage."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
@@ -145,7 +145,7 @@ class PickSkyLocalStorageTest(FunctionalTest):
|
|||||||
|
|
||||||
|
|
||||||
class PickSkyDelTest(FunctionalTest):
|
class PickSkyDelTest(FunctionalTest):
|
||||||
"""PICK SKY overlay gets a DEL btn at the wheel center: clicking opens the
|
"""CAST SKY overlay gets a DEL btn at the wheel center: clicking opens the
|
||||||
global guard portal; OK clears the wheel SVG, resets the form fields, &
|
global guard portal; OK clears the wheel SVG, resets the form fields, &
|
||||||
purges the localStorage entry that would otherwise rehydrate the form on
|
purges the localStorage entry that would otherwise rehydrate the form on
|
||||||
the next overlay open / page refresh. No server hit (the wheel here is
|
the next overlay open / page refresh. No server hit (the wheel here is
|
||||||
@@ -173,7 +173,7 @@ class PickSkyDelTest(FunctionalTest):
|
|||||||
self.create_pre_authenticated_session(self.founder_email)
|
self.create_pre_authenticated_session(self.founder_email)
|
||||||
self.browser.get(self.room_url)
|
self.browser.get(self.room_url)
|
||||||
|
|
||||||
# Open PICK SKY modal
|
# Open CAST SKY modal
|
||||||
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
|
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
|
||||||
self.browser.execute_script("arguments[0].click()", btn)
|
self.browser.execute_script("arguments[0].click()", btn)
|
||||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_sky_overlay"))
|
self.wait_for(lambda: self.browser.find_element(By.ID, "id_sky_overlay"))
|
||||||
|
|||||||
@@ -548,14 +548,14 @@ describe("SigSelect", () => {
|
|||||||
.toBe(card.dataset.nameTitle);
|
.toBe(card.dataset.nameTitle);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("major arcana reversed face: title, in qualifier slot (first after spin); qualifier in name slot (second)", () => {
|
it("major arcana reversed face: title in name slot (visually top after spin); qualifier in qualifier slot (visually bottom)", () => {
|
||||||
makeFixture({ polarity: "levity", userRole: "PC" });
|
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||||
card.dataset.arcana = "Major Arcana";
|
card.dataset.arcana = "Major Arcana";
|
||||||
card.dataset.nameTitle = "The Schizo";
|
card.dataset.nameTitle = "The Schizo";
|
||||||
hover();
|
hover();
|
||||||
// DOM-second element appears first after card spins — so title goes in qualifier slot
|
// Class matches semantic content: title → .fan-card-reversal-name, qualifier → .fan-card-reversal-qualifier
|
||||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("The Schizo,");
|
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("The Schizo,");
|
||||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("Elevated");
|
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Elevated");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("non-major without data-reversal-qualifier: qualifier mirrors polarity, name repeats card title", () => {
|
it("non-major without data-reversal-qualifier: qualifier mirrors polarity, name repeats card title", () => {
|
||||||
@@ -812,7 +812,7 @@ describe("SigSelect", () => {
|
|||||||
|
|
||||||
// ── polarity_room_done → tray sequence ─────────────────────────────────── //
|
// ── polarity_room_done → tray sequence ─────────────────────────────────── //
|
||||||
//
|
//
|
||||||
// After all 3 gamers in the user's polarity confirm TAKE SIG and the
|
// After all 3 gamers in the user's polarity confirm SAVE SIG and the
|
||||||
// 12s countdown expires, the server fires room:polarity_room_done. The
|
// 12s countdown expires, the server fires room:polarity_room_done. The
|
||||||
// sig-select handler should: (1) play the tray sequence — Tray.placeSig
|
// sig-select handler should: (1) play the tray sequence — Tray.placeSig
|
||||||
// with the user's selected stage card; (2) on Tray.placeSig's completion
|
// with the user's selected stage card; (2) on Tray.placeSig's completion
|
||||||
@@ -876,7 +876,7 @@ describe("SigSelect", () => {
|
|||||||
it("does NOT add the waiting msg when pick_sky_btn is already revealed", () => {
|
it("does NOT add the waiting msg when pick_sky_btn is already revealed", () => {
|
||||||
// pick_sky_available may fire DURING the tray sequence (other
|
// pick_sky_available may fire DURING the tray sequence (other
|
||||||
// polarity finishes first). When the tray callback then hangs +
|
// polarity finishes first). When the tray callback then hangs +
|
||||||
// dismisses, _settle must check whether PICK SKY is up and skip
|
// dismisses, _settle must check whether CAST SKY is up and skip
|
||||||
// the "Levity appraising…" / "Gravity settling…" message so it
|
// the "Levity appraising…" / "Gravity settling…" message so it
|
||||||
// doesn't co-exist w. the btn.
|
// doesn't co-exist w. the btn.
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
|
|||||||
@@ -827,16 +827,28 @@ html:has(.sig-backdrop) {
|
|||||||
.fan-card-name { color: rgba(var(--quiUser), 1); }
|
.fan-card-name { color: rgba(var(--quiUser), 1); }
|
||||||
.fan-card-arcana { color: rgba(var(--priUser), 1); }
|
.fan-card-arcana { color: rgba(var(--priUser), 1); }
|
||||||
}
|
}
|
||||||
// Polarity qualifier: same colour as the card title in this context
|
// Polarity title + qualifier text: --quiUser for levity (paired w. gravity's --terUser).
|
||||||
.sig-qualifier-above,
|
// All five selectors prefixed w. .sig-stage-card to match (or beat) the 0,4,0 specificity
|
||||||
.sig-qualifier-below { color: rgba(var(--quiUser), 1); }
|
// of the default `.sig-stage .sig-stage-card .fan-card-face .sig-qualifier-*` rule —
|
||||||
// Upright + reversal title glow — levity
|
// without the prefix the polarity color loses the cascade on .sig-qualifier-*.
|
||||||
|
.sig-stage-card .fan-card-name,
|
||||||
|
.sig-stage-card .fan-card-reversal-name,
|
||||||
|
.sig-stage-card .fan-card-reversal-qualifier,
|
||||||
|
.sig-stage-card .sig-qualifier-above,
|
||||||
|
.sig-stage-card .sig-qualifier-below { color: rgba(var(--quiUser), 1); }
|
||||||
|
// Stat-face label: levity stat-block bg is --priUser (opposite of levity card's
|
||||||
|
// --secUser bg), so the label takes the gravity-card text color (--terUser) to
|
||||||
|
// stay legible against the dark stat-block.
|
||||||
|
.sig-stat-block .stat-face-label { color: rgba(var(--terUser), 1); }
|
||||||
|
// Upright + reversal title glow — levity. Drop-shadow is WHITE here (was 0,0,0
|
||||||
|
// at 0.55) because the inverted-frame levity card uses a light --secUser bg,
|
||||||
|
// so a dark drop shadow reads as harsh smudge under the --quiUser title text.
|
||||||
.sig-stage-card .fan-card-name,
|
.sig-stage-card .fan-card-name,
|
||||||
.sig-stage-card .sig-qualifier-above,
|
.sig-stage-card .sig-qualifier-above,
|
||||||
.sig-stage-card .sig-qualifier-below,
|
.sig-stage-card .sig-qualifier-below,
|
||||||
.sig-stage-card .fan-card-reversal-name,
|
.sig-stage-card .fan-card-reversal-name,
|
||||||
.sig-stage-card .fan-card-reversal-qualifier {
|
.sig-stage-card .fan-card-reversal-qualifier {
|
||||||
text-shadow: 0 1px 1px rgba(0,0,0,0.55), 0 0 0.55rem rgba(var(--ninUser), 0.7);
|
text-shadow: 0 1px 1px rgba(255,255,255,0.55), 0 0 0.55rem rgba(var(--ninUser), 0.7);
|
||||||
}
|
}
|
||||||
// card-ref spans inside the caution tooltip — must match the base rule's
|
// card-ref spans inside the caution tooltip — must match the base rule's
|
||||||
// .sig-stat-block .sig-info-effect .card-ref specificity (0,3,0) to win.
|
// .sig-stat-block .sig-info-effect .card-ref specificity (0,3,0) to win.
|
||||||
@@ -853,9 +865,18 @@ html:has(.sig-backdrop) {
|
|||||||
// Caution tooltip: --tooltip-bg is black so priUser text (dark) would be invisible —
|
// Caution tooltip: --tooltip-bg is black so priUser text (dark) would be invisible —
|
||||||
// override to secUser (light) so body text reads against the dark backdrop.
|
// override to secUser (light) so body text reads against the dark backdrop.
|
||||||
.sig-info { color: rgba(var(--secUser), 1); }
|
.sig-info { color: rgba(var(--secUser), 1); }
|
||||||
// Polarity qualifier: terUser for gravity (quiUser is levity's equivalent)
|
// Polarity title + qualifier text: --terUser for gravity (paired w. levity's --quiUser).
|
||||||
.sig-qualifier-above,
|
// All five selectors prefixed w. .sig-stage-card to meet the 0,4,0 specificity of the
|
||||||
.sig-qualifier-below { color: rgba(var(--terUser), 1); }
|
// default `.sig-stage .sig-stage-card .fan-card-face .sig-qualifier-*` rule.
|
||||||
|
.sig-stage-card .fan-card-name,
|
||||||
|
.sig-stage-card .fan-card-reversal-name,
|
||||||
|
.sig-stage-card .fan-card-reversal-qualifier,
|
||||||
|
.sig-stage-card .sig-qualifier-above,
|
||||||
|
.sig-stage-card .sig-qualifier-below { color: rgba(var(--terUser), 1); }
|
||||||
|
// Stat-face label: gravity stat-block bg is --secUser (opposite of gravity card's
|
||||||
|
// --priUser bg), so the label takes the levity-card text color (--quiUser) to
|
||||||
|
// stay legible against the lighter stat-block.
|
||||||
|
.sig-stat-block .stat-face-label { color: rgba(var(--quiUser), 1); }
|
||||||
// Upright + reversal title glow — gravity
|
// Upright + reversal title glow — gravity
|
||||||
.sig-stage-card .fan-card-name,
|
.sig-stage-card .fan-card-name,
|
||||||
.sig-stage-card .sig-qualifier-above,
|
.sig-stage-card .sig-qualifier-above,
|
||||||
@@ -935,7 +956,7 @@ html:has(.sig-backdrop) {
|
|||||||
#id_room_menu { right: 2.5rem; }
|
#id_room_menu { right: 2.5rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── PICK SEA overlay ─────────────────────────────────────────────────────────
|
// ── DRAW SEA overlay ─────────────────────────────────────────────────────────
|
||||||
// Mirrors .sky-* structure but with columns reversed:
|
// Mirrors .sky-* structure but with columns reversed:
|
||||||
// left = transparent (Celtic Cross card positions)
|
// left = transparent (Celtic Cross card positions)
|
||||||
// right = rgba(--priUser) opaque (spread select)
|
// right = rgba(--priUser) opaque (spread select)
|
||||||
@@ -1548,14 +1569,17 @@ $_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6);
|
|||||||
100% { transform: perspective(600px) rotateY(0deg) scale(1); opacity: 1; }
|
100% { transform: perspective(600px) rotateY(0deg) scale(1); opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sea stage card title — polarity-specific colour + shared glow
|
// Sea stage card title — polarity-specific colour + glow. Drop shadow polarity-split:
|
||||||
$_sea-title-shadow: 1px 1px 0 rgba(0,0,0,1), 0 0 0.25rem rgba(var(--ninUser), 0.25);
|
// levity card has a light --secUser bg so a dark drop reads as smudge under the
|
||||||
|
// --quiUser title; gravity card is dark --priUser bg so the dark drop reads clean.
|
||||||
|
$_sea-title-shadow-levity: 1px 1px 0 rgba(255,255,255,1), 0 0 0.25rem rgba(var(--ninUser), 0.25);
|
||||||
|
$_sea-title-shadow-gravity: 1px 1px 0 rgba(0,0,0,1), 0 0 0.25rem rgba(var(--ninUser), 0.25);
|
||||||
$_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .fan-card-reversal-name, .fan-card-reversal-qualifier';
|
$_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .fan-card-reversal-name, .fan-card-reversal-qualifier';
|
||||||
|
|
||||||
.sea-stage--levity .sea-stage-card {
|
.sea-stage--levity .sea-stage-card {
|
||||||
@include stage-card-polarity(
|
@include stage-card-polarity(
|
||||||
$titles-color: rgba(var(--quiUser), 1),
|
$titles-color: rgba(var(--quiUser), 1),
|
||||||
$text-shadow: $_sea-title-shadow,
|
$text-shadow: $_sea-title-shadow-levity,
|
||||||
$invert-frame: true,
|
$invert-frame: true,
|
||||||
);
|
);
|
||||||
color: rgba(var(--priUser), 1);
|
color: rgba(var(--priUser), 1);
|
||||||
@@ -1565,7 +1589,7 @@ $_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .f
|
|||||||
.sea-stage--gravity .sea-stage-card {
|
.sea-stage--gravity .sea-stage-card {
|
||||||
@include stage-card-polarity(
|
@include stage-card-polarity(
|
||||||
$titles-color: rgba(var(--terUser), 1),
|
$titles-color: rgba(var(--terUser), 1),
|
||||||
$text-shadow: $_sea-title-shadow,
|
$text-shadow: $_sea-title-shadow-gravity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// ─── Sky (Pick Sky) overlay ────────────────────────────────────────────────
|
// ─── Sky (Pick Sky) overlay ────────────────────────────────────────────────
|
||||||
// Gaussian backdrop + centred modal, matching the gate/sig overlay pattern.
|
// Gaussian backdrop + centred modal, matching the gate/sig overlay pattern.
|
||||||
// Open state: html.sky-open (added by JS on PICK SKY click).
|
// Open state: html.sky-open (added by JS on CAST SKY click).
|
||||||
//
|
//
|
||||||
// Layout: header / two-column body (form | wheel) / footer
|
// Layout: header / two-column body (form | wheel) / footer
|
||||||
// Collapses to stacked single-column below 600 px.
|
// Collapses to stacked single-column below 600 px.
|
||||||
@@ -956,7 +956,7 @@ body[class*="-light"] #id_sky_tooltip_2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DEL btn pinned at the wheel center — appears wherever a wheel is shown
|
// DEL btn pinned at the wheel center — appears wherever a wheel is shown
|
||||||
// (Dashsky form#id_sky_delete_form, PICK SKY overlay #id_sky_delete_btn,
|
// (Dashsky form#id_sky_delete_form, CAST SKY overlay #id_sky_delete_btn,
|
||||||
// My Sky applet #id_applet_sky_delete_btn). Anchored to .sky-wheel-col /
|
// My Sky applet #id_applet_sky_delete_btn). Anchored to .sky-wheel-col /
|
||||||
// the applet section (both position:relative) so the btn sits over the SVG's
|
// the applet section (both position:relative) so the btn sits over the SVG's
|
||||||
// geometric center regardless of the surrounding layout.
|
// geometric center regardless of the surrounding layout.
|
||||||
@@ -1021,7 +1021,7 @@ body[class*="-light"] #id_sky_tooltip_2 {
|
|||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The (max-width:600px) block (written for the in-room PICK SKY modal where
|
// The (max-width:600px) block (written for the in-room CAST SKY modal where
|
||||||
// form-col is flex-row) sets align-self:flex-end on the btn — that's "right"
|
// form-col is flex-row) sets align-self:flex-end on the btn — that's "right"
|
||||||
// once we flip to flex-column. Reset.
|
// once we flip to flex-column. Reset.
|
||||||
.sky-page .sky-form-col > #id_sky_confirm {
|
.sky-page .sky-form-col > #id_sky_confirm {
|
||||||
|
|||||||
@@ -548,14 +548,14 @@ describe("SigSelect", () => {
|
|||||||
.toBe(card.dataset.nameTitle);
|
.toBe(card.dataset.nameTitle);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("major arcana reversed face: title, in qualifier slot (first after spin); qualifier in name slot (second)", () => {
|
it("major arcana reversed face: title in name slot (visually top after spin); qualifier in qualifier slot (visually bottom)", () => {
|
||||||
makeFixture({ polarity: "levity", userRole: "PC" });
|
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||||
card.dataset.arcana = "Major Arcana";
|
card.dataset.arcana = "Major Arcana";
|
||||||
card.dataset.nameTitle = "The Schizo";
|
card.dataset.nameTitle = "The Schizo";
|
||||||
hover();
|
hover();
|
||||||
// DOM-second element appears first after card spins — so title goes in qualifier slot
|
// Class matches semantic content: title → .fan-card-reversal-name, qualifier → .fan-card-reversal-qualifier
|
||||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("The Schizo,");
|
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("The Schizo,");
|
||||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("Elevated");
|
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Elevated");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("non-major without data-reversal-qualifier: qualifier mirrors polarity, name repeats card title", () => {
|
it("non-major without data-reversal-qualifier: qualifier mirrors polarity, name repeats card title", () => {
|
||||||
@@ -812,7 +812,7 @@ describe("SigSelect", () => {
|
|||||||
|
|
||||||
// ── polarity_room_done → tray sequence ─────────────────────────────────── //
|
// ── polarity_room_done → tray sequence ─────────────────────────────────── //
|
||||||
//
|
//
|
||||||
// After all 3 gamers in the user's polarity confirm TAKE SIG and the
|
// After all 3 gamers in the user's polarity confirm SAVE SIG and the
|
||||||
// 12s countdown expires, the server fires room:polarity_room_done. The
|
// 12s countdown expires, the server fires room:polarity_room_done. The
|
||||||
// sig-select handler should: (1) play the tray sequence — Tray.placeSig
|
// sig-select handler should: (1) play the tray sequence — Tray.placeSig
|
||||||
// with the user's selected stage card; (2) on Tray.placeSig's completion
|
// with the user's selected stage card; (2) on Tray.placeSig's completion
|
||||||
@@ -876,7 +876,7 @@ describe("SigSelect", () => {
|
|||||||
it("does NOT add the waiting msg when pick_sky_btn is already revealed", () => {
|
it("does NOT add the waiting msg when pick_sky_btn is already revealed", () => {
|
||||||
// pick_sky_available may fire DURING the tray sequence (other
|
// pick_sky_available may fire DURING the tray sequence (other
|
||||||
// polarity finishes first). When the tray callback then hangs +
|
// polarity finishes first). When the tray callback then hangs +
|
||||||
// dismisses, _settle must check whether PICK SKY is up and skip
|
// dismisses, _settle must check whether CAST SKY is up and skip
|
||||||
// the "Levity appraising…" / "Gravity settling…" message so it
|
// the "Levity appraising…" / "Gravity settling…" message so it
|
||||||
// doesn't co-exist w. the btn.
|
// doesn't co-exist w. the btn.
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
|
|||||||
@@ -270,7 +270,7 @@
|
|||||||
// Only redraw the wheel when a saved sky already exists on the page —
|
// Only redraw the wheel when a saved sky already exists on the page —
|
||||||
// pre-first-save we suppress the live wheel preview so it doesn't
|
// pre-first-save we suppress the live wheel preview so it doesn't
|
||||||
// shunt the form (and SAVE SKY) below the fold. Mirrors the My Sky
|
// shunt the form (and SAVE SKY) below the fold. Mirrors the My Sky
|
||||||
// applet's "no wheel until saved" UX. The in-room PICK SKY overlay
|
// applet's "no wheel until saved" UX. The in-room CAST SKY overlay
|
||||||
// intentionally still previews live.
|
// intentionally still previews live.
|
||||||
if (_savedSky) {
|
if (_savedSky) {
|
||||||
if (svgEl.querySelector('*')) {
|
if (svgEl.querySelector('*')) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{# PICK SEA overlay — Celtic Cross spread entry #}
|
{# DRAW SEA overlay — Celtic Cross spread entry #}
|
||||||
{# Included in room.html when table_status == "SKY_SELECT" and sky_confirmed #}
|
{# Included in room.html when table_status == "SKY_SELECT" and sky_confirmed #}
|
||||||
{# Layout is the reverse of PICK SKY: cards left (transparent), form right #}
|
{# Layout is the reverse of CAST SKY: cards left (transparent), form right #}
|
||||||
|
|
||||||
<div class="sea-backdrop"></div>
|
<div class="sea-backdrop"></div>
|
||||||
<div class="sea-overlay" id="id_sea_overlay"
|
<div class="sea-overlay" id="id_sea_overlay"
|
||||||
@@ -156,6 +156,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="fan-card-arcana"></p>
|
<p class="fan-card-arcana"></p>
|
||||||
<div class="fan-card-face-reversal">
|
<div class="fan-card-face-reversal">
|
||||||
|
{# Default DOM order — matches non-major arcana layout. stage-card.js #}
|
||||||
|
{# swaps the class names on these <p>s for Major arcana so each #}
|
||||||
|
{# element's class still matches its semantic content. #}
|
||||||
<p class="fan-card-reversal-name"></p>
|
<p class="fan-card-reversal-name"></p>
|
||||||
<p class="fan-card-reversal-qualifier"></p>
|
<p class="fan-card-reversal-qualifier"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
|
|||||||
<p class="fan-card-arcana"></p>
|
<p class="fan-card-arcana"></p>
|
||||||
<p class="fan-card-correspondence"></p>{# not shown in sig-select — game-kit only #}
|
<p class="fan-card-correspondence"></p>{# not shown in sig-select — game-kit only #}
|
||||||
<div class="fan-card-face-reversal">
|
<div class="fan-card-face-reversal">
|
||||||
|
{# Default DOM order — matches non-major arcana layout (qualifier visually #}
|
||||||
|
{# on top after spin, title visually below). stage-card.js swaps the class #}
|
||||||
|
{# names on these <p>s for Major arcana so each element's class still #}
|
||||||
|
{# matches its semantic content (title → .fan-card-reversal-name etc.). #}
|
||||||
<p class="fan-card-reversal-name"></p>
|
<p class="fan-card-reversal-name"></p>
|
||||||
<p class="fan-card-reversal-qualifier"></p>
|
<p class="fan-card-reversal-qualifier"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{# PICK SKY overlay — natal chart entry + D3 wheel preview #}
|
{# CAST SKY overlay — natal chart entry + D3 wheel preview #}
|
||||||
{# Included in room.html when table_status == "SKY_SELECT" #}
|
{# Included in room.html when table_status == "SKY_SELECT" #}
|
||||||
{# Opens when user clicks #id_pick_sky_btn; html.sky-open controls #}
|
{# Opens when user clicks #id_pick_sky_btn; html.sky-open controls #}
|
||||||
{# visibility via CSS — backdrop-filter blur + centred modal. #}
|
{# visibility via CSS — backdrop-filter blur + centred modal. #}
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
{# ── Wheel column ─────────────────────────────────────── #}
|
{# ── Wheel column ─────────────────────────────────────── #}
|
||||||
{# DEL btn is JS-injected after the wheel paints (see schedule #}
|
{# DEL btn is JS-injected after the wheel paints (see schedule #}
|
||||||
{# Preview success handler) — keeping it out of the template #}
|
{# Preview success handler) — keeping it out of the template #}
|
||||||
{# means a blank PICK SKY modal can never show a DEL action #}
|
{# means a blank CAST SKY modal can never show a DEL action #}
|
||||||
{# against a non-existent wheel. #}
|
{# against a non-existent wheel. #}
|
||||||
<div class="sky-wheel-col">
|
<div class="sky-wheel-col">
|
||||||
<svg id="id_sky_svg" class="sky-svg"></svg>
|
<svg id="id_sky_svg" class="sky-svg"></svg>
|
||||||
@@ -400,12 +400,12 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Sky confirmed → close sky & reload to land on hex w. PICK SEA ──────
|
// ── Sky confirmed → close sky & reload to land on hex w. DRAW SEA ──────
|
||||||
//
|
//
|
||||||
// The gamer should witness the table hex (now showing PICK SEA in place of
|
// The gamer should witness the table hex (now showing DRAW SEA in place of
|
||||||
// PICK SKY) before opting into the sea overlay. We reload the room page —
|
// CAST SKY) before opting into the sea overlay. We reload the room page —
|
||||||
// the server-side template will re-render with `sky_confirmed=True` so the
|
// the server-side template will re-render with `sky_confirmed=True` so the
|
||||||
// hex's btn flips automatically, and the user clicks PICK SEA to continue.
|
// hex's btn flips automatically, and the user clicks DRAW SEA to continue.
|
||||||
|
|
||||||
function _onSkyConfirmed() {
|
function _onSkyConfirmed() {
|
||||||
closeSky();
|
closeSky();
|
||||||
@@ -413,7 +413,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── DEL btn — JS-injected after the wheel paints; absent on a blank modal
|
// ── DEL btn — JS-injected after the wheel paints; absent on a blank modal
|
||||||
// PICK SKY's wheel is a live preview; un-saved data lives only in LS_KEY.
|
// CAST SKY's wheel is a live preview; un-saved data lives only in LS_KEY.
|
||||||
// The btn is created lazily after the first SkyWheel.draw so a blank modal
|
// The btn is created lazily after the first SkyWheel.draw so a blank modal
|
||||||
// can never offer a DEL action against a non-existent wheel; clearing the
|
// can never offer a DEL action against a non-existent wheel; clearing the
|
||||||
// SVG removes the btn from the DOM entirely (re-injected on next preview).
|
// SVG removes the btn from the DOM entirely (re-injected on next preview).
|
||||||
|
|||||||
@@ -43,16 +43,20 @@
|
|||||||
<p class="fan-card-arcana">{{ card.get_arcana_display }}</p>
|
<p class="fan-card-arcana">{{ card.get_arcana_display }}</p>
|
||||||
<div class="fan-card-face-reversal">
|
<div class="fan-card-face-reversal">
|
||||||
{% comment %}
|
{% comment %}
|
||||||
DOM order: reversal-name first, reversal-qualifier second.
|
Class names always match semantic content: qualifier text in
|
||||||
After SPIN's 180° rotation DOM-second appears visually on top.
|
.fan-card-reversal-qualifier, title text in .fan-card-reversal-name.
|
||||||
|
DOM order is per-arcana, controlling visual layout after the 180°
|
||||||
|
SPIN rotation (DOM-second appears visually on top):
|
||||||
|
Major / polarity-split — title on top → name class is DOM-second
|
||||||
|
Non-major — qualifier on top → qualifier class is DOM-second
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% if card.gravity_reversal %}
|
{% if card.gravity_reversal %}
|
||||||
{# Polarity-split reversal title — single line, qualifier slot empty. Title goes in the qualifier slot so it visually lands on top after spin. #}
|
{# Polarity-split: single-line title in the name slot, qualifier slot empty. #}
|
||||||
<p class="fan-card-reversal-name"></p>
|
<p class="fan-card-reversal-qualifier"></p>
|
||||||
<p class="fan-card-reversal-qualifier {{ card.title_squeeze_class }}">{{ card.gravity_reversal|italicize:card.italic_word }}</p>
|
<p class="fan-card-reversal-name {{ card.title_squeeze_class }}">{{ card.gravity_reversal|italicize:card.italic_word }}</p>
|
||||||
{% elif card.arcana == "MAJOR" %}
|
{% elif card.arcana == "MAJOR" %}
|
||||||
<p class="fan-card-reversal-name">{{ card.gravity_qualifier|default:card.levity_qualifier }}</p>
|
<p class="fan-card-reversal-qualifier">{{ card.gravity_qualifier|default:card.levity_qualifier }}</p>
|
||||||
<p class="fan-card-reversal-qualifier {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}{% if card.gravity_qualifier %},{% endif %}</p>
|
<p class="fan-card-reversal-name {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}{% if card.gravity_qualifier %},{% endif %}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="fan-card-reversal-name {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}</p>
|
<p class="fan-card-reversal-name {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}</p>
|
||||||
<p class="fan-card-reversal-qualifier">{{ card.reversal_qualifier|default:card.gravity_qualifier }}</p>
|
<p class="fan-card-reversal-qualifier">{{ card.reversal_qualifier|default:card.gravity_qualifier }}</p>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div id="id_pick_sigs_wrap"{% if starter_roles|length < 6 %} style="display:none"{% endif %}>
|
<div id="id_pick_sigs_wrap"{% if starter_roles|length < 6 %} style="display:none"{% endif %}>
|
||||||
<form method="POST" action="{% url 'epic:pick_sigs' room.id %}">
|
<form method="POST" action="{% url 'epic:pick_sigs' room.id %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button id="id_pick_sigs_btn" type="submit" class="btn btn-primary">PICK<br>SIGS</button>
|
<button id="id_pick_sigs_btn" type="submit" class="btn btn-primary">SCAN<br>SIGS</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -37,12 +37,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if room.table_status == "SKY_SELECT" %}
|
{% if room.table_status == "SKY_SELECT" %}
|
||||||
{% if sky_confirmed %}
|
{% if sky_confirmed %}
|
||||||
<button id="id_pick_sea_btn" class="btn btn-primary">PICK<br>SEA</button>
|
<button id="id_pick_sea_btn" class="btn btn-primary">DRAW<br>SEA</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button id="id_pick_sky_btn" class="btn btn-primary">PICK<br>SKY</button>
|
<button id="id_pick_sky_btn" class="btn btn-primary">CAST<br>SKY</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif room.table_status == "SIG_SELECT" %}
|
{% elif room.table_status == "SIG_SELECT" %}
|
||||||
<button id="id_pick_sky_btn" class="btn btn-primary" style="display:none">PICK<br>SKY</button>
|
<button id="id_pick_sky_btn" class="btn btn-primary" style="display:none">CAST<br>SKY</button>
|
||||||
{% if polarity_done %}
|
{% if polarity_done %}
|
||||||
<p id="id_hex_waiting_msg">{% if user_polarity == "levity" %}Gravity settling . . .{% else %}Levity appraising . . .{% endif %}</p>
|
<p id="id_hex_waiting_msg">{% if user_polarity == "levity" %}Gravity settling . . .{% else %}Levity appraising . . .{% endif %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user