diff --git a/src/apps/dashboard/static/apps/dashboard/game-kit.js b/src/apps/dashboard/static/apps/dashboard/game-kit.js index 4ad35eb..05d9f73 100644 --- a/src/apps/dashboard/static/apps/dashboard/game-kit.js +++ b/src/apps/dashboard/static/apps/dashboard/game-kit.js @@ -74,6 +74,11 @@ }); } + // gameboard.js re-fetches dialog content after DON and fires this event. + dialog.addEventListener('kit-content-refreshed', function () { + attachCardListeners(); + }); + function attachCardListeners() { dialog.querySelectorAll('.token[data-token-id]').forEach(function (card) { card.addEventListener('click', function () { diff --git a/src/apps/gameboard/static/apps/gameboard/gameboard.js b/src/apps/gameboard/static/apps/gameboard/gameboard.js index 51f3eae..50100d1 100644 --- a/src/apps/gameboard/static/apps/gameboard/gameboard.js +++ b/src/apps/gameboard/static/apps/gameboard/gameboard.js @@ -14,7 +14,6 @@ function initGameKitTooltips() { portal.style.display = 'none'; miniPortal.style.display = 'none'; - let equippedId = gameKit.dataset.equippedId || ''; let activeToken = null; let equipping = false; @@ -32,7 +31,16 @@ function initGameKitTooltips() { document.addEventListener('mousemove', (e) => { if (portal.classList.contains('active') && activeToken) { - const rects = [activeToken.getBoundingClientRect(), portal.getBoundingClientRect()]; + const tokenRect = activeToken.getBoundingClientRect(); + const portalRect = portal.getBoundingClientRect(); + // Expand left to cover button overflow outside portal edge + const expandedPortalRect = { + left: portalRect.left - 24, + top: portalRect.top, + right: portalRect.right, + bottom: portalRect.bottom, + }; + const rects = [tokenRect, expandedPortalRect]; if (miniPortal.classList.contains('active')) rects.push(miniPortal.getBoundingClientRect()); const left = Math.min(...rects.map(r => r.left)); const top = Math.min(...rects.map(r => r.top)); @@ -50,69 +58,157 @@ function initGameKitTooltips() { } }); - // buildMiniContent takes the full element so it can inspect data-deck-id vs data-token-id. + // buildMiniContent — text-only status; DON/DOFF buttons live in the main portal. function buildMiniContent(token) { const deckId = token.dataset.deckId; const tokenId = token.dataset.tokenId; - + const equippedId = gameKit.dataset.equippedId || ''; + const equippedDeckId = gameKit.dataset.equippedDeckId || ''; if (deckId) { - const equippedDeckId = gameKit.dataset.equippedDeckId || ''; - if (equippedDeckId && deckId === equippedDeckId) { - miniPortal.textContent = 'Equipped'; - } else { - const btn = document.createElement('button'); - btn.className = 'equip-deck-btn'; - btn.textContent = 'Equip Deck?'; - btn.addEventListener('click', (e) => { - e.stopPropagation(); - equipping = true; - gameKit.dataset.equippedDeckId = deckId; - fetch(`/gameboard/equip-deck/${deckId}/`, { - method: 'POST', - headers: {'X-CSRFToken': getCsrfToken()}, - }).then(r => { - if (r.ok && equipping) { - equipping = false; - closePortals(); - } else { - equipping = false; - } - }); - }); - miniPortal.innerHTML = ''; - miniPortal.appendChild(btn); - } + miniPortal.textContent = (equippedDeckId && deckId === equippedDeckId) ? 'Equipped' : 'Not Equipped'; } else if (tokenId) { - if (equippedId && tokenId === equippedId) { - miniPortal.textContent = 'Equipped'; - } else { - const btn = document.createElement('button'); - btn.className = 'equip-trinket-btn'; - btn.dataset.tokenId = tokenId; - btn.textContent = 'Equip Trinket?'; - btn.addEventListener('click', (e) => { - e.stopPropagation(); - equipping = true; - equippedId = tokenId; - gameKit.dataset.equippedId = equippedId; - fetch(`/gameboard/equip-trinket/${tokenId}/`, { - method: 'POST', - headers: {'X-CSRFToken': getCsrfToken()}, - }).then(r => { - if (r.ok && equipping) { - equipping = false; - closePortals(); - } else { - equipping = false; - } - }); - }); - miniPortal.innerHTML = ''; - miniPortal.appendChild(btn); - } + miniPortal.textContent = (equippedId && tokenId === equippedId) ? 'Equipped' : 'Not Equipped'; } } + // Update DON/DOFF button pair to reflect new equipped state. + function _setEquipState(donBtn, doffBtn, isEquipped) { + if (isEquipped) { + donBtn.classList.add('btn-disabled'); donBtn.textContent = '×'; + doffBtn.classList.remove('btn-disabled'); doffBtn.textContent = 'DOFF'; + } else { + donBtn.classList.remove('btn-disabled'); donBtn.textContent = 'DON'; + doffBtn.classList.add('btn-disabled'); doffBtn.textContent = '×'; + } + } + + // Sync all tokens' .tt DON/DOFF buttons after an equip state change. + function _syncTokenButtons(kind, newEquippedId) { + const selector = kind === 'deck' ? '[data-deck-id]' : '[data-token-id]'; + gameKit.querySelectorAll('.token' + selector).forEach(tokenEl => { + const id = kind === 'deck' ? tokenEl.dataset.deckId : tokenEl.dataset.tokenId; + const tt = tokenEl.querySelector('.tt'); + if (!tt) return; + const don = tt.querySelector('.btn-equip'); + const doff = tt.querySelector('.btn-unequip'); + if (!don || !doff) return; + _setEquipState(don, doff, id === newEquippedId); + }); + } + + // If the kit bag dialog is open, re-fetch its content (used after DON so the + // newly-equipped card appears without the user having to close+reopen). + function _refreshKitDialog() { + const dialog = document.getElementById('id_kit_bag_dialog'); + const kitBtn = document.getElementById('id_kit_btn'); + if (!dialog || !dialog.hasAttribute('open') || !kitBtn || !kitBtn.dataset.kitUrl) return; + fetch(kitBtn.dataset.kitUrl, { + headers: { 'X-Requested-With': 'XMLHttpRequest' }, + }) + .then(r => r.text()) + .then(html => { + dialog.innerHTML = html; + dialog.dispatchEvent(new CustomEvent('kit-content-refreshed')); + }); + } + + // If the kit bag dialog is open, replace the matching card with a placeholder. + function _syncKitBagDialog(kind, id) { + const dialog = document.getElementById('id_kit_bag_dialog'); + if (!dialog || !dialog.hasAttribute('open')) return; + const selector = kind === 'deck' + ? `.kit-bag-deck[data-deck-id="${id}"]` + : `.token[data-token-id="${id}"]`; + const card = dialog.querySelector(selector); + if (!card) return; + 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) { + const donBtn = portal.querySelector('.btn-equip'); + const doffBtn = portal.querySelector('.btn-unequip'); + if (!donBtn || !doffBtn) return; + + donBtn.addEventListener('click', (e) => { + e.stopPropagation(); + if (donBtn.classList.contains('btn-disabled') || equipping) return; + equipping = true; + const tokenId = donBtn.dataset.tokenId; + const deckId = donBtn.dataset.deckId; + if (tokenId) { + gameKit.dataset.equippedId = tokenId; + fetch(`/gameboard/equip-trinket/${tokenId}/`, { + method: 'POST', + headers: {'X-CSRFToken': getCsrfToken()}, + }).then(r => { + equipping = false; + if (r.ok) { + _setEquipState(donBtn, doffBtn, true); + _syncTokenButtons('trinket', tokenId); + buildMiniContent(token); + _refreshKitDialog(); + } + }); + } else if (deckId) { + gameKit.dataset.equippedDeckId = deckId; + fetch(`/gameboard/equip-deck/${deckId}/`, { + method: 'POST', + headers: {'X-CSRFToken': getCsrfToken()}, + }).then(r => { + equipping = false; + if (r.ok) { + _setEquipState(donBtn, doffBtn, true); + _syncTokenButtons('deck', deckId); + buildMiniContent(token); + _refreshKitDialog(); + } + }); + } + }); + + doffBtn.addEventListener('click', (e) => { + e.stopPropagation(); + if (doffBtn.classList.contains('btn-disabled') || equipping) return; + equipping = true; + const tokenId = doffBtn.dataset.tokenId; + const deckId = doffBtn.dataset.deckId; + if (tokenId) { + gameKit.dataset.equippedId = ''; + fetch(`/gameboard/unequip-trinket/${tokenId}/`, { + method: 'POST', + headers: {'X-CSRFToken': getCsrfToken()}, + }).then(r => { + equipping = false; + if (r.ok) { + _setEquipState(donBtn, doffBtn, false); + _syncTokenButtons('trinket', ''); + buildMiniContent(token); + _syncKitBagDialog('token', tokenId); + } + }); + } else if (deckId) { + gameKit.dataset.equippedDeckId = ''; + fetch(`/gameboard/unequip-deck/${deckId}/`, { + method: 'POST', + headers: {'X-CSRFToken': getCsrfToken()}, + }).then(r => { + equipping = false; + if (r.ok) { + _setEquipState(donBtn, doffBtn, false); + _syncTokenButtons('deck', ''); + buildMiniContent(token); + _syncKitBagDialog('deck', deckId); + } + }); + } + }); + } + function showPortals(token) { equipping = false; activeToken = token; @@ -129,6 +225,7 @@ function initGameKitTooltips() { miniPortal.classList.add('active'); miniPortal.style.display = 'block'; miniHeight = miniPortal.offsetHeight + 4; + wireDonDoff(token); } else { miniPortal.classList.remove('active'); miniPortal.style.display = 'none'; @@ -154,7 +251,10 @@ function initGameKitTooltips() { if (isEquippable) { const mainRect = portal.getBoundingClientRect(); - miniPortal.style.left = (mainRect.right - miniPortal.offsetWidth) + 'px'; + // Pin the right edge of the mini-portal to the right edge of the main portal. + // Using `right` (not `left`) means text width changes grow/shrink leftward. + miniPortal.style.left = ''; + miniPortal.style.right = Math.round(window.innerWidth - mainRect.right) + 'px'; miniPortal.style.top = (mainRect.bottom + 4) + 'px'; } } diff --git a/src/apps/gameboard/urls.py b/src/apps/gameboard/urls.py index 77c85b1..e96cac9 100644 --- a/src/apps/gameboard/urls.py +++ b/src/apps/gameboard/urls.py @@ -8,6 +8,8 @@ urlpatterns = [ path('toggle-applets', views.toggle_game_applets, name='toggle_game_applets'), path('equip-trinket//', views.equip_trinket, name='equip_trinket'), path('equip-deck//', views.equip_deck, name='equip_deck'), + path('unequip-trinket//', views.unequip_trinket, name='unequip_trinket'), + path('unequip-deck//', views.unequip_deck, name='unequip_deck'), path('game-kit/', views.game_kit, name='game_kit'), path('game-kit/toggle-sections', views.toggle_game_kit_sections, name='toggle_game_kit_sections'), path('game-kit/deck//', views.tarot_fan, name='tarot_fan'), diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py index 42dc8a0..0049efa 100644 --- a/src/apps/gameboard/views.py +++ b/src/apps/gameboard/views.py @@ -30,8 +30,8 @@ def gameboard(request): "pass_token": pass_token, "coin": coin, "carte": carte, - "equipped_trinket_id": str(request.user.equipped_trinket_id or ""), - "equipped_deck_id": str(request.user.equipped_deck_id or ""), + "equipped_trinket_id": request.user.equipped_trinket_id, + "equipped_deck_id": request.user.equipped_deck_id, "deck_variants": list(request.user.unlocked_decks.all()), "free_tokens": free_tokens, "free_count": len(free_tokens), @@ -60,8 +60,8 @@ def toggle_game_applets(request): "pass_token": request.user.tokens.filter(token_type=Token.PASS).first() if request.user.is_staff else None, "coin": request.user.tokens.filter(token_type=Token.COIN).first(), "carte": request.user.tokens.filter(token_type=Token.CARTE).first(), - "equipped_trinket_id": str(request.user.equipped_trinket_id or ""), - "equipped_deck_id": str(request.user.equipped_deck_id or ""), + "equipped_trinket_id": request.user.equipped_trinket_id, + "equipped_deck_id": request.user.equipped_deck_id, "deck_variants": list(request.user.unlocked_decks.all()), "free_tokens": list(request.user.tokens.filter( token_type=Token.FREE, expires_at__gt=timezone.now() @@ -102,6 +102,28 @@ def equip_deck(request, deck_id): return HttpResponse(status=405) +@login_required(login_url="/") +def unequip_trinket(request, token_id): + token = get_object_or_404(Token, pk=token_id, user=request.user) + if request.method == "POST": + if request.user.equipped_trinket_id == token.pk: + request.user.equipped_trinket = None + request.user.save(update_fields=["equipped_trinket"]) + return HttpResponse(status=204) + return HttpResponse(status=405) + + +@login_required(login_url="/") +def unequip_deck(request, deck_id): + get_object_or_404(DeckVariant, pk=deck_id) + if request.method == "POST": + if request.user.equipped_deck_id == deck_id: + request.user.equipped_deck = None + request.user.save(update_fields=["equipped_deck"]) + return HttpResponse(status=204) + return HttpResponse(status=405) + + def _game_kit_context(user): 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 diff --git a/src/apps/tooltips/templatetags/tooltip_tags.py b/src/apps/tooltips/templatetags/tooltip_tags.py index c2f9efe..7aced57 100644 --- a/src/apps/tooltips/templatetags/tooltip_tags.py +++ b/src/apps/tooltips/templatetags/tooltip_tags.py @@ -28,4 +28,5 @@ def tooltip(data): 'keywords_rev':extras.get('keywords_rev'), 'cautions': extras.get('cautions'), 'nav': extras.get('nav'), + 'equippable': data.get('equippable'), } diff --git a/src/functional_tests/test_gameboard.py b/src/functional_tests/test_gameboard.py index 4fd5e75..6436186 100644 --- a/src/functional_tests/test_gameboard.py +++ b/src/functional_tests/test_gameboard.py @@ -4,6 +4,8 @@ from selenium.webdriver.common.by import By from .base import FunctionalTest from apps.applets.models import Applet +from apps.epic.models import DeckVariant, Room +from apps.lyric.models import Token, User class GameboardNavigationTest(FunctionalTest): @@ -156,3 +158,139 @@ class GameboardAppletMenuTest(FunctionalTest): ) # 7. Assert no full page reload occurred self.assertTrue(self.browser.execute_script("return window.__no_reload_marker === true")) + + +class GameKitEquipTest(FunctionalTest): + """DON|DOFF equip buttons in the game kit applet portal tooltip.""" + + def setUp(self): + super().setUp() + self.earthman, _ = DeckVariant.objects.get_or_create( + slug="earthman", + defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, + ) + Applet.objects.get_or_create(slug="game-kit", defaults={"name": "Game Kit", "context": "gameboard"}) + self.create_pre_authenticated_session("gamer@equip.io") + self.gamer = User.objects.get(email="gamer@equip.io") + # Promote to staff so the pass_token appears in the game kit applet. + self.gamer.is_staff = True + self.gamer.save(update_fields=["is_staff"]) + self.gamer.unlocked_decks.add(self.earthman) + self.coin = self.gamer.tokens.filter(token_type=Token.COIN).first() + # Create a PASS token manually — starts unequipped (coin is auto-equipped). + self.pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS) + self.browser.set_window_size(1200, 900) + self.browser.get(self.live_server_url + "/gameboard/") + + def _hover_game_kit_token(self, token_el): + """Hover token, wait for portal, return portal element.""" + ActionChains(self.browser).move_to_element(token_el).perform() + self.wait_for( + lambda: self.assertTrue( + self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed() + ) + ) + return self.browser.find_element(By.ID, "id_tooltip_portal") + + def test_unequipped_token_shows_don_active_doff_disabled(self): + """Backstage Pass — naturally unequipped (coin is auto-equipped): DON active, DOFF is ×.""" + pass_el = self.wait_for( + lambda: self.browser.find_element(By.ID, "id_kit_pass") + ) + portal = self._hover_game_kit_token(pass_el) + don = portal.find_element(By.CSS_SELECTOR, ".btn-equip") + doff = portal.find_element(By.CSS_SELECTOR, ".btn-unequip") + self.assertNotIn("btn-disabled", don.get_attribute("class")) + self.assertIn("btn-disabled", doff.get_attribute("class")) + self.assertEqual(doff.text, "×") + + def test_equipped_token_shows_doff_active_don_disabled(self): + """Auto-equipped coin: DOFF active, DON is ×; mini-portal says Equipped.""" + coin = self.wait_for( + lambda: self.browser.find_element(By.ID, "id_kit_coin_on_a_string") + ) + portal = self._hover_game_kit_token(coin) + don = portal.find_element(By.CSS_SELECTOR, ".btn-equip") + doff = portal.find_element(By.CSS_SELECTOR, ".btn-unequip") + self.assertIn("btn-disabled", don.get_attribute("class")) + self.assertEqual(don.text, "×") + self.assertNotIn("btn-disabled", doff.get_attribute("class")) + mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal") + self.assertEqual(mini.text, "Equipped") + self.assertEqual(len(mini.find_elements(By.CSS_SELECTOR, "button")), 0) + + def test_doff_then_don_roundtrip(self): + """Full roundtrip: DOFF unequips (portal stays open, buttons swap, mini updates); + DON re-equips (buttons swap back, mini updates back, DB confirms).""" + coin = self.wait_for( + lambda: self.browser.find_element(By.ID, "id_kit_coin_on_a_string") + ) + portal = self._hover_game_kit_token(coin) + + # — DOFF — + portal.find_element(By.CSS_SELECTOR, ".btn-unequip").click() + self.wait_for( + lambda: self.assertIn( + "btn-disabled", + portal.find_element(By.CSS_SELECTOR, ".btn-unequip").get_attribute("class"), + ) + ) + self.assertEqual(portal.find_element(By.CSS_SELECTOR, ".btn-unequip").text, "×") + self.assertNotIn( + "btn-disabled", + portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"), + ) + self.assertEqual( + self.browser.find_element(By.ID, "id_mini_tooltip_portal").text, "Not Equipped" + ) + self.gamer.refresh_from_db() + self.assertIsNone(self.gamer.equipped_trinket) + + # — DON — + portal.find_element(By.CSS_SELECTOR, ".btn-equip").click() + self.wait_for( + lambda: self.assertIn( + "btn-disabled", + portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"), + ) + ) + self.assertEqual(portal.find_element(By.CSS_SELECTOR, ".btn-equip").text, "×") + self.assertNotIn( + "btn-disabled", + portal.find_element(By.CSS_SELECTOR, ".btn-unequip").get_attribute("class"), + ) + self.assertEqual( + self.browser.find_element(By.ID, "id_mini_tooltip_portal").text, "Equipped" + ) + self.gamer.refresh_from_db() + self.assertEqual(self.gamer.equipped_trinket, self.coin) + + def test_doff_updates_open_kit_bag_dialog(self): + """DOFF from game kit applet replaces trinket card in currently-open kit bag dialog.""" + self.gamer.equipped_trinket = self.coin + self.gamer.save(update_fields=["equipped_trinket"]) + self.browser.refresh() + # Open kit bag dialog + self.browser.find_element(By.ID, "id_kit_btn").click() + self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, + f'#id_kit_bag_dialog [data-token-id="{self.coin.id}"]', + ) + ) + # While dialog is open, hover coin and DOFF from game kit + coin = self.wait_for( + lambda: self.browser.find_element(By.ID, "id_kit_coin_on_a_string") + ) + portal = self._hover_game_kit_token(coin) + portal.find_element(By.CSS_SELECTOR, ".btn-unequip").click() + # Dialog updates: trinket card replaced with placeholder + self.wait_for( + lambda: self.assertEqual( + len(self.browser.find_elements( + By.CSS_SELECTOR, + f'#id_kit_bag_dialog [data-token-id="{self.coin.id}"]', + )), 0 + ) + ) + self.browser.find_element(By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-placeholder") diff --git a/src/static_src/scss/_button-pad.scss b/src/static_src/scss/_button-pad.scss index 2a5c076..0c83eb4 100644 --- a/src/static_src/scss/_button-pad.scss +++ b/src/static_src/scss/_button-pad.scss @@ -286,44 +286,6 @@ font-size: 0.75rem; // 0.63rem × 1.2 } - &.btn-disabled { - cursor: default !important; - pointer-events: none; - font-size: 1.2rem; - padding-bottom: 0.1rem; - color: rgba(var(--secUser), 0.25) !important; - background-color: rgba(var(--priUser), 1) !important; - border-color: rgba(var(--secUser), 0.25) !important; - box-shadow: - 0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5), - 0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25), - 0.25rem 0.25rem 0.25rem rgba(var(--secUser), 0.12) - ; - - &:hover { - text-shadow: - 0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25), - 0 0 1rem rgba(var(--priUser), 1) - ; - box-shadow: - 0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25), - 0 0 0.5rem rgba(var(--priUser), 0.12) - ; - } - - &:active { - text-shadow: - -0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25), - 0 0 0.12rem rgba(var(--priUser), 1) - ; - box-shadow: - -0.1rem -0.1rem 0.12rem rgba(var(--priUser), 0.75), - -0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25), - 0 0 0.5rem rgba(var(--secUser), 0.12) - ; - } - } - &.btn-nav-left { color: rgba(var(--priFs), 1); border-color: rgba(var(--priFs), 1); @@ -394,6 +356,76 @@ } } + &.btn-equip { + color: rgba(var(--priTk), 1); + border-color: rgba(var(--priTk), 1); + background-color: rgba(var(--terTk), 1); + box-shadow: + 0.1rem 0.1rem 0.12rem rgba(var(--terTk), 0.25), + 0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25), + 0.25rem 0.25rem 0.25rem rgba(var(--terTk), 0.12) + ; + + &:hover { + text-shadow: + 0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25), + 0 0 1rem rgba(var(--priTk), 1) + ; + box-shadow: + 0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priTk), 0.12) + ; + } + + &:active { + border: 0.18rem solid rgba(var(--priTk), 1); + text-shadow: + -0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25), + 0 0 0.12rem rgba(var(--priTk), 1) + ; + box-shadow: + -0.1rem -0.1rem 0.12rem rgba(var(--terTk), 0.25), + -0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priTk), 0.12) + ; + } + } + + &.btn-unequip { + color: rgba(var(--priMe), 1); + border-color: rgba(var(--priMe), 1); + background-color: rgba(var(--terMe), 1); + box-shadow: + 0.1rem 0.1rem 0.12rem rgba(var(--terMe), 0.25), + 0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25), + 0.25rem 0.25rem 0.25rem rgba(var(--terMe), 0.12) + ; + + &:hover { + text-shadow: + 0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25), + 0 0 1rem rgba(var(--priMe), 1) + ; + box-shadow: + 0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priMe), 0.12) + ; + } + + &:active { + border: 0.18rem solid rgba(var(--priMe), 1); + text-shadow: + -0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25), + 0 0 0.12rem rgba(var(--priMe), 1) + ; + box-shadow: + -0.1rem -0.1rem 0.12rem rgba(var(--terMe), 0.25), + -0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priMe), 0.12) + ; + } + } + &.btn-reverse { color: rgba(var(--priCy), 1); border-color: rgba(var(--priCy), 1); @@ -463,4 +495,43 @@ ; } } + + // Dead last — wins over all color modifiers by source order. + &.btn-disabled { + cursor: default !important; + pointer-events: none; + font-size: 1.2rem; + padding-bottom: 0.1rem; + color: rgba(var(--secUser), 0.25) !important; + background-color: rgba(var(--priUser), 1) !important; + border-color: rgba(var(--secUser), 0.25) !important; + box-shadow: + 0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5), + 0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25), + 0.25rem 0.25rem 0.25rem rgba(var(--secUser), 0.12) + ; + + &:hover { + text-shadow: + 0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25), + 0 0 1rem rgba(var(--priUser), 1) + ; + box-shadow: + 0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priUser), 0.12) + ; + } + + &:active { + text-shadow: + -0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25), + 0 0 0.12rem rgba(var(--priUser), 1) + ; + box-shadow: + -0.1rem -0.1rem 0.12rem rgba(var(--priUser), 0.75), + -0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--secUser), 0.12) + ; + } + } } diff --git a/src/static_src/scss/_game-kit.scss b/src/static_src/scss/_game-kit.scss index 072343c..4c0b434 100644 --- a/src/static_src/scss/_game-kit.scss +++ b/src/static_src/scss/_game-kit.scss @@ -112,6 +112,19 @@ .token-tooltip, .tt { z-index: 9999; + + // Buttons positioned on left edge of the fixed inline tooltip + .tt-equip-btns { + position: absolute; + left: -1rem; + top: 0; + display: flex; + flex-direction: column; + gap: 1.25rem; + z-index: 1; + + .btn { margin: 0; } + } } .kit-bag-deck { diff --git a/src/static_src/scss/_gameboard.scss b/src/static_src/scss/_gameboard.scss index 38d4ecd..6b41a9c 100644 --- a/src/static_src/scss/_gameboard.scss +++ b/src/static_src/scss/_gameboard.scss @@ -122,6 +122,25 @@ body.page-gameboard { position: fixed; z-index: 9999; + padding: 0.75rem 1.5rem; + + .tt-title { font-size: 1rem; } + .tt-description { padding: 0.125rem; font-size: 0.75rem; } + .tt-shoptalk { font-size: 0.75rem; opacity: 0.75; } + .tt-expiry { font-size: 1rem; color: rgba(var(--priRd), 1); } + + .tt-equip-btns { + position: absolute; + left: -1rem; + top: -1rem; + display: flex; + flex-direction: column; + gap: 0.25rem; + z-index: 1; + + .btn { margin: 0; } + } + &.active { display: block; } } diff --git a/src/static_src/scss/_wallet-tokens.scss b/src/static_src/scss/_wallet-tokens.scss index 0f3aa35..25151b9 100644 --- a/src/static_src/scss/_wallet-tokens.scss +++ b/src/static_src/scss/_wallet-tokens.scss @@ -67,9 +67,8 @@ transform: translateX(-50%); } - &:hover .token-tooltip, - &:hover .tt { - display: block; + &:hover .token-tooltip { + display: block; // legacy fallback; .tt is JS-portal-only (no CSS hover) } } diff --git a/src/templates/apps/gameboard/_partials/_applet-game-kit.html b/src/templates/apps/gameboard/_partials/_applet-game-kit.html index 23661b2..c69ed77 100644 --- a/src/templates/apps/gameboard/_partials/_applet-game-kit.html +++ b/src/templates/apps/gameboard/_partials/_applet-game-kit.html @@ -3,11 +3,14 @@ style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};" >

Game Kit

-
+
{% if pass_token %}
+
+ {% if pass_token.pk == equipped_trinket_id %}{% else %}{% endif %} +

{{ pass_token.tooltip_name }}

{{ pass_token.tooltip_description }}

{% if pass_token.tooltip_shoptalk %} @@ -21,6 +24,9 @@
+
+ {% if carte.pk == equipped_trinket_id %}{% else %}{% endif %} +

{{ carte.tooltip_name }}

{{ carte.tooltip_description }}

{% if carte.tooltip_shoptalk %} @@ -34,6 +40,9 @@
+
+ {% if coin.pk == equipped_trinket_id %}{% else %}{% endif %} +

{{ coin.tooltip_name }}

{{ coin.tooltip_description }}

{% if coin.tooltip_shoptalk %} @@ -62,6 +71,9 @@
+
+ {% if deck.pk == equipped_deck_id %}{% else %}{% endif %} +

{{ deck.name }}

{{ deck.card_count }} cards

diff --git a/src/templates/apps/tooltips/_tooltip.html b/src/templates/apps/tooltips/_tooltip.html index c20edfb..3438b7d 100644 --- a/src/templates/apps/tooltips/_tooltip.html +++ b/src/templates/apps/tooltips/_tooltip.html @@ -1,5 +1,17 @@
+ {% if equippable %} +
+ {% if equippable.is_equipped %} + + + {% else %} + + + {% endif %} +
+ {% endif %} +

{{ title }}

{% if type_label %}

{{ type_label }}

{% endif %} diff --git a/src/templates/core/_partials/_kit_bag_panel.html b/src/templates/core/_partials/_kit_bag_panel.html index e07c169..40556ff 100644 --- a/src/templates/core/_partials/_kit_bag_panel.html +++ b/src/templates/core/_partials/_kit_bag_panel.html @@ -25,10 +25,10 @@
-{% if equipped_trinket %}
Trinket
+ {% if equipped_trinket %} {% with token=equipped_trinket %}
{% endwith %} + {% else %} +
+ +
+ {% endif %}
-{% endif %} -{% if free_token or tithe_token %}
Tokens
@@ -97,6 +100,10 @@
{% endif %} + {% if not free_token and not tithe_token %} +
+ +
+ {% endif %}
-{% endif %}