Updated API
Some checks are pending
Deploy MMO Server / deploy (push) Waiting to run

This commit is contained in:
MaksSlyzar 2026-05-05 01:04:00 +03:00
parent 4075f56099
commit 7166cbc45f
341 changed files with 16 additions and 24788 deletions

View File

@ -1,383 +0,0 @@
# Ability JSON Schema Documentation
> This document explains the structure of ability definition files used in the system.
> It is intended for **wiki contributors** and **tool developers** alike.
---
## Top-Level Structure
Every ability file follows this wrapper:
```json
{
"abilities": {
"id": "namespace:category/name",
"displayName": "translation.key.for.name",
"description": "translation.key.for.description",
"math": [ ...nodes ]
}
}
```
| Field | Type | Required | Description |
|---------------|--------|----------|-------------|
| `id` | string | ✅ | Unique ID in `namespace:category/name` format |
| `displayName` | string | ✅ | i18n translation key for the display name |
| `description` | string | ✅ | i18n translation key for the tooltip/description |
| `math` | array | ✅ | Ordered list of effect nodes (see below) |
---
## The `id` Format
IDs follow the pattern: `namespace:category/name`
| Part | Example | Notes |
|-------------|---------------|-------|
| `namespace` | `original` | The mod or source that owns this ability |
| `category` | `fire` | The element, school, or grouping |
| `name` | `fireball` | The specific ability |
**Examples:**
- `original:fire/fireball`
- `original:ice/frost_bolt`
- `mymod:arcane/void_lance`
---
## Translation Keys
`displayName` and `description` are **not raw text** — they are keys looked up in a translation file.
**Convention:** `ability.<namespace>.<name>.<field>`
| Field | Example Key |
|---------------|--------------------------------------|
| `displayName` | `ability.original.fireball.name` |
| `description` | `ability.original.fireball.description` |
---
## Math Nodes
The `math` array is the heart of an ability. Each node is an object with at minimum:
```json
{ "id": "unique_node_id", "type": "node_type", ...fields }
```
| Field | Type | Required | Description |
|--------|--------|----------|-------------|
| `id` | string | ✅ | Unique name for this node within the ability |
| `type` | string | ✅ | Determines what this node does (see types below) |
---
## Node Types
---
### `base_value`
The foundational damage or healing number, before any modifiers.
```json
{
"id": "base_damage",
"type": "base_value",
"amount": 50,
"scaling": { "stat": "spell_power", "multiplier": 1.5 }
}
```
| Field | Type | Required | Description |
|--------------------|--------|----------|-------------|
| `amount` | number | ✅ | The flat base value |
| `scaling.stat` | string | ✅ | Which player stat to scale from |
| `scaling.multiplier` | number | ✅ | How much of that stat to add (`stat × multiplier`) |
> **Formula:** `final = amount + (player[stat] × multiplier)`
---
### `range`
How far the ability can reach and how it travels.
```json
{
"id": "cast_range",
"type": "range",
"min": 0,
"max": 30,
"unit": "meters",
"rangeType": "projectile"
}
```
| Field | Type | Required | Description |
|-------------|--------|----------|-------------|
| `min` | number | ✅ | Minimum range (use `> 0` for dead zones) |
| `max` | number | ✅ | Maximum range (`null` = unlimited) |
| `unit` | string | ✅ | `"meters"` |
| `rangeType` | string | ✅ | `projectile`, `hitscan`, `melee`, `aura` |
---
### `area_of_effect`
Defines the shape and spread of the ability's impact zone.
```json
{
"id": "explosion",
"type": "area_of_effect",
"shape": "sphere",
"radius": 5,
"unit": "meters",
"falloff": "linear"
}
```
| Field | Type | Required | Description |
|-----------|--------|----------|-------------|
| `shape` | string | ✅ | `sphere`, `cone`, `cylinder`, `line` |
| `radius` | number | ✅ | Size of the AoE |
| `unit` | string | ✅ | `"meters"` |
| `falloff` | string | ✅ | `none`, `linear`, `quadratic` — how damage drops off at edges |
---
### `damage`
Instant damage dealt. Supports **multiple damage types** in one node via `sources`.
```json
{
"id": "instant_damage",
"type": "damage",
"sources": [
{
"damageType": "fire",
"base_value": 50,
"scaling": { "stat": "spell_power", "multiplier": 1.5 }
},
{
"damageType": "physical",
"base_value": 15,
"scaling": { "stat": "strength", "multiplier": 0.5 }
}
]
}
```
| Field | Type | Required | Description |
|--------------------------|--------|----------|-------------|
| `sources` | array | ✅ | One entry per damage type |
| `sources[].damageType` | string | ✅ | e.g. `fire`, `physical`, `lightning`, `poison` |
| `sources[].base_value` | number | ✅ | Flat damage for this type |
| `sources[].scaling` | object | ✅ | Same `stat` / `multiplier` structure as `base_value` |
> **Note:** Multiple sources are applied **independently** — each scales off its own stat.
---
### `damage_over_time`
Repeating damage applied in ticks after the initial hit.
```json
{
"id": "burn_dot",
"type": "damage_over_time",
"damageType": "fire",
"damage_per_tick": 10,
"tick_interval_seconds": 1,
"duration_seconds": 5,
"scaling": { "stat": "spell_power", "multiplier": 0.3 },
"stacks": false
}
```
| Field | Type | Required | Description |
|-------------------------|---------|----------|-------------|
| `damageType` | string | ✅ | Damage type per tick |
| `damage_per_tick` | number | ✅ | Flat damage each tick |
| `tick_interval_seconds` | number | ✅ | Seconds between ticks |
| `duration_seconds` | number | ✅ | Total duration |
| `scaling` | object | ✅ | Same `stat` / `multiplier` structure |
| `stacks` | boolean | ✅ | `true` = re-applying adds a new stack; `false` = resets timer |
> **Total ticks:** `duration_seconds / tick_interval_seconds`
---
### `condition`
A status effect or triggered reaction applied to the target.
```json
{
"id": "ignite_debuff",
"type": "condition",
"chance": 0.75,
"duration_seconds": 5,
"effect": "reduce_fire_resistance",
"magnitude": -20
}
```
```json
{
"id": "explosion_knockback",
"type": "condition",
"chance": 1.0,
"effect": "knockback",
"force": 8,
"direction": "away_from_origin"
}
```
| Field | Type | Required | Description |
|--------------------|--------|----------|-------------|
| `chance` | number | ✅ | Probability `0.0``1.0` (`1.0` = always) |
| `effect` | string | ✅ | The condition to apply — mapped by the engine |
| `duration_seconds` | number | ❌ | How long the condition lasts (omit for instant effects) |
| `magnitude` | number | ❌ | Numeric modifier for stat-changing effects |
| `force` | number | ❌ | For displacement effects like knockback |
| `direction` | string | ❌ | `away_from_origin`, `toward_origin`, `up` |
> Multiple `condition` nodes are rolled **independently** per hit.
---
### `meta`
Gameplay configuration — cooldowns, costs, and tags.
```json
{
"id": "fireball_meta",
"type": "meta",
"cooldown_seconds": 12,
"mana_cost": 80,
"cast_time_seconds": 1.5,
"tags": ["fire", "aoe", "projectile", "dot"]
}
```
| Field | Type | Required | Description |
|----------------------|----------|----------|-------------|
| `cooldown_seconds` | number | ✅ | Recharge time after use |
| `mana_cost` | number | ✅ | Resource cost to cast |
| `cast_time_seconds` | number | ✅ | Time before the ability fires (`0` = instant) |
| `tags` | string[] | ✅ | Used for filtering, synergies, and resistances |
---
## Complete Example — Fireball
```json
{
"abilities": {
"id": "original:fire/fireball",
"displayName": "ability.original.fireball.name",
"description": "ability.original.fireball.description",
"math": [
{
"id": "base_damage",
"type": "base_value",
"amount": 50,
"scaling": { "stat": "spell_power", "multiplier": 1.5 }
},
{
"id": "cast_range",
"type": "range",
"min": 0,
"max": 30,
"unit": "meters",
"rangeType": "projectile"
},
{
"id": "explosion",
"type": "area_of_effect",
"shape": "sphere",
"radius": 5,
"unit": "meters",
"falloff": "linear"
},
{
"id": "instant_damage",
"type": "damage",
"sources": [
{
"damageType": "fire",
"base_value": 50,
"scaling": { "stat": "spell_power", "multiplier": 1.5 }
},
{
"damageType": "physical",
"base_value": 15,
"scaling": { "stat": "strength", "multiplier": 0.5 }
}
]
},
{
"id": "burn_dot",
"type": "damage_over_time",
"damageType": "fire",
"damage_per_tick": 10,
"tick_interval_seconds": 1,
"duration_seconds": 5,
"scaling": { "stat": "spell_power", "multiplier": 0.3 },
"stacks": false
},
{
"id": "ignite_debuff",
"type": "condition",
"chance": 0.75,
"duration_seconds": 5,
"effect": "reduce_fire_resistance",
"magnitude": -20
},
{
"id": "explosion_knockback",
"type": "condition",
"chance": 1.0,
"effect": "knockback",
"force": 8,
"direction": "away_from_origin"
},
{
"id": "fireball_meta",
"type": "meta",
"cooldown_seconds": 12,
"mana_cost": 80,
"cast_time_seconds": 1.5,
"tags": ["fire", "aoe", "projectile", "dot"]
}
]
}
}
```
---
## Quick Reference Card
| Node Type | Purpose | Key Fields |
|-------------------|-------------------------------------|------------|
| `base_value` | Core damage number + stat scaling | `amount`, `scaling` |
| `range` | How far & how the ability travels | `min`, `max`, `rangeType` |
| `area_of_effect` | Shape & spread of impact zone | `shape`, `radius`, `falloff` |
| `damage` | Instant hit damage (multi-type ok) | `sources[]` |
| `damage_over_time`| Tick damage over time | `damage_per_tick`, `tick_interval_seconds`, `duration_seconds`, `stacks` |
| `condition` | Status effects & reactions | `chance`, `effect`, `duration_seconds` |
| `meta` | Cooldown, cost, cast time, tags | `cooldown_seconds`, `mana_cost`, `cast_time_seconds`, `tags` |
---
## Rules & Conventions
1. Every ability **must** have at least one `meta` node.
2. `id` values must be **unique within the same ability file** — use descriptive names.
3. `displayName` and `description` must be **translation keys**, never raw text.
4. The `math` array is processed **top to bottom** — order matters for dependent effects.
5. `damage` nodes should come **after** any `area_of_effect` nodes that modify their area.
6. A `damage_over_time` node is **separate** from `damage` — both can coexist freely.
7. Multiple `condition` nodes are each rolled independently.
8. `tags` on the `meta` node are used by the engine for resistance checks, talent synergies, and UI filtering.

View File

@ -1,191 +0,0 @@
# Admin Panel
## - Items List
### Panel Setup
- Tags (Dynamic)
- Default (all)
- Alloys
- Circuits
- Customizables
- Ingots
- Materials
- Ores
- Personal
- Shop
- Space Ships
- Shields
- Weapons
## - Hostiles List
### Panel Setup
- Tags (Dynamic)
- Default (all)
- Ground Units
- Space Ships
## - Player List
- Tags (Dynamic)
- Default (all)
- Members
- Moderators
- Admins
## - Permissions
### Panel hierarchy
- Roles (Static)
- Edit
- Role Tag
- Role Name
- Permission Nodes
- Chat Font Color
- New
- Role Tag
- Role Name
- Permission Nodes
- Chat Font Color
- Users (Static)
- Edit
- Permission Nodes
# Permission Nodes
### Default Roles
- Members Permission Nodes
- permission.node.player.text.chat
- permission.node.player.text.chat.emote
- permission.node.player.text.chat.message
- permission.node.player.text.chat.message.direct
- permission.node.player.text.color.name
- permission.node.guild.join
- Moderator Permission Nodes
- global.roles.members.*
- permission.node.player.server.ban
- permission.node.player.server.timeout
- permission.node.player.server.unban
- permission.node.player.text.ban
- permission.node.player.text.timeout.["timeinseconds"]
- permission.node.player.text.unban
- Admin Permission Nodes
- global.roles.moderators.*
- permission.node.player.clean.database.all
- permission.node.player.clean.database.item
- permission.node.player.clean.inventory.all
- permission.node.player.clean.inventory.item
- permission.node.player.console
- permission.node.player.give.exp
- permission.node.player.give.item
- permission.node.player.give.skill
- permission.node.player.permission.add.["permission.node.*"]
- permission.node.player.permission.edit.["permission.node.*"]
- permission.node.player.permission.remove.["permission.node.*"]
- permission.node.player.role.add.["permission.node.*"]
- permission.node.player.role.edit
- permission.node.player.role.give
- permission.node.player.role.hierarchy
- permission.node.player.role.new
- permission.node.player.role.remove
- permission.node.player.role.remove.["permission.node.*"]
- Super Admin Permission Nodes (First Person On Server)
- permission.node.player.bypass
### Extra Nodes
- permission.node.tab.*.(allow/deny)
- permission.node.tab.dashboard.(allow/deny)
- permission.node.tab.dungeons.*
- ["permission.node.tab.dungeons.{datapackId}.{dungeonId}.(allow/deny)"]
- permission.node.tab.skills.*
- ["permission.node.tab.skills.{datapackId}.{skillsId}.(allow/deny)"]
- permission.node.tab.inventory.*
- ["permission.node.tab.inventory.{datapackId}.{core_systemId}.(allow/deny)"]
- permission.node.tab.shop.*
- ["permission.node.tab.shop.{datapackId}.{shopId}.(allow/deny)"]
- permission.node.tab.crafting.*
- ["permission.node.tab.crafting.{datapackId}.{craftingId}.(allow/deny)"]
- permission.node.tab.admin.*
- ["permission.node.tab.admin.{datapackId}.{adminId}.(allow/deny)"]
- permission.node.tab.chat.*
- ["permission.node.tab.chat.{datapackId}.{chatId}.(allow/deny)"]
- permission.node.tab.alerts.(allow/deny)
- permission.node.generate.credits.online.*.(allow/deny)
- permission.node.generate.credits.offline.*.(allow/deny)
- permission.node.generate.datacores.online.*.(allow/deny)
- permission.node.generate.datacores.offline.*.(allow/deny)
- permission.node.player.nickname.(allow/deny)
- permission.node.player.text.color.(allow/deny)
- permission.node.player.text.emote.(allow/deny)
### Guild Type Permissions
- guild.roles.*
- permission.node.guild.admin.*
- permission.node.guild.admin.alliance.*
- permission.node.guild.admin.alliance.create
- permission.node.guild.admin.alliance.join
- permission.node.guild.admin.alliance.leave
- permission.node.guild.admin.manage.*
- permission.node.guild.admin.manage.base.*
- permission.node.guild.admin.manage.base.type.*
- permission.node.guild.admin.manage.base.type.booster.*
- permission.node.guild.admin.manage.base.type.booster.{integar}
- permission.node.guild.admin.manage.base.type.mine
- permission.node.guild.admin.manage.base.type.mine.{integar}
- permission.node.guild.admin.manage.base.type.research.*
- permission.node.guild.admin.manage.base.type.research.{{datapackId}.{researchId}}
- permission.node.guild.admin.manage.base.type.storage.*
- permission.node.guild.admin.manage.base.type.storage.{integar}
- permission.node.guild.admin.manage.base.type.storage.{integar}.name
- permission.node.guild.admin.manage.base.type.storage.{integer}.limit.*
- permission.node.guild.admin.manage.base.type.storage.{integar}.limit.add
- permission.node.guild.admin.manage.base.type.storage.{integar}.limit.remove
- permission.node.guild.admin.manage.base.type.storage.{integar}.hidden
- permission.node.guild.admin.manage.message.announcement
- permission.node.guild.admin.manage.message.day
- permission.node.guild.admin.manage.message.recuitement
- permission.node.guild.admin.manage.name
- permission.node.guild.admin.manage.role.*
- permission.node.guild.admin.manage.role.create
- permission.node.guild.admin.manage.role.delete
- permission.node.guild.admin.manage.role.edit.*
- permission.node.guild.admin.manage.role.edit.hierarchy
- permission.node.guild.admin.manage.role.edit.name
- permission.node.guild.admin.manage.role.edit.permission
- permission.node.guild.invite
- permission.node.guild.join
- permission.node.guild.storage.*
- permission.node.guild.storage.add.*
- permission.node.guild.storage.add.{integer}
- permission.node.guild.storage.hidden.{integer}
- permission.node.guild.storage.remove.*
- permission.node.guild.storage.remove.{integer}
- permission.node.guild.text.chat.message.["guild.roles.*"]
### Alliance Type Permissions
- alliance.roles.*
- permission.node.alliance.admin.*
- permission.node.alliance.admin.manage.*
- permission.node.alliance.admin.manage.base.*
- permission.node.alliance.admin.manage.base.type.*
- permission.node.alliance.admin.manage.base.type.booster.*
- permission.node.alliance.admin.manage.base.type.booster.{integar}
- permission.node.alliance.admin.manage.base.type.mine
- permission.node.alliance.admin.manage.base.type.mine.{integar}
- permission.node.alliance.admin.manage.base.type.research.*
- permission.node.alliance.admin.manage.base.type.research.{{datapackId}.{researchId}}
- permission.node.alliance.admin.manage.base.type.storage.*
- permission.node.alliance.admin.manage.base.type.storage.{integar}
- permission.node.alliance.admin.manage.base.type.storage.{integar}.name
- permission.node.alliance.admin.manage.base.type.storage.{integer}.limit.*
- permission.node.alliance.admin.manage.base.type.storage.{integar}.limit.add
- permission.node.alliance.admin.manage.base.type.storage.{integar}.limit.remove
- permission.node.alliance.admin.manage.base.type.storage.{integar}.hidden
- permission.node.alliance.admin.manage.kick
- permission.node.alliance.admin.manage.message.announcement
- permission.node.alliance.admin.manage.message.day
- permission.node.alliance.admin.manage.message.recuitement
- permission.node.alliance.admin.manage.name
- permission.node.alliance.admin.manage.role.*
- permission.node.alliance.admin.manage.role.create
- permission.node.alliance.admin.manage.role.delete
- permission.node.alliance.admin.manage.role.edit.*
- permission.node.alliance.admin.manage.role.edit.hierarchy
- permission.node.alliance.admin.manage.role.edit.name
- permission.node.alliance.admin.manage.role.edit.permission
- permission.node.alliance.invite
- permission.node.alliance.join
- permission.node.alliance.storage.*
- permission.node.alliance.storage.add.*
- permission.node.alliance.storage.add.{integer}
- permission.node.alliance.storage.hidden.{integer}
- permission.node.alliance.storage.remove.*
- permission.node.alliance.storage.remove.{integer}
- permission.node.alliance.text.chat.message.["alliance.roles.*"]

View File

@ -1,18 +0,0 @@
# Quests Panel
## Quests new variables
```js
{
"quests": {
"id": "datapackID:questID",
"displayName": "items.materials.original.quest.quest_name",
"description": "items.materials.original.quest.quest_name.desc",
"repeatable": boolean,
"questType": String,
"requiredDone": [],
"unlockRequirements": [],
"onFinish": [],
"meta": {
}
}
}
```

View File

@ -1,20 +0,0 @@
# Store Panel
## Items new variables
```js
{
"materials": {
"id": "original:ore_coal",
"texture": "original/assets/textures/materials/ore/coal.png",
"displayName": "items.materials.original.ores.coal",
"description": "items.materials.original.ores.coal.desc",
"meta": {
"storeCategory": "original:materials",
"storeFeaturedDiscountPercentage": 0.5,
"storeFeaturedShowWeight": 10, #Higher number harder for role.
"storePrice": 50,
"storeSellValue": 10,
"storeShowWeight": 10 #Higher number harder for role.
}
}
}
```

View File

@ -1,23 +0,0 @@
{
"name": "api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node ./src/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"bcryptjs": "^3.0.3",
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"mongoose": "^9.3.0",
"socket.io": "^4.8.3"
}
}

24
client/.gitignore vendored
View File

@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,16 +0,0 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

View File

@ -1,29 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

View File

@ -1,25 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content"
/>
<title>Galaxy Strike Online</title>
<link
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Space+Mono:wght@400;700&display=swap"
rel="stylesheet"
/>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -1,29 +0,0 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.13.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"socket.io-client": "^4.8.3"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"vite": "^7.2.4"
}
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,137 +0,0 @@
import React, { useEffect, useState, useCallback, useRef } from "react";
import MainMenu from "./views/MainMenu/MainMenu";
import GameInterface from "./views/GameInterface/GameInterface";
import LoadingScreen from "./views/LoadingScreen/LoadingScreen.jsx";
import GameDataManager from "./services/GameDataManager";
import { useAuth } from "./hooks/useAuth";
import { useSocket } from "./hooks/useSocket";
function App() {
const { isConnected, connectToServer, socket } = useSocket();
const { user } = useAuth();
const [isInGame, setIsInGame] = useState(false);
const [isBooting, setIsBooting] = useState(false);
const [loadingProgress, setLoadingProgress] = useState(0);
const [statusText, setStatusText] = useState("");
const hasAttemptedBoot = useRef(false);
const fetchMetadata = useCallback(async (serverUrl) => {
try {
const response = await fetch(`${serverUrl}/api/game-metadata`);
if (!response.ok) throw new Error("Metadata fetch failed");
const data = await response.json();
GameDataManager.initialize(data);
return true;
} catch (err) {
console.error("Metadata Sync Error:", err);
return false;
}
}, []);
const bootSequence = useCallback(
async (serverUrl, token) => {
if (isBooting) return;
setIsBooting(true);
setLoadingProgress(10);
setStatusText("Initializing Systems...");
try {
setStatusText("Fetching Galactic Database...");
const success = await fetchMetadata(serverUrl);
if (!success) throw new Error("Initial metadata load failed");
setLoadingProgress(60);
setStatusText("Establishing Neural Link...");
const socketInstance = connectToServer(serverUrl, token);
socketInstance.once("session:ready", () => {
setLoadingProgress(100);
setStatusText("Ready to Launch");
setTimeout(() => {
setIsInGame(true);
setIsBooting(false);
}, 600);
});
socketInstance.once("connect_error", (err) => {
console.error("Socket error:", err);
setStatusText("Neural Link Failed");
setTimeout(() => setIsBooting(false), 2000);
});
} catch (err) {
console.error("Boot error:", err);
setStatusText("System Failure");
setTimeout(() => setIsBooting(false), 2000);
}
},
[connectToServer, isBooting, fetchMetadata],
);
useEffect(() => {
if (!socket || !isConnected) return;
const handleDataUpdate = async () => {
console.log("🔄 [System] Datapacks changed on server. Synchronizing...");
const savedServer = localStorage.getItem("activeServer");
if (!savedServer) return;
const { connectUrl } = JSON.parse(savedServer);
const success = await fetchMetadata(connectUrl);
if (success) {
console.log("✅ [System] Local database updated successfully.");
}
};
socket.on("system:data_updated", handleDataUpdate);
return () => {
socket.off("system:data_updated", handleDataUpdate);
};
}, [socket, isConnected, fetchMetadata]);
useEffect(() => {
if (user && !hasAttemptedBoot.current && !isConnected && !isInGame) {
const savedServer = localStorage.getItem("activeServer");
const token = user?.token;
if (savedServer && token) {
const server = JSON.parse(savedServer);
hasAttemptedBoot.current = true;
bootSequence(server.connectUrl, token);
}
}
}, [user, isConnected, isInGame, bootSequence]);
const handleStartGame = () => {
const savedServer = localStorage.getItem("activeServer");
const token = user?.token;
if (savedServer && token) {
const server = JSON.parse(savedServer);
bootSequence(server.connectUrl, token);
}
};
if (isBooting) {
return <LoadingScreen progress={loadingProgress} statusText={statusText} />;
}
return (
<div className="game-wrapper">
{isInGame && isConnected ? (
<GameInterface onExit={() => setIsInGame(false)} />
) : (
<MainMenu onStartGame={handleStartGame} />
)}
</div>
);
}
export default App;

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 228 KiB

View File

@ -1,86 +0,0 @@
.game-console-bottom {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 35vh; /* Займає третину екрана знизу */
background: rgba(10, 15, 20, 0.95);
border-top: 2px solid #00ffaa;
display: flex;
flex-direction: column;
z-index: 10000;
font-family: "Consolas", "Monaco", monospace;
box-shadow: 0 -5px 20px rgba(0, 0, 0, 0.5);
}
.console-scroll-area {
flex: 1;
overflow-y: auto;
padding: 10px;
font-size: 13px;
color: #e0e0e0;
}
.console-line {
margin-bottom: 4px;
border-left: 2px solid transparent;
padding-left: 8px;
}
.console-line:contains(">") {
color: #00ffaa;
}
/* Підказки випливають ВГОРУ */
.console-suggestions-upward {
position: absolute;
bottom: 45px; /* Над полем вводу */
left: 0;
width: 100%;
background: #151a20;
border-top: 1px solid #333;
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px;
max-height: 100px;
overflow-y: auto;
}
.suggestion-item {
padding: 3px 10px;
background: #252a30;
color: #888;
font-size: 12px;
border-radius: 4px;
}
.suggestion-item.selected {
background: #00ffaa;
color: #000;
font-weight: bold;
}
.console-input-form {
display: flex;
align-items: center;
background: #000;
padding: 5px 15px;
height: 40px;
}
.console-prompt {
color: #00ffaa;
margin-right: 10px;
font-weight: bold;
}
.console-input-form input {
flex: 1;
background: transparent;
border: none;
color: #fff;
font-family: inherit;
font-size: 16px;
outline: none;
}

View File

@ -1,139 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import "./Console.css";
import { useSocket } from "../../hooks/useSocket";
import ConsoleManager from "../../services/ConsoleManager";
import PlayerManager from "../../services/PlayerManager"; // Імпортуємо менеджер гравців
const Console = () => {
const { socket } = useSocket();
const [isOpen, setIsOpen] = useState(false);
const [input, setInput] = useState("");
const [suggestions, setSuggestions] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0);
const [logs, setLogs] = useState(ConsoleManager.logs);
const [playerNames, setPlayerNames] = useState([]);
const inputRef = useRef(null);
const scrollRef = useRef(null);
useEffect(() => {
ConsoleManager.init(socket, setLogs);
}, [socket]);
useEffect(() => {
const updatePlayers = (data) => {
setPlayerNames([...data.online, ...data.offline]);
};
const unsubscribe = PlayerManager.subscribe(updatePlayers);
setPlayerNames([
...PlayerManager.onlinePlayers,
...PlayerManager.offlinePlayers,
]);
return () => unsubscribe();
}, []);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [logs]);
useEffect(() => {
const handleGlobalKeyDown = (e) => {
if (e.key === "F9") {
e.preventDefault();
setIsOpen((prev) => !prev);
}
};
window.addEventListener("keydown", handleGlobalKeyDown);
return () => window.removeEventListener("keydown", handleGlobalKeyDown);
}, []);
useEffect(() => {
setSuggestions(ConsoleManager.getSuggestions(input, playerNames));
setSelectedIndex(0);
}, [input, playerNames]);
const handleKeyDown = (e) => {
if (e.key === "Tab" && suggestions.length > 0) {
e.preventDefault();
const parts = input.split(" ");
parts[parts.length - 1] = suggestions[selectedIndex];
setInput(parts.join(" ") + (parts.length < 3 ? " " : ""));
}
if (e.key === "ArrowDown") {
if (suggestions.length > 0) {
setSelectedIndex((prev) => (prev + 1) % suggestions.length);
} else {
const hist = ConsoleManager.getHistory("down");
if (hist !== null) setInput(hist);
}
}
if (e.key === "ArrowUp") {
if (suggestions.length > 0) {
setSelectedIndex(
(prev) => (prev - 1 + suggestions.length) % suggestions.length,
);
} else {
const hist = ConsoleManager.getHistory("up");
if (hist !== null) setInput(hist);
}
}
if (e.key === "Escape") setIsOpen(false);
};
const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return;
ConsoleManager.execute(input);
setInput("");
};
if (!isOpen) return null;
return (
<div className="game-console-bottom">
<div className="console-scroll-area" ref={scrollRef}>
{logs.map((line, i) => (
<div key={i} className="console-line">
{line}
</div>
))}
</div>
{suggestions.length > 0 && (
<div className="console-suggestions-upward">
{suggestions.map((s, i) => (
<div
key={i}
className={`suggestion-item ${i === selectedIndex ? "selected" : ""}`}
>
{s}
</div>
))}
</div>
)}
<form className="console-input-form" onSubmit={handleSubmit}>
<span className="console-prompt">#</span>
<input
ref={inputRef}
autoFocus
value={input}
onKeyDown={handleKeyDown}
onChange={(e) => setInput(e.target.value)}
placeholder="Type /command..."
/>
</form>
</div>
);
};
export default Console;

View File

@ -1,53 +0,0 @@
.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;
}

View File

@ -1,58 +0,0 @@
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;

View File

@ -1,27 +0,0 @@
import React from "react";
import "./styles/Button.css";
const Button = ({
children,
variant = "primary",
size = "medium",
icon,
id,
onClick,
className = "",
}) => {
const fullClassName =
`galaxy-button variant-${variant} size-${size} ${className}`.trim();
return (
<button className={fullClassName} id={id} onClick={onClick}>
<span className="button-content">
{icon && <i className={`fas ${icon} button-icon`}></i>}
<span className="button-text">{children}</span>
</span>
<span className="button-glow"></span>
</button>
);
};
export default Button;

View File

@ -1,14 +0,0 @@
import React from 'react';
const Card = ({ title, children, className = "" }) => {
return (
<div className={`card ${className}`}>
{title && <h3>{title}</h3>}
<div className="card-content">
{children}
</div>
</div>
);
};
export default Card;

View File

@ -1,20 +0,0 @@
const Input = ({ label, type = 'text', placeholder, id, ...props }) => {
return (
<div className="form-group">
<div className="form-group">
{label && <label htmlFor={id}>{label}</label>}
<input
type={type}
id={id}
placeholder={placeholder}
className="form-input"
{...props}
/>
</div>
</div>
);
};
export default Input;

View File

@ -1,116 +0,0 @@
.galaxy-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
outline: none;
border-radius: 10px;
/* Основний фон: глибокий синій з переходом */
background: linear-gradient(180deg, #0a243d 0%, #051626 100%);
/* Рамка: світло-блакитна, напівпрозора */
border: 1px solid rgba(0, 212, 255, 0.4);
border-radius: 2px;
color: #88eaff; /* Блідо-блакитний текст */
font-family: "Orbitron", sans-serif;
letter-spacing: 2px;
text-transform: uppercase;
transition: all 0.25s ease;
overflow: hidden;
/* Тінь для об'єму */
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
/* Куточки - тепер вони світяться блакитним */
.galaxy-button::before,
.galaxy-button::after {
content: "";
position: absolute;
width: 7px;
height: 7px;
border: 2px solid transparent;
transition: all 0.3s ease;
z-index: 2;
}
.galaxy-button::before {
top: -1px;
left: -1px;
border-top-color: #00d4ff;
border-left-color: #00d4ff;
}
.galaxy-button::after {
bottom: -1px;
right: -1px;
border-bottom-color: #00d4ff;
border-right-color: #00d4ff;
}
/* VARIANT: PRIMARY (Яскравий блакитний неон) */
.variant-primary {
background: linear-gradient(180deg, #0e3a5a 0%, #082336 100%);
border-color: #00d4ff;
color: #00d4ff;
font-weight: 700;
}
.variant-primary:hover {
background: #114a73;
color: #fff;
/* Ефект підсвічування всієї кнопки */
box-shadow:
0 0 20px rgba(0, 212, 255, 0.4),
inset 0 0 10px rgba(0, 212, 255, 0.2);
border-color: #88eaff;
}
/* VARIANT: SECONDARY (Спокійний синій) */
.variant-secondary {
background: linear-gradient(180deg, #102a43 0%, #0b1d2e 100%);
border-color: rgba(0, 212, 255, 0.2);
color: rgba(0, 212, 255, 0.6);
}
.variant-secondary:hover {
background: #163a5d;
border-color: rgba(0, 212, 255, 0.6);
color: #88eaff;
}
/* SIZES */
.size-large {
padding: 16px 40px;
font-size: 0.9rem;
}
.size-medium {
padding: 12px 24px;
font-size: 0.8rem;
}
/* CONTENT */
.button-content {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
z-index: 3;
text-shadow: 0 0 8px rgba(0, 212, 255, 0.5);
}
.button-icon {
font-size: 1.1em;
filter: drop-shadow(0 0 5px #00d4ff);
}
/* ACTIVE STATE */
.galaxy-button:active {
transform: scale(0.96);
filter: brightness(1.3);
background: #1a5c8a;
}

View File

@ -1,20 +0,0 @@
const getActiveServer = () => {
const saved = localStorage.getItem("activeServer");
if (!saved) return null;
try {
return JSON.parse(saved);
} catch (e) {
return null;
}
};
export const getServerUrl = () => {
const server = getActiveServer();
return server ? server.connectUrl : null;
};
export const config = {
get serverUrl() {
return getServerUrl();
},
};

View File

@ -1,72 +0,0 @@
import { createContext, useState } from "react";
import axios from "axios";
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const API_URL = `${import.meta.env.VITE_API_URL}/api/auth`;
console.log(API_URL);
const [user, setUser] = useState(() => {
const savedUser = localStorage.getItem("user");
return savedUser ? JSON.parse(savedUser) : null;
});
const [loading, setLoading] = useState(false);
const login = async (email, password) => {
setLoading(true);
try {
const response = await axios.post(`${API_URL}/login`, {
email,
password,
});
const data = response.data;
localStorage.setItem("user", JSON.stringify(data));
setUser(data);
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data?.message || "Login failed",
};
} finally {
setLoading(false);
}
};
const register = async (username, email, password) => {
setLoading(true);
try {
const response = await axios.post(`${API_URL}/register`, {
username,
email,
password,
});
const data = response.data;
localStorage.setItem("user", JSON.stringify(data));
setUser(data);
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data?.message || "Registration failed",
};
} finally {
setLoading(false);
}
};
const logout = () => {
console.log("Logout");
localStorage.removeItem("user");
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, register, logout, loading }}>
{children}
</AuthContext.Provider>
);
};

View File

@ -1,85 +0,0 @@
import React, {
createContext,
useState,
useCallback,
useRef,
useEffect,
} from "react";
import { io } from "socket.io-client";
import PlayerManager from "../services/PlayerManager"; // Імпортуємо менеджер
export const SocketContext = createContext(null);
export const SocketProvider = ({ children }) => {
const [socket, setSocket] = useState(null);
const [isConnected, setIsConnected] = useState(false);
const socketRef = useRef(null);
const connectToServer = useCallback((url, token) => {
if (socketRef.current?.connected) {
return socketRef.current;
}
const userInfo = JSON.parse(localStorage.getItem("user"));
const newSocket = io(url.replace("/game-api", ""), {
path: "/socket.io/",
auth: { token, username: userInfo?.username },
transports: ["websocket"],
reconnectionAttempts: 5,
});
newSocket.on("connect", () => {
console.log("✅ Connected with ID:", newSocket.id);
setIsConnected(true);
});
newSocket.on("session:ready", (data) => {
PlayerManager.setInitialState(data.onlinePlayers, data.offlinePlayers);
});
newSocket.on("player:joined", (data) => {
PlayerManager.handlePlayerJoined(data.username);
});
newSocket.on("player:left", (data) => {
PlayerManager.handlePlayerLeft(data.username);
});
newSocket.on("disconnect", (reason) => {
setIsConnected(false);
});
newSocket.on("connect_error", (err) => {
console.error("⚠️ Connection error:", err.message);
});
socketRef.current = newSocket;
setSocket(newSocket);
return newSocket;
}, []);
const disconnectFromServer = useCallback(() => {
if (socketRef.current) {
socketRef.current.disconnect();
socketRef.current = null;
setSocket(null);
setIsConnected(false);
}
}, []);
useEffect(() => {
return () => {
if (socketRef.current) socketRef.current.disconnect();
};
}, []);
return (
<SocketContext.Provider
value={{ socket, isConnected, connectToServer, disconnectFromServer }}
>
{children}
</SocketContext.Provider>
);
};

View File

@ -1,10 +0,0 @@
import { useContext } from "react";
import { AuthContext } from "../context/AuthContext";
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};

View File

@ -1,12 +0,0 @@
import { useContext } from "react";
import { SocketContext } from "../context/SocketContext";
export const useSocket = () => {
const context = useContext(SocketContext);
if (!context) {
throw new Error("useSocket must be used within a SocketProvider");
}
return context;
};

View File

@ -1,18 +0,0 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles/components.css";
import "./styles/tables.css";
import "./styles/main.css";
import { AuthProvider } from "./context/AuthContext";
import { SocketContext, SocketProvider } from "./context/SocketContext";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<AuthProvider>
<SocketProvider>
<App />
</SocketProvider>
</AuthProvider>
</React.StrictMode>,
);

View File

@ -1,101 +0,0 @@
import GameDataManager from "./GameDataManager";
class ConsoleManager {
constructor() {
this.commands = ["/clear", "/give", "/set_exp"];
this.logs = ["[System] Console Manager Ready. Press F9 to hide."];
this.history = [];
this.historyIndex = -1;
this.onLogsChange = null;
this.socket = null;
}
init(socket, onLogsChange) {
this.socket = socket;
this.onLogsChange = onLogsChange;
if (this.socket) {
this.socket.off("admin:log");
this.socket.on("admin:log", (msg) => this.addLog(`[Server] ${msg}`));
}
}
addLog(line) {
this.logs = [...this.logs.slice(-100), line];
if (this.onLogsChange) this.onLogsChange(this.logs);
}
getSuggestions(input, players = []) {
const safePlayers = (players || []).filter((p) => typeof p === "string");
const parts = input.split(" ");
if (parts.length === 0) return [];
const cmd = parts[0].toLowerCase();
if (parts.length === 1 && input.startsWith("/")) {
return this.commands.filter((c) => c.startsWith(cmd));
}
if (cmd === "/give") {
if (parts.length === 2) {
const search = parts[1].toLowerCase();
return safePlayers.filter((p) => p.toLowerCase().startsWith(search));
}
if (parts.length === 3) {
const search = parts[2].toLowerCase();
const itemIds = Array.from(GameDataManager.items.keys());
return itemIds
.filter((id) => id.toLowerCase().includes(search))
.slice(0, 15);
}
}
if (cmd === "/set_exp") {
if (parts.length === 2) {
const search = parts[1].toLowerCase();
return safePlayers.filter((p) => p.toLowerCase().startsWith(search));
}
}
if (cmd === "/clear" && parts.length === 2) {
const search = parts[1].toLowerCase();
return safePlayers.filter((p) => p.toLowerCase().startsWith(search));
}
return [];
}
execute(input) {
const trimmed = input.trim();
if (!trimmed) return;
this.addLog(`> ${trimmed}`);
if (this.history[0] !== trimmed) {
this.history.unshift(trimmed);
if (this.history.length > 50) this.history.pop();
}
this.historyIndex = -1;
if (this.socket) {
this.socket.emit("admin:command", { command: trimmed });
}
}
getHistory(direction) {
if (this.history.length === 0) return null;
if (direction === "up") {
this.historyIndex = Math.min(
this.historyIndex + 1,
this.history.length - 1,
);
} else {
this.historyIndex = Math.max(this.historyIndex - 1, -1);
}
return this.historyIndex === -1 ? "" : this.history[this.historyIndex];
}
}
export default new ConsoleManager();

View File

@ -1,239 +0,0 @@
class GameDataManager {
constructor() {
this.items = new Map();
this.recipes = new Map();
this.skills = new Map();
this.dungeons = new Map();
this.hostiles = new Map();
this.rooms = new Map();
this.quests = new Map();
this.translations = {};
this.manifest = {};
this.currentLang = localStorage.getItem("selected_lang") || "en_US";
this.isLoaded = false;
}
initialize(data) {
if (Array.isArray(data.items)) {
data.items.forEach((item) => this.items.set(item.id, item));
}
if (Array.isArray(data.recipes)) {
data.recipes.forEach((recipe) => this.recipes.set(recipe.id, recipe));
}
if (Array.isArray(data.dungeons)) {
data.dungeons.forEach((d) => this.dungeons.set(d.id, d));
}
if (Array.isArray(data.enemies)) {
data.enemies.forEach((e) => this.hostiles.set(e.id, e));
}
if (Array.isArray(data.skills)) {
data.skills.forEach((s) => this.skills.set(s.id, s));
}
if (Array.isArray(data.rooms)) {
data.rooms.forEach((r) => this.rooms.set(r.id, r));
}
if (Array.isArray(data.quests)) {
data.quests.forEach((q) => this.quests.set(q.id, q));
}
console.log(this.skills);
if (data.languages) {
this.translations = data.languages;
}
if (data.manifest) {
this.manifest = data.manifest;
}
console.log(this.manifest);
this.isLoaded = true;
}
getSkillCategories() {
return this._getCategoriesFromManifest("skills");
}
getSkillsByCategory(category) {
console.log(this.skills, category, "CATEGORY");
return Array.from(this.skills.values())
.filter((skill) => skill.meta.category === category)
.map((skill) => ({
...skill,
displayName: this.t(skill.displayName),
description: this.t(skill.description),
}));
}
t(key) {
if (!key) return "";
const langData = this.translations[this.currentLang];
if (!langData) return key;
if (langData[key]) return langData[key];
if (key.includes(".")) {
const value = key
.split(".")
.reduce((obj, i) => (obj ? obj[i] : null), langData);
if (value) return value;
}
return key;
}
getStatName(statPath) {
const fullKey = `stats.category.original.${statPath}`;
return this.t(fullKey);
}
_getCategoriesFromManifest(section) {
if (!this.manifest[section] || !this.manifest[section].categories)
return [];
return Object.entries(this.manifest[section].categories).map(
([id, config]) => ({
id,
displayName: this.t(config.displayName),
rawConfig: config,
}),
);
}
getRecipeCategories() {
return this._getCategoriesFromManifest("recipes");
}
getShopCategories() {
return this._getCategoriesFromManifest("shop");
}
getSkillCategories() {
return this._getCategoriesFromManifest("skills");
}
getRecipesByCategory(category) {
return Array.from(this.recipes.values())
.filter((recipe) => recipe.type === category)
.map((recipe) => this.getRecipe(recipe.id))
.filter(Boolean);
}
_cleanId(id) {
if (!id || typeof id !== "string") return id;
let clean = id.includes(":") ? id.split(":")[1] : id;
if (clean.includes("/")) {
const parts = clean.split("/");
clean = parts[parts.length - 1];
}
return clean;
}
getHostile(id) {
const hostile = this.hostiles.get(id);
if (!hostile) {
return {
id: id,
displayName: `Unknown Entity (${id})`,
level: 1,
stats: { hp: 100 },
};
}
return {
...hostile,
displayName: this.t(hostile.displayName),
};
}
getEnemy(id) {
return this.getHostile(id);
}
getItem(id) {
const item = this.items.get(id);
if (!item) {
return {
id: id,
displayName: id.replace(/_/g, " "),
texture: null,
description: "Data not found",
meta: { rarity: "common" },
};
}
return {
...item,
displayName: this.t(item.displayName),
description: this.t(item.description),
};
}
getRecipe(recipeId) {
const recipe = this.recipes.get(recipeId);
if (!recipe) return null;
const outputData = Object.entries(recipe.output || {})[0];
const targetItemId = outputData ? outputData[0] : recipe.id;
const resultItem = this.getItem(targetItemId);
return {
...recipe,
displayName: resultItem.displayName,
texture: resultItem.texture,
description: resultItem.description,
ingredients: (recipe.inputs || []).map((inputObj) => {
const [itemId, quantity] = Object.entries(inputObj)[0];
const itemInfo = this.getItem(itemId);
return {
itemId,
quantity,
...itemInfo,
};
}),
};
}
getDungeon(id) {
const dungeon = this.dungeons.get(id);
if (!dungeon) return null;
return {
...dungeon,
displayName: this.t(dungeon.displayName),
description: this.t(dungeon.description),
};
}
getRoom(id) {
const room = this.rooms.get(id);
if (!room) return null;
return {
...room,
displayName: this.t(room.displayName),
description: this.t(room.description),
};
}
getQuest(id) {
const quest = this.quests.get(id);
if (!quest) return null;
return {
...quest,
displayName: this.t(quest.displayName),
description: this.t(quest.description),
objectives: (quest.objectives || []).map((obj) => ({
...obj,
description: obj.description ? this.t(obj.description) : "",
})),
};
}
getAllQuests() {
return Array.from(this.quests.values()).map((q) => this.getQuest(q.id));
}
setLanguage(langCode) {
if (this.translations[langCode]) {
this.currentLang = langCode;
}
}
}
const instance = new GameDataManager();
export default instance;

View File

@ -1,46 +0,0 @@
class PlayerManager {
constructor() {
this.onlinePlayers = [];
this.offlinePlayers = [];
this.listeners = new Set();
}
setInitialState(online, offline) {
this.onlinePlayers = online || [];
this.offlinePlayers = offline || [];
this.notify();
}
handlePlayerJoined(username) {
if (!this.onlinePlayers.includes(username)) {
this.onlinePlayers.push(username);
this.offlinePlayers = this.offlinePlayers.filter((u) => u !== username);
this.notify();
}
}
handlePlayerLeft(username) {
this.onlinePlayers = this.onlinePlayers.filter((u) => u !== username);
if (!this.offlinePlayers.includes(username)) {
this.offlinePlayers.push(username);
}
this.notify();
}
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
notify() {
this.listeners.forEach((listener) =>
listener({
online: this.onlinePlayers,
offline: this.offlinePlayers,
}),
);
}
}
export default new PlayerManager();

View File

@ -1,10 +0,0 @@
import { io } from 'socket.io-client';
const SOCKET_URL = 'http://localhost:3000';
export const socket = io(SOCKET_URL, {
autoConnect: false,
auth: {
token: localStorage.getItem('token')
}
});

View File

@ -1,324 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
.dash-container {
padding: 15px;
position: relative;
overflow-y: auto;
overflow-x: hidden;
height: 100%;
box-sizing: border-box;
scrollbar-gutter: stable;
}
.dash-scanline {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: rgba(0, 212, 255, 0.05);
animation: scan 6s linear infinite;
pointer-events: none;
z-index: 1;
}
@keyframes scan {
0% {
top: 0;
}
100% {
top: 100%;
}
}
.dashboard-grid {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
max-width: 1400px;
margin: 0 auto;
position: relative;
z-index: 2;
width: 100%;
box-sizing: border-box;
align-items: start;
overflow: hidden;
}
.dash-card {
background: rgba(10, 15, 24, 0.95) !important;
border: 1px solid #1a2638 !important;
border-radius: 2px !important;
position: relative;
padding: 20px !important;
box-sizing: border-box;
overflow: hidden;
contain: paint;
}
.card-tag {
position: absolute;
top: 0;
right: 0;
background: #1a2638;
color: #00d4ff;
font-size: 8px;
padding: 2px 6px;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 1px;
}
.pilot-info {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 15px;
align-items: flex-start;
}
.pilot-avatar {
width: 50px;
height: 50px;
background: #05080c;
border: 1px solid #00d4ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
position: relative;
color: #00d4ff;
flex-shrink: 0;
}
.level-badge {
position: absolute;
bottom: -3px;
right: -3px;
background: #00d4ff;
color: #000;
font-size: 9px;
padding: 1px 4px;
font-weight: 900;
}
.pilot-details {
flex: 1;
min-width: 0;
width: 100%;
}
.pilot-details h3 {
margin: 0 0 5px 0;
font-size: 1rem;
color: #fff;
word-break: break-all;
overflow-wrap: break-word;
}
.status-online {
font-size: 10px;
color: #4a5d75;
margin: 0;
}
.exp-bar-container {
margin-top: 10px;
}
.exp-labels {
display: flex;
justify-content: space-between;
font-size: 9px;
color: #4a5d75;
margin-bottom: 3px;
}
.exp-track {
height: 3px;
background: #05080c;
border: 1px solid #1a2638;
}
.exp-fill {
height: 100%;
background: linear-gradient(90deg, #00d4ff, #00ff88);
box-shadow: 0 0 8px rgba(0, 212, 255, 0.4);
}
.resource-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.res-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
background: rgba(0, 0, 0, 0.4);
border-left: 2px solid #00d4ff;
position: relative;
contain: paint;
}
.res-content {
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
}
.res-label {
font-size: 9px;
color: #4a5d75;
text-transform: uppercase;
}
.res-val {
font-size: 1rem;
font-weight: 900;
color: #fff;
font-family: "Space Mono", monospace;
position: relative;
padding-right: 25px;
}
.credit-plus {
position: absolute;
top: 0;
right: 0;
color: #00ff88;
font-size: 0.8rem;
font-weight: 900;
pointer-events: none;
animation: floatUp 1.2s ease-out forwards;
}
@keyframes floatUp {
0% {
transform: translateY(5px);
opacity: 0;
}
20% {
opacity: 1;
}
100% {
transform: translateY(-15px);
opacity: 0;
}
}
.diag-grid {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
}
.diag-item {
background: rgba(0, 0, 0, 0.2);
padding: 8px 12px;
border: 1px solid #1a2638;
display: flex;
justify-content: space-between;
align-items: center;
}
.diag-status {
font-size: 10px;
color: #00ff88;
font-weight: bold;
}
.diag-item.warning {
border-color: rgba(255, 170, 0, 0.5);
}
.diag-item.warning .diag-status {
color: #ffaa00;
}
.diag-status.online {
text-shadow: 0 0 5px #00ff88;
}
@media (min-width: 600px) {
.dashboard-grid {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.dash-container {
padding: 20px;
}
}
@media (min-width: 992px) {
.dashboard-grid {
grid-template-columns: repeat(3, 1fr);
}
.pilot-info {
flex-direction: row;
align-items: center;
}
.pilot-details h3 {
font-size: 1.1rem;
word-break: normal;
}
.diag-grid {
grid-template-columns: 1fr 1fr;
}
}
*::-webkit-scrollbar {
width: 0px;
background: transparent;
}
* {
scrollbar-width: none;
-ms-overflow-style: none;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +0,0 @@
: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);
}
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%
);
}
*::-webkit-scrollbar {
width: 0px;
background: transparent;
}
* {
scrollbar-width: none;
-ms-overflow-style: none;
}

View File

@ -1,161 +0,0 @@
/* Galaxy Strike Online - Main Styles */
* {
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);
}
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%
);
}
@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;
}
}
/* Animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
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;
}
}
@media (max-width: 480px) {
.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;
}

View File

@ -1,842 +0,0 @@
/* Table Styles for Galaxy Strike Online */
/* Base Table Styles */
.dungeon-table,
.skills-table,
.base-rooms-table,
.base-upgrades-table,
.ship-gallery-table,
.starbase-management-table,
.starbase-shop-table,
.quests-table,
.inventory-table,
.shop-table {
width: 100%;
border-collapse: collapse;
background: var(--card-bg);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
margin: 10px 0;
}
.dungeon-table th,
.skills-table th,
.base-rooms-table th,
.base-upgrades-table th,
.ship-gallery-table th,
.starbase-management-table th,
.starbase-shop-table th,
.quests-table th,
.inventory-table th,
.shop-table th {
background: var(--gradient-primary);
color: var(--text-primary);
padding: 12px 15px;
text-align: left;
font-weight: 600;
font-size: 14px;
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
}
.dungeon-table td,
.skills-table td,
.base-rooms-table td,
.base-upgrades-table td,
.ship-gallery-table td,
.starbase-management-table td,
.starbase-shop-table td,
.quests-table td,
.inventory-table td,
.shop-table td {
padding: 12px 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
color: #e0e0e0;
font-size: 14px;
}
.dungeon-table tr:hover,
.skills-table tr:hover,
.base-rooms-table tr:hover,
.base-upgrades-table tr:hover,
.ship-gallery-table tr:hover,
.starbase-management-table tr:hover,
.starbase-shop-table tr:hover,
.quests-table tr:hover,
.inventory-table tr:hover,
.shop-table tr:hover {
background: rgba(102, 126, 234, 0.1);
transition: background 0.3s ease;
}
/* Dungeon Table Specific */
.dungeon-table .difficulty-easy { color: #00ff00; }
.dungeon-table .difficulty-medium { color: #ffff00; }
.dungeon-table .difficulty-hard { color: #ff9900; }
.dungeon-table .difficulty-extreme { color: #ff0000; }
/* Skills Table Specific */
.skills-table .skill-level {
font-weight: bold;
color: #667eea;
}
.skills-table .skill-progress {
width: 100px;
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
}
.skills-table .progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
transition: width 0.3s ease;
}
/* Base Tables Specific */
.base-rooms-table .room-status-active { color: #00ff00; }
.base-rooms-table .room-status-inactive { color: #ff0000; }
.base-rooms-table .room-status-upgrading { color: #ffff00; }
.base-upgrades-table .upgrade-level {
font-weight: bold;
color: #667eea;
}
/* Ship Gallery Grid Specific */
.ship-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
padding: 20px 0;
}
.ship-card {
background: var(--card-bg);
border-radius: 12px;
padding: 15px;
border: 2px solid var(--primary-color);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
text-align: center;
}
.ship-card:hover {
transform: translateY(-5px);
border-color: var(--primary-color);
box-shadow: 0 8px 30px rgba(0, 212, 255, 0.4);
}
.ship-card.active {
border-color: var(--success-color);
box-shadow: 0 8px 30px rgba(0, 255, 136, 0.2);
}
.ship-card.active::before {
content: "ACTIVE";
position: absolute;
top: 10px;
right: 10px;
background: var(--gradient-secondary);
color: var(--text-primary);
padding: 4px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.ship-card-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.ship-card-image {
width: 80px;
height: 80px;
border-radius: 8px;
object-fit: cover;
border: 2px solid var(--primary-color);
margin: 0 auto;
}
.ship-card-info {
text-align: center;
}
.ship-card-rarity {
color: var(--text-secondary);
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 4px 8px;
border-radius: 4px;
background: var(--hover-bg);
border: 1px solid var(--border-color);
}
.ship-card-rarity.common {
color: #888;
border-color: #888;
}
.ship-card-rarity.rare {
color: var(--primary-color);
border-color: var(--primary-color);
}
.ship-card-rarity.epic {
color: var(--accent-color);
border-color: var(--accent-color);
}
.ship-card-rarity.legendary {
color: var(--warning-color);
border-color: var(--warning-color);
}
.ship-card-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 15px;
}
.ship-card-stat {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.ship-card-stat .stat-label {
color: var(--text-muted);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.ship-card-stat .stat-value {
color: var(--text-secondary);
font-weight: bold;
font-size: 12px;
}
.ship-card-stat .stat-value.health {
color: var(--success-color);
}
.ship-card-stat .stat-value.attack {
color: var(--warning-color);
}
.ship-card-stat .stat-value.defense {
color: var(--accent-color);
}
.ship-card-stat .stat-value.speed {
color: var(--secondary-color);
}
.ship-card-actions {
display: flex;
gap: 10px;
justify-content: space-between;
}
.ship-card-actions .btn-action {
flex: 1;
padding: 8px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: all 0.3s ease;
text-transform: uppercase;
text-align: center;
}
.btn-action.btn-switch {
background: var(--gradient-primary);
color: var(--text-primary);
}
.btn-action.btn-switch:hover {
background: var(--gradient-secondary);
transform: translateY(-2px);
}
.btn-action.btn-switch:disabled {
background: var(--text-muted);
cursor: not-allowed;
transform: none;
}
.btn-action.btn-upgrade {
background: var(--gradient-secondary);
color: var(--text-primary);
}
.btn-action.btn-upgrade:hover {
background: var(--gradient-primary);
transform: translateY(-2px);
}
.btn-action.btn-repair {
background: var(--gradient-secondary);
color: var(--text-primary);
}
.btn-action.btn-repair:hover {
background: var(--gradient-primary);
transform: translateY(-2px);
}
/* Ship Gallery Layout */
.ship-layout {
display: flex;
gap: 30px;
margin-top: 20px;
}
.current-ship-section {
flex: 0 0 400px;
background: var(--card-bg);
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
border: 2px solid var(--primary-color);
}
.ship-grid-section {
flex: 1;
min-width: 0; /* Prevent flex item from overflowing */
}
.ship-grid-section h4 {
color: var(--primary-color);
margin-bottom: 20px;
font-size: 16px;
text-transform: uppercase;
letter-spacing: 1px;
}
.current-ship-section h4 {
color: var(--primary-color);
margin-bottom: 15px;
font-size: 18px;
text-transform: uppercase;
letter-spacing: 1px;
text-align: center;
}
/* Current Ship Display */
.current-ship-display {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
text-align: center;
}
.current-ship-image {
flex-shrink: 0;
order: 1;
}
.current-ship-image img {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: 8px;
border: 2px solid var(--primary-color);
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
}
.current-ship-details {
flex: 1;
order: 2;
min-width: 0;
text-align: center;
}
.current-ship-details h5 {
color: var(--text-primary);
margin-bottom: 15px;
font-size: 20px;
font-weight: bold;
text-align: center;
}
.current-ship-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.ship-stat {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: var(--hover-bg);
border-radius: 4px;
border: 1px solid var(--border-color);
}
.ship-stat .stat-label {
color: var(--text-muted);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.ship-stat .stat-value {
color: var(--text-secondary);
font-weight: bold;
font-size: 14px;
}
.ship-stat .stat-value.health {
color: var(--success-color);
}
.ship-stat .stat-value.attack {
color: var(--warning-color);
}
.ship-stat .stat-value.defense {
color: var(--accent-color);
}
.ship-stat .stat-value.speed {
color: var(--secondary-color);
}
.ship-table-section {
margin-top: 30px;
}
.ship-table-section h4 {
color: #667eea;
margin-bottom: 15px;
font-size: 16px;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Responsive Design */
@media (max-width: 768px) {
.ship-layout {
flex-direction: column;
gap: 20px;
}
.current-ship-section {
flex: 1;
width: 100%;
}
.current-ship-display {
flex-direction: row;
align-items: flex-start;
gap: 15px;
}
.current-ship-image img {
width: 100px;
height: 100px;
}
.current-ship-stats {
grid-template-columns: 1fr;
}
.ship-grid {
grid-template-columns: 1fr;
gap: 15px;
padding: 15px 0;
}
.ship-card {
padding: 15px;
}
.ship-card-header {
flex-direction: row;
align-items: flex-start;
gap: 12px;
}
.ship-card-image {
width: 80px;
height: 80px;
}
.ship-card-stats {
grid-template-columns: 1fr;
gap: 6px;
}
.ship-card-actions {
flex-direction: column;
gap: 8px;
}
.ship-card-actions .btn-action {
padding: 10px 12px;
font-size: 11px;
}
}
@media (max-width: 480px) {
.ship-layout {
gap: 15px;
}
.current-ship-section {
padding: 15px;
}
.current-ship-display {
flex-direction: column;
align-items: center;
text-align: center;
gap: 15px;
}
.current-ship-image {
order: 1;
}
.current-ship-details {
order: 2;
text-align: center;
}
.ship-grid {
grid-template-columns: 1fr;
gap: 10px;
padding: 10px 0;
}
.ship-card {
padding: 12px;
}
.ship-card-header {
flex-direction: column;
align-items: center;
text-align: center;
gap: 10px;
}
.ship-card-image {
width: 80px;
height: 80px;
margin: 0 auto;
}
.ship-card-info {
text-align: center;
}
.ship-card-name {
font-size: 14px;
}
.ship-card-class {
font-size: 11px;
}
}
/* Console Window Styles */
.console-window {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 600px;
height: 400px;
background: var(--bg-secondary);
border: 2px solid var(--primary-color);
border-radius: 8px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.8);
z-index: 10000;
display: none;
flex-direction: column;
font-family: 'Courier New', monospace;
}
.console-header {
background: var(--gradient-primary);
color: var(--text-primary);
padding: 10px 15px;
border-radius: 6px 6px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
font-size: 14px;
}
.console-close {
background: none;
border: none;
color: var(--text-primary);
font-size: 20px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background 0.3s ease;
}
.console-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.console-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.console-output {
flex: 1;
padding: 15px;
overflow-y: auto;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 12px;
line-height: 1.4;
border-bottom: 1px solid var(--border-color);
}
.console-output .console-line {
margin-bottom: 5px;
word-wrap: break-word;
}
.console-output .console-error {
color: var(--error-color);
}
.console-output .console-success {
color: var(--success-color);
}
.console-output .console-warning {
color: var(--warning-color);
}
.console-output .console-info {
color: var(--primary-color);
}
.console-input-container {
padding: 10px;
background: var(--bg-secondary);
}
.console-input {
width: 100%;
background: var(--bg-primary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 8px 12px;
font-family: 'Courier New', monospace;
font-size: 12px;
border-radius: 4px;
outline: none;
}
.console-input:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.2);
}
/* Starbase Tables Specific */
.starbase-management-table .starbase-level {
font-weight: bold;
color: #667eea;
}
.starbase-shop-table .starbase-cost {
font-weight: bold;
color: #ffd700;
}
/* Quests Table Specific */
.quests-table .quest-type-main { color: #667eea; }
.quests-table .quest-type-daily { color: #00ff00; }
.quests-table .quest-type-procedural { color: #ff9900; }
.quests-table .quest-type-completed { color: #888888; }
.quests-table .quest-type-failed { color: #ff0000; }
.quests-table .quest-progress {
width: 100px;
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
}
.quests-table .progress-fill {
height: 100%;
background: linear-gradient(90deg, #00ff00, #00cc00);
transition: width 0.3s ease;
}
/* Inventory Table Specific */
.inventory-table .item-rarity-common { color: #888888; }
.inventory-table .item-rarity-uncommon { color: #00ff00; }
.inventory-table .item-rarity-rare { color: #0088ff; }
.inventory-table .item-rarity-epic { color: #8833ff; }
.inventory-table .item-rarity-legendary { color: #ff8800; }
.inventory-table .item-stats {
font-size: 12px;
color: #cccccc;
}
/* Shop Table Specific */
.shop-table .item-price {
font-weight: bold;
color: #ffd700;
}
.shop-table .item-description {
font-size: 12px;
color: #cccccc;
max-width: 200px;
}
/* Action Buttons */
.dungeon-table .btn-action,
.skills-table .btn-action,
.base-rooms-table .btn-action,
.base-upgrades-table .btn-action,
.ship-gallery-table .btn-action,
.starbase-management-table .btn-action,
.starbase-shop-table .btn-action,
.quests-table .btn-action,
.inventory-table .btn-action,
.shop-table .btn-action {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: all 0.3s ease;
text-transform: uppercase;
}
.btn-action.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-action.btn-primary:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
transform: translateY(-1px);
}
.btn-action.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-action.btn-secondary:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.btn-action.btn-success {
background: linear-gradient(135deg, #00ff00 0%, #00cc00 100%);
color: white;
}
.btn-action.btn-success:hover {
background: linear-gradient(135deg, #00cc00 0%, #00ff00 100%);
transform: translateY(-1px);
}
.btn-action.btn-danger {
background: linear-gradient(135deg, #ff0000 0%, #cc0000 100%);
color: white;
}
.btn-action.btn-danger:hover {
background: linear-gradient(135deg, #cc0000 0%, #ff0000 100%);
transform: translateY(-1px);
}
/* Responsive Design */
@media (max-width: 768px) {
.dungeon-table,
.skills-table,
.base-rooms-table,
.base-upgrades-table,
.ship-gallery-table,
.starbase-management-table,
.starbase-shop-table,
.quests-table,
.inventory-table,
.shop-table {
font-size: 12px;
}
.dungeon-table th,
.skills-table th,
.base-rooms-table th,
.base-upgrades-table th,
.ship-gallery-table th,
.starbase-management-table th,
.starbase-shop-table th,
.quests-table th,
.inventory-table th,
.shop-table th {
padding: 8px 10px;
font-size: 12px;
}
.dungeon-table td,
.skills-table td,
.base-rooms-table td,
.base-upgrades-table td,
.ship-gallery-table td,
.starbase-management-table td,
.starbase-shop-table td,
.quests-table td,
.inventory-table td,
.shop-table td {
padding: 8px 10px;
font-size: 12px;
}
.btn-action {
padding: 4px 8px;
font-size: 10px;
}
}

View File

@ -1,33 +0,0 @@
.game-interface {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.hidden {
display: none !important;
}
.main-content {
flex: 1;
overflow: hidden; /* Remove exterior scrollbar */
padding: 1rem;
background: var(--bg-primary);
height: calc(100vh - 32px); /* Account for custom title bar */
}
.tab-content {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}

View File

@ -1,98 +0,0 @@
import React, { useEffect, useState } from "react";
import "./GameInterface.css";
import GameHeader from "./components/GameHeader";
import Navigation from "./components/Navigation";
import Console from "../../components/Console/Console.jsx";
import DashboardTab from "./tabs/DashboardTab";
import InventoryTab from "./tabs/InventoryTab";
import DungeonsTab from "./tabs/DungeonsTab";
import SkillsTab from "./tabs/SkillsTab";
import BaseTab from "./tabs/BaseTab";
import QuestsTab from "./tabs/QuestsTab";
import ShopTab from "./tabs/ShopTab";
import CraftingTab from "./tabs/CraftingTab";
import ChatTab from "./tabs/ChatTab";
import Notification from "./tabs/NotificationTab.jsx";
import DungeonScreen from "./components/DungeonScreen";
import { useSocket } from "../../hooks/useSocket.js";
import DatapackTab from "./tabs/DatapackTab.jsx";
const GameInterface = ({ onExit }) => {
const [activeTab, setActiveTab] = useState("dashboard");
const [activeDungeonSession, setActiveDungeonSession] = useState(null);
const { socket } = useSocket();
useEffect(() => {
if (!socket) return;
socket.on("dungeon:started", (sessionData) => {
setActiveDungeonSession(sessionData);
});
socket.on("dungeon:completed", (results) => {
setActiveDungeonSession(null);
});
socket.on("error", (err) => {
alert(`SYSTEM_ERROR: ${err.message}`);
});
return () => {
socket.off("dungeon:started");
socket.off("dungeon:completed");
socket.off("error");
};
}, [socket]);
const handleStartDungeon = (dungeonId) => {
socket.emit("dungeon:start", { dungeonId });
};
const handleAbortMission = () => {
if (
window.confirm(
"ARE YOU SURE YOU WANT TO ABORT? Energy will not be refunded.",
)
) {
setActiveDungeonSession(null);
}
};
if (activeDungeonSession) {
return (
<DungeonScreen
session={activeDungeonSession}
socket={socket}
onExit={handleAbortMission}
/>
);
}
const tabs = {
dashboard: <DashboardTab />,
dungeons: <DungeonsTab startDungeon={handleStartDungeon} />,
skills: <SkillsTab />,
base: <BaseTab />,
quests: <QuestsTab />,
inventory: <InventoryTab />,
shop: <ShopTab />,
crafting: <CraftingTab />,
datapack: <DatapackTab />,
chat: <ChatTab />,
notifications: <Notification />,
};
return (
<div id="gameInterface" className="game-interface">
<GameHeader onReturn={onExit} />
<Navigation activeTab={activeTab} onTabChange={setActiveTab} />
<main className="main-content">
{tabs[activeTab] || <DashboardTab />}
</main>
<Console />
</div>
);
};
export default GameInterface;

View File

@ -1,54 +0,0 @@
.category-selector {
display: flex;
gap: 12px;
margin-bottom: 20px;
padding: 10px 5px;
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
}
.category-selector::-webkit-scrollbar {
display: none;
}
.category-btn {
flex: 0 0 auto;
padding: 10px 22px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: #a0a0a0;
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;
}
.category-btn:hover {
background: rgba(0, 204, 255, 0.08);
border-color: #00ccff;
color: #fff;
}
.category-btn.active {
background: #00ccff;
border-color: #00ccff;
color: #000;
font-weight: 700;
box-shadow: 0 0 15px rgba(0, 204, 255, 0.3);
}
@media (max-width: 768px) {
.category-selector {
gap: 8px;
}
.category-btn {
padding: 8px 16px;
font-size: 0.75rem;
}
}

View File

@ -1,20 +0,0 @@
import React from "react";
import "./CategorySelector.css";
const CategorySelector = ({ categories, activeCategory, onCategoryChange }) => {
return (
<div className="category-selector">
{categories.map((cat) => (
<button
key={cat.id}
className={`category-btn ${activeCategory === cat.id ? "active" : ""}`}
onClick={() => onCategoryChange(cat.id)}
>
{cat.displayName}
</button>
))}
</div>
);
};
export default CategorySelector;

View File

@ -1,349 +0,0 @@
.dungeon-active-screen {
display: flex;
flex-direction: column;
height: 100vh;
background: radial-gradient(circle at center, #0a1118 0%, #05080c 100%);
padding: 30px;
gap: 20px;
font-family: "Space Mono", monospace;
color: #e0e6ed;
box-sizing: border-box;
overflow: hidden;
}
.dungeon-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
padding-bottom: 15px;
flex-shrink: 0;
}
.turn-progress-container {
width: 200px;
background: rgba(0, 0, 0, 0.5);
padding: 8px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.turn-label {
font-size: 9px;
font-family: "Orbitron", sans-serif;
letter-spacing: 2px;
margin-bottom: 5px;
text-align: center;
}
.turn-timer-bar {
height: 4px;
background: rgba(255, 255, 255, 0.05);
overflow: hidden;
}
.turn-timer-fill {
height: 100%;
}
.player-phase .turn-label {
color: #00d2ff;
}
.player-phase .turn-timer-fill {
background: #00d2ff;
box-shadow: 0 0 10px #00d2ff;
}
.enemy-phase .turn-label {
color: #ff4444;
}
.enemy-phase .turn-timer-fill {
background: #ff4444;
box-shadow: 0 0 10px #ff4444;
}
.battle-arena {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
}
.mobs-grid {
display: flex;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
}
.enemy-card {
width: 180px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
transition: all 0.3s;
position: relative;
}
.enemy-card.attacking {
border-color: #ff4444;
transform: scale(1.05);
box-shadow: 0 0 20px rgba(255, 68, 68, 0.4);
z-index: 10;
}
.enemy-action-progress {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: rgba(255, 0, 0, 0.2);
}
.inner-progress {
height: 100%;
background: #ff4444;
width: 0%;
animation: enemyCharge 2s linear forwards;
}
@keyframes enemyCharge {
from {
width: 0%;
}
to {
width: 100%;
}
}
.enemy-card.targetable:hover {
border-color: #00d2ff;
background: rgba(0, 210, 255, 0.05);
cursor: crosshair;
}
.enemy-hp-mini {
width: 100%;
height: 4px;
background: #000;
margin-bottom: 15px;
}
.enemy-hp-mini .fill {
height: 100%;
background: #ff4444;
transition: width 0.3s;
}
.player-section {
display: grid;
grid-template-columns: 300px 1fr;
gap: 20px;
height: 150px;
flex-shrink: 0;
}
.player-hp-main {
background: rgba(0, 0, 0, 0.3);
padding: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.2s;
}
.player-hp-main.taking-damage {
animation: playerShake 0.3s infinite;
border-color: #ff4444;
}
@keyframes playerShake {
0% {
transform: translate(1px, 1px);
}
25% {
transform: translate(-2px, -1px);
}
50% {
transform: translate(-1px, 2px);
}
75% {
transform: translate(2px, 1px);
}
100% {
transform: translate(0, 0);
}
}
.hp-bar-large {
height: 20px;
background: #000;
margin-top: 10px;
}
.hp-bar-large .fill {
height: 100%;
background: linear-gradient(90deg, #ff416c, #ff4b2b);
transition: width 0.4s ease-out;
}
.combat-log-wrapper {
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(0, 212, 255, 0.1);
overflow: hidden;
}
.combat-log {
padding: 15px;
height: 100%;
overflow-y: auto;
font-size: 0.75rem;
}
.log-entry {
margin-bottom: 5px;
color: #a0acba;
}
.log-arrow {
color: #00d2ff;
margin-right: 5px;
}
.ctrl-btn {
width: 100%;
height: 60px;
background: #00d4ff;
border: none;
font-family: "Orbitron", sans-serif;
font-weight: 900;
cursor: pointer;
clip-path: polygon(10px 0%, 100% 0%, calc(100% - 10px) 100%, 0% 100%);
}
.enemy-card.defeated {
filter: grayscale(1) brightness(0.4);
}
.enemy-card.targetable {
cursor: pointer;
pointer-events: all;
}
.hp-bar-large .fill,
.enemy-hp-mini .fill {
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Рамка для вибраного моба */
.enemy-card.selected {
border-color: #00d2ff;
box-shadow:
0 0 15px rgba(0, 210, 255, 0.4),
inset 0 0 10px rgba(0, 210, 255, 0.2);
transform: translateY(-5px);
}
.enemy-card.selectable:hover:not(.defeated) {
cursor: crosshair;
border-color: rgba(0, 210, 255, 0.5);
}
/* Іконка прицілу */
.target-aim {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 40px;
color: rgba(0, 210, 255, 0.3);
pointer-events: none;
z-index: 10;
animation: pulseAim 1.5s infinite;
}
@keyframes pulseAim {
0% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.3;
}
50% {
transform: translate(-50%, -50%) scale(1.1);
opacity: 0.6;
}
100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.3;
}
}
/* Контейнер для логу та кнопки */
.combat-interface-row {
display: flex;
gap: 15px;
height: 120px;
margin-top: 10px;
}
.combat-log-wrapper {
flex: 1;
}
/* Стильна кнопка атаки */
.btn-execute-combat {
width: 180px;
background: rgba(255, 0, 60, 0.1);
border: 1px solid #ff003c;
color: #ff003c;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
font-family: "Orbitron", sans-serif;
transition: all 0.2s;
position: relative;
overflow: hidden;
}
.btn-execute-combat:hover:not(.disabled) {
background: #ff003c;
color: #000;
box-shadow: 0 0 20px rgba(255, 0, 60, 0.4);
}
.btn-execute-combat.disabled {
opacity: 0.3;
border-color: #444;
color: #444;
cursor: not-allowed;
background: transparent;
}
.btn-glitch-content {
font-size: 1rem;
font-weight: bold;
letter-spacing: 1px;
}
.btn-sub-text {
font-size: 0.6rem;
margin-top: 4px;
opacity: 0.8;
}
.mob-stats-display {
display: flex;
gap: 8px;
font-size: 0.65rem;
margin-top: 5px;
color: rgba(255, 255, 255, 0.6);
}
.player-stats-row {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 0.8rem;
color: #00d2ff;
font-family: "Orbitron", sans-serif;
border-top: 1px solid rgba(0, 210, 255, 0.2);
padding-top: 5px;
}

View File

@ -1,269 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import GameDataManager from "../../../services/GameDataManager.js";
import "./DungeonScreen.css";
import DungeonFinish from "../tabs/components/DungeonFinish.jsx";
const DungeonScreen = ({ session, socket }) => {
const [battle, setBattle] = useState(session.battle || null);
const [roomIndex, setRoomIndex] = useState(session.roomIndex);
const [totalRooms, setTotalRooms] = useState(session.totalRooms || 1);
const [timeLeft, setTimeLeft] = useState(10);
const [summary, setSummary] = useState(null);
const [activeAttacker, setActiveAttacker] = useState(null);
const [selectedTarget, setSelectedTarget] = useState(null);
const [log, setLog] = useState([
"SYSTEM: Neural link established. Scanning sector...",
]);
const logEndRef = useRef(null);
const timerRef = useRef(null);
const addLog = (text) => {
const time = new Date().toLocaleTimeString([], {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
setLog((prev) => [...prev, `[${time}] ${text}`]);
};
useEffect(() => {
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [log]);
useEffect(() => {
if (!battle || battle.isOver || activeAttacker) {
if (timerRef.current) clearInterval(timerRef.current);
return;
}
const isPlayer = battle.turnOrder[battle.currentTurnIndex] === "player";
if (!isPlayer) return;
setTimeLeft(10);
if (timerRef.current) clearInterval(timerRef.current);
timerRef.current = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
handleCombatAction(null);
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timerRef.current);
}, [battle?.currentTurnIndex, battle?.isOver, activeAttacker]);
useEffect(() => {
socket.on("dungeon:battle_update", async (data) => {
if (data.log && Array.isArray(data.log)) {
for (const action of data.log) {
if (typeof action === "object" && action.attackerId) {
setActiveAttacker(action.attackerId);
action.messages?.forEach((msg) => addLog(msg));
if (action.hpState) {
setBattle((prev) => ({
...prev,
player: { ...prev.player, hp: action.hpState.playerHp },
enemies: prev.enemies.map((e) => {
const s = action.hpState.enemies.find(
(ae) => ae.id === e.instanceId,
);
return s ? { ...e, hp: s.hp, isDead: s.isDead } : e;
}),
}));
}
await new Promise((r) => setTimeout(r, 1500));
} else if (typeof action === "string") {
addLog(action);
}
}
}
setBattle(data.battle);
setActiveAttacker(null);
setSelectedTarget(null);
if (data.status === "victory")
addLog("MISSION_OBJECTIVE: Threats neutralized.");
if (data.status === "defeat") {
addLog("CRITICAL_ERROR: Bio-sign lost.");
setTimeout(() => window.location.reload(), 3000);
}
});
socket.on("dungeon:room_update", (data) => {
setRoomIndex(data.roomIndex);
setTotalRooms(data.totalRooms);
setBattle(data.battle);
addLog(`--- ENTERING SECTOR ${data.roomIndex + 1} ---`);
});
socket.on("dungeon:completed", (data) => {
setSummary(data.rewards);
addLog("MISSION_SUCCESS: All objectives secured.");
});
return () => {
socket.off("dungeon:battle_update");
socket.off("dungeon:room_update");
socket.off("dungeon:completed");
};
}, [socket]);
const handleCombatAction = (targetId = selectedTarget) => {
const isPlayer = battle?.turnOrder[battle?.currentTurnIndex] === "player";
if (!battle || battle.isOver || activeAttacker || !isPlayer) return;
socket.emit("dungeon:combat_action", { targetInstanceId: targetId });
if (!targetId) addLog("Sequence timeout! Skipping...");
else addLog("Initiating strike sequence...");
setSelectedTarget(null);
};
const handleNextRoom = () => {
socket.emit("dungeon:next_room");
};
const isPlayerTurn =
battle?.turnOrder[battle?.currentTurnIndex] === "player" &&
!activeAttacker &&
!battle.isOver;
return (
<div className="dungeon-active-screen">
{summary && (
<DungeonFinish
rewards={summary}
onExit={() => window.location.reload()}
/>
)}
<div className="dungeon-header">
<div className="room-progress">
<div className="progress-text">
SECTOR {roomIndex + 1} / {totalRooms}
</div>
<div className="progress-bar">
<div
className="fill"
style={{ width: `${((roomIndex + 1) / totalRooms) * 100}%` }}
></div>
</div>
</div>
{battle && !battle.isOver && (
<div
className={`turn-progress-container ${isPlayerTurn ? "player-phase" : "enemy-phase"}`}
>
<div className="turn-label">
{isPlayerTurn ? "YOUR TURN" : "PROCESSING..."}
</div>
<div className="turn-timer-bar">
<div
className="turn-timer-fill"
style={{
width: `${(timeLeft / 10) * 100}%`,
transition: timeLeft === 10 ? "none" : "width 1s linear",
visibility: isPlayerTurn ? "visible" : "hidden",
}}
/>
</div>
</div>
)}
</div>
<div className="battle-arena">
{battle ? (
<div className="mobs-grid">
{battle.enemies.map((mob) => (
<div
key={mob.instanceId}
className={`enemy-card ${mob.isDead ? "defeated" : ""} ${selectedTarget === mob.instanceId ? "selected" : ""} ${isPlayerTurn && !mob.isDead ? "selectable" : ""} ${activeAttacker === mob.instanceId ? "attacking" : ""}`}
onClick={() =>
isPlayerTurn &&
!mob.isDead &&
setSelectedTarget(mob.instanceId)
}
>
<div className="enemy-hp-mini">
<div
className="fill"
style={{ width: `${(mob.hp / mob.maxHp) * 100}%` }}
/>
</div>
<div className="enemy-icon">
<i
className={`fas ${mob.isDead ? "fa-skull-crossbones" : "fa-robot"}`}
></i>
</div>
<span className="mob-name">{GameDataManager.t(mob.name)}</span>
</div>
))}
</div>
) : (
<div className="empty-room">
<i className="fas fa-satellite-dish"></i>
<p>AREA SECURE</p>
</div>
)}
</div>
<div className="player-section">
{battle && (
<div
className={`player-hp-main ${activeAttacker && activeAttacker !== "player" ? "taking-damage" : ""}`}
>
<div className="hp-header">
<span>COMMANDER_INTEGRITY</span>
<span>
{battle.player.hp} / {battle.player.maxHp}
</span>
</div>
<div className="hp-bar-large">
<div
className="fill"
style={{
width: `${(battle.player.hp / battle.player.maxHp) * 100}%`,
}}
/>
</div>
</div>
)}
<div className="combat-interface-row">
<div className="combat-log custom-scroll">
{log.map((entry, i) => (
<div key={i} className="log-entry">
<span className="log-arrow">&gt;</span> {entry}
</div>
))}
<div ref={logEndRef} />
</div>
{battle && !battle.isOver && (
<button
className={`btn-execute-combat ${!selectedTarget || !isPlayerTurn ? "disabled" : ""}`}
disabled={!selectedTarget || !isPlayerTurn}
onClick={() => handleCombatAction()}
>
EXECUTE_STRIKE
</button>
)}
</div>
</div>
<div className="dungeon-controls">
{((battle?.isOver && battle.player.hp > 0) || !battle) && !summary && (
<button className="ctrl-btn next" onClick={handleNextRoom}>
PROCEED <i className="fas fa-chevron-right"></i>
</button>
)}
</div>
</div>
);
};
export default DungeonScreen;

View File

@ -1,110 +0,0 @@
.game-header {
height: 60px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
backdrop-filter: blur(10px);
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.logo {
font-family: "Orbitron", sans-serif;
font-size: 1.5rem;
font-weight: 900;
color: var(--primary-color);
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
.player-info {
display: flex;
flex-direction: column;
}
.player-name {
font-weight: 700;
color: var(--text-primary);
}
.player-level {
font-size: 0.8rem;
color: var(--primary-color);
}
.header-center {
flex: 1;
display: flex;
justify-content: center;
}
.resources {
display: flex;
gap: 1.5rem;
}
.resource {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--bg-tertiary);
border-radius: 20px;
border: 1px solid var(--border-color);
transition: all 0.3s ease;
}
.resource:hover {
border-color: var(--primary-color);
box-shadow: 0 0 10px rgba(0, 212, 255, 0.3);
}
.resource i {
color: var(--primary-color);
}
.header-right {
display: flex;
gap: 0.5rem;
}
.header-right {
display: flex;
gap: 0.75rem;
align-items: center;
}
.header-icon-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
color: var(--text-secondary);
width: 40px;
height: 40px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: 1.1rem;
}
.header-icon-btn:hover {
background: rgba(0, 212, 255, 0.1);
border-color: var(--primary-color);
color: var(--primary-color);
box-shadow: 0 0 15px rgba(0, 212, 255, 0.2);
}
.header-icon-btn.exit-btn:hover {
background: rgba(255, 87, 34, 0.1);
border-color: #ff5722;
color: #ff5722;
box-shadow: 0 0 15px rgba(255, 87, 34, 0.2);
}

View File

@ -1,60 +0,0 @@
import React, { useState } from "react";
import { useSocket } from "../../../hooks/useSocket";
import SettingsModal from "./SettingsModal";
import "./GameHeader.css";
const GameHeader = ({ onReturn }) => {
const { disconnectFromServer } = useSocket();
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const handleHomeClick = () => {
localStorage.removeItem("activeServer");
disconnectFromServer();
if (onReturn) {
onReturn();
}
};
return (
<>
<header className="game-header">
<div className="header-left">
<h1 className="logo">GSO</h1>
<div className="player-info">
<span className="player-name">Commander</span>
<span className="player-level">Lv. 1</span>
</div>
</div>
<div className="header-center">
<div className="resources">
<div className="resource">
<i className="fas fa-bolt"></i>
<span>100/100</span>
</div>
</div>
</div>
<div className="header-right">
<button
className="header-icon-btn"
onClick={() => setIsSettingsOpen(true)}
>
<i className="fas fa-cog"></i>
</button>
<button
className="header-icon-btn exit-btn"
onClick={handleHomeClick}
>
<i className="fas fa-home"></i>
</button>
</div>
</header>
{isSettingsOpen && (
<SettingsModal onClose={() => setIsSettingsOpen(false)} />
)}
</>
);
};
export default GameHeader;

View File

@ -1,141 +0,0 @@
.main-nav {
height: 45px;
background: #0a0f18;
border-bottom: 1px solid #1a2638;
display: flex;
align-items: stretch;
padding: 0;
overflow-x: auto;
position: relative;
z-index: 10000;
scrollbar-width: none;
}
.main-nav::-webkit-scrollbar {
display: none;
}
.nav-container {
display: flex;
padding: 0 5px;
gap: 2px;
}
.nav-btn {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0 12px;
margin-right: 10px;
background: transparent;
border: none;
color: #4a5d75;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
position: relative;
font-family: "Orbitron", sans-serif;
}
.nav-btn-content {
display: flex;
align-items: center;
gap: 6px;
}
.nav-btn i {
font-size: 0.9rem;
}
.nav-label {
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.nav-btn.active {
color: #00d4ff;
background: linear-gradient(to bottom, rgba(0, 212, 255, 0.08), transparent);
}
.nav-active-indicator {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #00d4ff;
box-shadow: 0 0 8px #00d4ff;
transform: scaleX(0);
transition: transform 0.2s ease;
}
.nav-btn.active .nav-active-indicator {
transform: scaleX(1);
}
@media (max-width: 768px) {
.main-nav {
height: 42px;
}
.nav-label {
display: none;
}
.nav-btn {
padding: 0 18px;
}
.nav-btn i {
font-size: 1.1rem;
}
}
.icon-wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.nav-badge {
position: absolute;
top: -8px;
right: -8px;
background: #ff3e3e;
color: white;
font-size: 10px;
font-weight: bold;
min-width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
box-shadow: 0 0 10px rgba(255, 62, 62, 0.5);
border: 1px solid #1a1a1a;
animation: pulse-red 2s infinite;
}
@keyframes pulse-red {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(255, 62, 62, 0.7);
}
70% {
transform: scale(1.1);
box-shadow: 0 0 0 5px rgba(255, 62, 62, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(255, 62, 62, 0);
}
}
.nav-btn.notifications.active i {
color: #ffd700;
}

View File

@ -1,82 +0,0 @@
import React, { useState, useEffect } from "react";
import GameDataManager from "../../../services/GameDataManager.js";
import { useSocket } from "../../../hooks/useSocket";
import "./Navigation.css";
const Navigation = ({ activeTab, onTabChange }) => {
const { socket } = useSocket();
const [unreadCount, setUnreadCount] = useState(0);
const tabs = [
{ id: "dashboard", icon: "fa-tachometer-alt" },
{ id: "dungeons", icon: "fa-dungeon" },
{ id: "skills", icon: "fa-graduation-cap" },
{ id: "quests", icon: "fa-store" },
{ id: "inventory", icon: "fa-archive" },
{ id: "shop", icon: "fa-store" },
{ id: "crafting", icon: "fa-hammer" },
{ id: "datapack", icon: "fa-list-ul" },
{ id: "chat", icon: "fa-comments" },
{ id: "notifications", icon: "fa-bell" },
];
useEffect(() => {
if (!socket) return;
const handleNotifyUpdate = (count) => {
setUnreadCount(count);
};
socket.on("notifications:unread_count", handleNotifyUpdate);
socket.on("notification:new", () => {
if (activeTab !== "notifications") {
setUnreadCount((prev) => prev + 1);
}
});
return () => {
socket.off("notifications:unread_count", handleNotifyUpdate);
socket.off("notification:new");
};
}, [socket, activeTab]);
const handleTabClick = (id) => {
if (id === "notifications") setUnreadCount(0);
onTabChange(id);
};
const getLabel = (id) => {
if (id === "itemlist") return "ITEM_LIST";
if (id === "chat") return "CHAT";
if (id === "notifications") return "ALERTS";
return GameDataManager.t(`category.tabs.original.${id}`);
};
return (
<nav className="main-nav">
<div className="nav-container">
{tabs.map((tab) => (
<button
key={tab.id}
className={`nav-btn ${activeTab === tab.id ? "active" : ""} ${tab.id}`}
onClick={() => handleTabClick(tab.id)}
>
<div className="nav-btn-content">
<div className="icon-wrapper">
<i className={`fas ${tab.icon}`}></i>
{tab.id === "notifications" && unreadCount > 0 && (
<span className="nav-badge">{unreadCount}</span>
)}
</div>
<span className="nav-label">{getLabel(tab.id)}</span>
</div>
<div className="nav-active-indicator"></div>
</button>
))}
</div>
</nav>
);
};
export default Navigation;

View File

@ -1,114 +0,0 @@
.settings-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.settings-modal {
background: #0a0e14;
border: 1px solid #00ffff44;
width: 350px;
box-shadow: 0 0 30px rgba(0, 255, 255, 0.1);
font-family: "Rajdhani", sans-serif;
}
.settings-header {
padding: 15px;
border-bottom: 1px solid rgba(0, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(0, 255, 255, 0.03);
}
.settings-title {
color: #00ffff;
font-weight: bold;
letter-spacing: 2px;
font-size: 14px;
}
.close-x {
background: none;
border: none;
color: #666;
font-size: 24px;
cursor: pointer;
}
.settings-body {
padding: 20px;
}
.section-label {
display: block;
color: #444;
font-size: 11px;
margin-bottom: 10px;
letter-spacing: 1px;
}
.lang-selector {
display: flex;
flex-direction: column;
gap: 8px;
}
.lang-option {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.05);
color: #ccc;
padding: 12px;
text-align: left;
cursor: pointer;
display: flex;
align-items: center;
transition: all 0.2s;
}
.lang-option:hover {
background: rgba(0, 255, 255, 0.05);
color: #fff;
}
.lang-option.active {
border-color: #00ffff;
color: #00ffff;
background: rgba(0, 255, 255, 0.08);
}
.lang-indicator {
width: 4px;
height: 4px;
background: currentColor;
margin-right: 12px;
}
.settings-footer {
padding: 15px;
text-align: center;
}
.btn-confirm {
width: 100%;
background: transparent;
border: 1px solid #00ffff66;
color: #00ffff;
padding: 10px;
cursor: pointer;
text-transform: uppercase;
font-size: 12px;
letter-spacing: 1px;
}
.btn-confirm:hover {
background: #00ffff11;
}

View File

@ -1,58 +0,0 @@
import React, { useState } from "react";
import GameDataManager from "../../../services/GameDataManager.js";
import "./SettingsModal.css";
const SettingsModal = ({ onClose }) => {
const [currentLang, setCurrentLang] = useState(GameDataManager.currentLang);
const languages = [
{ code: "en_US", name: "English" },
{ code: "fr_FR", name: "French" },
{ code: "uk_UA", name: "Ukrainian" },
];
const handleLanguageChange = (langCode) => {
GameDataManager.setLanguage(langCode);
localStorage.setItem("selected_lang", langCode);
setCurrentLang(langCode);
};
return (
<div className="settings-overlay" onClick={onClose}>
<div className="settings-modal" onClick={(e) => e.stopPropagation()}>
<div className="settings-header">
<span className="settings-title">SYSTEM_SETTINGS</span>
<button className="close-x" onClick={onClose}>
&times;
</button>
</div>
<div className="settings-body">
<section className="settings-section">
<label className="section-label">LANGUAGE_INTERFACE</label>
<div className="lang-selector">
{languages.map((lang) => (
<button
key={lang.code}
className={`lang-option ${currentLang === lang.code ? "active" : ""}`}
onClick={() => handleLanguageChange(lang.code)}
>
<span className="lang-indicator"></span>
{lang.name}
</button>
))}
</div>
</section>
</div>
<div className="settings-footer">
<button className="btn-confirm" onClick={onClose}>
CLOSE_MENU
</button>
</div>
</div>
</div>
);
};
export default SettingsModal;

View File

@ -1,40 +0,0 @@
import React, { useState } from 'react';
import "./styles/BaseTab.css"
const BaseTab = () => {
const [view, setView] = useState('overview');
return (
<div className="tab-content active">
<div className="base-navigation">
<button className={`base-nav-btn ${view === 'overview' ? 'active' : ''}`} onClick={() => setView('overview')}>Base Overview</button>
<button className={`base-nav-btn ${view === 'visualization' ? 'active' : ''}`} onClick={() => setView('visualization')}>Visualization</button>
<button className={`base-nav-btn ${view === 'ships' ? 'active' : ''}`} onClick={() => setView('ships')}>Ship Gallery</button>
<button className={`base-nav-btn ${view === 'starbases' ? 'active' : ''}`} onClick={() => setView('starbases')}>Starbases</button>
</div>
{view === 'overview' && (
<div className="base-container">
<div className="base-view">
<h3>Base Information</h3>
<div className="base-stats">
<div className="stat-item"><span>Power Usage:</span> <span className="stat-value">0/100</span></div>
<div className="stat-item"><span>Storage:</span> <span className="stat-value">1000</span></div>
</div>
</div>
</div>
)}
{view === 'ships' && (
<div className="ship-gallery-container">
<div className="current-ship-display">
<h4>Current Ship: Starter Cruiser</h4>
<img src="assets/textures/ships/starter_cruiser.png" alt="Ship" style={{ maxWidth: '200px' }} />
</div>
</div>
)}
</div>
);
};
export default BaseTab;

View File

@ -1,310 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import { useSocket } from "../../../hooks/useSocket";
import "./styles/ChatTab.css";
import PlayerManager from "../../../services/PlayerManager";
const ChatTab = () => {
const { socket } = useSocket();
const [activeChat, setActiveChat] = useState("global");
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [messages, setMessages] = useState([]);
const [friends, setFriends] = useState([]);
const [inputValue, setInputValue] = useState("");
const [showSidebar, setShowSidebar] = useState(true);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const [confirmUnfriend, setConfirmUnfriend] = useState(null);
const [onlineList, setOnlineList] = useState(
PlayerManager.onlinePlayers || [],
);
const messagesEndRef = useRef(null);
const activeChatRef = useRef(activeChat);
useEffect(() => {
activeChatRef.current = activeChat;
}, [activeChat]);
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth <= 768;
setIsMobile(mobile);
if (!mobile) setShowSidebar(true);
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
if (!socket) return;
socket.emit("friend:get_list");
if (activeChat === "global") {
socket.emit("chat:get_global_history");
} else {
socket.emit("chat:get_private_history", { friendId: activeChat });
}
}, [socket, activeChat]);
useEffect(() => {
if (!socket) return;
const interval = setInterval(() => {
setOnlineList([...PlayerManager.onlinePlayers]);
}, 3000);
const handleNewMessage = (msg) => {
const currentActive = activeChatRef.current;
const isGlobalMatch = currentActive === "global" && msg.type === "global";
const isPrivateMatch =
currentActive !== "global" &&
msg.type === "private" &&
(msg.senderId === currentActive || msg.receiverId === currentActive);
if (isGlobalMatch || isPrivateMatch) {
setMessages((prev) => {
if (prev.some((m) => m.id === msg.id)) return prev;
return [...prev, msg];
});
}
};
const handleHistory = (history) => setMessages(history);
const handleSearchResults = (results) => setSearchResults(results);
const handleFriendList = (list) => setFriends(list);
socket.on("chat:new_message", handleNewMessage);
socket.on("chat:global_history", handleHistory);
socket.on("chat:private_history", handleHistory);
socket.on("player:search_results", handleSearchResults);
socket.on("friend:list", handleFriendList);
return () => {
clearInterval(interval);
socket.off("chat:new_message", handleNewMessage);
socket.off("chat:global_history", handleHistory);
socket.off("chat:private_history", handleHistory);
socket.off("player:search_results", handleSearchResults);
socket.off("friend:list", handleFriendList);
};
}, [socket]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const handleSearch = (e) => {
const query = e.target.value;
setSearchQuery(query);
if (query.length > 1) {
socket.emit("player:search", { query });
} else {
setSearchResults([]);
}
};
const addFriend = (player) => {
socket.emit("friend:add", { friendId: player.id });
setSearchQuery("");
setSearchResults([]);
};
const removeFriend = () => {
if (confirmUnfriend) {
socket.emit("friend:remove", { friendId: confirmUnfriend.id });
if (activeChat === confirmUnfriend.id) setActiveChat("global");
setConfirmUnfriend(null);
}
};
const sendMessage = () => {
if (!inputValue.trim() || !socket) return;
socket.emit("chat:send_message", {
content: inputValue,
type: activeChat === "global" ? "global" : "private",
receiverId: activeChat === "global" ? null : activeChat,
});
setInputValue("");
};
const selectChat = (id) => {
setActiveChat(id);
if (isMobile) setShowSidebar(false);
};
const getChatName = () => {
if (activeChat === "global") return "GLOBAL_SYSTEM_CHAT";
const friend = friends.find((f) => f.id === activeChat);
return friend ? friend.username : "UNKNOWN_PILOT";
};
const isFriendOnline = (username) => {
return onlineList.includes(username);
};
return (
<div className={`chat-container ${isMobile ? "mobile" : ""}`}>
{confirmUnfriend && (
<div className="modal-overlay">
<div className="modal-content">
<h3>TERMINATE_CONTACT</h3>
<p>
Are you sure you want to remove {confirmUnfriend.username} from
your contacts?
</p>
<div className="modal-actions">
<button className="confirm-btn" onClick={removeFriend}>
CONFIRM
</button>
<button
className="cancel-btn"
onClick={() => setConfirmUnfriend(null)}
>
CANCEL
</button>
</div>
</div>
</div>
)}
<aside
className={`chat-sidebar ${isMobile && !showSidebar ? "hidden" : ""}`}
>
<div className="search-section">
<div className="card-tag">USER_SEARCH</div>
<div className="search-input-wrapper">
<input
type="text"
placeholder="SEARCH_PILOTS..."
value={searchQuery}
onChange={handleSearch}
/>
</div>
{searchResults.length > 0 && (
<div className="search-results-dropdown">
{searchResults.map((player) => (
<div
key={player.id}
className="search-result-item"
onClick={() => addFriend(player)}
>
<span>
{player.username} (LVL {player.level})
</span>
<i className="fas fa-plus"></i>
</div>
))}
</div>
)}
</div>
<div className="chats-list">
<div
className={`chat-item ${activeChat === "global" ? "active" : ""}`}
onClick={() => selectChat("global")}
>
<div className="chat-item-main">
<i className="fas fa-globe"></i>
<span>GLOBAL_CHANNEL</span>
</div>
</div>
<div className="friends-section-label">
<span className="label-text">CONTACTS</span>
<span className="label-line"></span>
</div>
<div className="friends-list">
{friends.map((friend) => (
<div
key={friend.id}
className={`chat-item ${activeChat === friend.id ? "active" : ""}`}
>
<div
className="chat-item-main"
onClick={() => selectChat(friend.id)}
>
<div
className={`status-dot ${isFriendOnline(friend.username) ? "online" : "offline"}`}
></div>
<span>{friend.username}</span>
</div>
<button
className="unfriend-btn"
onClick={(e) => {
e.stopPropagation();
setConfirmUnfriend(friend);
}}
>
<i className="fas fa-user-minus"></i>
</button>
</div>
))}
</div>
</div>
</aside>
<main className={`chat-main ${isMobile && showSidebar ? "hidden" : ""}`}>
<div className="chat-header">
{isMobile && (
<button className="back-btn" onClick={() => setShowSidebar(true)}>
<i className="fas fa-chevron-left"></i>
</button>
)}
<div className="active-chat-info">
<i
className={
activeChat === "global" ? "fas fa-globe" : "fas fa-user"
}
></i>
<h3>{getChatName()}</h3>
</div>
</div>
<div className="chat-messages">
{messages.length === 0 && (
<div className="message system">
<span className="msg-text">
NO_LOGS_FOUND. SECURE_LINE_READY...
</span>
</div>
)}
{messages.map((msg, index) => (
<div
key={msg.id || index}
className={`message ${msg.senderName === "System" ? "system" : ""}`}
>
<span className="msg-time">
[
{new Date(msg.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
]
</span>
<span className="msg-author">{msg.senderName}:</span>
<span className="msg-text">{msg.content}</span>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="chat-input-area">
<input
type="text"
placeholder="TYPE_MESSAGE..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
/>
<button className="send-btn" onClick={sendMessage}>
<i className="fas fa-paper-plane"></i>
</button>
</div>
</main>
</div>
);
};
export default ChatTab;

View File

@ -1,204 +0,0 @@
import React, { useState, useEffect } from "react";
import { useSocket } from "../../../hooks/useSocket";
import GameDataManager from "../../../services/GameDataManager";
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();
const [categories, setCategories] = useState([]);
const [recipes, setRecipes] = useState([]);
const [userInventory, setUserInventory] = useState([]);
const [activeCategory, setActiveCategory] = useState("");
const [selectedRecipe, setSelectedRecipe] = useState(null);
const [activeCraft, setActiveCraft] = useState(null);
useEffect(() => {
const manifestCategories = GameDataManager.getRecipeCategories();
setCategories(manifestCategories);
if (manifestCategories.length > 0 && !activeCategory) {
setActiveCategory(manifestCategories[0].id);
}
}, []);
useEffect(() => {
if (activeCategory) {
const filteredRecipes =
GameDataManager.getRecipesByCategory(activeCategory);
setRecipes(filteredRecipes);
}
}, [activeCategory]);
useEffect(() => {
if (!socket) return;
socket.emit("player:get_inventory");
socket.emit("player:check_active_craft");
const handleInventory = (data) => setUserInventory(data);
const handleCraftStarted = (data) => {
const recipeData = GameDataManager.getRecipe(data.recipeId);
const now = Date.now();
const diff = (data.finishAt - now) / 1000;
if (diff <= 0) {
setActiveCraft(null);
return;
}
setActiveCraft({
recipeId: data.recipeId,
name: recipeData?.displayName || data.recipeId,
finishAt: data.finishAt,
totalTime: data.totalTime || recipeData?.time_seconds || diff,
timeLeft: Math.max(0, Math.ceil(diff)),
});
if (recipeData) {
setSelectedRecipe(recipeData);
}
};
const handleCraftSuccess = () => {
setActiveCraft(null);
setSelectedRecipe(null);
socket.emit("player:get_inventory");
};
socket.on("player:inventory_data", handleInventory);
socket.on("player:craft_started", handleCraftStarted);
socket.on("player:craft_success", handleCraftSuccess);
return () => {
socket.off("player:inventory_data", handleInventory);
socket.off("player:craft_started", handleCraftStarted);
socket.off("player:craft_success", handleCraftSuccess);
};
}, [socket]);
useEffect(() => {
if (!activeCraft) return;
const timer = setInterval(() => {
const now = Date.now();
const diff = Math.max(0, Math.ceil((activeCraft.finishAt - now) / 1000));
if (diff <= 0) {
clearInterval(timer);
setActiveCraft(null);
} else {
setActiveCraft((prev) => (prev ? { ...prev, timeLeft: diff } : null));
}
}, 1000);
return () => clearInterval(timer);
}, [activeCraft?.finishAt]);
const getOwnedAmount = (itemId) => {
const item = userInventory.find((i) => (i.itemId || i.id) === itemId);
return item ? item.quantity : 0;
};
const handleStartCrafting = (recipe) => {
if (activeCraft) return;
socket.emit("player:craft_item", { recipeId: recipe.id });
};
return (
<div
className="tab-content active"
style={{ height: "100%", display: "flex", flexDirection: "column" }}
>
<MeteorRegion className="crafting-container">
{activeCraft && (
<div className="active-craft-panel">
<div className="craft-info">
<span>
<i className="fas fa-hammer fa-spin"></i> Assembling:{" "}
{activeCraft.name}
</span>
<span className="time-left">{activeCraft.timeLeft}s</span>
</div>
<div className="progress-bar-bg">
<div
className="progress-bar-fill"
style={{
width: `${Math.min(100, ((activeCraft.totalTime - activeCraft.timeLeft) / activeCraft.totalTime) * 100)}%`,
}}
></div>
</div>
</div>
)}
<div className="crafting-header">
<h2>
<i className="fas fa-microchip"></i> Fabrication Unit
</h2>
</div>
<CategorySelector
categories={categories}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<div className="crafting-grid">
{recipes.map((recipe) => {
const isThisRecipeCrafting = activeCraft?.recipeId === recipe.id;
const canCraft = recipe.ingredients.every(
(ing) => getOwnedAmount(ing.itemId) >= ing.quantity,
);
return (
<div
key={recipe.id}
className={`recipe-card ${!canCraft && !isThisRecipeCrafting ? "insufficient-resources" : ""} ${isThisRecipeCrafting ? "crafting-active" : ""}`}
onClick={() => setSelectedRecipe(recipe)}
>
<div className="recipe-icon">
{recipe.texture ? (
<img
width={64}
src={`${config.serverUrl}/static/${recipe.texture}`}
alt={recipe.displayName}
/>
) : (
<div className="fallback-icon">{recipe.displayName[0]}</div>
)}
</div>
<div className="recipe-info-main">
<span className="recipe-name">{recipe.displayName}</span>
<div className="recipe-badges">
<span className="badge-time">
<i className="fas fa-clock"></i> {recipe.time_seconds}s
</span>
</div>
</div>
{isThisRecipeCrafting && (
<div className="craft-overlay-mini">
<i className="fas fa-sync fa-spin"></i>
</div>
)}
</div>
);
})}
</div>
</MeteorRegion>
<CraftModal
recipe={selectedRecipe}
onClose={() => !activeCraft && setSelectedRecipe(null)}
onStartCraft={handleStartCrafting}
activeCraft={activeCraft}
getOwnedAmount={getOwnedAmount}
/>
</div>
);
};
export default CraftingTab;

View File

@ -1,154 +0,0 @@
import React, { useEffect, useState } from "react";
import Card from "../../../components/ui/Card";
import { useSocket } from "../../../hooks/useSocket";
import playerManager from "../../../services/PlayerManager";
import "./styles/DashboardTab.css";
const DashboardTab = () => {
const { socket, isConnected } = useSocket();
const [playerData, setPlayerData] = useState(null);
const [earnedPopup, setEarnedPopup] = useState(false);
const [credits, setCredits] = useState(0);
const [onlineCount, setOnlineCount] = useState(
playerManager.onlinePlayers.length,
);
const savedUser = JSON.parse(localStorage.getItem("user"));
const localUsername = savedUser?.username || "Unknown Pilot";
useEffect(() => {
const unsubscribe = playerManager.subscribe(({ online }) => {
setOnlineCount(online.length);
});
if (!socket) return;
socket.emit("player:get_dashboard");
const handleData = (data) => {
setPlayerData(data);
setCredits(data.credits);
};
const handleCreditsUpdate = ({ totalCredits }) => {
setCredits(totalCredits);
setEarnedPopup(true);
setTimeout(() => setEarnedPopup(false), 2000);
};
const handleOfflineReport = () => {
socket.emit("player:get_dashboard");
};
socket.on("player:dashboard_data", handleData);
socket.on("player:credits_update", handleCreditsUpdate);
socket.on("player:offline_report", handleOfflineReport);
return () => {
unsubscribe();
socket.off("player:dashboard_data", handleData);
socket.off("player:credits_update", handleCreditsUpdate);
socket.off("player:offline_report", handleOfflineReport);
};
}, [socket]);
const stats = playerData || { experience: 0, level: 1 };
const nextLevelExp = 1000;
const expProgress = Math.min((stats.experience / nextLevelExp) * 100, 100);
return (
<div className="dash-container">
<div className="dash-scanline"></div>
<div className="dashboard-grid">
<Card className="dash-card pilot-card">
<div className="card-tag">ID_RECOGNITION</div>
<div className="pilot-info">
<div className="pilot-avatar">
<i className="fas fa-user-astronaut"></i>
<div className="level-badge">{stats.level}</div>
</div>
<div className="pilot-details">
<h3>{localUsername}</h3>
<p className="status-online">RANK: VETERAN</p>
</div>
</div>
<div className="exp-bar-container">
<div className="exp-labels">
<span>EXP: {stats.experience}</span>
<span>NEXT: {nextLevelExp}</span>
</div>
<div className="exp-track">
<div
className="exp-fill"
style={{ width: `${expProgress}%` }}
></div>
</div>
</div>
</Card>
<Card className="dash-card resources-card">
<div className="card-tag">FINANCIAL_DATA</div>
<h3>VALUABLES</h3>
<div className="resource-list">
<div className="res-item gold">
<i className="fas fa-coins"></i>
<div className="res-content">
<span className="res-label">CREDITS</span>
<span className="res-val">
{credits.toLocaleString()}
{earnedPopup && <span className="credit-plus">+1</span>}
</span>
</div>
</div>
<div className="res-item crystal">
<i className="fas fa-microchip"></i>
<div className="res-content">
<span className="res-label">DATA_CORES</span>
<span className="res-val">
{Math.floor(stats.experience / 10)}
</span>
</div>
</div>
</div>
</Card>
<Card className="dash-card status-card">
<div className="card-tag">SYSTEM_DIAGNOSTICS</div>
<h3>SHIPS_LOG</h3>
<div className="diag-grid">
<div className="diag-item">
<span className="diag-label">ENGINE</span>
<span className="diag-status">OPTIMAL</span>
</div>
<div className="diag-item">
<span className="diag-label">SHIELDS</span>
<span className="diag-status">100%</span>
</div>
<div className="diag-item warning">
<span className="diag-label">CARGO</span>
<span className="diag-status">NEAR_FULL</span>
</div>
<div className="diag-item">
<span className="diag-label">NET</span>
<span
className={`diag-status ${isConnected ? "online" : "offline"}`}
>
{isConnected ? (
<span className="online-info">
<span className="online-dot"></span>
LIVE: {onlineCount}
</span>
) : (
"DISCONNECTED"
)}
</span>
</div>
</div>
</Card>
</div>
</div>
);
};
export default DashboardTab;

View File

@ -1,157 +0,0 @@
import React, { useState, useEffect } from "react";
import GameDataManager from "../../../services/GameDataManager";
import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
import { config } from "../../../config/api";
import DatapackDetailsModal from "./components/DatapackDetailsModal";
import "./styles/DatapackTab.css";
const DatapackTab = () => {
const [activeSection, setActiveSection] = useState("items");
const [searchQuery, setSearchQuery] = useState("");
const [displayList, setDisplayList] = useState([]);
const [selectedItem, setSelectedItem] = useState(null);
const sections = [
{ id: "items", label: "Items", icon: "fa-box" },
{ id: "hostiles", label: "Hostiles", icon: "fa-biohazard" },
{ id: "dungeons", label: "Dungeons", icon: "fa-dungeon" },
{ id: "recipes", label: "Recipes", icon: "fa-scroll" },
];
useEffect(() => {
let data = [];
switch (activeSection) {
case "items":
data = Array.from(GameDataManager.items.values()).map((i) =>
GameDataManager.getItem(i.id),
);
break;
case "hostiles":
data = Array.from(GameDataManager.hostiles.values()).map((h) =>
GameDataManager.getHostile(h.id),
);
break;
case "dungeons":
data = Array.from(GameDataManager.dungeons.values()).map((d) =>
GameDataManager.getDungeon(d.id),
);
break;
case "recipes":
data = Array.from(GameDataManager.recipes.values()).map((r) =>
GameDataManager.getRecipe(r.id),
);
break;
default:
data = [];
}
if (searchQuery) {
const query = searchQuery.toLowerCase();
data = data.filter(
(item) =>
item.displayName?.toLowerCase().includes(query) ||
item.id?.toLowerCase().includes(query),
);
}
setDisplayList(data);
}, [activeSection, searchQuery]);
return (
<div className="tab-content active datapack-tab-wrapper">
<MeteorRegion>
<div className="datapack-controls">
<div className="section-selector">
{sections.map((s) => (
<button
key={s.id}
className={`section-btn ${activeSection === s.id ? "active" : ""}`}
onClick={() => setActiveSection(s.id)}
>
<i className={`fas ${s.icon}`}></i>
<span>{s.label}</span>
</button>
))}
</div>
<div className="search-bar">
<i className="fas fa-search"></i>
<input
type="text"
placeholder="Search ID or Name..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
<div className="datapack-content">
<div className="datapack-grid">
{displayList.map((item) => (
<div
key={item.id}
className={`datapack-card ${activeSection === "hostiles" ? "hostile-card" : ""}`}
onClick={() =>
setSelectedItem({ ...item, sectionType: activeSection })
}
>
<div className="card-icon">
{item.texture ? (
<img
src={`${config.serverUrl}/static/${item.texture}`}
alt=""
/>
) : (
<div className="fallback-icon">
{item.displayName?.[0] || "?"}
</div>
)}
</div>
<div className="card-info">
<span className="card-name">{item.displayName}</span>
<span className="card-id">{item.id}</span>
{activeSection === "hostiles" &&
item.loot &&
item.loot.length > 0 && (
<div className="card-loot-preview">
{item.loot.map((lootEntry, idx) => {
const lootData = GameDataManager.getItem(
lootEntry.id,
);
return (
<div
key={`${item.id}-loot-${idx}`}
className="loot-mini-slot-text"
title={`${lootData?.displayName || lootEntry.id} (${(lootEntry.chance * 100).toFixed(0)}%)`}
>
<i
className="fas fa-box-open"
style={{ fontSize: "10px", marginRight: "4px" }}
></i>
<span>
{lootData?.displayName ||
lootEntry.id.split(":").pop()}
</span>
</div>
);
})}
</div>
)}
</div>
</div>
))}
</div>
</div>
</MeteorRegion>
{selectedItem && (
<DatapackDetailsModal
data={selectedItem}
onClose={() => setSelectedItem(null)}
/>
)}
</div>
);
};
export default DatapackTab;

View File

@ -1,144 +0,0 @@
import React, { useState, useEffect } from "react";
import GameDataManager from "../../../services/GameDataManager.js";
import "./styles/DungeonsTab.css";
const DungeonsTab = ({ startDungeon }) => {
const [dungeons, setDungeons] = useState([]);
const [selectedDungeon, setSelectedDungeon] = useState(null);
const [showSelector, setShowSelector] = useState(true);
useEffect(() => {
const allKeys = Array.from(GameDataManager.dungeons.keys());
const uniqueDungeons = Array.from(new Set(allKeys))
.map((id) => GameDataManager.getDungeon(id))
.filter(
(d, index, self) => d && self.findIndex((t) => t.id === d.id) === index,
);
setDungeons(uniqueDungeons);
if (uniqueDungeons.length > 0 && !selectedDungeon) {
setSelectedDungeon(uniqueDungeons[0]);
}
}, []);
const handleSelectDungeon = (id) => {
const translatedDungeon = GameDataManager.getDungeon(id);
setSelectedDungeon(translatedDungeon);
if (window.innerWidth <= 768) {
setShowSelector(false);
}
};
return (
<div className="tab-content active" id="dungeons-tab">
<div
className={`dungeons-container ${!showSelector ? "view-active" : ""}`}
>
<div className="dungeon-selector">
<div className="selector-header">
<h2 className="terminal-text">AVAILABLE_MISSIONS</h2>
<div className="header-line-decor"></div>
</div>
<div className="dungeon-list custom-scroll">
{dungeons.map((dungeon) => (
<div
key={dungeon.id}
className={`dungeon-summary-card ${selectedDungeon?.id === dungeon.id ? "active" : ""}`}
onClick={() => handleSelectDungeon(dungeon.id)}
>
<div className="card-selection-indicator"></div>
<div className="dungeon-brief">
<span className="name">{dungeon.displayName}</span>
<span className="energy-cost">{dungeon.energyCost} EN</span>
</div>
</div>
))}
</div>
</div>
<div className="dungeon-view">
{selectedDungeon ? (
<div className="dungeon-details-v2">
<div className="details-header-scan">
<button
className="back-to-list"
onClick={() => setShowSelector(true)}
>
<i className="fas fa-arrow-left"></i>
</button>
<div className="mission-info-group">
<div className="mission-type-label">MISSION_BRIEFING</div>
<h3 className="mission-title">
{selectedDungeon.displayName}
</h3>
</div>
<div className="scanline-horizontal"></div>
</div>
<div className="details-body custom-scroll">
<div className="description-box">
<p className="description-text">
{selectedDungeon.description ||
"No tactical briefing available for this sector."}
</p>
</div>
<div className="expected-rewards-section">
<div className="section-header">
<i className="fas fa-microchip"></i>
<h4>EXPECTED_REWARDS:</h4>
</div>
<div className="rewards-grid">
{selectedDungeon.lootTable?.map((loot, idx) => {
const item = GameDataManager.getItem(loot.itemId);
const rarity = item?.meta?.rarity || "common";
return (
<div key={idx} className={`reward-entry ${rarity}`}>
<div className="reward-icon-container">
{item?.texture ? (
<img src={item.texture} alt={item.displayName} />
) : (
<i className="fas fa-box-open"></i>
)}
</div>
<div className="reward-text">
<span className="reward-name">
{item?.displayName || loot.itemId}
</span>
<span className="reward-chance">
{loot.chance}% ACQUISITION
</span>
</div>
</div>
);
})}
</div>
</div>
<div className="action-area">
<button
className="initiate-deployment-btn"
onClick={() => startDungeon(selectedDungeon.id)}
>
<span className="glitch-text">INITIATE_DEPLOYMENT</span>
<i className="fas fa-chevron-right"></i>
</button>
</div>
</div>
</div>
) : (
<div className="dungeon-placeholder">
<div className="radar-scanner"></div>
<p className="blink-text">WAITING_FOR_COORDINATES...</p>
</div>
)}
</div>
</div>
</div>
);
};
export default DungeonsTab;

View File

@ -1,231 +0,0 @@
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();
const [items, setItems] = useState([]);
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 manifest = GameDataManager.manifest || {};
const coreSystems = manifest.core_systems?.categories || {};
const getFullTextureUrl = (path) => {
if (!path) return "/assets/no-image.png";
if (path.startsWith("http")) return path;
return `${ASSET_BASE_URL}${path}`;
};
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,
id: id,
textureUrl: getFullTextureUrl(staticData?.texture),
canEquip: !!staticData?.meta?.equipmentSlot,
rarity: staticData?.meta?.rarity || "common",
currentSlot: currentSlot,
};
};
useEffect(() => {
if (!socket) return;
const refreshData = () => {
socket.emit("player:get_inventory");
socket.emit("player:get_equipment");
};
refreshData();
const handleInventory = (rawItems) => {
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] }, slot);
}
});
setEquipment(mapped);
};
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 equipItem = (item) => {
const slot = item.meta?.equipmentSlot;
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) => (
<div className="equip-group">
<div className="group-label">{title}</div>
<div className="equip-list-compact grid-config">
{groupSlots.map((slot) => (
<div
key={slot.id}
className={`equip-row-mini ${equipment[slot.id] ? "occupied" : ""}`}
onClick={() =>
equipment[slot.id] &&
(setSelectedItem(equipment[slot.id]), setShowModal(true))
}
>
<span className="slot-name-tiny">{slot.label}</span>
<div
className={`equip-box-mini ${equipment[slot.id]?.rarity || ""}`}
>
{equipment[slot.id] ? (
<img
src={equipment[slot.id].textureUrl}
className="item-img-mini"
/>
) : (
<span className="plus">+</span>
)}
</div>
</div>
))}
</div>
</div>
);
return (
<div className="inv-adaptive-container">
<div className="inv-layout-wrapper">
<section className="inv-panel loadout">
<MeteorRegion>
{renderSlotGroup("SUIT_GEAR", equipmentSlots.personal)}
{renderSlotGroup("WEAPONRY", equipmentSlots.weapons)}
{renderSlotGroup("ACCESSORIES", equipmentSlots.accessories)}
<div className="separator" />
{renderSlotGroup("SHIP_MODULES", equipmentSlots.ship)}
</MeteorRegion>
</section>
<section className="inv-panel cargo">
<MeteorRegion>
<div className="cargo-grid-v2">
{items.map((item, idx) => {
const isEquipped = Object.values(equipment).some(
(e) => e?.id === item.id,
);
return (
<div
key={`${item.id}-${idx}`}
className={`item-slot ${item.rarity} ${isEquipped ? "equipped-in-storage" : ""}`}
onClick={() => {
setSelectedItem(item);
setShowModal(true);
}}
>
<img
src={item.textureUrl}
width={62}
className="item-img-grid"
/>
{isEquipped && <div className="equipped-tag">E</div>}
{item.quantity > 1 && (
<span className="qty-label">{item.quantity}</span>
)}
</div>
);
})}
</div>
</MeteorRegion>
</section>
</div>
{showModal &&
selectedItem &&
ReactDOM.createPortal(
<ItemModal
item={selectedItem}
isEquipped={
!!selectedItem.currentSlot ||
Object.values(equipment).some((e) => 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,
)}
</div>
);
};
export default InventoryTab;

View File

@ -1,116 +0,0 @@
import React, { useState, useEffect } from "react";
import { useSocket } from "../../../hooks/useSocket";
import "./styles/NotificationsTab.css";
const NotificationsTab = () => {
const { socket } = useSocket();
const [notifications, setNotifications] = useState([]);
useEffect(() => {
if (!socket) return;
socket.emit("notifications:get_all");
const handleNewNotify = (notify) => {
setNotifications((prev) => [notify, ...prev]);
};
const handleInitialNotify = (data) => {
setNotifications(data);
};
socket.on("notification:new", handleNewNotify);
socket.on("notifications:list", handleInitialNotify);
return () => {
socket.off("notification:new", handleNewNotify);
socket.off("notifications:list", handleInitialNotify);
};
}, [socket]);
const handleAction = (id, action, data) => {
if (action === "accept_friend") {
socket.emit("friend:accept", { id, friendId: data.fromId });
socket.emit("notification:read", { id });
} else if (action === "dismiss") {
socket.emit("notification:dismiss", { id });
} else {
socket.emit("notification:read", { id });
}
setNotifications((prev) => prev.filter((n) => n.id !== id));
};
const getIcon = (type) => {
switch (type) {
case "friend_request":
return "fas fa-user-plus";
case "crafting":
return "fas fa-hammer";
case "system":
return "fas fa-robot";
case "item_received":
return "fas fa-box-open";
case "inventory_clear":
return "fas fa-trash-alt";
default:
return "fas fa-bell";
}
};
return (
<div className="notifications-container">
<div className="notifications-header">
<div className="card-tag">SYSTEM_ALERTS</div>
<h2>NOTIFICATIONS</h2>
</div>
<div className="notifications-list">
{notifications.length === 0 && (
<div className="no-notifications">
<i className="fas fa-satellite-dish"></i>
<span>NO_ACTIVE_ALERTS_FOUND</span>
</div>
)}
{notifications.map((n) => (
<div key={n.id} className={`notify-card ${n.type} ${n.priority}`}>
<div className="notify-icon">
<i className={getIcon(n.type)}></i>
</div>
<div className="notify-content">
<div className="notify-title-row">
<h4>{n.title}</h4>
<span className="notify-time">
{new Date(n.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div>
<p>{n.message}</p>
</div>
<div className="notify-actions">
{n.type === "friend_request" && (
<button
className="action-btn accept"
onClick={() => handleAction(n.id, "accept_friend", n.data)}
>
ACCEPT
</button>
)}
<button
className="action-btn dismiss"
onClick={() => handleAction(n.id, "dismiss")}
>
<i className="fas fa-times"></i>
</button>
</div>
</div>
))}
</div>
</div>
);
};
export default NotificationsTab;

View File

@ -1,172 +0,0 @@
import React, { useEffect, useState, useMemo } from "react";
import Card from "../../../components/ui/Card";
import { useSocket } from "../../../hooks/useSocket";
import gameDataManager from "../../../services/GameDataManager";
import "./styles/QuestsTab.css";
const QuestsTab = () => {
const { socket } = useSocket();
const [quests, setQuests] = useState([]);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState("ACTIVE");
useEffect(() => {
if (!socket) return;
socket.emit("quest:get_list");
const localize = (q) => {
const staticData = gameDataManager.getQuest(q.id);
return {
...q,
displayName: staticData?.displayName || q.id,
description: staticData?.description || "",
objectives: q.objectives.map((obj, idx) => ({
...obj,
description: staticData?.objectives[idx]?.description || obj.type,
})),
};
};
const handleQuestData = (data) => {
const uniqueQuests = new Map();
data.forEach((q) => {
uniqueQuests.set(q.id, localize(q));
});
setQuests(Array.from(uniqueQuests.values()));
setLoading(false);
};
const handleQuestUpdate = (updatedQuest) => {
setQuests((prev) => {
const localized = localize(updatedQuest);
const questMap = new Map(prev.map((q) => [q.id, q]));
questMap.set(localized.id, localized);
return Array.from(questMap.values());
});
};
socket.on("quest:list_data", handleQuestData);
socket.on("quest:update", handleQuestUpdate);
return () => {
socket.off("quest:list_data", handleQuestData);
socket.off("quest:update", handleQuestUpdate);
};
}, [socket]);
const filteredQuests = useMemo(() => {
return quests.filter((q) =>
activeTab === "ACTIVE"
? q.status === "active" || q.status === "ready"
: q.status === "completed",
);
}, [quests, activeTab]);
const renderObjective = (obj, index) => {
const progress = Math.min(
(obj.currentAmount / obj.requiredAmount) * 100,
100,
);
return (
<div key={index} className="objective-item">
<div className="objective-info">
<span className="objective-desc">{obj.description || obj.type}</span>
<span className="objective-count">
{obj.currentAmount} / {obj.requiredAmount}
</span>
</div>
<div className="objective-progress-track">
<div
className="objective-progress-fill"
style={{ width: `${progress}%` }}
></div>
</div>
</div>
);
};
return (
<div className="quests-container">
<div className="dash-scanline"></div>
<div className="quests-header">
<h2 className="glitch-text" data-text="MISSION_LOG">
MISSION_LOG
</h2>
<div className="quest-tabs-nav">
<button
className={`nav-btn ${activeTab === "ACTIVE" ? "active" : ""}`}
onClick={() => setActiveTab("ACTIVE")}
>
ACTIVE_OPERATIONS
</button>
<button
className={`nav-btn ${activeTab === "COMPLETED" ? "active" : ""}`}
onClick={() => setActiveTab("COMPLETED")}
>
ARCHIVED_DATA
</button>
</div>
<div className="header-line"></div>
</div>
{loading ? (
<div className="loading-status">SCANNING_NEURAL_NETWORK...</div>
) : (
<div className="quests-grid">
{filteredQuests.length > 0 ? (
filteredQuests.map((quest) => (
<Card key={quest.id} className={`quest-card ${quest.status}`}>
<div className="card-tag">{quest.category || "MISSION"}</div>
<div className="quest-main">
<h3 className="quest-title">{quest.displayName}</h3>
<p className="quest-description">{quest.description}</p>
</div>
<div className="quest-objectives">
<div className="section-label">OBJECTIVES</div>
{quest.objectives.map((obj, idx) =>
renderObjective(obj, idx),
)}
</div>
<div className="quest-rewards">
<div className="section-label">REWARDS</div>
<div className="rewards-row">
{quest.rewards?.credits > 0 && (
<span className="reward-pill credits">
+{quest.rewards.credits} CR
</span>
)}
{quest.rewards?.xp > 0 && (
<span className="reward-pill xp">
+{quest.rewards.xp} XP
</span>
)}
</div>
</div>
{quest.status === "ready" && (
<button
className="claim-btn"
onClick={() =>
socket.emit("quest:claim_reward", { questId: quest.id })
}
>
COMPLETE_MISSION
</button>
)}
{quest.status === "completed" && (
<div className="completed-stamp">MISSION_ACCOMPLISHED</div>
)}
</Card>
))
) : (
<div className="no-quests">
<p>NO_{activeTab}_SIGNALS_FOUND</p>
</div>
)}
</div>
)}
</div>
);
};
export default QuestsTab;

View File

@ -1,46 +0,0 @@
import React, { useState } from 'react';
import CategorySelector from "../components/CategorySelector"; // Імпортуємо твій новий компонент
import "./styles/ShopTab.css";
const ShopTab = () => {
const [category, setCategory] = useState('ships');
const categories = ['Featured', 'ships', 'weapons', 'armors', 'cosmetics', 'materials'];
return (
<div className="tab-content active">
<div className="shop-container">
<div className="shop-header">
{/* Використовуємо універсальний компонент замість ручного мапінгу */}
<CategorySelector
categories={categories}
activeCategory={category}
onCategoryChange={setCategory}
/>
<div className="shop-resources">
<div className="card-resource">
<i className="fas fa-gem"></i>
<span id="gems">10</span>
</div>
<div className="card-resource">
<i className="fas fa-coins"></i>
<span id="credits">1,000</span>
</div>
</div>
</div>
<div className="shop-content">
<div className="shop-items-container">
<div className="shop-items">
<p className="status-text">Browsing {category}...</p>
{/* Тут буде логіка відображення товарів */}
</div>
</div>
</div>
</div>
</div>
);
};
export default ShopTab;

View File

@ -1,115 +0,0 @@
import React, { useState, useEffect } from "react";
import { useSocket } from "../../../hooks/useSocket";
import GameDataManager from "../../../services/GameDataManager";
import "./styles/SkillsTab.css";
import CategorySelector from "../components/CategorySelector";
import MeteorRegion from "../../../components/Meteor/MeteorRegion.jsx";
import { SkillCard } from "./components/SkillsCard.jsx";
const SkillsTab = () => {
const { socket } = useSocket();
const [categories, setCategories] = useState([]);
const [activeCategory, setActiveCategory] = useState("");
const [skills, setSkills] = useState([]);
const [playerSkills, setPlayerSkills] = useState({});
const [skillPoints, setSkillPoints] = useState(0);
useEffect(() => {
const manifestCategories = GameDataManager.getSkillCategories();
setCategories(manifestCategories);
if (manifestCategories.length > 0) {
setActiveCategory(manifestCategories[0].id);
}
}, []);
useEffect(() => {
if (activeCategory) {
const filtered = GameDataManager.getSkillsByCategory(activeCategory);
setSkills(filtered);
}
}, [activeCategory]);
useEffect(() => {
if (!socket) return;
socket.emit("player:get_skill_points");
socket.emit("player:get_skills");
const handleSkillPoints = (data) => setSkillPoints(data.points || 0);
const handleSkillsData = (data) => setPlayerSkills(data.skills || {});
socket.on("player:skill_points_data", handleSkillPoints);
socket.on("player:skills_data", handleSkillsData);
return () => {
socket.off("player:skill_points_data", handleSkillPoints);
socket.off("player:skills_data", handleSkillsData);
};
}, [socket]);
const handleUpgrade = (skillId) => {
socket.emit("player:upgrade_skill", { skillId });
};
return (
<div
className="tab-content active"
style={{ height: "100%", display: "flex", flexDirection: "column" }}
>
<MeteorRegion className="skills-container">
<div className="skills-header">
<div className="header-main">
<h2>
<i className="fas fa-microchip"></i> Neural Core
</h2>
<div className="skill-points-badge">
<span className="label">Uplink Points:</span>
<span className="value">{skillPoints}</span>
</div>
</div>
</div>
<CategorySelector
categories={categories}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<div className="skills-grid custom-scroll">
{skills.length > 0 ? (
skills.map((skill) => {
const progress = playerSkills[skill.id] || {
level: 0,
experience: 0,
};
const maxLv = skill.meta?.topLevel || 10;
const cost = progress.level === 0 ? 2 : 1;
return (
<SkillCard
key={skill.id}
skill={skill}
level={progress.level}
maxLevel={maxLv}
experience={progress.experience}
canAfford={skillPoints >= cost}
onUpgrade={() => handleUpgrade(skill.id)}
/>
);
})
) : (
<div className="empty-category">
<i className="fas fa-xs fa-terminal"></i>
<p>No active modules found in this sector.</p>
</div>
)}
</div>
</MeteorRegion>
</div>
);
};
export default SkillsTab;

View File

@ -1,198 +0,0 @@
.craft-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.85);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(6px);
}
.craft-modal {
background: #0f1115;
border: 1px solid rgba(0, 210, 255, 0.3);
width: 90%;
max-width: 400px; /* Трохи вужча для компактності */
border-radius: 12px;
position: relative;
padding: 20px;
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.9);
animation: modalSlideUp 0.3s ease-out;
color: #fff;
}
/* Header Section */
.modal-header-compact {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 18px;
}
.item-icon-box {
width: 70px;
height: 70px;
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(0, 210, 255, 0.4);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
box-shadow: inset 0 0 10px rgba(0, 210, 255, 0.1);
}
.item-icon-box img {
width: 50px;
height: 50px;
object-fit: contain;
}
.item-info-title h3 {
margin: 0;
font-family: "Orbitron", sans-serif;
font-size: 1.1rem;
color: #00d2ff;
text-transform: uppercase;
}
.item-tag {
font-size: 0.65rem;
color: #888;
letter-spacing: 1px;
}
/* Sections */
.details-section {
margin-bottom: 15px;
}
.section-label {
font-size: 0.75rem;
text-transform: uppercase;
color: #00d2ff;
margin-bottom: 8px;
opacity: 0.8;
display: block;
}
.description-text {
font-size: 0.85rem;
color: #aaa;
font-style: italic;
line-height: 1.4;
}
/* Resource List */
.res-container {
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
padding: 10px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.res-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
}
.res-row:last-child {
border-bottom: none;
}
.res-name {
font-size: 0.9rem;
color: #ccc;
display: flex;
align-items: center;
gap: 8px;
}
.res-amount {
font-family: "Courier New", monospace;
font-size: 0.9rem;
}
.val-bad {
color: #ff4444;
}
.val-good {
color: #00ff88;
}
/* Progress & Outcome */
.outcome-bar {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
margin-top: 10px;
color: #888;
}
.outcome-bar strong {
color: #fff;
}
/* Buttons */
.btn-craft-action {
width: 100%;
padding: 12px;
margin-top: 20px;
font-family: "Orbitron", sans-serif;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
border-radius: 4px;
text-transform: uppercase;
}
.btn-primary-craft {
background: rgba(0, 210, 255, 0.1);
border: 1px solid #00d2ff;
color: #00d2ff;
}
.btn-primary-craft:hover:not(:disabled) {
background: #00d2ff;
color: #000;
box-shadow: 0 0 15px rgba(0, 210, 255, 0.3);
}
.btn-primary-craft:disabled {
border-color: #444;
color: #444;
cursor: not-allowed;
}
.close-btn-top {
position: absolute;
top: 12px;
right: 12px;
background: none;
border: none;
color: #555;
font-size: 20px;
cursor: pointer;
}
.close-btn-top:hover {
color: #fff;
}
@keyframes modalSlideUp {
from {
transform: translateY(15px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

View File

@ -1,163 +0,0 @@
import React from "react";
import "./CraftModal.css";
import { getServerUrl } from "../../../../config/api";
const CraftModal = ({
recipe,
onClose,
onStartCraft,
activeCraft,
getOwnedAmount,
}) => {
if (!recipe) return null;
const CONNECT_URL = getServerUrl();
const ASSET_BASE_URL = `${CONNECT_URL}/static/`;
const getFullTextureUrl = (path) => {
if (!path) return "/assets/no-image.png";
return path.startsWith("http") ? path : `${ASSET_BASE_URL}${path}`;
};
const isBusy = !!activeCraft;
const outputQty = Object.values(recipe.output || {})[0] || 1;
const canAfford = recipe.ingredients?.every(
(ing) => getOwnedAmount(ing.itemId) >= ing.quantity,
);
return (
<div className="craft-modal-overlay" onClick={onClose}>
<div className="craft-modal" onClick={(e) => e.stopPropagation()}>
<button className="close-btn-top" onClick={onClose}>
&times;
</button>
{/* Header: Icon + Title */}
<div className="modal-header-compact">
<div className="item-icon-box">
<img
src={getFullTextureUrl(recipe.texture)}
alt={recipe.displayName}
/>
<div
className="item-qty-badge"
style={{
position: "absolute",
bottom: "-5px",
right: "-5px",
background: "#00d2ff",
color: "#000",
fontSize: "10px",
padding: "2px 5px",
borderRadius: "3px",
fontWeight: "bold",
}}
>
x{outputQty}
</div>
</div>
<div className="item-info-title">
<span className="item-tag">PROTOTYPE_UNIT</span>
<h3>{recipe.displayName}</h3>
</div>
</div>
{/* Description */}
<div className="details-section">
<p className="description-text">
{recipe.description ||
"Advanced composite material for high-tier construction."}
</p>
</div>
{/* Resources */}
<div className="details-section">
<span className="section-label">Required Materials</span>
<div className="res-container">
{recipe.ingredients?.map((ing) => {
const owned = getOwnedAmount(ing.itemId);
const hasEnough = owned >= ing.quantity;
return (
<div key={ing.itemId} className="res-row">
<span className="res-name">
<i
className={`fas fa-square`}
style={{
fontSize: "8px",
color: hasEnough ? "#00ff88" : "#ff4444",
}}
></i>
{ing.displayName}
</span>
<span
className={`res-amount ${hasEnough ? "val-good" : "val-bad"}`}
>
{owned} / {ing.quantity}
</span>
</div>
);
})}
</div>
</div>
{/* Outcome Info */}
<div className="outcome-bar">
<span>
Production Time: <strong>{recipe.time_seconds}s</strong>
</span>
</div>
{/* Action Button */}
<div className="modal-footer-minimal">
{activeCraft && activeCraft.recipeId === recipe.id ? (
<div style={{ marginTop: "15px" }}>
<div
style={{
fontSize: "12px",
color: "#00d2ff",
marginBottom: "5px",
textAlign: "center",
}}
>
Constructing... {activeCraft.timeLeft}s
</div>
<div
className="progress-bar-bg"
style={{
height: "4px",
background: "rgba(255,255,255,0.1)",
borderRadius: "2px",
overflow: "hidden",
}}
>
<div
className="progress-bar-fill"
style={{
width: `${((activeCraft.totalTime - activeCraft.timeLeft) / activeCraft.totalTime) * 100}%`,
height: "100%",
background: "#00d2ff",
boxShadow: "0 0 10px #00d2ff",
}}
></div>
</div>
</div>
) : (
<button
className="btn-craft-action btn-primary-craft"
disabled={!canAfford || isBusy}
onClick={() => canAfford && !isBusy && onStartCraft(recipe)}
>
{isBusy
? "System Busy"
: !canAfford
? "Low Resources"
: "Begin Construction"}
</button>
)}
</div>
</div>
</div>
);
};
export default CraftModal;

View File

@ -1,152 +0,0 @@
.datapack-modal-content {
background: #0f1115;
border: 1px solid var(--border-color);
width: 90%;
max-width: 450px;
border-radius: 12px;
position: relative;
padding: 25px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.8);
animation: modalSlideUp 0.3s ease-out;
}
.modal-headerr {
display: flex;
align-items: center;
gap: 20px;
justify-content: left;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.modal-icon-big {
width: 80px;
height: 80px;
background: rgba(0, 0, 0, 0.4);
border: 1px solid var(--border-color);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-icon-big img {
width: 60px;
}
.modal-title-group h3 {
margin: 0;
font-family: "Orbitron", sans-serif;
color: var(--primary-color);
font-size: 1.3rem;
}
.modal-raw-id {
font-size: 0.75rem;
color: var(--text-secondary);
opacity: 0.6;
}
.details-description {
font-size: 0.9rem;
line-height: 1.5;
color: #ccc;
margin-bottom: 20px;
font-style: italic;
}
.details-section h4 {
font-size: 0.8rem;
text-transform: uppercase;
color: var(--text-secondary);
letter-spacing: 1px;
margin-bottom: 10px;
border-left: 3px solid var(--primary-color);
padding-left: 10px;
}
.stat-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
}
.stat-label {
color: #888;
font-size: 0.85rem;
}
.stat-value {
color: #fff;
font-family: monospace;
}
@keyframes modalSlideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.loot-list-full {
display: flex;
flex-direction: column;
gap: 10px;
background: rgba(0, 0, 0, 0.2);
padding: 12px;
border-radius: 8px;
}
.loot-detail-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.loot-detail-item:last-child {
border-bottom: none;
}
.loot-item-icon {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
}
.loot-item-icon img {
width: 32px;
height: 32px;
object-fit: contain;
}
.loot-item-info {
display: flex;
flex-direction: column;
}
.loot-item-name {
font-weight: 600;
color: #fff;
font-size: 14px;
}
.loot-item-meta {
font-size: 12px;
color: #aaa;
}
.fallback-mini {
color: #444;
font-weight: bold;
}

View File

@ -1,130 +0,0 @@
import React from "react";
import GameDataManager from "../../../../services/GameDataManager";
import { config } from "../../../../config/api";
import "./DatapackDetailsModal.css";
const DatapackDetailsModal = ({ data, onClose }) => {
if (!data) return null;
const renderStats = () => {
if (!data.stats) return null;
return (
<div className="details-stats">
{Object.entries(data.stats).map(([key, value]) => (
<div key={key} className="stat-row">
<span className="stat-label">
{GameDataManager.getStatName(key) || key}:
</span>
<span className="stat-value">{value}</span>
</div>
))}
</div>
);
};
const renderLoot = () => {
if (!data.loot || data.loot.length === 0) return null;
return (
<div className="details-section">
<h4>Loot Table</h4>
<div className="loot-list-full">
{data.loot.map((entry, idx) => {
const itemInfo = GameDataManager.getItem(entry.id);
const countDisplay =
typeof entry.count === "object"
? `${entry.count.min}-${entry.count.max}`
: entry.count;
return (
<div key={idx} className="loot-detail-item">
<div className="loot-item-icon">
{itemInfo?.texture ? (
<img
src={`${config.serverUrl}/static/${itemInfo.texture}`}
alt={itemInfo.displayName}
/>
) : (
<div className="fallback-mini">?</div>
)}
</div>
<div className="loot-item-info">
<span className="loot-item-name">
{itemInfo?.displayName || entry.id}
</span>
<span className="loot-item-meta">
{Math.round(entry.chance * 100)}% chance Amount:{" "}
{countDisplay}
</span>
</div>
</div>
);
})}
</div>
</div>
);
};
return (
<div className="modal-overlay" onClick={onClose}>
<div
className="datapack-modal-content"
onClick={(e) => e.stopPropagation()}
>
<button className="modal-close" onClick={onClose}>
&times;
</button>
<div className="modal-headerr">
<div className="modal-icon-big">
{data.texture ? (
<img src={`${config.serverUrl}/static/${data.texture}`} alt="" />
) : (
<div className="fallback-icon">{data.displayName?.[0]}</div>
)}
</div>
<div className="modal-title-group">
<h3>{data.displayName}</h3>
<code className="modal-raw-id">{data.id}</code>
</div>
</div>
<div className="modal-body">
{data.description && (
<p className="details-description">{data.description}</p>
)}
<div className="details-section">
<h4>Properties & Stats</h4>
{renderStats()}
{data.sectionType === "hostiles" && data.level && (
<div className="stat-row">
<span className="stat-label">Base Level:</span>
<span className="stat-value">{data.level}</span>
</div>
)}
</div>
{data.sectionType === "hostiles" && renderLoot()}
{data.ingredients && (
<div className="details-section">
<h4>Recipe Requirements</h4>
<div className="ingredients-list">
{data.ingredients.map((ing, idx) => (
<div key={idx} className="ingredient-item">
<span>{ing.displayName}</span>
<span>x{ing.quantity}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default DatapackDetailsModal;

View File

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

View File

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

View File

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

View File

@ -1,120 +0,0 @@
import React, { useEffect } from "react";
import "./ItemModal.css";
import { getServerUrl } from "../../../../config/api";
const ItemModal = ({
item,
onClose,
onEquip,
onUnequip,
isEquipped,
getStatIcon,
formatStatName,
}) => {
useEffect(() => {
const handleKeyDown = (event) => {
if (event.keyCode === 69) {
if (isEquipped) {
onUnequip(item.currentSlot);
} else if (item && item.canEquip) {
onEquip(item);
}
onClose();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [item, isEquipped, onEquip, onUnequip, onClose]);
if (!item) return null;
const CONNECT_URL = getServerUrl();
const ASSET_BASE_URL = `${CONNECT_URL}/static/`;
const getFullTextureUrl = (path) => {
if (!path) return "/assets/no-image.png";
if (path.startsWith("http")) return path;
return `${ASSET_BASE_URL}${path}`;
};
return (
<div className="modal-overlay" onClick={onClose}>
<div
className="datapack-modal-content"
onClick={(e) => e.stopPropagation()}
>
<button className="modal-close" onClick={onClose}>
&times;
</button>
<div className="modal-header">
<div className={`modal-icon-big ${item.rarity}`}>
<img src={getFullTextureUrl(item.texture)} alt={item.displayName} />
</div>
<div className="modal-title-group">
<h3 className={item.rarity}>{item.displayName || item.name}</h3>
<div className="modal-raw-id">
{item.rarity?.toUpperCase()} SYSTEM_ID: {item.id}
</div>
</div>
</div>
<div className="details-section">
<p className="details-description">{item.description}</p>
</div>
<div className="details-section">
<h4>
<i className="fas fa-microchip"></i> Technical Specs
</h4>
<div className="item-stats-container">
{item.stats &&
Object.entries(item.stats).map(([statName, value]) => (
<div key={statName} className="stat-row">
<span className="stat-label">
<i className={getStatIcon?.(statName)}></i>{" "}
{formatStatName
? formatStatName(statName)
: statName.toUpperCase()}
</span>
<span className="stat-value">+{value}</span>
</div>
))}
</div>
</div>
<div className="modal-actions" style={{ marginTop: "20px" }}>
{isEquipped ? (
<button
className="btn-equip unequip"
onClick={() => {
onUnequip(item.currentSlot);
onClose();
}}
>
TERMINATE_CONNECTION (E)
</button>
) : (
item.canEquip && (
<button
className="btn-equip"
onClick={() => {
onEquip(item);
onClose();
}}
>
INITIALIZE_EQUIP (E)
</button>
)
)}
</div>
</div>
</div>
);
};
export default ItemModal;

View File

@ -1,175 +0,0 @@
.skill-item-card {
background: rgba(10, 15, 24, 0.95);
border: 1px solid #1a2638;
border-radius: 2px;
padding: 15px;
display: flex;
flex-direction: column;
gap: 12px;
position: relative;
transition: all 0.3s ease;
overflow: hidden;
}
.skill-item-card:hover {
border-color: #00d4ff;
box-shadow: 0 0 15px rgba(0, 212, 255, 0.1);
}
.skill-item-card.locked {
border-style: dashed;
opacity: 0.8;
}
.skill-item-card.locked .skill-icon-wrapper {
color: #4a5d75;
border-color: #1a2638;
}
.skill-item-card.mastered {
border-color: #00ff88;
}
.skill-card-header {
display: flex;
align-items: center;
gap: 12px;
}
.skill-icon-wrapper {
width: 40px;
height: 40px;
background: #05080c;
border: 1px solid #00d4ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #00d4ff;
flex-shrink: 0;
}
.skill-title-block {
flex: 1;
}
.skill-name {
font-size: 0.9rem;
font-weight: 900;
color: #fff;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 2px;
}
.skill-level-tag {
font-size: 10px;
color: #4a5d75;
font-family: "Space Mono", monospace;
}
.mastered .skill-level-tag {
color: #00ff88;
}
.skill-desc {
font-size: 11px;
color: #a0aec0;
line-height: 1.4;
margin: 0;
min-height: 32px;
}
.skill-progress-section {
margin-top: 5px;
}
.progress-info {
display: flex;
justify-content: space-between;
font-size: 9px;
color: #4a5d75;
margin-bottom: 4px;
text-transform: uppercase;
}
.skill-progress-bar {
height: 4px;
background: #05080c;
border: 1px solid #1a2638;
position: relative;
}
.skill-progress-bar .fill {
height: 100%;
background: linear-gradient(90deg, #00d4ff, #00ff88);
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.skill-card-actions {
margin-top: auto;
padding-top: 10px;
}
.btn-skill-unlock,
.btn-skill-upgrade {
width: 100%;
background: transparent;
border: 1px solid #00d4ff;
color: #00d4ff;
padding: 8px;
font-size: 10px;
font-weight: 900;
text-transform: uppercase;
cursor: pointer;
transition: all 0.2s ease;
font-family: "Space Mono", monospace;
}
.btn-skill-unlock:hover:not(:disabled),
.btn-skill-upgrade:hover:not(:disabled) {
background: rgba(0, 212, 255, 0.1);
box-shadow: 0 0 10px rgba(0, 212, 255, 0.2);
}
.btn-skill-unlock {
border-color: #ffaa00;
color: #ffaa00;
}
.btn-skill-unlock:hover:not(:disabled) {
background: rgba(255, 170, 0, 0.1);
box-shadow: 0 0 10px rgba(255, 170, 0, 0.2);
}
.btn-skill-unlock:disabled,
.btn-skill-upgrade:disabled {
border-color: #1a2638;
color: #4a5d75;
cursor: not-allowed;
opacity: 0.5;
}
.mastery-label {
text-align: center;
font-size: 10px;
color: #00ff88;
font-weight: 900;
padding: 8px;
border: 1px solid rgba(0, 255, 136, 0.2);
background: rgba(0, 255, 136, 0.05);
}
.lock-requirement {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: rgba(255, 68, 68, 0.1);
color: #ff4444;
font-size: 8px;
text-align: center;
padding: 2px 0;
text-transform: uppercase;
font-weight: 900;
}

View File

@ -1,76 +0,0 @@
import "./SkillsCard.css";
export const SkillCard = ({
skill,
level,
maxLevel,
experience,
onUpgrade,
canAfford,
}) => {
const isLocked = level === 0;
const isMaxLevel = level >= maxLevel;
const expToNext = Math.floor(100 * Math.pow(1.5, level));
const progressPercent = Math.min(100, (experience / expToNext) * 100);
return (
<div
className={`skill-item-card ${isLocked ? "locked" : ""} ${isMaxLevel ? "mastered" : ""}`}
>
<div className="skill-card-header">
<div className="skill-icon-wrapper">
{/* Іконка може бути в meta або за дефолтом */}
<i className={`fas ${skill.meta?.icon || "fa-atom"}`}></i>
</div>
<div className="skill-title-block">
<div className="skill-name">{skill.displayName}</div>
<div className="skill-level-tag">
{isMaxLevel ? "MAXED" : `RANK ${level}/${maxLevel}`}
</div>
</div>
</div>
<p className="skill-desc">{skill.description}</p>
{!isMaxLevel && !isLocked && (
<div className="skill-progress-section">
<div className="progress-info">
<span>Neural Sync</span>
<span>
{Math.floor(experience)} / {expToNext}
</span>
</div>
<div className="skill-progress-bar">
<div
className="fill"
style={{ width: `${progressPercent}%` }}
></div>
</div>
</div>
)}
<div className="skill-card-actions">
{isLocked ? (
<button
className="btn-skill-unlock"
disabled={!canAfford}
onClick={onUpgrade}
>
INSTALL MODULE (2 PTS)
</button>
) : isMaxLevel ? (
<div className="mastery-label">MODULE FULLY OPTIMIZED</div>
) : (
<button
className="btn-skill-upgrade"
disabled={!canAfford}
onClick={onUpgrade}
>
UPGRADE (1 PT)
</button>
)}
</div>
</div>
);
};

View File

@ -1,447 +0,0 @@
.chat-container {
display: flex;
height: 100%;
background: rgba(5, 8, 12, 0.9);
border: 1px solid var(--border-color);
overflow: hidden;
position: relative;
}
.chat-sidebar {
width: 300px;
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
transition: all 0.3s ease;
z-index: 2;
background: rgba(10, 15, 24, 0.6);
}
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
background: rgba(0, 0, 0, 0.2);
position: relative;
}
/* SEARCH SECTION */
.search-section {
padding: 20px 15px 15px;
border-bottom: 1px solid var(--border-color);
position: relative;
z-index: 10;
}
.search-input-wrapper {
display: flex;
gap: 5px;
margin-top: 10px;
}
.search-input-wrapper input {
flex: 1;
background: #05080c;
border: 1px solid var(--border-color);
color: #fff;
padding: 8px;
font-size: 12px;
font-family: "Geologica", sans-serif;
}
.search-results-dropdown {
position: absolute;
top: 100%;
left: 15px;
right: 15px;
background: #0a0f18;
border: 1px solid var(--primary-color);
border-top: none;
max-height: 200px;
overflow-y: auto;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.8);
}
.search-result-item {
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
font-size: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.search-result-item:hover {
background: rgba(0, 212, 255, 0.1);
}
/* CHAT LIST & ITEMS */
.chats-list {
flex: 1;
overflow-y: auto;
}
.friends-section-label {
padding: 20px 15px 10px;
display: flex;
align-items: center;
gap: 10px;
opacity: 0.4;
}
.label-text {
font-size: 10px;
letter-spacing: 2px;
font-weight: bold;
}
.label-line {
flex: 1;
height: 1px;
background: var(--border-color);
}
.chat-item {
padding: 12px 15px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.02);
transition: all 0.2s;
}
.chat-item:hover {
background: rgba(255, 255, 255, 0.03);
}
.chat-item.active {
background: rgba(0, 212, 255, 0.1);
box-shadow: inset 4px 0 0 var(--primary-color);
}
.chat-item-main {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.online {
background: #00ff88;
box-shadow: 0 0 5px #00ff88;
}
.status-dot.offline {
background: #444;
}
.unfriend-btn {
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.2);
padding: 8px;
cursor: pointer;
transition: all 0.2s;
opacity: 0;
}
.chat-item:hover .unfriend-btn {
opacity: 1;
}
.unfriend-btn:hover {
color: #ff3e3e;
text-shadow: 0 0 8px rgba(255, 62, 62, 0.4);
}
/* CHAT MAIN AREA */
.chat-header {
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--border-color);
background: rgba(10, 15, 24, 0.8);
}
.active-chat-info {
display: flex;
align-items: center;
gap: 10px;
}
.active-chat-info i {
color: var(--primary-color);
}
.chat-messages {
flex: 1;
padding: 15px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.message {
font-size: 13px;
line-height: 1.4;
min-height: min-content;
padding: 4px 0;
display: block;
}
.msg-time {
color: rgba(255, 255, 255, 0.3);
margin-right: 8px;
font-family: monospace;
}
.msg-author {
color: var(--primary-color);
font-weight: bold;
margin-right: 8px;
}
.message.system .msg-author {
color: #ff3e3e;
}
.message.system .msg-text {
color: #888;
word-break: break-all;
overflow-wrap: anywhere;
display: inline-block;
font-style: italic;
white-space: pre-wrap;
max-width: 100%;
}
.chat-input-area {
padding: 15px;
background: #05080c;
display: flex;
gap: 10px;
border-top: 1px solid var(--border-color);
}
.chat-input-area input {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
color: #fff;
padding: 10px 15px;
outline: none;
}
.send-btn {
background: transparent;
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 0 20px;
cursor: pointer;
transition: all 0.2s;
}
.send-btn:hover {
background: var(--primary-color);
color: #000;
}
/* MODAL STYLES */
.modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
backdrop-filter: blur(4px);
}
.modal-content {
background: #0a0f18;
border: 1px solid #ff3e3e;
padding: 25px;
width: 90%;
max-width: 350px;
text-align: center;
box-shadow: 0 0 30px rgba(255, 62, 62, 0.15);
}
.modal-content h3 {
color: #ff3e3e;
margin-bottom: 15px;
font-size: 14px;
letter-spacing: 2px;
}
.modal-content p {
color: #aaa;
font-size: 13px;
margin-bottom: 25px;
}
.modal-actions {
display: flex;
gap: 10px;
}
.confirm-btn,
.cancel-btn {
flex: 1;
padding: 10px;
font-size: 11px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.confirm-btn {
background: rgba(255, 62, 62, 0.1);
border: 1px solid #ff3e3e;
color: #ff3e3e;
}
.confirm-btn:hover {
background: #ff3e3e;
color: #fff;
}
.cancel-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
color: #fff;
}
/* MOBILE RESPONSIVENESS */
@media (max-width: 768px) {
.chat-sidebar {
width: 100%;
position: absolute;
height: 100%;
background: #0a0f18;
}
.chat-sidebar.hidden {
transform: translateX(-100%);
}
.chat-main {
width: 100%;
position: absolute;
height: 100%;
z-index: 1;
}
.chat-main.hidden {
transform: translateX(100%);
}
.back-btn {
background: transparent;
border: none;
color: var(--primary-color);
font-size: 1.2rem;
padding: 0 15px 0 0;
cursor: pointer;
}
}
/* Контейнер елемента списку */
.chat-item {
padding: 12px 15px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.02);
transition: all 0.2s;
}
.chat-item:hover {
background: rgba(255, 255, 255, 0.03);
}
.chat-item.active {
background: rgba(0, 212, 255, 0.1);
box-shadow: inset 4px 0 0 var(--primary-color);
}
.chat-item-main {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.unfriend-btn {
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.3);
padding: 8px;
cursor: pointer;
transition: all 0.2s;
opacity: 0;
}
@media (min-width: 769px) {
.chat-item:hover .unfriend-btn {
opacity: 1;
}
}
@media (max-width: 768px) {
.unfriend-btn {
opacity: 1;
color: rgba(255, 255, 255, 0.5);
padding: 12px;
}
.chat-item {
padding: 15px;
}
}
.unfriend-btn:hover {
color: #ff3e3e;
text-shadow: 0 0 8px rgba(255, 62, 62, 0.4);
}
.message {
font-size: 13px;
line-height: 1.4;
min-height: min-content;
padding: 4px 0;
display: block;
word-wrap: break-word;
}
.msg-text {
color: #fff;
word-break: break-all;
overflow-wrap: anywhere;
white-space: pre-wrap;
display: inline;
}
.message.system .msg-author {
color: #ff3e3e;
}
.message.system .msg-text {
color: #888;
font-style: italic;
}

View File

@ -1,145 +0,0 @@
.crafting-container {
padding: 20px;
height: 100%;
display: flex;
flex-direction: column;
}
.crafting-header {
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(140px, 1fr));
gap: 15px;
margin-top: 20px;
}
.recipe-card {
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);
}
.recipe-icon img {
max-width: 50px;
height: auto;
}
.recipe-name {
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: 10px;
margin-bottom: 15px;
padding: 5px 0;
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
}
.crafting-categories::-webkit-scrollbar {
display: none;
}
.crafting-cat-btn {
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;
}
@media (max-width: 600px) {
.crafting-container {
padding: 12px;
}
.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%;
}

View File

@ -1,272 +0,0 @@
.dash-container {
padding: 15px;
position: relative;
overflow-y: auto;
overflow-x: hidden;
height: 100%;
box-sizing: border-box;
scrollbar-gutter: stable;
}
.dash-scanline {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: rgba(0, 212, 255, 0.05);
animation: scan 6s linear infinite;
pointer-events: none;
z-index: 1;
}
@keyframes scan {
0% {
top: 0;
}
100% {
top: 100%;
}
}
.dashboard-grid {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
max-width: 1400px;
margin: 0 auto;
position: relative;
z-index: 2;
width: 100%;
box-sizing: border-box;
align-items: start;
}
.dash-card {
background: rgba(10, 15, 24, 0.95) !important;
border: 1px solid #1a2638 !important;
border-radius: 2px !important;
position: relative;
padding: 20px !important;
box-sizing: border-box;
overflow: hidden;
contain: paint;
}
.card-tag {
position: absolute;
top: 0;
right: 0;
background: #1a2638;
color: #00d4ff;
font-size: 8px;
padding: 2px 6px;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 1px;
}
.pilot-info {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 15px;
align-items: flex-start;
}
.pilot-avatar {
width: 50px;
height: 50px;
background: #05080c;
border: 1px solid #00d4ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
position: relative;
color: #00d4ff;
flex-shrink: 0;
}
.level-badge {
position: absolute;
bottom: -3px;
right: -3px;
background: #00d4ff;
color: #000;
font-size: 9px;
padding: 1px 4px;
font-weight: 900;
}
.pilot-details {
flex: 1;
min-width: 0;
width: 100%;
}
.pilot-details h3 {
margin: 0 0 5px 0;
font-size: 1rem;
color: #fff;
word-break: break-all;
}
.status-online {
font-size: 10px;
color: #4a5d75;
margin: 0;
}
.exp-bar-container {
margin-top: 10px;
}
.exp-labels {
display: flex;
justify-content: space-between;
font-size: 9px;
color: #4a5d75;
margin-bottom: 3px;
}
.exp-track {
height: 3px;
background: #05080c;
border: 1px solid #1a2638;
}
.exp-fill {
height: 100%;
background: linear-gradient(90deg, #00d4ff, #00ff88);
}
.resource-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.res-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
background: rgba(0, 0, 0, 0.4);
border-left: 2px solid #00d4ff;
position: relative;
contain: paint;
}
.res-content {
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
}
.res-val {
font-size: 1rem;
font-weight: 900;
color: #fff;
font-family: "Space Mono", monospace;
position: relative;
padding-right: 20px;
}
.credit-plus {
position: absolute;
top: 0;
right: 0;
color: #00ff88;
font-size: 0.8rem;
font-weight: 900;
pointer-events: none;
animation: floatUp 1.2s ease-out forwards;
}
@keyframes floatUp {
0% {
transform: translateY(5px);
opacity: 0;
}
20% {
opacity: 1;
}
100% {
transform: translateY(-15px);
opacity: 0;
}
}
.diag-grid {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
}
.diag-item {
background: rgba(0, 0, 0, 0.2);
padding: 8px 12px;
border: 1px solid #1a2638;
display: flex;
justify-content: space-between;
align-items: center;
}
.diag-status.online {
color: #00ff88;
display: flex;
align-items: center;
gap: 8px;
text-shadow: 0 0 8px rgba(0, 255, 136, 0.4);
}
.online-info {
display: flex;
align-items: center;
gap: 8px;
}
.online-dot {
width: 6px;
height: 6px;
background-color: #00ff88;
border-radius: 50%;
display: inline-block;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.4;
transform: scale(1.2);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@media (min-width: 600px) {
.dashboard-grid {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
}
@media (min-width: 992px) {
.dashboard-grid {
grid-template-columns: repeat(3, 1fr);
}
.pilot-info {
flex-direction: row;
}
.diag-grid {
grid-template-columns: 1fr 1fr;
}
}

View File

@ -1,153 +0,0 @@
.datapack-tab-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
.datapack-controls {
padding: 15px 20px;
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid var(--border-color);
}
.section-selector {
display: flex;
gap: 10px;
margin-bottom: 15px;
overflow-x: auto;
scrollbar-width: none;
}
.section-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
padding: 10px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
min-width: 80px;
}
.section-btn i {
font-size: 1.1rem;
}
.section-btn span {
font-size: 0.75rem;
font-family: "Orbitron", sans-serif;
}
.section-btn.active {
background: rgba(var(--primary-rgb), 0.1);
border-color: var(--primary-color);
color: var(--primary-color);
}
.search-bar {
position: relative;
width: 100%;
}
.search-bar i {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
}
.search-bar input {
width: 100%;
padding: 10px 10px 10px 35px;
background: #0a0a0c;
border: 1px solid var(--border-color);
border-radius: 4px;
color: #fff;
font-size: 0.9rem;
}
.datapack-content {
flex: 1;
padding: 20px;
}
.datapack-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
padding-bottom: 30px;
}
.datapack-card {
display: flex;
align-items: center;
gap: 15px;
padding: 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
}
.card-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
.card-icon img {
max-width: 32px;
}
.card-info {
display: flex;
flex-direction: column;
overflow: hidden;
}
.card-name {
color: #fff;
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-id {
font-size: 0.7rem;
color: var(--text-secondary);
font-family: monospace;
}
.card-meta {
font-size: 0.7rem;
color: var(--primary-color);
margin-top: 4px;
}
@media (max-width: 600px) {
.datapack-grid {
grid-template-columns: 1fr;
gap: 8px;
}
.datapack-controls {
padding: 10px;
}
.section-btn {
padding: 8px 5px;
}
.section-btn span {
font-size: 0.65rem;
}
}

View File

@ -1,370 +0,0 @@
#dungeons-tab {
height: 100%;
background: #05080c;
overflow: hidden;
}
.dungeons-container {
display: grid;
grid-template-columns: 300px 1fr;
height: 100%;
background: rgba(10, 15, 24, 0.9);
border-top: 1px solid rgba(0, 212, 255, 0.2);
}
.dungeon-selector {
border-right: 1px solid rgba(0, 212, 255, 0.1);
display: flex;
flex-direction: column;
background: rgba(0, 0, 0, 0.2);
}
.selector-header {
padding: 20px 15px;
}
.terminal-text {
font-family: "Space Mono", monospace;
color: #00d4ff;
font-size: 1rem;
letter-spacing: 2px;
margin: 0;
}
.header-line-decor {
height: 2px;
width: 50px;
background: #00d4ff;
margin-top: 5px;
box-shadow: 0 0 10px #00d4ff;
}
.dungeon-list {
flex: 1;
overflow-y: auto;
padding: 0 10px 20px 10px;
}
.dungeon-summary-card {
position: relative;
padding: 15px;
margin-bottom: 5px;
background: rgba(255, 255, 255, 0.02);
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.dungeon-summary-card:hover {
background: rgba(0, 212, 255, 0.05);
border-color: rgba(0, 212, 255, 0.1);
}
.dungeon-summary-card.active {
background: rgba(0, 212, 255, 0.1);
border-color: rgba(0, 212, 255, 0.3);
}
.card-selection-indicator {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0;
background: #00d4ff;
transition: 0.2s;
}
.active .card-selection-indicator {
width: 4px;
box-shadow: 0 0 15px #00d4ff;
}
.dungeon-brief .name {
display: block;
font-family: "Orbitron", sans-serif;
font-size: 0.9rem;
color: #fff;
text-transform: uppercase;
}
.dungeon-brief .energy-cost {
font-size: 0.75rem;
color: #00d4ff;
font-family: "Space Mono", monospace;
}
.dungeon-view {
padding: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dungeon-details-v2 {
background: rgba(0, 15, 25, 0.5);
border: 1px solid rgba(0, 212, 255, 0.1);
height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.details-header-scan {
padding: 20px;
border-bottom: 1px solid rgba(0, 212, 255, 0.1);
background: rgba(0, 212, 255, 0.03);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
gap: 15px;
}
.back-to-list {
display: none;
background: none;
border: 1px solid #00d4ff;
color: #00d4ff;
padding: 8px 12px;
cursor: pointer;
}
.scanline-horizontal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1px;
background: rgba(0, 212, 255, 0.2);
animation: scan_move 4s linear infinite;
}
@keyframes scan_move {
0% {
top: 0;
}
100% {
top: 100%;
}
}
.mission-type-label {
font-size: 0.7rem;
color: #4a5d75;
letter-spacing: 1px;
}
.mission-title {
margin: 5px 0 0 0;
font-size: 1.8rem;
color: #fff;
font-family: "Orbitron", sans-serif;
text-transform: uppercase;
}
.details-body {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.description-box {
background: rgba(0, 0, 0, 0.3);
padding: 15px;
border-left: 2px solid #00d4ff;
margin-bottom: 25px;
}
.description-text {
margin: 0;
color: #a0acba;
font-size: 0.9rem;
line-height: 1.6;
}
.section-header {
display: flex;
align-items: center;
gap: 10px;
color: #00d4ff;
margin-bottom: 15px;
}
.section-header h4 {
margin: 0;
font-size: 0.9rem;
letter-spacing: 1px;
}
.rewards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
}
.reward-entry {
display: flex;
align-items: center;
padding: 10px;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.05);
border-left-width: 4px;
}
.reward-icon-container {
width: 36px;
height: 36px;
background: #0a0f18;
border: 1px solid rgba(0, 212, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
flex-shrink: 0;
}
.reward-icon-container img {
width: 80%;
height: 80%;
object-fit: contain;
}
.reward-text {
display: flex;
flex-direction: column;
overflow: hidden;
}
.reward-name {
font-size: 0.75rem;
color: #fff;
text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.reward-chance {
font-size: 0.65rem;
color: #00ff88;
font-family: "Space Mono", monospace;
}
.reward-entry.common {
border-left-color: #4a5d75;
}
.reward-entry.uncommon {
border-left-color: #00ff88;
}
.reward-entry.rare {
border-left-color: #00d4ff;
}
.reward-entry.epic {
border-left-color: #a335ee;
}
.reward-entry.legendary {
border-left-color: #ffaa00;
}
.initiate-deployment-btn {
width: 100%;
padding: 18px;
background: #00d4ff;
border: none;
color: #000;
font-family: "Orbitron", sans-serif;
font-weight: 900;
font-size: 1rem;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: 0.3s;
clip-path: polygon(0 0, 100% 0, 100% 70%, 95% 100%, 0 100%);
}
.initiate-deployment-btn:hover {
background: #fff;
box-shadow: 0 0 20px rgba(0, 212, 255, 0.4);
}
.dungeon-placeholder {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #4a5d75;
}
.radar-scanner {
width: 80px;
height: 80px;
border: 2px solid rgba(0, 212, 255, 0.2);
border-radius: 50%;
position: relative;
margin-bottom: 20px;
}
.radar-scanner::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 50%;
height: 2px;
background: #00d4ff;
transform-origin: left center;
animation: radar_rotate 2s linear infinite;
}
@keyframes radar_rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media screen and (max-width: 768px) {
.dungeons-container {
grid-template-columns: 1fr;
}
.dungeons-container.view-active .dungeon-selector {
display: none;
}
.dungeons-container:not(.view-active) .dungeon-view {
display: none;
}
.back-to-list {
display: block;
}
.mission-title {
font-size: 1.3rem;
}
.dungeon-view {
padding: 10px;
}
.rewards-grid {
grid-template-columns: 1fr;
}
}
.custom-scroll::-webkit-scrollbar {
width: 4px;
}
.custom-scroll::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
.custom-scroll::-webkit-scrollbar-thumb {
background: #1a2638;
border-radius: 4px;
}

View File

@ -1,273 +0,0 @@
.inv-adaptive-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 15px;
height: calc(100vh - 140px);
font-family: "Orbitron", sans-serif;
color: #e0e6ed;
overflow: hidden;
}
.inv-header-compact {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #1a2638;
padding-bottom: 10px;
margin-bottom: 5px;
flex-shrink: 0;
}
.inv-logo {
font-size: 1.1rem;
color: #00d4ff;
letter-spacing: 3px;
margin: 0;
}
.inv-stats-bar {
font-size: 11px;
color: #4a5d75;
}
.text-cyan {
color: #00d4ff;
font-weight: bold;
}
.inv-layout-wrapper {
display: flex;
flex-direction: row;
gap: 20px;
flex: 1;
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 {
background: #1a2638;
color: #00d4ff;
font-size: 10px;
padding: 5px 12px;
letter-spacing: 2px;
font-weight: 800;
border-bottom: 1px solid #1a2638;
flex-shrink: 0;
}
.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;
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: 6px 10px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(26, 38, 56, 0.8);
transition: 0.2s;
cursor: pointer;
}
.equip-row-mini:hover {
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: 32px;
height: 32px;
background: #000;
border: 1px solid #1a2638;
display: flex;
align-items: center;
justify-content: center;
color: #00d4ff;
font-size: 0.9rem;
flex-shrink: 0;
}
.cargo {
flex: 1;
min-width: 0;
}
.cargo-grid-v2 {
display: grid;
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;
align-items: center;
justify-content: center;
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);
}
.separator {
height: 1px;
background: #1a2638;
margin: 10px 5px;
}
@media (max-width: 768px) {
.inv-adaptive-container {
height: auto;
overflow-y: visible;
}
.inv-layout-wrapper {
flex-direction: column;
height: auto;
}
.loadout {
width: 100%;
max-height: 300px;
}
.cargo {
height: 400px;
}
}
.item-slot.equipped-in-storage {
border: 2px solid #00d2ff;
box-shadow: inset 0 0 10px rgba(0, 210, 255, 0.3);
opacity: 0.8;
}
.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;
}
.qty-label {
position: absolute;
bottom: 2px;
right: 4px;
font-size: 11px;
font-weight: 800;
color: #fff;
text-shadow:
1px 1px 0 #000,
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
0 0 5px rgba(0, 0, 0, 0.8);
pointer-events: none;
z-index: 3;
}
.item-slot {
width: 60px;
height: 60px;
background: rgba(5, 8, 12, 0.9);
border: 1px solid #1a2638;
display: flex;
align-items: center;
justify-content: center;
position: relative;
cursor: pointer;
transition: 0.2s;
overflow: visible;
}
.item-img-grid {
max-width: 80%;
max-height: 80%;
object-fit: contain;
pointer-events: none;
}

View File

@ -1,148 +0,0 @@
.notifications-container {
padding: 20px;
height: 100%;
display: flex;
flex-direction: column;
background: rgba(10, 15, 24, 0.6);
}
.notifications-header {
margin-bottom: 20px;
}
.notifications-header h2 {
font-family: "Orbitron", sans-serif;
color: #fff;
letter-spacing: 2px;
margin-top: 5px;
}
.notifications-list {
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
padding-right: 5px;
}
.no-notifications {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15px;
padding: 100px 0;
color: #4a5d75;
font-family: "Orbitron", sans-serif;
font-size: 12px;
}
.no-notifications i {
font-size: 2rem;
opacity: 0.3;
}
.notify-card {
display: flex;
align-items: center;
background: rgba(26, 38, 56, 0.5);
border-left: 3px solid #00d4ff;
padding: 15px;
gap: 15px;
backdrop-filter: blur(5px);
border-radius: 0 4px 4px 0;
animation: notify-slide-in 0.3s ease-out;
}
.notify-card.friend_request {
border-left-color: #00ff88;
}
.notify-card.crafting {
border-left-color: #ffd700;
}
.notify-card.system {
border-left-color: #ff3e3e;
}
.notify-icon {
width: 35px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
border-radius: 50%;
color: #fff;
}
.notify-content {
flex: 1;
}
.notify-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.notify-content h4 {
margin: 0;
font-family: "Orbitron", sans-serif;
font-size: 12px;
color: #fff;
}
.notify-time {
font-size: 10px;
color: #4a5d75;
}
.notify-content p {
margin: 0;
font-size: 13px;
color: #a0aec0;
}
.notify-actions {
display: flex;
gap: 8px;
}
.action-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
padding: 6px 12px;
cursor: pointer;
font-family: "Orbitron", sans-serif;
font-size: 10px;
transition: all 0.2s;
}
.action-btn.accept {
background: #00ff88;
color: #000;
border: none;
font-weight: bold;
}
.action-btn.dismiss {
width: 28px;
padding: 6px 0;
}
.action-btn:hover {
filter: brightness(1.2);
}
@keyframes notify-slide-in {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}

View File

@ -1,211 +0,0 @@
.quests-container {
padding: 15px;
position: relative;
overflow-y: auto;
height: 100%;
box-sizing: border-box;
}
.quests-header {
margin-bottom: 25px;
border-left: 3px solid #00d4ff;
padding-left: 15px;
}
.glitch-text {
font-size: 1.2rem;
letter-spacing: 2px;
color: #fff;
text-transform: uppercase;
margin: 0;
font-weight: 900;
}
.header-line {
height: 1px;
background: linear-gradient(90deg, #00d4ff, transparent);
margin-top: 5px;
width: 200px;
}
.quests-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 15px;
position: relative;
z-index: 2;
}
.quest-card {
background: rgba(10, 15, 24, 0.95) !important;
border: 1px solid #1a2638 !important;
border-radius: 2px !important;
padding: 20px !important;
transition: border-color 0.3s ease;
}
/* Стан готовності - міняємо бордер на Cyan замість фіолетового */
.quest-card.ready {
border-color: #00ff88 !important; /* Зеленуватий акцент для готових квестів */
box-shadow: inset 0 0 10px rgba(0, 255, 136, 0.05);
}
.quest-main h3 {
margin: 0 0 10px 0;
font-size: 1rem;
color: #00d4ff;
text-transform: uppercase;
}
.quest-description {
font-size: 11px;
color: #4a5d75;
line-height: 1.4;
margin-bottom: 15px;
}
.section-label {
font-size: 8px;
color: #4a5d75;
margin: 15px 0 8px 0;
letter-spacing: 1px;
font-weight: 900;
text-transform: uppercase;
}
.objective-item {
margin-bottom: 12px;
}
.objective-info {
display: flex;
justify-content: space-between;
font-size: 10px;
margin-bottom: 4px;
color: #fff;
}
.objective-progress-track {
height: 3px;
background: #05080c;
border: 1px solid #1a2638;
}
.objective-progress-fill {
height: 100%;
background: #00d4ff;
box-shadow: 0 0 5px rgba(0, 212, 255, 0.3);
transition: width 0.5s ease;
}
.rewards-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.reward-pill {
padding: 4px 8px;
background: rgba(0, 0, 0, 0.4);
border-left: 2px solid #00d4ff;
font-size: 10px;
color: #fff;
font-family: "Space Mono", monospace;
}
.reward-pill.credits {
border-left-color: #00ff88;
color: #00ff88;
}
.reward-pill.xp {
border-left-color: #00d4ff;
}
.claim-btn {
width: 100%;
margin-top: 20px;
background: #00ff88;
border: none;
color: #000;
padding: 10px;
cursor: pointer;
font-size: 10px;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 2px;
transition: all 0.2s;
}
.claim-btn:hover {
background: #00cc6e;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 255, 136, 0.2);
}
.no-quests {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: #4a5d75;
border: 1px dashed #1a2638;
}
.no-quests i {
font-size: 2rem;
margin-bottom: 10px;
display: block;
}
.quest-tabs-nav {
display: flex;
gap: 20px;
margin-top: 15px;
}
.nav-btn {
background: transparent;
border: none;
color: #4a5d75;
font-family: "Space Mono", monospace;
font-size: 10px;
font-weight: 900;
letter-spacing: 1px;
cursor: pointer;
padding: 5px 0;
position: relative;
transition: color 0.3s;
text-transform: uppercase;
}
.nav-btn.active {
color: #00d4ff;
}
.nav-btn.active::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background: #00d4ff;
box-shadow: 0 0 8px #00d4ff;
}
.completed-stamp {
margin-top: 20px;
padding: 10px;
border: 1px solid #00ff88;
color: #00ff88;
text-align: center;
font-size: 10px;
font-weight: 900;
letter-spacing: 2px;
background: rgba(0, 255, 136, 0.05);
}
.quest-card.completed {
opacity: 0.7;
border-color: #1a2638 !important;
}

View File

@ -1,42 +0,0 @@
.shop-container {
max-width: 1200px;
margin: 0 auto;
max-height: calc(100vh - 232px); /* Adjusted for title bar and padding */
overflow-y: auto;
padding: 0.5rem;
}
.shop-categories {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
justify-content: center;
}
.shop-cat-btn {
padding: 0.75rem 1.5rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.3s ease;
}
.shop-cat-btn:hover {
border-color: var(--primary-color);
color: var(--text-primary);
}
.shop-cat-btn.active {
background: var(--gradient-primary);
color: var(--bg-primary);
border-color: transparent;
}
.shop-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}

View File

@ -1,133 +0,0 @@
.skills-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 20px;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
.skills-header {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(0, 212, 255, 0.1);
}
.header-main {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-main h2 {
margin: 0;
font-size: 1.2rem;
color: #fff;
text-transform: uppercase;
letter-spacing: 2px;
display: flex;
align-items: center;
gap: 10px;
}
.header-main h2 i {
color: #00d4ff;
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
.skill-points-badge {
background: rgba(0, 212, 255, 0.1);
border: 1px solid #00d4ff;
padding: 5px 15px;
display: flex;
align-items: center;
gap: 10px;
}
.skill-points-badge .label {
font-size: 9px;
color: #4a5d75;
text-transform: uppercase;
font-weight: 900;
}
.skill-points-badge .value {
font-size: 1.1rem;
color: #fff;
font-family: "Space Mono", monospace;
font-weight: 900;
}
.skills-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 15px;
overflow-y: auto;
padding-right: 10px;
flex: 1;
}
.custom-scroll::-webkit-scrollbar {
width: 4px;
}
.custom-scroll::-webkit-scrollbar-track {
background: rgba(5, 8, 12, 0.5);
}
.custom-scroll::-webkit-scrollbar-thumb {
background: #1a2638;
border-radius: 2px;
}
.custom-scroll::-webkit-scrollbar-thumb:hover {
background: #00d4ff;
}
.empty-category {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px;
color: #4a5d75;
text-align: center;
border: 1px dashed #1a2638;
background: rgba(0, 0, 0, 0.2);
}
.empty-category i {
font-size: 2rem;
margin-bottom: 15px;
opacity: 0.5;
}
.empty-category p {
font-family: "Space Mono", monospace;
font-size: 12px;
text-transform: uppercase;
}
@media (max-width: 600px) {
.skills-container {
padding: 10px;
}
.header-main {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.skill-points-badge {
width: 100%;
justify-content: space-between;
box-sizing: border-box;
}
.skills-grid {
grid-template-columns: 1fr;
}
}

View File

@ -1,60 +0,0 @@
.loading-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-primary);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
transition: opacity 0.5s ease;
}
.loading-content {
text-align: center;
max-width: 400px;
}
.game-title {
font-family: 'Orbitron', sans-serif;
font-size: 3rem;
font-weight: 900;
background: var(--gradient-primary);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 2rem;
text-transform: uppercase;
letter-spacing: 3px;
}
.loading-bar {
width: 100%;
height: 4px;
background: var(--bg-tertiary);
border-radius: 2px;
overflow: hidden;
margin-bottom: 1rem;
}
.loading-progress {
height: 100%;
background: var(--gradient-primary);
width: 0%;
transition: width 0.3s ease;
animation: loading-pulse 2s infinite;
}
@keyframes loading-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.loading-text {
color: var(--text-secondary);
font-size: 0.9rem;
}

View File

@ -1,26 +0,0 @@
import React, { useState, useEffect } from "react";
import "./LoadingScreen.css";
const LoadingScreen = ({
progress = 0,
statusText = "Initializing Universe...",
}) => {
return (
<div id="loadingScreen" className="loading-screen">
<div className="loading-content">
<h1 className="game-title">GALAXY STRIKE ONLINE</h1>
<div className="loading-bar">
<div
className="loading-progress"
style={{ width: `${progress}%`, transition: "width 0.3s ease" }}
></div>
</div>
<p className="loading-text">{statusText}</p>
</div>
</div>
);
};
export default LoadingScreen;

View File

@ -1,141 +0,0 @@
.main-menu {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-primary);
display: flex;
align-items: center;
justify-content: center;
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%
);
}
.menu-container {
width: 90%;
max-width: 800px;
background: var(--card-bg);
border-radius: 20px;
border: 1px solid var(--border-color);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
overflow: hidden;
}
.menu-header {
text-align: center;
padding: 40px 20px 20px;
position: relative;
}
.menu-title {
font-family: "Orbitron", sans-serif;
font-size: 3rem;
font-weight: 900;
color: var(--text-primary);
text-transform: uppercase;
letter-spacing: 3px;
margin-bottom: 10px;
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
}
@media screen and (max-width: 500px) {
.menu-title {
font-size: 24px;
}
}
.menu-subtitle {
font-size: 1.2rem;
color: var(--text-secondary);
font-weight: 400;
opacity: 0.9;
}
.menu-content {
padding: 30px;
}
.menu-section {
animation: fadeInUp 0.5s ease-out;
}
.section-title {
font-family: "Orbitron", sans-serif;
font-size: 1.8rem;
color: var(--primary-color);
text-align: center;
margin-bottom: 30px;
text-transform: uppercase;
letter-spacing: 2px;
}
/*footer*/
.menu-footer {
padding: 20px 30px;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.version-text {
color: var(--text-muted);
font-size: 0.9rem;
}
.footer-links {
display: flex;
gap: 20px;
}
.link-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 0.9rem;
transition: color 0.3s ease;
}
.link-btn:hover {
color: var(--primary-color);
}
/* Login Section */
.login-options {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 20px;
}
.btn-large {
padding: 20px 40px;
font-size: 1.2rem;
font-weight: 600;
border-radius: 12px;
transition: all 0.3s ease;
gap: 10px;
width: auto;
}
.btn-large i {
font-size: 1.4rem;
}

View File

@ -1,65 +0,0 @@
import React, { useEffect, useState } from "react";
import LoginSection from "./sections/LoginSection";
import ServerSection from "./sections/ServerSection";
import OptionsSection from "./sections/OptionsSection";
import "./MainMenu.css";
import { useAuth } from "../../hooks/useAuth";
const MainMenu = ({ onStartGame }) => {
const [menuView, setMenuView] = useState("LOGIN");
const [selectedServer, setSelectedServer] = useState(null);
const { user, logout } = useAuth();
useEffect(() => {
if (user) {
setMenuView("SERVERS");
} else {
setMenuView("LOGIN");
setSelectedServer(null);
}
}, [user]);
return (
<div id="mainMenu" className="main-menu">
<div className="menu-container">
<div className="menu-header">
<h1 className="menu-title">GALAXY STRIKE ONLINE</h1>
<p className="menu-subtitle">Space Idle MMORPG</p>
</div>
<div className="menu-content">
{menuView === "LOGIN" && (
<LoginSection onLogin={() => setMenuView("SERVERS")} />
)}
{menuView === "SERVERS" && (
<ServerSection
onBack={() => {
logout();
setMenuView("LOGIN");
}}
onSelect={(server) => {
setSelectedServer(server);
setMenuView("OPTIONS");
}}
onContinue={() => onStartGame()}
/>
)}
{menuView === "OPTIONS" && (
<OptionsSection
server={selectedServer}
onContinue={onStartGame}
onBack={() => {
setSelectedServer(null);
setMenuView("SERVERS");
}}
/>
)}
</div>
</div>
</div>
);
};
export default MainMenu;

View File

@ -1,56 +0,0 @@
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.form-input {
background: var(--bg-tertiary);
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 12px 16px;
color: var(--text-primary);
font-family: "Space Mono", monospace;
font-size: 1rem;
transition: all 0.3s ease;
}
.form-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 10px rgba(0, 212, 255, 0.3);
}
.form-input::placeholder {
color: var(--text-muted);
}
.access-title {
font-size: 30px;
padding-bottom: 10px;
color: var(--primary-color);
text-align: center;
}
@media screen and (max-width: 500px) {
.section-title {
font-size: 12px;
}
.access-title {
font-size: 20px;
}
}

View File

@ -1,140 +0,0 @@
import React, { useState } from "react";
import Button from "../../../components/ui/Button";
import Input from "../../../components/ui/Input";
import "./LoginSection.css";
import { useAuth } from "../../../hooks/useAuth.js";
const LoginSection = () => {
const { login, register, user } = useAuth();
const [formData, setFormData] = useState({
username: "",
email: "",
password: "",
});
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
const handleLogin = async () => {
setLoading(true);
setError(null);
const result = await login(formData.email, formData.password);
if (!result?.success) {
setError(result?.error || "Failed to login");
}
setLoading(false);
};
const handleRegister = async () => {
if (!formData.username) {
setError("Username is required for registration");
return;
}
setLoading(true);
setError(null);
const result = await register(
formData.username,
formData.email,
formData.password,
);
if (!result?.success) {
setError(result?.error || "Failed to register");
}
setLoading(false);
};
if (user) {
return (
<div className="menu-section">
<h2 className="menu-title">Welcome, {user.username}!</h2>
<p>You are now connected to the API.</p>
<Button variant="secondary" onClick={() => window.location.reload()}>
Go to Game
</Button>
</div>
);
}
return (
<div id="loginSection" className="menu-section">
<h2 className="access-title">Account Access</h2>
<div className="login-form">
{error && (
<div
className="error-message"
style={{ color: "#ff4d4d", marginBottom: "10px" }}
>
{error}
</div>
)}
<Input
label="Username (for Register)"
type="text"
name="username"
placeholder="Enter username"
value={formData.username}
onChange={handleChange}
/>
<Input
label="Email"
type="email"
name="email"
placeholder="Enter your email"
value={formData.email}
onChange={handleChange}
/>
<Input
label="Password"
type="password"
name="password"
placeholder="Enter your password"
value={formData.password}
onChange={handleChange}
/>
<div className="login-options">
<Button
variant="primary"
size="large"
icon="fa-sign-in-alt"
onClick={handleLogin}
disabled={loading}
>
{loading ? "Processing..." : "Login"}
</Button>
<Button
variant="secondary"
size="large"
icon="fa-user-plus"
onClick={handleRegister}
disabled={loading}
>
Register
</Button>
</div>
</div>
<div className="login-notice">
<p>
<i className="fas fa-info-circle"></i> Connect to the live server to
play
</p>
</div>
</div>
);
};
export default LoginSection;

View File

@ -1,136 +0,0 @@
/* Базова сітка та контейнери */
.options-grid {
display: flex;
justify-content: space-between;
gap: 30px;
margin-bottom: 30px;
align-items: stretch;
}
.options-left,
.options-right {
display: flex;
flex-direction: column;
gap: 15px;
min-width: 220px;
}
.options-left {
justify-content: flex-end; /* Кнопка Back буде знизу зліва */
}
.options-right {
justify-content: flex-end; /* Кнопка Continue буде знизу справа */
}
.options-center {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1;
min-width: 320px;
}
/* Картка сервера (Центр) */
.save-info-display {
background: rgba(0, 212, 255, 0.05);
border: 1px solid rgba(0, 212, 255, 0.3);
box-shadow: 0 0 20px rgba(0, 212, 255, 0.1);
border-radius: 12px;
padding: 25px;
text-align: center;
width: 100%;
position: relative;
overflow: hidden;
}
/* Ефект "Скляної панелі" для заголовка */
.save-info-title {
color: #00d4ff;
margin-bottom: 20px;
font-size: 1.1em;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 800;
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
padding-bottom: 10px;
}
/* Текстові блоки всередині картки */
.server-status-text {
font-size: 0.8em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 4px;
}
.server-highlight {
font-size: 1.8em;
font-weight: 900;
color: #ffffff;
text-shadow: 0 0 15px rgba(0, 212, 255, 0.6);
margin-bottom: 8px;
}
.server-url {
font-family: "Share Tech Mono", monospace;
color: #00d4ff;
font-size: 0.85em;
background: rgba(0, 0, 0, 0.3);
padding: 4px 10px;
border-radius: 4px;
display: inline-block;
}
/* Блок опису та регіону */
.server-extra-info {
margin-top: 20px;
padding-top: 15px;
border-top: 1px dashed rgba(255, 255, 255, 0.1);
}
.region-tag {
color: #ff9d00; /* Колір для акценту на регіоні */
font-weight: bold;
font-size: 0.9em;
}
.server-description-text {
color: rgba(255, 255, 255, 0.8);
font-size: 0.95em;
line-height: 1.5;
margin-top: 8px;
font-style: italic;
}
/* Кнопки */
.options-btn {
height: 40px;
font-size: 12px;
margin: 5px;
transition: all 0.3s ease;
}
/* Адаптивність */
@media (max-width: 768px) {
.options-grid {
flex-direction: column;
align-items: center;
}
.options-btn {
width: 100%;
}
.options-center {
order: -1;
width: 100%;
}
.options-left,
.options-right {
width: 100%;
align-items: center;
}
}

View File

@ -1,76 +0,0 @@
import React, { useEffect } from "react";
import Button from "../../../components/ui/Button";
import "./OptionsSection.css";
const OptionsSection = ({ server, onBack, onContinue }) => {
const handleContinue = () => {
const authData = JSON.parse(localStorage.getItem("user"));
const token = authData?.token;
if (!token || !server?.connectUrl) return;
localStorage.setItem("activeServer", JSON.stringify(server));
onContinue(server, token);
};
useEffect(() => {
console.log(server);
}, []);
return (
<div id="optionsSection" className="menu-section">
<h2 className="server-section-title">
{server?.serverName || "Game Options"}
</h2>
<div className="server-details">
<div className="server-info-tags">
{server?.isOfficial ? (
<div className="badge official">
<i className="fas fa-check-circle"></i> Official
</div>
) : (
<div className="badge modded">
<i className="fas fa-tools"></i> Modded
</div>
)}
{server?.region && (
<span className="badge region">
<i className="fas fa-globe-europe"></i> {server.region}
</span>
)}
</div>
{server?.description && (
<div className="server-description">
<p>{server.description}</p>
</div>
)}
</div>
<div className="options-actions">
<Button
variant="primary"
size="large"
icon="fa-gamepad"
onClick={handleContinue}
className="options-btn"
>
Connect to Server
</Button>
<Button
variant="secondary"
size="large"
icon="fa-arrow-left"
onClick={onBack}
className="options-btn back-btn"
>
Back to List
</Button>
</div>
</div>
);
};
export default OptionsSection;

View File

@ -1,49 +0,0 @@
import Button from '../../../components/ui/Button';
import "./ServerConfirmationSection.css";
const ServerConfirmSection = ({ serverData, onBack, onJoin }) => {
const server = serverData || {
name: "Server Name",
type: "Public",
region: "US East",
players: "0/10",
owner: "Unknown"
};
return (
<div id="serverConfirmSection" className="menu-section">
<h2 className="section-title">Server Selected</h2>
<div className="server-confirmation">
<div className="confirm-actions-left">
<Button variant="primary" size="large" icon="fa-sign-in-alt" onClick={onJoin}>
Join Server
</Button>
</div>
<div className="selected-server-info-center">
<div className="server-preview">
<h3 id="selectedServerName">{server.name}</h3>
<div className="server-details">
<p className="server-info">Type: <span>{server.type}</span></p>
<p className="server-info">Region: <span>{server.region}</span></p>
<p className="server-info">Players: <span>{server.players}</span></p>
<p className="server-info">Owner: <span>{server.owner}</span></p>
</div>
</div>
</div>
<div className="confirm-actions-right">
<Button variant="info" size="large" icon="fa-info">
More Info
</Button>
<Button variant="warning" size="large" icon="fa-arrow-left" onClick={onBack}>
Back to List
</Button>
</div>
</div>
</div>
);
};
export default ServerConfirmSection;

View File

@ -1,59 +0,0 @@
.server-confirmation {
display: flex;
justify-content: space-between;
align-items: center;
gap: 30px;
margin-bottom: 30px;
}
.server-preview {
background: var(--bg-tertiary);
border: 2px solid var(--primary-color);
border-radius: 15px;
padding: 25px;
text-align: center;
min-width: 300px;
box-shadow: 0 10px 30px rgba(0, 212, 255, 0.2);
}
.server-preview h3 {
color: var(--primary-color);
font-size: 1.4rem;
font-weight: 700;
margin-bottom: 15px;
text-transform: uppercase;
letter-spacing: 1px;
}
.server-details {
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.6;
}
.server-info {
margin: 8px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.server-info span {
font-weight: 600;
color: var(--text-primary);
}
.confirm-actions-left, .confirm-actions-right {
display: flex;
flex-direction: column;
gap: 15px;
min-width: 200px;
}
.selected-server-info-center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -1,265 +0,0 @@
.server-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.server-filters {
display: flex;
gap: 10px;
}
.filter-select {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 2px;
padding: 8px 12px;
color: var(--text-primary);
font-family: "Orbitron", sans-serif;
font-size: 0.8rem;
cursor: pointer;
text-transform: uppercase;
}
.filter-select:focus {
outline: none;
border-color: var(--primary-color);
}
.server-list {
max-height: calc(100vh - 350px);
overflow-y: auto;
margin-bottom: 20px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: rgba(0, 0, 0, 0.2);
padding: 10px;
}
.server-card {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-secondary);
padding: 12px 20px;
margin-bottom: 8px;
border-radius: 2px;
border: 1px solid var(--border-color);
position: relative;
}
.server-card::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--primary-color);
box-shadow: 0 0 10px var(--primary-color);
opacity: 0.6;
}
.server-card:hover {
background: var(--bg-tertiary);
border-color: var(--primary-color);
}
.server-card:hover::before {
opacity: 1;
}
.server-info {
display: flex;
align-items: center;
flex: 1;
}
.server-name {
font-family: "Orbitron", sans-serif;
font-size: 1.1rem;
font-weight: 700;
color: var(--primary-color);
margin-right: 20px;
text-transform: uppercase;
letter-spacing: 1px;
}
.server-players {
font-family: "Space Mono", monospace;
font-size: 0.85rem;
color: var(--text-secondary);
background: rgba(0, 0, 0, 0.3);
padding: 2px 8px;
border-radius: 4px;
max-width: 100px;
}
.status-indicator.online {
width: 10px;
height: 10px;
background: var(--success-color);
border-radius: 50%;
box-shadow: 0 0 10px var(--success-color);
margin-right: 15px;
animation: status-pulse 2s infinite ease-in-out;
}
@keyframes status-pulse {
0%,
100% {
opacity: 0.7;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.15);
}
}
.server-section-title {
font-family: "Orbitron", sans-serif;
font-size: 1.5rem;
margin-bottom: 20px;
text-align: center;
color: var(--primary-color);
text-transform: uppercase;
letter-spacing: 4px;
}
.server-loading,
.server-empty {
text-align: center;
padding: 40px;
color: var(--text-muted);
font-family: "Orbitron", sans-serif;
}
.server-loading i {
font-size: 2rem;
margin-bottom: 10px;
color: var(--primary-color);
}
/* Кнопки */
.server-actions {
display: flex;
justify-content: center;
margin-top: 20px;
}
.galaxy-button {
height: 35px;
min-width: 120px;
}
.search-wrapper {
position: relative;
flex: 1;
min-width: 200px;
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--primary-color);
opacity: 0.6;
}
.server-search-input {
width: 100%;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 2px;
padding: 10px 12px 10px 35px;
color: var(--text-primary);
font-family: "Orbitron", sans-serif;
font-size: 0.85rem;
transition: all 0.3s ease;
}
.server-search-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 10px rgba(0, 212, 255, 0.2);
}
.control-group {
display: flex;
gap: 10px;
align-items: center;
}
.server-details-text {
display: flex;
flex-direction: column;
}
@media screen and (max-width: 600px) {
.server-card {
flex-direction: column;
text-align: center;
button {
font-size: 12px;
}
}
.server-info {
flex-direction: column;
gap: 10px;
}
.server-name {
margin-right: 0;
font-size: 12px;
}
.status-indicator.online {
margin-right: 0;
margin-bottom: 5px;
}
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 15px;
}
.status-indicator.online {
background: var(--success-color);
box-shadow: 0 0 10px var(--success-color);
animation: status-pulse 2s infinite ease-in-out;
}
.status-indicator.offline {
background: #555;
box-shadow: none;
animation: none;
opacity: 0.5;
}
@keyframes status-pulse {
0%,
100% {
opacity: 0.7;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.15);
}
}
@media screen and (max-width: 600px) {
.status-indicator.online,
.status-indicator.offline {
margin-right: 0;
margin-bottom: 5px;
}
}

View File

@ -1,152 +0,0 @@
import React, { useState, useEffect } from "react";
import axios from "axios";
import Button from "../../../components/ui/Button";
import "./ServerSection.css";
const ServerSection = ({ onBack, onSelect }) => {
const [servers, setServers] = useState([]);
const [loading, setLoading] = useState(false);
const [joiningId, setJoiningId] = useState(null);
const [searchTerm, setSearchTerm] = useState("");
const fetchServers = async () => {
setLoading(true);
const startTime = Date.now();
const API_URL = import.meta.env.VITE_API_URL;
try {
const response = await axios.get(`${API_URL}/api/servers/list`);
const elapsedTime = Date.now() - startTime;
const remainingTime = Math.max(0, 2000 - elapsedTime);
await new Promise((resolve) => setTimeout(resolve, remainingTime));
setServers(response.data);
} catch (error) {
console.error("Error fetching servers:", error);
} finally {
setLoading(false);
}
};
const handleJoinServer = async (server) => {
setJoiningId(server._id);
const API_URL = import.meta.env.VITE_API_URL;
try {
const token = JSON.parse(localStorage.getItem("user")).token;
const response = await axios.post(
`${API_URL}/api/servers/join`,
{ serverId: server._id },
{ headers: { Authorization: `Bearer ${token}` } },
);
if (response.data.success) {
console.log(response.data);
onSelect(response.data);
}
} catch (error) {
console.error("Join request failed:", error);
} finally {
setJoiningId(null);
}
};
useEffect(() => {
fetchServers();
}, []);
const filteredServers = servers.filter((server) =>
searchTerm.length > 0
? server.name?.toLowerCase().includes(searchTerm.toLowerCase())
: true,
);
return (
<div id="serverSection" className="menu-section">
<h2 className="server-section-title">Galaxy Browser</h2>
<div className="server-controls">
<div className="search-wrapper">
<i className="fas fa-search search-icon"></i>
<input
type="text"
className="server-search-input"
placeholder="Search sector..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="control-group">
<Button
variant="secondary"
icon="fa-sync"
onClick={fetchServers}
disabled={loading}
>
{loading ? "Scanning..." : "Refresh"}
</Button>
<div className="server-filters">
<select className="filter-select">
<option value="">All Regions</option>
<option value="europe">Europe</option>
<option value="ukraine">Ukraine</option>
</select>
</div>
</div>
</div>
<div className="server-list">
{loading ? (
<div className="server-loading">
<i className="fas fa-spinner fa-spin"></i>
<p>Synchronizing with Star-Net...</p>
</div>
) : filteredServers.length > 0 ? (
filteredServers.map((server) => (
<div key={server._id || server.serverName} className="server-card">
<div className="server-info">
<div
className={`status-indicator ${server.status === "online" ? "online" : "offline"}`}
></div>
<div className="server-details-text">
<span className="server-name">{server.name}</span>
<span className="server-players">
{server.playersOnline || 0} / {server.maxPlayers || 100}
</span>
</div>
</div>
<Button
variant="primary"
size="small"
onClick={() => handleJoinServer(server)}
>
SELECT
</Button>
</div>
))
) : (
<div className="server-empty">
<i className="fas fa-satellite-dish"></i>
<p>
{searchTerm
? "No sectors match your search."
: "No active universes found."}
</p>
</div>
)}
</div>
<div className="server-actions">
<Button variant="secondary" icon="fa-arrow-left" onClick={onBack}>
Back to Login
</Button>
</div>
</div>
);
};
export default ServerSection;

View File

@ -1,7 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

View File

@ -1,98 +0,0 @@
### Dungeon Datapack Documentation
### Dungeon Configuration
**Path:** `datapacks/[namespace]/dungeons/[path]/dungeon.json`
```json
{
"dungeon": {
"id": "Unique system ID (namespace:path/name)",
"displayName": "Translation key for the dungeon name",
"description": "Translation key for the atmospheric location description",
"meta": {
"energyCost": "Amount of energy required for entry",
"repeatable": "Boolean (true/false): whether the dungeon can be replayed",
"raid": "Whether this dungeon is designed for a group of players"
},
"rooms": [
{ "id": "Reference to a specific Room ID from the available rooms pool" }
]
}
}
```
### Room Configuration
**Path:** `datapacks/[namespace]/rooms/[path]/room.json`
```json
{
"room": {
"id": "Unique system ID (namespace:path/name)",
"displayName": "Translation key for the UI display name",
"description": "Translation key for the atmospheric flavor text shown upon entry",
"hostiles": [
"Array of Enemy IDs to spawn in this room (e.g., ['id1', 'id2'])"
],
"gainXp": "Fixed amount of experience awarded for clearing the room",
"credits": "Fixed amount of currency guaranteed to drop",
"loot": [],
"meta": {
"isBossRoom": "Boolean: whether to trigger boss-fight logic"
}
}
}
```
### Hostile Configuration
Path: datapacks/[namespace]/enemies/hostiles/[path]/enemy.json
```json
{
"hostile": {
"id": "Unique system ID (namespace:path/name)",
"displayName": "Translation key for the enemy's name",
"stats": {
"health": "Numerical HP value",
"defense": "Numerical defense value",
"damage": "Numerical base damage value",
"critical.chance": "Critical hit chance",
"attack.rate": "Attack speed (seconds)"
},
"loot": [],
"meta": {}
}
}
```
### Loot System Specification
This structure is used to define item drops within **Rooms** (on completion) or from **Hostiles** (on death).
```json
{
"loot": [
{
"id": "Unique Item ID (namespace:items/item_name)",
"chance": "Probability of dropping, from 0.0 (0%) to 1.0 (100%)",
"count": {
"min": "Minimum number of items to drop if the chance succeeds",
"max": "Maximum number of items to drop if the chance succeeds"
}
},
{
"id": "Unique Item ID",
"chance": "Probability of dropping",
"count": "Fixed integer value if the item amount is always constant"
}
]
}
```
### Project Examples Reference
- **Dungeon Example:** [crystal_mine](example/datapack/ex/data/dungeons/caverns/crystal_mine.json)
- **Room Examples:** \* [crystal_hallway](example/datapack/ex/data/enemies/rooms/caverns/crystal_hallway.json)
- **Hostile Example:** [crystal_guardian](example/datapack/ex/data/enemies/hostiles/caverns/crystal_guardian.json)

View File

@ -1,17 +0,0 @@
{
"dungeon": {
"id": "original:caverns/crystal_mine",
"displayName": "dungeons.original.caverns.crystal_mine.name",
"description": "dungeons.original.caverns.crystal_mine.desc",
"meta": {
"energyCost": 15,
"repeatable": true,
"raid": false
},
"rooms": [
{ "id": "original:caverns/crystal_hallway" },
{ "id": "original:caverns/crystal_hallway" },
{ "id": "original:caverns/core_vault" }
]
}
}

View File

@ -1,36 +0,0 @@
{
"hostile": {
"id": "original:caverns/crystal_guardian",
"displayName": "enemies.original.caverns.crystal_guardian.name",
"stats": {
"health": 120,
"defense": 0.25,
"damage": 18.0,
"critical,chance": 0.15,
"attack.rate": 2.5
},
"loot": [
{
"id": "original:ore_coal",
"chance": 0.8,
"count": { "min": 2, "max": 5 }
},
{
"id": "original:ore_copper",
"chance": 0.6,
"count": { "min": 1, "max": 3 }
},
{
"id": "original:crystal_flux_core",
"chance": 0.15,
"count": 1
},
{
"id": "original:crystal_dimentional",
"chance": 0.02,
"count": 1
}
],
"meta": {}
}
}

View File

@ -1,28 +0,0 @@
{
"room": {
"id": "original:caverns/core_vault",
"displayName": "rooms.original.caverns.core_vault.name",
"description": "rooms.original.caverns.core_vault.desc",
"hostiles": [
"original:caverns/crystal_guardian",
"original:pirate/boarding_drone"
],
"gainXp": 120,
"credits": 850,
"loot": [
{
"id": "original:crystal_flux_core",
"chance": 0.5,
"count": 1
},
{
"id": "original:ore_copper",
"chance": 1.0,
"count": { "min": 5, "max": 10 }
}
],
"meta": {
"isBossRoom": true
}
}
}

View File

@ -1,23 +0,0 @@
{
"room": {
"id": "original:caverns/crystal_hallway",
"displayName": "rooms.original.caverns.crystal_hallway.name",
"description": "rooms.original.caverns.crystal_hallway.desc",
"hostiles": [
"original:pirate/boarding_drone",
"original:pirate/boarding_drone"
],
"gainXp": 45,
"credits": 250,
"loot": [
{
"id": "original:ore_coal",
"chance": 1.0,
"count": { "min": 3, "max": 6 }
}
],
"meta": {
"isBossRoom": false
}
}
}

Some files were not shown because too many files have changed in this diff Show More