316 lines
14 KiB
JavaScript
316 lines
14 KiB
JavaScript
/**
|
|
* Galaxy Strike Online — ContentLoader
|
|
* Single source of truth for all game content.
|
|
* Reads from data/gso/ JSON files at startup; never hardcodes data.
|
|
*
|
|
* Usage:
|
|
* const loader = new ContentLoader();
|
|
* await loader.load();
|
|
* const skills = loader.getAllSkills();
|
|
* const recipes = loader.getAllRecipes();
|
|
* // etc.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const DATA_ROOT = path.join(__dirname, '..', 'data', 'gso');
|
|
|
|
class ContentLoader {
|
|
constructor() {
|
|
// Raw maps — populated by load()
|
|
this.skills = new Map(); // id -> skill
|
|
this.enemies = new Map(); // id -> enemy
|
|
this.dungeons = new Map(); // id -> dungeon
|
|
this.recipes = new Map(); // id -> recipe
|
|
this.items = new Map(); // id -> item
|
|
this.quests = new Map(); // id -> quest
|
|
this.craftingTabs = [];
|
|
this.questCategories = new Map(); // category -> category index
|
|
|
|
this._loaded = false;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Public bootstrap
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
async load() {
|
|
if (this._loaded) return;
|
|
console.log('[CONTENT LOADER] Loading all game content from data/gso/…');
|
|
|
|
this._loadSkills();
|
|
this._loadEnemies();
|
|
this._loadDungeons();
|
|
this._loadItems();
|
|
this._loadRecipes();
|
|
this._loadQuests();
|
|
|
|
this._loaded = true;
|
|
|
|
console.log('[CONTENT LOADER] Done.');
|
|
console.log(` Skills: ${this.skills.size}`);
|
|
console.log(` Enemies: ${this.enemies.size}`);
|
|
console.log(` Dungeons: ${this.dungeons.size}`);
|
|
console.log(` Items: ${this.items.size}`);
|
|
console.log(` Recipes: ${this.recipes.size}`);
|
|
console.log(` Quests: ${this.quests.size}`);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Skills
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
_loadSkills() {
|
|
const root = path.join(DATA_ROOT, 'skills');
|
|
for (const cat of this._subdirs(root)) {
|
|
for (const file of this._jsonFiles(path.join(root, cat))) {
|
|
const skill = this._readJson(path.join(root, cat, file));
|
|
if (skill && skill.id) {
|
|
this.skills.set(skill.id, skill);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
getAllSkills() { return Array.from(this.skills.values()); }
|
|
getSkill(id) { return this.skills.get(id) || null; }
|
|
getSkillsByCategory(cat) { return this.getAllSkills().filter(s => s.category === cat); }
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Enemies
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
_loadEnemies() {
|
|
const root = path.join(DATA_ROOT, 'enemies');
|
|
for (const file of this._jsonFiles(root)) {
|
|
const enemy = this._readJson(path.join(root, file));
|
|
if (enemy && enemy.id) this.enemies.set(enemy.id, enemy);
|
|
}
|
|
// Also load from dungeons/enemies.json (datapack array format)
|
|
const bulkPath = path.join(DATA_ROOT, 'dungeons', 'enemies.json');
|
|
try {
|
|
const bulk = this._readJson(bulkPath);
|
|
if (Array.isArray(bulk)) {
|
|
for (const e of bulk) {
|
|
if (e && e.id && !this.enemies.has(e.id)) this.enemies.set(e.id, e);
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
getAllEnemies() { return Array.from(this.enemies.values()); }
|
|
getEnemy(id) { return this.enemies.get(id) || null; }
|
|
getEnemiesByRarity(r) { return this.getAllEnemies().filter(e => e.rarity === r); }
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Dungeons
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
_loadDungeons() {
|
|
const root = path.join(DATA_ROOT, 'dungeons');
|
|
for (const file of this._jsonFiles(root)) {
|
|
const dungeon = this._readJson(path.join(root, file));
|
|
if (dungeon && dungeon.id) this.dungeons.set(dungeon.id, dungeon);
|
|
}
|
|
}
|
|
|
|
getAllDungeons() { return Array.from(this.dungeons.values()); }
|
|
getDungeon(id) { return this.dungeons.get(id) || null; }
|
|
getDungeonsByDifficulty(d) { return this.getAllDungeons().filter(dn => dn.difficulty === d); }
|
|
getDungeonsGroupedByDifficulty() {
|
|
const order = ['easy','medium','hard','extreme'];
|
|
const out = {};
|
|
for (const dn of this.getAllDungeons()) {
|
|
if (!out[dn.difficulty]) out[dn.difficulty] = [];
|
|
out[dn.difficulty].push(dn);
|
|
}
|
|
return out;
|
|
}
|
|
getAvailableDungeons(playerLevel = 1) {
|
|
return this.getAllDungeons().filter(d => d.minLevel <= playerLevel);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Items — multi-type, multi-subdir
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
_loadItems() {
|
|
const root = path.join(DATA_ROOT, 'items');
|
|
this._walkJsonDir(root, (filePath) => {
|
|
const data = this._readJson(filePath);
|
|
if (!data || !data.templates) return;
|
|
|
|
// Each file has { templates: { <type>: { ...itemData } } }
|
|
for (const [type, itemData] of Object.entries(data.templates)) {
|
|
if (itemData && itemData.id) {
|
|
this.items.set(itemData.id, { ...itemData, type });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
getAllItems() { return Array.from(this.items.values()); }
|
|
getItem(id) { return this.items.get(id) || null; }
|
|
getItemsByType(type) { return this.getAllItems().filter(i => i.type === type); }
|
|
getShopItems() {
|
|
return this.getAllItems().filter(i =>
|
|
Array.isArray(i.categories) && i.categories.includes('shop')
|
|
);
|
|
}
|
|
getShopItemsByCategory() {
|
|
const out = {};
|
|
for (const item of this.getShopItems()) {
|
|
const t = item.type;
|
|
if (!out[t]) out[t] = [];
|
|
out[t].push(item);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Recipes
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
_loadRecipes() {
|
|
const root = path.join(DATA_ROOT, 'recipes');
|
|
|
|
// crafting_tabs.json
|
|
const tabsFile = path.join(root, 'crafting_tabs.json');
|
|
if (fs.existsSync(tabsFile)) {
|
|
this.craftingTabs = this._readJson(tabsFile)?.craftingTypes || [];
|
|
}
|
|
|
|
// One JSON per recipe, grouped into subdirs
|
|
this._walkJsonDir(root, (filePath) => {
|
|
if (filePath.endsWith('crafting_tabs.json')) return;
|
|
if (filePath.endsWith('manifest.json')) return;
|
|
|
|
const data = this._readJson(filePath);
|
|
if (!data || !data.recipe) return;
|
|
|
|
// Derive a stable ID from the craft.id field or the path
|
|
const id = data.craft?.id || data.path?.replace(/\//g, ':') || path.basename(filePath, '.json');
|
|
|
|
// Normalise inputs → array format expected by client
|
|
const inputs = data.recipe.inputs || {};
|
|
const materials = Object.entries(inputs).map(([itemId, qty]) => ({ id: itemId, quantity: qty }));
|
|
|
|
// Output item id
|
|
const outputObj = data.recipe.output || {};
|
|
const outputId = Object.keys(outputObj)[0] || null;
|
|
const outputQty = outputId ? outputObj[outputId] : 0;
|
|
|
|
// Time — any *_time_seconds key
|
|
const timeKey = Object.keys(data.recipe).find(k => k.endsWith('_time_seconds'));
|
|
const craftingTime = timeKey ? (data.recipe[timeKey] * 1000) : 5000;
|
|
|
|
this.recipes.set(id, {
|
|
id,
|
|
path: data.path || '',
|
|
name: outputId ? this._itemName(outputId) : id,
|
|
type: data.craft?.type || 'misc',
|
|
category: data.craft?.type || 'misc',
|
|
subCategory: data.craft?.subCategory || 'misc',
|
|
craftId: data.craft?.id || id,
|
|
materials,
|
|
output: outputId,
|
|
outputQty,
|
|
craftingTime,
|
|
experience: Math.round(craftingTime / 200), // scaled default
|
|
requirements: {},
|
|
unlocked: false,
|
|
rawRecipe: data.recipe
|
|
});
|
|
});
|
|
}
|
|
|
|
getAllRecipes() { return Array.from(this.recipes.values()); }
|
|
getRecipe(id) { return this.recipes.get(id) || null; }
|
|
getRecipesByType(type) { return this.getAllRecipes().filter(r => r.type === type); }
|
|
getRecipesByCategory(cat) { return this.getAllRecipes().filter(r => r.category === cat); }
|
|
getCraftingTabs() { return this.craftingTabs; }
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Quests
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
_loadQuests() {
|
|
const root = path.join(DATA_ROOT, 'quests');
|
|
for (const cat of this._subdirs(root)) {
|
|
for (const file of this._jsonFiles(path.join(root, cat))) {
|
|
const quest = this._readJson(path.join(root, cat, file));
|
|
if (quest && quest.id) this.quests.set(quest.id, quest);
|
|
}
|
|
}
|
|
|
|
// Category index files
|
|
const catRoot = path.join(DATA_ROOT, 'category', 'quests');
|
|
if (fs.existsSync(catRoot)) {
|
|
for (const file of this._jsonFiles(catRoot)) {
|
|
const cat = this._readJson(path.join(catRoot, file));
|
|
if (cat && cat.tabId) this.questCategories.set(cat.tabId, cat);
|
|
}
|
|
}
|
|
}
|
|
|
|
getAllQuests() { return Array.from(this.quests.values()); }
|
|
getQuest(id) { return this.quests.get(id) || null; }
|
|
getQuestsByCategory(cat) { return this.getAllQuests().filter(q => q.category === cat); }
|
|
getQuestCategories() { return Array.from(this.questCategories.values()); }
|
|
getQuestsGroupedByCategory() {
|
|
const out = {};
|
|
for (const q of this.getAllQuests()) {
|
|
if (!out[q.category]) out[q.category] = [];
|
|
out[q.category].push(q);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// Helpers
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
_readJson(filePath) {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
} catch (e) {
|
|
console.warn(`[CONTENT LOADER] Failed to read ${filePath}:`, e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
_subdirs(dir) {
|
|
if (!fs.existsSync(dir)) return [];
|
|
return fs.readdirSync(dir).filter(f => {
|
|
try { return fs.statSync(path.join(dir, f)).isDirectory(); } catch { return false; }
|
|
});
|
|
}
|
|
|
|
_jsonFiles(dir) {
|
|
if (!fs.existsSync(dir)) return [];
|
|
return fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
}
|
|
|
|
/** Recursively walk a directory and call cb(filePath) for every .json file */
|
|
_walkJsonDir(dir, cb) {
|
|
if (!fs.existsSync(dir)) return;
|
|
for (const entry of fs.readdirSync(dir)) {
|
|
const full = path.join(dir, entry);
|
|
const stat = fs.statSync(full);
|
|
if (stat.isDirectory()) {
|
|
this._walkJsonDir(full, cb);
|
|
} else if (entry.endsWith('.json') && !entry.endsWith('manifest.json')) {
|
|
cb(full);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Best-effort human name from an item id (snake_case → Title Case) */
|
|
_itemName(id) {
|
|
return id.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
}
|
|
}
|
|
|
|
module.exports = ContentLoader;
|