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.`, );