Game-Server/Galaxy-Strike-Online-main/GameServer/systems/ContentLoader.js

306 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);
}
}
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;