mildly better turn input

This commit is contained in:
Pagwin 2026-06-19 14:07:32 -04:00
parent f52871318e
commit 2207cf2fbc

View file

@ -518,11 +518,73 @@
if (value !== undefined && value !== null && value !== "") sel.value = String(value);
return sel;
}
// Plain turn-number selects (no "final" entry): a blank option (value "")
// labeled however the caller likes, then turn 0..count-1. Used for the
// cities' Arrival/Departure turns where blank means "unbounded" rather
// than "final".
const plainTurnSelects = new Set();
function fillPlainTurnOptions(sel) {
const prev = sel.value;
sel.innerHTML = "";
sel.append(el("option", {value: ""}, sel._blankLabel || ""));
for (let t = 0; t < turnsCount(); t++)
sel.append(el("option", {value: String(t)}, "turn " + t));
sel.value = [...sel.options].some(o => o.value === prev) ? prev : "";
}
function plainTurnSelect(value, blankLabel) {
const sel = el("select");
sel._blankLabel = blankLabel;
plainTurnSelects.add(sel);
fillPlainTurnOptions(sel);
if (value !== undefined && value !== null && value !== "") sel.value = String(value);
return sel;
}
// Multi-turn picker: a row of checkboxes ("final" + turn 0..count-1).
// _selected() returns the checked values as strings ("" = final). Registers
// for refresh so its options track the Turns count, preserving selections.
const multiTurnGroups = new Set();
function fillMultiTurnOptions(group) {
const prev = new Set((group._boxes || []).filter(b => b.checked).map(b => b.value));
group._boxes = [];
group.innerHTML = "";
const mk = (val, label) => {
const cb = el("input", {type: "checkbox"});
cb.value = val;
if (prev.has(val)) cb.checked = true;
group._boxes.push(cb);
return el("label", {style: "display:inline-block;margin-right:.6rem;font-size:.85rem"},
[cb, " " + label]);
};
group.append(mk("", "final"));
for (let t = 0; t < turnsCount(); t++) group.append(mk(String(t), "turn " + t));
}
function multiTurnSelect(values) {
const group = el("div", {class: "multi-turn"});
group._boxes = [];
group._selected = () => group._boxes.filter(b => b.checked).map(b => b.value);
multiTurnGroups.add(group);
fillMultiTurnOptions(group);
if (values && values.length) {
const want = new Set(values.map(v => (v === "" || v == null) ? "" : String(v)));
for (const b of group._boxes) if (want.has(b.value)) b.checked = true;
}
return group;
}
function refreshTurnSelects() {
for (const sel of turnSelects) {
if (!sel.isConnected) {turnSelects.delete(sel); continue;}
fillTurnOptions(sel);
}
for (const sel of plainTurnSelects) {
if (!sel.isConnected) {plainTurnSelects.delete(sel); continue;}
fillPlainTurnOptions(sel);
}
for (const group of multiTurnGroups) {
if (!group.isConnected) {multiTurnGroups.delete(group); continue;}
fillMultiTurnOptions(group);
}
}
// --- starting resources & tradeable ---
@ -562,7 +624,16 @@
placeholder: "Bearhearth, Kingsland"
});
const forced = el("input", {value: "", placeholder: "0:upgrade"});
const avail = el("input", {value: "", placeholder: ""});
// Available turns are given as an inclusive arrival..departure range and
// expanded into the explicit list the solver expects. Blank arrival means
// "from turn 0", blank departure means "through the last turn"; both blank
// means available on every turn.
const arrival = plainTurnSelect(undefined, "start");
const departure = plainTurnSelect(undefined, "end");
if (c.available_turns && c.available_turns.length) {
arrival.value = String(Math.min(...c.available_turns));
departure.value = String(Math.max(...c.available_turns));
}
// Upgrade checkboxes: the 3 universal upgrades plus the current type's
// type-specific upgrade. State persists across type changes (hidden boxes
@ -616,8 +687,14 @@
if (adj) o.adjacent = adj;
const fa = parsePairs(forced.value);
if (Object.keys(fa).length) o.forced_action = fa;
const at = parseInts(avail.value);
if (at) o.available_turns = at;
const a = arrival.value, d = departure.value;
if (a !== "" || d !== "") {
const lo = a !== "" ? +a : 0;
const hi = d !== "" ? +d : Math.max(0, turnsCount() - 1);
const at = [];
for (let t = lo; t <= hi; t++) at.push(t);
if (at.length) o.available_turns = at;
}
return o;
};
card.append(
@ -630,7 +707,8 @@
field("Upgrades (already installed)", upWrap),
field("Adjacent cities (csv of names)", adjacent),
field("Forced actions (turn:action, csv)", forced),
field("Avail turns (csv, blank=all)", avail),
field("Arrival turn (blank=start)", arrival),
field("Departure turn (blank=end)", departure),
vatGroup,
el("div", {class: "card-actions"}, removeBtn(card)));
document.getElementById("cities").append(card);
@ -798,15 +876,21 @@
const to = selectEl(RESOURCES, c.to || "capital");
const toAmt = num(c.to_amount ?? c.amount ?? 1, {min: 0});
const maxCount = num(c.max_count ?? 1, {min: 1});
const turn = turnSelect(c.turn);
// Multiple turns may be selected; each chosen turn yields its own copy of
// this conversion in the problem JSON (one entry per turn).
const turns = multiTurnSelect(c.turn != null ? [c.turn] : []);
card._get = () => {
const o = {
const base = {
from: from.value, to: to.value,
from_amount: +fromAmt.value, to_amount: +toAmt.value,
max_count: +maxCount.value,
};
if (turn.value !== "") o.turn = +turn.value;
const sel = turns._selected();
return (sel.length ? sel : [""]).map(tv => {
const o = {...base};
if (tv !== "") o.turn = +tv;
return o;
});
};
card.append(
field("From", from),
@ -814,7 +898,7 @@
field("To", to),
field("To amount", toAmt),
field("Max count", maxCount),
field("Turn (blank=final)", turn),
field("Turns (none=final)", turns),
el("div", {class: "card-actions"}, removeBtn(card)));
document.getElementById("optional_conversions").append(card);
}
@ -858,7 +942,7 @@
objective: {terms: collect("terms")},
resource_constraints: collect("constraints"),
conversions: collect("conversions"),
optional_conversions: collect("optional_conversions"),
optional_conversions: collect("optional_conversions").flat(),
};
}