This commit is contained in:
cowcannon 2026-04-18 13:13:02 -05:00
commit 0d20d630ce
36 changed files with 1511 additions and 295 deletions

View File

@ -31,7 +31,6 @@ 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 (data.languages) { if (data.languages) {
this.translations = data.languages; this.translations = data.languages;
} }

View File

@ -420,3 +420,439 @@
opacity: 0.4; opacity: 0.4;
font-size: 0.6rem; font-size: 0.6rem;
} }
.dungeon-active-screen {
display: flex;
flex-direction: column;
height: 100vh;
background: radial-gradient(circle at center, #0a1118 0%, #05080c 100%);
padding: 30px;
gap: 25px;
font-family: "Space Mono", monospace;
color: #e0e6ed;
box-sizing: border-box;
overflow: hidden;
}
.dungeon-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
padding-bottom: 15px;
position: relative;
flex-shrink: 0;
}
.dungeon-header::after {
content: "";
position: absolute;
bottom: -1px;
left: 0;
width: 60px;
height: 3px;
background: #00d4ff;
box-shadow: 0 0 15px #00d4ff;
}
.progress-text {
font-size: 0.75rem;
letter-spacing: 2px;
color: #00d4ff;
font-weight: bold;
}
.progress-bar {
width: 250px;
height: 6px;
background: rgba(255, 255, 255, 0.05);
margin-top: 8px;
border-radius: 3px;
overflow: hidden;
}
.progress-bar .fill {
height: 100%;
background: linear-gradient(90deg, #00d4ff, #0088ff);
box-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
transition: width 0.5s ease-in-out;
}
.dungeon-title-area {
text-align: right;
}
.dungeon-name {
font-family: "Orbitron", sans-serif;
font-size: 1.4rem;
font-weight: 900;
letter-spacing: 1px;
text-transform: uppercase;
color: #fff;
}
.dungeon-status-tag {
font-size: 0.7rem;
color: #ff4444;
animation: blink 1.5s infinite;
}
.battle-layout {
display: grid;
grid-template-columns: 1.2fr 1fr;
flex: 1;
gap: 30px;
min-height: 0;
}
.enemy-display {
position: relative;
height: 100%;
min-height: 0;
}
.enemy-card {
background: linear-gradient(
180deg,
rgba(255, 68, 68, 0.08) 0%,
rgba(0, 0, 0, 0) 100%
);
border: 1px solid rgba(255, 68, 68, 0.2);
border-radius: 4px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.enemy-card.defeated {
filter: grayscale(1) brightness(0.5);
border-color: rgba(160, 172, 186, 0.2);
background: rgba(0, 0, 0, 0.4);
}
.threat-tag {
color: #ff4444;
font-family: "Orbitron", sans-serif;
font-size: 0.8rem;
letter-spacing: 4px;
margin-bottom: 30px;
text-shadow: 0 0 10px rgba(255, 68, 68, 0.5);
}
.enemy-icon {
font-size: 6rem;
color: #fff;
margin-bottom: 25px;
filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.2));
}
.enemy-name {
font-family: "Orbitron", sans-serif;
font-size: 1.8rem;
margin: 0 0 20px 0;
color: #fff;
text-transform: uppercase;
}
.enemy-hp-container {
width: 60%;
margin-bottom: 30px;
}
.hp-label {
font-size: 0.6rem;
color: #a0acba;
margin-bottom: 6px;
text-align: center;
letter-spacing: 1px;
}
.hp-bar-mini {
height: 8px;
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 2px;
}
.hp-fill-mini {
height: 100%;
background: #ff4444;
box-shadow: 0 0 15px rgba(255, 68, 68, 0.6);
transition: width 0.3s ease-out;
}
.combat-log-wrapper {
display: flex;
flex-direction: column;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(26, 38, 56, 0.8);
min-height: 0;
max-height: 100%;
overflow: hidden;
}
.log-header {
background: rgba(26, 38, 56, 0.5);
padding: 10px 15px;
font-size: 0.7rem;
font-weight: bold;
letter-spacing: 1px;
color: #00d4ff;
border-bottom: 1px solid rgba(26, 38, 56, 0.8);
flex-shrink: 0;
}
.combat-log {
flex: 1;
padding: 20px;
font-size: 0.8rem;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
scrollbar-width: thin;
min-height: 0;
}
.log-entry {
line-height: 1.4;
color: #a0acba;
word-break: break-all;
}
.log-arrow {
color: #00d4ff;
margin-right: 8px;
font-weight: bold;
}
.dungeon-controls {
display: flex;
gap: 20px;
height: 70px;
flex-shrink: 0;
}
.ctrl-btn {
flex: 1;
border: none;
font-family: "Orbitron", sans-serif;
font-weight: 900;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 1rem;
letter-spacing: 2px;
clip-path: polygon(10px 0%, 100% 0%, calc(100% - 10px) 100%, 0% 100%);
transition: all 0.2s;
}
.ctrl-btn.combat {
background: #ff4444;
color: #000;
}
.ctrl-btn.loot {
background: #ffaa00;
color: #000;
}
.ctrl-btn.next {
background: #00d4ff;
color: #000;
}
.environment-panel {
background: rgba(0, 20, 40, 0.6);
border: 1px solid rgba(0, 255, 255, 0.2);
padding: 12px;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.env-info {
display: flex;
align-items: center;
gap: 12px;
}
@media screen and (max-width: 768px) {
.dungeon-active-screen {
padding: 15px;
gap: 15px;
height: 100%;
overflow-y: auto;
}
.dungeon-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.progress-bar {
width: 100%;
}
.dungeon-title-area {
text-align: left;
width: 100%;
}
.battle-layout {
grid-template-columns: 1fr;
display: flex;
flex-direction: column;
gap: 15px;
flex: none;
}
.enemy-display {
min-height: 300px;
}
.enemy-icon {
font-size: 4rem;
}
.enemy-name {
font-size: 1.3rem;
}
.combat-log-wrapper {
min-width: 100%;
max-height: 250px;
}
.dungeon-controls {
height: 60px;
position: sticky;
bottom: 0;
background: #05080c;
padding-top: 10px;
margin-top: auto;
}
.ctrl-btn {
font-size: 0.8rem;
letter-spacing: 1px;
}
}
@media screen and (max-width: 480px) {
.enemy-display {
min-height: 260px;
}
.enemy-hp-container {
width: 90%;
}
.ctrl-btn {
gap: 5px;
font-size: 0.75rem;
}
}
.enemy-info-footer {
display: flex;
align-items: center;
justify-content: space-between;
width: 90%;
margin-top: auto;
padding-bottom: 15px;
gap: 10px;
}
.card-id {
opacity: 0.4;
font-size: 0.6rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
}
.enemy-info-footer span:first-child {
white-space: nowrap;/
}
@media screen and (max-width: 768px) {
.enemy-display {
min-height: 220px;
flex: 0 0 auto;
}
.enemy-card {
padding: 10px;
}
.enemy-icon {
font-size: 3rem;
margin-bottom: 10px;
}
.enemy-name {
font-size: 1.1rem;
margin-bottom: 10px;
}
.threat-tag {
margin-bottom: 10px;
font-size: 0.65rem;
letter-spacing: 2px;
}
.enemy-hp-container {
margin-bottom: 15px;
}
.combat-log-wrapper {
max-height: 180px;
}
.log-header {
padding: 6px 12px;
font-size: 0.6rem;
}
.combat-log {
padding: 10px;
gap: 4px;
}
.log-entry {
font-size: 0.7rem;
line-height: 1.2;
}
.log-arrow {
margin-right: 4px;
}
.enemy-info-footer {
padding-bottom: 10px;
font-size: 0.6rem;
}
}
@media screen and (max-width: 400px) {
.enemy-display {
min-height: 190px;
}
.enemy-icon {
font-size: 2.5rem;
}
.combat-log-wrapper {
max-height: 150px;
}
}

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import GameDataManager from "../../../services/GameDataManager.js"; import GameDataManager from "../../../services/GameDataManager.js";
import "./DungeonScreen.css"; import "./DungeonScreen.css";
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);
@ -9,6 +10,7 @@ const DungeonScreen = ({ session, socket }) => {
const [enemyHp, setEnemyHp] = useState(null); const [enemyHp, setEnemyHp] = useState(null);
const [isEnemyDefeated, setIsEnemyDefeated] = useState(false); const [isEnemyDefeated, setIsEnemyDefeated] = useState(false);
const [isLooted, setIsLooted] = useState(false); const [isLooted, setIsLooted] = useState(false);
const [summary, setSummary] = useState(null);
const [log, setLog] = useState([ const [log, setLog] = useState([
"SYSTEM: Neural link established. Scanning sector...", "SYSTEM: Neural link established. Scanning sector...",
]); ]);
@ -47,6 +49,7 @@ const DungeonScreen = ({ session, socket }) => {
socket.on("dungeon:combat_result", (data) => { socket.on("dungeon:combat_result", (data) => {
if (data.message) addLog(data.message); if (data.message) addLog(data.message);
if (data.enemyHp !== undefined) { if (data.enemyHp !== undefined) {
const maxHp = currentEnemy?.stats?.health || 100; const maxHp = currentEnemy?.stats?.health || 100;
const hpPercent = (data.enemyHp / maxHp) * 100; const hpPercent = (data.enemyHp / maxHp) * 100;
@ -55,12 +58,27 @@ const DungeonScreen = ({ session, socket }) => {
if (data.targetDefeated) { if (data.targetDefeated) {
setIsEnemyDefeated(true); setIsEnemyDefeated(true);
addLog("TARGET_NEUTRALIZED: Threat eliminated."); addLog("TARGET_NEUTRALIZED: Threat eliminated.");
if (data.loot && data.loot.length > 0) {
addLog("SCANNING FOR DROPPED ASSETS...");
data.loot.forEach((item) => {
const itemData = GameDataManager.getItem(item.id);
const itemName = itemData?.displayName || item.id;
addLog(`RECOVERED: ${itemName} x${item.count}`);
});
} }
}
});
socket.on("dungeon:completed", (data) => {
setSummary(data.rewards);
addLog("MISSION_SUCCESS: All objectives secured.");
}); });
return () => { return () => {
socket.off("dungeon:room_update"); socket.off("dungeon:room_update");
socket.off("dungeon:combat_result"); socket.off("dungeon:combat_result");
socket.off("dungeon:completed");
}; };
}, [socket, currentEnemy]); }, [socket, currentEnemy]);
@ -91,6 +109,13 @@ const DungeonScreen = ({ session, socket }) => {
return ( return (
<div className="dungeon-active-screen"> <div className="dungeon-active-screen">
{summary && (
<DungeonFinish
rewards={summary}
onExit={() => window.location.reload()}
/>
)}
<div className="dungeon-header"> <div className="dungeon-header">
<div className="room-progress"> <div className="room-progress">
<div className="progress-text"> <div className="progress-text">

View File

@ -58,6 +58,7 @@ const DatapackTab = () => {
return ( return (
<div className="tab-content active datapack-tab-wrapper"> <div className="tab-content active datapack-tab-wrapper">
<MeteorRegion>
<div className="datapack-controls"> <div className="datapack-controls">
<div className="section-selector"> <div className="section-selector">
{sections.map((s) => ( {sections.map((s) => (
@ -83,12 +84,12 @@ const DatapackTab = () => {
</div> </div>
</div> </div>
<MeteorRegion className="datapack-content"> <div className="datapack-content">
<div className="datapack-grid"> <div className="datapack-grid">
{displayList.map((item) => ( {displayList.map((item) => (
<div <div
key={item.id} key={item.id}
className="datapack-card" className={`datapack-card ${activeSection === "hostiles" ? "hostile-card" : ""}`}
onClick={() => onClick={() =>
setSelectedItem({ ...item, sectionType: activeSection }) setSelectedItem({ ...item, sectionType: activeSection })
} }
@ -105,15 +106,44 @@ const DatapackTab = () => {
</div> </div>
)} )}
</div> </div>
<div className="card-info"> <div className="card-info">
<span className="card-name">{item.displayName}</span> <span className="card-name">{item.displayName}</span>
<span className="card-id">{item.id}</span> <span className="card-id">{item.id}</span>
{activeSection === "hostiles" &&
item.loot &&
item.loot.length > 0 && (
<div className="card-loot-preview">
{item.loot.map((lootEntry, idx) => {
const lootData = GameDataManager.getItem(
lootEntry.id,
);
return (
<div
key={`${item.id}-loot-${idx}`}
className="loot-mini-slot-text"
title={`${lootData?.displayName || lootEntry.id} (${(lootEntry.chance * 100).toFixed(0)}%)`}
>
<i
className="fas fa-box-open"
style={{ fontSize: "10px", marginRight: "4px" }}
></i>
<span>
{lootData?.displayName ||
lootEntry.id.split(":").pop()}
</span>
</div>
);
})}
</div>
)}
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div>
</MeteorRegion> </MeteorRegion>
{selectedItem && ( {selectedItem && (
<DatapackDetailsModal <DatapackDetailsModal
data={selectedItem} data={selectedItem}

View File

@ -5,10 +5,10 @@ import "./styles/DungeonsTab.css";
const DungeonsTab = ({ startDungeon }) => { const DungeonsTab = ({ startDungeon }) => {
const [dungeons, setDungeons] = useState([]); const [dungeons, setDungeons] = useState([]);
const [selectedDungeon, setSelectedDungeon] = useState(null); const [selectedDungeon, setSelectedDungeon] = useState(null);
const [showSelector, setShowSelector] = useState(true);
useEffect(() => { useEffect(() => {
const allKeys = Array.from(GameDataManager.dungeons.keys()); const allKeys = Array.from(GameDataManager.dungeons.keys());
const uniqueDungeons = Array.from(new Set(allKeys)) const uniqueDungeons = Array.from(new Set(allKeys))
.map((id) => GameDataManager.getDungeon(id)) .map((id) => GameDataManager.getDungeon(id))
.filter( .filter(
@ -16,7 +16,6 @@ const DungeonsTab = ({ startDungeon }) => {
); );
setDungeons(uniqueDungeons); setDungeons(uniqueDungeons);
if (uniqueDungeons.length > 0 && !selectedDungeon) { if (uniqueDungeons.length > 0 && !selectedDungeon) {
setSelectedDungeon(uniqueDungeons[0]); setSelectedDungeon(uniqueDungeons[0]);
} }
@ -25,11 +24,16 @@ const DungeonsTab = ({ startDungeon }) => {
const handleSelectDungeon = (id) => { const handleSelectDungeon = (id) => {
const translatedDungeon = GameDataManager.getDungeon(id); const translatedDungeon = GameDataManager.getDungeon(id);
setSelectedDungeon(translatedDungeon); setSelectedDungeon(translatedDungeon);
if (window.innerWidth <= 768) {
setShowSelector(false);
}
}; };
return ( return (
<div className="tab-content active" id="dungeons-tab"> <div className="tab-content active" id="dungeons-tab">
<div className="dungeons-container"> <div
className={`dungeons-container ${!showSelector ? "view-active" : ""}`}
>
<div className="dungeon-selector"> <div className="dungeon-selector">
<div className="selector-header"> <div className="selector-header">
<h2 className="terminal-text">AVAILABLE_MISSIONS</h2> <h2 className="terminal-text">AVAILABLE_MISSIONS</h2>
@ -57,8 +61,18 @@ const DungeonsTab = ({ startDungeon }) => {
{selectedDungeon ? ( {selectedDungeon ? (
<div className="dungeon-details-v2"> <div className="dungeon-details-v2">
<div className="details-header-scan"> <div className="details-header-scan">
<button
className="back-to-list"
onClick={() => setShowSelector(true)}
>
<i className="fas fa-arrow-left"></i>
</button>
<div className="mission-info-group">
<div className="mission-type-label">MISSION_BRIEFING</div> <div className="mission-type-label">MISSION_BRIEFING</div>
<h3 className="mission-title">{selectedDungeon.displayName}</h3> <h3 className="mission-title">
{selectedDungeon.displayName}
</h3>
</div>
<div className="scanline-horizontal"></div> <div className="scanline-horizontal"></div>
</div> </div>
@ -92,10 +106,10 @@ const DungeonsTab = ({ startDungeon }) => {
</div> </div>
<div className="reward-text"> <div className="reward-text">
<span className="reward-name"> <span className="reward-name">
{item.displayName} {item?.displayName || loot.itemId}
</span> </span>
<span className="reward-chance"> <span className="reward-chance">
{loot.chance}% ACQUISITION_CHANCE {loot.chance}% ACQUISITION
</span> </span>
</div> </div>
</div> </div>

View File

@ -18,24 +18,23 @@
border-radius: 12px; border-radius: 12px;
width: 90%; width: 90%;
max-width: 450px; max-width: 450px;
padding: 24px;
box-shadow: 0 0 30px rgba(0, 204, 255, 0.15); box-shadow: 0 0 30px rgba(0, 204, 255, 0.15);
} }
.modal-header { .modal-headerr {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 10px;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1); border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 15px;
margin-bottom: 20px;
} }
.modal-header h3 { .modal-header {
margin: 0; margin: 0;
color: #00ccff; color: #00ccff;
font-family: "Orbitron", sans-serif; font-family: "Orbitron", sans-serif;
font-size: 1.1rem; font-size: 0.8rem;
} }
.close-x { .close-x {
@ -125,7 +124,7 @@
height: 100%; height: 100%;
background: var(--primary-color); background: var(--primary-color);
box-shadow: 0 0 10px var(--primary-color); box-shadow: 0 0 10px var(--primary-color);
transition: width 1s linear; /* Плавне заповнення */ transition: width 1s linear;
} }
@keyframes pulse { @keyframes pulse {
@ -140,7 +139,6 @@
} }
} }
/* Стани інгредієнтів */
.res-item { .res-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -161,7 +159,6 @@
background: rgba(255, 68, 68, 0.1); background: rgba(255, 68, 68, 0.1);
} }
/* Кольори значень */
.val-red { .val-red {
color: #ff4444; color: #ff4444;
font-weight: bold; font-weight: bold;
@ -181,7 +178,6 @@
font-size: 0.9rem; font-size: 0.9rem;
} }
/* Кнопка */
.btn-start-craft { .btn-start-craft {
background: #28a745; background: #28a745;
color: white; color: white;
@ -202,3 +198,60 @@
background: #218838; background: #218838;
box-shadow: 0 0 10px rgba(40, 167, 69, 0.4); box-shadow: 0 0 10px rgba(40, 167, 69, 0.4);
} }
.item-preview-header {
display: flex;
gap: 20px;
padding: 15px;
background: rgba(0, 212, 255, 0.05);
border: 1px solid rgba(0, 212, 255, 0.1);
margin-bottom: 20px;
border-radius: 4px;
}
.item-icon-container {
position: relative;
width: 90px;
height: 90px;
background: #000;
border: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.item-display-icon {
max-width: 80%;
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;
padding: 2px 8px;
font-size: 11px;
font-weight: bold;
border-radius: 2px;
}
.item-type-tag {
display: block;
font-size: 10px;
color: var(--primary-color);
letter-spacing: 1px;
margin-bottom: 5px;
opacity: 0.8;
}
.item-description {
font-size: 13px;
color: #ccc;
line-height: 1.4;
margin: 0;
}

View File

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import "./CraftModal.css"; import "./CraftModal.css";
import { getServerUrl } from "../../../../config/api";
const CraftModal = ({ const CraftModal = ({
recipe, recipe,
@ -10,6 +11,15 @@ const CraftModal = ({
}) => { }) => {
if (!recipe) return null; if (!recipe) return null;
const CONNECT_URL = getServerUrl();
const ASSET_BASE_URL = `${CONNECT_URL}/static/`;
const getFullTextureUrl = (path) => {
if (!path) return "/assets/no-image.png";
if (path.startsWith("http")) return path;
return `${ASSET_BASE_URL}${path}`;
};
const isBusy = !!activeCraft; const isBusy = !!activeCraft;
const outputQty = Object.values(recipe.output || {})[0] || 1; const outputQty = Object.values(recipe.output || {})[0] || 1;
const canAfford = recipe.ingredients?.every( const canAfford = recipe.ingredients?.every(
@ -19,7 +29,7 @@ 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-header"> <div className="modal-headerr">
<h3> <h3>
<i className="fas fa-tools"></i> Construction: {recipe.displayName} <i className="fas fa-tools"></i> Construction: {recipe.displayName}
</h3> </h3>
@ -29,6 +39,25 @@ const CraftModal = ({
</div> </div>
<div className="modal-body"> <div className="modal-body">
{/* Секція з картинкою предмета */}
<div className="item-preview-header">
<div className="item-icon-container">
<img
src={getFullTextureUrl(recipe.texture)}
alt={recipe.displayName}
className="item-display-icon"
/>
<div className="item-qty-badge">x{outputQty}</div>
</div>
<div className="item-header-info">
<span className="item-type-tag">PROTOTYPE_UNIT</span>
<p className="item-description">
{recipe.description ||
"Technical data encrypted or unavailable."}
</p>
</div>
</div>
<div className="requirements-section"> <div className="requirements-section">
<h4> <h4>
<i className="fas fa-list-ul"></i> Required Resources <i className="fas fa-list-ul"></i> Required Resources

View File

@ -10,10 +10,11 @@
animation: modalSlideUp 0.3s ease-out; animation: modalSlideUp 0.3s ease-out;
} }
.modal-header { .modal-headerr {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20px; gap: 20px;
justify-content: left;
margin-bottom: 20px; margin-bottom: 20px;
padding-bottom: 15px; padding-bottom: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05);
@ -91,3 +92,61 @@
opacity: 1; opacity: 1;
} }
} }
.loot-list-full {
display: flex;
flex-direction: column;
gap: 10px;
background: rgba(0, 0, 0, 0.2);
padding: 12px;
border-radius: 8px;
}
.loot-detail-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.loot-detail-item:last-child {
border-bottom: none;
}
.loot-item-icon {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
}
.loot-item-icon img {
width: 32px;
height: 32px;
object-fit: contain;
}
.loot-item-info {
display: flex;
flex-direction: column;
}
.loot-item-name {
font-weight: 600;
color: #fff;
font-size: 14px;
}
.loot-item-meta {
font-size: 12px;
color: #aaa;
}
.fallback-mini {
color: #444;
font-weight: bold;
}

View File

@ -22,6 +22,49 @@ const DatapackDetailsModal = ({ data, onClose }) => {
); );
}; };
const renderLoot = () => {
if (!data.loot || data.loot.length === 0) return null;
return (
<div className="details-section">
<h4>Loot Table</h4>
<div className="loot-list-full">
{data.loot.map((entry, idx) => {
const itemInfo = GameDataManager.getItem(entry.id);
const countDisplay =
typeof entry.count === "object"
? `${entry.count.min}-${entry.count.max}`
: entry.count;
return (
<div key={idx} className="loot-detail-item">
<div className="loot-item-icon">
{itemInfo?.texture ? (
<img
src={`${config.serverUrl}/static/${itemInfo.texture}`}
alt={itemInfo.displayName}
/>
) : (
<div className="fallback-mini">?</div>
)}
</div>
<div className="loot-item-info">
<span className="loot-item-name">
{itemInfo?.displayName || entry.id}
</span>
<span className="loot-item-meta">
{Math.round(entry.chance * 100)}% chance Amount:{" "}
{countDisplay}
</span>
</div>
</div>
);
})}
</div>
</div>
);
};
return ( return (
<div className="modal-overlay" onClick={onClose}> <div className="modal-overlay" onClick={onClose}>
<div <div
@ -32,7 +75,7 @@ const DatapackDetailsModal = ({ data, onClose }) => {
&times; &times;
</button> </button>
<div className="modal-header"> <div className="modal-headerr">
<div className="modal-icon-big"> <div className="modal-icon-big">
{data.texture ? ( {data.texture ? (
<img src={`${config.serverUrl}/static/${data.texture}`} alt="" /> <img src={`${config.serverUrl}/static/${data.texture}`} alt="" />
@ -63,6 +106,8 @@ const DatapackDetailsModal = ({ data, onClose }) => {
)} )}
</div> </div>
{data.sectionType === "hostiles" && renderLoot()}
{data.ingredients && ( {data.ingredients && (
<div className="details-section"> <div className="details-section">
<h4>Recipe Requirements</h4> <h4>Recipe Requirements</h4>

View File

@ -0,0 +1,187 @@
.dungeon-summary-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(2, 5, 8, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
backdrop-filter: blur(8px);
}
.summary-card {
width: 490px;
background: #0a0f18;
border: 1px solid #00d4ff;
padding: 40px;
position: relative;
box-shadow: 0 0 50px rgba(0, 212, 255, 0.15);
}
.summary-title {
color: #00d4ff;
font-family: "Orbitron", sans-serif;
letter-spacing: 4px;
margin-bottom: 5px;
font-size: 1.5rem;
}
.summary-line {
height: 2px;
background: linear-gradient(90deg, #00d4ff, transparent);
margin-bottom: 30px;
}
.reward-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.stat-box {
background: rgba(26, 38, 56, 0.3);
padding: 15px;
border-left: 3px solid #00d4ff;
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 10px;
color: #4a5d75;
margin-bottom: 5px;
}
.stat-value {
font-size: 1.2rem;
font-weight: bold;
}
.loot-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
gap: 15px;
margin-top: 15px;
max-height: 250px;
overflow-y: auto;
padding-right: 5px;
}
.loot-item-slot {
width: 70px;
height: 70px;
background: #05080c;
border: 1px solid #1a2638;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.loot-img-container img {
max-width: 80%;
max-height: 80%;
}
.loot-qty {
position: absolute;
bottom: 2px;
right: 5px;
font-size: 11px;
color: #00d4ff;
font-weight: bold;
text-shadow: 1px 1px 2px #000;
}
.summary-btn {
margin-top: 40px;
width: 100%;
padding: 15px;
background: transparent;
border: 1px solid #00d4ff;
color: #00d4ff;
font-family: "Orbitron", sans-serif;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 2px;
}
.summary-btn:hover {
background: rgba(0, 212, 255, 0.1);
box-shadow: inset 0 0 15px rgba(0, 212, 255, 0.3);
}
@media screen and (max-width: 768px) {
.dungeon-summary-overlay {
padding: 15px;
}
.summary-card {
width: 100%;
max-width: 400px;
padding: 25px 20px;
box-sizing: border-box;
}
.summary-title {
font-size: 1.1rem;
letter-spacing: 2px;
text-align: center;
}
.summary-line {
margin-bottom: 20px;
}
.reward-stats {
grid-template-columns: 1fr;
gap: 10px;
margin-bottom: 20px;
}
.stat-box {
padding: 10px;
}
.stat-value {
font-size: 1rem;
}
.loot-grid {
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 10px;
max-height: 200px;
}
.loot-item-slot {
width: 60px;
height: 60px;
}
.summary-btn {
margin-top: 25px;
padding: 12px;
font-size: 0.8rem;
letter-spacing: 1px;
}
}
@media screen and (max-width: 380px) {
.summary-card {
padding: 20px 15px;
}
.loot-grid {
grid-template-columns: repeat(4, 1fr);
}
.loot-item-slot {
width: 55px;
height: 55px;
}
}

View File

@ -0,0 +1,74 @@
import React from "react";
import GameDataManager from "../../../../services/GameDataManager.js";
import { getServerUrl } from "../../../../config/api.js";
import "./DungeonFinish.css";
const DungeonFinish = ({ rewards, onExit }) => {
const CONNECT_URL = getServerUrl();
const ASSET_BASE_URL = `${CONNECT_URL}/static/`;
const getFullTextureUrl = (path) => {
if (!path) return "/assets/no-image.png";
if (path.startsWith("http")) return path;
return `${ASSET_BASE_URL}${path}`;
};
return (
<div className="dungeon-summary-overlay">
<div className="summary-card">
<div className="summary-header">
<div className="glitch-wrapper">
<h2 className="summary-title" data-text="MISSION_ACCOMPLISHED">
MISSION_ACCOMPLISHED
</h2>
</div>
<div className="summary-line"></div>
</div>
<div className="summary-body">
<div className="reward-stats">
<div className="stat-box">
<span className="stat-label">EXPERIENCE_DATA</span>
<span className="stat-value">+{rewards.xp || 0} XP</span>
</div>
<div className="stat-box">
<span className="stat-label">CREDITS_TRANSFER</span>
<span className="stat-value cyan-text">
+{rewards.credits || 0} CR
</span>
</div>
</div>
<div className="loot-section">
<h4 className="section-label">ASSETS_RECOVERED</h4>
<div className="loot-grid">
{rewards.items && rewards.items.length > 0 ? (
rewards.items.map((item, idx) => {
const itemData = GameDataManager.getItem(item.id);
const textureUrl = getFullTextureUrl(itemData?.texture);
return (
<div key={idx} className="loot-item-slot">
<div className="loot-img-container">
<img src={textureUrl} />
</div>
<span className="loot-qty">x{item.count}</span>
<div className="loot-name-hint"></div>
</div>
);
})
) : (
<div className="no-loot-msg">NO_RESOURCES_FOUND</div>
)}
</div>
</div>
</div>
<button className="summary-btn" onClick={onExit}>
CONFIRM & RETURN TO BASE
</button>
</div>
</div>
);
};
export default DungeonFinish;

View File

@ -12,133 +12,184 @@
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
} }
.modal-content { .datapack-modal-content {
background: #12151a; background: #0f1115;
border: 1px solid #00d2ff; border: 1px solid rgba(0, 210, 255, 0.3);
border-radius: 8px; width: 90%;
width: 100%; max-width: 450px;
max-width: 400px; border-radius: 12px;
padding: 25px;
position: relative; position: relative;
box-shadow: 0 0 30px rgba(0, 210, 255, 0.2); padding: 25px;
color: #fff; box-shadow: 0 20px 50px rgba(0, 0, 0, 0.8);
font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif; animation: modalSlideUp 0.3s ease-out;
}
.modal-close {
position: absolute;
top: 10px;
right: 15px;
background: none;
border: none;
color: #888;
font-size: 28px;
cursor: pointer;
transition: color 0.2s;
}
.modal-close:hover {
color: #fff; color: #fff;
} }
.details-header { .modal-header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 15px; gap: 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1); margin-bottom: 20px;
padding-bottom: 10px; padding-bottom: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
} }
.item-name { .modal-icon-big {
width: 80px;
height: 80px;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.modal-icon-big img {
width: 60px;
height: 60px;
object-fit: contain;
}
.modal-icon-big.common {
border-color: #888;
}
.modal-icon-big.rare {
border-color: #0070dd;
box-shadow: inset 0 0 10px rgba(0, 112, 221, 0.2);
}
.modal-icon-big.epic {
border-color: #a335ee;
box-shadow: inset 0 0 10px rgba(163, 53, 238, 0.2);
}
.modal-icon-big.legendary {
border-color: #ff8000;
box-shadow: inset 0 0 10px rgba(255, 128, 0, 0.2);
}
.modal-title-group h3 {
margin: 0; margin: 0;
font-size: 1.2rem; font-family: "Orbitron", sans-serif;
font-size: 1.3rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px;
} }
.rarity-badge { .modal-title-group h3.common {
font-size: 0.7rem; color: #fff;
padding: 2px 8px;
border-radius: 4px;
text-transform: uppercase;
background: rgba(255, 255, 255, 0.1);
} }
.modal-title-group h3.rare {
.item-name.common { color: #00d2ff;
color: #ffffff;
} }
.item-name.uncommon { .modal-title-group h3.epic {
color: #1eff00;
}
.item-name.rare {
color: #0070dd;
}
.item-name.epic {
color: #a335ee; color: #a335ee;
} }
.item-name.legendary { .modal-title-group h3.legendary {
color: #ff8000; color: #ff8000;
} }
.item-description { .modal-raw-id {
font-size: 0.7rem;
color: #888;
margin-top: 4px;
font-family: monospace;
}
.details-description {
font-size: 0.9rem; font-size: 0.9rem;
line-height: 1.5;
color: #aaa; color: #aaa;
line-height: 1.4;
margin-bottom: 20px; margin-bottom: 20px;
font-style: italic;
}
.details-section h4 {
font-size: 0.8rem;
text-transform: uppercase;
color: #00d2ff;
letter-spacing: 1px;
margin-bottom: 10px;
border-left: 3px solid #00d2ff;
padding-left: 10px;
} }
.item-stats-container { .item-stats-container {
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.2);
padding: 10px; padding: 10px;
border-radius: 4px; border-radius: 8px;
margin-bottom: 25px;
} }
.stat-row { .stat-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 5px 0; padding: 8px 0;
font-size: 0.85rem; border-bottom: 1px solid rgba(255, 255, 255, 0.03);
}
.stat-row:last-child {
border-bottom: none;
} }
.stat-label { .stat-label {
color: #00d2ff; color: #888;
font-size: 0.85rem;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.stat-value { .stat-value {
color: #fff; color: #00ff88;
font-family: monospace;
font-weight: bold; font-weight: bold;
} }
.btn-equip { .btn-equip {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
background: none; background: rgba(0, 210, 255, 0.05);
border: 1px solid #00d2ff; border: 1px solid #00d2ff;
color: #00d2ff; color: #00d2ff;
cursor: pointer; cursor: pointer;
text-transform: uppercase; font-family: "Orbitron", sans-serif;
font-weight: bold; font-size: 0.8rem;
letter-spacing: 1px;
transition: all 0.2s; transition: all 0.2s;
} }
.btn-equip:hover { .btn-equip:hover {
background: #00d2ff; background: #00d2ff;
color: #000; color: #000;
box-shadow: 0 0 15px rgba(0, 210, 255, 0.4);
} }
.btn-equip.unequip { .btn-equip.unequip {
border-color: #ff4444; border-color: #ff4444;
color: #ff4444; color: #ff4444;
background: rgba(255, 68, 68, 0.05);
} }
.btn-equip.unequip:hover { .btn-equip.unequip:hover {
background: #ff4444; background: #ff4444;
color: #fff; color: #fff;
} }
.modal-close {
position: absolute;
top: 15px;
right: 15px;
background: none;
border: none;
color: #444;
font-size: 24px;
cursor: pointer;
}
@keyframes modalSlideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

View File

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import "./ItemModal.css"; import "./ItemModal.css";
import { getServerUrl } from "../../../../config/api";
const ItemModal = ({ const ItemModal = ({
item, item,
@ -12,23 +13,45 @@ const ItemModal = ({
}) => { }) => {
if (!item) return null; if (!item) return null;
const CONNECT_URL = getServerUrl();
const ASSET_BASE_URL = `${CONNECT_URL}/static/`;
const getFullTextureUrl = (path) => {
if (!path) return "/assets/no-image.png";
if (path.startsWith("http")) return path;
return `${ASSET_BASE_URL}${path}`;
};
return ( return (
<div className="modal-overlay" onClick={onClose}> <div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}> <div
className="datapack-modal-content"
onClick={(e) => e.stopPropagation()}
>
<button className="modal-close" onClick={onClose}> <button className="modal-close" onClick={onClose}>
&times; &times;
</button> </button>
<div className="details-view"> <div className="modal-header">
<div className="details-header"> <div className={`modal-icon-big ${item.rarity}`}>
<h4 className={`item-name ${item.rarity}`}> <img src={getFullTextureUrl(item.texture)} alt={item.displayName} />
{item.displayName || item.name} </div>
</h4> <div className="modal-title-group">
<span className={`rarity-badge ${item.rarity}`}>{item.rarity}</span> <h3 className={item.rarity}>{item.displayName || item.name}</h3>
<div className="modal-raw-id">
{item.rarity?.toUpperCase()} SYSTEM_ID: {item.id}
</div>
</div>
</div> </div>
<p className="item-description">{item.description}</p> <div className="details-section">
<p className="details-description">{item.description}</p>
</div>
<div className="details-section">
<h4>
<i className="fas fa-microchip"></i> Technical Specs
</h4>
<div className="item-stats-container"> <div className="item-stats-container">
{item.stats && {item.stats &&
Object.entries(item.stats).map(([statName, value]) => ( Object.entries(item.stats).map(([statName, value]) => (
@ -43,7 +66,9 @@ const ItemModal = ({
</div> </div>
))} ))}
</div> </div>
</div>
<div className="modal-actions" style={{ marginTop: "20px" }}>
{isEquipped ? ( {isEquipped ? (
<button <button
className="btn-equip unequip" className="btn-equip unequip"
@ -52,7 +77,7 @@ const ItemModal = ({
onClose(); onClose();
}} }}
> >
DISCONNECT_SYSTEM TERMINATE_CONNECTION
</button> </button>
) : ( ) : (
item.canEquip && ( item.canEquip && (

View File

@ -194,6 +194,9 @@
.message { .message {
font-size: 13px; font-size: 13px;
line-height: 1.4; line-height: 1.4;
min-height: min-content;
padding: 4px 0;
display: block;
} }
.msg-time { .msg-time {
@ -214,7 +217,12 @@
.message.system .msg-text { .message.system .msg-text {
color: #888; color: #888;
word-break: break-all;
overflow-wrap: anywhere;
display: inline-block;
font-style: italic; font-style: italic;
white-space: pre-wrap;
max-width: 100%;
} }
.chat-input-area { .chat-input-area {
@ -411,3 +419,29 @@
color: #ff3e3e; color: #ff3e3e;
text-shadow: 0 0 8px rgba(255, 62, 62, 0.4); text-shadow: 0 0 8px rgba(255, 62, 62, 0.4);
} }
.message {
font-size: 13px;
line-height: 1.4;
min-height: min-content;
padding: 4px 0;
display: block;
word-wrap: break-word;
}
.msg-text {
color: #fff;
word-break: break-all;
overflow-wrap: anywhere;
white-space: pre-wrap;
display: inline;
}
.message.system .msg-author {
color: #ff3e3e;
}
.message.system .msg-text {
color: #888;
font-style: italic;
}

View File

@ -1,4 +1,3 @@
/* Контейнер вкладки */
#dungeons-tab { #dungeons-tab {
height: 100%; height: 100%;
background: #05080c; background: #05080c;
@ -13,7 +12,6 @@
border-top: 1px solid rgba(0, 212, 255, 0.2); border-top: 1px solid rgba(0, 212, 255, 0.2);
} }
/* --- Ліва панель --- */
.dungeon-selector { .dungeon-selector {
border-right: 1px solid rgba(0, 212, 255, 0.1); border-right: 1px solid rgba(0, 212, 255, 0.1);
display: flex; display: flex;
@ -96,7 +94,6 @@
font-family: "Space Mono", monospace; font-family: "Space Mono", monospace;
} }
/* --- Права панель --- */
.dungeon-view { .dungeon-view {
padding: 20px; padding: 20px;
display: flex; display: flex;
@ -119,6 +116,18 @@
background: rgba(0, 212, 255, 0.03); background: rgba(0, 212, 255, 0.03);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: flex;
align-items: center;
gap: 15px;
}
.back-to-list {
display: none;
background: none;
border: 1px solid #00d4ff;
color: #00d4ff;
padding: 8px 12px;
cursor: pointer;
} }
.scanline-horizontal { .scanline-horizontal {
@ -174,11 +183,6 @@
line-height: 1.6; line-height: 1.6;
} }
/* --- Нагороди --- */
.expected-rewards-section {
margin-bottom: 30px;
}
.section-header { .section-header {
display: flex; display: flex;
align-items: center; align-items: center;
@ -195,7 +199,7 @@
.rewards-grid { .rewards-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px; gap: 10px;
} }
@ -209,14 +213,15 @@
} }
.reward-icon-container { .reward-icon-container {
width: 40px; width: 36px;
height: 40px; height: 36px;
background: #0a0f18; background: #0a0f18;
border: 1px solid rgba(0, 212, 255, 0.2); border: 1px solid rgba(0, 212, 255, 0.2);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-right: 15px; margin-right: 12px;
flex-shrink: 0;
} }
.reward-icon-container img { .reward-icon-container img {
@ -225,29 +230,27 @@
object-fit: contain; object-fit: contain;
} }
.reward-icon-container i {
color: #4a5d75;
font-size: 1.2rem;
}
.reward-text { .reward-text {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
} }
.reward-name { .reward-name {
font-size: 0.85rem; font-size: 0.75rem;
color: #fff; color: #fff;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.reward-chance { .reward-chance {
font-size: 0.7rem; font-size: 0.65rem;
color: #00ff88; color: #00ff88;
font-family: "Space Mono", monospace; font-family: "Space Mono", monospace;
} }
/* Кольори раритетності */
.reward-entry.common { .reward-entry.common {
border-left-color: #4a5d75; border-left-color: #4a5d75;
} }
@ -264,20 +267,15 @@
border-left-color: #ffaa00; border-left-color: #ffaa00;
} }
/* --- Кнопка --- */
.action-area {
margin-top: 20px;
}
.initiate-deployment-btn { .initiate-deployment-btn {
width: 100%; width: 100%;
padding: 20px; padding: 18px;
background: #00d4ff; background: #00d4ff;
border: none; border: none;
color: #000; color: #000;
font-family: "Orbitron", sans-serif; font-family: "Orbitron", sans-serif;
font-weight: 900; font-weight: 900;
font-size: 1.1rem; font-size: 1rem;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -288,15 +286,9 @@
.initiate-deployment-btn:hover { .initiate-deployment-btn:hover {
background: #fff; background: #fff;
box-shadow: 0 0 30px rgba(0, 212, 255, 0.5); box-shadow: 0 0 20px rgba(0, 212, 255, 0.4);
transform: translateY(-2px);
} }
.initiate-deployment-btn i {
font-size: 1.2rem;
}
/* --- Placeholder --- */
.dungeon-placeholder { .dungeon-placeholder {
height: 100%; height: 100%;
display: flex; display: flex;
@ -307,8 +299,8 @@
} }
.radar-scanner { .radar-scanner {
width: 100px; width: 80px;
height: 100px; height: 80px;
border: 2px solid rgba(0, 212, 255, 0.2); border: 2px solid rgba(0, 212, 255, 0.2);
border-radius: 50%; border-radius: 50%;
position: relative; position: relative;
@ -336,17 +328,43 @@
} }
} }
/* Кастомний скрол */ @media screen and (max-width: 768px) {
.dungeons-container {
grid-template-columns: 1fr;
}
.dungeons-container.view-active .dungeon-selector {
display: none;
}
.dungeons-container:not(.view-active) .dungeon-view {
display: none;
}
.back-to-list {
display: block;
}
.mission-title {
font-size: 1.3rem;
}
.dungeon-view {
padding: 10px;
}
.rewards-grid {
grid-template-columns: 1fr;
}
}
.custom-scroll::-webkit-scrollbar { .custom-scroll::-webkit-scrollbar {
width: 5px; width: 4px;
} }
.custom-scroll::-webkit-scrollbar-track { .custom-scroll::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.1);
} }
.custom-scroll::-webkit-scrollbar-thumb { .custom-scroll::-webkit-scrollbar-thumb {
background: #1a2638; background: #1a2638;
border-radius: 10px; border-radius: 4px;
}
.custom-scroll::-webkit-scrollbar-thumb:hover {
background: #00d4ff;
} }

View File

@ -233,3 +233,41 @@
border-radius: 3px; border-radius: 3px;
z-index: 2; z-index: 2;
} }
.qty-label {
position: absolute;
bottom: 2px;
right: 4px;
font-size: 11px;
font-weight: 800;
color: #fff;
text-shadow:
1px 1px 0 #000,
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
0 0 5px rgba(0, 0, 0, 0.8);
pointer-events: none;
z-index: 3;
}
.item-slot {
width: 60px;
height: 60px;
background: rgba(5, 8, 12, 0.9);
border: 1px solid #1a2638;
display: flex;
align-items: center;
justify-content: center;
position: relative;
cursor: pointer;
transition: 0.2s;
overflow: visible;
}
.item-img-grid {
max-width: 80%;
max-height: 80%;
object-fit: contain;
pointer-events: none;
}

View File

@ -7,8 +7,10 @@
"energyCost": 0, "energyCost": 0,
"repeatable": false, "repeatable": false,
"missionArea": "space", "missionArea": "space",
"raid": false, "_comment_1":"Future raid type picking, when you can have friends in the dugeon to help you.", "raid": false,
"missionAllowed": [], "_comment_2":"Future ship type picking, when ship classes are started" "_comment_1": "Future raid type picking, when you can have friends in the dugeon to help you.",
"missionAllowed": [],
"_comment_2": "Future ship type picking, when ship classes are started"
}, },
"rooms": [ "rooms": [
{ "id": "original:tutorial/tutorial_enemy_room" }, { "id": "original:tutorial/tutorial_enemy_room" },

View File

@ -8,6 +8,19 @@
"damage": 4, "damage": 4,
"critical.chance": 0.3, "critical.chance": 0.3,
"attack.rate": 2 "attack.rate": 2
} },
"loot": [
{
"id": "original:ore_coal",
"chance": 1.0,
"count": 50
},
{
"id": "original:ore_copper",
"chance": 1.0,
"count": 20
}
],
"meta": {}
} }
} }

View File

@ -6,8 +6,19 @@
"health": 30, "health": 30,
"defense": 0.0, "defense": 0.0,
"damage": 2, "damage": 2,
"critical,chance": 0.0, "critical.chance": 0.0,
"attack.rate": 1 "attack.rate": 1
},
"loot": [
{
"id": "original:alloy_steel",
"chance": 0.4,
"count": {
"min": 1,
"max": 2
} }
} }
],
"meta": {}
}
} }

View File

@ -1,8 +1,14 @@
{ {
"rooms": { "rooms": {
"id": "original:tutorial/tutorial_boss_room", "id": "original:tutorial/tutorial_boss_room",
"displayName": "rooms.original.tutorial.tutorial_boss_room.name",
"description": "rooms.original.tutorial.tutorial_boss_room.desc",
"hostiles": ["original:tutorial/tutorial_boss_hostile"], "hostiles": ["original:tutorial/tutorial_boss_hostile"],
"gainXp": 4, "gainXp": 4,
"credits": 200 "credits": 200,
"loot": [],
"meta": {
"isBossRoom": true
}
} }
} }

View File

@ -1,8 +1,14 @@
{ {
"rooms": { "rooms": {
"id": "original:tutorial/tutorial_enemy_room", "id": "original:tutorial/tutorial_enemy_room",
"displayName": "rooms.original.tutorial.tutorial_enemy_room.name",
"description": "rooms.original.tutorial.tutorial_enemy_room.desc",
"hostiles": ["original:tutorial/tutorial_hostile"], "hostiles": ["original:tutorial/tutorial_hostile"],
"gainXp": 3, "gainXp": 3,
"credits": 30 "credits": 30,
"loot": [],
"meta": {
"isBossRoom": false
}
} }
} }

View File

@ -1,6 +1,20 @@
{ {
"rooms": { "rooms": {
"id": "original:tutorial/tutorial_loot_room", "id": "original:tutorial/tutorial_loot_room",
"loot": [{ "id": "original:bio_pulp", "count": 1 }] "displayName": "rooms.original.tutorial.tutorial_loot_room.name",
"description": "rooms.original.tutorial.tutorial_loot_room.desc",
"hostiles": [],
"gainXp": 0,
"credits": 0,
"loot": [
{
"id": "original:bio_pulp",
"chance": 1.0,
"count": 1
}
],
"meta": {
"isBossRoom": false
}
} }
} }

View File

@ -1,11 +1,16 @@
{ {
"materials": { "materials": {
"id": "original:circuit_advanced", "id": "original:circuit_advanced",
"texture": "test/assets/textures/materials/circuits/advanced_circuit.png", "texture": "original/assets/textures/materials/circuits/advanced_circuit.png",
"displayName": "items.materials.original.circuits.advanced", "displayName": "items.materials.original.circuits.advanced",
"description": "items.materials.original.circuits.advanced.desc", "description": "items.materials.original.circuits.advanced.desc",
"meta": { "meta": {
"storeCategory": "original:materials" "storeCategory": "original:materials",
"storePrice": 50,
"storeSellValue": 10,
"storeShowWeight": 10,
"storeFeaturedDiscountPercentage": 0,
"storeFeaturedShowWeight": 10
} }
} }
} }

View File

@ -1,11 +1,16 @@
{ {
"materials": { "materials": {
"id": "original:circuit_ai_core", "id": "original:circuit_ai_core",
"texture": "test/assets/textures/materials/circuits/ai_core.gif", "texture": "original/assets/textures/materials/circuits/ai_core.gif",
"displayName": "items.materials.original.circuits.ai_core", "displayName": "items.materials.original.circuits.ai_core",
"description": "items.materials.original.circuits.ai_core.desc", "description": "items.materials.original.circuits.ai_core.desc",
"meta": { "meta": {
"storeCategory": "original:materials" "storeCategory": "original:materials",
"storePrice": 50,
"storeSellValue": 10,
"storeShowWeight": 10,
"storeFeaturedDiscountPercentage": 0,
"storeFeaturedShowWeight": 10
} }
} }
} }

View File

@ -1,11 +1,16 @@
{ {
"materials": { "materials": {
"id": "original:circuit_basic", "id": "original:circuit_basic",
"texture": "test/assets/textures/materials/circuits/basic_circuit.png", "texture": "original/assets/textures/materials/circuits/basic_circuit.png",
"displayName": "items.materials.original.circuits.basic", "displayName": "items.materials.original.circuits.basic",
"description": "items.materials.original.circuits.basic.desc", "description": "items.materials.original.circuits.basic.desc",
"meta": { "meta": {
"storeCategory": "original:materials" "storeCategory": "original:materials",
"storePrice": 50,
"storeSellValue": 10,
"storeShowWeight": 10,
"storeFeaturedDiscountPercentage": 0,
"storeFeaturedShowWeight": 10
} }
} }
} }

View File

@ -1,11 +1,16 @@
{ {
"materials": { "materials": {
"id": "original:circuit_processing_unit", "id": "original:circuit_processing_unit",
"texture": "test/assets/textures/materials/circuits/processing_unit.png", "texture": "original/assets/textures/materials/circuits/processing_unit.png",
"displayName": "items.materials.original.circuits.processing_unit", "displayName": "items.materials.original.circuits.processing_unit",
"description": "items.materials.original.circuits.processing_unit.desc", "description": "items.materials.original.circuits.processing_unit.desc",
"meta": { "meta": {
"storeCategory": "original:materials" "storeCategory": "original:materials",
"storePrice": 50,
"storeSellValue": 10,
"storeShowWeight": 10,
"storeFeaturedDiscountPercentage": 0,
"storeFeaturedShowWeight": 10
} }
} }
} }

View File

@ -1,11 +1,16 @@
{ {
"materials": { "materials": {
"id": "original:circuit_quantum_processor", "id": "original:circuit_quantum_processor",
"texture": "test/assets/textures/materials/circuits/quantum_processor.png", "texture": "original/assets/textures/materials/circuits/quantum_processor.png",
"displayName": "items.materials.original.circuits.quantum_processor", "displayName": "items.materials.original.circuits.quantum_processor",
"description": "items.materials.original.circuits.quantum_processor.desc", "description": "items.materials.original.circuits.quantum_processor.desc",
"meta": { "meta": {
"storeCategory": "original:materials" "storeCategory": "original:materials",
"storePrice": 50,
"storeSellValue": 10,
"storeShowWeight": 10,
"storeFeaturedDiscountPercentage": 0,
"storeFeaturedShowWeight": 10
} }
} }
} }

View File

@ -147,7 +147,7 @@ class DatapackLoader {
const data = json[typeKey]; const data = json[typeKey];
if (!data) return; if (!data) return;
const fullId = `${packName}:${data.id}`; const fullId = `${data.id}`;
switch (typeKey) { switch (typeKey) {
case "armour": case "armour":

View File

@ -16,7 +16,6 @@ class DungeonManager {
currentEnemyHp: undefined, currentEnemyHp: undefined,
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.getCurrentRoomData(playerId);
} }
@ -27,44 +26,79 @@ class DungeonManager {
const dungeon = DatapackLoader.getDungeon(session.dungeonId); const dungeon = DatapackLoader.getDungeon(session.dungeonId);
const roomRef = dungeon.rooms[session.currentRoomIndex]; const roomRef = dungeon.rooms[session.currentRoomIndex];
const roomData = DatapackLoader.getRoom(roomRef.id); const rawRoom = DatapackLoader.getRoom(roomRef.id);
if (!roomData) return null;
const hostiles = (roomData.hostiles || []) if (!rawRoom) return null;
.map((hId) => {
const hostile = DatapackLoader.getEnemy(hId); const hostiles = (rawRoom.hostiles || [])
return hostile ? { ...hostile } : null; .map((hId) => DatapackLoader.getEnemy(hId))
})
.filter(Boolean); .filter(Boolean);
return { return {
roomIndex: session.currentRoomIndex, roomIndex: session.currentRoomIndex,
totalRooms: dungeon.rooms.length, totalRooms: dungeon.rooms.length,
config: roomData, config: rawRoom,
hostiles, hostiles,
}; };
} }
processCombatStep(playerId, enemyId) {
const session = this.activeSessions.get(playerId);
if (!session || session.isFinished) return null;
const enemy = DatapackLoader.getEnemy(enemyId);
if (!enemy) return null;
if (session.currentEnemyHp === undefined) {
session.currentEnemyHp = enemy.stats?.health || 100;
}
const damage = Math.floor(Math.random() * 10) + 20;
session.currentEnemyHp -= damage;
const isDefeated = session.currentEnemyHp <= 0;
let lootDropped = [];
if (isDefeated) {
if (enemy.loot) {
lootDropped = this._generateLoot(enemy.loot);
session.rewards.items.push(...lootDropped);
}
session.currentEnemyHp = undefined;
}
return {
damageDealt: damage,
enemyHp: Math.max(0, session.currentEnemyHp || 0),
targetDefeated: isDefeated,
loot: lootDropped,
};
}
moveToNextRoom(playerId) { 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 roomRef = dungeon.rooms[session.currentRoomIndex];
const currentRoom = DatapackLoader.getRoom(roomRef.id); const rawRoom = DatapackLoader.getRoom(roomRef.id);
if (currentRoom) { if (rawRoom) {
session.rewards.xp += currentRoom.gainXp || 0; if (rawRoom.hostiles) {
session.rewards.credits += currentRoom.credits || 0; rawRoom.hostiles.forEach((hId) => {
const enemy = DatapackLoader.getEnemy(hId);
if (currentRoom.loot && Array.isArray(currentRoom.loot)) { if (enemy) {
currentRoom.loot.forEach((item) => { session.rewards.xp += enemy.gainXp || 0;
session.rewards.items.push({ ...item }); 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));
}
} }
session.currentEnemyHp = undefined;
if (session.currentRoomIndex < dungeon.rooms.length - 1) { if (session.currentRoomIndex < dungeon.rooms.length - 1) {
session.currentRoomIndex++; session.currentRoomIndex++;
@ -72,10 +106,23 @@ class DungeonManager {
} }
session.isFinished = true; session.isFinished = true;
return { return { status: "completed", rewards: session.rewards };
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) {

View File

@ -9,25 +9,16 @@ module.exports = (io, socket) => {
try { try {
if (!userId) return; if (!userId) return;
const dungeon = DatapackLoader.getDungeon(dungeonId); const dungeon = DatapackLoader.getDungeon(dungeonId);
if (!dungeon) {
return socket.emit("error", { message: "Dungeon not found" });
}
const player = await Player.findByPk(userId); const player = await Player.findByPk(userId);
const energyCost = dungeon.meta?.energyCost || 0; const energyCost = dungeon?.meta?.energyCost || 0;
if (player.energy < energyCost) { if (!dungeon)
return socket.emit("error", { message: "Dungeon not found" });
if (player.energy < energyCost)
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 firstRoom = dungeonManager.startDungeon(userId, dungeonId);
if (!firstRoom) {
return socket.emit("error", {
message: "Failed to initialize dungeon",
});
}
socket.emit("dungeon:started", { socket.emit("dungeon:started", {
dungeonId: dungeon.id, dungeonId: dungeon.id,
@ -38,69 +29,34 @@ module.exports = (io, socket) => {
remainingEnergy: player.energy - energyCost, remainingEnergy: player.energy - energyCost,
}); });
} catch (err) { } catch (err) {
console.error("Dungeon Start Error:", err);
socket.emit("error", { message: "Critical deployment failure" }); socket.emit("error", { message: "Critical deployment failure" });
} }
}); });
socket.on("dungeon:combat_step", async ({ enemyId }) => { socket.on("dungeon:combat_step", async ({ enemyId }) => {
try { const result = dungeonManager.processCombatStep(userId, enemyId);
if (!userId) return; if (!result) return;
const session = dungeonManager.activeSessions.get(userId);
if (!session || session.isFinished) return;
const enemyTemplate = DatapackLoader.getEnemy(enemyId);
if (!enemyTemplate) {
return socket.emit("error", { message: "Target data corrupted" });
}
if (session.currentEnemyHp === undefined) {
session.currentEnemyHp = enemyTemplate.stats?.health || 100;
}
const damage = Math.floor(Math.random() * 10) + 20;
session.currentEnemyHp -= damage;
const isDefeated = session.currentEnemyHp <= 0;
socket.emit("dungeon:combat_result", { socket.emit("dungeon:combat_result", {
damageDealt: damage, ...result,
enemyHp: Math.max(0, session.currentEnemyHp), message: result.targetDefeated
targetDefeated: isDefeated, ? "Enemy eliminated!"
message: `Strike successful. Dealt ${damage} damage.`, : `Strike successful. Dealt ${result.damageDealt} damage.`,
}); });
if (isDefeated) {
session.currentEnemyHp = undefined;
}
} catch (err) {
console.error("Combat Error:", err);
}
}); });
socket.on("dungeon:next_room", async () => { socket.on("dungeon:next_room", async () => {
try { try {
if (!userId) return;
const nextRoom = dungeonManager.moveToNextRoom(userId); const nextRoom = dungeonManager.moveToNextRoom(userId);
if (!nextRoom) { if (!nextRoom)
return socket.emit("error", { return socket.emit("error", { message: "Navigation error" });
message: "Could not proceed to next room",
});
}
if (nextRoom.status === "completed") { if (nextRoom.status === "completed") {
await finalizeDungeon(socket, nextRoom.rewards); await finalizeDungeon(socket, nextRoom.rewards);
} else { } else {
socket.emit("dungeon:room_update", { socket.emit("dungeon:room_update", nextRoom);
room: nextRoom.config,
hostiles: nextRoom.hostiles,
roomIndex: nextRoom.roomIndex,
});
} }
} catch (err) { } catch (err) {
console.error("Dungeon Progress Error:", err);
socket.emit("error", { message: "Navigation system error" }); socket.emit("error", { message: "Navigation system error" });
} }
}); });
@ -115,14 +71,33 @@ async function finalizeDungeon(socket, sessionRewards) {
try { try {
const player = await Player.findByPk(userId); const player = await Player.findByPk(userId);
if (sessionRewards.credits > 0) { if (sessionRewards.credits > 0)
await player.increment("credits", { by: sessionRewards.credits }); await player.increment("credits", { by: sessionRewards.credits });
if (sessionRewards.xp > 0)
await player.increment("experience", { by: sessionRewards.xp });
if (sessionRewards.items.length > 0) {
const consolidated = sessionRewards.items.reduce((acc, curr) => {
acc[curr.id] = (acc[curr.id] || 0) + curr.count;
return acc;
}, {});
for (const [itemId, totalCount] of Object.entries(consolidated)) {
const [invItem] = await Inventory.findOrCreate({
where: { playerId: userId, itemId: itemId },
defaults: { quantity: 0 },
});
await invItem.increment("quantity", { by: totalCount });
} }
socket.emit("dungeon:completed", { sessionRewards.items = Object.entries(consolidated).map(
rewards: sessionRewards, ([id, count]) => ({ id, count }),
message: "Mission successful. All objectives secured.", );
}); }
socket.emit("dungeon:completed", { rewards: sessionRewards });
} catch (err) {
socket.emit("error", { message: "Failed to save rewards" });
} finally { } finally {
dungeonManager.leaveDungeon(userId); dungeonManager.leaveDungeon(userId);
} }