- {location.items.map((item: any, i: number) => (
-
- {item.image_path ? (
-
})
{
- (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 ? (
+
})
{
+ (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 }) :
+ (
+
-
{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 ? (
![]()
)}
- >
- ) : (
- <>
-
- >
- )}
-
-
+
+ >
+ ) : (
+
+
+
+ )}
+
)
}
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