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/InventoryTab.jsx b/client/src/views/GameInterface/tabs/InventoryTab.jsx
index e3d2839..e4df088 100644
--- a/client/src/views/GameInterface/tabs/InventoryTab.jsx
+++ b/client/src/views/GameInterface/tabs/InventoryTab.jsx
@@ -1,10 +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 GameDataManager from "../../../services/GameDataManager.js";
import ItemModal from "./components/ItemModal";
import "./styles/InventoryTab.css";
import { getServerUrl } from "../../../config/api.js";
+import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
const InventoryTab = () => {
const { socket } = useSocket();
@@ -12,6 +13,7 @@ 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/`;
@@ -88,32 +90,32 @@ const InventoryTab = () => {
const equipmentSlots = {
personal: Object.keys(coreSystems)
.filter(
- (key) =>
- key.startsWith("original:personal_") &&
- !key.includes("accessory") &&
- key !== "original:personal_weapons",
+ (k) =>
+ k.startsWith("original:personal_") &&
+ !k.includes("accessory") &&
+ k !== "original:personal_weapons",
)
- .map((key) => ({
- id: key,
- label: GameDataManager.t(coreSystems[key].displayName),
+ .map((k) => ({
+ id: k,
+ label: GameDataManager.t(coreSystems[k].displayName),
})),
weapons: Object.keys(coreSystems)
- .filter((key) => key === "original:personal_weapons")
- .map((key) => ({
- id: key,
- label: GameDataManager.t(coreSystems[key].displayName),
+ .filter((k) => k === "original:personal_weapons")
+ .map((k) => ({
+ id: k,
+ label: GameDataManager.t(coreSystems[k].displayName),
})),
accessories: Object.keys(coreSystems)
- .filter((key) => key.includes("personal_accessory"))
- .map((key) => ({
- id: key,
- label: GameDataManager.t(coreSystems[key].displayName),
+ .filter((k) => k.includes("personal_accessory"))
+ .map((k) => ({
+ id: k,
+ label: GameDataManager.t(coreSystems[k].displayName),
})),
ship: Object.keys(coreSystems)
- .filter((key) => key.startsWith("original:ship_"))
- .map((key) => ({
- id: key,
- label: GameDataManager.t(coreSystems[key].displayName),
+ .filter((k) => k.startsWith("original:ship_"))
+ .map((k) => ({
+ id: k,
+ label: GameDataManager.t(coreSystems[k].displayName),
})),
};
@@ -152,43 +154,49 @@ const InventoryTab = () => {
return (
-
- {renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)}
- {renderSlotGroup("WEAPONRY", equipmentSlots.weapons)}
- {renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)}
-
- {renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)}
+
+
+ {renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)}
+ {renderSlotGroup("WEAPONRY", equipmentSlots.weapons)}
+ {renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)}
+
+ {renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)}
+
+
-
- {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}
- )}
-
- );
- })}
-
+
+
+ {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 &&
ReactDOM.createPortal(
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()}