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([ const appletContainerIds = new Set([
'id_applets_container', 'id_applets_container',
'id_game_applets_container', 'id_game_applets_container',
'id_gk_sections_container',
'id_wallet_applets_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 from channels.generic.websocket import AsyncJsonWebsocketConsumer
LEVITY_ROLES = {"PC", "NC", "SC"}
GRAVITY_ROLES = {"BC", "EC", "AC"}
class RoomConsumer(AsyncJsonWebsocketConsumer): class RoomConsumer(AsyncJsonWebsocketConsumer):
async def connect(self): async def connect(self):
self.room_id = self.scope["url_route"]["kwargs"]["room_id"] self.room_id = self.scope["url_route"]["kwargs"]["room_id"]
self.group_name = f"room_{self.room_id}" self.group_name = f"room_{self.room_id}"
await self.channel_layer.group_add(self.group_name, self.channel_name) 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() await self.accept()
async def disconnect(self, close_code): async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.group_name, self.channel_name) 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): 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): async def gate_update(self, event):
await self.send_json(event) await self.send_json(event)
@@ -23,8 +52,14 @@ class RoomConsumer(AsyncJsonWebsocketConsumer):
async def turn_changed(self, event): async def turn_changed(self, event):
await self.send_json(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) await self.send_json(event)
async def sig_selected(self, event): async def sig_selected(self, event):
await self.send_json(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.testing.websocket import WebsocketCommunicator
from channels.layers import get_channel_layer 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 from core.asgi import application
@@ -52,19 +55,33 @@ class RoomConsumerTest(SimpleTestCase):
await communicator.disconnect() 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/") communicator = WebsocketCommunicator(application, "/ws/room/00000000-0000-0000-0000-000000000001/")
await communicator.connect() await communicator.connect()
channel_layer = get_channel_layer() channel_layer = get_channel_layer()
await channel_layer.group_send( await channel_layer.group_send(
"room_00000000-0000-0000-0000-000000000001", "room_00000000-0000-0000-0000-000000000001",
{"type": "roles_revealed", "assignments": {"1": "PC", "2": "BC"}}, {"type": "all_roles_filled"},
) )
response = await communicator.receive_json_from() response = await communicator.receive_json_from()
self.assertEqual(response["type"], "roles_revealed") self.assertEqual(response["type"], "all_roles_filled")
self.assertIn("assignments", response)
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() await communicator.disconnect()
@@ -83,3 +100,67 @@ class RoomConsumerTest(SimpleTestCase):
self.assertEqual(response["gate_state"], "some_state") self.assertEqual(response["gate_state"], "some_state")
await communicator.disconnect() 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.urls import reverse
from django.utils import timezone from django.utils import timezone
from apps.drama.models import GameEvent
from apps.lyric.models import Token, User from apps.lyric.models import Token, User
from apps.epic.models import ( from apps.epic.models import (
DeckVariant, GateSlot, Room, RoomInvite, TableSeat, TarotCard, DeckVariant, GateSlot, Room, RoomInvite, TableSeat, TarotCard,
@@ -643,7 +644,7 @@ class SelectRoleViewTest(TestCase):
).order_by("slot_number").first() ).order_by("slot_number").first()
self.assertEqual(next_active.slot_number, 2) 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"] roles = ["PC", "BC", "SC", "AC", "NC"]
for i, role in enumerate(roles): for i, role in enumerate(roles):
seat = TableSeat.objects.get(room=self.room, slot_number=i + 1) seat = TableSeat.objects.get(room=self.room, slot_number=i + 1)
@@ -655,7 +656,7 @@ class SelectRoleViewTest(TestCase):
data={"role": "EC"}, data={"role": "EC"},
) )
self.room.refresh_from_db() 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): def test_select_role_notifies_turn_changed(self):
with patch("apps.epic.views._notify_turn_changed") as mock_notify: 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) 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"] roles = ["PC", "BC", "SC", "AC", "NC"]
for i, role in enumerate(roles): for i, role in enumerate(roles):
seat = TableSeat.objects.get(room=self.room, slot_number=i + 1) seat = TableSeat.objects.get(room=self.room, slot_number=i + 1)
seat.role = role seat.role = role
seat.save() seat.save()
self.client.force_login(self.gamers[5]) 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( self.client.post(
reverse("epic:select_role", kwargs={"room_id": self.room.id}), reverse("epic:select_role", kwargs={"room_id": self.room.id}),
data={"role": "EC"}, 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): class RoomActionsViewTest(TestCase):
def setUp(self): def setUp(self):
self.owner = User.objects.create(email="owner@test.io") self.owner = User.objects.create(email="owner@test.io")
@@ -987,3 +1057,63 @@ class SelectSigCardViewTest(TestCase):
).first() ).first()
response = self.client.post(self.url, data={"card_id": last_card.id}) response = self.client.post(self.url, data={"card_id": last_card.id})
self.assertIn(response.status_code, (200, 302)) 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/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>/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-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-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>/select-sig', views.select_sig, name='select_sig'),
path('room/<uuid:room_id>/gate/invite', views.invite_gamer, name='invite_gamer'), 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): def _notify_all_roles_filled(room_id):
assignments = {
str(seat.slot_number): seat.role
for seat in TableSeat.objects.filter(room_id=room_id).order_by("slot_number")
}
async_to_sync(get_channel_layer().group_send)( async_to_sync(get_channel_layer().group_send)(
f'room_{room_id}', 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(): if room.table_seats.filter(role__isnull=True).exists():
_notify_turn_changed(room_id) _notify_turn_changed(room_id)
else: 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.table_status = Room.SIG_SELECT
room.save() room.save()
record(room, GameEvent.ROLES_REVEALED) _notify_sig_select_started(room_id)
_notify_roles_revealed(room_id)
return HttpResponse(status=200)
return redirect("epic:room", room_id=room_id) return redirect("epic:room", room_id=room_id)

View File

@@ -365,6 +365,16 @@ class GameKitPageTest(FunctionalTest):
slug=slug, slug=slug,
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "gameboard"}, 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( self.earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman", slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},

View File

@@ -79,6 +79,7 @@
#id_dash_applet_menu { @extend %applet-menu; } #id_dash_applet_menu { @extend %applet-menu; }
#id_game_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_wallet_applet_menu { @extend %applet-menu; }
#id_room_menu { @extend %applet-menu; } #id_room_menu { @extend %applet-menu; }
#id_billboard_applet_menu { @extend %applet-menu; } #id_billboard_applet_menu { @extend %applet-menu; }
@@ -99,6 +100,7 @@
#id_dash_applet_menu, #id_dash_applet_menu,
#id_game_applet_menu, #id_game_applet_menu,
#id_game_kit_menu,
#id_wallet_applet_menu, #id_wallet_applet_menu,
#id_billboard_applet_menu { #id_billboard_applet_menu {
position: fixed; position: fixed;
@@ -125,6 +127,7 @@
#id_dash_applet_menu, #id_dash_applet_menu,
#id_game_applet_menu, #id_game_applet_menu,
#id_game_kit_menu,
#id_wallet_applet_menu, #id_wallet_applet_menu,
#id_billboard_applet_menu { #id_billboard_applet_menu {
right: calc(#{$sidebar-w} + 1rem); right: calc(#{$sidebar-w} + 1rem);
@@ -227,4 +230,4 @@
#id_game_applets_container { @extend %applets-grid; } #id_game_applets_container { @extend %applets-grid; }
#id_wallet_applets_container { @extend %applets-grid; } #id_wallet_applets_container { @extend %applets-grid; }
#id_billboard_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"> <div id="id_gk_sections_container">
{% for entry in applets %} {% for entry in applets %}
{% if entry.applet.slug == 'gk-trinkets' and entry.visible %} {% 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> <h2>Trinkets</h2>
<div class="gk-items"> <div class="gk-items">
{% if pass_token %} {% if pass_token %}
@@ -30,7 +30,7 @@
{% endif %} {% endif %}
{% if entry.applet.slug == 'gk-tokens' and entry.visible %} {% 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> <h2>Tokens</h2>
<div class="gk-items"> <div class="gk-items">
{% for token in free_tokens %} {% for token in free_tokens %}
@@ -53,7 +53,7 @@
{% endif %} {% endif %}
{% if entry.applet.slug == 'gk-decks' and entry.visible %} {% 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> <h2>Card Decks</h2>
<div class="gk-items"> <div class="gk-items">
{% for deck in unlocked_decks %} {% for deck in unlocked_decks %}
@@ -70,7 +70,7 @@
{% endif %} {% endif %}
{% if entry.applet.slug == 'gk-dice' and entry.visible %} {% 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> <h2>Dice Sets</h2>
<div class="gk-items"> <div class="gk-items">
{% include "core/_partials/_forthcoming.html" %} {% include "core/_partials/_forthcoming.html" %}

View File

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