From e74a209bb856d0cea2e4cd1eb84273648163f686 Mon Sep 17 00:00:00 2001 From: MaksSlyzar Date: Sat, 4 Apr 2026 20:04:45 +0300 Subject: [PATCH] Added Dungeon Manager. --- client/src/services/GameDataManager.js | 20 ++- .../components/DungeonScreen.css | 84 +++++++++-- .../components/DungeonScreen.jsx | 77 +++++++--- .../rooms/pirates/pirate_ambush_zone.json | 2 +- .../rooms/pirates/pirate_boss_bridge.json | 2 +- .../rooms/pirates/pirate_patrol_room.json | 2 +- .../rooms/pirates/pirate_supply_bay.json | 2 +- .../enemies/rooms/tutorial/tutorial_boss.json | 2 +- game-server/src/game/DungeonManager.js | 86 +++++++++++ .../src/sockets/handlers/dungeonHandler.js | 142 ++++++++---------- 10 files changed, 294 insertions(+), 125 deletions(-) create mode 100644 game-server/src/game/DungeonManager.js diff --git a/client/src/services/GameDataManager.js b/client/src/services/GameDataManager.js index 141d527..9f3a987 100644 --- a/client/src/services/GameDataManager.js +++ b/client/src/services/GameDataManager.js @@ -1,5 +1,3 @@ -import React from "react"; - class GameDataManager { constructor() { this.items = new Map(); @@ -7,6 +5,7 @@ class GameDataManager { this.skills = new Map(); this.dungeons = new Map(); this.hostiles = new Map(); + this.rooms = new Map(); this.translations = {}; this.manifest = {}; this.currentLang = localStorage.getItem("selected_lang") || "en_US"; @@ -29,6 +28,9 @@ class GameDataManager { if (Array.isArray(data.skills)) { data.skills.forEach((s) => this.skills.set(s.id, s)); } + if (Array.isArray(data.rooms)) { + data.rooms.forEach((r) => this.rooms.set(r.id, r)); + } if (data.languages) { this.translations = data.languages; @@ -122,6 +124,10 @@ class GameDataManager { }; } + getEnemy(id) { + return this.getHostile(id); + } + getItem(id) { const item = this.items.get(id); @@ -176,6 +182,16 @@ class GameDataManager { }; } + getRoom(id) { + const room = this.rooms.get(id); + if (!room) return null; + return { + ...room, + displayName: this.t(room.displayName), + description: this.t(room.description), + }; + } + setLanguage(langCode) { if (this.translations[langCode]) { this.currentLang = langCode; diff --git a/client/src/views/GameInterface/components/DungeonScreen.css b/client/src/views/GameInterface/components/DungeonScreen.css index bcc2348..8dc10cd 100644 --- a/client/src/views/GameInterface/components/DungeonScreen.css +++ b/client/src/views/GameInterface/components/DungeonScreen.css @@ -8,9 +8,9 @@ font-family: "Space Mono", monospace; color: #e0e6ed; box-sizing: border-box; + overflow: hidden; } -/* HEADER */ .dungeon-header { display: flex; justify-content: space-between; @@ -18,6 +18,7 @@ border-bottom: 1px solid rgba(0, 212, 255, 0.3); padding-bottom: 15px; position: relative; + flex-shrink: 0; } .dungeon-header::after { @@ -73,19 +74,18 @@ animation: blink 1.5s infinite; } -/* LAYOUT */ .battle-layout { display: grid; grid-template-columns: 1.2fr 1fr; flex: 1; gap: 30px; - min-height: 0; /* Важливо для overflow лога */ + min-height: 0; } -/* ENEMY CARD */ .enemy-display { position: relative; height: 100%; + min-height: 0; } .enemy-card { @@ -152,7 +152,6 @@ text-transform: uppercase; } -/* HP BAR MINI */ .enemy-hp-container { width: 60%; margin-bottom: 30px; @@ -187,12 +186,14 @@ color: rgba(160, 172, 186, 0.7); } -/* COMBAT LOG */ .combat-log-wrapper { display: flex; flex-direction: column; background: rgba(0, 0, 0, 0.3); border: 1px solid rgba(26, 38, 56, 0.8); + min-height: 0; + max-height: 100%; + overflow: hidden; } .log-header { @@ -203,6 +204,7 @@ letter-spacing: 1px; color: #00d4ff; border-bottom: 1px solid rgba(26, 38, 56, 0.8); + flex-shrink: 0; } .combat-log { @@ -215,12 +217,14 @@ overflow-y: auto; scrollbar-width: thin; scrollbar-color: #1a2638 transparent; + min-height: 0; } .log-entry { line-height: 1.4; color: #a0acba; animation: slideIn 0.2s ease-out; + word-break: break-all; } .log-arrow { @@ -229,11 +233,11 @@ font-weight: bold; } -/* CONTROLS */ .dungeon-controls { display: flex; gap: 20px; height: 70px; + flex-shrink: 0; } .ctrl-btn { @@ -277,7 +281,6 @@ transform: scale(0.98); } -/* UTILS */ .empty-room { height: 100%; display: flex; @@ -325,19 +328,17 @@ border-radius: 2px; } -/* Вирівнювання футера картки */ .enemy-info-footer { display: flex; - justify-content: space-between; /* Розносимо LVL та ID по боках */ - width: 80%; /* Щоб не були впритул до країв */ - margin-top: auto; /* Притискаємо до низу */ + justify-content: space-between; + width: 80%; + margin-top: auto; padding-bottom: 20px; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1px; } -/* Ефект трясіння при атаці */ .enemy-card.taking-damage { animation: shake 0.2s ease-in-out; border-color: #ffffff; @@ -362,7 +363,60 @@ } } -/* Покращення читабельності лога */ .combat-log-wrapper { - min-width: 300px; /* Щоб лог не стискався занадто сильно */ + min-width: 300px; +} + +.environment-panel { + background: rgba(0, 20, 40, 0.6); + border: 1px solid rgba(0, 255, 255, 0.2); + margin-bottom: 15px; + padding: 12px; + display: flex; + flex-direction: column; + position: relative; + flex-shrink: 0; +} + +.env-header { + font-size: 0.65rem; + color: #00ffff; + opacity: 0.7; + letter-spacing: 1.5px; + margin-bottom: 8px; +} + +.env-info { + display: flex; + align-items: center; + gap: 12px; +} + +.env-icon { + width: 36px; + height: 36px; + background: rgba(0, 255, 255, 0.1); + border: 1px solid rgba(0, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; + color: #00ffff; + font-size: 1.1rem; +} + +.env-details .env-id { + font-family: "Orbitron", sans-serif; + font-size: 0.8rem; + color: #fff; +} + +.env-details .env-type { + font-size: 0.6rem; + color: #888; + margin-top: 2px; +} + +.card-id { + opacity: 0.4; + font-size: 0.6rem; } diff --git a/client/src/views/GameInterface/components/DungeonScreen.jsx b/client/src/views/GameInterface/components/DungeonScreen.jsx index c5a3439..cc9600f 100644 --- a/client/src/views/GameInterface/components/DungeonScreen.jsx +++ b/client/src/views/GameInterface/components/DungeonScreen.jsx @@ -4,6 +4,7 @@ import "./DungeonScreen.css"; const DungeonScreen = ({ session, socket }) => { const [roomData, setRoomData] = useState(session.room); + const [hostiles, setHostiles] = useState(session.hostiles || []); const [roomIndex, setRoomIndex] = useState(session.roomIndex); const [enemyHp, setEnemyHp] = useState(null); const [isEnemyDefeated, setIsEnemyDefeated] = useState(false); @@ -14,31 +15,43 @@ const DungeonScreen = ({ session, socket }) => { const logEndRef = useRef(null); - // Отримуємо дані про ворога та данж через GameDataManager - const rawEnemyId = roomData?.hostiles?.[0] || null; - const enemyData = rawEnemyId ? GameDataManager.getEnemy(rawEnemyId) : null; + const currentEnemy = hostiles.length > 0 ? hostiles[0] : null; const dungeonData = GameDataManager.getDungeon(session.dungeonId); - // Авто-скрол лога до останнього запису + const getEnemyDisplayName = (enemy) => { + if (!enemy) return "UNKNOWN_ENTITY"; + const data = GameDataManager.getEnemy(enemy.id); + return data?.displayName || enemy.displayName || enemy.id; + }; + + const getRoomDisplayName = (room) => { + if (!room) return "UNKNOWN_LOCATION"; + const data = GameDataManager.getRoom(room.id); + return data?.displayName || room.displayName || room.id; + }; + useEffect(() => { logEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [log]); useEffect(() => { - // Слухаємо оновлення кімнати (перехід на наступну) socket.on("dungeon:room_update", (data) => { setRoomData(data.room); + setHostiles(data.hostiles || []); setRoomIndex(data.roomIndex); setIsEnemyDefeated(false); setIsLooted(false); - setEnemyHp(null); // Скидаємо HP для нового ворога + setEnemyHp(null); addLog(`--- ENTERING SECTOR ${data.roomIndex + 1} ---`); }); - // Слухаємо результати бою від сервера socket.on("dungeon:combat_result", (data) => { if (data.message) addLog(data.message); - if (data.enemyHp !== undefined) setEnemyHp(data.enemyHp); + if (data.enemyHp !== undefined) { + const maxHp = currentEnemy?.stats?.health || 100; + const hpPercent = (data.enemyHp / maxHp) * 100; + setEnemyHp(hpPercent); + } if (data.targetDefeated) { setIsEnemyDefeated(true); addLog("TARGET_NEUTRALIZED: Threat eliminated."); @@ -49,7 +62,7 @@ const DungeonScreen = ({ session, socket }) => { socket.off("dungeon:room_update"); socket.off("dungeon:combat_result"); }; - }, [socket]); + }, [socket, currentEnemy]); const addLog = (text) => { const time = new Date().toLocaleTimeString([], { @@ -62,15 +75,12 @@ const DungeonScreen = ({ session, socket }) => { }; const handleCombat = () => { - if (isEnemyDefeated) return; - socket.emit("dungeon:combat_step", { enemyId: rawEnemyId }); - addLog( - `Initiating strike sequence on ${enemyData?.displayName || "Target"}...`, - ); + if (isEnemyDefeated || !currentEnemy) return; + socket.emit("dungeon:combat_step", { enemyId: currentEnemy.id }); + addLog(`Initiating strike sequence...`); }; const handleLoot = () => { - socket.emit("dungeon:get_loot"); setIsLooted(true); addLog("Loot encryption bypassed. Resources transferred."); }; @@ -103,11 +113,34 @@ const DungeonScreen = ({ session, socket }) => { +
+
ENVIRONMENT_SCAN
+
+
+ +
+
+
{getRoomDisplayName(roomData)}
+
+ TYPE: {hostiles.length > 0 ? "COMBAT_ZONE" : "SECURE_AREA"} +
+
+
+
+
- {enemyData ? ( + {currentEnemy ? (
{isEnemyDefeated ? "SIGNAL_LOST" : "HOSTILE_DETECTED"} @@ -117,7 +150,9 @@ const DungeonScreen = ({ session, socket }) => { className={`fas ${isEnemyDefeated ? "fa-skull-crossbones" : "fa-robot"}`} >
-

{enemyData.displayName}

+

+ {getEnemyDisplayName(currentEnemy)} +

STRUCTURE INTEGRITY
@@ -132,8 +167,8 @@ const DungeonScreen = ({ session, socket }) => {
- LVL: {enemyData.level || 1} - ID: {GameDataManager._cleanId(rawEnemyId)} + LVL: {currentEnemy.level || 1} + {currentEnemy.id}
) : ( @@ -158,7 +193,7 @@ const DungeonScreen = ({ session, socket }) => {
- {!isEnemyDefeated && enemyData && ( + {!isEnemyDefeated && currentEnemy && ( @@ -170,7 +205,7 @@ const DungeonScreen = ({ session, socket }) => { )} - {(isLooted || !enemyData) && ( + {(isLooted || !currentEnemy) && ( diff --git a/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_ambush_zone.json b/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_ambush_zone.json index b0c5332..e020d8a 100644 --- a/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_ambush_zone.json +++ b/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_ambush_zone.json @@ -1,6 +1,6 @@ { "rooms": { - "id": "original:pirate_ambush_zone", + "id": "original:pirate/pirate_ambush_zone", "hostiles": [ "original:pirates/scout_drone", "original:pirates/raider_frigate", diff --git a/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_boss_bridge.json b/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_boss_bridge.json index 6a1d7ee..2c9e294 100644 --- a/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_boss_bridge.json +++ b/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_boss_bridge.json @@ -1,6 +1,6 @@ { "rooms": { - "id": "original:pirate_boss_bridge", + "id": "original:pirate/pirate_boss_bridge", "hostiles": ["original:pirates/black_mark_cruiser"], "gainXp": 100, "credits": 2500 diff --git a/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_patrol_room.json b/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_patrol_room.json index fe26cb9..cda0c2e 100644 --- a/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_patrol_room.json +++ b/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_patrol_room.json @@ -1,6 +1,6 @@ { "rooms": { - "id": "original:pirate_patrol_room", + "id": "original:pirate/pirate_patrol_room", "hostiles": [ "original:pirates/scout_drone", "original:pirates/scout_drone" diff --git a/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_supply_bay.json b/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_supply_bay.json index 54be8fe..6171c9e 100644 --- a/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_supply_bay.json +++ b/game-server/datapacks/original/data/enemies/rooms/pirates/pirate_supply_bay.json @@ -1,6 +1,6 @@ { "rooms": { - "id": "original:pirate_supply_bay", + "id": "original:pirate/pirate_supply_bay", "hostiles": [], "gainXp": 5, "credits": 800, diff --git a/game-server/datapacks/original/data/enemies/rooms/tutorial/tutorial_boss.json b/game-server/datapacks/original/data/enemies/rooms/tutorial/tutorial_boss.json index edb6201..f60ef65 100644 --- a/game-server/datapacks/original/data/enemies/rooms/tutorial/tutorial_boss.json +++ b/game-server/datapacks/original/data/enemies/rooms/tutorial/tutorial_boss.json @@ -1,6 +1,6 @@ { "rooms": { - "id": "original:tutorial/tutorial_boss", + "id": "original:tutorial/tutorial_boss_room", "hostiles": ["original:tutorial/tutorial_boss_hostile"], "gainXp": 4, "credits": 200 diff --git a/game-server/src/game/DungeonManager.js b/game-server/src/game/DungeonManager.js new file mode 100644 index 0000000..b4d741c --- /dev/null +++ b/game-server/src/game/DungeonManager.js @@ -0,0 +1,86 @@ +const DatapackLoader = require("./DatapackLoader"); + +class DungeonManager { + constructor() { + this.activeSessions = new Map(); + } + + startDungeon(playerId, dungeonId) { + const dungeon = DatapackLoader.getDungeon(dungeonId); + if (!dungeon || !dungeon.rooms?.length) return null; + + const session = { + dungeonId, + currentRoomIndex: 0, + isFinished: false, + currentEnemyHp: undefined, + rewards: { xp: 0, credits: 0, items: [] }, + }; + + this.activeSessions.set(playerId, session); + return this.getCurrentRoomData(playerId); + } + + getCurrentRoomData(playerId) { + const session = this.activeSessions.get(playerId); + if (!session) return null; + + const dungeon = DatapackLoader.getDungeon(session.dungeonId); + const roomRef = dungeon.rooms[session.currentRoomIndex]; + const roomData = DatapackLoader.getRoom(roomRef.id); + if (!roomData) return null; + + const hostiles = (roomData.hostiles || []) + .map((hId) => { + const hostile = DatapackLoader.getEnemy(hId); + return hostile ? { ...hostile } : null; + }) + .filter(Boolean); + + return { + roomIndex: session.currentRoomIndex, + totalRooms: dungeon.rooms.length, + config: roomData, + hostiles, + }; + } + + moveToNextRoom(playerId) { + const session = this.activeSessions.get(playerId); + if (!session || session.isFinished) return null; + + const dungeon = DatapackLoader.getDungeon(session.dungeonId); + const roomRef = dungeon.rooms[session.currentRoomIndex]; + const currentRoom = DatapackLoader.getRoom(roomRef.id); + + if (currentRoom) { + session.rewards.xp += currentRoom.gainXp || 0; + session.rewards.credits += currentRoom.credits || 0; + + if (currentRoom.loot && Array.isArray(currentRoom.loot)) { + currentRoom.loot.forEach((item) => { + session.rewards.items.push({ ...item }); + }); + } + } + + session.currentEnemyHp = undefined; + + if (session.currentRoomIndex < dungeon.rooms.length - 1) { + session.currentRoomIndex++; + return this.getCurrentRoomData(playerId); + } + + session.isFinished = true; + return { + status: "completed", + rewards: session.rewards, + }; + } + + leaveDungeon(playerId) { + this.activeSessions.delete(playerId); + } +} + +module.exports = new DungeonManager(); diff --git a/game-server/src/sockets/handlers/dungeonHandler.js b/game-server/src/sockets/handlers/dungeonHandler.js index b4f3d58..a3e0934 100644 --- a/game-server/src/sockets/handlers/dungeonHandler.js +++ b/game-server/src/sockets/handlers/dungeonHandler.js @@ -1,6 +1,6 @@ const { Player, Inventory } = require("../../models"); -const sessionManager = require("../../game/SessionManager"); const DatapackLoader = require("../../game/DatapackLoader"); +const dungeonManager = require("../../game/DungeonManager"); module.exports = (io, socket) => { const userId = socket.user?.id; @@ -8,80 +8,71 @@ module.exports = (io, socket) => { socket.on("dungeon:start", async ({ dungeonId }) => { try { if (!userId) return; - const dungeon = DatapackLoader.getDungeon(dungeonId); if (!dungeon) { - return socket.emit("error", { message: "Dungeon coordinates invalid" }); + return socket.emit("error", { message: "Dungeon not found" }); } const player = await Player.findByPk(userId); - if (player.energy < dungeon.energyCost) { + const energyCost = dungeon.meta?.energyCost || 0; + + if (player.energy < energyCost) { + return socket.emit("error", { message: "Insufficient energy" }); + } + + await player.decrement("energy", { by: energyCost }); + + const firstRoom = dungeonManager.startDungeon(userId, dungeonId); + if (!firstRoom) { return socket.emit("error", { - message: "Insufficient energy for deployment", + message: "Failed to initialize dungeon", }); } - await player.decrement("energy", { by: dungeon.energyCost }); - - const dungeonSession = { - dungeonId: dungeon.id, - currentRoomIndex: 0, - isCompleted: false, - startTime: Date.now(), - rewards: [], - }; - - sessionManager.setPlayerScene(socket.id, "dungeon", dungeonSession); - - const firstRoomId = dungeon.rooms[0].id; - const firstRoomData = DatapackLoader.getRoom(firstRoomId); - socket.emit("dungeon:started", { dungeonId: dungeon.id, - room: firstRoomData, - roomIndex: 0, - totalRooms: dungeon.rooms.length, - remainingEnergy: player.energy - dungeon.energyCost, + room: firstRoom.config, + hostiles: firstRoom.hostiles, + roomIndex: firstRoom.roomIndex, + totalRooms: firstRoom.totalRooms, + remainingEnergy: player.energy - energyCost, }); - - console.log(`[Dungeon] Player ${userId} started ${dungeonId}`); } catch (err) { console.error("Dungeon Start Error:", err); - socket.emit("error", { message: "Failed to initiate deployment" }); + socket.emit("error", { message: "Critical deployment failure" }); } }); - socket.on("dungeon:combat_step", async (data) => { + socket.on("dungeon:combat_step", async ({ enemyId }) => { try { - const session = sessionManager.getPlayerSession(socket.id); - if (!session || session.scene !== "dungeon" || !session.sceneData) return; + if (!userId) return; + + const session = dungeonManager.activeSessions.get(userId); + if (!session || session.isFinished) return; - const dungeonSession = session.sceneData; - const enemyId = sessionManager._cleanId(data.enemyId); const enemyTemplate = DatapackLoader.getEnemy(enemyId); - if (!enemyTemplate) { return socket.emit("error", { message: "Target data corrupted" }); } - if (dungeonSession.currentEnemyHp === undefined) { - dungeonSession.currentEnemyHp = enemyTemplate.stats?.hp || 100; + if (session.currentEnemyHp === undefined) { + session.currentEnemyHp = enemyTemplate.stats?.health || 100; } - const damage = Math.floor(Math.random() * 11) + 15; - dungeonSession.currentEnemyHp -= damage; + const damage = Math.floor(Math.random() * 10) + 20; + session.currentEnemyHp -= damage; - const isDefeated = dungeonSession.currentEnemyHp <= 0; + const isDefeated = session.currentEnemyHp <= 0; socket.emit("dungeon:combat_result", { damageDealt: damage, - enemyHp: Math.max(0, dungeonSession.currentEnemyHp), + enemyHp: Math.max(0, session.currentEnemyHp), targetDefeated: isDefeated, - message: `Strike successful. Dealt ${damage} damage to units.`, + message: `Strike successful. Dealt ${damage} damage.`, }); if (isDefeated) { - console.log(`[Dungeon] Enemy ${enemyId} defeated by ${session.id}`); + session.currentEnemyHp = undefined; } } catch (err) { console.error("Combat Error:", err); @@ -90,62 +81,49 @@ module.exports = (io, socket) => { socket.on("dungeon:next_room", async () => { try { - const session = sessionManager.getPlayerSession(socket.id); + if (!userId) return; - if (!session || session.scene !== "dungeon" || !session.sceneData) { - console.error( - `[Dungeon Error] Invalid session for socket ${socket.id}`, - ); - return; + const nextRoom = dungeonManager.moveToNextRoom(userId); + if (!nextRoom) { + return socket.emit("error", { + message: "Could not proceed to next room", + }); } - const dungeonId = session.sceneData.dungeonId; // ТУТ була помилка (читання з undefined) - const dungeon = DatapackLoader.getDungeon(dungeonId); - - const nextIndex = session.sceneData.currentRoomIndex + 1; - - if (nextIndex < dungeon.rooms.length) { - session.sceneData.currentRoomIndex = nextIndex; - - const nextRoomId = dungeon.rooms[nextIndex].id; - const nextRoomData = DatapackLoader.getRoom(nextRoomId); - - socket.emit("dungeon:room_update", { - room: nextRoomData, - roomIndex: nextIndex, - }); + if (nextRoom.status === "completed") { + await finalizeDungeon(socket, nextRoom.rewards); } else { - await finalizeDungeon(socket, session.sceneData, dungeon); + socket.emit("dungeon:room_update", { + room: nextRoom.config, + hostiles: nextRoom.hostiles, + roomIndex: nextRoom.roomIndex, + }); } } catch (err) { console.error("Dungeon Progress Error:", err); - socket.emit("error", { message: "Failed to process next room" }); + socket.emit("error", { message: "Navigation system error" }); } }); + + socket.on("dungeon:leave", () => { + if (userId) dungeonManager.leaveDungeon(userId); + }); }; -async function finalizeDungeon(socket, sessionData, dungeon) { +async function finalizeDungeon(socket, sessionRewards) { const userId = socket.user.id; - const earnedLoot = []; + try { + const player = await Player.findByPk(userId); - dungeon.lootTable.forEach((entry) => { - if (Math.random() * 100 <= entry.chance) { - earnedLoot.push({ itemId: entry.itemId, quantity: 1 }); + if (sessionRewards.credits > 0) { + await player.increment("credits", { by: sessionRewards.credits }); } - }); - for (const item of earnedLoot) { - const [invItem, created] = await Inventory.findOrCreate({ - where: { playerId: userId, itemId: item.itemId }, - defaults: { quantity: 0 }, + socket.emit("dungeon:completed", { + rewards: sessionRewards, + message: "Mission successful. All objectives secured.", }); - await invItem.increment("quantity", { by: item.quantity }); + } finally { + dungeonManager.leaveDungeon(userId); } - - sessionManager.setPlayerScene(socket.id, "world", null); - - socket.emit("dungeon:completed", { - rewards: earnedLoot, - message: "Mission successful. All objectives secured.", - }); }