From 16f038448366e0e780f37f3a7113cc99e68ffbbb Mon Sep 17 00:00:00 2001 From: rope Date: Tue, 20 Oct 2026 19:32:27 +0100 Subject: [PATCH] Graphical work order item picker Co-Authored-By: Claude Opus 5.5 --- static/app.js | 68 ++++++++++++++++++++++++++++++++++++----------- static/styles.css | 13 +++++++++ 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/static/app.js b/static/app.js index b96874b..c8dbf0f 100644 --- a/static/app.js +++ b/static/app.js @@ -1243,15 +1243,33 @@ function orderCard(o) { `; } +function orderPicker(list) { + const d = draft, actions = Object.entries(list).map(([id, action]) => [id, C.actions.find((x) => x.id === action)]); + const skills = [...new Set(actions.map(([, a]) => a.skill))]; + const picked = d.item && actions.find(([id]) => id === d.item)?.[1]; + d.skill = skills.includes(d.skill) ? d.skill : picked?.skill || skills[0]; + const inSkill = actions.filter(([, a]) => a.skill === d.skill); + const groups = [...new Set(inSkill.map(([, a]) => a.group).filter(Boolean))]; + if (!groups.includes(d.group)) d.group = picked?.skill === d.skill && picked.group || groups[0]; + const q = (d.search || "").trim().toLowerCase(); + const shown = q ? actions.filter(([id]) => item(id).name.toLowerCase().includes(q)) + : inSkill.filter(([, a]) => !groups.length || a.group === d.group); + const others = S.players.filter((p) => p.id !== S.uid); + const tiles = shown.sort((x, y) => x[1].level - y[1].level).map(([id, a]) => { + const makers = others.filter((p) => p.levels[a.skill] >= a.level).length; + const tip = `${item(id).name} · ${skillOf(a.skill).name} ${a.level}${d.kind === "craft" ? ` · 👥 ${makers}` : ""}`; + return ``; + }).join(""); + return `
+
${skills.map((k) => ``).join("")} +
+ ${groups.length && !q ? `
${groups.map((g) => ``).join("")}
` : ""} +
${tiles || `—`}
`; +} + function orderComposer() { const d = draft, list = d.kind === "craft" ? C.order_recipes : C.order_raw; - const bySkill = {}; - for (const [id, action] of Object.entries(list)) { - const a = C.actions.find((x) => x.id === action); - (bySkill[a.skill] ||= []).push([id, a.level]); - } - const options = Object.entries(bySkill).map(([skill, items]) => `${items - .sort((x, y) => x[1] - y[1]).map(([id, level]) => ``).join("")}`).join(""); let slots = "", maker = ""; const a = d.item && C.actions.find((x) => x.id === list[d.item]); if (a) { @@ -1274,9 +1292,10 @@ function orderComposer() { return `
+ ${orderPicker(list)}
-
- ${a ? `
${item(d.item).icon}
${esc(item(d.item).name)}${maker}
` : ""} +
${a ? `
${item(d.item).icon}
${esc(item(d.item).name)}${maker}
` + : `
—
`}
× max ${fmt(C.order_max[d.kind])}
${slots ? `
${slots}
` : ""}
@@ -1604,7 +1623,10 @@ document.addEventListener("click", (e) => { draft = draft ? null : { kind: "craft", item: "", qty: 1, provide: [], reward: 0 }; return render(); } - if (d.draftKind) { draft = { kind: d.draftKind, item: "", qty: 1, provide: [], reward: 0 }; return render(); } + if (d.draftKind) { draft = { kind: d.draftKind, item: "", qty: 1, provide: [], reward: 0 }; clearSearch(); return render(); } + if (d.draftSkill) { draft.skill = d.draftSkill; draft.search = ""; clearSearch(); return render(); } + if (d.draftGroup) { draft.group = d.draftGroup; return render(); } + if (d.draftItem) { pickOrderItem(d.draftItem); return render(); } if (d.provide) { draft.provide = draft.provide.includes(d.provide) ? draft.provide.filter((x) => x !== d.provide) : [...draft.provide, d.provide]; return render(); @@ -1664,16 +1686,30 @@ document.addEventListener("click", (e) => { if ("devEvent" in d) return act("/api/dev/event", {}); }); +function clearSearch() { + const field = $("[data-keep=order-search]"); + if (field) field.value = ""; +} + +function pickOrderItem(id) { + const list = draft.kind === "craft" ? C.order_recipes : C.order_raw; + draft.item = list[id] ? id : ""; + draft.provide = []; + // A starting price: what the item's worth, each. + draft.reward = draft.item ? item(draft.item).value * (draft.kind === "craft" ? draft.qty : 1) : 0; +} + +document.addEventListener("input", (e) => { + if (e.target.dataset.keep !== "order-search" || !draft) return; + draft.search = e.target.value; + render(); +}); + document.addEventListener("change", (e) => { const key = e.target.dataset.draft; if (!key || !draft) return; if (key === "item") { - const list = draft.kind === "craft" ? C.order_recipes : C.order_raw; - draft.item = e.target.value; - draft.provide = []; - // A starting price: what the item's worth, each. - draft.reward = draft.item ? item(draft.item).value * (draft.kind === "craft" ? draft.qty : 1) : 0; - if (!list[draft.item]) draft.item = ""; + pickOrderItem(e.target.value); } else { const before = draft.qty; draft[key] = Math.max(key === "qty" ? 1 : 0, Math.floor(Number(e.target.value) || 0)); diff --git a/static/styles.css b/static/styles.css index ee5d4a3..2a31494 100644 --- a/static/styles.css +++ b/static/styles.css @@ -603,3 +603,16 @@ button.sheet-skill:hover { background: var(--panel-2); } .reward-input input { width: 110px; } @media (max-width: 900px) { .compose-grid { grid-template-columns: 1fr; gap: 14px; } } .compose .why { color: var(--warn); font-weight: 600; font-size: 13px; } +.picker { display: grid; gap: 10px; margin-bottom: 18px; padding-bottom: 16px; border-bottom: 1px solid var(--line); } +.pick-skills { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } +.pick-skill { width: 40px; height: 40px; border-radius: 10px; border: 1px solid var(--line); background: var(--panel); display: grid; place-items: center; } +.pick-skill .gi { width: 24px; height: 24px; } +.pick-skill.on { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold); } +.pick-skills input { margin-left: auto; width: 160px; } +.tabs.sm { margin: 0; gap: 4px; } +.tabs.sm .tab { padding: 3px 10px; font-size: 13px; } +.pick-grid { gap: 9px; } +.doll-slot.pick.nobody > .gi { opacity: .35; } +.pick-lvl { position: absolute; bottom: -5px; right: -5px; font-size: 10px; font-weight: 700; line-height: 1; padding: 2px 4px; border-radius: 6px; background: var(--panel); border: 1px solid var(--line); color: var(--muted); } +.doll-slot.pick.on .pick-lvl { border-color: var(--gold); } +.doll-slot.pick.on .pick-lvl { color: var(--gold); }