diff --git a/client/src/components/Meteor/MeteorRegion.css b/client/src/components/Meteor/MeteorRegion.css
new file mode 100644
index 0000000..9b2cbe1
--- /dev/null
+++ b/client/src/components/Meteor/MeteorRegion.css
@@ -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;
+}
diff --git a/client/src/components/Meteor/MeteorRegion.jsx b/client/src/components/Meteor/MeteorRegion.jsx
new file mode 100644
index 0000000..3e280d0
--- /dev/null
+++ b/client/src/components/Meteor/MeteorRegion.jsx
@@ -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 (
+
+
+ {children}
+
+
+ {isVisible && (
+
+ )}
+
+ );
+};
+
+export default MeteorRegion;
diff --git a/client/src/styles/App.css b/client/src/styles/App.css
index 8733cf6..3c2425f 100644
--- a/client/src/styles/App.css
+++ b/client/src/styles/App.css
@@ -312,3 +312,13 @@
grid-template-columns: 1fr 1fr;
}
}
+
+*::-webkit-scrollbar {
+ width: 0px;
+ background: transparent;
+}
+
+* {
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
diff --git a/client/src/styles/index.css b/client/src/styles/index.css
index 7d250fe..4931b60 100644
--- a/client/src/styles/index.css
+++ b/client/src/styles/index.css
@@ -40,3 +40,13 @@ body {
transparent 50%
);
}
+
+*::-webkit-scrollbar {
+ width: 0px;
+ background: transparent;
+}
+
+* {
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
diff --git a/client/src/styles/main.css b/client/src/styles/main.css
index fa21fef..718ec87 100644
--- a/client/src/styles/main.css
+++ b/client/src/styles/main.css
@@ -1,137 +1,161 @@
/* Galaxy Strike Online - Main Styles */
* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
}
:root {
- --primary-color: #00d4ff;
- --secondary-color: #ff6b35;
- --accent-color: #ff00ff;
- --bg-primary: #0a0e1a;
- --bg-secondary: #151923;
- --bg-tertiary: #1e2433;
- --text-primary: #ffffff;
- --text-secondary: #b8c5d6;
- --text-muted: #6b7c93;
- --border-color: #2a3241;
- --success-color: #00ff88;
- --warning-color: #ffaa00;
- --error-color: #ff3366;
- --card-bg: rgba(30, 36, 51, 0.8);
- --hover-bg: rgba(0, 212, 255, 0.1);
- --gradient-primary: linear-gradient(135deg, #00d4ff, #0099cc);
- --gradient-secondary: linear-gradient(135deg, #ff6b35, #ff4500);
+ --primary-color: #00d4ff;
+ --secondary-color: #ff6b35;
+ --accent-color: #ff00ff;
+ --bg-primary: #0a0e1a;
+ --bg-secondary: #151923;
+ --bg-tertiary: #1e2433;
+ --text-primary: #ffffff;
+ --text-secondary: #b8c5d6;
+ --text-muted: #6b7c93;
+ --border-color: #2a3241;
+ --success-color: #00ff88;
+ --warning-color: #ffaa00;
+ --error-color: #ff3366;
+ --card-bg: rgba(30, 36, 51, 0.8);
+ --hover-bg: rgba(0, 212, 255, 0.1);
+ --gradient-primary: linear-gradient(135deg, #00d4ff, #0099cc);
+ --gradient-secondary: linear-gradient(135deg, #ff6b35, #ff4500);
}
body {
- font-family: 'Space Mono', monospace;
- background: var(--bg-primary);
- color: var(--text-primary);
- overflow: hidden;
- background-image:
- radial-gradient(circle at 20% 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%);
+ font-family: "Space Mono", monospace;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ overflow: hidden;
+ background-image:
+ radial-gradient(
+ circle at 20% 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) {
- .server-controls {
- flex-direction: column;
- align-items: stretch;
- }
-
- .server-filters {
- justify-content: center;
- }
-
- .server-confirmation {
- flex-direction: column;
- gap: 20px;
- }
-
- .confirm-actions-left, .confirm-actions-right {
- width: 100%;
- max-width: 300px;
- }
-
- .server-details {
- flex-direction: column;
- gap: 8px;
- }
+ .server-controls {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .server-filters {
+ justify-content: center;
+ }
+
+ .server-confirmation {
+ flex-direction: column;
+ gap: 20px;
+ }
+
+ .confirm-actions-left,
+ .confirm-actions-right {
+ width: 100%;
+ max-width: 300px;
+ }
+
+ .server-details {
+ flex-direction: column;
+ gap: 8px;
+ }
}
/* Animations */
@keyframes fadeInUp {
- from {
- opacity: 0;
- transform: translateY(30px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
+ from {
+ opacity: 0;
+ transform: translateY(30px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
@keyframes pulse {
- 0%, 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.5;
- }
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
}
@media (max-width: 768px) {
- .dashboard-grid {
- grid-template-columns: 1fr;
- }
-
- .dungeons-container {
- grid-template-columns: 1fr;
- }
-
- .base-container {
- grid-template-columns: 1fr;
- }
-
- .inventory-container {
- grid-template-columns: 1fr;
- }
-
- .main-nav {
- overflow-x: scroll;
- }
-
- .resources {
- flex-direction: column;
- gap: 0.5rem;
- }
-
- .resource {
- padding: 0.25rem 0.5rem;
- font-size: 0.8rem;
- }
-
- .game-title {
- font-size: 2rem;
- }
+ .dashboard-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .dungeons-container {
+ grid-template-columns: 1fr;
+ }
+
+ .base-container {
+ grid-template-columns: 1fr;
+ }
+
+ .inventory-container {
+ grid-template-columns: 1fr;
+ }
+
+ .main-nav {
+ overflow-x: scroll;
+ }
+
+ .resources {
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+
+ .resource {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.8rem;
+ }
+
+ .game-title {
+ font-size: 2rem;
+ }
}
@media (max-width: 480px) {
- .header-center {
- display: none;
- }
-
- .nav-btn span {
- display: none;
- }
-
- .nav-btn {
- padding: 0.5rem;
- width: 100px;
- justify-content: center;
- }
+ .header-center {
+ display: none;
+ }
+
+ .nav-btn span {
+ display: none;
+ }
+
+ .nav-btn {
+ padding: 0.5rem;
+ width: 100px;
+ justify-content: center;
+ }
+}
+
+*::-webkit-scrollbar {
+ width: 0px;
+ background: transparent;
+}
+
+* {
+ scrollbar-width: none;
+ -ms-overflow-style: none;
}
diff --git a/client/src/views/GameInterface/tabs/CraftingTab.jsx b/client/src/views/GameInterface/tabs/CraftingTab.jsx
index a4f985a..23544f0 100644
--- a/client/src/views/GameInterface/tabs/CraftingTab.jsx
+++ b/client/src/views/GameInterface/tabs/CraftingTab.jsx
@@ -5,6 +5,7 @@ import "./styles/CraftingTab.css";
import CategorySelector from "../components/CategorySelector";
import CraftModal from "./components/CraftModal";
import { config } from "../../../config/api";
+import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
const CraftingTab = () => {
const { socket } = useSocket();
@@ -109,8 +110,11 @@ const CraftingTab = () => {
};
return (
-
-
+
+
{activeCraft && (
@@ -184,7 +188,7 @@ const CraftingTab = () => {
);
})}
-
+
{
const { socket } = useSocket();
@@ -11,24 +13,12 @@ const InventoryTab = () => {
const [equipment, setEquipment] = useState({});
const [selectedItem, setSelectedItem] = useState(null);
const [showModal, setShowModal] = useState(false);
+
const CONNECT_URL = getServerUrl();
const ASSET_BASE_URL = `${CONNECT_URL}/static/`;
- const equipmentSlots = {
- personal: [
- { 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 manifest = GameDataManager.manifest || {};
+ const coreSystems = manifest.core_systems?.categories || {};
const getFullTextureUrl = (path) => {
if (!path) return "/assets/no-image.png";
@@ -36,11 +26,10 @@ const InventoryTab = () => {
return `${ASSET_BASE_URL}${path}`;
};
- const enrichItemData = (serverItem) => {
+ const enrichItemData = (serverItem, currentSlot = null) => {
if (!serverItem || (!serverItem.itemId && !serverItem.id)) return null;
const id = serverItem.itemId || serverItem.id;
const staticData = GameDataManager.getItem(id);
-
return {
...serverItem,
...staticData,
@@ -48,28 +37,29 @@ const InventoryTab = () => {
textureUrl: getFullTextureUrl(staticData?.texture),
canEquip: !!staticData?.meta?.equipmentSlot,
rarity: staticData?.meta?.rarity || "common",
+ currentSlot: currentSlot,
};
};
useEffect(() => {
if (!socket) return;
- const refresh = () => {
+ const refreshData = () => {
socket.emit("player:get_inventory");
socket.emit("player:get_equipment");
};
- refresh();
+ refreshData();
const handleInventory = (rawItems) => {
- setItems(rawItems.map(enrichItemData).filter(Boolean));
+ setItems(rawItems.map((item) => enrichItemData(item)).filter(Boolean));
};
const handleEquipment = (rawEquip) => {
const mapped = {};
Object.keys(rawEquip).forEach((slot) => {
if (rawEquip[slot]) {
- mapped[slot] = enrichItemData({ itemId: rawEquip[slot] });
+ mapped[slot] = enrichItemData({ itemId: rawEquip[slot] }, slot);
}
});
setEquipment(mapped);
@@ -77,39 +67,70 @@ const InventoryTab = () => {
socket.on("player:inventory_data", handleInventory);
socket.on("player:equipment_data", handleEquipment);
+ socket.on("player:item_equipped", refreshData);
+ socket.on("player:item_unequipped", refreshData);
return () => {
socket.off("player:inventory_data", handleInventory);
socket.off("player:equipment_data", handleEquipment);
+ socket.off("player:item_equipped", refreshData);
+ socket.off("player:item_unequipped", refreshData);
};
}, [socket]);
- const handleItemClick = (item) => {
- setSelectedItem(item);
- setShowModal(true);
- };
-
const equipItem = (item) => {
const slot = item.meta?.equipmentSlot;
- if (slot) {
- socket.emit("player:equip_item", { itemId: item.id, slot });
- setShowModal(false);
- }
+ if (slot) socket.emit("player:equip_item", { itemId: item.id, slot });
};
const unequipItem = (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) => (
{title}
-
+
{groupSlots.map((slot) => (
equipment[slot.id] && unequipItem(slot.id)}
+ onClick={() =>
+ equipment[slot.id] &&
+ (setSelectedItem(equipment[slot.id]), setShowModal(true))
+ }
>
{slot.label}
{
{equipment[slot.id] ? (

) : (
@@ -133,88 +153,77 @@ const InventoryTab = () => {
return (
-
-
TACTICAL_INVENTORY_V2
-
-
- LOAD: {items.length}/50
-
-
-
-
- CORE_SYSTEMS
-
- {renderSlotGroup("PERSON", equipmentSlots.personal)}
- {renderSlotGroup("SHIP", equipmentSlots.ship)}
+
+ {renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)}
+ {renderSlotGroup("WEAPONRY", equipmentSlots.weapons)}
+ {renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)}
+
+ {renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)}
+
- STORAGE_UNITS
-
- {items.map((item, idx) => (
-
handleItemClick(item)}
- >
-

- {item.quantity > 1 && (
-
{item.quantity}
- )}
-
- ))}
-
+
+
+ {items.map((item, idx) => {
+ const isEquipped = Object.values(equipment).some(
+ (e) => e?.id === item.id,
+ );
+ return (
+
{
+ setSelectedItem(item);
+ setShowModal(true);
+ }}
+ >
+

+ {isEquipped &&
E
}
+ {item.quantity > 1 && (
+
{item.quantity}
+ )}
+
+ );
+ })}
+
+
- {showModal && selectedItem && (
-
setShowModal(false)}>
-
e.stopPropagation()}>
-
-
-

-
-
{selectedItem.displayName || selectedItem.id}
-
-
-
-
- {selectedItem.description || "No data available."}
-
-
-
- {selectedItem.stats &&
- Object.entries(selectedItem.stats).map(([k, v]) => (
-
-
- {GameDataManager.getStatName?.(k)?.toUpperCase() ||
- k.toUpperCase()}
-
- +{v}
-
- ))}
-
-
- {selectedItem.canEquip && (
-
- )}
-
-
-
- )}
+ {showModal &&
+ selectedItem &&
+ ReactDOM.createPortal(
+
e?.id === selectedItem.id)
+ }
+ onClose={() => {
+ setShowModal(false);
+ setSelectedItem(null);
+ }}
+ onEquip={equipItem}
+ onUnequip={(slot) => {
+ const actualSlot =
+ slot ||
+ Object.keys(equipment).find(
+ (k) => equipment[k].id === selectedItem.id,
+ );
+ unequipItem(actualSlot);
+ }}
+ formatStatName={(n) => GameDataManager.getStatName(n).toUpperCase()}
+ getStatIcon={(n) => GameDataManager.getStatIcon?.(n)}
+ />,
+ document.body,
+ )}
);
};
diff --git a/client/src/views/GameInterface/tabs/ItemListTab.jsx b/client/src/views/GameInterface/tabs/ItemListTab.jsx
index b018290..56f0c51 100644
--- a/client/src/views/GameInterface/tabs/ItemListTab.jsx
+++ b/client/src/views/GameInterface/tabs/ItemListTab.jsx
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
import GameDataManager from "../../../services/GameDataManager.js";
import "./styles/ItemListTab.css";
import { config } from "../../../config/api.js";
+import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
const ItemListTab = () => {
const [allItems, setAllItems] = useState([]);
@@ -83,7 +84,7 @@ const ItemListTab = () => {
-
+
DATA_DESCRIPTION
{selectedItem.description}
@@ -103,7 +104,7 @@ const ItemListTab = () => {
)}
-
+
)}
@@ -131,7 +132,8 @@ const ItemListTab = () => {
))}
-
+
+
{filteredItems.map((item) => (
{
))}
-
+
{(!isMobile || (isMobile && selectedItem)) && renderInspector()}
diff --git a/client/src/views/GameInterface/tabs/components/ItemModal.css b/client/src/views/GameInterface/tabs/components/ItemModal.css
index ae9c3b8..39290a0 100644
--- a/client/src/views/GameInterface/tabs/components/ItemModal.css
+++ b/client/src/views/GameInterface/tabs/components/ItemModal.css
@@ -1,36 +1,28 @@
-/* Ховаємо праву панель на мобілках */
-@media (max-width: 768px) {
- .inventory-container {
- grid-template-columns: 1fr; /* Тільки одна колонка (сітка) */
- }
- .item-details {
- display: none; /* Панель справа зникає */
- }
-}
-
-/* Стилі модального вікна */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
- height: 100%;
+ width: 100vw;
+ height: 100vh;
background: rgba(0, 0, 0, 0.85);
display: flex;
justify-content: center;
align-items: center;
- z-index: 1000;
- padding: 20px;
- width: 100%;
+ z-index: 9999;
+ backdrop-filter: blur(4px);
}
.modal-content {
- background: var(--card-bg);
- border: 1px solid var(--primary-color);
- border-radius: 12px;
+ background: #12151a;
+ border: 1px solid #00d2ff;
+ border-radius: 8px;
width: 100%;
- padding: 40px;
+ max-width: 400px;
+ padding: 25px;
position: relative;
box-shadow: 0 0 30px rgba(0, 210, 255, 0.2);
+ color: #fff;
+ font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.modal-close {
@@ -39,7 +31,114 @@
right: 15px;
background: none;
border: none;
- color: #fff;
- font-size: 24px;
+ color: #888;
+ font-size: 28px;
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;
}
diff --git a/client/src/views/GameInterface/tabs/styles/CraftingTab.css b/client/src/views/GameInterface/tabs/styles/CraftingTab.css
index 1ee2b12..0244106 100644
--- a/client/src/views/GameInterface/tabs/styles/CraftingTab.css
+++ b/client/src/views/GameInterface/tabs/styles/CraftingTab.css
@@ -1,109 +1,145 @@
.crafting-container {
- padding: 20px;
+ padding: 20px;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
}
.crafting-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.crafting-header h2 {
+ font-size: 1.2rem;
}
.crafting-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
- gap: 15px;
- margin-top: 20px;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: 15px;
+ margin-top: 20px;
}
.recipe-card {
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- padding: 20px;
- border-radius: 8px;
- text-align: center;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 10px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ padding: 15px;
+ border-radius: 8px;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
}
.recipe-card:hover {
- transform: translateY(-5px);
- border-color: var(--primary-color);
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
+ transform: translateY(-5px);
+ border-color: var(--primary-color);
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
-.recipe-icon {
- font-size: 1.5rem;
- color: var(--primary-color);
+.recipe-icon img {
+ max-width: 50px;
+ height: auto;
}
.recipe-name {
- font-size: 0.9rem;
- color: #fff;
+ font-size: 0.85rem;
+ line-height: 1.2;
+ color: #fff;
+ font-weight: 500;
+}
+
+.badge-time {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
}
-/* Категорії */
.crafting-categories {
- display: flex;
- gap: 12px;
- margin-bottom: 20px;
- padding: 10px 5px;
- overflow-x: auto;
- scrollbar-width: none;
- -webkit-overflow-scrolling: touch;
+ display: flex;
+ gap: 10px;
+ margin-bottom: 15px;
+ padding: 5px 0;
+ overflow-x: auto;
+ scrollbar-width: none;
+ -webkit-overflow-scrolling: touch;
}
.crafting-categories::-webkit-scrollbar {
- display: none;
+ display: none;
}
.crafting-cat-btn {
- flex: 0 0 auto;
- padding: 10px 22px;
- background: rgba(255, 255, 255, 0.03);
- border: 1px solid var(--border-color);
- border-radius: 6px;
- color: var(--text-secondary);
- font-family: 'Orbitron', sans-serif;
- font-size: 0.85rem;
- text-transform: uppercase;
- letter-spacing: 1px;
- white-space: nowrap;
- cursor: pointer;
- transition: all 0.25s ease;
-}
-
-.crafting-cat-btn:hover {
- background: rgba(var(--primary-rgb), 0.08);
- border-color: var(--primary-color);
- color: #fff;
+ flex: 0 0 auto;
+ padding: 8px 16px;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ color: var(--text-secondary);
+ font-family: "Orbitron", sans-serif;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ white-space: nowrap;
+ cursor: pointer;
+ transition: all 0.2s ease;
}
.crafting-cat-btn.active {
- background: var(--primary-color);
- border-color: var(--primary-color);
- color: #000;
- font-weight: 700;
- box-shadow: 0 0 15px rgba(var(--primary-rgb), 0.3);
+ background: var(--primary-color);
+ border-color: var(--primary-color);
+ color: #000;
+ font-weight: 700;
}
-@media (max-width: 768px) {
- .crafting-categories {
- gap: 8px;
- margin-bottom: 15px;
- }
-
- .crafting-cat-btn {
- padding: 8px 16px;
- font-size: 0.75rem;
- }
+@media (max-width: 600px) {
+ .crafting-container {
+ padding: 12px;
+ }
- .crafting-grid {
- grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
- gap: 10px;
- }
+ .crafting-header {
+ margin-bottom: 12px;
+ }
+
+ .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%;
}
diff --git a/client/src/views/GameInterface/tabs/styles/InventoryTab.css b/client/src/views/GameInterface/tabs/styles/InventoryTab.css
index 0175e71..9243ec2 100644
--- a/client/src/views/GameInterface/tabs/styles/InventoryTab.css
+++ b/client/src/views/GameInterface/tabs/styles/InventoryTab.css
@@ -1,4 +1,3 @@
-/* Базовий контейнер з обмеженням висоти */
.inv-adaptive-container {
display: flex;
flex-direction: column;
@@ -10,7 +9,6 @@
overflow: hidden;
}
-/* Header */
.inv-header-compact {
display: flex;
justify-content: space-between;
@@ -18,6 +16,7 @@
border-bottom: 1px solid #1a2638;
padding-bottom: 10px;
margin-bottom: 5px;
+ flex-shrink: 0;
}
.inv-logo {
@@ -26,16 +25,17 @@
letter-spacing: 3px;
margin: 0;
}
+
.inv-stats-bar {
font-size: 11px;
color: #4a5d75;
}
+
.text-cyan {
color: #00d4ff;
font-weight: bold;
}
-/* Layout Wrapper */
.inv-layout-wrapper {
display: flex;
flex-direction: row;
@@ -44,13 +44,13 @@
min-height: 0;
}
-/* Спільні стилі панелей */
.inv-panel {
background: rgba(10, 15, 24, 0.85);
border: 1px solid #1a2638;
display: flex;
flex-direction: column;
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.5);
+ min-height: 0;
}
.panel-label {
@@ -61,26 +61,55 @@
letter-spacing: 2px;
font-weight: 800;
border-bottom: 1px solid #1a2638;
+ flex-shrink: 0;
}
-/* Loadout (L) */
.loadout {
width: 240px;
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 {
padding: 10px;
- overflow-y: auto;
display: flex;
flex-direction: column;
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 {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 8px 12px;
+ padding: 6px 10px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(26, 38, 56, 0.8);
transition: 0.2s;
@@ -91,37 +120,46 @@
border-color: #00d4ff;
background: rgba(0, 212, 255, 0.05);
}
+
.slot-name-tiny {
font-size: 9px;
color: #4a5d75;
+ max-width: 140px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.equip-box-mini {
- width: 36px;
- height: 36px;
+ width: 32px;
+ height: 32px;
background: #000;
border: 1px solid #1a2638;
display: flex;
align-items: center;
justify-content: center;
color: #00d4ff;
- font-size: 1.1rem;
+ font-size: 0.9rem;
+ flex-shrink: 0;
}
-/* Cargo (R) */
.cargo {
flex: 1;
+ min-width: 0;
}
+
.cargo-grid-v2 {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(65px, 1fr));
- grid-auto-rows: 65px;
- gap: 10px;
+ grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
+ gap: 8px;
padding: 15px;
overflow-y: auto;
+ flex: 1;
}
.item-slot {
+ width: 60px;
+ height: 60px;
background: rgba(5, 8, 12, 0.9);
border: 1px solid #1a2638;
display: flex;
@@ -130,183 +168,68 @@
position: relative;
cursor: pointer;
transition: 0.2s;
+ overflow: hidden;
+}
+
+.item-img-grid,
+.item-img-mini {
+ max-width: 90%;
+ max-height: 90%;
+ object-fit: contain;
}
.item-slot:hover {
border-color: #00d4ff;
background: rgba(0, 212, 255, 0.1);
}
+
.item-slot.active {
border-color: #00d4ff;
box-shadow: inset 0 0 10px rgba(0, 212, 255, 0.3);
}
-/* Rarity Colors */
-.item-slot.rare,
-.equip-box-mini.rare {
- border-bottom: 2px solid #00d4ff;
-}
-.item-slot.epic,
-.equip-box-mini.epic {
- border-bottom: 2px solid #a335ee;
-}
-.item-slot.legendary,
-.equip-box-mini.legendary {
- border-bottom: 2px solid #ffaa00;
+.separator {
+ height: 1px;
+ background: #1a2638;
+ margin: 10px 5px;
}
-.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) {
+ .inv-adaptive-container {
+ height: auto;
+ overflow-y: visible;
+ }
+
.inv-layout-wrapper {
flex-direction: column;
- overflow-y: auto;
+ height: auto;
}
.loadout {
width: 100%;
- flex: 0 0 auto;
- }
-
- .equip-list-compact {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 8px;
+ max-height: 300px;
}
.cargo {
- min-height: 450px;
- flex: 0 0 auto;
- }
-
- .inv-adaptive-container {
- height: 100%;
- overflow-y: auto;
- padding: 10px;
+ height: 400px;
}
}
-.cargo-grid-v2 {
- display: grid;
- grid-template-columns: repeat(
- auto-fill,
- minmax(60px, 1fr)
- ); /* Кожен слот мінімум 60px */
- gap: 8px;
- padding: 10px;
+.item-slot.equipped-in-storage {
+ border: 2px solid #00d2ff;
+ box-shadow: inset 0 0 10px rgba(0, 210, 255, 0.3);
+ opacity: 0.8;
}
-.item-slot {
- width: 60px; /* Жорсткий розмір слота */
- height: 60px; /* Жорсткий розмір слота */
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(0, 0, 0, 0.5);
- border: 1px solid #333;
- overflow: hidden; /* Обрізаємо все, що виходить за межі 60x60 */
+.equipped-tag {
+ position: absolute;
+ top: -5px;
+ right: -5px;
+ background: #00d2ff;
+ color: #000;
+ font-size: 10px;
+ font-weight: bold;
+ padding: 2px 5px;
+ border-radius: 3px;
+ z-index: 2;
}
diff --git a/game-server/src/game/InventoryManager.js b/game-server/src/game/InventoryManager.js
index 100e5c4..5fa74cb 100644
--- a/game-server/src/game/InventoryManager.js
+++ b/game-server/src/game/InventoryManager.js
@@ -11,25 +11,7 @@ class InventoryManager {
async getEquipment(playerId) {
const player = await Player.findByPk(playerId);
- if (!player) return {};
-
- 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;
+ return player ? player.equipment : {};
}
async equipItem(playerId, itemId, slot) {
@@ -44,14 +26,31 @@ class InventoryManager {
throw new Error("INVALID_SLOT_FOR_ITEM");
}
- const dbField = `equipped_${slot}`.replace("original:", "");
- await Player.update({ [dbField]: itemId }, { where: { id: playerId } });
+ const player = await Player.findByPk(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;
}
async unequipItem(playerId, slot) {
- const dbField = `equipped_${slot}`;
- await Player.update({ [dbField]: null }, { where: { id: playerId } });
+ const player = await Player.findByPk(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;
}
}
diff --git a/game-server/src/game/SessionManager.js b/game-server/src/game/SessionManager.js
index 74e9188..9ed8b67 100644
--- a/game-server/src/game/SessionManager.js
+++ b/game-server/src/game/SessionManager.js
@@ -1,6 +1,6 @@
class SessionManager {
constructor() {
- this.sessions = new Map(); // socket.id => session data
+ this.sessions = new Map();
}
addPlayer(socketId, playerRaw) {
@@ -15,29 +15,18 @@ class SessionManager {
scene: "world",
sceneData: null,
joinedAt: Date.now(),
- 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,
- },
+ equipment: playerRaw.equipment || {},
});
}
updateEquipment(socketId, slot, itemId) {
const session = this.sessions.get(socketId);
- if (session && session.equipment.hasOwnProperty(slot)) {
- session.equipment[slot] = itemId;
- } else if (session) {
- session.equipment[slot] = itemId;
+ if (session) {
+ if (itemId === null) {
+ delete session.equipment[slot];
+ } else {
+ session.equipment[slot] = itemId;
+ }
}
}
@@ -47,7 +36,6 @@ class SessionManager {
session.scene = sceneName;
session.sceneData = data;
session.status = sceneName === "dungeon" ? "in_mission" : "active";
- console.log(`[Session] Player ${session.id} switched to ${sceneName}`);
}
}
diff --git a/game-server/src/models/Player.js b/game-server/src/models/Player.js
index 9679098..3a0d908 100644
--- a/game-server/src/models/Player.js
+++ b/game-server/src/models/Player.js
@@ -42,22 +42,17 @@ const Player = sequelize.define("Player", {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
-
- equipped_personal_helmet: { type: DataTypes.STRING },
- equipped_personal_suit: { type: DataTypes.STRING },
- equipped_personal_gloves: { type: DataTypes.STRING },
- equipped_personal_backpack: { type: DataTypes.STRING },
- equipped_personal_boots: { type: DataTypes.STRING },
- equipped_personal_weapons: { type: DataTypes.STRING },
-
- equipped_personal_accessory_1: { type: DataTypes.STRING },
- 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 },
+ equipment: {
+ type: DataTypes.TEXT,
+ defaultValue: "{}",
+ get() {
+ const val = this.getDataValue("equipment");
+ return val ? JSON.parse(val) : {};
+ },
+ set(val) {
+ this.setDataValue("equipment", JSON.stringify(val));
+ },
+ },
});
module.exports = Player;
diff --git a/game-server/src/sockets/handlers/inventoryHandler.js b/game-server/src/sockets/handlers/inventoryHandler.js
index 074ecea..1c4fc0f 100644
--- a/game-server/src/sockets/handlers/inventoryHandler.js
+++ b/game-server/src/sockets/handlers/inventoryHandler.js
@@ -6,31 +6,20 @@ module.exports = (io, socket) => {
if (!userId) return;
socket.on("player:get_inventory", async () => {
- try {
- const items = await inventoryManager.getInventory(userId);
- socket.emit("player:inventory_data", items);
- } catch (err) {
- socket.emit("error", { message: "LOAD_INVENTORY_FAILED" });
- }
+ const items = await inventoryManager.getInventory(userId);
+ socket.emit("player:inventory_data", items);
});
socket.on("player:get_equipment", async () => {
- try {
- const equipment = await inventoryManager.getEquipment(userId);
- socket.emit("player:equipment_data", equipment);
- } catch (err) {
- console.error(err);
- }
+ const equipment = await inventoryManager.getEquipment(userId);
+ socket.emit("player:equipment_data", equipment);
});
socket.on("player:equip_item", async ({ itemId, slot }) => {
try {
const itemInfo = await inventoryManager.equipItem(userId, itemId, slot);
-
sessionManager.updateEquipment(socket.id, slot, itemId);
-
socket.emit("player:item_equipped", { slot, itemId });
-
socket.broadcast.emit("player:visible_changed", {
playerId: userId,
slot,
@@ -45,7 +34,6 @@ module.exports = (io, socket) => {
try {
await inventoryManager.unequipItem(userId, slot);
sessionManager.updateEquipment(socket.id, slot, null);
-
socket.emit("player:item_unequipped", { slot });
socket.broadcast.emit("player:visible_changed", {
playerId: userId,
@@ -53,7 +41,7 @@ module.exports = (io, socket) => {
texturePath: null,
});
} catch (err) {
- console.error(err);
+ socket.emit("error", { message: "UNEQUIP_FAILED" });
}
});
};