mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 23:50:12 +01:00
feat: add ideal counts slider UI with smart validation
This commit is contained in:
parent
9ab3835e2a
commit
35bff901d2
12 changed files with 217 additions and 11 deletions
|
|
@ -128,6 +128,7 @@ ENABLE_PRESETS = _as_bool(os.getenv("ENABLE_PRESETS"), False)
|
|||
ALLOW_MUST_HAVES = _as_bool(os.getenv("ALLOW_MUST_HAVES"), True)
|
||||
SHOW_MUST_HAVE_BUTTONS = _as_bool(os.getenv("SHOW_MUST_HAVE_BUTTONS"), False)
|
||||
ENABLE_CUSTOM_THEMES = _as_bool(os.getenv("ENABLE_CUSTOM_THEMES"), True)
|
||||
WEB_IDEALS_UI = os.getenv("WEB_IDEALS_UI", "slider").strip().lower() # 'input' or 'slider'
|
||||
ENABLE_PARTNER_MECHANICS = _as_bool(os.getenv("ENABLE_PARTNER_MECHANICS"), True)
|
||||
ENABLE_PARTNER_SUGGESTIONS = _as_bool(os.getenv("ENABLE_PARTNER_SUGGESTIONS"), True)
|
||||
RANDOM_MODES = _as_bool(os.getenv("RANDOM_MODES"), True) # initial snapshot (legacy)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from ..app import (
|
|||
_sanitize_theme,
|
||||
ENABLE_PARTNER_MECHANICS,
|
||||
ENABLE_PARTNER_SUGGESTIONS,
|
||||
WEB_IDEALS_UI,
|
||||
)
|
||||
from ..services.build_utils import (
|
||||
step5_base_ctx,
|
||||
|
|
@ -1356,6 +1357,7 @@ async def build_new_modal(request: Request) -> HTMLResponse:
|
|||
"allow_must_haves": ALLOW_MUST_HAVES, # Add feature flag
|
||||
"show_must_have_buttons": SHOW_MUST_HAVE_BUTTONS,
|
||||
"enable_custom_themes": ENABLE_CUSTOM_THEMES,
|
||||
"ideals_ui_mode": WEB_IDEALS_UI, # 'input' or 'slider'
|
||||
"form": {
|
||||
"prefer_combos": bool(sess.get("prefer_combos")),
|
||||
"combo_count": sess.get("combo_target_count"),
|
||||
|
|
@ -1364,6 +1366,15 @@ async def build_new_modal(request: Request) -> HTMLResponse:
|
|||
"use_owned_only": bool(sess.get("use_owned_only")),
|
||||
"prefer_owned": bool(sess.get("prefer_owned")),
|
||||
"swap_mdfc_basics": bool(sess.get("swap_mdfc_basics")),
|
||||
# Add ideal values from session (will be None on first load, triggering defaults)
|
||||
"ramp": sess.get("ideals", {}).get("ramp"),
|
||||
"lands": sess.get("ideals", {}).get("lands"),
|
||||
"basic_lands": sess.get("ideals", {}).get("basic_lands"),
|
||||
"creatures": sess.get("ideals", {}).get("creatures"),
|
||||
"removal": sess.get("ideals", {}).get("removal"),
|
||||
"wipes": sess.get("ideals", {}).get("wipes"),
|
||||
"card_advantage": sess.get("ideals", {}).get("card_advantage"),
|
||||
"protection": sess.get("ideals", {}).get("protection"),
|
||||
},
|
||||
"tag_slot_html": None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -678,3 +678,52 @@ img.lqip.loaded { filter: blur(0); opacity: 1; }
|
|||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
/* Ideals Slider Styling */
|
||||
.ideals-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 6px;
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ideals-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: var(--ring);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.ideals-slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.2);
|
||||
}
|
||||
|
||||
.ideals-slider::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: var(--ring);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.ideals-slider::-moz-range-thumb:hover {
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.2);
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
|
|
|||
124
code/web/templates/build/_new_deck_ideals.html
Normal file
124
code/web/templates/build/_new_deck_ideals.html
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<fieldset>
|
||||
<legend>Ideal Counts</legend>
|
||||
<div class="muted" style="font-size:12px; margin-bottom:0.75rem;">
|
||||
Sliders start at recommended defaults. Adjust as needed for your deck strategy.
|
||||
<span id="ideals-validation-warning" style="color:#f59e0b; display:none; margin-left:0.5rem;"></span>
|
||||
</div>
|
||||
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:1rem;">
|
||||
{% set use_sliders = ideals_ui_mode == 'slider' %}
|
||||
{% set ideals_data = [
|
||||
('ramp', labels.ramp, defaults.ramp, 0, 30),
|
||||
('lands', labels.lands, defaults.lands, 25, 45),
|
||||
('basic_lands', labels.basic_lands, defaults.basic_lands, 0, 40),
|
||||
('creatures', labels.creatures, defaults.creatures, 0, 70),
|
||||
('removal', labels.removal, defaults.removal, 0, 30),
|
||||
('wipes', labels.wipes, defaults.wipes, 0, 15),
|
||||
('card_advantage', labels.card_advantage, defaults.card_advantage, 0, 30),
|
||||
('protection', labels.protection, defaults.protection, 0, 20)
|
||||
] %}
|
||||
|
||||
{% for field_name, field_label, default_val, min_val, max_val in ideals_data %}
|
||||
<div>
|
||||
<label style="display:block; margin-bottom:0.25rem;">
|
||||
<span>{{ field_label }}</span>
|
||||
{% if use_sliders %}
|
||||
<div style="display:flex; align-items:center; gap:0.5rem; margin-top:0.25rem;">
|
||||
<input
|
||||
type="range"
|
||||
name="{{ field_name }}"
|
||||
min="{{ min_val }}"
|
||||
max="{{ max_val }}"
|
||||
value="{{ form[field_name] if form and form[field_name] is not none else default_val }}"
|
||||
class="ideals-slider"
|
||||
data-field="{{ field_name }}"
|
||||
style="flex:1; cursor:pointer;"
|
||||
/>
|
||||
<output
|
||||
id="{{ field_name }}_value"
|
||||
class="slider-value"
|
||||
style="min-width:2.5rem; text-align:center; font-weight:600; color:var(--accent); font-size:14px;"
|
||||
>
|
||||
{{ form[field_name] if form and form[field_name] is not none else default_val }}
|
||||
</output>
|
||||
</div>
|
||||
{% else %}
|
||||
<input
|
||||
type="number"
|
||||
name="{{ field_name }}"
|
||||
min="{{ min_val }}"
|
||||
max="100"
|
||||
value="{{ form[field_name] if form and form[field_name] is not none else '' }}"
|
||||
placeholder="{{ default_val }} (Default)"
|
||||
style="width:100%; margin-top:0.25rem;"
|
||||
/>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Validate ideal counts to prevent over-allocation
|
||||
function validateIdealCounts() {
|
||||
const warning = document.getElementById('ideals-validation-warning');
|
||||
if (!warning) return;
|
||||
|
||||
// Get all ideal values
|
||||
const getValue = (name) => {
|
||||
const input = document.querySelector('[name="' + name + '"]');
|
||||
return input ? parseInt(input.value) || 0 : 0;
|
||||
};
|
||||
|
||||
const lands = getValue('lands');
|
||||
const creatures = getValue('creatures');
|
||||
const ramp = getValue('ramp');
|
||||
const removal = getValue('removal');
|
||||
const wipes = getValue('wipes');
|
||||
const cardAdvantage = getValue('card_advantage');
|
||||
const protection = getValue('protection');
|
||||
|
||||
// Calculate estimated total with overlap factor
|
||||
// lands + creatures + (spells / 2) since spells often overlap with creatures
|
||||
const spells = ramp + removal + wipes + cardAdvantage + protection;
|
||||
const estimatedTotal = lands + creatures + Math.ceil(spells / 2);
|
||||
|
||||
// Check if estimated total exceeds deck size (99 for EDH)
|
||||
if (estimatedTotal > 99) {
|
||||
warning.textContent = '⚠ Estimated total ~' + estimatedTotal + ' cards (max 99). Reduce counts to avoid build issues.';
|
||||
warning.style.display = 'inline';
|
||||
warning.style.color = '#ef4444'; // Red for hard conflict
|
||||
} else if (estimatedTotal > 90) {
|
||||
warning.textContent = '⚠ Estimated total ~' + estimatedTotal + ' cards. Deck may be tight on slots.';
|
||||
warning.style.display = 'inline';
|
||||
warning.style.color = '#f59e0b'; // Orange for warning
|
||||
} else {
|
||||
warning.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Attach validation to all ideal inputs
|
||||
{% if use_sliders %}
|
||||
document.querySelectorAll('.ideals-slider').forEach(slider => {
|
||||
const fieldName = slider.dataset.field;
|
||||
const output = document.getElementById(fieldName + '_value');
|
||||
|
||||
// Update display on input
|
||||
slider.addEventListener('input', function() {
|
||||
output.textContent = this.value;
|
||||
validateIdealCounts();
|
||||
});
|
||||
|
||||
// Ensure initial value is displayed
|
||||
output.textContent = slider.value;
|
||||
});
|
||||
{% else %}
|
||||
document.querySelectorAll('[name="ramp"], [name="lands"], [name="basic_lands"], [name="creatures"], [name="removal"], [name="wipes"], [name="card_advantage"], [name="protection"]').forEach(input => {
|
||||
input.addEventListener('input', validateIdealCounts);
|
||||
input.addEventListener('change', validateIdealCounts);
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
// Run initial validation
|
||||
validateIdealCounts();
|
||||
</script>
|
||||
</fieldset>
|
||||
|
|
@ -110,6 +110,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% include "build/_new_deck_ideals.html" %}
|
||||
{% if allow_must_haves %}
|
||||
<fieldset>
|
||||
<legend>Include/Exclude Cards</legend>
|
||||
|
|
@ -213,16 +214,6 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
{% include "build/_new_deck_skip_controls.html" %}
|
||||
<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;">
|
||||
{% for key, label in labels.items() %}
|
||||
<label>{{ label }}
|
||||
<input type="number" name="{{ key }}" value="{{ defaults[key] }}" min="0" />
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
<div class="modal-footer" style="display:flex; gap:.5rem; justify-content:space-between; margin-top:1rem;">
|
||||
<button type="button" class="btn" onclick="this.closest('.modal').remove()">Cancel</button>
|
||||
<div style="display:flex; gap:.5rem;">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue