156 lines
5.2 KiB
JavaScript
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.`,
|
|
);
|