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( gamer = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='sig_reservations' 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( card = models.ForeignKey(
'TarotCard', on_delete=models.CASCADE, related_name='sig_reservations' '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 django.utils import timezone
from apps.drama.models import GameEvent, record from apps.drama.models import GameEvent, record
from django.db.models import Case, IntegerField, Value, When
from apps.epic.models import ( 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, active_sig_seat, debit_token, levity_sig_cards, gravity_sig_cards,
select_token, sig_deck_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"} 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 = { _ROLE_SCRAWL_NAMES = {
"PC": "Player", "NC": "Narrator", "EC": "Economist", "PC": "Player", "NC": "Narrator", "EC": "Economist",
"SC": "Shepherd", "AC": "Alchemist", "BC": "Builder", "SC": "Shepherd", "AC": "Alchemist", "BC": "Builder",
@@ -242,7 +261,7 @@ def _role_select_context(room, user):
"slots": room.gate_slots.order_by("slot_number"), "slots": room.gate_slots.order_by("slot_number"),
} }
if room.table_status == Room.SIG_SELECT: 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_role = user_seat.role if user_seat else None
user_polarity = None user_polarity = None
if user_role in _LEVITY_ROLES: if user_role in _LEVITY_ROLES:
@@ -583,7 +602,7 @@ def sig_reserve(request, room_id):
if room.table_status != Room.SIG_SELECT: if room.table_status != Room.SIG_SELECT:
return HttpResponse(status=400) 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: if not user_seat or not user_seat.role:
return HttpResponse(status=403) return HttpResponse(status=403)
@@ -622,7 +641,7 @@ def sig_reserve(request, room_id):
SigReservation.objects.create( SigReservation.objects.create(
room=room, gamer=request.user, card=card, 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) _notify_sig_reserved(room_id, card.pk, user_seat.role, reserved=True)
return HttpResponse(status=200) return HttpResponse(status=200)

View File

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

View File

@@ -34,14 +34,15 @@ def _assign_all_roles(room, role_order=None):
slug="earthman", slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, 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"} _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): for number in (11, 12, 13, 14):
TarotCard.objects.get_or_create( TarotCard.objects.get_or_create(
deck_variant=earthman, deck_variant=earthman,
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em", 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()}"}, "name": f"{_NAME[number]} of {suit.capitalize()}"},
) )
for number, name, slug in [ for number, name, slug in [
@@ -257,13 +258,13 @@ class SigSelectChannelsTest(ChannelsFunctionalTest):
) )
reserved_card = self.browser.find_element(By.CSS_SELECTOR, reserved_card_sel) reserved_card = self.browser.find_element(By.CSS_SELECTOR, reserved_card_sel)
border_color = self.browser.execute_script( box_shadow = self.browser.execute_script(
"return window.getComputedStyle(arguments[0]).borderTopColor", "return window.getComputedStyle(arguments[0]).boxShadow",
reserved_card, reserved_card,
) )
self.assertEqual( self.assertIn(
border_color, "rgb(255, 207, 52)", "255, 207, 52", box_shadow,
f"Expected --priYl border for NC reservation, got {border_color}", f"Expected --priYl (255,207,52) in box-shadow for NC reservation, got {box_shadow}",
) )
finally: finally:
@@ -311,7 +312,7 @@ class SigSelectThemeTest(FunctionalTest):
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/") 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.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( above = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above") 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.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.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( above = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above") lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")

View File

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