feat: Token.BAND (Wristband) — non-admin variant of PASS, admin-awarded via Django admin to any user (NOT auto-granted on signal, NO is_staff coupling, NO model-layer guard). Mirrors PASS at runtime — fills 1 gate slot, never consumed, stays equipped, no current_room tie, no expiry, no In-Use microtooltip — but separates the policy concerns so PASS stays a deliberate staff-only trinket while BAND becomes the regular-user version (promotional / play-reward / staging give-away). Tooltip prose: name "Wristband", desc "Admit All Entry" (shared w. PASS — phrasing reflects the never-depleted lifetime, not multi-slot semantics), shoptalk "Unlimited free entry (BYOB)", expiry "no expiry". fa-ring icon across all 4 surfaces (Game Kit applet #id_kit_wristband between PASS + CARTE, gk-trinkets section, kit-bag dialog Trinket slot, wallet PASS→BAND→COIN elif chain). Priority chain — PASS → BAND → COIN → FREE → TITHE — wired identically into both apps.epic.models.select_token (room gatekeeper) + apps.gameboard.models._select_my_sea_token (my-sea gatekeeper); BAND wins over consumables for any holder while PASS still wins for staff who happen to hold both. debit_token + debit_my_sea_token treat BAND same as PASS: slot marked FILLED w. debited_token_type=BAND, token row preserved, current_room untouched, equipped_trinket unchanged. View contexts (gameboard, toggle_game_applets, _game_kit_context, wallet, toggle_wallet_applets) pass a band key — universal lookup, NO is_staff filter. Migration lyric/0007_alter_token_token_type — choices-only AlterField. TDD — 5 FTs in test_trinket_wristband.py (test_band_not_auto_equipped_after_award, test_band_tooltip_renders_full_prose, test_band_uses_fa_ring_icon, test_equipped_band_shows_equipped_mini_tooltip, test_equipped_band_shows_doff_active_don_disabled); 4 tooltip UTs (BandTokenTooltipTest); 5 model ITs (BandTokenAdminAwardTest — no-auto-grant for non-staff + staff, admin-can-award to either branch, not-auto-equipped); 2 priority-chain ITs (test_returns_band_when_held_and_no_pass, test_pass_still_wins_over_band_for_staff); 1 debit IT (test_debit_band_does_not_consume_or_unequip). 1145 IT/UT + 5 FT green. A boost-pass / promo-band w. richer semantics (multi-slot admit, time-window, etc.) lands as YET-ANOTHER token_type later — keep BAND the minimal "PASS minus admin gate" trinket so the policy axis stays clean. Captured in [[sprint-band-trinket-may21]] alongside the standing auto-commit rule [[feedback-auto-commit-after-build]]
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-21 12:33:09 -04:00
parent 0f60c73f3b
commit 99ffdb3943
14 changed files with 312 additions and 6 deletions

View File

@@ -20,6 +20,22 @@
</div>
</div>
{% endif %}
{% if band %}
<div id="id_kit_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_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>

View File

@@ -10,6 +10,12 @@
<span>{{ pass_token.tooltip_name }}</span>
</div>
{% endif %}
{% if band %}
<div class="gk-trinket-card" data-token-id="{{ band.pk }}">
<i class="fa-solid fa-ring"></i>
<span>{{ band.tooltip_name }}</span>
</div>
{% endif %}
{% if carte %}
<div class="gk-trinket-card" data-token-id="{{ carte.pk }}">
<i class="fa-solid fa-money-check"></i>
@@ -22,7 +28,7 @@
<span>{{ coin.tooltip_name }}</span>
</div>
{% endif %}
{% if not pass_token and not carte and not coin %}
{% if not pass_token and not band and not carte and not coin %}
<p class="gk-empty"><em>No trinkets yet.</em></p>
{% endif %}
</div>

View File

@@ -16,6 +16,18 @@
<p class="tt-expiry">{{ pass_token.tooltip_expiry }}</p>
</div>
</div>
{% elif band %}
<div id="id_band_token" class="token">
<i class="fa-solid fa-ring"></i>
<div class="tt">
<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>
{% elif coin %}
<div id="id_coin_on_a_string" class="token">
<i class="fa-solid fa-medal"></i>

View File

@@ -43,6 +43,8 @@
<i class="fa-solid fa-medal"></i>
{% elif token.token_type == "carte" %}
<i class="fa-solid fa-money-check"></i>
{% elif token.token_type == "band" %}
<i class="fa-solid fa-ring"></i>
{% else %}
<i class="fa-solid fa-clipboard"></i>
{% endif %}