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));
}
console.log(this.quests);
console.log(this.skills);
if (data.languages) {
this.translations = data.languages;
}
if (data.manifest) {
this.manifest = data.manifest;
}
console.log(this.manifest);
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) {
if (!key) return "";
const langData = this.translations[this.currentLang];

View File

@ -328,3 +328,22 @@
margin-top: 4px;
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";
const DungeonScreen = ({ session, socket }) => {
const [roomData, setRoomData] = useState(session.room);
const [roomIndex, setRoomIndex] = useState(session.roomIndex);
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 [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([], {
@ -114,21 +28,111 @@ const DungeonScreen = ({ session, socket }) => {
setLog((prev) => [...prev, `[${time}] ${text}`]);
};
const handleCombatAction = () => {
const targetId = selectedTarget;
if (!battle || battle.isOver || activeAttacker || !targetId) return;
if (battle.turnOrder[battle.currentTurnIndex] !== "player") return;
useEffect(() => {
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [log]);
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 });
addLog(`Initiating strike sequence...`);
setSelectedTarget(null); // Скидаємо вибір після атаки
if (!targetId) addLog("Sequence timeout! Skipping...");
else addLog("Initiating strike sequence...");
setSelectedTarget(null);
};
const handleNextRoom = () => {
socket.emit("dungeon:next_room");
};
const isPlayerTurn = battle?.turnOrder[battle?.currentTurnIndex] === "player";
const isPlayerTurn =
battle?.turnOrder[battle?.currentTurnIndex] === "player" &&
!activeAttacker &&
!battle.isOver;
return (
<div className="dungeon-active-screen">
@ -139,40 +143,35 @@ const DungeonScreen = ({ session, socket }) => {
/>
)}
{/* Header section remains the same */}
<div className="dungeon-header">
<div className="room-progress">
<div className="progress-text">
SECTOR {roomIndex + 1} / {session.totalRooms}
SECTOR {roomIndex + 1} / {totalRooms}
</div>
<div className="progress-bar">
<div
className="fill"
style={{
width: `${((roomIndex + 1) / session.totalRooms) * 100}%`,
}}
style={{ width: `${((roomIndex + 1) / totalRooms) * 100}%` }}
></div>
</div>
</div>
{battle && (
{battle && !battle.isOver && (
<div
className={`turn-progress-container ${isPlayerTurn ? "player-phase" : "enemy-phase"}`}
>
<div className="turn-label">
{isPlayerTurn ? "YOUR TURN" : "ENEMY ACTION"}
{isPlayerTurn ? "YOUR TURN" : "PROCESSING..."}
</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",
width: `${(timeLeft / 10) * 100}%`,
transition: timeLeft === 10 ? "none" : "width 1s linear",
visibility: isPlayerTurn ? "visible" : "hidden",
}}
></div>
/>
</div>
</div>
)}
@ -184,29 +183,18 @@ const DungeonScreen = ({ session, socket }) => {
{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" : ""}
`}
className={`enemy-card ${mob.isDead ? "defeated" : ""} ${selectedTarget === mob.instanceId ? "selected" : ""} ${isPlayerTurn && !mob.isDead ? "selectable" : ""} ${activeAttacker === mob.instanceId ? "attacking" : ""}`}
onClick={() =>
!mob.isDead &&
isPlayerTurn &&
!mob.isDead &&
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
@ -214,7 +202,6 @@ const DungeonScreen = ({ session, socket }) => {
></i>
</div>
<span className="mob-name">{GameDataManager.t(mob.name)}</span>
{!mob.isDead && <span className="mob-atk">ATK: {mob.atk}</span>}
</div>
))}
</div>
@ -229,7 +216,7 @@ const DungeonScreen = ({ session, socket }) => {
<div className="player-section">
{battle && (
<div
className={`player-hp-main ${activeAttacker ? "taking-damage" : ""}`}
className={`player-hp-main ${activeAttacker && activeAttacker !== "player" ? "taking-damage" : ""}`}
>
<div className="hp-header">
<span>COMMANDER_INTEGRITY</span>
@ -243,40 +230,33 @@ const DungeonScreen = ({ session, socket }) => {
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 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>
{battle && !battle.isOver && (
<button
className={`btn-execute-combat ${!selectedTarget || !isPlayerTurn ? "disabled" : ""}`}
disabled={!selectedTarget || !isPlayerTurn}
onClick={handleCombatAction}
onClick={() => handleCombatAction()}
>
<div className="btn-glitch-content">EXECUTE_STRIKE</div>
<div className="btn-sub-text">
{selectedTarget ? "TARGET_LOCKED" : "SELECT_TARGET"}
</div>
EXECUTE_STRIKE
</button>
)}
</div>
</div>
<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}>
PROCEED <i className="fas fa-chevron-right"></i>
</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 CategorySelector from "../components/CategorySelector";
import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
import { SkillCard } from "./components/SkillsCard.jsx";
const SkillsTab = () => {
const [category, setCategory] = useState('combat');
const categories = [
{ id: 'combat', label: 'Combat' },
{ id: 'science', label: 'Science' },
{ id: 'crafting', label: 'Crafting' }
];
const { socket } = useSocket();
const [skillsData] = useState(() => {
const catIds = categories.map(c => c.id);
return Array.from({ length: 5 }, (_, i) => ({
id: `skill-${i}`,
category: catIds[i % catIds.length],
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 [categories, setCategories] = useState([]);
const [activeCategory, setActiveCategory] = useState("");
const [skills, setSkills] = useState([]);
const [playerSkills, setPlayerSkills] = useState({});
const [skillPoints, setSkillPoints] = useState(0);
const handleUpgrade = (category, id) => {
console.log(`Upgrading ${id} in ${category}`);
useEffect(() => {
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 (
<div className="tab-content active">
<div className="skills-container">
<div
className="tab-content active"
style={{ height: "100%", display: "flex", flexDirection: "column" }}
>
<MeteorRegion className="skills-container">
<div className="skills-header">
<h2><i className="fas fa-graduation-cap"></i> Skills</h2>
<div className="skill-points-display">
<span className="skill-points">Skill Points: 0</span>
<div className="header-main">
<h2>
<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 className="skill-categories">
{categories.map(cat => (
<button
key={cat.id}
className={`skill-cat-btn ${category === cat.id ? 'active' : ''}`}
onClick={() => setCategory(cat.id)}
>
{cat.label}
</button>
))}
</div>
<div className="skills-grid">
{filteredSkills.length > 0 ? (
filteredSkills.map(skill => (
<SkillComponent
key={skill.id}
skill={skill}
activeCategory={category}
skillId={skill.id}
onUnlock={(cat, id) => console.log('Unlock', cat, id)}
onUpgrade={(cat, id) => console.log('Upgrade', cat, id)}
/>
))
<CategorySelector
categories={categories}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<div className="skills-grid custom-scroll">
{skills.length > 0 ? (
skills.map((skill) => {
const progress = playerSkills[skill.id] || {
level: 0,
experience: 0,
};
const maxLv = skill.meta?.topLevel || 10;
const cost = progress.level === 0 ? 2 : 1;
return (
<SkillCard
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>
);
};
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>
: ''}
</MeteorRegion>
</div>
);
};
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 {
max-width: 1200px;
margin: 0 auto;
max-height: calc(100vh - 232px); /* Adjusted for title bar and padding */
overflow-y: auto;
padding: 0.5rem;
display: flex;
flex-direction: column;
height: 100%;
padding: 20px;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
.skill-categories {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
justify-content: center;
.skills-header {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(0, 212, 255, 0.1);
}
.skill-cat-btn {
padding: 0.75rem 1.5rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.3s ease;
.header-main {
display: flex;
justify-content: space-between;
align-items: center;
}
.skill-cat-btn:hover {
border-color: var(--primary-color);
color: var(--text-primary);
.header-main h2 {
margin: 0;
font-size: 1.2rem;
color: #fff;
text-transform: uppercase;
letter-spacing: 2px;
display: flex;
align-items: center;
gap: 10px;
}
.skill-cat-btn.active {
background: var(--gradient-primary);
color: var(--bg-primary);
border-color: transparent;
.header-main h2 i {
color: #00d4ff;
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
.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 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
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",
"description": "dungeons.original.Kaleidoscope.desc",
"meta": {
"energyCost": 10,
"energyCost": 0,
"repeatable": true,
"raid": false
},
@ -23,4 +23,5 @@
}
]
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -71,7 +71,7 @@ class DatapackLoader {
});
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 CombatService = require("./CombatService");
const DatapackLoaderRef = require("./DatapackLoader");
const CombatServiceRef = require("./CombatService");
const QuestsManager = require("./QuestsManager");
const { Player } = require("../models");
@ -9,12 +9,10 @@ class DungeonManager {
}
async startDungeon(playerId, dungeonId) {
const dungeon = DatapackLoader.getDungeon(dungeonId);
const dungeon = DatapackLoaderRef.getDungeon(dungeonId);
if (!dungeon || !dungeon.rooms?.length) return null;
const player = await Player.findByPk(playerId);
if (!player) return null;
const session = {
playerId,
dungeonId,
@ -31,12 +29,11 @@ class DungeonManager {
async initRoom(playerId, playerInstance = null) {
const session = this.activeSessions.get(playerId);
if (!session) return null;
const roomData = this.getCurrentRoomData(playerId);
const player = playerInstance || (await Player.findByPk(playerId));
if (roomData.hostiles.length > 0) {
session.battle = CombatService.initializeBattle(
session.battle = CombatServiceRef.initializeBattle(
player,
roomData.hostiles,
);
@ -48,14 +45,11 @@ class DungeonManager {
getCurrentRoomData(playerId) {
const session = this.activeSessions.get(playerId);
if (!session) return null;
const dungeon = DatapackLoader.getDungeon(session.dungeonId);
const dungeon = DatapackLoaderRef.getDungeon(session.dungeonId);
const roomRef = dungeon.rooms[session.currentRoomIndex];
const rawRoom = DatapackLoader.getRoom(roomRef.id);
const rawRoom = DatapackLoaderRef.getRoom(roomRef.id);
const hostiles = (rawRoom.hostiles || [])
.map((hId) => DatapackLoader.getEnemy(hId))
.map((hId) => DatapackLoaderRef.getEnemy(hId))
.filter(Boolean);
return {
@ -71,134 +65,155 @@ class DungeonManager {
if (!session || !session.battle || session.battle.isOver) return null;
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);
const playerDead = battle.player.hp <= 0;
if (playerDead) {
battle.isOver = true;
battle.player.hp = 0;
return {
battle,
log: [...log, "CRITICAL_FAILURE: Mission terminated."],
status: "defeat",
};
let logMessages;
if (!targetInstanceId) {
logMessages = CombatServiceRef.handleSkip(battle);
} else {
logMessages = CombatServiceRef.handleAttack(battle, targetInstanceId);
}
if (allEnemiesDead) {
battle.isOver = true;
const playerAction = {
attackerId: "player",
messages: logMessages,
hpState: this._captureHpState(battle),
};
const roomData = this.getCurrentRoomData(playerId);
const roomConfig = roomData.config;
return this._afterAction(session, [playerAction], socket);
}
session.rewards.xp += roomConfig.gainXp || 0;
session.rewards.credits += roomConfig.credits || 0;
_captureHpState(battle) {
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)) {
roomConfig.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;
}
_afterAction(session, actionLogs, socket) {
const battle = session.battle;
const existingItem = session.rewards.items.find(
(i) => i.id === l.id,
);
if (existingItem) {
existingItem.count += finalCount;
} else {
session.rewards.items.push({ id: l.id, count: finalCount });
}
}
});
}
battle.enemies.forEach((enemy) => {
battle.enemies.forEach((enemy) => {
if (enemy.isDead && !enemy.rewardGiven) {
enemy.rewardGiven = true;
session.rewards.xp += enemy.gainXp || 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(
playerId,
session.playerId,
"KILL_ENEMY",
enemy.id,
1,
socket,
);
}
});
if (enemy.loot && Array.isArray(enemy.loot)) {
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 allEnemiesDead = battle.enemies.every((e) => e.isDead);
const existingItem = session.rewards.items.find(
(i) => i.id === l.id,
);
if (existingItem) {
existingItem.count += finalCount;
} else {
session.rewards.items.push({ id: l.id, count: finalCount });
}
}
});
}
});
return { battle, log, status: "victory" };
if (allEnemiesDead) {
battle.isOver = true;
const roomConfig = this.getCurrentRoomData(session.playerId).config;
if (roomConfig && !roomConfig.rewardGiven) {
roomConfig.rewardGiven = true;
session.rewards.xp += roomConfig.gainXp || 0;
session.rewards.credits += roomConfig.credits || 0;
if (roomConfig.loot) this._distributeLoot(session, roomConfig.loot);
}
return { battle, log: actionLogs, 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;
battle.currentTurnIndex =
(battle.currentTurnIndex + 1) % battle.turnOrder.length;
battle.turnStartTime = Date.now();
const currentEntityId = battle.turnOrder[battle.currentTurnIndex];
if (currentEntityId !== "player") {
const enemy = battle.enemies.find(
(e) => e.instanceId === currentEntityId,
);
if (!enemy || enemy.isDead) return this._nextTurn(session, lastLog);
const currentId = battle.turnOrder[battle.currentTurnIndex];
if (currentId === "player") return { battle, log: accumulatedLogs };
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) {
battle.isOver = true;
return { battle, log: [...lastLog, ...enemyLog], status: "defeat" };
}
const enemyMessages = CombatServiceRef.handleAttack(battle, null);
accumulatedLogs.push({
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) {
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.currentRoomIndex < dungeon.rooms.length - 1) {
session.currentRoomIndex++;
return this.initRoom(playerId);
if (session.isFinished) {
return { status: "completed", rewards: session.rewards };
}
session.isFinished = true;
return { status: "completed", rewards: session.rewards };
const dungeon = DatapackLoaderRef.getDungeon(session.dungeonId);
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) {

View File

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