Updated Skills Tab. Added Turn to Dungeon Manager. Fixed DungeonScreen.
This commit is contained in:
parent
4f0ad9eca6
commit
2892a57949
@ -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];
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,13 +230,11 @@ 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">
|
||||
@ -258,25 +243,20 @@ const DungeonScreen = ({ session, socket }) => {
|
||||
))}
|
||||
<div ref={logEndRef} />
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@ -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="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>
|
||||
</div>
|
||||
<div className="skill-categories">
|
||||
{categories.map(cat => (
|
||||
<button
|
||||
key={cat.id}
|
||||
className={`skill-cat-btn ${category === cat.id ? 'active' : ''}`}
|
||||
onClick={() => setCategory(cat.id)}
|
||||
<div
|
||||
className="tab-content active"
|
||||
style={{ height: "100%", display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
{cat.label}
|
||||
</button>
|
||||
))}
|
||||
<MeteorRegion className="skills-container">
|
||||
<div className="skills-header">
|
||||
<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 className="skills-grid">
|
||||
{filteredSkills.length > 0 ? (
|
||||
filteredSkills.map(skill => (
|
||||
<SkillComponent
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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}
|
||||
activeCategory={category}
|
||||
skillId={skill.id}
|
||||
onUnlock={(cat, id) => console.log('Unlock', cat, id)}
|
||||
onUpgrade={(cat, id) => console.log('Upgrade', cat, id)}
|
||||
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>
|
||||
</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 className="empty-category">
|
||||
<i className="fas fa-xs fa-terminal"></i>
|
||||
<p>No active modules found in this sector.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!skill.unlocked ?
|
||||
<div class="skill-requirement">Requires Level {skill.requiredLevel}</div>
|
||||
: ''}
|
||||
|
||||
</MeteorRegion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkillsTab;
|
||||
|
||||
|
||||
175
client/src/views/GameInterface/tabs/components/SkillsCard.css
Normal file
175
client/src/views/GameInterface/tabs/components/SkillsCard.css
Normal 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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
|
||||
.skill-categories {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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;
|
||||
.skills-header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.1);
|
||||
}
|
||||
|
||||
.skill-cat-btn:hover {
|
||||
border-color: var(--primary-color);
|
||||
color: var(--text-primary);
|
||||
.header-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.skill-cat-btn.active {
|
||||
background: var(--gradient-primary);
|
||||
color: var(--bg-primary);
|
||||
border-color: transparent;
|
||||
.header-main h2 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"displayName": "dungeons.original.Kaleidoscope",
|
||||
"description": "dungeons.original.Kaleidoscope.desc",
|
||||
"meta": {
|
||||
"energyCost": 10,
|
||||
"energyCost": 0,
|
||||
"repeatable": true,
|
||||
"raid": false
|
||||
},
|
||||
@ -24,3 +24,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"room": {
|
||||
"rooms": {
|
||||
"id": "original:themed/broken_reactor",
|
||||
"displayName": "rooms.original.themed.broken_reactor",
|
||||
"description": "rooms.original.themed.broken_reactor.desc",
|
||||
@ -42,3 +42,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"room": {
|
||||
"rooms": {
|
||||
"id": "original:themed/cold",
|
||||
"displayName": "rooms.original.themed.cold",
|
||||
"description": "rooms.original.themed.cold.desc",
|
||||
@ -15,3 +15,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"room": {
|
||||
"rooms": {
|
||||
"id": "original:themed/heat_anomaly",
|
||||
"displayName": "rooms.original.themed.heat_anomaly",
|
||||
"description": "rooms.original.themed.heat_anomaly.desc",
|
||||
@ -16,3 +16,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
@ -25,3 +25,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => ({
|
||||
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: h.stats?.health || 50,
|
||||
maxHp: h.stats?.health || 50,
|
||||
hp: hHp,
|
||||
maxHp: hHp,
|
||||
atk: h.stats?.attack || 10,
|
||||
def: h.stats?.defence || 0,
|
||||
res: h.stats?.resistance || 0,
|
||||
isDead: false,
|
||||
})),
|
||||
turnOrder: [],
|
||||
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();
|
||||
|
||||
@ -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`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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,135 +65,156 @@ 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;
|
||||
let logMessages;
|
||||
if (!targetInstanceId) {
|
||||
logMessages = CombatServiceRef.handleSkip(battle);
|
||||
} else {
|
||||
logMessages = CombatServiceRef.handleAttack(battle, targetInstanceId);
|
||||
}
|
||||
|
||||
if (playerDead) {
|
||||
battle.isOver = true;
|
||||
battle.player.hp = 0;
|
||||
const playerAction = {
|
||||
attackerId: "player",
|
||||
messages: logMessages,
|
||||
hpState: this._captureHpState(battle),
|
||||
};
|
||||
|
||||
return this._afterAction(session, [playerAction], socket);
|
||||
}
|
||||
|
||||
_captureHpState(battle) {
|
||||
return {
|
||||
battle,
|
||||
log: [...log, "CRITICAL_FAILURE: Mission terminated."],
|
||||
status: "defeat",
|
||||
playerHp: battle.player.hp,
|
||||
enemies: battle.enemies.map((e) => ({
|
||||
id: e.instanceId,
|
||||
hp: e.hp,
|
||||
isDead: e.isDead,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (allEnemiesDead) {
|
||||
battle.isOver = true;
|
||||
|
||||
const roomData = this.getCurrentRoomData(playerId);
|
||||
const roomConfig = roomData.config;
|
||||
|
||||
session.rewards.xp += roomConfig.gainXp || 0;
|
||||
session.rewards.credits += roomConfig.credits || 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
_afterAction(session, actionLogs, socket) {
|
||||
const battle = session.battle;
|
||||
|
||||
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 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" };
|
||||
const allEnemiesDead = battle.enemies.every((e) => e.isDead);
|
||||
|
||||
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 this._nextTurn(session, log);
|
||||
return { battle, log: actionLogs, status: "victory" };
|
||||
}
|
||||
|
||||
_nextTurn(session, lastLog = []) {
|
||||
const battle = session.battle;
|
||||
battle.currentTurnIndex =
|
||||
(battle.currentTurnIndex + 1) % battle.turnOrder.length;
|
||||
|
||||
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 enemyLog = CombatService.handleAttack(battle, null);
|
||||
|
||||
if (battle.player.hp <= 0) {
|
||||
battle.isOver = true;
|
||||
return { battle, log: [...lastLog, ...enemyLog], status: "defeat" };
|
||||
return { battle, log: actionLogs, status: "defeat" };
|
||||
}
|
||||
|
||||
return this._nextTurn(session, [...lastLog, ...enemyLog]);
|
||||
return this._nextTurn(session, actionLogs);
|
||||
}
|
||||
|
||||
return { battle, log: 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 currentId = battle.turnOrder[battle.currentTurnIndex];
|
||||
if (currentId === "player") return { battle, log: accumulatedLogs };
|
||||
|
||||
const enemy = battle.enemies.find((e) => e.instanceId === currentId);
|
||||
if (!enemy || enemy.isDead) return this._nextTurn(session, accumulatedLogs);
|
||||
|
||||
const enemyMessages = CombatServiceRef.handleAttack(battle, null);
|
||||
accumulatedLogs.push({
|
||||
attackerId: currentId,
|
||||
messages: enemyMessages,
|
||||
hpState: this._captureHpState(battle),
|
||||
});
|
||||
|
||||
if (battle.player.hp <= 0 || battle.enemies.every((e) => e.isDead)) {
|
||||
return this._afterAction(session, accumulatedLogs);
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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) {
|
||||
this.activeSessions.delete(playerId);
|
||||
|
||||
@ -29,6 +29,7 @@ module.exports = (io, socket) => {
|
||||
remainingEnergy: player.energy - energyCost,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
socket.emit("error", { message: "Deployment failure" });
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user