Compare commits
3 Commits
8ca3f79561
...
9a00f96fe5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a00f96fe5 | ||
|
|
544ce978d5 | ||
|
|
0e4101ce95 |
@@ -102,7 +102,7 @@ var SigSelect = (function () {
|
||||
// (unified w. my_sign 2026-06-03) — previously the sig stat-block was a
|
||||
// reduced label-only copy, so this call was absent and those fields
|
||||
// stayed blank.
|
||||
StageCard.populateStatExtras(statBlock, card);
|
||||
StageCard.populateStatExtras(statBlock, card, { polarity: userPolarity });
|
||||
|
||||
stageCard.style.display = '';
|
||||
stage.classList.add('sig-stage--active');
|
||||
|
||||
@@ -287,8 +287,38 @@ var StageCard = (function () {
|
||||
var rawTitle = card.name_title || card.name || '';
|
||||
var title = rawTitle.split(',')[0].trim();
|
||||
var arcana = _arcanaDisplay(card);
|
||||
var isMajor = _isMajor(card);
|
||||
|
||||
// Polarity qualifier (Elevated/Enlightened/Graven) on the stat-block —
|
||||
// the always-visible home now that image-mode decks hide the text card
|
||||
// face. Mirrors populateCard's above/below placement: non-major above
|
||||
// the title, major below it (title carries a trailing comma so the
|
||||
// qualifier reads as a subtitle). Only filled when a polarity is
|
||||
// supplied (sig / my_sign / sea); the kit fan omits it. Cards 48-49 +
|
||||
// trumps 19-21 carry a polarity-split emanation override whose title
|
||||
// already incorporates the qualifier inline → no separate slot.
|
||||
var polarity = opts.polarity || '';
|
||||
var qualifier = polarity === 'levity' ? (card.levity_qualifier || '')
|
||||
: polarity === 'gravity' ? (card.gravity_qualifier || '')
|
||||
: '';
|
||||
var emanationOverride = polarity === 'levity' ? (card.levity_emanation || '')
|
||||
: polarity === 'gravity' ? (card.gravity_emanation || '')
|
||||
: '';
|
||||
if (emanationOverride) {
|
||||
title = emanationOverride;
|
||||
qualifier = '';
|
||||
}
|
||||
var qAbove = qualifier && !isMajor ? qualifier : '';
|
||||
var qBelow = qualifier && isMajor ? qualifier : '';
|
||||
|
||||
statBlock.querySelectorAll('.stat-face-title').forEach(function (el) {
|
||||
el.textContent = title;
|
||||
el.textContent = qBelow ? title + ',' : title;
|
||||
});
|
||||
statBlock.querySelectorAll('.stat-face-qualifier--above').forEach(function (el) {
|
||||
el.textContent = qAbove;
|
||||
});
|
||||
statBlock.querySelectorAll('.stat-face-qualifier--below').forEach(function (el) {
|
||||
el.textContent = qBelow;
|
||||
});
|
||||
statBlock.querySelectorAll('.stat-face-arcana').forEach(function (el) {
|
||||
el.textContent = arcana;
|
||||
|
||||
@@ -6,11 +6,12 @@ the hex. No partial scroll — `scroll-snap-stop: always` enforces the binary.
|
||||
|
||||
FT bucket: game_room. Built to DRY-template onto my_sea next.
|
||||
"""
|
||||
from django.test import tag
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from .base import FunctionalTest
|
||||
from .base import FunctionalTest, ChannelsFunctionalTest
|
||||
from .room_page import _equip_earthman_deck, _fill_room_via_orm
|
||||
from apps.drama.models import GameEvent
|
||||
from apps.drama.models import GameEvent, record
|
||||
from apps.epic.models import Room
|
||||
from apps.lyric.models import User
|
||||
|
||||
@@ -189,3 +190,54 @@ class RoomScrollOfEventsTest(FunctionalTest):
|
||||
By.CSS_SELECTOR,
|
||||
"#id_drama_scroll .drama-event[data-label='frame']",
|
||||
).is_displayed())
|
||||
|
||||
|
||||
@tag("channels")
|
||||
class RoomScrollLiveRefreshTest(ChannelsFunctionalTest):
|
||||
"""The SCROLL applet refreshes live over WebSocket — no page reload. A
|
||||
GameEvent recorded by any actor nudges every open room socket (record() →
|
||||
on_commit broadcast → RoomConsumer.scroll_update relay → room-scroll.js
|
||||
re-fetches `core/_partials/_scroll.html` + swaps #id_drama_scroll), so an
|
||||
open feed grows on its own. Needs daphne + a real channel layer (channels
|
||||
stage)."""
|
||||
|
||||
EMAILS = [
|
||||
"disco@test.io", "amigo@test.io", "bud@test.io",
|
||||
"pal@test.io", "dude@test.io", "bro@test.io",
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.viewer = User.objects.create(email="disco@test.io", username="disco")
|
||||
_equip_earthman_deck(self.viewer)
|
||||
self.room = Room.objects.create(name="Willawonky", owner=self.viewer)
|
||||
_fill_room_via_orm(self.room, self.EMAILS)
|
||||
# One seed line so the feed isn't empty at open.
|
||||
record(self.room, GameEvent.SLOT_FILLED, actor=self.viewer,
|
||||
slot_number=1, token_type="carte", token_display="Carte Blanche",
|
||||
renewal_days=7)
|
||||
self.room.table_status = Room.ROLE_SELECT
|
||||
self.room.gate_status = Room.OPEN
|
||||
self.room.save()
|
||||
|
||||
def _rows(self):
|
||||
return len(self.browser.find_elements(
|
||||
By.CSS_SELECTOR, "#id_drama_scroll .drama-event"))
|
||||
|
||||
def test_feed_grows_live_when_an_event_is_recorded(self):
|
||||
self.create_pre_authenticated_session("disco@test.io")
|
||||
self.browser.set_window_size(820, 600)
|
||||
self.browser.get(self.live_server_url + f"/gameboard/room/{self.room.id}/")
|
||||
# Wait for the room WebSocket to actually open (room.js sets _roomSocket).
|
||||
self.wait_for(lambda: self.assertEqual(
|
||||
self.browser.execute_script(
|
||||
"return window._roomSocket ? window._roomSocket.readyState : -1;"),
|
||||
1))
|
||||
self.wait_for(lambda: self.assertEqual(self._rows(), 1))
|
||||
|
||||
# Another action is recorded server-side → broadcasts scroll_update on
|
||||
# commit; the open feed must grow WITHOUT a reload.
|
||||
record(self.room, GameEvent.SLOT_FILLED, actor=self.viewer,
|
||||
slot_number=2, token_type="carte", token_display="Carte Blanche",
|
||||
renewal_days=7)
|
||||
self.wait_for(lambda: self.assertEqual(self._rows(), 2))
|
||||
|
||||
@@ -255,57 +255,80 @@ class SigSelectThemeTest(FunctionalTest):
|
||||
ActionChains(self.browser).move_to_element(card).perform()
|
||||
return card
|
||||
|
||||
def _select_card_and_freeze(self, css):
|
||||
"""Click a card → OK → wait for the stage to freeze. The stat-block is
|
||||
`display:none` until `.sig-stage--frozen` (only the card preview shows
|
||||
on hover); OK'ing a card reveals it. The qualifier lives on the frozen
|
||||
stat-block now (the hover-only text card-face is going away as decks
|
||||
gain images), so assertions need the frozen state."""
|
||||
card = self.browser.find_element(By.CSS_SELECTOR, css)
|
||||
card.click()
|
||||
ok = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-focused .sig-ok-btn")
|
||||
)
|
||||
ok.click()
|
||||
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-stage--frozen"))
|
||||
|
||||
# ── ST1: Levity (Leavened) qualifier ──────────────────────────────────── #
|
||||
|
||||
# Qualifier asserts target the frozen stat-block (.stat-face--upright), not
|
||||
# the text card-face: image-mode decks hide .fan-card-face, and the qualifier
|
||||
# is moving to the always-present stat-block. It mirrors the card-face
|
||||
# above/below placement — non-major puts the qualifier ABOVE the title, major
|
||||
# BELOW it. See stage-card.js populateStatExtras + _stat_face.html.
|
||||
UPRIGHT_QUAL_ABOVE = ".sig-stat-block .stat-face--upright .stat-face-qualifier--above"
|
||||
UPRIGHT_QUAL_BELOW = ".sig-stat-block .stat-face--upright .stat-face-qualifier--below"
|
||||
|
||||
def test_levity_non_major_card_shows_elevated_above(self):
|
||||
"""Hovering a non-major card in the levity overlay shows 'Elevated' in
|
||||
qualifier-above and nothing in qualifier-below."""
|
||||
"""Selecting (OK) a non-major card shows 'Elevated' in the frozen
|
||||
stat-block qualifier-above and nothing in qualifier-below."""
|
||||
room = self._setup_sig_room()
|
||||
self.create_pre_authenticated_session("founder@test.io") # PC = levity
|
||||
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||
|
||||
self._hover_card('.sig-card[data-arcana="Middle Arcana"]')
|
||||
self._select_card_and_freeze('.sig-card[data-arcana="Middle Arcana"]')
|
||||
|
||||
above = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
|
||||
)
|
||||
self.assertEqual(above.text, "Elevated")
|
||||
below = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below")
|
||||
self.wait_for(lambda: self.assertEqual(
|
||||
self.browser.find_element(By.CSS_SELECTOR, self.UPRIGHT_QUAL_ABOVE).text,
|
||||
"Elevated",
|
||||
))
|
||||
below = self.browser.find_element(By.CSS_SELECTOR, self.UPRIGHT_QUAL_BELOW)
|
||||
self.assertEqual(below.text, "")
|
||||
|
||||
def test_levity_major_card_shows_enlightened_below(self):
|
||||
"""Hovering a major arcana card in the levity overlay shows 'Enlightened' in
|
||||
qualifier-below and nothing in qualifier-above."""
|
||||
"""Selecting (OK) a major arcana card shows 'Enlightened' in the frozen
|
||||
stat-block qualifier-below and nothing in qualifier-above."""
|
||||
room = self._setup_sig_room()
|
||||
self.create_pre_authenticated_session("founder@test.io") # PC = levity
|
||||
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||
|
||||
self._hover_card('.sig-card[data-arcana="Major Arcana"]')
|
||||
self._select_card_and_freeze('.sig-card[data-arcana="Major Arcana"]')
|
||||
|
||||
below = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below")
|
||||
)
|
||||
self.assertEqual(below.text, "Enlightened")
|
||||
above = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
|
||||
self.wait_for(lambda: self.assertEqual(
|
||||
self.browser.find_element(By.CSS_SELECTOR, self.UPRIGHT_QUAL_BELOW).text,
|
||||
"Enlightened",
|
||||
))
|
||||
above = self.browser.find_element(By.CSS_SELECTOR, self.UPRIGHT_QUAL_ABOVE)
|
||||
self.assertEqual(above.text, "")
|
||||
|
||||
# ── ST2: Gravity (Graven) qualifier ───────────────────────────────────── #
|
||||
|
||||
def test_gravity_non_major_card_shows_graven_above(self):
|
||||
"""EC (bud) sees the gravity overlay; hovering a non-major card shows 'Graven'."""
|
||||
"""EC (bud) sees the gravity overlay; selecting (OK) a non-major card
|
||||
shows 'Graven' in the frozen stat-block qualifier-above."""
|
||||
room = self._setup_sig_room()
|
||||
self.create_pre_authenticated_session("bud@test.io") # EC = gravity
|
||||
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||
|
||||
self._hover_card('.sig-card[data-arcana="Middle Arcana"]')
|
||||
self._select_card_and_freeze('.sig-card[data-arcana="Middle Arcana"]')
|
||||
|
||||
above = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
|
||||
)
|
||||
self.assertEqual(above.text, "Graven")
|
||||
self.wait_for(lambda: self.assertEqual(
|
||||
self.browser.find_element(By.CSS_SELECTOR, self.UPRIGHT_QUAL_ABOVE).text,
|
||||
"Graven",
|
||||
))
|
||||
|
||||
# ── ST3: Correspondence not shown ─────────────────────────────────────── #
|
||||
|
||||
@@ -325,6 +348,72 @@ class SigSelectThemeTest(FunctionalTest):
|
||||
self.assertEqual(corr.text, "")
|
||||
|
||||
|
||||
class SigStageUnifiedTest(FunctionalTest):
|
||||
"""The Sig Select stage unified with the my_sign card-stage apparatus
|
||||
(2026-06-03): the green --duoUser felt fills the hex pane (no dark Gaussian
|
||||
backdrop), and the stat-block is the shared DRY `_stat_face.html` (rank-chip
|
||||
+ title + arcana), not the old label-only copy. No WebSocket — local stage
|
||||
updates; plain FunctionalTest."""
|
||||
|
||||
EMAILS = [
|
||||
"founder@test.io", "amigo@test.io", "bud@test.io",
|
||||
"pal@test.io", "dude@test.io", "bro@test.io",
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.browser.set_window_size(800, 1200)
|
||||
Applet.objects.get_or_create(slug="new-game", defaults={"name": "New Game", "context": "gameboard"})
|
||||
Applet.objects.get_or_create(slug="my-games", defaults={"name": "My Games", "context": "gameboard"})
|
||||
|
||||
def _open_sig(self):
|
||||
founder, _ = User.objects.get_or_create(email=self.EMAILS[0])
|
||||
room = Room.objects.create(name="Felt Test", owner=founder)
|
||||
_fill_room_via_orm(room, self.EMAILS)
|
||||
_assign_all_roles(room)
|
||||
self.create_pre_authenticated_session("founder@test.io") # PC = levity
|
||||
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||
return room
|
||||
|
||||
def test_stage_fills_hex_pane_on_felt_no_dark_backdrop(self):
|
||||
"""The sig stage renders INSIDE the hex pane on edge-to-edge green felt
|
||||
(my_sea-style), replacing the old fixed dark-Gaussian modal."""
|
||||
self._open_sig()
|
||||
self.assertTrue(
|
||||
self.browser.find_elements(By.CSS_SELECTOR, ".room-hex-pane.has-sig-stage"))
|
||||
# The overlay is a descendant of the hex pane (not a root-level modal).
|
||||
inside = self.browser.execute_script(
|
||||
"var o=document.querySelector('.sig-overlay');"
|
||||
"return !!(o && o.closest('.room-hex-pane'));")
|
||||
self.assertTrue(inside)
|
||||
# The dark blur backdrop element is gone…
|
||||
self.assertFalse(self.browser.find_elements(By.CSS_SELECTOR, ".sig-backdrop"))
|
||||
# …and the overlay's own background is the green --duoUser felt, not a
|
||||
# translucent-black wash.
|
||||
bg = self.browser.execute_script(
|
||||
"return getComputedStyle(document.querySelector('.sig-overlay'))"
|
||||
".backgroundColor;")
|
||||
self.assertNotIn("rgba(0, 0, 0", bg)
|
||||
|
||||
def test_stat_block_is_dry_stat_face_with_rank_and_title(self):
|
||||
"""OK'ing a card freezes the stage and reveals the rich DRY stat-face —
|
||||
rank-chip + title + arcana (the old reduced block had none of these)."""
|
||||
self._open_sig()
|
||||
card = self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-card"))
|
||||
card.click()
|
||||
ok = self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-ok-btn"))
|
||||
ok.click()
|
||||
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-stage--frozen"))
|
||||
# populateStatExtras filled the title + rank chip (was the missing call).
|
||||
title = self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".sig-stat-block .stat-face-title")
|
||||
rank = self.browser.find_element(
|
||||
By.CSS_SELECTOR, ".sig-stat-block .stat-chip-rank")
|
||||
self.wait_for(lambda: self.assertTrue(title.text.strip()))
|
||||
self.assertTrue(rank.text.strip())
|
||||
|
||||
|
||||
# ── SAVE SIG / WAIT NVM — ready gate ──────────────────────────────────────────
|
||||
#
|
||||
# SAVE SIG (.btn.btn-primary) appears at the bottom-left corner of the card
|
||||
|
||||
@@ -34,10 +34,18 @@ describe("SigSelect", () => {
|
||||
<button class="btn btn-info fyi-btn" type="button">FYI</button>
|
||||
<div class="stat-face stat-face--upright">
|
||||
<p class="stat-face-label">Emanation</p>
|
||||
<p class="stat-face-qualifier stat-face-qualifier--above"></p>
|
||||
<p class="stat-face-title"></p>
|
||||
<p class="stat-face-qualifier stat-face-qualifier--below"></p>
|
||||
<p class="stat-face-arcana"></p>
|
||||
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
||||
</div>
|
||||
<div class="stat-face stat-face--reversed">
|
||||
<p class="stat-face-label">Reversal</p>
|
||||
<p class="stat-face-qualifier stat-face-qualifier--above"></p>
|
||||
<p class="stat-face-title"></p>
|
||||
<p class="stat-face-qualifier stat-face-qualifier--below"></p>
|
||||
<p class="stat-face-arcana"></p>
|
||||
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
||||
</div>
|
||||
<button class="btn btn-nav-left fyi-prev" type="button">◀</button>
|
||||
@@ -742,6 +750,51 @@ describe("SigSelect", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Polarity theming — stat-block qualifier ────────────────────────────── //
|
||||
//
|
||||
// The qualifier's always-visible home: image-mode decks hide the text card
|
||||
// face, so the stat-block (.stat-face--upright) carries Elevated/Enlightened/
|
||||
// Graven now. Mirrors the card-face above/below placement (non-major above
|
||||
// the title, major below it w. a trailing-comma title).
|
||||
|
||||
describe("polarity theming — stat-block qualifier", () => {
|
||||
const UPRIGHT = ".stat-face--upright";
|
||||
|
||||
it("levity non-major puts qualifier ABOVE the stat-block title, below empty", () => {
|
||||
makeFixture({ polarity: 'levity', userRole: 'PC' }); // fixture card = Minor Arcana
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("Elevated");
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("");
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-title").textContent).toBe("King of Pentacles");
|
||||
});
|
||||
|
||||
it("levity major puts qualifier BELOW the stat-block title, above empty, title gets comma", () => {
|
||||
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||
card.dataset.arcana = "Major Arcana";
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("");
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("Elevated");
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-title").textContent).toBe("King of Pentacles,");
|
||||
});
|
||||
|
||||
it("gravity non-major puts 'Graven' ABOVE the stat-block title", () => {
|
||||
makeFixture({ polarity: 'gravity', userRole: 'BC' });
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("Graven");
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("");
|
||||
});
|
||||
|
||||
it("re-hovering a different arcana re-places the stat-block qualifier slot", () => {
|
||||
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); // non-major → above
|
||||
card.dataset.arcana = "Major Arcana";
|
||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); // major → below
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("");
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("Elevated");
|
||||
});
|
||||
});
|
||||
|
||||
// ── WAIT NVM glow pulse ────────────────────────────────────────────────────── //
|
||||
|
||||
describe("WAIT NVM glow pulse", () => {
|
||||
|
||||
@@ -176,7 +176,12 @@
|
||||
// the rest are --quiUser. Let's change the latter to match the
|
||||
// former" — applet's `.stat-face-title` was already --quaUser;
|
||||
// shared mixin now matches so all 4 stat-block surfaces unify).
|
||||
.stat-face-title {
|
||||
// Title + polarity qualifier share one style (user-spec 2026-06-03 —
|
||||
// "'Elevated' needs to be the same style as 'Jack of Crowns' below it").
|
||||
// Qualifier is the subtitle hugging the title (above for non-major, below
|
||||
// for major); both pick up the arcana-keyed color override below.
|
||||
.stat-face-title,
|
||||
.stat-face-qualifier {
|
||||
font-size: calc(var(--sig-card-w, 120px) * 0.105);
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
@@ -184,7 +189,8 @@
|
||||
text-wrap: balance;
|
||||
color: rgba(var(--quaUser), 1);
|
||||
}
|
||||
[data-arcana-key="MAJOR"] .stat-face-title {
|
||||
[data-arcana-key="MAJOR"] .stat-face-title,
|
||||
[data-arcana-key="MAJOR"] .stat-face-qualifier {
|
||||
color: rgba(var(--terUser), 1);
|
||||
}
|
||||
|
||||
@@ -195,10 +201,15 @@
|
||||
opacity: 0.6;
|
||||
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07);
|
||||
}
|
||||
// `:empty` rule hides title + arcana when stage-card.js hasn't populated
|
||||
// them yet (rest state) — prevents zero-height paragraphs from inflating
|
||||
// the stat block vertical layout.
|
||||
// Qualifier hugs the title — tighten the gap on the side that touches it
|
||||
// (below the above-slot, above the below-slot) so the two read as one unit.
|
||||
.stat-face-qualifier--above { margin-bottom: calc(var(--sig-card-w, 120px) * 0.01); }
|
||||
.stat-face-qualifier--below { margin-top: calc(var(--sig-card-w, 120px) * -0.01); }
|
||||
// `:empty` rule hides title + arcana + qualifier slots when stage-card.js
|
||||
// hasn't populated them yet (rest state) — prevents zero-height paragraphs
|
||||
// from inflating the stat block vertical layout.
|
||||
.stat-face-title:empty,
|
||||
.stat-face-qualifier:empty,
|
||||
.stat-face-arcana:empty { display: none; }
|
||||
|
||||
.stat-keywords {
|
||||
|
||||
@@ -34,10 +34,18 @@ describe("SigSelect", () => {
|
||||
<button class="btn btn-info fyi-btn" type="button">FYI</button>
|
||||
<div class="stat-face stat-face--upright">
|
||||
<p class="stat-face-label">Emanation</p>
|
||||
<p class="stat-face-qualifier stat-face-qualifier--above"></p>
|
||||
<p class="stat-face-title"></p>
|
||||
<p class="stat-face-qualifier stat-face-qualifier--below"></p>
|
||||
<p class="stat-face-arcana"></p>
|
||||
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
||||
</div>
|
||||
<div class="stat-face stat-face--reversed">
|
||||
<p class="stat-face-label">Reversal</p>
|
||||
<p class="stat-face-qualifier stat-face-qualifier--above"></p>
|
||||
<p class="stat-face-title"></p>
|
||||
<p class="stat-face-qualifier stat-face-qualifier--below"></p>
|
||||
<p class="stat-face-arcana"></p>
|
||||
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
||||
</div>
|
||||
<button class="btn btn-nav-left fyi-prev" type="button">◀</button>
|
||||
@@ -742,6 +750,51 @@ describe("SigSelect", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Polarity theming — stat-block qualifier ────────────────────────────── //
|
||||
//
|
||||
// The qualifier's always-visible home: image-mode decks hide the text card
|
||||
// face, so the stat-block (.stat-face--upright) carries Elevated/Enlightened/
|
||||
// Graven now. Mirrors the card-face above/below placement (non-major above
|
||||
// the title, major below it w. a trailing-comma title).
|
||||
|
||||
describe("polarity theming — stat-block qualifier", () => {
|
||||
const UPRIGHT = ".stat-face--upright";
|
||||
|
||||
it("levity non-major puts qualifier ABOVE the stat-block title, below empty", () => {
|
||||
makeFixture({ polarity: 'levity', userRole: 'PC' }); // fixture card = Minor Arcana
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("Elevated");
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("");
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-title").textContent).toBe("King of Pentacles");
|
||||
});
|
||||
|
||||
it("levity major puts qualifier BELOW the stat-block title, above empty, title gets comma", () => {
|
||||
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||
card.dataset.arcana = "Major Arcana";
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("");
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("Elevated");
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-title").textContent).toBe("King of Pentacles,");
|
||||
});
|
||||
|
||||
it("gravity non-major puts 'Graven' ABOVE the stat-block title", () => {
|
||||
makeFixture({ polarity: 'gravity', userRole: 'BC' });
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("Graven");
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("");
|
||||
});
|
||||
|
||||
it("re-hovering a different arcana re-places the stat-block qualifier slot", () => {
|
||||
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); // non-major → above
|
||||
card.dataset.arcana = "Major Arcana";
|
||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); // major → below
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--above").textContent).toBe("");
|
||||
expect(statBlock.querySelector(UPRIGHT + " .stat-face-qualifier--below").textContent).toBe("Elevated");
|
||||
});
|
||||
});
|
||||
|
||||
// ── WAIT NVM glow pulse ────────────────────────────────────────────────────── //
|
||||
|
||||
describe("WAIT NVM glow pulse", () => {
|
||||
|
||||
@@ -238,7 +238,7 @@ this billboard surface re-brands to "Sign".
|
||||
StageCard.populateCard(stageCard, _currentCard, _polarity());
|
||||
StageCard.populateKeywords(statBlock,
|
||||
_currentCard.keywords_upright, _currentCard.keywords_reversed);
|
||||
StageCard.populateStatExtras(statBlock, _currentCard);
|
||||
StageCard.populateStatExtras(statBlock, _currentCard, { polarity: _polarity() });
|
||||
_fyiData = StageCard.buildInfoData(_currentCard);
|
||||
_fyiIdx = 0;
|
||||
if (fyiPanel) StageCard.renderFyi(fyiPanel, _fyiData, _fyiIdx);
|
||||
|
||||
@@ -32,7 +32,15 @@ Args:
|
||||
<p class="stat-face-label">{{ label_text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{# Polarity qualifier (Elevated/Enlightened/Graven) — mirrors the card-face #}
|
||||
{# above/below placement: non-major arcana puts it ABOVE the title, major #}
|
||||
{# puts it BELOW (the title then carries a trailing comma). JS-only #}
|
||||
{# (stage-card.js populateStatExtras w. opts.polarity); empty server-side #}
|
||||
{# since the qualifier is polarity-dependent. The card-face copy is going #}
|
||||
{# away as decks gain images, so the stat-block becomes its sole home. #}
|
||||
<p class="stat-face-qualifier stat-face-qualifier--above"></p>
|
||||
<p class="stat-face-title">{% if card %}{{ card.name }}{% endif %}</p>
|
||||
<p class="stat-face-qualifier stat-face-qualifier--below"></p>
|
||||
<p class="stat-face-arcana">{% if card %}{{ card.get_arcana_display }}{% endif %}</p>
|
||||
<ul class="stat-keywords"{% if keywords_ul_id %} id="{{ keywords_ul_id }}"{% endif %}>{% if card %}{% for kw in card.keywords_upright %}<li>{{ kw }}</li>{% endfor %}{% endif %}</ul>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user