my-sea spectator: render owner's draw as the identical interactive cross stage; owner seated in 1C when paid OR drawn — TDD
Phase 1 + 2 of the my-sea spectator/voice batch (user-spec 2026-05-29). ── Phase 1: spectator VIEW DRAW parity ── The visitor's VIEW DRAW rendered _my_sea_readonly_draw.html — a flat `.my-sea-scroll` strip that, out of its applet context, blew a single card up to fill the viewport. It now renders the SAME `.my-sea-cross` picker + `_sea_stage` modal the owner sees, populated from the owner's draw, read-only but fully interactive (click card → magnified stage, hover, SPIN, FYI). No FLIP / DEL / AUTO DRAW / deck-stacks / spread combobox — the visitor watches. - `_saved_by_position(saved_hand)` extracted as a shared helper (owner picker + spectator render build the IDENTICAL cross); my_sea refactored onto it. - my_sea_visit context gains `saved_by_position`, `label_by_position`, `default_spread`, and the OWNER's `sea_deck_data` (so sea.js resolves each clicked slot's full card face for the stage). - new `_my_sea_visit_cross.html` mirrors the owner cross + includes `_sea_stage` under `#id_sea_overlay`; my_sea_visit.html embeds the owner deck JSON + loads stage-card.js + sea.js + a trimmed seed IIFE (reconstructs SeaDeal's `_seaHand` from the filled slots so each card is clickable into the stage). - deletes the obsolete `_my_sea_readonly_draw.html`. ── Phase 2: owner 1C seating ── The owner is "seated" in 1C whenever committed to a draw cycle — paid for one (deposit reserved / paid-through credit) OR partially/completely drawn — not only once a card lands. Previously a paid-but-undrawn owner (the PAID DRAW landing) and the visitor's view of her showed the semi-opaque `.fa-ban` default. Seat 1C now carries persistent `.seated` + `.fa-circle-check` (sync on refresh; the one-shot flare just settles into it). - my_sea: new `seat1_seated = hand_non_empty or show_paid_draw`; my_sea.html seat 1C keys on it (class + data-seat-token + status icon). - my_sea_visit: `seat1_present = owner drawn OR owner paid` so the visitor sees the owner seated on the spectator hex under the same conditions. - seat flare bumped 1.5s → 2s (my-sea-seats.js GLOW_MS + _room.scss keyframe). Tests: +2 spectator-cross ITs, +1 spectator-cross FT (Phase 1); +4 owner-seat ITs, +2 visitor both-seated/owner-seated ITs, +1 owner-seating FT (Phase 2). 286 gameboard ITs/UTs green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -194,6 +194,35 @@ def toggle_game_kit_sections(request):
|
||||
return redirect("game_kit")
|
||||
|
||||
|
||||
def _saved_by_position(saved_hand):
|
||||
"""Build the per-position saved-card lookup the picker cross renders from
|
||||
(`_my_sea_slot.html`). Keyed by position slug ("lay", "cover", ...); each
|
||||
value carries the pre-resolved card fields so the template never hits the
|
||||
DB per slot. Shared by the owner picker (`my_sea`) AND the read-only
|
||||
spectator render (`my_sea_visit`) so both draw the IDENTICAL cross."""
|
||||
saved = {}
|
||||
if not saved_hand:
|
||||
return saved
|
||||
from apps.epic.models import TarotCard
|
||||
ids = [e["card_id"] for e in saved_hand]
|
||||
cards_by_id = {c.id: c for c in TarotCard.objects.filter(id__in=ids)}
|
||||
for entry in saved_hand:
|
||||
c = cards_by_id.get(entry["card_id"])
|
||||
saved[entry["position"]] = {
|
||||
"card_id": entry["card_id"],
|
||||
"reversed": entry.get("reversed", False),
|
||||
"polarity": entry.get("polarity", "gravity"),
|
||||
"corner_rank": c.corner_rank if c else "",
|
||||
"suit_icon": c.suit_icon if c else "",
|
||||
"has_card_images": (c.deck_variant.has_card_images
|
||||
if c and c.deck_variant else False),
|
||||
"image_url": c.image_url if c else "",
|
||||
"arcana": c.arcana if c else "",
|
||||
"name": c.name if c else "",
|
||||
}
|
||||
return saved
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
def my_sea(request):
|
||||
"""Shell view for the My Sea standalone page.
|
||||
@@ -295,29 +324,9 @@ def my_sea(request):
|
||||
# entry=saved_by_position.lay %}` block. The card fields (corner_rank,
|
||||
# suit_icon) come pre-resolved so the template doesn't need to do a
|
||||
# DB lookup per slot.
|
||||
saved_by_position = {}
|
||||
if saved_hand:
|
||||
from apps.epic.models import TarotCard
|
||||
ids = [e["card_id"] for e in saved_hand]
|
||||
cards_by_id = {c.id: c for c in TarotCard.objects.filter(id__in=ids)}
|
||||
for entry in saved_hand:
|
||||
c = cards_by_id.get(entry["card_id"])
|
||||
saved_by_position[entry["position"]] = {
|
||||
"card_id": entry["card_id"],
|
||||
"reversed": entry.get("reversed", False),
|
||||
"polarity": entry.get("polarity", "gravity"),
|
||||
"corner_rank": c.corner_rank if c else "",
|
||||
"suit_icon": c.suit_icon if c else "",
|
||||
# Sprint A.7-polish: extra fields for image-mode slot render
|
||||
# in `_my_sea_slot.html`. Empty strings when the card's deck
|
||||
# has no images (legacy text-only); template branches on
|
||||
# `has_card_images` to pick render mode.
|
||||
"has_card_images": (c.deck_variant.has_card_images
|
||||
if c and c.deck_variant else False),
|
||||
"image_url": c.image_url if c else "",
|
||||
"arcana": c.arcana if c else "",
|
||||
"name": c.name if c else "",
|
||||
}
|
||||
# Per-position lookup for `_my_sea_slot.html` — see `_saved_by_position`
|
||||
# (shared with the read-only spectator render so both draw the same cross).
|
||||
saved_by_position = _saved_by_position(saved_hand)
|
||||
|
||||
# @taxman Brief payloads w. NVM-persistence (user-spec 2026-05-26). The
|
||||
# FREE DRAW Brief surfaces ONLY when an active draw exists AND the user
|
||||
@@ -371,6 +380,12 @@ def my_sea(request):
|
||||
"saved_by_position": saved_by_position,
|
||||
"next_free_draw_at": next_free_draw_at,
|
||||
"hand_complete": hand_complete,
|
||||
# Owner seated in 1C whenever committed to a draw cycle — paid for one
|
||||
# (deposit reserved OR paid-through credit, via show_paid_draw) OR has
|
||||
# cards down (hand_non_empty). Drives the persistent `.seated` +
|
||||
# `.fa-circle-check` on the landing's 1C chair (user-spec 2026-05-29);
|
||||
# a DEL'd row at GATE VIEW (empty hand, no paid credit) reads unseated.
|
||||
"seat1_seated": hand_non_empty or show_paid_draw,
|
||||
"show_picker": show_picker,
|
||||
"show_cont_draw": show_cont_draw,
|
||||
# Sub-btn .active flag for the burger fan — Sea sub-btn lights up
|
||||
@@ -871,21 +886,47 @@ def my_sea_visit(request, owner_id):
|
||||
owner_draw = active_draw_for(owner)
|
||||
sig_card, sig_reversed = _resolve_sig(owner, owner_draw)
|
||||
owner_hand_non_empty = owner_draw is not None and bool(owner_draw.hand)
|
||||
# Owner seated in 1C (as the visitor sees it) whenever she's committed to a
|
||||
# draw cycle — drawn OR paid (deposit reserved / paid-through credit). Keeps
|
||||
# the owner's 1C chair persistently `.seated` on the spectator hex even
|
||||
# before a card lands (user-spec 2026-05-29); mirrors my_sea's seat1_seated.
|
||||
owner_paid = owner_draw is not None and (
|
||||
owner_draw.deposit_token_id is not None
|
||||
or owner_draw.paid_through_at is not None
|
||||
)
|
||||
owner_seated = owner_hand_non_empty or owner_paid
|
||||
# Read-only spectator render parity (Phase 1, 2026-05-29): the visitor's
|
||||
# VIEW DRAW renders the SAME `.my-sea-cross` picker + `_sea_stage` the
|
||||
# owner sees, populated from the owner's draw. `saved_by_position` fills
|
||||
# the slots; `label_by_position` captions them per the owner's spread;
|
||||
# `sea_deck_data` is the OWNER's deck so sea.js can resolve each clicked
|
||||
# slot's full card face for the magnified stage.
|
||||
owner_slots = latest_draw_slots(owner)
|
||||
return render(request, "apps/gameboard/my_sea_visit.html", {
|
||||
"spectator": True,
|
||||
"is_owner": False,
|
||||
"read_only": True,
|
||||
"owner": owner,
|
||||
"sea_invite": invite,
|
||||
"seat1_present": owner_hand_non_empty,
|
||||
"seat1_present": owner_seated,
|
||||
"seat2_present": invite.is_present,
|
||||
"owner_draw_id": owner_draw.id if owner_draw is not None else "",
|
||||
"voice_active": invite.voice_active,
|
||||
"voice_room_id": f"mysea-{owner.id}",
|
||||
"significator": sig_card,
|
||||
"significator_reversed": sig_reversed,
|
||||
"my_sea_slots": latest_draw_slots(owner),
|
||||
"my_sea_slots": owner_slots,
|
||||
"owner_hand_non_empty": owner_hand_non_empty,
|
||||
# Read-only cross-stage parity payload.
|
||||
"default_spread": (owner_draw.spread if owner_draw is not None
|
||||
else "situation-action-outcome"),
|
||||
"saved_by_position": _saved_by_position(
|
||||
owner_draw.hand if owner_draw is not None else []),
|
||||
"label_by_position": {s["position"]: s["label"] for s in owner_slots},
|
||||
"sea_deck_data": (
|
||||
_my_sea_deck_data(owner, exclude_id=sig_card.id if sig_card else None)
|
||||
if sig_card is not None else {"levity": [], "gravity": []}
|
||||
),
|
||||
# Owner-only controls forced off on the spectator surface.
|
||||
"sea_btn_active": False,
|
||||
"sea_first_draw_pending": False,
|
||||
|
||||
Reference in New Issue
Block a user