v0.13.2 - Rewrite armory.js with actual page DOM support

- Scrap the old floating panel approach (wrong DOM selectors)
- Now injects a toolbar directly into the equipment list on the Armory page
- Parses equipment from native #equiplist table rows using correct
  DOM selectors (hover_equip, select_equip, eqids[] checkboxes)
- Detects current screen (organize/sell/salvage/modify) to show relevant
  bulk-select buttons per tab
- Sell tab: select Crude, select <=Fair
- Salvage tab: select salvage-grade, select <=Average
- Organize tab: select equipped (for pinning)
- Plus: Select All, Invert, Clear on every screen
- Grades by quality: Keep (Magnificent+), Keep (Superior),
  Salvage (Average-Fair), Sell (Crude)
- Resets properly across page navigations (single-use id guard)
This commit is contained in:
GaboGG 2026-07-26 11:33:10 -04:00
parent 1669167373
commit 2f680a630d
3 changed files with 456 additions and 477 deletions

View file

@ -2608,207 +2608,193 @@ function enhanceAbilities() {
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
// ARMORY — smart equipment inventory management // ARMORY — smart equipment inventory management
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
//
// The Armory (ss=am) has native tabs: Organize, Modify, Repair, Soulbind,
// Purchase, Sell, Salvage. Each tab has its own equipment list with
// checkboxes (<input name="eqids[]">). The page has native submit buttons
// for each action (Sell Equipment, Salvage Equipment, etc.).
//
// Our enhancements:
// - Add bulk-select buttons above the equipment list
// - Auto-check items based on quality thresholds
// - Show quick summary of what's worth keeping vs discarding
// ───────────────────────────────────────────────────────────────────────
// Analyse equipment quality and assign a grade
function gradeEquipment(name) { function gradeEquipment(name) {
const q = KB.qualities.find(q => name.includes(q)); const q = KB.qualities.find(q => name.includes(q));
if (!q) return { action: 'keep', label: '❓', color: '#888', reason: 'Unknown' }; if (!q) return { action: 'keep', label: '❓', color: '#888', reason: 'Unknown' };
const qi = KB.qualities.indexOf(q); const qi = KB.qualities.indexOf(q);
// Magnificent+ (index >= 5): always keep
if (qi >= 5) return { action: 'keep', label: '✅ Keep', color: '#0f0', reason: `${q}+` }; if (qi >= 5) return { action: 'keep', label: '✅ Keep', color: '#0f0', reason: `${q}+` };
// Superior (index 3): borderline — keep if it's a weapon/armor we need if (qi >= 3) return { action: 'keep', label: '⬆ Keep', color: '#fdcb00', reason: q };
if (qi === 3) return { action: 'keep', label: '⬆ Keep', color: '#fdcb00', reason: 'Superior' };
// Average/Fair (1-2): salvage for materials
if (qi >= 1) return { action: 'salvage', label: '♻ Salvage', color: '#888', reason: q }; if (qi >= 1) return { action: 'salvage', label: '♻ Salvage', color: '#888', reason: q };
// Crude (0): sell
return { action: 'sell', label: '💰 Sell', color: '#f80', reason: 'Crude' }; return { action: 'sell', label: '💰 Sell', color: '#f80', reason: 'Crude' };
} }
function enhanceArmory() { function enhanceArmory() {
const main = document.getElementById('mainpane'); const equipList = document.getElementById('equiplist');
if (!main || document.getElementById('hv-armory-panel')) return; if (!equipList || document.getElementById('hv-armory-bar')) return;
// ── Parse equipment from the page ── // ── Parse current equipment in the list ──
const items = []; const items = [];
let currentCategory = ''; let currentCategory = '';
$$('tr', main).forEach(row => { $$('tr', equipList).forEach(row => {
// Category label rows (Weapon, Armor sections)
if (row.className === 'eqtplabel') { if (row.className === 'eqtplabel') {
currentCategory = (row.textContent || '').trim(); currentCategory = (row.textContent || '').trim();
return; return;
} }
// Find the equip item link/ID const cb = row.querySelector('input[name="eqids[]"]');
const links = row.querySelectorAll('a[href*="set_equip"]'); if (!cb) return;
if (links.length === 0) return;
// Item has an onmouseover for tooltip const id = cb.value;
const omo = row.getAttribute('onmouseover') || '';
const idMatch = omo.match(/hover_equip\((\d+)\)/);
if (!idMatch) return;
const id = idMatch[1];
const label = row.querySelector('label'); const label = row.querySelector('label');
const name = label ? (label.textContent || '').trim() : ''; const name = label ? (label.textContent || '').trim() : '';
// Detect level — HV renders Lv with CSS font in a .btm1 inside the row // Status icons in the label text
const levelDiv = row.querySelector('.btm1 label, .btm1'); const equipped = name.includes('🗡');
const levelText = levelDiv ? readCSSText(levelDiv) : ''; const locked = name.includes('🔒');
const lvMatch = levelText.match(/lv\.?\s*(\d+)/i); const pinned = name.includes('📌');
const itemLevel = lvMatch ? parseInt(lvMatch[1]) : 0; const stored = name.includes('📦');
const protected_ = name.includes('🛡');
// Detect status indicators // Strip status icons for clean name
const rowHtml = row.innerHTML || ''; const cleanName = name.replace(/[🗡🔒📌📦🛡️]/g, '').trim();
const isLocked = rowHtml.includes('lock.png') || rowHtml.includes('lck.png');
const isEquipped = rowHtml.includes('eqp.png') || rowHtml.includes('Equipped');
const isPinned = rowHtml.includes('pin.png');
const isStored = rowHtml.includes('store.png');
// Check for Sell/Salvage buttons in this row
const sellBtn = row.querySelector('input[value="Sell"], a[onclick*="sell_equip"]');
const salvBtn = row.querySelector('input[value="Salvage"], a[onclick*="salvage_equip"]');
const quality = KB.qualities.find(q => name.includes(q)) || '?';
items.push({ items.push({
id, name, quality, slot: currentCategory, id, name: cleanName, slot: currentCategory,
level: itemLevel, equipped, locked, pinned, stored, protected: protected_,
locked: isLocked, equipped: isEquipped, checkbox: cb, row,
pinned: isPinned, stored: isStored, grade: gradeEquipment(cleanName),
canSell: !!sellBtn, canSalvage: !!salvBtn,
row,
}); });
}); });
if (items.length === 0) return; if (items.length === 0) return;
// ── Grade each item ── // ── Detect which armory tab we're on ──
const graded = items.map(item => ({ let screen = 'organize';
...item, const url = window.location.href || '';
grade: gradeEquipment(item.name, item.slot), const sm = url.match(/screen=(\w+)/);
})); if (sm) screen = sm[1];
// ── Sorting: equipped/pinned first, then by quality descending, then by level ── // ── Counts ──
const qwords = KB.qualities; const sellCount = items.filter(i => i.grade.action === 'sell' && !i.equipped && !i.locked).length;
graded.sort((a, b) => { const salvCount = items.filter(i => i.grade.action === 'salvage' && !i.equipped && !i.locked).length;
if (a.equipped && !b.equipped) return -1; const keepCount = items.filter(i => i.grade.action === 'keep').length;
if (!a.equipped && b.equipped) return 1;
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
const qDiff = (qwords.indexOf(b.quality) - qwords.indexOf(a.quality));
if (qDiff !== 0) return qDiff;
return b.level - a.level;
});
// ── Summary counts ── // ── Build the toolbar ──
const keepCount = graded.filter(i => i.grade.action === 'keep').length; const toolbar = document.createElement('div');
const sellCount = graded.filter(i => i.grade.action === 'sell').length; toolbar.id = 'hv-armory-bar';
const salvCount = graded.filter(i => i.grade.action === 'salvage').length; toolbar.style.cssText = css({
const equippedCount = graded.filter(i => i.equipped).length; display: 'flex',
gap: '4px',
// ── Build bulk action buttons ── flexWrap: 'wrap',
function bulkSell() { padding: '4px 6px',
const toSell = graded.filter(i => i.grade.action === 'sell' && i.canSell && !i.equipped && !i.locked); marginBottom: '2px',
if (toSell.length === 0) return; background: '#1a1a2e',
toSell.forEach(item => { borderRadius: '4px',
const btn = item.row.querySelector('input[value="Sell"], a[onclick*="sell_equip"]');
if (btn) btn.click();
});
}
function bulkSalvage() {
const toSalv = graded.filter(i => i.grade.action === 'salvage' && i.canSalvage && !i.equipped && !i.locked);
if (toSalv.length === 0) return;
toSalv.forEach(item => {
const btn = item.row.querySelector('input[value="Salvage"], a[onclick*="salvage_equip"]');
if (btn) btn.click();
});
}
// ── Build the panel ──
const qColor = (q) => { const i = qwords.indexOf(q); return i >= 5 ? '#0f0' : i >= 3 ? '#fdcb00' : i >= 1 ? '#888' : '#f80'; };
let body = `<div style="display:flex;gap:4px;margin-bottom:6px">
<input type="button" value="💰 Sell Crude (${sellCount})" style="padding:3px 8px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:10px" id="hv-armory-bulk-sell">
<input type="button" value="♻ Salvage ≤Fair (${salvCount})" style="padding:3px 8px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px" id="hv-armory-bulk-salvage">
</div>`;
body += `<div style="border-top:1px solid #374151;margin:4px 0;padding:4px 0;font-size:9px;color:#888">
Keep: ${keepCount} | Salvage: ${salvCount} | 💰 Sell: ${sellCount} | 🗡 Equipped: ${equippedCount}
</div>`;
// ── Table ──
body += '<table style="width:100%;font-size:9px;border-collapse:collapse">';
body += '<tr style="color:#666"><th style="text-align:left;padding:1px 2px">Q</th>' +
'<th style="text-align:left;padding:1px 2px">Item</th>' +
'<th style="text-align:center;padding:1px 2px">Lv</th>' +
'<th style="text-align:right;padding:1px 2px">Action</th></tr>';
let lastSlot = '';
for (const item of graded) {
// Slot separator
if (item.slot && item.slot !== lastSlot) {
lastSlot = item.slot;
body += `<tr><td colspan="4" style="padding:4px 2px 1px;color:#555;font-size:8px;border-bottom:1px solid #333">${item.slot}</td></tr>`;
}
const statusIcon = item.equipped ? '🗡' : item.locked ? '🔒' : item.stored ? '📦' : item.pinned ? '📌' : '';
const nameShort = item.name.length > 28 ? item.name.slice(0, 27) + '…' : item.name;
body += `<tr><td style="padding:1px 2px;color:${qColor(item.quality)}">${item.quality[0]}</td>
<td style="padding:1px 2px">${statusIcon} ${nameShort}</td>
<td style="padding:1px 2px;text-align:center;color:#888">${item.level || '?'}</td>
<td style="padding:1px 2px;text-align:right;color:${item.grade.color}">${item.grade.label}</td></tr>`;
}
body += '</table>';
body += '<div style="border-top:1px solid #374151;margin:6px 0;padding:4px 0;font-size:9px;color:#555">' +
'Status: 🗡Equipped 📌Pinned 🔒Locked 📦Stored</div>';
// ── Build panel container ──
const collapsed = localStorage[SP + 'armoryCollapsed'] === '1';
const panel = document.createElement('div');
panel.id = 'hv-armory-panel';
panel.style.cssText = css({
position: 'fixed',
top: '60px',
right: '4px',
zIndex: '9995',
background: '#111827',
color: '#d1d5db',
padding: '0',
borderRadius: '8px',
fontSize: '10px', fontSize: '10px',
fontFamily: 'monospace', fontFamily: 'monospace',
maxWidth: '350px', alignItems: 'center',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 0 15px rgba(0,0,0,0.6)',
border: '1px solid #374151',
}); });
panel.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 8px;cursor:pointer" id="hv-armory-header"> // Summary
<b style="color:#fdcb00;font-size:10px">🛡 Armory (${items.length})</b> const summary = document.createElement('span');
<span id="hv-armory-toggle" style="color:#888;font-size:12px">${collapsed ? '▶' : '▼'}</span> summary.style.cssText = 'color:#888;margin-right:6px';
</div> summary.textContent = `${keepCount}${salvCount} 💰${sellCount}`;
<div id="hv-armory-body" style="padding:0 8px 6px;display:${collapsed ? 'none' : 'block'}">${body}</div>`; toolbar.appendChild(summary);
panel.querySelector('#hv-armory-header').onclick = () => { // ── Smart auto-select: check boxes based on grade ──
const b = document.getElementById('hv-armory-body'); function autoSelect(action) {
const t = document.getElementById('hv-armory-toggle'); items.forEach(item => {
const h = b.style.display === 'none'; if (item.grade.action !== action) return;
b.style.display = h ? 'block' : 'none'; if (item.equipped || item.locked) return;
t.textContent = h ? '▼' : '▶'; item.checkbox.checked = true;
localStorage[SP + 'armoryCollapsed'] = h ? '0' : '1'; });
// Trigger the game's update function if available
if (typeof update_selected_count === 'function') update_selected_count();
}
// ── Select by quality ──
function selectByQuality(below) {
const idx = KB.qualities.indexOf(below);
items.forEach(item => {
const qi = KB.qualities.indexOf(KB.qualities.find(q => item.name.includes(q)));
if (qi < 0) return;
if (qi <= idx && !item.equipped && !item.locked) {
item.checkbox.checked = true;
}
});
if (typeof update_selected_count === 'function') update_selected_count();
}
function clearAll() {
items.forEach(item => item.checkbox.checked = false);
if (typeof update_selected_count === 'function') update_selected_count();
}
// Buttons — only show relevant ones for current screen
if (screen === 'sell') {
const b = document.createElement('input');
b.type = 'button';
b.value = `💰 Select Crude (${sellCount})`;
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:10px';
b.onclick = () => autoSelect('sell');
toolbar.appendChild(b);
const b2 = document.createElement('input');
b2.type = 'button';
b2.value = '💰 Select ≤Fair';
b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#a00;color:#fff;border:none;border-radius:3px;font-size:10px';
b2.onclick = () => selectByQuality('Fair');
toolbar.appendChild(b2);
}
if (screen === 'salvage') {
const b = document.createElement('input');
b.type = 'button';
b.value = `♻ Select Salvage (${salvCount})`;
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px';
b.onclick = () => autoSelect('salvage');
toolbar.appendChild(b);
const b2 = document.createElement('input');
b2.type = 'button';
b2.value = '♻ Select ≤Average';
b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px';
b2.onclick = () => selectByQuality('Average');
toolbar.appendChild(b2);
// "Sell Salvaged Equipment" toggle is already native
}
if (screen === 'organize') {
const b = document.createElement('input');
b.type = 'button';
b.value = '📌 Pin Equipped';
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#2a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px';
b.onclick = () => {
// Select all equipped items so they can be pinned
items.forEach(item => { if (item.equipped) item.checkbox.checked = true; });
if (typeof update_selected_count === 'function') update_selected_count();
}; };
toolbar.appendChild(b);
}
document.body.appendChild(panel); // Clear button always available
const clear = document.createElement('input');
clear.type = 'button';
clear.value = '✗ Clear';
clear.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px';
clear.onclick = clearAll;
toolbar.appendChild(clear);
// ── Wire bulk buttons ── // ── Insert into page ──
const sellBulk = document.getElementById('hv-armory-bulk-sell'); const eqSelect = document.getElementById('equipselect_outer') ||
const salvBulk = document.getElementById('hv-armory-bulk-salvage'); document.querySelector('#equipselect_left, #armory_right > div');
if (equipList && equipList.parentNode) {
if (sellBulk) sellBulk.onclick = bulkSell; equipList.parentNode.insertBefore(toolbar, equipList);
if (salvBulk) salvBulk.onclick = bulkSalvage; }
} }
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════

View file

@ -2608,207 +2608,193 @@ function enhanceAbilities() {
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
// ARMORY — smart equipment inventory management // ARMORY — smart equipment inventory management
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
//
// The Armory (ss=am) has native tabs: Organize, Modify, Repair, Soulbind,
// Purchase, Sell, Salvage. Each tab has its own equipment list with
// checkboxes (<input name="eqids[]">). The page has native submit buttons
// for each action (Sell Equipment, Salvage Equipment, etc.).
//
// Our enhancements:
// - Add bulk-select buttons above the equipment list
// - Auto-check items based on quality thresholds
// - Show quick summary of what's worth keeping vs discarding
// ───────────────────────────────────────────────────────────────────────
// Analyse equipment quality and assign a grade
function gradeEquipment(name) { function gradeEquipment(name) {
const q = KB.qualities.find(q => name.includes(q)); const q = KB.qualities.find(q => name.includes(q));
if (!q) return { action: 'keep', label: '❓', color: '#888', reason: 'Unknown' }; if (!q) return { action: 'keep', label: '❓', color: '#888', reason: 'Unknown' };
const qi = KB.qualities.indexOf(q); const qi = KB.qualities.indexOf(q);
// Magnificent+ (index >= 5): always keep
if (qi >= 5) return { action: 'keep', label: '✅ Keep', color: '#0f0', reason: `${q}+` }; if (qi >= 5) return { action: 'keep', label: '✅ Keep', color: '#0f0', reason: `${q}+` };
// Superior (index 3): borderline — keep if it's a weapon/armor we need if (qi >= 3) return { action: 'keep', label: '⬆ Keep', color: '#fdcb00', reason: q };
if (qi === 3) return { action: 'keep', label: '⬆ Keep', color: '#fdcb00', reason: 'Superior' };
// Average/Fair (1-2): salvage for materials
if (qi >= 1) return { action: 'salvage', label: '♻ Salvage', color: '#888', reason: q }; if (qi >= 1) return { action: 'salvage', label: '♻ Salvage', color: '#888', reason: q };
// Crude (0): sell
return { action: 'sell', label: '💰 Sell', color: '#f80', reason: 'Crude' }; return { action: 'sell', label: '💰 Sell', color: '#f80', reason: 'Crude' };
} }
function enhanceArmory() { function enhanceArmory() {
const main = document.getElementById('mainpane'); const equipList = document.getElementById('equiplist');
if (!main || document.getElementById('hv-armory-panel')) return; if (!equipList || document.getElementById('hv-armory-bar')) return;
// ── Parse equipment from the page ── // ── Parse current equipment in the list ──
const items = []; const items = [];
let currentCategory = ''; let currentCategory = '';
$$('tr', main).forEach(row => { $$('tr', equipList).forEach(row => {
// Category label rows (Weapon, Armor sections)
if (row.className === 'eqtplabel') { if (row.className === 'eqtplabel') {
currentCategory = (row.textContent || '').trim(); currentCategory = (row.textContent || '').trim();
return; return;
} }
// Find the equip item link/ID const cb = row.querySelector('input[name="eqids[]"]');
const links = row.querySelectorAll('a[href*="set_equip"]'); if (!cb) return;
if (links.length === 0) return;
// Item has an onmouseover for tooltip const id = cb.value;
const omo = row.getAttribute('onmouseover') || '';
const idMatch = omo.match(/hover_equip\((\d+)\)/);
if (!idMatch) return;
const id = idMatch[1];
const label = row.querySelector('label'); const label = row.querySelector('label');
const name = label ? (label.textContent || '').trim() : ''; const name = label ? (label.textContent || '').trim() : '';
// Detect level — HV renders Lv with CSS font in a .btm1 inside the row // Status icons in the label text
const levelDiv = row.querySelector('.btm1 label, .btm1'); const equipped = name.includes('🗡');
const levelText = levelDiv ? readCSSText(levelDiv) : ''; const locked = name.includes('🔒');
const lvMatch = levelText.match(/lv\.?\s*(\d+)/i); const pinned = name.includes('📌');
const itemLevel = lvMatch ? parseInt(lvMatch[1]) : 0; const stored = name.includes('📦');
const protected_ = name.includes('🛡');
// Detect status indicators // Strip status icons for clean name
const rowHtml = row.innerHTML || ''; const cleanName = name.replace(/[🗡🔒📌📦🛡️]/g, '').trim();
const isLocked = rowHtml.includes('lock.png') || rowHtml.includes('lck.png');
const isEquipped = rowHtml.includes('eqp.png') || rowHtml.includes('Equipped');
const isPinned = rowHtml.includes('pin.png');
const isStored = rowHtml.includes('store.png');
// Check for Sell/Salvage buttons in this row
const sellBtn = row.querySelector('input[value="Sell"], a[onclick*="sell_equip"]');
const salvBtn = row.querySelector('input[value="Salvage"], a[onclick*="salvage_equip"]');
const quality = KB.qualities.find(q => name.includes(q)) || '?';
items.push({ items.push({
id, name, quality, slot: currentCategory, id, name: cleanName, slot: currentCategory,
level: itemLevel, equipped, locked, pinned, stored, protected: protected_,
locked: isLocked, equipped: isEquipped, checkbox: cb, row,
pinned: isPinned, stored: isStored, grade: gradeEquipment(cleanName),
canSell: !!sellBtn, canSalvage: !!salvBtn,
row,
}); });
}); });
if (items.length === 0) return; if (items.length === 0) return;
// ── Grade each item ── // ── Detect which armory tab we're on ──
const graded = items.map(item => ({ let screen = 'organize';
...item, const url = window.location.href || '';
grade: gradeEquipment(item.name, item.slot), const sm = url.match(/screen=(\w+)/);
})); if (sm) screen = sm[1];
// ── Sorting: equipped/pinned first, then by quality descending, then by level ── // ── Counts ──
const qwords = KB.qualities; const sellCount = items.filter(i => i.grade.action === 'sell' && !i.equipped && !i.locked).length;
graded.sort((a, b) => { const salvCount = items.filter(i => i.grade.action === 'salvage' && !i.equipped && !i.locked).length;
if (a.equipped && !b.equipped) return -1; const keepCount = items.filter(i => i.grade.action === 'keep').length;
if (!a.equipped && b.equipped) return 1;
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
const qDiff = (qwords.indexOf(b.quality) - qwords.indexOf(a.quality));
if (qDiff !== 0) return qDiff;
return b.level - a.level;
});
// ── Summary counts ── // ── Build the toolbar ──
const keepCount = graded.filter(i => i.grade.action === 'keep').length; const toolbar = document.createElement('div');
const sellCount = graded.filter(i => i.grade.action === 'sell').length; toolbar.id = 'hv-armory-bar';
const salvCount = graded.filter(i => i.grade.action === 'salvage').length; toolbar.style.cssText = css({
const equippedCount = graded.filter(i => i.equipped).length; display: 'flex',
gap: '4px',
// ── Build bulk action buttons ── flexWrap: 'wrap',
function bulkSell() { padding: '4px 6px',
const toSell = graded.filter(i => i.grade.action === 'sell' && i.canSell && !i.equipped && !i.locked); marginBottom: '2px',
if (toSell.length === 0) return; background: '#1a1a2e',
toSell.forEach(item => { borderRadius: '4px',
const btn = item.row.querySelector('input[value="Sell"], a[onclick*="sell_equip"]');
if (btn) btn.click();
});
}
function bulkSalvage() {
const toSalv = graded.filter(i => i.grade.action === 'salvage' && i.canSalvage && !i.equipped && !i.locked);
if (toSalv.length === 0) return;
toSalv.forEach(item => {
const btn = item.row.querySelector('input[value="Salvage"], a[onclick*="salvage_equip"]');
if (btn) btn.click();
});
}
// ── Build the panel ──
const qColor = (q) => { const i = qwords.indexOf(q); return i >= 5 ? '#0f0' : i >= 3 ? '#fdcb00' : i >= 1 ? '#888' : '#f80'; };
let body = `<div style="display:flex;gap:4px;margin-bottom:6px">
<input type="button" value="💰 Sell Crude (${sellCount})" style="padding:3px 8px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:10px" id="hv-armory-bulk-sell">
<input type="button" value="♻ Salvage ≤Fair (${salvCount})" style="padding:3px 8px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px" id="hv-armory-bulk-salvage">
</div>`;
body += `<div style="border-top:1px solid #374151;margin:4px 0;padding:4px 0;font-size:9px;color:#888">
Keep: ${keepCount} | Salvage: ${salvCount} | 💰 Sell: ${sellCount} | 🗡 Equipped: ${equippedCount}
</div>`;
// ── Table ──
body += '<table style="width:100%;font-size:9px;border-collapse:collapse">';
body += '<tr style="color:#666"><th style="text-align:left;padding:1px 2px">Q</th>' +
'<th style="text-align:left;padding:1px 2px">Item</th>' +
'<th style="text-align:center;padding:1px 2px">Lv</th>' +
'<th style="text-align:right;padding:1px 2px">Action</th></tr>';
let lastSlot = '';
for (const item of graded) {
// Slot separator
if (item.slot && item.slot !== lastSlot) {
lastSlot = item.slot;
body += `<tr><td colspan="4" style="padding:4px 2px 1px;color:#555;font-size:8px;border-bottom:1px solid #333">${item.slot}</td></tr>`;
}
const statusIcon = item.equipped ? '🗡' : item.locked ? '🔒' : item.stored ? '📦' : item.pinned ? '📌' : '';
const nameShort = item.name.length > 28 ? item.name.slice(0, 27) + '…' : item.name;
body += `<tr><td style="padding:1px 2px;color:${qColor(item.quality)}">${item.quality[0]}</td>
<td style="padding:1px 2px">${statusIcon} ${nameShort}</td>
<td style="padding:1px 2px;text-align:center;color:#888">${item.level || '?'}</td>
<td style="padding:1px 2px;text-align:right;color:${item.grade.color}">${item.grade.label}</td></tr>`;
}
body += '</table>';
body += '<div style="border-top:1px solid #374151;margin:6px 0;padding:4px 0;font-size:9px;color:#555">' +
'Status: 🗡Equipped 📌Pinned 🔒Locked 📦Stored</div>';
// ── Build panel container ──
const collapsed = localStorage[SP + 'armoryCollapsed'] === '1';
const panel = document.createElement('div');
panel.id = 'hv-armory-panel';
panel.style.cssText = css({
position: 'fixed',
top: '60px',
right: '4px',
zIndex: '9995',
background: '#111827',
color: '#d1d5db',
padding: '0',
borderRadius: '8px',
fontSize: '10px', fontSize: '10px',
fontFamily: 'monospace', fontFamily: 'monospace',
maxWidth: '350px', alignItems: 'center',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 0 15px rgba(0,0,0,0.6)',
border: '1px solid #374151',
}); });
panel.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 8px;cursor:pointer" id="hv-armory-header"> // Summary
<b style="color:#fdcb00;font-size:10px">🛡 Armory (${items.length})</b> const summary = document.createElement('span');
<span id="hv-armory-toggle" style="color:#888;font-size:12px">${collapsed ? '▶' : '▼'}</span> summary.style.cssText = 'color:#888;margin-right:6px';
</div> summary.textContent = `${keepCount}${salvCount} 💰${sellCount}`;
<div id="hv-armory-body" style="padding:0 8px 6px;display:${collapsed ? 'none' : 'block'}">${body}</div>`; toolbar.appendChild(summary);
panel.querySelector('#hv-armory-header').onclick = () => { // ── Smart auto-select: check boxes based on grade ──
const b = document.getElementById('hv-armory-body'); function autoSelect(action) {
const t = document.getElementById('hv-armory-toggle'); items.forEach(item => {
const h = b.style.display === 'none'; if (item.grade.action !== action) return;
b.style.display = h ? 'block' : 'none'; if (item.equipped || item.locked) return;
t.textContent = h ? '▼' : '▶'; item.checkbox.checked = true;
localStorage[SP + 'armoryCollapsed'] = h ? '0' : '1'; });
// Trigger the game's update function if available
if (typeof update_selected_count === 'function') update_selected_count();
}
// ── Select by quality ──
function selectByQuality(below) {
const idx = KB.qualities.indexOf(below);
items.forEach(item => {
const qi = KB.qualities.indexOf(KB.qualities.find(q => item.name.includes(q)));
if (qi < 0) return;
if (qi <= idx && !item.equipped && !item.locked) {
item.checkbox.checked = true;
}
});
if (typeof update_selected_count === 'function') update_selected_count();
}
function clearAll() {
items.forEach(item => item.checkbox.checked = false);
if (typeof update_selected_count === 'function') update_selected_count();
}
// Buttons — only show relevant ones for current screen
if (screen === 'sell') {
const b = document.createElement('input');
b.type = 'button';
b.value = `💰 Select Crude (${sellCount})`;
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:10px';
b.onclick = () => autoSelect('sell');
toolbar.appendChild(b);
const b2 = document.createElement('input');
b2.type = 'button';
b2.value = '💰 Select ≤Fair';
b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#a00;color:#fff;border:none;border-radius:3px;font-size:10px';
b2.onclick = () => selectByQuality('Fair');
toolbar.appendChild(b2);
}
if (screen === 'salvage') {
const b = document.createElement('input');
b.type = 'button';
b.value = `♻ Select Salvage (${salvCount})`;
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px';
b.onclick = () => autoSelect('salvage');
toolbar.appendChild(b);
const b2 = document.createElement('input');
b2.type = 'button';
b2.value = '♻ Select ≤Average';
b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px';
b2.onclick = () => selectByQuality('Average');
toolbar.appendChild(b2);
// "Sell Salvaged Equipment" toggle is already native
}
if (screen === 'organize') {
const b = document.createElement('input');
b.type = 'button';
b.value = '📌 Pin Equipped';
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#2a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px';
b.onclick = () => {
// Select all equipped items so they can be pinned
items.forEach(item => { if (item.equipped) item.checkbox.checked = true; });
if (typeof update_selected_count === 'function') update_selected_count();
}; };
toolbar.appendChild(b);
}
document.body.appendChild(panel); // Clear button always available
const clear = document.createElement('input');
clear.type = 'button';
clear.value = '✗ Clear';
clear.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px';
clear.onclick = clearAll;
toolbar.appendChild(clear);
// ── Wire bulk buttons ── // ── Insert into page ──
const sellBulk = document.getElementById('hv-armory-bulk-sell'); const eqSelect = document.getElementById('equipselect_outer') ||
const salvBulk = document.getElementById('hv-armory-bulk-salvage'); document.querySelector('#equipselect_left, #armory_right > div');
if (equipList && equipList.parentNode) {
if (sellBulk) sellBulk.onclick = bulkSell; equipList.parentNode.insertBefore(toolbar, equipList);
if (salvBulk) salvBulk.onclick = bulkSalvage; }
} }
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════

View file

@ -1,205 +1,212 @@
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
// ARMORY — smart equipment inventory management // ARMORY — smart equipment inventory management
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
//
// The Armory (ss=am) has native tabs: Organize, Modify, Repair, Soulbind,
// Purchase, Sell, Salvage. Each tab has its own equipment list with
// checkboxes (<input name="eqids[]">). The page has native submit buttons
// for each action (Sell Equipment, Salvage Equipment, etc.).
//
// Our enhancements:
// - Add bulk-select buttons above the equipment list
// - Auto-check items based on quality thresholds
// - Show quick summary of what's worth keeping vs discarding
// ───────────────────────────────────────────────────────────────────────
// Analyse equipment quality and assign a grade
function gradeEquipment(name) { function gradeEquipment(name) {
const q = KB.qualities.find(q => name.includes(q)); const q = KB.qualities.find(q => name.includes(q));
if (!q) return { action: 'keep', label: '❓', color: '#888', reason: 'Unknown' }; if (!q) return { action: 'keep', label: '❓', color: '#888', reason: 'Unknown' };
const qi = KB.qualities.indexOf(q); const qi = KB.qualities.indexOf(q);
// Magnificent+ (index >= 5): always keep
if (qi >= 5) return { action: 'keep', label: '✅ Keep', color: '#0f0', reason: `${q}+` }; if (qi >= 5) return { action: 'keep', label: '✅ Keep', color: '#0f0', reason: `${q}+` };
// Superior (index 3): borderline — keep if it's a weapon/armor we need if (qi >= 3) return { action: 'keep', label: '⬆ Keep', color: '#fdcb00', reason: q };
if (qi === 3) return { action: 'keep', label: '⬆ Keep', color: '#fdcb00', reason: 'Superior' };
// Average/Fair (1-2): salvage for materials
if (qi >= 1) return { action: 'salvage', label: '♻ Salvage', color: '#888', reason: q }; if (qi >= 1) return { action: 'salvage', label: '♻ Salvage', color: '#888', reason: q };
// Crude (0): sell
return { action: 'sell', label: '💰 Sell', color: '#f80', reason: 'Crude' }; return { action: 'sell', label: '💰 Sell', color: '#f80', reason: 'Crude' };
} }
function enhanceArmory() { function enhanceArmory() {
const main = document.getElementById('mainpane'); const equipList = document.getElementById('equiplist');
if (!main || document.getElementById('hv-armory-panel')) return; if (!equipList || document.getElementById('hv-armory-bar')) return;
// ── Parse equipment from the page ── // ── Parse current equipment in the list ──
const items = []; const items = [];
let currentCategory = ''; let currentCategory = '';
$$('tr', main).forEach(row => { $$('tr', equipList).forEach(row => {
// Category label rows (Weapon, Armor sections)
if (row.className === 'eqtplabel') { if (row.className === 'eqtplabel') {
currentCategory = (row.textContent || '').trim(); currentCategory = (row.textContent || '').trim();
return; return;
} }
// Find the equip item link/ID const cb = row.querySelector('input[name="eqids[]"]');
const links = row.querySelectorAll('a[href*="set_equip"]'); if (!cb) return;
if (links.length === 0) return;
// Item has an onmouseover for tooltip const id = cb.value;
const omo = row.getAttribute('onmouseover') || '';
const idMatch = omo.match(/hover_equip\((\d+)\)/);
if (!idMatch) return;
const id = idMatch[1];
const label = row.querySelector('label'); const label = row.querySelector('label');
const name = label ? (label.textContent || '').trim() : ''; const name = label ? (label.textContent || '').trim() : '';
// Detect level — HV renders Lv with CSS font in a .btm1 inside the row // Status icons in the label text
const levelDiv = row.querySelector('.btm1 label, .btm1'); const equipped = name.includes('🗡');
const levelText = levelDiv ? readCSSText(levelDiv) : ''; const locked = name.includes('🔒');
const lvMatch = levelText.match(/lv\.?\s*(\d+)/i); const pinned = name.includes('📌');
const itemLevel = lvMatch ? parseInt(lvMatch[1]) : 0; const stored = name.includes('📦');
const protected_ = name.includes('🛡');
// Detect status indicators // Strip status icons for clean name
const rowHtml = row.innerHTML || ''; const cleanName = name.replace(/[🗡🔒📌📦🛡️]/g, '').trim();
const isLocked = rowHtml.includes('lock.png') || rowHtml.includes('lck.png');
const isEquipped = rowHtml.includes('eqp.png') || rowHtml.includes('Equipped');
const isPinned = rowHtml.includes('pin.png');
const isStored = rowHtml.includes('store.png');
// Check for Sell/Salvage buttons in this row
const sellBtn = row.querySelector('input[value="Sell"], a[onclick*="sell_equip"]');
const salvBtn = row.querySelector('input[value="Salvage"], a[onclick*="salvage_equip"]');
const quality = KB.qualities.find(q => name.includes(q)) || '?';
items.push({ items.push({
id, name, quality, slot: currentCategory, id, name: cleanName, slot: currentCategory,
level: itemLevel, equipped, locked, pinned, stored, protected: protected_,
locked: isLocked, equipped: isEquipped, checkbox: cb, row,
pinned: isPinned, stored: isStored, grade: gradeEquipment(cleanName),
canSell: !!sellBtn, canSalvage: !!salvBtn,
row,
}); });
}); });
if (items.length === 0) return; if (items.length === 0) return;
// ── Grade each item ── // ── Detect which armory tab we're on ──
const graded = items.map(item => ({ let screen = 'organize';
...item, const url = window.location.href || '';
grade: gradeEquipment(item.name, item.slot), const sm = url.match(/screen=(\w+)/);
})); if (sm) screen = sm[1];
// ── Sorting: equipped/pinned first, then by quality descending, then by level ── // ── Counts ──
const qwords = KB.qualities; const sellCount = items.filter(i => i.grade.action === 'sell' && !i.equipped && !i.locked).length;
graded.sort((a, b) => { const salvCount = items.filter(i => i.grade.action === 'salvage' && !i.equipped && !i.locked).length;
if (a.equipped && !b.equipped) return -1; const keepCount = items.filter(i => i.grade.action === 'keep').length;
if (!a.equipped && b.equipped) return 1;
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
const qDiff = (qwords.indexOf(b.quality) - qwords.indexOf(a.quality));
if (qDiff !== 0) return qDiff;
return b.level - a.level;
});
// ── Summary counts ── // ── Build the toolbar ──
const keepCount = graded.filter(i => i.grade.action === 'keep').length; const toolbar = document.createElement('div');
const sellCount = graded.filter(i => i.grade.action === 'sell').length; toolbar.id = 'hv-armory-bar';
const salvCount = graded.filter(i => i.grade.action === 'salvage').length; toolbar.style.cssText = css({
const equippedCount = graded.filter(i => i.equipped).length; display: 'flex',
gap: '4px',
// ── Build bulk action buttons ── flexWrap: 'wrap',
function bulkSell() { padding: '4px 6px',
const toSell = graded.filter(i => i.grade.action === 'sell' && i.canSell && !i.equipped && !i.locked); marginBottom: '2px',
if (toSell.length === 0) return; background: '#1a1a2e',
toSell.forEach(item => { borderRadius: '4px',
const btn = item.row.querySelector('input[value="Sell"], a[onclick*="sell_equip"]');
if (btn) btn.click();
});
}
function bulkSalvage() {
const toSalv = graded.filter(i => i.grade.action === 'salvage' && i.canSalvage && !i.equipped && !i.locked);
if (toSalv.length === 0) return;
toSalv.forEach(item => {
const btn = item.row.querySelector('input[value="Salvage"], a[onclick*="salvage_equip"]');
if (btn) btn.click();
});
}
// ── Build the panel ──
const qColor = (q) => { const i = qwords.indexOf(q); return i >= 5 ? '#0f0' : i >= 3 ? '#fdcb00' : i >= 1 ? '#888' : '#f80'; };
let body = `<div style="display:flex;gap:4px;margin-bottom:6px">
<input type="button" value="💰 Sell Crude (${sellCount})" style="padding:3px 8px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:10px" id="hv-armory-bulk-sell">
<input type="button" value="♻ Salvage ≤Fair (${salvCount})" style="padding:3px 8px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px" id="hv-armory-bulk-salvage">
</div>`;
body += `<div style="border-top:1px solid #374151;margin:4px 0;padding:4px 0;font-size:9px;color:#888">
Keep: ${keepCount} | Salvage: ${salvCount} | 💰 Sell: ${sellCount} | 🗡 Equipped: ${equippedCount}
</div>`;
// ── Table ──
body += '<table style="width:100%;font-size:9px;border-collapse:collapse">';
body += '<tr style="color:#666"><th style="text-align:left;padding:1px 2px">Q</th>' +
'<th style="text-align:left;padding:1px 2px">Item</th>' +
'<th style="text-align:center;padding:1px 2px">Lv</th>' +
'<th style="text-align:right;padding:1px 2px">Action</th></tr>';
let lastSlot = '';
for (const item of graded) {
// Slot separator
if (item.slot && item.slot !== lastSlot) {
lastSlot = item.slot;
body += `<tr><td colspan="4" style="padding:4px 2px 1px;color:#555;font-size:8px;border-bottom:1px solid #333">${item.slot}</td></tr>`;
}
const statusIcon = item.equipped ? '🗡' : item.locked ? '🔒' : item.stored ? '📦' : item.pinned ? '📌' : '';
const nameShort = item.name.length > 28 ? item.name.slice(0, 27) + '…' : item.name;
body += `<tr><td style="padding:1px 2px;color:${qColor(item.quality)}">${item.quality[0]}</td>
<td style="padding:1px 2px">${statusIcon} ${nameShort}</td>
<td style="padding:1px 2px;text-align:center;color:#888">${item.level || '?'}</td>
<td style="padding:1px 2px;text-align:right;color:${item.grade.color}">${item.grade.label}</td></tr>`;
}
body += '</table>';
body += '<div style="border-top:1px solid #374151;margin:6px 0;padding:4px 0;font-size:9px;color:#555">' +
'Status: 🗡Equipped 📌Pinned 🔒Locked 📦Stored</div>';
// ── Build panel container ──
const collapsed = localStorage[SP + 'armoryCollapsed'] === '1';
const panel = document.createElement('div');
panel.id = 'hv-armory-panel';
panel.style.cssText = css({
position: 'fixed',
top: '60px',
right: '4px',
zIndex: '9995',
background: '#111827',
color: '#d1d5db',
padding: '0',
borderRadius: '8px',
fontSize: '10px', fontSize: '10px',
fontFamily: 'monospace', fontFamily: 'monospace',
maxWidth: '350px', alignItems: 'center',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 0 15px rgba(0,0,0,0.6)',
border: '1px solid #374151',
}); });
panel.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 8px;cursor:pointer" id="hv-armory-header"> // Summary
<b style="color:#fdcb00;font-size:10px">🛡 Armory (${items.length})</b> const summary = document.createElement('span');
<span id="hv-armory-toggle" style="color:#888;font-size:12px">${collapsed ? '▶' : '▼'}</span> summary.style.cssText = 'color:#888;margin-right:6px';
</div> summary.textContent = `${keepCount}${salvCount} 💰${sellCount}`;
<div id="hv-armory-body" style="padding:0 8px 6px;display:${collapsed ? 'none' : 'block'}">${body}</div>`; toolbar.appendChild(summary);
panel.querySelector('#hv-armory-header').onclick = () => { // ── Smart auto-select: check boxes based on grade ──
const b = document.getElementById('hv-armory-body'); function autoSelect(action) {
const t = document.getElementById('hv-armory-toggle'); items.forEach(item => {
const h = b.style.display === 'none'; if (item.grade.action !== action) return;
b.style.display = h ? 'block' : 'none'; if (item.equipped || item.locked) return;
t.textContent = h ? '▼' : '▶'; item.checkbox.checked = true;
localStorage[SP + 'armoryCollapsed'] = h ? '0' : '1'; });
// Trigger the game's update function if available
if (typeof update_selected_count === 'function') update_selected_count();
}
// ── Select by quality ──
function selectByQuality(below) {
const idx = KB.qualities.indexOf(below);
items.forEach(item => {
const qi = KB.qualities.indexOf(KB.qualities.find(q => item.name.includes(q)));
if (qi < 0) return;
if (qi <= idx && !item.equipped && !item.locked) {
item.checkbox.checked = true;
}
});
if (typeof update_selected_count === 'function') update_selected_count();
}
function clearAll() {
items.forEach(item => item.checkbox.checked = false);
if (typeof update_selected_count === 'function') update_selected_count();
}
// Buttons — only show relevant ones for current screen
if (screen === 'sell') {
const b = document.createElement('input');
b.type = 'button';
b.value = `💰 Select Crude (${sellCount})`;
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:10px';
b.onclick = () => autoSelect('sell');
toolbar.appendChild(b);
const b2 = document.createElement('input');
b2.type = 'button';
b2.value = '💰 Select ≤Fair';
b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#a00;color:#fff;border:none;border-radius:3px;font-size:10px';
b2.onclick = () => selectByQuality('Fair');
toolbar.appendChild(b2);
}
if (screen === 'salvage') {
const b = document.createElement('input');
b.type = 'button';
b.value = `♻ Select Salvage (${salvCount})`;
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px';
b.onclick = () => autoSelect('salvage');
toolbar.appendChild(b);
const b2 = document.createElement('input');
b2.type = 'button';
b2.value = '♻ Select ≤Average';
b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px';
b2.onclick = () => selectByQuality('Average');
toolbar.appendChild(b2);
// "Sell Salvaged Equipment" toggle is already native
}
if (screen === 'organize') {
const b = document.createElement('input');
b.type = 'button';
b.value = '📌 Pin Equipped';
b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#2a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px';
b.onclick = () => {
// Select all equipped items so they can be pinned
items.forEach(item => { if (item.equipped) item.checkbox.checked = true; });
if (typeof update_selected_count === 'function') update_selected_count();
}; };
toolbar.appendChild(b);
}
document.body.appendChild(panel); // Clear button always available
const clear = document.createElement('input');
clear.type = 'button';
clear.value = '✗ Clear';
clear.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px';
clear.onclick = clearAll;
toolbar.appendChild(clear);
// ── Wire bulk buttons ── // Select All / Invert Selection (useful on any screen)
const sellBulk = document.getElementById('hv-armory-bulk-sell'); const selAll = document.createElement('input');
const salvBulk = document.getElementById('hv-armory-bulk-salvage'); selAll.type = 'button';
selAll.value = '☐ All';
selAll.style.cssText = 'padding:2px 6px;cursor:pointer;background:#3a4a6a;color:#fff;border:none;border-radius:3px;font-size:10px';
selAll.onclick = () => {
items.forEach(item => { item.checkbox.checked = true; });
if (typeof update_selected_count === 'function') update_selected_count();
};
toolbar.appendChild(selAll);
if (sellBulk) sellBulk.onclick = bulkSell; const invert = document.createElement('input');
if (salvBulk) salvBulk.onclick = bulkSalvage; invert.type = 'button';
invert.value = '⊞ Invert';
invert.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px';
invert.onclick = () => {
items.forEach(item => { item.checkbox.checked = !item.checkbox.checked; });
if (typeof update_selected_count === 'function') update_selected_count();
};
toolbar.appendChild(invert);
// ── Insert into page ──
const eqSelect = document.getElementById('equipselect_outer') ||
document.querySelector('#equipselect_left, #armory_right > div');
if (equipList && equipList.parentNode) {
equipList.parentNode.insertBefore(toolbar, equipList);
}
} }