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] ? ( {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 - {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()}> -
-
- preview -
-

{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" }); } }); };