/** * DungeonSystem — wraps ContentLoader for dungeon/enemy defs + manages instances. */ class DungeonSystem { constructor(contentLoader) { this.loader = contentLoader; this.io = null; this.instances = new Map(); // instanceId -> instance this.playerInstances = new Map(); // userId -> instanceId this.roomTypes = { entrance: { name: 'Entrance', enemies: 0, rewards: false }, corridor: { name: 'Corridor', enemies: 1, rewards: false }, chamber: { name: 'Chamber', enemies: 2, rewards: true }, treasure: { name: 'Treasure Room',enemies: 0, rewards: true }, merchant: { name: 'Merchant', enemies: 0, rewards: false, isMerchant: true }, boss: { name: 'Boss Room', enemies: 1, rewards: true, isBoss: true }, exit: { name: 'Exit', enemies: 0, rewards: false } }; } setIO(io) { this.io = io; } // ── Content queries (delegated to ContentLoader) ────────────────── getAllDungeons() { return this.loader.getAllDungeons(); } getDungeon(id) { return this.loader.getDungeon(id); } getDungeonsByDifficulty(d) { return this.loader.getDungeonsByDifficulty(d); } getDungeonsGroupedByDifficulty() { return this.loader.getDungeonsGroupedByDifficulty(); } getAvailableDungeons(lvl) { return this.loader.getAvailableDungeons(lvl); } getEnemyTemplates() { return this.loader.getAllEnemies(); } getEnemy(id) { return this.loader.getEnemy(id); } // ── Instance management ─────────────────────────────────────────── createInstance(dungeonId, userId, _partyIds = []) { const def = this.loader.getDungeon(dungeonId); if (!def) return null; const [minR, maxR] = def.roomCount || [4, 6]; const roomCount = Math.floor(Math.random() * (maxR - minR + 1)) + minR; const instanceId = `${userId}_${dungeonId}_${Date.now()}`; const instance = { instanceId, dungeonId, userId, startTime: Date.now(), currentRoom: 0, totalRooms: roomCount, rooms: this._generateRooms(def, roomCount), completed: false, failed: false }; this.instances.set(instanceId, instance); this.playerInstances.set(userId, instanceId); return instance; } getPlayerInstance(userId) { const id = this.playerInstances.get(userId); return id ? this.instances.get(id) || null : null; } getPlayerCompletedDungeons(_userId) { // Could be persisted to DB; return empty array for now return []; } startEncounter(instanceId, userId) { const instance = this.instances.get(instanceId); if (!instance) return { success: false, error: 'Instance not found' }; const room = instance.rooms[instance.currentRoom]; if (!room) return { success: false, error: 'No more rooms' }; return { success: true, room, currentRoom: instance.currentRoom, totalRooms: instance.totalRooms }; } completeEncounter(instanceId, userId, result = {}) { const instance = this.instances.get(instanceId); if (!instance) return { success: false, error: 'Instance not found' }; const room = instance.rooms[instance.currentRoom]; if (room) room.cleared = true; instance.currentRoom++; const isComplete = instance.currentRoom >= instance.totalRooms; const rewards = isComplete ? this._generateRewards(instance.dungeonId) : []; if (isComplete) instance.completed = true; return { success: true, room, rewards, completed: isComplete }; } moveToNextRoom(instanceId, userId) { const instance = this.instances.get(instanceId); if (!instance) return { success: false, error: 'Instance not found' }; const room = instance.rooms[instance.currentRoom]; return { success: true, room: room || null, currentRoom: instance.currentRoom, totalRooms: instance.totalRooms }; } completeDungeon(instanceId) { const instance = this.instances.get(instanceId); if (!instance) return { success: false, error: 'Instance not found' }; const rewards = this._generateRewards(instance.dungeonId); const def = this.loader.getDungeon(instance.dungeonId); this.instances.delete(instanceId); this.playerInstances.delete(instance.userId); return { success: true, rewards, dungeonName: def?.name, timeMs: Date.now() - instance.startTime }; } // ── Internals ───────────────────────────────────────────────────── _generateRooms(def, count) { const rooms = []; const enemyPool = def.enemyPool || []; for (let i = 0; i < count; i++) { const isFirst = i === 0; const isLast = i === count - 1; const isBoss = i === count - 2 && count > 2; let type = 'corridor', enemies = []; if (isFirst) type = 'entrance'; else if (isLast) type = 'exit'; else if (isBoss) { type = 'boss'; const bossId = enemyPool[enemyPool.length - 1] || enemyPool[0]; const tmpl = this.loader.getEnemy(bossId); if (tmpl) enemies.push({ ...tmpl, health: Math.round(tmpl.health * 2), attack: Math.round(tmpl.attack * 1.5) }); } else { const roll = Math.random(); if (roll < 0.08) type = 'merchant'; else if (roll < 0.22) type = 'treasure'; else if (roll < 0.52) type = 'chamber'; else type = 'corridor'; const ec = this.roomTypes[type]?.enemies || 0; for (let e = 0; e < ec; e++) { const eid = enemyPool[Math.floor(Math.random() * enemyPool.length)]; const tmpl = this.loader.getEnemy(eid); if (tmpl) enemies.push({ ...tmpl }); } } rooms.push({ index: i, type, enemies, cleared: false, rewards: [] }); } return rooms; } _generateRewards(dungeonId, isBossRoom = false) { const def = this.loader.getDungeon(dungeonId); if (!def?.lootTable) return []; const rewards = []; const totalWeight = def.lootTable.reduce((s, e) => s + (e.weight || 0), 0); const picks = Math.floor(Math.random() * 3) + 1; // Validate item IDs exist before adding to rewards const validEntry = (entry) => { const item = this.loader.getItem ? this.loader.getItem(entry.itemId) : true; if (!item) { console.warn(`[DungeonSystem] Invalid lootTable itemId: ${entry.itemId} in dungeon ${dungeonId}`); return false; } return true; }; for (let i = 0; i < picks; i++) { let roll = Math.random() * totalWeight; for (const entry of def.lootTable) { roll -= entry.weight; if (roll <= 0) { if (!validEntry(entry)) break; const qty = Math.floor(Math.random() * (entry.qtyMax - entry.qtyMin + 1)) + entry.qtyMin; rewards.push({ itemId: entry.itemId, quantity: qty }); break; } } } // Boss guaranteed rare+ drop if (isBossRoom || def.bossGuaranteedRare) { const rareLoot = def.lootTable.filter(e => { const item = this.loader.getItem ? this.loader.getItem(e.itemId) : null; return item && ['rare', 'epic', 'legendary'].includes(item.rarity); }); if (rareLoot.length > 0) { const pick = rareLoot[Math.floor(Math.random() * rareLoot.length)]; if (validEntry(pick)) { rewards.push({ itemId: pick.itemId, quantity: pick.qtyMin || 1, guaranteed: true }); } } } return rewards; } /** Generate a merchant shop offer for a mid-dungeon merchant room (3 items, 10% discount) */ getMerchantOffer(dungeonId) { const allItems = this.loader.getAllItems ? this.loader.getAllItems() : []; const shopItems = allItems.filter(item => item.categories?.includes('shop') && item.price > 0); if (shopItems.length === 0) return []; const shuffled = shopItems.sort(() => Math.random() - 0.5).slice(0, 3); return shuffled.map(item => ({ itemId: item.id, name: item.name, originalPrice: item.price, discountedPrice: Math.floor(item.price * 0.9), currency: item.currency || 'credits', })); } } module.exports = DungeonSystem;