My Sea iter-4a follow-up batch: modal port + draw polish + label positioning + Major Arcana fix — TDD
User-driven bug-squash + UX-polish cycle on top of iter 4a (ca2a62f). All 14 fixes ship behind the same iter-4a banner since they close the substage's UX gaps without expanding scope to iter 4b's persistence layer.
SeaDeal modal port — extracted apps/gameboard/_partials/_sea_stage.html shared by gameroom + my-sea; aliased .my-sea-picker w. id=id_sea_overlay so SeaDeal.init() finds it; FLIP click → SeaDeal.openStage delegation instead of bare _fillSlot. Fixes the user-reported 'thumbnail disappears' bug — slot was landing at opacity 0 (.--filled w.o .--visible) because SeaDeal's _hideStage (which adds --visible on modal dismiss) was never running. 3 new FTs cover the modal flow.
Spread lock + DEL reshuffle — _lockSpread/_unlockSpread toggle .sea-select--locked class on the combobox; first deposit locks, _resetHand unlocks. _reshuffleDeck Fisher-Yates over combined piles + re-rolls 25% reversal axis on DEL so successive DELs don't re-deal the same hand. Verified Claudezilla: 3 DEL cycles produced distinct lay cards (150 → 114 → 155).
Cover/cross empty slots — subtle dotted outline (transparent bg + 0.25 alpha border) w. --duoUser mask reveal on hover/touch. Per the user spec; rule lives in _card-deck.scss (shared between gameroom + my-sea). Plus matching label-opacity (0.25 idle → 0.6 hover) via CSS :hover ancestor propagation.
DOS spec — Solution moved from cover → crown per user correction. DRAW_ORDER ['loom', 'cross', 'crown']; POSITION_LABELS {loom: Desire, cross: Obstacle, crown: Solution}; SCSS hide list flipped from [leave, crown, lay] → [leave, cover, lay]; FT/IT assertions updated.
SAO → DOS soft-reload bug — Firefox autofill on hidden input restored the previous-session DOS value, tripping combobox.js's change-event guard. Fix: autocomplete=off + force-sync hidden.value from server-rendered aria-selected option in init. Captured as feedback_firefox_autofill_hidden_inputs (generalizable trap).
.sea-pos-label outside .sea-card-slot — moved label to be a sibling of the slot in the cell, so SeaDeal innerHTML clobber on draw doesn't erase it. Per-position absolute positioning touching slot borders: crown/cover above (translate -50%, 0.1rem, scaleY 1.2); lay/cross below (translate -50%, -0.1rem, scaleY 1.2); leave left, CCW (writing-mode vertical-rl + rotate 180deg + scaleX 1.2); loom right, CW (writing-mode vertical-rl + scaleX 1.2). scaleX for rotated labels (not scaleY) — perpendicular to text-flow is the visible-width direction after rotation. .my-sea-cross gap bumped to 1.75rem for label clearance.
Escape Velocity label swaps — POSITION_LABELS for escape-velocity: {crown: Crown, leave: Lay, cover: Cover, cross: Cross, loom: Loom, lay: Leave}. Replaces the Waite-Smith Behind/Beneath/Before per user spec.
SPREAD dropdown portal — .my-sea-form-col .sea-form-main { overflow: visible } + .sea-select-list { z-index: 1000 } so the dropdown extends past the form-main scroll area + sits above the picker stacking ints. Gameroom .sea-form-main still scrolls (only my-sea opts out).
Major Arcana polarity-split rendering — added 9 missing _card_dict keys to my-sea's _my_sea_deck_data to match gameroom epic.views.sea_deck's contract: levity_emanation, gravity_emanation, levity_reversal, gravity_reversal, italic_word, keywords_upright, keywords_reversed, energies, operations. Without these StageCard.populateCard falls through to plain name_title for trumps 19-21 + cards 48-49. Iter 4b cleanup candidate: extract apps.epic.utils.card_dict() to DRY the now-identical helpers.
Tests deferred — user explicitly belayed FT runs during the bug-fix substage. Iter 4b will re-establish a green sweep before its commit lands.
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:
@@ -249,6 +249,21 @@ def _my_sea_deck_data(user):
|
|||||||
"levity_qualifier": c.levity_qualifier,
|
"levity_qualifier": c.levity_qualifier,
|
||||||
"gravity_qualifier": c.gravity_qualifier,
|
"gravity_qualifier": c.gravity_qualifier,
|
||||||
"reversal_qualifier": c.reversal_qualifier,
|
"reversal_qualifier": c.reversal_qualifier,
|
||||||
|
# Polarity-split full-title overrides — required for Major
|
||||||
|
# Arcana (Earthman trumps 19-21 + cards 48-49) to render
|
||||||
|
# their per-polarity emanation/reversal names on the stage
|
||||||
|
# card. Without these StageCard.populateCard falls back to
|
||||||
|
# the plain `name_title` w. no qualifier. Mirrors the
|
||||||
|
# gameroom `epic.views.sea_deck` JSON shape exactly.
|
||||||
|
"levity_emanation": c.levity_emanation,
|
||||||
|
"gravity_emanation": c.gravity_emanation,
|
||||||
|
"levity_reversal": c.levity_reversal,
|
||||||
|
"gravity_reversal": c.gravity_reversal,
|
||||||
|
"italic_word": c.italic_word,
|
||||||
|
"keywords_upright": c.keywords_upright,
|
||||||
|
"keywords_reversed": c.keywords_reversed,
|
||||||
|
"energies": c.energies,
|
||||||
|
"operations": c.operations,
|
||||||
"reversed": random.random() < reversal_prob,
|
"reversed": random.random() < reversal_prob,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -533,7 +533,7 @@ class MySeaSpreadFormTest(FunctionalTest):
|
|||||||
"past-present-future": {"leave", "cover", "loom"},
|
"past-present-future": {"leave", "cover", "loom"},
|
||||||
"situation-action-outcome": {"lay", "cover", "crown"},
|
"situation-action-outcome": {"lay", "cover", "crown"},
|
||||||
"mind-body-spirit": {"crown", "lay", "loom"},
|
"mind-body-spirit": {"crown", "lay", "loom"},
|
||||||
"desire-obstacle-solution": {"loom", "cross", "cover"},
|
"desire-obstacle-solution": {"loom", "cross", "crown"},
|
||||||
"waite-smith": ALL_POSITIONS,
|
"waite-smith": ALL_POSITIONS,
|
||||||
"escape-velocity": ALL_POSITIONS,
|
"escape-velocity": ALL_POSITIONS,
|
||||||
}
|
}
|
||||||
@@ -583,7 +583,7 @@ class MySeaSpreadFormTest(FunctionalTest):
|
|||||||
"situation-action-outcome": {"lay": "Situation", "cover": "Action", "crown": "Outcome"},
|
"situation-action-outcome": {"lay": "Situation", "cover": "Action", "crown": "Outcome"},
|
||||||
"past-present-future": {"leave": "Past", "cover": "Present", "loom": "Future"},
|
"past-present-future": {"leave": "Past", "cover": "Present", "loom": "Future"},
|
||||||
"mind-body-spirit": {"crown": "Mind", "lay": "Body", "loom": "Spirit"},
|
"mind-body-spirit": {"crown": "Mind", "lay": "Body", "loom": "Spirit"},
|
||||||
"desire-obstacle-solution": {"loom": "Desire", "cross": "Obstacle","cover":"Solution"},
|
"desire-obstacle-solution": {"loom": "Desire", "cross": "Obstacle","crown":"Solution"},
|
||||||
"waite-smith": {"crown": "Crown", "leave": "Beneath", "cover": "Cover",
|
"waite-smith": {"crown": "Crown", "leave": "Beneath", "cover": "Cover",
|
||||||
"cross": "Cross", "loom": "Before", "lay": "Behind"},
|
"cross": "Cross", "loom": "Before", "lay": "Behind"},
|
||||||
}
|
}
|
||||||
@@ -670,9 +670,10 @@ class MySeaCardDrawTest(FunctionalTest):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _draw_one(self, picker, polarity):
|
def _draw_open_modal(self, picker, polarity):
|
||||||
"""Click a polarity swatch + the FLIP btn that appears →
|
"""Click a polarity swatch + the FLIP btn that appears → opens
|
||||||
deposits a card. `polarity` is `'levity'` or `'gravity'`."""
|
the SeaDeal stage modal. Returns the stage element so callers
|
||||||
|
can assert on it before dismissing."""
|
||||||
stack = picker.find_element(
|
stack = picker.find_element(
|
||||||
By.CSS_SELECTOR, f".sea-deck-stack--{polarity}"
|
By.CSS_SELECTOR, f".sea-deck-stack--{polarity}"
|
||||||
)
|
)
|
||||||
@@ -680,9 +681,45 @@ class MySeaCardDrawTest(FunctionalTest):
|
|||||||
flip = self.wait_for(
|
flip = self.wait_for(
|
||||||
lambda: stack.find_element(By.CSS_SELECTOR, ".sea-stack-ok")
|
lambda: stack.find_element(By.CSS_SELECTOR, ".sea-stack-ok")
|
||||||
)
|
)
|
||||||
# FLIP btn becomes visible after the stack click; wait for it.
|
|
||||||
self.wait_for(lambda: self.assertTrue(flip.is_displayed()))
|
self.wait_for(lambda: self.assertTrue(flip.is_displayed()))
|
||||||
flip.click()
|
flip.click()
|
||||||
|
# SeaDeal.openStage shows #id_sea_stage. Wait for the modal.
|
||||||
|
return self.wait_for(
|
||||||
|
lambda: self._stage_visible()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _stage_visible(self):
|
||||||
|
stage = self.browser.find_element(By.CSS_SELECTOR, "#id_sea_stage")
|
||||||
|
if not stage.is_displayed():
|
||||||
|
raise AssertionError("sea-stage not visible after FLIP click")
|
||||||
|
return stage
|
||||||
|
|
||||||
|
def _dismiss_modal(self):
|
||||||
|
"""Click the stage backdrop → SeaDeal._hideStage → modal hides +
|
||||||
|
slot gains `.--visible` (thumbnail fades in).
|
||||||
|
|
||||||
|
Uses `execute_script` to dispatch the click rather than a native
|
||||||
|
Selenium `.click()` — `.sea-stage-content` overlays the backdrop
|
||||||
|
visually (centered card + stat block), so Selenium reports
|
||||||
|
ElementClickInterceptedException for a direct click. This is
|
||||||
|
the documented Selenium-limitation exception per the TDD skill;
|
||||||
|
the actual backdrop-click → close behaviour is Jasmine-tested
|
||||||
|
in [[SeaDealSpec.js]] / "Backdrop click closes the stage"."""
|
||||||
|
self.browser.execute_script(
|
||||||
|
"document.querySelector('#id_sea_stage .sea-stage-backdrop').click();"
|
||||||
|
)
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertFalse(
|
||||||
|
self.browser.find_element(By.CSS_SELECTOR, "#id_sea_stage").is_displayed()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _draw_one(self, picker, polarity):
|
||||||
|
"""Full single-draw cycle: open modal + dismiss it. Used by FTs
|
||||||
|
that need to deposit multiple cards in sequence (the stage
|
||||||
|
backdrop blocks subsequent deck-stack clicks)."""
|
||||||
|
self._draw_open_modal(picker, polarity)
|
||||||
|
self._dismiss_modal()
|
||||||
|
|
||||||
# ── Test 1 ───────────────────────────────────────────────────────────────
|
# ── Test 1 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -890,3 +927,71 @@ class MySeaCardDrawTest(FunctionalTest):
|
|||||||
hint = picker.find_element(By.CSS_SELECTOR, ".sea-reversal-hint")
|
hint = picker.find_element(By.CSS_SELECTOR, ".sea-reversal-hint")
|
||||||
self.assertIn("25", hint.text)
|
self.assertIn("25", hint.text)
|
||||||
self.assertIn("reversal", hint.text.lower())
|
self.assertIn("reversal", hint.text.lower())
|
||||||
|
|
||||||
|
# ── Test (modal bug fix) ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_flip_click_opens_portaled_stage_modal(self):
|
||||||
|
"""Bug fix (2026-05-19): the user-reported missing modal. After
|
||||||
|
clicking the deck stack + the FLIP btn that appears, SeaDeal.
|
||||||
|
openStage should fire — showing `#id_sea_stage` (position-fixed
|
||||||
|
full-viewport portal) above everything else. Before the fix the
|
||||||
|
slot got filled directly at opacity 0 → 'thumbnail summarily
|
||||||
|
disappears'. Now: modal opens; slot stays at `--filled` but
|
||||||
|
`--visible` is NOT added yet (waits for backdrop dismiss)."""
|
||||||
|
picker = self._enter_picker_phase()
|
||||||
|
stage = self._draw_open_modal(picker, "levity")
|
||||||
|
# Stage card carries the drawn card's data — non-empty corner rank.
|
||||||
|
rank = stage.find_element(
|
||||||
|
By.CSS_SELECTOR, ".sea-stage-card .fan-card-corner--tl .fan-corner-rank"
|
||||||
|
)
|
||||||
|
self.assertTrue(rank.text.strip(), "stage card should display the drawn card's corner rank")
|
||||||
|
# Slot in the cross is in `.--filled` state but the thumbnail is
|
||||||
|
# invisible until the modal dismisses (the bug we're guarding).
|
||||||
|
slot = picker.find_element(
|
||||||
|
By.CSS_SELECTOR, ".sea-pos-lay .sea-card-slot.sea-card-slot--filled"
|
||||||
|
)
|
||||||
|
self.assertNotIn(
|
||||||
|
"sea-card-slot--visible", slot.get_attribute("class"),
|
||||||
|
"slot should still be in pre-reveal opacity-0 state while modal is open",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Test (modal bug fix, dismiss reveal) ───────────────────────────────
|
||||||
|
|
||||||
|
def test_backdrop_click_dismisses_modal_and_reveals_thumbnail(self):
|
||||||
|
"""Bug fix part 2: clicking the `.sea-stage-backdrop` closes the
|
||||||
|
modal AND adds `.sea-card-slot--visible` to the deposited slot,
|
||||||
|
making the thumbnail fade in. Confirms the user-reported 'card
|
||||||
|
appears where the slot was' behavior post-dismiss."""
|
||||||
|
picker = self._enter_picker_phase()
|
||||||
|
self._draw_open_modal(picker, "levity")
|
||||||
|
self._dismiss_modal()
|
||||||
|
slot = picker.find_element(
|
||||||
|
By.CSS_SELECTOR, ".sea-pos-lay .sea-card-slot.sea-card-slot--filled"
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"sea-card-slot--visible", slot.get_attribute("class"),
|
||||||
|
"post-dismiss, the slot should fade in via `.--visible`",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Test (modal bug fix, stat block populates) ─────────────────────────
|
||||||
|
|
||||||
|
def test_modal_stage_renders_stat_block_dom_contract(self):
|
||||||
|
"""SeaDeal._populate populates the stat-block keyword `<ul>`s
|
||||||
|
via `#id_sea_stat_upright` / `#id_sea_stat_reversed`. The DOM
|
||||||
|
contract — these IDs exist inside the stage — is what this FT
|
||||||
|
pins; the actual stat content (keyword text, qualifier render)
|
||||||
|
is exercised by [[SeaDealSpec.js]]. Earthman seed cards in the
|
||||||
|
iter-4a FT pile carry empty keyword arrays so we can't assert
|
||||||
|
text content here without enriching the seed."""
|
||||||
|
picker = self._enter_picker_phase()
|
||||||
|
self._draw_open_modal(picker, "levity")
|
||||||
|
# Stat-block UL elements exist inside the visible stage.
|
||||||
|
upright = self.browser.find_element(By.CSS_SELECTOR, "#id_sea_stat_upright")
|
||||||
|
reversed_ul = self.browser.find_element(By.CSS_SELECTOR, "#id_sea_stat_reversed")
|
||||||
|
self.assertIsNotNone(upright)
|
||||||
|
self.assertIsNotNone(reversed_ul)
|
||||||
|
# The sea stat block is inside the visible stage modal.
|
||||||
|
stat_block = self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_sea_stage .sea-stat-block"
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(stat_block)
|
||||||
|
|||||||
@@ -1324,9 +1324,30 @@ $sea-card-h: 6.5rem;
|
|||||||
|
|
||||||
.sea-pos-cover { z-index: 3; } // above sig (z-index: 2)
|
.sea-pos-cover { z-index: 3; } // above sig (z-index: 2)
|
||||||
.sea-pos-cross { z-index: 4; } // above cover
|
.sea-pos-cross { z-index: 4; } // above cover
|
||||||
// Empty Cover/Cross slots are invisible — they reveal only once a card is deposited
|
// Empty Cover/Cross slots — subtle dotted outline (no fill) so the
|
||||||
|
// underlying Sig card shows through. Hovering/touching reveals the
|
||||||
|
// full --duoUser mask, opaquing the slot + obscuring the Sig behind.
|
||||||
|
// Border + label dim to 0.25 alpha default; bounce to full on hover.
|
||||||
|
// The filled-slot hover behavior (opacity 0.3/0.15 → 1) at lines 1300-
|
||||||
|
// 1301 is untouched — this only restyles the EMPTY state.
|
||||||
.sea-pos-cover .sea-card-slot--empty,
|
.sea-pos-cover .sea-card-slot--empty,
|
||||||
.sea-pos-cross .sea-card-slot--empty { opacity: 0; pointer-events: none; }
|
.sea-pos-cross .sea-card-slot--empty {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: rgba(var(--terUser), 0.25);
|
||||||
|
box-shadow: none;
|
||||||
|
pointer-events: auto;
|
||||||
|
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||||
|
|
||||||
|
.sea-pos-label { opacity: 0.25; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-pos-cover .sea-card-slot--empty:hover,
|
||||||
|
.sea-pos-cross .sea-card-slot--empty:hover {
|
||||||
|
background-color: rgba(var(--duoUser), 1);
|
||||||
|
border-color: rgba(var(--terUser), 1);
|
||||||
|
|
||||||
|
.sea-pos-label { opacity: 0.6; }
|
||||||
|
}
|
||||||
|
|
||||||
.sea-pos-cross .sea-card-slot { transform: rotate(90deg); }
|
.sea-pos-cross .sea-card-slot { transform: rotate(90deg); }
|
||||||
|
|
||||||
|
|||||||
@@ -297,10 +297,19 @@ body.page-gameboard {
|
|||||||
// PPF: leave (1) cover (2) loom (3) — horizontal middle row
|
// PPF: leave (1) cover (2) loom (3) — horizontal middle row
|
||||||
// SAO: lay (1) cover (2) crown (3) — vertical center column
|
// SAO: lay (1) cover (2) crown (3) — vertical center column
|
||||||
// MBS: crown (1) lay (2) loom (3) — T-shape (crown + lay vertical, loom right)
|
// MBS: crown (1) lay (2) loom (3) — T-shape (crown + lay vertical, loom right)
|
||||||
// DOS: loom (1) cross (2) cover (3) — sig-anchored cluster + loom
|
// DOS: loom (1) cross (2) crown (3) — loom right · cross overlay · crown above
|
||||||
// CC variants: all 6 positions (Waite-Smith / Escape Velocity differ in DRAW ORDER only,
|
// CC variants: all 6 positions (Waite-Smith / Escape Velocity differ in DRAW ORDER only,
|
||||||
// not in position visibility).
|
// not in position visibility).
|
||||||
|
|
||||||
|
// Bump grid gap on my-sea (gameroom .sea-cross stays at 0.5rem since
|
||||||
|
// gameroom slots have no per-position labels). The vertical leave/loom
|
||||||
|
// labels need ~1.5rem of horizontal clearance from adjacent cells, and
|
||||||
|
// the horizontal crown/cover/lay/cross labels need ~1rem of vertical
|
||||||
|
// clearance so they don't overlap into the next row.
|
||||||
|
.my-sea-cross {
|
||||||
|
gap: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
.my-sea-cross[data-spread="past-present-future"] {
|
.my-sea-cross[data-spread="past-present-future"] {
|
||||||
.sea-pos-crown,
|
.sea-pos-crown,
|
||||||
.sea-pos-cross,
|
.sea-pos-cross,
|
||||||
@@ -321,28 +330,106 @@ body.page-gameboard {
|
|||||||
|
|
||||||
.my-sea-cross[data-spread="desire-obstacle-solution"] {
|
.my-sea-cross[data-spread="desire-obstacle-solution"] {
|
||||||
.sea-pos-leave,
|
.sea-pos-leave,
|
||||||
.sea-pos-crown,
|
.sea-pos-cover,
|
||||||
.sea-pos-lay { display: none; }
|
.sea-pos-lay { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Celtic Cross variants (waite-smith / escape-velocity) — all positions
|
// Celtic Cross variants (waite-smith / escape-velocity) — all positions
|
||||||
// visible by default. No `display: none` overrides needed.
|
// visible by default. No `display: none` overrides needed.
|
||||||
|
|
||||||
// Position-name caption inside each empty `.sea-card-slot--empty` —
|
// Position-name caption — re-appropriates the GRAVITY/LEVITY
|
||||||
// re-appropriates the GRAVITY/LEVITY `.sea-stack-name` typographic
|
// `.sea-stack-name` typographic look (_card-deck.scss line 1557):
|
||||||
// look (_card-deck.scss line 1557): small uppercase letter-spaced w.
|
// small uppercase letter-spaced w. a subtle scaleY stretch,
|
||||||
// a subtle scaleY stretch, --terUser ink at 0.6 opacity. No polarity
|
// --terUser ink at 0.6 opacity. No polarity coloring — these are
|
||||||
// coloring — these are spread-position labels, not deck identifiers.
|
// spread-position labels, not deck identifiers.
|
||||||
|
//
|
||||||
|
// Labels live OUTSIDE the .sea-card-slot (sibling, inside the crucifix
|
||||||
|
// cell or the cover/cross wrapper) so they survive SeaDeal._fillSlot's
|
||||||
|
// `slot.innerHTML = …` clobber on draw. Each label is absolute-
|
||||||
|
// positioned to nearly touch the slot's nearest border per the user-
|
||||||
|
// locked spec:
|
||||||
|
// crown / cover — above top border
|
||||||
|
// lay / cross — below bottom border
|
||||||
|
// leave — left of left border, rotated 90° CCW
|
||||||
|
// loom — right of right border, rotated 90° CW
|
||||||
.sea-pos-label {
|
.sea-pos-label {
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
opacity: 0.6;
|
opacity: 1;
|
||||||
transform: scaleY(1.2);
|
color: rgba(var(--seciUser), 1);
|
||||||
color: rgba(var(--terUser), 1);
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cells need `position: relative` so absolute label children anchor
|
||||||
|
// to them. `.sea-pos-core` already has `position: relative` per the
|
||||||
|
// existing rule in _card-deck.scss line 1311; the other crucifix
|
||||||
|
// cells need it added.
|
||||||
|
.my-sea-cross .sea-crucifix-cell { position: relative; }
|
||||||
|
|
||||||
|
// Above top border — overlaps slot's top edge by 0.1rem (per the
|
||||||
|
// `.sea-stack-name` "tuck under" treatment in _card-deck.scss:1564).
|
||||||
|
.sea-pos-crown > .sea-pos-label,
|
||||||
|
.sea-pos-cover > .sea-pos-label {
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0.1rem) scaleY(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cover + cross labels dim w. their slots — they sit on top of the
|
||||||
|
// sig card so a vivid label would compete w. the sig at idle. Default
|
||||||
|
// 0.25 opacity matches the slot's faint dotted-outline at idle; the
|
||||||
|
// parent's :hover state (propagated up when the inside `.sea-card-
|
||||||
|
// slot:hover` fires per CSS hover-ancestor rules) boosts to the
|
||||||
|
// `.sea-pos-label` baseline 0.6, matching the slot's `--duoUser` mask
|
||||||
|
// reveal.
|
||||||
|
.sea-pos-cover > .sea-pos-label,
|
||||||
|
.sea-pos-cross > .sea-pos-label {
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-pos-cover:hover > .sea-pos-label,
|
||||||
|
.sea-pos-cross:hover > .sea-pos-label {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Below bottom border — same `0.1rem` overlap but downward.
|
||||||
|
.sea-pos-lay > .sea-pos-label,
|
||||||
|
.sea-pos-cross > .sea-pos-label {
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -0.1rem) scaleY(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left of left border, rotated 90° CCW — text reads bottom-to-top.
|
||||||
|
// `writing-mode: vertical-rl` puts text top-to-bottom (CW); a 180°
|
||||||
|
// rotation flips it to read bottom-to-top (CCW), satisfying the user-
|
||||||
|
// locked "Leave: counterclockwise" spec.
|
||||||
|
//
|
||||||
|
// `scaleX(1.2)` (instead of the horizontal labels' scaleY) widens the
|
||||||
|
// character column (perpendicular to text-flow) — for vertical-rl
|
||||||
|
// labels, that's the visible "width" the user noticed had been lost
|
||||||
|
// at this angle. Without it, the rotated labels look squat.
|
||||||
|
.sea-pos-leave > .sea-pos-label {
|
||||||
|
right: 100%;
|
||||||
|
top: 50%;
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
transform: translate(0.1rem, -50%) rotate(180deg) scaleX(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right of right border, rotated 90° CW — text reads top-to-bottom.
|
||||||
|
// Native `writing-mode: vertical-rl` direction; no extra rotation.
|
||||||
|
.sea-pos-loom > .sea-pos-label {
|
||||||
|
left: 100%;
|
||||||
|
top: 50%;
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
transform: translate(-0.1rem, -50%) scaleX(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section dividers inside the SPREAD combobox — labels "3-card spreads"
|
// Section dividers inside the SPREAD combobox — labels "3-card spreads"
|
||||||
@@ -370,6 +457,26 @@ body.page-gameboard {
|
|||||||
.my-sea-form-col {
|
.my-sea-form-col {
|
||||||
flex: 0 0 16rem;
|
flex: 0 0 16rem;
|
||||||
max-width: 16rem;
|
max-width: 16rem;
|
||||||
|
|
||||||
|
// Portal the SPREAD dropdown out of `.sea-form-main`'s overflow
|
||||||
|
// clip — by default the gameroom's `.sea-form-main { overflow-y:
|
||||||
|
// auto }` (from _card-deck.scss:1424) keeps the modal contents
|
||||||
|
// scrollable, but for my-sea's much shorter form the dropdown gets
|
||||||
|
// clipped instead of overlaying the LOCK HAND / DEL btns below.
|
||||||
|
// Setting overflow visible here lets the absolute-positioned
|
||||||
|
// `.sea-select-list` extend past the form area + sit "above
|
||||||
|
// everything else" via its existing z-index: 100.
|
||||||
|
.sea-form-main {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bump the dropdown z-index well above the picker's stacking ints
|
||||||
|
// (cover z:3, cross z:4, modal stage z:9999 only opens on draw
|
||||||
|
// anyway). 1000 sits above any in-page layer the user might be
|
||||||
|
// interacting w. when they open the SPREAD picker.
|
||||||
|
.sea-select-list {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LOCK HAND post-commit visual-lock: dim everything that mutates the
|
// LOCK HAND post-commit visual-lock: dim everything that mutates the
|
||||||
@@ -385,3 +492,14 @@ body.page-gameboard {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SPREAD combobox lock — applied after the first deposit so the user
|
||||||
|
// can't switch spread mid-draw + scramble the in-progress hand's
|
||||||
|
// position-to-card mapping. DEL releases the lock by removing this
|
||||||
|
// class. Same `pointer-events: none` treatment as `.btn-disabled` per
|
||||||
|
// [[feedback_btn_disabled_pointer_events]].
|
||||||
|
.sea-select.sea-select--locked {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|||||||
@@ -139,50 +139,9 @@
|
|||||||
</div>{# /.sea-modal-wrap #}
|
</div>{# /.sea-modal-wrap #}
|
||||||
|
|
||||||
{# ── Sea stage — big card viewer ─────────────────────────────────────────── #}
|
{# ── Sea stage — big card viewer ─────────────────────────────────────────── #}
|
||||||
<div class="sea-stage" id="id_sea_stage" style="display:none">
|
{# Extracted to a shared partial so the my-sea picker (Sprint 5 iter 4-bugs) #}
|
||||||
<div class="sea-stage-backdrop"></div>
|
{# reuses the same DOM contract that SeaDeal binds to. #}
|
||||||
<div class="sea-stage-content">
|
{% include "apps/gameboard/_partials/_sea_stage.html" %}
|
||||||
<div class="sig-stage-card sea-stage-card">
|
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
|
||||||
<span class="fan-corner-rank"></span>
|
|
||||||
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
|
||||||
</div>
|
|
||||||
<div class="fan-card-face">
|
|
||||||
<div class="fan-card-face-upright">
|
|
||||||
<p class="fan-card-name-group"></p>
|
|
||||||
<p class="sig-qualifier-above"></p>
|
|
||||||
<p class="fan-card-name"></p>
|
|
||||||
<p class="sig-qualifier-below"></p>
|
|
||||||
</div>
|
|
||||||
<p class="fan-card-arcana"></p>
|
|
||||||
<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-qualifier"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fan-card-corner fan-card-corner--br">
|
|
||||||
<span class="fan-corner-rank"></span>
|
|
||||||
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="sig-stat-block sea-stat-block">
|
|
||||||
<button class="btn btn-reverse spin-btn" type="button">SPIN</button>
|
|
||||||
<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>
|
|
||||||
<ul class="stat-keywords" id="id_sea_stat_upright"></ul>
|
|
||||||
</div>
|
|
||||||
<div class="stat-face stat-face--reversed">
|
|
||||||
<p class="stat-face-label">Reversal</p>
|
|
||||||
<ul class="stat-keywords" id="id_sea_stat_reversed"></ul>
|
|
||||||
</div>
|
|
||||||
{% include "apps/gameboard/_partials/_sig_fyi_panel.html" with panel_id="id_sea_fyi_panel" panel_extra_attrs='style="display:none"' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>{# /.sea-overlay #}
|
</div>{# /.sea-overlay #}
|
||||||
|
|
||||||
|
|||||||
52
src/templates/apps/gameboard/_partials/_sea_stage.html
Normal file
52
src/templates/apps/gameboard/_partials/_sea_stage.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{# Sea stage — full-viewport portaled modal (`position: fixed; inset: 0` #}
|
||||||
|
{# per _card-deck.scss:1615) that opens above the picker / overlay when #}
|
||||||
|
{# `SeaDeal.openStage(card, posSelector, isLevity)` fires. Hosts the #}
|
||||||
|
{# full card face + stat block + SPIN / FYI controls; click backdrop to #}
|
||||||
|
{# dismiss + reveal the deposited card thumbnail in its slot. #}
|
||||||
|
{# #}
|
||||||
|
{# Shared by the gameroom SEA SELECT phase and the my-sea picker — same #}
|
||||||
|
{# HTML, same SeaDeal module bindings; only the parent overlay differs. #}
|
||||||
|
<div class="sea-stage" id="id_sea_stage" style="display:none">
|
||||||
|
<div class="sea-stage-backdrop"></div>
|
||||||
|
<div class="sea-stage-content">
|
||||||
|
<div class="sig-stage-card sea-stage-card">
|
||||||
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
|
<span class="fan-corner-rank"></span>
|
||||||
|
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
||||||
|
</div>
|
||||||
|
<div class="fan-card-face">
|
||||||
|
<div class="fan-card-face-upright">
|
||||||
|
<p class="fan-card-name-group"></p>
|
||||||
|
<p class="sig-qualifier-above"></p>
|
||||||
|
<p class="fan-card-name"></p>
|
||||||
|
<p class="sig-qualifier-below"></p>
|
||||||
|
</div>
|
||||||
|
<p class="fan-card-arcana"></p>
|
||||||
|
<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-qualifier"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fan-card-corner fan-card-corner--br">
|
||||||
|
<span class="fan-corner-rank"></span>
|
||||||
|
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sig-stat-block sea-stat-block">
|
||||||
|
<button class="btn btn-reverse spin-btn" type="button">SPIN</button>
|
||||||
|
<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>
|
||||||
|
<ul class="stat-keywords" id="id_sea_stat_upright"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="stat-face stat-face--reversed">
|
||||||
|
<p class="stat-face-label">Reversal</p>
|
||||||
|
<ul class="stat-keywords" id="id_sea_stat_reversed"></ul>
|
||||||
|
</div>
|
||||||
|
{% include "apps/gameboard/_partials/_sig_fyi_panel.html" with panel_id="id_sea_fyi_panel" panel_extra_attrs='style="display:none"' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -75,18 +75,28 @@
|
|||||||
{# Each empty slot carries a `.sea-pos-label` caption (re- #}
|
{# Each empty slot carries a `.sea-pos-label` caption (re- #}
|
||||||
{# appropriated from the GRAVITY/LEVITY .sea-stack-name look) #}
|
{# appropriated from the GRAVITY/LEVITY .sea-stack-name look) #}
|
||||||
{# that JS updates per spread. #}
|
{# that JS updates per spread. #}
|
||||||
<div class="my-sea-picker" style="display:none">
|
{# #}
|
||||||
|
{# `id="id_sea_overlay"` aliases the picker to what SeaDeal #}
|
||||||
|
{# binds to (the gameroom uses the same ID on a different #}
|
||||||
|
{# page — no DOM collision since my-sea + gameroom never co- #}
|
||||||
|
{# exist in one DOM). FLIP click delegates to SeaDeal. #}
|
||||||
|
{# openStage(), which fills the slot AND opens the portaled #}
|
||||||
|
{# stage modal w. SPIN / FYI controls. #}
|
||||||
|
<div class="my-sea-picker" id="id_sea_overlay" style="display:none">
|
||||||
<div class="sea-cards-col">
|
<div class="sea-cards-col">
|
||||||
<div class="sea-cross my-sea-cross" data-spread="{{ default_spread }}">
|
<div class="sea-cross my-sea-cross" data-spread="{{ default_spread }}">
|
||||||
|
{# `.sea-pos-label` lives OUTSIDE the slot so SeaDeal._fillSlot's #}
|
||||||
|
{# `slot.innerHTML = …` (which writes the drawn card's corner- #}
|
||||||
|
{# rank + suit-icon) doesn't clobber it. Labels persist as #}
|
||||||
|
{# adjacent siblings + are positioned via absolute SCSS to #}
|
||||||
|
{# touch the slot's nearest edge. #}
|
||||||
<div class="sea-crucifix-cell sea-pos-crown">
|
<div class="sea-crucifix-cell sea-pos-crown">
|
||||||
<div class="sea-card-slot sea-card-slot--empty">
|
<span class="sea-pos-label" data-position="crown">Outcome</span>
|
||||||
<span class="sea-pos-label" data-position="crown">Outcome</span>
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="sea-crucifix-cell sea-pos-leave">
|
<div class="sea-crucifix-cell sea-pos-leave">
|
||||||
<div class="sea-card-slot sea-card-slot--empty">
|
<span class="sea-pos-label" data-position="leave"></span>
|
||||||
<span class="sea-pos-label" data-position="leave"></span>
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="sea-crucifix-cell sea-pos-core">
|
<div class="sea-crucifix-cell sea-pos-core">
|
||||||
<div class="sig-stage-card sea-sig-card"
|
<div class="sig-stage-card sea-sig-card"
|
||||||
@@ -95,25 +105,21 @@
|
|||||||
{% if significator.suit_icon %}<i class="fa-solid {{ significator.suit_icon }}"></i>{% endif %}
|
{% if significator.suit_icon %}<i class="fa-solid {{ significator.suit_icon }}"></i>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="sea-pos-cover">
|
<div class="sea-pos-cover">
|
||||||
<div class="sea-card-slot sea-card-slot--empty">
|
<span class="sea-pos-label" data-position="cover">Action</span>
|
||||||
<span class="sea-pos-label" data-position="cover">Action</span>
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="sea-pos-cross">
|
<div class="sea-pos-cross">
|
||||||
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing">
|
<span class="sea-pos-label" data-position="cross"></span>
|
||||||
<span class="sea-pos-label" data-position="cross"></span>
|
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sea-crucifix-cell sea-pos-loom">
|
<div class="sea-crucifix-cell sea-pos-loom">
|
||||||
<div class="sea-card-slot sea-card-slot--empty">
|
<span class="sea-pos-label" data-position="loom"></span>
|
||||||
<span class="sea-pos-label" data-position="loom"></span>
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="sea-crucifix-cell sea-pos-lay">
|
<div class="sea-crucifix-cell sea-pos-lay">
|
||||||
<div class="sea-card-slot sea-card-slot--empty">
|
<span class="sea-pos-label" data-position="lay">Situation</span>
|
||||||
<span class="sea-pos-label" data-position="lay">Situation</span>
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,8 +135,16 @@
|
|||||||
<div class="sea-field">
|
<div class="sea-field">
|
||||||
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
|
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
|
||||||
<p class="sea-reversal-hint">{{ reversals_pct|default:25 }}% reversals</p>
|
<p class="sea-reversal-hint">{{ reversals_pct|default:25 }}% reversals</p>
|
||||||
|
{# autocomplete="off" opts the hidden input out of #}
|
||||||
|
{# Firefox's form-history autofill, which otherwise #}
|
||||||
|
{# restores the LAST value on soft reload (F5). #}
|
||||||
|
{# Without this, combobox.js's `select(i)` short- #}
|
||||||
|
{# circuits its change-event dispatch when the #}
|
||||||
|
{# user re-picks the value Firefox already restored #}
|
||||||
|
{# → my-sea's sync() never fires → data-spread on #}
|
||||||
|
{# .my-sea-cross stays stuck on SAO default. #}
|
||||||
<input type="hidden" id="id_sea_spread" name="spread"
|
<input type="hidden" id="id_sea_spread" name="spread"
|
||||||
value="{{ default_spread }}">
|
value="{{ default_spread }}" autocomplete="off">
|
||||||
<div class="sea-select"
|
<div class="sea-select"
|
||||||
data-combobox
|
data-combobox
|
||||||
data-combobox-target="id_sea_spread"
|
data-combobox-target="id_sea_spread"
|
||||||
@@ -180,11 +194,21 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{# Sea stage — portaled modal that opens on FLIP click via #}
|
||||||
|
{# SeaDeal.openStage. `position:fixed; inset:0` covers the #}
|
||||||
|
{# viewport; click backdrop to dismiss + reveal the slot #}
|
||||||
|
{# thumbnail. #}
|
||||||
|
{% include "apps/gameboard/_partials/_sea_stage.html" %}
|
||||||
</div>
|
</div>
|
||||||
{# Sprint 5 iter 4a — shuffled deck (levity + gravity halves, #}
|
{# Sprint 5 iter 4a — shuffled deck (levity + gravity halves, #}
|
||||||
{# sig excluded) embedded as JSON; JS reads on init and #}
|
{# sig excluded) embedded as JSON; JS reads on init and #}
|
||||||
{# pops from the relevant pile on each deposit. #}
|
{# pops from the relevant pile on each deposit. #}
|
||||||
{{ sea_deck_data|json_script:"id_my_sea_deck" }}
|
{{ sea_deck_data|json_script:"id_my_sea_deck" }}
|
||||||
|
{# StageCard + SeaDeal — both bind to `#id_sea_overlay` (the #}
|
||||||
|
{# my-sea-picker) + `#id_sea_stage` (the stage partial) on #}
|
||||||
|
{# DOMContentLoaded; openStage() runs on FLIP click below. #}
|
||||||
|
<script src="{% static 'apps/epic/stage-card.js' %}"></script>
|
||||||
|
<script src="{% static 'apps/epic/sea.js' %}"></script>
|
||||||
<script src="{% static 'apps/epic/combobox.js' %}"></script>
|
<script src="{% static 'apps/epic/combobox.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
@@ -198,7 +222,7 @@
|
|||||||
'past-present-future': ['leave', 'cover', 'loom'],
|
'past-present-future': ['leave', 'cover', 'loom'],
|
||||||
'situation-action-outcome': ['lay', 'cover', 'crown'],
|
'situation-action-outcome': ['lay', 'cover', 'crown'],
|
||||||
'mind-body-spirit': ['crown', 'lay', 'loom'],
|
'mind-body-spirit': ['crown', 'lay', 'loom'],
|
||||||
'desire-obstacle-solution': ['loom', 'cross', 'cover'],
|
'desire-obstacle-solution': ['loom', 'cross', 'crown'],
|
||||||
'waite-smith': ['cover', 'cross', 'crown', 'lay', 'loom', 'leave'],
|
'waite-smith': ['cover', 'cross', 'crown', 'lay', 'loom', 'leave'],
|
||||||
'escape-velocity': ['cover', 'cross', 'lay', 'leave', 'crown', 'loom'],
|
'escape-velocity': ['cover', 'cross', 'lay', 'leave', 'crown', 'loom'],
|
||||||
};
|
};
|
||||||
@@ -206,9 +230,13 @@
|
|||||||
'past-present-future': { leave: 'Past', cover: 'Present', loom: 'Future' },
|
'past-present-future': { leave: 'Past', cover: 'Present', loom: 'Future' },
|
||||||
'situation-action-outcome': { lay: 'Situation', cover: 'Action', crown: 'Outcome' },
|
'situation-action-outcome': { lay: 'Situation', cover: 'Action', crown: 'Outcome' },
|
||||||
'mind-body-spirit': { crown: 'Mind', lay: 'Body', loom: 'Spirit' },
|
'mind-body-spirit': { crown: 'Mind', lay: 'Body', loom: 'Spirit' },
|
||||||
'desire-obstacle-solution': { loom: 'Desire', cross: 'Obstacle',cover: 'Solution' },
|
'desire-obstacle-solution': { loom: 'Desire', cross: 'Obstacle',crown: 'Solution' },
|
||||||
'waite-smith': { crown: 'Crown', leave: 'Beneath', cover: 'Cover', cross: 'Cross', loom: 'Before', lay: 'Behind' },
|
'waite-smith': { crown: 'Crown', leave: 'Beneath', cover: 'Cover', cross: 'Cross', loom: 'Before', lay: 'Behind' },
|
||||||
'escape-velocity': { crown: 'Crown', leave: 'Beneath', cover: 'Cover', cross: 'Cross', loom: 'Before', lay: 'Behind' },
|
// Escape Velocity remaps the diagonal positions per the
|
||||||
|
// user-locked spec (2026-05-19): Beneath→Lay, Before→
|
||||||
|
// Loom, Behind→Leave. Crown/Cover/Cross keep the WS
|
||||||
|
// names.
|
||||||
|
'escape-velocity': { crown: 'Crown', leave: 'Lay', cover: 'Cover', cross: 'Cross', loom: 'Loom', lay: 'Leave' },
|
||||||
};
|
};
|
||||||
var hidden = document.getElementById('id_sea_spread');
|
var hidden = document.getElementById('id_sea_spread');
|
||||||
var cross = document.querySelector('.my-sea-cross');
|
var cross = document.querySelector('.my-sea-cross');
|
||||||
@@ -216,6 +244,7 @@
|
|||||||
var lockBtn= document.getElementById('id_sea_lock_hand');
|
var lockBtn= document.getElementById('id_sea_lock_hand');
|
||||||
var delBtn = document.getElementById('id_sea_del');
|
var delBtn = document.getElementById('id_sea_del');
|
||||||
var deckEl = document.getElementById('id_my_sea_deck');
|
var deckEl = document.getElementById('id_my_sea_deck');
|
||||||
|
var seaSelect = document.querySelector('[data-combobox][data-combobox-target="id_sea_spread"]');
|
||||||
if (!hidden || !cross || !picker) return;
|
if (!hidden || !cross || !picker) return;
|
||||||
|
|
||||||
// ── Deck state ──────────────────────────────────────────
|
// ── Deck state ──────────────────────────────────────────
|
||||||
@@ -272,7 +301,10 @@
|
|||||||
|
|
||||||
function _emptySlot(cell) {
|
function _emptySlot(cell) {
|
||||||
// DEL restores each filled slot to its initial empty
|
// DEL restores each filled slot to its initial empty
|
||||||
// state w. the .sea-pos-label re-rendered inside.
|
// state. `.sea-pos-label` is now a SIBLING of the slot
|
||||||
|
// (outside it) so SeaDeal's innerHTML clobber on draw
|
||||||
|
// doesn't touch it — we don't need to re-render the
|
||||||
|
// label here, just wipe slot contents + classes.
|
||||||
var slot = cell.querySelector('.sea-card-slot');
|
var slot = cell.querySelector('.sea-card-slot');
|
||||||
if (!slot) return;
|
if (!slot) return;
|
||||||
slot.className = slot.className
|
slot.className = slot.className
|
||||||
@@ -284,21 +316,55 @@
|
|||||||
slot.classList.add('sea-card-slot--empty');
|
slot.classList.add('sea-card-slot--empty');
|
||||||
delete slot.dataset.cardId;
|
delete slot.dataset.cardId;
|
||||||
delete slot.dataset.posKey;
|
delete slot.dataset.posKey;
|
||||||
var posName = '';
|
slot.innerHTML = '';
|
||||||
cell.classList.forEach(function (cls) {
|
}
|
||||||
var m = /^sea-pos-(.+)$/.exec(cls);
|
|
||||||
if (m && m[1] !== 'core' && m[1] !== 'label') posName = m[1];
|
function _lockSpread() {
|
||||||
|
// Lock the SPREAD combobox after the first deposit —
|
||||||
|
// switching spread mid-draw would scramble the in-
|
||||||
|
// progress hand. Unlocks on DEL.
|
||||||
|
if (seaSelect) {
|
||||||
|
seaSelect.classList.add('sea-select--locked');
|
||||||
|
seaSelect.setAttribute('aria-disabled', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function _unlockSpread() {
|
||||||
|
if (seaSelect) {
|
||||||
|
seaSelect.classList.remove('sea-select--locked');
|
||||||
|
seaSelect.removeAttribute('aria-disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _reshuffleDeck() {
|
||||||
|
// Fisher-Yates re-shuffle on DEL — re-distributes cards
|
||||||
|
// across both polarity halves + re-rolls the 25% reversal
|
||||||
|
// axis per card. Page-load shuffle already came from the
|
||||||
|
// server (`_my_sea_deck_data`); subsequent shuffles run
|
||||||
|
// client-side so DEL → fresh-hand doesn't require a
|
||||||
|
// network round-trip.
|
||||||
|
var all = (_deckData.levity || []).concat(_deckData.gravity || []);
|
||||||
|
for (var i = all.length - 1; i > 0; i--) {
|
||||||
|
var j = Math.floor(Math.random() * (i + 1));
|
||||||
|
var tmp = all[i]; all[i] = all[j]; all[j] = tmp;
|
||||||
|
}
|
||||||
|
// Clone each card (don't mutate the immutable server payload
|
||||||
|
// — DEL can fire many times; we don't want successive shuffles
|
||||||
|
// to fold previous reversal flips into the next round).
|
||||||
|
all = all.map(function (c) {
|
||||||
|
var clone = {};
|
||||||
|
for (var k in c) if (Object.prototype.hasOwnProperty.call(c, k)) clone[k] = c[k];
|
||||||
|
clone.reversed = Math.random() < 0.25;
|
||||||
|
return clone;
|
||||||
});
|
});
|
||||||
var labels = POSITION_LABELS[hidden.value] || {};
|
var mid = Math.floor(all.length / 2);
|
||||||
slot.innerHTML =
|
_levityPile = all.slice(0, mid);
|
||||||
'<span class="sea-pos-label" data-position="' + posName + '">' +
|
_gravityPile = all.slice(mid);
|
||||||
(labels[posName] || '') +
|
|
||||||
'</span>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _resetHand() {
|
function _resetHand() {
|
||||||
_filled = 0;
|
_filled = 0;
|
||||||
_hideOk();
|
_hideOk();
|
||||||
|
_unlockSpread();
|
||||||
cross.querySelectorAll(
|
cross.querySelectorAll(
|
||||||
'.sea-crucifix-cell.sea-pos-crown, ' +
|
'.sea-crucifix-cell.sea-pos-crown, ' +
|
||||||
'.sea-crucifix-cell.sea-pos-leave, ' +
|
'.sea-crucifix-cell.sea-pos-leave, ' +
|
||||||
@@ -306,9 +372,14 @@
|
|||||||
'.sea-crucifix-cell.sea-pos-lay, ' +
|
'.sea-crucifix-cell.sea-pos-lay, ' +
|
||||||
'.sea-pos-cover, .sea-pos-cross'
|
'.sea-pos-cover, .sea-pos-cross'
|
||||||
).forEach(_emptySlot);
|
).forEach(_emptySlot);
|
||||||
_levityPile = (_deckData.levity || []).slice();
|
_reshuffleDeck();
|
||||||
_gravityPile = (_deckData.gravity || []).slice();
|
|
||||||
if (lockBtn) lockBtn.disabled = true;
|
if (lockBtn) lockBtn.disabled = true;
|
||||||
|
// Wipe SeaDeal's stage state too — closes a lingering
|
||||||
|
// modal + clears its `_seaHand` map so previously-
|
||||||
|
// drawn cards can't reopen via slot-tap focus.
|
||||||
|
if (window.SeaDeal && window.SeaDeal.resetHand) {
|
||||||
|
SeaDeal.resetHand();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _setLocked(on) {
|
function _setLocked(on) {
|
||||||
@@ -343,8 +414,27 @@
|
|||||||
var order = _currentOrder();
|
var order = _currentOrder();
|
||||||
var posName = order[_filled];
|
var posName = order[_filled];
|
||||||
if (card && posName) {
|
if (card && posName) {
|
||||||
_fillSlot(posName, card, isLevity);
|
// Delegate to SeaDeal — it `_fillSlot`s
|
||||||
|
// (sets corner-rank + suit-icon + polarity
|
||||||
|
// class on the slot at opacity 0) AND opens
|
||||||
|
// the portaled stage modal w. SPIN / FYI.
|
||||||
|
// Click-the-backdrop dismisses + the slot
|
||||||
|
// fades to `.--visible` revealing the
|
||||||
|
// thumbnail.
|
||||||
|
if (window.SeaDeal && window.SeaDeal.openStage) {
|
||||||
|
SeaDeal.openStage(card, '.sea-pos-' + posName, isLevity);
|
||||||
|
} else {
|
||||||
|
// Defensive fallback for environments
|
||||||
|
// where sea.js failed to load (e.g.
|
||||||
|
// collectstatic miss). Render the slot
|
||||||
|
// visibly so the draw isn't lost.
|
||||||
|
_fillSlot(posName, card, isLevity);
|
||||||
|
}
|
||||||
_filled++;
|
_filled++;
|
||||||
|
// First deposit locks the SPREAD combobox —
|
||||||
|
// switching mid-draw would scramble the
|
||||||
|
// in-progress hand's position mapping.
|
||||||
|
if (_filled === 1) _lockSpread();
|
||||||
if (lockBtn) lockBtn.disabled = (_filled < order.length);
|
if (lockBtn) lockBtn.disabled = (_filled < order.length);
|
||||||
}
|
}
|
||||||
_hideOk();
|
_hideOk();
|
||||||
@@ -390,6 +480,21 @@
|
|||||||
_filled = 0;
|
_filled = 0;
|
||||||
if (lockBtn) lockBtn.disabled = true;
|
if (lockBtn) lockBtn.disabled = true;
|
||||||
|
|
||||||
|
// Belt-and-braces autofill defense (paired w. autocomplete=
|
||||||
|
// off on the hidden input above). Firefox occasionally
|
||||||
|
// restores form-history values on soft reload even on
|
||||||
|
// hidden inputs; if it does, hidden.value diverges from
|
||||||
|
// the server-rendered aria-selected option + combobox.js
|
||||||
|
// short-circuits its change-event dispatch on subsequent
|
||||||
|
// option picks of the autofilled value. Force-sync from
|
||||||
|
// the server-rendered aria-selected source-of-truth.
|
||||||
|
var _initialOpt = document.querySelector(
|
||||||
|
'.sea-select-list [role="option"][aria-selected="true"]'
|
||||||
|
);
|
||||||
|
if (_initialOpt && hidden.value !== _initialOpt.dataset.value) {
|
||||||
|
hidden.value = _initialOpt.dataset.value;
|
||||||
|
}
|
||||||
|
|
||||||
// Exposed for iter 4b / future surfaces.
|
// Exposed for iter 4b / future surfaces.
|
||||||
window._mySeaDrawOrder = DRAW_ORDER;
|
window._mySeaDrawOrder = DRAW_ORDER;
|
||||||
}());
|
}());
|
||||||
|
|||||||
Reference in New Issue
Block a user