This commit is contained in:
parent
4075f56099
commit
7166cbc45f
@ -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.
|
||||
191
Panel_Admin.md
191
Panel_Admin.md
@ -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.*"]
|
||||
@ -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": {
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -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
24
client/.gitignore
vendored
@ -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?
|
||||
@ -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.
|
||||
@ -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_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
@ -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>
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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 |
@ -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 |
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
},
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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>,
|
||||
);
|
||||
@ -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();
|
||||
@ -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;
|
||||
@ -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();
|
||||
@ -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')
|
||||
}
|
||||
});
|
||||
@ -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
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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">></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;
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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}>
|
||||
×
|
||||
</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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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}>
|
||||
×
|
||||
</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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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}>
|
||||
×
|
||||
</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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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}>
|
||||
×
|
||||
</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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
@ -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%;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
@ -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)
|
||||
@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -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": {}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
Loading…
Reference in New Issue
Block a user