233 lines
10 KiB
JavaScript
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();
|
|
});
|
|
})();
|