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:
@@ -1,45 +0,0 @@
|
||||
{# Read-only render of the owner's draw for a my-sea spectator (Phase B of #}
|
||||
{# [[my-sea-invite-voice-blueprint]]). Mirrors `_applet-my-sea.html`'s slot #}
|
||||
{# markup off the same `latest_draw_slots` payload (`my_sea_slots`), but #}
|
||||
{# standalone + with NO interactive affordances (FLIP / DEL / AUTO DRAW). #}
|
||||
<div class="my-sea-scroll my-sea-visit-scroll">
|
||||
{% for slot in my_sea_slots %}
|
||||
{% if slot.card %}
|
||||
<div class="my-sea-slot-wrap">
|
||||
<div class="my-sea-slot my-sea-slot--filled my-sea-slot--{{ slot.polarity }}{% if slot.reversed %} my-sea-slot--reversed{% endif %}{% if slot.card.deck_variant.has_card_images %} my-sea-slot--image{% endif %}"
|
||||
data-position="{{ slot.position }}"
|
||||
data-card-id="{{ slot.card.id }}"
|
||||
data-arcana-key="{{ slot.card.arcana }}">
|
||||
{% if slot.card.deck_variant.has_card_images %}
|
||||
<img class="sig-stage-card-img" src="{{ slot.card.image_url }}" alt="{{ slot.card.name }}">
|
||||
{% else %}
|
||||
<div class="fan-card-corner fan-card-corner--tl">
|
||||
<span class="fan-corner-rank">{{ slot.card.corner_rank }}</span>
|
||||
{% if slot.card.suit_icon %}<i class="fa-solid {{ slot.card.suit_icon }}"></i>{% endif %}
|
||||
</div>
|
||||
<div class="fan-card-face">
|
||||
{% if slot.face.qualifier_first %}
|
||||
<p class="fan-card-qualifier">{{ slot.face.qualifier }}</p>
|
||||
<p class="fan-card-name">{{ slot.face.title }}</p>
|
||||
{% else %}
|
||||
<p class="fan-card-name">{{ slot.face.title }}</p>
|
||||
<p class="fan-card-qualifier">{{ slot.face.qualifier }}</p>
|
||||
{% endif %}
|
||||
<p class="fan-card-arcana">{{ slot.card.get_arcana_display }}</p>
|
||||
</div>
|
||||
<div class="fan-card-corner fan-card-corner--br">
|
||||
<span class="fan-corner-rank">{{ slot.card.corner_rank }}</span>
|
||||
{% if slot.card.suit_icon %}<i class="fa-solid {{ slot.card.suit_icon }}"></i>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="my-sea-slot-label">{{ slot.label }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="my-sea-slot-wrap">
|
||||
<div class="my-sea-slot my-sea-slot--empty" data-position="{{ slot.position }}"></div>
|
||||
<span class="my-sea-slot-label my-sea-slot-label--empty">{{ slot.label }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
{# Read-only spectator render of the owner's draw (Phase 1, 2026-05-29). #}
|
||||
{# Reuses the owner's `.sea-cross.my-sea-cross` picker DOM (sig center + 6 #}
|
||||
{# position cells via _my_sea_slot.html) + the shared `_sea_stage.html` #}
|
||||
{# modal, bound by sea.js for click→stage + SPIN + FYI + hover — so the #}
|
||||
{# visitor sees IDENTICALLY what the owner sees on her own my_sea, just #}
|
||||
{# without the FLIP / DEL / AUTO DRAW / deck-stacks / spread combobox #}
|
||||
{# (the visitor only watches). `#id_sea_overlay` is what sea.js binds to; #}
|
||||
{# `--locked` mirrors the owner's hand-complete picker so no draw #}
|
||||
{# affordances surface even if a stray handler fires. #}
|
||||
<div class="my-sea-picker my-sea-picker--locked my-sea-visit-picker"
|
||||
id="id_sea_overlay" data-spectator="true">
|
||||
<div class="sea-cards-col">
|
||||
<div class="sea-cross my-sea-cross" data-spread="{{ default_spread }}">
|
||||
<div class="sea-crucifix-cell sea-pos-crown">
|
||||
<span class="sea-pos-label" data-position="crown">{{ label_by_position.crown }}</span>
|
||||
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="crown" saved=saved_by_position.crown crossing=False %}
|
||||
</div>
|
||||
<div class="sea-crucifix-cell sea-pos-leave">
|
||||
<span class="sea-pos-label" data-position="leave">{{ label_by_position.leave }}</span>
|
||||
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="leave" saved=saved_by_position.leave crossing=False %}
|
||||
</div>
|
||||
<div class="sea-crucifix-cell sea-pos-core">
|
||||
<div class="sig-stage-card sea-sig-card{% if significator.deck_variant.has_card_images %} sig-stage-card--image{% endif %}"
|
||||
data-card-id="{{ significator.id }}"
|
||||
data-arcana-key="{{ significator.arcana }}">
|
||||
{% if significator.deck_variant.has_card_images %}
|
||||
<img class="sig-stage-card-img" src="{{ significator.image_url }}" alt="{{ significator.name }}">
|
||||
{% else %}
|
||||
<span class="fan-corner-rank">{{ significator.corner_rank }}</span>
|
||||
{% if significator.suit_icon %}<i class="fa-solid {{ significator.suit_icon }}"></i>{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="sea-pos-cover">
|
||||
<span class="sea-pos-label" data-position="cover">{{ label_by_position.cover }}</span>
|
||||
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="cover" saved=saved_by_position.cover crossing=False %}
|
||||
</div>
|
||||
<div class="sea-pos-cross">
|
||||
<span class="sea-pos-label" data-position="cross">{{ label_by_position.cross }}</span>
|
||||
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="cross" saved=saved_by_position.cross crossing=True %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sea-crucifix-cell sea-pos-loom">
|
||||
<span class="sea-pos-label" data-position="loom">{{ label_by_position.loom }}</span>
|
||||
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="loom" saved=saved_by_position.loom crossing=False %}
|
||||
</div>
|
||||
<div class="sea-crucifix-cell sea-pos-lay">
|
||||
<span class="sea-pos-label" data-position="lay">{{ label_by_position.lay }}</span>
|
||||
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="lay" saved=saved_by_position.lay crossing=False %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "apps/gameboard/_partials/_sea_stage.html" %}
|
||||
</div>
|
||||
@@ -94,10 +94,10 @@
|
||||
{# semantics clean. `.position-status-icon` + #}
|
||||
{# `.fa-ban` are unchanged — already role- #}
|
||||
{# agnostic in _room.scss. #}
|
||||
<div class="table-seat{% if n == '1' and hand_non_empty %} seated{% endif %}" data-slot="{{ n }}"{% if n == '1' and hand_non_empty %} data-seat-token="owner-{{ request.user.id }}-{{ active_draw.id }}"{% endif %}>
|
||||
<div class="table-seat{% if n == '1' and seat1_seated %} seated{% endif %}" data-slot="{{ n }}"{% if n == '1' and seat1_seated %} data-seat-token="owner-{{ request.user.id }}-{{ active_draw.id }}"{% endif %}>
|
||||
<i class="fa-solid fa-chair"></i>
|
||||
<span class="seat-position-label">{{ n }}C</span>
|
||||
<i class="position-status-icon fa-solid {% if n == '1' and hand_non_empty %}fa-circle-check{% else %}fa-ban{% endif %}"></i>
|
||||
<i class="position-status-icon fa-solid {% if n == '1' and seat1_seated %}fa-circle-check{% else %}fa-ban{% endif %}"></i>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -67,9 +67,12 @@
|
||||
</div>
|
||||
|
||||
{% if seat2_present %}
|
||||
{# Owner's draw, read-only. Hidden until VIEW DRAW toggles it in. #}
|
||||
{# Owner's draw, read-only. Hidden until VIEW DRAW toggles it in. Renders #}
|
||||
{# the SAME interactive cross stage the owner sees (click→stage, hover, #}
|
||||
{# SPIN, FYI) off the owner's draw payload — see _my_sea_visit_cross.html. #}
|
||||
<div id="id_my_sea_visit_draw" class="my-sea-visit-draw" style="display:none">
|
||||
{% include "apps/gameboard/_partials/_my_sea_readonly_draw.html" %}
|
||||
{{ sea_deck_data|json_script:"id_my_sea_deck" }}
|
||||
{% include "apps/gameboard/_partials/_my_sea_visit_cross.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -84,6 +87,53 @@
|
||||
{% block scripts %}
|
||||
<script src="{% static 'apps/gameboard/my-sea-seats.js' %}"></script>
|
||||
<script src="{% static 'apps/epic/burger-btn.js' %}"></script>
|
||||
{% if seat2_present %}
|
||||
{# Read-only cross stage — StageCard + SeaDeal bind to #id_sea_overlay #}
|
||||
{# (inside #id_my_sea_visit_draw) for click→stage + SPIN + FYI + hover. #}
|
||||
{# A trimmed seed IIFE (no picker/FLIP/DEL machinery) reconstructs #}
|
||||
{# SeaDeal's `_seaHand` from the server-rendered filled slots so each #}
|
||||
{# card is clickable into the magnified stage — same logic as my_sea's #}
|
||||
{# saved-hand restore, just without the draw affordances. #}
|
||||
<script src="{% static 'apps/epic/stage-card.js' %}"></script>
|
||||
<script src="{% static 'apps/epic/sea.js' %}"></script>
|
||||
<script>
|
||||
(function () {
|
||||
function _seed() {
|
||||
if (!window.SeaDeal || !window.SeaDeal.seedHand) return;
|
||||
var cross = document.querySelector('.my-sea-cross');
|
||||
if (!cross) return;
|
||||
var deckEl = document.getElementById('id_my_sea_deck');
|
||||
var deck = {};
|
||||
try { deck = JSON.parse((deckEl && deckEl.textContent) || '{}'); } catch (e) {}
|
||||
var byId = {};
|
||||
(deck.levity || []).concat(deck.gravity || []).forEach(function (c) {
|
||||
byId[c.id] = c;
|
||||
});
|
||||
var seed = {};
|
||||
cross.querySelectorAll('.sea-card-slot.sea-card-slot--filled').forEach(function (slot) {
|
||||
var posName = slot.dataset.posKey;
|
||||
var card = byId[parseInt(slot.dataset.cardId, 10)];
|
||||
if (!posName || !card) return;
|
||||
var slotCard = {};
|
||||
for (var k in card) {
|
||||
if (Object.prototype.hasOwnProperty.call(card, k)) slotCard[k] = card[k];
|
||||
}
|
||||
// Per-instance reversed/polarity come from the slot's DOM
|
||||
// class (the owner's saved hand, not the deck-shuffle axis).
|
||||
slotCard.reversed = slot.classList.contains('sea-card-slot--reversed');
|
||||
var isLevity = slot.classList.contains('sea-card-slot--levity');
|
||||
seed[posName] = { card: slotCard, isLevity: isLevity };
|
||||
});
|
||||
window.SeaDeal.seedHand(seed);
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', _seed);
|
||||
} else {
|
||||
_seed();
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function () {
|
||||
// VIEW DRAW toggles the read-only draw against the table hex.
|
||||
|
||||
Reference in New Issue
Block a user