Fix GameTooltip blocking issue and translate Found string
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -49,11 +49,20 @@ export const GameProgressBar: React.FC<GameProgressBarProps> = ({
|
||||
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)';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -339,7 +339,7 @@ function InventoryModal({
|
||||
<button className="action-btn drop" onClick={() => {
|
||||
playSfx('/audio/sfx/drop.wav')
|
||||
onDropItem(item.item_id, item.id, item.quantity)
|
||||
}}>{t('game.dropAll')}</button>
|
||||
}}>{t('common.all')}</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="location-view">
|
||||
<div className="location-info">
|
||||
@@ -312,28 +312,9 @@ function LocationView({
|
||||
<div className="entity-section items-section">
|
||||
<h3>{t('location.itemsOnGround')}</h3>
|
||||
<div className="entity-list">
|
||||
{location.items.map((item: any, i: number) => (
|
||||
<div key={i} className="entity-card item-card">
|
||||
{item.image_path ? (
|
||||
<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">
|
||||
{location.items.map((item: any, i: number) => {
|
||||
return (
|
||||
<div key={i} className="entity-card item-card">
|
||||
<GameTooltip content={
|
||||
<div className="item-info-tooltip-content">
|
||||
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
|
||||
@@ -368,50 +349,79 @@ function LocationView({
|
||||
)}
|
||||
</div>
|
||||
}>
|
||||
<button className="entity-action-btn info">{t('common.info')}</button>
|
||||
</GameTooltip>
|
||||
</div>
|
||||
{item.quantity === 1 ? (
|
||||
<button
|
||||
className="entity-action-btn pickup"
|
||||
onClick={() => {
|
||||
playSfx('/audio/sfx/pickup.wav')
|
||||
onPickup(item.id, 1)
|
||||
}}
|
||||
>
|
||||
{t('common.pickUp')}
|
||||
</button>
|
||||
) : (
|
||||
<div className="item-pickup-btn-container">
|
||||
<button className="entity-action-btn pickup">{t('common.pickUp')} ▼</button>
|
||||
<div className="item-pickup-menu">
|
||||
<button className="item-pickup-option" onClick={() => {
|
||||
playSfx('/audio/sfx/pickup.wav')
|
||||
onPickup(item.id, 1)
|
||||
}}>{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 className="entity-content-wrapper" style={{ display: 'flex', alignItems: 'center', gap: '1rem', flex: 1, cursor: 'help' }}>
|
||||
{item.image_path ? (
|
||||
<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>
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<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">
|
||||
{/* Top row */}
|
||||
{renderCompassButton('northwest', '↖️', 'nw')}
|
||||
@@ -116,9 +187,7 @@ function MovementControls({
|
||||
|
||||
{/* Middle row */}
|
||||
{renderCompassButton('west', '⬅️', 'w')}
|
||||
<div className="compass-center">
|
||||
<div className="compass-icon">🧭</div>
|
||||
</div>
|
||||
{renderCenterButton()}
|
||||
{renderCompassButton('east', '➡️', 'e')}
|
||||
|
||||
{/* Bottom row */}
|
||||
@@ -127,107 +196,43 @@ function MovementControls({
|
||||
{renderCompassButton('southeast', '↘️', 'se')}
|
||||
</div>
|
||||
|
||||
{/* Cooldown indicator */}
|
||||
{movementCooldown > 0 && (
|
||||
<div className="cooldown-indicator">
|
||||
⏳ {t('messages.waitBeforeMovingSimple', { seconds: movementCooldown })}
|
||||
{/* Special movements (Vertical only now, since Enter/Exit are in center) */}
|
||||
{(location.directions.includes('up') || location.directions.includes('down')) && (
|
||||
<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>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* Surroundings - outside movement controls */}
|
||||
|
||||
@@ -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 (
|
||||
<GameTooltip content={tooltipContent}>
|
||||
<div className={`equipment-slot game-slot ${item ? 'filled' : 'empty'}`}>
|
||||
{item ? (
|
||||
<>
|
||||
<GameTooltip content={t('game.unequip')}>
|
||||
<button className="equipment-unequip-btn game-btn game-btn-icon" onClick={(e) => { e.stopPropagation(); onUnequipItem(slot); }}>✕</button>
|
||||
</GameTooltip>
|
||||
<div className={`equipment-slot game-slot ${item ? 'filled' : 'empty'}`}>
|
||||
{item ? (
|
||||
<>
|
||||
<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>
|
||||
</GameTooltip>
|
||||
<GameTooltip content={tooltipContent}>
|
||||
<div className="equipment-item-content">
|
||||
{item.image_path ? (
|
||||
<img
|
||||
@@ -138,19 +140,19 @@ function PlayerSidebar({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<img
|
||||
src={getAssetPath(`/images/placeholder/${slot}_placeholder.webp`)}
|
||||
alt={label}
|
||||
className="equipment-placeholder-img"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain', opacity: 0.5 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</GameTooltip>
|
||||
</GameTooltip>
|
||||
</>
|
||||
) : (
|
||||
<GameTooltip content={label}>
|
||||
<img
|
||||
src={getAssetPath(`/images/placeholder/${slot}_placeholder.webp`)}
|
||||
alt={label}
|
||||
className="equipment-placeholder-img"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain', opacity: 0.5 }}
|
||||
/>
|
||||
</GameTooltip>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
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<void>
|
||||
): [GameEngineState, GameEngineActions] {
|
||||
const { t } = useTranslation()
|
||||
// All state declarations
|
||||
const [playerState, setPlayerState] = useState<PlayerState | null>(null)
|
||||
const [location, setLocation] = useState<Location | null>(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}`
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user