API/client/src/views/GameInterface/components/DungeonScreen.jsx
2026-04-21 08:48:52 +03:00

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">&gt;</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;