pick_sigs view + cursor polarity groups; game kit gear menu; housekeeping
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

roles_revealed WS event removed; select_role last pick now fires _notify_all_roles_filled() + stays in ROLE_SELECT; new pick_sigs view (POST /room/<uuid>/pick-sigs) transitions ROLE_SELECT→SIG_SELECT + broadcasts sig_select_started; room.html shows .pick-sigs-btn when all 6 roles filled; PICK SIGS btn absent during mid-selection; 11 new/modified ITs in SelectRoleViewTest + RoomViewAllRolesFilledTest + PickSigsViewTest

consumer: LEVITY_ROLES {PC/NC/SC} + GRAVITY_ROLES {BC/EC/AC}; connects to per-polarity cursor group (cursors_{id}_levity/gravity); receive_json routes cursor_move to cursor group; new handlers all_roles_filled, sig_select_started, cursor_move; CursorMoveConsumerTest (TransactionTestCase, @tag channels): levity cursor reaches fellow levity player, does not reach gravity player

game kit gear menu: #id_game_kit_menu registered in _applets.scss %applet-menu + fixed-position + landscape offset; id_gk_sections_container added to appletContainerIds in applets.js so OK submit dismisses menu; _game_kit_sections.html sections use entry.applet.grid_cols/grid_rows (was hardcoded 6); %applets-grid applied to #id_gk_sections_container (direct parent of sections, not outer wrapper); FT setUp seeds gk-* applets via get_or_create

drama test reorg: integrated/test_views.py deleted (no drama views); two test classes moved to epic/tests/integrated/test_views.py + GameEvent import added; drama/tests/unit/test_models.py → drama/tests/integrated/test_models.py; unit/ dir removed

login form: position:fixed + vertically centred in base styles across all breakpoints; 24rem width, text-align:center; landscape block reduced to left/right sidebar offsets; alert moved below h2; left-side position indicator slots 3/4/5 column order flipped via CSS data-slot selectors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-04 14:33:35 -04:00
parent 188365f412
commit b74f8e1bb1
13 changed files with 313 additions and 111 deletions

View File

@@ -28,6 +28,7 @@ document.addEventListener('DOMContentLoaded', initGearMenus);
const appletContainerIds = new Set([
'id_applets_container',
'id_game_applets_container',
'id_gk_sections_container',
'id_wallet_applets_container',
]);

View File

@@ -1,77 +0,0 @@
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from apps.drama.models import GameEvent
from apps.epic.models import GateSlot, Room, TableSeat
from apps.lyric.models import Token, User
class ConfirmTokenRecordsSlotFilledTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="gamer@test.io")
self.client.force_login(self.user)
self.room = Room.objects.create(name="Test Room", owner=self.user)
self.token = Token.objects.create(user=self.user, token_type=Token.TITHE)
self.slot = self.room.gate_slots.get(slot_number=1)
self.slot.gamer = self.user
self.slot.status = GateSlot.RESERVED
self.slot.reserved_at = timezone.now()
self.slot.save()
def test_confirm_token_records_slot_filled_event(self):
session = self.client.session
session["kit_token_id"] = str(self.token.id)
session.save()
self.client.post(reverse("epic:confirm_token", args=[self.room.id]))
event = GameEvent.objects.get(room=self.room, verb=GameEvent.SLOT_FILLED)
self.assertEqual(event.actor, self.user)
self.assertEqual(event.data["slot_number"], 1)
self.assertEqual(event.data["token_type"], Token.TITHE)
def test_no_event_recorded_if_no_reserved_slot(self):
self.slot.gamer = None
self.slot.status = GateSlot.EMPTY
self.slot.save()
self.client.post(reverse("epic:confirm_token", args=[self.room.id]))
self.assertEqual(GameEvent.objects.filter(verb=GameEvent.SLOT_FILLED).count(), 0)
class SelectRoleRecordsRoleSelectedTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="player@test.io")
self.client.force_login(self.user)
self.room = Room.objects.create(
name="Role Room", owner=self.user, table_status=Room.ROLE_SELECT
)
self.seat = TableSeat.objects.create(
room=self.room, gamer=self.user, slot_number=1
)
def test_select_role_records_role_selected_event(self):
self.client.post(
reverse("epic:select_role", args=[self.room.id]),
data={"role": "PC"},
)
event = GameEvent.objects.get(room=self.room, verb=GameEvent.ROLE_SELECTED)
self.assertEqual(event.actor, self.user)
self.assertEqual(event.data["role"], "PC")
self.assertEqual(event.data["slot_number"], 1)
def test_roles_revealed_event_recorded_when_all_seats_assigned(self):
# Only one seat — assigning it triggers roles_revealed
self.client.post(
reverse("epic:select_role", args=[self.room.id]),
data={"role": "PC"},
)
self.assertTrue(
GameEvent.objects.filter(room=self.room, verb=GameEvent.ROLES_REVEALED).exists()
)
def test_no_event_if_role_already_taken(self):
TableSeat.objects.create(room=self.room, gamer=self.user, slot_number=2, role="PC")
self.client.post(
reverse("epic:select_role", args=[self.room.id]),
data={"role": "PC"},
)
self.assertEqual(GameEvent.objects.filter(verb=GameEvent.ROLE_SELECTED).count(), 0)

View File

@@ -1,18 +1,47 @@
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
LEVITY_ROLES = {"PC", "NC", "SC"}
GRAVITY_ROLES = {"BC", "EC", "AC"}
class RoomConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
self.room_id = self.scope["url_route"]["kwargs"]["room_id"]
self.group_name = f"room_{self.room_id}"
await self.channel_layer.group_add(self.group_name, self.channel_name)
self.cursor_group = None
user = self.scope.get("user")
if user and user.is_authenticated:
seat = await self._get_seat(user)
if seat:
if seat.role in LEVITY_ROLES:
self.cursor_group = f"cursors_{self.room_id}_levity"
elif seat.role in GRAVITY_ROLES:
self.cursor_group = f"cursors_{self.room_id}_gravity"
if self.cursor_group:
await self.channel_layer.group_add(self.cursor_group, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.group_name, self.channel_name)
if self.cursor_group:
await self.channel_layer.group_discard(self.cursor_group, self.channel_name)
async def receive_json(self, content):
pass # handlers added as events introduced
if content.get("type") == "cursor_move" and self.cursor_group:
await self.channel_layer.group_send(
self.cursor_group,
{"type": "cursor_move", "x": content.get("x"), "y": content.get("y")},
)
@database_sync_to_async
def _get_seat(self, user):
from apps.epic.models import TableSeat
return TableSeat.objects.filter(room_id=self.room_id, gamer=user).first()
async def gate_update(self, event):
await self.send_json(event)
@@ -23,8 +52,14 @@ class RoomConsumer(AsyncJsonWebsocketConsumer):
async def turn_changed(self, event):
await self.send_json(event)
async def roles_revealed(self, event):
async def all_roles_filled(self, event):
await self.send_json(event)
async def sig_select_started(self, event):
await self.send_json(event)
async def sig_selected(self, event):
await self.send_json(event)
async def cursor_move(self, event):
await self.send_json(event)

View File

@@ -1,7 +1,10 @@
from channels.db import database_sync_to_async
from channels.testing.websocket import WebsocketCommunicator
from channels.layers import get_channel_layer
from django.test import SimpleTestCase, override_settings
from django.test import Client, SimpleTestCase, TransactionTestCase, override_settings, tag
from apps.epic.models import Room, TableSeat
from apps.lyric.models import User
from core.asgi import application
@@ -52,19 +55,33 @@ class RoomConsumerTest(SimpleTestCase):
await communicator.disconnect()
async def test_receives_roles_revealed_broadcast(self):
async def test_receives_all_roles_filled_broadcast(self):
communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
await communicator.connect()
channel_layer = get_channel_layer()
await channel_layer.group_send(
"room_00000000-0000-0000-0000-000000000001",
{"type": "roles_revealed", "assignments": {"1": "PC", "2": "BC"}},
{"type": "all_roles_filled"},
)
response = await communicator.receive_json_from()
self.assertEqual(response["type"], "roles_revealed")
self.assertIn("assignments", response)
self.assertEqual(response["type"], "all_roles_filled")
await communicator.disconnect()
async def test_receives_sig_select_started_broadcast(self):
communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
await communicator.connect()
channel_layer = get_channel_layer()
await channel_layer.group_send(
"room_00000000-0000-0000-0000-000000000001",
{"type": "sig_select_started"},
)
response = await communicator.receive_json_from()
self.assertEqual(response["type"], "sig_select_started")
await communicator.disconnect()
@@ -83,3 +100,67 @@ class RoomConsumerTest(SimpleTestCase):
self.assertEqual(response["gate_state"], "some_state")
await communicator.disconnect()
@tag('channels')
@override_settings(CHANNEL_LAYERS=TEST_CHANNEL_LAYERS)
class CursorMoveConsumerTest(TransactionTestCase):
"""Cursor moves are broadcast only within the same polarity group
(levity: PC/NC/SC — gravity: BC/EC/AC)."""
async def _make_communicator(self, user, room):
client = Client()
await database_sync_to_async(client.force_login)(user)
session_key = await database_sync_to_async(lambda: client.session.session_key)()
comm = WebsocketCommunicator(
application,
f"/ws/room/{room.id}/",
headers=[(b"cookie", f"sessionid={session_key}".encode())],
)
connected, _ = await comm.connect()
self.assertTrue(connected)
return comm
async def test_levity_cursor_received_by_fellow_levity_player(self):
pc_user = await database_sync_to_async(User.objects.create)(email="pc@test.io")
nc_user = await database_sync_to_async(User.objects.create)(email="nc@test.io")
room = await database_sync_to_async(Room.objects.create)(name="T", owner=pc_user)
await database_sync_to_async(TableSeat.objects.create)(
room=room, gamer=pc_user, slot_number=1, role="PC"
)
await database_sync_to_async(TableSeat.objects.create)(
room=room, gamer=nc_user, slot_number=2, role="NC"
)
pc_comm = await self._make_communicator(pc_user, room)
nc_comm = await self._make_communicator(nc_user, room)
await pc_comm.send_json_to({"type": "cursor_move", "x": 0.5, "y": 0.3})
msg = await nc_comm.receive_json_from(timeout=2)
self.assertEqual(msg["type"], "cursor_move")
self.assertAlmostEqual(msg["x"], 0.5)
await pc_comm.disconnect()
await nc_comm.disconnect()
async def test_levity_cursor_not_received_by_gravity_player(self):
pc_user = await database_sync_to_async(User.objects.create)(email="pc@test.io")
bc_user = await database_sync_to_async(User.objects.create)(email="bc@test.io")
room = await database_sync_to_async(Room.objects.create)(name="T", owner=pc_user)
await database_sync_to_async(TableSeat.objects.create)(
room=room, gamer=pc_user, slot_number=1, role="PC"
)
await database_sync_to_async(TableSeat.objects.create)(
room=room, gamer=bc_user, slot_number=2, role="BC"
)
pc_comm = await self._make_communicator(pc_user, room)
bc_comm = await self._make_communicator(bc_user, room)
await pc_comm.send_json_to({"type": "cursor_move", "x": 0.5, "y": 0.3})
self.assertTrue(await bc_comm.receive_nothing(timeout=1))
await pc_comm.disconnect()
await bc_comm.disconnect()

View File

@@ -5,6 +5,7 @@ from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from apps.drama.models import GameEvent
from apps.lyric.models import Token, User
from apps.epic.models import (
DeckVariant, GateSlot, Room, RoomInvite, TableSeat, TarotCard,
@@ -643,7 +644,7 @@ class SelectRoleViewTest(TestCase):
).order_by("slot_number").first()
self.assertEqual(next_active.slot_number, 2)
def test_all_selected_sets_sig_select(self):
def test_all_selected_stays_role_select_status(self):
roles = ["PC", "BC", "SC", "AC", "NC"]
for i, role in enumerate(roles):
seat = TableSeat.objects.get(room=self.room, slot_number=i + 1)
@@ -655,7 +656,7 @@ class SelectRoleViewTest(TestCase):
data={"role": "EC"},
)
self.room.refresh_from_db()
self.assertEqual(self.room.table_status, Room.SIG_SELECT)
self.assertEqual(self.room.table_status, Room.ROLE_SELECT)
def test_select_role_notifies_turn_changed(self):
with patch("apps.epic.views._notify_turn_changed") as mock_notify:
@@ -665,14 +666,14 @@ class SelectRoleViewTest(TestCase):
)
mock_notify.assert_called_once_with(self.room.id)
def test_select_role_notifies_roles_revealed_when_last(self):
def test_select_role_notifies_all_roles_filled_when_last(self):
roles = ["PC", "BC", "SC", "AC", "NC"]
for i, role in enumerate(roles):
seat = TableSeat.objects.get(room=self.room, slot_number=i + 1)
seat.role = role
seat.save()
self.client.force_login(self.gamers[5])
with patch("apps.epic.views._notify_roles_revealed") as mock_notify:
with patch("apps.epic.views._notify_all_roles_filled") as mock_notify:
self.client.post(
reverse("epic:select_role", kwargs={"room_id": self.room.id}),
data={"role": "EC"},
@@ -742,6 +743,75 @@ class SelectRoleViewTest(TestCase):
)
class RoomViewAllRolesFilledTest(TestCase):
"""Room view in ROLE_SELECT with all seats assigned shows PICK SIGS button."""
def setUp(self):
import lxml.html
self.lxml = lxml.html
self.owner = User.objects.create(email="owner@test.io")
self.room = Room.objects.create(name="Test Room", owner=self.owner)
self.room.table_status = Room.ROLE_SELECT
self.room.save()
all_roles = ["PC", "BC", "SC", "AC", "NC", "EC"]
for i, role in enumerate(all_roles, start=1):
user = User.objects.create(email=f"p{i}@test.io")
TableSeat.objects.create(room=self.room, gamer=user, slot_number=i, role=role)
self.client.force_login(self.owner)
def test_pick_sigs_btn_present_when_all_roles_filled(self):
response = self.client.get(reverse("epic:room", kwargs={"room_id": self.room.id}))
parsed = self.lxml.fromstring(response.content)
[_] = parsed.cssselect(".pick-sigs-btn")
def test_pick_sigs_btn_absent_during_role_select(self):
# Clear one role — still mid-pick, button must not appear
TableSeat.objects.filter(room=self.room, slot_number=6).update(role=None)
response = self.client.get(reverse("epic:room", kwargs={"room_id": self.room.id}))
parsed = self.lxml.fromstring(response.content)
self.assertEqual(len(parsed.cssselect(".pick-sigs-btn")), 0)
class PickSigsViewTest(TestCase):
def setUp(self):
self.owner = User.objects.create(email="owner@test.io")
self.room = Room.objects.create(name="Test Room", owner=self.owner)
self.room.table_status = Room.ROLE_SELECT
self.room.save()
all_roles = ["PC", "BC", "SC", "AC", "NC", "EC"]
for i, role in enumerate(all_roles, start=1):
user = User.objects.create(email=f"p{i}@test.io")
TableSeat.objects.create(room=self.room, gamer=user, slot_number=i, role=role)
self.client.force_login(self.owner)
self.url = reverse("epic:pick_sigs", kwargs={"room_id": self.room.id})
def test_pick_sigs_requires_login(self):
self.client.logout()
response = self.client.post(self.url)
self.assertEqual(response.status_code, 302)
self.assertIn("/accounts/login/", response.url)
def test_pick_sigs_transitions_to_sig_select(self):
self.client.post(self.url)
self.room.refresh_from_db()
self.assertEqual(self.room.table_status, Room.SIG_SELECT)
def test_pick_sigs_redirects_to_room(self):
response = self.client.post(self.url)
self.assertRedirects(response, reverse("epic:room", args=[self.room.id]))
def test_pick_sigs_is_noop_if_not_role_select(self):
self.room.table_status = Room.SIG_SELECT
self.room.save()
self.client.post(self.url)
self.room.refresh_from_db()
self.assertEqual(self.room.table_status, Room.SIG_SELECT)
def test_pick_sigs_notifies_sig_select_started(self):
with patch("apps.epic.views._notify_sig_select_started") as mock_notify:
self.client.post(self.url)
mock_notify.assert_called_once_with(self.room.id)
class RoomActionsViewTest(TestCase):
def setUp(self):
self.owner = User.objects.create(email="owner@test.io")
@@ -987,3 +1057,63 @@ class SelectSigCardViewTest(TestCase):
).first()
response = self.client.post(self.url, data={"card_id": last_card.id})
self.assertIn(response.status_code, (200, 302))
class ConfirmTokenRecordsSlotFilledTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="gamer@test.io")
self.client.force_login(self.user)
self.room = Room.objects.create(name="Test Room", owner=self.user)
self.token = Token.objects.create(user=self.user, token_type=Token.TITHE)
self.slot = self.room.gate_slots.get(slot_number=1)
self.slot.gamer = self.user
self.slot.status = GateSlot.RESERVED
self.slot.reserved_at = timezone.now()
self.slot.save()
def test_confirm_token_records_slot_filled_event(self):
session = self.client.session
session["kit_token_id"] = str(self.token.id)
session.save()
self.client.post(reverse("epic:confirm_token", args=[self.room.id]))
event = GameEvent.objects.get(room=self.room, verb=GameEvent.SLOT_FILLED)
self.assertEqual(event.actor, self.user)
self.assertEqual(event.data["slot_number"], 1)
self.assertEqual(event.data["token_type"], Token.TITHE)
def test_no_event_recorded_if_no_reserved_slot(self):
self.slot.gamer = None
self.slot.status = GateSlot.EMPTY
self.slot.save()
self.client.post(reverse("epic:confirm_token", args=[self.room.id]))
self.assertEqual(GameEvent.objects.filter(verb=GameEvent.SLOT_FILLED).count(), 0)
class SelectRoleRecordsRoleSelectedTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="player@test.io")
self.client.force_login(self.user)
self.room = Room.objects.create(
name="Role Room", owner=self.user, table_status=Room.ROLE_SELECT
)
self.seat = TableSeat.objects.create(
room=self.room, gamer=self.user, slot_number=1
)
def test_select_role_records_role_selected_event(self):
self.client.post(
reverse("epic:select_role", args=[self.room.id]),
data={"role": "PC"},
)
event = GameEvent.objects.get(room=self.room, verb=GameEvent.ROLE_SELECTED)
self.assertEqual(event.actor, self.user)
self.assertEqual(event.data["role"], "PC")
self.assertEqual(event.data["slot_number"], 1)
def test_no_event_if_role_already_taken(self):
TableSeat.objects.create(room=self.room, gamer=self.user, slot_number=2, role="PC")
self.client.post(
reverse("epic:select_role", args=[self.room.id]),
data={"role": "PC"},
)
self.assertEqual(GameEvent.objects.filter(verb=GameEvent.ROLE_SELECTED).count(), 0)

View File

@@ -13,6 +13,7 @@ urlpatterns = [
path('room/<uuid:room_id>/gate/return_token', views.return_token, name='return_token'),
path('room/<uuid:room_id>/gate/release_slot', views.release_slot, name='release_slot'),
path('room/<uuid:room_id>/pick-roles', views.pick_roles, name='pick_roles'),
path('room/<uuid:room_id>/pick-sigs', views.pick_sigs, name='pick_sigs'),
path('room/<uuid:room_id>/select-role', views.select_role, name='select_role'),
path('room/<uuid:room_id>/select-sig', views.select_sig, name='select_sig'),
path('room/<uuid:room_id>/gate/invite', views.invite_gamer, name='invite_gamer'),

View File

@@ -41,14 +41,17 @@ def _notify_turn_changed(room_id):
)
def _notify_roles_revealed(room_id):
assignments = {
str(seat.slot_number): seat.role
for seat in TableSeat.objects.filter(room_id=room_id).order_by("slot_number")
}
def _notify_all_roles_filled(room_id):
async_to_sync(get_channel_layer().group_send)(
f'room_{room_id}',
{'type': 'roles_revealed', 'assignments': assignments},
{'type': 'all_roles_filled'},
)
def _notify_sig_select_started(room_id):
async_to_sync(get_channel_layer().group_send)(
f'room_{room_id}',
{'type': 'sig_select_started'},
)
@@ -443,11 +446,19 @@ def select_role(request, room_id):
if room.table_seats.filter(role__isnull=True).exists():
_notify_turn_changed(room_id)
else:
_notify_all_roles_filled(room_id)
return HttpResponse(status=200)
return redirect("epic:room", room_id=room_id)
@login_required
def pick_sigs(request, room_id):
if request.method == "POST":
room = Room.objects.get(id=room_id)
if room.table_status == Room.ROLE_SELECT:
room.table_status = Room.SIG_SELECT
room.save()
record(room, GameEvent.ROLES_REVEALED)
_notify_roles_revealed(room_id)
return HttpResponse(status=200)
_notify_sig_select_started(room_id)
return redirect("epic:room", room_id=room_id)

View File

@@ -365,6 +365,16 @@ class GameKitPageTest(FunctionalTest):
slug=slug,
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "gameboard"},
)
for slug, name in [
("gk-trinkets", "Trinkets"),
("gk-tokens", "Tokens"),
("gk-decks", "Card Decks"),
("gk-dice", "Dice Sets"),
]:
Applet.objects.get_or_create(
slug=slug,
defaults={"name": name, "grid_cols": 3, "grid_rows": 3, "context": "game-kit"},
)
self.earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},

View File

@@ -79,6 +79,7 @@
#id_dash_applet_menu { @extend %applet-menu; }
#id_game_applet_menu { @extend %applet-menu; }
#id_game_kit_menu { @extend %applet-menu; }
#id_wallet_applet_menu { @extend %applet-menu; }
#id_room_menu { @extend %applet-menu; }
#id_billboard_applet_menu { @extend %applet-menu; }
@@ -99,6 +100,7 @@
#id_dash_applet_menu,
#id_game_applet_menu,
#id_game_kit_menu,
#id_wallet_applet_menu,
#id_billboard_applet_menu {
position: fixed;
@@ -125,6 +127,7 @@
#id_dash_applet_menu,
#id_game_applet_menu,
#id_game_kit_menu,
#id_wallet_applet_menu,
#id_billboard_applet_menu {
right: calc(#{$sidebar-w} + 1rem);
@@ -227,4 +230,4 @@
#id_game_applets_container { @extend %applets-grid; }
#id_wallet_applets_container { @extend %applets-grid; }
#id_billboard_applets_container { @extend %applets-grid; }
#id_game_kit_applets_container { @extend %applets-grid; }
#id_gk_sections_container { @extend %applets-grid; }

View File

@@ -1,7 +1,7 @@
<div id="id_gk_sections_container">
{% for entry in applets %}
{% if entry.applet.slug == 'gk-trinkets' and entry.visible %}
<section id="id_gk_trinkets" style="--applet-cols: 6; --applet-rows: 3;">
<section id="id_gk_trinkets" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">
<h2>Trinkets</h2>
<div class="gk-items">
{% if pass_token %}
@@ -30,7 +30,7 @@
{% endif %}
{% if entry.applet.slug == 'gk-tokens' and entry.visible %}
<section id="id_gk_tokens" style="--applet-cols: 6; --applet-rows: 3;">
<section id="id_gk_tokens" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">
<h2>Tokens</h2>
<div class="gk-items">
{% for token in free_tokens %}
@@ -53,7 +53,7 @@
{% endif %}
{% if entry.applet.slug == 'gk-decks' and entry.visible %}
<section id="id_gk_decks" style="--applet-cols: 6; --applet-rows: 3;">
<section id="id_gk_decks" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">
<h2>Card Decks</h2>
<div class="gk-items">
{% for deck in unlocked_decks %}
@@ -70,7 +70,7 @@
{% endif %}
{% if entry.applet.slug == 'gk-dice' and entry.visible %}
<section id="id_gk_dice" style="--applet-cols: 6; --applet-rows: 3;">
<section id="id_gk_dice" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};">
<h2>Dice Sets</h2>
<div class="gk-items">
{% include "core/_partials/_forthcoming.html" %}

View File

@@ -13,15 +13,22 @@
<div class="table-hex-border">
<div class="table-hex">
<div class="table-center">
{% if room.table_status == "ROLE_SELECT" and card_stack_state %}
<div class="card-stack" data-state="{{ card_stack_state }}"
data-starter-roles="{{ starter_roles|join:',' }}"
data-user-slots="{{ user_slots|join:',' }}"
data-active-slot="{{ active_slot }}">
{% if card_stack_state == "ineligible" %}
<i class="fa-solid fa-ban"></i>
{% endif %}
</div>
{% if room.table_status == "ROLE_SELECT" %}
{% if starter_roles|length == 6 %}
<form method="POST" action="{% url 'epic:pick_sigs' room.id %}">
{% csrf_token %}
<button type="submit" class="pick-sigs-btn btn btn-primary btn-xl">PICK SIGS</button>
</form>
{% elif card_stack_state %}
<div class="card-stack" data-state="{{ card_stack_state }}"
data-starter-roles="{{ starter_roles|join:',' }}"
data-user-slots="{{ user_slots|join:',' }}"
data-active-slot="{{ active_slot }}">
{% if card_stack_state == "ineligible" %}
<i class="fa-solid fa-ban"></i>
{% endif %}
</div>
{% endif %}
{% endif %}
</div>
</div>