Sky/Sea Select: ?seat-aware so a CARTE owner drives all 6 seats — TDD

A Carte Blanche gamer owns all 6 seats but could only cast sky / draw sea for ONE:
the sky/sea state IS per-seat (Character.seat), but the code resolved to the fixed
canonical (PC-first) seat, ignoring the ?seat switched to via the GATE VIEW pos-
circles — so the tray switched but the sky wheel / sea spread stuck on the
canonical seat + saves wrote back to it. Mirrors Sig Select's existing ?seat path.

- generalize `_acting_sig_seat` → `_acting_seat` (logic is sig-agnostic; 3 callers)
- `_role_select_context` SKY_SELECT branch keys off `selected_seat` (the ?seat-aware
  seat, already computed) instead of `_canonical_seat`: user_polarity,
  confirmed_char, user_seat_role, my_tray_sig, saved_by_position, saved_sea_spread,
  sea_default_spread, hand_complete, sea_back_image_url
- sky_save / sky_delete / sea_save / sea_delete / sea_deck resolve the acting seat
  via `_acting_seat(…, request.GET.get("seat"))`; sea_partial threads seat_param
- the sky + sea felts carry `?seat={{ current_slot }}` on their save/delete/deck/
  sea_partial action URLs so the POSTs target the switched-to seat
- single-seat flow unchanged (no ?seat → canonical fallback)
- ITs: CARTE owner — ?seat switches the displayed spread; sea_save/sky_save target
  the switched seat leaving the canonical seat's Character intact; felt URLs carry
  ?seat. 949 epic+gameboard ITs green.

; FLIP tint tweak (parallel edit): drop the polarity border, bump the flipped-back
overlay --quiUser/--terUser to 0.6 alpha

[[project-sig-select-seat-switch-open-problems]] [[project-deck-segment-model]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-07 22:44:06 -04:00
parent de59cb7e69
commit d09dca56c0
5 changed files with 138 additions and 33 deletions

View File

@@ -176,11 +176,11 @@ def _canonical_user_seat(room, user):
return room.table_seats.filter(gamer=user).order_by(_SIG_SEAT_ORDERING).first()
def _acting_sig_seat(room, user, seat_param):
"""The seat a SIG_SELECT action (reserve / ready / release) targets: the
`?seat=N` override when N is one of the user's owned slots, else the
canonical seat. Lets a CARTE multi-seat owner hold + ready a distinct sig
per owned seat (one reservation per seat)."""
def _acting_seat(room, user, seat_param):
"""The seat an action targets: the `?seat=N` override when N is one of the
user's owned slots, else the canonical seat. Lets a CARTE multi-seat owner
act per-seat — a distinct sig reservation in SIG_SELECT, a distinct sky/sea
Character in SKY_SELECT — across all six owned seats."""
seat = _canonical_user_seat(room, user)
if seat_param:
try:
@@ -630,7 +630,14 @@ def _role_select_context(room, user, seat_param=None):
ctx["sig_cards"] = []
if room.table_status == Room.SKY_SELECT:
user_role = _canonical_seat.role if _canonical_seat else None
# CARTE seat-switch: the sky/sea state is per-SEAT (Character.seat), so key
# this whole block off the seat the viewer switched to (`selected_seat` =
# ?seat-aware), NOT the fixed canonical seat — without it the tray switched
# but the sky wheel / sea spread stuck on the canonical seat, so a CARTE
# owner couldn't drive his other 5 seats thru CAST SKY / DRAW SEA
# (user-flagged 2026-06-07). Falls back to canonical if unset.
_sky_seat = selected_seat or _canonical_seat
user_role = _sky_seat.role if _sky_seat else None
user_polarity = None
if user_role in _LEVITY_ROLES:
user_polarity = 'levity'
@@ -639,15 +646,15 @@ def _role_select_context(room, user, seat_param=None):
ctx["user_polarity"] = user_polarity
confirmed_char = (
Character.objects.filter(
seat=_canonical_seat,
seat=_sky_seat,
confirmed_at__isnull=False,
retired_at__isnull=True,
).first()
if _canonical_seat else None
if _sky_seat else None
)
sky_confirmed = confirmed_char is not None
ctx["sky_confirmed"] = sky_confirmed
ctx["user_seat_role"] = _canonical_seat.role if _canonical_seat else ''
ctx["user_seat_role"] = _sky_seat.role if _sky_seat else ''
# Burger-fan Sky sub-btn: ACTIVE once the sky is saved (confirmed) — the
# burger then pulses thrice (--priTk) on load + the sub-btn re-opens the
# saved wheel. `saved_sky_json` primes that reopen so the felt draws the
@@ -660,7 +667,7 @@ def _role_select_context(room, user, seat_param=None):
)
if sky_confirmed:
# Fall back to seat.significator for Characters created before the sync was added
ctx["my_tray_sig"] = confirmed_char.significator or _canonical_seat.significator
ctx["my_tray_sig"] = confirmed_char.significator or _sky_seat.significator
# DRAW SEA persistence — pre-render the saved Celtic-Cross hand from
# the seat's Character.celtic_cross so a reload lands the filled
# cross + the right spread (mirrors my-sea's saved_by_position, but
@@ -684,7 +691,7 @@ def _role_select_context(room, user, seat_param=None):
# NOT request.user.equipped_deck (which `select_role` nulls out once
# the deck is contributed to the room → the back-img silently never
# rendered in the gameroom + FLIP no-op'd). User-flagged 2026-06-07.
_back_deck = _canonical_seat.deck_variant if _canonical_seat else None
_back_deck = _sky_seat.deck_variant if _sky_seat else None
ctx["sea_back_image_url"] = (
_back_deck.back_image_url
if (_back_deck and _back_deck.has_card_images) else ""
@@ -1344,7 +1351,7 @@ def sig_reserve(request, room_id):
# CARTE per-seat sig: honor a ?seat=N override (carried on the reserve URL)
# so the hold targets the SELECTED owned seat, not the canonical PC one.
user_seat = _acting_sig_seat(room, request.user, request.GET.get("seat"))
user_seat = _acting_seat(room, request.user, request.GET.get("seat"))
if not user_seat or not user_seat.role:
return HttpResponse(status=403)
@@ -1440,7 +1447,7 @@ def sig_ready(request, room_id):
return HttpResponse(status=400)
# Per-seat ready: a CARTE multi-seat owner readies each owned seat's sig
# independently (the ?seat=N rides the ready URL, like the reserve URL).
user_seat = _acting_sig_seat(room, request.user, request.GET.get("seat"))
user_seat = _acting_seat(room, request.user, request.GET.get("seat"))
if user_seat is None:
return HttpResponse(status=403)
@@ -1745,7 +1752,7 @@ def sky_save(request, room_id):
return HttpResponse(status=405)
room = Room.objects.get(id=room_id)
seat = _canonical_user_seat(room, request.user)
seat = _acting_seat(room, request.user, request.GET.get("seat"))
if seat is None:
return HttpResponse(status=403)
@@ -1797,7 +1804,7 @@ def sky_delete(request, room_id):
if request.method != 'POST':
return HttpResponse(status=405)
room = Room.objects.get(id=room_id)
seat = _canonical_user_seat(room, request.user)
seat = _acting_seat(room, request.user, request.GET.get("seat"))
if seat is None:
return HttpResponseForbidden()
Character.objects.filter(seat=seat, retired_at__isnull=True).delete()
@@ -1829,7 +1836,7 @@ def sea_save(request, room_id):
if request.method != 'POST':
return HttpResponse(status=405)
room = Room.objects.get(id=room_id)
seat = _canonical_user_seat(room, request.user)
seat = _acting_seat(room, request.user, request.GET.get("seat"))
if seat is None:
return HttpResponse(status=403)
try:
@@ -1856,7 +1863,7 @@ def sea_delete(request, room_id):
if request.method != 'POST':
return HttpResponse(status=405)
room = Room.objects.get(id=room_id)
seat = _canonical_user_seat(room, request.user)
seat = _acting_seat(room, request.user, request.GET.get("seat"))
if seat is None:
return HttpResponseForbidden()
char = _confirmed_character_for(seat)
@@ -1876,7 +1883,7 @@ def sea_deck(request, room_id):
"""
import random as _random
room = Room.objects.get(id=room_id)
seat = _canonical_user_seat(room, request.user)
seat = _acting_seat(room, request.user, request.GET.get("seat"))
if seat is None:
return HttpResponse(status=403)
@@ -1909,7 +1916,9 @@ def sea_deck(request, room_id):
def sea_partial(request, room_id):
"""Return the rendered sea overlay partial for in-page injection after sky confirm."""
room = Room.objects.get(id=room_id)
ctx = _role_select_context(room, request.user)
# ?seat threads through so the injected felt (CARTE seat-switch) reflects the
# selected seat's spread + carries the right seat onto its own action URLs.
ctx = _role_select_context(room, request.user, request.GET.get("seat"))
if not ctx.get('sky_confirmed'):
return HttpResponse(status=403)
ctx['room'] = room