"use strict"; // Items tab: every item of the game, read from its packs by src/main/items.js (names from the text // table, icons from xml/uiimageset.xml through UIRes, prices, box contents, descriptions). It reads // the packs as they are, so it always shows the current rs.iop / map.iop; Reload reads them again. (() => { const IT = { src: null, data: null, cat: "etc", text: null, table: null, loading: false, detailSeq: 0 }; window.IT = IT; // (dev probes) const n = (v) => Number(v || 0).toLocaleString(); const PRICE = (s) => (s.cash ? `${n(s.cash)} cash` : s.peso ? `${n(s.peso)} peso` : ""); // ---------- icons (UIRes: imageset → texture, same drawing as the window editor) ---------- function iconCanvas(name, px) { const c = el("canvas", { class: "it-icon", width: px, height: px }); c.style.width = `${px}px`; c.style.height = `${px}px`; if (name) drawIcon(c, name, px); return c; } async function drawIcon(c, name, px) { const i = name.indexOf("#"); const im = i > 0 ? await UIRes.image(name.slice(0, i), name.slice(i + 1)) : await UIRes.image(null, null, name); if (!im?.bmp) { c.classList.add("missing"); c.title = `${name} (not found)`; return; } const ctx = c.getContext("2d"); const scale = Math.min(px / im.w, px / im.h); const w = Math.round(im.w * scale); const h = Math.round(im.h * scale); ctx.imageSmoothingQuality = "high"; ctx.drawImage(im.bmp, im.x, im.y, im.w, im.h, (px - w) / 2, (px - h) / 2, w, h); c.title = name; } // ---------- list ---------- function table() { if (IT.table) return IT.table; IT.table = new VTable( $("it-table"), [ { key: "icon", label: "", width: 40, format: (r) => iconCanvas(r.icon, 30) }, { key: "id", label: "ID", width: 90, cls: "num mono" }, { key: "name", label: "Name", grow: true, width: 200, format: (r) => r.name || "(no name)", sort: (r) => r.name || "￿", title: true }, { key: "kind", label: "Type", width: 110, title: true }, { key: "price", label: "Price", width: 120, cls: "num", format: (r) => (r.shop?.length ? PRICE(r.shop.find((s) => s.cash || s.peso) || r.shop[0]) : ""), sort: (r) => (r.shop?.[0]?.cash || 0) * 1e6 + (r.shop?.[0]?.peso || 0) }, ], { multi: false, rowHeight: 36, id: "items" }, ); IT.table.keyOf = (r) => r.index; IT.table.onSelect = (_sel, r) => r && showDetail(r); return IT.table; } function renderCats() { const nav = $("it-cats"); nav.replaceChildren( ...IT.data.categories.map((c) => el("button", { class: c.id === IT.cat ? "on" : "", title: c.error || "", onclick: () => pickCat(c.id) }, [ el("span", { text: c.label }), el("b", { text: c.error ? "!" : n(c.items.filter(shown).length) }), ]), ), ); } const shown = (it) => !$("it-hide").checked || (it.name && !/^STR\(/i.test(it.name)); function pickCat(id) { IT.cat = id; renderCats(); const c = IT.data.categories.find((x) => x.id === id); table().setRows(c.items.map((it, index) => ({ ...it, index })), { animate: "fade" }); filter(); $("it-detail").replaceChildren(el("div", { class: "it-empty muted", text: c.error ? `Couldn't read this category: ${c.error}` : "Pick an item." })); } function filter() { const q = $("it-search").value.trim().toLowerCase(); table().setView((r) => shown(r) && (!q || String(r.id).includes(q) || (r.name || "").toLowerCase().includes(q) || (r.icon || "").toLowerCase().includes(q) || (r.section || "").toLowerCase().includes(q))); $("it-count").textContent = `${n(table().view.length)} of ${n(table().rows.length)}`; } // ---------- detail ---------- function row(label, value) { return value === undefined || value === null || value === "" ? null : el("div", { class: "it-kv" }, [el("span", { class: "muted", text: label }), el("span", { text: String(value) })]); } async function showDetail(r) { const seq = ++IT.detailSeq; const box = $("it-detail"); const shop = (r.shop || []).filter((s) => s.cash || s.peso || s.value || s.period); box.replaceChildren( el("div", { class: "it-head" }, [ iconCanvas(r.icon, 72), el("div", {}, [ el("h3", { text: r.name || "(no name)" }), el("div", { class: "muted", text: `${IT.data.categories.find((c) => c.id === IT.cat)?.label} · ID ${r.id}` }), el("code", { class: "muted", text: r.icon || "no icon" }), ]), ]), el("div", { class: "it-kvs" }, [ row("Original", r.originalName), row("Type", r.kind), row("Group", r.group), row("Grade", r.grade || null), row("Classes", r.enableClass), row("Level", r.needLevel || null), row("Sell price", r.sellPeso ? `${n(r.sellPeso)} peso` : r.canSell === false ? "can't be sold" : null), row("Active", r.active === null || r.active === undefined ? null : r.active ? "yes" : "no"), row("Source", `${r.file} [${r.section}]`), ].filter(Boolean)), ...(shop.length ? [ el("h4", { text: "Shop" }), el("table", { class: "it-shop" }, [ el("tr", {}, ["Item", "Count / days", "Peso", "Cash", "Bonus peso", "On sale"].map((h) => el("th", { text: h }))), ...shop.map((s) => el("tr", { class: s.active === false ? "off" : "" }, [ el("td", { text: s.label || "" }), el("td", { text: s.period ? `${n(s.period)} h` : s.value ? n(s.value) : "" }), el("td", { class: "num", text: s.peso ? n(s.peso) : "" }), el("td", { class: "num", text: s.cash ? n(s.cash) : "" }), el("td", { class: "num", text: s.bonusPeso ? n(s.bonusPeso) : "" }), el("td", { text: s.active === null || s.active === undefined ? "" : s.active ? "yes" : "no" }), ]), ), ]), ] : []), ...(r.contents?.length ? [ el("h4", { text: `Contents (${r.contents.length})` }), el( "div", { class: "it-contents" }, r.contents.map((c) => el("div", { class: "it-content" }, [ iconCanvas(c.icon, 26), el("span", { text: c.name && !/^STR\(/i.test(c.name) ? c.name : `#${c.code}` }), el("span", { class: "muted num", text: c.percent !== null && c.percent !== undefined ? `${c.percent.toFixed(c.percent < 1 ? 2 : 1)}%` : "" }), ]), ), ), ] : []), el("div", { class: "it-manuals muted", text: r.manuals?.length ? "Loading the description…" : "" }), ); let d = null; try { d = await call("items:detail", IT.src, IT.text, IT.cat, r.index); } catch (err) { toast(err.message, true); } if (seq !== IT.detailSeq || !d) return; const manuals = box.querySelector(".it-manuals"); manuals.classList.remove("muted"); manuals.replaceChildren( ...d.manuals.map((m) => el("div", { class: "it-manual" }, [ el("h4", { text: `${m.label} description` }, [el("span", { class: "muted", text: ` ${m.file} [Manual${m.n}]` })]), ...(m.manual ? [ ...(m.manual.title.length ? [el("div", { class: "it-mtitle" }, m.manual.title.map((x) => el("span", { text: x.text, style: x.color ? `color:${x.color}` : "" })))] : []), ...m.manual.lines.map((l) => el("div", { class: "it-mline" }, l.runs.map((x) => el("span", { text: x.text, style: x.color ? `color:${x.color}` : "" })))), ] : [el("div", { class: "muted", text: "(missing)" })]), ]), ), el("details", { class: "it-raw" }, [ el("summary", { text: `All fields (${Object.keys(d.fields).length})` }), el("table", {}, Object.entries(d.fields).map(([k, v]) => el("tr", {}, [el("td", { class: "mono", text: k }), el("td", { class: "mono", text: v })]))), ]), ); } // ---------- loading ---------- async function load({ reload = false } = {}) { if (IT.loading) return; if (!IT.src) IT.src = profileSource(); if (!IT.src) { $("it-source").textContent = "This profile has no game folder: set one in the profile, or pick a folder."; return; } IT.loading = true; $("it-source").textContent = `Reading ${IT.src.folder}…`; try { const [data] = await Promise.all([call("items:load", IT.src, { textFile: IT.text, reload }), UIRes.load(IT.src)]); IT.data = data; IT.text = data.text; $("it-text").replaceChildren(...data.texts.map((t) => el("option", { value: t.file, text: t.label, selected: t.file === data.text }))); $("it-source").textContent = `${IT.src.folder} · ${n(data.strings)} texts from ${data.text || "no text table"}${data.missing.length ? ` · missing: ${data.missing.join(", ")}` : ""}`; if (!data.categories.some((c) => c.id === IT.cat)) IT.cat = data.categories[0]?.id; pickCat(IT.cat); } catch (err) { $("it-source").textContent = err.message; toast(err.message, true); } finally { IT.loading = false; } } $("it-search").addEventListener("input", filter); $("it-hide").addEventListener("change", () => (renderCats(), filter())); $("it-text").addEventListener("change", () => { IT.text = $("it-text").value; load(); }); $("it-reload").addEventListener("click", () => { UIRes.reset(); load({ reload: true }); }); $("it-folder").addEventListener("click", async () => { const dir = await call("dialog:folder", "Game folder (with rs.iop / map.iop)"); if (!dir) return; IT.src = { kind: "packs", folder: dir, name: dir }; IT.data = null; UIRes.reset(); load(); }); // first time the tab opens: the profile's game folder document.querySelector('#tabs [data-view="items"]').addEventListener("click", () => { if (!IT.data) load(); }); window.addEventListener("profile-changed", () => { IT.src = null; IT.data = null; if ($("view-items").classList.contains("on")) load(); }); })();