diff --git a/src/apps/applets/static/apps/applets/applets.js b/src/apps/applets/static/apps/applets/applets.js index 62ef4b5..33af8a8 100644 --- a/src/apps/applets/static/apps/applets/applets.js +++ b/src/apps/applets/static/apps/applets/applets.js @@ -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', ]); diff --git a/src/apps/drama/tests/unit/test_models.py b/src/apps/drama/tests/integrated/test_models.py similarity index 100% rename from src/apps/drama/tests/unit/test_models.py rename to src/apps/drama/tests/integrated/test_models.py diff --git a/src/apps/drama/tests/integrated/test_views.py b/src/apps/drama/tests/integrated/test_views.py deleted file mode 100644 index 2d5d32b..0000000 --- a/src/apps/drama/tests/integrated/test_views.py +++ /dev/null @@ -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) diff --git a/src/apps/drama/tests/unit/__init__.py b/src/apps/drama/tests/unit/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/apps/epic/consumers.py b/src/apps/epic/consumers.py index cc28f5b..44e5000 100644 --- a/src/apps/epic/consumers.py +++ b/src/apps/epic/consumers.py @@ -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) diff --git a/src/apps/epic/tests/integrated/test_consumers.py b/src/apps/epic/tests/integrated/test_consumers.py index 537883d..5490c1d 100644 --- a/src/apps/epic/tests/integrated/test_consumers.py +++ b/src/apps/epic/tests/integrated/test_consumers.py @@ -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() diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index 7d6dc65..439e781 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -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) diff --git a/src/apps/epic/urls.py b/src/apps/epic/urls.py index 2ffe7d5..adb972e 100644 --- a/src/apps/epic/urls.py +++ b/src/apps/epic/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ path('room//gate/return_token', views.return_token, name='return_token'), path('room//gate/release_slot', views.release_slot, name='release_slot'), path('room//pick-roles', views.pick_roles, name='pick_roles'), + path('room//pick-sigs', views.pick_sigs, name='pick_sigs'), path('room//select-role', views.select_role, name='select_role'), path('room//select-sig', views.select_sig, name='select_sig'), path('room//gate/invite', views.invite_gamer, name='invite_gamer'), diff --git a/src/apps/epic/views.py b/src/apps/epic/views.py index d18e8ca..4626170 100644 --- a/src/apps/epic/views.py +++ b/src/apps/epic/views.py @@ -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) diff --git a/src/functional_tests/test_component_cards_tarot.py b/src/functional_tests/test_component_cards_tarot.py index 6a119e3..e8be2d7 100644 --- a/src/functional_tests/test_component_cards_tarot.py +++ b/src/functional_tests/test_component_cards_tarot.py @@ -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}, diff --git a/src/static_src/scss/_applets.scss b/src/static_src/scss/_applets.scss index 1ca4947..8f6af18 100644 --- a/src/static_src/scss/_applets.scss +++ b/src/static_src/scss/_applets.scss @@ -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; } diff --git a/src/templates/apps/gameboard/_partials/_game_kit_sections.html b/src/templates/apps/gameboard/_partials/_game_kit_sections.html index fadf6af..b8d1440 100644 --- a/src/templates/apps/gameboard/_partials/_game_kit_sections.html +++ b/src/templates/apps/gameboard/_partials/_game_kit_sections.html @@ -1,7 +1,7 @@
{% for entry in applets %} {% if entry.applet.slug == 'gk-trinkets' and entry.visible %} -
+

Trinkets

{% if pass_token %} @@ -30,7 +30,7 @@ {% endif %} {% if entry.applet.slug == 'gk-tokens' and entry.visible %} -
+

Tokens

{% for token in free_tokens %} @@ -53,7 +53,7 @@ {% endif %} {% if entry.applet.slug == 'gk-decks' and entry.visible %} -
+

Card Decks

{% for deck in unlocked_decks %} @@ -70,7 +70,7 @@ {% endif %} {% if entry.applet.slug == 'gk-dice' and entry.visible %} -
+

Dice Sets

{% include "core/_partials/_forthcoming.html" %} diff --git a/src/templates/apps/gameboard/room.html b/src/templates/apps/gameboard/room.html index 6632462..06a5e51 100644 --- a/src/templates/apps/gameboard/room.html +++ b/src/templates/apps/gameboard/room.html @@ -13,15 +13,22 @@
- {% if room.table_status == "ROLE_SELECT" and card_stack_state %} -
- {% if card_stack_state == "ineligible" %} - - {% endif %} -
+ {% if room.table_status == "ROLE_SELECT" %} + {% if starter_roles|length == 6 %} +
+ {% csrf_token %} + +
+ {% elif card_stack_state %} +
+ {% if card_stack_state == "ineligible" %} + + {% endif %} +
+ {% endif %} {% endif %}