Files
healer-man/scripts/generate-loot-catalog.mjs
T
2026-08-14 15:56:39 -04:00

156 lines
5.2 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { DatabaseSync } from "node:sqlite";
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(scriptDirectory, "..");
const schemaPath = path.join(projectRoot, "data", "loot", "schema.sql");
const seedPaths = [
path.join(projectRoot, "data", "loot", "wailing-caverns.sql"),
];
const runtimeCatalogPath = path.join(projectRoot, "src", "game", "generated", "lootCatalog.json");
const publicDirectory = path.join(projectRoot, "public", "data");
const publicCatalogPath = path.join(publicDirectory, "loot-catalog.json");
const publicDatabasePath = path.join(publicDirectory, "healer-man-loot.sqlite");
function loadDatabase(database) {
database.exec(fs.readFileSync(schemaPath, "utf8"));
for (const seedPath of seedPaths) database.exec(fs.readFileSync(seedPath, "utf8"));
}
function validateDatabase(database) {
const foreignKeyFailures = database.prepare("PRAGMA foreign_key_check").all();
if (foreignKeyFailures.length) {
throw new Error(`Loot SQL has ${foreignKeyFailures.length} foreign-key violation(s).`);
}
const emptyTables = database.prepare(`
SELECT b.boss_key
FROM boss_template b
LEFT JOIN loot_table_entry e ON e.loot_table_id = b.loot_table_id
GROUP BY b.boss_key
HAVING COUNT(e.item_id) = 0
`).all();
if (emptyTables.length) {
throw new Error(`Bosses without loot entries: ${emptyTables.map((row) => row.boss_key).join(", ")}`);
}
const counts = database.prepare(`
SELECT
(SELECT COUNT(*) FROM boss_template) AS bosses,
(SELECT COUNT(*) FROM item_template) AS items,
(SELECT COUNT(*) FROM loot_table_entry) AS entries
`).get();
if (!counts || counts.bosses < 1 || counts.items < 1 || counts.entries < 1) {
throw new Error("Loot SQL must contain bosses, items, and loot-table entries.");
}
return counts;
}
function buildCatalog(database) {
const bossRows = database.prepare(`
SELECT
b.boss_key,
b.creature_entry,
b.display_name AS boss_name,
b.dungeon_id,
b.loot_table_id,
lt.rolls_min,
lt.rolls_max
FROM boss_template b
JOIN loot_table lt ON lt.loot_table_id = b.loot_table_id
ORDER BY b.dungeon_id, b.boss_key
`).all();
const itemRows = database.prepare(`
SELECT *
FROM boss_loot_browser
ORDER BY dungeon_id, boss_key, item_id
`).all();
const statRows = database.prepare(`
SELECT item_id, stat_key, stat_value
FROM item_stat
ORDER BY item_id, stat_key
`).all();
const weaponRows = database.prepare(`
SELECT item_id, damage_min, damage_max, damage_school, speed_ms
FROM item_weapon
ORDER BY item_id
`).all();
const statsByItem = new Map();
for (const row of statRows) {
const stats = statsByItem.get(row.item_id) ?? {};
stats[row.stat_key] = row.stat_value;
statsByItem.set(row.item_id, stats);
}
const weaponsByItem = new Map(weaponRows.map((row) => [row.item_id, {
damageMin: row.damage_min,
damageMax: row.damage_max,
damageSchool: row.damage_school,
speedMs: row.speed_ms,
}]));
const itemsByBoss = new Map();
for (const row of itemRows) {
const items = itemsByBoss.get(row.boss_key) ?? [];
items.push(row);
itemsByBoss.set(row.boss_key, items);
}
return {
schemaVersion: 2,
generatedFrom: [
"data/loot/schema.sql",
...seedPaths.map((seedPath) => path.relative(projectRoot, seedPath).replaceAll("\\", "/")),
],
bosses: bossRows.map((boss) => ({
bossKey: boss.boss_key,
creatureEntry: boss.creature_entry,
bossName: boss.boss_name,
dungeonId: boss.dungeon_id,
lootTableId: boss.loot_table_id,
rollsMin: boss.rolls_min,
rollsMax: boss.rolls_max,
items: (itemsByBoss.get(boss.boss_key) ?? []).map((item) => ({
itemId: item.item_id,
itemKey: item.item_key,
itemName: item.item_name,
quality: item.quality,
inventorySlot: item.inventory_slot,
armorType: item.armor_type,
weaponType: item.weapon_type,
sourceItemLevel: item.source_item_level,
scalable: Boolean(item.scalable),
weight: item.weight,
minQuantity: item.min_quantity,
maxQuantity: item.max_quantity,
sourceUrl: item.source_url,
sourceStats: statsByItem.get(item.item_id) ?? {},
sourceWeapon: weaponsByItem.get(item.item_id) ?? null,
})),
})),
};
}
fs.mkdirSync(path.dirname(runtimeCatalogPath), { recursive: true });
fs.mkdirSync(publicDirectory, { recursive: true });
const memoryDatabase = new DatabaseSync(":memory:");
loadDatabase(memoryDatabase);
const counts = validateDatabase(memoryDatabase);
const catalog = buildCatalog(memoryDatabase);
memoryDatabase.close();
const serializedCatalog = `${JSON.stringify(catalog, null, 2)}\n`;
fs.writeFileSync(runtimeCatalogPath, serializedCatalog);
fs.writeFileSync(publicCatalogPath, serializedCatalog);
fs.rmSync(publicDatabasePath, { force: true });
const publicDatabase = new DatabaseSync(publicDatabasePath);
loadDatabase(publicDatabase);
validateDatabase(publicDatabase);
publicDatabase.close();
console.log(
`Generated loot catalog: ${counts.bosses} bosses, ${counts.items} items, ${counts.entries} table entries.`,
);