diff --git a/index.html b/index.html index 21e9899..89f2a32 100644 --- a/index.html +++ b/index.html @@ -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; - return o; + 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(), }; }