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:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user