Files

233 lines
10 KiB
JavaScript

"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();
});
})();