Files
echoes-of-the-ash/pwa/src/components/game/PlayerSidebar.tsx

333 lines
14 KiB
TypeScript

import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { PlayerState, Profile, Equipment } from './types'
import { getAssetPath } from '../../utils/assetPath'
import { getTranslatedText } from '../../utils/i18nUtils'
import InventoryModal from './InventoryModal'
interface PlayerSidebarProps {
playerState: PlayerState
profile: Profile | null
equipment: Equipment
inventoryFilter: string
inventoryCategoryFilter: string
mobileMenuOpen: string
onSetInventoryFilter: (filter: string) => void
onSetInventoryCategoryFilter: (category: string) => void
onUseItem: (itemId: number, invId: number) => void
onEquipItem: (invId: number) => void
onUnequipItem: (slot: string) => void
onDropItem: (itemId: number, invId: number, quantity: number) => void
onSpendPoint: (stat: string) => void
}
function PlayerSidebar({
playerState,
profile,
equipment,
inventoryFilter,
inventoryCategoryFilter,
mobileMenuOpen,
onSetInventoryFilter,
onSetInventoryCategoryFilter,
onUseItem,
onEquipItem,
onUnequipItem,
onDropItem,
onSpendPoint
}: PlayerSidebarProps) {
const [showInventory, setShowInventory] = useState(false)
const { t } = useTranslation()
const renderEquipmentSlot = (slot: string, item: any, emoji: string, label: string) => (
<div className={`equipment-slot ${item ? 'filled' : 'empty'}`}>
{item ? (
<>
<button className="equipment-unequip-btn" onClick={() => onUnequipItem(slot)} title={t('game.unequip')}></button>
<div className="equipment-item-content">
{item.image_path ? (
<img
src={getAssetPath(item.image_path)}
alt={getTranslatedText(item.name)}
className="equipment-emoji"
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={`equipment-emoji ${item.image_path ? 'hidden' : ''}`}>{item.emoji}</span>
<span className={`equipment-name ${item.tier ? `tier-${item.tier}` : ''}`}>{getTranslatedText(item.name)}</span>
{item.durability && item.durability !== null && (
<span className="equipment-durability">{item.durability}/{item.max_durability}</span>
)}
</div>
<div className="equipment-tooltip">
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
{/* Use unique_stats if available, otherwise fall back to base stats */}
{(item.unique_stats || item.stats) && Object.keys(item.unique_stats || item.stats).length > 0 && (
<>
{(item.unique_stats?.armor || item.stats?.armor) && (
<div className="item-tooltip-stat">
{t('stats.armor')}: +{item.unique_stats?.armor || item.stats?.armor}
</div>
)}
{(item.unique_stats?.hp_max || item.stats?.hp_max) && (
<div className="item-tooltip-stat">
{t('stats.hp')}: +{item.unique_stats?.hp_max || item.stats?.hp_max}
</div>
)}
{(item.unique_stats?.stamina_max || item.stats?.stamina_max) && (
<div className="item-tooltip-stat">
{t('stats.stamina')}: +{item.unique_stats?.stamina_max || item.stats?.stamina_max}
</div>
)}
{(item.unique_stats?.damage_min !== undefined || item.stats?.damage_min !== undefined) &&
(item.unique_stats?.damage_max !== undefined || item.stats?.damage_max !== undefined) && (
<div className="item-tooltip-stat">
{t('stats.damage')}: {item.unique_stats?.damage_min || item.stats?.damage_min}-{item.unique_stats?.damage_max || item.stats?.damage_max}
</div>
)}
{(item.unique_stats?.weight_capacity || item.stats?.weight_capacity) && (
<div className="item-tooltip-stat">
{t('stats.weight')}: +{item.unique_stats?.weight_capacity || item.stats?.weight_capacity}kg
</div>
)}
{(item.unique_stats?.volume_capacity || item.stats?.volume_capacity) && (
<div className="item-tooltip-stat">
{t('stats.volume')}: +{item.unique_stats?.volume_capacity || item.stats?.volume_capacity}L
</div>
)}
</>
)}
{item.durability !== undefined && item.durability !== null && (
<div className="item-tooltip-stat">
{t('stats.durability')}: {item.durability}/{item.max_durability}
</div>
)}
{item.tier !== undefined && item.tier !== null && item.tier > 0 && (
<div className="item-tooltip-stat">
Tier: {item.tier}
</div>
)}
</div>
</>
) : (
<>
<span className="equipment-emoji">{emoji}</span>
<span className="equipment-slot-label">{label}</span>
</>
)}
</div>
)
return (
<div className={`right-sidebar mobile-menu-panel ${mobileMenuOpen === 'right' ? 'open' : ''}`}>
{/* Profile Stats */}
<div className="profile-sidebar">
<h3>{t('game.character')}</h3>
<div className="sidebar-stat-bars">
<div className="sidebar-stat-bar">
<div className="sidebar-stat-header">
<span className="sidebar-stat-label">{t('stats.hp')}</span>
<span className="sidebar-stat-numbers">{playerState.health}/{playerState.max_health}</span>
</div>
<div className="sidebar-progress-bar">
<div
className="sidebar-progress-fill health"
style={{ width: `${(playerState.health / playerState.max_health) * 100}%` }}
></div>
<span className="progress-percentage">{Math.round((playerState.health / playerState.max_health) * 100)}%</span>
</div>
</div>
<div className="sidebar-stat-bar">
<div className="sidebar-stat-header">
<span className="sidebar-stat-label">{t('stats.stamina')}</span>
<span className="sidebar-stat-numbers">{playerState.stamina}/{playerState.max_stamina}</span>
</div>
<div className="sidebar-progress-bar">
<div
className="sidebar-progress-fill stamina"
style={{ width: `${(playerState.stamina / playerState.max_stamina) * 100}%` }}
></div>
<span className="progress-percentage">{Math.round((playerState.stamina / playerState.max_stamina) * 100)}%</span>
</div>
</div>
</div>
{profile && (
<div className="sidebar-stats">
<div className="sidebar-stat-row">
<span className="sidebar-label">{t('stats.level')}:</span>
<span className="sidebar-value">{profile.level}</span>
</div>
<div className="sidebar-stat-bar">
<div className="sidebar-stat-header">
<span className="sidebar-stat-label">{t('stats.xp')}</span>
<span className="sidebar-stat-numbers">{profile.xp} / {(profile.level * 100)}</span>
</div>
<div className="sidebar-progress-bar">
<div
className="sidebar-progress-fill xp"
style={{ width: `${(profile.xp / (profile.level * 100)) * 100}%` }}
></div>
<span className="progress-percentage">{Math.round((profile.xp / (profile.level * 100)) * 100)}%</span>
</div>
</div>
{profile.unspent_points > 0 && (
<div className="sidebar-stat-row highlight">
<span className="sidebar-label">{t('stats.unspentPoints')}:</span>
<span className="sidebar-value">{profile.unspent_points}</span>
</div>
)}
<div className="sidebar-divider"></div>
{/* Compact 2x2 Stats Grid */}
<div className="stats-grid">
<div className="sidebar-stat-row compact">
<span className="sidebar-label">{t('stats.strength')}:</span>
<span className="sidebar-value">{profile.strength}</span>
{profile.unspent_points > 0 && (
<button className="stat-plus-btn" onClick={() => onSpendPoint('strength')}>+</button>
)}
</div>
<div className="sidebar-stat-row compact">
<span className="sidebar-label">{t('stats.agility')}:</span>
<span className="sidebar-value">{profile.agility}</span>
{profile.unspent_points > 0 && (
<button className="stat-plus-btn" onClick={() => onSpendPoint('agility')}>+</button>
)}
</div>
<div className="sidebar-stat-row compact">
<span className="sidebar-label">{t('stats.endurance')}:</span>
<span className="sidebar-value">{profile.endurance}</span>
{profile.unspent_points > 0 && (
<button className="stat-plus-btn" onClick={() => onSpendPoint('endurance')}>+</button>
)}
</div>
<div className="sidebar-stat-row compact">
<span className="sidebar-label">{t('stats.intellect')}:</span>
<span className="sidebar-value">{profile.intellect}</span>
{profile.unspent_points > 0 && (
<button className="stat-plus-btn" onClick={() => onSpendPoint('intellect')}>+</button>
)}
</div>
</div>
<div className="sidebar-divider"></div>
{/* Inventory Capacity - matching HP/Stamina/XP style */}
<div className="sidebar-stat-bar">
<div className="sidebar-stat-header">
<span className="sidebar-stat-label">{t('stats.weight')}</span>
<span className="sidebar-stat-numbers">{(profile.current_weight || 0).toFixed(1)}/{profile.max_weight || 0}kg</span>
</div>
<div className="sidebar-progress-bar">
<div
className="sidebar-progress-fill weight"
style={{ width: `${Math.min(100, ((profile.current_weight || 0) / (profile.max_weight || 1)) * 100)}%` }}
></div>
<span className="progress-percentage">{Math.round(Math.min(100, ((profile.current_weight || 0) / (profile.max_weight || 1)) * 100))}%</span>
</div>
</div>
<div className="sidebar-stat-bar">
<div className="sidebar-stat-header">
<span className="sidebar-stat-label">{t('stats.volume')}</span>
<span className="sidebar-stat-numbers">{(profile.current_volume || 0).toFixed(1)}/{profile.max_volume || 0}L</span>
</div>
<div className="sidebar-progress-bar">
<div
className="sidebar-progress-fill volume"
style={{ width: `${Math.min(100, ((profile.current_volume || 0) / (profile.max_volume || 1)) * 100)}%` }}
></div>
<span className="progress-percentage">{Math.round(Math.min(100, ((profile.current_volume || 0) / (profile.max_volume || 1)) * 100))}%</span>
</div>
</div>
<button
className="open-inventory-btn"
onClick={() => setShowInventory(true)}
style={{
width: '100%',
padding: '1rem',
marginTop: '1rem',
backgroundColor: '#2c3e50',
border: '1px solid #34495e',
borderRadius: '8px',
color: '#ecf0f1',
fontSize: '1.1rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0.5rem',
transition: 'all 0.2s'
}}
>
{t('game.inventory')}
</button>
</div>
)}
</div>
{/* Equipment Display - Proper Grid Layout */}
<div className="equipment-sidebar">
<h3>{t('game.equipment')}</h3>
<div className="equipment-grid">
{/* Row 1: Head */}
<div className="equipment-row">
{renderEquipmentSlot('head', equipment.head, '🪖', t('equipment.head'))}
</div>
{/* Row 2: Weapon, Torso, Backpack */}
<div className="equipment-row three-cols">
{renderEquipmentSlot('weapon', equipment.weapon, '⚔️', t('equipment.weapon'))}
{renderEquipmentSlot('torso', equipment.torso, '👕', t('equipment.torso'))}
{renderEquipmentSlot('backpack', equipment.backpack, '🎒', t('equipment.backpack'))}
</div>
{/* Row 3: Legs & Feet */}
<div className="equipment-row two-cols">
{renderEquipmentSlot('legs', equipment.legs, '👖', t('equipment.legs'))}
{renderEquipmentSlot('feet', equipment.feet, '👟', t('equipment.feet'))}
</div>
</div>
</div>
{/* Inventory Modal */}
{showInventory && profile && (
<InventoryModal
playerState={playerState}
profile={profile}
equipment={equipment}
inventoryFilter={inventoryFilter}
inventoryCategoryFilter={inventoryCategoryFilter}
onClose={() => setShowInventory(false)}
onSetInventoryFilter={onSetInventoryFilter}
onSetInventoryCategoryFilter={onSetInventoryCategoryFilter}
onUseItem={onUseItem}
onEquipItem={onEquipItem}
onUnequipItem={onUnequipItem}
onDropItem={onDropItem}
/>
)}
</div>
)
}
export default PlayerSidebar