Added Dungeon Manager.
This commit is contained in:
parent
25090a5316
commit
e74a209bb8
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
86
game-server/src/game/DungeonManager.js
Normal file
86
game-server/src/game/DungeonManager.js
Normal 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();
|
||||||
@ -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;
|
||||||
|
|
||||||
|
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", {
|
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", {
|
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) {
|
socket.emit("dungeon:completed", {
|
||||||
const [invItem, created] = await Inventory.findOrCreate({
|
rewards: sessionRewards,
|
||||||
where: { playerId: userId, itemId: item.itemId },
|
message: "Mission successful. All objectives secured.",
|
||||||
defaults: { quantity: 0 },
|
|
||||||
});
|
});
|
||||||
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.",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user