Merge branch 'main' of https://github.com/Korvarix/Galaxy-Strike-Online
This commit is contained in:
commit
4f31d18cf2
53
client/src/components/Meteor/MeteorRegion.css
Normal file
53
client/src/components/Meteor/MeteorRegion.css
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
.meteor-region-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meteor-region-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 15px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meteor-region-content::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meteor-track-local {
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
top: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
width: 1px;
|
||||||
|
background: rgba(0, 210, 255, 0.1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meteor-slider-local {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -1px;
|
||||||
|
width: 2px;
|
||||||
|
height: 60px;
|
||||||
|
background: linear-gradient(to bottom, transparent, #00d2ff, #fff);
|
||||||
|
transition: transform 0.1s linear;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meteor-glow-local {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 10px #00d2ff;
|
||||||
|
}
|
||||||
58
client/src/components/Meteor/MeteorRegion.jsx
Normal file
58
client/src/components/Meteor/MeteorRegion.jsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||||
|
import "./MeteorRegion.css";
|
||||||
|
|
||||||
|
const MeteorRegion = ({ children, className = "", maxHeight }) => {
|
||||||
|
const [scrollProgress, setScrollProgress] = useState(0);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
const updateScroll = useCallback(() => {
|
||||||
|
const el = containerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = el;
|
||||||
|
const progress = scrollTop / (scrollHeight - clientHeight);
|
||||||
|
|
||||||
|
setScrollProgress(isNaN(progress) ? 0 : progress);
|
||||||
|
setIsVisible(scrollHeight > clientHeight);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = containerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(updateScroll);
|
||||||
|
resizeObserver.observe(el);
|
||||||
|
el.addEventListener("scroll", updateScroll);
|
||||||
|
|
||||||
|
updateScroll();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
el.removeEventListener("scroll", updateScroll);
|
||||||
|
};
|
||||||
|
}, [updateScroll]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`meteor-region-wrapper ${className}`} style={{ maxHeight }}>
|
||||||
|
<div className="meteor-region-content" ref={containerRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isVisible && (
|
||||||
|
<div className="meteor-track-local">
|
||||||
|
<div
|
||||||
|
className="meteor-slider-local"
|
||||||
|
style={{
|
||||||
|
transform: `translateY(${scrollProgress * (containerRef.current?.clientHeight - 70)}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="meteor-glow-local" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MeteorRegion;
|
||||||
@ -312,3 +312,13 @@
|
|||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 0px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|||||||
@ -40,3 +40,13 @@ body {
|
|||||||
transparent 50%
|
transparent 50%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 0px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,137 +1,161 @@
|
|||||||
/* Galaxy Strike Online - Main Styles */
|
/* Galaxy Strike Online - Main Styles */
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--primary-color: #00d4ff;
|
--primary-color: #00d4ff;
|
||||||
--secondary-color: #ff6b35;
|
--secondary-color: #ff6b35;
|
||||||
--accent-color: #ff00ff;
|
--accent-color: #ff00ff;
|
||||||
--bg-primary: #0a0e1a;
|
--bg-primary: #0a0e1a;
|
||||||
--bg-secondary: #151923;
|
--bg-secondary: #151923;
|
||||||
--bg-tertiary: #1e2433;
|
--bg-tertiary: #1e2433;
|
||||||
--text-primary: #ffffff;
|
--text-primary: #ffffff;
|
||||||
--text-secondary: #b8c5d6;
|
--text-secondary: #b8c5d6;
|
||||||
--text-muted: #6b7c93;
|
--text-muted: #6b7c93;
|
||||||
--border-color: #2a3241;
|
--border-color: #2a3241;
|
||||||
--success-color: #00ff88;
|
--success-color: #00ff88;
|
||||||
--warning-color: #ffaa00;
|
--warning-color: #ffaa00;
|
||||||
--error-color: #ff3366;
|
--error-color: #ff3366;
|
||||||
--card-bg: rgba(30, 36, 51, 0.8);
|
--card-bg: rgba(30, 36, 51, 0.8);
|
||||||
--hover-bg: rgba(0, 212, 255, 0.1);
|
--hover-bg: rgba(0, 212, 255, 0.1);
|
||||||
--gradient-primary: linear-gradient(135deg, #00d4ff, #0099cc);
|
--gradient-primary: linear-gradient(135deg, #00d4ff, #0099cc);
|
||||||
--gradient-secondary: linear-gradient(135deg, #ff6b35, #ff4500);
|
--gradient-secondary: linear-gradient(135deg, #ff6b35, #ff4500);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Space Mono', monospace;
|
font-family: "Space Mono", monospace;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(circle at 20% 50%, rgba(0, 212, 255, 0.1) 0%, transparent 50%),
|
radial-gradient(
|
||||||
radial-gradient(circle at 80% 80%, rgba(255, 107, 53, 0.1) 0%, transparent 50%),
|
circle at 20% 50%,
|
||||||
radial-gradient(circle at 40% 20%, rgba(255, 0, 255, 0.05) 0%, transparent 50%);
|
rgba(0, 212, 255, 0.1) 0%,
|
||||||
|
transparent 50%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 80% 80%,
|
||||||
|
rgba(255, 107, 53, 0.1) 0%,
|
||||||
|
transparent 50%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
circle at 40% 20%,
|
||||||
|
rgba(255, 0, 255, 0.05) 0%,
|
||||||
|
transparent 50%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.server-controls {
|
.server-controls {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-filters {
|
.server-filters {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-confirmation {
|
.server-confirmation {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-actions-left, .confirm-actions-right {
|
.confirm-actions-left,
|
||||||
width: 100%;
|
.confirm-actions-right {
|
||||||
max-width: 300px;
|
width: 100%;
|
||||||
}
|
max-width: 300px;
|
||||||
|
}
|
||||||
.server-details {
|
|
||||||
flex-direction: column;
|
.server-details {
|
||||||
gap: 8px;
|
flex-direction: column;
|
||||||
}
|
gap: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
@keyframes fadeInUp {
|
@keyframes fadeInUp {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(30px);
|
transform: translateY(30px);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% {
|
0%,
|
||||||
opacity: 1;
|
100% {
|
||||||
}
|
opacity: 1;
|
||||||
50% {
|
}
|
||||||
opacity: 0.5;
|
50% {
|
||||||
}
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.dashboard-grid {
|
.dashboard-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dungeons-container {
|
.dungeons-container {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.base-container {
|
.base-container {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory-container {
|
.inventory-container {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav {
|
.main-nav {
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resources {
|
.resources {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource {
|
.resource {
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-title {
|
.game-title {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.header-center {
|
.header-center {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-btn span {
|
.nav-btn span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-btn {
|
.nav-btn {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 0px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import "./styles/CraftingTab.css";
|
|||||||
import CategorySelector from "../components/CategorySelector";
|
import CategorySelector from "../components/CategorySelector";
|
||||||
import CraftModal from "./components/CraftModal";
|
import CraftModal from "./components/CraftModal";
|
||||||
import { config } from "../../../config/api";
|
import { config } from "../../../config/api";
|
||||||
|
import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
|
||||||
|
|
||||||
const CraftingTab = () => {
|
const CraftingTab = () => {
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
@ -109,8 +110,11 @@ const CraftingTab = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tab-content active">
|
<div
|
||||||
<div className="crafting-container">
|
className="tab-content active"
|
||||||
|
style={{ height: "100%", display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<MeteorRegion className="crafting-container">
|
||||||
{activeCraft && (
|
{activeCraft && (
|
||||||
<div className="active-craft-panel">
|
<div className="active-craft-panel">
|
||||||
<div className="craft-info">
|
<div className="craft-info">
|
||||||
@ -184,7 +188,7 @@ const CraftingTab = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MeteorRegion>
|
||||||
|
|
||||||
<CraftModal
|
<CraftModal
|
||||||
recipe={selectedRecipe}
|
recipe={selectedRecipe}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
import { useSocket } from "../../../hooks/useSocket";
|
import { useSocket } from "../../../hooks/useSocket";
|
||||||
import GameDataManager from "../../../services/GameDataManager.js";
|
import GameDataManager from "../../../services/GameDataManager.js";
|
||||||
import Button from "../../../components/ui/Button";
|
import ItemModal from "./components/ItemModal";
|
||||||
import "./styles/InventoryTab.css";
|
import "./styles/InventoryTab.css";
|
||||||
import { getServerUrl } from "../../../config/api.js";
|
import { getServerUrl } from "../../../config/api.js";
|
||||||
|
import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
|
||||||
|
|
||||||
const InventoryTab = () => {
|
const InventoryTab = () => {
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
@ -11,24 +13,12 @@ const InventoryTab = () => {
|
|||||||
const [equipment, setEquipment] = useState({});
|
const [equipment, setEquipment] = useState({});
|
||||||
const [selectedItem, setSelectedItem] = useState(null);
|
const [selectedItem, setSelectedItem] = useState(null);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
|
||||||
const CONNECT_URL = getServerUrl();
|
const CONNECT_URL = getServerUrl();
|
||||||
const ASSET_BASE_URL = `${CONNECT_URL}/static/`;
|
const ASSET_BASE_URL = `${CONNECT_URL}/static/`;
|
||||||
|
|
||||||
const equipmentSlots = {
|
const manifest = GameDataManager.manifest || {};
|
||||||
personal: [
|
const coreSystems = manifest.core_systems?.categories || {};
|
||||||
{ id: "personal_helmet", label: "HELMET" },
|
|
||||||
{ id: "personal_suit", label: "SUIT" },
|
|
||||||
{ id: "personal_gloves", label: "GLOVES" },
|
|
||||||
{ id: "personal_boots", label: "BOOTS" },
|
|
||||||
{ id: "personal_backpack", label: "BACKPACK" },
|
|
||||||
{ id: "personal_weapons", label: "WEAPON" },
|
|
||||||
],
|
|
||||||
ship: [
|
|
||||||
{ id: "ship_hull", label: "HULL" },
|
|
||||||
{ id: "ship_shields", label: "SHIELDS" },
|
|
||||||
{ id: "ship_engines", label: "ENGINES" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFullTextureUrl = (path) => {
|
const getFullTextureUrl = (path) => {
|
||||||
if (!path) return "/assets/no-image.png";
|
if (!path) return "/assets/no-image.png";
|
||||||
@ -36,11 +26,10 @@ const InventoryTab = () => {
|
|||||||
return `${ASSET_BASE_URL}${path}`;
|
return `${ASSET_BASE_URL}${path}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const enrichItemData = (serverItem) => {
|
const enrichItemData = (serverItem, currentSlot = null) => {
|
||||||
if (!serverItem || (!serverItem.itemId && !serverItem.id)) return null;
|
if (!serverItem || (!serverItem.itemId && !serverItem.id)) return null;
|
||||||
const id = serverItem.itemId || serverItem.id;
|
const id = serverItem.itemId || serverItem.id;
|
||||||
const staticData = GameDataManager.getItem(id);
|
const staticData = GameDataManager.getItem(id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...serverItem,
|
...serverItem,
|
||||||
...staticData,
|
...staticData,
|
||||||
@ -48,28 +37,29 @@ const InventoryTab = () => {
|
|||||||
textureUrl: getFullTextureUrl(staticData?.texture),
|
textureUrl: getFullTextureUrl(staticData?.texture),
|
||||||
canEquip: !!staticData?.meta?.equipmentSlot,
|
canEquip: !!staticData?.meta?.equipmentSlot,
|
||||||
rarity: staticData?.meta?.rarity || "common",
|
rarity: staticData?.meta?.rarity || "common",
|
||||||
|
currentSlot: currentSlot,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!socket) return;
|
if (!socket) return;
|
||||||
|
|
||||||
const refresh = () => {
|
const refreshData = () => {
|
||||||
socket.emit("player:get_inventory");
|
socket.emit("player:get_inventory");
|
||||||
socket.emit("player:get_equipment");
|
socket.emit("player:get_equipment");
|
||||||
};
|
};
|
||||||
|
|
||||||
refresh();
|
refreshData();
|
||||||
|
|
||||||
const handleInventory = (rawItems) => {
|
const handleInventory = (rawItems) => {
|
||||||
setItems(rawItems.map(enrichItemData).filter(Boolean));
|
setItems(rawItems.map((item) => enrichItemData(item)).filter(Boolean));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEquipment = (rawEquip) => {
|
const handleEquipment = (rawEquip) => {
|
||||||
const mapped = {};
|
const mapped = {};
|
||||||
Object.keys(rawEquip).forEach((slot) => {
|
Object.keys(rawEquip).forEach((slot) => {
|
||||||
if (rawEquip[slot]) {
|
if (rawEquip[slot]) {
|
||||||
mapped[slot] = enrichItemData({ itemId: rawEquip[slot] });
|
mapped[slot] = enrichItemData({ itemId: rawEquip[slot] }, slot);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setEquipment(mapped);
|
setEquipment(mapped);
|
||||||
@ -77,39 +67,70 @@ const InventoryTab = () => {
|
|||||||
|
|
||||||
socket.on("player:inventory_data", handleInventory);
|
socket.on("player:inventory_data", handleInventory);
|
||||||
socket.on("player:equipment_data", handleEquipment);
|
socket.on("player:equipment_data", handleEquipment);
|
||||||
|
socket.on("player:item_equipped", refreshData);
|
||||||
|
socket.on("player:item_unequipped", refreshData);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off("player:inventory_data", handleInventory);
|
socket.off("player:inventory_data", handleInventory);
|
||||||
socket.off("player:equipment_data", handleEquipment);
|
socket.off("player:equipment_data", handleEquipment);
|
||||||
|
socket.off("player:item_equipped", refreshData);
|
||||||
|
socket.off("player:item_unequipped", refreshData);
|
||||||
};
|
};
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
const handleItemClick = (item) => {
|
|
||||||
setSelectedItem(item);
|
|
||||||
setShowModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const equipItem = (item) => {
|
const equipItem = (item) => {
|
||||||
const slot = item.meta?.equipmentSlot;
|
const slot = item.meta?.equipmentSlot;
|
||||||
if (slot) {
|
if (slot) socket.emit("player:equip_item", { itemId: item.id, slot });
|
||||||
socket.emit("player:equip_item", { itemId: item.id, slot });
|
|
||||||
setShowModal(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const unequipItem = (slot) => {
|
const unequipItem = (slot) => {
|
||||||
socket.emit("player:unequip_item", { slot });
|
socket.emit("player:unequip_item", { slot });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const equipmentSlots = {
|
||||||
|
personal: Object.keys(coreSystems)
|
||||||
|
.filter(
|
||||||
|
(k) =>
|
||||||
|
k.startsWith("original:personal_") &&
|
||||||
|
!k.includes("accessory") &&
|
||||||
|
k !== "original:personal_weapons",
|
||||||
|
)
|
||||||
|
.map((k) => ({
|
||||||
|
id: k,
|
||||||
|
label: GameDataManager.t(coreSystems[k].displayName),
|
||||||
|
})),
|
||||||
|
weapons: Object.keys(coreSystems)
|
||||||
|
.filter((k) => k === "original:personal_weapons")
|
||||||
|
.map((k) => ({
|
||||||
|
id: k,
|
||||||
|
label: GameDataManager.t(coreSystems[k].displayName),
|
||||||
|
})),
|
||||||
|
accessories: Object.keys(coreSystems)
|
||||||
|
.filter((k) => k.includes("personal_accessory"))
|
||||||
|
.map((k) => ({
|
||||||
|
id: k,
|
||||||
|
label: GameDataManager.t(coreSystems[k].displayName),
|
||||||
|
})),
|
||||||
|
ship: Object.keys(coreSystems)
|
||||||
|
.filter((k) => k.startsWith("original:ship_"))
|
||||||
|
.map((k) => ({
|
||||||
|
id: k,
|
||||||
|
label: GameDataManager.t(coreSystems[k].displayName),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
const renderSlotGroup = (title, groupSlots) => (
|
const renderSlotGroup = (title, groupSlots) => (
|
||||||
<div className="equip-group">
|
<div className="equip-group">
|
||||||
<div className="group-label">{title}</div>
|
<div className="group-label">{title}</div>
|
||||||
<div className="equip-list-compact">
|
<div className="equip-list-compact grid-config">
|
||||||
{groupSlots.map((slot) => (
|
{groupSlots.map((slot) => (
|
||||||
<div
|
<div
|
||||||
key={slot.id}
|
key={slot.id}
|
||||||
className={`equip-row-mini ${equipment[slot.id] ? "occupied" : ""}`}
|
className={`equip-row-mini ${equipment[slot.id] ? "occupied" : ""}`}
|
||||||
onClick={() => equipment[slot.id] && unequipItem(slot.id)}
|
onClick={() =>
|
||||||
|
equipment[slot.id] &&
|
||||||
|
(setSelectedItem(equipment[slot.id]), setShowModal(true))
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="slot-name-tiny">{slot.label}</span>
|
<span className="slot-name-tiny">{slot.label}</span>
|
||||||
<div
|
<div
|
||||||
@ -118,7 +139,6 @@ const InventoryTab = () => {
|
|||||||
{equipment[slot.id] ? (
|
{equipment[slot.id] ? (
|
||||||
<img
|
<img
|
||||||
src={equipment[slot.id].textureUrl}
|
src={equipment[slot.id].textureUrl}
|
||||||
alt={slot.id}
|
|
||||||
className="item-img-mini"
|
className="item-img-mini"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -133,88 +153,77 @@ const InventoryTab = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inv-adaptive-container">
|
<div className="inv-adaptive-container">
|
||||||
<div className="inv-header-compact">
|
|
||||||
<h2 className="inv-logo">TACTICAL_INVENTORY_V2</h2>
|
|
||||||
<div className="inv-stats-bar">
|
|
||||||
<span>
|
|
||||||
LOAD: <span className="text-cyan">{items.length}/50</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="inv-layout-wrapper">
|
<div className="inv-layout-wrapper">
|
||||||
<section className="inv-panel loadout">
|
<section className="inv-panel loadout">
|
||||||
<div className="panel-label">CORE_SYSTEMS</div>
|
<MeteorRegion>
|
||||||
|
{renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)}
|
||||||
{renderSlotGroup("PERSON", equipmentSlots.personal)}
|
{renderSlotGroup("WEAPONRY", equipmentSlots.weapons)}
|
||||||
{renderSlotGroup("SHIP", equipmentSlots.ship)}
|
{renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)}
|
||||||
|
<div className="separator" />
|
||||||
|
{renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)}
|
||||||
|
</MeteorRegion>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="inv-panel cargo">
|
<section className="inv-panel cargo">
|
||||||
<div className="panel-label">STORAGE_UNITS</div>
|
<MeteorRegion>
|
||||||
<div className="cargo-grid-v2">
|
<div className="cargo-grid-v2">
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => {
|
||||||
<div
|
const isEquipped = Object.values(equipment).some(
|
||||||
key={`${item.id}-${idx}`}
|
(e) => e?.id === item.id,
|
||||||
className={`item-slot ${item.rarity} ${selectedItem?.id === item.id ? "active" : ""}`}
|
);
|
||||||
onClick={() => handleItemClick(item)}
|
return (
|
||||||
>
|
<div
|
||||||
<img
|
key={`${item.id}-${idx}`}
|
||||||
src={item.textureUrl}
|
className={`item-slot ${item.rarity} ${isEquipped ? "equipped-in-storage" : ""}`}
|
||||||
alt="item"
|
onClick={() => {
|
||||||
width={62}
|
setSelectedItem(item);
|
||||||
className="item-img-grid"
|
setShowModal(true);
|
||||||
/>
|
}}
|
||||||
{item.quantity > 1 && (
|
>
|
||||||
<span className="qty-label">{item.quantity}</span>
|
<img
|
||||||
)}
|
src={item.textureUrl}
|
||||||
</div>
|
width={62}
|
||||||
))}
|
className="item-img-grid"
|
||||||
</div>
|
/>
|
||||||
|
{isEquipped && <div className="equipped-tag">E</div>}
|
||||||
|
{item.quantity > 1 && (
|
||||||
|
<span className="qty-label">{item.quantity}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</MeteorRegion>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showModal && selectedItem && (
|
{showModal &&
|
||||||
<div className="inv-modal-overlay" onClick={() => setShowModal(false)}>
|
selectedItem &&
|
||||||
<div className="inv-modal-box" onClick={(e) => e.stopPropagation()}>
|
ReactDOM.createPortal(
|
||||||
<div className={`modal-header-scan ${selectedItem.rarity}`}>
|
<ItemModal
|
||||||
<div className="modal-preview-icon">
|
item={selectedItem}
|
||||||
<img src={selectedItem.textureUrl} alt="preview" width={32} />
|
isEquipped={
|
||||||
</div>
|
!!selectedItem.currentSlot ||
|
||||||
<h3>{selectedItem.displayName || selectedItem.id}</h3>
|
Object.values(equipment).some((e) => e?.id === selectedItem.id)
|
||||||
</div>
|
}
|
||||||
|
onClose={() => {
|
||||||
<div className="modal-body-scan">
|
setShowModal(false);
|
||||||
<p className="description-text">
|
setSelectedItem(null);
|
||||||
{selectedItem.description || "No data available."}
|
}}
|
||||||
</p>
|
onEquip={equipItem}
|
||||||
|
onUnequip={(slot) => {
|
||||||
<div className="stats-scanner">
|
const actualSlot =
|
||||||
{selectedItem.stats &&
|
slot ||
|
||||||
Object.entries(selectedItem.stats).map(([k, v]) => (
|
Object.keys(equipment).find(
|
||||||
<div key={k} className="stat-line">
|
(k) => equipment[k].id === selectedItem.id,
|
||||||
<span className="label">
|
);
|
||||||
{GameDataManager.getStatName?.(k)?.toUpperCase() ||
|
unequipItem(actualSlot);
|
||||||
k.toUpperCase()}
|
}}
|
||||||
</span>
|
formatStatName={(n) => GameDataManager.getStatName(n).toUpperCase()}
|
||||||
<span className="value">+{v}</span>
|
getStatIcon={(n) => GameDataManager.getStatIcon?.(n)}
|
||||||
</div>
|
/>,
|
||||||
))}
|
document.body,
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{selectedItem.canEquip && (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
className="modal-equip-btn"
|
|
||||||
onClick={() => equipItem(selectedItem)}
|
|
||||||
>
|
|
||||||
INITIALIZE_EQUIP
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
|
|||||||
import GameDataManager from "../../../services/GameDataManager.js";
|
import GameDataManager from "../../../services/GameDataManager.js";
|
||||||
import "./styles/ItemListTab.css";
|
import "./styles/ItemListTab.css";
|
||||||
import { config } from "../../../config/api.js";
|
import { config } from "../../../config/api.js";
|
||||||
|
import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
|
||||||
|
|
||||||
const ItemListTab = () => {
|
const ItemListTab = () => {
|
||||||
const [allItems, setAllItems] = useState([]);
|
const [allItems, setAllItems] = useState([]);
|
||||||
@ -83,7 +84,7 @@ const ItemListTab = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="inspector-grid">
|
<MeteorRegion className="inspector-grid">
|
||||||
<div className="inspector-section">
|
<div className="inspector-section">
|
||||||
<div className="section-title">DATA_DESCRIPTION</div>
|
<div className="section-title">DATA_DESCRIPTION</div>
|
||||||
<p>{selectedItem.description}</p>
|
<p>{selectedItem.description}</p>
|
||||||
@ -103,7 +104,7 @@ const ItemListTab = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</MeteorRegion>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -131,7 +132,8 @@ const ItemListTab = () => {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="items-list-scroll">
|
|
||||||
|
<MeteorRegion className="items-list-scroll">
|
||||||
{filteredItems.map((item) => (
|
{filteredItems.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@ -147,7 +149,7 @@ const ItemListTab = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</MeteorRegion>
|
||||||
</div>
|
</div>
|
||||||
{(!isMobile || (isMobile && selectedItem)) && renderInspector()}
|
{(!isMobile || (isMobile && selectedItem)) && renderInspector()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,36 +1,28 @@
|
|||||||
/* Ховаємо праву панель на мобілках */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.inventory-container {
|
|
||||||
grid-template-columns: 1fr; /* Тільки одна колонка (сітка) */
|
|
||||||
}
|
|
||||||
.item-details {
|
|
||||||
display: none; /* Панель справа зникає */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стилі модального вікна */
|
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 100%;
|
width: 100vw;
|
||||||
|
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: 1000;
|
z-index: 9999;
|
||||||
padding: 20px;
|
backdrop-filter: blur(4px);
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: var(--card-bg);
|
background: #12151a;
|
||||||
border: 1px solid var(--primary-color);
|
border: 1px solid #00d2ff;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 40px;
|
max-width: 400px;
|
||||||
|
padding: 25px;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 0 30px rgba(0, 210, 255, 0.2);
|
box-shadow: 0 0 30px rgba(0, 210, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close {
|
.modal-close {
|
||||||
@ -39,7 +31,114 @@
|
|||||||
right: 15px;
|
right: 15px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #888;
|
||||||
font-size: 24px;
|
font-size: 28px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rarity-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name.common {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.item-name.uncommon {
|
||||||
|
color: #1eff00;
|
||||||
|
}
|
||||||
|
.item-name.rare {
|
||||||
|
color: #0070dd;
|
||||||
|
}
|
||||||
|
.item-name.epic {
|
||||||
|
color: #a335ee;
|
||||||
|
}
|
||||||
|
.item-name.legendary {
|
||||||
|
color: #ff8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #aaa;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-stats-container {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 5px 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #00d2ff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-equip {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #00d2ff;
|
||||||
|
color: #00d2ff;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-equip:hover {
|
||||||
|
background: #00d2ff;
|
||||||
|
color: #000;
|
||||||
|
box-shadow: 0 0 15px rgba(0, 210, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-equip.unequip {
|
||||||
|
border-color: #ff4444;
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-equip.unequip:hover {
|
||||||
|
background: #ff4444;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,109 +1,145 @@
|
|||||||
.crafting-container {
|
.crafting-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crafting-header {
|
.crafting-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crafting-header h2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crafting-grid {
|
.crafting-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-card {
|
.recipe-card {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
padding: 20px;
|
padding: 15px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-card:hover {
|
.recipe-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-icon {
|
.recipe-icon img {
|
||||||
font-size: 1.5rem;
|
max-width: 50px;
|
||||||
color: var(--primary-color);
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-name {
|
.recipe-name {
|
||||||
font-size: 0.9rem;
|
font-size: 0.85rem;
|
||||||
color: #fff;
|
line-height: 1.2;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Категорії */
|
|
||||||
.crafting-categories {
|
.crafting-categories {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 15px;
|
||||||
padding: 10px 5px;
|
padding: 5px 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crafting-categories::-webkit-scrollbar {
|
.crafting-categories::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crafting-cat-btn {
|
.crafting-cat-btn {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
padding: 10px 22px;
|
padding: 8px 16px;
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-family: 'Orbitron', sans-serif;
|
font-family: "Orbitron", sans-serif;
|
||||||
font-size: 0.85rem;
|
font-size: 0.75rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 0.5px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.25s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
|
||||||
|
|
||||||
.crafting-cat-btn:hover {
|
|
||||||
background: rgba(var(--primary-rgb), 0.08);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.crafting-cat-btn.active {
|
.crafting-cat-btn.active {
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
color: #000;
|
color: #000;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
box-shadow: 0 0 15px rgba(var(--primary-rgb), 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 600px) {
|
||||||
.crafting-categories {
|
.crafting-container {
|
||||||
gap: 8px;
|
padding: 12px;
|
||||||
margin-bottom: 15px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.crafting-cat-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crafting-grid {
|
.crafting-header {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
margin-bottom: 12px;
|
||||||
gap: 10px;
|
}
|
||||||
}
|
|
||||||
|
.crafting-header h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crafting-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-card {
|
||||||
|
padding: 10px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-icon img {
|
||||||
|
max-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crafting-cat-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meteor-region-content {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
/* Базовий контейнер з обмеженням висоти */
|
|
||||||
.inv-adaptive-container {
|
.inv-adaptive-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -10,7 +9,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.inv-header-compact {
|
.inv-header-compact {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -18,6 +16,7 @@
|
|||||||
border-bottom: 1px solid #1a2638;
|
border-bottom: 1px solid #1a2638;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inv-logo {
|
.inv-logo {
|
||||||
@ -26,16 +25,17 @@
|
|||||||
letter-spacing: 3px;
|
letter-spacing: 3px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inv-stats-bar {
|
.inv-stats-bar {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #4a5d75;
|
color: #4a5d75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-cyan {
|
.text-cyan {
|
||||||
color: #00d4ff;
|
color: #00d4ff;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Layout Wrapper */
|
|
||||||
.inv-layout-wrapper {
|
.inv-layout-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -44,13 +44,13 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Спільні стилі панелей */
|
|
||||||
.inv-panel {
|
.inv-panel {
|
||||||
background: rgba(10, 15, 24, 0.85);
|
background: rgba(10, 15, 24, 0.85);
|
||||||
border: 1px solid #1a2638;
|
border: 1px solid #1a2638;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.5);
|
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.5);
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-label {
|
.panel-label {
|
||||||
@ -61,26 +61,55 @@
|
|||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
border-bottom: 1px solid #1a2638;
|
border-bottom: 1px solid #1a2638;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loadout (L) */
|
|
||||||
.loadout {
|
.loadout {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loadout::-webkit-scrollbar,
|
||||||
|
.cargo-grid-v2::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadout::-webkit-scrollbar-thumb,
|
||||||
|
.cargo-grid-v2::-webkit-scrollbar-thumb {
|
||||||
|
background: #1a2638;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadout::-webkit-scrollbar-thumb:hover,
|
||||||
|
.cargo-grid-v2::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
.equip-list-compact {
|
.equip-list-compact {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.equip-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #4a5d75;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding-left: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
.equip-row-mini {
|
.equip-row-mini {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 12px;
|
padding: 6px 10px;
|
||||||
background: rgba(255, 255, 255, 0.02);
|
background: rgba(255, 255, 255, 0.02);
|
||||||
border: 1px solid rgba(26, 38, 56, 0.8);
|
border: 1px solid rgba(26, 38, 56, 0.8);
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
@ -91,37 +120,46 @@
|
|||||||
border-color: #00d4ff;
|
border-color: #00d4ff;
|
||||||
background: rgba(0, 212, 255, 0.05);
|
background: rgba(0, 212, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.slot-name-tiny {
|
.slot-name-tiny {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: #4a5d75;
|
color: #4a5d75;
|
||||||
|
max-width: 140px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.equip-box-mini {
|
.equip-box-mini {
|
||||||
width: 36px;
|
width: 32px;
|
||||||
height: 36px;
|
height: 32px;
|
||||||
background: #000;
|
background: #000;
|
||||||
border: 1px solid #1a2638;
|
border: 1px solid #1a2638;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #00d4ff;
|
color: #00d4ff;
|
||||||
font-size: 1.1rem;
|
font-size: 0.9rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cargo (R) */
|
|
||||||
.cargo {
|
.cargo {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cargo-grid-v2 {
|
.cargo-grid-v2 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(65px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||||
grid-auto-rows: 65px;
|
gap: 8px;
|
||||||
gap: 10px;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-slot {
|
.item-slot {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
background: rgba(5, 8, 12, 0.9);
|
background: rgba(5, 8, 12, 0.9);
|
||||||
border: 1px solid #1a2638;
|
border: 1px solid #1a2638;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -130,183 +168,68 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-img-grid,
|
||||||
|
.item-img-mini {
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-slot:hover {
|
.item-slot:hover {
|
||||||
border-color: #00d4ff;
|
border-color: #00d4ff;
|
||||||
background: rgba(0, 212, 255, 0.1);
|
background: rgba(0, 212, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-slot.active {
|
.item-slot.active {
|
||||||
border-color: #00d4ff;
|
border-color: #00d4ff;
|
||||||
box-shadow: inset 0 0 10px rgba(0, 212, 255, 0.3);
|
box-shadow: inset 0 0 10px rgba(0, 212, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rarity Colors */
|
.separator {
|
||||||
.item-slot.rare,
|
height: 1px;
|
||||||
.equip-box-mini.rare {
|
background: #1a2638;
|
||||||
border-bottom: 2px solid #00d4ff;
|
margin: 10px 5px;
|
||||||
}
|
|
||||||
.item-slot.epic,
|
|
||||||
.equip-box-mini.epic {
|
|
||||||
border-bottom: 2px solid #a335ee;
|
|
||||||
}
|
|
||||||
.item-slot.legendary,
|
|
||||||
.equip-box-mini.legendary {
|
|
||||||
border-bottom: 2px solid #ffaa00;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.qty-label {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 2px;
|
|
||||||
right: 5px;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #00d4ff;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
.item-slot.empty {
|
|
||||||
opacity: 0.15;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Styles */
|
|
||||||
.inv-modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.9);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 2000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-modal-box {
|
|
||||||
background: #0a0f18;
|
|
||||||
border: 1px solid #1a2638;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 380px;
|
|
||||||
box-shadow: 0 0 40px rgba(0, 0, 0, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header-scan {
|
|
||||||
padding: 20px;
|
|
||||||
border-bottom: 1px solid #1a2638;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header-scan.rare {
|
|
||||||
border-left: 5px solid #00d4ff;
|
|
||||||
}
|
|
||||||
.modal-header-scan.legendary {
|
|
||||||
border-left: 5px solid #ffaa00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rarity-text {
|
|
||||||
font-size: 10px;
|
|
||||||
color: #4a5d75;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.modal-header-scan h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-x {
|
|
||||||
position: absolute;
|
|
||||||
top: 15px;
|
|
||||||
right: 15px;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #4a5d75;
|
|
||||||
font-size: 24px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body-scan {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.description-text {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #8a9cb3;
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-scanner {
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
padding: 12px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.stat-line {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 6px 0;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
.stat-line .value {
|
|
||||||
color: #00ff88;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-equip-btn {
|
|
||||||
width: 100%;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптивність під мобільні пристрої */
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.inv-adaptive-container {
|
||||||
|
height: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.inv-layout-wrapper {
|
.inv-layout-wrapper {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadout {
|
.loadout {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 0 0 auto;
|
max-height: 300px;
|
||||||
}
|
|
||||||
|
|
||||||
.equip-list-compact {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cargo {
|
.cargo {
|
||||||
min-height: 450px;
|
height: 400px;
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-adaptive-container {
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cargo-grid-v2 {
|
.item-slot.equipped-in-storage {
|
||||||
display: grid;
|
border: 2px solid #00d2ff;
|
||||||
grid-template-columns: repeat(
|
box-shadow: inset 0 0 10px rgba(0, 210, 255, 0.3);
|
||||||
auto-fill,
|
opacity: 0.8;
|
||||||
minmax(60px, 1fr)
|
|
||||||
); /* Кожен слот мінімум 60px */
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-slot {
|
.equipped-tag {
|
||||||
width: 60px; /* Жорсткий розмір слота */
|
position: absolute;
|
||||||
height: 60px; /* Жорсткий розмір слота */
|
top: -5px;
|
||||||
position: relative;
|
right: -5px;
|
||||||
display: flex;
|
background: #00d2ff;
|
||||||
align-items: center;
|
color: #000;
|
||||||
justify-content: center;
|
font-size: 10px;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
font-weight: bold;
|
||||||
border: 1px solid #333;
|
padding: 2px 5px;
|
||||||
overflow: hidden; /* Обрізаємо все, що виходить за межі 60x60 */
|
border-radius: 3px;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,25 +11,7 @@ class InventoryManager {
|
|||||||
|
|
||||||
async getEquipment(playerId) {
|
async getEquipment(playerId) {
|
||||||
const player = await Player.findByPk(playerId);
|
const player = await Player.findByPk(playerId);
|
||||||
if (!player) return {};
|
return player ? player.equipment : {};
|
||||||
|
|
||||||
const slots = [
|
|
||||||
"personal_helmet",
|
|
||||||
"personal_suit",
|
|
||||||
"personal_gloves",
|
|
||||||
"personal_backpack",
|
|
||||||
"personal_boots",
|
|
||||||
"personal_weapons",
|
|
||||||
"ship_hull",
|
|
||||||
"ship_shields",
|
|
||||||
"ship_engines",
|
|
||||||
];
|
|
||||||
|
|
||||||
const equipment = {};
|
|
||||||
slots.forEach((slot) => {
|
|
||||||
equipment[slot] = player[`equipped_${slot}`] || null;
|
|
||||||
});
|
|
||||||
return equipment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async equipItem(playerId, itemId, slot) {
|
async equipItem(playerId, itemId, slot) {
|
||||||
@ -44,14 +26,31 @@ class InventoryManager {
|
|||||||
throw new Error("INVALID_SLOT_FOR_ITEM");
|
throw new Error("INVALID_SLOT_FOR_ITEM");
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbField = `equipped_${slot}`.replace("original:", "");
|
const player = await Player.findByPk(playerId);
|
||||||
await Player.update({ [dbField]: itemId }, { where: { id: playerId } });
|
if (!player) throw new Error("PLAYER_NOT_FOUND");
|
||||||
|
|
||||||
|
const currentEquip = player.equipment;
|
||||||
|
currentEquip[slot] = itemId;
|
||||||
|
|
||||||
|
player.equipment = currentEquip;
|
||||||
|
player.changed("equipment", true);
|
||||||
|
await player.save();
|
||||||
|
|
||||||
return itemInfo;
|
return itemInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
async unequipItem(playerId, slot) {
|
async unequipItem(playerId, slot) {
|
||||||
const dbField = `equipped_${slot}`;
|
const player = await Player.findByPk(playerId);
|
||||||
await Player.update({ [dbField]: null }, { where: { id: playerId } });
|
if (!player) throw new Error("PLAYER_NOT_FOUND");
|
||||||
|
|
||||||
|
const currentEquip = player.equipment;
|
||||||
|
if (currentEquip[slot]) {
|
||||||
|
delete currentEquip[slot];
|
||||||
|
player.equipment = currentEquip;
|
||||||
|
player.changed("equipment", true);
|
||||||
|
await player.save();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
class SessionManager {
|
class SessionManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.sessions = new Map(); // socket.id => session data
|
this.sessions = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
addPlayer(socketId, playerRaw) {
|
addPlayer(socketId, playerRaw) {
|
||||||
@ -15,29 +15,18 @@ class SessionManager {
|
|||||||
scene: "world",
|
scene: "world",
|
||||||
sceneData: null,
|
sceneData: null,
|
||||||
joinedAt: Date.now(),
|
joinedAt: Date.now(),
|
||||||
equipment: {
|
equipment: playerRaw.equipment || {},
|
||||||
personal_helmet: playerRaw.equipped_personal_helmet,
|
|
||||||
personal_suit: playerRaw.equipped_personal_suit,
|
|
||||||
personal_gloves: playerRaw.equipped_personal_gloves,
|
|
||||||
personal_backpack: playerRaw.equipped_personal_backpack,
|
|
||||||
personal_boots: playerRaw.equipped_personal_boots,
|
|
||||||
personal_weapons: playerRaw.equipped_personal_weapons,
|
|
||||||
|
|
||||||
ship_hull: playerRaw.equipped_ship_hull,
|
|
||||||
ship_shields: playerRaw.equipped_ship_shields,
|
|
||||||
ship_engines: playerRaw.equipped_ship_engines,
|
|
||||||
ship_weapon_1: playerRaw.equipped_ship_weapon_1,
|
|
||||||
ship_weapon_2: playerRaw.equipped_ship_weapon_2,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEquipment(socketId, slot, itemId) {
|
updateEquipment(socketId, slot, itemId) {
|
||||||
const session = this.sessions.get(socketId);
|
const session = this.sessions.get(socketId);
|
||||||
if (session && session.equipment.hasOwnProperty(slot)) {
|
if (session) {
|
||||||
session.equipment[slot] = itemId;
|
if (itemId === null) {
|
||||||
} else if (session) {
|
delete session.equipment[slot];
|
||||||
session.equipment[slot] = itemId;
|
} else {
|
||||||
|
session.equipment[slot] = itemId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +36,6 @@ class SessionManager {
|
|||||||
session.scene = sceneName;
|
session.scene = sceneName;
|
||||||
session.sceneData = data;
|
session.sceneData = data;
|
||||||
session.status = sceneName === "dungeon" ? "in_mission" : "active";
|
session.status = sceneName === "dungeon" ? "in_mission" : "active";
|
||||||
console.log(`[Session] Player ${session.id} switched to ${sceneName}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,22 +42,17 @@ const Player = sequelize.define("Player", {
|
|||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
defaultValue: DataTypes.NOW,
|
defaultValue: DataTypes.NOW,
|
||||||
},
|
},
|
||||||
|
equipment: {
|
||||||
equipped_personal_helmet: { type: DataTypes.STRING },
|
type: DataTypes.TEXT,
|
||||||
equipped_personal_suit: { type: DataTypes.STRING },
|
defaultValue: "{}",
|
||||||
equipped_personal_gloves: { type: DataTypes.STRING },
|
get() {
|
||||||
equipped_personal_backpack: { type: DataTypes.STRING },
|
const val = this.getDataValue("equipment");
|
||||||
equipped_personal_boots: { type: DataTypes.STRING },
|
return val ? JSON.parse(val) : {};
|
||||||
equipped_personal_weapons: { type: DataTypes.STRING },
|
},
|
||||||
|
set(val) {
|
||||||
equipped_personal_accessory_1: { type: DataTypes.STRING },
|
this.setDataValue("equipment", JSON.stringify(val));
|
||||||
equipped_personal_accessory_2: { type: DataTypes.STRING },
|
},
|
||||||
|
},
|
||||||
equipped_ship_hull: { type: DataTypes.STRING },
|
|
||||||
equipped_ship_shields: { type: DataTypes.STRING },
|
|
||||||
equipped_ship_engines: { type: DataTypes.STRING },
|
|
||||||
equipped_ship_weapon_1: { type: DataTypes.STRING },
|
|
||||||
equipped_ship_weapon_2: { type: DataTypes.STRING },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = Player;
|
module.exports = Player;
|
||||||
|
|||||||
@ -6,31 +6,20 @@ module.exports = (io, socket) => {
|
|||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
socket.on("player:get_inventory", async () => {
|
socket.on("player:get_inventory", async () => {
|
||||||
try {
|
const items = await inventoryManager.getInventory(userId);
|
||||||
const items = await inventoryManager.getInventory(userId);
|
socket.emit("player:inventory_data", items);
|
||||||
socket.emit("player:inventory_data", items);
|
|
||||||
} catch (err) {
|
|
||||||
socket.emit("error", { message: "LOAD_INVENTORY_FAILED" });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("player:get_equipment", async () => {
|
socket.on("player:get_equipment", async () => {
|
||||||
try {
|
const equipment = await inventoryManager.getEquipment(userId);
|
||||||
const equipment = await inventoryManager.getEquipment(userId);
|
socket.emit("player:equipment_data", equipment);
|
||||||
socket.emit("player:equipment_data", equipment);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("player:equip_item", async ({ itemId, slot }) => {
|
socket.on("player:equip_item", async ({ itemId, slot }) => {
|
||||||
try {
|
try {
|
||||||
const itemInfo = await inventoryManager.equipItem(userId, itemId, slot);
|
const itemInfo = await inventoryManager.equipItem(userId, itemId, slot);
|
||||||
|
|
||||||
sessionManager.updateEquipment(socket.id, slot, itemId);
|
sessionManager.updateEquipment(socket.id, slot, itemId);
|
||||||
|
|
||||||
socket.emit("player:item_equipped", { slot, itemId });
|
socket.emit("player:item_equipped", { slot, itemId });
|
||||||
|
|
||||||
socket.broadcast.emit("player:visible_changed", {
|
socket.broadcast.emit("player:visible_changed", {
|
||||||
playerId: userId,
|
playerId: userId,
|
||||||
slot,
|
slot,
|
||||||
@ -45,7 +34,6 @@ module.exports = (io, socket) => {
|
|||||||
try {
|
try {
|
||||||
await inventoryManager.unequipItem(userId, slot);
|
await inventoryManager.unequipItem(userId, slot);
|
||||||
sessionManager.updateEquipment(socket.id, slot, null);
|
sessionManager.updateEquipment(socket.id, slot, null);
|
||||||
|
|
||||||
socket.emit("player:item_unequipped", { slot });
|
socket.emit("player:item_unequipped", { slot });
|
||||||
socket.broadcast.emit("player:visible_changed", {
|
socket.broadcast.emit("player:visible_changed", {
|
||||||
playerId: userId,
|
playerId: userId,
|
||||||
@ -53,7 +41,7 @@ module.exports = (io, socket) => {
|
|||||||
texturePath: null,
|
texturePath: null,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
socket.emit("error", { message: "UNEQUIP_FAILED" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user