diff --git a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Alchemist.svg b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Alchemist.svg index 3889cfa..64d202b 100644 --- a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Alchemist.svg +++ b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Alchemist.svg @@ -1,5 +1,5 @@ - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Builder.svg b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Builder.svg index 7847681..c50af91 100644 --- a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Builder.svg +++ b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Builder.svg @@ -1,5 +1,5 @@ - + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Economist.svg b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Economist.svg index ffc91e4..776c55b 100644 --- a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Economist.svg +++ b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Economist.svg @@ -1,5 +1,5 @@ - + - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Narrator.svg b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Narrator.svg index 905a011..1162ed0 100644 --- a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Narrator.svg +++ b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Narrator.svg @@ -1,5 +1,5 @@ - + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Player.svg b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Player.svg index eaaf7c2..c2cf203 100644 --- a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Player.svg +++ b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Player.svg @@ -1,5 +1,5 @@ - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Shepherd.svg b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Shepherd.svg index 2cfc2c7..7581cf5 100644 --- a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Shepherd.svg +++ b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-Shepherd.svg @@ -1,5 +1,5 @@ - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-blank.svg b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-blank.svg new file mode 100644 index 0000000..3aa103c --- /dev/null +++ b/src/apps/epic/static/apps/epic/icons/cards-roles/starter-role-blank.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/apps/gameboard/static/apps/gameboard/game-kit.js b/src/apps/gameboard/static/apps/gameboard/game-kit.js index 54b2840..e434566 100644 --- a/src/apps/gameboard/static/apps/gameboard/game-kit.js +++ b/src/apps/gameboard/static/apps/gameboard/game-kit.js @@ -190,6 +190,10 @@ var GameKit = (function () { function openFan(deckId) { currentDeckId = deckId; currentIndex = restorePosition(deckId); + // Always open on the levity-painted face — the FLIP cue. The bookmark + // (sessionStorage) intentionally tracks card index only, not polarity, + // so the styling matches the SSR gravity default until JS repaints. + _polarity = 'levity'; fetch('/gameboard/game-kit/deck/' + deckId + '/') .then(function (r) { return r.text(); }) @@ -405,10 +409,12 @@ var GameKit = (function () { _polarity = 'levity'; init(); }, - // Test seam: skip fetch by reading already-rendered #id_fan_content + // Test seam: skip fetch by reading already-rendered #id_fan_content. + // Mirrors openFan's polarity reset so reopen-after-FLIP is exercisable. _testOpen: function () { cards = Array.from(fanContent.querySelectorAll('.fan-card')); currentIndex = 0; + _polarity = 'levity'; updateFan(); }, _testNavigate: navigate, diff --git a/src/apps/gameboard/static/apps/gameboard/gameboard.js b/src/apps/gameboard/static/apps/gameboard/gameboard.js index 57d7a84..a3e74de 100644 --- a/src/apps/gameboard/static/apps/gameboard/gameboard.js +++ b/src/apps/gameboard/static/apps/gameboard/gameboard.js @@ -59,6 +59,13 @@ function initGameKitTooltips() { }); // buildMiniContent — text-only status; DON/DOFF buttons live in the main portal. + // In-use rows append the room name as "In-Use: ", capped at 24 chars + // (overflow → 21 chars + "…"). The room name lives on the token element's + // data-in-use-room-name (decks) or data-current-room-name (trinkets). + function _inUseLabel(roomName) { + const full = 'In-Use: ' + roomName; + return full.length > 24 ? full.slice(0, 21) + '…' : full; + } function buildMiniContent(token) { const deckId = token.dataset.deckId; const tokenId = token.dataset.tokenId; @@ -67,14 +74,14 @@ function initGameKitTooltips() { const inUseDeckIds = new Set((gameKit.dataset.inUseDeckIds || '').split(',').filter(Boolean)); if (deckId) { if (inUseDeckIds.has(deckId)) { - miniPortal.textContent = 'In-Use'; + miniPortal.textContent = _inUseLabel(token.dataset.inUseRoomName || ''); } else { miniPortal.textContent = (equippedDeckId && deckId === equippedDeckId) ? 'Equipped' : 'Not Equipped'; } } else if (tokenId) { const currentRoomName = token.dataset.currentRoomName || ''; if (currentRoomName) { - miniPortal.textContent = 'In-Use'; + miniPortal.textContent = _inUseLabel(currentRoomName); } else { miniPortal.textContent = (equippedId && tokenId === equippedId) ? 'Equipped' : 'Not Equipped'; } diff --git a/src/apps/gameboard/tests/integrated/test_views.py b/src/apps/gameboard/tests/integrated/test_views.py index 9354a9a..964efef 100644 --- a/src/apps/gameboard/tests/integrated/test_views.py +++ b/src/apps/gameboard/tests/integrated/test_views.py @@ -87,9 +87,9 @@ class GameboardDeckInUseTest(TestCase): ) self.assertEqual(len(active_doff), 0) - def test_in_use_deck_tooltip_shows_game_name(self): - [label] = self.parsed.cssselect("#id_kit_earthman_deck .tt-deck-game-name") - self.assertIn("Wildfire", label.text_content()) + def test_in_use_deck_carries_room_name_for_mini_portal(self): + [el] = self.parsed.cssselect("#id_kit_earthman_deck") + self.assertEqual("Wildfire", el.get("data-in-use-room-name")) def test_non_in_use_deck_has_normal_don(self): fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate") diff --git a/src/functional_tests/test_deck_contribution.py b/src/functional_tests/test_deck_contribution.py index 991ada0..866a426 100644 --- a/src/functional_tests/test_deck_contribution.py +++ b/src/functional_tests/test_deck_contribution.py @@ -111,15 +111,11 @@ class DeckContributionTest(FunctionalTest): earthman_card = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck") ) - earthman_card.click() # open tooltip - - # Tooltip shows the game name (CSS-hidden; read textContent not .text) - tooltip = self.wait_for( - lambda: self.browser.find_element(By.CSS_SELECTOR, ".tt-deck-game-name") + # Game name lives on data-in-use-room-name; the mini-portal renders it as + # "In-Use: " on hover (Jasmine covers the JS truncation). + self.assertEqual( + room.name, earthman_card.get_attribute("data-in-use-room-name") ) - self.assertIn(room.name.upper(), tooltip.get_attribute("textContent").upper()) - - # Mini-tooltip portal shows "In-Use" on hover — covered by gameboard.js Jasmine tests # ── Sprint 2 ───────────────────────────────────────────────────────────────── @@ -173,18 +169,16 @@ class DeckInUseGameKitTest(FunctionalTest): self.assertIn("btn-disabled", doff_btn.get_attribute("class"), "DOFF should be present but disabled for an in-use deck") - def test_tooltip_names_the_game_for_in_use_deck(self): - """Opening an in-use deck's tooltip shows the room name it is contributing to.""" + def test_in_use_deck_carries_game_name_for_mini_portal(self): + """The in-use deck token exposes the room name via data-in-use-room-name + so the mini-portal can render it as 'In-Use: ' on hover.""" gamer, earthman, room, seat = self._setup_in_use_deck() self.create_pre_authenticated_session(GAMER_EMAIL) self.browser.get(self.live_server_url + "/gameboard/") - self.wait_for( + deck_el = self.wait_for( lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_earthman_deck") - ).click() - game_label = self.wait_for( - lambda: self.browser.find_element(By.CSS_SELECTOR, ".tt-deck-game-name") ) - self.assertIn(room.name.upper(), game_label.get_attribute("textContent").upper()) + self.assertEqual(room.name, deck_el.get_attribute("data-in-use-room-name")) def test_non_contributing_deck_has_normal_don_doff(self): """A deck not assigned to any active seat shows the normal DON/DOFF apparatus.""" diff --git a/src/functional_tests/test_trinket_carte_blanche.py b/src/functional_tests/test_trinket_carte_blanche.py index f6fc2fb..24753fa 100644 --- a/src/functional_tests/test_trinket_carte_blanche.py +++ b/src/functional_tests/test_trinket_carte_blanche.py @@ -332,8 +332,9 @@ class CarteBlancheTest(FunctionalTest): ) def test_carte_in_use_game_kit_shows_room_attribution(self): - """While Carte Blanche is deposited in a room, its Game Kit tooltip - shows 'In game: ' so the gamer knows where it's committed.""" + """While Carte Blanche is deposited in a room, its Game Kit token carries + the room name on data-current-room-name so the mini-portal can render + 'In-Use: ' on hover.""" self.create_pre_authenticated_session("blanche@test.io") self.browser.get(self.live_server_url + "/gameboard/") self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit")) @@ -374,12 +375,12 @@ class CarteBlancheTest(FunctionalTest): lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.claimed") ) - # Game Kit panel is on /gameboard/, not the gate page — navigate back to check tooltip + # Game Kit panel is on /gameboard/, not the gate page — navigate back to check token self.browser.get(self.live_server_url + "/gameboard/") - carte_tt = self.wait_for( - lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_carte_blanche .tt") + carte_el = self.wait_for( + lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_carte_blanche") ) - self.assertIn( + self.assertEqual( "Commitment Room", - carte_tt.get_attribute("textContent"), + carte_el.get_attribute("data-current-room-name"), ) diff --git a/src/static/tests/FanStageSpec.js b/src/static/tests/FanStageSpec.js index 4b2ddc7..5fc01c0 100644 --- a/src/static/tests/FanStageSpec.js +++ b/src/static/tests/FanStageSpec.js @@ -305,4 +305,42 @@ describe("FanStage", () => { expect(activeCard().dataset.polarity).toBe("gravity"); }); }); + + // ── Bookmark vs polarity — sessionStorage stores card index only ─────── // + // + // The bookmark must NOT encode FLIP state: closing a modal in gravity and + // reopening it should always land on the levity-painted face (the SSR + // default + the FLIP cue). _testOpen mirrors openFan's polarity reset so + // a re-call simulates a real close + reopen. + + describe("openFan polarity reset", () => { + beforeEach(() => makeFixture()); + + function flipBtn() { return testDiv.querySelector("#id_fan_flip"); } + function activeCard() { return testDiv.querySelector(".fan-card--active"); } + + it("resets polarity to levity on reopen even after FLIP-to-gravity", () => { + GameKit._testOpen(); + flipBtn().click(); + jasmine.clock().tick(500); + expect(activeCard().dataset.polarity).toBe("gravity"); + + // Re-open: bookmark would restore currentIndex, polarity must reset. + GameKit._testOpen(); + jasmine.clock().tick(250); + expect(activeCard().dataset.polarity).toBe("levity"); + }); + + it("does not persist polarity in sessionStorage", () => { + GameKit._testOpen(); + flipBtn().click(); + jasmine.clock().tick(500); + // Bookmark stores currentIndex only — no polarity key anywhere. + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + const val = sessionStorage.getItem(key); + expect(val).not.toMatch(/levity|gravity/); + } + }); + }); }); diff --git a/src/static_src/tests/FanStageSpec.js b/src/static_src/tests/FanStageSpec.js index 4b2ddc7..5fc01c0 100644 --- a/src/static_src/tests/FanStageSpec.js +++ b/src/static_src/tests/FanStageSpec.js @@ -305,4 +305,42 @@ describe("FanStage", () => { expect(activeCard().dataset.polarity).toBe("gravity"); }); }); + + // ── Bookmark vs polarity — sessionStorage stores card index only ─────── // + // + // The bookmark must NOT encode FLIP state: closing a modal in gravity and + // reopening it should always land on the levity-painted face (the SSR + // default + the FLIP cue). _testOpen mirrors openFan's polarity reset so + // a re-call simulates a real close + reopen. + + describe("openFan polarity reset", () => { + beforeEach(() => makeFixture()); + + function flipBtn() { return testDiv.querySelector("#id_fan_flip"); } + function activeCard() { return testDiv.querySelector(".fan-card--active"); } + + it("resets polarity to levity on reopen even after FLIP-to-gravity", () => { + GameKit._testOpen(); + flipBtn().click(); + jasmine.clock().tick(500); + expect(activeCard().dataset.polarity).toBe("gravity"); + + // Re-open: bookmark would restore currentIndex, polarity must reset. + GameKit._testOpen(); + jasmine.clock().tick(250); + expect(activeCard().dataset.polarity).toBe("levity"); + }); + + it("does not persist polarity in sessionStorage", () => { + GameKit._testOpen(); + flipBtn().click(); + jasmine.clock().tick(500); + // Bookmark stores currentIndex only — no polarity key anywhere. + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + const val = sessionStorage.getItem(key); + expect(val).not.toMatch(/levity|gravity/); + } + }); + }); }); diff --git a/src/templates/apps/gameboard/_partials/_applet-game-kit.html b/src/templates/apps/gameboard/_partials/_applet-game-kit.html index 8257519..08f3b80 100644 --- a/src/templates/apps/gameboard/_partials/_applet-game-kit.html +++ b/src/templates/apps/gameboard/_partials/_applet-game-kit.html @@ -33,7 +33,6 @@

{{ carte.tooltip_shoptalk }}

{% endif %}

{{ carte.tooltip_expiry }}

- {% if carte.current_room %}

In game: {{ carte.current_room.name }}

{% endif %} {% endif %} @@ -69,7 +68,7 @@ {% endwith %} {% endif %} {% for deck in deck_variants %} -
+
@@ -79,7 +78,6 @@

{{ deck.card_count }}-card Tarot deck

{% if deck.description %}

{{ deck.description }}

{% endif %}

Stock version (0 substitutions)

- {% if deck.in_use_room_name %}

In game: {{ deck.in_use_room_name }}

{% endif %}
{% empty %} diff --git a/src/templates/apps/gameboard/_partials/_tarot_fan.html b/src/templates/apps/gameboard/_partials/_tarot_fan.html index be681ad..dd1e1c0 100644 --- a/src/templates/apps/gameboard/_partials/_tarot_fan.html +++ b/src/templates/apps/gameboard/_partials/_tarot_fan.html @@ -26,17 +26,17 @@
- {% if card.levity_emanation %} + {% if card.gravity_emanation %} {# Polarity-split title (cards 48-49 + trumps 19-21); no qualifier slots — qualifier is baked into the title between "The" and the proper noun #} -

{{ card.levity_emanation|italicize:card.italic_word }}

+

{{ card.gravity_emanation|italicize:card.italic_word }}

{% else %} {% if card.name_group %}

{{ card.name_group }}

{% endif %} - {% if card.arcana != "MAJOR" and card.levity_qualifier %} -

{{ card.levity_qualifier }}

+ {% if card.arcana != "MAJOR" and card.gravity_qualifier %} +

{{ card.gravity_qualifier }}

{% endif %} -

{{ card.name_title|italicize:card.italic_word }}{% if card.arcana == "MAJOR" and card.levity_qualifier %},{% endif %}

- {% if card.arcana == "MAJOR" and card.levity_qualifier %} -

{{ card.levity_qualifier }}

+

{{ card.name_title|italicize:card.italic_word }}{% if card.arcana == "MAJOR" and card.gravity_qualifier %},{% endif %}

+ {% if card.arcana == "MAJOR" and card.gravity_qualifier %} +

{{ card.gravity_qualifier }}

{% endif %} {% endif %}
@@ -46,13 +46,13 @@ DOM order: reversal-name first, reversal-qualifier second. After SPIN's 180° rotation DOM-second appears visually on top. {% endcomment %} - {% if card.levity_reversal %} + {% if card.gravity_reversal %} {# Polarity-split reversal title — single line, qualifier slot empty. Title goes in the qualifier slot so it visually lands on top after spin. #}

-

{{ card.levity_reversal|italicize:card.italic_word }}

+

{{ card.gravity_reversal|italicize:card.italic_word }}

{% elif card.arcana == "MAJOR" %} -

{{ card.levity_qualifier|default:card.gravity_qualifier }}

-

{{ card.name_title|italicize:card.italic_word }}{% if card.levity_qualifier %},{% endif %}

+

{{ card.gravity_qualifier|default:card.levity_qualifier }}

+

{{ card.name_title|italicize:card.italic_word }}{% if card.gravity_qualifier %},{% endif %}

{% else %}

{{ card.name_title|italicize:card.italic_word }}

{{ card.reversal_qualifier|default:card.gravity_qualifier }}