From 539377e63d7e7ce219c975b1d5794d462e2fcb8c Mon Sep 17 00:00:00 2001 From: Joan Date: Fri, 6 Feb 2026 11:23:32 +0100 Subject: [PATCH] Fix GameTooltip blocking issue and translate Found string --- api/routers/game_routes.py | 4 +- api/services/helpers.py | 2 + docs/visual_style_guide.md | 139 ++++++++++ gamedata/locations.json | 30 +-- pwa/src/components/Game.css | 249 +++++++++++++++++- pwa/src/components/common/GameProgressBar.tsx | 11 +- pwa/src/components/game/InventoryModal.css | 3 + pwa/src/components/game/InventoryModal.tsx | 2 +- pwa/src/components/game/LocationView.tsx | 140 +++++----- pwa/src/components/game/MovementControls.tsx | 211 +++++++-------- pwa/src/components/game/PlayerSidebar.tsx | 42 +-- pwa/src/components/game/Workbench.css | 2 + pwa/src/components/game/game_pickup.css | 41 +++ .../components/game/hooks/useGameEngine.ts | 4 +- pwa/src/i18n/locales/en.json | 7 +- pwa/src/i18n/locales/es.json | 7 +- pwa/src/index.css | 28 ++ 17 files changed, 700 insertions(+), 222 deletions(-) create mode 100644 docs/visual_style_guide.md create mode 100644 pwa/src/components/game/game_pickup.css diff --git a/api/routers/game_routes.py b/api/routers/game_routes.py index edd0519..24fa53c 100644 --- a/api/routers/game_routes.py +++ b/api/routers/game_routes.py @@ -442,7 +442,7 @@ async def get_current_location(request: Request, current_user: dict = Depends(ge "id": action.id, "name": action.label, "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, "cooldown_remaining": remaining_cooldown }) @@ -1053,7 +1053,7 @@ async def interact( "instance_id": interact_req.interactable_id, "action_id": interact_req.action_id, "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() } diff --git a/api/services/helpers.py b/api/services/helpers.py index 8612338..4fcaa7c 100644 --- a/api/services/helpers.py +++ b/api/services/helpers.py @@ -88,10 +88,12 @@ GAME_MESSAGES = { # Interaction '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."}, 'object_not_found': {'en': "Object not found", 'es': "Objeto no encontrado"}, '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"}, + 'interactable_cooldown': {'en': "{user} used {action} on {interactable}", 'es': "{user} usó {action} en {interactable}"}, # Item Usage 'item_used': {'en': "Used {name}", 'es': "Usado {name}"}, diff --git a/docs/visual_style_guide.md b/docs/visual_style_guide.md new file mode 100644 index 0000000..a87154f --- /dev/null +++ b/docs/visual_style_guide.md @@ -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: `` +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 +}> + + +``` + +## 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 ``. + +### 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. + diff --git a/gamedata/locations.json b/gamedata/locations.json index 703bb5a..7a98fcd 100644 --- a/gamedata/locations.json +++ b/gamedata/locations.json @@ -633,7 +633,7 @@ "es": "" }, "success": { - "en": "You find some tools: [Rusty Nails], [Wood Planks], and a [Flashlight]!", + "en": "You find some tools!", "es": "" } } @@ -793,11 +793,11 @@ "id": "warehouse", "name": { "en": "🏭 Warehouse District", - "es": "" + "es": "🏭 Distrito de Almacenes" }, "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.", - "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", "x": 4, @@ -897,11 +897,11 @@ "id": "warehouse_interior", "name": { "en": "📦 Warehouse Interior", - "es": "" + "es": "📦 Interior del almacén" }, "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.", - "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", "x": 4.5, @@ -953,11 +953,11 @@ "id": "subway", "name": { "en": "🚇 Subway Station Entrance", - "es": "" + "es": "🚇 Entrada de la Estación de Metro" }, "description": { "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", "x": -4, @@ -1104,11 +1104,11 @@ "id": "subway_tunnels", "name": { "en": "🚊 Subway Tunnels", - "es": "" + "es": "🚊 Túneles de Metro" }, "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.", - "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", "x": -4.5, @@ -1167,11 +1167,11 @@ "id": "office_building", "name": { "en": "🏢 Office Building", - "es": "" + "es": "🏢 Edificio de Oficinas" }, "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.", - "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", "x": 3.5, @@ -1229,11 +1229,11 @@ "id": "office_interior", "name": { "en": "💼 Office Floors", - "es": "" + "es": "💼 Pisos de Oficinas" }, "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.", - "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", "x": 4, @@ -1297,11 +1297,11 @@ "id": "location_1760791397492", "name": { "en": "Subway Section A", - "es": "" + "es": "Sección A del metro" }, "description": { "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", "x": -5, diff --git a/pwa/src/components/Game.css b/pwa/src/components/Game.css index 0ae9a80..17de56e 100644 --- a/pwa/src/components/Game.css +++ b/pwa/src/components/Game.css @@ -792,6 +792,8 @@ html { line-height: 1; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); display: block; + /* Ensure consistent vertical centering regardless of cost */ + margin-bottom: 0; } .compass-cost { @@ -801,7 +803,12 @@ html { text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); display: block; 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) { @@ -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 { text-align: center; @@ -1008,6 +1090,7 @@ body.no-scroll { } .interact-btn { + width: 100%; padding: 0.5rem 1rem; border: none; background: rgba(255, 193, 7, 0.3); @@ -1896,6 +1979,12 @@ body.no-scroll { margin: auto; } +/* Remove .large variation to enforce uniformity */ +.equipment-slot.large { + min-width: 0; + grid-column: auto; +} + .equipment-slot { background: rgba(0, 0, 0, 0.5); border: 2px solid rgba(255, 255, 255, 0.2); @@ -1908,9 +1997,9 @@ body.no-scroll { /* Changed from center to space-between */ gap: 0.25rem; /* Fixed dimensions for consistent sizing */ - min-height: 100px; - min-width: 80px; - max-width: 100%; + height: 100px; + width: 100%; + box-sizing: border-box; transition: all 0.2s; cursor: pointer; overflow: visible; @@ -1942,10 +2031,6 @@ body.no-scroll { } -.equipment-slot.large { - min-width: 150px; -} - .equipment-slot.empty { border-color: rgba(255, 255, 255, 0.1); background: rgba(0, 0, 0, 0.3); @@ -4265,4 +4350,152 @@ body.no-scroll { border-color: #9ca3af; box-shadow: 0 0 15px rgba(75, 85, 99, 0.3); 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); } \ No newline at end of file diff --git a/pwa/src/components/common/GameProgressBar.tsx b/pwa/src/components/common/GameProgressBar.tsx index 12b3029..1283df6 100644 --- a/pwa/src/components/common/GameProgressBar.tsx +++ b/pwa/src/components/common/GameProgressBar.tsx @@ -49,11 +49,20 @@ export const GameProgressBar: React.FC = ({ case 'enemy_health': return 'linear-gradient(90deg, #ef4444, #b91c1c)'; // Red case 'stamina': return 'linear-gradient(90deg, #eab308, #ca8a04)'; 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': if (percentage < 15) return 'linear-gradient(90deg, #ef4444, #b91c1c)'; // Red if (percentage < 50) return 'linear-gradient(90deg, #eab308, #ca8a04)'; // Yellow return 'linear-gradient(90deg, #10b981, #059669)'; // Green - default: return undefined; + default: return 'linear-gradient(90deg, #10b981, #059669)'; } }; diff --git a/pwa/src/components/game/InventoryModal.css b/pwa/src/components/game/InventoryModal.css index bbd191f..d763113 100644 --- a/pwa/src/components/game/InventoryModal.css +++ b/pwa/src/components/game/InventoryModal.css @@ -228,6 +228,7 @@ flex-direction: column; padding: 1.5rem; overflow: hidden; + background: var(--game-bg-app); } .inventory-search-bar { @@ -240,6 +241,8 @@ border-radius: var(--game-radius-md); margin-bottom: 1.5rem; color: var(--game-text-primary); + width: 100%; + box-sizing: border-box; } .inventory-search-bar input { diff --git a/pwa/src/components/game/InventoryModal.tsx b/pwa/src/components/game/InventoryModal.tsx index 8c7f8c3..486b779 100644 --- a/pwa/src/components/game/InventoryModal.tsx +++ b/pwa/src/components/game/InventoryModal.tsx @@ -339,7 +339,7 @@ function InventoryModal({ + }}>{t('common.all')} )} diff --git a/pwa/src/components/game/LocationView.tsx b/pwa/src/components/game/LocationView.tsx index 34c583e..3ad4050 100644 --- a/pwa/src/components/game/LocationView.tsx +++ b/pwa/src/components/game/LocationView.tsx @@ -1,4 +1,3 @@ - import type { Location, PlayerState, CombatState, Profile, WorkbenchTab } from './types' import { useTranslation } from 'react-i18next' import { useAudio } from '../../contexts/AudioContext' @@ -86,6 +85,7 @@ function LocationView({ }: LocationViewProps) { const { t } = useTranslation() const { playSfx } = useAudio() + return (
@@ -312,28 +312,9 @@ function LocationView({

{t('location.itemsOnGround')}

- {location.items.map((item: any, i: number) => ( -
- {item.image_path ? ( - {getTranslatedText(item.name)} { - (e.target as HTMLImageElement).style.display = 'none'; - const icon = (e.target as HTMLImageElement).nextElementSibling; - if (icon) icon.classList.remove('hidden'); - }} - /> - ) : null} - {item.emoji || '📦'} -
-
- {getTranslatedText(item.name) || 'Unknown Item'} -
- {item.quantity > 1 &&
×{item.quantity}
} -
-
+ {location.items.map((item: any, i: number) => { + return ( +
{item.description &&
{getTranslatedText(item.description)}
} @@ -368,50 +349,79 @@ function LocationView({ )}
}> - - -
- {item.quantity === 1 ? ( - - ) : ( -
- -
- - {item.quantity >= 5 && ( - - )} - {item.quantity >= 10 && ( - - )} - +
+ {item.image_path ? ( + {getTranslatedText(item.name)} { + (e.target as HTMLImageElement).style.display = 'none'; + const icon = (e.target as HTMLImageElement).nextElementSibling; + if (icon) icon.classList.remove('hidden'); + }} + /> + ) : null} + {item.emoji || '📦'} +
+
+ {getTranslatedText(item.name) || 'Unknown Item'} +
+ {item.quantity > 1 &&
×{item.quantity}
} +
+ + +
+ {item.quantity === 1 ? ( + + ) : ( +
+ + + {item.quantity >= 5 && ( + + )} + + {item.quantity >= 10 && ( + + )} + + +
+ )}
- )} -
- ))} +
+ ); + })}
)} diff --git a/pwa/src/components/game/MovementControls.tsx b/pwa/src/components/game/MovementControls.tsx index ab2a9a3..9d5604e 100644 --- a/pwa/src/components/game/MovementControls.tsx +++ b/pwa/src/components/game/MovementControls.tsx @@ -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 ( +
+
🧭
+
+ ); + } + + // 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 }) : + ( +
+
{label}
+
⚡ {t('game.stamina')}: {stamina}
+ {destination &&
{destination}
} +
+ ); + + return ( +
+ + + +
+ ); + }; + return ( <>
-

{t('game.travel')}

+

0 ? 'cooldown-active' : ''}> + {movementCooldown > 0 ? ( + ⏳ {t('messages.waitBeforeMovingSimple', { seconds: movementCooldown })} + ) : ( + {t('game.travel')} + )} +

{/* Top row */} {renderCompassButton('northwest', '↖️', 'nw')} @@ -116,9 +187,7 @@ function MovementControls({ {/* Middle row */} {renderCompassButton('west', '⬅️', 'w')} -
-
🧭
-
+ {renderCenterButton()} {renderCompassButton('east', '➡️', 'e')} {/* Bottom row */} @@ -127,107 +196,43 @@ function MovementControls({ {renderCompassButton('southeast', '↘️', 'se')}
- {/* Cooldown indicator */} - {movementCooldown > 0 && ( -
- ⏳ {t('messages.waitBeforeMovingSimple', { seconds: movementCooldown })} + {/* Special movements (Vertical only now, since Enter/Exit are in center) */} + {(location.directions.includes('up') || location.directions.includes('down')) && ( +
+ {location.directions.includes('up') && ( + 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : ( +
+
{t('directions.up')}
+
⚡ {t('game.stamina')}: {getStaminaCost('up')}
+
+ )}> + +
+ )} + {location.directions.includes('down') && ( + 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : ( +
+
{t('directions.down')}
+
⚡ {t('game.stamina')}: {getStaminaCost('down')}
+
+ )}> + +
+ )}
)} - - {/* Special movements */} -
- {location.directions.includes('up') && ( - 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : ( -
-
{t('directions.up')}
-
⚡ {t('game.stamina')}: {getStaminaCost('up')}
-
- )}> - -
- )} - {location.directions.includes('down') && ( - 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : ( -
-
{t('directions.down')}
-
⚡ {t('game.stamina')}: {getStaminaCost('down')}
-
- )}> - -
- )} - {location.directions.includes('enter') && ( - 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : ( -
-
{t('directions.enter')}
-
⚡ {t('game.stamina')}: {getStaminaCost('enter')}
-
- )}> - -
- )} - {location.directions.includes('inside') && ( - 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : ( -
-
{t('directions.inside')}
-
⚡ {t('game.stamina')}: {getStaminaCost('inside')}
-
- )}> - -
- )} - {location.directions.includes('exit') && ( - 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : t('directions.exit')}> - - - )} - {location.directions.includes('outside') && ( - 0 ? t('messages.waitBeforeMoving', { seconds: movementCooldown }) : combatState ? t('messages.cannotTravelCombat') : ( -
-
{t('directions.outside')}
-
⚡ {t('game.stamina')}: {getStaminaCost('outside')}
-
- )}> - -
- )} -
{/* Surroundings - outside movement controls */} diff --git a/pwa/src/components/game/PlayerSidebar.tsx b/pwa/src/components/game/PlayerSidebar.tsx index 4b2f3a6..b6a7020 100644 --- a/pwa/src/components/game/PlayerSidebar.tsx +++ b/pwa/src/components/game/PlayerSidebar.tsx @@ -6,6 +6,7 @@ import { getTranslatedText } from '../../utils/i18nUtils' import InventoryModal from './InventoryModal' import { GameProgressBar } from '../common/GameProgressBar' import { GameTooltip } from '../common/GameTooltip' +import { useAudio } from '../../contexts/AudioContext' interface PlayerSidebarProps { playerState: PlayerState @@ -40,6 +41,7 @@ function PlayerSidebar({ }: PlayerSidebarProps) { const [showInventory, setShowInventory] = useState(false) const { t } = useTranslation() + const { playSfx } = useAudio() const renderEquipmentSlot = (slot: string, item: any, label: string) => { // Construct the tooltip content if item exists @@ -108,13 +110,13 @@ function PlayerSidebar({ ) : label; // Show label if no item return ( - -
- {item ? ( - <> - - - +
+ {item ? ( + <> + + + +
{item.image_path ? ( )}
- - ) : ( - <> - {label} - - )} -
- + + + ) : ( + + {label} + + )} +
) } diff --git a/pwa/src/components/game/Workbench.css b/pwa/src/components/game/Workbench.css index 11a2783..9c995e7 100644 --- a/pwa/src/components/game/Workbench.css +++ b/pwa/src/components/game/Workbench.css @@ -181,6 +181,8 @@ padding: 1rem; border-bottom: 1px solid var(--game-border-color); background: var(--game-bg-input); + width: 100%; + box-sizing: border-box; /* Match search bar bg */ } diff --git a/pwa/src/components/game/game_pickup.css b/pwa/src/components/game/game_pickup.css new file mode 100644 index 0000000..03c8775 --- /dev/null +++ b/pwa/src/components/game/game_pickup.css @@ -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); +} \ No newline at end of file diff --git a/pwa/src/components/game/hooks/useGameEngine.ts b/pwa/src/components/game/hooks/useGameEngine.ts index 0065d7e..69fbf5e 100644 --- a/pwa/src/components/game/hooks/useGameEngine.ts +++ b/pwa/src/components/game/hooks/useGameEngine.ts @@ -1,5 +1,6 @@ // useGameEngine - Core game state and logic hook import { useState, useEffect, useRef, useCallback } from 'react' +import { useTranslation } from 'react-i18next' import api from '../../../services/api' import type { PlayerState, @@ -143,6 +144,7 @@ export function useGameEngine( token: string | null, _handleWebSocketMessage: (message: any) => Promise ): [GameEngineState, GameEngineActions] { + const { t } = useTranslation() // All state declarations const [playerState, setPlayerState] = useState(null) const [location, setLocation] = useState(null) @@ -843,7 +845,7 @@ export function useGameEngine( const data = response.data let msg = data.message 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) { msg += `\n❤️ HP: ${data.hp_change > 0 ? '+' : ''}${data.hp_change}` diff --git a/pwa/src/i18n/locales/en.json b/pwa/src/i18n/locales/en.json index a005daa..991d8cc 100644 --- a/pwa/src/i18n/locales/en.json +++ b/pwa/src/i18n/locales/en.json @@ -21,7 +21,8 @@ "pickUpAll": "Pick Up All", "qty": "Qty", "enemy": "Enemy", - "you": "You" + "you": "You", + "all": "All" }, "auth": { "login": "Login", @@ -63,7 +64,6 @@ "salvage": "♻️ Salvage", "pickUp": "Pick Up", "drop": "Drop", - "dropAll": "All", "use": "Use", "equip": "Equip", "unequip": "Unequip", @@ -90,7 +90,8 @@ "burning": "Burning", "poisoned": "Poisoned" }, - "effectAlreadyActive": "Effect already active" + "effectAlreadyActive": "Effect already active", + "found": "Found" }, "location": { "recentActivity": "📜 Recent Activity", diff --git a/pwa/src/i18n/locales/es.json b/pwa/src/i18n/locales/es.json index 43bd404..0a7a34b 100644 --- a/pwa/src/i18n/locales/es.json +++ b/pwa/src/i18n/locales/es.json @@ -19,7 +19,8 @@ "fight": "Luchar", "pickUp": "Recoger", "pickUpAll": "Recoger Todo", - "qty": "Cant" + "qty": "Cant", + "all": "Todo" }, "auth": { "login": "Iniciar sesión", @@ -61,7 +62,6 @@ "salvage": "♻️ Desguazar", "pickUp": "Recoger", "drop": "Soltar", - "dropAll": "Todo", "use": "Usar", "equip": "Equipar", "unequip": "Desequipar", @@ -88,7 +88,8 @@ "burning": "Quemadura", "poisoned": "Envenenamiento" }, - "effectAlreadyActive": "Efecto ya activo" + "effectAlreadyActive": "Efecto ya activo", + "found": "Encontrado" }, "location": { "recentActivity": "📜 Actividad Reciente", diff --git a/pwa/src/index.css b/pwa/src/index.css index ad0d8ca..228d11c 100644 --- a/pwa/src/index.css +++ b/pwa/src/index.css @@ -172,4 +172,32 @@ img.emoji { margin: 0 0.05em 0 0.1em; vertical-align: -0.1em; 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); } \ No newline at end of file