deck contribution sprint 2 + Carte Blanche safeguards — TDD

Sprint 2 UI (game kit applet):
- _applet-game-kit.html: in-use deck → two disabled × buttons, .tt-deck-game-name;
  in-use Carte Blanche → two disabled × buttons, data-current-room-name,
  .tt-token-room-name; tooltip content mirrors kit bag panel (Default, card count,
  description, Stock version)
- gameboard.js buildMiniContent: 'In-Use' for tokens w. data-current-room-name set
- _kit_bag_panel.html: Deck section always renders (placeholder when unequipped)

View safeguards:
- select_role: look up existing deck from prior seat in same room before
  equipped_deck (Carte Blanche multi-seat); only unequip when using equipped_deck
- drop_token Carte: reject 409 if token.current_room is a different room;
  unequip from equipped_trinket on drop

ITs: SelectRoleMultiSeatTest (2), DropTokenViewTest +3 (carte drop, unequip, lock)

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:
Disco DeDisco
2026-04-27 23:24:43 -04:00
parent 94a864b05b
commit fa68c74b51
8 changed files with 196 additions and 22 deletions

View File

@@ -142,6 +142,39 @@ class DropTokenViewTest(TestCase):
response, reverse("epic:gatekeeper", args=[self.room.id]) response, reverse("epic:gatekeeper", args=[self.room.id])
) )
def test_carte_drop_sets_current_room(self):
carte = Token.objects.create(user=self.gamer, token_type=Token.CARTE)
self.client.post(
reverse("epic:drop_token", kwargs={"room_id": self.room.id}),
data={"token_id": carte.pk},
)
carte.refresh_from_db()
self.assertEqual(carte.current_room, self.room)
def test_carte_drop_unequips_trinket(self):
carte = Token.objects.create(user=self.gamer, token_type=Token.CARTE)
self.gamer.equipped_trinket = carte
self.gamer.save(update_fields=["equipped_trinket"])
self.client.post(
reverse("epic:drop_token", kwargs={"room_id": self.room.id}),
data={"token_id": carte.pk},
)
self.gamer.refresh_from_db()
self.assertIsNone(self.gamer.equipped_trinket)
def test_carte_drop_rejected_when_already_in_different_room(self):
other_room = Room.objects.create(name="Other Room", owner=self.gamer)
carte = Token.objects.create(
user=self.gamer, token_type=Token.CARTE, current_room=other_room,
)
response = self.client.post(
reverse("epic:drop_token", kwargs={"room_id": self.room.id}),
data={"token_id": carte.pk},
)
self.assertEqual(response.status_code, 409)
carte.refresh_from_db()
self.assertEqual(carte.current_room, other_room) # unchanged
class ConfirmTokenViewTest(TestCase): class ConfirmTokenViewTest(TestCase):
def setUp(self): def setUp(self):
@@ -701,6 +734,15 @@ class SelectRoleViewTest(TestCase):
seat = TableSeat.objects.get(room=self.room, slot_number=1) seat = TableSeat.objects.get(room=self.room, slot_number=1)
self.assertIsNone(seat.deck_variant) self.assertIsNone(seat.deck_variant)
def test_select_role_unequips_deck_from_user(self):
earthman = DeckVariant.objects.get(slug="earthman")
self.client.post(
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
data={"role": "PC"},
)
self.founder.refresh_from_db()
self.assertIsNone(self.founder.equipped_deck)
def test_select_role_requires_login(self): def test_select_role_requires_login(self):
self.client.logout() self.client.logout()
response = self.client.post( response = self.client.post(
@@ -764,6 +806,55 @@ class SelectRoleViewTest(TestCase):
) )
class SelectRoleMultiSeatTest(TestCase):
"""Carte Blanche multi-seat: second role reuses the deck from the first seat."""
def setUp(self):
self.founder = User.objects.create(email="founder@test.io")
self.client.force_login(self.founder)
self.room = Room.objects.create(name="Test Room", owner=self.founder)
self.room.gate_status = Room.OPEN
self.room.table_status = Room.ROLE_SELECT
self.room.save()
self.earthman = DeckVariant.objects.get(slug="earthman")
def test_second_role_inherits_deck_from_first_seat_in_room(self):
# Founder's first seat: PC already taken with deck assigned
TableSeat.objects.create(
room=self.room, gamer=self.founder, slot_number=1,
role="PC", deck_variant=self.earthman,
)
# Deck unequipped after first role
self.founder.equipped_deck = None
self.founder.save(update_fields=["equipped_deck"])
# Founder's second seat (Carte Blanche): no role yet
second_seat = TableSeat.objects.create(
room=self.room, gamer=self.founder, slot_number=2,
)
self.client.post(
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
data={"role": "BC"},
)
second_seat.refresh_from_db()
self.assertEqual(second_seat.deck_variant, self.earthman)
def test_second_role_does_not_unequip_again(self):
"""No-op unequip when deck was already cleared by the first role."""
TableSeat.objects.create(
room=self.room, gamer=self.founder, slot_number=1,
role="PC", deck_variant=self.earthman,
)
self.founder.equipped_deck = None
self.founder.save(update_fields=["equipped_deck"])
TableSeat.objects.create(room=self.room, gamer=self.founder, slot_number=2)
self.client.post(
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
data={"role": "BC"},
)
self.founder.refresh_from_db()
self.assertIsNone(self.founder.equipped_deck) # still None, not broken
class RoomViewAllRolesFilledTest(TestCase): class RoomViewAllRolesFilledTest(TestCase):
"""Room view in ROLE_SELECT with all seats assigned shows PICK SIGS button.""" """Room view in ROLE_SELECT with all seats assigned shows PICK SIGS button."""
def setUp(self): def setUp(self):

View File

@@ -405,8 +405,13 @@ def drop_token(request, room_id):
if token.token_type == Token.CARTE: if token.token_type == Token.CARTE:
# CARTE enters the machine without reserving a slot — all slots # CARTE enters the machine without reserving a slot — all slots
# become individually claimable via .drop-token-btn # become individually claimable via .drop-token-btn
if token.current_room_id and token.current_room_id != room.id:
return HttpResponse(status=409)
token.current_room = room token.current_room = room
token.save() token.save()
if request.user.equipped_trinket_id == token.pk:
request.user.equipped_trinket = None
request.user.save(update_fields=["equipped_trinket"])
request.session["kit_token_id"] = str(token.id) request.session["kit_token_id"] = str(token.id)
_notify_gate_update(room_id) _notify_gate_update(room_id)
return redirect("epic:gatekeeper", room_id=room_id) return redirect("epic:gatekeeper", room_id=room_id)
@@ -566,6 +571,7 @@ def select_role(request, room_id):
valid_roles = [code for code, _ in TableSeat.ROLE_CHOICES] valid_roles = [code for code, _ in TableSeat.ROLE_CHOICES]
if not role or role not in valid_roles: if not role or role not in valid_roles:
return redirect("epic:room", room_id=room_id) return redirect("epic:room", room_id=room_id)
existing = None
with transaction.atomic(): with transaction.atomic():
active_seat = room.table_seats.select_for_update().filter( active_seat = room.table_seats.select_for_update().filter(
role__isnull=True role__isnull=True
@@ -575,8 +581,16 @@ def select_role(request, room_id):
if room.table_seats.filter(role=role).exists(): if room.table_seats.filter(role=role).exists():
return HttpResponse(status=409) return HttpResponse(status=409)
active_seat.role = role active_seat.role = role
active_seat.deck_variant = request.user.equipped_deck existing = room.table_seats.filter(
gamer=request.user, deck_variant__isnull=False,
).exclude(pk=active_seat.pk).first()
active_seat.deck_variant = (
existing.deck_variant if existing else request.user.equipped_deck
)
active_seat.save() active_seat.save()
if not existing and request.user.equipped_deck:
request.user.equipped_deck = None
request.user.save(update_fields=["equipped_deck"])
record(room, GameEvent.ROLE_SELECTED, actor=request.user, record(room, GameEvent.ROLE_SELECTED, actor=request.user,
role=role, slot_number=active_seat.slot_number, role=role, slot_number=active_seat.slot_number,
role_display=dict(TableSeat.ROLE_CHOICES).get(role, role)) role_display=dict(TableSeat.ROLE_CHOICES).get(role, role))

View File

@@ -64,10 +64,20 @@ function initGameKitTooltips() {
const tokenId = token.dataset.tokenId; const tokenId = token.dataset.tokenId;
const equippedId = gameKit.dataset.equippedId || ''; const equippedId = gameKit.dataset.equippedId || '';
const equippedDeckId = gameKit.dataset.equippedDeckId || ''; const equippedDeckId = gameKit.dataset.equippedDeckId || '';
const inUseDeckIds = new Set((gameKit.dataset.inUseDeckIds || '').split(',').filter(Boolean));
if (deckId) { if (deckId) {
miniPortal.textContent = (equippedDeckId && deckId === equippedDeckId) ? 'Equipped' : 'Not Equipped'; if (inUseDeckIds.has(deckId)) {
miniPortal.textContent = 'In-Use';
} else {
miniPortal.textContent = (equippedDeckId && deckId === equippedDeckId) ? 'Equipped' : 'Not Equipped';
}
} else if (tokenId) { } else if (tokenId) {
miniPortal.textContent = (equippedId && tokenId === equippedId) ? 'Equipped' : 'Not Equipped'; const currentRoomName = token.dataset.currentRoomName || '';
if (currentRoomName) {
miniPortal.textContent = 'In-Use';
} else {
miniPortal.textContent = (equippedId && tokenId === equippedId) ? 'Equipped' : 'Not Equipped';
}
} }
} }

View File

@@ -4,7 +4,7 @@ from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from apps.applets.models import Applet, UserApplet from apps.applets.models import Applet, UserApplet
from apps.epic.models import DeckVariant from apps.epic.models import DeckVariant, Room, TableSeat
from apps.lyric.models import Token, User from apps.lyric.models import Token, User
@@ -61,6 +61,45 @@ class GameboardViewTest(TestCase):
[_] = self.parsed.cssselect("#id_game_kit #id_kit_dice_set") [_] = self.parsed.cssselect("#id_game_kit #id_kit_dice_set")
class GameboardDeckInUseTest(TestCase):
"""Sprint 2: game kit applet renders in-use state for a deck assigned to an active seat."""
def setUp(self):
self.user = User.objects.create(email="gamer@test.io")
self.client.force_login(self.user)
Applet.objects.get_or_create(slug="game-kit", defaults={"name": "Game Kit", "context": "gameboard"})
self.earthman = DeckVariant.objects.get(slug="earthman")
self.room = Room.objects.create(name="Wildfire", owner=self.user)
self.seat = TableSeat.objects.create(
room=self.room, gamer=self.user, slot_number=1,
deck_variant=self.earthman,
)
response = self.client.get("/gameboard/")
self.parsed = lxml.html.fromstring(response.content)
def test_in_use_deck_don_is_disabled(self):
[don] = self.parsed.cssselect("#id_kit_earthman_deck .btn-equip")
self.assertIn("btn-disabled", don.get("class", ""))
def test_in_use_deck_doff_is_absent(self):
active_doff = self.parsed.cssselect(
"#id_kit_earthman_deck .btn-unequip:not(.btn-disabled)"
)
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_non_in_use_deck_has_normal_don(self):
fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
self.user.unlocked_decks.add(fiorentine)
response = self.client.get("/gameboard/")
parsed = lxml.html.fromstring(response.content)
[don] = parsed.cssselect("#id_kit_fiorentine_deck .btn-equip")
self.assertNotIn("btn-disabled", don.get("class", ""))
class ToggleGameAppletsViewTest(TestCase): class ToggleGameAppletsViewTest(TestCase):
def setUp(self): def setUp(self):
self.user = User.objects.create(email="gamer@test.io") self.user = User.objects.create(email="gamer@test.io")

View File

@@ -4,7 +4,20 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone from django.utils import timezone
from apps.applets.utils import applet_context, apply_applet_toggle from apps.applets.utils import applet_context, apply_applet_toggle
from apps.epic.models import DeckVariant, Room
def _annotate_deck_in_use(decks, user):
"""Attach .in_use_room_name to each deck — the name of the active room using it, or None."""
active = {
ts.deck_variant_id: ts.room.name
for ts in TableSeat.objects.filter(
gamer=user, deck_variant__isnull=False,
).select_related("room")
}
for deck in decks:
deck.in_use_room_name = active.get(deck.pk)
return decks
from apps.epic.models import DeckVariant, Room, TableSeat
from apps.epic.utils import rooms_for_user from apps.epic.utils import rooms_for_user
from apps.lyric.models import Token from apps.lyric.models import Token
@@ -31,7 +44,7 @@ def gameboard(request):
"carte": carte, "carte": carte,
"equipped_trinket_id": request.user.equipped_trinket_id, "equipped_trinket_id": request.user.equipped_trinket_id,
"equipped_deck_id": request.user.equipped_deck_id, "equipped_deck_id": request.user.equipped_deck_id,
"deck_variants": list(request.user.unlocked_decks.all()), "deck_variants": _annotate_deck_in_use(list(request.user.unlocked_decks.all()), request.user),
"free_tokens": free_tokens, "free_tokens": free_tokens,
"free_count": len(free_tokens), "free_count": len(free_tokens),
"applets": applet_context(request.user, "gameboard"), "applets": applet_context(request.user, "gameboard"),
@@ -55,7 +68,7 @@ def toggle_game_applets(request):
"carte": request.user.tokens.filter(token_type=Token.CARTE).first(), "carte": request.user.tokens.filter(token_type=Token.CARTE).first(),
"equipped_trinket_id": request.user.equipped_trinket_id, "equipped_trinket_id": request.user.equipped_trinket_id,
"equipped_deck_id": request.user.equipped_deck_id, "equipped_deck_id": request.user.equipped_deck_id,
"deck_variants": list(request.user.unlocked_decks.all()), "deck_variants": _annotate_deck_in_use(list(request.user.unlocked_decks.all()), request.user),
"free_tokens": free_tokens, "free_tokens": free_tokens,
"free_count": len(free_tokens), "free_count": len(free_tokens),
"my_games": rooms_for_user(request.user), "my_games": rooms_for_user(request.user),

View File

@@ -96,7 +96,7 @@ class DeckContributionTest(FunctionalTest):
)) ))
# Navigate to Game Kit → Card Decks to verify UI state # Navigate to Game Kit → Card Decks to verify UI state
self.browser.get(self.live_server_url + "/gameboard/game-kit/") self.browser.get(self.live_server_url + "/gameboard/")
decks_btn = self.wait_for( decks_btn = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck") lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck")
) )
@@ -152,7 +152,7 @@ class DeckInUseGameKitTest(FunctionalTest):
session_key = self.create_pre_authenticated_session(GAMER_EMAIL) session_key = self.create_pre_authenticated_session(GAMER_EMAIL)
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
self.browser.add_cookie({"name": "sessionid", "value": session_key}) self.browser.add_cookie({"name": "sessionid", "value": session_key})
self.browser.get(self.live_server_url + "/gameboard/game-kit/") self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck") lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck")
).click() ).click()
@@ -180,7 +180,7 @@ class DeckInUseGameKitTest(FunctionalTest):
session_key = self.create_pre_authenticated_session(GAMER_EMAIL) session_key = self.create_pre_authenticated_session(GAMER_EMAIL)
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
self.browser.add_cookie({"name": "sessionid", "value": session_key}) self.browser.add_cookie({"name": "sessionid", "value": session_key})
self.browser.get(self.live_server_url + "/gameboard/game-kit/") self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck") lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck")
).click() ).click()
@@ -201,7 +201,7 @@ class DeckInUseGameKitTest(FunctionalTest):
session_key = self.create_pre_authenticated_session(GAMER_EMAIL) session_key = self.create_pre_authenticated_session(GAMER_EMAIL)
self.browser.get(self.live_server_url) self.browser.get(self.live_server_url)
self.browser.add_cookie({"name": "sessionid", "value": session_key}) self.browser.add_cookie({"name": "sessionid", "value": session_key})
self.browser.get(self.live_server_url + "/gameboard/game-kit/") self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for( self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck") lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_kit_card_deck")
).click() ).click()

View File

@@ -3,7 +3,7 @@
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 'game_kit' %}">Game Kit</a></h2> <h2><a href="{% url 'game_kit' %}">Game Kit</a></h2>
<div id="id_game_kit" data-equipped-id="{{ equipped_trinket_id|default:'' }}" data-equipped-deck-id="{{ equipped_deck_id|default:'' }}"> <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 %}">
{% if pass_token %} {% if pass_token %}
<div id="id_kit_pass" class="token" 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>
@@ -21,11 +21,11 @@
</div> </div>
{% endif %} {% endif %}
{% if carte %} {% if carte %}
<div id="id_kit_carte_blanche" class="token" 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>
<div class="tt"> <div class="tt">
<div class="tt-equip-btns"> <div class="tt-equip-btns">
{% if 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 %} {% 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> </div>
<h4 class="tt-title">{{ carte.tooltip_name }}</h4> <h4 class="tt-title">{{ carte.tooltip_name }}</h4>
<p class="tt-description">{{ carte.tooltip_description }}</p> <p class="tt-description">{{ carte.tooltip_description }}</p>
@@ -33,6 +33,7 @@
<p class="tt-shoptalk"><em>{{ carte.tooltip_shoptalk }}</em></p> <p class="tt-shoptalk"><em>{{ carte.tooltip_shoptalk }}</em></p>
{% endif %} {% endif %}
<p class="tt-expiry">{{ carte.tooltip_expiry }}</p> <p class="tt-expiry">{{ carte.tooltip_expiry }}</p>
{% if carte.current_room %}<p class="tt-token-room-name">In game: {{ carte.current_room.name }}</p>{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -72,10 +73,13 @@
<i class="fa-regular fa-id-badge"></i> <i class="fa-regular fa-id-badge"></i>
<div class="tt"> <div class="tt">
<div class="tt-equip-btns"> <div class="tt-equip-btns">
{% if 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 %} {% 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> </div>
<h4 class="tt-title">{{ deck.name }}</h4> <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 }} cards</p> <p class="tt-description">{{ deck.card_count }}-card Tarot deck</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>
{% if deck.in_use_room_name %}<p class="tt-deck-game-name">In game: {{ deck.in_use_room_name }}</p>{% endif %}
</div> </div>
</div> </div>
{% empty %} {% empty %}

View File

@@ -1,20 +1,23 @@
{% if equipped_deck %}
<div class="kit-bag-section"> <div class="kit-bag-section">
<span class="kit-bag-label">Deck</span> <span class="kit-bag-label">Deck</span>
<div class="kit-bag-row"> <div class="kit-bag-row">
{% if equipped_deck %}
<div class="kit-bag-deck" data-deck-id="{{ equipped_deck.pk }}"> <div class="kit-bag-deck" data-deck-id="{{ equipped_deck.pk }}">
<i class="fa-regular fa-id-badge"></i> <i class="fa-regular fa-id-badge"></i>
<div class="tt"> <div class="tt">
<h4 class="tt-title">{{ equipped_deck.name }}{% if equipped_deck.is_default %} <span class="token-count">(Default)</span>{% endif %}</h4> <h4 class="tt-title">{{ equipped_deck.name }}{% if equipped_deck.is_default %} <span class="token-count">(Default)</span>{% endif %}</h4>
<p class="tt-description">{{ equipped_deck.card_count }}-card Tarot deck</p> <p class="tt-description">{{ equipped_deck.card_count }}-card Tarot deck</p>
<p class="tt-shoptalk"><em>placeholder comment</em></p> {% if equipped_deck.description %}<p class="tt-shoptalk"><em>{{ equipped_deck.description }}</em></p>{% endif %}
<p class="tt-effect">active</p> <p class="tt-shoptalk">Stock version <span class="tt-subcounter">(0 substitutions)</span></p>
<p class="tt-expiry">Stock version</p>
</div> </div>
</div> </div>
{% else %}
<div class="kit-bag-placeholder">
<i class="fa-regular fa-id-badge"></i>
</div>
{% endif %}
</div> </div>
</div> </div>
{% endif %}
<div class="kit-bag-section"> <div class="kit-bag-section">
<span class="kit-bag-label">Dice</span> <span class="kit-bag-label">Dice</span>