added PICK SKY ready gate: SigReservation.ready + countdown_remaining fields, Room.SKY_SELECT status + sig_select_started_at, sig_ready + sig_confirm views, WS notifiers for countdown_start/cancel/polarity_room_done/pick_sky_available, migration 0031, PICK SKY btn in hex center at SKY_SELECT, tray cell 2 sig card placeholder; FTs SRG1-8 written (pending JS/consumer)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-09 01:17:24 -04:00
parent 3800c5bdad
commit df421fb6c0
9 changed files with 1003 additions and 1 deletions

View File

@@ -93,6 +93,34 @@ def _notify_sig_reserved(room_id, card_id, role, reserved):
)
def _notify_countdown_start(room_id, polarity, *, seconds):
async_to_sync(get_channel_layer().group_send)(
f'cursors_{room_id}_{polarity}',
{'type': 'countdown_start', 'polarity': polarity, 'seconds': seconds},
)
def _notify_countdown_cancel(room_id, polarity, *, seconds_remaining):
async_to_sync(get_channel_layer().group_send)(
f'cursors_{room_id}_{polarity}',
{'type': 'countdown_cancel', 'polarity': polarity, 'seconds_remaining': seconds_remaining},
)
def _notify_polarity_room_done(room_id, polarity):
async_to_sync(get_channel_layer().group_send)(
f'room_{room_id}',
{'type': 'polarity_room_done', 'polarity': polarity},
)
def _notify_pick_sky_available(room_id):
async_to_sync(get_channel_layer().group_send)(
f'room_{room_id}',
{'type': 'pick_sky_available'},
)
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
_SIG_SEAT_ORDERING = Case(
@@ -260,6 +288,10 @@ def _role_select_context(room, user):
"gate_positions": _gate_positions(room),
"slots": room.gate_slots.order_by("slot_number"),
}
# Tray cell 2: sig card (set once polarity group confirms)
_canonical_seat = _canonical_user_seat(room, user) if user.is_authenticated else None
ctx["my_tray_sig"] = _canonical_seat.significator if _canonical_seat else None
if room.table_status == Room.SIG_SELECT:
user_seat = _canonical_user_seat(room, user) if user.is_authenticated else None
user_role = user_seat.role if user_seat else None
@@ -647,6 +679,118 @@ def sig_reserve(request, room_id):
return HttpResponse(status=200)
@login_required
def sig_ready(request, room_id):
"""Toggle ready/unready for the polarity-room countdown.
POST body: action=ready|unready [, seconds_remaining=<int>]
"""
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 = _canonical_user_seat(room, request.user)
if user_seat is None:
return HttpResponse(status=403)
action = request.POST.get("action", "ready")
reservation = SigReservation.objects.filter(room=room, gamer=request.user).first()
if action == "ready":
if reservation is None:
return HttpResponse(status=400)
reservation.ready = True
reservation.save(update_fields=["ready"])
# Check if all three in this polarity are now ready
polarity = reservation.polarity
polarity_roles = _LEVITY_ROLES if polarity == SigReservation.LEVITY else _GRAVITY_ROLES
ready_count = SigReservation.objects.filter(
room=room, polarity=polarity, ready=True
).count()
if ready_count == 3:
# Use saved countdown_remaining if a pause was recorded, else 12
saved = SigReservation.objects.filter(
room=room, polarity=polarity
).exclude(countdown_remaining__isnull=True).values_list(
"countdown_remaining", flat=True
).first()
seconds = saved if saved is not None else 12
_notify_countdown_start(room_id, polarity, seconds=seconds)
else: # unready
if reservation is not None:
reservation.ready = False
reservation.save(update_fields=["ready"])
polarity = reservation.polarity
# Save remaining seconds on all polarity reservations
try:
seconds_remaining = int(request.POST.get("seconds_remaining", 12))
except (TypeError, ValueError):
seconds_remaining = 12
SigReservation.objects.filter(room=room, polarity=polarity).update(
countdown_remaining=seconds_remaining
)
_notify_countdown_cancel(room_id, polarity, seconds_remaining=seconds_remaining)
return HttpResponse(status=200)
@login_required
def sig_confirm(request, room_id):
"""Client posts this when the polarity-room countdown reaches zero.
POST body: polarity=levity|gravity
Sets significators on the three seats and broadcasts polarity_room_done.
When both polarities are confirmed, broadcasts pick_sky_available and
transitions the room to SKY_SELECT.
"""
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 = _canonical_user_seat(room, request.user)
if user_seat is None:
return HttpResponse(status=403)
seat_polarity = SigReservation.LEVITY if user_seat.role in _LEVITY_ROLES else SigReservation.GRAVITY
polarity = request.POST.get("polarity", seat_polarity)
polarity_roles = _LEVITY_ROLES if polarity == SigReservation.LEVITY else _GRAVITY_ROLES
# Idempotency: if all seats in this polarity already have significators, skip
already_done = not room.table_seats.filter(
role__in=polarity_roles, significator__isnull=True
).exists()
if already_done:
return HttpResponse(status=200)
# Guard: all three must be ready
ready_reservations = list(
SigReservation.objects.filter(room=room, polarity=polarity, ready=True)
.select_related("seat", "card")
)
if len(ready_reservations) < 3:
return HttpResponse(status=400)
# Set significators from reservations
for res in ready_reservations:
if res.seat:
res.seat.significator = res.card
res.seat.save(update_fields=["significator"])
_notify_polarity_room_done(room_id, polarity)
# Check if both polarities are now confirmed
all_done = not room.table_seats.filter(significator__isnull=True).exists()
if all_done:
room.table_status = Room.SKY_SELECT
room.save(update_fields=["table_status"])
_notify_pick_sky_available(room_id)
return HttpResponse(status=200)
@login_required
def select_sig(request, room_id):
if request.method != "POST":