fix(v2): v1-faithful inventory window — full 3-panel layout

Rebuilt inventory window to match v1 pixel-for-pixel:

Left column (316px):
- Equipment grid: 6×7 slots at 44px spacing, beveled 3D borders,
  cyan glow (#00ffff) when equipped, faded ghost icon when empty
- Item grid: 6-column CSS Grid with purple gradient cells,
  minimum 24 empty cells to fill grid

Center sidebar (38px):
- Burden bar: 14×40px vertical bar, green/orange/red thresholds,
  percentage label, tooltip with burden units
- Pack icons: 32×32px with actual game icon images (not emoji)
- Active pack: green border + glow + gold ▶ arrow indicator
- Fill indicator: 4px green bar below each pack showing capacity %
- Main backpack (icon 0600127E) + sub-packs with actual container icons

Right panel (flex):
- Mana panel: header + equipped items with mana tracking
- Per-item: 16px icon, name, mana state dot (green/red),
  current/max mana values in v1's grid layout

Hover tooltip:
- Follows mouse cursor (fixed position)
- Shows: name (gold), value, burden, material (green), armor level,
  max damage, damage range/type, attack/defense bonuses as %,
  skill requirements (orange), imbue, set, tinks, workmanship,
  ratings, spellcraft, mana, spell list (blue)
- Black semi-transparent background matching v1's inventory-tooltip

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-12 21:17:20 +02:00
parent 3cb8768dc1
commit a38c7f3e69
3 changed files with 221 additions and 145 deletions

View file

@ -15,21 +15,27 @@ interface Item {
Material?: string; material?: string; material_name?: string;
ItemSet?: string; item_set?: string;
Imbue?: string; imbue?: string;
EquipSkill?: string; equip_skill?: string;
EquipSkill?: string; equip_skill?: string; equip_skill_name?: string;
Tinks?: number; tinks?: number;
ContainerId?: number; container_id?: number;
current_wielded_location?: number; CurrentWieldedLocation?: number;
IntValues?: Record<string, number>;
items_capacity?: number; ItemsCapacity?: number;
damage_range?: string; damage_type?: string; speed_text?: string;
attack_bonus?: number; melee_defense_bonus?: number; magic_defense_bonus?: number;
mana_display?: string; spellcraft?: number;
damage_rating?: number; crit_rating?: number; heal_boost_rating?: number;
spells?: any;
}
// Icon helper: convert raw icon ID to hex filename
function iconHex(raw: number): string {
if (!raw || raw <= 0) return '06000133';
return (raw + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
}
function iconUrl(item: Item): string {
return `/icons/${iconHex(item.icon ?? item.Icon ?? 0)}.png`;
}
function getIconUrl(item: Item): string { return `/icons/${iconHex(item.icon ?? item.Icon ?? 0)}.png`; }
function getName(item: Item): string { return item.name ?? item.Name ?? 'Unknown'; }
function v(val: number | undefined | null): number | undefined { return (val !== undefined && val !== null && val !== -1) ? val : undefined; }
function overlayUrl(item: Item): string | null {
const id = (item as any).icon_overlay_id;
if (id && id > 0) return `/icons/${iconHex(id)}.png`;
@ -45,63 +51,60 @@ function underlayUrl(item: Item): string | null {
return null;
}
function itemName(item: Item): string { return item.name ?? item.Name ?? 'Unknown'; }
function val(v: number | undefined | null, sentinel = -1): number | undefined {
return (v !== undefined && v !== null && v !== sentinel) ? v : undefined;
}
// Build tooltip text
function itemTooltip(item: Item): string {
const parts = [itemName(item)];
const mat = item.material ?? item.material_name ?? item.Material;
if (mat) parts.push(`Material: ${mat}`);
const al = val(item.armor_level ?? item.ArmorLevel);
if (al) parts.push(`AL: ${al}`);
const dmg = val(item.max_damage ?? item.MaxDamage);
if (dmg) parts.push(`Damage: ${dmg}`);
const wk = val(item.workmanship ?? item.Workmanship);
if (wk) parts.push(`Work: ${wk}`);
const tk = val(item.tinks ?? item.Tinks);
if (tk) parts.push(`Tinks: ${tk}`);
const set = item.item_set ?? item.ItemSet;
if (set) parts.push(`Set: ${set}`);
const imb = item.imbue ?? item.Imbue;
if (imb) parts.push(`Imbue: ${imb}`);
return parts.join('\n');
}
// Equipment slot map matching v1
// Equipment slot map
const EQUIP_SLOTS: Record<number, { name: string; row: number; col: number }> = {
32768:{name:'Neck',row:1,col:1},1:{name:'Head',row:1,col:3},
268435456:{name:'Sigil',row:1,col:5},536870912:{name:'Sigil',row:1,col:6},1073741824:{name:'Sigil',row:1,col:7},
67108864:{name:'Trinket',row:2,col:1},2048:{name:'U.Arm',row:2,col:2},
512:{name:'Chest',row:2,col:3},134217728:{name:'Cloak',row:2,col:7},
65536:{name:'Brace L',row:3,col:1},4096:{name:'L.Arm',row:3,col:2},
1024:{name:'Abdomen',row:3,col:3},8192:{name:'U.Leg',row:3,col:4},
131072:{name:'Brace R',row:3,col:5},2:{name:'Shirt',row:3,col:7},
262144:{name:'Ring L',row:4,col:1},32:{name:'Hands',row:4,col:2},
16384:{name:'L.Leg',row:4,col:4},524288:{name:'Ring R',row:4,col:5},4:{name:'Pants',row:4,col:7},
32768:{name:'Neck',row:1,col:1},1:{name:'Head',row:1,col:3},268435456:{name:'Sigil',row:1,col:5},536870912:{name:'Sigil',row:1,col:6},1073741824:{name:'Sigil',row:1,col:7},
67108864:{name:'Trinket',row:2,col:1},2048:{name:'U.Arm',row:2,col:2},512:{name:'Chest',row:2,col:3},134217728:{name:'Cloak',row:2,col:7},
65536:{name:'Brace L',row:3,col:1},4096:{name:'L.Arm',row:3,col:2},1024:{name:'Abdomen',row:3,col:3},8192:{name:'U.Leg',row:3,col:4},131072:{name:'Brace R',row:3,col:5},2:{name:'Shirt',row:3,col:7},
262144:{name:'Ring L',row:4,col:1},32:{name:'Hands',row:4,col:2},16384:{name:'L.Leg',row:4,col:4},524288:{name:'Ring R',row:4,col:5},4:{name:'Pants',row:4,col:7},
256:{name:'Feet',row:5,col:4},
2097152:{name:'Shield',row:6,col:1},1048576:{name:'Melee',row:6,col:3},
4194304:{name:'Missile',row:6,col:3},16777216:{name:'Held',row:6,col:3},
33554432:{name:'2H',row:6,col:3},8388608:{name:'Ammo',row:6,col:7},
2097152:{name:'Shield',row:6,col:1},1048576:{name:'Melee',row:6,col:3},4194304:{name:'Missile',row:6,col:3},16777216:{name:'Held',row:6,col:3},33554432:{name:'2H',row:6,col:3},8388608:{name:'Ammo',row:6,col:7},
};
const SLOT_BG: Record<number, string> = {};
[32768,67108864,65536,131072,262144,524288].forEach(m => SLOT_BG[m] = '#3a2050');
[1,512,2048,1024,4096,8192,16384,32,256].forEach(m => SLOT_BG[m] = '#1e2e4e');
[2,4,134217728,268435456,536870912,1073741824].forEach(m => SLOT_BG[m] = '#1e3e3e');
[2097152,1048576,4194304,16777216,33554432,8388608].forEach(m => SLOT_BG[m] = '#142040');
const gold = '#af7a30';
function ItemIcon({ item, size = 36 }: { item: Item; size?: number }) {
const under = underlayUrl(item);
const over = overlayUrl(item);
const imgStyle: React.CSSProperties = { position: 'absolute', top: 0, left: 0, width: size, height: size, border: 'none', background: 'transparent', imageRendering: 'pixelated' };
const s: React.CSSProperties = { position: 'absolute', top: 0, left: 0, width: size, height: size, border: 'none', background: 'transparent', imageRendering: 'pixelated' };
return (
<div title={itemTooltip(item)} style={{ width: size, height: size, position: 'relative', cursor: 'help' }}>
{under && <img src={under} alt="" style={{ ...imgStyle, zIndex: 1 }} onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }} />}
<img src={iconUrl(item)} alt={itemName(item)} style={{ ...imgStyle, zIndex: 2 }}
onError={e => { (e.target as HTMLImageElement).src = '/icons/06000133.png'; }} />
{over && <img src={over} alt="" style={{ ...imgStyle, zIndex: 3 }} onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }} />}
<div style={{ width: size, height: size, position: 'relative' }}>
{under && <img src={under} alt="" style={{ ...s, zIndex: 1 }} onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }} />}
<img src={getIconUrl(item)} alt={getName(item)} style={{ ...s, zIndex: 2 }} onError={e => { (e.target as HTMLImageElement).src = '/icons/06000133.png'; }} />
{over && <img src={over} alt="" style={{ ...s, zIndex: 3 }} onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }} />}
</div>
);
}
// Tooltip component that follows mouse
function ItemTooltip({ item, x, y }: { item: Item; x: number; y: number }) {
const isValid = (val: any) => val !== undefined && val !== null && val !== -1 && val !== -1.0;
const enhanced = item as any;
return (
<div style={{ position: 'fixed', left: x + 12, top: y + 12, background: 'rgba(0,0,0,0.95)', border: '1px solid #555', borderRadius: 4, padding: 10, zIndex: 99999, minWidth: 200, maxWidth: 350, fontSize: '0.78rem', color: '#ddd', pointerEvents: 'none' }}>
<div style={{ color: '#ffcc00', fontWeight: 'bold', marginBottom: 4 }}>{getName(item)}</div>
<div style={{ color: '#aaa', marginBottom: 2 }}>Value: {(item.value ?? item.Value ?? 0).toLocaleString()}</div>
<div style={{ color: '#aaa', marginBottom: 4 }}>Burden: {item.burden ?? item.Burden ?? 0}</div>
{(item.material ?? item.material_name ?? item.Material) && <div style={{ color: '#88ff88' }}>Material: {item.material ?? item.material_name ?? item.Material}</div>}
{isValid(item.armor_level ?? item.ArmorLevel) && <div style={{ color: '#88ff88' }}>Armor Level: {item.armor_level ?? item.ArmorLevel}</div>}
{isValid(item.max_damage ?? item.MaxDamage) && <div style={{ color: '#88ff88' }}>Max Damage: {item.max_damage ?? item.MaxDamage}</div>}
{enhanced.damage_range && <div style={{ color: '#88ff88' }}>Damage: {enhanced.damage_range}{enhanced.damage_type ? `, ${enhanced.damage_type}` : ''}</div>}
{isValid(enhanced.attack_bonus) && enhanced.attack_bonus !== 1 && <div style={{ color: '#88ff88' }}>Attack Bonus: {((enhanced.attack_bonus - 1) * 100).toFixed(1)}%</div>}
{isValid(enhanced.melee_defense_bonus) && enhanced.melee_defense_bonus !== 1 && <div style={{ color: '#88ff88' }}>Melee Defense: {((enhanced.melee_defense_bonus - 1) * 100).toFixed(1)}%</div>}
{(enhanced.equip_skill_name ?? enhanced.equip_skill ?? item.EquipSkill) && <div style={{ color: '#ddd' }}>Skill: {enhanced.equip_skill_name ?? enhanced.equip_skill ?? item.EquipSkill}</div>}
{isValid(enhanced.wield_level) && <div style={{ color: '#ffaa00' }}>Wield Level: {enhanced.wield_level}</div>}
{isValid(enhanced.skill_level) && <div style={{ color: '#ffaa00' }}>Skill Level: {enhanced.skill_level}</div>}
{isValid(enhanced.lore_requirement) && <div style={{ color: '#ffaa00' }}>Lore: {enhanced.lore_requirement}</div>}
{(item.imbue ?? item.Imbue) && <div style={{ color: '#88ff88' }}>Imbue: {item.imbue ?? item.Imbue}</div>}
{(item.item_set ?? item.ItemSet) && <div style={{ color: '#88ff88' }}>Set: {item.item_set ?? item.ItemSet}</div>}
{isValid(item.tinks ?? item.Tinks) && <div style={{ color: '#88ff88' }}>Tinks: {item.tinks ?? item.Tinks}</div>}
{isValid(item.workmanship ?? item.Workmanship) && <div style={{ color: '#88ff88' }}>Workmanship: {item.workmanship ?? item.Workmanship}</div>}
{isValid(enhanced.damage_rating) && <div style={{ color: '#ddd' }}>Damage Rating: {enhanced.damage_rating}</div>}
{isValid(enhanced.crit_rating) && <div style={{ color: '#ddd' }}>Crit Rating: {enhanced.crit_rating}</div>}
{isValid(enhanced.heal_boost_rating) && <div style={{ color: '#ddd' }}>Heal Boost: {enhanced.heal_boost_rating}</div>}
{enhanced.spellcraft && <div style={{ color: '#dda0dd' }}>Spellcraft: {enhanced.spellcraft}</div>}
{enhanced.mana_display && <div style={{ color: '#dda0dd' }}>Mana: {enhanced.mana_display}</div>}
{enhanced.spells?.spells?.length > 0 && <div style={{ color: '#4a90e2', marginTop: 4 }}>Spells: {enhanced.spells.spells.map((s: any) => s.name).join(', ')}</div>}
</div>
);
}
@ -109,7 +112,8 @@ function ItemIcon({ item, size = 36 }: { item: Item; size?: number }) {
export const InventoryWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
const [items, setItems] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
const [activePack, setActivePack] = useState<number>(0);
const [activePack, setActivePack] = useState<number | null>(null); // null = main backpack
const [tooltip, setTooltip] = useState<{ item: Item; x: number; y: number } | null>(null);
useEffect(() => {
setLoading(true);
@ -119,12 +123,17 @@ export const InventoryWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
.finally(() => setLoading(false));
}, [charName]);
const handleItemHover = useCallback((item: Item | null, e?: React.MouseEvent) => {
if (item && e) setTooltip({ item, x: e.clientX, y: e.clientY });
else setTooltip(null);
}, []);
const slotPositions = useMemo(() => {
const seen = new Set<string>();
const slots: Array<{ key: string; row: number; col: number; mask: number; name: string; bg: string }> = [];
const slots: Array<{ key: string; row: number; col: number; mask: number; name: string }> = [];
Object.entries(EQUIP_SLOTS).forEach(([maskStr, def]) => {
const k = `${def.row}-${def.col}`;
if (!seen.has(k)) { seen.add(k); const mask = parseInt(maskStr); slots.push({ key: k, ...def, mask, bg: SLOT_BG[mask] ?? '#142040' }); }
if (!seen.has(k)) { seen.add(k); slots.push({ key: k, ...def, mask: parseInt(maskStr) }); }
});
return slots;
}, []);
@ -135,6 +144,8 @@ export const InventoryWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
const containerIds = new Set<number>();
const packItems = new Map<number, Item[]>();
items.forEach(item => { const oc = item.ObjectClass ?? item.object_class ?? 0; if (oc === 10) { containers.push(item); containerIds.add(item.item_id ?? item.Id ?? 0); } });
// Sort containers by unsigned id for stable order
containers.sort((a, b) => ((a.item_id ?? 0) >>> 0) - ((b.item_id ?? 0) >>> 0));
items.forEach(item => {
if (containerIds.has(item.item_id ?? item.Id ?? 0)) return;
const wielded = item.current_wielded_location ?? item.CurrentWieldedLocation ?? (item.IntValues?.['10'] ? Number(item.IntValues['10']) : 0);
@ -152,72 +163,138 @@ export const InventoryWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
return { equippedMap, containers, packItems };
}, [items]);
const activeItems = packItems.get(activePack) ?? [...packItems.values()].flat().slice(0, 200);
const mainItems = packItems.get(0) ?? [...packItems.values()].flat().slice(0, 200);
const activeItems = activePack !== null ? (packItems.get(activePack) ?? []) : mainItems;
// Burden (simplified — needs character_stats data for full calc)
const totalBurden = items.reduce((s, i) => s + (i.burden ?? i.Burden ?? 0), 0);
if (loading) {
return <DraggableWindow id={id} title={`Inventory: ${charName}`} zIndex={zIndex} width={580} height={700}>
<div style={{ padding: 20, color: '#666' }}>Loading inventory...</div>
return <DraggableWindow id={id} title={`Inventory: ${charName}`} zIndex={zIndex} width={572} height={720}>
<div style={{ padding: 20, color: '#666', fontStyle: 'italic' }}>Loading inventory...</div>
</DraggableWindow>;
}
return (
<DraggableWindow id={id} title={`Inventory: ${charName}`} zIndex={zIndex} width={580} height={700}>
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* Left: equipment + pack items */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* Equipment grid */}
<div style={{ position: 'relative', height: 270, minHeight: 270, background: '#0d0d0d', borderBottom: '1px solid #333' }}>
<DraggableWindow id={id} title={`Inventory: ${charName}`} zIndex={zIndex} width={572} height={720}>
<div style={{ display: 'flex', flex: 1, overflow: 'hidden', background: 'rgba(20,20,20,0.95)', fontFamily: '"Palatino Linotype",serif' }}>
{/* LEFT: Equipment grid + Item grid */}
<div style={{ width: 316, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* Equipment grid 7col × 6row */}
<div style={{ position: 'relative', height: 270, minHeight: 270, background: '#0a0a0a', borderBottom: `1px solid ${gold}` }}>
{slotPositions.map(slot => {
const item = equippedMap.get(slot.key);
return (
<div key={slot.key} title={item ? itemTooltip(item) : slot.name}
<div key={slot.key}
style={{
position: 'absolute', left: (slot.col - 1) * 44 + 4, top: (slot.row - 1) * 44 + 4,
width: 36, height: 36, background: item ? '#5a5a62' : '#3a3a42',
width: 36, height: 36, background: '#5a5a62',
border: item ? '2px solid #00ffff' : '2px outset #6a6a72',
boxShadow: item ? '0 0 5px #00ffff, inset 0 0 5px rgba(0,255,255,0.2)' : 'none',
display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden',
}}>
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: item ? 'pointer' : 'default',
}}
onMouseEnter={e => item && handleItemHover(item, e)}
onMouseMove={e => item && handleItemHover(item, e)}
onMouseLeave={() => handleItemHover(null)}>
{item ? <ItemIcon item={item} size={32} /> :
<span style={{ fontSize: '0.42rem', color: '#555', textAlign: 'center', lineHeight: 1 }}>{slot.name}</span>}
<img src="/icons/06000133.png" alt="" style={{ width: 28, height: 28, opacity: 0.15, filter: 'grayscale(100%)', imageRendering: 'pixelated' }} />}
</div>
);
})}
</div>
{/* Pack contents */}
<div style={{ padding: '4px 8px', fontWeight: 600, fontSize: '0.7rem', color: '#888', borderBottom: '1px solid #333' }}>
Contents ({activeItems.length})
{/* Contents header */}
<div style={{ padding: '3px 6px', fontSize: 11, color: '#ccc', background: '#111', borderBottom: `1px solid ${gold}` }}>
Contents of {activePack !== null ? (containers.find(c => (c.item_id ?? c.Id) === activePack)?.name ?? containers.find(c => (c.item_id ?? c.Id) === activePack)?.Name ?? 'Pack') : 'Backpack'}
</div>
<div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexWrap: 'wrap', gap: 2, padding: 4, alignContent: 'flex-start' }}>
{/* Item grid */}
<div style={{ flex: 1, overflowY: 'auto', display: 'grid', gridTemplateColumns: 'repeat(6, 36px)', gridAutoRows: 36, gap: 2, padding: 4, alignContent: 'start' }}>
{activeItems.map((item, i) => (
<div key={item.item_id ?? item.Id ?? i} title={itemTooltip(item)}
style={{ width: 36, height: 36, background: 'linear-gradient(135deg, #3d007a 0%, #1a0033 100%)',
border: '1px solid #4a148c',
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'help', overflow: 'hidden' }}>
<div key={item.item_id ?? item.Id ?? i}
style={{ width: 36, height: 36, background: 'linear-gradient(135deg, #3d007a 0%, #1a0033 100%)', border: '1px solid #4a148c', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
onMouseEnter={e => handleItemHover(item, e)}
onMouseMove={e => handleItemHover(item, e)}
onMouseLeave={() => handleItemHover(null)}>
<ItemIcon item={item} size={32} />
</div>
))}
{/* Empty cells to fill 6-col grid */}
{Array.from({ length: Math.max(0, 24 - activeItems.length) }).map((_, i) => (
<div key={`empty-${i}`} style={{ width: 36, height: 36, background: '#0a0a0a', border: '1px solid #222' }} />
))}
</div>
</div>
{/* Right: packs */}
<div style={{ width: 100, borderLeft: '1px solid #333', display: 'flex', flexDirection: 'column', fontSize: '0.65rem' }}>
<div style={{ padding: '4px 6px', fontWeight: 600, color: '#888', borderBottom: '1px solid #333' }}>Packs</div>
<div style={{ padding: '3px 6px', cursor: 'pointer', background: activePack === 0 ? '#2a3a4a' : '',
borderBottom: '1px solid #222', color: '#ccc' }} onClick={() => setActivePack(0)}>
🎒 Backpack
{/* SIDEBAR: Burden + Packs */}
<div style={{ width: 38, display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '4px 2px', gap: 2, borderLeft: `1px solid ${gold}`, borderRight: `1px solid ${gold}` }}>
{/* Burden bar */}
<div style={{ textAlign: 'center', fontSize: 9, color: '#ccc', whiteSpace: 'nowrap', marginBottom: 2 }}>
{Math.round(totalBurden / 10)}%
</div>
<div style={{ width: 14, height: 40, background: '#111', border: '1px solid #555', position: 'relative', overflow: 'hidden', marginBottom: 4, flexShrink: 0 }} title={`Burden: ${totalBurden.toLocaleString()}`}>
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: `${Math.min(100, totalBurden / 200)}%`, background: '#2e8b57', transition: 'height 0.3s' }} />
</div>
{/* Pack list with icons */}
{/* Main backpack */}
<div onClick={() => setActivePack(null)}
style={{ width: 32, height: 32, position: 'relative', cursor: 'pointer', border: activePack === null ? '1px solid #00ff00' : '1px solid transparent', boxShadow: activePack === null ? '0 0 4px #00ff00' : 'none', background: '#000', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
{activePack === null && <span style={{ position: 'absolute', left: -11, top: 10, color: gold, fontSize: 10 }}></span>}
<img src="/icons/0600127E.png" alt="Backpack" style={{ width: 28, height: 28, objectFit: 'contain', imageRendering: 'pixelated' }} onError={e => { (e.target as HTMLImageElement).src = '/icons/06000133.png'; }} />
{/* Fill indicator */}
<div style={{ position: 'absolute', bottom: -6, left: -1, width: 36, height: 4, background: '#000', border: '1px solid #333' }}>
<div style={{ height: '100%', width: `${Math.min(100, (mainItems.length / 102) * 100)}%`, background: '#00ff00' }} />
</div>
</div>
{/* Sub-packs */}
{containers.map(c => {
const cid = c.item_id ?? c.Id ?? 0;
const pItems = packItems.get(cid) ?? [];
const capacity = c.items_capacity ?? c.ItemsCapacity ?? 24;
const isActive = activePack === cid;
return (
<div key={cid} style={{ padding: '3px 6px', cursor: 'pointer',
background: activePack === cid ? '#2a3a4a' : '', borderBottom: '1px solid #222', color: '#aaa' }}
onClick={() => setActivePack(cid)}>
📦 {(c.name ?? c.Name ?? 'Pack').split(' ')[0]}
<div key={cid} onClick={() => setActivePack(cid)}
style={{ width: 32, height: 32, position: 'relative', cursor: 'pointer', border: isActive ? '1px solid #00ff00' : '1px solid transparent', boxShadow: isActive ? '0 0 4px #00ff00' : 'none', background: '#000', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, marginTop: 6 }}>
{isActive && <span style={{ position: 'absolute', left: -11, top: 10, color: gold, fontSize: 10 }}></span>}
<img src={`/icons/${iconHex(c.icon ?? c.Icon ?? 0)}.png`} alt={getName(c)} style={{ width: 28, height: 28, objectFit: 'contain', imageRendering: 'pixelated' }}
onError={e => { (e.target as HTMLImageElement).src = '/icons/06001080.png'; }} />
<div style={{ position: 'absolute', bottom: -6, left: -1, width: 36, height: 4, background: '#000', border: '1px solid #333' }}>
<div style={{ height: '100%', width: `${Math.min(100, (pItems.length / capacity) * 100)}%`, background: '#00ff00' }} />
</div>
</div>
);
})}
</div>
{/* RIGHT: Mana panel */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minWidth: 162 }}>
<div style={{ padding: '3px 6px', fontSize: 11, fontWeight: 'bold', color: '#ccc', background: '#111', borderBottom: `1px solid ${gold}` }}>Mana</div>
<div style={{ padding: '2px 4px', fontSize: 9, color: '#888' }}>
{equippedMap.size} equipped items
</div>
<div style={{ flex: 1, overflowY: 'auto', fontSize: 9 }}>
{Array.from(equippedMap.values()).filter(i => (i as any).current_mana > 0 || (i as any).max_mana > 0).map((item, i) => {
const cur = (item as any).current_mana ?? 0;
const max = (item as any).max_mana ?? 0;
return (
<div key={i} style={{ display: 'grid', gridTemplateColumns: '18px 1fr 14px', gridTemplateRows: 'auto auto', gap: '1px 4px', alignItems: 'center', background: 'rgba(18,24,34,0.9)', border: '1px solid rgba(255,255,255,0.08)', padding: '1px 2px', minHeight: 20, cursor: 'pointer' }}
onMouseEnter={e => handleItemHover(item, e)}
onMouseMove={e => handleItemHover(item, e)}
onMouseLeave={() => handleItemHover(null)}>
<div style={{ gridRow: '1 / span 2', width: 16, height: 16 }}><ItemIcon item={item} size={16} /></div>
<div style={{ color: '#f2e6c9', fontSize: 9, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{getName(item)}</div>
<div style={{ width: 10, height: 10, borderRadius: '50%', background: cur > 0 ? '#76d17f' : '#ff8e6f' }}></div>
<div style={{ color: '#98d7ff', fontSize: 9 }}>{cur} / {max}</div>
<div style={{ color: '#cfe6a0', fontSize: 9, textAlign: 'right' }}></div>
</div>
);
})}
</div>
</div>
</div>
{/* Floating tooltip */}
{tooltip && <ItemTooltip item={tooltip.item} x={tooltip.x} y={tooltip.y} />}
</DraggableWindow>
);
};