mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-24 03:20:12 +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
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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue