fix(ui): stable sort for ground items, improved loot modal (images/desc, outside click)
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user