This repository has been archived on 2026-05-04. You can view files and clone it, but cannot push or open issues or pull requests.
Galaxy-Strike-Online/GameServer/systems/DungeonSystem.js
2026-03-11 00:32:45 -03:00

215 lines
9.1 KiB
JavaScript

/**
* 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;