Compare commits

...

4 Commits

6 changed files with 64 additions and 17 deletions

View File

@@ -0,0 +1,23 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0029_fix_schizo_cautions'),
]
operations = [
migrations.AddField(
model_name='sigreservation',
name='seat',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='sig_reservation',
to='epic.tableseat',
),
),
]

View File

@@ -359,6 +359,10 @@ class SigReservation(models.Model):
gamer = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='sig_reservations'
)
seat = models.ForeignKey(
'TableSeat', null=True, blank=True,
on_delete=models.SET_NULL, related_name='sig_reservation',
)
card = models.ForeignKey(
'TarotCard', on_delete=models.CASCADE, related_name='sig_reservations'
)

View File

@@ -10,8 +10,11 @@ from django.shortcuts import redirect, render
from django.utils import timezone
from apps.drama.models import GameEvent, record
from django.db.models import Case, IntegerField, Value, When
from apps.epic.models import (
GateSlot, Room, RoomInvite, SigReservation, TableSeat, TarotCard, TarotDeck,
GateSlot, Room, RoomInvite, SIG_SEAT_ORDER, SigReservation, TableSeat,
TarotCard, TarotDeck,
active_sig_seat, debit_token, levity_sig_cards, gravity_sig_cards,
select_token, sig_deck_cards,
)
@@ -92,6 +95,22 @@ def _notify_sig_reserved(room_id, card_id, role, reserved):
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
_SIG_SEAT_ORDERING = Case(
*[When(role=r, then=Value(i)) for i, r in enumerate(SIG_SEAT_ORDER)],
default=Value(99),
output_field=IntegerField(),
)
def _canonical_user_seat(room, user):
"""Return the user's seat whose role comes first in PC→NC→EC→SC→AC→BC order.
In normal play (one user = one seat) this is equivalent to .first().
For Carte Blanche (one user = all seats) it returns the PC seat, ensuring
sig-select cursor placement is seat-based, not position/slot-based.
"""
return room.table_seats.filter(gamer=user).order_by(_SIG_SEAT_ORDERING).first()
_ROLE_SCRAWL_NAMES = {
"PC": "Player", "NC": "Narrator", "EC": "Economist",
"SC": "Shepherd", "AC": "Alchemist", "BC": "Builder",
@@ -242,7 +261,7 @@ def _role_select_context(room, user):
"slots": room.gate_slots.order_by("slot_number"),
}
if room.table_status == Room.SIG_SELECT:
user_seat = room.table_seats.filter(gamer=user).first() if user.is_authenticated else None
user_seat = _canonical_user_seat(room, user) if user.is_authenticated else None
user_role = user_seat.role if user_seat else None
user_polarity = None
if user_role in _LEVITY_ROLES:
@@ -583,7 +602,7 @@ def sig_reserve(request, room_id):
if room.table_status != Room.SIG_SELECT:
return HttpResponse(status=400)
user_seat = room.table_seats.filter(gamer=request.user).first()
user_seat = _canonical_user_seat(room, request.user)
if not user_seat or not user_seat.role:
return HttpResponse(status=403)
@@ -622,7 +641,7 @@ def sig_reserve(request, room_id):
SigReservation.objects.create(
room=room, gamer=request.user, card=card,
role=user_seat.role, polarity=polarity,
seat=user_seat, role=user_seat.role, polarity=polarity,
)
_notify_sig_reserved(room_id, card.pk, user_seat.role, reserved=True)
return HttpResponse(status=200)

View File

@@ -57,13 +57,13 @@ INSTALLED_APPS = [
# Board apps
'apps.dashboard',
'apps.gameboard',
'apps.billboard',
# Gamer apps
'apps.lyric',
'apps.epic',
'apps.drama',
'apps.billboard',
'apps.ap',
# Custom apps
'apps.ap',
'apps.api',
'apps.applets',
'functional_tests',

View File

@@ -34,14 +34,15 @@ def _assign_all_roles(room, role_order=None):
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs)
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs).
# _sig_unique_cards() filters arcana=MIDDLE, suits BRANDS/CROWNS/BLADES/GRAILS (Earthman).
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
for suit in ("WANDS", "PENTACLES", "SWORDS", "CUPS"):
for suit in ("BRANDS", "CROWNS", "BLADES", "GRAILS"):
for number in (11, 12, 13, 14):
TarotCard.objects.get_or_create(
deck_variant=earthman,
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
defaults={"arcana": "MINOR", "suit": suit, "number": number,
defaults={"arcana": "MIDDLE", "suit": suit, "number": number,
"name": f"{_NAME[number]} of {suit.capitalize()}"},
)
for number, name, slug in [
@@ -257,13 +258,13 @@ class SigSelectChannelsTest(ChannelsFunctionalTest):
)
reserved_card = self.browser.find_element(By.CSS_SELECTOR, reserved_card_sel)
border_color = self.browser.execute_script(
"return window.getComputedStyle(arguments[0]).borderTopColor",
box_shadow = self.browser.execute_script(
"return window.getComputedStyle(arguments[0]).boxShadow",
reserved_card,
)
self.assertEqual(
border_color, "rgb(255, 207, 52)",
f"Expected --priYl border for NC reservation, got {border_color}",
self.assertIn(
"255, 207, 52", box_shadow,
f"Expected --priYl (255,207,52) in box-shadow for NC reservation, got {box_shadow}",
)
finally:
@@ -311,7 +312,7 @@ class SigSelectThemeTest(FunctionalTest):
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
self._hover_card('.sig-card[data-arcana="Minor Arcana"]')
self._hover_card('.sig-card[data-arcana="Middle Arcana"]')
above = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
@@ -346,7 +347,7 @@ class SigSelectThemeTest(FunctionalTest):
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
self._hover_card('.sig-card[data-arcana="Minor Arcana"]')
self._hover_card('.sig-card[data-arcana="Middle Arcana"]')
above = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")

View File

@@ -507,7 +507,7 @@ html:has(.sig-backdrop) {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9999;
z-index: 200; // above sig-overlay (120), below tray (310)
overflow: visible;
}