fix(v2): inventory window — v1-style equipment grid + pack sidebar
Rebuilt InventoryWindow to match v1's three-panel layout: Left column: - Equipment grid: 6×7 slot grid (44px per slot) with color-coded backgrounds matching v1's EQUIP_SLOTS map (purple=jewelry, blue=armor, teal=clothing, dark blue=weapons). Items placed in correct slots using wielded_location bitmask matching. - Pack contents: 40px item tiles in a wrapping grid, hover for full item tooltip (name, material, AL, damage, workmanship) Right sidebar: - Pack browser: main backpack + sub-packs (containers). Click to switch which pack's contents are shown. Item counts per pack. Equipment slot layout matches v1 exactly: Row 1 (Neck, Head, Sigils), Row 2 (Trinket, Upper Arm, Chest, Cloak), Row 3 (Bracelets, Lower Arm, Abdomen, Upper Leg, Shirt), Row 4 (Rings, Hands, Lower Leg, Pants), Row 5 (Feet), Row 6 (Shield, Weapon, Ammo). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
52e1bcd6b8
commit
863adb0c3c
3 changed files with 222 additions and 150 deletions
|
|
@ -1,32 +1,46 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { DraggableWindow } from './DraggableWindow';
|
||||
import { apiFetch } from '../../api/client';
|
||||
|
||||
interface Props { id: string; charName: string; zIndex: number; }
|
||||
|
||||
interface Item {
|
||||
Name: string;
|
||||
ObjectClass?: number;
|
||||
Icon?: number;
|
||||
Value?: number;
|
||||
Burden?: number;
|
||||
ArmorLevel?: number;
|
||||
MaxDamage?: number;
|
||||
Workmanship?: number;
|
||||
Material?: string;
|
||||
ItemSet?: string;
|
||||
Imbue?: string;
|
||||
EquipSkill?: string;
|
||||
Mastery?: string;
|
||||
Tinks?: number;
|
||||
WieldLevel?: number;
|
||||
ContainerId?: number;
|
||||
item_id?: number; Name: string; ObjectClass?: number; object_class?: number;
|
||||
Icon?: number; Value?: number; Burden?: number; ArmorLevel?: number;
|
||||
MaxDamage?: number; Workmanship?: number; Material?: string; ItemSet?: string;
|
||||
Imbue?: string; EquipSkill?: string; Tinks?: number; ContainerId?: number;
|
||||
container_id?: number; current_wielded_location?: number;
|
||||
}
|
||||
|
||||
// Equipment slot positions matching v1's EQUIP_SLOTS
|
||||
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 (Blue)', row: 1, col: 5 }, 536870912: { name: 'Sigil (Yellow)', row: 1, col: 6 },
|
||||
1073741824: { name: 'Sigil (Red)', row: 1, col: 7 },
|
||||
67108864: { name: 'Trinket', row: 2, col: 1 }, 2048: { name: 'Upper Arm', row: 2, col: 2 },
|
||||
512: { name: 'Chest Armor', row: 2, col: 3 }, 134217728: { name: 'Cloak', row: 2, col: 7 },
|
||||
65536: { name: 'Bracelet (L)', row: 3, col: 1 }, 4096: { name: 'Lower Arm', row: 3, col: 2 },
|
||||
1024: { name: 'Abdomen', row: 3, col: 3 }, 8192: { name: 'Upper Leg', row: 3, col: 4 },
|
||||
131072: { name: 'Bracelet (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: 'Lower 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: 'Two Handed', row: 6, col: 3 }, 8388608: { name: 'Ammo', row: 6, col: 7 },
|
||||
};
|
||||
|
||||
const SLOT_COLOR: Record<number, string> = {};
|
||||
[32768,67108864,65536,131072,262144,524288].forEach(m => SLOT_COLOR[m] = '#4a3060');
|
||||
[1,512,2048,1024,4096,8192,16384,32,256].forEach(m => SLOT_COLOR[m] = '#2a3a5a');
|
||||
[2,4,134217728,268435456,536870912,1073741824].forEach(m => SLOT_COLOR[m] = '#2a4a4a');
|
||||
[2097152,1048576,4194304,16777216,33554432,8388608].forEach(m => SLOT_COLOR[m] = '#1a2a4a');
|
||||
|
||||
export const InventoryWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState('');
|
||||
const [activePack, setActivePack] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
|
@ -36,90 +50,147 @@ export const InventoryWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
|
|||
.finally(() => setLoading(false));
|
||||
}, [charName]);
|
||||
|
||||
const filtered = filter
|
||||
? items.filter(i => i.Name?.toLowerCase().includes(filter.toLowerCase()))
|
||||
: items;
|
||||
// Build unique slot positions
|
||||
const slotPositions = useMemo(() => {
|
||||
const seen = new Set<string>();
|
||||
const slots: Array<{ key: string; row: number; col: number; mask: number; name: string; color: 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, color: SLOT_COLOR[mask] ?? '#1a2a4a' });
|
||||
}
|
||||
});
|
||||
return slots;
|
||||
}, []);
|
||||
|
||||
// Separate equipped items (those with equipment-related fields) from pack contents
|
||||
const equipped = filtered.filter(i =>
|
||||
(i.ArmorLevel && i.ArmorLevel > 0) || (i.MaxDamage && i.MaxDamage > 0) ||
|
||||
i.Imbue || i.ItemSet || (i.Tinks && i.Tinks > 0)
|
||||
);
|
||||
const other = filtered.filter(i => !equipped.includes(i));
|
||||
// Categorize items
|
||||
const { equippedMap, containers, packItems } = useMemo(() => {
|
||||
const equippedMap = new Map<string, Item>();
|
||||
const containers: Item[] = [];
|
||||
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 ?? 0); }
|
||||
});
|
||||
|
||||
items.forEach(item => {
|
||||
if (containerIds.has(item.item_id ?? 0)) return;
|
||||
const wielded = item.current_wielded_location ?? 0;
|
||||
if (wielded > 0) {
|
||||
// Place in first matching slot
|
||||
for (const [maskStr, def] of Object.entries(EQUIP_SLOTS)) {
|
||||
const slotMask = parseInt(maskStr);
|
||||
if ((wielded & slotMask) === slotMask) {
|
||||
const key = `${def.row}-${def.col}`;
|
||||
if (!equippedMap.has(key)) { equippedMap.set(key, item); break; }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const cid = item.container_id ?? item.ContainerId ?? 0;
|
||||
if (!packItems.has(cid)) packItems.set(cid, []);
|
||||
packItems.get(cid)!.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return { equippedMap, containers, packItems };
|
||||
}, [items]);
|
||||
|
||||
const activeItems = packItems.get(activePack) ?? packItems.get(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>
|
||||
</DraggableWindow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DraggableWindow id={id} title={`Inventory: ${charName}`} zIndex={zIndex} width={620} height={550}>
|
||||
<div style={{ padding: '4px 8px', borderBottom: '1px solid #333' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={filter}
|
||||
onChange={e => setFilter(e.target.value)}
|
||||
placeholder="Filter items..."
|
||||
style={{ width: '100%', padding: '4px 8px', fontSize: '0.75rem', background: '#222', color: '#eee', border: '1px solid #444', borderRadius: 3, outline: 'none', boxSizing: 'border-box' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ overflowY: 'auto', flex: 1, fontSize: '0.73rem' }}>
|
||||
{loading ? (
|
||||
<div style={{ padding: 16, color: '#666' }}>Loading inventory...</div>
|
||||
) : items.length === 0 ? (
|
||||
<div style={{ padding: 16, color: '#666' }}>No inventory data</div>
|
||||
) : (
|
||||
<>
|
||||
{equipped.length > 0 && (
|
||||
<>
|
||||
<div style={{ padding: '6px 8px', fontWeight: 600, color: '#88f', fontSize: '0.7rem', borderBottom: '1px solid #333' }}>
|
||||
Equipment & Notable Items ({equipped.length})
|
||||
<DraggableWindow id={id} title={`Inventory: ${charName}`} zIndex={zIndex} width={580} height={700}>
|
||||
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
||||
{/* Left column: equipment grid + pack items */}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
{/* Equipment grid */}
|
||||
<div style={{ position: 'relative', height: 270, minHeight: 270, background: '#111', borderBottom: '1px solid #333' }}>
|
||||
{slotPositions.map(slot => {
|
||||
const item = equippedMap.get(slot.key);
|
||||
return (
|
||||
<div key={slot.key} title={item ? `${item.Name}${item.Material ? ` (${item.Material})` : ''}` : slot.name}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: (slot.col - 1) * 44 + 4,
|
||||
top: (slot.row - 1) * 44 + 4,
|
||||
width: 40, height: 40,
|
||||
background: item ? slot.color : `${slot.color}44`,
|
||||
border: `1px solid ${item ? '#666' : '#333'}`,
|
||||
borderRadius: 3,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: item ? '0.58rem' : '0.5rem',
|
||||
color: item ? '#ddd' : '#555',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
cursor: item ? 'help' : 'default',
|
||||
padding: 1, textAlign: 'center', lineHeight: 1.1,
|
||||
}}>
|
||||
{item ? item.Name?.split(' ').slice(0, 2).join(' ') : slot.name}
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid #333', color: '#777', fontSize: '0.65rem' }}>
|
||||
<th style={{ textAlign: 'left', padding: '3px 6px' }}>Item</th>
|
||||
<th style={{ textAlign: 'left', padding: '3px 4px' }}>Material</th>
|
||||
<th style={{ textAlign: 'left', padding: '3px 4px' }}>Set</th>
|
||||
<th style={{ textAlign: 'left', padding: '3px 4px' }}>Imbue</th>
|
||||
<th style={{ textAlign: 'right', padding: '3px 4px' }}>AL</th>
|
||||
<th style={{ textAlign: 'right', padding: '3px 4px' }}>Dmg</th>
|
||||
<th style={{ textAlign: 'right', padding: '3px 4px' }}>Wk</th>
|
||||
<th style={{ textAlign: 'right', padding: '3px 4px' }}>Tink</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{equipped.map((item, i) => (
|
||||
<tr key={i} style={{ borderBottom: '1px solid #1a1a1a', color: '#ccc' }}>
|
||||
<td style={{ padding: '2px 6px', maxWidth: 180, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: 500 }}>{item.Name}</td>
|
||||
<td style={{ padding: '2px 4px', color: '#888', fontSize: '0.68rem' }}>{item.Material || ''}</td>
|
||||
<td style={{ padding: '2px 4px', color: '#9d9', fontSize: '0.68rem' }}>{item.ItemSet || ''}</td>
|
||||
<td style={{ padding: '2px 4px', color: '#da8', fontSize: '0.68rem' }}>{item.Imbue || ''}</td>
|
||||
<td style={{ textAlign: 'right', padding: '2px 4px' }}>{item.ArmorLevel && item.ArmorLevel > 0 ? item.ArmorLevel : ''}</td>
|
||||
<td style={{ textAlign: 'right', padding: '2px 4px', color: '#f88' }}>{item.MaxDamage && item.MaxDamage > 0 ? item.MaxDamage : ''}</td>
|
||||
<td style={{ textAlign: 'right', padding: '2px 4px' }}>{item.Workmanship && item.Workmanship > 0 ? item.Workmanship : ''}</td>
|
||||
<td style={{ textAlign: 'right', padding: '2px 4px', color: '#8af' }}>{item.Tinks && item.Tinks > 0 ? item.Tinks : ''}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
{other.length > 0 && (
|
||||
<>
|
||||
<div style={{ padding: '6px 8px', fontWeight: 600, color: '#888', fontSize: '0.7rem', borderBottom: '1px solid #333' }}>
|
||||
Pack Contents ({other.length})
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 2, padding: 4 }}>
|
||||
{other.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{ fontSize: '0.65rem', padding: '2px 6px', background: '#252525', borderRadius: 3, color: '#aaa', cursor: 'default' }}
|
||||
title={`${item.Name}${item.Value ? ` (${item.Value} pyreal)` : ''}`}
|
||||
>
|
||||
{item.Name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Pack contents header */}
|
||||
<div style={{ padding: '4px 8px', fontWeight: 600, fontSize: '0.7rem', color: '#888', borderBottom: '1px solid #333' }}>
|
||||
Contents ({activeItems.length} items)
|
||||
</div>
|
||||
|
||||
{/* Item grid */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexWrap: 'wrap', gap: 2, padding: 4, alignContent: 'flex-start' }}>
|
||||
{activeItems.map((item, i) => (
|
||||
<div key={item.item_id ?? i}
|
||||
title={`${item.Name}${item.Material ? ` [${item.Material}]` : ''}${item.ArmorLevel && item.ArmorLevel > 0 ? ` AL:${item.ArmorLevel}` : ''}${item.MaxDamage && item.MaxDamage > 0 ? ` Dmg:${item.MaxDamage}` : ''}${item.Workmanship ? ` WK:${item.Workmanship}` : ''}`}
|
||||
style={{
|
||||
width: 40, height: 40,
|
||||
background: '#252525', border: '1px solid #333', borderRadius: 2,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: '0.48rem', color: '#aaa', padding: 1,
|
||||
textAlign: 'center', lineHeight: 1.1, cursor: 'help', overflow: 'hidden',
|
||||
}}>
|
||||
{item.Name?.split(' ').slice(0, 2).join('\n')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right sidebar: packs + burden */}
|
||||
<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>
|
||||
|
||||
{/* Main backpack */}
|
||||
<div style={{ padding: '3px 6px', cursor: 'pointer', background: activePack === 0 ? '#2a3a4a' : '',
|
||||
borderBottom: '1px solid #222', color: '#ccc' }}
|
||||
onClick={() => setActivePack(0)}>
|
||||
🎒 Backpack
|
||||
<span style={{ color: '#666', marginLeft: 4 }}>({(packItems.get(0) ?? []).length})</span>
|
||||
</div>
|
||||
|
||||
{/* Sub-packs */}
|
||||
{containers.map(c => {
|
||||
const cid = c.item_id ?? 0;
|
||||
const count = (packItems.get(cid) ?? []).length;
|
||||
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?.split(' ')[0] ?? 'Pack'}
|
||||
<span style={{ color: '#666', marginLeft: 4 }}>({count})</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</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-BmKo5eig.js"></script>
|
||||
<script type="module" crossorigin src="/v2/assets/index-BXNfpUzm.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