fix(ui): stable sort for ground items, improved loot modal (images/desc, outside click)

This commit is contained in:
Joan
2026-02-07 22:44:51 +01:00
parent ff9472048d
commit c9d180379a

View File

@@ -360,96 +360,98 @@ function LocationView({
</div> </div>
)} )}
{/* Items on Ground */} {/* Items on Ground - Stable Sort */}
{location.items.length > 0 && ( {location.items.length > 0 && (
<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 grid-view"> <div className="entity-list grid-view">
{location.items.map((item: any, i: number) => { {[...location.items]
const isShaking = failedActionItemId == item.id; .sort((a: any, b: any) => (a.id || 0) - (b.id || 0))
const itemId = `item-${item.id}-${i}`; .map((item: any, i: number) => {
const isShaking = failedActionItemId == item.id;
const itemId = `item-${item.id}-${i}`;
// Pickup Options Helper // Pickup Options Helper
const renderPickupOptions = () => { const renderPickupOptions = () => {
const options = []; const options = [];
options.push({ label: 'x1', qty: 1 }); options.push({ label: 'x1', qty: 1 });
if (item.quantity >= 5) options.push({ label: 'x5', qty: 5 }); if (item.quantity >= 5) options.push({ label: 'x5', qty: 5 });
if (item.quantity >= 10) options.push({ label: 'x10', qty: 10 }); if (item.quantity >= 10) options.push({ label: 'x10', qty: 10 });
if (item.quantity > 1) options.push({ label: t('common.all'), qty: item.quantity }); if (item.quantity > 1) options.push({ label: t('common.all'), qty: item.quantity });
return options.map(opt => ( return options.map(opt => (
<GameButton <GameButton
key={opt.label} key={opt.label}
variant="success" variant="success"
size="sm" size="sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); // Prevent closing e.stopPropagation(); // Prevent closing
onPickup(item.id, opt.qty); onPickup(item.id, opt.qty);
}} }}
style={{ width: '100%', justifyContent: 'flex-start', marginBottom: '2px' }} style={{ width: '100%', justifyContent: 'flex-start', marginBottom: '2px' }}
>
🤚 {t('common.pickUp')} ({opt.label})
</GameButton>
));
};
return (
<div key={i} className={`entity-card item-card ${isShaking ? 'shake' : ''} grid-card`}
onClick={(e) => handleDropdownClick(e, itemId)}
>
<GameTooltip content={
<div className="item-info-tooltip-content">
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
{item.weight !== undefined && item.weight > 0 && (
<div className="item-tooltip-stat">
{t('stats.weight')}: {item.weight}kg {item.quantity > 1 && `(Total: ${(item.weight * item.quantity).toFixed(2)}kg)`}
</div>
)}
{item.volume !== undefined && item.volume > 0 && (
<div className="item-tooltip-stat">
📦 {t('stats.volume')}: {item.volume}L {item.quantity > 1 && `(Total: ${(item.volume * item.quantity).toFixed(2)}L)`}
</div>
)}
</div>
}>
<div className="entity-content-wrapper grid-content">
{item.image_path ? (
<img
src={getAssetPath(item.image_path)}
alt={getTranslatedText(item.name)}
className="entity-icon"
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
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' : ''}`} style={!item.image_path ? { fontSize: '2rem' } : {}}>{item.emoji || '📦'}</span>
{item.quantity > 1 && (
<div className="grid-quantity">x{item.quantity}</div>
)}
</div>
</GameTooltip>
{activeDropdown === itemId && (
<GameDropdown
isOpen={true}
onClose={() => setActiveDropdown(null)}
position={dropdownPos}
width="160px"
> >
<div className="game-dropdown-header">{getTranslatedText(item.name)}</div> 🤚 {t('common.pickUp')} ({opt.label})
<div className="pickup-options"> </GameButton>
{renderPickupOptions()} ));
};
return (
<div key={item.id} className={`entity-card item-card ${isShaking ? 'shake' : ''} grid-card`}
onClick={(e) => handleDropdownClick(e, itemId)}
>
<GameTooltip content={
<div className="item-info-tooltip-content">
{item.description && <div className="item-tooltip-desc">{getTranslatedText(item.description)}</div>}
{item.weight !== undefined && item.weight > 0 && (
<div className="item-tooltip-stat">
{t('stats.weight')}: {item.weight}kg {item.quantity > 1 && `(Total: ${(item.weight * item.quantity).toFixed(2)}kg)`}
</div>
)}
{item.volume !== undefined && item.volume > 0 && (
<div className="item-tooltip-stat">
📦 {t('stats.volume')}: {item.volume}L {item.quantity > 1 && `(Total: ${(item.volume * item.quantity).toFixed(2)}L)`}
</div>
)}
</div> </div>
</GameDropdown> }>
)} <div className="entity-content-wrapper grid-content">
</div> {item.image_path ? (
); <img
})} src={getAssetPath(item.image_path)}
alt={getTranslatedText(item.name)}
className="entity-icon"
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
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' : ''}`} style={!item.image_path ? { fontSize: '2rem' } : {}}>{item.emoji || '📦'}</span>
{item.quantity > 1 && (
<div className="grid-quantity">x{item.quantity}</div>
)}
</div>
</GameTooltip>
{activeDropdown === itemId && (
<GameDropdown
isOpen={true}
onClose={() => setActiveDropdown(null)}
position={dropdownPos}
width="160px"
>
<div className="game-dropdown-header">{getTranslatedText(item.name)}</div>
<div className="pickup-options">
{renderPickupOptions()}
</div>
</GameDropdown>
)}
</div>
);
})}
</div> </div>
</div> </div>
)} )}
@@ -496,8 +498,8 @@ function LocationView({
{/* Corpse Loot Overlay Modal */} {/* Corpse Loot Overlay Modal */}
{expandedCorpse && corpseDetails && corpseDetails.loot_items && ( {expandedCorpse && corpseDetails && corpseDetails.loot_items && (
<div className="corpse-loot-overlay"> <div className="corpse-loot-overlay" onClick={() => onSetExpandedCorpse(null)}>
<div className="corpse-loot-modal"> <div className="corpse-loot-modal" onClick={(e) => e.stopPropagation()}>
<div className="corpse-details-header"> <div className="corpse-details-header">
<h4>{t('location.lootableItems')}</h4> <h4>{t('location.lootableItems')}</h4>
<button <button
@@ -512,10 +514,28 @@ function LocationView({
<div className="corpse-items-list"> <div className="corpse-items-list">
{corpseDetails.loot_items.map((item: any) => ( {corpseDetails.loot_items.map((item: any) => (
<div key={item.index} className={`corpse-item ${!item.can_loot ? 'locked' : ''}`}> <div key={item.index} className={`corpse-item ${!item.can_loot ? 'locked' : ''}`}>
<div className="corpse-item-info"> {/* Item Image */}
<div className="corpse-item-image">
{item.image_path ? (
<img
src={getAssetPath(item.image_path)}
alt={item.item_name}
className="item-img-thumb"
style={{ width: '40px', height: '40px', objectFit: 'contain', marginRight: '10px' }}
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
// Fallback emoji next to it will show if image fails?
// Current logic doesn't have fallback emoji element sibling, just keeping it simple.
}}
/>
) : <span style={{ fontSize: '2rem', marginRight: '10px' }}>{item.emoji || '📦'}</span>}
</div>
<div className="corpse-item-info" style={{ flex: 1 }}>
<div className="corpse-item-name"> <div className="corpse-item-name">
{item.emoji} {getTranslatedText(item.item_name)} {getTranslatedText(item.item_name)}
</div> </div>
{item.description && <div className="corpse-item-desc" style={{ fontSize: '0.75rem', color: '#a0aec0' }}>{getTranslatedText(item.description)}</div>}
<div className="corpse-item-qty"> <div className="corpse-item-qty">
{t('common.qty')}: {item.quantity_min}{item.quantity_min !== item.quantity_max ? `-${item.quantity_max}` : ''} {t('common.qty')}: {item.quantity_min}{item.quantity_min !== item.quantity_max ? `-${item.quantity_max}` : ''}
</div> </div>
@@ -525,6 +545,7 @@ function LocationView({
</div> </div>
)} )}
</div> </div>
<GameTooltip content={!item.can_loot ? `Requires ${getTranslatedText(item.required_tool_name)}` : 'Loot this item'}> <GameTooltip content={!item.can_loot ? `Requires ${getTranslatedText(item.required_tool_name)}` : 'Loot this item'}>
<button <button
className="corpse-item-loot-btn" className="corpse-item-loot-btn"