290 lines
9.2 KiB
JavaScript
290 lines
9.2 KiB
JavaScript
import React, { useState, useEffect, useRef } from "react";
|
|
import GameDataManager from "../../../services/GameDataManager.js";
|
|
import "./DungeonScreen.css";
|
|
import DungeonFinish from "../tabs/components/DungeonFinish.jsx";
|
|
|
|
const DungeonScreen = ({ session, socket }) => {
|
|
const [roomData, setRoomData] = useState(session.room);
|
|
const [roomIndex, setRoomIndex] = useState(session.roomIndex);
|
|
const [battle, setBattle] = useState(session.battle || null);
|
|
const [timeLeft, setTimeLeft] = useState(10);
|
|
const [summary, setSummary] = useState(null);
|
|
const [activeAttacker, setActiveAttacker] = useState(null);
|
|
|
|
const [selectedTarget, setSelectedTarget] = useState(null);
|
|
|
|
const [log, setLog] = useState([
|
|
"SYSTEM: Neural link established. Scanning sector...",
|
|
]);
|
|
|
|
const logEndRef = useRef(null);
|
|
const timerRef = useRef(null);
|
|
const dungeonData = GameDataManager.getDungeon(session.dungeonId);
|
|
|
|
useEffect(() => {
|
|
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
}, [log]);
|
|
|
|
useEffect(() => {
|
|
setSelectedTarget(null);
|
|
}, [battle?.currentTurnIndex]);
|
|
|
|
useEffect(() => {
|
|
if (!battle || battle.isOver || activeAttacker) return;
|
|
|
|
const isPlayer = battle.turnOrder[battle.currentTurnIndex] === "player";
|
|
const maxTime = isPlayer ? 10 : 4;
|
|
setTimeLeft(maxTime);
|
|
|
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
|
|
timerRef.current = setInterval(() => {
|
|
setTimeLeft((prev) => {
|
|
if (prev <= 1) {
|
|
if (isPlayer) handleCombatAction();
|
|
return 0;
|
|
}
|
|
return prev - 1;
|
|
});
|
|
}, 1000);
|
|
|
|
return () => clearInterval(timerRef.current);
|
|
}, [battle?.currentTurnIndex, battle?.isOver, activeAttacker]);
|
|
|
|
useEffect(() => {
|
|
socket.on("dungeon:room_update", (data) => {
|
|
setRoomData(data.room);
|
|
setRoomIndex(data.roomIndex);
|
|
setBattle(data.battle);
|
|
addLog(`--- ENTERING SECTOR ${data.roomIndex + 1} ---`);
|
|
});
|
|
|
|
socket.on("dungeon:failed", (data) => {
|
|
addLog(`--- TERMINAL ERROR: ${data.message} ---`);
|
|
setTimeout(() => window.location.reload(), 3000);
|
|
});
|
|
|
|
socket.on("dungeon:battle_update", async (data) => {
|
|
const turnOrder = data.battle.turnOrder;
|
|
const lastIndex =
|
|
(data.battle.currentTurnIndex - 1 + turnOrder.length) %
|
|
turnOrder.length;
|
|
const lastActorId = turnOrder[lastIndex];
|
|
|
|
if (lastActorId !== "player" && !data.battle.isOver) {
|
|
setActiveAttacker(lastActorId);
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
setBattle(data.battle);
|
|
if (data.log) data.log.forEach((msg) => addLog(msg));
|
|
setActiveAttacker(null);
|
|
} else {
|
|
setBattle(data.battle);
|
|
if (data.log) data.log.forEach((msg) => addLog(msg));
|
|
setActiveAttacker(null);
|
|
}
|
|
|
|
if (data.status === "victory")
|
|
addLog("MISSION_OBJECTIVE: Threats neutralized.");
|
|
if (data.status === "defeat") {
|
|
addLog("CRITICAL_ERROR: Bio-sign lost.");
|
|
setTimeout(() => window.location.reload(), 3000);
|
|
}
|
|
});
|
|
|
|
socket.on("dungeon:completed", (data) => {
|
|
setSummary(data.rewards);
|
|
addLog("MISSION_SUCCESS: All objectives secured.");
|
|
});
|
|
|
|
return () => {
|
|
socket.off("dungeon:room_update");
|
|
socket.off("dungeon:battle_update");
|
|
socket.off("dungeon:completed");
|
|
socket.off("dungeon:failed");
|
|
};
|
|
}, [socket]);
|
|
|
|
const addLog = (text) => {
|
|
const time = new Date().toLocaleTimeString([], {
|
|
hour12: false,
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
});
|
|
setLog((prev) => [...prev, `[${time}] ${text}`]);
|
|
};
|
|
|
|
const handleCombatAction = () => {
|
|
const targetId = selectedTarget;
|
|
if (!battle || battle.isOver || activeAttacker || !targetId) return;
|
|
if (battle.turnOrder[battle.currentTurnIndex] !== "player") return;
|
|
|
|
socket.emit("dungeon:combat_action", { targetInstanceId: targetId });
|
|
addLog(`Initiating strike sequence...`);
|
|
setSelectedTarget(null); // Скидаємо вибір після атаки
|
|
};
|
|
|
|
const handleNextRoom = () => {
|
|
socket.emit("dungeon:next_room");
|
|
};
|
|
|
|
const isPlayerTurn = battle?.turnOrder[battle?.currentTurnIndex] === "player";
|
|
|
|
return (
|
|
<div className="dungeon-active-screen">
|
|
{summary && (
|
|
<DungeonFinish
|
|
rewards={summary}
|
|
onExit={() => window.location.reload()}
|
|
/>
|
|
)}
|
|
|
|
{/* Header section remains the same */}
|
|
<div className="dungeon-header">
|
|
<div className="room-progress">
|
|
<div className="progress-text">
|
|
SECTOR {roomIndex + 1} / {session.totalRooms}
|
|
</div>
|
|
<div className="progress-bar">
|
|
<div
|
|
className="fill"
|
|
style={{
|
|
width: `${((roomIndex + 1) / session.totalRooms) * 100}%`,
|
|
}}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
{battle && (
|
|
<div
|
|
className={`turn-progress-container ${isPlayerTurn ? "player-phase" : "enemy-phase"}`}
|
|
>
|
|
<div className="turn-label">
|
|
{isPlayerTurn ? "YOUR TURN" : "ENEMY ACTION"}
|
|
</div>
|
|
<div className="turn-timer-bar">
|
|
<div
|
|
className="turn-timer-fill"
|
|
style={{
|
|
width: `${(timeLeft / (isPlayerTurn ? 10 : 4)) * 100}%`,
|
|
transition:
|
|
timeLeft === 10 || timeLeft === 4
|
|
? "none"
|
|
: "width 1s linear",
|
|
}}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="battle-arena">
|
|
{battle ? (
|
|
<div className="mobs-grid">
|
|
{battle.enemies.map((mob) => (
|
|
<div
|
|
key={mob.instanceId}
|
|
className={`enemy-card
|
|
${mob.isDead ? "defeated" : ""}
|
|
${selectedTarget === mob.instanceId ? "selected" : ""}
|
|
${isPlayerTurn && !mob.isDead ? "selectable" : ""}
|
|
${activeAttacker === mob.instanceId ? "attacking" : ""}
|
|
`}
|
|
onClick={() =>
|
|
!mob.isDead &&
|
|
isPlayerTurn &&
|
|
setSelectedTarget(mob.instanceId)
|
|
}
|
|
>
|
|
{selectedTarget === mob.instanceId && (
|
|
<div className="target-aim">
|
|
<i className="fas fa-crosshairs"></i>
|
|
</div>
|
|
)}
|
|
|
|
<div className="enemy-hp-mini">
|
|
<div
|
|
className="fill"
|
|
style={{ width: `${(mob.hp / mob.maxHp) * 100}%` }}
|
|
></div>
|
|
</div>
|
|
<div className="enemy-icon">
|
|
<i
|
|
className={`fas ${mob.isDead ? "fa-skull-crossbones" : "fa-robot"}`}
|
|
></i>
|
|
</div>
|
|
<span className="mob-name">{GameDataManager.t(mob.name)}</span>
|
|
{!mob.isDead && <span className="mob-atk">ATK: {mob.atk}</span>}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="empty-room">
|
|
<i className="fas fa-satellite-dish"></i>
|
|
<p>AREA SECURE</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="player-section">
|
|
{battle && (
|
|
<div
|
|
className={`player-hp-main ${activeAttacker ? "taking-damage" : ""}`}
|
|
>
|
|
<div className="hp-header">
|
|
<span>COMMANDER_INTEGRITY</span>
|
|
<span>
|
|
{battle.player.hp} / {battle.player.maxHp}
|
|
</span>
|
|
</div>
|
|
<div className="hp-bar-large">
|
|
<div
|
|
className="fill"
|
|
style={{
|
|
width: `${(battle.player.hp / battle.player.maxHp) * 100}%`,
|
|
}}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="combat-interface-row">
|
|
<div className="combat-log-wrapper">
|
|
<div className="combat-log custom-scroll">
|
|
{log.map((entry, i) => (
|
|
<div key={i} className="log-entry">
|
|
<span className="log-arrow">></span> {entry}
|
|
</div>
|
|
))}
|
|
<div ref={logEndRef} />
|
|
</div>
|
|
</div>
|
|
|
|
{battle && !battle.isOver && (
|
|
<button
|
|
className={`btn-execute-combat ${!selectedTarget || !isPlayerTurn ? "disabled" : ""}`}
|
|
disabled={!selectedTarget || !isPlayerTurn}
|
|
onClick={handleCombatAction}
|
|
>
|
|
<div className="btn-glitch-content">EXECUTE_STRIKE</div>
|
|
<div className="btn-sub-text">
|
|
{selectedTarget ? "TARGET_LOCKED" : "SELECT_TARGET"}
|
|
</div>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="dungeon-controls">
|
|
{((battle?.isOver && battle.player.hp > 0) || !battle) && (
|
|
<button className="ctrl-btn next" onClick={handleNextRoom}>
|
|
PROCEED <i className="fas fa-chevron-right"></i>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DungeonScreen;
|