Updated Skills Tab. Added Turn to Dungeon Manager. Fixed DungeonScreen.

This commit is contained in:
MaksSlyzar 2026-04-25 14:52:17 +03:00
parent 4f0ad9eca6
commit 2892a57949
20 changed files with 815 additions and 494 deletions

View File

@ -36,17 +36,31 @@ class GameDataManager {
data.quests.forEach((q) => this.quests.set(q.id, q)); data.quests.forEach((q) => this.quests.set(q.id, q));
} }
console.log(this.quests); console.log(this.skills);
if (data.languages) { if (data.languages) {
this.translations = data.languages; this.translations = data.languages;
} }
if (data.manifest) { if (data.manifest) {
this.manifest = data.manifest; this.manifest = data.manifest;
} }
console.log(this.manifest);
this.isLoaded = true; this.isLoaded = true;
} }
getSkillCategories() {
return this._getCategoriesFromManifest("skills");
}
getSkillsByCategory(category) {
console.log(this.skills, category, "CATEGORY");
return Array.from(this.skills.values())
.filter((skill) => skill.meta.category === category)
.map((skill) => ({
...skill,
displayName: this.t(skill.displayName),
description: this.t(skill.description),
}));
}
t(key) { t(key) {
if (!key) return ""; if (!key) return "";
const langData = this.translations[this.currentLang]; const langData = this.translations[this.currentLang];

View File

@ -328,3 +328,22 @@
margin-top: 4px; margin-top: 4px;
opacity: 0.8; opacity: 0.8;
} }
.mob-stats-display {
display: flex;
gap: 8px;
font-size: 0.65rem;
margin-top: 5px;
color: rgba(255, 255, 255, 0.6);
}
.player-stats-row {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 0.8rem;
color: #00d2ff;
font-family: "Orbitron", sans-serif;
border-top: 1px solid rgba(0, 210, 255, 0.2);
padding-top: 5px;
}

View File

@ -4,105 +4,19 @@ import "./DungeonScreen.css";
import DungeonFinish from "../tabs/components/DungeonFinish.jsx"; import DungeonFinish from "../tabs/components/DungeonFinish.jsx";
const DungeonScreen = ({ session, socket }) => { const DungeonScreen = ({ session, socket }) => {
const [roomData, setRoomData] = useState(session.room);
const [roomIndex, setRoomIndex] = useState(session.roomIndex);
const [battle, setBattle] = useState(session.battle || null); const [battle, setBattle] = useState(session.battle || null);
const [roomIndex, setRoomIndex] = useState(session.roomIndex);
const [totalRooms, setTotalRooms] = useState(session.totalRooms || 1);
const [timeLeft, setTimeLeft] = useState(10); const [timeLeft, setTimeLeft] = useState(10);
const [summary, setSummary] = useState(null); const [summary, setSummary] = useState(null);
const [activeAttacker, setActiveAttacker] = useState(null); const [activeAttacker, setActiveAttacker] = useState(null);
const [selectedTarget, setSelectedTarget] = useState(null); const [selectedTarget, setSelectedTarget] = useState(null);
const [log, setLog] = useState([ const [log, setLog] = useState([
"SYSTEM: Neural link established. Scanning sector...", "SYSTEM: Neural link established. Scanning sector...",
]); ]);
const logEndRef = useRef(null); const logEndRef = useRef(null);
const timerRef = 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 addLog = (text) => {
const time = new Date().toLocaleTimeString([], { const time = new Date().toLocaleTimeString([], {
@ -114,21 +28,111 @@ const DungeonScreen = ({ session, socket }) => {
setLog((prev) => [...prev, `[${time}] ${text}`]); setLog((prev) => [...prev, `[${time}] ${text}`]);
}; };
const handleCombatAction = () => { useEffect(() => {
const targetId = selectedTarget; logEndRef.current?.scrollIntoView({ behavior: "smooth" });
if (!battle || battle.isOver || activeAttacker || !targetId) return; }, [log]);
if (battle.turnOrder[battle.currentTurnIndex] !== "player") return;
useEffect(() => {
if (!battle || battle.isOver || activeAttacker) {
if (timerRef.current) clearInterval(timerRef.current);
return;
}
const isPlayer = battle.turnOrder[battle.currentTurnIndex] === "player";
if (!isPlayer) return;
setTimeLeft(10);
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
handleCombatAction(null);
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timerRef.current);
}, [battle?.currentTurnIndex, battle?.isOver, activeAttacker]);
useEffect(() => {
socket.on("dungeon:battle_update", async (data) => {
if (data.log && Array.isArray(data.log)) {
for (const action of data.log) {
if (typeof action === "object" && action.attackerId) {
setActiveAttacker(action.attackerId);
action.messages?.forEach((msg) => addLog(msg));
if (action.hpState) {
setBattle((prev) => ({
...prev,
player: { ...prev.player, hp: action.hpState.playerHp },
enemies: prev.enemies.map((e) => {
const s = action.hpState.enemies.find(
(ae) => ae.id === e.instanceId,
);
return s ? { ...e, hp: s.hp, isDead: s.isDead } : e;
}),
}));
}
await new Promise((r) => setTimeout(r, 1500));
} else if (typeof action === "string") {
addLog(action);
}
}
}
setBattle(data.battle);
setActiveAttacker(null);
setSelectedTarget(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:room_update", (data) => {
setRoomIndex(data.roomIndex);
setTotalRooms(data.totalRooms);
setBattle(data.battle);
addLog(`--- ENTERING SECTOR ${data.roomIndex + 1} ---`);
});
socket.on("dungeon:completed", (data) => {
setSummary(data.rewards);
addLog("MISSION_SUCCESS: All objectives secured.");
});
return () => {
socket.off("dungeon:battle_update");
socket.off("dungeon:room_update");
socket.off("dungeon:completed");
};
}, [socket]);
const handleCombatAction = (targetId = selectedTarget) => {
const isPlayer = battle?.turnOrder[battle?.currentTurnIndex] === "player";
if (!battle || battle.isOver || activeAttacker || !isPlayer) return;
socket.emit("dungeon:combat_action", { targetInstanceId: targetId }); socket.emit("dungeon:combat_action", { targetInstanceId: targetId });
addLog(`Initiating strike sequence...`); if (!targetId) addLog("Sequence timeout! Skipping...");
setSelectedTarget(null); // Скидаємо вибір після атаки else addLog("Initiating strike sequence...");
setSelectedTarget(null);
}; };
const handleNextRoom = () => { const handleNextRoom = () => {
socket.emit("dungeon:next_room"); socket.emit("dungeon:next_room");
}; };
const isPlayerTurn = battle?.turnOrder[battle?.currentTurnIndex] === "player"; const isPlayerTurn =
battle?.turnOrder[battle?.currentTurnIndex] === "player" &&
!activeAttacker &&
!battle.isOver;
return ( return (
<div className="dungeon-active-screen"> <div className="dungeon-active-screen">
@ -139,40 +143,35 @@ const DungeonScreen = ({ session, socket }) => {
/> />
)} )}
{/* Header section remains the same */}
<div className="dungeon-header"> <div className="dungeon-header">
<div className="room-progress"> <div className="room-progress">
<div className="progress-text"> <div className="progress-text">
SECTOR {roomIndex + 1} / {session.totalRooms} SECTOR {roomIndex + 1} / {totalRooms}
</div> </div>
<div className="progress-bar"> <div className="progress-bar">
<div <div
className="fill" className="fill"
style={{ style={{ width: `${((roomIndex + 1) / totalRooms) * 100}%` }}
width: `${((roomIndex + 1) / session.totalRooms) * 100}%`,
}}
></div> ></div>
</div> </div>
</div> </div>
{battle && ( {battle && !battle.isOver && (
<div <div
className={`turn-progress-container ${isPlayerTurn ? "player-phase" : "enemy-phase"}`} className={`turn-progress-container ${isPlayerTurn ? "player-phase" : "enemy-phase"}`}
> >
<div className="turn-label"> <div className="turn-label">
{isPlayerTurn ? "YOUR TURN" : "ENEMY ACTION"} {isPlayerTurn ? "YOUR TURN" : "PROCESSING..."}
</div> </div>
<div className="turn-timer-bar"> <div className="turn-timer-bar">
<div <div
className="turn-timer-fill" className="turn-timer-fill"
style={{ style={{
width: `${(timeLeft / (isPlayerTurn ? 10 : 4)) * 100}%`, width: `${(timeLeft / 10) * 100}%`,
transition: transition: timeLeft === 10 ? "none" : "width 1s linear",
timeLeft === 10 || timeLeft === 4 visibility: isPlayerTurn ? "visible" : "hidden",
? "none"
: "width 1s linear",
}} }}
></div> />
</div> </div>
</div> </div>
)} )}
@ -184,29 +183,18 @@ const DungeonScreen = ({ session, socket }) => {
{battle.enemies.map((mob) => ( {battle.enemies.map((mob) => (
<div <div
key={mob.instanceId} key={mob.instanceId}
className={`enemy-card className={`enemy-card ${mob.isDead ? "defeated" : ""} ${selectedTarget === mob.instanceId ? "selected" : ""} ${isPlayerTurn && !mob.isDead ? "selectable" : ""} ${activeAttacker === mob.instanceId ? "attacking" : ""}`}
${mob.isDead ? "defeated" : ""}
${selectedTarget === mob.instanceId ? "selected" : ""}
${isPlayerTurn && !mob.isDead ? "selectable" : ""}
${activeAttacker === mob.instanceId ? "attacking" : ""}
`}
onClick={() => onClick={() =>
!mob.isDead &&
isPlayerTurn && isPlayerTurn &&
!mob.isDead &&
setSelectedTarget(mob.instanceId) setSelectedTarget(mob.instanceId)
} }
> >
{selectedTarget === mob.instanceId && (
<div className="target-aim">
<i className="fas fa-crosshairs"></i>
</div>
)}
<div className="enemy-hp-mini"> <div className="enemy-hp-mini">
<div <div
className="fill" className="fill"
style={{ width: `${(mob.hp / mob.maxHp) * 100}%` }} style={{ width: `${(mob.hp / mob.maxHp) * 100}%` }}
></div> />
</div> </div>
<div className="enemy-icon"> <div className="enemy-icon">
<i <i
@ -214,7 +202,6 @@ const DungeonScreen = ({ session, socket }) => {
></i> ></i>
</div> </div>
<span className="mob-name">{GameDataManager.t(mob.name)}</span> <span className="mob-name">{GameDataManager.t(mob.name)}</span>
{!mob.isDead && <span className="mob-atk">ATK: {mob.atk}</span>}
</div> </div>
))} ))}
</div> </div>
@ -229,7 +216,7 @@ const DungeonScreen = ({ session, socket }) => {
<div className="player-section"> <div className="player-section">
{battle && ( {battle && (
<div <div
className={`player-hp-main ${activeAttacker ? "taking-damage" : ""}`} className={`player-hp-main ${activeAttacker && activeAttacker !== "player" ? "taking-damage" : ""}`}
> >
<div className="hp-header"> <div className="hp-header">
<span>COMMANDER_INTEGRITY</span> <span>COMMANDER_INTEGRITY</span>
@ -243,40 +230,33 @@ const DungeonScreen = ({ session, socket }) => {
style={{ style={{
width: `${(battle.player.hp / battle.player.maxHp) * 100}%`, width: `${(battle.player.hp / battle.player.maxHp) * 100}%`,
}} }}
></div> />
</div> </div>
</div> </div>
)} )}
<div className="combat-interface-row"> <div className="combat-interface-row">
<div className="combat-log-wrapper"> <div className="combat-log custom-scroll">
<div className="combat-log custom-scroll"> {log.map((entry, i) => (
{log.map((entry, i) => ( <div key={i} className="log-entry">
<div key={i} className="log-entry"> <span className="log-arrow">&gt;</span> {entry}
<span className="log-arrow">&gt;</span> {entry} </div>
</div> ))}
))} <div ref={logEndRef} />
<div ref={logEndRef} />
</div>
</div> </div>
{battle && !battle.isOver && ( {battle && !battle.isOver && (
<button <button
className={`btn-execute-combat ${!selectedTarget || !isPlayerTurn ? "disabled" : ""}`} className={`btn-execute-combat ${!selectedTarget || !isPlayerTurn ? "disabled" : ""}`}
disabled={!selectedTarget || !isPlayerTurn} disabled={!selectedTarget || !isPlayerTurn}
onClick={handleCombatAction} onClick={() => handleCombatAction()}
> >
<div className="btn-glitch-content">EXECUTE_STRIKE</div> EXECUTE_STRIKE
<div className="btn-sub-text">
{selectedTarget ? "TARGET_LOCKED" : "SELECT_TARGET"}
</div>
</button> </button>
)} )}
</div> </div>
</div> </div>
<div className="dungeon-controls"> <div className="dungeon-controls">
{((battle?.isOver && battle.player.hp > 0) || !battle) && ( {((battle?.isOver && battle.player.hp > 0) || !battle) && !summary && (
<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,160 +1,115 @@
import React, { useEffect, useState } from 'react'; import React, { useState, useEffect } from "react";
import { useSocket } from "../../../hooks/useSocket";
import GameDataManager from "../../../services/GameDataManager";
import "./styles/SkillsTab.css"; import "./styles/SkillsTab.css";
import CategorySelector from "../components/CategorySelector";
import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
import { SkillCard } from "./components/SkillsCard.jsx";
const SkillsTab = () => { const SkillsTab = () => {
const [category, setCategory] = useState('combat'); const { socket } = useSocket();
const categories = [
{ id: 'combat', label: 'Combat' },
{ id: 'science', label: 'Science' },
{ id: 'crafting', label: 'Crafting' }
];
const [skillsData] = useState(() => { const [categories, setCategories] = useState([]);
const catIds = categories.map(c => c.id); const [activeCategory, setActiveCategory] = useState("");
return Array.from({ length: 5 }, (_, i) => ({ const [skills, setSkills] = useState([]);
id: `skill-${i}`, const [playerSkills, setPlayerSkills] = useState({});
category: catIds[i % catIds.length], const [skillPoints, setSkillPoints] = useState(0);
name: `Tech ${i + 1}`,
currentLevel: i < 3 ? 1 : 0,
maxLevel: 10,
experience: 20,
experienceToNext: 100,
description: "Specialized module for advanced space operations.",
iconClass: i % 3 === 0 ? "fa-shield-alt" : i % 3 === 1 ? "fa-flask" : "fa-tools",
unlocked: i < 3,
requiredLevel: 1
}));
});
const handleUnlock = (category, id) => {
console.log(`Unlocking ${id} in ${category}`);
};
const handleUpgrade = (category, id) => { useEffect(() => {
console.log(`Upgrading ${id} in ${category}`); const manifestCategories = GameDataManager.getSkillCategories();
setCategories(manifestCategories);
if (manifestCategories.length > 0) {
setActiveCategory(manifestCategories[0].id);
}
}, []);
useEffect(() => {
if (activeCategory) {
const filtered = GameDataManager.getSkillsByCategory(activeCategory);
setSkills(filtered);
}
}, [activeCategory]);
useEffect(() => {
if (!socket) return;
socket.emit("player:get_skill_points");
socket.emit("player:get_skills");
const handleSkillPoints = (data) => setSkillPoints(data.points || 0);
const handleSkillsData = (data) => setPlayerSkills(data.skills || {});
socket.on("player:skill_points_data", handleSkillPoints);
socket.on("player:skills_data", handleSkillsData);
return () => {
socket.off("player:skill_points_data", handleSkillPoints);
socket.off("player:skills_data", handleSkillsData);
};
}, [socket]);
const handleUpgrade = (skillId) => {
socket.emit("player:upgrade_skill", { skillId });
}; };
const filteredSkills = skillsData.filter(skill => skill.category === category);
return ( return (
<div className="tab-content active"> <div
<div className="skills-container"> className="tab-content active"
style={{ height: "100%", display: "flex", flexDirection: "column" }}
>
<MeteorRegion className="skills-container">
<div className="skills-header"> <div className="skills-header">
<h2><i className="fas fa-graduation-cap"></i> Skills</h2> <div className="header-main">
<div className="skill-points-display"> <h2>
<span className="skill-points">Skill Points: 0</span> <i className="fas fa-microchip"></i> Neural Core
</h2>
<div className="skill-points-badge">
<span className="label">Uplink Points:</span>
<span className="value">{skillPoints}</span>
</div>
</div> </div>
</div> </div>
<div className="skill-categories">
{categories.map(cat => ( <CategorySelector
<button categories={categories}
key={cat.id} activeCategory={activeCategory}
className={`skill-cat-btn ${category === cat.id ? 'active' : ''}`} onCategoryChange={setActiveCategory}
onClick={() => setCategory(cat.id)} />
>
{cat.label} <div className="skills-grid custom-scroll">
</button> {skills.length > 0 ? (
))} skills.map((skill) => {
</div> const progress = playerSkills[skill.id] || {
<div className="skills-grid"> level: 0,
{filteredSkills.length > 0 ? ( experience: 0,
filteredSkills.map(skill => ( };
<SkillComponent const maxLv = skill.meta?.topLevel || 10;
key={skill.id}
skill={skill} const cost = progress.level === 0 ? 2 : 1;
activeCategory={category}
skillId={skill.id} return (
onUnlock={(cat, id) => console.log('Unlock', cat, id)} <SkillCard
onUpgrade={(cat, id) => console.log('Upgrade', cat, id)} key={skill.id}
/> skill={skill}
)) level={progress.level}
maxLevel={maxLv}
experience={progress.experience}
canAfford={skillPoints >= cost}
onUpgrade={() => handleUpgrade(skill.id)}
/>
);
})
) : ( ) : (
<p className="empty-msg">No skills discovered in this category yet.</p> <div className="empty-category">
<i className="fas fa-xs fa-terminal"></i>
<p>No active modules found in this sector.</p>
</div>
)} )}
</div> </div>
</div> </MeteorRegion>
</div>
);
};
const SkillComponent = ({
skill,
activeCategory,
skillId,
onUnlock,
onUpgrade,
}) => {
const {
name,
currentLevel,
maxLevel,
description,
iconClass,
experience,
experienceToNext,
unlocked,
requiredLevel
} = skill;
const progressPercent = (experience / experienceToNext) * 100;
return (
<div className={`skill-item ${!unlocked ? 'locked' : ''}`}>
<div className="skill-header">
<div className="skill-icon">
<i className={`fas ${iconClass}`}></i>
</div>
<div className="skill-info">
<div className="skill-name">{name}</div>
<div className="skill-level">Lv. {currentLevel}/{maxLevel}</div>
</div>
</div>
<div className="skill-description">{description}</div>
{unlocked && currentLevel < maxLevel && (
<div className="skill-progress">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${progressPercent}%` }}
></div>
</div>
<span>{experience}/{experienceToNext} XP</span>
</div>
)}
{currentLevel >= maxLevel && (
<div className="skill-max-level">
<span>MAX LEVEL REACHED</span>
</div>
)}
<div className="skill-actions">
{!unlocked ? (
<button
className="btn btn-warning"
onClick={() => onUnlock(activeCategory, skillId)}
>
Unlock (2 Points)
</button>
) : currentLevel < maxLevel ? (
<button
className="btn btn-primary"
onClick={() => onUpgrade(activeCategory, skillId)}
>
Upgrade (1 Point)
</button>
) : (
<span className="max-level-text">Mastered</span>
)}
</div>
{!skill.unlocked ?
<div class="skill-requirement">Requires Level {skill.requiredLevel}</div>
: ''}
</div> </div>
); );
}; };
export default SkillsTab; export default SkillsTab;

View File

@ -0,0 +1,175 @@
.skill-item-card {
background: rgba(10, 15, 24, 0.95);
border: 1px solid #1a2638;
border-radius: 2px;
padding: 15px;
display: flex;
flex-direction: column;
gap: 12px;
position: relative;
transition: all 0.3s ease;
overflow: hidden;
}
.skill-item-card:hover {
border-color: #00d4ff;
box-shadow: 0 0 15px rgba(0, 212, 255, 0.1);
}
.skill-item-card.locked {
border-style: dashed;
opacity: 0.8;
}
.skill-item-card.locked .skill-icon-wrapper {
color: #4a5d75;
border-color: #1a2638;
}
.skill-item-card.mastered {
border-color: #00ff88;
}
.skill-card-header {
display: flex;
align-items: center;
gap: 12px;
}
.skill-icon-wrapper {
width: 40px;
height: 40px;
background: #05080c;
border: 1px solid #00d4ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #00d4ff;
flex-shrink: 0;
}
.skill-title-block {
flex: 1;
}
.skill-name {
font-size: 0.9rem;
font-weight: 900;
color: #fff;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 2px;
}
.skill-level-tag {
font-size: 10px;
color: #4a5d75;
font-family: "Space Mono", monospace;
}
.mastered .skill-level-tag {
color: #00ff88;
}
.skill-desc {
font-size: 11px;
color: #a0aec0;
line-height: 1.4;
margin: 0;
min-height: 32px;
}
.skill-progress-section {
margin-top: 5px;
}
.progress-info {
display: flex;
justify-content: space-between;
font-size: 9px;
color: #4a5d75;
margin-bottom: 4px;
text-transform: uppercase;
}
.skill-progress-bar {
height: 4px;
background: #05080c;
border: 1px solid #1a2638;
position: relative;
}
.skill-progress-bar .fill {
height: 100%;
background: linear-gradient(90deg, #00d4ff, #00ff88);
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.skill-card-actions {
margin-top: auto;
padding-top: 10px;
}
.btn-skill-unlock,
.btn-skill-upgrade {
width: 100%;
background: transparent;
border: 1px solid #00d4ff;
color: #00d4ff;
padding: 8px;
font-size: 10px;
font-weight: 900;
text-transform: uppercase;
cursor: pointer;
transition: all 0.2s ease;
font-family: "Space Mono", monospace;
}
.btn-skill-unlock:hover:not(:disabled),
.btn-skill-upgrade:hover:not(:disabled) {
background: rgba(0, 212, 255, 0.1);
box-shadow: 0 0 10px rgba(0, 212, 255, 0.2);
}
.btn-skill-unlock {
border-color: #ffaa00;
color: #ffaa00;
}
.btn-skill-unlock:hover:not(:disabled) {
background: rgba(255, 170, 0, 0.1);
box-shadow: 0 0 10px rgba(255, 170, 0, 0.2);
}
.btn-skill-unlock:disabled,
.btn-skill-upgrade:disabled {
border-color: #1a2638;
color: #4a5d75;
cursor: not-allowed;
opacity: 0.5;
}
.mastery-label {
text-align: center;
font-size: 10px;
color: #00ff88;
font-weight: 900;
padding: 8px;
border: 1px solid rgba(0, 255, 136, 0.2);
background: rgba(0, 255, 136, 0.05);
}
.lock-requirement {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: rgba(255, 68, 68, 0.1);
color: #ff4444;
font-size: 8px;
text-align: center;
padding: 2px 0;
text-transform: uppercase;
font-weight: 900;
}

View File

@ -0,0 +1,76 @@
import "./SkillsCard.css";
export const SkillCard = ({
skill,
level,
maxLevel,
experience,
onUpgrade,
canAfford,
}) => {
const isLocked = level === 0;
const isMaxLevel = level >= maxLevel;
const expToNext = Math.floor(100 * Math.pow(1.5, level));
const progressPercent = Math.min(100, (experience / expToNext) * 100);
return (
<div
className={`skill-item-card ${isLocked ? "locked" : ""} ${isMaxLevel ? "mastered" : ""}`}
>
<div className="skill-card-header">
<div className="skill-icon-wrapper">
{/* Іконка може бути в meta або за дефолтом */}
<i className={`fas ${skill.meta?.icon || "fa-atom"}`}></i>
</div>
<div className="skill-title-block">
<div className="skill-name">{skill.displayName}</div>
<div className="skill-level-tag">
{isMaxLevel ? "MAXED" : `RANK ${level}/${maxLevel}`}
</div>
</div>
</div>
<p className="skill-desc">{skill.description}</p>
{!isMaxLevel && !isLocked && (
<div className="skill-progress-section">
<div className="progress-info">
<span>Neural Sync</span>
<span>
{Math.floor(experience)} / {expToNext}
</span>
</div>
<div className="skill-progress-bar">
<div
className="fill"
style={{ width: `${progressPercent}%` }}
></div>
</div>
</div>
)}
<div className="skill-card-actions">
{isLocked ? (
<button
className="btn-skill-unlock"
disabled={!canAfford}
onClick={onUpgrade}
>
INSTALL MODULE (2 PTS)
</button>
) : isMaxLevel ? (
<div className="mastery-label">MODULE FULLY OPTIMIZED</div>
) : (
<button
className="btn-skill-upgrade"
disabled={!canAfford}
onClick={onUpgrade}
>
UPGRADE (1 PT)
</button>
)}
</div>
</div>
);
};

View File

@ -1,42 +1,133 @@
.skills-container { .skills-container {
max-width: 1200px; display: flex;
margin: 0 auto; flex-direction: column;
max-height: calc(100vh - 232px); /* Adjusted for title bar and padding */ height: 100%;
overflow-y: auto; padding: 20px;
padding: 0.5rem; box-sizing: border-box;
position: relative;
overflow: hidden;
} }
.skill-categories { .skills-header {
display: flex; margin-bottom: 20px;
gap: 0.5rem; padding-bottom: 15px;
margin-bottom: 2rem; border-bottom: 1px solid rgba(0, 212, 255, 0.1);
justify-content: center;
} }
.skill-cat-btn { .header-main {
padding: 0.75rem 1.5rem; display: flex;
background: var(--bg-tertiary); justify-content: space-between;
border: 1px solid var(--border-color); align-items: center;
border-radius: 8px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.3s ease;
} }
.skill-cat-btn:hover { .header-main h2 {
border-color: var(--primary-color); margin: 0;
color: var(--text-primary); font-size: 1.2rem;
color: #fff;
text-transform: uppercase;
letter-spacing: 2px;
display: flex;
align-items: center;
gap: 10px;
} }
.skill-cat-btn.active { .header-main h2 i {
background: var(--gradient-primary); color: #00d4ff;
color: var(--bg-primary); text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
border-color: transparent; }
.skill-points-badge {
background: rgba(0, 212, 255, 0.1);
border: 1px solid #00d4ff;
padding: 5px 15px;
display: flex;
align-items: center;
gap: 10px;
}
.skill-points-badge .label {
font-size: 9px;
color: #4a5d75;
text-transform: uppercase;
font-weight: 900;
}
.skill-points-badge .value {
font-size: 1.1rem;
color: #fff;
font-family: "Space Mono", monospace;
font-weight: 900;
} }
.skills-grid { .skills-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem; gap: 15px;
overflow-y: auto;
padding-right: 10px;
flex: 1;
} }
.custom-scroll::-webkit-scrollbar {
width: 4px;
}
.custom-scroll::-webkit-scrollbar-track {
background: rgba(5, 8, 12, 0.5);
}
.custom-scroll::-webkit-scrollbar-thumb {
background: #1a2638;
border-radius: 2px;
}
.custom-scroll::-webkit-scrollbar-thumb:hover {
background: #00d4ff;
}
.empty-category {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px;
color: #4a5d75;
text-align: center;
border: 1px dashed #1a2638;
background: rgba(0, 0, 0, 0.2);
}
.empty-category i {
font-size: 2rem;
margin-bottom: 15px;
opacity: 0.5;
}
.empty-category p {
font-family: "Space Mono", monospace;
font-size: 12px;
text-transform: uppercase;
}
@media (max-width: 600px) {
.skills-container {
padding: 10px;
}
.header-main {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.skill-points-badge {
width: 100%;
justify-content: space-between;
box-sizing: border-box;
}
.skills-grid {
grid-template-columns: 1fr;
}
}

View File

@ -4,7 +4,7 @@
"displayName": "dungeons.original.Kaleidoscope", "displayName": "dungeons.original.Kaleidoscope",
"description": "dungeons.original.Kaleidoscope.desc", "description": "dungeons.original.Kaleidoscope.desc",
"meta": { "meta": {
"energyCost": 10, "energyCost": 0,
"repeatable": true, "repeatable": true,
"raid": false "raid": false
}, },
@ -23,4 +23,5 @@
} }
] ]
} }
} }

View File

@ -1,5 +1,5 @@
{ {
"room": { "rooms": {
"id": "original:themed/broken_reactor", "id": "original:themed/broken_reactor",
"displayName": "rooms.original.themed.broken_reactor", "displayName": "rooms.original.themed.broken_reactor",
"description": "rooms.original.themed.broken_reactor.desc", "description": "rooms.original.themed.broken_reactor.desc",
@ -41,4 +41,5 @@
"isBossRoom": true "isBossRoom": true
} }
} }
} }

View File

@ -1,5 +1,5 @@
{ {
"room": { "rooms": {
"id": "original:themed/cold", "id": "original:themed/cold",
"displayName": "rooms.original.themed.cold", "displayName": "rooms.original.themed.cold",
"description": "rooms.original.themed.cold.desc", "description": "rooms.original.themed.cold.desc",
@ -14,4 +14,5 @@
"isBossRoom": false "isBossRoom": false
} }
} }
} }

View File

@ -1,5 +1,5 @@
{ {
"room": { "rooms": {
"id": "original:themed/heat_anomaly", "id": "original:themed/heat_anomaly",
"displayName": "rooms.original.themed.heat_anomaly", "displayName": "rooms.original.themed.heat_anomaly",
"description": "rooms.original.themed.heat_anomaly.desc", "description": "rooms.original.themed.heat_anomaly.desc",
@ -15,4 +15,5 @@
"isBossRoom": false "isBossRoom": false
} }
} }
} }

View File

@ -1,5 +1,5 @@
{ {
"room": { "rooms": {
"id": "original:themed/the_rat_one", "id": "original:themed/the_rat_one",
"displayName": "rooms.original.themed.the_rat_one", "displayName": "rooms.original.themed.the_rat_one",
"description": "rooms.original.themed.the_rat_one.desc", "description": "rooms.original.themed.the_rat_one.desc",
@ -24,4 +24,5 @@
"isBossRoom": false "isBossRoom": false
} }
} }
} }

View File

@ -4,7 +4,7 @@
"displayName": "skills.category.original.combat.engine_effiency", "displayName": "skills.category.original.combat.engine_effiency",
"description": "skills.category.original.combat.engine_effiency.desc", "description": "skills.category.original.combat.engine_effiency.desc",
"meta": { "meta": {
"category": "combat", "category": "original:combat",
"topLevel": 10, "topLevel": 10,
"math": { "math": {
"start": 500, "start": 500,

View File

@ -4,7 +4,7 @@
"displayName": "skills.category.original.combat.shield_effiency", "displayName": "skills.category.original.combat.shield_effiency",
"description": "skills.category.original.combat.shield_effiency.desc", "description": "skills.category.original.combat.shield_effiency.desc",
"meta": { "meta": {
"category": "combat", "category": "original:combat",
"topLevel": 10, "topLevel": 10,
"math": { "math": {
"start": 500, "start": 500,

View File

@ -4,7 +4,7 @@
"displayName": "skills.category.original.combat.thruster_effiency", "displayName": "skills.category.original.combat.thruster_effiency",
"description": "skills.category.original.combat.thruster_effiency.desc", "description": "skills.category.original.combat.thruster_effiency.desc",
"meta": { "meta": {
"category": "combat", "category": "original:combat",
"topLevel": 10, "topLevel": 10,
"math": { "math": {
"start": 500, "start": 500,

View File

@ -4,7 +4,7 @@
"displayName": "skills.category.original.combat.weapon_effiency", "displayName": "skills.category.original.combat.weapon_effiency",
"description": "skills.category.original.combat.weapon_effiency.desc", "description": "skills.category.original.combat.weapon_effiency.desc",
"meta": { "meta": {
"category": "combat", "category": "original:combat",
"topLevel": 10, "topLevel": 10,
"math": { "math": {
"start": 500, "start": 500,

View File

@ -4,78 +4,69 @@ class CombatService {
initializeBattle(player, hostiles) { initializeBattle(player, hostiles) {
const equipmentStats = this.calculateEquipmentStats(player.equipment); const equipmentStats = this.calculateEquipmentStats(player.equipment);
const maxHp = 100 + (equipmentStats.health || 0); const playerMaxHp = 100 + (equipmentStats.health || 0);
const atk = 25 + (equipmentStats.attack || 0); const playerAtk = 25 + (equipmentStats.attack || 0);
const playerDef = equipmentStats.defence || 0;
const playerRes = equipmentStats.resistance || 0;
const battle = { const battle = {
player: { player: {
id: player.id, id: player.id,
name: player.username || "Commander", name: player.username || "Commander",
hp: maxHp, hp: playerMaxHp,
maxHp: maxHp, maxHp: playerMaxHp,
atk: atk, atk: playerAtk,
def: playerDef,
res: playerRes,
stats: equipmentStats, stats: equipmentStats,
}, },
enemies: hostiles.map((h, index) => ({ enemies: hostiles.map((h, index) => {
...h, const hHp = h.stats?.health || 50;
instanceId: `mob_${index}`, return {
id: h.id, ...h,
name: h.displayName || h.name || `Hostile ${index + 1}`, instanceId: `mob_${index}`,
hp: h.stats?.health || 50, id: h.id,
maxHp: h.stats?.health || 50, name: h.displayName || h.name || `Hostile ${index + 1}`,
atk: h.stats?.attack || 10, hp: hHp,
isDead: false, maxHp: hHp,
})), atk: h.stats?.attack || 10,
turnOrder: [], def: h.stats?.defence || 0,
res: h.stats?.resistance || 0,
isDead: false,
rewardGiven: false,
gainXp: h.gainXp || 0,
credits: h.credits || 0,
loot: h.loot || [],
};
}),
turnOrder: ["player", ...hostiles.map((_, i) => `mob_${i}`)],
currentTurnIndex: 0, currentTurnIndex: 0,
turnStartTime: Date.now(), turnStartTime: Date.now(),
isOver: false, isOver: false,
}; };
battle.turnOrder = ["player", ...battle.enemies.map((e) => e.instanceId)];
return battle; return battle;
} }
calculateEquipmentStats(equipment) { calculateEquipmentStats(equipment) {
const totals = { const totals = { health: 0, attack: 0, defence: 0, resistance: 0 };
health: 0,
attack: 0,
defence: 0,
resistance: 0,
};
if (!equipment) return totals; if (!equipment) return totals;
Object.values(equipment).forEach((itemId) => { Object.values(equipment).forEach((itemId) => {
if (!itemId) return; if (!itemId) return;
const itemData = DatapackLoader.getItem(itemId); const itemData = DatapackLoader.getItem(itemId);
if (itemData && itemData.stats) { if (itemData && itemData.stats) {
Object.entries(itemData.stats).forEach(([key, value]) => { Object.entries(itemData.stats).forEach(([key, value]) => {
const lowerKey = key.toLowerCase(); const lowerKey = key.toLowerCase();
if (lowerKey.includes("health") || lowerKey === "hp")
if (lowerKey.includes("health") || lowerKey === "hp") {
totals.health += value; totals.health += value;
} else if ( else if (lowerKey.includes("attack") || lowerKey.includes("atk"))
lowerKey.includes("attack") ||
lowerKey.includes("damage") ||
lowerKey.includes("atk")
) {
totals.attack += value; totals.attack += value;
} else if ( else if (lowerKey.includes("def")) totals.defence += value;
lowerKey.includes("defence") || else if (lowerKey.includes("res")) totals.resistance += value;
lowerKey.includes("armor") ||
lowerKey.includes("defense")
) {
totals.defence += value;
} else if (lowerKey.includes("resistance")) {
totals.resistance += value;
}
}); });
} }
}); });
return totals; return totals;
} }
@ -88,38 +79,37 @@ class CombatService {
(e) => e.instanceId === targetInstanceId, (e) => e.instanceId === targetInstanceId,
); );
if (target && !target.isDead) { if (target && !target.isDead) {
const damage = battle.player.atk; const dmg = Math.max(
target.hp -= damage; 1,
Math.round(battle.player.atk - target.def * 0.5),
log.push(`Player dealt ${damage} damage to ${target.name}`); );
target.hp = Math.max(0, target.hp - dmg);
if (target.hp <= 0) { log.push(`Commander dealt ${dmg} damage to ${target.name}`);
target.hp = 0; if (target.hp === 0) {
target.isDead = true; target.isDead = true;
log.push(`${target.name} destroyed!`); log.push(`${target.name} neutralized.`);
} }
} }
} else { } else {
const enemy = battle.enemies.find((e) => e.instanceId === attackerId); const enemy = battle.enemies.find((e) => e.instanceId === attackerId);
if (enemy && !enemy.isDead) { if (enemy && !enemy.isDead && battle.player.hp > 0) {
const playerDef = battle.player.stats.defence || 0; const dmg = Math.max(
const finalDamage = Math.max(
1, 1,
Math.round(enemy.atk - playerDef * 0.5), Math.round(enemy.atk - battle.player.def * 0.5),
); );
battle.player.hp = Math.max(0, battle.player.hp - dmg);
battle.player.hp -= finalDamage; log.push(`${enemy.name} deals ${dmg} damage to Commander`);
log.push(`${enemy.name} deals ${finalDamage} damage to Player`); if (battle.player.hp === 0) battle.isOver = true;
if (battle.player.hp <= 0) {
battle.player.hp = 0;
battle.isOver = true;
}
} }
} }
return log; return log;
} }
handleSkip(battle) {
return [
`Sequence timeout: ${battle.turnOrder[battle.currentTurnIndex]} skipped turn.`,
];
}
} }
module.exports = new CombatService(); module.exports = new CombatService();

View File

@ -71,7 +71,7 @@ class DatapackLoader {
}); });
console.log( console.log(
`🚀 Registry Ready: ${this.registry.items.size} Items, ${this.registry.dungeons.size} Dungeons, ${this.registry.quests.size} Quests, ${this.registry.languages.size} Langs, ${manifestCount} Manifests`, `🚀 Registry Ready: ${this.registry.items.size} Items, ${this.registry.dungeons.size} Dungeons, ${this.registry.quests.size} Quests, ${this.registry.languages.size} Langs, ${manifestCount} Manifests ${this.registry.rooms.size} Rooms`,
); );
} }

View File

@ -1,5 +1,5 @@
const DatapackLoader = require("./DatapackLoader"); const DatapackLoaderRef = require("./DatapackLoader");
const CombatService = require("./CombatService"); const CombatServiceRef = require("./CombatService");
const QuestsManager = require("./QuestsManager"); const QuestsManager = require("./QuestsManager");
const { Player } = require("../models"); const { Player } = require("../models");
@ -9,12 +9,10 @@ class DungeonManager {
} }
async startDungeon(playerId, dungeonId) { async startDungeon(playerId, dungeonId) {
const dungeon = DatapackLoader.getDungeon(dungeonId); const dungeon = DatapackLoaderRef.getDungeon(dungeonId);
if (!dungeon || !dungeon.rooms?.length) return null; if (!dungeon || !dungeon.rooms?.length) return null;
const player = await Player.findByPk(playerId); const player = await Player.findByPk(playerId);
if (!player) return null;
const session = { const session = {
playerId, playerId,
dungeonId, dungeonId,
@ -31,12 +29,11 @@ class DungeonManager {
async initRoom(playerId, playerInstance = null) { async initRoom(playerId, playerInstance = null) {
const session = this.activeSessions.get(playerId); const session = this.activeSessions.get(playerId);
if (!session) return null; if (!session) return null;
const roomData = this.getCurrentRoomData(playerId); const roomData = this.getCurrentRoomData(playerId);
const player = playerInstance || (await Player.findByPk(playerId)); const player = playerInstance || (await Player.findByPk(playerId));
if (roomData.hostiles.length > 0) { if (roomData.hostiles.length > 0) {
session.battle = CombatService.initializeBattle( session.battle = CombatServiceRef.initializeBattle(
player, player,
roomData.hostiles, roomData.hostiles,
); );
@ -48,14 +45,11 @@ class DungeonManager {
getCurrentRoomData(playerId) { getCurrentRoomData(playerId) {
const session = this.activeSessions.get(playerId); const session = this.activeSessions.get(playerId);
if (!session) return null; const dungeon = DatapackLoaderRef.getDungeon(session.dungeonId);
const dungeon = DatapackLoader.getDungeon(session.dungeonId);
const roomRef = dungeon.rooms[session.currentRoomIndex]; const roomRef = dungeon.rooms[session.currentRoomIndex];
const rawRoom = DatapackLoader.getRoom(roomRef.id); const rawRoom = DatapackLoaderRef.getRoom(roomRef.id);
const hostiles = (rawRoom.hostiles || []) const hostiles = (rawRoom.hostiles || [])
.map((hId) => DatapackLoader.getEnemy(hId)) .map((hId) => DatapackLoaderRef.getEnemy(hId))
.filter(Boolean); .filter(Boolean);
return { return {
@ -71,134 +65,155 @@ class DungeonManager {
if (!session || !session.battle || session.battle.isOver) return null; if (!session || !session.battle || session.battle.isOver) return null;
const battle = session.battle; const battle = session.battle;
const log = CombatService.handleAttack(battle, targetInstanceId); if (battle.turnOrder[battle.currentTurnIndex] !== "player") return null;
const allEnemiesDead = battle.enemies.every((e) => e.isDead); let logMessages;
const playerDead = battle.player.hp <= 0; if (!targetInstanceId) {
logMessages = CombatServiceRef.handleSkip(battle);
if (playerDead) { } else {
battle.isOver = true; logMessages = CombatServiceRef.handleAttack(battle, targetInstanceId);
battle.player.hp = 0;
return {
battle,
log: [...log, "CRITICAL_FAILURE: Mission terminated."],
status: "defeat",
};
} }
if (allEnemiesDead) { const playerAction = {
battle.isOver = true; attackerId: "player",
messages: logMessages,
hpState: this._captureHpState(battle),
};
const roomData = this.getCurrentRoomData(playerId); return this._afterAction(session, [playerAction], socket);
const roomConfig = roomData.config; }
session.rewards.xp += roomConfig.gainXp || 0; _captureHpState(battle) {
session.rewards.credits += roomConfig.credits || 0; return {
playerHp: battle.player.hp,
enemies: battle.enemies.map((e) => ({
id: e.instanceId,
hp: e.hp,
isDead: e.isDead,
})),
};
}
if (roomConfig.loot && Array.isArray(roomConfig.loot)) { _afterAction(session, actionLogs, socket) {
roomConfig.loot.forEach((l) => { const battle = session.battle;
if (Math.random() <= (l.chance || 1)) {
let finalCount = 1;
if (typeof l.count === "object") {
finalCount =
Math.floor(Math.random() * (l.count.max - l.count.min + 1)) +
l.count.min;
} else if (typeof l.count === "number") {
finalCount = l.count;
}
const existingItem = session.rewards.items.find( battle.enemies.forEach((enemy) => {
(i) => i.id === l.id, if (enemy.isDead && !enemy.rewardGiven) {
); enemy.rewardGiven = true;
if (existingItem) {
existingItem.count += finalCount;
} else {
session.rewards.items.push({ id: l.id, count: finalCount });
}
}
});
}
battle.enemies.forEach((enemy) => {
session.rewards.xp += enemy.gainXp || 0; session.rewards.xp += enemy.gainXp || 0;
session.rewards.credits += enemy.credits || 0; session.rewards.credits += enemy.credits || 0;
if (enemy.loot && Array.isArray(enemy.loot)) {
const lootMessages = this._distributeLoot(session, enemy.loot);
actionLogs.push(...lootMessages);
}
QuestsManager.trackProgress( QuestsManager.trackProgress(
playerId, session.playerId,
"KILL_ENEMY", "KILL_ENEMY",
enemy.id, enemy.id,
1, 1,
socket, socket,
); );
}
});
if (enemy.loot && Array.isArray(enemy.loot)) { const allEnemiesDead = battle.enemies.every((e) => e.isDead);
enemy.loot.forEach((l) => {
if (Math.random() <= (l.chance || 1)) {
let finalCount = 1;
if (typeof l.count === "object") {
finalCount =
Math.floor(Math.random() * (l.count.max - l.count.min + 1)) +
l.count.min;
} else if (typeof l.count === "number") {
finalCount = l.count;
}
const existingItem = session.rewards.items.find( if (allEnemiesDead) {
(i) => i.id === l.id, battle.isOver = true;
); const roomConfig = this.getCurrentRoomData(session.playerId).config;
if (existingItem) { if (roomConfig && !roomConfig.rewardGiven) {
existingItem.count += finalCount; roomConfig.rewardGiven = true;
} else { session.rewards.xp += roomConfig.gainXp || 0;
session.rewards.items.push({ id: l.id, count: finalCount }); session.rewards.credits += roomConfig.credits || 0;
} if (roomConfig.loot) this._distributeLoot(session, roomConfig.loot);
} }
}); return { battle, log: actionLogs, status: "victory" };
}
});
return { battle, log, status: "victory" };
} }
return this._nextTurn(session, log); if (battle.player.hp <= 0) {
battle.isOver = true;
return { battle, log: actionLogs, status: "defeat" };
}
return this._nextTurn(session, actionLogs);
} }
_nextTurn(session, lastLog = []) { _distributeLoot(session, lootTable) {
const rewardsLog = [];
lootTable.forEach((item) => {
if (Math.random() <= (item.chance || 1)) {
let finalCount = 0;
if (typeof item.count === "object" && item.count !== null) {
const min = item.count.min || 0;
const max = item.count.max || 1;
finalCount = Math.floor(Math.random() * (max - min + 1)) + min;
} else {
finalCount = Number(item.count) || 1;
}
if (finalCount > 0) {
const existing = session.rewards.items.find((i) => i.id === item.id);
if (existing) {
existing.count += finalCount;
} else {
session.rewards.items.push({ id: item.id, count: finalCount });
}
const shortName = item.id.split(":").pop().replace("_", " ");
rewardsLog.push(
`RECOVERED: ${finalCount}x ${shortName.toUpperCase()}`,
);
}
}
});
return rewardsLog;
}
_nextTurn(session, accumulatedLogs = []) {
const battle = session.battle; const battle = session.battle;
battle.currentTurnIndex = battle.currentTurnIndex =
(battle.currentTurnIndex + 1) % battle.turnOrder.length; (battle.currentTurnIndex + 1) % battle.turnOrder.length;
battle.turnStartTime = Date.now();
const currentEntityId = battle.turnOrder[battle.currentTurnIndex]; const currentId = battle.turnOrder[battle.currentTurnIndex];
if (currentEntityId !== "player") { if (currentId === "player") return { battle, log: accumulatedLogs };
const enemy = battle.enemies.find(
(e) => e.instanceId === currentEntityId,
);
if (!enemy || enemy.isDead) return this._nextTurn(session, lastLog);
const enemyLog = CombatService.handleAttack(battle, null); const enemy = battle.enemies.find((e) => e.instanceId === currentId);
if (!enemy || enemy.isDead) return this._nextTurn(session, accumulatedLogs);
if (battle.player.hp <= 0) { const enemyMessages = CombatServiceRef.handleAttack(battle, null);
battle.isOver = true; accumulatedLogs.push({
return { battle, log: [...lastLog, ...enemyLog], status: "defeat" }; attackerId: currentId,
} messages: enemyMessages,
hpState: this._captureHpState(battle),
});
return this._nextTurn(session, [...lastLog, ...enemyLog]); if (battle.player.hp <= 0 || battle.enemies.every((e) => e.isDead)) {
return this._afterAction(session, accumulatedLogs);
} }
return { battle, log: lastLog }; return this._nextTurn(session, accumulatedLogs);
} }
async moveToNextRoom(playerId) { async moveToNextRoom(playerId) {
const session = this.activeSessions.get(playerId); const session = this.activeSessions.get(playerId);
if (!session || session.isFinished) return null; if (!session) return { error: "Session not found" };
const dungeon = DatapackLoader.getDungeon(session.dungeonId); if (session.isFinished) {
if (session.currentRoomIndex < dungeon.rooms.length - 1) { return { status: "completed", rewards: session.rewards };
session.currentRoomIndex++;
return this.initRoom(playerId);
} }
session.isFinished = true; const dungeon = DatapackLoaderRef.getDungeon(session.dungeonId);
return { status: "completed", rewards: session.rewards };
if (session.currentRoomIndex < dungeon.rooms.length - 1) {
session.currentRoomIndex++;
const newRoomData = await this.initRoom(playerId);
return { status: "next_room", ...newRoomData };
} else {
session.isFinished = true;
return { status: "completed", rewards: session.rewards };
}
} }
leaveDungeon(playerId) { leaveDungeon(playerId) {

View File

@ -29,6 +29,7 @@ module.exports = (io, socket) => {
remainingEnergy: player.energy - energyCost, remainingEnergy: player.energy - energyCost,
}); });
} catch (err) { } catch (err) {
console.log(err);
socket.emit("error", { message: "Deployment failure" }); socket.emit("error", { message: "Deployment failure" });
} }
}); });