mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-22 02:20:13 +01:00
feat(combos): add Combos & Synergies detection, chip-style UI with dual hover; JSON persistence and headless honoring; stage ordering; docs and tests; bump to v2.2.1
This commit is contained in:
parent
cc16c6f13a
commit
6c48fb3437
38 changed files with 2042 additions and 131 deletions
|
|
@ -104,6 +104,9 @@
|
|||
.card-hover { position: fixed; pointer-events: none; z-index: 9999; display: none; }
|
||||
.card-hover-inner { display:flex; gap:12px; align-items:flex-start; }
|
||||
.card-hover img { width: 320px; height: auto; display: block; border-radius: 8px; box-shadow: 0 6px 18px rgba(0,0,0,.55); border: 1px solid var(--border); background: var(--panel); }
|
||||
.card-hover .dual {
|
||||
display:flex; gap:12px; align-items:flex-start;
|
||||
}
|
||||
.card-meta { background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: .5rem .6rem; max-width: 320px; font-size: 13px; line-height: 1.4; box-shadow: 0 6px 18px rgba(0,0,0,.35); }
|
||||
.card-meta ul { margin:.25rem 0; padding-left: 1.1rem; list-style: disc; }
|
||||
.card-meta li { margin:.1rem 0; }
|
||||
|
|
@ -180,9 +183,15 @@
|
|||
inner.className = 'card-hover-inner';
|
||||
var img = document.createElement('img');
|
||||
img.alt = 'Card preview';
|
||||
var img2 = document.createElement('img');
|
||||
img2.alt = 'Card preview'; img2.style.display = 'none';
|
||||
var meta = document.createElement('div');
|
||||
meta.className = 'card-meta';
|
||||
inner.appendChild(img);
|
||||
var dual = document.createElement('div');
|
||||
dual.className = 'dual';
|
||||
dual.appendChild(img);
|
||||
dual.appendChild(img2);
|
||||
inner.appendChild(dual);
|
||||
inner.appendChild(meta);
|
||||
pop.appendChild(inner);
|
||||
document.body.appendChild(pop);
|
||||
|
|
@ -259,12 +268,14 @@
|
|||
if (x + rect.width + 8 > vw) cardPop.style.left = (e.clientX - rect.width - 16) + 'px';
|
||||
if (y + rect.height + 8 > vh) cardPop.style.top = (e.clientY - rect.height - 16) + 'px';
|
||||
}
|
||||
function attachCardHover() {
|
||||
function attachCardHover() {
|
||||
document.querySelectorAll('[data-card-name]').forEach(function(el) {
|
||||
if (el.__cardHoverBound) return; // avoid duplicate bindings
|
||||
el.__cardHoverBound = true;
|
||||
el.addEventListener('mouseenter', function(e) {
|
||||
var img = cardPop.querySelector('img');
|
||||
var img = cardPop.querySelector('.card-hover-inner img');
|
||||
var img2 = cardPop.querySelector('.card-hover-inner .dual img:nth-child(2)');
|
||||
if (img2) img2.style.display = 'none';
|
||||
var meta = cardPop.querySelector('.card-meta');
|
||||
var name = el.getAttribute('data-card-name') || '';
|
||||
var vi = 0; // always start at 'normal' on hover
|
||||
|
|
@ -304,6 +315,33 @@
|
|||
el.addEventListener('mousemove', positionCard);
|
||||
el.addEventListener('mouseleave', function() { cardPop.style.display = 'none'; });
|
||||
});
|
||||
// Dual-card hover for combo rows
|
||||
document.querySelectorAll('[data-combo-names]').forEach(function(el){
|
||||
if (el.__comboHoverBound) return; el.__comboHoverBound = true;
|
||||
el.addEventListener('mouseenter', function(e){
|
||||
var namesAttr = el.getAttribute('data-combo-names') || '';
|
||||
var parts = namesAttr.split('||');
|
||||
var a = (parts[0]||'').trim(); var b = (parts[1]||'').trim();
|
||||
if (!a || !b) return;
|
||||
var img = cardPop.querySelector('.card-hover-inner img');
|
||||
var img2 = cardPop.querySelector('.card-hover-inner .dual img:nth-child(2)');
|
||||
var meta = cardPop.querySelector('.card-meta');
|
||||
if (img2) img2.style.display = '';
|
||||
var vi1 = 0, vi2 = 0; var triedNoCache1 = false, triedNoCache2 = false;
|
||||
img.src = buildCardUrl(a, PREVIEW_VERSIONS[vi1], false);
|
||||
img2.src = buildCardUrl(b, PREVIEW_VERSIONS[vi2], false);
|
||||
function err1(){ if (vi1 < PREVIEW_VERSIONS.length - 1){ vi1 += 1; img.src = buildCardUrl(a, PREVIEW_VERSIONS[vi1], false);} else if (!triedNoCache1){ triedNoCache1 = true; img.src = buildCardUrl(a, PREVIEW_VERSIONS[vi1], true);} else { img.removeEventListener('error', err1);} }
|
||||
function err2(){ if (vi2 < PREVIEW_VERSIONS.length - 1){ vi2 += 1; img2.src = buildCardUrl(b, PREVIEW_VERSIONS[vi2], false);} else if (!triedNoCache2){ triedNoCache2 = true; img2.src = buildCardUrl(b, PREVIEW_VERSIONS[vi2], true);} else { img2.removeEventListener('error', err2);} }
|
||||
img.addEventListener('error', err1, { once:false });
|
||||
img2.addEventListener('error', err2, { once:false });
|
||||
img.addEventListener('load', function on1(){ img.removeEventListener('load', on1); img.removeEventListener('error', err1); });
|
||||
img2.addEventListener('load', function on2(){ img2.removeEventListener('load', on2); img2.removeEventListener('error', err2); });
|
||||
meta.style.display = 'none'; meta.innerHTML = '';
|
||||
positionCard(e);
|
||||
});
|
||||
el.addEventListener('mousemove', positionCard);
|
||||
el.addEventListener('mouseleave', function(){ cardPop.style.display='none'; });
|
||||
});
|
||||
}
|
||||
attachCardHover();
|
||||
bindAllCardImageRetries();
|
||||
|
|
|
|||
28
code/web/templates/build/_combo_limit_modal.html
Normal file
28
code/web/templates/build/_combo_limit_modal.html
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<div class="modal" id="combo-modal" role="dialog" aria-modal="true" aria-labelledby="combo-modal-title">
|
||||
<div class="modal-content">
|
||||
<h3 id="combo-modal-title" style="margin-top:0;">Combos & Synergies — Auto-complete plan</h3>
|
||||
<p class="muted" style="margin:.25rem 0 .75rem 0;">You're prioritizing combos. Choose how many to aim for and the balance of early vs late-game pieces.</p>
|
||||
<form hx-post="/build/combos/save" hx-target="#combo-modal" hx-swap="outerHTML" style="display:grid; gap:.75rem;">
|
||||
<div>
|
||||
<label for="combo_count"><strong>How many combos would you like?</strong></label>
|
||||
<input id="combo_count" name="count" type="number" min="0" max="10" step="1" value="{{ count|default(2) }}" style="width:6rem; margin-left:.5rem;" />
|
||||
</div>
|
||||
<fieldset style="border:1px solid var(--border); padding:.5rem; border-radius:8px;">
|
||||
<legend><strong>Balance of early-game vs late-game</strong></legend>
|
||||
<label style="display:flex; align-items:center; gap:.35rem; margin:.25rem 0;">
|
||||
<input type="radio" name="balance" value="early" {% if balance == 'early' %}checked{% endif %}/> Early-game focus (cheap, quick setups)
|
||||
</label>
|
||||
<label style="display:flex; align-items:center; gap:.35rem; margin:.25rem 0;">
|
||||
<input type="radio" name="balance" value="late" {% if balance == 'late' %}checked{% endif %}/> Late-game focus (setup-dependent payoffs)
|
||||
</label>
|
||||
<label style="display:flex; align-items:center; gap:.35rem; margin:.25rem 0;">
|
||||
<input type="radio" name="balance" value="mix" {% if balance == 'mix' or not balance %}checked{% endif %}/> Mix of both
|
||||
</label>
|
||||
</fieldset>
|
||||
<div style="display:flex; gap:.5rem; justify-content:flex-end;">
|
||||
<button type="submit" class="btn">Save</button>
|
||||
<button type="button" class="btn" hx-post="/build/combos/save" hx-vals='{"skip":"1"}' hx-target="#combo-modal" hx-swap="outerHTML">Dismiss</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
76
code/web/templates/build/_combos_panel.html
Normal file
76
code/web/templates/build/_combos_panel.html
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<div class="panel" style="margin-top:1rem;">
|
||||
<div style="display:flex; align-items:center; gap:.5rem; flex-wrap:wrap;">
|
||||
<h3 style="margin:0;">Combos & Synergies</h3>
|
||||
{% if versions and (versions.combos or versions.synergies) %}
|
||||
<span class="muted">lists v{{ versions.combos }}{% if versions.synergies %} / {{ versions.synergies }}{% endif %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<section style="margin-top:.5rem;">
|
||||
<div class="muted" style="font-weight:600; margin-bottom:.25rem;">Detected combos ({{ combos|length }})</div>
|
||||
{% if combos and combos|length %}
|
||||
<ul style="list-style:none; padding:0; margin:0; display:grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap:.25rem .75rem;">
|
||||
{% for c in combos %}
|
||||
<li style="border:1px solid var(--border); border-radius:8px; padding:.35rem .5rem; background:#0f1115;" data-combo-names="{{ c.a }}||{{ c.b }}">
|
||||
<span data-card-name="{{ c.a }}">{{ c.a }}</span>
|
||||
<span class="muted"> + </span>
|
||||
<span data-card-name="{{ c.b }}">{{ c.b }}</span>
|
||||
{% if c.cheap_early or c.setup_dependent %}
|
||||
<span class="muted" style="margin-left:.4rem; font-size:12px;">
|
||||
{% if c.cheap_early %}<span title="Cheap/Early" style="border:1px solid var(--border); padding:.05rem .35rem; border-radius:999px;">cheap/early</span>{% endif %}
|
||||
{% if c.setup_dependent %}<span title="Setup Dependent" style="border:1px solid var(--border); padding:.05rem .35rem; border-radius:999px; margin-left:.25rem;">setup</span>{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="muted">None found.</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section style="margin-top:.5rem;">
|
||||
<div class="muted" style="font-weight:600; margin-bottom:.25rem;">Detected synergies ({{ synergies|length }})</div>
|
||||
{% if synergies and synergies|length %}
|
||||
<ul style="list-style:none; padding:0; margin:0; display:grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap:.25rem .75rem;">
|
||||
{% for s in synergies %}
|
||||
<li style="border:1px solid var(--border); border-radius:8px; padding:.35rem .5rem; background:#0f1115;" data-combo-names="{{ s.a }}||{{ s.b }}">
|
||||
<span data-card-name="{{ s.a }}">{{ s.a }}</span>
|
||||
<span class="muted"> + </span>
|
||||
<span data-card-name="{{ s.b }}">{{ s.b }}</span>
|
||||
{% if s.tags %}
|
||||
<span class="muted" style="margin-left:.4rem; font-size:12px;">
|
||||
{% for t in s.tags %}<span style="border:1px solid var(--border); padding:.05rem .35rem; border-radius:999px; margin-right:.25rem;">{{ t }}</span>{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="muted">None found.</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% if suggestions and suggestions|length %}
|
||||
<div style="margin-top:.75rem;">
|
||||
<h4 style="margin:0 0 .25rem 0;">Suggestions</h4>
|
||||
<ul style="list-style:none; padding:0; margin:0; display:grid; gap:.25rem;">
|
||||
{% for s in suggestions %}
|
||||
<li style="border:1px solid var(--border); border-radius:8px; padding:.35rem .5rem; background:#0f1115;">
|
||||
{% if s.kind == 'add' %}Add <strong data-card-name="{{ s.name }}">{{ s.name }}</strong> (partner: <span data-card-name="{{ s.have }}">{{ s.have }}</span>)
|
||||
{% elif s.kind == 'cut' %}Cut <strong data-card-name="{{ s.name }}">{{ s.name }}</strong> (pairs with <span data-card-name="{{ s.partner }}">{{ s.partner }}</span>)
|
||||
{% else %}{{ s.kind|title }} <strong data-card-name="{{ s.name }}">{{ s.name }}</strong>{% endif %}
|
||||
{% set badges = [] %}
|
||||
{% if s.cheap_early %}{% set _ = badges.append('cheap/early') %}{% endif %}
|
||||
{% if s.setup_dependent %}{% set _ = badges.append('setup-dependent') %}{% endif %}
|
||||
{% if badges and badges|length %}
|
||||
<span class="muted">{ {{ badges|join(', ') }} }</span>
|
||||
{% endif %}
|
||||
{% if s.tags and s.tags|length %}
|
||||
<span class="muted">[{{ s.tags|join(', ') }}]</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -49,6 +49,32 @@
|
|||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Preferences</legend>
|
||||
<label title="When enabled, the builder will try to auto-complete missing combo partners near the end of the build (respecting owned-only and locks).">
|
||||
<input type="checkbox" name="prefer_combos" id="pref-combos-chk" /> Prioritize combos (auto-complete partners)
|
||||
</label>
|
||||
<div id="pref-combos-config" style="margin-top:.5rem; padding:.5rem; border:1px solid var(--border); border-radius:8px; display:none;">
|
||||
<div style="display:flex; gap:1rem; align-items:center; flex-wrap:wrap;">
|
||||
<label>
|
||||
<span>How many combos?</span>
|
||||
<input type="number" name="combo_count" min="0" max="10" step="1" value="{{ form.combo_count if form and form.combo_count is not none else 2 }}" style="width:6rem; margin-left:.5rem;" />
|
||||
</label>
|
||||
<div>
|
||||
<div class="muted" style="font-size:12px; margin-bottom:.25rem;">Balance of early vs late-game</div>
|
||||
<label style="display:inline-flex; align-items:center; gap:.25rem; margin-right:.5rem;">
|
||||
<input type="radio" name="combo_balance" value="early" {% if form and form.combo_balance == 'early' %}checked{% endif %}/> Early
|
||||
</label>
|
||||
<label style="display:inline-flex; align-items:center; gap:.25rem; margin-right:.5rem;">
|
||||
<input type="radio" name="combo_balance" value="late" {% if form and form.combo_balance == 'late' %}checked{% endif %}/> Late
|
||||
</label>
|
||||
<label style="display:inline-flex; align-items:center; gap:.25rem;">
|
||||
<input type="radio" name="combo_balance" value="mix" {% if not form or (form and (not form.combo_balance or form.combo_balance == 'mix')) %}checked{% endif %}/> Mix
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<details style="margin-top:.5rem;">
|
||||
<summary>Advanced options (ideals)</summary>
|
||||
<div style="display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:.5rem; margin-top:.5rem;">
|
||||
|
|
@ -146,4 +172,18 @@
|
|||
function onKey(e){ if (e.key === 'Escape'){ e.preventDefault(); closeModal(); } }
|
||||
document.addEventListener('keydown', onKey);
|
||||
})();
|
||||
|
||||
// Toggle combos config visibility based on checkbox
|
||||
(function(){
|
||||
try {
|
||||
var form = document.querySelector('.modal form');
|
||||
var chk = form && form.querySelector('#pref-combos-chk');
|
||||
var box = form && form.querySelector('#pref-combos-config');
|
||||
if (!chk || !box) return;
|
||||
function sync(){ box.style.display = chk.checked ? 'block' : 'none'; }
|
||||
chk.addEventListener('change', sync);
|
||||
// Initial state
|
||||
sync();
|
||||
} catch(_){}
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
</aside>
|
||||
<div class="grow" data-skeleton>
|
||||
<div hx-get="/build/banner" hx-trigger="load"></div>
|
||||
<div hx-get="/build/multicopy/check" hx-trigger="load" hx-swap="afterend"></div>
|
||||
<div hx-get="/build/multicopy/check" hx-trigger="load" hx-swap="afterend"></div>
|
||||
|
||||
<p>Commander: <strong>{{ commander }}</strong></p>
|
||||
<p>Tags: {{ tags|default([])|join(', ') }}</p>
|
||||
|
|
@ -49,6 +49,9 @@
|
|||
{% if added_total is not none %}
|
||||
<span class="chip"><span class="dot" style="background: var(--blue-main);"></span> Added {{ added_total }}</span>
|
||||
{% endif %}
|
||||
{% if prefer_combos %}
|
||||
<span class="chip" title="Combos plan"><span class="dot" style="background: var(--orange-main);"></span> Combos: {{ combo_target_count }} ({{ combo_balance }})</span>
|
||||
{% endif %}
|
||||
{% if clamped_overflow is defined and clamped_overflow and (clamped_overflow > 0) %}
|
||||
<span class="chip" title="Trimmed overflow from this stage"><span class="dot" style="background: var(--red-main);"></span> Clamped {{ clamped_overflow }}</span>
|
||||
{% endif %}
|
||||
|
|
@ -77,6 +80,10 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if status and status.startswith('Build complete') %}
|
||||
<div hx-get="/build/combos" hx-trigger="load" hx-swap="afterend"></div>
|
||||
{% endif %}
|
||||
|
||||
{% if locked_cards is defined and locked_cards %}
|
||||
<details id="locked-section" style="margin-top:.5rem;">
|
||||
<summary>Locked cards (always kept)</summary>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
|
||||
|
||||
{% if summary %}
|
||||
{{ render_cached('partials/deck_summary.html', cfg_name, request=request, summary=summary, game_changers=game_changers, owned_set=owned_set) | safe }}
|
||||
{{ render_cached('partials/deck_summary.html', cfg_name, request=request, summary=summary, game_changers=game_changers, owned_set=owned_set, combos=combos, synergies=synergies, versions=versions) | safe }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<form method="get" action="/decks/compare" class="panel" style="display:flex; gap:.5rem; align-items:center; flex-wrap:wrap;">
|
||||
<label>Deck A
|
||||
<select name="A" required>
|
||||
<option value="">Choose…</option>
|
||||
<option value="" data-mtime="0">Choose…</option>
|
||||
{% for opt in options %}
|
||||
<option value="{{ opt.name }}" data-mtime="{{ opt.mtime }}" {% if A == opt.name %}selected{% endif %}>{{ opt.label }}</option>
|
||||
{% endfor %}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
</label>
|
||||
<label>Deck B
|
||||
<select name="B" required>
|
||||
<option value="">Choose…</option>
|
||||
<option value="" data-mtime="0">Choose…</option>
|
||||
{% for opt in options %}
|
||||
<option value="{{ opt.name }}" data-mtime="{{ opt.mtime }}" {% if B == opt.name %}selected{% endif %}>{{ opt.label }}</option>
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ render_cached('partials/deck_summary.html', name, request=request, summary=summary, game_changers=game_changers, owned_set=owned_set) | safe }}
|
||||
{{ render_cached('partials/deck_summary.html', name, request=request, summary=summary, game_changers=game_changers, owned_set=owned_set, combos=combos, synergies=synergies, versions=versions) | safe }}
|
||||
{% else %}
|
||||
<div class="muted">No summary available.</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@
|
|||
<div><strong>Render count:</strong> <span id="perf-renders">0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
|
||||
<h3 style="margin-top:0">Combos & Synergies (ad-hoc)</h3>
|
||||
<div class="muted" style="margin-bottom:.35rem">Paste card names (one per line) and detect two-card combos and synergies using current lists.</div>
|
||||
<textarea id="diag-combos-input" rows="6" style="width:100%; resize:vertical; font-family: var(--mono);"></textarea>
|
||||
<div style="margin-top:.5rem; display:flex; gap:.5rem; align-items:center">
|
||||
<button class="btn" id="diag-combos-run">Detect</button>
|
||||
<small class="muted">Runs in diagnostics mode only.</small>
|
||||
</div>
|
||||
<pre id="diag-combos-out" style="margin-top:.5rem; white-space:pre-wrap"></pre>
|
||||
</div>
|
||||
{% if enable_pwa %}
|
||||
<div class="card" style="background:#0f1115; border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
|
||||
<h3 style="margin-top:0">PWA status</h3>
|
||||
|
|
@ -86,6 +96,48 @@
|
|||
});
|
||||
}
|
||||
}catch(_){ }
|
||||
// Combos & synergies ad-hoc tester
|
||||
try{
|
||||
var runBtn = document.getElementById('diag-combos-run');
|
||||
var ta = document.getElementById('diag-combos-input');
|
||||
var out = document.getElementById('diag-combos-out');
|
||||
function parseLines(){
|
||||
var v = (ta && ta.value) || '';
|
||||
return v.split(/\r?\n/).map(function(s){ return s.trim(); }).filter(Boolean);
|
||||
}
|
||||
async function run(){
|
||||
if (!ta || !out) return;
|
||||
out.textContent = 'Running…';
|
||||
try{
|
||||
var resp = await fetch('/diagnostics/combos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ names: parseLines() })});
|
||||
if (!resp.ok){ out.textContent = 'Error '+resp.status; return; }
|
||||
var data = await resp.json();
|
||||
var lines = [];
|
||||
// Versions
|
||||
try{
|
||||
if (data.versions){
|
||||
var vLine = 'List versions: ';
|
||||
if (data.versions.combos) vLine += 'combos v'+ String(data.versions.combos);
|
||||
if (data.versions.synergies) vLine += (data.versions.combos? ', ' : '') + 'synergies v'+ String(data.versions.synergies);
|
||||
lines.push(vLine);
|
||||
}
|
||||
}catch(_){ }
|
||||
lines.push('Combos: '+ data.counts.combos);
|
||||
(data.combos||[]).forEach(function(c){
|
||||
var badges = [];
|
||||
if (c.cheap_early) badges.push('cheap/early');
|
||||
if (c.setup_dependent) badges.push('setup-dependent');
|
||||
var tagStr = (c.tags && c.tags.length? ' ['+c.tags.join(', ')+']' : '');
|
||||
var badgeStr = badges.length ? ' {'+badges.join(', ')+'}' : '';
|
||||
lines.push(' - '+c.a+' + '+c.b+ tagStr + badgeStr);
|
||||
});
|
||||
lines.push('Synergies: '+ data.counts.synergies);
|
||||
(data.synergies||[]).forEach(function(s){ lines.push(' - '+s.a+' + '+s.b+(s.tags && s.tags.length? ' ['+s.tags.join(', ')+']':'')); });
|
||||
out.textContent = lines.join('\n');
|
||||
}catch(e){ out.textContent = 'Failed: '+ (e && e.message? e.message : 'Unknown error'); }
|
||||
}
|
||||
if (runBtn){ runBtn.addEventListener('click', run); }
|
||||
}catch(_){ }
|
||||
try{
|
||||
var p = document.getElementById('pwaStatus');
|
||||
if (p){
|
||||
|
|
|
|||
|
|
@ -1,11 +1,60 @@
|
|||
<hr style="margin:1.25rem 0; border-color: var(--border);" />
|
||||
<h4>Deck Summary</h4>
|
||||
{% if versions and (versions.combos or versions.synergies) %}
|
||||
<div class="muted" style="font-size:12px; margin:.1rem 0 .4rem 0;">Combos/Synergies lists: v{{ versions.combos or '?' }} / v{{ versions.synergies or '?' }}</div>
|
||||
{% endif %}
|
||||
<div class="muted" style="font-size:12px; margin:.15rem 0 .4rem 0; display:flex; gap:.75rem; align-items:center; flex-wrap:wrap;">
|
||||
<span>Legend:</span>
|
||||
<span><span class="game-changer" style="font-weight:600;">Game Changer</span> <span class="muted" style="opacity:.8;">(green highlight)</span></span>
|
||||
<span><span class="owned-flag" style="margin:0 .25rem 0 .1rem;">✔</span>Owned • <span class="owned-flag" style="margin:0 .25rem 0 .1rem;">✖</span>Not owned</span>
|
||||
</div>
|
||||
|
||||
<!-- Detected Combos & Synergies (top) -->
|
||||
{% if combos or synergies %}
|
||||
<section style="margin-top:.25rem;">
|
||||
<h5>Combos & Synergies</h5>
|
||||
{% if combos %}
|
||||
<div style="margin:.25rem 0 .5rem 0;">
|
||||
<div class="muted" style="font-weight:600; margin-bottom:.25rem;">Detected Combos ({{ combos|length }})</div>
|
||||
<ul style="list-style:none; padding:0; margin:0; display:grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap:.25rem .75rem;">
|
||||
{% for c in combos %}
|
||||
<li style="border:1px solid var(--border); border-radius:8px; padding:.35rem .5rem; background:#0f1115;" data-combo-names="{{ c.a }}||{{ c.b }}">
|
||||
<span data-card-name="{{ c.a }}">{{ c.a }}</span>
|
||||
<span class="muted"> + </span>
|
||||
<span data-card-name="{{ c.b }}">{{ c.b }}</span>
|
||||
{% if c.cheap_early or c.setup_dependent %}
|
||||
<span class="muted" style="margin-left:.4rem; font-size:12px;">
|
||||
{% if c.cheap_early %}<span title="Cheap/Early" style="border:1px solid var(--border); padding:.05rem .35rem; border-radius:999px;">cheap/early</span>{% endif %}
|
||||
{% if c.setup_dependent %}<span title="Setup Dependent" style="border:1px solid var(--border); padding:.05rem .35rem; border-radius:999px; margin-left:.25rem;">setup</span>{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if synergies %}
|
||||
<div style="margin:.25rem 0 .5rem 0;">
|
||||
<div class="muted" style="font-weight:600; margin-bottom:.25rem;">Detected Synergies ({{ synergies|length }})</div>
|
||||
<ul style="list-style:none; padding:0; margin:0; display:grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap:.25rem .75rem;">
|
||||
{% for s in synergies %}
|
||||
<li style="border:1px solid var(--border); border-radius:8px; padding:.35rem .5rem; background:#0f1115;" data-combo-names="{{ s.a }}||{{ s.b }}">
|
||||
<span data-card-name="{{ s.a }}">{{ s.a }}</span>
|
||||
<span class="muted"> + </span>
|
||||
<span data-card-name="{{ s.b }}">{{ s.b }}</span>
|
||||
{% if s.tags %}
|
||||
<span class="muted" style="margin-left:.4rem; font-size:12px;">
|
||||
{% for t in s.tags %}<span style="border:1px solid var(--border); padding:.05rem .35rem; border-radius:999px; margin-right:.25rem;">{{ t }}</span>{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- Card Type Breakdown with names-only list and hover preview -->
|
||||
<section style="margin-top:.5rem;">
|
||||
<h5>Card Types</h5>
|
||||
|
|
@ -99,6 +148,7 @@
|
|||
<div class="muted">No type data available.</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var listBtn = document.querySelector('.seg-btn[data-view="list"]');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue