Fix GameTooltip blocking issue and translate Found string
This commit is contained in:
@@ -442,7 +442,7 @@ async def get_current_location(request: Request, current_user: dict = Depends(ge
|
|||||||
"id": action.id,
|
"id": action.id,
|
||||||
"name": action.label,
|
"name": action.label,
|
||||||
"stamina_cost": action.stamina_cost,
|
"stamina_cost": action.stamina_cost,
|
||||||
"description": f"Costs {action.stamina_cost} stamina",
|
"description": get_game_message('costs_stamina', locale, cost=action.stamina_cost),
|
||||||
"on_cooldown": is_on_cooldown,
|
"on_cooldown": is_on_cooldown,
|
||||||
"cooldown_remaining": remaining_cooldown
|
"cooldown_remaining": remaining_cooldown
|
||||||
})
|
})
|
||||||
@@ -1053,7 +1053,7 @@ async def interact(
|
|||||||
"instance_id": interact_req.interactable_id,
|
"instance_id": interact_req.interactable_id,
|
||||||
"action_id": interact_req.action_id,
|
"action_id": interact_req.action_id,
|
||||||
"cooldown_remaining": cooldown_remaining,
|
"cooldown_remaining": cooldown_remaining,
|
||||||
"message": f"{current_user['name']} used {get_locale_string(action_display, locale)} on {get_locale_string(interactable_name, locale)}"
|
"message": get_game_message('interactable_cooldown', locale, user=current_user, interactable=get_locale_string(interactable_name, locale), action=get_locale_string(action_display, locale)),
|
||||||
},
|
},
|
||||||
"timestamp": datetime.utcnow().isoformat()
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,10 +88,12 @@ GAME_MESSAGES = {
|
|||||||
|
|
||||||
# Interaction
|
# Interaction
|
||||||
'not_enough_stamina': {'en': "Not enough stamina. Need {cost}, have {current}.", 'es': "No tienes suficiente stamina. Necesitas {cost}, tienes {current}."},
|
'not_enough_stamina': {'en': "Not enough stamina. Need {cost}, have {current}.", 'es': "No tienes suficiente stamina. Necesitas {cost}, tienes {current}."},
|
||||||
|
'costs_stamina': {'en': "Costs {cost} stamina", 'es': "Cuesta {cost} de aguante"},
|
||||||
'cooldown_wait': {'en': "This action is still on cooldown. Wait {seconds} seconds.", 'es': "Esta acción está en enfriamiento. Espera {seconds} segundos."},
|
'cooldown_wait': {'en': "This action is still on cooldown. Wait {seconds} seconds.", 'es': "Esta acción está en enfriamiento. Espera {seconds} segundos."},
|
||||||
'object_not_found': {'en': "Object not found", 'es': "Objeto no encontrado"},
|
'object_not_found': {'en': "Object not found", 'es': "Objeto no encontrado"},
|
||||||
'action_not_found': {'en': "Action not found", 'es': "Acción no encontrada"},
|
'action_not_found': {'en': "Action not found", 'es': "Acción no encontrada"},
|
||||||
'action_no_outcomes': {'en': "Action has no defined outcomes", 'es': "La acción no tiene resultados definidos"},
|
'action_no_outcomes': {'en': "Action has no defined outcomes", 'es': "La acción no tiene resultados definidos"},
|
||||||
|
'interactable_cooldown': {'en': "{user} used {action} on {interactable}", 'es': "{user} usó {action} en {interactable}"},
|
||||||
|
|
||||||
# Item Usage
|
# Item Usage
|
||||||
'item_used': {'en': "Used {name}", 'es': "Usado {name}"},
|
'item_used': {'en': "Used {name}", 'es': "Usado {name}"},
|
||||||
|
|||||||
139
docs/visual_style_guide.md
Normal file
139
docs/visual_style_guide.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Visual Style Guide - Echoes of the Ash
|
||||||
|
|
||||||
|
This document defines the unified visual system for the application, ensuring a consistent, mature "videogame" aesthetic.
|
||||||
|
|
||||||
|
## 1. Core Design Philosophy
|
||||||
|
- **Aesthetic**: **Mature Post-Apocalyptic**. Think "High-Tech Scavenger".
|
||||||
|
- **Dark & Gritty**: Deep blacks (`#0a0a0a`) mixed with industrial dark greys (`#1a1a20`).
|
||||||
|
- **Sharp & Distinct**: Avoid overly rounded "Web 2.0" corners. Use chamfered corners (clip-path) or tight radii (2px-4px) for a militaristic/industrial feel.
|
||||||
|
- **Glassmorphism**: Use semi-transparent backgrounds with blur (`backdrop-filter`) to keep the player connected to the world.
|
||||||
|
- **Cinematic**: High contrast text, subtle glows on active elements, and distinct borders.
|
||||||
|
- **Interaction**: **Instant Feedback**.
|
||||||
|
- **Custom Tooltips**: **MANDATORY**. Do NOT use the HTML `title` attribute. All information must appear instantly in a custom game-styled tooltip anchor.
|
||||||
|
- **Micro-animations**: Subtle pulses, border glows on hover.
|
||||||
|
|
||||||
|
## 2. CSS Variables (Design Tokens)
|
||||||
|
Defined in `:root`.
|
||||||
|
|
||||||
|
### Colors
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* Backgrounds */
|
||||||
|
--game-bg-app: #050505; /* Deepest black */
|
||||||
|
--game-bg-panel: rgba(18, 18, 24, 0.96); /* Almost solid panels */
|
||||||
|
--game-bg-glass: rgba(10, 10, 15, 0.85); /* Overlays */
|
||||||
|
--game-bg-slot: rgba(0, 0, 0, 0.5); /* Item slots - darker than panels */
|
||||||
|
--game-bg-slot-hover: rgba(255, 255, 255, 0.08);
|
||||||
|
|
||||||
|
/* Borders / Separators */
|
||||||
|
--game-border-color: rgba(255, 255, 255, 0.12);
|
||||||
|
--game-border-active: rgba(255, 255, 255, 0.4);
|
||||||
|
--game-border-highlight: #ff6b6b; /* Red accent border */
|
||||||
|
|
||||||
|
/* Corner Radius - Tighter for mature look */
|
||||||
|
--game-radius-xs: 2px;
|
||||||
|
--game-radius-sm: 4px;
|
||||||
|
--game-radius-md: 6px;
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--game-font-main: 'Saira Condensed', system-ui, sans-serif;
|
||||||
|
--game-text-primary: #e0e0e0; /* Off-white is better for eyes than pure white */
|
||||||
|
--game-text-secondary: #94a3b8; /* Cool grey */
|
||||||
|
--game-text-highlight: #fbbf24; /* Amber/Gold */
|
||||||
|
--game-text-danger: #ef4444;
|
||||||
|
|
||||||
|
/* Semantic Colors (Desaturated/Industrial) */
|
||||||
|
--game-color-primary: #e11d48; /* Blood Red - Action/Health */
|
||||||
|
--game-color-stamina: #d97706; /* Amber - Stamina */
|
||||||
|
--game-color-magic: #3b82f6; /* Blue - Mana/Tech */
|
||||||
|
--game-color-success: #10b981; /* Emerald - Durability High */
|
||||||
|
--game-color-warning: #f59e0b; /* Amber - Warning */
|
||||||
|
|
||||||
|
/* Rarity Colors */
|
||||||
|
--rarity-common: #9ca3af;
|
||||||
|
--rarity-uncommon: #ffffff;
|
||||||
|
--rarity-rare: #34d399;
|
||||||
|
--rarity-epic: #60a5fa;
|
||||||
|
--rarity-legendary: #fbbf24;
|
||||||
|
|
||||||
|
/* Effects */
|
||||||
|
--game-shadow-panel: 0 8px 32px rgba(0, 0, 0, 0.8);
|
||||||
|
--game-shadow-glow: 0 0 15px rgba(225, 29, 72, 0.3); /* Subtle red glow */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Tooltip System (CRITICAL)
|
||||||
|
**Goal**: Replicate a native game HUD tooltip.
|
||||||
|
**Rule**: NEVER use `title="Description"`.
|
||||||
|
|
||||||
|
### The Component: `<GameTooltip />`
|
||||||
|
Every interactive element with info must be wrapped or associated with this component.
|
||||||
|
- **Behavior**: Appears instantly on hover (0ms delay). Follows cursor OR anchored to element.
|
||||||
|
- **Visuals**:
|
||||||
|
- Background: Solid dark (`#0f0f12`) with high opacity (98%).
|
||||||
|
- Border: Thin accent border (`1px solid --game-border-color`).
|
||||||
|
- Shadow: Strong drop shadow to separate from UI.
|
||||||
|
- Content: Supports HTML (rich text, stats, icons).
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Usage Example
|
||||||
|
<GameTooltip content={<ItemStats item={sword} />}>
|
||||||
|
<button className="game-slot">
|
||||||
|
<img src="sword.png" />
|
||||||
|
</button>
|
||||||
|
</GameTooltip>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Reusable Component Classes
|
||||||
|
|
||||||
|
### Panels & Containers
|
||||||
|
**`.game-panel`**
|
||||||
|
- **Look**: Industrial, solid.
|
||||||
|
- **CSS**:
|
||||||
|
```css
|
||||||
|
background: var(--game-bg-panel);
|
||||||
|
border: 1px solid var(--game-border-color);
|
||||||
|
box-shadow: var(--game-shadow-panel);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border-radius: var(--game-radius-sm);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
**`.game-btn`**
|
||||||
|
- **Look**: Rectangular, tactile.
|
||||||
|
- **States**:
|
||||||
|
- Normal: Dark grey background, light border.
|
||||||
|
- Hover: Glow effect, border brightens.
|
||||||
|
- Active: Presses down (transform).
|
||||||
|
- **CSS**:
|
||||||
|
```css
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-family: var(--game-font-main);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px); /* Mecha-cut corners */
|
||||||
|
/* OR simple tight radius */
|
||||||
|
border-radius: 2px;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Items & Slots
|
||||||
|
**`.game-slot`**
|
||||||
|
- **Background**: `var(--game-bg-slot)` (Darker than panel).
|
||||||
|
- **Border**: `1px solid var(--game-border-color)`.
|
||||||
|
- **Interaction**: On hover, background lightens slightly, border acts as highlight.
|
||||||
|
|
||||||
|
## 5. Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Tooltip System (Priority)
|
||||||
|
1. Create `src/components/common/GameTooltip.tsx`.
|
||||||
|
2. Implement global tooltip context/store if needed for performance, or use a lightweight library like generic CSS hover + React portal.
|
||||||
|
3. **Audit**: Grep for `title=` in all files and replace with `<GameTooltip>`.
|
||||||
|
|
||||||
|
### Phase 2: Design Token Migration
|
||||||
|
1. Define all variables in [index.css](file:///opt/dockers/echoes_of_the_ashes/pwa/src/index.css).
|
||||||
|
2. Update `src/styles/components.css` (or new file) with utility classes.
|
||||||
|
|
||||||
|
### Phase 3: Component Reskinning
|
||||||
|
1. **InventoryModal**: Apply `.game-panel` and standard slots.
|
||||||
|
2. **PlayerSidebar**: Update bars and equipment slots.
|
||||||
|
3. **HUD/Sidebar**: Ensure borders are strict and consistent.
|
||||||
|
|
||||||
@@ -633,7 +633,7 @@
|
|||||||
"es": ""
|
"es": ""
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"en": "You find some tools: [Rusty Nails], [Wood Planks], and a [Flashlight]!",
|
"en": "You find some tools!",
|
||||||
"es": ""
|
"es": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -793,11 +793,11 @@
|
|||||||
"id": "warehouse",
|
"id": "warehouse",
|
||||||
"name": {
|
"name": {
|
||||||
"en": "🏭 Warehouse District",
|
"en": "🏭 Warehouse District",
|
||||||
"es": ""
|
"es": "🏭 Distrito de Almacenes"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"en": "Rows of industrial warehouses stretch before you. Metal doors creak in the wind. The loading docks are littered with debris and abandoned cargo.",
|
"en": "Rows of industrial warehouses stretch before you. Metal doors creak in the wind. The loading docks are littered with debris and abandoned cargo.",
|
||||||
"es": ""
|
"es": "Filas de almacenes industriales se extienden ante ti. Las puertas metálicas crujen en el viento. Los muelles de carga están cubiertos de basura y carga abandonada."
|
||||||
},
|
},
|
||||||
"image_path": "images/locations/warehouse.webp",
|
"image_path": "images/locations/warehouse.webp",
|
||||||
"x": 4,
|
"x": 4,
|
||||||
@@ -897,11 +897,11 @@
|
|||||||
"id": "warehouse_interior",
|
"id": "warehouse_interior",
|
||||||
"name": {
|
"name": {
|
||||||
"en": "📦 Warehouse Interior",
|
"en": "📦 Warehouse Interior",
|
||||||
"es": ""
|
"es": "📦 Interior del almacén"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"en": "Inside the warehouse, towering shelves cast long shadows. Scattered crates and pallets suggest this was once a distribution center. The back office door hangs open.",
|
"en": "Inside the warehouse, towering shelves cast long shadows. Scattered crates and pallets suggest this was once a distribution center. The back office door hangs open.",
|
||||||
"es": ""
|
"es": "Dentro del almacén, las estanterías altas proyectan sombras largas. Los cajones y los palets dispersos sugieren que esto alguna vez fue un centro de distribución. La puerta del despacho de la oficina de atrás se coloca abierta."
|
||||||
},
|
},
|
||||||
"image_path": "images/locations/warehouse_interior.webp",
|
"image_path": "images/locations/warehouse_interior.webp",
|
||||||
"x": 4.5,
|
"x": 4.5,
|
||||||
@@ -953,11 +953,11 @@
|
|||||||
"id": "subway",
|
"id": "subway",
|
||||||
"name": {
|
"name": {
|
||||||
"en": "🚇 Subway Station Entrance",
|
"en": "🚇 Subway Station Entrance",
|
||||||
"es": ""
|
"es": "🚇 Entrada de la Estación de Metro"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"en": "Stairs descend into darkness. The entrance to an abandoned subway station yawns before you. Emergency lighting flickers somewhere below.",
|
"en": "Stairs descend into darkness. The entrance to an abandoned subway station yawns before you. Emergency lighting flickers somewhere below.",
|
||||||
"es": ""
|
"es": "Los escalones descienden en la oscuridad. La entrada a una estación de metro abandonada se abre ante ti. La luz de emergencia titila por debajo de algún lugar."
|
||||||
},
|
},
|
||||||
"image_path": "images/locations/subway.webp",
|
"image_path": "images/locations/subway.webp",
|
||||||
"x": -4,
|
"x": -4,
|
||||||
@@ -1104,11 +1104,11 @@
|
|||||||
"id": "subway_tunnels",
|
"id": "subway_tunnels",
|
||||||
"name": {
|
"name": {
|
||||||
"en": "🚊 Subway Tunnels",
|
"en": "🚊 Subway Tunnels",
|
||||||
"es": ""
|
"es": "🚊 Túneles de Metro"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"en": "Dark subway tunnels stretch into blackness. Flickering emergency lights cast eerie shadows. The third rail is dead, but you should still watch your step.",
|
"en": "Dark subway tunnels stretch into blackness. Flickering emergency lights cast eerie shadows. The third rail is dead, but you should still watch your step.",
|
||||||
"es": ""
|
"es": "Los túneles de metro oscuros se extienden en la oscuridad. Las luces de emergencia titilan castellando sombras. El tercer rail está muerto, pero aún debes prestar atención a tus pies."
|
||||||
},
|
},
|
||||||
"image_path": "images/locations/subway_tunnels.webp",
|
"image_path": "images/locations/subway_tunnels.webp",
|
||||||
"x": -4.5,
|
"x": -4.5,
|
||||||
@@ -1167,11 +1167,11 @@
|
|||||||
"id": "office_building",
|
"id": "office_building",
|
||||||
"name": {
|
"name": {
|
||||||
"en": "🏢 Office Building",
|
"en": "🏢 Office Building",
|
||||||
"es": ""
|
"es": "🏢 Edificio de Oficinas"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"en": "A five-story office building with shattered windows. The lobby is trashed, but the stairs appear intact. You can hear the wind whistling through the upper floors.",
|
"en": "A five-story office building with shattered windows. The lobby is trashed, but the stairs appear intact. You can hear the wind whistling through the upper floors.",
|
||||||
"es": ""
|
"es": "Un edificio de oficinas de cinco pisos con ventanas rotas. El lobby está despeinado, pero las escaleras parecen intactas. Puedes escuchar el viento susurrando por las plantas superiores."
|
||||||
},
|
},
|
||||||
"image_path": "images/locations/office_building.webp",
|
"image_path": "images/locations/office_building.webp",
|
||||||
"x": 3.5,
|
"x": 3.5,
|
||||||
@@ -1229,11 +1229,11 @@
|
|||||||
"id": "office_interior",
|
"id": "office_interior",
|
||||||
"name": {
|
"name": {
|
||||||
"en": "💼 Office Floors",
|
"en": "💼 Office Floors",
|
||||||
"es": ""
|
"es": "💼 Pisos de Oficinas"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"en": "Cubicles stretch across the floor. Papers scatter in the breeze from broken windows. Filing cabinets stand open, already ransacked. A corner office looks promising.",
|
"en": "Cubicles stretch across the floor. Papers scatter in the breeze from broken windows. Filing cabinets stand open, already ransacked. A corner office looks promising.",
|
||||||
"es": ""
|
"es": "Los cubículos se extienden por el suelo. Los papeles se dispersan en el viento de las ventanas rotas. Los cajones de archivo se colocan abiertos, ya despojados. Un despacho de esquina parece prometedor."
|
||||||
},
|
},
|
||||||
"image_path": "images/locations/office_interior.webp",
|
"image_path": "images/locations/office_interior.webp",
|
||||||
"x": 4,
|
"x": 4,
|
||||||
@@ -1297,11 +1297,11 @@
|
|||||||
"id": "location_1760791397492",
|
"id": "location_1760791397492",
|
||||||
"name": {
|
"name": {
|
||||||
"en": "Subway Section A",
|
"en": "Subway Section A",
|
||||||
"es": ""
|
"es": "Sección A del metro"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"en": "A shady dimly lit subway section. All you can see are abandoned train tracks and some garbage lying around. ",
|
"en": "A shady dimly lit subway section. All you can see are abandoned train tracks and some garbage lying around. ",
|
||||||
"es": ""
|
"es": "Una sección oscura y despeinada del metro. Todo lo que puedes ver son rutas de tren abandonadas y algunos desechos de basura por el suelo."
|
||||||
},
|
},
|
||||||
"image_path": "images/locations/subway_section_a.jpg",
|
"image_path": "images/locations/subway_section_a.jpg",
|
||||||
"x": -5,
|
"x": -5,
|
||||||
|
|||||||
@@ -792,6 +792,8 @@ html {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
display: block;
|
display: block;
|
||||||
|
/* Ensure consistent vertical centering regardless of cost */
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compass-cost {
|
.compass-cost {
|
||||||
@@ -801,7 +803,12 @@ html {
|
|||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
margin-top: 0.25rem;
|
/* Use absolute positioning to prevent layout shifts */
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compass-btn:hover:not(:disabled) {
|
.compass-btn:hover:not(:disabled) {
|
||||||
@@ -855,6 +862,81 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compass-center {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-center-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--game-color-primary);
|
||||||
|
background: radial-gradient(circle, rgba(225, 29, 72, 0.2) 0%, rgba(20, 20, 20, 0.8) 100%);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-shadow: 0 0 15px rgba(225, 29, 72, 0.2);
|
||||||
|
padding: 0.25rem;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-center-btn:hover:not(:disabled) {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 0 20px rgba(225, 29, 72, 0.4);
|
||||||
|
background: radial-gradient(circle, rgba(225, 29, 72, 0.3) 0%, rgba(30, 30, 30, 0.9) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-center-btn:active:not(:disabled) {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compass-center-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-btn-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
line-height: 1.1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-btn-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Cooldown State */
|
||||||
|
.movement-controls h3.cooldown-active {
|
||||||
|
color: var(--game-color-warning);
|
||||||
|
animation: pulse-text 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-text {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Cooldown indicator */
|
/* Cooldown indicator */
|
||||||
.cooldown-indicator {
|
.cooldown-indicator {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -1008,6 +1090,7 @@ body.no-scroll {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.interact-btn {
|
.interact-btn {
|
||||||
|
width: 100%;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
background: rgba(255, 193, 7, 0.3);
|
background: rgba(255, 193, 7, 0.3);
|
||||||
@@ -1896,6 +1979,12 @@ body.no-scroll {
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Remove .large variation to enforce uniformity */
|
||||||
|
.equipment-slot.large {
|
||||||
|
min-width: 0;
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.equipment-slot {
|
.equipment-slot {
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
@@ -1908,9 +1997,9 @@ body.no-scroll {
|
|||||||
/* Changed from center to space-between */
|
/* Changed from center to space-between */
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
/* Fixed dimensions for consistent sizing */
|
/* Fixed dimensions for consistent sizing */
|
||||||
min-height: 100px;
|
height: 100px;
|
||||||
min-width: 80px;
|
width: 100%;
|
||||||
max-width: 100%;
|
box-sizing: border-box;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
@@ -1942,10 +2031,6 @@ body.no-scroll {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.equipment-slot.large {
|
|
||||||
min-width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.equipment-slot.empty {
|
.equipment-slot.empty {
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
@@ -4265,4 +4350,152 @@ body.no-scroll {
|
|||||||
border-color: #9ca3af;
|
border-color: #9ca3af;
|
||||||
box-shadow: 0 0 15px rgba(75, 85, 99, 0.3);
|
box-shadow: 0 0 15px rgba(75, 85, 99, 0.3);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pickup Action Group (Split Button) */
|
||||||
|
.pickup-actions-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: visible;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-main-btn {
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
background: linear-gradient(135deg, #48bb78, #38a169);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-main-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #38a169, #2f855a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-toggle-btn {
|
||||||
|
padding: 0 0.4rem;
|
||||||
|
background: linear-gradient(135deg, #48bb78, #38a169);
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-toggle-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #38a169, #2f855a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
right: 0;
|
||||||
|
background: var(--game-bg-panel);
|
||||||
|
border: 1px solid var(--game-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
animation: fadeIn 0.1s ease-out;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-option {
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--game-text-primary);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-option:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-option:hover {
|
||||||
|
background: rgba(72, 187, 120, 0.2);
|
||||||
|
color: #48bb78;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Single button variant (no split) */
|
||||||
|
.pickup-btn-single {
|
||||||
|
padding: 0.4rem 1rem;
|
||||||
|
background: linear-gradient(135deg, #48bb78, #38a169);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-btn-single:hover {
|
||||||
|
background: linear-gradient(135deg, #38a169, #2f855a);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(56, 161, 105, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pickup Action Group (Row of Buttons) */
|
||||||
|
.pickup-actions-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(72, 187, 120, 0.4);
|
||||||
|
/* Green border */
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.pickup {
|
||||||
|
background: transparent;
|
||||||
|
color: #48bb78;
|
||||||
|
/* Green text */
|
||||||
|
border: none;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.pickup:hover {
|
||||||
|
background: rgba(72, 187, 120, 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.pickup.single {
|
||||||
|
/* Special styling if we want the single button to look distinct,
|
||||||
|
but keeping it consistent with group for now */
|
||||||
|
border: 1px solid rgba(72, 187, 120, 0.4);
|
||||||
|
background: rgba(72, 187, 120, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.pickup.single:hover {
|
||||||
|
background: rgba(72, 187, 120, 0.2);
|
||||||
}
|
}
|
||||||
@@ -49,11 +49,20 @@ export const GameProgressBar: React.FC<GameProgressBarProps> = ({
|
|||||||
case 'enemy_health': return 'linear-gradient(90deg, #ef4444, #b91c1c)'; // Red
|
case 'enemy_health': return 'linear-gradient(90deg, #ef4444, #b91c1c)'; // Red
|
||||||
case 'stamina': return 'linear-gradient(90deg, #eab308, #ca8a04)';
|
case 'stamina': return 'linear-gradient(90deg, #eab308, #ca8a04)';
|
||||||
case 'xp': return 'linear-gradient(90deg, #8b5cf6, #7c3aed)'; // Purple for XP?
|
case 'xp': return 'linear-gradient(90deg, #8b5cf6, #7c3aed)'; // Purple for XP?
|
||||||
|
// Green if empty, yellow if half, red if full
|
||||||
|
case 'weight':
|
||||||
|
if (percentage < 65) return 'linear-gradient(90deg, #10b981, #059669)'; // Green
|
||||||
|
if (percentage < 85) return 'linear-gradient(90deg, #eab308, #ca8a04)'; // Yellow
|
||||||
|
return 'linear-gradient(90deg, #ef4444, #b91c1c)'; // Red
|
||||||
|
case 'volume':
|
||||||
|
if (percentage < 65) return 'linear-gradient(90deg, #10b981, #059669)'; // Green
|
||||||
|
if (percentage < 85) return 'linear-gradient(90deg, #eab308, #ca8a04)'; // Yellow
|
||||||
|
return 'linear-gradient(90deg, #ef4444, #b91c1c)'; // Red
|
||||||
case 'durability':
|
case 'durability':
|
||||||
if (percentage < 15) return 'linear-gradient(90deg, #ef4444, #b91c1c)'; // Red
|
if (percentage < 15) return 'linear-gradient(90deg, #ef4444, #b91c1c)'; // Red
|
||||||
if (percentage < 50) return 'linear-gradient(90deg, #eab308, #ca8a04)'; // Yellow
|
if (percentage < 50) return 'linear-gradient(90deg, #eab308, #ca8a04)'; // Yellow
|
||||||
return 'linear-gradient(90deg, #10b981, #059669)'; // Green
|
return 'linear-gradient(90deg, #10b981, #059669)'; // Green
|
||||||
default: return undefined;
|
default: return 'linear-gradient(90deg, #10b981, #059669)';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -228,6 +228,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background: var(--game-bg-app);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory-search-bar {
|
.inventory-search-bar {
|
||||||
@@ -240,6 +241,8 @@
|
|||||||
border-radius: var(--game-radius-md);
|
border-radius: var(--game-radius-md);
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
color: var(--game-text-primary);
|
color: var(--game-text-primary);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory-search-bar input {
|
.inventory-search-bar input {
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ function InventoryModal({
|
|||||||
<button className="action-btn drop" onClick={() => {
|
<button className="action-btn drop" onClick={() => {
|
||||||
playSfx('/audio/sfx/drop.wav')
|
playSfx('/audio/sfx/drop.wav')
|
||||||
onDropItem(item.item_id, item.id, item.quantity)
|
onDropItem(item.item_id, item.id, item.quantity)
|
||||||
}}>{t('game.dropAll')}</button>
|
}}>{t('common.all')}</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import type { Location, PlayerState, CombatState, Profile, WorkbenchTab } from './types'
|
import type { Location, PlayerState, CombatState, Profile, WorkbenchTab } from './types'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAudio } from '../../contexts/AudioContext'
|
import { useAudio } from '../../contexts/AudioContext'
|
||||||
@@ -86,6 +85,7 @@ function LocationView({
|
|||||||
}: LocationViewProps) {
|
}: LocationViewProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { playSfx } = useAudio()
|
const { playSfx } = useAudio()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="location-view">
|
<div className="location-view">
|
||||||
<div className="location-info">
|
<div className="location-info">
|
||||||
@@ -312,28 +312,9 @@ function LocationView({
|
|||||||
<div className="entity-section items-section">
|
<div className="entity-section items-section">
|
||||||
<h3>{t('location.itemsOnGround')}</h3>
|
<h3>{t('location.itemsOnGround')}</h3>
|
||||||
<div className="entity-list">
|
<div className="entity-list">
|
||||||
{location.items.map((item: any, i: number) => (
|
{location.items.map((item: any, i: number) => {
|
||||||
<div key={i} className="entity-card item-card">
|
return (
|
||||||
{item.image_path ? (
|
<div key={i} className="entity-card item-card">
|
||||||
<img
|
|
||||||
src={getAssetPath(item.image_path)}
|
|
||||||
alt={getTranslatedText(item.name)}
|
|
||||||
className="entity-icon"
|
|
||||||
onError={(e) => {
|
|
||||||
(e.target as HTMLImageElement).style.display = 'none';
|
|
||||||
const icon = (e.target as HTMLImageElement).nextElementSibling;
|
|
||||||
if (icon) icon.classList.remove('hidden');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<span className={`entity-icon ${item.image_path ? 'hidden' : ''}`}>{item.emoji || '📦'}</span>
|
|
||||||
<div className="entity-info">
|
|
||||||
<div className={`entity-name ${item.tier ? `tier-${item.tier}` : ''}`}>
|
|
||||||
{getTranslatedText(item.name) || 'Unknown Item'}
|
|
||||||
</div>
|
|
||||||
{item.quantity > 1 && <div className="entity-quantity">×{item.quantity}</div>}
|
|
||||||
</div>
|
|
||||||
<div className="item-info-btn-container">
|
|
||||||
<GameTooltip content={
|
<GameTooltip content={
|
||||||
<div className="item-info-tooltip-content">
|
<div className="item-info-tooltip-content">
|
||||||
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
|
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
|
||||||
@@ -368,50 +349,79 @@ function LocationView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<button className="entity-action-btn info">{t('common.info')}</button>
|
<div className="entity-content-wrapper" style={{ display: 'flex', alignItems: 'center', gap: '1rem', flex: 1, cursor: 'help' }}>
|
||||||
</GameTooltip>
|
{item.image_path ? (
|
||||||
</div>
|
<img
|
||||||
{item.quantity === 1 ? (
|
src={getAssetPath(item.image_path)}
|
||||||
<button
|
alt={getTranslatedText(item.name)}
|
||||||
className="entity-action-btn pickup"
|
className="entity-icon"
|
||||||
onClick={() => {
|
onError={(e) => {
|
||||||
playSfx('/audio/sfx/pickup.wav')
|
(e.target as HTMLImageElement).style.display = 'none';
|
||||||
onPickup(item.id, 1)
|
const icon = (e.target as HTMLImageElement).nextElementSibling;
|
||||||
}}
|
if (icon) icon.classList.remove('hidden');
|
||||||
>
|
}}
|
||||||
{t('common.pickUp')}
|
/>
|
||||||
</button>
|
) : null}
|
||||||
) : (
|
<span className={`entity-icon ${item.image_path ? 'hidden' : ''}`}>{item.emoji || '📦'}</span>
|
||||||
<div className="item-pickup-btn-container">
|
<div className="entity-info">
|
||||||
<button className="entity-action-btn pickup">{t('common.pickUp')} ▼</button>
|
<div className={`entity-name ${item.tier ? `tier-${item.tier}` : ''}`}>
|
||||||
<div className="item-pickup-menu">
|
{getTranslatedText(item.name) || 'Unknown Item'}
|
||||||
<button className="item-pickup-option" onClick={() => {
|
</div>
|
||||||
playSfx('/audio/sfx/pickup.wav')
|
{item.quantity > 1 && <div className="entity-quantity">×{item.quantity}</div>}
|
||||||
onPickup(item.id, 1)
|
</div>
|
||||||
}}>{t('common.pickUp')} 1</button>
|
|
||||||
{item.quantity >= 5 && (
|
|
||||||
<button className="item-pickup-option" onClick={() => {
|
|
||||||
playSfx('/audio/sfx/pickup.wav')
|
|
||||||
onPickup(item.id, 5)
|
|
||||||
}}>{t('common.pickUp')} 5</button>
|
|
||||||
)}
|
|
||||||
{item.quantity >= 10 && (
|
|
||||||
<button className="item-pickup-option" onClick={() => {
|
|
||||||
playSfx('/audio/sfx/pickup.wav')
|
|
||||||
onPickup(item.id, 10)
|
|
||||||
}}>{t('common.pickUp')} 10</button>
|
|
||||||
)}
|
|
||||||
<button className="item-pickup-option" onClick={() => {
|
|
||||||
playSfx('/audio/sfx/pickup.wav')
|
|
||||||
onPickup(item.id, item.quantity)
|
|
||||||
}}>
|
|
||||||
{t('common.pickUpAll')} ({item.quantity})
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</GameTooltip>
|
||||||
|
|
||||||
|
<div className="item-actions-row">
|
||||||
|
{item.quantity === 1 ? (
|
||||||
|
<button
|
||||||
|
className="action-btn pickup single"
|
||||||
|
onClick={() => {
|
||||||
|
playSfx('/audio/sfx/pickup.wav')
|
||||||
|
onPickup(item.id, 1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('common.pickUp')}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="pickup-actions-group">
|
||||||
|
<button className="action-btn pickup" onClick={() => {
|
||||||
|
playSfx('/audio/sfx/pickup.wav')
|
||||||
|
onPickup(item.id, 1)
|
||||||
|
}}>
|
||||||
|
x1
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{item.quantity >= 5 && (
|
||||||
|
<button className="action-btn pickup" onClick={() => {
|
||||||
|
playSfx('/audio/sfx/pickup.wav')
|
||||||
|
onPickup(item.id, 5)
|
||||||
|
}}>
|
||||||
|
x5
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{item.quantity >= 10 && (
|
||||||
|
<button className="action-btn pickup" onClick={() => {
|
||||||
|
playSfx('/audio/sfx/pickup.wav')
|
||||||
|
onPickup(item.id, 10)
|
||||||
|
}}>
|
||||||
|
x10
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button className="action-btn pickup" onClick={() => {
|
||||||
|
playSfx('/audio/sfx/pickup.wav')
|
||||||
|
onPickup(item.id, item.quantity)
|
||||||
|
}}>
|
||||||
|
{t('common.all') || 'All'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -104,10 +104,81 @@ function MovementControls({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to render center button (compass center or action)
|
||||||
|
const renderCenterButton = () => {
|
||||||
|
// Check for special directions that should go in the center
|
||||||
|
const insideDir = location.directions.includes('inside') ? 'inside' : null;
|
||||||
|
const outsideDir = location.directions.includes('outside') ? 'outside' : null;
|
||||||
|
const enterDir = location.directions.includes('enter') ? 'enter' : null;
|
||||||
|
const exitDir = location.directions.includes('exit') ? 'exit' : null;
|
||||||
|
|
||||||
|
// Priority: Inside/Outside (usually mutually exclusive) > Enter/Exit
|
||||||
|
const centerDirection = insideDir || outsideDir || enterDir || exitDir;
|
||||||
|
|
||||||
|
if (!centerDirection) {
|
||||||
|
// Default Compass Icon
|
||||||
|
return (
|
||||||
|
<div className="compass-center">
|
||||||
|
<div className="compass-icon">🧭</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action Button Logic
|
||||||
|
const stamina = getStaminaCost(centerDirection);
|
||||||
|
const destination = getDestinationName(centerDirection);
|
||||||
|
const insufficientStamina = profile ? profile.stamina < stamina : false;
|
||||||
|
const disabled = !!combatState || movementCooldown > 0 || insufficientStamina || (profile?.is_dead ?? false);
|
||||||
|
|
||||||
|
// Labels and Icons
|
||||||
|
let label = t('directions.' + centerDirection);
|
||||||
|
let icon = '🚪';
|
||||||
|
if (centerDirection === 'inside') icon = '🏠';
|
||||||
|
if (centerDirection === 'outside') icon = '🌳';
|
||||||
|
|
||||||
|
const tooltipText = profile?.is_dead ? t('messages.youAreDead') :
|
||||||
|
movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) :
|
||||||
|
combatState ? t('messages.cannotTravelCombat') :
|
||||||
|
insufficientStamina ? t('messages.notEnoughStamina', { need: stamina, have: profile?.stamina ?? 0 }) :
|
||||||
|
(
|
||||||
|
<div className="movement-tooltip">
|
||||||
|
<div className="tooltip-title">{label}</div>
|
||||||
|
<div className="tooltip-stat">⚡ {t('game.stamina')}: {stamina}</div>
|
||||||
|
{destination && <div className="tooltip-desc">{destination}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="compass-center">
|
||||||
|
<GameTooltip content={tooltipText}>
|
||||||
|
<button
|
||||||
|
className="compass-center-btn"
|
||||||
|
onClick={() => onMove(centerDirection)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<span className="center-btn-icon" key={`icon-${centerDirection}`}>{icon}</span>
|
||||||
|
<span className="center-btn-label" key={`label-${centerDirection}`}>{label}</span>
|
||||||
|
{movementCooldown > 0 ? (
|
||||||
|
<span className="compass-cost" key="cost-timer">⏳{movementCooldown}s</span>
|
||||||
|
) : (
|
||||||
|
<span className="compass-cost" key="cost-stamina">⚡{stamina}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</GameTooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="movement-controls">
|
<div className="movement-controls">
|
||||||
<h3>{t('game.travel')}</h3>
|
<h3 className={movementCooldown > 0 ? 'cooldown-active' : ''}>
|
||||||
|
{movementCooldown > 0 ? (
|
||||||
|
<span key="timer">⏳ {t('messages.waitBeforeMovingSimple', { seconds: movementCooldown })}</span>
|
||||||
|
) : (
|
||||||
|
<span key="title">{t('game.travel')}</span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
<div className="compass-grid">
|
<div className="compass-grid">
|
||||||
{/* Top row */}
|
{/* Top row */}
|
||||||
{renderCompassButton('northwest', '↖️', 'nw')}
|
{renderCompassButton('northwest', '↖️', 'nw')}
|
||||||
@@ -116,9 +187,7 @@ function MovementControls({
|
|||||||
|
|
||||||
{/* Middle row */}
|
{/* Middle row */}
|
||||||
{renderCompassButton('west', '⬅️', 'w')}
|
{renderCompassButton('west', '⬅️', 'w')}
|
||||||
<div className="compass-center">
|
{renderCenterButton()}
|
||||||
<div className="compass-icon">🧭</div>
|
|
||||||
</div>
|
|
||||||
{renderCompassButton('east', '➡️', 'e')}
|
{renderCompassButton('east', '➡️', 'e')}
|
||||||
|
|
||||||
{/* Bottom row */}
|
{/* Bottom row */}
|
||||||
@@ -127,107 +196,43 @@ function MovementControls({
|
|||||||
{renderCompassButton('southeast', '↘️', 'se')}
|
{renderCompassButton('southeast', '↘️', 'se')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cooldown indicator */}
|
{/* Special movements (Vertical only now, since Enter/Exit are in center) */}
|
||||||
{movementCooldown > 0 && (
|
{(location.directions.includes('up') || location.directions.includes('down')) && (
|
||||||
<div className="cooldown-indicator">
|
<div className="special-moves">
|
||||||
⏳ {t('messages.waitBeforeMovingSimple', { seconds: movementCooldown })}
|
{location.directions.includes('up') && (
|
||||||
|
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
||||||
|
<div className="movement-tooltip">
|
||||||
|
<div className="tooltip-title">{t('directions.up')}</div>
|
||||||
|
<div className="tooltip-stat">⚡ {t('game.stamina')}: {getStaminaCost('up')}</div>
|
||||||
|
</div>
|
||||||
|
)}>
|
||||||
|
<button
|
||||||
|
onClick={() => onMove('up')}
|
||||||
|
className="special-btn"
|
||||||
|
disabled={!!combatState || movementCooldown > 0}
|
||||||
|
>
|
||||||
|
⬆️ {t('directions.up')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('up')}`}</span>
|
||||||
|
</button>
|
||||||
|
</GameTooltip>
|
||||||
|
)}
|
||||||
|
{location.directions.includes('down') && (
|
||||||
|
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
||||||
|
<div className="movement-tooltip">
|
||||||
|
<div className="tooltip-title">{t('directions.down')}</div>
|
||||||
|
<div className="tooltip-stat">⚡ {t('game.stamina')}: {getStaminaCost('down')}</div>
|
||||||
|
</div>
|
||||||
|
)}>
|
||||||
|
<button
|
||||||
|
onClick={() => onMove('down')}
|
||||||
|
className="special-btn"
|
||||||
|
disabled={!!combatState || movementCooldown > 0}
|
||||||
|
>
|
||||||
|
⬇️ {t('directions.down')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('down')}`}</span>
|
||||||
|
</button>
|
||||||
|
</GameTooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Special movements */}
|
|
||||||
<div className="special-moves">
|
|
||||||
{location.directions.includes('up') && (
|
|
||||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
|
||||||
<div className="movement-tooltip">
|
|
||||||
<div className="tooltip-title">{t('directions.up')}</div>
|
|
||||||
<div className="tooltip-stat">⚡ {t('game.stamina')}: {getStaminaCost('up')}</div>
|
|
||||||
</div>
|
|
||||||
)}>
|
|
||||||
<button
|
|
||||||
onClick={() => onMove('up')}
|
|
||||||
className="special-btn"
|
|
||||||
disabled={!!combatState || movementCooldown > 0}
|
|
||||||
>
|
|
||||||
⬆️ {t('directions.up')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('up')}`}</span>
|
|
||||||
</button>
|
|
||||||
</GameTooltip>
|
|
||||||
)}
|
|
||||||
{location.directions.includes('down') && (
|
|
||||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
|
||||||
<div className="movement-tooltip">
|
|
||||||
<div className="tooltip-title">{t('directions.down')}</div>
|
|
||||||
<div className="tooltip-stat">⚡ {t('game.stamina')}: {getStaminaCost('down')}</div>
|
|
||||||
</div>
|
|
||||||
)}>
|
|
||||||
<button
|
|
||||||
onClick={() => onMove('down')}
|
|
||||||
className="special-btn"
|
|
||||||
disabled={!!combatState || movementCooldown > 0}
|
|
||||||
>
|
|
||||||
⬇️ {t('directions.down')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('down')}`}</span>
|
|
||||||
</button>
|
|
||||||
</GameTooltip>
|
|
||||||
)}
|
|
||||||
{location.directions.includes('enter') && (
|
|
||||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
|
||||||
<div className="movement-tooltip">
|
|
||||||
<div className="tooltip-title">{t('directions.enter')}</div>
|
|
||||||
<div className="tooltip-stat">⚡ {t('game.stamina')}: {getStaminaCost('enter')}</div>
|
|
||||||
</div>
|
|
||||||
)}>
|
|
||||||
<button
|
|
||||||
onClick={() => onMove('enter')}
|
|
||||||
className="special-btn"
|
|
||||||
disabled={!!combatState || movementCooldown > 0}
|
|
||||||
>
|
|
||||||
🚪 {t('directions.enter')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('enter')}`}</span>
|
|
||||||
</button>
|
|
||||||
</GameTooltip>
|
|
||||||
)}
|
|
||||||
{location.directions.includes('inside') && (
|
|
||||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
|
||||||
<div className="movement-tooltip">
|
|
||||||
<div className="tooltip-title">{t('directions.inside')}</div>
|
|
||||||
<div className="tooltip-stat">⚡ {t('game.stamina')}: {getStaminaCost('inside')}</div>
|
|
||||||
</div>
|
|
||||||
)}>
|
|
||||||
<button
|
|
||||||
onClick={() => onMove('inside')}
|
|
||||||
className="special-btn"
|
|
||||||
disabled={!!combatState || movementCooldown > 0}
|
|
||||||
>
|
|
||||||
🚪 {t('directions.inside')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('inside')}`}</span>
|
|
||||||
</button>
|
|
||||||
</GameTooltip>
|
|
||||||
)}
|
|
||||||
{location.directions.includes('exit') && (
|
|
||||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : t('directions.exit')}>
|
|
||||||
<button
|
|
||||||
onClick={() => onMove('exit')}
|
|
||||||
className="special-btn"
|
|
||||||
disabled={!!combatState || movementCooldown > 0}
|
|
||||||
>
|
|
||||||
🚪 {t('directions.exit')}
|
|
||||||
</button>
|
|
||||||
</GameTooltip>
|
|
||||||
)}
|
|
||||||
{location.directions.includes('outside') && (
|
|
||||||
<GameTooltip content={movementCooldown > 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : (
|
|
||||||
<div className="movement-tooltip">
|
|
||||||
<div className="tooltip-title">{t('directions.outside')}</div>
|
|
||||||
<div className="tooltip-stat">⚡ {t('game.stamina')}: {getStaminaCost('outside')}</div>
|
|
||||||
</div>
|
|
||||||
)}>
|
|
||||||
<button
|
|
||||||
onClick={() => onMove('outside')}
|
|
||||||
className="special-btn"
|
|
||||||
disabled={!!combatState || movementCooldown > 0}
|
|
||||||
>
|
|
||||||
🚪 {t('directions.outside')} <span className="compass-cost">{movementCooldown > 0 ? `⏳${movementCooldown}s` : `⚡${getStaminaCost('outside')}`}</span>
|
|
||||||
</button>
|
|
||||||
</GameTooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Surroundings - outside movement controls */}
|
{/* Surroundings - outside movement controls */}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getTranslatedText } from '../../utils/i18nUtils'
|
|||||||
import InventoryModal from './InventoryModal'
|
import InventoryModal from './InventoryModal'
|
||||||
import { GameProgressBar } from '../common/GameProgressBar'
|
import { GameProgressBar } from '../common/GameProgressBar'
|
||||||
import { GameTooltip } from '../common/GameTooltip'
|
import { GameTooltip } from '../common/GameTooltip'
|
||||||
|
import { useAudio } from '../../contexts/AudioContext'
|
||||||
|
|
||||||
interface PlayerSidebarProps {
|
interface PlayerSidebarProps {
|
||||||
playerState: PlayerState
|
playerState: PlayerState
|
||||||
@@ -40,6 +41,7 @@ function PlayerSidebar({
|
|||||||
}: PlayerSidebarProps) {
|
}: PlayerSidebarProps) {
|
||||||
const [showInventory, setShowInventory] = useState(false)
|
const [showInventory, setShowInventory] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { playSfx } = useAudio()
|
||||||
|
|
||||||
const renderEquipmentSlot = (slot: string, item: any, label: string) => {
|
const renderEquipmentSlot = (slot: string, item: any, label: string) => {
|
||||||
// Construct the tooltip content if item exists
|
// Construct the tooltip content if item exists
|
||||||
@@ -108,13 +110,13 @@ function PlayerSidebar({
|
|||||||
) : label; // Show label if no item
|
) : label; // Show label if no item
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GameTooltip content={tooltipContent}>
|
<div className={`equipment-slot game-slot ${item ? 'filled' : 'empty'}`}>
|
||||||
<div className={`equipment-slot game-slot ${item ? 'filled' : 'empty'}`}>
|
{item ? (
|
||||||
{item ? (
|
<>
|
||||||
<>
|
<GameTooltip content={t('game.unequip')}>
|
||||||
<GameTooltip content={t('game.unequip')}>
|
<button className="equipment-unequip-btn game-btn game-btn-icon" onClick={(e) => { e.stopPropagation(); onUnequipItem(slot); playSfx('/audio/sfx/unequip.wav'); }}>✕</button>
|
||||||
<button className="equipment-unequip-btn game-btn game-btn-icon" onClick={(e) => { e.stopPropagation(); onUnequipItem(slot); }}>✕</button>
|
</GameTooltip>
|
||||||
</GameTooltip>
|
<GameTooltip content={tooltipContent}>
|
||||||
<div className="equipment-item-content">
|
<div className="equipment-item-content">
|
||||||
{item.image_path ? (
|
{item.image_path ? (
|
||||||
<img
|
<img
|
||||||
@@ -138,19 +140,19 @@ function PlayerSidebar({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</GameTooltip>
|
||||||
) : (
|
</>
|
||||||
<>
|
) : (
|
||||||
<img
|
<GameTooltip content={label}>
|
||||||
src={getAssetPath(`/images/placeholder/${slot}_placeholder.webp`)}
|
<img
|
||||||
alt={label}
|
src={getAssetPath(`/images/placeholder/${slot}_placeholder.webp`)}
|
||||||
className="equipment-placeholder-img"
|
alt={label}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'contain', opacity: 0.5 }}
|
className="equipment-placeholder-img"
|
||||||
/>
|
style={{ width: '100%', height: '100%', objectFit: 'contain', opacity: 0.5 }}
|
||||||
</>
|
/>
|
||||||
)}
|
</GameTooltip>
|
||||||
</div>
|
)}
|
||||||
</GameTooltip>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,8 @@
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-bottom: 1px solid var(--game-border-color);
|
border-bottom: 1px solid var(--game-border-color);
|
||||||
background: var(--game-bg-input);
|
background: var(--game-bg-input);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
/* Match search bar bg */
|
/* Match search bar bg */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
41
pwa/src/components/game/game_pickup.css
Normal file
41
pwa/src/components/game/game_pickup.css
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/* Pickup Action Group (Row of Buttons) */
|
||||||
|
.pickup-actions-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(72, 187, 120, 0.4);
|
||||||
|
/* Green border */
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.pickup {
|
||||||
|
background: transparent;
|
||||||
|
color: #48bb78;
|
||||||
|
/* Green text */
|
||||||
|
border: none;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.pickup:hover {
|
||||||
|
background: rgba(72, 187, 120, 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.pickup.single {
|
||||||
|
/* Special styling if we want the single button to look distinct,
|
||||||
|
but keeping it consistent with group for now */
|
||||||
|
border: 1px solid rgba(72, 187, 120, 0.4);
|
||||||
|
background: rgba(72, 187, 120, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn.pickup.single:hover {
|
||||||
|
background: rgba(72, 187, 120, 0.2);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// useGameEngine - Core game state and logic hook
|
// useGameEngine - Core game state and logic hook
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import api from '../../../services/api'
|
import api from '../../../services/api'
|
||||||
import type {
|
import type {
|
||||||
PlayerState,
|
PlayerState,
|
||||||
@@ -143,6 +144,7 @@ export function useGameEngine(
|
|||||||
token: string | null,
|
token: string | null,
|
||||||
_handleWebSocketMessage: (message: any) => Promise<void>
|
_handleWebSocketMessage: (message: any) => Promise<void>
|
||||||
): [GameEngineState, GameEngineActions] {
|
): [GameEngineState, GameEngineActions] {
|
||||||
|
const { t } = useTranslation()
|
||||||
// All state declarations
|
// All state declarations
|
||||||
const [playerState, setPlayerState] = useState<PlayerState | null>(null)
|
const [playerState, setPlayerState] = useState<PlayerState | null>(null)
|
||||||
const [location, setLocation] = useState<Location | null>(null)
|
const [location, setLocation] = useState<Location | null>(null)
|
||||||
@@ -843,7 +845,7 @@ export function useGameEngine(
|
|||||||
const data = response.data
|
const data = response.data
|
||||||
let msg = data.message
|
let msg = data.message
|
||||||
if (data.items_found && data.items_found.length > 0) {
|
if (data.items_found && data.items_found.length > 0) {
|
||||||
msg += '\n\n📦 Found: ' + data.items_found.join(', ')
|
msg += '\n\n📦 ' + t('game.found') + ': ' + data.items_found.join(', ')
|
||||||
}
|
}
|
||||||
if (data.hp_change) {
|
if (data.hp_change) {
|
||||||
msg += `\n❤️ HP: ${data.hp_change > 0 ? '+' : ''}${data.hp_change}`
|
msg += `\n❤️ HP: ${data.hp_change > 0 ? '+' : ''}${data.hp_change}`
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"pickUpAll": "Pick Up All",
|
"pickUpAll": "Pick Up All",
|
||||||
"qty": "Qty",
|
"qty": "Qty",
|
||||||
"enemy": "Enemy",
|
"enemy": "Enemy",
|
||||||
"you": "You"
|
"you": "You",
|
||||||
|
"all": "All"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
@@ -63,7 +64,6 @@
|
|||||||
"salvage": "♻️ Salvage",
|
"salvage": "♻️ Salvage",
|
||||||
"pickUp": "Pick Up",
|
"pickUp": "Pick Up",
|
||||||
"drop": "Drop",
|
"drop": "Drop",
|
||||||
"dropAll": "All",
|
|
||||||
"use": "Use",
|
"use": "Use",
|
||||||
"equip": "Equip",
|
"equip": "Equip",
|
||||||
"unequip": "Unequip",
|
"unequip": "Unequip",
|
||||||
@@ -90,7 +90,8 @@
|
|||||||
"burning": "Burning",
|
"burning": "Burning",
|
||||||
"poisoned": "Poisoned"
|
"poisoned": "Poisoned"
|
||||||
},
|
},
|
||||||
"effectAlreadyActive": "Effect already active"
|
"effectAlreadyActive": "Effect already active",
|
||||||
|
"found": "Found"
|
||||||
},
|
},
|
||||||
"location": {
|
"location": {
|
||||||
"recentActivity": "📜 Recent Activity",
|
"recentActivity": "📜 Recent Activity",
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
"fight": "Luchar",
|
"fight": "Luchar",
|
||||||
"pickUp": "Recoger",
|
"pickUp": "Recoger",
|
||||||
"pickUpAll": "Recoger Todo",
|
"pickUpAll": "Recoger Todo",
|
||||||
"qty": "Cant"
|
"qty": "Cant",
|
||||||
|
"all": "Todo"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "Iniciar sesión",
|
"login": "Iniciar sesión",
|
||||||
@@ -61,7 +62,6 @@
|
|||||||
"salvage": "♻️ Desguazar",
|
"salvage": "♻️ Desguazar",
|
||||||
"pickUp": "Recoger",
|
"pickUp": "Recoger",
|
||||||
"drop": "Soltar",
|
"drop": "Soltar",
|
||||||
"dropAll": "Todo",
|
|
||||||
"use": "Usar",
|
"use": "Usar",
|
||||||
"equip": "Equipar",
|
"equip": "Equipar",
|
||||||
"unequip": "Desequipar",
|
"unequip": "Desequipar",
|
||||||
@@ -88,7 +88,8 @@
|
|||||||
"burning": "Quemadura",
|
"burning": "Quemadura",
|
||||||
"poisoned": "Envenenamiento"
|
"poisoned": "Envenenamiento"
|
||||||
},
|
},
|
||||||
"effectAlreadyActive": "Efecto ya activo"
|
"effectAlreadyActive": "Efecto ya activo",
|
||||||
|
"found": "Encontrado"
|
||||||
},
|
},
|
||||||
"location": {
|
"location": {
|
||||||
"recentActivity": "📜 Actividad Reciente",
|
"recentActivity": "📜 Actividad Reciente",
|
||||||
|
|||||||
@@ -172,4 +172,32 @@ img.emoji {
|
|||||||
margin: 0 0.05em 0 0.1em;
|
margin: 0 0.05em 0 0.1em;
|
||||||
vertical-align: -0.1em;
|
vertical-align: -0.1em;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--game-border-active);
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--game-text-secondary);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox support */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--game-border-active) rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user