215 lines
9.1 KiB
JavaScript
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;
|