Tray follows the ?seat-selected seat, not the canonical PC seat
A multi-seat (CARTE) gamer clicking a pos-circle switched the sig overlay (via the ?seat=N override) but the tray stayed pinned to the role-canonical PC seat — my_tray_role read assigned_seats[0] (role-sorted, always PC) and my_tray_sig read _canonical_user_seat. So every circle put the PC icon in the tray regardless of which one was clicked. Re-point both tray keys to the seat occupying current_slot (the acting seat). Single-seat gamers are unaffected — their lone slot IS current_slot, so selected_seat == their canonical seat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -810,6 +810,81 @@ class PositionTooltipCarteRenderTest(TestCase):
|
|||||||
self.assertNotIn("drop-token-btn", content)
|
self.assertNotIn("drop-token-btn", content)
|
||||||
|
|
||||||
|
|
||||||
|
class CarteTrayFollowsSelectedSeatTest(TestCase):
|
||||||
|
"""A multi-seat (CARTE) gamer's tray must mirror the seat at the
|
||||||
|
?seat-selected pos-circle (current_slot), not the canonical PC seat.
|
||||||
|
|
||||||
|
Regression: my_tray_role / my_tray_sig were pinned to the role-sorted
|
||||||
|
assigned_seats[0] (always PC) and _canonical_user_seat, so clicking ANY
|
||||||
|
pos-circle put the PC icon in the tray. The seat-switch (?seat=N) only
|
||||||
|
re-pointed the sig OVERLAY; the tray ignored it entirely."""
|
||||||
|
|
||||||
|
# Reverse canonical pick order: select_role fills the lowest open slot
|
||||||
|
# each turn, so picking BC,AC,SC,EC,NC,PC seats them at slots 1..6.
|
||||||
|
SLOT_ROLES = {1: "BC", 2: "AC", 3: "SC", 4: "EC", 5: "NC", 6: "PC"}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.viewer = User.objects.create(email="disco@test.io", username="disco")
|
||||||
|
self.deck, _ = DeckVariant.objects.get_or_create(
|
||||||
|
slug="earthman",
|
||||||
|
defaults={"name": "Earthman", "card_count": 106, "is_default": True},
|
||||||
|
)
|
||||||
|
self.room = Room.objects.create(name="Carte Room", owner=self.viewer)
|
||||||
|
self.room.gate_slots.update(
|
||||||
|
gamer=self.viewer, status=GateSlot.FILLED,
|
||||||
|
filled_at=timezone.now(), debited_token_type=Token.CARTE,
|
||||||
|
)
|
||||||
|
Token.objects.create(
|
||||||
|
user=self.viewer, token_type=Token.CARTE,
|
||||||
|
current_room=self.room, slots_claimed=6,
|
||||||
|
)
|
||||||
|
self.room.gate_status = Room.OPEN
|
||||||
|
self.room.table_status = Room.ROLE_SELECT
|
||||||
|
self.room.save()
|
||||||
|
for n, role in self.SLOT_ROLES.items():
|
||||||
|
TableSeat.objects.create(
|
||||||
|
room=self.room, gamer=self.viewer, slot_number=n,
|
||||||
|
role=role, deck_variant=self.deck,
|
||||||
|
)
|
||||||
|
self.client.force_login(self.viewer)
|
||||||
|
self.room_url = reverse("epic:room", kwargs={"room_id": self.room.id})
|
||||||
|
|
||||||
|
def test_seat_param_points_tray_role_at_that_seat(self):
|
||||||
|
# ?seat=4 → pos-circle 4 holds EC, so the tray role card is EC, not PC.
|
||||||
|
response = self.client.get(self.room_url + "?seat=4")
|
||||||
|
self.assertEqual(response.context["my_tray_role"], "EC")
|
||||||
|
|
||||||
|
def test_each_owned_circle_yields_its_own_seat_role(self):
|
||||||
|
for n, role in self.SLOT_ROLES.items():
|
||||||
|
response = self.client.get(self.room_url + f"?seat={n}")
|
||||||
|
self.assertEqual(
|
||||||
|
response.context["my_tray_role"], role,
|
||||||
|
f"pos-circle {n} should put role {role} in the tray",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_default_no_seat_tray_follows_lowest_owned_circle(self):
|
||||||
|
# No ?seat → current_slot is the lowest owned pos-circle (1 = BC),
|
||||||
|
# NOT the role-canonical PC seat (slot 6).
|
||||||
|
response = self.client.get(self.room_url)
|
||||||
|
self.assertEqual(response.context["my_tray_role"], "BC")
|
||||||
|
|
||||||
|
def test_seat_param_points_tray_sig_at_that_seat(self):
|
||||||
|
sig = TarotCard.objects.create(
|
||||||
|
deck_variant=self.deck, slug="ec-sig",
|
||||||
|
arcana="MIDDLE", suit="BRANDS", number=13, name="Queen of Brands",
|
||||||
|
)
|
||||||
|
ec_seat = TableSeat.objects.get(room=self.room, slot_number=4)
|
||||||
|
ec_seat.significator = sig
|
||||||
|
ec_seat.save(update_fields=["significator"])
|
||||||
|
# Switch to EC's circle → its sig rides the tray.
|
||||||
|
response = self.client.get(self.room_url + "?seat=4")
|
||||||
|
self.assertEqual(response.context["my_tray_sig"], sig)
|
||||||
|
# The PC circle (slot 6) has no sig → switching there shows none
|
||||||
|
# (proving the tray no longer falls back to a single canonical seat).
|
||||||
|
response = self.client.get(self.room_url + "?seat=6")
|
||||||
|
self.assertIsNone(response.context["my_tray_sig"])
|
||||||
|
|
||||||
|
|
||||||
class PickRolesViewTest(TestCase):
|
class PickRolesViewTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.founder = User.objects.create(email="founder@test.io")
|
self.founder = User.objects.create(email="founder@test.io")
|
||||||
|
|||||||
@@ -458,7 +458,16 @@ def _role_select_context(room, user, seat_param=None):
|
|||||||
if _turn is not None and _turn.slot_number == _seat_n
|
if _turn is not None and _turn.slot_number == _seat_n
|
||||||
else "ineligible"
|
else "ineligible"
|
||||||
)
|
)
|
||||||
_my_role = assigned_seats[0].role if assigned_seats else None
|
# The tray mirrors the seat the viewer is ACTING AS — the seat occupying
|
||||||
|
# current_slot (the ?seat-selected pos-circle, else their lowest owned) —
|
||||||
|
# NOT the role-canonical PC seat. So a multi-seat (CARTE) gamer's tray
|
||||||
|
# follows whichever circle they switched to. A single-seat gamer's lone
|
||||||
|
# slot IS current_slot, so selected_seat == their canonical seat (no-op).
|
||||||
|
selected_seat = (
|
||||||
|
room.table_seats.filter(gamer=user, slot_number=current_slot).first()
|
||||||
|
if user.is_authenticated and current_slot is not None else None
|
||||||
|
)
|
||||||
|
_my_role = selected_seat.role if selected_seat else None
|
||||||
# `equipped_deck_id` gates the JS role-select guard (role-select.js L165).
|
# `equipped_deck_id` gates the JS role-select guard (role-select.js L165).
|
||||||
# Falls back to ANY of the user's seats in this room w. deck_variant set
|
# Falls back to ANY of the user's seats in this room w. deck_variant set
|
||||||
# — covers the CARTE multi-seat return path where the first role-pick
|
# — covers the CARTE multi-seat return path where the first role-pick
|
||||||
@@ -517,7 +526,10 @@ def _role_select_context(room, user, seat_param=None):
|
|||||||
|
|
||||||
# Tray cell 2: sig card (set once polarity group confirms)
|
# Tray cell 2: sig card (set once polarity group confirms)
|
||||||
_canonical_seat = _canonical_user_seat(room, user) if user.is_authenticated else None
|
_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
|
# Sig cell follows the acting seat too (see selected_seat above), so a CARTE
|
||||||
|
# gamer sees the sig of the circle they switched to. SKY_SELECT overrides
|
||||||
|
# this below from the confirmed Character.
|
||||||
|
ctx["my_tray_sig"] = selected_seat.significator if selected_seat else None
|
||||||
|
|
||||||
if room.table_status == Room.SIG_SELECT:
|
if room.table_status == Room.SIG_SELECT:
|
||||||
user_seat = _canonical_user_seat(room, user) if user.is_authenticated else None
|
user_seat = _canonical_user_seat(room, user) if user.is_authenticated else None
|
||||||
|
|||||||
Reference in New Issue
Block a user