sig-select sprint: SigReservation model + sig_reserve view (OK/NVM hold); full sig-select.js rewrite with stage preview, WS hover cursors, reservation lock (must NVM before OK-ing another card — enforced server-side 409 + JS guard); sizeSigModal() + sizeSigCard() in room.js (JS-based card sizing avoids libsass cqw/cqh limitation); stat block hidden until OK pressed; mobile touch: dismiss stage on outside-grid tap when unfocused; 17 IT + Jasmine specs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
@@ -10,8 +11,9 @@ from django.utils import timezone
|
||||
|
||||
from apps.drama.models import GameEvent, record
|
||||
from apps.epic.models import (
|
||||
GateSlot, Room, RoomInvite, TableSeat, TarotCard, TarotDeck,
|
||||
active_sig_seat, debit_token, select_token, sig_deck_cards, sig_seat_order,
|
||||
GateSlot, Room, RoomInvite, SigReservation, TableSeat, TarotCard, TarotDeck,
|
||||
active_sig_seat, debit_token, levity_sig_cards, gravity_sig_cards,
|
||||
select_token, sig_deck_cards,
|
||||
)
|
||||
from apps.lyric.models import Token
|
||||
|
||||
@@ -74,6 +76,20 @@ def _notify_sig_selected(room_id, card_id, role, deck_type='levity'):
|
||||
)
|
||||
|
||||
|
||||
_LEVITY_ROLES = {'PC', 'NC', 'SC'}
|
||||
_GRAVITY_ROLES = {'BC', 'EC', 'AC'}
|
||||
|
||||
|
||||
def _notify_sig_reserved(room_id, card_id, role, reserved):
|
||||
"""Broadcast a sig_reserved event to the matching polarity cursor group."""
|
||||
polarity = 'levity' if role in _LEVITY_ROLES else 'gravity'
|
||||
async_to_sync(get_channel_layer().group_send)(
|
||||
f'cursors_{room_id}_{polarity}',
|
||||
{'type': 'sig_reserved', 'card_id': str(card_id) if card_id else None,
|
||||
'role': role, 'reserved': reserved},
|
||||
)
|
||||
|
||||
|
||||
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
|
||||
|
||||
|
||||
@@ -216,16 +232,35 @@ def _role_select_context(room, user):
|
||||
}
|
||||
if room.table_status == Room.SIG_SELECT:
|
||||
user_seat = room.table_seats.filter(gamer=user).first() if user.is_authenticated else None
|
||||
partner_role = TableSeat.PARTNER_MAP.get(user_seat.role) if user_seat and user_seat.role else None
|
||||
partner_seat = room.table_seats.filter(role=partner_role).first() if partner_role else None
|
||||
user_role = user_seat.role if user_seat else None
|
||||
user_polarity = None
|
||||
if user_role in _LEVITY_ROLES:
|
||||
user_polarity = 'levity'
|
||||
elif user_role in _GRAVITY_ROLES:
|
||||
user_polarity = 'gravity'
|
||||
|
||||
ctx["user_seat"] = user_seat
|
||||
ctx["partner_seat"] = partner_seat
|
||||
ctx["revealed_seats"] = room.table_seats.filter(role_revealed=True).order_by("slot_number")
|
||||
raw_sig_cards = sig_deck_cards(room)
|
||||
half = len(raw_sig_cards) // 2
|
||||
ctx["sig_cards"] = [(c, 'levity') for c in raw_sig_cards[:half]] + [(c, 'gravity') for c in raw_sig_cards[half:]]
|
||||
ctx["sig_seats"] = sig_seat_order(room)
|
||||
ctx["sig_active_seat"] = active_sig_seat(room)
|
||||
ctx["user_polarity"] = user_polarity
|
||||
ctx["sig_reserve_url"] = f"/gameboard/room/{room.id}/sig-reserve"
|
||||
|
||||
# Pre-load existing reservations for this polarity so JS can restore
|
||||
# grabbed state on page load/refresh. Keyed by str(card_id) → role.
|
||||
if user_polarity:
|
||||
polarity_const = SigReservation.LEVITY if user_polarity == 'levity' else SigReservation.GRAVITY
|
||||
reservations = {
|
||||
str(res.card_id): res.role
|
||||
for res in room.sig_reservations.filter(polarity=polarity_const)
|
||||
}
|
||||
else:
|
||||
reservations = {}
|
||||
ctx["sig_reservations_json"] = json.dumps(reservations)
|
||||
|
||||
if user_polarity == 'levity':
|
||||
ctx["sig_cards"] = levity_sig_cards(room)
|
||||
elif user_polarity == 'gravity':
|
||||
ctx["sig_cards"] = gravity_sig_cards(room)
|
||||
else:
|
||||
ctx["sig_cards"] = []
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -526,6 +561,60 @@ def gate_status(request, room_id):
|
||||
return render(request, "apps/gameboard/_partials/_gatekeeper.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def sig_reserve(request, room_id):
|
||||
"""Provisional card hold (OK / NVM) during SIG_SELECT.
|
||||
POST body: card_id=<uuid>, action=reserve|release
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return HttpResponse(status=405)
|
||||
room = Room.objects.get(id=room_id)
|
||||
if room.table_status != Room.SIG_SELECT:
|
||||
return HttpResponse(status=400)
|
||||
|
||||
user_seat = room.table_seats.filter(gamer=request.user).first()
|
||||
if not user_seat or not user_seat.role:
|
||||
return HttpResponse(status=403)
|
||||
|
||||
action = request.POST.get("action", "reserve")
|
||||
|
||||
if action == "release":
|
||||
SigReservation.objects.filter(room=room, gamer=request.user).delete()
|
||||
_notify_sig_reserved(room_id, None, user_seat.role, reserved=False)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
# Reserve action
|
||||
card_id = request.POST.get("card_id")
|
||||
try:
|
||||
card = TarotCard.objects.get(pk=card_id)
|
||||
except TarotCard.DoesNotExist:
|
||||
return HttpResponse(status=400)
|
||||
|
||||
polarity = SigReservation.LEVITY if user_seat.role in _LEVITY_ROLES else SigReservation.GRAVITY
|
||||
|
||||
# Block if another gamer in the same polarity already holds this card
|
||||
if SigReservation.objects.filter(
|
||||
room=room, card=card, polarity=polarity
|
||||
).exclude(gamer=request.user).exists():
|
||||
return HttpResponse(status=409)
|
||||
|
||||
# Block if this gamer already holds a *different* card — must NVM first
|
||||
existing = SigReservation.objects.filter(room=room, gamer=request.user).first()
|
||||
if existing and existing.card != card:
|
||||
return HttpResponse(status=409)
|
||||
|
||||
# Idempotent: already holding the same card
|
||||
if existing:
|
||||
return HttpResponse(status=200)
|
||||
|
||||
SigReservation.objects.create(
|
||||
room=room, gamer=request.user, card=card,
|
||||
role=user_seat.role, polarity=polarity,
|
||||
)
|
||||
_notify_sig_reserved(room_id, card.pk, user_seat.role, reserved=True)
|
||||
return HttpResponse(status=200)
|
||||
|
||||
|
||||
@login_required
|
||||
def select_sig(request, room_id):
|
||||
if request.method != "POST":
|
||||
|
||||
Reference in New Issue
Block a user