From 2892a579498eef963ea3a328bb712ed157ef374c Mon Sep 17 00:00:00 2001 From: MaksSlyzar Date: Sat, 25 Apr 2026 14:52:17 +0300 Subject: [PATCH] Updated Skills Tab. Added Turn to Dungeon Manager. Fixed DungeonScreen. --- client/src/services/GameDataManager.js | 16 +- .../components/DungeonScreen.css | 19 ++ .../components/DungeonScreen.jsx | 264 ++++++++---------- .../views/GameInterface/tabs/SkillsTab.jsx | 237 +++++++--------- .../tabs/components/SkillsCard.css | 175 ++++++++++++ .../tabs/components/SkillsCard.jsx | 76 +++++ .../GameInterface/tabs/styles/SkillsTab.css | 147 ++++++++-- .../data/dungeons/themed/kaleidoscope.json | 5 +- .../enemies/rooms/themed/broken_reactor.json | 5 +- .../data/enemies/rooms/themed/cold.json | 5 +- .../enemies/rooms/themed/heat_anomaly.json | 5 +- .../enemies/rooms/themed/the_rat_one.json | 5 +- .../data/skills/combat/engine_effiency.json | 2 +- .../data/skills/combat/shield_effiency.json | 2 +- .../data/skills/combat/thruster_speed.json | 2 +- .../data/skills/combat/weapon_effiency.json | 2 +- game-server/src/game/CombatService.js | 118 ++++---- game-server/src/game/DatapackLoader.js | 2 +- game-server/src/game/DungeonManager.js | 221 ++++++++------- .../src/sockets/handlers/dungeonHandler.js | 1 + 20 files changed, 815 insertions(+), 494 deletions(-) create mode 100644 client/src/views/GameInterface/tabs/components/SkillsCard.css create mode 100644 client/src/views/GameInterface/tabs/components/SkillsCard.jsx diff --git a/client/src/services/GameDataManager.js b/client/src/services/GameDataManager.js index a6fdcc7..76ebee5 100644 --- a/client/src/services/GameDataManager.js +++ b/client/src/services/GameDataManager.js @@ -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]; diff --git a/client/src/views/GameInterface/components/DungeonScreen.css b/client/src/views/GameInterface/components/DungeonScreen.css index e605352..c8d07a0 100644 --- a/client/src/views/GameInterface/components/DungeonScreen.css +++ b/client/src/views/GameInterface/components/DungeonScreen.css @@ -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; +} diff --git a/client/src/views/GameInterface/components/DungeonScreen.jsx b/client/src/views/GameInterface/components/DungeonScreen.jsx index 3481c63..4656567 100644 --- a/client/src/views/GameInterface/components/DungeonScreen.jsx +++ b/client/src/views/GameInterface/components/DungeonScreen.jsx @@ -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 (
@@ -139,40 +143,35 @@ const DungeonScreen = ({ session, socket }) => { /> )} - {/* Header section remains the same */}
- SECTOR {roomIndex + 1} / {session.totalRooms} + SECTOR {roomIndex + 1} / {totalRooms}
- {battle && ( + {battle && !battle.isOver && (
- {isPlayerTurn ? "YOUR TURN" : "ENEMY ACTION"} + {isPlayerTurn ? "YOUR TURN" : "PROCESSING..."}
+ />
)} @@ -184,29 +183,18 @@ const DungeonScreen = ({ session, socket }) => { {battle.enemies.map((mob) => (
- !mob.isDead && isPlayerTurn && + !mob.isDead && setSelectedTarget(mob.instanceId) } > - {selectedTarget === mob.instanceId && ( -
- -
- )} -
+ />
{ >
{GameDataManager.t(mob.name)} - {!mob.isDead && ATK: {mob.atk}}
))}
@@ -229,7 +216,7 @@ const DungeonScreen = ({ session, socket }) => {
{battle && (
COMMANDER_INTEGRITY @@ -243,40 +230,33 @@ const DungeonScreen = ({ session, socket }) => { style={{ width: `${(battle.player.hp / battle.player.maxHp) * 100}%`, }} - >
+ />
)} -
-
-
- {log.map((entry, i) => ( -
- > {entry} -
- ))} -
-
+
+ {log.map((entry, i) => ( +
+ > {entry} +
+ ))} +
- {battle && !battle.isOver && ( )}
- {((battle?.isOver && battle.player.hp > 0) || !battle) && ( + {((battle?.isOver && battle.player.hp > 0) || !battle) && !summary && ( diff --git a/client/src/views/GameInterface/tabs/SkillsTab.jsx b/client/src/views/GameInterface/tabs/SkillsTab.jsx index 94958ac..3a197a5 100644 --- a/client/src/views/GameInterface/tabs/SkillsTab.jsx +++ b/client/src/views/GameInterface/tabs/SkillsTab.jsx @@ -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 ( -
-
+
+
-

Skills

-
- Skill Points: 0 +
+

+ Neural Core +

+
+ Uplink Points: + {skillPoints} +
-
- {categories.map(cat => ( - - ))} -
-
- {filteredSkills.length > 0 ? ( - filteredSkills.map(skill => ( - console.log('Unlock', cat, id)} - onUpgrade={(cat, id) => console.log('Upgrade', cat, id)} - /> - )) + + + +
+ {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 ( + = cost} + onUpgrade={() => handleUpgrade(skill.id)} + /> + ); + }) ) : ( -

No skills discovered in this category yet.

+
+ +

No active modules found in this sector.

+
)}
-
-
- ); -}; - -const SkillComponent = ({ - skill, - activeCategory, - skillId, - onUnlock, - onUpgrade, -}) => { - const { - name, - currentLevel, - maxLevel, - description, - iconClass, - experience, - experienceToNext, - unlocked, - requiredLevel - } = skill; - - const progressPercent = (experience / experienceToNext) * 100; - - return ( -
-
-
- -
-
-
{name}
-
Lv. {currentLevel}/{maxLevel}
-
-
- -
{description}
- - {unlocked && currentLevel < maxLevel && ( -
-
-
-
- {experience}/{experienceToNext} XP -
- )} - - {currentLevel >= maxLevel && ( -
- MAX LEVEL REACHED -
- )} - -
- {!unlocked ? ( - - ) : currentLevel < maxLevel ? ( - - ) : ( - Mastered - )} -
- - {!skill.unlocked ? -
Requires Level {skill.requiredLevel}
- : ''} - +
); }; export default SkillsTab; - diff --git a/client/src/views/GameInterface/tabs/components/SkillsCard.css b/client/src/views/GameInterface/tabs/components/SkillsCard.css new file mode 100644 index 0000000..7ef375c --- /dev/null +++ b/client/src/views/GameInterface/tabs/components/SkillsCard.css @@ -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; +} diff --git a/client/src/views/GameInterface/tabs/components/SkillsCard.jsx b/client/src/views/GameInterface/tabs/components/SkillsCard.jsx new file mode 100644 index 0000000..54fdbf1 --- /dev/null +++ b/client/src/views/GameInterface/tabs/components/SkillsCard.jsx @@ -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 ( +
+
+
+ {/* Іконка може бути в meta або за дефолтом */} + +
+
+
{skill.displayName}
+
+ {isMaxLevel ? "MAXED" : `RANK ${level}/${maxLevel}`} +
+
+
+ +

{skill.description}

+ + {!isMaxLevel && !isLocked && ( +
+
+ Neural Sync + + {Math.floor(experience)} / {expToNext} + +
+
+
+
+
+ )} + +
+ {isLocked ? ( + + ) : isMaxLevel ? ( +
MODULE FULLY OPTIMIZED
+ ) : ( + + )} +
+
+ ); +}; diff --git a/client/src/views/GameInterface/tabs/styles/SkillsTab.css b/client/src/views/GameInterface/tabs/styles/SkillsTab.css index a6c22a2..4f199b2 100644 --- a/client/src/views/GameInterface/tabs/styles/SkillsTab.css +++ b/client/src/views/GameInterface/tabs/styles/SkillsTab.css @@ -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; + } +} diff --git a/game-server/datapacks/original/data/dungeons/themed/kaleidoscope.json b/game-server/datapacks/original/data/dungeons/themed/kaleidoscope.json index cf3abb3..5be6251 100644 --- a/game-server/datapacks/original/data/dungeons/themed/kaleidoscope.json +++ b/game-server/datapacks/original/data/dungeons/themed/kaleidoscope.json @@ -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 @@ } ] } -} \ No newline at end of file +} + diff --git a/game-server/datapacks/original/data/enemies/rooms/themed/broken_reactor.json b/game-server/datapacks/original/data/enemies/rooms/themed/broken_reactor.json index 69cf677..bf576d2 100644 --- a/game-server/datapacks/original/data/enemies/rooms/themed/broken_reactor.json +++ b/game-server/datapacks/original/data/enemies/rooms/themed/broken_reactor.json @@ -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 } } -} \ No newline at end of file +} + diff --git a/game-server/datapacks/original/data/enemies/rooms/themed/cold.json b/game-server/datapacks/original/data/enemies/rooms/themed/cold.json index 5106a7e..abd14aa 100644 --- a/game-server/datapacks/original/data/enemies/rooms/themed/cold.json +++ b/game-server/datapacks/original/data/enemies/rooms/themed/cold.json @@ -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 } } -} \ No newline at end of file +} + diff --git a/game-server/datapacks/original/data/enemies/rooms/themed/heat_anomaly.json b/game-server/datapacks/original/data/enemies/rooms/themed/heat_anomaly.json index 1392fa7..6178172 100644 --- a/game-server/datapacks/original/data/enemies/rooms/themed/heat_anomaly.json +++ b/game-server/datapacks/original/data/enemies/rooms/themed/heat_anomaly.json @@ -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 } } -} \ No newline at end of file +} + diff --git a/game-server/datapacks/original/data/enemies/rooms/themed/the_rat_one.json b/game-server/datapacks/original/data/enemies/rooms/themed/the_rat_one.json index 7f759f2..aa5ae94 100644 --- a/game-server/datapacks/original/data/enemies/rooms/themed/the_rat_one.json +++ b/game-server/datapacks/original/data/enemies/rooms/themed/the_rat_one.json @@ -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 } } -} \ No newline at end of file +} + diff --git a/game-server/datapacks/original/data/skills/combat/engine_effiency.json b/game-server/datapacks/original/data/skills/combat/engine_effiency.json index 228c288..d036020 100644 --- a/game-server/datapacks/original/data/skills/combat/engine_effiency.json +++ b/game-server/datapacks/original/data/skills/combat/engine_effiency.json @@ -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, diff --git a/game-server/datapacks/original/data/skills/combat/shield_effiency.json b/game-server/datapacks/original/data/skills/combat/shield_effiency.json index dae5e05..1627747 100644 --- a/game-server/datapacks/original/data/skills/combat/shield_effiency.json +++ b/game-server/datapacks/original/data/skills/combat/shield_effiency.json @@ -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, diff --git a/game-server/datapacks/original/data/skills/combat/thruster_speed.json b/game-server/datapacks/original/data/skills/combat/thruster_speed.json index 99a780f..9ae9427 100644 --- a/game-server/datapacks/original/data/skills/combat/thruster_speed.json +++ b/game-server/datapacks/original/data/skills/combat/thruster_speed.json @@ -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, diff --git a/game-server/datapacks/original/data/skills/combat/weapon_effiency.json b/game-server/datapacks/original/data/skills/combat/weapon_effiency.json index 7119d21..e9c985f 100644 --- a/game-server/datapacks/original/data/skills/combat/weapon_effiency.json +++ b/game-server/datapacks/original/data/skills/combat/weapon_effiency.json @@ -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, diff --git a/game-server/src/game/CombatService.js b/game-server/src/game/CombatService.js index 3ba99d9..5b37840 100644 --- a/game-server/src/game/CombatService.js +++ b/game-server/src/game/CombatService.js @@ -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(); diff --git a/game-server/src/game/DatapackLoader.js b/game-server/src/game/DatapackLoader.js index 2a21a8b..ffb1eec 100644 --- a/game-server/src/game/DatapackLoader.js +++ b/game-server/src/game/DatapackLoader.js @@ -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`, ); } diff --git a/game-server/src/game/DungeonManager.js b/game-server/src/game/DungeonManager.js index a92260d..e9e6226 100644 --- a/game-server/src/game/DungeonManager.js +++ b/game-server/src/game/DungeonManager.js @@ -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) { diff --git a/game-server/src/sockets/handlers/dungeonHandler.js b/game-server/src/sockets/handlers/dungeonHandler.js index 7cd3714..6126d75 100644 --- a/game-server/src/sockets/handlers/dungeonHandler.js +++ b/game-server/src/sockets/handlers/dungeonHandler.js @@ -29,6 +29,7 @@ module.exports = (io, socket) => { remainingEnergy: player.energy - energyCost, }); } catch (err) { + console.log(err); socket.emit("error", { message: "Deployment failure" }); } });