Merge branch 'main' of https://github.com/Korvarix/Galaxy-Strike-Online
This commit is contained in:
commit
7257b1cd2f
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 228 KiB |
@ -6,6 +6,7 @@ class GameDataManager {
|
|||||||
this.dungeons = new Map();
|
this.dungeons = new Map();
|
||||||
this.hostiles = new Map();
|
this.hostiles = new Map();
|
||||||
this.rooms = new Map();
|
this.rooms = new Map();
|
||||||
|
this.quests = new Map();
|
||||||
this.translations = {};
|
this.translations = {};
|
||||||
this.manifest = {};
|
this.manifest = {};
|
||||||
this.currentLang = localStorage.getItem("selected_lang") || "en_US";
|
this.currentLang = localStorage.getItem("selected_lang") || "en_US";
|
||||||
@ -31,10 +32,14 @@ class GameDataManager {
|
|||||||
if (Array.isArray(data.rooms)) {
|
if (Array.isArray(data.rooms)) {
|
||||||
data.rooms.forEach((r) => this.rooms.set(r.id, r));
|
data.rooms.forEach((r) => this.rooms.set(r.id, r));
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(data.quests)) {
|
||||||
|
data.quests.forEach((q) => this.quests.set(q.id, q));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(this.quests);
|
||||||
if (data.languages) {
|
if (data.languages) {
|
||||||
this.translations = data.languages;
|
this.translations = data.languages;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.manifest) {
|
if (data.manifest) {
|
||||||
this.manifest = data.manifest;
|
this.manifest = data.manifest;
|
||||||
}
|
}
|
||||||
@ -191,6 +196,24 @@ class GameDataManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getQuest(id) {
|
||||||
|
const quest = this.quests.get(id);
|
||||||
|
if (!quest) return null;
|
||||||
|
return {
|
||||||
|
...quest,
|
||||||
|
displayName: this.t(quest.displayName),
|
||||||
|
description: this.t(quest.description),
|
||||||
|
objectives: (quest.objectives || []).map((obj) => ({
|
||||||
|
...obj,
|
||||||
|
description: obj.description ? this.t(obj.description) : "",
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllQuests() {
|
||||||
|
return Array.from(this.quests.values()).map((q) => this.getQuest(q.id));
|
||||||
|
}
|
||||||
|
|
||||||
setLanguage(langCode) {
|
setLanguage(langCode) {
|
||||||
if (this.translations[langCode]) {
|
if (this.translations[langCode]) {
|
||||||
this.currentLang = langCode;
|
this.currentLang = langCode;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -5,68 +5,89 @@ import DungeonFinish from "../tabs/components/DungeonFinish.jsx";
|
|||||||
|
|
||||||
const DungeonScreen = ({ session, socket }) => {
|
const DungeonScreen = ({ session, socket }) => {
|
||||||
const [roomData, setRoomData] = useState(session.room);
|
const [roomData, setRoomData] = useState(session.room);
|
||||||
const [hostiles, setHostiles] = useState(session.hostiles || []);
|
|
||||||
const [roomIndex, setRoomIndex] = useState(session.roomIndex);
|
const [roomIndex, setRoomIndex] = useState(session.roomIndex);
|
||||||
const [enemyHp, setEnemyHp] = useState(null);
|
const [battle, setBattle] = useState(session.battle || null);
|
||||||
const [isEnemyDefeated, setIsEnemyDefeated] = useState(false);
|
const [timeLeft, setTimeLeft] = useState(10);
|
||||||
const [isLooted, setIsLooted] = useState(false);
|
|
||||||
const [summary, setSummary] = useState(null);
|
const [summary, setSummary] = useState(null);
|
||||||
|
const [activeAttacker, setActiveAttacker] = useState(null);
|
||||||
|
|
||||||
|
const [selectedTarget, setSelectedTarget] = useState(null);
|
||||||
|
|
||||||
const [log, setLog] = useState([
|
const [log, setLog] = useState([
|
||||||
"SYSTEM: Neural link established. Scanning sector...",
|
"SYSTEM: Neural link established. Scanning sector...",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const logEndRef = useRef(null);
|
const logEndRef = useRef(null);
|
||||||
|
const timerRef = useRef(null);
|
||||||
const currentEnemy = hostiles.length > 0 ? hostiles[0] : null;
|
|
||||||
const dungeonData = GameDataManager.getDungeon(session.dungeonId);
|
const dungeonData = GameDataManager.getDungeon(session.dungeonId);
|
||||||
|
|
||||||
const getEnemyDisplayName = (enemy) => {
|
|
||||||
if (!enemy) return "UNKNOWN_ENTITY";
|
|
||||||
const data = GameDataManager.getEnemy(enemy.id);
|
|
||||||
return data?.displayName || enemy.displayName || enemy.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRoomDisplayName = (room) => {
|
|
||||||
if (!room) return "UNKNOWN_LOCATION";
|
|
||||||
const data = GameDataManager.getRoom(room.id);
|
|
||||||
return data?.displayName || room.displayName || room.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
}, [log]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
socket.on("dungeon:room_update", (data) => {
|
socket.on("dungeon:room_update", (data) => {
|
||||||
setRoomData(data.room);
|
setRoomData(data.room);
|
||||||
setHostiles(data.hostiles || []);
|
|
||||||
setRoomIndex(data.roomIndex);
|
setRoomIndex(data.roomIndex);
|
||||||
setIsEnemyDefeated(false);
|
setBattle(data.battle);
|
||||||
setIsLooted(false);
|
|
||||||
setEnemyHp(null);
|
|
||||||
addLog(`--- ENTERING SECTOR ${data.roomIndex + 1} ---`);
|
addLog(`--- ENTERING SECTOR ${data.roomIndex + 1} ---`);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("dungeon:combat_result", (data) => {
|
socket.on("dungeon:failed", (data) => {
|
||||||
if (data.message) addLog(data.message);
|
addLog(`--- TERMINAL ERROR: ${data.message} ---`);
|
||||||
|
setTimeout(() => window.location.reload(), 3000);
|
||||||
|
});
|
||||||
|
|
||||||
if (data.enemyHp !== undefined) {
|
socket.on("dungeon:battle_update", async (data) => {
|
||||||
const maxHp = currentEnemy?.stats?.health || 100;
|
const turnOrder = data.battle.turnOrder;
|
||||||
const hpPercent = (data.enemyHp / maxHp) * 100;
|
const lastIndex =
|
||||||
setEnemyHp(hpPercent);
|
(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.targetDefeated) {
|
|
||||||
setIsEnemyDefeated(true);
|
|
||||||
addLog("TARGET_NEUTRALIZED: Threat eliminated.");
|
|
||||||
|
|
||||||
if (data.loot && data.loot.length > 0) {
|
if (data.status === "victory")
|
||||||
addLog("SCANNING FOR DROPPED ASSETS...");
|
addLog("MISSION_OBJECTIVE: Threats neutralized.");
|
||||||
data.loot.forEach((item) => {
|
if (data.status === "defeat") {
|
||||||
const itemData = GameDataManager.getItem(item.id);
|
addLog("CRITICAL_ERROR: Bio-sign lost.");
|
||||||
const itemName = itemData?.displayName || item.id;
|
setTimeout(() => window.location.reload(), 3000);
|
||||||
addLog(`RECOVERED: ${itemName} x${item.count}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -77,10 +98,11 @@ const DungeonScreen = ({ session, socket }) => {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off("dungeon:room_update");
|
socket.off("dungeon:room_update");
|
||||||
socket.off("dungeon:combat_result");
|
socket.off("dungeon:battle_update");
|
||||||
socket.off("dungeon:completed");
|
socket.off("dungeon:completed");
|
||||||
|
socket.off("dungeon:failed");
|
||||||
};
|
};
|
||||||
}, [socket, currentEnemy]);
|
}, [socket]);
|
||||||
|
|
||||||
const addLog = (text) => {
|
const addLog = (text) => {
|
||||||
const time = new Date().toLocaleTimeString([], {
|
const time = new Date().toLocaleTimeString([], {
|
||||||
@ -92,21 +114,22 @@ const DungeonScreen = ({ session, socket }) => {
|
|||||||
setLog((prev) => [...prev, `[${time}] ${text}`]);
|
setLog((prev) => [...prev, `[${time}] ${text}`]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCombat = () => {
|
const handleCombatAction = () => {
|
||||||
if (isEnemyDefeated || !currentEnemy) return;
|
const targetId = selectedTarget;
|
||||||
socket.emit("dungeon:combat_step", { enemyId: currentEnemy.id });
|
if (!battle || battle.isOver || activeAttacker || !targetId) return;
|
||||||
addLog(`Initiating strike sequence...`);
|
if (battle.turnOrder[battle.currentTurnIndex] !== "player") return;
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoot = () => {
|
socket.emit("dungeon:combat_action", { targetInstanceId: targetId });
|
||||||
setIsLooted(true);
|
addLog(`Initiating strike sequence...`);
|
||||||
addLog("Loot encryption bypassed. Resources transferred.");
|
setSelectedTarget(null); // Скидаємо вибір після атаки
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNextRoom = () => {
|
const handleNextRoom = () => {
|
||||||
socket.emit("dungeon:next_room");
|
socket.emit("dungeon:next_room");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isPlayerTurn = battle?.turnOrder[battle?.currentTurnIndex] === "player";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dungeon-active-screen">
|
<div className="dungeon-active-screen">
|
||||||
{summary && (
|
{summary && (
|
||||||
@ -116,6 +139,7 @@ const DungeonScreen = ({ session, socket }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Header section remains the same */}
|
||||||
<div className="dungeon-header">
|
<div className="dungeon-header">
|
||||||
<div className="room-progress">
|
<div className="room-progress">
|
||||||
<div className="progress-text">
|
<div className="progress-text">
|
||||||
@ -130,107 +154,129 @@ const DungeonScreen = ({ session, socket }) => {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dungeon-title-area">
|
|
||||||
<div className="dungeon-name">
|
|
||||||
{dungeonData?.displayName || "MISSION_ACTIVE"}
|
|
||||||
</div>
|
|
||||||
<div className="dungeon-status-tag">LIVE_FEED</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="environment-panel">
|
{battle && (
|
||||||
<div className="env-header">ENVIRONMENT_SCAN</div>
|
<div
|
||||||
<div className="env-info">
|
className={`turn-progress-container ${isPlayerTurn ? "player-phase" : "enemy-phase"}`}
|
||||||
<div className="env-icon">
|
>
|
||||||
<i
|
<div className="turn-label">
|
||||||
className={`fas ${
|
{isPlayerTurn ? "YOUR TURN" : "ENEMY ACTION"}
|
||||||
roomData?.id?.includes("boss")
|
</div>
|
||||||
? "fa-skull"
|
<div className="turn-timer-bar">
|
||||||
: roomData?.id?.includes("loot")
|
<div
|
||||||
? "fa-box-open"
|
className="turn-timer-fill"
|
||||||
: "fa-microchip"
|
style={{
|
||||||
}`}
|
width: `${(timeLeft / (isPlayerTurn ? 10 : 4)) * 100}%`,
|
||||||
></i>
|
transition:
|
||||||
</div>
|
timeLeft === 10 || timeLeft === 4
|
||||||
<div className="env-details">
|
? "none"
|
||||||
<div className="env-id">{getRoomDisplayName(roomData)}</div>
|
: "width 1s linear",
|
||||||
<div className="env-type">
|
}}
|
||||||
TYPE: {hostiles.length > 0 ? "COMBAT_ZONE" : "SECURE_AREA"}
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="battle-layout">
|
<div className="battle-arena">
|
||||||
<div
|
{battle ? (
|
||||||
className={`enemy-display ${isEnemyDefeated ? "target-lost" : ""}`}
|
<div className="mobs-grid">
|
||||||
>
|
{battle.enemies.map((mob) => (
|
||||||
{currentEnemy ? (
|
<div
|
||||||
<div className={`enemy-card ${isEnemyDefeated ? "defeated" : ""}`}>
|
key={mob.instanceId}
|
||||||
<div className="threat-tag">
|
className={`enemy-card
|
||||||
{isEnemyDefeated ? "SIGNAL_LOST" : "HOSTILE_DETECTED"}
|
${mob.isDead ? "defeated" : ""}
|
||||||
</div>
|
${selectedTarget === mob.instanceId ? "selected" : ""}
|
||||||
<div className="enemy-icon">
|
${isPlayerTurn && !mob.isDead ? "selectable" : ""}
|
||||||
<i
|
${activeAttacker === mob.instanceId ? "attacking" : ""}
|
||||||
className={`fas ${isEnemyDefeated ? "fa-skull-crossbones" : "fa-robot"}`}
|
`}
|
||||||
></i>
|
onClick={() =>
|
||||||
</div>
|
!mob.isDead &&
|
||||||
<h3 className="enemy-name">
|
isPlayerTurn &&
|
||||||
{getEnemyDisplayName(currentEnemy)}
|
setSelectedTarget(mob.instanceId)
|
||||||
</h3>
|
}
|
||||||
<div className="enemy-hp-container">
|
>
|
||||||
<div className="hp-label">STRUCTURE INTEGRITY</div>
|
{selectedTarget === mob.instanceId && (
|
||||||
<div className="hp-bar-mini">
|
<div className="target-aim">
|
||||||
|
<i className="fas fa-crosshairs"></i>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="enemy-hp-mini">
|
||||||
<div
|
<div
|
||||||
className="hp-fill-mini"
|
className="fill"
|
||||||
style={{
|
style={{ width: `${(mob.hp / mob.maxHp) * 100}%` }}
|
||||||
width: isEnemyDefeated
|
|
||||||
? "0%"
|
|
||||||
: `${enemyHp !== null ? enemyHp : 100}%`,
|
|
||||||
}}
|
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="enemy-icon">
|
||||||
<div className="enemy-info-footer">
|
<i
|
||||||
<span>LVL: {currentEnemy.level || 1}</span>
|
className={`fas ${mob.isDead ? "fa-skull-crossbones" : "fa-robot"}`}
|
||||||
<span className="card-id">{currentEnemy.id}</span>
|
></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span className="mob-name">{GameDataManager.t(mob.name)}</span>
|
||||||
) : (
|
{!mob.isDead && <span className="mob-atk">ATK: {mob.atk}</span>}
|
||||||
<div className="empty-room">
|
|
||||||
<i className="fas fa-satellite-dish"></i>
|
|
||||||
<p>NO HOSTILES IN RANGE</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="combat-log-wrapper">
|
|
||||||
<div className="log-header">COMBAT_LOG_V3.0</div>
|
|
||||||
<div className="combat-log custom-scroll">
|
|
||||||
{log.map((entry, i) => (
|
|
||||||
<div key={i} className="log-entry">
|
|
||||||
<span className="log-arrow">></span> {entry}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div ref={logEndRef} />
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="empty-room">
|
||||||
|
<i className="fas fa-satellite-dish"></i>
|
||||||
|
<p>AREA SECURE</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="player-section">
|
||||||
|
{battle && (
|
||||||
|
<div
|
||||||
|
className={`player-hp-main ${activeAttacker ? "taking-damage" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="hp-header">
|
||||||
|
<span>COMMANDER_INTEGRITY</span>
|
||||||
|
<span>
|
||||||
|
{battle.player.hp} / {battle.player.maxHp}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="hp-bar-large">
|
||||||
|
<div
|
||||||
|
className="fill"
|
||||||
|
style={{
|
||||||
|
width: `${(battle.player.hp / battle.player.maxHp) * 100}%`,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="combat-interface-row">
|
||||||
|
<div className="combat-log-wrapper">
|
||||||
|
<div className="combat-log custom-scroll">
|
||||||
|
{log.map((entry, i) => (
|
||||||
|
<div key={i} className="log-entry">
|
||||||
|
<span className="log-arrow">></span> {entry}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={logEndRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{battle && !battle.isOver && (
|
||||||
|
<button
|
||||||
|
className={`btn-execute-combat ${!selectedTarget || !isPlayerTurn ? "disabled" : ""}`}
|
||||||
|
disabled={!selectedTarget || !isPlayerTurn}
|
||||||
|
onClick={handleCombatAction}
|
||||||
|
>
|
||||||
|
<div className="btn-glitch-content">EXECUTE_STRIKE</div>
|
||||||
|
<div className="btn-sub-text">
|
||||||
|
{selectedTarget ? "TARGET_LOCKED" : "SELECT_TARGET"}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dungeon-controls">
|
<div className="dungeon-controls">
|
||||||
{!isEnemyDefeated && currentEnemy && (
|
{((battle?.isOver && battle.player.hp > 0) || !battle) && (
|
||||||
<button className="ctrl-btn combat" onClick={handleCombat}>
|
|
||||||
<i className="fas fa-bolt"></i> ENGAGE
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isEnemyDefeated && !isLooted && (
|
|
||||||
<button className="ctrl-btn loot" onClick={handleLoot}>
|
|
||||||
<i className="fas fa-download"></i> COLLECT_ASSETS
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(isLooted || !currentEnemy) && (
|
|
||||||
<button className="ctrl-btn next" onClick={handleNextRoom}>
|
<button className="ctrl-btn next" onClick={handleNextRoom}>
|
||||||
PROCEED <i className="fas fa-chevron-right"></i>
|
PROCEED <i className="fas fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
margin-right: 10px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: #4a5d75;
|
color: #4a5d75;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ const Navigation = ({ activeTab, onTabChange }) => {
|
|||||||
{ id: "dashboard", icon: "fa-tachometer-alt" },
|
{ id: "dashboard", icon: "fa-tachometer-alt" },
|
||||||
{ id: "dungeons", icon: "fa-dungeon" },
|
{ id: "dungeons", icon: "fa-dungeon" },
|
||||||
{ id: "skills", icon: "fa-graduation-cap" },
|
{ id: "skills", icon: "fa-graduation-cap" },
|
||||||
|
{ id: "quests", icon: "fa-store" },
|
||||||
{ id: "inventory", icon: "fa-archive" },
|
{ id: "inventory", icon: "fa-archive" },
|
||||||
{ id: "shop", icon: "fa-store" },
|
{ id: "shop", icon: "fa-store" },
|
||||||
{ id: "crafting", icon: "fa-hammer" },
|
{ id: "crafting", icon: "fa-hammer" },
|
||||||
|
|||||||
@ -1,32 +1,170 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState, useMemo } from "react";
|
||||||
|
import Card from "../../../components/ui/Card";
|
||||||
|
import { useSocket } from "../../../hooks/useSocket";
|
||||||
|
import gameDataManager from "../../../services/GameDataManager";
|
||||||
import "./styles/QuestsTab.css";
|
import "./styles/QuestsTab.css";
|
||||||
|
|
||||||
const QuestsTab = () => {
|
const QuestsTab = () => {
|
||||||
const [type, setType] = useState('main');
|
const { socket } = useSocket();
|
||||||
|
const [quests, setQuests] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [activeTab, setActiveTab] = useState("ACTIVE");
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<div className="tab-content active">
|
if (!socket) return;
|
||||||
<div className="quests-container">
|
|
||||||
<div className="quest-tabs">
|
socket.emit("quest:get_list");
|
||||||
{['main', 'daily', 'weekly', 'completed'].map(t => (
|
|
||||||
<button
|
const localize = (q) => {
|
||||||
key={t}
|
const staticData = gameDataManager.getQuest(q.id);
|
||||||
className={`quest-tab-btn ${type === t ? 'active' : ''}`}
|
return {
|
||||||
onClick={() => setType(t)}
|
...q,
|
||||||
>
|
displayName: staticData?.displayName || q.id,
|
||||||
{t === 'main' ? 'Main Story' : t.charAt(0).toUpperCase() + t.slice(1)}
|
description: staticData?.description || "",
|
||||||
</button>
|
objectives: q.objectives.map((obj, idx) => ({
|
||||||
))}
|
...obj,
|
||||||
|
description: staticData?.objectives[idx]?.description || obj.type,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuestData = (data) => {
|
||||||
|
const uniqueQuests = new Map();
|
||||||
|
data.forEach((q) => {
|
||||||
|
uniqueQuests.set(q.id, localize(q));
|
||||||
|
});
|
||||||
|
|
||||||
|
setQuests(Array.from(uniqueQuests.values()));
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuestUpdate = (updatedQuest) => {
|
||||||
|
setQuests((prev) => {
|
||||||
|
const localized = localize(updatedQuest);
|
||||||
|
const questMap = new Map(prev.map((q) => [q.id, q]));
|
||||||
|
questMap.set(localized.id, localized);
|
||||||
|
return Array.from(questMap.values());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("quest:list_data", handleQuestData);
|
||||||
|
socket.on("quest:update", handleQuestUpdate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off("quest:list_data", handleQuestData);
|
||||||
|
socket.off("quest:update", handleQuestUpdate);
|
||||||
|
};
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
const filteredQuests = useMemo(() => {
|
||||||
|
return quests.filter((q) =>
|
||||||
|
activeTab === "ACTIVE"
|
||||||
|
? q.status === "active" || q.status === "ready"
|
||||||
|
: q.status === "completed",
|
||||||
|
);
|
||||||
|
}, [quests, activeTab]);
|
||||||
|
|
||||||
|
const renderObjective = (obj, index) => {
|
||||||
|
const progress = Math.min(
|
||||||
|
(obj.currentAmount / obj.requiredAmount) * 100,
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div key={index} className="objective-item">
|
||||||
|
<div className="objective-info">
|
||||||
|
<span className="objective-desc">{obj.description || obj.type}</span>
|
||||||
|
<span className="objective-count">
|
||||||
|
{obj.currentAmount} / {obj.requiredAmount}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="objective-progress-track">
|
||||||
<div className="daily-countdown">Daily quests reset in: 00:00:00</div>
|
<div
|
||||||
|
className="objective-progress-fill"
|
||||||
<div className="quest-list">
|
style={{ width: `${progress}%` }}
|
||||||
<div className="empty-quests">
|
></div>
|
||||||
<p>No {type} quests available at the moment.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="quests-container">
|
||||||
|
<div className="dash-scanline"></div>
|
||||||
|
<div className="quests-header">
|
||||||
|
<h2 className="glitch-text" data-text="MISSION_LOG">
|
||||||
|
MISSION_LOG
|
||||||
|
</h2>
|
||||||
|
<div className="quest-tabs-nav">
|
||||||
|
<button
|
||||||
|
className={`nav-btn ${activeTab === "ACTIVE" ? "active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("ACTIVE")}
|
||||||
|
>
|
||||||
|
ACTIVE_OPERATIONS
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`nav-btn ${activeTab === "COMPLETED" ? "active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("COMPLETED")}
|
||||||
|
>
|
||||||
|
ARCHIVED_DATA
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="header-line"></div>
|
||||||
|
</div>
|
||||||
|
{loading ? (
|
||||||
|
<div className="loading-status">SCANNING_NEURAL_NETWORK...</div>
|
||||||
|
) : (
|
||||||
|
<div className="quests-grid">
|
||||||
|
{filteredQuests.length > 0 ? (
|
||||||
|
filteredQuests.map((quest) => (
|
||||||
|
<Card key={quest.id} className={`quest-card ${quest.status}`}>
|
||||||
|
<div className="card-tag">{quest.category || "MISSION"}</div>
|
||||||
|
<div className="quest-main">
|
||||||
|
<h3 className="quest-title">{quest.displayName}</h3>
|
||||||
|
<p className="quest-description">{quest.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="quest-objectives">
|
||||||
|
<div className="section-label">OBJECTIVES</div>
|
||||||
|
{quest.objectives.map((obj, idx) =>
|
||||||
|
renderObjective(obj, idx),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="quest-rewards">
|
||||||
|
<div className="section-label">REWARDS</div>
|
||||||
|
<div className="rewards-row">
|
||||||
|
{quest.rewards?.credits > 0 && (
|
||||||
|
<span className="reward-pill credits">
|
||||||
|
+{quest.rewards.credits} CR
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{quest.rewards?.xp > 0 && (
|
||||||
|
<span className="reward-pill xp">
|
||||||
|
+{quest.rewards.xp} XP
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{quest.status === "ready" && (
|
||||||
|
<button
|
||||||
|
className="claim-btn"
|
||||||
|
onClick={() =>
|
||||||
|
socket.emit("quest:claim_reward", { questId: quest.id })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
COMPLETE_MISSION
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{quest.status === "completed" && (
|
||||||
|
<div className="completed-stamp">MISSION_ACCOMPLISHED</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="no-quests">
|
||||||
|
<p>NO_{activeTab}_SIGNALS_FOUND</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,256 +2,197 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
background: rgba(0, 0, 0, 0.85);
|
background: rgba(0, 0, 0, 0.85);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 2000;
|
z-index: 9999;
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.craft-modal {
|
.craft-modal {
|
||||||
background: #151921;
|
background: #0f1115;
|
||||||
border: 1px solid #00ccff;
|
border: 1px solid rgba(0, 210, 255, 0.3);
|
||||||
border-radius: 12px;
|
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 450px;
|
max-width: 400px; /* Трохи вужча для компактності */
|
||||||
box-shadow: 0 0 30px rgba(0, 204, 255, 0.15);
|
border-radius: 12px;
|
||||||
}
|
position: relative;
|
||||||
|
padding: 20px;
|
||||||
.modal-headerr {
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.9);
|
||||||
display: flex;
|
animation: modalSlideUp 0.3s ease-out;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: center;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
margin: 0;
|
|
||||||
color: #00ccff;
|
|
||||||
font-family: "Orbitron", sans-serif;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-x {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.requirements-section,
|
/* Header Section */
|
||||||
.outcome-section {
|
.modal-header-compact {
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.res-grid {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 15px;
|
||||||
background: rgba(255, 255, 255, 0.02);
|
margin-bottom: 18px;
|
||||||
padding: 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.res-item {
|
.item-icon-box {
|
||||||
display: flex;
|
width: 70px;
|
||||||
justify-content: space-between;
|
height: 70px;
|
||||||
color: #e0e0e0;
|
background: rgba(0, 0, 0, 0.5);
|
||||||
}
|
border: 1px solid rgba(0, 210, 255, 0.4);
|
||||||
|
|
||||||
.btn-start-craft {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
background: #00ccff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #000;
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
transition: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-start-craft:hover {
|
|
||||||
background: #0099cc;
|
|
||||||
box-shadow: 0 0 10px rgba(0, 204, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
color: #888;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active-craft-panel {
|
|
||||||
background: rgba(0, 204, 255, 0.1);
|
|
||||||
border: 1px solid var(--primary-color);
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 20px;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.craft-info {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
margin-bottom: 8px;
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: inset 0 0 10px rgba(0, 210, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon-box img {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-info-title h3 {
|
||||||
|
margin: 0;
|
||||||
font-family: "Orbitron", sans-serif;
|
font-family: "Orbitron", sans-serif;
|
||||||
font-size: 0.9rem;
|
font-size: 1.1rem;
|
||||||
color: var(--primary-color);
|
color: #00d2ff;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-bg {
|
.item-tag {
|
||||||
width: 100%;
|
font-size: 0.65rem;
|
||||||
height: 10px;
|
color: #888;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
letter-spacing: 1px;
|
||||||
border-radius: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-fill {
|
/* Sections */
|
||||||
height: 100%;
|
.details-section {
|
||||||
background: var(--primary-color);
|
margin-bottom: 15px;
|
||||||
box-shadow: 0 0 10px var(--primary-color);
|
|
||||||
transition: width 1s linear;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
.section-label {
|
||||||
0% {
|
font-size: 0.75rem;
|
||||||
box-shadow: 0 0 5px rgba(0, 204, 255, 0.2);
|
text-transform: uppercase;
|
||||||
}
|
color: #00d2ff;
|
||||||
50% {
|
margin-bottom: 8px;
|
||||||
box-shadow: 0 0 15px rgba(0, 204, 255, 0.4);
|
opacity: 0.8;
|
||||||
}
|
display: block;
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 5px rgba(0, 204, 255, 0.2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.res-item {
|
.description-text {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #aaa;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resource List */
|
||||||
|
.res-container {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.res-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 12px;
|
padding: 6px 0;
|
||||||
background: rgba(255, 255, 255, 0.05);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
border-left: 4px solid transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.res-item.enough {
|
.res-row:last-child {
|
||||||
border-left-color: #44ff44;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.res-item.not-enough {
|
.res-name {
|
||||||
border-left-color: #ff4444;
|
font-size: 0.9rem;
|
||||||
background: rgba(255, 68, 68, 0.1);
|
color: #ccc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.val-red {
|
.res-amount {
|
||||||
color: #ff4444;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.val-green {
|
|
||||||
color: #44ff44;
|
|
||||||
}
|
|
||||||
.icon-red {
|
|
||||||
color: #ff4444;
|
|
||||||
}
|
|
||||||
.icon-green {
|
|
||||||
color: #44ff44;
|
|
||||||
}
|
|
||||||
|
|
||||||
.res-quantity-info {
|
|
||||||
font-family: "Courier New", monospace;
|
font-family: "Courier New", monospace;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-start-craft {
|
.val-bad {
|
||||||
background: #28a745;
|
color: #ff4444;
|
||||||
color: white;
|
}
|
||||||
border: none;
|
.val-good {
|
||||||
padding: 10px 20px;
|
color: #00ff88;
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: bold;
|
|
||||||
transition: 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-start-craft.disabled {
|
/* Progress & Outcome */
|
||||||
background: #444 !important;
|
.outcome-bar {
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-start-craft:not(.disabled):hover {
|
|
||||||
background: #218838;
|
|
||||||
box-shadow: 0 0 10px rgba(40, 167, 69, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-preview-header {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
justify-content: space-between;
|
||||||
padding: 15px;
|
font-size: 0.85rem;
|
||||||
background: rgba(0, 212, 255, 0.05);
|
margin-top: 10px;
|
||||||
border: 1px solid rgba(0, 212, 255, 0.1);
|
color: #888;
|
||||||
margin-bottom: 20px;
|
}
|
||||||
|
|
||||||
|
.outcome-bar strong {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn-craft-action {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-family: "Orbitron", sans-serif;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-icon-container {
|
.btn-primary-craft {
|
||||||
position: relative;
|
background: rgba(0, 210, 255, 0.1);
|
||||||
width: 90px;
|
border: 1px solid #00d2ff;
|
||||||
height: 90px;
|
color: #00d2ff;
|
||||||
background: #000;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-display-icon {
|
.btn-primary-craft:hover:not(:disabled) {
|
||||||
max-width: 80%;
|
background: #00d2ff;
|
||||||
max-height: 80%;
|
|
||||||
object-fit: contain;
|
|
||||||
filter: drop-shadow(0 0 5px var(--primary-color));
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-qty-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: -8px;
|
|
||||||
right: -8px;
|
|
||||||
background: var(--primary-color);
|
|
||||||
color: #000;
|
color: #000;
|
||||||
padding: 2px 8px;
|
box-shadow: 0 0 15px rgba(0, 210, 255, 0.3);
|
||||||
font-size: 11px;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-type-tag {
|
.btn-primary-craft:disabled {
|
||||||
display: block;
|
border-color: #444;
|
||||||
font-size: 10px;
|
color: #444;
|
||||||
color: var(--primary-color);
|
cursor: not-allowed;
|
||||||
letter-spacing: 1px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-description {
|
.close-btn-top {
|
||||||
font-size: 13px;
|
position: absolute;
|
||||||
color: #ccc;
|
top: 12px;
|
||||||
line-height: 1.4;
|
right: 12px;
|
||||||
margin: 0;
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #555;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn-top:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalSlideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(15px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,7 @@ const CraftModal = ({
|
|||||||
|
|
||||||
const getFullTextureUrl = (path) => {
|
const getFullTextureUrl = (path) => {
|
||||||
if (!path) return "/assets/no-image.png";
|
if (!path) return "/assets/no-image.png";
|
||||||
if (path.startsWith("http")) return path;
|
return path.startsWith("http") ? path : `${ASSET_BASE_URL}${path}`;
|
||||||
return `${ASSET_BASE_URL}${path}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isBusy = !!activeCraft;
|
const isBusy = !!activeCraft;
|
||||||
@ -29,120 +28,131 @@ const CraftModal = ({
|
|||||||
return (
|
return (
|
||||||
<div className="craft-modal-overlay" onClick={onClose}>
|
<div className="craft-modal-overlay" onClick={onClose}>
|
||||||
<div className="craft-modal" onClick={(e) => e.stopPropagation()}>
|
<div className="craft-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="modal-headerr">
|
<button className="close-btn-top" onClick={onClose}>
|
||||||
<h3>
|
×
|
||||||
<i className="fas fa-tools"></i> Construction: {recipe.displayName}
|
</button>
|
||||||
</h3>
|
|
||||||
<button className="close-x" onClick={onClose}>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal-body">
|
{/* Header: Icon + Title */}
|
||||||
{/* Секція з картинкою предмета */}
|
<div className="modal-header-compact">
|
||||||
<div className="item-preview-header">
|
<div className="item-icon-box">
|
||||||
<div className="item-icon-container">
|
<img
|
||||||
<img
|
src={getFullTextureUrl(recipe.texture)}
|
||||||
src={getFullTextureUrl(recipe.texture)}
|
alt={recipe.displayName}
|
||||||
alt={recipe.displayName}
|
/>
|
||||||
className="item-display-icon"
|
<div
|
||||||
/>
|
className="item-qty-badge"
|
||||||
<div className="item-qty-badge">x{outputQty}</div>
|
style={{
|
||||||
</div>
|
position: "absolute",
|
||||||
<div className="item-header-info">
|
bottom: "-5px",
|
||||||
<span className="item-type-tag">PROTOTYPE_UNIT</span>
|
right: "-5px",
|
||||||
<p className="item-description">
|
background: "#00d2ff",
|
||||||
{recipe.description ||
|
color: "#000",
|
||||||
"Technical data encrypted or unavailable."}
|
fontSize: "10px",
|
||||||
</p>
|
padding: "2px 5px",
|
||||||
|
borderRadius: "3px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
x{outputQty}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="item-info-title">
|
||||||
|
<span className="item-tag">PROTOTYPE_UNIT</span>
|
||||||
|
<h3>{recipe.displayName}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="requirements-section">
|
{/* Description */}
|
||||||
<h4>
|
<div className="details-section">
|
||||||
<i className="fas fa-list-ul"></i> Required Resources
|
<p className="description-text">
|
||||||
</h4>
|
{recipe.description ||
|
||||||
<div className="res-grid">
|
"Advanced composite material for high-tier construction."}
|
||||||
{recipe.ingredients?.map((ing) => {
|
</p>
|
||||||
const owned = getOwnedAmount(ing.itemId);
|
</div>
|
||||||
const hasEnough = owned >= ing.quantity;
|
|
||||||
|
|
||||||
return (
|
{/* Resources */}
|
||||||
<div
|
<div className="details-section">
|
||||||
key={ing.itemId}
|
<span className="section-label">Required Materials</span>
|
||||||
className={`res-item ${hasEnough ? "enough" : "not-enough"}`}
|
<div className="res-container">
|
||||||
|
{recipe.ingredients?.map((ing) => {
|
||||||
|
const owned = getOwnedAmount(ing.itemId);
|
||||||
|
const hasEnough = owned >= ing.quantity;
|
||||||
|
return (
|
||||||
|
<div key={ing.itemId} className="res-row">
|
||||||
|
<span className="res-name">
|
||||||
|
<i
|
||||||
|
className={`fas fa-square`}
|
||||||
|
style={{
|
||||||
|
fontSize: "8px",
|
||||||
|
color: hasEnough ? "#00ff88" : "#ff4444",
|
||||||
|
}}
|
||||||
|
></i>
|
||||||
|
{ing.displayName}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`res-amount ${hasEnough ? "val-good" : "val-bad"}`}
|
||||||
>
|
>
|
||||||
<div className="res-main-info">
|
{owned} / {ing.quantity}
|
||||||
<i
|
</span>
|
||||||
className={`fas fa-cube ${hasEnough ? "icon-green" : "icon-red"}`}
|
</div>
|
||||||
></i>
|
);
|
||||||
<span className="res-name">{ing.displayName}</span>
|
})}
|
||||||
</div>
|
|
||||||
<div className="res-quantity-info">
|
|
||||||
<span
|
|
||||||
className={`owned-val ${hasEnough ? "val-green" : "val-red"}`}
|
|
||||||
>
|
|
||||||
{owned}
|
|
||||||
</span>
|
|
||||||
<span className="required-val"> / {ing.quantity}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="outcome-section">
|
|
||||||
<h4>
|
|
||||||
<i className="fas fa-box-open"></i> Outcome
|
|
||||||
</h4>
|
|
||||||
<div className="outcome-info">
|
|
||||||
<div className="outcome-row">
|
|
||||||
<span>Result:</span>
|
|
||||||
<strong>
|
|
||||||
{recipe.displayName} x{outputQty}
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
<div className="outcome-row">
|
|
||||||
<span>Time:</span>
|
|
||||||
<strong>{recipe.time_seconds}s</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="modal-footer">
|
{/* Outcome Info */}
|
||||||
|
<div className="outcome-bar">
|
||||||
|
<span>
|
||||||
|
Production Time: <strong>{recipe.time_seconds}s</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
|
<div className="modal-footer-minimal">
|
||||||
{activeCraft && activeCraft.recipeId === recipe.id ? (
|
{activeCraft && activeCraft.recipeId === recipe.id ? (
|
||||||
<div className="modal-progress-container">
|
<div style={{ marginTop: "15px" }}>
|
||||||
<div className="progress-text">
|
<div
|
||||||
Processing... {activeCraft.timeLeft}s
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "#00d2ff",
|
||||||
|
marginBottom: "5px",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Constructing... {activeCraft.timeLeft}s
|
||||||
</div>
|
</div>
|
||||||
<div className="progress-bar-bg">
|
<div
|
||||||
|
className="progress-bar-bg"
|
||||||
|
style={{
|
||||||
|
height: "4px",
|
||||||
|
background: "rgba(255,255,255,0.1)",
|
||||||
|
borderRadius: "2px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="progress-bar-fill"
|
className="progress-bar-fill"
|
||||||
style={{
|
style={{
|
||||||
width: `${((activeCraft.totalTime - activeCraft.timeLeft) / activeCraft.totalTime) * 100}%`,
|
width: `${((activeCraft.totalTime - activeCraft.timeLeft) / activeCraft.totalTime) * 100}%`,
|
||||||
|
height: "100%",
|
||||||
|
background: "#00d2ff",
|
||||||
|
boxShadow: "0 0 10px #00d2ff",
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<button
|
||||||
<button
|
className="btn-craft-action btn-primary-craft"
|
||||||
className={`btn-start-craft ${!canAfford || isBusy ? "disabled" : ""}`}
|
disabled={!canAfford || isBusy}
|
||||||
onClick={() => canAfford && !isBusy && onStartCraft(recipe)}
|
onClick={() => canAfford && !isBusy && onStartCraft(recipe)}
|
||||||
disabled={!canAfford || isBusy}
|
>
|
||||||
>
|
{isBusy
|
||||||
{isBusy
|
? "System Busy"
|
||||||
? "System Busy..."
|
: !canAfford
|
||||||
: !canAfford
|
? "Low Resources"
|
||||||
? "Insufficient Resources"
|
: "Begin Construction"}
|
||||||
: `Start Construction (${recipe.time_seconds}s)`}
|
</button>
|
||||||
</button>
|
|
||||||
<button className="btn-cancel" onClick={onClose}>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -193,3 +193,22 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-qty-tag {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -5px;
|
||||||
|
right: -5px;
|
||||||
|
background: #00d2ff;
|
||||||
|
color: #000;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: monospace;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 210, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-equip {
|
||||||
|
letter-spacing: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import "./ItemModal.css";
|
import "./ItemModal.css";
|
||||||
import { getServerUrl } from "../../../../config/api";
|
import { getServerUrl } from "../../../../config/api";
|
||||||
|
|
||||||
@ -11,6 +11,25 @@ const ItemModal = ({
|
|||||||
getStatIcon,
|
getStatIcon,
|
||||||
formatStatName,
|
formatStatName,
|
||||||
}) => {
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
if (event.keyCode === 69) {
|
||||||
|
if (isEquipped) {
|
||||||
|
onUnequip(item.currentSlot);
|
||||||
|
} else if (item && item.canEquip) {
|
||||||
|
onEquip(item);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [item, isEquipped, onEquip, onUnequip, onClose]);
|
||||||
|
|
||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
|
|
||||||
const CONNECT_URL = getServerUrl();
|
const CONNECT_URL = getServerUrl();
|
||||||
@ -77,7 +96,7 @@ const ItemModal = ({
|
|||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
TERMINATE_CONNECTION
|
TERMINATE_CONNECTION (E)
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
item.canEquip && (
|
item.canEquip && (
|
||||||
@ -88,7 +107,7 @@ const ItemModal = ({
|
|||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
INITIALIZE_EQUIP
|
INITIALIZE_EQUIP (E)
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -0,0 +1,211 @@
|
|||||||
|
.quests-container {
|
||||||
|
padding: 15px;
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quests-header {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
border-left: 3px solid #00d4ff;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glitch-text {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: #fff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-line {
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, #00d4ff, transparent);
|
||||||
|
margin-top: 5px;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quests-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quest-card {
|
||||||
|
background: rgba(10, 15, 24, 0.95) !important;
|
||||||
|
border: 1px solid #1a2638 !important;
|
||||||
|
border-radius: 2px !important;
|
||||||
|
padding: 20px !important;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стан готовності - міняємо бордер на Cyan замість фіолетового */
|
||||||
|
.quest-card.ready {
|
||||||
|
border-color: #00ff88 !important; /* Зеленуватий акцент для готових квестів */
|
||||||
|
box-shadow: inset 0 0 10px rgba(0, 255, 136, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quest-main h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #00d4ff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quest-description {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #4a5d75;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
font-size: 8px;
|
||||||
|
color: #4a5d75;
|
||||||
|
margin: 15px 0 8px 0;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objective-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objective-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objective-progress-track {
|
||||||
|
height: 3px;
|
||||||
|
background: #05080c;
|
||||||
|
border: 1px solid #1a2638;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objective-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: #00d4ff;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 212, 255, 0.3);
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rewards-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-pill {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-left: 2px solid #00d4ff;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: "Space Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-pill.credits {
|
||||||
|
border-left-color: #00ff88;
|
||||||
|
color: #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-pill.xp {
|
||||||
|
border-left-color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
background: #00ff88;
|
||||||
|
border: none;
|
||||||
|
color: #000;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-btn:hover {
|
||||||
|
background: #00cc6e;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 255, 136, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-quests {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #4a5d75;
|
||||||
|
border: 1px dashed #1a2638;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-quests i {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quest-tabs-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #4a5d75;
|
||||||
|
font-family: "Space Mono", monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 0;
|
||||||
|
position: relative;
|
||||||
|
transition: color 0.3s;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn.active {
|
||||||
|
color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn.active::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: #00d4ff;
|
||||||
|
box-shadow: 0 0 8px #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-stamp {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #00ff88;
|
||||||
|
color: #00ff88;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
background: rgba(0, 255, 136, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quest-card.completed {
|
||||||
|
opacity: 0.7;
|
||||||
|
border-color: #1a2638 !important;
|
||||||
|
}
|
||||||
@ -1,40 +1,41 @@
|
|||||||
{
|
{
|
||||||
"_comment_Admin" : "",
|
"_comment_Admin": "",
|
||||||
"admin.category.core.dungeon_list" : "Dungeon list",
|
"admin.category.core.dungeon_list": "Dungeon list",
|
||||||
"admin.category.core.hostile_list" : "Hostile list",
|
"admin.category.core.hostile_list": "Hostile list",
|
||||||
"admin.category.core.item_list" : "Item list",
|
"admin.category.core.item_list": "Item list",
|
||||||
"admin.category.core.player_list" : "Player list",
|
"admin.category.core.player_list": "Player list",
|
||||||
"admin.category.core.item_list.all" : "All",
|
"admin.category.core.item_list.all": "All",
|
||||||
"admin.category.core.hostile_list.all" : "All",
|
"admin.category.core.hostile_list.all": "All",
|
||||||
"admin.category.core.player_list.all" : "All",
|
"admin.category.core.player_list.all": "All",
|
||||||
"_comment_Stats" : "",
|
"_comment_Stats": "",
|
||||||
"stats.category.core.attack.base" : "Attack",
|
"stats.category.core.attack.base": "Attack",
|
||||||
"stats.category.core.attack.chance" : "Attack Chance",
|
"stats.category.core.attack.chance": "Attack Chance",
|
||||||
"stats.category.core.attack.rate" : "Attack Rate",
|
"stats.category.core.attack.rate": "Attack Rate",
|
||||||
"stats.category.core.defence.base" : "Defence",
|
"stats.category.core.defence.base": "Defence",
|
||||||
"stats.category.core.defence.chance" : "Defence Chance",
|
"stats.category.core.defence.chance": "Defence Chance",
|
||||||
"stats.category.core.defence.rate" : "Defence Rate",
|
"stats.category.core.defence.rate": "Defence Rate",
|
||||||
"stats.category.core.health" : "Health",
|
"stats.category.core.health": "Health",
|
||||||
"stats.category.core.penetration.base" : "Penetration",
|
"stats.category.core.penetration.base": "Penetration",
|
||||||
"stats.category.core.penetration.chance" : "Penetration Chance",
|
"stats.category.core.penetration.chance": "Penetration Chance",
|
||||||
"stats.category.core.penetration.rate" : "Penetration Rate",
|
"stats.category.core.penetration.rate": "Penetration Rate",
|
||||||
"stats.category.core.reflect.base" : "Reflect",
|
"stats.category.core.reflect.base": "Reflect",
|
||||||
"stats.category.core.reflect.chance" : "Reflection Chance",
|
"stats.category.core.reflect.chance": "Reflection Chance",
|
||||||
"stats.category.core.reflect.rate" : "Reflection Rate",
|
"stats.category.core.reflect.rate": "Reflection Rate",
|
||||||
"stats.category.core.resistance.base" : "Resistance",
|
"stats.category.core.resistance.base": "Resistance",
|
||||||
"stats.category.core.resistance.cold" : "Cold Resistance",
|
"stats.category.core.resistance.cold": "Cold Resistance",
|
||||||
"stats.category.core.resistance.gamma" : "Gamma Resistance",
|
"stats.category.core.resistance.gamma": "Gamma Resistance",
|
||||||
"stats.category.core.resistance.heat" : "Heat Resistance",
|
"stats.category.core.resistance.heat": "Heat Resistance",
|
||||||
"stats.category.core.resistance.ion" : "Ion Resistance",
|
"stats.category.core.resistance.ion": "Ion Resistance",
|
||||||
"stats.category.core.resistance.physical" : "Physical Resistance",
|
"stats.category.core.resistance.physical": "Physical Resistance",
|
||||||
"stats.category.core.resistance.plasma" : "Plasma Resistance",
|
"stats.category.core.resistance.plasma": "Plasma Resistance",
|
||||||
"_comment_Tabs" : "",
|
"_comment_Tabs": "",
|
||||||
"category.tabs.core.crafting" : "Crafting",
|
"category.tabs.core.crafting": "Crafting",
|
||||||
"category.tabs.core.dashboard" : "Dashboard",
|
"category.tabs.core.dashboard": "Dashboard",
|
||||||
"category.tabs.core.datapack" : "Datapack",
|
"category.tabs.core.datapack": "Datapack",
|
||||||
"category.tabs.core.dungeons" : "Dungeons",
|
"category.tabs.core.dungeons": "Dungeons",
|
||||||
"category.tabs.core.inventory" : "Inventory",
|
"category.tabs.core.inventory": "Inventory",
|
||||||
"category.tabs.core.quest" : "Quests",
|
"category.tabs.core.quest": "Quests",
|
||||||
"category.tabs.core.shop" : "Shop",
|
"category.tabs.core.shop": "Shop",
|
||||||
"category.tabs.core.skills" : "Skills"
|
"category.tabs.core.skills": "Skills"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,249 +1,253 @@
|
|||||||
{
|
{
|
||||||
"_comment_Admin" : "",
|
"_comment_Admin": "",
|
||||||
"admin.category.original.hostile_list" : "Hostile List",
|
"admin.category.original.hostile_list": "Hostile List",
|
||||||
"admin.category.original.item_list" : "Item List",
|
"admin.category.original.item_list": "Item List",
|
||||||
"admin.category.original.player_list" : "Player List",
|
"admin.category.original.player_list": "Player List",
|
||||||
"admin.category.original.item_list.all" : "All",
|
"admin.category.original.item_list.all": "All",
|
||||||
"admin.category.original.item_list.alloys" : "Alloys",
|
"admin.category.original.item_list.alloys": "Alloys",
|
||||||
"admin.category.original.item_list.circuits" : "Circuits",
|
"admin.category.original.item_list.circuits": "Circuits",
|
||||||
"admin.category.original.item_list.customizables" : "Customizables",
|
"admin.category.original.item_list.customizables": "Customizables",
|
||||||
"admin.category.original.item_list.ingots" : "Ingots",
|
"admin.category.original.item_list.ingots": "Ingots",
|
||||||
"admin.category.original.item_list.maerials" : "Materials",
|
"admin.category.original.item_list.maerials": "Materials",
|
||||||
"admin.category.original.item_list.ores" : "Ores",
|
"admin.category.original.item_list.ores": "Ores",
|
||||||
"admin.category.original.item_list.personal" : "Personal",
|
"admin.category.original.item_list.personal": "Personal",
|
||||||
"admin.category.original.item_list.ships" : "Ships",
|
"admin.category.original.item_list.ships": "Ships",
|
||||||
"admin.category.original.item_list.shields" : "Shields",
|
"admin.category.original.item_list.shields": "Shields",
|
||||||
"admin.category.original.item_list.weapons" : "Weapons",
|
"admin.category.original.item_list.weapons": "Weapons",
|
||||||
"admin.category.original.hostile_list.all" : "All",
|
"admin.category.original.hostile_list.all": "All",
|
||||||
"admin.category.original.hostile_list.ground" : "Ground Units",
|
"admin.category.original.hostile_list.ground": "Ground Units",
|
||||||
"admin.category.original.hostile_list.ships" : "Ships",
|
"admin.category.original.hostile_list.ships": "Ships",
|
||||||
"admin.category.original.player_list.all" : "All",
|
"admin.category.original.player_list.all": "All",
|
||||||
"admin.category.original.player_list.members" : "Members",
|
"admin.category.original.player_list.members": "Members",
|
||||||
"admin.category.original.player_list.moderators" : "Moderators",
|
"admin.category.original.player_list.moderators": "Moderators",
|
||||||
"admin.category.original.player_list.admins" : "Admins",
|
"admin.category.original.player_list.admins": "Admins",
|
||||||
"admin.category.original.item_list.materials" : "",
|
"_comment_Core_Systems": "",
|
||||||
"admin.category.original.hostile_list.ship" : "",
|
"core_systems.category.original.person.backpack": "Personal Backpack",
|
||||||
"_comment_Core_Systems" : "",
|
"core_systems.category.original.person.helmet": "Personal Helmet",
|
||||||
"core_systems.category.original.person.backpack" : "Personal Backpack",
|
"core_systems.category.original.person.suit": "Personal Suit",
|
||||||
"core_systems.category.original.person.helmet" : "Personal Helmet",
|
"core_systems.category.original.person.gloves": "Personal Gloves",
|
||||||
"core_systems.category.original.person.suit" : "Personal Suit",
|
"core_systems.category.original.person.boots": "Personal Boots",
|
||||||
"core_systems.category.original.person.gloves" : "Personal Gloves",
|
"core_systems.category.original.person.accessory_1": "Personal Accessory 1",
|
||||||
"core_systems.category.original.person.boots" : "Personal Boots",
|
"core_systems.category.original.person.accessory_2": "Personal Accessory 2",
|
||||||
"core_systems.category.original.person.accessory_1" : "Personal Accessory 1",
|
"core_systems.category.original.person.accessory_3": "Personal Accessory 3",
|
||||||
"core_systems.category.original.person.accessory_2" : "Personal Accessory 2",
|
"core_systems.category.original.person.accessory_4": "Personal Accessory 4",
|
||||||
"core_systems.category.original.person.accessory_3" : "Personal Accessory 3",
|
"core_systems.category.original.person.weapon": "Personal Weapon",
|
||||||
"core_systems.category.original.person.accessory_4" : "Personal Accessory 4",
|
"core_systems.category.original.ship.hull": "Ship Hull",
|
||||||
"core_systems.category.original.person.weapon" : "Personal Weapon",
|
"core_systems.category.original.ship.shields": "Ship Shield",
|
||||||
"core_systems.category.original.ship.hull" : "Ship Hull",
|
"core_systems.category.original.ship.engines": "Ship Engine",
|
||||||
"core_systems.category.original.ship.shields" : "Ship Shield",
|
"core_systems.category.original.ship.weapon_1": "Ship Weapon 1",
|
||||||
"core_systems.category.original.ship.engines" : "Ship Engine",
|
"core_systems.category.original.ship.weapon_2": "Ship Weapon 2",
|
||||||
"core_systems.category.original.ship.weapon_1" : "Ship Weapon 1",
|
"core_systems.category.original.ship.thruster_1": "Ship Thruster 1",
|
||||||
"core_systems.category.original.ship.weapon_2" : "Ship Weapon 2",
|
"core_systems.category.original.ship.thruster_2": "Ship Thruster 2",
|
||||||
"core_systems.category.original.ship.thruster_1" : "Ship Thruster 1",
|
"core_systems.category.original.ship.thruster_3": "Ship Thruster 3",
|
||||||
"core_systems.category.original.ship.thruster_2" : "Ship Thruster 2",
|
"core_systems.category.original.ship.thruster_4": "Ship Thruster 4",
|
||||||
"core_systems.category.original.ship.thruster_3" : "Ship Thruster 3",
|
"_comment_Dungeons": "",
|
||||||
"core_systems.category.original.ship.thruster_4" : "Ship Thruster 4",
|
"dungeons.original.pirate.pirates_outpost": "Pirate Outpost",
|
||||||
"_comment_Dungeons" : "",
|
"dungeons.original.pirate.pirates_outpost.desc": "A hidden supply station belonging to the Black Mark syndicate.",
|
||||||
"dungeons.original.pirate.pirates_outpost" : "Pirate Outpost",
|
"dungeons.original.tutorial.tutorial": "Tutorial",
|
||||||
"dungeons.original.pirate.pirates_outpost.desc" : "A hidden supply station belonging to the Black Mark syndicate.",
|
"dungeons.original.tutorial.tutorial.desc": "A one time dungeon.",
|
||||||
"dungeons.original.tutorial.tutorial" : "Tutorial",
|
"_comment_Enemies": "",
|
||||||
"dungeons.original.tutorial.tutorial.desc" : "A one time dungeon.",
|
"enemies.original.pirate.black_mark_heavy_cruiser": "Black Mark Heavy Cruiser",
|
||||||
"_comment_Enemies" : "",
|
"enemies.original.pirate.raider_frigate": "Raider Frigate",
|
||||||
"enemies.original.pirate.black_mark_heavy_cruiser" : "Black Mark Heavy Cruiser",
|
"enemies.original.pirate.snacher_clipper": "Snacher Clipper",
|
||||||
"enemies.original.pirate.raider_frigate" : "Raider Frigate",
|
"enemies.original.pirate.corvid_corvette": "Corvid Corvette",
|
||||||
"enemies.original.pirate.snacher_clipper" : "Snacher Clipper",
|
"enemies.original.pirate.scout_drone": "Scout Drone",
|
||||||
"enemies.original.pirate.corvid_corvette" : "Corvid Corvette",
|
"enemies.original.tutorial.tutorial_hostile": "Tutorial hostile",
|
||||||
"enemies.original.pirate.scout_drone" : "Scout Drone",
|
"enemies.original.tutorial.tutorial_boss_hostile": "Tutorial Boss",
|
||||||
"enemies.original.tutorial.tutorial_hostile" : "Tutorial hostile",
|
"_comment_Equipment_Personal": "",
|
||||||
"enemies.original.tutorial.tutorial_boss_hostile" : "Tutorial Boss",
|
"items.materials.original.personal.accessory.basic_personal_accessory": "Personal accessory",
|
||||||
"_comment_Equipment_Personal" : "",
|
"items.materials.original.personal.accessory.basic_personal_accessory.desc": "Test accessory",
|
||||||
"items.materials.original.personal.accessory.basic_personal_accessory" : "Personal accessory",
|
"items.materials.original.personal.backpack.basic_personal_backpack": "Personal backpack",
|
||||||
"items.materials.original.personal.accessory.basic_personal_accessory.desc" : "Test accessory",
|
"items.materials.original.personal.backpack.basic_personal_backpack.desc": "Test backpack",
|
||||||
"items.materials.original.personal.backpack.basic_personal_backpack" : "Personal backpack",
|
"items.materials.original.personal.armor.boots.basic_personal_boots": "Personal boots",
|
||||||
"items.materials.original.personal.backpack.basic_personal_backpack.desc" : "Test backpack",
|
"items.materials.original.personal.armor.boots.basic_personal_boots.desc": "Test boots",
|
||||||
"items.materials.original.personal.backpack.personal_shield." : "Personal Shield",
|
"items.materials.original.personal.armor.gloves.basic_personal_gloves": "Personal gloves",
|
||||||
"items.materials.original.personal.backpack.personal_shield.desc" : "Provides a lot of protection but lacks sufficent storage space",
|
"items.materials.original.personal.armor.gloves.basic_personal_gloves.desc": "Test gloves",
|
||||||
"items.materials.original.personal.armor.boots.basic_personal_boots" : "Personal boots",
|
"items.materials.original.personal.suit.basic_personal_suit": "Personal suit",
|
||||||
"items.materials.original.personal.armor.boots.basic_personal_boots.desc" : "Test boots",
|
"items.materials.original.personal.suit.basic_personal_suit.desc": "Test suit",
|
||||||
"items.materials.original.personal.armor.gloves.basic_personal_gloves" : "Personal gloves",
|
"items.materials.original.personal.weapon.basic_personal_weapon": "Personal weapon",
|
||||||
"items.materials.original.personal.armor.gloves.basic_personal_gloves.desc" : "Test gloves",
|
"items.materials.original.personal.weapon.basic_personal_weapon.desc": "Test weapon",
|
||||||
"items.materials.original.personal.suit.basic_personal_suit" : "Personal suit",
|
"items.materials.original.personal.backpack.personal_shield." : "Personal Shield",
|
||||||
"items.materials.original.personal.suit.basic_personal_suit.desc" : "Test suit",
|
"items.materials.original.personal.backpack.personal_shield.desc" : "Provides a lot of protection but lacks sufficent storage space",
|
||||||
"items.materials.original.personal.weapon.basic_personal_weapon" : "Personal weapon",
|
"_comment_Equipment_Ship": "",
|
||||||
"items.materials.original.personal.weapon.basic_personal_weapon.desc" : "Test weapon",
|
"items.materials.original.ship.engine.basic_ship_engines": "Ship engines",
|
||||||
"_comment_Equipment_Ship" : "",
|
"items.materials.original.ship.engine.basic_ship_engines.desc": "Test engines",
|
||||||
"items.materials.original.ship.engine.basic_ship_engines" : "Ship engines",
|
"items.materials.original.ship.engine.rtg.": "RTG",
|
||||||
"items.materials.original.ship.engine.basic_ship_engines.desc" : "Test engines",
|
"items.materials.original.ship.engine.rtg.desc": "very baisic and low power genarator with a long track record of reliability",
|
||||||
"items.materials.original.ship.engine.rtg." : "RTG",
|
"items.materials.original.ship.engine.gen1_fission_reactor": "Gen 1 nuclear reactor",
|
||||||
"items.materials.original.ship.engine.rtg.desc" : "very basic and low power genarator with a long track record of reliability",
|
"items.materials.original.ship.engine.gen1_fission_reactor.desc": "A boiling water reactor. Little more than a pile of glowing rocks in some hot water",
|
||||||
"items.materials.original.ship.engine.gen1_fission_reactor" : "Gen 1 nuclear reactor",
|
"items.materials.original.ship.engine.gen2_fission_reactor": "Gen 2 nuclear reactor",
|
||||||
"items.materials.original.ship.engine.gen1_fission_reactor.desc" : "A boiling water reactor. Little more than a pile of glowing rocks in some hot water",
|
"items.materials.original.ship.engine.gen2_fission_reactor.desc": "A Gas cooled reactor. Uses CO2 as a coolant allowing it to run hotter and at higher pressures and be much more power dense. The high power density and pressures comes at a price of reliability",
|
||||||
"items.materials.original.ship.engine.gen2_fission_reactor" : "Gen 2 nuclear reactor",
|
"items.materials.original.ship.engine.gen3_fission_reactor": "Gen 3 nuclear reactor",
|
||||||
"items.materials.original.ship.engine.gen2_fission_reactor.desc" : "A Gas cooled reactor. Uses CO2 as a coolant allowing it to run hotter and at higher pressures and be much more power dense. The high power density and pressures comes at a price of reliability",
|
"items.materials.original.ship.engine.gen3_fission_reactor.desc": "A Molten salt reactor. The pinical of fission power. Uses molten salt as its coolant allowing for very high temperatures and low pressures. Safer than most reactors",
|
||||||
"items.materials.original.ship.engine.gen3_fission_reactor" : "Gen 3 nuclear reactor",
|
"items.materials.original.ship.plating.basic_plating": "Ship plating",
|
||||||
"items.materials.original.ship.engine.gen3_fission_reactor.desc" : "A Molten salt reactor. The pinical of fission power. Uses molten salt as its coolant allowing for very high temperatures and low pressures. Safer than most reactors",
|
"items.materials.original.ship.plating.basic_plating.desc": "Better at stopping sing large hits",
|
||||||
"items.materials.original.ship.plating.basic_plating" : "Ship plating",
|
"items.materials.original.ship.plating.heavy_plating": "Heavy Plating",
|
||||||
"items.materials.original.ship.plating.basic_plating.desc" : "Better at stopping sing large hits",
|
"items.materials.original.ship.plating.heavy_plating.desc": "Strong plating capable of resisting high damage impacts",
|
||||||
"items.materials.original.ship.plating.heavy_plating" : "Heavy Plating",
|
"items.materials.original.ship.plating.reflective_plating.": "Reflective plating",
|
||||||
"items.materials.original.ship.plating.heavy_plating.desc" : "Strong plating capable of resisting high damage impacts",
|
"items.materials.original.ship.plating.reflective_plating.desc": "About as strong as heavy plating but reflects damage back at attackers",
|
||||||
"items.materials.original.ship.plating.reflective_plating." : "Reflective plating",
|
"items.materials.original.ship.shields.basic_shield": "Ship shield",
|
||||||
"items.materials.original.ship.plating.reflective_plating.desc" : "About as strong as heavy plating but reflects damage back at attackers",
|
"items.materials.original.ship.shields.basic_shield.desc": "Better at stopping many smaller hits. Weak to ion damage",
|
||||||
"items.materials.original.ship.shields.basic_shield" : "Ship shield",
|
"items.materials.original.ship.shields.heavy_shield.": "Heavy shield",
|
||||||
"items.materials.original.ship.shields.basic_shield.desc" : "Better at stopping many smaller hits. Weak to ion damage",
|
"items.materials.original.ship.shields.heavy_shield.desc": "A high capacity shild for dealing with lots of inbond threats",
|
||||||
"items.materials.original.ship.shields.heavy_shield." : "Heavy shield",
|
"items.materials.original.ship.shields.reflecter_shield.": "Reflecter Shields",
|
||||||
"items.materials.original.ship.shields.heavy_shield.desc" : "A high capacity shild for dealing with lots of inbond threats",
|
"items.materials.original.ship.shields.reflecter_shield.desc": "About as strong as heavy shields but reflects a lot of damage back at the attacker",
|
||||||
"items.materials.original.ship.shields.reflecter_shield." : "Reflecter Shields",
|
"items.materials.original.ship.thruster.basic_ship_thruster": "Ship thruster",
|
||||||
"items.materials.original.ship.shields.reflecter_shield.desc" : "About as strong as heavy shields but reflects a lot of damage back at the attacker",
|
"items.materials.original.ship.thruster.basic_ship_thruster.desc": "Test thruster",
|
||||||
"items.materials.original.ship.thruster.basic_ship_thruster" : "Ship thruster",
|
"items.materials.original.ship.thruster.big_ship_thruster": "Big ship thruster",
|
||||||
"items.materials.original.ship.thruster.basic_ship_thruster.desc" : "Test thruster",
|
"items.materials.original.ship.thruster.big_ship_thruster.desc": "Used as a main drive for a ship",
|
||||||
"items.materials.original.ship.thruster.big_ship_thruster" : "Big ship thruster",
|
"items.materials.original.ship.weapon.basic_ship_weapon": "Ship weapon",
|
||||||
"items.materials.original.ship.thruster.big_ship_thruster.desc" : "Used as a main drive for a ship",
|
"items.materials.original.ship.weapon.basic_ship_weapon.desc": "Test weapon",
|
||||||
"items.materials.original.ship.weapon.basic_ship_weapon" : "Ship weapon",
|
"items.materials.original.ship.weapon.unstable_partical_cannon": "Unstable partical cannon",
|
||||||
"items.materials.original.ship.weapon.basic_ship_weapon.desc" : "Test weapon",
|
"items.materials.original.ship.weapon.unstable_partical_cannon.desc": "Does a lot of damage in a small space but to power it requires a lot of changes to the ships internals making it more prone to damage",
|
||||||
"items.materials.original.ship.weapon.unstable_partical_cannon" : "Unstable partical cannon",
|
"items.materials.original.ship.weapon.serpent_missiles" : "Serpent Missiles",
|
||||||
"items.materials.original.ship.weapon.unstable_partical_cannon.desc" : "Does a lot of damage in a small space but to power it requires a lot of changes to the ships internals making it more prone to damage",
|
"items.materials.original.ship.weapon.serpent_missiles.desc" : "Missiles do more damage for their tier but prone to magazine detonations",
|
||||||
"items.materials.original.ship.weapon.serpent_missiles" : "Serpent Missiles",
|
"_comment_Materials": "",
|
||||||
"items.materials.original.ship.weapon.serpent_missiles.desc" : "Missiles do more damage for their tier but prone to magazine detonations",
|
"items.materials.original.bio.bio_pulp": "Bio pulp",
|
||||||
"_comment_Materials" : "",
|
"items.materials.original.bio.bio_pulp.desc": "A pile of biological material.",
|
||||||
"items.materials.original.bio.bio_pulp" : "Bio pulp",
|
"items.materials.original.alloys.steel": "Steel ingot",
|
||||||
"items.materials.original.bio.bio_pulp.desc" : "A pile of biological material.",
|
"items.materials.original.alloys.steel.desc": "A steel ingot.",
|
||||||
"items.materials.original.alloys.steel" : "Steel ingot",
|
"items.materials.original.alloys.titanium_weave": "Titanium weave",
|
||||||
"items.materials.original.alloys.steel.desc" : "A steel ingot.",
|
"items.materials.original.alloys.titanium_weave.desc": "used where flexibility does not compromise strength",
|
||||||
"items.materials.original.alloys.titanium_weave" : "Titanium weave",
|
"items.materials.original.alloys.void_steel": "Void steel",
|
||||||
"items.materials.original.alloys.titanium_weave.desc" : "used where flexibility does not compromise strength",
|
"items.materials.original.alloys.void_steel.desc": "Steel that is very strong and increadably light absorbent",
|
||||||
"items.materials.original.alloys.void_steel" : "Void steel",
|
"items.materials.original.alloys.chronotanium": "Chronotanium ingot",
|
||||||
"items.materials.original.alloys.void_steel.desc" : "Steel that is very strong and increadably light absorbent",
|
"items.materials.original.alloys.chronotanium.desc": "A chronite-titianium alloy for strong and energetic applications",
|
||||||
"items.materials.original.alloys.chronotanium" : "Chronotanium ingot",
|
"items.materials.original.alloys.neutronium_composite": "Neutronium composite",
|
||||||
"items.materials.original.alloys.chronotanium.desc" : "A chronite-titianium alloy for strong and energetic applications",
|
"items.materials.original.alloys.neutronium_composite.desc": "A compostite of neutronium of",
|
||||||
"items.materials.original.alloys.neutronium_composite" : "Neutronium composite",
|
"items.materials.original.alloys.superconductor": "Superconductor",
|
||||||
"items.materials.original.alloys.neutronium_composite.desc" : "A compostite of neutronium of",
|
"items.materials.original.alloys.superconductor.desc": "A material with no resistance and expells all magnetic fields very usefull in high energy compnents",
|
||||||
"items.materials.original.alloys.superconductor" : "Superconductor",
|
"items.materials.original.circuits.basic": "Basic Circuit",
|
||||||
"items.materials.original.alloys.superconductor.desc" : "A material with no resistance and expells all magnetic fields very usefull in high energy compnents",
|
"items.materials.original.circuits.basic.desc": "Basic electronics used in simple electromecanical systems. Probably made in someone's shed. Rated for common Tier systems.",
|
||||||
"items.materials.original.circuits.basic" : "Basic Circuit",
|
"items.materials.original.circuits.advanced": "Advanced Circuit",
|
||||||
"items.materials.original.circuits.basic.desc" : "Basic electronics used in simple electromecanical systems. Probably made in someone's shed. Rated for common Tier systems.",
|
"items.materials.original.circuits.advanced.desc": "Advanced electronics used in electromecanical systems, featuring transistors for compact switching. Made with industrial Machines. Rated for uncommon Tier systems.",
|
||||||
"items.materials.original.circuits.advanced" : "Advanced Circuit",
|
"items.materials.original.circuits.processing_unit": "Processing unit",
|
||||||
"items.materials.original.circuits.advanced.desc" : "Advanced electronics used in electromecanical systems, featuring transistors for compact switching. Made with industrial Machines. Rated for uncommon Tier systems.",
|
"items.materials.original.circuits.processing_unit.desc": "Highly Advanced electronics used in demanding systems, featuring Integrated circuts replacing entire boards. Made with precision UV lithography machines. Rated for Rare Tier systems.",
|
||||||
"items.materials.original.circuits.processing_unit" : "Processing unit",
|
"items.materials.original.circuits.quantum_processor": "Quantum Processor",
|
||||||
"items.materials.original.circuits.processing_unit.desc" : "Highly Advanced electronics used in demanding systems, featuring Integrated circuts replacing entire boards. Made with precision UV lithography machines. Rated for Rare Tier systems.",
|
"items.materials.original.circuits.quantum_processor.desc": "Increadably electronics used in complex systems, featuring quantum cores for unparalleled parallel computation. Made with . Rated for Epic Tier systems.",
|
||||||
"items.materials.original.circuits.quantum_processor" : "Quantum Processor",
|
"items.materials.original.circuits.ai_core": "Ai Core",
|
||||||
"items.materials.original.circuits.quantum_processor.desc" : "Increadably electronics used in complex systems, featuring quantum cores for unparalleled parallel computation. Made with . Rated for Epic Tier systems.",
|
"items.materials.original.circuits.ai_core.desc": "A semi sapient general intelligence, featuring advanced reasoning skills and simulated simulations, it will never truly know if it is in another simulation. Made under incudulus supervison and adhears to strict laws. Warrrenty void if not reset every terran standerd season.",
|
||||||
"items.materials.original.circuits.ai_core" : "Ai Core",
|
"items.materials.original.crystal.flux": "Flux crystal",
|
||||||
"items.materials.original.circuits.ai_core.desc" : "A semi sapient general intelligence, featuring advanced reasoning skills and simulated simulations, it will never truly know if it is in another simulation. Made under incudulus supervison and adhears to strict laws. Warrrenty void if not reset every terran standerd season.",
|
"items.materials.original.crystal.flux.desc": "A crystal whose properties are in constant flux. Commenly used in high power electrical aplications",
|
||||||
"items.materials.original.crystal.flux" : "Flux crystal",
|
"items.materials.original.crystal.flux_core": "Flux Core",
|
||||||
"items.materials.original.crystal.flux.desc" : "A crystal whose properties are in constant flux. Commenly used in high power electrical aplications",
|
"items.materials.original.crystal.flux_core.desc": "The crystal tamed can nhow be used in more demanding applications",
|
||||||
"items.materials.original.crystal.flux_core" : "Flux Core",
|
"items.materials.original.crystal.void": "Void crystal",
|
||||||
"items.materials.original.crystal.flux_core.desc" : "The crystal tamed can nhow be used in more demanding applications",
|
"items.materials.original.crystal.void.desc": "A crystal that seems to sap the very light from the room. Commenly used in armor and stealth applications",
|
||||||
"items.materials.original.crystal.void" : "Void crystal",
|
"items.materials.original.crystal.dimentional": "Dimentional crystal",
|
||||||
"items.materials.original.crystal.void.desc" : "A crystal that seems to sap the very light from the room. Commenly used in armor and stealth applications",
|
"items.materials.original.crystal.dimentional.desc": "reality warps at its edges, imagine the possibilities",
|
||||||
"items.materials.original.crystal.dimentional" : "Dimentional crystal",
|
"items.materials.original.crystal.neutronium": "Neutronium",
|
||||||
"items.materials.original.crystal.dimentional.desc" : "reality warps at its edges, imagine the possibilities",
|
"items.materials.original.crystal.neutronium.desc": "A hyper dense piece of along dead star",
|
||||||
"items.materials.original.crystal.neutronium" : "Neutronium",
|
"items.materials.original.ingots.aluminum": "Aluminum ingot",
|
||||||
"items.materials.original.crystal.neutronium.desc" : "A hyper dense piece of along dead star",
|
"items.materials.original.ingots.aluminum.desc": "An aluminum ingot.",
|
||||||
"items.materials.original.ingots.aluminum" : "Aluminum ingot",
|
"items.materials.original.ingots.carbon": "carbon ingot",
|
||||||
"items.materials.original.ingots.aluminum.desc" : "An aluminum ingot.",
|
"items.materials.original.ingots.carbon.desc": "A carbon ingot.",
|
||||||
"items.materials.original.ingots.carbon" : "carbon ingot",
|
"items.materials.original.ingots.chronite": "Chronite Ingot",
|
||||||
"items.materials.original.ingots.carbon.desc" : "A carbon ingot.",
|
"items.materials.original.ingots.chronite.desc": "A chronite ingot.",
|
||||||
"items.materials.original.ingots.chronite" : "Chronite Ingot",
|
"items.materials.original.ingots.copper": "Copper ingot",
|
||||||
"items.materials.original.ingots.chronite.desc" : "A chronite ingot.",
|
"items.materials.original.ingots.copper.desc": "A copper ingot.",
|
||||||
"items.materials.original.ingots.copper" : "Copper ingot",
|
"items.materials.original.ingots.gold": "Gold ingot",
|
||||||
"items.materials.original.ingots.copper.desc" : "A copper ingot.",
|
"items.materials.original.ingots.gold.desc": "A gold ingot.",
|
||||||
"items.materials.original.ingots.gold" : "Gold ingot",
|
"items.materials.original.ingots.iron": "Iron Ingot",
|
||||||
"items.materials.original.ingots.gold.desc" : "A gold ingot.",
|
"items.materials.original.ingots.iron.desc": "A iron ingot.",
|
||||||
"items.materials.original.ingots.iron" : "Iron Ingot",
|
"items.materials.original.ingots.titanium": "Titanium ingot",
|
||||||
"items.materials.original.ingots.iron.desc" : "A iron ingot.",
|
"items.materials.original.ingots.titanium.desc": "A titanium ingot.",
|
||||||
"items.materials.original.ingots.titanium" : "Titanium ingot",
|
"items.materials.original.ingots.tungsten": "Tungsten ingot",
|
||||||
"items.materials.original.ingots.titanium.desc" : "A titanium ingot.",
|
"items.materials.original.ingots.tungsten.desc": "A tungsten ingot.",
|
||||||
"items.materials.original.ingots.tungsten" : "Tungsten ingot",
|
"items.materials.original.ores.bauxite": "Bauxite ore",
|
||||||
"items.materials.original.ingots.tungsten.desc" : "A tungsten ingot.",
|
"items.materials.original.ores.bauxite.desc": "A pile of bauxite ore.",
|
||||||
"items.materials.original.ores.bauxite" : "Bauxite ore",
|
"items.materials.original.ores.chronite": "Chronium ore",
|
||||||
"items.materials.original.ores.bauxite.desc" : "A pile of bauxite ore.",
|
"items.materials.original.ores.chronite.desc": "A pile of chronium ore.",
|
||||||
"items.materials.original.ores.chronite" : "Chronium ore",
|
"items.materials.original.ores.coal": "Coal ore",
|
||||||
"items.materials.original.ores.chronite.desc" : "A pile of chronium ore.",
|
"items.materials.original.ores.coal.desc": "A pile of coal ore.",
|
||||||
"items.materials.original.ores.coal" : "Coal ore",
|
"items.materials.original.ores.copper": "Copper ore",
|
||||||
"items.materials.original.ores.coal.desc" : "A pile of coal ore.",
|
"items.materials.original.ores.copper.desc": "A pile of copper ore.",
|
||||||
"items.materials.original.ores.copper" : "Copper ore",
|
"items.materials.original.ores.gold": "Gold ore",
|
||||||
"items.materials.original.ores.copper.desc" : "A pile of copper ore.",
|
"items.materials.original.ores.gold.desc": "A pile of gold ore.",
|
||||||
"items.materials.original.ores.gold" : "Gold ore",
|
"items.materials.original.ores.ilunite": "Ilunite ore",
|
||||||
"items.materials.original.ores.gold.desc" : "A pile of gold ore.",
|
"items.materials.original.ores.ilunite.desc": "A pile of ilunite ore.",
|
||||||
"items.materials.original.ores.ilunite" : "Ilunite ore",
|
"items.materials.original.ores.iron": "Iron ore",
|
||||||
"items.materials.original.ores.ilunite.desc" : "A pile of ilunite ore.",
|
"items.materials.original.ores.iron.desc": "A pile of iron ore.",
|
||||||
"items.materials.original.ores.iron" : "Iron ore",
|
"items.materials.original.ores.wolframite": "Wolframite ore",
|
||||||
"items.materials.original.ores.iron.desc" : "A pile of iron ore.",
|
"items.materials.original.ores.wolframite.desc": "A pile of wolframite ore.",
|
||||||
"items.materials.original.ores.wolframite" : "Wolframite ore",
|
"items.materials.original.plating.basic_ship_plating": "Ship plating",
|
||||||
"items.materials.original.ores.wolframite.desc" : "A pile of wolframite ore.",
|
"items.materials.original.plating.basic_ship_plating.desc": "Just basic ship plating.",
|
||||||
"items.materials.original.plating.basic_ship_plating" : "Ship plating",
|
"_comment_Quests": "",
|
||||||
"items.materials.original.plating.basic_ship_plating.desc" : "Just basic ship plating.",
|
"quests.category.original.story": "Story",
|
||||||
"_comment_Quests" : "",
|
"quests.category.original.daily": "Daily",
|
||||||
"quests.category.original.story" : "Story",
|
"quests.category.original.weekly": "Weekly",
|
||||||
"quests.category.original.daily" : "Daily",
|
"quests.category.original.monthly": "Monthly",
|
||||||
"quests.category.original.weekly" : "Weekly",
|
"quests.category.original.seasonal": "Seasons",
|
||||||
"quests.category.original.monthly" : "Monthly",
|
"quests.original.tutorial.starter_kit": "Starter Kit: Neural Link",
|
||||||
"quests.category.original.seasonal" : "Seasons",
|
"quests.tutorial.slay_boss.name": "Trial by Fire",
|
||||||
"_comment_Recipes" : "",
|
"quests.tutorial.slay_boss.desc": "Prove your combat capabilities by neutralizing the Tutorial Boss unit.",
|
||||||
"recipes.category.original.alloys" : "Alloys",
|
"quests.tutorial.slay_boss.obj1": "Defeat the Tutorial Boss",
|
||||||
"recipes.category.original.circuits" : "Circuits",
|
"_comment_Recipes": "",
|
||||||
"recipes.category.original.crystals" : "Crystals",
|
"recipes.category.original.alloys": "Alloys",
|
||||||
"recipes.category.original.food" : "Food",
|
"recipes.category.original.circuits": "Circuits",
|
||||||
"recipes.category.original.forging" : "Forging",
|
"recipes.category.original.crystals": "Crystals",
|
||||||
"recipes.category.original.hull_sections" : "Hull Sections",
|
"recipes.category.original.food": "Food",
|
||||||
"recipes.category.original.hulls" : "Hulls",
|
"recipes.category.original.forging": "Forging",
|
||||||
"recipes.category.original.organics" : "Organics",
|
"recipes.category.original.hull_sections": "Hull Sections",
|
||||||
"recipes.category.original.spacesuit_parts" : "Spacesuit Parts",
|
"recipes.category.original.hulls": "Hulls",
|
||||||
"_comment_Shop" : "",
|
"recipes.category.original.organics": "Organics",
|
||||||
"shop.category.original.consumables" : "Consumables",
|
"recipes.category.original.spacesuit_parts": "Spacesuit Parts",
|
||||||
"shop.category.original.defence" : "Defence",
|
"_comment_Shop": "",
|
||||||
"shop.category.original.featured" : "Featured",
|
"shop.category.original.consumables": "Consumables",
|
||||||
"shop.category.original.materials" : "Materials",
|
"shop.category.original.defence": "Defence",
|
||||||
"shop.category.original.premium" : "Premium",
|
"shop.category.original.featured": "Featured",
|
||||||
"shop.category.original.ships" : "Ships",
|
"shop.category.original.materials": "Materials",
|
||||||
"shop.category.original.weapons" : "Weapons",
|
"shop.category.original.premium": "Premium",
|
||||||
"shop.category.original.personal.equipment" : "Personal Equipment",
|
"shop.category.original.ships": "Ships",
|
||||||
"shop.category.original.ship.equipment" : "Ship Equipment",
|
"shop.category.original.weapons": "Weapons",
|
||||||
"_comment_Skills" : "",
|
"shop.category.original.personal.equipment" : "Personal Equipment",
|
||||||
"skills.category.original.combat" : "Combat",
|
"shop.category.original.ship.equipment" : "Ship Equipment",
|
||||||
"skills.category.original.combat.weapon_effiency" : "Weapon Effiency",
|
"_comment_Skills": "",
|
||||||
"skills.category.original.combat.weapon_effiency.desc" : "Let's get those weapons better!",
|
"skills.category.original.combat": "Combat",
|
||||||
"skills.category.original.crafting" : "Crafting",
|
"skills.category.original.combat.weapon_effiency": "Weapon Effiency",
|
||||||
"skills.category.original.crafting.forging" : "Forging",
|
"skills.category.original.combat.weapon_effiency.desc": "Let's get those weapons better!",
|
||||||
"skills.category.original.crafting.forging.desc" : "To forge the basics.",
|
"skills.category.original.crafting": "Crafting",
|
||||||
"skills.category.original.crafting.alloying" : "Alloying",
|
"skills.category.original.crafting.blacksmithing": "Blacksmithing",
|
||||||
"skills.category.original.crafting.alloying.desc" : "Lets start alloy making.",
|
"skills.category.original.crafting.blacksmithing.desc": "To forge the basics.",
|
||||||
"skills.category.original.science" : "Science",
|
"skills.category.original.crafting.alloying": "Alloying",
|
||||||
"skills.category.original.science.alien_technology" : "Alien Technology",
|
"skills.category.original.crafting.alloying.desc": "Lets start alloy making.",
|
||||||
"skills.category.original.science.alien_technology.desc" : "Unknown Mysterious Tech",
|
"skills.category.original.science": "Science",
|
||||||
"skills.category.original.science.biology_engineering" : "Biology Engineering",
|
"skills.category.original.science.alien_technology": "Alien Technology",
|
||||||
"skills.category.original.science.biology_engineering.desc" : "Maybe we will unlock bio-tech?",
|
"skills.category.original.science.alien_technology.desc": "Unknown Mysterious Tech",
|
||||||
"skills.category.original.crafting.ship_manufacturing" : "Ship Manufacturing",
|
"skills.category.original.science.biology_engineering": "Biology Engineering",
|
||||||
"skills.category.original.crafting.ship_manufacturing.desc" : "To build a ship to sail the stars",
|
"skills.category.original.science.biology_engineering.desc": "Maybe we will unlock bio-tech?",
|
||||||
"skills.category.original.crafting.high_energetics" : "High Energetics",
|
"skills.category.original.crafting.ship_manufacturing" : "Ship Manufacturing",
|
||||||
"skills.category.original.crafting.high_energetics.desc" : "Learn how to make and manage reactors and energy weapons",
|
"skills.category.original.crafting.ship_manufacturing.desc" : "To build a ship to sail the stars",
|
||||||
"_comment_Stats" : "",
|
"skills.category.original.crafting.high_energetics" : "High Energetics",
|
||||||
"stats.category.original.attack.base" : "Attack",
|
"skills.category.original.crafting.high_energetics.desc" : "Learn how to make and manage reactors and energy weapons",
|
||||||
"stats.category.original.attack.chance" : "Attack Chance",
|
"_comment_Stats": "",
|
||||||
"stats.category.original.attack.rate" : "Attack Rate",
|
"stats.category.original.attack.base": "Attack",
|
||||||
"stats.category.original.defence.base" : "Defence",
|
"stats.category.original.attack.chance": "Attack Chance",
|
||||||
"stats.category.original.defence.chance" : "Defence Chance",
|
"stats.category.original.attack.rate": "Attack Rate",
|
||||||
"stats.category.original.defence.rate" : "Defence Rate",
|
"stats.category.original.defence.base": "Defence",
|
||||||
"stats.category.original.health" : "Health",
|
"stats.category.original.defence.chance": "Defence Chance",
|
||||||
"stats.category.original.penetration.base" : "Penetration",
|
"stats.category.original.defence.rate": "Defence Rate",
|
||||||
"stats.category.original.penetration.chance" : "Penetration Chance",
|
"stats.category.original.health": "Health",
|
||||||
"stats.category.original.penetration.rate" : "Penetration Rate",
|
"stats.category.original.penetration.base": "Penetration",
|
||||||
"stats.category.original.reflect.base" : "Reflect",
|
"stats.category.original.penetration.chance": "Penetration Chance",
|
||||||
"stats.category.original.reflect.chance" : "Reflection Chance",
|
"stats.category.original.penetration.rate": "Penetration Rate",
|
||||||
"stats.category.original.reflect.rating" : "Reflection Rating",
|
"stats.category.original.reflect.base": "Reflect",
|
||||||
"stats.category.original.resistance.base" : "Resistance",
|
"stats.category.original.reflect.chance": "Reflection Chance",
|
||||||
"stats.category.original.resistance.cold" : "Cold Resistance",
|
"stats.category.original.reflect.rating": "Reflection Rating",
|
||||||
"stats.category.original.resistance.gamma" : "Gamma Resistance",
|
"stats.category.original.resistance.base": "Resistance",
|
||||||
"stats.category.original.resistance.heat" : "Heat Resistance",
|
"stats.category.original.resistance.cold": "Cold Resistance",
|
||||||
"stats.category.original.resistance.ion" : "Ion Resistance",
|
"stats.category.original.resistance.gamma": "Gamma Resistance",
|
||||||
"stats.category.original.resistance.physical" : "Physical Resistance",
|
"stats.category.original.resistance.heat": "Heat Resistance",
|
||||||
"stats.category.original.resistance.plasma" : "Plasma Resistance",
|
"stats.category.original.resistance.ion": "Ion Resistance",
|
||||||
"_comment_Tabs" : "",
|
"stats.category.original.resistance.physical": "Physical Resistance",
|
||||||
"category.tabs.original.crafting" : "Crafting",
|
"stats.category.original.resistance.plasma": "Plasma Resistance",
|
||||||
"category.tabs.original.dashboard" : "Dashboard",
|
"_comment_Tabs": "",
|
||||||
"category.tabs.original.datapack" : "Debug Tab",
|
"category.tabs.original.crafting": "Crafting",
|
||||||
"category.tabs.original.dungeons" : "Dungeons",
|
"category.tabs.original.dashboard": "Dashboard",
|
||||||
"category.tabs.original.inventory" : "Inventory",
|
"category.tabs.original.datapack": "Debug Tab",
|
||||||
"category.tabs.original.shop" : "Shop",
|
"category.tabs.original.dungeons": "Dungeons",
|
||||||
"category.tabs.original.skills" : "Skills"
|
"category.tabs.original.inventory": "Inventory",
|
||||||
}
|
"category.tabs.original.shop": "Shop",
|
||||||
|
"category.tabs.original.skills": "Skills",
|
||||||
|
"category.tabs.original.quests": "Quests"
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"id": "original:tutorial/tutorial_boss_hostile",
|
"id": "original:tutorial/tutorial_boss_hostile",
|
||||||
"displayName": "enemies.original.tutorial.tutorial_boss_hostile",
|
"displayName": "enemies.original.tutorial.tutorial_boss_hostile",
|
||||||
"stats": {
|
"stats": {
|
||||||
"health": 90,
|
"health": 40,
|
||||||
"defense": 1.3,
|
"defense": 1.3,
|
||||||
"damage": 4,
|
"damage": 4,
|
||||||
"critical.chance": 0.3,
|
"critical.chance": 0.3,
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
"loot": [
|
"loot": [
|
||||||
{
|
{
|
||||||
"id": "original:alloy_steel",
|
"id": "original:alloy_steel",
|
||||||
"chance": 0.4,
|
"chance": 0.8,
|
||||||
"count": {
|
"count": {
|
||||||
"min": 1,
|
"min": 1,
|
||||||
"max": 2
|
"max": 2
|
||||||
|
|||||||
@ -3,7 +3,10 @@
|
|||||||
"id": "original:tutorial/tutorial_enemy_room",
|
"id": "original:tutorial/tutorial_enemy_room",
|
||||||
"displayName": "rooms.original.tutorial.tutorial_enemy_room.name",
|
"displayName": "rooms.original.tutorial.tutorial_enemy_room.name",
|
||||||
"description": "rooms.original.tutorial.tutorial_enemy_room.desc",
|
"description": "rooms.original.tutorial.tutorial_enemy_room.desc",
|
||||||
"hostiles": ["original:tutorial/tutorial_hostile"],
|
"hostiles": [
|
||||||
|
"original:tutorial/tutorial_hostile",
|
||||||
|
"original:tutorial/tutorial_hostile"
|
||||||
|
],
|
||||||
"gainXp": 3,
|
"gainXp": 3,
|
||||||
"credits": 30,
|
"credits": 30,
|
||||||
"loot": [],
|
"loot": [],
|
||||||
|
|||||||
@ -1,18 +1,15 @@
|
|||||||
{
|
{
|
||||||
"plating": {
|
"weapons": {
|
||||||
"id": "original:basic_personal_weapon",
|
"id": "original:basic_personal_weapon",
|
||||||
"displayName": "items.materials.original.personal.weapon.basic_personal_weapon",
|
"displayName": "items.materials.original.personal.weapon.basic_personal_weapon",
|
||||||
"description": "items.materials.original.personal.weapon.basic_personal_weapon.desc",
|
"description": "items.materials.original.personal.weapon.basic_personal_weapon.desc",
|
||||||
"texture": "original/assets/textures/equipment/personal/weapon/basic_weapon.png",
|
"texture": "original/assets/textures/equipment/personal/weapon/basic_weapon.png",
|
||||||
"stats": {
|
"stats": {
|
||||||
"health": 15,
|
"attack.base": 20
|
||||||
"resistance.base": 0.125,
|
|
||||||
"defence.rating": 0.9,
|
|
||||||
"reflect.chance": 0.0125
|
|
||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"rarity": "common",
|
"rarity": "common",
|
||||||
"equipmentSlot": "original:personal_suit",
|
"equipmentSlot": "original:personal_weapons",
|
||||||
"storeCategory": "original:personal",
|
"storeCategory": "original:personal",
|
||||||
"dungeon": "ground"
|
"dungeon": "ground"
|
||||||
}
|
}
|
||||||
55
game-server/datapacks/original/data/quests/starter_kit.json
Normal file
55
game-server/datapacks/original/data/quests/starter_kit.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"quest": {
|
||||||
|
"id": "original:tutorial/starter_kit",
|
||||||
|
"displayName": "quests.original.tutorial.starter_kit",
|
||||||
|
"description": "Welcome, Commander. Your neural link is active. Initial equipment has been authorized.",
|
||||||
|
"category": "STORY",
|
||||||
|
"minLevel": 1,
|
||||||
|
|
||||||
|
"objectives": [
|
||||||
|
{
|
||||||
|
"type": "LOGIN",
|
||||||
|
"requiredAmount": 1,
|
||||||
|
"currentAmount": 0,
|
||||||
|
"description": "Initialize system uplink"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"rewards": {
|
||||||
|
"xp": 50,
|
||||||
|
"credits": 500,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "original:basic_personal_accessory",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "original:basic_personal_backpack",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "original:basic_personal_boots",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "original:basic_personal_gloves",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "original:basic_personal_suit",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "original:basic_personal_weapon",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"meta": {
|
||||||
|
"autoAccept": true,
|
||||||
|
"autoComplete": false,
|
||||||
|
"priority": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"quest": {
|
||||||
|
"id": "original:tutorial/slay_tutorial_boss",
|
||||||
|
"displayName": "quests.tutorial.slay_boss.name",
|
||||||
|
"description": "quests.tutorial.slay_boss.desc",
|
||||||
|
"category": "STORY",
|
||||||
|
"meta": {
|
||||||
|
"autoAccept": true,
|
||||||
|
"priority": 10
|
||||||
|
},
|
||||||
|
"objectives": [
|
||||||
|
{
|
||||||
|
"type": "KILL_ENEMY",
|
||||||
|
"targetId": "original:tutorial/tutorial_boss_hostile",
|
||||||
|
"requiredAmount": 1,
|
||||||
|
"description": "quests.tutorial.slay_boss.obj1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rewards": {
|
||||||
|
"credits": 500,
|
||||||
|
"xp": 1000,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "original:materials/data_core",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,7 +32,7 @@ sequelize.initDatabase = async () => {
|
|||||||
await sequelize.query("PRAGMA foreign_keys = OFF;");
|
await sequelize.query("PRAGMA foreign_keys = OFF;");
|
||||||
}
|
}
|
||||||
|
|
||||||
await sequelize.sync({ alter: true });
|
await sequelize.sync();
|
||||||
|
|
||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
await sequelize.query("PRAGMA foreign_keys = ON;");
|
await sequelize.query("PRAGMA foreign_keys = ON;");
|
||||||
|
|||||||
125
game-server/src/game/CombatService.js
Normal file
125
game-server/src/game/CombatService.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
const DatapackLoader = require("./DatapackLoader");
|
||||||
|
|
||||||
|
class CombatService {
|
||||||
|
initializeBattle(player, hostiles) {
|
||||||
|
const equipmentStats = this.calculateEquipmentStats(player.equipment);
|
||||||
|
|
||||||
|
const maxHp = 100 + (equipmentStats.health || 0);
|
||||||
|
const atk = 25 + (equipmentStats.attack || 0);
|
||||||
|
|
||||||
|
const battle = {
|
||||||
|
player: {
|
||||||
|
id: player.id,
|
||||||
|
name: player.username || "Commander",
|
||||||
|
hp: maxHp,
|
||||||
|
maxHp: maxHp,
|
||||||
|
atk: atk,
|
||||||
|
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: [],
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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") {
|
||||||
|
totals.health += value;
|
||||||
|
} else if (
|
||||||
|
lowerKey.includes("attack") ||
|
||||||
|
lowerKey.includes("damage") ||
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return totals;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAttack(battle, targetInstanceId) {
|
||||||
|
const attackerId = battle.turnOrder[battle.currentTurnIndex];
|
||||||
|
const log = [];
|
||||||
|
|
||||||
|
if (attackerId === "player") {
|
||||||
|
const target = battle.enemies.find(
|
||||||
|
(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;
|
||||||
|
target.isDead = true;
|
||||||
|
log.push(`${target.name} destroyed!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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(
|
||||||
|
1,
|
||||||
|
Math.round(enemy.atk - playerDef * 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new CombatService();
|
||||||
@ -11,6 +11,7 @@ class DatapackLoader {
|
|||||||
dungeons: new Map(),
|
dungeons: new Map(),
|
||||||
enemies: new Map(),
|
enemies: new Map(),
|
||||||
rooms: new Map(),
|
rooms: new Map(),
|
||||||
|
quests: new Map(),
|
||||||
languages: new Map(),
|
languages: new Map(),
|
||||||
manifest: {},
|
manifest: {},
|
||||||
};
|
};
|
||||||
@ -70,9 +71,10 @@ class DatapackLoader {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`🚀 Registry Ready: ${this.registry.items.size} Items, ${this.registry.dungeons.size} Dungeons, ${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`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecipe(id) {
|
getRecipe(id) {
|
||||||
return this.registry.recipes.get(id);
|
return this.registry.recipes.get(id);
|
||||||
}
|
}
|
||||||
@ -89,6 +91,7 @@ class DatapackLoader {
|
|||||||
);
|
);
|
||||||
return Array.from(categories);
|
return Array.from(categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadLanguages(langPath) {
|
loadLanguages(langPath) {
|
||||||
try {
|
try {
|
||||||
const files = fs.readdirSync(langPath).filter((f) => f.endsWith(".json"));
|
const files = fs.readdirSync(langPath).filter((f) => f.endsWith(".json"));
|
||||||
@ -156,7 +159,6 @@ class DatapackLoader {
|
|||||||
case "weapons":
|
case "weapons":
|
||||||
data.type = typeKey;
|
data.type = typeKey;
|
||||||
this.registry.items.set(fullId, data);
|
this.registry.items.set(fullId, data);
|
||||||
this.registry.items.set(data.id, data);
|
|
||||||
break;
|
break;
|
||||||
case "recipe":
|
case "recipe":
|
||||||
const recipeId = json.craft?.id || data.id;
|
const recipeId = json.craft?.id || data.id;
|
||||||
@ -164,19 +166,18 @@ class DatapackLoader {
|
|||||||
break;
|
break;
|
||||||
case "skills":
|
case "skills":
|
||||||
this.registry.skills.set(fullId, data);
|
this.registry.skills.set(fullId, data);
|
||||||
this.registry.skills.set(data.id, data);
|
|
||||||
break;
|
break;
|
||||||
case "dungeon":
|
case "dungeon":
|
||||||
this.registry.dungeons.set(fullId, data);
|
this.registry.dungeons.set(fullId, data);
|
||||||
this.registry.dungeons.set(data.id, data);
|
|
||||||
break;
|
break;
|
||||||
case "hostile":
|
case "hostile":
|
||||||
this.registry.enemies.set(fullId, data);
|
this.registry.enemies.set(fullId, data);
|
||||||
this.registry.enemies.set(data.id, data);
|
|
||||||
break;
|
break;
|
||||||
case "rooms":
|
case "rooms":
|
||||||
this.registry.rooms.set(fullId, data);
|
this.registry.rooms.set(fullId, data);
|
||||||
this.registry.rooms.set(data.id, data);
|
break;
|
||||||
|
case "quest":
|
||||||
|
this.registry.quests.set(fullId, data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -187,15 +188,29 @@ class DatapackLoader {
|
|||||||
getItem(id) {
|
getItem(id) {
|
||||||
return this.registry.items.get(id);
|
return this.registry.items.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
getEnemy(id) {
|
getEnemy(id) {
|
||||||
return this.registry.enemies.get(id);
|
return this.registry.enemies.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDungeon(id) {
|
getDungeon(id) {
|
||||||
return this.registry.dungeons.get(id);
|
return this.registry.dungeons.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoom(id) {
|
getRoom(id) {
|
||||||
return this.registry.rooms.get(id);
|
return this.registry.rooms.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getQuest(id) {
|
||||||
|
return this.registry.quests.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAutoStartQuests() {
|
||||||
|
return Array.from(this.registry.quests.values()).filter(
|
||||||
|
(q) => q.meta?.autoAccept,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getRecipes() {
|
getRecipes() {
|
||||||
return Array.from(this.registry.recipes.values());
|
return Array.from(this.registry.recipes.values());
|
||||||
}
|
}
|
||||||
@ -208,6 +223,7 @@ class DatapackLoader {
|
|||||||
dungeons: Array.from(this.registry.dungeons.values()),
|
dungeons: Array.from(this.registry.dungeons.values()),
|
||||||
enemies: Array.from(this.registry.enemies.values()),
|
enemies: Array.from(this.registry.enemies.values()),
|
||||||
rooms: Array.from(this.registry.rooms.values()),
|
rooms: Array.from(this.registry.rooms.values()),
|
||||||
|
quests: Array.from(this.registry.quests.values()),
|
||||||
languages: Object.fromEntries(this.registry.languages),
|
languages: Object.fromEntries(this.registry.languages),
|
||||||
manifest: this.registry.manifest,
|
manifest: this.registry.manifest,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,23 +1,49 @@
|
|||||||
const DatapackLoader = require("./DatapackLoader");
|
const DatapackLoader = require("./DatapackLoader");
|
||||||
|
const CombatService = require("./CombatService");
|
||||||
|
const QuestsManager = require("./QuestsManager");
|
||||||
|
const { Player } = require("../models");
|
||||||
|
|
||||||
class DungeonManager {
|
class DungeonManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.activeSessions = new Map();
|
this.activeSessions = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
startDungeon(playerId, dungeonId) {
|
async startDungeon(playerId, dungeonId) {
|
||||||
const dungeon = DatapackLoader.getDungeon(dungeonId);
|
const dungeon = DatapackLoader.getDungeon(dungeonId);
|
||||||
if (!dungeon || !dungeon.rooms?.length) return null;
|
if (!dungeon || !dungeon.rooms?.length) return null;
|
||||||
|
|
||||||
|
const player = await Player.findByPk(playerId);
|
||||||
|
if (!player) return null;
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
|
playerId,
|
||||||
dungeonId,
|
dungeonId,
|
||||||
currentRoomIndex: 0,
|
currentRoomIndex: 0,
|
||||||
isFinished: false,
|
isFinished: false,
|
||||||
currentEnemyHp: undefined,
|
battle: null,
|
||||||
rewards: { xp: 0, credits: 0, items: [] },
|
rewards: { xp: 0, credits: 0, items: [] },
|
||||||
};
|
};
|
||||||
|
|
||||||
this.activeSessions.set(playerId, session);
|
this.activeSessions.set(playerId, session);
|
||||||
return this.getCurrentRoomData(playerId);
|
return this.initRoom(playerId, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
player,
|
||||||
|
roomData.hostiles,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
session.battle = null;
|
||||||
|
}
|
||||||
|
return { ...roomData, battle: session.battle };
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentRoomData(playerId) {
|
getCurrentRoomData(playerId) {
|
||||||
@ -28,8 +54,6 @@ class DungeonManager {
|
|||||||
const roomRef = dungeon.rooms[session.currentRoomIndex];
|
const roomRef = dungeon.rooms[session.currentRoomIndex];
|
||||||
const rawRoom = DatapackLoader.getRoom(roomRef.id);
|
const rawRoom = DatapackLoader.getRoom(roomRef.id);
|
||||||
|
|
||||||
if (!rawRoom) return null;
|
|
||||||
|
|
||||||
const hostiles = (rawRoom.hostiles || [])
|
const hostiles = (rawRoom.hostiles || [])
|
||||||
.map((hId) => DatapackLoader.getEnemy(hId))
|
.map((hId) => DatapackLoader.getEnemy(hId))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
@ -42,89 +66,141 @@ class DungeonManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
processCombatStep(playerId, enemyId) {
|
processCombatAction(playerId, targetInstanceId, socket = null) {
|
||||||
const session = this.activeSessions.get(playerId);
|
const session = this.activeSessions.get(playerId);
|
||||||
if (!session || session.isFinished) return null;
|
if (!session || !session.battle || session.battle.isOver) return null;
|
||||||
|
|
||||||
const enemy = DatapackLoader.getEnemy(enemyId);
|
const battle = session.battle;
|
||||||
if (!enemy) return null;
|
const log = CombatService.handleAttack(battle, targetInstanceId);
|
||||||
|
|
||||||
if (session.currentEnemyHp === undefined) {
|
const allEnemiesDead = battle.enemies.every((e) => e.isDead);
|
||||||
session.currentEnemyHp = enemy.stats?.health || 100;
|
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",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const damage = Math.floor(Math.random() * 10) + 20;
|
if (allEnemiesDead) {
|
||||||
session.currentEnemyHp -= damage;
|
battle.isOver = true;
|
||||||
|
|
||||||
const isDefeated = session.currentEnemyHp <= 0;
|
const roomData = this.getCurrentRoomData(playerId);
|
||||||
let lootDropped = [];
|
const roomConfig = roomData.config;
|
||||||
|
|
||||||
if (isDefeated) {
|
session.rewards.xp += roomConfig.gainXp || 0;
|
||||||
if (enemy.loot) {
|
session.rewards.credits += roomConfig.credits || 0;
|
||||||
lootDropped = this._generateLoot(enemy.loot);
|
|
||||||
session.rewards.items.push(...lootDropped);
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
session.currentEnemyHp = undefined;
|
|
||||||
|
battle.enemies.forEach((enemy) => {
|
||||||
|
session.rewards.xp += enemy.gainXp || 0;
|
||||||
|
session.rewards.credits += enemy.credits || 0;
|
||||||
|
|
||||||
|
QuestsManager.trackProgress(
|
||||||
|
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" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return this._nextTurn(session, log);
|
||||||
damageDealt: damage,
|
|
||||||
enemyHp: Math.max(0, session.currentEnemyHp || 0),
|
|
||||||
targetDefeated: isDefeated,
|
|
||||||
loot: lootDropped,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
moveToNextRoom(playerId) {
|
_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 this._nextTurn(session, [...lastLog, ...enemyLog]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { battle, log: lastLog };
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveToNextRoom(playerId) {
|
||||||
const session = this.activeSessions.get(playerId);
|
const session = this.activeSessions.get(playerId);
|
||||||
if (!session || session.isFinished) return null;
|
if (!session || session.isFinished) return null;
|
||||||
|
|
||||||
const dungeon = DatapackLoader.getDungeon(session.dungeonId);
|
const dungeon = DatapackLoader.getDungeon(session.dungeonId);
|
||||||
const roomRef = dungeon.rooms[session.currentRoomIndex];
|
|
||||||
const rawRoom = DatapackLoader.getRoom(roomRef.id);
|
|
||||||
|
|
||||||
if (rawRoom) {
|
|
||||||
if (rawRoom.hostiles) {
|
|
||||||
rawRoom.hostiles.forEach((hId) => {
|
|
||||||
const enemy = DatapackLoader.getEnemy(hId);
|
|
||||||
if (enemy) {
|
|
||||||
session.rewards.xp += enemy.gainXp || 0;
|
|
||||||
session.rewards.credits += enemy.credits || 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
session.rewards.xp += rawRoom.gainXp || 0;
|
|
||||||
session.rewards.credits += rawRoom.credits || 0;
|
|
||||||
if (rawRoom.loot) {
|
|
||||||
session.rewards.items.push(...this._generateLoot(rawRoom.loot));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.currentRoomIndex < dungeon.rooms.length - 1) {
|
if (session.currentRoomIndex < dungeon.rooms.length - 1) {
|
||||||
session.currentRoomIndex++;
|
session.currentRoomIndex++;
|
||||||
return this.getCurrentRoomData(playerId);
|
return this.initRoom(playerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.isFinished = true;
|
session.isFinished = true;
|
||||||
return { status: "completed", rewards: session.rewards };
|
return { status: "completed", rewards: session.rewards };
|
||||||
}
|
}
|
||||||
|
|
||||||
_generateLoot(lootTable) {
|
|
||||||
const dropped = [];
|
|
||||||
lootTable.forEach((entry) => {
|
|
||||||
if (Math.random() <= (entry.chance || 1.0)) {
|
|
||||||
const count =
|
|
||||||
typeof entry.count === "object"
|
|
||||||
? Math.floor(
|
|
||||||
Math.random() * (entry.count.max - entry.count.min + 1),
|
|
||||||
) + entry.count.min
|
|
||||||
: entry.count || 1;
|
|
||||||
dropped.push({ id: entry.id, count });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return dropped;
|
|
||||||
}
|
|
||||||
|
|
||||||
leaveDungeon(playerId) {
|
leaveDungeon(playerId) {
|
||||||
this.activeSessions.delete(playerId);
|
this.activeSessions.delete(playerId);
|
||||||
}
|
}
|
||||||
|
|||||||
163
game-server/src/game/QuestsManager.js
Normal file
163
game-server/src/game/QuestsManager.js
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
const DatapackLoader = require("./DatapackLoader");
|
||||||
|
const { PlayerQuest, Player, Inventory, sequelize } = require("../models");
|
||||||
|
|
||||||
|
class QuestsManager {
|
||||||
|
async onPlayerLogin(playerId, socket = null) {
|
||||||
|
try {
|
||||||
|
await this.checkAutoQuests(playerId, socket);
|
||||||
|
await this.trackProgress(playerId, "LOGIN", null, 1, socket);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAutoQuests(playerId, socket = null) {
|
||||||
|
const allQuests = Array.from(DatapackLoader.registry.quests);
|
||||||
|
const player = await Player.findByPk(playerId);
|
||||||
|
|
||||||
|
for (const quest of allQuests) {
|
||||||
|
if (quest.meta?.autoAccept && player.level >= (quest.minLevel || 0)) {
|
||||||
|
const [pq, created] = await PlayerQuest.findOrCreate({
|
||||||
|
where: { playerId, questId: quest.id },
|
||||||
|
defaults: {
|
||||||
|
status: "active",
|
||||||
|
progress: quest.objectives.map((obj) => ({
|
||||||
|
...obj,
|
||||||
|
currentAmount: 0,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (created && socket) {
|
||||||
|
socket.emit("quest:new", {
|
||||||
|
id: pq.questId,
|
||||||
|
status: pq.status,
|
||||||
|
objectives: pq.progress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackProgress(playerId, type, targetId, amount = 1, socket = null) {
|
||||||
|
try {
|
||||||
|
const activeQuests = await PlayerQuest.findAll({
|
||||||
|
where: { playerId, status: "active" },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const pq of activeQuests) {
|
||||||
|
const staticData = DatapackLoader.getQuest(pq.questId);
|
||||||
|
if (!staticData) continue;
|
||||||
|
|
||||||
|
let isChanged = false;
|
||||||
|
const currentProgress = pq.progress;
|
||||||
|
|
||||||
|
const updatedProgress = currentProgress.map((obj) => {
|
||||||
|
if (
|
||||||
|
obj.type === type &&
|
||||||
|
(obj.targetId === targetId || type === "LOGIN")
|
||||||
|
) {
|
||||||
|
if (obj.currentAmount < obj.requiredAmount) {
|
||||||
|
obj.currentAmount = Math.min(
|
||||||
|
obj.currentAmount + amount,
|
||||||
|
obj.requiredAmount,
|
||||||
|
);
|
||||||
|
isChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isChanged) {
|
||||||
|
const isReady = updatedProgress.every(
|
||||||
|
(obj) => obj.currentAmount >= obj.requiredAmount,
|
||||||
|
);
|
||||||
|
|
||||||
|
await pq.update({
|
||||||
|
progress: updatedProgress,
|
||||||
|
status: isReady ? "ready" : "active",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
socket.emit("quest:update", {
|
||||||
|
id: pq.questId,
|
||||||
|
status: isReady ? "ready" : "active",
|
||||||
|
objectives: updatedProgress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReady && staticData.meta?.autoComplete) {
|
||||||
|
await this.claimRewards(playerId, pq.questId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async claimRewards(playerId, questId) {
|
||||||
|
const t = await sequelize.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pq = await PlayerQuest.findOne({
|
||||||
|
where: { playerId, questId, status: "ready" },
|
||||||
|
transaction: t,
|
||||||
|
lock: t.LOCK.UPDATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pq) {
|
||||||
|
await t.rollback();
|
||||||
|
throw new Error("QUEST_NOT_READY_OR_CLAIMED");
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticData = DatapackLoader.getQuest(questId);
|
||||||
|
const player = await Player.findByPk(playerId, { transaction: t });
|
||||||
|
const rewards = staticData.rewards;
|
||||||
|
|
||||||
|
await pq.update({ status: "completed" }, { transaction: t });
|
||||||
|
|
||||||
|
if (rewards.credits) {
|
||||||
|
await player.increment("credits", {
|
||||||
|
by: rewards.credits,
|
||||||
|
transaction: t,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rewards.xp) {
|
||||||
|
await player.increment("experience", {
|
||||||
|
by: rewards.xp,
|
||||||
|
transaction: t,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rewards.items?.length > 0) {
|
||||||
|
for (const item of rewards.items) {
|
||||||
|
const [invItem] = await Inventory.findOrCreate({
|
||||||
|
where: { playerId, itemId: item.id },
|
||||||
|
defaults: { quantity: 0 },
|
||||||
|
transaction: t,
|
||||||
|
});
|
||||||
|
await invItem.increment("quantity", {
|
||||||
|
by: item.count,
|
||||||
|
transaction: t,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await t.commit();
|
||||||
|
const updatedPlayer = await player.reload();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
rewards,
|
||||||
|
newTotalCredits: updatedPlayer.credits,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (t) await t.rollback();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new QuestsManager();
|
||||||
41
game-server/src/models/PlayerQuest.js
Normal file
41
game-server/src/models/PlayerQuest.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const { DataTypes } = require("sequelize");
|
||||||
|
const sequelize = require("../config/db");
|
||||||
|
|
||||||
|
const PlayerQuest = sequelize.define(
|
||||||
|
"PlayerQuest",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
playerId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
questId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.ENUM("active", "ready", "completed"),
|
||||||
|
defaultValue: "active",
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
tableName: "player_quests",
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
unique: true,
|
||||||
|
fields: ["playerId", "questId"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = PlayerQuest;
|
||||||
@ -4,9 +4,14 @@ const Inventory = require("./Inventory");
|
|||||||
const setupAssociations = require("./associations");
|
const setupAssociations = require("./associations");
|
||||||
const Notification = require("./Notification");
|
const Notification = require("./Notification");
|
||||||
const Friend = require("./Friend.js");
|
const Friend = require("./Friend.js");
|
||||||
|
const PlayerQuest = require("./PlayerQuest.js");
|
||||||
|
|
||||||
Player.hasMany(Inventory, { foreignKey: "playerId", as: "inventory" });
|
Player.hasMany(Inventory, { foreignKey: "playerId", as: "inventory" });
|
||||||
Inventory.belongsTo(Player, { foreignKey: "playerId" });
|
Inventory.belongsTo(Player, { foreignKey: "playerId" });
|
||||||
|
|
||||||
|
Player.hasMany(PlayerQuest, { foreignKey: "playerId", as: "quests" });
|
||||||
|
PlayerQuest.belongsTo(Player, { foreignKey: "playerId" });
|
||||||
|
|
||||||
setupAssociations();
|
setupAssociations();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -15,4 +20,5 @@ module.exports = {
|
|||||||
Inventory,
|
Inventory,
|
||||||
Notification,
|
Notification,
|
||||||
Friend,
|
Friend,
|
||||||
|
PlayerQuest,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const { Op } = require("sequelize");
|
|||||||
const Player = require("../../models/Player");
|
const Player = require("../../models/Player");
|
||||||
const sessionManager = require("../../game/SessionManager");
|
const sessionManager = require("../../game/SessionManager");
|
||||||
const economyService = require("../../game/EconomyService.js");
|
const economyService = require("../../game/EconomyService.js");
|
||||||
|
const QuestsManager = require("../../game/QuestsManager");
|
||||||
|
|
||||||
module.exports = async (io, socket) => {
|
module.exports = async (io, socket) => {
|
||||||
const userId = socket.user?.id;
|
const userId = socket.user?.id;
|
||||||
@ -26,6 +27,9 @@ module.exports = async (io, socket) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await QuestsManager.onPlayerLogin(userId, socket);
|
||||||
|
await player.reload();
|
||||||
|
|
||||||
const offlineCredits = await economyService.handleOfflineEarnings(player);
|
const offlineCredits = await economyService.handleOfflineEarnings(player);
|
||||||
|
|
||||||
socket.playerId = userId;
|
socket.playerId = userId;
|
||||||
@ -34,9 +38,9 @@ module.exports = async (io, socket) => {
|
|||||||
|
|
||||||
const playerRaw = player.get({ plain: true });
|
const playerRaw = player.get({ plain: true });
|
||||||
sessionManager.addPlayer(socket.id, playerRaw);
|
sessionManager.addPlayer(socket.id, playerRaw);
|
||||||
|
|
||||||
const onlinePlayersData = sessionManager.getAllOnline();
|
const onlinePlayersData = sessionManager.getAllOnline();
|
||||||
const onlineUsernames = onlinePlayersData.map((p) => p.username);
|
const onlineUsernames = onlinePlayersData.map((p) => p.username);
|
||||||
|
|
||||||
const onlineIds = onlinePlayersData.map((p) => p.id);
|
const onlineIds = onlinePlayersData.map((p) => p.id);
|
||||||
|
|
||||||
const offlinePlayersModels = await Player.findAll({
|
const offlinePlayersModels = await Player.findAll({
|
||||||
@ -64,6 +68,7 @@ module.exports = async (io, socket) => {
|
|||||||
|
|
||||||
socket.broadcast.emit("player:joined", { username: playerRaw.username });
|
socket.broadcast.emit("player:joined", { username: playerRaw.username });
|
||||||
socket.join(`user_${socket.user.id}`);
|
socket.join(`user_${socket.user.id}`);
|
||||||
|
|
||||||
socket.on("player:get_dashboard", async () => {
|
socket.on("player:get_dashboard", async () => {
|
||||||
try {
|
try {
|
||||||
const p = await Player.findByPk(userId);
|
const p = await Player.findByPk(userId);
|
||||||
|
|||||||
@ -18,36 +18,49 @@ module.exports = (io, socket) => {
|
|||||||
return socket.emit("error", { message: "Insufficient energy" });
|
return socket.emit("error", { message: "Insufficient energy" });
|
||||||
|
|
||||||
await player.decrement("energy", { by: energyCost });
|
await player.decrement("energy", { by: energyCost });
|
||||||
const firstRoom = dungeonManager.startDungeon(userId, dungeonId);
|
const startData = await dungeonManager.startDungeon(userId, dungeonId);
|
||||||
|
|
||||||
socket.emit("dungeon:started", {
|
socket.emit("dungeon:started", {
|
||||||
dungeonId: dungeon.id,
|
dungeonId: dungeon.id,
|
||||||
room: firstRoom.config,
|
room: startData.config,
|
||||||
hostiles: firstRoom.hostiles,
|
hostiles: startData.hostiles,
|
||||||
roomIndex: firstRoom.roomIndex,
|
battle: startData.battle,
|
||||||
totalRooms: firstRoom.totalRooms,
|
roomIndex: startData.roomIndex,
|
||||||
|
totalRooms: startData.totalRooms,
|
||||||
remainingEnergy: player.energy - energyCost,
|
remainingEnergy: player.energy - energyCost,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
socket.emit("error", { message: "Critical deployment failure" });
|
socket.emit("error", { message: "Deployment failure" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("dungeon:combat_step", async ({ enemyId }) => {
|
socket.on("dungeon:combat_action", async ({ targetInstanceId }) => {
|
||||||
const result = dungeonManager.processCombatStep(userId, enemyId);
|
try {
|
||||||
if (!result) return;
|
if (!userId) return;
|
||||||
|
const result = dungeonManager.processCombatAction(
|
||||||
|
userId,
|
||||||
|
targetInstanceId,
|
||||||
|
);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
socket.emit("dungeon:combat_result", {
|
socket.emit("dungeon:battle_update", {
|
||||||
...result,
|
battle: result.battle,
|
||||||
message: result.targetDefeated
|
log: result.log,
|
||||||
? "Enemy eliminated!"
|
status: result.status,
|
||||||
: `Strike successful. Dealt ${result.damageDealt} damage.`,
|
});
|
||||||
});
|
|
||||||
|
if (result.status === "defeat") {
|
||||||
|
dungeonManager.leaveDungeon(userId);
|
||||||
|
socket.emit("dungeon:failed", { message: "Neural link severed." });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
socket.emit("error", { message: "Synchronization error" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("dungeon:next_room", async () => {
|
socket.on("dungeon:next_room", async () => {
|
||||||
try {
|
try {
|
||||||
const nextRoom = dungeonManager.moveToNextRoom(userId);
|
if (!userId) return;
|
||||||
|
const nextRoom = await dungeonManager.moveToNextRoom(userId);
|
||||||
if (!nextRoom)
|
if (!nextRoom)
|
||||||
return socket.emit("error", { message: "Navigation error" });
|
return socket.emit("error", { message: "Navigation error" });
|
||||||
|
|
||||||
@ -57,7 +70,7 @@ module.exports = (io, socket) => {
|
|||||||
socket.emit("dungeon:room_update", nextRoom);
|
socket.emit("dungeon:room_update", nextRoom);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
socket.emit("error", { message: "Navigation system error" });
|
socket.emit("error", { message: "Navigation error" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -90,13 +103,16 @@ async function finalizeDungeon(socket, sessionRewards) {
|
|||||||
await invItem.increment("quantity", { by: totalCount });
|
await invItem.increment("quantity", { by: totalCount });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Оновлюємо масив для фронтенда, щоб не було дублікатів у списку
|
||||||
sessionRewards.items = Object.entries(consolidated).map(
|
sessionRewards.items = Object.entries(consolidated).map(
|
||||||
([id, count]) => ({ id, count }),
|
([id, count]) => ({ id, count }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("FINAL REWARDS SAVED:", sessionRewards);
|
||||||
socket.emit("dungeon:completed", { rewards: sessionRewards });
|
socket.emit("dungeon:completed", { rewards: sessionRewards });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
socket.emit("error", { message: "Failed to save rewards" });
|
socket.emit("error", { message: "Failed to save rewards" });
|
||||||
} finally {
|
} finally {
|
||||||
dungeonManager.leaveDungeon(userId);
|
dungeonManager.leaveDungeon(userId);
|
||||||
|
|||||||
89
game-server/src/sockets/handlers/questsHandler.js
Normal file
89
game-server/src/sockets/handlers/questsHandler.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
const questsManager = require("../../game/QuestsManager");
|
||||||
|
const DatapackLoader = require("../../game/DatapackLoader");
|
||||||
|
const { PlayerQuest } = require("../../models");
|
||||||
|
|
||||||
|
module.exports = (io, socket) => {
|
||||||
|
const playerId = socket.user?.id;
|
||||||
|
|
||||||
|
socket.on("quest:get_list", async () => {
|
||||||
|
if (!playerId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const autoQuests = DatapackLoader.getAutoStartQuests();
|
||||||
|
const existingQuests = await PlayerQuest.findAll({
|
||||||
|
where: { playerId },
|
||||||
|
attributes: ["questId"],
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingIds = existingQuests.map((q) => q.questId);
|
||||||
|
const missingQuests = autoQuests.filter(
|
||||||
|
(aq) => !existingIds.includes(aq.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missingQuests.length > 0) {
|
||||||
|
const toCreate = missingQuests.map((aq) => ({
|
||||||
|
playerId,
|
||||||
|
questId: aq.id,
|
||||||
|
status: "active",
|
||||||
|
progress: aq.objectives.map((obj) => ({ ...obj, currentAmount: 0 })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
await PlayerQuest.bulkCreate(toCreate, { ignoreDuplicates: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const all = await PlayerQuest.findAll({
|
||||||
|
where: { playerId },
|
||||||
|
order: [["updatedAt", "DESC"]],
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.emit(
|
||||||
|
"quest:list_data",
|
||||||
|
all.map((q) => ({
|
||||||
|
id: q.questId,
|
||||||
|
status: q.status,
|
||||||
|
objectives: q.progress,
|
||||||
|
rewards: DatapackLoader.getQuest(q.questId)?.rewards,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Quest sync error:", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("quest:claim_reward", async ({ questId }) => {
|
||||||
|
try {
|
||||||
|
const result = await questsManager.claimRewards(playerId, questId);
|
||||||
|
|
||||||
|
socket.emit("player:credits_update", {
|
||||||
|
totalCredits: result.newTotalCredits,
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.emit("quest:reward_claimed", {
|
||||||
|
questId,
|
||||||
|
rewards: result.rewards,
|
||||||
|
});
|
||||||
|
|
||||||
|
const all = await PlayerQuest.findAll({
|
||||||
|
where: { playerId },
|
||||||
|
order: [["updatedAt", "DESC"]],
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.emit(
|
||||||
|
"quest:list_data",
|
||||||
|
all.map((q) => ({
|
||||||
|
id: q.questId,
|
||||||
|
status: q.status,
|
||||||
|
objectives: q.progress,
|
||||||
|
rewards: DatapackLoader.getQuest(q.questId)?.rewards,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
const msg =
|
||||||
|
err.message === "QUEST_NOT_READY_OR_CLAIMED"
|
||||||
|
? "Reward already claimed or objective not met."
|
||||||
|
: "Failed to claim reward.";
|
||||||
|
socket.emit("error", { message: msg });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -7,6 +7,7 @@ const dungeonHandler = require("./handlers/dungeonHandler");
|
|||||||
const chatHandler = require("./handlers/chatHandler");
|
const chatHandler = require("./handlers/chatHandler");
|
||||||
const notificationHandler = require("./handlers/notificationHandler");
|
const notificationHandler = require("./handlers/notificationHandler");
|
||||||
const socialHandler = require("./handlers/socialHandler");
|
const socialHandler = require("./handlers/socialHandler");
|
||||||
|
const questsHandler = require("./handlers/questsHandler");
|
||||||
|
|
||||||
const initSockets = (io) => {
|
const initSockets = (io) => {
|
||||||
io.use(socketAuth);
|
io.use(socketAuth);
|
||||||
@ -20,6 +21,7 @@ const initSockets = (io) => {
|
|||||||
chatHandler(io, socket);
|
chatHandler(io, socket);
|
||||||
socialHandler(io, socket);
|
socialHandler(io, socket);
|
||||||
notificationHandler(io, socket);
|
notificationHandler(io, socket);
|
||||||
|
questsHandler(io, socket);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user