Added Dungeon Manager.

This commit is contained in:
MaksSlyzar 2026-04-04 20:04:45 +03:00
parent 25090a5316
commit e74a209bb8
10 changed files with 294 additions and 125 deletions

View File

@ -1,5 +1,3 @@
import React from "react";
class GameDataManager { class GameDataManager {
constructor() { constructor() {
this.items = new Map(); this.items = new Map();
@ -7,6 +5,7 @@ class GameDataManager {
this.skills = new Map(); this.skills = new Map();
this.dungeons = new Map(); this.dungeons = new Map();
this.hostiles = new Map(); this.hostiles = new Map();
this.rooms = new Map();
this.translations = {}; this.translations = {};
this.manifest = {}; this.manifest = {};
this.currentLang = localStorage.getItem("selected_lang") || "en_US"; this.currentLang = localStorage.getItem("selected_lang") || "en_US";
@ -29,6 +28,9 @@ class GameDataManager {
if (Array.isArray(data.skills)) { if (Array.isArray(data.skills)) {
data.skills.forEach((s) => this.skills.set(s.id, s)); 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) { if (data.languages) {
this.translations = data.languages; this.translations = data.languages;
@ -122,6 +124,10 @@ class GameDataManager {
}; };
} }
getEnemy(id) {
return this.getHostile(id);
}
getItem(id) { getItem(id) {
const item = this.items.get(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) { setLanguage(langCode) {
if (this.translations[langCode]) { if (this.translations[langCode]) {
this.currentLang = langCode; this.currentLang = langCode;

View File

@ -8,9 +8,9 @@
font-family: "Space Mono", monospace; font-family: "Space Mono", monospace;
color: #e0e6ed; color: #e0e6ed;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden;
} }
/* HEADER */
.dungeon-header { .dungeon-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -18,6 +18,7 @@
border-bottom: 1px solid rgba(0, 212, 255, 0.3); border-bottom: 1px solid rgba(0, 212, 255, 0.3);
padding-bottom: 15px; padding-bottom: 15px;
position: relative; position: relative;
flex-shrink: 0;
} }
.dungeon-header::after { .dungeon-header::after {
@ -73,19 +74,18 @@
animation: blink 1.5s infinite; animation: blink 1.5s infinite;
} }
/* LAYOUT */
.battle-layout { .battle-layout {
display: grid; display: grid;
grid-template-columns: 1.2fr 1fr; grid-template-columns: 1.2fr 1fr;
flex: 1; flex: 1;
gap: 30px; gap: 30px;
min-height: 0; /* Важливо для overflow лога */ min-height: 0;
} }
/* ENEMY CARD */
.enemy-display { .enemy-display {
position: relative; position: relative;
height: 100%; height: 100%;
min-height: 0;
} }
.enemy-card { .enemy-card {
@ -152,7 +152,6 @@
text-transform: uppercase; text-transform: uppercase;
} }
/* HP BAR MINI */
.enemy-hp-container { .enemy-hp-container {
width: 60%; width: 60%;
margin-bottom: 30px; margin-bottom: 30px;
@ -187,12 +186,14 @@
color: rgba(160, 172, 186, 0.7); color: rgba(160, 172, 186, 0.7);
} }
/* COMBAT LOG */
.combat-log-wrapper { .combat-log-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(26, 38, 56, 0.8); border: 1px solid rgba(26, 38, 56, 0.8);
min-height: 0;
max-height: 100%;
overflow: hidden;
} }
.log-header { .log-header {
@ -203,6 +204,7 @@
letter-spacing: 1px; letter-spacing: 1px;
color: #00d4ff; color: #00d4ff;
border-bottom: 1px solid rgba(26, 38, 56, 0.8); border-bottom: 1px solid rgba(26, 38, 56, 0.8);
flex-shrink: 0;
} }
.combat-log { .combat-log {
@ -215,12 +217,14 @@
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #1a2638 transparent; scrollbar-color: #1a2638 transparent;
min-height: 0;
} }
.log-entry { .log-entry {
line-height: 1.4; line-height: 1.4;
color: #a0acba; color: #a0acba;
animation: slideIn 0.2s ease-out; animation: slideIn 0.2s ease-out;
word-break: break-all;
} }
.log-arrow { .log-arrow {
@ -229,11 +233,11 @@
font-weight: bold; font-weight: bold;
} }
/* CONTROLS */
.dungeon-controls { .dungeon-controls {
display: flex; display: flex;
gap: 20px; gap: 20px;
height: 70px; height: 70px;
flex-shrink: 0;
} }
.ctrl-btn { .ctrl-btn {
@ -277,7 +281,6 @@
transform: scale(0.98); transform: scale(0.98);
} }
/* UTILS */
.empty-room { .empty-room {
height: 100%; height: 100%;
display: flex; display: flex;
@ -325,19 +328,17 @@
border-radius: 2px; border-radius: 2px;
} }
/* Вирівнювання футера картки */
.enemy-info-footer { .enemy-info-footer {
display: flex; display: flex;
justify-content: space-between; /* Розносимо LVL та ID по боках */ justify-content: space-between;
width: 80%; /* Щоб не були впритул до країв */ width: 80%;
margin-top: auto; /* Притискаємо до низу */ margin-top: auto;
padding-bottom: 20px; padding-bottom: 20px;
font-size: 0.7rem; font-size: 0.7rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
} }
/* Ефект трясіння при атаці */
.enemy-card.taking-damage { .enemy-card.taking-damage {
animation: shake 0.2s ease-in-out; animation: shake 0.2s ease-in-out;
border-color: #ffffff; border-color: #ffffff;
@ -362,7 +363,60 @@
} }
} }
/* Покращення читабельності лога */
.combat-log-wrapper { .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;
} }

View File

@ -4,6 +4,7 @@ import "./DungeonScreen.css";
const DungeonScreen = ({ session, socket }) => { const DungeonScreen = ({ session, socket }) => {
const [roomData, setRoomData] = useState(session.room); const [roomData, setRoomData] = useState(session.room);
const [hostiles, setHostiles] = useState(session.hostiles || []);
const [roomIndex, setRoomIndex] = useState(session.roomIndex); const [roomIndex, setRoomIndex] = useState(session.roomIndex);
const [enemyHp, setEnemyHp] = useState(null); const [enemyHp, setEnemyHp] = useState(null);
const [isEnemyDefeated, setIsEnemyDefeated] = useState(false); const [isEnemyDefeated, setIsEnemyDefeated] = useState(false);
@ -14,31 +15,43 @@ const DungeonScreen = ({ session, socket }) => {
const logEndRef = useRef(null); const logEndRef = useRef(null);
// Отримуємо дані про ворога та данж через GameDataManager const currentEnemy = hostiles.length > 0 ? hostiles[0] : null;
const rawEnemyId = roomData?.hostiles?.[0] || null;
const enemyData = rawEnemyId ? GameDataManager.getEnemy(rawEnemyId) : null;
const dungeonData = GameDataManager.getDungeon(session.dungeonId); 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(() => { useEffect(() => {
logEndRef.current?.scrollIntoView({ behavior: "smooth" }); logEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [log]); }, [log]);
useEffect(() => { useEffect(() => {
// Слухаємо оновлення кімнати (перехід на наступну)
socket.on("dungeon:room_update", (data) => { socket.on("dungeon:room_update", (data) => {
setRoomData(data.room); setRoomData(data.room);
setHostiles(data.hostiles || []);
setRoomIndex(data.roomIndex); setRoomIndex(data.roomIndex);
setIsEnemyDefeated(false); setIsEnemyDefeated(false);
setIsLooted(false); setIsLooted(false);
setEnemyHp(null); // Скидаємо HP для нового ворога setEnemyHp(null);
addLog(`--- ENTERING SECTOR ${data.roomIndex + 1} ---`); addLog(`--- ENTERING SECTOR ${data.roomIndex + 1} ---`);
}); });
// Слухаємо результати бою від сервера
socket.on("dungeon:combat_result", (data) => { socket.on("dungeon:combat_result", (data) => {
if (data.message) addLog(data.message); 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) { if (data.targetDefeated) {
setIsEnemyDefeated(true); setIsEnemyDefeated(true);
addLog("TARGET_NEUTRALIZED: Threat eliminated."); addLog("TARGET_NEUTRALIZED: Threat eliminated.");
@ -49,7 +62,7 @@ const DungeonScreen = ({ session, socket }) => {
socket.off("dungeon:room_update"); socket.off("dungeon:room_update");
socket.off("dungeon:combat_result"); socket.off("dungeon:combat_result");
}; };
}, [socket]); }, [socket, currentEnemy]);
const addLog = (text) => { const addLog = (text) => {
const time = new Date().toLocaleTimeString([], { const time = new Date().toLocaleTimeString([], {
@ -62,15 +75,12 @@ const DungeonScreen = ({ session, socket }) => {
}; };
const handleCombat = () => { const handleCombat = () => {
if (isEnemyDefeated) return; if (isEnemyDefeated || !currentEnemy) return;
socket.emit("dungeon:combat_step", { enemyId: rawEnemyId }); socket.emit("dungeon:combat_step", { enemyId: currentEnemy.id });
addLog( addLog(`Initiating strike sequence...`);
`Initiating strike sequence on ${enemyData?.displayName || "Target"}...`,
);
}; };
const handleLoot = () => { const handleLoot = () => {
socket.emit("dungeon:get_loot");
setIsLooted(true); setIsLooted(true);
addLog("Loot encryption bypassed. Resources transferred."); addLog("Loot encryption bypassed. Resources transferred.");
}; };
@ -103,11 +113,34 @@ const DungeonScreen = ({ session, socket }) => {
</div> </div>
</div> </div>
<div className="environment-panel">
<div className="env-header">ENVIRONMENT_SCAN</div>
<div className="env-info">
<div className="env-icon">
<i
className={`fas ${
roomData?.id?.includes("boss")
? "fa-skull"
: roomData?.id?.includes("loot")
? "fa-box-open"
: "fa-microchip"
}`}
></i>
</div>
<div className="env-details">
<div className="env-id">{getRoomDisplayName(roomData)}</div>
<div className="env-type">
TYPE: {hostiles.length > 0 ? "COMBAT_ZONE" : "SECURE_AREA"}
</div>
</div>
</div>
</div>
<div className="battle-layout"> <div className="battle-layout">
<div <div
className={`enemy-display ${isEnemyDefeated ? "target-lost" : ""}`} className={`enemy-display ${isEnemyDefeated ? "target-lost" : ""}`}
> >
{enemyData ? ( {currentEnemy ? (
<div className={`enemy-card ${isEnemyDefeated ? "defeated" : ""}`}> <div className={`enemy-card ${isEnemyDefeated ? "defeated" : ""}`}>
<div className="threat-tag"> <div className="threat-tag">
{isEnemyDefeated ? "SIGNAL_LOST" : "HOSTILE_DETECTED"} {isEnemyDefeated ? "SIGNAL_LOST" : "HOSTILE_DETECTED"}
@ -117,7 +150,9 @@ const DungeonScreen = ({ session, socket }) => {
className={`fas ${isEnemyDefeated ? "fa-skull-crossbones" : "fa-robot"}`} className={`fas ${isEnemyDefeated ? "fa-skull-crossbones" : "fa-robot"}`}
></i> ></i>
</div> </div>
<h3 className="enemy-name">{enemyData.displayName}</h3> <h3 className="enemy-name">
{getEnemyDisplayName(currentEnemy)}
</h3>
<div className="enemy-hp-container"> <div className="enemy-hp-container">
<div className="hp-label">STRUCTURE INTEGRITY</div> <div className="hp-label">STRUCTURE INTEGRITY</div>
<div className="hp-bar-mini"> <div className="hp-bar-mini">
@ -132,8 +167,8 @@ const DungeonScreen = ({ session, socket }) => {
</div> </div>
</div> </div>
<div className="enemy-info-footer"> <div className="enemy-info-footer">
<span>LVL: {enemyData.level || 1}</span> <span>LVL: {currentEnemy.level || 1}</span>
<span>ID: {GameDataManager._cleanId(rawEnemyId)}</span> <span className="card-id">{currentEnemy.id}</span>
</div> </div>
</div> </div>
) : ( ) : (
@ -158,7 +193,7 @@ const DungeonScreen = ({ session, socket }) => {
</div> </div>
<div className="dungeon-controls"> <div className="dungeon-controls">
{!isEnemyDefeated && enemyData && ( {!isEnemyDefeated && currentEnemy && (
<button className="ctrl-btn combat" onClick={handleCombat}> <button className="ctrl-btn combat" onClick={handleCombat}>
<i className="fas fa-bolt"></i> ENGAGE <i className="fas fa-bolt"></i> ENGAGE
</button> </button>
@ -170,7 +205,7 @@ const DungeonScreen = ({ session, socket }) => {
</button> </button>
)} )}
{(isLooted || !enemyData) && ( {(isLooted || !currentEnemy) && (
<button className="ctrl-btn next" onClick={handleNextRoom}> <button className="ctrl-btn next" onClick={handleNextRoom}>
PROCEED <i className="fas fa-chevron-right"></i> PROCEED <i className="fas fa-chevron-right"></i>
</button> </button>

View File

@ -1,6 +1,6 @@
{ {
"rooms": { "rooms": {
"id": "original:pirate_ambush_zone", "id": "original:pirate/pirate_ambush_zone",
"hostiles": [ "hostiles": [
"original:pirates/scout_drone", "original:pirates/scout_drone",
"original:pirates/raider_frigate", "original:pirates/raider_frigate",

View File

@ -1,6 +1,6 @@
{ {
"rooms": { "rooms": {
"id": "original:pirate_boss_bridge", "id": "original:pirate/pirate_boss_bridge",
"hostiles": ["original:pirates/black_mark_cruiser"], "hostiles": ["original:pirates/black_mark_cruiser"],
"gainXp": 100, "gainXp": 100,
"credits": 2500 "credits": 2500

View File

@ -1,6 +1,6 @@
{ {
"rooms": { "rooms": {
"id": "original:pirate_patrol_room", "id": "original:pirate/pirate_patrol_room",
"hostiles": [ "hostiles": [
"original:pirates/scout_drone", "original:pirates/scout_drone",
"original:pirates/scout_drone" "original:pirates/scout_drone"

View File

@ -1,6 +1,6 @@
{ {
"rooms": { "rooms": {
"id": "original:pirate_supply_bay", "id": "original:pirate/pirate_supply_bay",
"hostiles": [], "hostiles": [],
"gainXp": 5, "gainXp": 5,
"credits": 800, "credits": 800,

View File

@ -1,6 +1,6 @@
{ {
"rooms": { "rooms": {
"id": "original:tutorial/tutorial_boss", "id": "original:tutorial/tutorial_boss_room",
"hostiles": ["original:tutorial/tutorial_boss_hostile"], "hostiles": ["original:tutorial/tutorial_boss_hostile"],
"gainXp": 4, "gainXp": 4,
"credits": 200 "credits": 200

View File

@ -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();

View File

@ -1,6 +1,6 @@
const { Player, Inventory } = require("../../models"); const { Player, Inventory } = require("../../models");
const sessionManager = require("../../game/SessionManager");
const DatapackLoader = require("../../game/DatapackLoader"); const DatapackLoader = require("../../game/DatapackLoader");
const dungeonManager = require("../../game/DungeonManager");
module.exports = (io, socket) => { module.exports = (io, socket) => {
const userId = socket.user?.id; const userId = socket.user?.id;
@ -8,80 +8,71 @@ module.exports = (io, socket) => {
socket.on("dungeon:start", async ({ dungeonId }) => { socket.on("dungeon:start", async ({ dungeonId }) => {
try { try {
if (!userId) return; if (!userId) return;
const dungeon = DatapackLoader.getDungeon(dungeonId); const dungeon = DatapackLoader.getDungeon(dungeonId);
if (!dungeon) { if (!dungeon) {
return socket.emit("error", { message: "Dungeon coordinates invalid" }); return socket.emit("error", { message: "Dungeon not found" });
} }
const player = await Player.findByPk(userId); const player = await Player.findByPk(userId);
if (player.energy < dungeon.energyCost) { const energyCost = dungeon.meta?.energyCost || 0;
return socket.emit("error", {
message: "Insufficient energy for deployment", if (player.energy < energyCost) {
}); return socket.emit("error", { message: "Insufficient energy" });
} }
await player.decrement("energy", { by: dungeon.energyCost }); await player.decrement("energy", { by: energyCost });
const dungeonSession = { const firstRoom = dungeonManager.startDungeon(userId, dungeonId);
dungeonId: dungeon.id, if (!firstRoom) {
currentRoomIndex: 0, return socket.emit("error", {
isCompleted: false, message: "Failed to initialize dungeon",
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", { socket.emit("dungeon:started", {
dungeonId: dungeon.id, dungeonId: dungeon.id,
room: firstRoomData, room: firstRoom.config,
roomIndex: 0, hostiles: firstRoom.hostiles,
totalRooms: dungeon.rooms.length, roomIndex: firstRoom.roomIndex,
remainingEnergy: player.energy - dungeon.energyCost, totalRooms: firstRoom.totalRooms,
remainingEnergy: player.energy - energyCost,
}); });
console.log(`[Dungeon] Player ${userId} started ${dungeonId}`);
} catch (err) { } catch (err) {
console.error("Dungeon Start Error:", 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 { try {
const session = sessionManager.getPlayerSession(socket.id); if (!userId) return;
if (!session || session.scene !== "dungeon" || !session.sceneData) 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); const enemyTemplate = DatapackLoader.getEnemy(enemyId);
if (!enemyTemplate) { if (!enemyTemplate) {
return socket.emit("error", { message: "Target data corrupted" }); return socket.emit("error", { message: "Target data corrupted" });
} }
if (dungeonSession.currentEnemyHp === undefined) { if (session.currentEnemyHp === undefined) {
dungeonSession.currentEnemyHp = enemyTemplate.stats?.hp || 100; session.currentEnemyHp = enemyTemplate.stats?.health || 100;
} }
const damage = Math.floor(Math.random() * 11) + 15; const damage = Math.floor(Math.random() * 10) + 20;
dungeonSession.currentEnemyHp -= damage; session.currentEnemyHp -= damage;
const isDefeated = dungeonSession.currentEnemyHp <= 0; const isDefeated = session.currentEnemyHp <= 0;
socket.emit("dungeon:combat_result", { socket.emit("dungeon:combat_result", {
damageDealt: damage, damageDealt: damage,
enemyHp: Math.max(0, dungeonSession.currentEnemyHp), enemyHp: Math.max(0, session.currentEnemyHp),
targetDefeated: isDefeated, targetDefeated: isDefeated,
message: `Strike successful. Dealt ${damage} damage to units.`, message: `Strike successful. Dealt ${damage} damage.`,
}); });
if (isDefeated) { if (isDefeated) {
console.log(`[Dungeon] Enemy ${enemyId} defeated by ${session.id}`); session.currentEnemyHp = undefined;
} }
} catch (err) { } catch (err) {
console.error("Combat Error:", err); console.error("Combat Error:", err);
@ -90,62 +81,49 @@ module.exports = (io, socket) => {
socket.on("dungeon:next_room", async () => { socket.on("dungeon:next_room", async () => {
try { try {
const session = sessionManager.getPlayerSession(socket.id); if (!userId) return;
if (!session || session.scene !== "dungeon" || !session.sceneData) { const nextRoom = dungeonManager.moveToNextRoom(userId);
console.error( if (!nextRoom) {
`[Dungeon Error] Invalid session for socket ${socket.id}`, return socket.emit("error", {
); message: "Could not proceed to next room",
return; });
} }
const dungeonId = session.sceneData.dungeonId; // ТУТ була помилка (читання з undefined) if (nextRoom.status === "completed") {
const dungeon = DatapackLoader.getDungeon(dungeonId); await finalizeDungeon(socket, nextRoom.rewards);
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,
});
} else { } else {
await finalizeDungeon(socket, session.sceneData, dungeon); socket.emit("dungeon:room_update", {
room: nextRoom.config,
hostiles: nextRoom.hostiles,
roomIndex: nextRoom.roomIndex,
});
} }
} catch (err) { } catch (err) {
console.error("Dungeon Progress Error:", 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 userId = socket.user.id;
const earnedLoot = []; try {
const player = await Player.findByPk(userId);
dungeon.lootTable.forEach((entry) => { if (sessionRewards.credits > 0) {
if (Math.random() * 100 <= entry.chance) { await player.increment("credits", { by: sessionRewards.credits });
earnedLoot.push({ itemId: entry.itemId, quantity: 1 });
} }
});
for (const item of earnedLoot) {
const [invItem, created] = await Inventory.findOrCreate({
where: { playerId: userId, itemId: item.itemId },
defaults: { quantity: 0 },
});
await invItem.increment("quantity", { by: item.quantity });
}
sessionManager.setPlayerScene(socket.id, "world", null);
socket.emit("dungeon:completed", { socket.emit("dungeon:completed", {
rewards: earnedLoot, rewards: sessionRewards,
message: "Mission successful. All objectives secured.", message: "Mission successful. All objectives secured.",
}); });
} finally {
dungeonManager.leaveDungeon(userId);
}
} }