mtg_python_deckbuilder/code/web/templates/partials/deck_summary.html

832 lines
No EOL
43 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<div id="deck-summary" data-summary>
<hr class="summary-divider" />
<h4>Deck Summary</h4>
<section class="summary-section">
<h5>Card Types</h5>
<div class="summary-view-controls">
<span class="muted">View:</span>
<div class="seg" role="tablist" aria-label="Type view">
<button type="button" class="seg-btn" data-view="list" aria-selected="true" onclick="(function(btn){var list=document.getElementById('typeview-list');var thumbs=document.getElementById('typeview-thumbs');if(!list||!thumbs)return;list.classList.remove('hidden');thumbs.classList.add('hidden');btn.setAttribute('aria-selected','true');var other=btn.parentElement.querySelector('.seg-btn[data-view=thumbs]');if(other)other.setAttribute('aria-selected','false');try{localStorage.setItem('summaryTypeView','list');}catch(e){}})(this)">List</button>
<button type="button" class="seg-btn" data-view="thumbs" onclick="(function(btn){var list=document.getElementById('typeview-list');var thumbs=document.getElementById('typeview-thumbs');if(!list||!thumbs)return;list.classList.add('hidden');thumbs.classList.remove('hidden');btn.setAttribute('aria-selected','true');var other=btn.parentElement.querySelector('.seg-btn[data-view=list]');if(other)other.setAttribute('aria-selected','false');try{localStorage.setItem('summaryTypeView','thumbs');}catch(e){}; (function(){var tv=document.getElementById('typeview-thumbs'); if(!tv) return; tv.querySelectorAll('.stack-wrap').forEach(function(sw){var grid=sw.querySelector('.stack-grid'); if(!grid) return; var cs=getComputedStyle(sw); var cardW=parseFloat(cs.getPropertyValue('--card-w'))||160; var gap=10; var width=sw.clientWidth; if(!width||width<cardW){ sw.style.setProperty('--cols','1'); return;} var cols=Math.max(1,Math.floor((width+gap)/(cardW+gap))); sw.style.setProperty('--cols',String(cols));}); })();})(this)">Thumbnails</button>
</div>
</div>
<div style="display:none" hx-on:load="(function(){try{var mode=localStorage.getItem('summaryTypeView')||'list';if(mode==='thumbs'){var list=document.getElementById('typeview-list');var thumbs=document.getElementById('typeview-thumbs');if(list&&thumbs){list.classList.add('hidden');thumbs.classList.remove('hidden');var lb=document.querySelector('.seg-btn[data-view=list]');var tb=document.querySelector('.seg-btn[data-view=thumbs]');if(lb&&tb){lb.setAttribute('aria-selected','false');tb.setAttribute('aria-selected','true');}thumbs.querySelectorAll('.stack-wrap').forEach(function(sw){var grid=sw.querySelector('.stack-grid');if(!grid)return;var cs=getComputedStyle(sw);var cardW=parseFloat(cs.getPropertyValue('--card-w'))||160;var gap=10;var width=sw.clientWidth;if(!width||width<cardW){sw.style.setProperty('--cols','1');return;}var cols=Math.max(1,Math.floor((width+gap)/(cardW+gap)));sw.style.setProperty('--cols',String(cols));});}}catch(e){}})()"></div>
{% set tb = summary.type_breakdown %}
{% set synergies_norm = [] %}
{% if synergies %}
{% set synergies_norm = synergies|map('trim')|map('lower')|list %}
{% endif %}
{% if tb and tb.counts %}
<style>
.seg { display:inline-flex; border:1px solid var(--border); border-radius:8px; overflow:hidden; }
.seg-btn { background:var(--panel); color:var(--text); border:none; padding:.35rem .6rem; cursor:pointer; font-size:12px; }
.seg-btn[aria-selected="true"] { background:var(--bg); }
.typeview { margin-top:.25rem; }
.typeview.hidden { display:none; }
.stack-wrap { --card-w: 160px; --card-h: 224px; --cols: 9; --overlap: .5; overflow: visible; padding: 6px 0 calc(var(--card-h) * (1 - var(--overlap))) 0; }
.stack-grid { display: grid; grid-template-columns: repeat(var(--cols), var(--card-w)); grid-auto-rows: calc(var(--card-h) * var(--overlap)); column-gap: 10px; }
.stack-card { width: var(--card-w); height: var(--card-h); border-radius:8px; box-shadow: 0 6px 18px rgba(0,0,0,.55); border:1px solid var(--border); background:var(--panel); transition: transform .06s ease, box-shadow .06s ease; position: relative; }
.stack-card img { width: var(--card-w); height: var(--card-h); display:block; border-radius:8px; }
.stack-card:hover { z-index: 999; transform: translateY(-2px); box-shadow: 0 10px 22px rgba(0,0,0,.6); }
.count-badge { position:absolute; top:6px; right:6px; background:var(--panel); color:var(--text); border:1px solid var(--border); border-radius:12px; font-size:12px; line-height:18px; height:18px; padding:0 6px; pointer-events:none; }
.owned-badge { position:absolute; top:6px; left:6px; background:var(--panel); color:var(--text); border:1px solid var(--border); border-radius:12px; font-size:12px; line-height:18px; height:18px; min-width:18px; padding:0 6px; text-align:center; pointer-events:none; z-index: 2; }
.dfc-thumb-badge { position:absolute; bottom:8px; left:6px; background:rgba(15,23,42,.92); border:1px solid #34d399; color:#bbf7d0; border-radius:12px; font-size:11px; line-height:18px; height:18px; padding:0 6px; pointer-events:none; }
.dfc-thumb-badge.counts { border-color:#60a5fa; color:#bfdbfe; }
.owned-flag { font-size:.95rem; opacity:.9; }
</style>
<div id="typeview-list" class="typeview">
{% for t in tb.order %}
<div class="summary-type-heading">
{{ t }} — {{ tb.counts[t] }}{% if tb.total %} ({{ '%.1f' % (tb.counts[t] * 100.0 / tb.total) }}%){% endif %}
</div>
{% set clist = tb.cards.get(t, []) %}
{% if clist %}
<style>
.list-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap:.35rem .75rem; margin:.25rem 0 .75rem 0; }
@media (max-width: 1199px) {
.list-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
}
.list-row { display:grid; grid-template-columns: 4ch 1.25ch minmax(0,1fr) auto 1.6em; align-items:center; column-gap:.45rem; width:100%; }
.list-row .count { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-variant-numeric: tabular-nums; font-feature-settings: 'tnum'; text-align:right; color:#94a3b8; }
.list-row .times { color:#94a3b8; text-align:center; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.list-row .name { display:inline-block; padding: 2px 4px; border-radius: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.list-row .flip-slot { min-width:2.4em; display:flex; justify-content:flex-start; align-items:center; }
.list-row .owned-flag { width: 1.6em; min-width: 1.6em; text-align:center; display:inline-block; }
.dfc-land-chip { display:inline-flex; align-items:center; gap:.25rem; padding:2px 6px; border-radius:999px; font-size:11px; font-weight:600; background:var(--panel); border:1px solid var(--border); color:var(--text); line-height:1; }
.dfc-land-chip.extra { border-color:#34d399; color:#a7f3d0; }
.dfc-land-chip.counts { border-color:#60a5fa; color:#bfdbfe; }
</style>
<div class="list-grid"{% if virtualize %} data-virtualize="list" data-virtualize-min="90"{% endif %}>
{% for c in clist %}
{# Compute overlaps with detected deck synergies when available #}
{% set overlaps = [] %}
{% if synergies_norm and c.tags %}
{% for tg in c.tags %}
{% set tag_trim = tg|trim %}
{% if tag_trim and (tag_trim|lower) in synergies_norm and tag_trim not in overlaps %}
{% set _ = overlaps.append(tag_trim) %}
{% endif %}
{% endfor %}
{% endif %}
<div class="list-row {% if (game_changers and (c.name in game_changers)) or ('game_changer' in (c.role or '') or 'Game Changer' in (c.role or '')) %}game-changer{% endif %}"
data-card-name="{{ c.name }}" data-original-name="{{ c.name }}" data-role="{{ c.role }}" data-tags="{{ (c.tags|map('trim')|join(', ')) if c.tags else '' }}"{% if overlaps %} data-overlaps="{{ overlaps|join(', ') }}"{% endif %}>
{% set cnt = c.count if c.count else 1 %}
{% set owned = (owned_set is defined and c.name and (c.name|lower in owned_set)) %}
<span class="count">{{ cnt }}</span>
<span class="times">x</span>
<span class="name dfc-anchor" title="{{ c.name }}" data-card-name="{{ c.name }}" data-count="{{ cnt }}" data-role="{{ c.role }}" data-tags="{{ (c.tags|map('trim')|join(', ')) if c.tags else '' }}"{% if c.metadata_tags %} data-metadata-tags="{{ (c.metadata_tags|map('trim')|join(', ')) }}"{% endif %}{% if overlaps %} data-overlaps="{{ overlaps|join(', ') }}"{% endif %}>{{ c.name }}</span>
<span class="flip-slot" aria-hidden="true">
{% if c.dfc_land %}
<span class="dfc-land-chip {% if c.dfc_adds_extra_land %}extra{% else %}counts{% endif %}" title="{{ c.dfc_note or 'Modal double-faced land' }}">DFC land{% if c.dfc_adds_extra_land %} +1{% endif %}</span>
{% endif %}
</span>
<span class="owned-flag" title="{{ 'Owned' if owned else 'Not owned' }}" aria-label="{{ 'Owned' if owned else 'Not owned' }}">{% if owned %}✔{% else %}✖{% endif %}</span>
</div>
{% endfor %}
</div>
{% else %}
<div class="muted mb-3">No cards in this type.</div>
{% endif %}
{% endfor %}
</div>
<div id="typeview-thumbs" class="typeview hidden">
{% for t in tb.order %}
<div class="summary-type-heading">
{{ t }} — {{ tb.counts[t] }}{% if tb.total %} ({{ '%.1f' % (tb.counts[t] * 100.0 / tb.total) }}%){% endif %}
</div>
{% set clist = tb.cards.get(t, []) %}
{% if clist %}
<div class="stack-wrap">
<div class="stack-grid">
{% for c in clist %}
{% set cnt = c.count if c.count else 1 %}
{% set owned = (owned_set is defined and c.name and (c.name|lower in owned_set)) %}
{% set overlaps = [] %}
{% if synergies_norm and c.tags %}
{% for tg in c.tags %}
{% set tag_trim = tg|trim %}
{% if tag_trim and (tag_trim|lower) in synergies_norm and tag_trim not in overlaps %}
{% set _ = overlaps.append(tag_trim) %}
{% endif %}
{% endfor %}
{% endif %}
<div class="stack-card {% if (game_changers and (c.name in game_changers)) or ('game_changer' in (c.role or '') or 'Game Changer' in (c.role or '')) %}game-changer{% endif %}">
<img class="card-thumb" loading="lazy" decoding="async" src="{{ c.name|card_image('normal') }}" alt="{{ c.name }} image" data-card-name="{{ c.name }}" data-count="{{ cnt }}" data-role="{{ c.role }}" data-tags="{{ (c.tags|map('trim')|join(', ')) if c.tags else '' }}"{% if overlaps %} data-overlaps="{{ overlaps|join(', ') }}"{% endif %}
srcset="{{ c.name|card_image('small') }} 160w, {{ c.name|card_image('normal') }} 488w"
sizes="(max-width: 1200px) 160px, 240px" />
<div class="count-badge">{{ cnt }}x</div>
<div class="owned-badge" title="{{ 'Owned' if owned else 'Not owned' }}" aria-label="{{ 'Owned' if owned else 'Not owned' }}">{% if owned %}✔{% else %}✖{% endif %}</div>
{% if c.dfc_land %}
<div class="dfc-thumb-badge {% if c.dfc_adds_extra_land %}extra{% else %}counts{% endif %}" title="{{ c.dfc_note or 'Modal double-faced land' }}">DFC{% if c.dfc_adds_extra_land %}+1{% endif %}</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% else %}
<div class="muted mb-3">No cards in this type.</div>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="muted">No type data available.</div>
{% endif %}
</section>
<!-- Deck Summary initializer script moved below markup for proper element availability -->
<!-- Land Summary -->
{% set land = summary.land_summary if summary else None %}
{% if land %}
<section class="summary-section-lg">
<h5>Land Summary</h5>
<div class="muted summary-type-heading mb-1">
{{ land.headline or ('Lands: ' ~ (land.traditional or 0)) }}
</div>
<div class="deck-metrics-wrap">
<div class="muted">Traditional land slots: <strong>{{ land.traditional or 0 }}</strong></div>
<div class="muted">MDFC land additions: <strong>{{ land.dfc_lands or 0 }}</strong></div>
<div class="muted">Total with MDFCs: <strong>{{ land.with_dfc or land.traditional or 0 }}</strong></div>
</div>
{% if land.dfc_cards %}
<details class="mt-2">
<summary>MDFC mana sources ({{ land.dfc_cards|length }})</summary>
<ul class="land-breakdown-list">
{% for card in land.dfc_cards %}
{% set extra = card.adds_extra_land or card.counts_as_extra %}
{% set colors = card.colors or [] %}
<li class="muted land-breakdown-item">
<span class="chip"><span class="dot dot-green"></span> {{ card.name }} ×{{ card.count or 1 }}</span>
<span>Colors: {{ colors|join(', ') if colors else '' }}</span>
{% if extra %}
<span class="chip land-note-chip-expand">{{ card.note or 'Adds extra land slot' }}</span>
{% else %}
<span class="chip land-note-chip-counts">{{ card.note or 'Counts as land slot' }}</span>
{% endif %}
{% if card.faces %}
<ul class="land-breakdown-subs">
{% for face in card.faces %}
{% set face_name = face.get('face') or face.get('faceName') or 'Face' %}
{% set face_type = face.get('type') or '' %}
{% set mana_cost = face.get('mana_cost') %}
{% set mana_value = face.get('mana_value') %}
{% set produces = face.get('produces_mana') %}
<li class="land-breakdown-sub">
<span>{{ face_name }}</span>
<span>— {{ face_type }}</span>
{% if mana_cost %}<span>• Mana Cost: {{ mana_cost }}</span>{% endif %}
{% if mana_value is not none %}<span>• MV: {{ mana_value }}</span>{% endif %}
{% if produces %}<span>• Produces mana</span>{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
</details>
{% endif %}
</section>
{% endif %}
<!-- Mana Overview Row: Pips • Sources • Curve -->
<section class="summary-section-lg">
<details class="analytics-accordion" id="mana-overview-accordion" data-lazy-load data-analytics-type="mana">
<summary class="combo-summary">
<span>Mana Overview</span>
<span class="muted text-xs font-normal ml-2">(pips • sources • curve)</span>
</summary>
<div class="analytics-content mt-3">
<h5 class="mt-0 mb-2">Mana Overview</h5>
{% set deck_colors = summary.colors or [] %}
<div class="mana-row">
<!-- Pips Panel -->
<div class="mana-panel">
<div class="muted mana-panel-heading">Mana Pips (non-lands)</div>
{% set pd = summary.pip_distribution %}
{% if pd %}
{% set colors = deck_colors if deck_colors else ['W','U','B','R','G'] %}
<div class="chart-bars">
{% for color in colors %}
{% set w = (pd.weights[color] if pd.weights and color in pd.weights else 0) %}
{% set pct = (w * 100) | int %}
<div class="chart-column">
{% set count_val = (pd.counts[color] if pd.counts and color in pd.counts else 0) %}
{% set pc = pd['cards'] if 'cards' in pd else None %}
{% set c_cards = (pc[color] if pc and (color in pc) else []) %}
{% set parts = [] %}
{% for c in c_cards %}
{% set label = c.name ~ ((" ×" ~ c.count) if c.count and c.count>1 else '') %}
{% if c.dfc %}
{% set label = label ~ ' (DFC)' %}
{% endif %}
{% set _ = parts.append(label) %}
{% endfor %}
{% set cards_line = parts|join(' • ') %}
{% set pct_f = (pd.weights[color] * 100) if pd.weights and color in pd.weights else 0 %}
<svg width="28" height="120" aria-label="{{ color }} {{ pct }}%" class="chart-svg" data-type="pips" data-color="{{ color }}" data-count="{{ '%.1f' % count_val }}" data-pct="{{ '%.1f' % pct_f }}" data-cards="{{ cards_line }}">
<rect x="2" y="2" width="24" height="116" fill="#14171c" stroke="var(--border)" rx="4" ry="4" pointer-events="all"></rect>
{% set h = (pct * 1.0) | int %}
{% set bar_h = (h if h>2 else 2) %}
{% set y = 118 - bar_h %}
<rect x="2" y="{{ y }}" width="24" height="{{ bar_h }}" fill="#3b82f6" rx="4" ry="4" pointer-events="all"></rect>
</svg>
<div class="muted mt-1">{{ color }}</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="muted">No pip data.</div>
{% endif %}
</div>
<!-- Sources Panel -->
<div class="mana-panel">
<div class="flex items-center justify-between gap-3 mb-1">
<div class="muted mana-panel-heading">Mana Sources</div>
<label class="muted text-xs form-label-icon">
<input type="checkbox" id="toggle-show-c" /> Show colorless (C)
</label>
</div>
{% set mg = summary.mana_generation %}
{% if mg %}
{% set colors = deck_colors if deck_colors else ['W','U','B','R','G'] %}
{# If colorless sources exist, append 'C' to colors for display #}
{% if 'C' in mg and (mg.get('C', 0) > 0) and ('C' not in colors) %}
{% set colors = colors + ['C'] %}
{% endif %}
{% set ns = namespace(max_src=0) %}
{% for color in colors %}
{% set val = mg.get(color, 0) %}
{% if val > ns.max_src %}{% set ns.max_src = val %}{% endif %}
{% endfor %}
{% set denom = (ns.max_src if ns.max_src and ns.max_src > 0 else 1) %}
<div class="sources-bars chart-bars">
{% for color in colors %}
{% set val = mg.get(color, 0) %}
{% set pct = (val * 100 / denom) | int %}
{% set pct_f = (100.0 * (val / (mg.total_sources or 1))) %}
{% set mgc = mg['cards'] if 'cards' in mg else None %}
{% set c_cards = (mgc[color] if mgc and (color in mgc) else []) %}
{% set parts = [] %}
{% for c in c_cards %}
{% set _ = parts.append(c.name ~ ((" ×" ~ c.count) if c.count and c.count>1 else '')) %}
{% endfor %}
{% set cards_line = parts|join(' • ') %}
<div class="chart-column" data-color="{{ color }}">
<svg width="28" height="120" aria-label="{{ color }} {{ val }}" class="chart-svg" data-type="sources" data-color="{{ color }}" data-val="{{ val }}" data-pct="{{ '%.1f' % pct_f }}" data-cards="{{ cards_line }}">
<rect x="2" y="2" width="24" height="116" fill="#14171c" stroke="var(--border)" rx="4" ry="4" pointer-events="all"></rect>
{% set bar_h = (pct if pct>2 else 2) %}
{% set y = 118 - bar_h %}
<rect x="2" y="{{ y }}" width="24" height="{{ bar_h }}" fill="#10b981" rx="4" ry="4" pointer-events="all"></rect>
</svg>
<div class="muted mt-1">{{ color }}</div>
</div>
{% endfor %}
</div>
<div class="muted mt-1">Total sources: {{ mg.total_sources or 0 }}</div>
{% else %}
<div class="muted">No mana source data.</div>
{% endif %}
</div>
<!-- Curve Panel -->
<div class="mana-panel">
<div class="muted mana-panel-heading">Mana Curve (non-lands)</div>
{% set mc = summary.mana_curve %}
{% if mc %}
{% set ts = mc.total_spells or 0 %}
{% set denom = (ts if ts and ts > 0 else 1) %}
<div class="chart-bars">
{% for label in ['0','1','2','3','4','5','6+'] %}
{% set val = mc.get(label, 0) %}
{% set pct = (val * 100 / denom) | int %}
{% set cards = (mc.cards[label] if mc.cards and (label in mc.cards) else []) %}
{% set parts = [] %}
{% for c in cards %}
{% set _ = parts.append(c.name ~ ((" ×" ~ c.count) if c.count and c.count>1 else '')) %}
{% endfor %}
{% set cards_line = parts|join(' • ') %}
{% set pct_f = (100.0 * (val / denom)) %}
<div class="chart-column">
<svg width="28" height="120" aria-label="{{ label }} {{ val }}" class="chart-svg" data-type="curve" data-label="{{ label }}" data-val="{{ val }}" data-pct="{{ '%.1f' % pct_f }}" data-cards="{{ cards_line }}">
<rect x="2" y="2" width="24" height="116" fill="#14171c" stroke="var(--border)" rx="4" ry="4" pointer-events="all"></rect>
{% set bar_h = (pct if pct>2 else 2) %}
{% set y = 118 - bar_h %}
<rect x="2" y="{{ y }}" width="24" height="{{ bar_h }}" fill="#f59e0b" rx="4" ry="4" pointer-events="all"></rect>
</svg>
<div class="muted mt-1">{{ label }}</div>
</div>
{% endfor %}
</div>
<div class="muted mt-1">Total spells: {{ mc.total_spells or 0 }}</div>
{% else %}
<div class="muted">No curve data.</div>
{% endif %}
</div>
</div>
</div>
</details>
</section>
<!-- Test Hand (7 random cards; duplicates allowed only for basic lands) -->
<section class="summary-section-lg">
<details class="analytics-accordion" id="test-hand-accordion" data-lazy-load data-analytics-type="testhand">
<summary class="combo-summary">
<span>Test Hand</span>
<span class="muted text-xs font-normal ml-2">(draw 7 random cards)</span>
</summary>
<div class="analytics-content mt-3">
<h5 class="flex items-center gap-3 flex-wrap mt-0 mb-1">Test Hand
<span class="muted text-xs font-normal">Draw 7 at random (no repeats except for basic lands).</span>
</h5>
<div class="flex gap-2 items-center flex-wrap mb-2">
<button type="button" id="btn-new-hand">New Hand</button>
</div>
<div class="stack-wrap hand-row-overlap" id="test-hand">
<div class="stack-grid" id="test-hand-grid"></div>
</div>
<script>
(function(){
var GC_SET = (function(){
try {
var els = document.querySelectorAll('#typeview-list .game-changer [data-card-name], #typeview-thumbs .game-changer [data-card-name]');
var s = new Set();
els.forEach(function(el){ var n = el.getAttribute('data-card-name'); if(n) s.add(n); });
return s;
} catch(e) { return new Set(); }
})();
var BASE_BASICS = ["Plains","Island","Swamp","Mountain","Forest","Wastes"];
function isBasicLand(name){
if (!name) return false;
if (BASE_BASICS.indexOf(name) >= 0) return true;
if (name.startsWith('Snow-Covered ')) {
var base = name.substring('Snow-Covered '.length);
return BASE_BASICS.indexOf(base) >= 0;
}
return false;
}
function collectDeck(){
var deck = [];
document.querySelectorAll('#typeview-list span[data-card-name]').forEach(function(el){
var name = el.getAttribute('data-card-name');
var cnt = parseInt(el.getAttribute('data-count') || '1', 10);
if (name) deck.push({ name: name, count: (isFinite(cnt) && cnt>0 ? cnt : 1) });
});
return deck;
}
function buildPool(deck){
var pool = [];
deck.forEach(function(it){
var n = Math.max(1, parseInt(it.count || 1, 10));
for (var i=0;i<n;i++){ pool.push(it.name); }
});
return pool;
}
function drawHand(deck){
var pool = buildPool(deck);
if (!pool.length) return [];
var picked = {};
var hand = [];
var attempts = 0;
while (hand.length < 7 && attempts < 500) {
attempts++;
var idx = Math.floor(Math.random() * pool.length);
var name = pool[idx];
if (!name) continue;
var allowDup = isBasicLand(name);
if (!allowDup && picked[name]) continue;
hand.push(name);
if (!allowDup) picked[name] = true;
pool.splice(idx, 1);
if (!pool.length) break;
}
return hand;
}
function compress(hand){
var map = {};
hand.forEach(function(n){ map[n] = (map[n]||0) + 1; });
var out = [];
Object.keys(map).forEach(function(n){ out.push({name:n, count: map[n]}); });
out.sort(function(a,b){ return hand.indexOf(a.name) - hand.indexOf(b.name); });
return out;
}
function render(hand){
var grid = document.getElementById('test-hand-grid');
if (!grid) return;
grid.innerHTML = '';
var host = document.getElementById('test-hand');
if (host){ host.style.setProperty('--mid', (hand.length ? (hand.length - 1)/2 : 0)); host.style.setProperty('--count', hand.length); }
hand.forEach(function(name, idx){
if (!name) return;
var div = document.createElement('div');
div.className = 'stack-card';
if (GC_SET && GC_SET.has(name)) {
div.className += ' game-changer';
}
div.style.setProperty('--i', idx);
var mid = (hand.length - 1) / 2;
var diff = Math.abs(idx - mid);
var peakRaise = 22; // px raise at center (accentuate arc)
var dropPer = 5; // linear component per distance step
// Strengthen curve so the very outer cards sit lower
var outerExtra = 24; // quadratic downward px strongest at edges
var edgeBias = 8; // cubic bias for far edges
var norm = (mid > 0 ? diff / mid : 0); // 0..1
var curve = norm * norm * outerExtra; // quadratic easing
var curve3 = norm * norm * norm * edgeBias; // cubic accentuation
var y = (diff * dropPer) + curve + curve3 - peakRaise; // center negative (raised), edges positive (lower)
// Minor smoothing so second-from-edge isn't too low
if (mid >= 2 && Math.abs(diff - (mid - 1)) < 0.001) { y += 2; }
div.style.setProperty('--ty', y + 'px');
div.innerHTML = (
'<img src="/api/images/normal/' + encodeURIComponent(name) + '" alt="' + name + '" data-card-name="' + name + '" />'
);
grid.appendChild(div);
});
}
function newHand(){ var deck = collectDeck(); render(drawHand(deck)); }
var btn = document.getElementById('btn-new-hand');
if (btn) btn.addEventListener('click', newHand);
// Fan effect — desktop default (>=900px, hover-capable pointer)
var handEl = document.getElementById('test-hand');
(function(){
if(!handEl) return;
var onEnter = function(){ handEl.classList.add('fan'); };
var onLeave = function(){ handEl.classList.remove('fan'); };
var mq = window.matchMedia('(any-hover: hover) and (pointer: fine) and (min-width: 900px)');
function attach(){ handEl.addEventListener('mouseenter', onEnter); handEl.addEventListener('mouseleave', onLeave); }
function detach(){ handEl.removeEventListener('mouseenter', onEnter); handEl.removeEventListener('mouseleave', onLeave); }
// Desktop: fan is default; Mobile/tablet: no fan
function apply(){
if (mq.matches) {
detach();
handEl.classList.add('fan');
} else {
detach();
handEl.classList.remove('fan');
}
}
try {
if (typeof mq.addEventListener === 'function') mq.addEventListener('change', apply);
else if (typeof mq.addListener === 'function') mq.addListener(apply);
} catch(_) {}
apply();
})();
newHand();
})();
</script>
<style>
/* Base overlapping hand: 160px cards (same as deck thumbnails) */
#test-hand.hand-row-overlap{ padding-bottom:.9rem; --fan-gap:28px; --card-w:160px; --card-h:224px; }
#test-hand.hand-row-overlap .stack-grid{ display:flex !important; gap:0; overflow-x:auto; scrollbar-width:thin; }
#test-hand.hand-row-overlap .stack-card{ width:var(--card-w); height:var(--card-h); transition: transform .25s ease, margin-left .25s ease, width .25s ease, height .25s ease; flex: 0 0 auto; }
/* Dynamic overlap: show ~30% of next card */
#test-hand.hand-row-overlap .stack-card + .stack-card{ margin-left: calc(var(--card-w) * -0.7); }
#test-hand.hand-row-overlap .stack-card img{ width:var(--card-w); height:var(--card-h); display:block; }
#test-hand.hand-row-overlap .stack-card:hover{ z-index:999; transform:translateY(-4px); }
/* Desktop sizing for Test Hand */
@media (min-width:900px){
#test-hand.hand-row-overlap{ --card-w:280px; --card-h:392px; --fan-gap:40px; }
#test-hand.hand-row-overlap .stack-card + .stack-card{ margin-left: calc(var(--card-w) * -0.7); }
#test-hand.hand-row-overlap.fan{ --card-w:280px; --card-h:392px; }
}
/* Hover fan-out: spread cards horizontally, enlarge if not already large */
#test-hand.hand-row-overlap.fan{ --fan-overlap:0.40; --fan-gap:0px; }
#test-hand.hand-row-overlap.fan .stack-card + .stack-card{ margin-left:0; }
#test-hand.hand-row-overlap.fan .stack-grid{ justify-content:center; overflow:visible; padding-left:0; }
/* Fan transform now applies a 40% overlap (visible width ~60%) while keeping center aligned */
#test-hand.hand-row-overlap.fan .stack-card{ position:relative; transform: translateX(calc((var(--i) - var(--mid)) * (var(--fan-gap) - (var(--card-w) * var(--fan-overlap))))) translateY(var(--ty,0px)) rotate(calc((var(--i) - var(--mid)) * 4deg)); }
/* Responsive adjustments */
@media (max-width:900px){
#test-hand.hand-row-overlap.fan{ --card-w:240px; --card-h:336px; --fan-overlap:0.40; --fan-gap:0px; }
}
@media (max-width:640px){
#test-hand.hand-row-overlap{ --card-w:150px; --card-h:210px; }
#test-hand.hand-row-overlap.fan{ --card-w:200px; --card-h:280px; --fan-overlap:0.40; --fan-gap:0px; }
}
@media (min-width:640px) and (max-width:899px){
#test-hand.hand-row-overlap{ --card-w:160px; --card-h:224px; }
}
@media (max-width:480px){
#test-hand.hand-row-overlap{ --card-w:140px; --card-h:196px; }
#test-hand.hand-row-overlap .stack-card + .stack-card{ margin-left: calc(var(--card-w) * -0.65); }
#test-hand.hand-row-overlap.fan{ --card-w:180px; --card-h:252px; --fan-overlap:0.40; --fan-gap:0px; }
#test-hand.hand-row-overlap.fan .stack-grid{ padding-left:0; }
}
</style>
</div>
</details>
</section>
<style>
.chart-tooltip { position: fixed; background: var(--panel); color: var(--text); border: 1px solid var(--border); padding: .4rem .55rem; border-radius: 6px; font-size: 12px; line-height: 1.3; white-space: pre-line; z-index: 9999; display: none; box-shadow: 0 4px 16px rgba(0,0,0,.4); max-width: 90vw; }
/* Pinned tooltip gets pointer events for Copy button */
.chart-tooltip.pinned { pointer-events: auto; border-color: #f59e0b; box-shadow: 0 4px 20px rgba(245,158,11,.3); }
/* Unpinned tooltip has no pointer events (hover only) */
.chart-tooltip:not(.pinned) { pointer-events: none; }
/* Cross-highlight from charts to cards */
.chart-highlight { border-radius: 6px; background: rgba(245,158,11,.08); box-shadow: 0 0 0 2px #f59e0b inset; }
/* For list view, ensure baseline padding so no layout shift on highlight */
#typeview-list .list-row .name { display:inline-block; padding: 2px 4px; border-radius: 6px; }
/* Ensure stack-card gets visible highlight */
.stack-card.chart-highlight { box-shadow: 0 0 0 2px #f59e0b, 0 6px 18px rgba(0,0,0,.55); }
/* Chart columns get cursor pointer */
.chart-column svg { cursor: pointer; transition: opacity 0.15s ease; }
.chart-column svg:hover { opacity: 0.85; }
/* Chart bar backgrounds - use CSS variable for theme support */
.chart-svg rect:first-of-type { fill: var(--bg) !important; }
</style>
<script>
(function() {
function ensureTip() {
var tip = document.getElementById('chart-tooltip');
if (!tip) {
tip = document.createElement('div');
tip.id = 'chart-tooltip';
tip.className = 'chart-tooltip';
document.body.appendChild(tip);
}
return tip;
}
var tip = ensureTip();
var hoverTimer = null;
var lastNames = [];
var lastType = '';
var pinnedNames = [];
var pinnedType = '';
var pinnedEl = null;
function clearHoverTimer(){ if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; } }
function position(e) {
tip.style.display = 'block';
var vw = window.innerWidth || document.documentElement.clientWidth;
var vh = window.innerHeight || document.documentElement.clientHeight;
var isMobile = vw < 768;
if (isMobile) {
// Mobile: fixed to lower-right corner
tip.style.right = '8px';
tip.style.bottom = '8px';
tip.style.left = 'auto';
tip.style.top = 'auto';
tip.style.maxWidth = 'calc(100vw - 16px)';
} else {
// Desktop: fixed to lower-left corner
tip.style.left = '8px';
tip.style.bottom = '8px';
tip.style.right = 'auto';
tip.style.top = 'auto';
tip.style.maxWidth = '400px';
}
}
function buildTip(el, isPinned) {
// Render tooltip with safe DOM
tip.innerHTML = '';
var t = el.getAttribute('data-type');
var header = document.createElement('div');
header.style.fontWeight = '600';
header.style.marginBottom = '.25rem';
header.style.display = 'flex';
header.style.alignItems = 'center';
header.style.justifyContent = 'space-between';
header.style.gap = '.5rem';
var titleSpan = document.createElement('span');
var listText = '';
if (t === 'pips') {
titleSpan.textContent = el.dataset.color + ': ' + (el.dataset.count || '0') + ' (' + (el.dataset.pct || '0') + '%)';
listText = (el.dataset.cards || '').split(' • ').filter(Boolean).join('\n');
} else if (t === 'sources') {
titleSpan.textContent = el.dataset.color + ': ' + (el.dataset.val || '0') + ' (' + (el.dataset.pct || '0') + '%)';
listText = (el.dataset.cards || '').split(' • ').filter(Boolean).join('\n');
} else if (t === 'curve') {
titleSpan.textContent = el.dataset.label + ': ' + (el.dataset.val || '0') + ' (' + (el.dataset.pct || '0') + '%)';
listText = (el.dataset.cards || '').split(' • ').filter(Boolean).join('\n');
} else {
titleSpan.textContent = el.getAttribute('aria-label') || '';
}
header.appendChild(titleSpan);
// Add Copy button that works with pinned tooltips
if (listText && isPinned) {
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.fontSize = '11px';
btn.style.padding = '.15rem .35rem';
btn.style.border = '1px solid var(--border)';
btn.style.background = '#12161c';
btn.style.color = '#e5e7eb';
btn.style.borderRadius = '4px';
btn.style.cursor = 'pointer';
btn.style.flexShrink = '0';
btn.addEventListener('click', function(e){
e.stopPropagation();
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(listText);
} else {
var ta = document.createElement('textarea');
ta.value = listText; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta);
}
btn.textContent = 'Copied!';
setTimeout(function(){ btn.textContent = 'Copy'; }, 1200);
} catch(_) {}
});
header.appendChild(btn);
}
tip.appendChild(header);
if (listText) {
var pre = document.createElement('pre');
pre.style.margin = '.25rem 0 0 0';
pre.style.whiteSpace = 'pre-wrap';
pre.style.fontSize = '12px';
pre.textContent = listText;
tip.appendChild(pre);
}
// Add hint for pinning on desktop
if (!isPinned && window.innerWidth >= 768) {
var hint = document.createElement('div');
hint.style.marginTop = '.35rem';
hint.style.fontSize = '11px';
hint.style.color = '#9ca3af';
hint.style.fontStyle = 'italic';
hint.textContent = 'Click to pin';
tip.appendChild(hint);
}
}
function normalizeList(list) {
if (!Array.isArray(list)) return [];
return list.map(function(n){
if (!n) return '';
var s = String(n);
// Strip trailing " ×<num>" count suffix if present
s = s.replace(/\s×\d+$/,'');
return s.trim();
}).filter(Boolean);
}
function unpin() {
if (pinnedEl) {
pinnedEl.style.outline = '';
pinnedEl = null;
}
if (pinnedNames && pinnedNames.length) {
highlightNames(pinnedNames, false);
}
pinnedNames = [];
pinnedType = '';
tip.classList.remove('pinned');
tip.style.display = 'none';
}
function pin(el, e) {
// Unpin previous if different element
if (pinnedEl && pinnedEl !== el) {
unpin();
}
// Toggle: if clicking same element, unpin
if (pinnedEl === el) {
unpin();
return;
}
// Pin new element
pinnedEl = el;
el.style.outline = '2px solid #f59e0b';
el.style.outlineOffset = '2px';
var dataType = el.getAttribute('data-type');
pinnedNames = normalizeList((el.dataset.cards || '').split(' • ').filter(Boolean));
pinnedType = dataType;
tip.classList.add('pinned');
buildTip(el, true);
position(e);
highlightNames(pinnedNames, true);
}
function attach() {
// Attach to SVG elements with data-type for better hover zones
document.querySelectorAll('svg[data-type]').forEach(function(el) {
el.addEventListener('mouseenter', function(e) {
// Don't show hover tooltip if this element is pinned
if (pinnedEl === el) return;
clearHoverTimer();
buildTip(el, false);
position(e);
// Cross-highlight for mana curve bars -> card items
try {
var dataType = el.getAttribute('data-type');
if (dataType === 'curve' || dataType === 'pips' || dataType === 'sources') {
lastNames = normalizeList((el.dataset.cards || '').split(' • ').filter(Boolean));
lastType = dataType;
// Only apply hover highlights if nothing is pinned
if (!pinnedEl) {
highlightNames(lastNames, true);
}
}
} catch(_) {}
});
el.addEventListener('mousemove', function(e) {
if (pinnedEl === el) return;
position(e);
});
el.addEventListener('mouseleave', function() {
// Don't hide if pinned
if (pinnedEl) return;
clearHoverTimer();
hoverTimer = setTimeout(function(){
tip.style.display = 'none';
try { if (lastNames && lastNames.length && !pinnedEl) highlightNames(lastNames, false); } catch(_) {}
lastNames = []; lastType = '';
}, 200);
});
el.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
pin(el, e);
});
});
// Keep tooltip open while hovering it (for pinned tooltips with Copy button)
tip.addEventListener('mouseenter', function(){
clearHoverTimer();
});
tip.addEventListener('mouseleave', function(){
// Don't hide if pinned
if (pinnedEl) return;
tip.style.display = 'none';
try { if (lastNames && lastNames.length) highlightNames(lastNames, false); } catch(_) {}
lastNames = []; lastType = '';
});
// Click outside to unpin
document.addEventListener('click', function(e) {
if (!pinnedEl) return;
// Don't unpin if clicking the tooltip itself or a chart
if (tip.contains(e.target) || e.target.closest('svg[data-type]')) return;
unpin();
});
// Initialize Show C toggle
initShowCToggle();
}
function initShowCToggle(){
var cb = document.getElementById('toggle-show-c');
var container = document.querySelector('.sources-bars');
if (!cb || !container) return;
// Default ON; restore prior state
var pref = 'true';
try { var v = localStorage.getItem('showColorlessC'); if (v!==null) pref = v; } catch(_) {}
cb.checked = (pref !== 'false');
function apply(){
var on = cb.checked;
try { localStorage.setItem('showColorlessC', String(on)); } catch(_) {}
container.querySelectorAll('[data-color="C"]').forEach(function(el){
el.style.display = on ? '' : 'none';
});
}
cb.addEventListener('change', apply);
apply();
}
function highlightNames(names, on){
if (!Array.isArray(names) || names.length === 0) return;
// List view spans - target only the .name span, not the parent .list-row
try {
document.querySelectorAll('#typeview-list .list-row .name[data-card-name]').forEach(function(it){
var n = it.getAttribute('data-card-name');
if (!n) return;
var match = names.indexOf(n) !== -1;
// Toggle highlight only on the inline span so it doesn't fill the entire grid cell
it.classList.toggle('chart-highlight', !!(on && match));
if (!on && !match) it.classList.remove('chart-highlight');
});
} catch(_) {}
// Thumbs view images
try {
document.querySelectorAll('#typeview-thumbs [data-card-name]').forEach(function(it){
var n = it.getAttribute('data-card-name');
if (!n) return;
var tile = it.closest('.stack-card') || it;
var match = names.indexOf(n) !== -1;
tile.classList.toggle('chart-highlight', !!(on && match));
if (!on && !match) tile.classList.remove('chart-highlight');
});
} catch(_) {}
// If virtualized lists are enabled, auto-scroll the Step 5 grid to the first match
try {
if (on && window.scrollCardIntoView && Array.isArray(names) && names.length) {
window.scrollCardIntoView(names[0]);
}
} catch(_) {}
}
attach();
document.addEventListener('htmx:afterSwap', function() { attach(); });
})();
</script>
</div>