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:
parent
1669167373
commit
2f680a630d
3 changed files with 456 additions and 477 deletions
|
|
@ -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;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
|
||||||
|
|
@ -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;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
|
||||||
323
src/armory.js
323
src/armory.js
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue