(1) **My Sea applet dynamic population.** Applet at `_applet-my-sea.html` was referencing an undefined `latest_draw_cards` template var — fell through to "No draws yet" even when the user had an active draw. New helpers in `apps/gameboard/models.py`: `DRAW_ORDER` + `POSITION_LABELS` constants (Python mirrors of the JS dicts in `my_sea.html:274-293`) + `latest_draw_slots(user)` builder that pairs each spread position w. its drawn card + display label + polarity. Wired through `gameboard()` + `toggle_game_applets()` views as `my_sea_slots`. Applet now renders all spread slots in DRAW_ORDER: filled = `.my-sea-slot--filled.my-sea-slot--{gravity,levity}` w. corner-tl + face (name + arcana) + corner-br (mirror) markup (same shape language as my_sign.html `.sig-stage-card`), empty = `.my-sea-slot--empty` w. `0.15rem dashed rgba(var(--terUser), 1)` border (matches the picker's `.sea-card-slot` style exactly so the applet reads as a true scaled-down twin). Container queries (`container-type: size` on `.my-sea-scroll`) lift `--slot-w` to fill the applet's vertical aperture (`min(100cqi, calc((100cqh - 1rem) * 5 / 8))` carves the label row). Position labels pulled tight against the slot's bottom border (`margin-top: -0.15rem` crosses the border line) + vertically stretched (`transform: scaleY(1.4)` mirroring `.sea-pos-label` in `_card-deck.scss:1671-1684`) — empty-slot labels keep the same `--secUser` ink as filled-slot labels for title cohesion across the row. Horizontal-scroll on multi-card spreads via mousewheel — `bindMySeaWheel()` in `gameboard.js` translates vertical wheel events to `scrollLeft += deltaY` (lifted verbatim from `bindPaletteWheel` in `dashboard.js:7-14`).
(2) **lay/leave POSITION_LABELS swap fix.** User caught in the Escape Velocity picker that LEFT slot read "Lay" + BOTTOM slot read "Leave" — opposite of traditional Celtic Cross semantics (LEFT = Behind/past, BOTTOM = Beneath/root). Root cause: POSITION_LABELS for both Waite-Smith + Escape Velocity had `lay`/`leave` slug→label assignments inverted vs the CSS grid's spatial mapping (`_card-deck.scss:1276-1279` puts slug `lay` at BOTTOM, slug `leave` at LEFT). Fix in 5 places: `my_sea.html:287,292` JS POSITION_LABELS (WS: lay→"Beneath", leave→"Behind"; EV: lay→"Lay", leave→"Leave"), `gameboard/models.py:44-47` Python mirror, `test_game_my_sea.py:618-619` FT label-assertion table, `_sea_overlay.html:28,53` annotated comments (`sea-pos-leave` → "Behind (past) — CC pos 6 / EV pos 4"; `sea-pos-lay` → "Beneath (root) — CC pos 4 / EV pos 3"). Slug-to-CSS mapping, DRAW_ORDER, + DB persistence unchanged → no migrations, no data invalidation. **Crucial for Voronoi mapping correctness** per user spec.
(3) **My Sign applet — stage-card layout + stat-block beside.** Applet card markup upgraded to mirror my_sign.html `.sig-stage-card`: corner-tl + face (name + arcana centred) + corner-br (mirror, rotated 180°). Sized to fill applet height via container queries (`--applet-card-w: min(48cqi, 62.5cqh)` — 48cqi caps the card at half the row to leave room for the stat-block). Sibling `.my-sign-applet-stat-block` partial added — emanation/reversal face label + keyword list (from `card.keywords_upright` / `keywords_reversed` keyed off `significator_reversed`), no SPIN/FYI buttons (applet is read-only). Styling cribbed from `.sig-stat-block` in `_card-deck.scss:595-607` — priUser-translucent bg + terUser border + matching `--applet-card-w` sizing.
(4) **My Sea sign-gate refactored to Brief banner.** Was an inline `.my-sea-sign-gate` div w. its own SCSS — broke from the project's `Brief.showBanner` portal pattern. Refactored to a shared `_my_sea_sign_gate_brief.html` partial that fires `Brief.showBanner` w. title="Sign required" + line_text="Look!—pick your sign before drawing the Sea." + post_url=`/billboard/my-sign/`. Brief portals to the page-level h2 anchor via `note.js`'s `_alignToH2` (gaussian-glass `.note-banner` shell, FYI button → my-sign picker, NVM dismisses). Modifier class `.my-sea-sign-gate-brief` added post-render for FT selector disambiguation. note.js load hoisted to gameboard.html `{% block scripts %}` + the top of `my_sea.html {% block content %}` (single load per page — note.js declares `const Brief = ...` at global scope, second load = SyntaxError). All `.my-sea-sign-gate{,--applet,__line,__actions,__back,__fyi}` SCSS deleted. FTs (`test_no_sig_renders_lookline_gate_on_standalone_page` + 5 siblings) + ITs (`test_my_sea_applet_fires_sign_gate_brief_for_user_without_sig` etc.) updated to assert `.note-banner.my-sea-sign-gate-brief` + the JS-rendered FYI/NVM buttons inside the Brief shell.
(5) **Levity card text invisibility fix.** My-sea applet levity slots (--secUser bg) rendered their corner-rank + suit-icon invisible because `.fan-card-corner` carries a global `color: rgba(var(--secUser), 0.75)` rule at `_card-deck.scss:312-319` (specificity 0,1,0) that out-specifics the slot's inherited `color: --priUser`. Same trap as the `.fan-card-name { color: --quiUser }` global. Fix at `_gameboard.scss` inside the levity rule: explicit `.fan-card-corner { color: rgba(var(--priUser), 1) }` + `.fan-card-name { color: rgba(var(--priUser), 1) }` + `.fan-card-arcana { color: rgba(var(--priUser), 0.7) }` overrides at (1,3,1) specificity — beats the globals without `!important`. **Trap captured in memory** — pattern repeats across game-kit, my-sign, my-sea so worth pinning.
(6) **--duoUser olive on all five personal-data surfaces.** Per user spec, the four "personal" applets (My Sign on billboard, My Sea on gameboard, My Sky on dashboard) + the standalone Dashsky page + the standalone My Sign page got `background-color: rgba(var(--duoUser), 1)` so they read as a unified olive-bg group across navigation surfaces. For Dashsky specifically, the form column also got the override (`.sky-page .sky-form-col { background: --duoUser }`) — the base `.sky-form-col { background: --priUser }` (`_sky.scss:137`, shared w. the in-room CAST SKY modal) was leaving the dashsky form column purple inside the otherwise-olive page. Scoped to `.sky-page` so the in-room modal's purple form-col stays intact (sits over --secUser room bg, needs that contrast). One detour caught: tried `body.page-sky { background-color: --duoUser }` to fill the gap below .sky-page's content-sized aperture but it bled to navbar + footer (which sit outside .container) — reverted.
**TDD coverage**: 3 new ITs in `apps/gameboard/tests/integrated/test_views.py` — `test_my_sea_applet_renders_drawn_cards_in_draw_order` (SAO 1-of-3 fills `lay` slot, cover/crown render as empty placeholders), `test_my_sea_applet_labels_match_locked_spread` (SAO labels exactly Situation/Action/Outcome), `test_my_sea_applet_waite_smith_labels_post_fix` (regression pin for the WS Cover/Cross/Crown/**Beneath**/Before/**Behind** sequence post-swap-fix). Existing my-sea applet ITs updated to match the new selector vocabulary (`.my-sea-slot--filled` instead of `.my-sea-card`, Brief script substring instead of `.my-sea-sign-gate--applet`). 6 my-sea FTs updated to the Brief-banner contract. 1214/1214 IT/UT green.
**.gitignore**: temporary entry for `src/apps/epic/static/apps/epic/images/cards-faces/minchiate-fiorentine/` until images get renamed — flagged for removal once the rename lands. (Per user's wget download of the Minchiate faces into the gameboard cards/ tree this session.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
263 lines
12 KiB
HTML
263 lines
12 KiB
HTML
{% load static %}
|
|
{# DRAW SEA overlay — Celtic Cross spread entry #}
|
|
{# Included in room.html when table_status == "SKY_SELECT" and sky_confirmed #}
|
|
{# Layout is the reverse of CAST SKY: cards left (transparent), form right #}
|
|
|
|
<div class="sea-backdrop"></div>
|
|
<div class="sea-overlay" id="id_sea_overlay"
|
|
data-sea-deck-url="{% url 'epic:sea_deck' room.id %}"
|
|
data-sea-user-polarity="{{ user_polarity }}">
|
|
|
|
<div class="sea-modal-wrap">
|
|
<div class="sea-modal">
|
|
|
|
<header class="sea-modal-header">
|
|
<h2>SEA <span>SELECT</span></h2>
|
|
<p>Draw +6 cards to describe your character's influences and seed the map.</p>
|
|
</header>
|
|
|
|
<div class="sea-modal-body">
|
|
|
|
{# ── Cards column (transparent) ───────────────────────────── #}
|
|
<div class="sea-cards-col">
|
|
<div class="sea-cross">
|
|
{# Crown — CC pos 3 / EV pos 5 #}
|
|
<div class="sea-crucifix-cell sea-pos-crown">
|
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
|
</div>
|
|
{# Behind (past) — CC pos 6 / EV pos 4 #}
|
|
<div class="sea-crucifix-cell sea-pos-leave">
|
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
|
</div>
|
|
{# Center — Significator (always placed) + Cover + Cross overlaid #}
|
|
<div class="sea-crucifix-cell sea-pos-core">
|
|
<div class="sig-stage-card sea-sig-card" style="--sig-card-w: 4rem">
|
|
{% if my_tray_sig %}
|
|
<span class="fan-corner-rank">{{ my_tray_sig.corner_rank }}</span>
|
|
{% if my_tray_sig.suit_icon %}<i class="fa-solid {{ my_tray_sig.suit_icon }}"></i>{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
{# Cover — CC/EV pos 1, stacked face-up on Sig #}
|
|
<div class="sea-pos-cover">
|
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
|
</div>
|
|
{# Cross — CC/EV pos 2, rotated 90° on Cover #}
|
|
<div class="sea-pos-cross">
|
|
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing"></div>
|
|
</div>
|
|
</div>
|
|
{# Before (future) — CC pos 5 / EV pos 6 #}
|
|
<div class="sea-crucifix-cell sea-pos-loom">
|
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
|
</div>
|
|
{# Beneath (root) — CC pos 4 / EV pos 3 #}
|
|
<div class="sea-crucifix-cell sea-pos-lay">
|
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Form column (priUser / opaque) ───────────────────────── #}
|
|
<div class="sea-form-col">
|
|
|
|
<div class="sea-form-main">
|
|
<div class="sea-field">
|
|
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
|
|
{% comment %}
|
|
Reversal-rate hint — `stack_reversal_pct` flows from
|
|
apps.epic.utils.stack_reversal_probability via the
|
|
view. Currently a module default; placeholder UI for
|
|
a forthcoming per-user setting.
|
|
{% endcomment %}
|
|
<p class="sea-reversal-hint">{{ stack_reversal_pct|default:25 }}% reversals</p>
|
|
{% comment %}
|
|
Custom combobox — native <select> dropdowns ignore most CSS on
|
|
Firefox/Chrome (OS-rendered list); this gives full styling control.
|
|
combobox.js wires up the keyboard nav, click-outside-to-close, and
|
|
writes the chosen value to the hidden <input id="id_sea_spread"> so
|
|
sea.js's existing `spreadSel.value` read still works.
|
|
{% endcomment %}
|
|
<input type="hidden" id="id_sea_spread" name="spread"
|
|
value="{% if user_polarity == 'levity' %}waite-smith{% else %}escape-velocity{% endif %}">
|
|
<div class="sea-select"
|
|
data-combobox
|
|
data-combobox-target="id_sea_spread"
|
|
role="combobox"
|
|
aria-expanded="false"
|
|
aria-haspopup="listbox"
|
|
aria-labelledby="id_sea_spread_label"
|
|
tabindex="0">
|
|
<span class="sea-select-current">{% if user_polarity == "levity" %}Celtic Cross, Waite-Smith{% else %}Celtic Cross, Escape Velocity{% endif %}</span>
|
|
<span class="sea-select-arrow" aria-hidden="true">▾</span>
|
|
<ul class="sea-select-list" role="listbox">
|
|
{% if user_polarity == "levity" %}
|
|
<li role="option" data-value="waite-smith" aria-selected="true">Celtic Cross, Waite-Smith</li>
|
|
<li role="option" data-value="escape-velocity" aria-selected="false">Celtic Cross, Escape Velocity</li>
|
|
{% else %}
|
|
<li role="option" data-value="escape-velocity" aria-selected="true">Celtic Cross, Escape Velocity</li>
|
|
<li role="option" data-value="waite-smith" aria-selected="false">Celtic Cross, Waite-Smith</li>
|
|
{% endif %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
{# Two face-down deck piles — tap to proffer OK #}
|
|
<div class="sea-stacks">
|
|
<span class="sea-stacks-label">DECKS</span>
|
|
<div class="sea-deck-stack sea-deck-stack--gravity">
|
|
<div class="sea-stack-face">
|
|
<button class="btn btn-reveal sea-stack-ok" type="button">FLIP</button>
|
|
</div>
|
|
<span class="sea-stack-name">Gravity</span>
|
|
</div>
|
|
<div class="sea-deck-stack sea-deck-stack--levity">
|
|
<div class="sea-stack-face">
|
|
<button class="btn btn-reveal sea-stack-ok" type="button">FLIP</button>
|
|
</div>
|
|
<span class="sea-stack-name">Levity</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sea-form-actions">
|
|
<button type="button" id="id_sea_lock_hand" class="btn btn-primary" disabled>
|
|
LOCK HAND
|
|
</button>
|
|
<button type="button" id="id_sea_del" class="btn btn-danger">
|
|
DEL
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>{# /.sea-modal-body #}
|
|
|
|
</div>{# /.sea-modal #}
|
|
|
|
<button type="button" id="id_sea_cancel" class="btn btn-cancel btn-sm">NVM</button>
|
|
|
|
</div>{# /.sea-modal-wrap #}
|
|
|
|
{# ── Sea stage — big card viewer ─────────────────────────────────────────── #}
|
|
{# Extracted to a shared partial so the my-sea picker (Sprint 5 iter 4-bugs) #}
|
|
{# reuses the same DOM contract that SeaDeal binds to. #}
|
|
{% include "apps/gameboard/_partials/_sea_stage.html" %}
|
|
|
|
</div>{# /.sea-overlay #}
|
|
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
|
|
const overlay = document.getElementById('id_sea_overlay');
|
|
const cancelBtn = document.getElementById('id_sea_cancel');
|
|
|
|
function openSea() {
|
|
document.documentElement.classList.add('sea-open');
|
|
}
|
|
|
|
function closeSea() {
|
|
document.documentElement.classList.remove('sea-open');
|
|
}
|
|
|
|
const pickSeaBtn = document.getElementById('id_pick_sea_btn');
|
|
if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea);
|
|
cancelBtn.addEventListener('click', closeSea);
|
|
|
|
// ── Deck draw ──────────────────────────────────────────────────────────────
|
|
|
|
const SEA_DECK_URL = overlay.dataset.seaDeckUrl;
|
|
|
|
const SPREAD_ORDER = {
|
|
'waite-smith': ['.sea-pos-cover', '.sea-pos-cross', '.sea-pos-crown', '.sea-pos-lay', '.sea-pos-loom', '.sea-pos-leave'],
|
|
'escape-velocity': ['.sea-pos-cover', '.sea-pos-cross', '.sea-pos-lay', '.sea-pos-leave', '.sea-pos-crown', '.sea-pos-loom'],
|
|
};
|
|
|
|
let levityPile = [], gravityPile = [];
|
|
let _filled = 0;
|
|
let _activeStack = null;
|
|
|
|
const spreadSel = overlay.querySelector('#id_sea_spread');
|
|
const lockBtn = overlay.querySelector('#id_sea_lock_hand');
|
|
const delBtn = overlay.querySelector('#id_sea_del');
|
|
|
|
function _spreadKey() {
|
|
return spreadSel ? spreadSel.value : 'waite-smith';
|
|
}
|
|
|
|
function _nextPosSelector() {
|
|
const order = SPREAD_ORDER[_spreadKey()] || SPREAD_ORDER['waite-smith'];
|
|
return order[_filled] || null;
|
|
}
|
|
|
|
function _hideOk() {
|
|
if (_activeStack) {
|
|
const ok = _activeStack.querySelector('.sea-stack-ok');
|
|
if (ok) ok.style.display = 'none';
|
|
_activeStack.classList.remove('sea-deck-stack--active');
|
|
_activeStack = null;
|
|
}
|
|
}
|
|
|
|
function _showOk(stack) {
|
|
_hideOk();
|
|
_activeStack = stack;
|
|
stack.classList.add('sea-deck-stack--active');
|
|
const ok = stack.querySelector('.sea-stack-ok');
|
|
if (ok) ok.style.display = '';
|
|
}
|
|
|
|
function _reset() {
|
|
_filled = 0;
|
|
_hideOk();
|
|
overlay.querySelectorAll('.sea-card-slot').forEach(s => {
|
|
s.classList.remove('sea-card-slot--filled', 'sea-card-slot--visible', 'sea-card-slot--focused', 'sea-card-slot--levity', 'sea-card-slot--gravity');
|
|
s.classList.add('sea-card-slot--empty');
|
|
s.innerHTML = '';
|
|
delete s.dataset.cardId;
|
|
delete s.dataset.posKey;
|
|
});
|
|
if (lockBtn) lockBtn.disabled = true;
|
|
if (window.SeaDeal) SeaDeal.resetHand();
|
|
_fetchDeck();
|
|
}
|
|
|
|
function _fetchDeck() {
|
|
fetch(SEA_DECK_URL, { credentials: 'same-origin' })
|
|
.then(r => r.json())
|
|
.then(data => { levityPile = data.levity || []; gravityPile = data.gravity || []; })
|
|
.catch(() => {});
|
|
}
|
|
|
|
overlay.querySelectorAll('.sea-deck-stack').forEach(stack => {
|
|
stack.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
_activeStack === stack ? _hideOk() : _showOk(stack);
|
|
});
|
|
const ok = stack.querySelector('.sea-stack-ok');
|
|
if (ok) {
|
|
ok.style.display = 'none';
|
|
ok.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
const isLevity = stack.classList.contains('sea-deck-stack--levity');
|
|
const pile = isLevity ? levityPile : gravityPile;
|
|
const card = pile.length ? pile.shift() : null;
|
|
const pos = _nextPosSelector();
|
|
if (card && pos) {
|
|
_filled++;
|
|
if (lockBtn) lockBtn.disabled = (_filled < 6);
|
|
if (window.SeaDeal) SeaDeal.openStage(card, pos, isLevity);
|
|
}
|
|
_hideOk();
|
|
});
|
|
}
|
|
});
|
|
|
|
overlay.addEventListener('click', _hideOk);
|
|
if (delBtn) delBtn.addEventListener('click', _reset);
|
|
|
|
_fetchDeck();
|
|
if (window.SeaDeal) SeaDeal.reinit();
|
|
})();
|
|
</script>
|