polish + bugfix session — wallet/Game Kit applet realign; my_sea label/shadow polish; DEL/FLIP state machine; sig-change cooldown loophole closure; sky-wheel planet shadow; Fiorentine additive numerals; kit-bag DOFF async refresh — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed

End-of-session bundle 2026-05-26 covering ~10 distinct threads atop the A.7.5-polish-8 sky-wheel mini-portal commit (9cdd2cd). A.8 room.html sprint deferred per user — waiting on image scraping for RWS + future decks so the room can apply the image-mode pattern uniformly w.o. straddling text-mode fallback for unequippable Earthman Shabby Cardstock.

**(1) Game Kit + My Wallet applet realignment** — user spec "this isn't a place for tokens" / "only equippables should be there". Game Kit applet (/gameboard/, _applet-game-kit.html) drops the Free Token block — only PASS/BAND/CARTE/COIN trinkets + decks + dice remain. Free + Tithe tokens MOVED to the My Wallet applet on /dashboard/ (_applet-wallet.html rewrite). All trinkets COPIED into Wallet w. same .tt tooltip + DON/DOFF wiring so the user can equip from either surface. Stacked free/tithe icons (single icon per type) carry a .shop-badge ×N count (fa-coins for free, fa-piggy-bank for tithe — the latter standardized from outlier fa-hand-holding-dollar, now matching wallet / kit_bag / shop seed / FTs). Writs placeholder gets the same .token + .tt chrome ("Base currency unit ; Earned at the gate, spent in the shop"). 99+ cap on all badges. home_page view in apps/dashboard/views.py now passes pass/band/carte/coin + free/tithe tokens + counts + equipped_trinket_id. gameboard.js loaded on dashboard for the hover-portal tooltip system; #id_game_kit wrapper added (uses display: contents to stay transparent to the section-grid layout). Standalone game_kit.html page (_game_kit_sections.html) also reorganized — trinkets/tokens/decks each use bare .token icons w. centered flex row + 2rem gap, 1.5rem font-size to match gameboard sizing. id_game_kit outer wrapper data attrs (equipped-id, equipped-deck-id, in-use-deck-ids) feed buildMiniContent() for Equipped/Not Equipped/In-Use status.

**(2) My Sea label + shadow polish (my_sea.html Cross + applet)** — user spec "labels appear below and beneath the card, w. the card's shadow obscuring the very top of the label" per the GRAVITY/LEVITY .sea-stack-name pattern. .sea-pos-label repositioning: CROWN + COVER ABOVE slot (bottom: 100%; translate(-50%, -0.4rem)), LAY + CROSS BELOW slot (top: 100%; translate(-50%, 0.3rem)), LEAVE + LOOM increased breathing room (translate -0.4rem LEFT / 0.4rem RIGHT — was 0.1rem overlap). CROWN cell translateY(-0.5rem) UP + LAY cell translateY(0.5rem) DOWN for COVER/CROSS label breathing room. Filled-card downward shadow chain (1px 2px 0 black, 0 4px 0 black-faint, 2px 5px 5px black-blur) scoped to .my-sea-cross .sea-card-slot--filled only — empty dashed placeholders stay shadowless per user spec ("only the cards that replace [slots] should [have shadows]"). Four rotation-correction overrides for box-shadow rotating w. element transform: base (0deg), reversed (180deg sign-flip), cross (90deg matrix rotation → 2px -1px), cross+reversed (270deg → -2px 1px). Saved here for future reference since the matrix derivation is non-obvious: CSS rotate(θ) CW maps offset (a, b) → screen (a·cos θ − b·sin θ, a·sin θ + b·cos θ); solving for unrotated offsets that produce screen-down-right post-rotation gives the 4 chains. My Sea applet .my-sea-slot-label (z-index 0, margin-top 0.15rem) + .my-sea-slot--filled shadow + reversed-variant shadow inversion all mirror the page treatment.

**(3) DEL btn + FLIP btn state machine** — user spec: DEL un-disables as soon as ANY card drawn (was gated on hand_complete) ; FLIP btn .btn-disabled + text swap to × once hand complete. _setComplete(on) toggles FLIP btn class + label (parity w. DEL convention: × disabled / word active) ; new _setHasDrawn(on) helper extracted (was bundled in _setComplete). Wired into 4 transitions: (a) manual deposit _filled === 1, (b) initial page-load seed when _filled > 0, (c) AUTO DRAW path post-POST (CRITICAL FIX — was missing, only manual deposit synced DEL even though server already committed all cards on AUTO DRAW), (d) _resetHand spread-switch reset. Template DEL btn gates on saved_by_position (any draw); FLIP btn gates on hand_complete. Test test_partial_hand_del_btn_carries_btn_disabled inverted to test_partial_hand_del_btn_is_enabled per the new spec.

**(4) Sig-change MySeaDraw RESET (cooldown loophole closure)** — user-reported revenue-stream loophole 2026-05-26: switching sig used to re-open the FREE DRAW gate + forfeit any paid-draw credit, because apps/gameboard/views.py:266's `in_cooldown = active_draw is not None` keyed entirely off the MySeaDraw row's existence (NOT off User.last_free_draw_at, which is the cooldown TIMER but doesn't drive the in_cooldown decision). Initial draft DELETED the row on sig change — turned out too aggressive: lost both the cooldown anchor (created_at via the active_draw check) AND the paid-state fields (deposit_token_id, paid_through_at). FIX: save_sign on actual sig change `.update(hand=[], significator_id=new, significator_reversed=new)` — preserves cooldown + paid revenue, just resets the hand + sig snapshot. clear_sign left untouched (sig-cleared user can't draw anyway per my_sea_lock's no_significator guard; row sits dormant until re-pick routes through save_sign's reset). Guarded w. sig_changed so re-saving the same sig is a no-op. User.last_free_draw_at was always safe — User-level field, only ever set in my_sea_lock, never cleared (user confirmed the Brief shows 11:59pm consistently). Subtle architectural note for future: the in_cooldown decision being row-existence-based rather than timestamp-based is the load-bearing implicit dependency this loophole exposed; any refactor that delete()s the row needs to either flip in_cooldown to consult last_free_draw_at OR preserve the row as we did here.

**(5) Kit-bag DOFF async refresh** — user-reported 2026-05-26: deck disappears entirely from kit-bag on first DOFF; only manual page refresh restores the placeholder. Root cause: _syncKitBagDialog() in gameboard.js did card.querySelector('i') for the placeholder icon — worked for trinket/token cards (single FA <i>) but BROKE for image-equipped decks whose card-stack icon is <svg class="deck-stack-icon"> (no <i> to copy → empty placeholder div). DROP the client-side optimization, route both DOFF paths thru _refreshKitDialog() (symmetric w. DON). Single source of truth = server-rendered _kit_bag_panel.html's placeholder branch (re-renders _deck_stack_icon.html w.o. the deck arg for the empty-fill SVG).

**(6) Sky-wheel planet circle shadow** — user spec "tight 1px 1px black shadow at opacity 0.7 on planet circle groups in all sky locations". Base `filter: drop-shadow(1px 1px 0 rgba(0,0,0,0.7))` on .nw-planet-group so planet badges lift off the wheel rings on /dashboard/sky/ + My Sky applet + any future surface. Hover/active state chains shadow + glow ("drop-shadow ... ; drop-shadow(0 0 5px primary-lm)") since CSS filter REPLACES rather than APPENDS — shadow has to be re-stated on the hover rule to persist during interaction. Elements/signs/houses groups keep their glow-only hover (the request was planet-specific).

**(7) TarotCard suit_icon + Fiorentine additive numerals** — (a) suit_icon property pre-checks for major arcana trump 0 → fa-hat-cowboy-side (Fool/Nomad/Matto archetype) and trump 1 → fa-hat-wizard (Magician/Schizo/Bagatto archetype), pinned BEFORE the self.icon branch so even a deck seed supplying a different icon for these ranks normalizes to the convention. Earthman's seed already aligns; Minchiate (empty icon field) used to fall thru to fa-hand-dots. (b) _to_roman() adds _FIORENTINE_ADDITIVE_NUMERALS = {4:'IIII', 19:'XVIIII', 24:'XXIIII', 29:'XXVIIII', 34:'XXXIIII', 39:'XXXVIIII'} pre-check — locked-in 6-exception list per user-corrected spec (initial draft used universal additive form, user clarified "no, only these specific ones, e.g. trump 9 still prints IX + trump 14 still prints XIV per the actual Minchiate deck art"). +2 regression tests: additive overrides + non-overridden subtractive (9=IX, 14=XIV, 44=XLIV, 49=XLIX).

**(8) Gear menu NVM font fix** — _my_sea_gear.html's NVM btn changed from <a class="btn"> to <button onclick="location.href=..."> per [[feedback-btn-vs-anchor-font-family]] (anchor inherits body serif font; button stays sans-serif by browser default). Brief's NVM uses <button> + reads correctly — this matches it.

**(9) Image-mode slot transparency overrides** — 3 surfaces got `overflow: visible` (base overflow: hidden was clipping the contour-stroke filter chain) + transparent bg/border re-states for image-equipped Minchiate cards on (a) .my-sea-cross .sea-card-slot--filled + image variant, (b) .sig-stage-card.sea-sig-card.sig-stage-card--image base + levity-polarity nested override, (c) .sea-deck-stack--single .sea-stack-face:has(.sea-stack-face-img) (using :has() to key off the conditional back-img child). Followup to A.7.5-polish-* sprint — those surfaces' image-mode bg overrides didn't include overflow.

Tests: 1336/1336 IT+UT total green (was 1322 before the session). No FT runs per [[feedback-ft-run-discipline]]; visual verify ongoing by user across the session via Firefox reload.

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:
Disco DeDisco
2026-05-26 01:18:51 -04:00
parent 9cdd2cda68
commit 955bdc7f67
18 changed files with 686 additions and 132 deletions

View File

@@ -68,21 +68,11 @@
</div>
</div>
{% endif %}
{% if free_tokens %}
{% with free_tokens.0 as token %}
<div id="id_kit_free_token" class="token">
<i class="fa-solid fa-coins"></i>
<div class="tt">
<h4 class="tt-title">{{ token.tooltip_name }}{% if free_count > 1 %} <span class="token-count">(×{{ free_count }})</span>{% endif %}</h4>
<p class="tt-description">{{ token.tooltip_description }}</p>
{% if token.tooltip_shoptalk %}
<p class="tt-shoptalk"><em>{{ token.tooltip_shoptalk }}</em></p>
{% endif %}
<p class="tt-expiry">{{ token.tooltip_expiry }}</p>
</div>
</div>
{% endwith %}
{% endif %}
{# Free Tokens REMOVED from Game Kit applet — they're not equippable. #}
{# Now displayed in the My Wallet applet (`_applet-wallet.html` on #}
{# /dashboard/) alongside Tithe Tokens + Writs (user spec 2026-05-25 #}
{# PM). Only equippables (PASS/BAND/CARTE/COIN trinkets, decks, dice) #}
{# belong in Game Kit since this is the equip surface. #}
{% for deck in deck_variants %}
<div id="id_kit_{{ deck.short_key }}_deck" class="token deck-variant" data-deck-id="{{ deck.pk }}" data-in-use-room-name="{{ deck.in_use_room_name|default:'' }}">
{# Sprint A.4 — card-deck stack icon replaces the old fa-id-badge. #}

View File

@@ -1,31 +1,70 @@
<div id="id_gk_sections_container">
{% comment %}
`#id_game_kit` wraps every section so `gameboard.js`'s `initGameKitTooltips
()` finds ALL `.token` elements (trinkets + tokens + decks) under one
scope. Data attrs feed `buildMiniContent()` for the Equipped / Not
Equipped / In-Use status text.
{% endcomment %}
<div id="id_game_kit" data-equipped-id="{{ equipped_trinket_id|default:'' }}" data-equipped-deck-id="{{ equipped_deck_id|default:'' }}" data-in-use-deck-ids="{% for d in deck_variants %}{% if d.in_use_room_name %}{{ d.pk }},{% endif %}{% endfor %}">
{% for entry in applets %}
{% if entry.applet.slug == 'gk-trinkets' and entry.visible %}
<section id="id_gk_trinkets" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">
<h2>Trinkets</h2>
<div class="gk-items">
{% if pass_token %}
<div class="gk-trinket-card" data-token-id="{{ pass_token.pk }}">
<div id="id_kit_pass" class="token" data-token-id="{{ pass_token.pk }}">
<i class="fa-solid fa-clipboard"></i>
<span>{{ pass_token.tooltip_name }}</span>
<div class="tt">
<div class="tt-equip-btns">
{% if pass_token.pk == equipped_trinket_id %}<button class="btn btn-equip btn-disabled" data-token-id="{{ pass_token.pk }}">×</button><button class="btn btn-unequip" data-token-id="{{ pass_token.pk }}">DOFF</button>{% else %}<button class="btn btn-equip" data-token-id="{{ pass_token.pk }}">DON</button><button class="btn btn-unequip btn-disabled" data-token-id="{{ pass_token.pk }}">×</button>{% endif %}
</div>
<h4 class="tt-title">{{ pass_token.tooltip_name }}</h4>
<p class="tt-description">{{ pass_token.tooltip_description }}</p>
{% if pass_token.tooltip_shoptalk %}<p class="tt-shoptalk"><em>{{ pass_token.tooltip_shoptalk }}</em></p>{% endif %}
<p class="tt-expiry">{{ pass_token.tooltip_expiry }}</p>
</div>
</div>
{% endif %}
{% if band %}
<div class="gk-trinket-card" data-token-id="{{ band.pk }}">
<div id="id_kit_wristband" class="token" data-token-id="{{ band.pk }}">
<i class="fa-solid fa-ring"></i>
<span>{{ band.tooltip_name }}</span>
<div class="tt">
<div class="tt-equip-btns">
{% if band.pk == equipped_trinket_id %}<button class="btn btn-equip btn-disabled" data-token-id="{{ band.pk }}">×</button><button class="btn btn-unequip" data-token-id="{{ band.pk }}">DOFF</button>{% else %}<button class="btn btn-equip" data-token-id="{{ band.pk }}">DON</button><button class="btn btn-unequip btn-disabled" data-token-id="{{ band.pk }}">×</button>{% endif %}
</div>
<h4 class="tt-title">{{ band.tooltip_name }}</h4>
<p class="tt-description">{{ band.tooltip_description }}</p>
{% if band.tooltip_shoptalk %}<p class="tt-shoptalk"><em>{{ band.tooltip_shoptalk }}</em></p>{% endif %}
<p class="tt-expiry">{{ band.tooltip_expiry }}</p>
</div>
</div>
{% endif %}
{% if carte %}
<div class="gk-trinket-card" data-token-id="{{ carte.pk }}">
<div id="id_kit_carte_blanche" class="token" data-token-id="{{ carte.pk }}" data-current-room-name="{{ carte.current_room.name|default:'' }}">
<i class="fa-solid fa-money-check"></i>
<span>{{ carte.tooltip_name }}</span>
<div class="tt">
<div class="tt-equip-btns">
{% if carte.current_room %}<button class="btn btn-equip btn-disabled" data-token-id="{{ carte.pk }}">×</button><button class="btn btn-unequip btn-disabled" data-token-id="{{ carte.pk }}">×</button>{% elif carte.pk == equipped_trinket_id %}<button class="btn btn-equip btn-disabled" data-token-id="{{ carte.pk }}">×</button><button class="btn btn-unequip" data-token-id="{{ carte.pk }}">DOFF</button>{% else %}<button class="btn btn-equip" data-token-id="{{ carte.pk }}">DON</button><button class="btn btn-unequip btn-disabled" data-token-id="{{ carte.pk }}">×</button>{% endif %}
</div>
<h4 class="tt-title">{{ carte.tooltip_name }}</h4>
<p class="tt-description">{{ carte.tooltip_description }}</p>
{% if carte.tooltip_shoptalk %}<p class="tt-shoptalk"><em>{{ carte.tooltip_shoptalk }}</em></p>{% endif %}
<p class="tt-expiry">{{ carte.tooltip_expiry }}</p>
</div>
</div>
{% endif %}
{% if coin %}
<div class="gk-trinket-card" data-token-id="{{ coin.pk }}">
<div id="id_kit_coin_on_a_string" class="token" data-token-id="{{ coin.pk }}" data-current-room-name="{{ coin.current_room.name|default:'' }}">
<i class="fa-solid fa-medal"></i>
<span>{{ coin.tooltip_name }}</span>
<div class="tt">
<div class="tt-equip-btns">
{% if coin.current_room %}<button class="btn btn-equip btn-disabled" data-token-id="{{ coin.pk }}">×</button><button class="btn btn-unequip btn-disabled" data-token-id="{{ coin.pk }}">×</button>{% elif coin.pk == equipped_trinket_id %}<button class="btn btn-equip btn-disabled" data-token-id="{{ coin.pk }}">×</button><button class="btn btn-unequip" data-token-id="{{ coin.pk }}">DOFF</button>{% else %}<button class="btn btn-equip" data-token-id="{{ coin.pk }}">DON</button><button class="btn btn-unequip btn-disabled" data-token-id="{{ coin.pk }}">×</button>{% endif %}
</div>
<h4 class="tt-title">{{ coin.tooltip_name }}</h4>
<p class="tt-description">{{ coin.tooltip_description }}</p>
{% if coin.tooltip_shoptalk %}<p class="tt-shoptalk"><em>{{ coin.tooltip_shoptalk }}</em></p>{% endif %}
<p class="tt-expiry">{{ coin.tooltip_expiry }}</p>
</div>
</div>
{% endif %}
{% if not pass_token and not band and not carte and not coin %}
@@ -38,19 +77,46 @@
{% if entry.applet.slug == 'gk-tokens' and entry.visible %}
<section id="id_gk_tokens" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">
<h2>Tokens</h2>
{% comment %}
Free + Tithe tokens stack to ONE icon per type — the count
renders both in the .tt-title (×N chip) and on the icon itself
via `.shop-badge` (parity w. wallet.html's Tithe Bundle badge).
Single-token case OMITS the badge (user spec). Cap at "99+" so
a runaway count can't overflow the 1.5rem badge circle.
{% endcomment %}
<div class="gk-items">
{% for token in free_tokens %}
<div class="gk-token-card" data-token-id="{{ token.pk }}">
{% if free_tokens %}
{% with free_tokens.0 as token %}
<div id="id_kit_free_token" class="token">
<i class="fa-solid fa-coins"></i>
<span>{{ token.tooltip_name }}</span>
{% if free_count > 1 %}<span class="shop-badge">{% if free_count > 99 %}99+{% else %}{{ free_count }}{% endif %}</span>{% endif %}
<div class="tt">
<h4 class="tt-title">{{ token.tooltip_name }}{% if free_count > 1 %} <span class="token-count">(×{% if free_count > 99 %}99+{% else %}{{ free_count }}{% endif %})</span>{% endif %}</h4>
<p class="tt-description">{{ token.tooltip_description }}</p>
{% if token.tooltip_shoptalk %}<p class="tt-shoptalk"><em>{{ token.tooltip_shoptalk }}</em></p>{% endif %}
<p class="tt-expiry">{{ token.tooltip_expiry }}</p>
</div>
</div>
{% endfor %}
{% for token in tithe_tokens %}
<div class="gk-token-card" data-token-id="{{ token.pk }}">
<i class="fa-solid fa-hand-holding-dollar"></i>
<span>{{ token.tooltip_name }}</span>
{% endwith %}
{% endif %}
{% if tithe_tokens %}
{% with tithe_tokens.0 as token %}
<div id="id_kit_tithe_token" class="token">
{# `fa-piggy-bank` is the canonical tithe-token glyph — #}
{# matches wallet (_applet-wallet-tokens.html), kit bag #}
{# (_kit_bag_panel.html), shop seed (0009_seed_shop_items) #}
{# + FTs. The pre-rewrite version of this template used #}
{# `fa-hand-holding-dollar` — sole outlier, now aligned. #}
<i class="fa-solid fa-piggy-bank"></i>
{% if tithe_count > 1 %}<span class="shop-badge">{% if tithe_count > 99 %}99+{% else %}{{ tithe_count }}{% endif %}</span>{% endif %}
<div class="tt">
<h4 class="tt-title">{{ token.tooltip_name }}{% if tithe_count > 1 %} <span class="token-count">(×{% if tithe_count > 99 %}99+{% else %}{{ tithe_count }}{% endif %})</span>{% endif %}</h4>
<p class="tt-description">{{ token.tooltip_description }}</p>
<p class="tt-expiry">{{ token.tooltip_expiry }}</p>
</div>
</div>
{% endfor %}
{% endwith %}
{% endif %}
{% if not free_tokens and not tithe_tokens %}
<p class="gk-empty"><em>No tokens yet.</em></p>
{% endif %}
@@ -62,11 +128,18 @@
<section id="id_gk_decks" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">
<h2>Card Decks</h2>
<div class="gk-items">
{% for deck in unlocked_decks %}
<div class="gk-deck-card" data-deck-id="{{ deck.pk }}">
<i class="fa-regular fa-id-badge"></i>
<span>{{ deck.name }}</span>
<small>{{ deck.card_count }} cards</small>
{% for deck in deck_variants %}
<div id="id_kit_{{ deck.short_key }}_deck" class="gk-deck-card token deck-variant" data-deck-id="{{ deck.pk }}" data-in-use-room-name="{{ deck.in_use_room_name|default:'' }}">
{% include "apps/gameboard/_partials/_deck_stack_icon.html" %}
<div class="tt">
<div class="tt-equip-btns">
{% if deck.in_use_room_name %}<button class="btn btn-equip btn-disabled" data-deck-id="{{ deck.pk }}">×</button><button class="btn btn-unequip btn-disabled" data-deck-id="{{ deck.pk }}">×</button>{% elif deck.pk == equipped_deck_id %}<button class="btn btn-equip btn-disabled" data-deck-id="{{ deck.pk }}">×</button><button class="btn btn-unequip" data-deck-id="{{ deck.pk }}">DOFF</button>{% else %}<button class="btn btn-equip" data-deck-id="{{ deck.pk }}">DON</button><button class="btn btn-unequip btn-disabled" data-deck-id="{{ deck.pk }}">×</button>{% endif %}
</div>
<h4 class="tt-title">{{ deck.name }}{% if deck.is_default %} <span class="token-count">(Default)</span>{% endif %}</h4>
<p class="tt-description">{{ deck.card_count }}-card Tarot deck{% if deck.is_polarized %} <span class="tt-x2">(×2)</span>{% endif %}</p>
{% if deck.description %}<p class="tt-shoptalk"><em>{{ deck.description }}</em></p>{% endif %}
<p class="tt-shoptalk">Stock version <span class="tt-subcounter">(0 substitutions)</span></p>
</div>
</div>
{% empty %}
<p class="gk-empty"><em>No decks unlocked.</em></p>
@@ -97,4 +170,5 @@
</section>
{% endif %}
{% endfor %}
</div>
</div>

View File

@@ -3,7 +3,11 @@
{# committing" affordance; NVM nav-backs to /gameboard/ mirroring the #}
{# room's gear-menu convention. Rendered unconditionally (no active- #}
{# draw guard) so fresh users + post-DEL states still see it. #}
{# `<button>` not `<a class="btn">` — `.btn` doesn't reset font-family, so #}
{# the anchor inherits body's serif font (the Brief's NVM uses `<button>` #}
{# + reads correctly as sans-serif). Inline `onclick` preserves the nav #}
{# semantics the href had. See [[feedback-btn-vs-anchor-font-family]]. #}
<div id="id_my_sea_menu" style="display:none">
<a href="{% url 'gameboard' %}" class="btn btn-cancel">NVM</a>
<button type="button" class="btn btn-cancel" onclick="location.href='{% url 'gameboard' %}'">NVM</button>
</div>
{% include "apps/applets/_partials/_gear.html" with menu_id="id_my_sea_menu" %}

View File

@@ -12,6 +12,15 @@
{% include "apps/gameboard/_partials/_game_kit_sections.html" %}
</div>
</div>
{% comment %}
Tooltip portal scaffolds for the gk-decks card-stack icons — `gameboard.js`'s
`initGameKitTooltips()` positions deck tooltips into `#id_tooltip_portal` +
the Equipped / Not Equipped status into `#id_mini_tooltip_portal` (text-only
mini). Mirrors the gameboard.html main page's portal pattern verbatim so the
deck row reads identically on /gameboard/ + /gameboard/game-kit/.
{% endcomment %}
<div id="id_tooltip_portal" class="token-tooltip" style="display:none;"></div>
<div id="id_mini_tooltip_portal" class="token-tooltip token-tooltip--mini" style="display:none;"></div>
<dialog id="id_tarot_fan_dialog">
<div class="tarot-fan-wrap">
@@ -41,5 +50,10 @@
{# Brief.showBanner — needed for the Baltimorean Note-unlock banner the #}
{# pronouns applet fires on first `bawlmorese` selection. #}
<script src="{% static 'apps/dashboard/note.js' %}"></script>
{# gameboard.js for `initGameKitTooltips()` — the gk-decks section's #}
{# `#id_game_kit` wrapper + `.token deck-variant` icons hook into its #}
{# hover-portal + DON/DOFF wiring. game-kit.js still owns the carousel #}
{# (fan modal) — clicking the icon opens the fan via .gk-deck-card. #}
<script src="{% static 'apps/gameboard/gameboard.js' %}"></script>
<script src="{% static 'apps/gameboard/game-kit.js' %}"></script>
{% endblock scripts %}

View File

@@ -221,18 +221,22 @@
{# Critical: this collapse is my_sea-ONLY — room.html #}
{# keeps the dual layout since multiple gamers contribute #}
{# (each might bring a different polarization). #}
{# FLIP btn picks up `.btn-disabled` once the hand is #}
{# complete — visual signal that no more draws remain. #}
{# JS `_setComplete(on)` toggles the same class on #}
{# live transition (last-card landed). #}
{% if request.user.equipped_deck.is_polarized %}
<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>
<button class="btn btn-reveal sea-stack-ok{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}&times;{% else %}FLIP{% endif %}</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>
<button class="btn btn-reveal sea-stack-ok{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}&times;{% else %}FLIP{% endif %}</button>
</div>
<span class="sea-stack-name">Levity</span>
</div>
@@ -245,7 +249,7 @@
{% if request.user.equipped_deck.has_card_images %}
<img class="sea-stack-face-img" src="{{ request.user.equipped_deck.back_image_url }}" alt="">
{% endif %}
<button class="btn btn-reveal sea-stack-ok" type="button">FLIP</button>
<button class="btn btn-reveal sea-stack-ok{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}&times;{% else %}FLIP{% endif %}</button>
</div>
</div>
</div>
@@ -266,18 +270,16 @@
class="btn btn-primary"
data-state="{% if hand_complete %}gate-view{% else %}auto-draw{% endif %}"
data-gate-url="{% url 'my_sea_gate' %}">{% if hand_complete %}GATE<br>VIEW{% else %}AUTO<br>DRAW{% endif %}</button>
{# DEL starts `.btn-disabled` until the hand is #}
{# complete — per Sprint-5-iter-4c spec, the 1/day #}
{# quota is committed at first-card-draw + can't be #}
{# refunded by an early DEL. Case-by-case `&times;` #}
{# rendering matches game-kit tooltip + DON/DOFF #}
{# convention (user-spec 2026-05-20): a disabled btn #}
{# carries × in its own text node, not via a CSS #}
{# overlay. JS `_setComplete` swaps inner text in #}
{# lockstep with the `.btn-disabled` class toggle. #}
{# DEL gates on whether ANY card has been drawn (user #}
{# spec 2026-05-26): pre-first-draw it's disabled; as #}
{# soon as `saved_by_position` has at least one entry #}
{# the user can DEL the in-progress hand. `&times;` #}
{# is the disabled-state label (parity w. game-kit's #}
{# DON/DOFF disabled convention). JS `_setHasDrawn` #}
{# swaps text + class in lockstep w. live deposits. #}
<button type="button"
id="id_sea_del"
class="btn btn-danger{% if not hand_complete %} btn-disabled{% endif %}">{% if hand_complete %}DEL{% else %}&times;{% endif %}</button>
class="btn btn-danger{% if not saved_by_position %} btn-disabled{% endif %}">{% if saved_by_position %}DEL{% else %}&times;{% endif %}</button>
</div>
</div>
{# Sea stage — portaled modal that opens on FLIP click via #}
@@ -464,12 +466,14 @@
function _resetHand() {
// Spread-switch reset only (mid-draw guard inside sync()).
// Iter 4c removed the explicit DEL-resets-hand pathway:
// pre-completion DEL is `.btn-disabled`; post-completion
// DEL routes to the guard portal → server-side delete.
// Post-DEL routes through the guard portal → server-side
// delete + page reload, so this path doesn't fire there.
// Re-disable DEL + re-enable FLIP for the fresh-empty state.
_filled = 0;
_hideOk();
_unlockSpread();
_setHasDrawn(false);
_setComplete(false);
cross.querySelectorAll(
'.sea-crucifix-cell.sea-pos-crown, ' +
'.sea-crucifix-cell.sea-pos-leave, ' +
@@ -484,24 +488,24 @@
}
function _setComplete(on) {
// Iter 4c — single-call state transition for "hand
// complete": DEL un-disables, action btn becomes GATE
// VIEW, FLIP buttons on the deck stacks render as
// disabled when shown. The deck STACKS themselves stay
// click-responsive (so the user can see the disabled
// FLIP feedback) — `_locked` gates the actual draw.
//
// DEL btn text follows the game-kit tooltip convention
// (user-spec 2026-05-20): disabled state reads × (the
// template's initial render does the same), active
// state reads DEL. Lockstep w. the `.btn-disabled`
// class so the visual + label always agree.
// State transition for "hand complete": deck-stack FLIP
// buttons render as disabled (visual signal that no more
// draws remain), action btn becomes GATE VIEW. The deck
// STACKS themselves stay click-responsive so the user
// sees the disabled-FLIP feedback — `_locked` gates the
// actual draw at the click-handler level. DEL state was
// here previously but is now driven by `_setHasDrawn`
// instead (user spec 2026-05-26: DEL unlocks at first
// draw, NOT at completion).
_locked = on;
picker.classList.toggle('my-sea-picker--locked', on);
if (delBtn) {
delBtn.classList.toggle('btn-disabled', !on);
delBtn.innerHTML = on ? 'DEL' : '×';
}
picker.querySelectorAll('.sea-deck-stack .sea-stack-ok').forEach(function (btn) {
btn.classList.toggle('btn-disabled', on);
// Mirrors DEL btn convention: disabled label is × (the
// template's initial render does the same), active is
// 'FLIP'. Keeps visual + class in lockstep.
btn.innerHTML = on ? '×' : 'FLIP';
});
if (actionBtn) {
actionBtn.dataset.state = on ? 'gate-view' : 'auto-draw';
actionBtn.innerHTML = on ? 'GATE<br>VIEW' : 'AUTO<br>DRAW';
@@ -509,6 +513,19 @@
_hideOk();
}
function _setHasDrawn(on) {
// DEL btn state — un-disables as soon as ANY card has
// been drawn (user spec 2026-05-26). Pre-first-draw the
// user can't DEL an empty hand; once a card is down,
// DEL routes to the guard portal → server-side delete.
// Disabled-state label `×` mirrors the game-kit DON/DOFF
// convention.
if (delBtn) {
delBtn.classList.toggle('btn-disabled', !on);
delBtn.innerHTML = on ? 'DEL' : '×';
}
}
// ── Deck-stack click → show FLIP → click FLIP → deposit ─
// Iter 4c — stacks remain click-responsive even after hand
// is complete (so the user sees the disabled-FLIP feedback,
@@ -539,7 +556,10 @@
_fillSlot(posName, card, isLevity);
}
_filled++;
if (_filled === 1) _lockSpread();
if (_filled === 1) {
_lockSpread();
_setHasDrawn(true); // first card → DEL enables
}
// Per-placement upsert — server stays current
// so a navigate-away mid-draw still persists
// the partial hand. User-spec 2026-05-20.
@@ -626,7 +646,16 @@
_setComplete(true);
return;
}
// First-draw transitions — mirror the manual-flip flow.
// Lock spread + un-disable DEL as soon as the POST
// commits (we KNOW ≥1 card is now server-saved). Was a
// bug: AUTO DRAW only synced DEL state at completion
// via `_setComplete(true)`, so a mid-animation refresh
// (or backing out during the staggered placeNext loop)
// would leave DEL stuck disabled even though cards were
// already saved (user-reported 2026-05-26).
if (_filled === 0) _lockSpread();
_setHasDrawn(true);
var idx = 0;
function placeNext() {
if (idx >= autoEntries.length) {
@@ -840,8 +869,10 @@
if (_filled >= _currentOrder().length) {
_setComplete(true);
_lockSpread();
_setHasDrawn(true); // also enables DEL post-completion
} else if (_filled > 0) {
_lockSpread();
_setHasDrawn(true); // any partial draw → DEL enabled
}
// Seed SeaDeal's `_seaHand` from the server-rendered