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
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:
@@ -292,6 +292,7 @@ def my_sign(request):
|
|||||||
def save_sign(request):
|
def save_sign(request):
|
||||||
"""Persist the user's sign choice — POST { card_id, reversed }."""
|
"""Persist the user's sign choice — POST { card_id, reversed }."""
|
||||||
from apps.epic.models import TarotCard
|
from apps.epic.models import TarotCard
|
||||||
|
from apps.gameboard.models import MySeaDraw
|
||||||
if request.method != "POST":
|
if request.method != "POST":
|
||||||
return redirect("billboard:my_sign")
|
return redirect("billboard:my_sign")
|
||||||
card_id = request.POST.get("card_id")
|
card_id = request.POST.get("card_id")
|
||||||
@@ -300,9 +301,24 @@ def save_sign(request):
|
|||||||
card = TarotCard.objects.get(pk=card_id)
|
card = TarotCard.objects.get(pk=card_id)
|
||||||
except (TarotCard.DoesNotExist, ValueError, TypeError):
|
except (TarotCard.DoesNotExist, ValueError, TypeError):
|
||||||
return HttpResponseForbidden("invalid card_id")
|
return HttpResponseForbidden("invalid card_id")
|
||||||
|
sig_changed = request.user.significator_id != card.pk
|
||||||
request.user.significator = card
|
request.user.significator = card
|
||||||
request.user.significator_reversed = reversed_flag
|
request.user.significator_reversed = reversed_flag
|
||||||
request.user.save(update_fields=["significator", "significator_reversed"])
|
request.user.save(update_fields=["significator", "significator_reversed"])
|
||||||
|
# Sig change RESETS (but does NOT delete) any active MySeaDraw so the
|
||||||
|
# next /my-sea/ visit reads the NEW sig snapshot — while PRESERVING the
|
||||||
|
# cooldown anchor (row's `created_at` gates `in_cooldown` per views.py
|
||||||
|
# line 266) + paid-state fields (`deposit_token_id`, `paid_through_at`).
|
||||||
|
# Deleting the row would re-open the FREE DRAW gate (loophole — user
|
||||||
|
# could circumvent the 24h cooldown by re-picking a sig) AND forfeit
|
||||||
|
# any paid-draw credit the user already committed. Reset clears just
|
||||||
|
# the hand + sig snapshot, leaving cooldown + paid revenue intact.
|
||||||
|
if sig_changed:
|
||||||
|
MySeaDraw.objects.filter(user=request.user).update(
|
||||||
|
hand=[],
|
||||||
|
significator_id=card.pk,
|
||||||
|
significator_reversed=reversed_flag,
|
||||||
|
)
|
||||||
return redirect("billboard:my_sign")
|
return redirect("billboard:my_sign")
|
||||||
|
|
||||||
|
|
||||||
@@ -318,6 +334,14 @@ def clear_sign(request):
|
|||||||
request.user.significator = None
|
request.user.significator = None
|
||||||
request.user.significator_reversed = False
|
request.user.significator_reversed = False
|
||||||
request.user.save(update_fields=["significator", "significator_reversed"])
|
request.user.save(update_fields=["significator", "significator_reversed"])
|
||||||
|
# MySeaDraw is INTENTIONALLY left alone on sig-clear. Without a sig the
|
||||||
|
# user can't draw anyway (`my_sea_lock` returns 400 `no_significator`),
|
||||||
|
# and /my-sea/ routes to its sign-gate Brief via `user_has_sig`. The
|
||||||
|
# row's cooldown anchor + paid-state fields must survive a sig clear
|
||||||
|
# so the user can't re-open the FREE DRAW gate or forfeit paid credit
|
||||||
|
# by toggling sig-clear → sig-pick (user-reported loophole 2026-05-26).
|
||||||
|
# When the user re-picks via save_sign, that view's reset path updates
|
||||||
|
# the row's sig snapshot + clears the hand cleanly.
|
||||||
return redirect("billboard:my_sign")
|
return redirect("billboard:my_sign")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,27 @@ def home_page(request):
|
|||||||
}
|
}
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
context["applets"] = applet_context(request.user, "dashboard")
|
context["applets"] = applet_context(request.user, "dashboard")
|
||||||
|
# My Wallet applet — show all trinkets (PASS/BAND/CARTE/COIN) + stacked
|
||||||
|
# Free + Tithe tokens (badge count) + Writs placeholder. Trinkets COPY
|
||||||
|
# the Game Kit applet's tooltip + DON/DOFF wiring so the user can equip
|
||||||
|
# from either surface; Free + Tithe tokens MOVED off the Game Kit applet
|
||||||
|
# since they aren't equippable (user spec 2026-05-25 PM).
|
||||||
|
user = request.user
|
||||||
|
free_tokens = list(user.tokens.filter(
|
||||||
|
token_type=Token.FREE, expires_at__gt=timezone.now()
|
||||||
|
).order_by("expires_at"))
|
||||||
|
tithe_tokens = list(user.tokens.filter(token_type=Token.TITHE))
|
||||||
|
context.update({
|
||||||
|
"coin": user.tokens.filter(token_type=Token.COIN).first(),
|
||||||
|
"pass_token": user.tokens.filter(token_type=Token.PASS).first() if user.is_staff else None,
|
||||||
|
"band": user.tokens.filter(token_type=Token.BAND).first(),
|
||||||
|
"carte": user.tokens.filter(token_type=Token.CARTE).first(),
|
||||||
|
"free_tokens": free_tokens,
|
||||||
|
"tithe_tokens": tithe_tokens,
|
||||||
|
"free_count": len(free_tokens),
|
||||||
|
"tithe_count": len(tithe_tokens),
|
||||||
|
"equipped_trinket_id": user.equipped_trinket_id,
|
||||||
|
})
|
||||||
return render(request, "apps/dashboard/home.html", context)
|
return render(request, "apps/dashboard/home.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -351,10 +351,27 @@ class TarotCard(models.Model):
|
|||||||
ordering = ["deck_variant", "arcana", "suit", "number"]
|
ordering = ["deck_variant", "arcana", "suit", "number"]
|
||||||
unique_together = [("deck_variant", "slug")]
|
unique_together = [("deck_variant", "slug")]
|
||||||
|
|
||||||
|
# Per-trump overrides for Fiorentine Minchiate fidelity — the historical
|
||||||
|
# deck art uses additive numerals at these specific ranks only (NOT every
|
||||||
|
# 4/9 ending; e.g. trump 9 = IX, trump 14 = XIV stay subtractive per the
|
||||||
|
# actual printed cards). Earthman's 0-49 trumps inherit the same mapping
|
||||||
|
# for visual consistency w. the Fiorentine deck. Other ranks fall through
|
||||||
|
# to the standard subtractive `_to_roman` algorithm.
|
||||||
|
_FIORENTINE_ADDITIVE_NUMERALS = {
|
||||||
|
4: 'IIII',
|
||||||
|
19: 'XVIIII',
|
||||||
|
24: 'XXIIII',
|
||||||
|
29: 'XXVIIII',
|
||||||
|
34: 'XXXIIII',
|
||||||
|
39: 'XXXVIIII',
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_roman(n):
|
def _to_roman(n):
|
||||||
if n == 0:
|
if n == 0:
|
||||||
return '0'
|
return '0'
|
||||||
|
if n in TarotCard._FIORENTINE_ADDITIVE_NUMERALS:
|
||||||
|
return TarotCard._FIORENTINE_ADDITIVE_NUMERALS[n]
|
||||||
val = [50, 40, 10, 9, 5, 4, 1]
|
val = [50, 40, 10, 9, 5, 4, 1]
|
||||||
syms = ['L','XL','X','IX','V','IV','I']
|
syms = ['L','XL','X','IX','V','IV','I']
|
||||||
result = ''
|
result = ''
|
||||||
@@ -485,6 +502,18 @@ class TarotCard(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def suit_icon(self):
|
def suit_icon(self):
|
||||||
|
if self.arcana == self.MAJOR:
|
||||||
|
# Trump 0 (Fool / Nomad / Matto) + trump 1 (Magician / Schizo /
|
||||||
|
# Bagatto) carry universal symbol overrides — cowboy-hat-side for
|
||||||
|
# the wanderer/fool archetype, wizard-hat for the magus archetype.
|
||||||
|
# Pinned BEFORE the `self.icon` branch so even a deck seed that
|
||||||
|
# supplies a different icon for these two ranks gets normalized
|
||||||
|
# to the convention (Earthman's seed already aligns; Minchiate's
|
||||||
|
# empty icon field used to fall through to fa-hand-dots).
|
||||||
|
if self.number == 0:
|
||||||
|
return 'fa-hat-cowboy-side'
|
||||||
|
if self.number == 1:
|
||||||
|
return 'fa-hat-wizard'
|
||||||
if self.icon:
|
if self.icon:
|
||||||
return self.icon
|
return self.icon
|
||||||
if self.arcana == self.MAJOR:
|
if self.arcana == self.MAJOR:
|
||||||
@@ -493,7 +522,7 @@ class TarotCard(models.Model):
|
|||||||
# card overrides still win via the `self.icon` branch above (the
|
# card overrides still win via the `self.icon` branch above (the
|
||||||
# Earthman seed sets `icon="fa-hand-dots"` explicitly for trumps
|
# Earthman seed sets `icon="fa-hand-dots"` explicitly for trumps
|
||||||
# 2+, which was the only place this fallback used to live; trumps
|
# 2+, which was the only place this fallback used to live; trumps
|
||||||
# 0/1 + every Minchiate trump now pick it up for free).
|
# 2+ Minchiate trumps still pick it up for free here).
|
||||||
return 'fa-hand-dots'
|
return 'fa-hand-dots'
|
||||||
return {
|
return {
|
||||||
self.BRANDS: 'fa-wand-sparkles',
|
self.BRANDS: 'fa-wand-sparkles',
|
||||||
|
|||||||
@@ -48,6 +48,26 @@ class TarotCardCornerRankTest(SimpleTestCase):
|
|||||||
def test_court_king_gives_K(self):
|
def test_court_king_gives_K(self):
|
||||||
self.assertEqual(_card('MIDDLE', 14, 'CROWNS').corner_rank, 'K')
|
self.assertEqual(_card('MIDDLE', 14, 'CROWNS').corner_rank, 'K')
|
||||||
|
|
||||||
|
def test_major_arcana_fiorentine_additive_overrides(self):
|
||||||
|
"""The 6 trumps the Fiorentine deck prints in additive form (NOT a
|
||||||
|
universal pattern shift — e.g. trump 9 still prints IX, trump 14
|
||||||
|
still prints XIV, per the actual deck art). Locked-in list."""
|
||||||
|
self.assertEqual(_card('MAJOR', 4).corner_rank, 'IIII')
|
||||||
|
self.assertEqual(_card('MAJOR', 19).corner_rank, 'XVIIII')
|
||||||
|
self.assertEqual(_card('MAJOR', 24).corner_rank, 'XXIIII')
|
||||||
|
self.assertEqual(_card('MAJOR', 29).corner_rank, 'XXVIIII')
|
||||||
|
self.assertEqual(_card('MAJOR', 34).corner_rank, 'XXXIIII')
|
||||||
|
self.assertEqual(_card('MAJOR', 39).corner_rank, 'XXXVIIII')
|
||||||
|
|
||||||
|
def test_major_arcana_non_overridden_ranks_stay_subtractive(self):
|
||||||
|
"""Trumps NOT in the additive-override list keep standard subtractive
|
||||||
|
Roman numerals. Regression guard against accidental pattern-shift."""
|
||||||
|
self.assertEqual(_card('MAJOR', 9).corner_rank, 'IX')
|
||||||
|
self.assertEqual(_card('MAJOR', 14).corner_rank, 'XIV')
|
||||||
|
self.assertEqual(_card('MAJOR', 40).corner_rank, 'XL')
|
||||||
|
self.assertEqual(_card('MAJOR', 44).corner_rank, 'XLIV')
|
||||||
|
self.assertEqual(_card('MAJOR', 49).corner_rank, 'XLIX')
|
||||||
|
|
||||||
|
|
||||||
class TarotCardSuitIconTest(SimpleTestCase):
|
class TarotCardSuitIconTest(SimpleTestCase):
|
||||||
"""TarotCard.suit_icon — icon class resolution."""
|
"""TarotCard.suit_icon — icon class resolution."""
|
||||||
|
|||||||
@@ -129,22 +129,15 @@ function initGameKitTooltips() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the kit bag dialog is open, replace the matching card with a placeholder.
|
// Kit-bag DOFF refresh used to do a client-side replace-with-placeholder
|
||||||
function _syncKitBagDialog(kind, id) {
|
// here (faster than a re-fetch) but it queried `card.querySelector('i')`
|
||||||
const dialog = document.getElementById('id_kit_bag_dialog');
|
// for the placeholder icon — works for trinket/token cards (single FA
|
||||||
if (!dialog || !dialog.hasAttribute('open')) return;
|
// `<i>`) but BREAKS for image-equipped decks whose card-stack icon is
|
||||||
const selector = kind === 'deck'
|
// an `<svg class="deck-stack-icon">` (no `<i>` to copy → empty placeholder
|
||||||
? `.kit-bag-deck[data-deck-id="${id}"]`
|
// until manual refresh). Symmetric w. DON now: re-fetch the panel HTML
|
||||||
: `.token[data-token-id="${id}"]`;
|
// so the server-rendered `_kit_bag_panel.html` placeholder branch (which
|
||||||
const card = dialog.querySelector(selector);
|
// re-renders `_deck_stack_icon.html` w/o the `deck` arg for the empty
|
||||||
if (!card) return;
|
// fill) is the single source of truth.
|
||||||
const placeholder = document.createElement('div');
|
|
||||||
placeholder.className = 'kit-bag-placeholder';
|
|
||||||
const icon = card.querySelector('i');
|
|
||||||
if (icon) placeholder.innerHTML = icon.outerHTML;
|
|
||||||
card.parentNode.insertBefore(placeholder, card);
|
|
||||||
card.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function wireDonDoff(token) {
|
function wireDonDoff(token) {
|
||||||
const donBtn = portal.querySelector('.btn-equip');
|
const donBtn = portal.querySelector('.btn-equip');
|
||||||
@@ -205,7 +198,7 @@ function initGameKitTooltips() {
|
|||||||
_setEquipState(donBtn, doffBtn, false);
|
_setEquipState(donBtn, doffBtn, false);
|
||||||
_syncTokenButtons('trinket', '');
|
_syncTokenButtons('trinket', '');
|
||||||
buildMiniContent(token);
|
buildMiniContent(token);
|
||||||
_syncKitBagDialog('token', tokenId);
|
_refreshKitDialog();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (deckId) {
|
} else if (deckId) {
|
||||||
@@ -219,7 +212,7 @@ function initGameKitTooltips() {
|
|||||||
_setEquipState(donBtn, doffBtn, false);
|
_setEquipState(donBtn, doffBtn, false);
|
||||||
_syncTokenButtons('deck', '');
|
_syncTokenButtons('deck', '');
|
||||||
buildMiniContent(token);
|
buildMiniContent(token);
|
||||||
_syncKitBagDialog('deck', deckId);
|
_refreshKitDialog();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,8 +442,15 @@ class GameboardViewTest(TestCase):
|
|||||||
def test_game_kit_has_coin_on_a_string(self):
|
def test_game_kit_has_coin_on_a_string(self):
|
||||||
[_] = self.parsed.cssselect("#id_game_kit #id_kit_coin_on_a_string")
|
[_] = self.parsed.cssselect("#id_game_kit #id_kit_coin_on_a_string")
|
||||||
|
|
||||||
def test_game_kit_has_free_token(self):
|
def test_game_kit_has_no_free_token(self):
|
||||||
[_] = self.parsed.cssselect("#id_game_kit #id_kit_free_token")
|
"""Free Tokens were moved off the Game Kit applet 2026-05-25 PM —
|
||||||
|
only equippable items (PASS/BAND/CARTE/COIN trinkets + decks + dice)
|
||||||
|
belong here. Free + Tithe tokens now live in the My Wallet applet
|
||||||
|
on /dashboard/ (`_applet-wallet.html`)."""
|
||||||
|
self.assertEqual(
|
||||||
|
len(self.parsed.cssselect("#id_game_kit #id_kit_free_token")), 0,
|
||||||
|
"Free Token must NOT render in the Game Kit applet — it's not equippable",
|
||||||
|
)
|
||||||
|
|
||||||
def test_game_kit_shows_deck_variant_cards(self):
|
def test_game_kit_shows_deck_variant_cards(self):
|
||||||
decks = self.parsed.cssselect("#id_game_kit .deck-variant")
|
decks = self.parsed.cssselect("#id_game_kit .deck-variant")
|
||||||
@@ -2070,15 +2077,19 @@ class MySeaViewWithPartialHandTest(TestCase):
|
|||||||
self.assertContains(response, 'data-state="auto-draw"')
|
self.assertContains(response, 'data-state="auto-draw"')
|
||||||
self.assertContains(response, "AUTO")
|
self.assertContains(response, "AUTO")
|
||||||
|
|
||||||
def test_partial_hand_del_btn_carries_btn_disabled(self):
|
def test_partial_hand_del_btn_is_enabled(self):
|
||||||
|
"""User spec 2026-05-26: DEL is enabled as soon as ANY card has been
|
||||||
|
drawn (not gated until hand_complete as before). The partial-hand
|
||||||
|
fixture has one saved slot, so DEL should be active w. label 'DEL'."""
|
||||||
import re
|
import re
|
||||||
response = self.client.get(reverse("my_sea"))
|
response = self.client.get(reverse("my_sea"))
|
||||||
html = response.content.decode()
|
html = response.content.decode()
|
||||||
m = re.search(
|
m = re.search(
|
||||||
r'<button[^>]*id="id_sea_del"[^>]*class="([^"]*)"', html,
|
r'<button[^>]*id="id_sea_del"[^>]*class="([^"]*)"[^>]*>([^<]*)<', html,
|
||||||
)
|
)
|
||||||
self.assertIsNotNone(m)
|
self.assertIsNotNone(m)
|
||||||
self.assertIn("btn-disabled", m.group(1))
|
self.assertNotIn("btn-disabled", m.group(1))
|
||||||
|
self.assertEqual(m.group(2).strip(), "DEL")
|
||||||
|
|
||||||
def test_partial_hand_picker_does_NOT_carry_locked_class(self):
|
def test_partial_hand_picker_does_NOT_carry_locked_class(self):
|
||||||
# Hand is mid-progress; locked class only applies on completion.
|
# Hand is mid-progress; locked class only applies on completion.
|
||||||
|
|||||||
@@ -155,7 +155,21 @@ def _game_kit_context(user):
|
|||||||
"carte": carte,
|
"carte": carte,
|
||||||
"free_tokens": free_tokens,
|
"free_tokens": free_tokens,
|
||||||
"tithe_tokens": tithe_tokens,
|
"tithe_tokens": tithe_tokens,
|
||||||
"unlocked_decks": list(user.unlocked_decks.all()),
|
# `free_count` / `tithe_count` mirror the gameboard Game Kit applet's
|
||||||
|
# context — drive the `(×N)` chip in the .tt-title plus the rendered
|
||||||
|
# `.shop-badge` on the icon. Both capped to "99+" at the template
|
||||||
|
# level so a runaway count doesn't blow the badge layout.
|
||||||
|
"free_count": len(free_tokens),
|
||||||
|
"tithe_count": len(tithe_tokens),
|
||||||
|
# `deck_variants` is the annotated list (carries .in_use_room_name
|
||||||
|
# per `_annotate_deck_in_use`); the gk-decks section uses the same
|
||||||
|
# SVG card-stack icon + tt-tooltip pattern as the gameboard's Game
|
||||||
|
# Kit applet so the deck row reads identically on both surfaces.
|
||||||
|
"deck_variants": _annotate_deck_in_use(list(user.unlocked_decks.all()), user),
|
||||||
|
"equipped_deck_id": user.equipped_deck_id,
|
||||||
|
# `equipped_trinket_id` powers the gk-trinkets section's DON/DOFF
|
||||||
|
# buttons + mini-portal Equipped status (parity w. gameboard applet).
|
||||||
|
"equipped_trinket_id": user.equipped_trinket_id,
|
||||||
"applets": applet_context(user, "game-kit"),
|
"applets": applet_context(user, "game-kit"),
|
||||||
"pronoun_options": pronoun_options,
|
"pronoun_options": pronoun_options,
|
||||||
"current_pronouns": user.pronouns,
|
"current_pronouns": user.pronouns,
|
||||||
|
|||||||
@@ -1346,9 +1346,12 @@ html:has(.sig-backdrop) {
|
|||||||
// Sprint A.7.5-polish-4 — same image-mode override as the base rule
|
// Sprint A.7.5-polish-4 — same image-mode override as the base rule
|
||||||
// above. Without this the 0,3,0 levity rule's --secUser bg would
|
// above. Without this the 0,3,0 levity rule's --secUser bg would
|
||||||
// re-clothe the sea-sig-card under levity even in image mode.
|
// re-clothe the sea-sig-card under levity even in image mode.
|
||||||
|
// `overflow: visible` pinned here too so the stroke-contour isn't
|
||||||
|
// clipped under levity polarity (parity w. base above).
|
||||||
&.sig-stage-card--image {
|
&.sig-stage-card--image {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1753,10 +1756,18 @@ $sea-card-h: 6.5rem;
|
|||||||
// the my_sign-applet pattern in `_billboard.scss`. Filter chain on
|
// the my_sign-applet pattern in `_billboard.scss`. Filter chain on
|
||||||
// `.sig-stage-card-img` is still inherited from the shared rule (the
|
// `.sig-stage-card-img` is still inherited from the shared rule (the
|
||||||
// image-mode rule's img descendant selector doesn't lose anywhere).
|
// image-mode rule's img descendant selector doesn't lose anywhere).
|
||||||
|
//
|
||||||
|
// `overflow: visible` re-stated — base `.sig-stage-card.sea-sig-card`
|
||||||
|
// above sets `overflow: hidden` at (0,2,0) which beats the shared
|
||||||
|
// image-mode rule's `overflow: visible` by source order. Without this
|
||||||
|
// the contour-stroke filter chain (drop-shadow ±0.2rem cardinals)
|
||||||
|
// gets clipped to the card's bounding rectangle, painting a uniform
|
||||||
|
// rectangular frame instead of an alpha-following contour stroke.
|
||||||
&.sig-stage-card--image {
|
&.sig-stage-card--image {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fan-card-face {
|
.fan-card-face {
|
||||||
@@ -1988,6 +1999,19 @@ $_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6);
|
|||||||
box-shadow: $_sea-shadow;
|
box-shadow: $_sea-shadow;
|
||||||
overflow: hidden; // clip the back-img to the rounded-rect face
|
overflow: hidden; // clip the back-img to the rounded-rect face
|
||||||
}
|
}
|
||||||
|
// Sprint A.7.5-polish-9 — image-mode override for the single deck stack.
|
||||||
|
// When the back-img child is present (deck has_card_images), drop the
|
||||||
|
// neutral palette bg + gold border so only the actual card-back image
|
||||||
|
// reads. `:has()` selector keys off the conditional `<img.sea-stack-face-
|
||||||
|
// img>` child rendered by the my_sea.html template, no marker class needed.
|
||||||
|
// border-color: transparent (not border: 0) so the 0.15rem solid base
|
||||||
|
// border-width stays — the image's object-fit: cover fills the inside, and
|
||||||
|
// keeping the border-box dimensions identical means hover/active glow
|
||||||
|
// (box-shadow) renders at the same offset as the polarized variants.
|
||||||
|
.sea-deck-stack--single .sea-stack-face:has(.sea-stack-face-img) {
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
.sea-stack-face-img {
|
.sea-stack-face-img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
@@ -82,14 +82,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.kit-bag-label {
|
.kit-bag-label {
|
||||||
font-size: 0.55rem;
|
font-size: 0.6rem;
|
||||||
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.2em;
|
||||||
color: rgba(var(--quaUser), 0.75);
|
color: rgba(var(--secUser), 0.5);
|
||||||
writing-mode: vertical-rl;
|
writing-mode: vertical-rl;
|
||||||
text-orientation: mixed;
|
text-orientation: mixed;
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg) scaleX(1.3);
|
||||||
padding: 0 0.25rem 0 0.5rem;
|
padding: 0 0.25rem 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,9 +188,67 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gk-deck-card,
|
// `.gk-deck-card` + `.gk-trinket-card` + `.gk-token-card` dropped from this
|
||||||
.gk-trinket-card,
|
// multi-class rule — the gk-decks/trinkets/tokens sections all render as
|
||||||
.gk-token-card,
|
// bare `.token` icons (FA glyph or SVG card-stack) sized by their own rules
|
||||||
|
// in `_gameboard.scss`; no border + no padding-shell since the hover-portal
|
||||||
|
// tooltip carries the name/count/description. Pronoun cards still use the
|
||||||
|
// bordered card-shell shape via this rule.
|
||||||
|
//
|
||||||
|
// Icon-row treatment for Trinkets / Tokens / Card Decks — the small icons
|
||||||
|
// look lonely huddled on the left edge of the wider applet rectangle, so
|
||||||
|
// center them along the main axis + bump the gap. Scoped to the 3 specific
|
||||||
|
// section IDs so it doesn't bleed to Pronouns (whose cards fill the row +
|
||||||
|
// look fine left-anchored).
|
||||||
|
#id_gk_trinkets .gk-items,
|
||||||
|
#id_gk_tokens .gk-items,
|
||||||
|
#id_gk_decks .gk-items {
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `#id_game_kit` wraps every section in `_game_kit_sections.html` so
|
||||||
|
// `gameboard.js`'s `getElementById('id_game_kit')` finds ONE scope covering
|
||||||
|
// trinkets + tokens + decks. But that wrapper sits between `#id_gk_sections
|
||||||
|
// _container` (the CSS-grid parent — see `_applets.scss:268`) and its
|
||||||
|
// `<section>` children, breaking the `grid-column: span var(--applet-cols)`
|
||||||
|
// layout that needs the sections as DIRECT children. `display: contents`
|
||||||
|
// makes the wrapper transparent to layout — sections still resolve as grid
|
||||||
|
// children of `#id_gk_sections_container` — while keeping the wrapper a
|
||||||
|
// real DOM element JS can query + read data-attrs off.
|
||||||
|
#id_gk_sections_container > #id_game_kit { display: contents; }
|
||||||
|
|
||||||
|
// My Wallet applet (`_applet-wallet.html` on /dashboard/) uses the same
|
||||||
|
// `#id_game_kit` wrapper so `gameboard.js` picks up its `.token` icons +
|
||||||
|
// fires the hover-portal tooltips. Lay items out as a centered flex row
|
||||||
|
// (parity w. the standalone game_kit.html sections — see `#id_gk_trinkets
|
||||||
|
// .gk-items, #id_gk_tokens .gk-items, #id_gk_decks .gk-items` above).
|
||||||
|
// `height: 100%` keeps the row vertically centered inside the applet box
|
||||||
|
// since the section's body has the h2 absolute-positioned + claims full
|
||||||
|
// box height.
|
||||||
|
#id_applet_wallet > #id_game_kit.wallet-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2rem;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `.token` is a flex child of `.gk-items` — make it a positioning context
|
||||||
|
// so the `.shop-badge` (position: absolute, top: -0.8rem, right: -1.2rem)
|
||||||
|
// anchors off the icon, NOT off some grandparent. Also pin `font-size:
|
||||||
|
// 1.5rem` so the standalone game_kit.html icons match the gameboard's
|
||||||
|
// Game Kit applet sizing — `_gameboard.scss:79` sets this for `#id_applet
|
||||||
|
// _game_kit #id_game_kit .token` (only fires on /gameboard/ since `#id_
|
||||||
|
// applet_game_kit` doesn't exist on this page); we re-state at the same
|
||||||
|
// 1.5rem so the visual weight is identical across the two surfaces.
|
||||||
|
#id_game_kit .token {
|
||||||
|
position: relative;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.gk-pronoun-card {
|
.gk-pronoun-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -434,7 +434,10 @@ body.page-gameboard {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
// z-index 0 (was 2) — labels sit BEHIND the slot so the slot's
|
||||||
|
// downward shadow can visually obscure the label's top edge per the
|
||||||
|
// .sea-stack-name "tuck under" treatment (user spec 2026-05-26).
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cells need `position: relative` so absolute label children anchor
|
// Cells need `position: relative` so absolute label children anchor
|
||||||
@@ -443,22 +446,102 @@ body.page-gameboard {
|
|||||||
// cells need it added.
|
// cells need it added.
|
||||||
.my-sea-cross .sea-crucifix-cell { position: relative; }
|
.my-sea-cross .sea-crucifix-cell { position: relative; }
|
||||||
|
|
||||||
// Above top border — overlaps slot's top edge by 0.1rem (per the
|
// CROWN + COVER labels — ABOVE the slot per user spec 2026-05-26.
|
||||||
// `.sea-stack-name` "tuck under" treatment in _card-deck.scss:1564).
|
// Cross convention: crown is at the top + cover is the central card
|
||||||
|
// being covered, so their labels belong above. `bottom: 100%` anchors
|
||||||
|
// label's bottom edge to slot's top; `-0.4rem` translate-Y pushes it
|
||||||
|
// further away so the labels read clearly w. breathing room.
|
||||||
.sea-pos-crown > .sea-pos-label,
|
.sea-pos-crown > .sea-pos-label,
|
||||||
.sea-pos-cover > .sea-pos-label {
|
.sea-pos-cover > .sea-pos-label {
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, 0.1rem) scaleY(1.2);
|
transform: translate(-50%, -0.4rem) scaleY(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// LAY + CROSS labels — BELOW the slot. `0.3rem` translate-Y pushes each
|
||||||
|
// label a bit further from its slot's bottom edge than the prior tuck-
|
||||||
|
// under (which crowded the label up against the slot's border). The
|
||||||
|
// filled-card shadow (added below to `.sea-card-slot--filled`, NOT the
|
||||||
|
// empty slot per user clarification 2026-05-26) extends 0.25rem downward
|
||||||
|
// via the `$_sea-shadow` chain — covers the label's top edge w/o the
|
||||||
|
// label having to physically overlap the card's border-box.
|
||||||
|
.sea-pos-lay > .sea-pos-label,
|
||||||
|
.sea-pos-cross > .sea-pos-label {
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0.3rem) scaleY(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breathing room around COVER + CROSS labels — bump CROWN cell UP by
|
||||||
|
// 0.5rem + LAY cell DOWN by 0.5rem so the COVER label (below the central
|
||||||
|
// sig card) + CROSS label (also in the central row) have vertical space
|
||||||
|
// w/o colliding into the crown's slot from above or the lay's slot from
|
||||||
|
// below. Translate (not margin) so the surrounding grid layout doesn't
|
||||||
|
// reflow — cells stay in their grid-areas but visually shift.
|
||||||
|
.my-sea-cross .sea-pos-crown { transform: translateY(-0.5rem); }
|
||||||
|
.my-sea-cross .sea-pos-lay { transform: translateY( 0.5rem); }
|
||||||
|
|
||||||
|
// Filled-card downward shadow — only on the my-sea Cross page (NOT the
|
||||||
|
// picker or other surfaces using `.sea-card-slot`), and only the FILLED
|
||||||
|
// variant (the dashed empty slot stays shadowless per user clarification
|
||||||
|
// 2026-05-26: "the slots themselves should not have box-shadows ... only
|
||||||
|
// the cards that replace them should"). Mirrors `.sea-stack-face`'s
|
||||||
|
// `$_sea-shadow` chain in `_card-deck.scss:1976` (the GRAVITY/LEVITY deck
|
||||||
|
// stacks): solid-black 1px×2px offset shadow + a softer 4px-down spread +
|
||||||
|
// a 2px×5px blurred falloff. The image-mode slot still keeps its filter-
|
||||||
|
// chain contour-stroke + 1px depth shadow from `_card-deck.scss:837` — the
|
||||||
|
// box-shadow layers cleanly over it for a richer depth read.
|
||||||
|
.my-sea-cross .sea-card-slot--filled {
|
||||||
|
box-shadow:
|
||||||
|
1px 2px 0 rgba(0, 0, 0, 0.7),
|
||||||
|
0 4px 0 rgba(0, 0, 0, 0.18),
|
||||||
|
2px 5px 5px rgba(0, 0, 0, 0.5);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotated-card shadow corrections — `box-shadow` rotates w. the element's
|
||||||
|
// `transform`, so any rotated card's down-right shadow rotates to a wrong
|
||||||
|
// direction. Each rotation case needs its offsets pre-inverted so the
|
||||||
|
// post-rotation render still reads down-right.
|
||||||
|
//
|
||||||
|
// Derivation: CSS rotate(θ) CW maps offset (a, b) → screen (a·cos θ −
|
||||||
|
// b·sin θ, a·sin θ + b·cos θ). To get screen (1, 2) after rotation, solve
|
||||||
|
// for unrotated (a, b). Below shows the result for each rotation in play:
|
||||||
|
// • 180° (reversed, `.sea-card-slot--reversed` at _card-deck:1616):
|
||||||
|
// (1, 2) → (−1, −2). Flip all signs.
|
||||||
|
// • 90° (cross, `.sea-pos-cross .sea-card-slot` at _card-deck:1705):
|
||||||
|
// (1, 2) → (2, −1). Swap + negate y.
|
||||||
|
// • 270° (cross + reversed at _card-deck:1620): (1, 2) → (−2, 1).
|
||||||
|
//
|
||||||
|
// Specificity ladder: base filled (0,2,0) < reversed-only (0,3,0) = cross-
|
||||||
|
// only (0,3,0) < cross+reversed (0,4,0). For a card matching multiple, the
|
||||||
|
// most-specific rule wins; cross-only + reversed-only tie at (0,3,0) but
|
||||||
|
// only one matches per card (cross OR non-cross), so no source-order trap.
|
||||||
|
.my-sea-cross .sea-card-slot--filled.sea-card-slot--reversed {
|
||||||
|
box-shadow:
|
||||||
|
-1px -2px 0 rgba(0, 0, 0, 0.7),
|
||||||
|
0 -4px 0 rgba(0, 0, 0, 0.18),
|
||||||
|
-2px -5px 5px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.my-sea-cross .sea-pos-cross .sea-card-slot--filled {
|
||||||
|
box-shadow:
|
||||||
|
2px -1px 0 rgba(0, 0, 0, 0.7),
|
||||||
|
4px 0 0 rgba(0, 0, 0, 0.18),
|
||||||
|
5px -2px 5px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.my-sea-cross .sea-pos-cross .sea-card-slot--filled.sea-card-slot--reversed {
|
||||||
|
box-shadow:
|
||||||
|
-2px 1px 0 rgba(0, 0, 0, 0.7),
|
||||||
|
-4px 0 0 rgba(0, 0, 0, 0.18),
|
||||||
|
-5px 2px 5px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cover + cross labels dim w. their slots — they sit on top of the
|
// 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
|
// 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
|
// 0.5 opacity matches the slot's faint dotted-outline at idle; the
|
||||||
// parent's :hover state (propagated up when the inside `.sea-card-
|
// parent's :hover state (propagated up when the inside `.sea-card-
|
||||||
// slot:hover` fires per CSS hover-ancestor rules) boosts to the
|
// slot:hover` fires per CSS hover-ancestor rules) boosts to full.
|
||||||
// `.sea-pos-label` baseline 0.6, matching the slot's `--duoUser` mask
|
|
||||||
// reveal.
|
|
||||||
.sea-pos-cover > .sea-pos-label,
|
.sea-pos-cover > .sea-pos-label,
|
||||||
.sea-pos-cross > .sea-pos-label {
|
.sea-pos-cross > .sea-pos-label {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@@ -470,14 +553,6 @@ body.page-gameboard {
|
|||||||
opacity: 1;
|
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.
|
// Left of left border, rotated 90° CCW — text reads bottom-to-top.
|
||||||
// `writing-mode: vertical-rl` puts text top-to-bottom (CW); a 180°
|
// `writing-mode: vertical-rl` puts text top-to-bottom (CW); a 180°
|
||||||
// rotation flips it to read bottom-to-top (CCW), satisfying the user-
|
// rotation flips it to read bottom-to-top (CCW), satisfying the user-
|
||||||
@@ -491,7 +566,11 @@ body.page-gameboard {
|
|||||||
right: 100%;
|
right: 100%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
writing-mode: vertical-rl;
|
writing-mode: vertical-rl;
|
||||||
transform: translate(0.1rem, -50%) rotate(180deg) scaleX(1.2);
|
// User spec 2026-05-26: parity w. CROWN/LAY's ~0.4rem breathing room.
|
||||||
|
// Was `0.1rem` overlap (tuck-under); now `-0.4rem` pulls the label
|
||||||
|
// further LEFT, AWAY from the slot's left edge so it reads cleanly w/o
|
||||||
|
// colliding into the slot's border-radius zone.
|
||||||
|
transform: translate(-0.3rem, -50%) rotate(180deg) scaleX(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right of right border, rotated 90° CW — text reads top-to-bottom.
|
// Right of right border, rotated 90° CW — text reads top-to-bottom.
|
||||||
@@ -500,7 +579,8 @@ body.page-gameboard {
|
|||||||
left: 100%;
|
left: 100%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
writing-mode: vertical-rl;
|
writing-mode: vertical-rl;
|
||||||
transform: translate(-0.1rem, -50%) scaleX(1.2);
|
// User spec 2026-05-26: same `0.4rem` AWAY distance as LEAVE (mirrored).
|
||||||
|
transform: translate(0.3rem, -50%) scaleX(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section dividers inside the SPREAD combobox — labels "3-card spreads"
|
// Section dividers inside the SPREAD combobox — labels "3-card spreads"
|
||||||
@@ -808,10 +888,19 @@ body.page-gameboard {
|
|||||||
// PNG card-back is unobstructed. Filter-chain / contour-stroke / depth
|
// PNG card-back is unobstructed. Filter-chain / contour-stroke / depth
|
||||||
// shadow on `.sig-stage-card-img` still come from the shared rule (no
|
// shadow on `.sig-stage-card-img` still come from the shared rule (no
|
||||||
// collision — different selector target).
|
// collision — different selector target).
|
||||||
|
//
|
||||||
|
// `overflow: visible` is critical — the base `.my-sea-slot` rule above
|
||||||
|
// sets `overflow: hidden` at (1,1,0) which BEATS the shared image-mode
|
||||||
|
// rule's `overflow: visible` at (0,2,0) on the ID axis. Without re-
|
||||||
|
// stating here, the fourfold contour-stroke drop-shadows get clipped
|
||||||
|
// by the slot bounding box → reads as a uniform rectangular frame
|
||||||
|
// around the image, defeating the "stroke follows alpha contour"
|
||||||
|
// illusion. See [[feedback-scss-id-context-specificity-trap]].
|
||||||
.my-sea-slot--filled.my-sea-slot--image {
|
.my-sea-slot--filled.my-sea-slot--image {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty slot — matches the my_sea.html picker's empty `.sea-card-
|
// Empty slot — matches the my_sea.html picker's empty `.sea-card-
|
||||||
@@ -829,16 +918,21 @@ body.page-gameboard {
|
|||||||
border-width: 0.15rem !important;
|
border-width: 0.15rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label — sibling of the slot inside the wrap, sits BELOW the slot
|
// Label — sibling of the slot inside the wrap, sits BELOW the slot.
|
||||||
// box (mirrors the my_sea.html picker's `.sea-pos-label` placement).
|
// User spec 2026-05-26: parity w. my_sea.html's `.sea-pos-lay` label
|
||||||
// `margin-top: -0.15rem` crosses the slot's bottom border so the
|
// treatment — label tucked just below the slot w. the filled card's
|
||||||
// label's top edge sits flush against it; `position: relative;
|
// downward shadow (added below to `.my-sea-slot--filled`) obscuring
|
||||||
// z-index: 2` keeps the label text rendering ATOP the slot's bottom
|
// the label's top edge. Empty dashed slots stay shadowless so the
|
||||||
// border (dotted for empty slots, solid for filled).
|
// label sits cleanly below them.
|
||||||
.my-sea-slot-label {
|
.my-sea-slot-label {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
// z-index 0 (was 2) — slot's shadow lives on top + casts over the
|
||||||
margin-top: -0.05rem;
|
// label's top edge; the label is behind the slot in stacking order.
|
||||||
|
z-index: 0;
|
||||||
|
// Small gap (was `-0.05rem` flush margin) so the label doesn't
|
||||||
|
// glue to the slot's bottom border on the empty-slot case where
|
||||||
|
// there's no shadow to bridge the visual gap.
|
||||||
|
margin-top: 0.15rem;
|
||||||
padding: 0 0.2rem;
|
padding: 0 0.2rem;
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -852,6 +946,32 @@ body.page-gameboard {
|
|||||||
transform: scaleY(1.3);
|
transform: scaleY(1.3);
|
||||||
transform-origin: top center;
|
transform-origin: top center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filled-card downward shadow — applet parity w. my_sea.html's
|
||||||
|
// `.my-sea-cross .sea-card-slot--filled` rule. Empty dashed slots
|
||||||
|
// stay shadowless (per user clarification 2026-05-26: only the cards
|
||||||
|
// that replace the slots should carry a shadow).
|
||||||
|
.my-sea-slot--filled {
|
||||||
|
box-shadow:
|
||||||
|
1px 2px 0 rgba(0, 0, 0, 0.7),
|
||||||
|
0 4px 0 rgba(0, 0, 0, 0.18),
|
||||||
|
2px 5px 5px rgba(0, 0, 0, 0.5);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reversed-card shadow inversion — parity w. my_sea.html's
|
||||||
|
// `.sea-card-slot--reversed` override below. The applet's `.my-sea-
|
||||||
|
// slot--reversed` (line 843) also adds `rotate(180deg)` which flips the
|
||||||
|
// shadow up-left; invert ALL offsets so post-rotation it reads down-
|
||||||
|
// right. See [[feedback-css-transform-rotates-box-shadow]] (or similar
|
||||||
|
// future-note) for the gotcha.
|
||||||
|
.my-sea-slot--filled.my-sea-slot--reversed {
|
||||||
|
box-shadow:
|
||||||
|
-1px -2px 0 rgba(0, 0, 0, 0.7),
|
||||||
|
0 -4px 0 rgba(0, 0, 0, 0.18),
|
||||||
|
-2px -5px 5px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
// `.my-sea-slot-label--empty` intentionally has NO per-state recolor
|
// `.my-sea-slot-label--empty` intentionally has NO per-state recolor
|
||||||
// — the empty-state label keeps the same `--secUser` ink as the
|
// — the empty-state label keeps the same `--secUser` ink as the
|
||||||
// filled-slot label per user spec 2026-05-22 (pins position identity
|
// filled-slot label per user spec 2026-05-22 (pins position identity
|
||||||
|
|||||||
@@ -503,9 +503,23 @@ html.sky-open .sky-modal-wrap {
|
|||||||
.nw-sign-group,
|
.nw-sign-group,
|
||||||
.nw-house-group { cursor: pointer; }
|
.nw-house-group { cursor: pointer; }
|
||||||
|
|
||||||
|
// Planets get a base 1px×1px black drop-shadow so the colored circle
|
||||||
|
// reads as a depthed badge floating above the wheel rings — user spec
|
||||||
|
// 2026-05-25 PM. Chained w. the hover/active glow below (CSS `filter`
|
||||||
|
// replaces rather than appends, so the chain has to be re-stated on the
|
||||||
|
// hover rule to keep the shadow when the glow kicks in).
|
||||||
|
.nw-planet-group {
|
||||||
|
filter: drop-shadow(1px 1px 0 rgba(0, 0, 0, 0.7));
|
||||||
|
}
|
||||||
|
|
||||||
.nw-planet-group:hover,
|
.nw-planet-group:hover,
|
||||||
.nw-planet-group.nw-planet--active,
|
.nw-planet-group.nw-planet--active,
|
||||||
.nw-planet-group.nw-planet--asp-active,
|
.nw-planet-group.nw-planet--asp-active {
|
||||||
|
filter:
|
||||||
|
drop-shadow(1px 1px 0 rgba(0, 0, 0, 0.7))
|
||||||
|
drop-shadow(0 0 5px rgba(var(--priLm), 1));
|
||||||
|
}
|
||||||
|
|
||||||
.nw-element-group:hover,
|
.nw-element-group:hover,
|
||||||
.nw-element-group.nw-element--active,
|
.nw-element-group.nw-element--active,
|
||||||
.nw-sign-group:hover,
|
.nw-sign-group:hover,
|
||||||
|
|||||||
@@ -3,5 +3,111 @@
|
|||||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||||
>
|
>
|
||||||
<h2><a href="{% url "wallet" %}" class="wallet-manage-link">My Wallet</a></h2>
|
<h2><a href="{% url "wallet" %}" class="wallet-manage-link">My Wallet</a></h2>
|
||||||
<span><i class="fa-solid fa-ticket"></i>: {{ user.wallet.writs }}</span>
|
{% comment %}
|
||||||
|
`id="id_game_kit"` lets `gameboard.js`'s `initGameKitTooltips()` scope its
|
||||||
|
`.token` query here. `display: contents` (in `_game-kit.scss`) keeps this
|
||||||
|
wrapper layout-invisible so the section's own grid still applies. Trinkets
|
||||||
|
(PASS/BAND/CARTE/COIN) COPY the Game Kit applet's tooltip + DON/DOFF
|
||||||
|
wiring so the user can equip from either surface; Free + Tithe tokens MOVE
|
||||||
|
here from Game Kit (they aren't equippable). Writs uses the same `.token`
|
||||||
|
+ `.tt` chrome so all four item types read as one consistent inventory row.
|
||||||
|
{% endcomment %}
|
||||||
|
<div id="id_game_kit" class="wallet-items" data-equipped-id="{{ equipped_trinket_id|default:'' }}" data-equipped-deck-id="" data-in-use-deck-ids="">
|
||||||
|
{% if pass_token %}
|
||||||
|
<div id="id_wallet_pass" class="token" data-token-id="{{ pass_token.pk }}">
|
||||||
|
<i class="fa-solid fa-clipboard"></i>
|
||||||
|
<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 id="id_wallet_wristband" class="token" data-token-id="{{ band.pk }}">
|
||||||
|
<i class="fa-solid fa-ring"></i>
|
||||||
|
<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 id="id_wallet_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>
|
||||||
|
<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 id="id_wallet_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>
|
||||||
|
<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 free_tokens %}
|
||||||
|
{% with free_tokens.0 as token %}
|
||||||
|
<div id="id_wallet_free_token" class="token">
|
||||||
|
<i class="fa-solid fa-coins"></i>
|
||||||
|
{% 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>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
{% if tithe_tokens %}
|
||||||
|
{% with tithe_tokens.0 as token %}
|
||||||
|
<div id="id_wallet_tithe_token" class="token">
|
||||||
|
<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>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
{# Writs placeholder — currency tier (NOT a Token model row). Tooltip #}
|
||||||
|
{# mirrors the Token-style chrome so the row reads as one inventory. #}
|
||||||
|
{# Count lives in the badge (Writs count > 0); empty case still renders #}
|
||||||
|
{# the icon since Writs are the base balance unit. #}
|
||||||
|
<div id="id_wallet_writs" class="token">
|
||||||
|
<i class="fa-solid fa-ticket"></i>
|
||||||
|
{% if user.wallet.writs > 0 %}<span class="shop-badge">{% if user.wallet.writs > 99 %}99+{% else %}{{ user.wallet.writs }}{% endif %}</span>{% endif %}
|
||||||
|
<div class="tt">
|
||||||
|
<h4 class="tt-title">Writs{% if user.wallet.writs > 0 %} <span class="token-count">(×{% if user.wallet.writs > 99 %}99+{% else %}{{ user.wallet.writs }}{% endif %})</span>{% endif %}</h4>
|
||||||
|
<p class="tt-description">Base currency unit</p>
|
||||||
|
<p class="tt-shoptalk"><em>Earned at the gate; spent in the shop.</em></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
<script src="{% static "apps/dashboard/dashboard.js" %}"></script>
|
<script src="{% static "apps/dashboard/dashboard.js" %}"></script>
|
||||||
<script src="{% static "apps/dashboard/note.js" %}"></script>
|
<script src="{% static "apps/dashboard/note.js" %}"></script>
|
||||||
|
{# `gameboard.js` powers the hover-portal tooltip system for `.token` icons #}
|
||||||
|
{# inside `#id_game_kit`. The My Wallet applet uses this scaffold so its #}
|
||||||
|
{# trinkets + tokens + writs read with the same chrome as the gameboard's #}
|
||||||
|
{# Game Kit applet. `initGameKitTooltips()` no-ops if `#id_game_kit` isn't #}
|
||||||
|
{# present (e.g. the My Wallet applet hidden via toggle). #}
|
||||||
|
<script src="{% static "apps/gameboard/gameboard.js" %}"></script>
|
||||||
<script>
|
<script>
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
// #id_text — new-post applet on billboard.html;
|
// #id_text — new-post applet on billboard.html;
|
||||||
|
|||||||
@@ -68,21 +68,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if free_tokens %}
|
{# Free Tokens REMOVED from Game Kit applet — they're not equippable. #}
|
||||||
{% with free_tokens.0 as token %}
|
{# Now displayed in the My Wallet applet (`_applet-wallet.html` on #}
|
||||||
<div id="id_kit_free_token" class="token">
|
{# /dashboard/) alongside Tithe Tokens + Writs (user spec 2026-05-25 #}
|
||||||
<i class="fa-solid fa-coins"></i>
|
{# PM). Only equippables (PASS/BAND/CARTE/COIN trinkets, decks, dice) #}
|
||||||
<div class="tt">
|
{# belong in Game Kit since this is the equip surface. #}
|
||||||
<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 %}
|
|
||||||
{% for deck in deck_variants %}
|
{% 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:'' }}">
|
<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. #}
|
{# Sprint A.4 — card-deck stack icon replaces the old fa-id-badge. #}
|
||||||
|
|||||||
@@ -1,31 +1,70 @@
|
|||||||
<div id="id_gk_sections_container">
|
<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 %}
|
{% for entry in applets %}
|
||||||
{% if entry.applet.slug == 'gk-trinkets' and entry.visible %}
|
{% 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 }};">
|
<section id="id_gk_trinkets" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">
|
||||||
<h2>Trinkets</h2>
|
<h2>Trinkets</h2>
|
||||||
<div class="gk-items">
|
<div class="gk-items">
|
||||||
{% if pass_token %}
|
{% 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>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if band %}
|
{% 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>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if carte %}
|
{% 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>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if coin %}
|
{% 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>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not pass_token and not band and not carte and not coin %}
|
{% 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 %}
|
{% 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 }};">
|
<section id="id_gk_tokens" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">
|
||||||
<h2>Tokens</h2>
|
<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">
|
<div class="gk-items">
|
||||||
{% for token in free_tokens %}
|
{% if free_tokens %}
|
||||||
<div class="gk-token-card" data-token-id="{{ token.pk }}">
|
{% with free_tokens.0 as token %}
|
||||||
|
<div id="id_kit_free_token" class="token">
|
||||||
<i class="fa-solid fa-coins"></i>
|
<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>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% 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>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
{% if not free_tokens and not tithe_tokens %}
|
{% if not free_tokens and not tithe_tokens %}
|
||||||
<p class="gk-empty"><em>No tokens yet.</em></p>
|
<p class="gk-empty"><em>No tokens yet.</em></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -62,11 +128,18 @@
|
|||||||
<section id="id_gk_decks" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">
|
<section id="id_gk_decks" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">
|
||||||
<h2>Card Decks</h2>
|
<h2>Card Decks</h2>
|
||||||
<div class="gk-items">
|
<div class="gk-items">
|
||||||
{% for deck in unlocked_decks %}
|
{% for deck in deck_variants %}
|
||||||
<div class="gk-deck-card" data-deck-id="{{ deck.pk }}">
|
<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:'' }}">
|
||||||
<i class="fa-regular fa-id-badge"></i>
|
{% include "apps/gameboard/_partials/_deck_stack_icon.html" %}
|
||||||
<span>{{ deck.name }}</span>
|
<div class="tt">
|
||||||
<small>{{ deck.card_count }} cards</small>
|
<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>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<p class="gk-empty"><em>No decks unlocked.</em></p>
|
<p class="gk-empty"><em>No decks unlocked.</em></p>
|
||||||
@@ -97,4 +170,5 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
{# committing" affordance; NVM nav-backs to /gameboard/ mirroring the #}
|
{# committing" affordance; NVM nav-backs to /gameboard/ mirroring the #}
|
||||||
{# room's gear-menu convention. Rendered unconditionally (no active- #}
|
{# room's gear-menu convention. Rendered unconditionally (no active- #}
|
||||||
{# draw guard) so fresh users + post-DEL states still see it. #}
|
{# 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">
|
<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>
|
</div>
|
||||||
{% include "apps/applets/_partials/_gear.html" with menu_id="id_my_sea_menu" %}
|
{% include "apps/applets/_partials/_gear.html" with menu_id="id_my_sea_menu" %}
|
||||||
|
|||||||
@@ -12,6 +12,15 @@
|
|||||||
{% include "apps/gameboard/_partials/_game_kit_sections.html" %}
|
{% include "apps/gameboard/_partials/_game_kit_sections.html" %}
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<dialog id="id_tarot_fan_dialog">
|
||||||
<div class="tarot-fan-wrap">
|
<div class="tarot-fan-wrap">
|
||||||
@@ -41,5 +50,10 @@
|
|||||||
{# Brief.showBanner — needed for the Baltimorean Note-unlock banner the #}
|
{# Brief.showBanner — needed for the Baltimorean Note-unlock banner the #}
|
||||||
{# pronouns applet fires on first `bawlmorese` selection. #}
|
{# pronouns applet fires on first `bawlmorese` selection. #}
|
||||||
<script src="{% static 'apps/dashboard/note.js' %}"></script>
|
<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>
|
<script src="{% static 'apps/gameboard/game-kit.js' %}"></script>
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
|||||||
@@ -221,18 +221,22 @@
|
|||||||
{# Critical: this collapse is my_sea-ONLY — room.html #}
|
{# Critical: this collapse is my_sea-ONLY — room.html #}
|
||||||
{# keeps the dual layout since multiple gamers contribute #}
|
{# keeps the dual layout since multiple gamers contribute #}
|
||||||
{# (each might bring a different polarization). #}
|
{# (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 %}
|
{% if request.user.equipped_deck.is_polarized %}
|
||||||
<div class="sea-stacks">
|
<div class="sea-stacks">
|
||||||
<span class="sea-stacks-label">DECKS</span>
|
<span class="sea-stacks-label">DECKS</span>
|
||||||
<div class="sea-deck-stack sea-deck-stack--gravity">
|
<div class="sea-deck-stack sea-deck-stack--gravity">
|
||||||
<div class="sea-stack-face">
|
<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 %}×{% else %}FLIP{% endif %}</button>
|
||||||
</div>
|
</div>
|
||||||
<span class="sea-stack-name">Gravity</span>
|
<span class="sea-stack-name">Gravity</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sea-deck-stack sea-deck-stack--levity">
|
<div class="sea-deck-stack sea-deck-stack--levity">
|
||||||
<div class="sea-stack-face">
|
<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 %}×{% else %}FLIP{% endif %}</button>
|
||||||
</div>
|
</div>
|
||||||
<span class="sea-stack-name">Levity</span>
|
<span class="sea-stack-name">Levity</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -245,7 +249,7 @@
|
|||||||
{% if request.user.equipped_deck.has_card_images %}
|
{% if request.user.equipped_deck.has_card_images %}
|
||||||
<img class="sea-stack-face-img" src="{{ request.user.equipped_deck.back_image_url }}" alt="">
|
<img class="sea-stack-face-img" src="{{ request.user.equipped_deck.back_image_url }}" alt="">
|
||||||
{% endif %}
|
{% 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 %}×{% else %}FLIP{% endif %}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -266,18 +270,16 @@
|
|||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
data-state="{% if hand_complete %}gate-view{% else %}auto-draw{% endif %}"
|
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>
|
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 #}
|
{# DEL gates on whether ANY card has been drawn (user #}
|
||||||
{# complete — per Sprint-5-iter-4c spec, the 1/day #}
|
{# spec 2026-05-26): pre-first-draw it's disabled; as #}
|
||||||
{# quota is committed at first-card-draw + can't be #}
|
{# soon as `saved_by_position` has at least one entry #}
|
||||||
{# refunded by an early DEL. Case-by-case `×` #}
|
{# the user can DEL the in-progress hand. `×` #}
|
||||||
{# rendering matches game-kit tooltip + DON/DOFF #}
|
{# is the disabled-state label (parity w. game-kit's #}
|
||||||
{# convention (user-spec 2026-05-20): a disabled btn #}
|
{# DON/DOFF disabled convention). JS `_setHasDrawn` #}
|
||||||
{# carries × in its own text node, not via a CSS #}
|
{# swaps text + class in lockstep w. live deposits. #}
|
||||||
{# overlay. JS `_setComplete` swaps inner text in #}
|
|
||||||
{# lockstep with the `.btn-disabled` class toggle. #}
|
|
||||||
<button type="button"
|
<button type="button"
|
||||||
id="id_sea_del"
|
id="id_sea_del"
|
||||||
class="btn btn-danger{% if not hand_complete %} btn-disabled{% endif %}">{% if hand_complete %}DEL{% else %}×{% endif %}</button>
|
class="btn btn-danger{% if not saved_by_position %} btn-disabled{% endif %}">{% if saved_by_position %}DEL{% else %}×{% endif %}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{# Sea stage — portaled modal that opens on FLIP click via #}
|
{# Sea stage — portaled modal that opens on FLIP click via #}
|
||||||
@@ -464,12 +466,14 @@
|
|||||||
|
|
||||||
function _resetHand() {
|
function _resetHand() {
|
||||||
// Spread-switch reset only (mid-draw guard inside sync()).
|
// Spread-switch reset only (mid-draw guard inside sync()).
|
||||||
// Iter 4c removed the explicit DEL-resets-hand pathway:
|
// Post-DEL routes through the guard portal → server-side
|
||||||
// pre-completion DEL is `.btn-disabled`; post-completion
|
// delete + page reload, so this path doesn't fire there.
|
||||||
// DEL routes to the guard portal → server-side delete.
|
// Re-disable DEL + re-enable FLIP for the fresh-empty state.
|
||||||
_filled = 0;
|
_filled = 0;
|
||||||
_hideOk();
|
_hideOk();
|
||||||
_unlockSpread();
|
_unlockSpread();
|
||||||
|
_setHasDrawn(false);
|
||||||
|
_setComplete(false);
|
||||||
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, ' +
|
||||||
@@ -484,24 +488,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _setComplete(on) {
|
function _setComplete(on) {
|
||||||
// Iter 4c — single-call state transition for "hand
|
// State transition for "hand complete": deck-stack FLIP
|
||||||
// complete": DEL un-disables, action btn becomes GATE
|
// buttons render as disabled (visual signal that no more
|
||||||
// VIEW, FLIP buttons on the deck stacks render as
|
// draws remain), action btn becomes GATE VIEW. The deck
|
||||||
// disabled when shown. The deck STACKS themselves stay
|
// STACKS themselves stay click-responsive so the user
|
||||||
// click-responsive (so the user can see the disabled
|
// sees the disabled-FLIP feedback — `_locked` gates the
|
||||||
// FLIP feedback) — `_locked` gates the actual draw.
|
// actual draw at the click-handler level. DEL state was
|
||||||
//
|
// here previously but is now driven by `_setHasDrawn`
|
||||||
// DEL btn text follows the game-kit tooltip convention
|
// instead (user spec 2026-05-26: DEL unlocks at first
|
||||||
// (user-spec 2026-05-20): disabled state reads × (the
|
// draw, NOT at completion).
|
||||||
// template's initial render does the same), active
|
|
||||||
// state reads DEL. Lockstep w. the `.btn-disabled`
|
|
||||||
// class so the visual + label always agree.
|
|
||||||
_locked = on;
|
_locked = on;
|
||||||
picker.classList.toggle('my-sea-picker--locked', on);
|
picker.classList.toggle('my-sea-picker--locked', on);
|
||||||
if (delBtn) {
|
picker.querySelectorAll('.sea-deck-stack .sea-stack-ok').forEach(function (btn) {
|
||||||
delBtn.classList.toggle('btn-disabled', !on);
|
btn.classList.toggle('btn-disabled', on);
|
||||||
delBtn.innerHTML = on ? 'DEL' : '×';
|
// 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) {
|
if (actionBtn) {
|
||||||
actionBtn.dataset.state = on ? 'gate-view' : 'auto-draw';
|
actionBtn.dataset.state = on ? 'gate-view' : 'auto-draw';
|
||||||
actionBtn.innerHTML = on ? 'GATE<br>VIEW' : 'AUTO<br>DRAW';
|
actionBtn.innerHTML = on ? 'GATE<br>VIEW' : 'AUTO<br>DRAW';
|
||||||
@@ -509,6 +513,19 @@
|
|||||||
_hideOk();
|
_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 ─
|
// ── Deck-stack click → show FLIP → click FLIP → deposit ─
|
||||||
// Iter 4c — stacks remain click-responsive even after hand
|
// Iter 4c — stacks remain click-responsive even after hand
|
||||||
// is complete (so the user sees the disabled-FLIP feedback,
|
// is complete (so the user sees the disabled-FLIP feedback,
|
||||||
@@ -539,7 +556,10 @@
|
|||||||
_fillSlot(posName, card, isLevity);
|
_fillSlot(posName, card, isLevity);
|
||||||
}
|
}
|
||||||
_filled++;
|
_filled++;
|
||||||
if (_filled === 1) _lockSpread();
|
if (_filled === 1) {
|
||||||
|
_lockSpread();
|
||||||
|
_setHasDrawn(true); // first card → DEL enables
|
||||||
|
}
|
||||||
// Per-placement upsert — server stays current
|
// Per-placement upsert — server stays current
|
||||||
// so a navigate-away mid-draw still persists
|
// so a navigate-away mid-draw still persists
|
||||||
// the partial hand. User-spec 2026-05-20.
|
// the partial hand. User-spec 2026-05-20.
|
||||||
@@ -626,7 +646,16 @@
|
|||||||
_setComplete(true);
|
_setComplete(true);
|
||||||
return;
|
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();
|
if (_filled === 0) _lockSpread();
|
||||||
|
_setHasDrawn(true);
|
||||||
var idx = 0;
|
var idx = 0;
|
||||||
function placeNext() {
|
function placeNext() {
|
||||||
if (idx >= autoEntries.length) {
|
if (idx >= autoEntries.length) {
|
||||||
@@ -840,8 +869,10 @@
|
|||||||
if (_filled >= _currentOrder().length) {
|
if (_filled >= _currentOrder().length) {
|
||||||
_setComplete(true);
|
_setComplete(true);
|
||||||
_lockSpread();
|
_lockSpread();
|
||||||
|
_setHasDrawn(true); // also enables DEL post-completion
|
||||||
} else if (_filled > 0) {
|
} else if (_filled > 0) {
|
||||||
_lockSpread();
|
_lockSpread();
|
||||||
|
_setHasDrawn(true); // any partial draw → DEL enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed SeaDeal's `_seaHand` from the server-rendered
|
// Seed SeaDeal's `_seaHand` from the server-rendered
|
||||||
|
|||||||
Reference in New Issue
Block a user