diff --git a/src/apps/billboard/views.py b/src/apps/billboard/views.py index 20d3a51..660e7a9 100644 --- a/src/apps/billboard/views.py +++ b/src/apps/billboard/views.py @@ -268,14 +268,21 @@ def doff_title(request, slug): def my_sign(request): """Render the picker — same 18-card pile as room sig-select (16 middle arcana courts + Major 0 & 1), pulled from the user's equipped deck. - Polarity is determined post-hoc by the FLIP btn (significator_reversed).""" + Polarity is determined post-hoc by the FLIP btn (significator_reversed). + + Backup-deck branch: if the user has no equipped_deck AND no saved sig, + `personal_sig_cards` falls back to the Earthman pile and the template + renders an intro Brief banner labeling the backup as "Earthman [Shabby + Paperboard]" with FYI (→ Game Kit) + NVM (dismiss + proceed) actions.""" from apps.epic.models import personal_sig_cards - deck = request.user.equipped_deck - cards = personal_sig_cards(request.user) if deck else [] + cards = personal_sig_cards(request.user) + no_equipped_deck = request.user.equipped_deck is None + sig = request.user.significator return render(request, "apps/billboard/my_sign.html", { "cards": cards, - "equipped_deck": deck, - "current_significator": request.user.significator, + "no_equipped_deck": no_equipped_deck, + "show_backup_intro_banner": no_equipped_deck and sig is None, + "current_significator": sig, "current_significator_reversed": request.user.significator_reversed, "page_class": "page-billboard page-my-sign", }) diff --git a/src/apps/epic/models.py b/src/apps/epic/models.py index 2213e77..e77b1ea 100644 --- a/src/apps/epic/models.py +++ b/src/apps/epic/models.py @@ -529,12 +529,18 @@ def _sig_unique_cards(room): def personal_sig_cards(user): """Solo equivalent of levity_sig_cards / gravity_sig_cards — uses - User.equipped_deck instead of room.deck_variant. For the My Sig picker - at /billboard/my-sig/. Same 18-card pile (16 middle arcana + Major 0 + 1), - filtered by the user's Note unlocks (Schizo/Nomad lines).""" - return _filter_major_unlocks( - _sig_unique_cards_for_deck(user.equipped_deck), user, - ) + User.equipped_deck instead of room.deck_variant. For the Game Sign + picker at /billboard/my-sign/. Same 18-card pile (16 middle arcana + + Major 0 + 1), filtered by the user's Note unlocks (Schizo/Nomad lines). + + Fallback: if the user has no equipped_deck (e.g. their only deck is + in-use as a TableSeat.deck_variant in an active room), fall back to + the Earthman deck. The picker UI labels this "Earthman [Shabby + Paperboard]" via a Brief banner — the cards are identical, the deck + identity is just a UX framing for "temporary, doesn't belong to your + Game Kit inventory".""" + deck = user.equipped_deck or DeckVariant.objects.filter(slug="earthman").first() + return _filter_major_unlocks(_sig_unique_cards_for_deck(deck), user) def _filter_major_unlocks(cards, user): diff --git a/src/apps/epic/tests/integrated/test_models.py b/src/apps/epic/tests/integrated/test_models.py index a493f60..ca9dd3c 100644 --- a/src/apps/epic/tests/integrated/test_models.py +++ b/src/apps/epic/tests/integrated/test_models.py @@ -516,12 +516,20 @@ class PersonalSigCardsTest(TestCase): cards = personal_sig_cards(user) self.assertEqual(len(cards), 16) - def test_empty_when_user_has_no_equipped_deck(self): + def test_falls_back_to_earthman_when_no_equipped_deck(self): + """Sprint 4a-follow contract: instead of returning an empty pile when + the user has no equipped_deck (e.g. their deck is in-use as a + TableSeat.deck_variant in an active room), personal_sig_cards falls + back to the Earthman deck. The picker labels this "Earthman [Shabby + Paperboard]" via a Brief banner at the view layer.""" from apps.epic.models import personal_sig_cards user = User.objects.create(email="dekless@test.io") user.equipped_deck = None user.save(update_fields=["equipped_deck"]) - self.assertEqual(personal_sig_cards(user), []) + cards = personal_sig_cards(user) + self.assertEqual(len(cards), 16) + # All cards should belong to the Earthman deck (the fallback) + self.assertTrue(all(c.deck_variant.slug == "earthman" for c in cards)) def test_schizo_note_unlocks_major_1(self): from apps.drama.models import Note diff --git a/src/functional_tests/test_bill_my_sign.py b/src/functional_tests/test_bill_my_sign.py index ae85bc0..5d5d80d 100644 --- a/src/functional_tests/test_bill_my_sign.py +++ b/src/functional_tests/test_bill_my_sign.py @@ -160,3 +160,109 @@ class MySignPickerTest(FunctionalTest): )), 0, ) + + +class MySignBackupDeckTest(FunctionalTest): + """User with no equipped_deck + no saved sig should still be able to pick. + + Sprint 4a-follow: no-equipped-deck path. Page renders a Brief banner + nudging "Look!—no deck is equipped. Navigate to the game kit to equip + one (FYI) or (NVM) proceed with Earthman [Shabby Paperboard] deck." — + NVM dismisses + picker stays usable w. backup deck cards; FYI links + to the gameboard (Game Kit applet).""" + + serialized_rollback = True + + def setUp(self): + super().setUp() + for slug, name in [ + ("my-sign", "Game Sign"), ("my-scrolls", "My Scrolls"), + ("my-buds", "My Buds"), ("most-recent-scroll", "Most Recent Scroll"), + ]: + Applet.objects.get_or_create( + slug=slug, defaults={"name": name, "context": "billboard"}, + ) + self.email = "dekless@test.io" + self.gamer = User.objects.create(email=self.email) + # Simulate "deck-in-use elsewhere" — clear equipped_deck (the + # symmetry of the room flow that hands the deck off to a TableSeat). + self.gamer.equipped_deck = None + self.gamer.save(update_fields=["equipped_deck"]) + + # ── Test 1 ─────────────────────────────────────────────────────────────── + + def test_brief_banner_renders_when_no_deck_equipped_and_no_sig(self): + """Banner should appear via Brief.showBanner() w. title "Default deck + warning" + the Shabby Cardstock copy + FYI + NVM buttons.""" + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/billboard/my-sign/") + banner = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, ".my-sign-intro-banner" + ) + ) + self.assertIn("Default deck warning", banner.text) + self.assertIn("no deck is equipped", banner.text) + self.assertIn("Shabby Cardstock", banner.text) + # Brief banner action buttons: .note-banner__nvm + .note-banner__fyi + self.assertTrue( + banner.find_element(By.CSS_SELECTOR, ".note-banner__fyi").is_displayed() + ) + self.assertTrue( + banner.find_element(By.CSS_SELECTOR, ".note-banner__nvm").is_displayed() + ) + + # ── Test 2 ─────────────────────────────────────────────────────────────── + + def test_picker_renders_18_card_pile_using_backup_deck(self): + """No equipped deck → personal_sig_cards falls back to Earthman, so the + picker grid still has the full sig pile (16 cards w. no Note unlocks).""" + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/billboard/my-sign/") + # Grid present + populated (find_elements returns N cards) + self.wait_for( + lambda: self.browser.find_element(By.CSS_SELECTOR, ".my-sign-deck-grid") + ) + cards = self.browser.find_elements( + By.CSS_SELECTOR, ".my-sign-deck-grid .sig-card" + ) + self.assertEqual(len(cards), 16) + + # ── Test 3 ─────────────────────────────────────────────────────────────── + + def test_nvm_dismisses_banner_and_keeps_picker_usable(self): + """NVM click hides the banner; the grid remains interactive (can + click a sig-card + SAVE SIGN persists against the backup deck).""" + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/billboard/my-sign/") + banner = self.wait_for( + lambda: self.browser.find_element(By.CSS_SELECTOR, ".my-sign-intro-banner") + ) + banner.find_element(By.CSS_SELECTOR, ".note-banner__nvm").click() + self.wait_for( + lambda: self.assertEqual( + len(self.browser.find_elements( + By.CSS_SELECTOR, ".my-sign-intro-banner" + )), + 0, + ) + ) + + # ── Test 4 ─────────────────────────────────────────────────────────────── + + def test_fyi_links_to_gameboard(self): + """FYI button is `.note-banner__fyi` rendered as by + Brief.showBanner pointing at the Brief's `post_url` — here /gameboard/ + so the user can equip a deck via Game Kit.""" + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/billboard/my-sign/") + fyi = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, ".my-sign-intro-banner .note-banner__fyi" + ) + ) + href = fyi.get_attribute("href") or "" + self.assertTrue( + href.endswith("/gameboard/"), + f"FYI should link to /gameboard/, got {href!r}", + ) diff --git a/src/static_src/scss/rootvars.scss b/src/static_src/scss/rootvars.scss index e21814c..db39e3c 100644 --- a/src/static_src/scss/rootvars.scss +++ b/src/static_src/scss/rootvars.scss @@ -338,7 +338,7 @@ /* Lord Baltimore Hues */ // yellow - --priBlt: 235, 171, 0; + --priBlt: 235, 191, 0; --secBlt: 187, 147, 52; // white --terBlt: 255, 255, 255; @@ -347,7 +347,7 @@ --quiBlt: 0, 0, 0; --sixBlt: 162, 170, 173; // purple - --sepBlt: 26, 25, 95; + --sepBlt: 50, 30, 95; --octBlt: 157, 34, 53; // orange --ninBlt: 221, 73, 38; @@ -446,7 +446,7 @@ --secUser: var(--terBlt); --terUser: var(--ninBlt); --quaUser: var(--priBlt); - --quiUser: var(--terYl); + --quiUser: var(--terMze); --sixUser: var(--quiBlt); --sepUser: var(--quiBlt); --octUser: var(--quiBlt); diff --git a/src/templates/apps/billboard/my_sign.html b/src/templates/apps/billboard/my_sign.html index 1ead9e3..ae16808 100644 --- a/src/templates/apps/billboard/my_sign.html +++ b/src/templates/apps/billboard/my_sign.html @@ -16,12 +16,6 @@ {% if current_significator %}data-current-card-id="{{ current_significator.id }}"{% endif %} data-current-reversed="{{ current_significator_reversed|yesno:'true,false' }}"> - {% if not equipped_deck %} -

- Equip a card deck first in the - Game Kit to pick your significator. -

- {% else %}