wired PICK SKY server-side polarity countdown via threading.Timer (tasks.py); fixed polarity_done overlay gating on refresh; cleared sig-select floats on overlay dismiss; filtered Redact events from Most Recent applet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -301,10 +301,24 @@ def _role_select_context(room, user):
|
||||
elif user_role in _GRAVITY_ROLES:
|
||||
user_polarity = 'gravity'
|
||||
|
||||
user_reservation = SigReservation.objects.filter(
|
||||
room=room, gamer=user
|
||||
).first() if user.is_authenticated else None
|
||||
ctx["user_seat"] = user_seat
|
||||
ctx["user_polarity"] = user_polarity
|
||||
ctx["user_ready"] = bool(user_reservation and user_reservation.ready)
|
||||
ctx["sig_reserve_url"] = f"/gameboard/room/{room.id}/sig-reserve"
|
||||
|
||||
# Has this gamer's polarity already had significators assigned?
|
||||
# (Other polarity still in progress — stay in SIG_SELECT but skip the overlay.)
|
||||
if user_polarity:
|
||||
_polarity_roles = _LEVITY_ROLES if user_polarity == 'levity' else _GRAVITY_ROLES
|
||||
ctx["polarity_done"] = not room.table_seats.filter(
|
||||
role__in=_polarity_roles, significator__isnull=True
|
||||
).exists()
|
||||
else:
|
||||
ctx["polarity_done"] = False
|
||||
|
||||
# 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:
|
||||
@@ -643,6 +657,21 @@ def sig_reserve(request, room_id):
|
||||
if action == "release":
|
||||
existing = SigReservation.objects.filter(room=room, gamer=request.user).first()
|
||||
released_card_id = existing.card_id if existing else None
|
||||
if existing and existing.ready:
|
||||
# Gamer released while ready — treat as an implicit WAIT NVM
|
||||
prior = room.events.filter(
|
||||
actor=request.user, verb=GameEvent.SIG_READY
|
||||
).last()
|
||||
if prior and not prior.data.get("retracted"):
|
||||
prior.data["retracted"] = True
|
||||
prior.save(update_fields=["data"])
|
||||
record(room, GameEvent.SIG_UNREADY, actor=request.user)
|
||||
polarity = existing.polarity
|
||||
all_ready = SigReservation.objects.filter(
|
||||
room=room, polarity=polarity, ready=True
|
||||
).count() == 3
|
||||
if all_ready:
|
||||
_notify_countdown_cancel(room_id, polarity, seconds_remaining=12)
|
||||
SigReservation.objects.filter(room=room, gamer=request.user).delete()
|
||||
_notify_sig_reserved(room_id, released_card_id, user_seat.role, reserved=False)
|
||||
return HttpResponse(status=200)
|
||||
@@ -699,8 +728,31 @@ def sig_ready(request, room_id):
|
||||
if action == "ready":
|
||||
if reservation is None:
|
||||
return HttpResponse(status=400)
|
||||
if reservation.ready:
|
||||
return HttpResponse(status=200) # idempotent — already ready, don't re-trigger countdown
|
||||
reservation.ready = True
|
||||
reservation.save(update_fields=["ready"])
|
||||
card = reservation.card
|
||||
if card and card.arcana == TarotCard.MIDDLE:
|
||||
_pol_prefix = "Leavened" if reservation.polarity == SigReservation.LEVITY else "Graven"
|
||||
_card_display = f"{_pol_prefix} {card.name_title}"
|
||||
elif card and card.arcana == TarotCard.MAJOR:
|
||||
_base = card.name_title.removeprefix("The ")
|
||||
_pol_suffix = "of Light" if reservation.polarity == SigReservation.LEVITY else "from the Grave"
|
||||
_card_display = f"{_base} {_pol_suffix}"
|
||||
else:
|
||||
_card_display = card.name_title if card else "a card"
|
||||
record(room, GameEvent.SIG_READY, actor=request.user,
|
||||
card_name=_card_display,
|
||||
corner_rank=card.corner_rank if card else "",
|
||||
suit_icon=card.suit_icon if card else "")
|
||||
# Retract the most recent un-retracted SIG_UNREADY (cancellation is now moot)
|
||||
prior_unready = room.events.filter(
|
||||
actor=request.user, verb=GameEvent.SIG_UNREADY
|
||||
).last()
|
||||
if prior_unready and not prior_unready.data.get("retracted"):
|
||||
prior_unready.data["retracted"] = True
|
||||
prior_unready.save(update_fields=["data"])
|
||||
|
||||
# Check if all three in this polarity are now ready
|
||||
polarity = reservation.polarity
|
||||
@@ -709,6 +761,7 @@ def sig_ready(request, room_id):
|
||||
room=room, polarity=polarity, ready=True
|
||||
).count()
|
||||
if ready_count == 3:
|
||||
from apps.epic.tasks import schedule_polarity_confirm
|
||||
# Use saved countdown_remaining if a pause was recorded, else 12
|
||||
saved = SigReservation.objects.filter(
|
||||
room=room, polarity=polarity
|
||||
@@ -716,12 +769,21 @@ def sig_ready(request, room_id):
|
||||
"countdown_remaining", flat=True
|
||||
).first()
|
||||
seconds = saved if saved is not None else 12
|
||||
schedule_polarity_confirm(str(room_id), polarity, seconds)
|
||||
_notify_countdown_start(room_id, polarity, seconds=seconds)
|
||||
|
||||
else: # unready
|
||||
if reservation is not None:
|
||||
reservation.ready = False
|
||||
reservation.save(update_fields=["ready"])
|
||||
# Mark the most recent un-retracted SIG_READY event for this actor
|
||||
prior = room.events.filter(
|
||||
actor=request.user, verb=GameEvent.SIG_READY
|
||||
).last()
|
||||
if prior and not prior.data.get("retracted"):
|
||||
prior.data["retracted"] = True
|
||||
prior.save(update_fields=["data"])
|
||||
record(room, GameEvent.SIG_UNREADY, actor=request.user)
|
||||
polarity = reservation.polarity
|
||||
|
||||
# Save remaining seconds on all polarity reservations
|
||||
@@ -733,61 +795,15 @@ def sig_ready(request, room_id):
|
||||
countdown_remaining=seconds_remaining
|
||||
)
|
||||
_notify_countdown_cancel(room_id, polarity, seconds_remaining=seconds_remaining)
|
||||
from apps.epic.tasks import cancel_polarity_confirm
|
||||
cancel_polarity_confirm(str(room_id), polarity)
|
||||
|
||||
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)
|
||||
|
||||
"""No-op: polarity confirmation is now driven server-side by threading.Timer in tasks.py."""
|
||||
return HttpResponse(status=200)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user