PICK SEA Sprint C: sea stage card viewer — FLIP in, SPIN/FYI, deposit/re-expand — TDD
- sea.js: SeaDeal module — openStage() shows big card viewer w. flip-in animation; SPIN toggles stage-card--reversed; FYI shows energies/operations (Energy/Operation titles, PRV/NXT nav); backdrop click deposits card to slot; click deposited slot re-opens stage; resetHand() clears hand on DEL - sea_deck view: adds name_group/name_title/reversal/keywords_upright/keywords_reversed/ energies/operations to each card dict (full sig-select stage data set) - _sea_overlay.html: data-sea-user-polarity attr; sea stage HTML (sig-stage-card shell + fan-card-face-upright/reversal structure + sea-stat-block w. SPIN/FYI/PRV/NXT); FLIP click calls SeaDeal.openStage(); _fillPos removed (sea.js handles slot fill); _reset calls SeaDeal.resetHand() - room.html: sea.js included alongside sig-select.js - _card-deck.scss: sea-stage layout (fixed overlay, backdrop, content row); sea-stage-card w. @keyframes sea-flip-in (3D rotateY perspective); sea-stat-block scoped styles incl. SPIN/FYI btns, stat faces, sig-info FYI panel - SeaDealSpec.js: 20 Jasmine specs — openStage, SPIN, FYI, backdrop dismiss, slot re-expand Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
270
src/apps/epic/static/apps/epic/sea.js
Normal file
270
src/apps/epic/static/apps/epic/sea.js
Normal file
@@ -0,0 +1,270 @@
|
||||
var SeaDeal = (function () {
|
||||
'use strict';
|
||||
|
||||
var overlay, stage, stageCard, statBlock;
|
||||
var spinBtn, fyiBtn, bdrop;
|
||||
var fyiPanel, fyiTitle, fyiType, fyiEffect, fyiIndex, fyiPrev, fyiNext;
|
||||
|
||||
var _userPolarity = 'levity';
|
||||
var _seaHand = {}; // posSelector → {card, isLevity}
|
||||
var _viewingPos = null;
|
||||
var _infoData = [];
|
||||
var _infoIdx = 0;
|
||||
var _infoOpen = false;
|
||||
var _spinOrigLabel, _fyiOrigLabel;
|
||||
|
||||
// ── Keyword list ──────────────────────────────────────────────────────────
|
||||
|
||||
function _populateList(listEl, items) {
|
||||
if (!listEl) return;
|
||||
listEl.innerHTML = (items || []).map(function (k) {
|
||||
return '<li>' + k + '</li>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Stage card population ─────────────────────────────────────────────────
|
||||
|
||||
function _populate(card, isLevity) {
|
||||
var qualifier = isLevity
|
||||
? (card.levity_qualifier || '')
|
||||
: (card.gravity_qualifier || '');
|
||||
var isMajor = card.arcana === 'MAJOR';
|
||||
var title = card.name_title || card.name || '';
|
||||
|
||||
// Corners
|
||||
stageCard.querySelectorAll('.fan-corner-rank').forEach(function (el) {
|
||||
el.textContent = card.corner_rank || '';
|
||||
});
|
||||
stageCard.querySelectorAll('.stage-suit-icon').forEach(function (el) {
|
||||
if (card.suit_icon) {
|
||||
el.className = 'fa-solid ' + card.suit_icon + ' stage-suit-icon';
|
||||
el.style.display = '';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Upright face
|
||||
var nameGroupEl = stageCard.querySelector('.fan-card-name-group');
|
||||
if (nameGroupEl) nameGroupEl.textContent = card.name_group || '';
|
||||
var arcanaEl = stageCard.querySelector('.fan-card-arcana');
|
||||
if (arcanaEl) arcanaEl.textContent = isMajor ? 'Major Arcana' : 'Middle Arcana';
|
||||
stageCard.querySelector('.fan-card-name').textContent = isMajor ? title + ',' : title;
|
||||
stageCard.querySelector('.sig-qualifier-above').textContent = isMajor ? '' : qualifier;
|
||||
stageCard.querySelector('.sig-qualifier-below').textContent = isMajor ? qualifier : '';
|
||||
|
||||
// Reversal face (same slot-swap logic as sig-select)
|
||||
var reversal = card.reversal || '';
|
||||
if (isMajor) {
|
||||
stageCard.querySelector('.fan-card-reversal-qualifier').textContent = title + ',';
|
||||
stageCard.querySelector('.fan-card-reversal-name').textContent = qualifier;
|
||||
} else if (reversal) {
|
||||
stageCard.querySelector('.fan-card-reversal-qualifier').textContent = reversal;
|
||||
stageCard.querySelector('.fan-card-reversal-name').textContent = title;
|
||||
} else {
|
||||
stageCard.querySelector('.fan-card-reversal-qualifier').textContent = qualifier;
|
||||
stageCard.querySelector('.fan-card-reversal-name').textContent = '';
|
||||
}
|
||||
|
||||
// Keywords
|
||||
_populateList(overlay.querySelector('#id_sea_stat_upright'), card.keywords_upright);
|
||||
_populateList(overlay.querySelector('#id_sea_stat_reversed'), card.keywords_reversed);
|
||||
|
||||
// FYI data (energies + operations)
|
||||
_infoData = (card.energies || []).map(function (e) {
|
||||
return { type: e.type, effect: e.effect, category: 'energies' };
|
||||
}).concat((card.operations || []).map(function (o) {
|
||||
return { type: o.type, effect: o.effect, category: 'operations' };
|
||||
}));
|
||||
_infoIdx = 0;
|
||||
|
||||
// Reset SPIN
|
||||
stageCard.classList.remove('stage-card--reversed');
|
||||
statBlock.classList.remove('is-reversed');
|
||||
_closeInfo();
|
||||
}
|
||||
|
||||
// ── FYI info panel ────────────────────────────────────────────────────────
|
||||
|
||||
function _renderInfo() {
|
||||
if (!fyiPanel) return;
|
||||
if (_infoData.length === 0) {
|
||||
fyiTitle.textContent = 'Energy';
|
||||
fyiTitle.className = 'sig-info-title sig-info-title--energies';
|
||||
if (fyiType) fyiType.textContent = '';
|
||||
if (fyiEffect) fyiEffect.innerHTML = '<em>No interactions defined.</em>';
|
||||
fyiIndex.textContent = '';
|
||||
return;
|
||||
}
|
||||
var entry = _infoData[_infoIdx];
|
||||
var isEnergies = entry.category === 'energies';
|
||||
fyiTitle.textContent = isEnergies ? 'Energy' : 'Operation';
|
||||
fyiTitle.className = 'sig-info-title sig-info-title--' + entry.category;
|
||||
if (fyiType) fyiType.textContent = entry.type || '';
|
||||
if (fyiEffect) fyiEffect.innerHTML = entry.effect || '';
|
||||
fyiIndex.textContent = _infoData.length > 1
|
||||
? (_infoIdx + 1) + ' / ' + _infoData.length : '';
|
||||
}
|
||||
|
||||
function _openInfo() {
|
||||
_infoOpen = true;
|
||||
_renderInfo();
|
||||
if (fyiPanel) fyiPanel.style.display = '';
|
||||
if (spinBtn) { spinBtn.classList.add('btn-disabled'); spinBtn.textContent = '×'; }
|
||||
if (fyiBtn) { fyiBtn.classList.add('btn-disabled'); fyiBtn.textContent = '×'; }
|
||||
stage.classList.add('sea-info-open');
|
||||
}
|
||||
|
||||
function _closeInfo() {
|
||||
_infoOpen = false;
|
||||
if (fyiPanel) fyiPanel.style.display = 'none';
|
||||
if (spinBtn) { spinBtn.classList.remove('btn-disabled'); spinBtn.textContent = _spinOrigLabel || 'SPIN'; }
|
||||
if (fyiBtn) { fyiBtn.classList.remove('btn-disabled'); fyiBtn.textContent = _fyiOrigLabel || 'FYI'; }
|
||||
if (stage) stage.classList.remove('sea-info-open');
|
||||
}
|
||||
|
||||
// ── Slot fill ─────────────────────────────────────────────────────────────
|
||||
|
||||
function _fillSlot(posSelector, card, isLevity) {
|
||||
var cell = overlay.querySelector(posSelector);
|
||||
if (!cell) return;
|
||||
var slot = cell.querySelector('.sea-card-slot');
|
||||
if (!slot) return;
|
||||
slot.classList.remove('sea-card-slot--empty');
|
||||
slot.classList.add('sea-card-slot--filled');
|
||||
slot.classList.add(isLevity ? 'sea-card-slot--levity' : 'sea-card-slot--gravity');
|
||||
slot.dataset.cardId = String(card.id);
|
||||
slot.dataset.posKey = posSelector;
|
||||
slot.innerHTML =
|
||||
'<span class="fan-corner-rank">' + card.corner_rank + '</span>' +
|
||||
(card.suit_icon ? '<i class="fa-solid ' + card.suit_icon + '"></i>' : '');
|
||||
}
|
||||
|
||||
// ── Show / hide stage ─────────────────────────────────────────────────────
|
||||
|
||||
function _showStage() {
|
||||
stage.style.display = '';
|
||||
stageCard.classList.add('sea-stage-card--shown');
|
||||
}
|
||||
|
||||
function _hideStage() {
|
||||
stage.style.display = 'none';
|
||||
stageCard.classList.remove('sea-stage-card--shown');
|
||||
_viewingPos = null;
|
||||
_closeInfo();
|
||||
}
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
function openStage(card, posSelector, isLevity) {
|
||||
_viewingPos = posSelector;
|
||||
_seaHand[posSelector] = { card: card, isLevity: isLevity };
|
||||
_populate(card, isLevity);
|
||||
_fillSlot(posSelector, card, isLevity);
|
||||
_showStage();
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function init() {
|
||||
overlay = document.getElementById('id_sea_overlay');
|
||||
if (!overlay) return;
|
||||
|
||||
stage = overlay.querySelector('#id_sea_stage');
|
||||
stageCard = stage && stage.querySelector('.sea-stage-card');
|
||||
statBlock = stage && stage.querySelector('.sea-stat-block');
|
||||
if (!stage || !stageCard || !statBlock) return;
|
||||
|
||||
_userPolarity = overlay.dataset.seaUserPolarity || 'levity';
|
||||
|
||||
spinBtn = statBlock.querySelector('.sea-spin-btn');
|
||||
fyiBtn = statBlock.querySelector('.sea-fyi-btn');
|
||||
bdrop = stage.querySelector('.sea-stage-backdrop');
|
||||
|
||||
fyiPanel = overlay.querySelector('#id_sea_fyi_panel');
|
||||
fyiTitle = fyiPanel && fyiPanel.querySelector('.sig-info-title');
|
||||
fyiType = fyiPanel && fyiPanel.querySelector('.sig-info-type');
|
||||
fyiEffect = fyiPanel && fyiPanel.querySelector('.sig-info-effect');
|
||||
fyiIndex = fyiPanel && fyiPanel.querySelector('.sig-info-index');
|
||||
fyiPrev = statBlock.querySelector('.sea-fyi-prev');
|
||||
fyiNext = statBlock.querySelector('.sea-fyi-next');
|
||||
|
||||
_spinOrigLabel = spinBtn ? spinBtn.textContent : 'SPIN';
|
||||
_fyiOrigLabel = fyiBtn ? fyiBtn.textContent : 'FYI';
|
||||
|
||||
// SPIN
|
||||
if (spinBtn) {
|
||||
spinBtn.addEventListener('click', function () {
|
||||
if (spinBtn.classList.contains('btn-disabled')) return;
|
||||
statBlock.classList.toggle('is-reversed');
|
||||
stageCard.classList.toggle('stage-card--reversed');
|
||||
});
|
||||
}
|
||||
|
||||
// FYI
|
||||
if (fyiBtn) {
|
||||
fyiBtn.addEventListener('click', function () {
|
||||
if (fyiBtn.classList.contains('btn-disabled')) return;
|
||||
_infoOpen ? _closeInfo() : _openInfo();
|
||||
});
|
||||
}
|
||||
|
||||
// FYI nav
|
||||
if (fyiPrev) {
|
||||
fyiPrev.addEventListener('click', function () {
|
||||
_infoIdx = (_infoIdx - 1 + _infoData.length) % _infoData.length;
|
||||
_renderInfo();
|
||||
});
|
||||
}
|
||||
if (fyiNext) {
|
||||
fyiNext.addEventListener('click', function () {
|
||||
_infoIdx = (_infoIdx + 1) % _infoData.length;
|
||||
_renderInfo();
|
||||
});
|
||||
}
|
||||
|
||||
// Backdrop dismiss
|
||||
if (bdrop) {
|
||||
bdrop.addEventListener('click', _hideStage);
|
||||
}
|
||||
|
||||
// Click on deposited slot → re-open
|
||||
overlay.addEventListener('click', function (e) {
|
||||
var slot = e.target.closest('.sea-card-slot--filled');
|
||||
if (!slot) return;
|
||||
var pos = slot.dataset.posKey;
|
||||
if (!pos || !_seaHand[pos]) return;
|
||||
var h = _seaHand[pos];
|
||||
_viewingPos = pos;
|
||||
_populate(h.card, h.isLevity);
|
||||
_showStage();
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
function resetHand() {
|
||||
_seaHand = {};
|
||||
_viewingPos = null;
|
||||
_hideStage();
|
||||
}
|
||||
|
||||
return {
|
||||
openStage: openStage,
|
||||
resetHand: resetHand,
|
||||
_testInit: function () {
|
||||
overlay = null; stage = null; stageCard = null; statBlock = null;
|
||||
spinBtn = null; fyiBtn = null; bdrop = null;
|
||||
fyiPanel = null; fyiTitle = null; fyiType = null;
|
||||
fyiEffect = null; fyiIndex = null; fyiPrev = null; fyiNext = null;
|
||||
_userPolarity = 'levity';
|
||||
_seaHand = {}; _viewingPos = null;
|
||||
_infoData = []; _infoIdx = 0; _infoOpen = false;
|
||||
init();
|
||||
},
|
||||
};
|
||||
}());
|
||||
@@ -1141,8 +1141,15 @@ def sea_deck(request, room_id):
|
||||
'number': c.number,
|
||||
'corner_rank': c.corner_rank,
|
||||
'suit_icon': c.suit_icon,
|
||||
'name_group': c.name_group,
|
||||
'name_title': c.name_title,
|
||||
'levity_qualifier': c.levity_qualifier,
|
||||
'gravity_qualifier': c.gravity_qualifier,
|
||||
'reversal': c.reversal,
|
||||
'keywords_upright': c.keywords_upright,
|
||||
'keywords_reversed': c.keywords_reversed,
|
||||
'energies': c.energies,
|
||||
'operations': c.operations,
|
||||
}
|
||||
|
||||
available = list(
|
||||
|
||||
292
src/static/tests/SeaDealSpec.js
Normal file
292
src/static/tests/SeaDealSpec.js
Normal file
@@ -0,0 +1,292 @@
|
||||
describe("SeaDeal", () => {
|
||||
let testDiv, overlay, stage, stageCard, statBlock;
|
||||
|
||||
const CARD = {
|
||||
id: 99, name: "Queen of Crowns",
|
||||
arcana: "MIDDLE", suit: "CROWNS", number: 13,
|
||||
corner_rank: "Q", suit_icon: "fa-crown",
|
||||
name_group: "", name_title: "Queen of Crowns",
|
||||
levity_qualifier: "Elevated", gravity_qualifier: "Graven",
|
||||
reversal: "Vacant",
|
||||
keywords_upright: ["nurturing", "practical", "abundance"],
|
||||
keywords_reversed: ["financial dependence", "smothering"],
|
||||
energies: [{ type: "LIBIDO", effect: "Energy entry." }],
|
||||
operations: [{ type: "COVER", effect: "Operation entry." }],
|
||||
};
|
||||
|
||||
function makeFixture({ polarity = "levity" } = {}) {
|
||||
testDiv = document.createElement("div");
|
||||
testDiv.innerHTML = `
|
||||
<div id="id_sea_overlay" data-sea-user-polarity="${polarity}">
|
||||
|
||||
<!-- Cross grid (simplified) -->
|
||||
<div class="sea-cross">
|
||||
<div class="sea-cross-cell sea-pos-crown">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
<div class="sea-cross-cell sea-pos-past">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
<div class="sea-cross-cell sea-pos-center">
|
||||
<div class="sea-sig-card"></div>
|
||||
<div class="sea-pos-cover">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
<div class="sea-pos-cross">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sea-cross-cell sea-pos-future">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
<div class="sea-cross-cell sea-pos-root">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sea stage (big card viewer) -->
|
||||
<div class="sea-stage" id="id_sea_stage" style="display:none">
|
||||
<div class="sea-stage-backdrop"></div>
|
||||
<div class="sea-stage-content">
|
||||
<div class="sig-stage-card sea-stage-card" style="--sig-card-w:140px">
|
||||
<div class="fan-card-corner fan-card-corner--tl">
|
||||
<span class="fan-corner-rank"></span>
|
||||
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
||||
</div>
|
||||
<div class="fan-card-face">
|
||||
<div class="fan-card-face-upright">
|
||||
<p class="fan-card-name-group"></p>
|
||||
<p class="sig-qualifier-above"></p>
|
||||
<h3 class="fan-card-name"></h3>
|
||||
<p class="sig-qualifier-below"></p>
|
||||
</div>
|
||||
<p class="fan-card-arcana"></p>
|
||||
<div class="fan-card-face-reversal">
|
||||
<p class="fan-card-reversal-name"></p>
|
||||
<p class="fan-card-reversal-qualifier"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fan-card-corner fan-card-corner--br">
|
||||
<span class="fan-corner-rank"></span>
|
||||
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sig-stat-block sea-stat-block">
|
||||
<button class="btn btn-reverse sea-spin-btn" type="button">SPIN</button>
|
||||
<button class="btn btn-info sea-fyi-btn" type="button">FYI</button>
|
||||
<div class="stat-face stat-face--upright">
|
||||
<p class="stat-face-label">Emanation</p>
|
||||
<ul class="stat-keywords" id="id_sea_stat_upright"></ul>
|
||||
</div>
|
||||
<div class="stat-face stat-face--reversed">
|
||||
<p class="stat-face-label">Reversal</p>
|
||||
<ul class="stat-keywords" id="id_sea_stat_reversed"></ul>
|
||||
</div>
|
||||
<div class="sig-info" id="id_sea_fyi_panel" style="display:none">
|
||||
<div class="sig-info-header">
|
||||
<h4 class="sig-info-title"></h4>
|
||||
<p class="sig-info-type"></p>
|
||||
</div>
|
||||
<p class="sig-info-effect"></p>
|
||||
<span class="sig-info-index"></span>
|
||||
</div>
|
||||
<button class="btn btn-nav-left sea-fyi-prev" type="button">PRV</button>
|
||||
<button class="btn btn-nav-right sea-fyi-next" type="button">NXT</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(testDiv);
|
||||
overlay = testDiv.querySelector("#id_sea_overlay");
|
||||
stage = testDiv.querySelector("#id_sea_stage");
|
||||
stageCard = testDiv.querySelector(".sea-stage-card");
|
||||
statBlock = testDiv.querySelector(".sea-stat-block");
|
||||
SeaDeal._testInit();
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
if (testDiv) testDiv.remove();
|
||||
// Purge any stale overlays left by tests that called makeFixture() twice
|
||||
document.querySelectorAll('#id_sea_overlay').forEach(el => el.remove());
|
||||
});
|
||||
|
||||
// ── openStage ────────────────────────────────────────────────────────── //
|
||||
|
||||
describe("openStage()", () => {
|
||||
beforeEach(() => makeFixture());
|
||||
|
||||
it("makes #id_sea_stage visible", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
expect(stage.style.display).not.toBe("none");
|
||||
});
|
||||
|
||||
it("populates corner rank on stage card", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
const rank = stageCard.querySelector(".fan-card-corner--tl .fan-corner-rank");
|
||||
expect(rank.textContent).toBe("Q");
|
||||
});
|
||||
|
||||
it("shows suit icon on stage card", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
const icon = stageCard.querySelector(".fan-card-corner--tl .stage-suit-icon");
|
||||
expect(icon.style.display).not.toBe("none");
|
||||
expect(icon.classList.contains("fa-crown")).toBe(true);
|
||||
});
|
||||
|
||||
it("populates upright card name for non-major", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
expect(stageCard.querySelector(".fan-card-name").textContent).toBe("Queen of Crowns");
|
||||
});
|
||||
|
||||
it("puts levity qualifier in qualifier-above for non-major", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
expect(stageCard.querySelector(".sig-qualifier-above").textContent).toBe("Elevated");
|
||||
});
|
||||
|
||||
it("puts gravity qualifier in qualifier-above for non-major gravity", () => {
|
||||
// Re-init with gravity polarity (no double-append; beforeEach already ran)
|
||||
overlay.dataset.seaUserPolarity = "gravity";
|
||||
SeaDeal._testInit();
|
||||
stageCard = testDiv.querySelector(".sea-stage-card");
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", false);
|
||||
expect(stageCard.querySelector(".sig-qualifier-above").textContent).toBe("Graven");
|
||||
});
|
||||
|
||||
it("puts reversal word in reversal-qualifier slot", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Vacant");
|
||||
});
|
||||
|
||||
it("populates upright keywords in stat block", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
const items = testDiv.querySelectorAll("#id_sea_stat_upright li");
|
||||
expect(items.length).toBe(3);
|
||||
expect(items[0].textContent).toBe("nurturing");
|
||||
});
|
||||
|
||||
it("populates reversed keywords in stat block", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
const items = testDiv.querySelectorAll("#id_sea_stat_reversed li");
|
||||
expect(items.length).toBe(2);
|
||||
});
|
||||
|
||||
it("fills the target position slot", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
const slot = testDiv.querySelector(".sea-pos-cover .sea-card-slot");
|
||||
expect(slot.classList.contains("sea-card-slot--filled")).toBe(true);
|
||||
});
|
||||
|
||||
it("resets SPIN to upright when opening a new card", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
stageCard.classList.add("stage-card--reversed");
|
||||
statBlock.classList.add("is-reversed");
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cross", true);
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── SPIN in sea stage ─────────────────────────────────────────────────── //
|
||||
|
||||
describe("SPIN btn in sea stage", () => {
|
||||
beforeEach(() => {
|
||||
makeFixture();
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
});
|
||||
|
||||
it("toggles is-reversed on stat block", () => {
|
||||
testDiv.querySelector(".sea-spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||
});
|
||||
|
||||
it("toggles stage-card--reversed on stage card", () => {
|
||||
testDiv.querySelector(".sea-spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||
});
|
||||
|
||||
it("second SPIN click restores upright", () => {
|
||||
const btn = testDiv.querySelector(".sea-spin-btn");
|
||||
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── FYI in sea stage ──────────────────────────────────────────────────── //
|
||||
|
||||
describe("FYI btn in sea stage", () => {
|
||||
beforeEach(() => {
|
||||
makeFixture();
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
});
|
||||
|
||||
it("FYI click shows the info panel", () => {
|
||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector("#id_sea_fyi_panel").style.display).not.toBe("none");
|
||||
});
|
||||
|
||||
it("shows first energy entry title as 'Energy'", () => {
|
||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Energy");
|
||||
});
|
||||
|
||||
it("shows first entry type", () => {
|
||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("LIBIDO");
|
||||
});
|
||||
|
||||
it("NXT advances to operation entry", () => {
|
||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
testDiv.querySelector(".sea-fyi-next").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Operation");
|
||||
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("COVER");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Backdrop dismiss ──────────────────────────────────────────────────── //
|
||||
|
||||
describe("stage backdrop click", () => {
|
||||
beforeEach(() => {
|
||||
makeFixture();
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
});
|
||||
|
||||
it("hides the sea stage", () => {
|
||||
testDiv.querySelector(".sea-stage-backdrop").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stage.style.display).toBe("none");
|
||||
});
|
||||
|
||||
it("leaves the slot filled after dismiss", () => {
|
||||
testDiv.querySelector(".sea-stage-backdrop").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
const slot = testDiv.querySelector(".sea-pos-cover .sea-card-slot");
|
||||
expect(slot.classList.contains("sea-card-slot--filled")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Re-open from deposited slot ───────────────────────────────────────── //
|
||||
|
||||
describe("clicking a deposited slot", () => {
|
||||
beforeEach(() => {
|
||||
makeFixture();
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
// Dismiss first
|
||||
testDiv.querySelector(".sea-stage-backdrop").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
it("re-opens the sea stage", () => {
|
||||
expect(stage.style.display).toBe("none");
|
||||
const slot = testDiv.querySelector(".sea-pos-cover .sea-card-slot");
|
||||
slot.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stage.style.display).not.toBe("none");
|
||||
});
|
||||
|
||||
it("re-populates stage with the same card rank", () => {
|
||||
const slot = testDiv.querySelector(".sea-pos-cover .sea-card-slot");
|
||||
slot.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
const rank = stageCard.querySelector(".fan-card-corner--tl .fan-corner-rank");
|
||||
expect(rank.textContent).toBe("Q");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,7 @@
|
||||
<script src="RoleSelectSpec.js"></script>
|
||||
<script src="TraySpec.js"></script>
|
||||
<script src="SigSelectSpec.js"></script>
|
||||
<script src="SeaDealSpec.js"></script>
|
||||
<script src="NatusWheelSpec.js"></script>
|
||||
<script src="NoteSpec.js"></script>
|
||||
<script src="NotePageSpec.js"></script>
|
||||
@@ -32,6 +33,7 @@
|
||||
<script src="/static/apps/epic/role-select.js"></script>
|
||||
<script src="/static/apps/epic/tray.js"></script>
|
||||
<script src="/static/apps/epic/sig-select.js"></script>
|
||||
<script src="/static/apps/epic/sea.js"></script>
|
||||
<script src="/static/apps/gameboard/d3.min.js"></script>
|
||||
<script src="/static/apps/gameboard/natus-wheel.js"></script>
|
||||
<!-- Jasmine env config (optional) -->
|
||||
|
||||
@@ -402,6 +402,42 @@
|
||||
|
||||
// DOFF btn
|
||||
&.btn-unequip {
|
||||
color: rgba(var(--priId), 1);
|
||||
border-color: rgba(var(--priId), 1);
|
||||
background-color: rgba(var(--terId), 1);
|
||||
box-shadow:
|
||||
0.1rem 0.1rem 0.12rem rgba(var(--terId), 0.25),
|
||||
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||
0.25rem 0.25rem 0.25rem rgba(var(--terId), 0.12)
|
||||
;
|
||||
|
||||
&:hover {
|
||||
text-shadow:
|
||||
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
|
||||
0 0 1rem rgba(var(--priId), 1)
|
||||
;
|
||||
box-shadow:
|
||||
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
|
||||
0 0 0.5rem rgba(var(--priId), 0.12)
|
||||
;
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: 0.18rem solid rgba(var(--priId), 1);
|
||||
text-shadow:
|
||||
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||
0 0 0.12rem rgba(var(--priId), 1)
|
||||
;
|
||||
box-shadow:
|
||||
-0.1rem -0.1rem 0.12rem rgba(var(--terId), 0.25),
|
||||
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
|
||||
0 0 0.5rem rgba(var(--priId), 0.12)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
// FLIP btn
|
||||
&.btn-reveal {
|
||||
color: rgba(var(--priMe), 1);
|
||||
border-color: rgba(var(--priMe), 1);
|
||||
background-color: rgba(var(--terMe), 1);
|
||||
@@ -436,42 +472,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// FLIP btn
|
||||
&.btn-reveal {
|
||||
color: rgba(var(--priCy), 1);
|
||||
border-color: rgba(var(--priCy), 1);
|
||||
background-color: rgba(var(--terCy), 1);
|
||||
box-shadow:
|
||||
0.1rem 0.1rem 0.12rem rgba(var(--terCy), 0.25),
|
||||
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||
0.25rem 0.25rem 0.25rem rgba(var(--terCy), 0.12)
|
||||
;
|
||||
|
||||
&:hover {
|
||||
text-shadow:
|
||||
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
|
||||
0 0 1rem rgba(var(--priCy), 1)
|
||||
;
|
||||
box-shadow:
|
||||
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
|
||||
0 0 0.5rem rgba(var(--priCy), 0.12)
|
||||
;
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: 0.18rem solid rgba(var(--priCy), 1);
|
||||
text-shadow:
|
||||
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||
0 0 0.12rem rgba(var(--priCy), 1)
|
||||
;
|
||||
box-shadow:
|
||||
-0.1rem -0.1rem 0.12rem rgba(var(--terCy), 0.25),
|
||||
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
|
||||
0 0 0.5rem rgba(var(--priCy), 0.12)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
// SPIN btn
|
||||
&.btn-reverse {
|
||||
color: rgba(var(--priCy), 1);
|
||||
|
||||
@@ -606,6 +606,14 @@ html:has(.sig-backdrop) {
|
||||
// Polarity qualifier: same colour as the card title in this context
|
||||
.sig-qualifier-above,
|
||||
.sig-qualifier-below { color: rgba(var(--quiUser), 1); }
|
||||
// Upright + reversal title glow — levity
|
||||
.sig-stage-card .fan-card-name,
|
||||
.sig-stage-card .sig-qualifier-above,
|
||||
.sig-stage-card .sig-qualifier-below,
|
||||
.sig-stage-card .fan-card-reversal-name,
|
||||
.sig-stage-card .fan-card-reversal-qualifier {
|
||||
text-shadow: 0 1px 1px rgba(0,0,0,0.55), 0 0 0.55rem rgba(var(--ninUser), 0.7);
|
||||
}
|
||||
// card-ref spans inside the caution tooltip — must match the base rule's
|
||||
// .sig-stat-block .sig-info-effect .card-ref specificity (0,3,0) to win.
|
||||
.sig-info-effect .card-ref { color: rgba(var(--quiUser), 1); }
|
||||
@@ -624,6 +632,14 @@ html:has(.sig-backdrop) {
|
||||
// Polarity qualifier: terUser for gravity (quiUser is levity's equivalent)
|
||||
.sig-qualifier-above,
|
||||
.sig-qualifier-below { color: rgba(var(--terUser), 1); }
|
||||
// Upright + reversal title glow — gravity
|
||||
.sig-stage-card .fan-card-name,
|
||||
.sig-stage-card .sig-qualifier-above,
|
||||
.sig-stage-card .sig-qualifier-below,
|
||||
.sig-stage-card .fan-card-reversal-name,
|
||||
.sig-stage-card .fan-card-reversal-qualifier {
|
||||
text-shadow: 1px 1px 0 rgba(0,0,0,1), 0 0 0.25rem rgba(var(--ninUser), 0.25);
|
||||
}
|
||||
// Cursor colours live in .sig-cursor-float[data-role] rules (portal elements)
|
||||
}
|
||||
|
||||
@@ -785,7 +801,9 @@ $sea-card-h: 6.5rem;
|
||||
.sea-card-slot {
|
||||
width: $sea-card-w;
|
||||
height: $sea-card-h;
|
||||
border: 0.15rem dashed rgba(var(--terUser), 0.45);
|
||||
background-color: rgba(var(--duoUser), 1);
|
||||
border: 0.15rem dashed rgba(var(--terUser), 1);
|
||||
box-shadow: 0 0 2px rgba(var(--priUser), 0.5);
|
||||
border-radius: 0.3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -843,13 +861,16 @@ $sea-card-h: 6.5rem;
|
||||
|
||||
.sea-pos-cross .sea-card-slot { transform: rotate(90deg); }
|
||||
|
||||
// Sig card in center slot — compact rank + icon display
|
||||
// Sig card in center slot — compact rank + icon display; tilted CCW so Cover slot peeks through
|
||||
.sea-sig-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.2rem;
|
||||
transform: rotate(-5deg);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.fan-corner-rank {
|
||||
font-size: 1.2rem;
|
||||
@@ -975,8 +996,10 @@ $sea-card-h: 6.5rem;
|
||||
.sea-stack-ok {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
transform: translateY(-50%);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
@@ -1033,11 +1056,154 @@ $_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6);
|
||||
// NVM button — same positioning as .natus-modal-wrap > .btn-cancel
|
||||
.sea-modal-wrap > .btn-cancel {
|
||||
position: absolute;
|
||||
top: -0.75rem;
|
||||
right: -0.75rem;
|
||||
top: -1rem;
|
||||
right: -1rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
// ── Sea stage — big card viewer ───────────────────────────────────────────────
|
||||
|
||||
.sea-stage {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sea-stage-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sea-stage-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
// Stage card — size matches sig-select stage (--sig-card-w driven by inline style)
|
||||
.sea-stage-card {
|
||||
flex-shrink: 0;
|
||||
width: var(--sig-card-w, 140px);
|
||||
height: auto;
|
||||
aspect-ratio: 5 / 8;
|
||||
border-radius: 0.5rem;
|
||||
background: rgba(var(--priUser), 1);
|
||||
border: 0.15rem solid rgba(var(--secUser), 0.6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
padding: 0.25rem;
|
||||
overflow: hidden;
|
||||
transform-style: preserve-3d;
|
||||
|
||||
// Flip-in animation when stage opens
|
||||
&--shown {
|
||||
animation: sea-flip-in 0.35s ease forwards;
|
||||
}
|
||||
|
||||
.fan-card-corner--tl,
|
||||
.fan-card-corner--br {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
line-height: 1.1;
|
||||
gap: 0.1rem;
|
||||
.fan-corner-rank { font-size: calc(var(--sig-card-w, 140px) * 0.133); font-weight: 700; }
|
||||
i { font-size: calc(var(--sig-card-w, 140px) * 0.1); }
|
||||
}
|
||||
|
||||
.fan-card-face {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0.25rem 0.15rem;
|
||||
gap: 0.2rem;
|
||||
|
||||
.fan-card-face-upright { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; }
|
||||
.fan-card-face-reversal { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; padding-top: 0.1rem; }
|
||||
.fan-card-name-group { font-size: calc(var(--sig-card-w, 140px) * 0.073); opacity: 0.6; }
|
||||
.sig-qualifier-above,
|
||||
.sig-qualifier-below,
|
||||
.fan-card-reversal-qualifier { font-size: calc(var(--sig-card-w, 140px) * 0.093); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; }
|
||||
.fan-card-name,
|
||||
.fan-card-reversal-name { font-size: calc(var(--sig-card-w, 140px) * 0.093); font-weight: 600; color: rgba(var(--quiUser), 1); transition: opacity 0.2s; }
|
||||
.fan-card-arcana { font-size: calc(var(--sig-card-w, 140px) * 0.067); text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
|
||||
.fan-card-reversal-qualifier,
|
||||
.fan-card-reversal-name { transform: rotate(180deg); opacity: 0.25; }
|
||||
}
|
||||
|
||||
&.stage-card--reversed {
|
||||
transform: rotate(180deg);
|
||||
.fan-card-reversal-qualifier,
|
||||
.fan-card-reversal-name { opacity: 1; }
|
||||
.fan-card-name,
|
||||
.sig-qualifier-above,
|
||||
.sig-qualifier-below { opacity: 0.25; }
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sea-flip-in {
|
||||
0% { transform: perspective(600px) rotateY(-90deg) scale(0.4); opacity: 0; }
|
||||
60% { transform: perspective(600px) rotateY(8deg) scale(1.03); opacity: 1; }
|
||||
100% { transform: perspective(600px) rotateY(0deg) scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
// Sea stat block — reuses sig-select stat-block sizing, scoped to sea-stage
|
||||
.sea-stage-content .sea-stat-block {
|
||||
flex: 0 0 auto;
|
||||
width: var(--sig-card-w, 140px);
|
||||
height: calc(var(--sig-card-w, 140px) * 8 / 5);
|
||||
background: rgba(var(--priUser), 0.85);
|
||||
border-radius: 0.4rem;
|
||||
border: 0.1rem solid rgba(var(--terUser), 0.15);
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
.sea-spin-btn { position: absolute; top: -1rem; right: -1rem; margin: 0; z-index: 50; }
|
||||
.sea-fyi-btn { position: absolute; top: 1.25rem; right: -1rem; margin: 0; z-index: 50; }
|
||||
|
||||
.stat-face { display: none; padding: calc(var(--sig-card-w, 140px) * 0.37) calc(var(--sig-card-w, 140px) * 0.1) calc(var(--sig-card-w, 140px) * 0.08); }
|
||||
.stat-face--upright { display: block; }
|
||||
&.is-reversed { .stat-face--upright { display: none; } .stat-face--reversed { display: block; } }
|
||||
|
||||
.stat-face-label { font-size: calc(var(--sig-card-w, 140px) * 0.063); text-transform: uppercase; letter-spacing: 0.09em; opacity: 0.4; margin: 0 0 calc(var(--sig-card-w, 140px) * 0.07); }
|
||||
.stat-keywords { list-style: none; padding: 0; margin: 0;
|
||||
li { font-size: calc(var(--sig-card-w, 140px) * 0.083); padding: calc(var(--sig-card-w, 140px) * 0.042) 0; opacity: 0.85; border-bottom: 0.05rem solid rgba(var(--terUser), 0.12); &:last-child { border-bottom: none; } }
|
||||
}
|
||||
|
||||
.sig-info {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 60;
|
||||
background-color: rgba(var(--tooltip-bg), 0.6);
|
||||
backdrop-filter: blur(6px);
|
||||
border-radius: 0.4rem;
|
||||
border: 0.1rem solid rgba(var(--priYl), 0.35);
|
||||
padding: 0.75rem;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
&[style*="display"]:not([style*="none"]) { display: flex; }
|
||||
}
|
||||
|
||||
.sea-fyi-prev,
|
||||
.sea-fyi-next { display: inline-flex; position: absolute; bottom: -1rem; margin: 0; z-index: 70; }
|
||||
.sea-fyi-prev { left: -1rem; }
|
||||
.sea-fyi-next { right: -1rem; }
|
||||
}
|
||||
|
||||
@media (orientation: landscape) {
|
||||
html.sea-open body .container .navbar,
|
||||
html.sea-open body #id_footer {
|
||||
|
||||
@@ -57,7 +57,8 @@ html:has(.gate-backdrop) {
|
||||
|
||||
html:has(.gate-backdrop) #id_aperture_fill,
|
||||
html:has(.sig-backdrop) #id_aperture_fill,
|
||||
html:has(.role-select-backdrop) #id_aperture_fill {
|
||||
html:has(.role-select-backdrop) #id_aperture_fill,
|
||||
html.sea-open #id_aperture_fill {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
292
src/static_src/tests/SeaDealSpec.js
Normal file
292
src/static_src/tests/SeaDealSpec.js
Normal file
@@ -0,0 +1,292 @@
|
||||
describe("SeaDeal", () => {
|
||||
let testDiv, overlay, stage, stageCard, statBlock;
|
||||
|
||||
const CARD = {
|
||||
id: 99, name: "Queen of Crowns",
|
||||
arcana: "MIDDLE", suit: "CROWNS", number: 13,
|
||||
corner_rank: "Q", suit_icon: "fa-crown",
|
||||
name_group: "", name_title: "Queen of Crowns",
|
||||
levity_qualifier: "Elevated", gravity_qualifier: "Graven",
|
||||
reversal: "Vacant",
|
||||
keywords_upright: ["nurturing", "practical", "abundance"],
|
||||
keywords_reversed: ["financial dependence", "smothering"],
|
||||
energies: [{ type: "LIBIDO", effect: "Energy entry." }],
|
||||
operations: [{ type: "COVER", effect: "Operation entry." }],
|
||||
};
|
||||
|
||||
function makeFixture({ polarity = "levity" } = {}) {
|
||||
testDiv = document.createElement("div");
|
||||
testDiv.innerHTML = `
|
||||
<div id="id_sea_overlay" data-sea-user-polarity="${polarity}">
|
||||
|
||||
<!-- Cross grid (simplified) -->
|
||||
<div class="sea-cross">
|
||||
<div class="sea-cross-cell sea-pos-crown">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
<div class="sea-cross-cell sea-pos-past">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
<div class="sea-cross-cell sea-pos-center">
|
||||
<div class="sea-sig-card"></div>
|
||||
<div class="sea-pos-cover">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
<div class="sea-pos-cross">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sea-cross-cell sea-pos-future">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
<div class="sea-cross-cell sea-pos-root">
|
||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sea stage (big card viewer) -->
|
||||
<div class="sea-stage" id="id_sea_stage" style="display:none">
|
||||
<div class="sea-stage-backdrop"></div>
|
||||
<div class="sea-stage-content">
|
||||
<div class="sig-stage-card sea-stage-card" style="--sig-card-w:140px">
|
||||
<div class="fan-card-corner fan-card-corner--tl">
|
||||
<span class="fan-corner-rank"></span>
|
||||
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
||||
</div>
|
||||
<div class="fan-card-face">
|
||||
<div class="fan-card-face-upright">
|
||||
<p class="fan-card-name-group"></p>
|
||||
<p class="sig-qualifier-above"></p>
|
||||
<h3 class="fan-card-name"></h3>
|
||||
<p class="sig-qualifier-below"></p>
|
||||
</div>
|
||||
<p class="fan-card-arcana"></p>
|
||||
<div class="fan-card-face-reversal">
|
||||
<p class="fan-card-reversal-name"></p>
|
||||
<p class="fan-card-reversal-qualifier"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fan-card-corner fan-card-corner--br">
|
||||
<span class="fan-corner-rank"></span>
|
||||
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sig-stat-block sea-stat-block">
|
||||
<button class="btn btn-reverse sea-spin-btn" type="button">SPIN</button>
|
||||
<button class="btn btn-info sea-fyi-btn" type="button">FYI</button>
|
||||
<div class="stat-face stat-face--upright">
|
||||
<p class="stat-face-label">Emanation</p>
|
||||
<ul class="stat-keywords" id="id_sea_stat_upright"></ul>
|
||||
</div>
|
||||
<div class="stat-face stat-face--reversed">
|
||||
<p class="stat-face-label">Reversal</p>
|
||||
<ul class="stat-keywords" id="id_sea_stat_reversed"></ul>
|
||||
</div>
|
||||
<div class="sig-info" id="id_sea_fyi_panel" style="display:none">
|
||||
<div class="sig-info-header">
|
||||
<h4 class="sig-info-title"></h4>
|
||||
<p class="sig-info-type"></p>
|
||||
</div>
|
||||
<p class="sig-info-effect"></p>
|
||||
<span class="sig-info-index"></span>
|
||||
</div>
|
||||
<button class="btn btn-nav-left sea-fyi-prev" type="button">PRV</button>
|
||||
<button class="btn btn-nav-right sea-fyi-next" type="button">NXT</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(testDiv);
|
||||
overlay = testDiv.querySelector("#id_sea_overlay");
|
||||
stage = testDiv.querySelector("#id_sea_stage");
|
||||
stageCard = testDiv.querySelector(".sea-stage-card");
|
||||
statBlock = testDiv.querySelector(".sea-stat-block");
|
||||
SeaDeal._testInit();
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
if (testDiv) testDiv.remove();
|
||||
// Purge any stale overlays left by tests that called makeFixture() twice
|
||||
document.querySelectorAll('#id_sea_overlay').forEach(el => el.remove());
|
||||
});
|
||||
|
||||
// ── openStage ────────────────────────────────────────────────────────── //
|
||||
|
||||
describe("openStage()", () => {
|
||||
beforeEach(() => makeFixture());
|
||||
|
||||
it("makes #id_sea_stage visible", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
expect(stage.style.display).not.toBe("none");
|
||||
});
|
||||
|
||||
it("populates corner rank on stage card", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
const rank = stageCard.querySelector(".fan-card-corner--tl .fan-corner-rank");
|
||||
expect(rank.textContent).toBe("Q");
|
||||
});
|
||||
|
||||
it("shows suit icon on stage card", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
const icon = stageCard.querySelector(".fan-card-corner--tl .stage-suit-icon");
|
||||
expect(icon.style.display).not.toBe("none");
|
||||
expect(icon.classList.contains("fa-crown")).toBe(true);
|
||||
});
|
||||
|
||||
it("populates upright card name for non-major", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
expect(stageCard.querySelector(".fan-card-name").textContent).toBe("Queen of Crowns");
|
||||
});
|
||||
|
||||
it("puts levity qualifier in qualifier-above for non-major", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
expect(stageCard.querySelector(".sig-qualifier-above").textContent).toBe("Elevated");
|
||||
});
|
||||
|
||||
it("puts gravity qualifier in qualifier-above for non-major gravity", () => {
|
||||
// Re-init with gravity polarity (no double-append; beforeEach already ran)
|
||||
overlay.dataset.seaUserPolarity = "gravity";
|
||||
SeaDeal._testInit();
|
||||
stageCard = testDiv.querySelector(".sea-stage-card");
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", false);
|
||||
expect(stageCard.querySelector(".sig-qualifier-above").textContent).toBe("Graven");
|
||||
});
|
||||
|
||||
it("puts reversal word in reversal-qualifier slot", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Vacant");
|
||||
});
|
||||
|
||||
it("populates upright keywords in stat block", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
const items = testDiv.querySelectorAll("#id_sea_stat_upright li");
|
||||
expect(items.length).toBe(3);
|
||||
expect(items[0].textContent).toBe("nurturing");
|
||||
});
|
||||
|
||||
it("populates reversed keywords in stat block", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
const items = testDiv.querySelectorAll("#id_sea_stat_reversed li");
|
||||
expect(items.length).toBe(2);
|
||||
});
|
||||
|
||||
it("fills the target position slot", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
const slot = testDiv.querySelector(".sea-pos-cover .sea-card-slot");
|
||||
expect(slot.classList.contains("sea-card-slot--filled")).toBe(true);
|
||||
});
|
||||
|
||||
it("resets SPIN to upright when opening a new card", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
stageCard.classList.add("stage-card--reversed");
|
||||
statBlock.classList.add("is-reversed");
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cross", true);
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── SPIN in sea stage ─────────────────────────────────────────────────── //
|
||||
|
||||
describe("SPIN btn in sea stage", () => {
|
||||
beforeEach(() => {
|
||||
makeFixture();
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
});
|
||||
|
||||
it("toggles is-reversed on stat block", () => {
|
||||
testDiv.querySelector(".sea-spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||
});
|
||||
|
||||
it("toggles stage-card--reversed on stage card", () => {
|
||||
testDiv.querySelector(".sea-spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||
});
|
||||
|
||||
it("second SPIN click restores upright", () => {
|
||||
const btn = testDiv.querySelector(".sea-spin-btn");
|
||||
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── FYI in sea stage ──────────────────────────────────────────────────── //
|
||||
|
||||
describe("FYI btn in sea stage", () => {
|
||||
beforeEach(() => {
|
||||
makeFixture();
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
});
|
||||
|
||||
it("FYI click shows the info panel", () => {
|
||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector("#id_sea_fyi_panel").style.display).not.toBe("none");
|
||||
});
|
||||
|
||||
it("shows first energy entry title as 'Energy'", () => {
|
||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Energy");
|
||||
});
|
||||
|
||||
it("shows first entry type", () => {
|
||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("LIBIDO");
|
||||
});
|
||||
|
||||
it("NXT advances to operation entry", () => {
|
||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
testDiv.querySelector(".sea-fyi-next").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Operation");
|
||||
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("COVER");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Backdrop dismiss ──────────────────────────────────────────────────── //
|
||||
|
||||
describe("stage backdrop click", () => {
|
||||
beforeEach(() => {
|
||||
makeFixture();
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
});
|
||||
|
||||
it("hides the sea stage", () => {
|
||||
testDiv.querySelector(".sea-stage-backdrop").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stage.style.display).toBe("none");
|
||||
});
|
||||
|
||||
it("leaves the slot filled after dismiss", () => {
|
||||
testDiv.querySelector(".sea-stage-backdrop").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
const slot = testDiv.querySelector(".sea-pos-cover .sea-card-slot");
|
||||
expect(slot.classList.contains("sea-card-slot--filled")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Re-open from deposited slot ───────────────────────────────────────── //
|
||||
|
||||
describe("clicking a deposited slot", () => {
|
||||
beforeEach(() => {
|
||||
makeFixture();
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
// Dismiss first
|
||||
testDiv.querySelector(".sea-stage-backdrop").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
it("re-opens the sea stage", () => {
|
||||
expect(stage.style.display).toBe("none");
|
||||
const slot = testDiv.querySelector(".sea-pos-cover .sea-card-slot");
|
||||
slot.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stage.style.display).not.toBe("none");
|
||||
});
|
||||
|
||||
it("re-populates stage with the same card rank", () => {
|
||||
const slot = testDiv.querySelector(".sea-pos-cover .sea-card-slot");
|
||||
slot.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
const rank = stageCard.querySelector(".fan-card-corner--tl .fan-corner-rank");
|
||||
expect(rank.textContent).toBe("Q");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,7 @@
|
||||
<script src="RoleSelectSpec.js"></script>
|
||||
<script src="TraySpec.js"></script>
|
||||
<script src="SigSelectSpec.js"></script>
|
||||
<script src="SeaDealSpec.js"></script>
|
||||
<script src="NatusWheelSpec.js"></script>
|
||||
<script src="NoteSpec.js"></script>
|
||||
<script src="NotePageSpec.js"></script>
|
||||
@@ -32,6 +33,7 @@
|
||||
<script src="/static/apps/epic/role-select.js"></script>
|
||||
<script src="/static/apps/epic/tray.js"></script>
|
||||
<script src="/static/apps/epic/sig-select.js"></script>
|
||||
<script src="/static/apps/epic/sea.js"></script>
|
||||
<script src="/static/apps/gameboard/d3.min.js"></script>
|
||||
<script src="/static/apps/gameboard/natus-wheel.js"></script>
|
||||
<!-- Jasmine env config (optional) -->
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
<div class="sea-backdrop"></div>
|
||||
<div class="sea-overlay" id="id_sea_overlay"
|
||||
data-sea-deck-url="{% url 'epic:sea_deck' room.id %}">
|
||||
data-sea-deck-url="{% url 'epic:sea_deck' room.id %}"
|
||||
data-sea-user-polarity="{{ user_polarity }}">
|
||||
|
||||
<div class="sea-modal-wrap">
|
||||
<div class="sea-modal">
|
||||
@@ -78,13 +79,13 @@
|
||||
<span class="sea-stacks-label">DECKS</span>
|
||||
<div class="sea-deck-stack sea-deck-stack--gravity">
|
||||
<div class="sea-stack-face">
|
||||
<button class="btn btn-confirm sea-stack-ok" type="button">OK</button>
|
||||
<button class="btn btn-reveal sea-stack-ok" type="button">FLIP</button>
|
||||
</div>
|
||||
<span class="sea-stack-name">Gravity</span>
|
||||
</div>
|
||||
<div class="sea-deck-stack sea-deck-stack--levity">
|
||||
<div class="sea-stack-face">
|
||||
<button class="btn btn-confirm sea-stack-ok" type="button">OK</button>
|
||||
<button class="btn btn-reveal sea-stack-ok" type="button">FLIP</button>
|
||||
</div>
|
||||
<span class="sea-stack-name">Levity</span>
|
||||
</div>
|
||||
@@ -109,6 +110,59 @@
|
||||
<button type="button" id="id_sea_cancel" class="btn btn-cancel btn-sm">NVM</button>
|
||||
|
||||
</div>{# /.sea-modal-wrap #}
|
||||
|
||||
{# ── Sea stage — big card viewer ─────────────────────────────────────────── #}
|
||||
<div class="sea-stage" id="id_sea_stage" style="display:none">
|
||||
<div class="sea-stage-backdrop"></div>
|
||||
<div class="sea-stage-content">
|
||||
<div class="sig-stage-card sea-stage-card" style="--sig-card-w:140px">
|
||||
<div class="fan-card-corner fan-card-corner--tl">
|
||||
<span class="fan-corner-rank"></span>
|
||||
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
||||
</div>
|
||||
<div class="fan-card-face">
|
||||
<div class="fan-card-face-upright">
|
||||
<p class="fan-card-name-group"></p>
|
||||
<p class="sig-qualifier-above"></p>
|
||||
<h3 class="fan-card-name"></h3>
|
||||
<p class="sig-qualifier-below"></p>
|
||||
</div>
|
||||
<p class="fan-card-arcana"></p>
|
||||
<div class="fan-card-face-reversal">
|
||||
<p class="fan-card-reversal-name"></p>
|
||||
<p class="fan-card-reversal-qualifier"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fan-card-corner fan-card-corner--br">
|
||||
<span class="fan-corner-rank"></span>
|
||||
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sig-stat-block sea-stat-block">
|
||||
<button class="btn btn-reverse sea-spin-btn" type="button">SPIN</button>
|
||||
<button class="btn btn-info sea-fyi-btn" type="button">FYI</button>
|
||||
<div class="stat-face stat-face--upright">
|
||||
<p class="stat-face-label">Emanation</p>
|
||||
<ul class="stat-keywords" id="id_sea_stat_upright"></ul>
|
||||
</div>
|
||||
<div class="stat-face stat-face--reversed">
|
||||
<p class="stat-face-label">Reversal</p>
|
||||
<ul class="stat-keywords" id="id_sea_stat_reversed"></ul>
|
||||
</div>
|
||||
<div class="sig-info" id="id_sea_fyi_panel" style="display:none">
|
||||
<div class="sig-info-header">
|
||||
<h4 class="sig-info-title"></h4>
|
||||
<p class="sig-info-type"></p>
|
||||
</div>
|
||||
<p class="sig-info-effect"></p>
|
||||
<span class="sig-info-index"></span>
|
||||
</div>
|
||||
<button class="btn btn-nav-left sea-fyi-prev" type="button">PRV</button>
|
||||
<button class="btn btn-nav-right sea-fyi-next" type="button">NXT</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>{# /.sea-overlay #}
|
||||
|
||||
<script>
|
||||
@@ -129,7 +183,6 @@
|
||||
const pickSeaBtn = document.getElementById('id_pick_sea_btn');
|
||||
if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea);
|
||||
cancelBtn.addEventListener('click', closeSea);
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSea(); });
|
||||
|
||||
// ── Deck draw ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -174,22 +227,6 @@
|
||||
if (ok) ok.style.display = '';
|
||||
}
|
||||
|
||||
function _fillPos(sel, card, isLevity) {
|
||||
const cell = overlay.querySelector(sel);
|
||||
if (!cell) return;
|
||||
const slot = cell.querySelector('.sea-card-slot');
|
||||
if (!slot) return;
|
||||
slot.classList.remove('sea-card-slot--empty');
|
||||
slot.classList.add('sea-card-slot--filled');
|
||||
slot.classList.add(isLevity ? 'sea-card-slot--levity' : 'sea-card-slot--gravity');
|
||||
slot.dataset.cardId = String(card.id);
|
||||
slot.innerHTML =
|
||||
`<span class="fan-corner-rank">${card.corner_rank}</span>` +
|
||||
(card.suit_icon ? `<i class="fa-solid ${card.suit_icon}"></i>` : '');
|
||||
_filled++;
|
||||
if (lockBtn) lockBtn.disabled = (_filled < 6);
|
||||
}
|
||||
|
||||
function _reset() {
|
||||
_filled = 0;
|
||||
_hideOk();
|
||||
@@ -198,8 +235,10 @@
|
||||
s.classList.add('sea-card-slot--empty');
|
||||
s.innerHTML = '';
|
||||
delete s.dataset.cardId;
|
||||
delete s.dataset.posKey;
|
||||
});
|
||||
if (lockBtn) lockBtn.disabled = true;
|
||||
if (window.SeaDeal) SeaDeal.resetHand();
|
||||
_fetchDeck();
|
||||
}
|
||||
|
||||
@@ -224,7 +263,11 @@
|
||||
const pile = isLevity ? levityPile : gravityPile;
|
||||
const card = pile.length ? pile.shift() : null;
|
||||
const pos = _nextPosSelector();
|
||||
if (card && pos) _fillPos(pos, card, isLevity);
|
||||
if (card && pos) {
|
||||
_filled++;
|
||||
if (lockBtn) lockBtn.disabled = (_filled < 6);
|
||||
if (window.SeaDeal) SeaDeal.openStage(card, pos, isLevity);
|
||||
}
|
||||
_hideOk();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -113,5 +113,6 @@
|
||||
<script src="{% static 'apps/epic/gatekeeper.js' %}"></script>
|
||||
<script src="{% static 'apps/epic/role-select.js' %}"></script>
|
||||
<script src="{% static 'apps/epic/sig-select.js' %}"></script>
|
||||
<script src="{% static 'apps/epic/sea.js' %}"></script>
|
||||
<script src="{% static 'apps/epic/tray.js' %}"></script>
|
||||
{% endblock scripts %}
|
||||
|
||||
Reference in New Issue
Block a user