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:
parent
3cb8768dc1
commit
a38c7f3e69
3 changed files with 221 additions and 145 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -5,7 +5,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Mosswart Overlord v2</title>
|
||||
<link rel="icon" type="image/png" href="/icons/7735.png" />
|
||||
<script type="module" crossorigin src="/v2/assets/index-Cx0MIJoT.js"></script>
|
||||
<script type="module" crossorigin src="/v2/assets/index-BeLlP-ET.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/v2/assets/index-DrsM2PEe.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue