v0.13.1 - Smart Armory, removed broken sell/salvage buttons

- Removed enhanceEquipShop() — broken bulk sell/salvage from equip shop page
- Rewrote enhanceArmory() with actionable equipment management panel:
  - Parses all equipment from the Armory (ss=am) page
  - Grades each piece by quality: Keep (Magnificent+), Keep (Superior),
    Salvage (Average-Fair), Sell (Crude)
  - Bulk Sell / Bulk Salvage buttons that click native game buttons
  - Shows slot separators, quality colors, status icons (equipped/locked/stored)
  - Sorting: equipped/pinned first, then by quality, then by level
  - Summary counts for each action category
This commit is contained in:
GaboGG 2026-07-26 11:28:21 -04:00
parent 2107527c72
commit 1669167373
5 changed files with 427 additions and 238 deletions

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name HV Unified
// @namespace hvunified
// @version 0.12.0
// @version 0.13.0
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
// @author GaboGG + Hermes
// @match *://*.hentaiverse.org/*
@ -17,7 +17,7 @@
// CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.12.0';
const VERSION = '0.13.0';
const CFG = {
// — Battle automation
@ -1769,54 +1769,6 @@ function enhanceItemShop() {
});
}
// ── Equip Shop: quick sell/salvage buttons ──
function enhanceEquipShop() {
if (!CFG.autoSell) return;
const m = document.getElementById('mainpane');
if (!m || document.getElementById('hv-quick-sell')) return;
const r = document.createElement('div');
r.id = 'hv-quick-sell';
r.style.cssText = 'margin:6px;display:flex;gap:6px;flex-wrap:wrap';
['Crude', 'Fair', 'Average', 'Superior'].forEach(q => {
const b = document.createElement('input');
b.type = 'button';
b.value = `🔻 Sell ≤${q}`;
b.style.cssText = 'padding:3px 8px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:11px';
b.onclick = () => {
$$('tr', m).forEach(row => {
const t = (row.textContent || '');
const qi = ['Crude', 'Fair', 'Average', 'Superior'].indexOf(q);
if (['Crude', 'Fair', 'Average', 'Superior'].some((ql, i) => i <= qi && t.includes(ql)) && t.includes('Sell')) {
const sb = row.querySelector('input[value="Sell"]');
if (sb) sb.click();
}
});
};
r.appendChild(b);
});
const s = document.createElement('input');
s.type = 'button';
s.value = '♻ Salvage ≤Average';
s.style.cssText = 'padding:3px 8px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:11px';
s.onclick = () => {
$$('tr', m).forEach(row => {
const t = (row.textContent || '');
if ((t.includes('Crude') || t.includes('Fair') || t.includes('Average')) && t.includes('Salvage')) {
const sb = row.querySelector('input[value="Salvage"]');
if (sb) sb.click();
}
});
};
r.appendChild(s);
const ta = m.querySelector('div');
if (ta) ta.insertBefore(r, ta.firstChild);
}
// ── Equip advice: KEEP/SELL tags ──
function evaluateEquipment(text) {
@ -2654,66 +2606,168 @@ function enhanceAbilities() {
}
// ═══════════════════════════════════════════════════════════════════════
// ARMORY — equipment management panel
// ARMORY — smart equipment inventory management
// ═══════════════════════════════════════════════════════════════════════
// Analyse equipment quality and assign a grade
function gradeEquipment(name) {
const q = KB.qualities.find(q => name.includes(q));
if (!q) return { action: 'keep', label: '❓', color: '#888', reason: 'Unknown' };
const qi = KB.qualities.indexOf(q);
// Magnificent+ (index >= 5): always keep
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: 'Superior' };
// Average/Fair (1-2): salvage for materials
if (qi >= 1) return { action: 'salvage', label: '♻ Salvage', color: '#888', reason: q };
// Crude (0): sell
return { action: 'sell', label: '💰 Sell', color: '#f80', reason: 'Crude' };
}
function enhanceArmory() {
const main = document.getElementById('mainpane');
if (!main || document.getElementById('hv-armory')) return;
if (!main || document.getElementById('hv-armory-panel')) return;
// ── Parse equipment from the page ──
const items = [];
let currentCategory = '';
$$('tr', main).forEach(row => {
// Category label rows (Weapon, Armor sections)
if (row.className === 'eqtplabel') {
currentCategory = (row.textContent || '').trim();
return;
}
// Find the equip item link/ID
const links = row.querySelectorAll('a[href*="set_equip"]');
if (links.length === 0) return;
// Item has an onmouseover for tooltip
const omo = row.getAttribute('onmouseover') || '';
const idMatch = omo.match(/hover_equip\((\d+)\)/);
if (!idMatch) return;
const id = idMatch[1];
const label = row.querySelector('label');
if (!label) return;
const name = (label.textContent || '').trim();
const name = label ? (label.textContent || '').trim() : '';
// Detect level — HV renders Lv with CSS font in a .btm1 inside the row
const levelDiv = row.querySelector('.btm1 label, .btm1');
const levelText = levelDiv ? readCSSText(levelDiv) : '';
const lvMatch = levelText.match(/lv\.?\s*(\d+)/i);
const itemLevel = lvMatch ? parseInt(lvMatch[1]) : 0;
// Detect status indicators
const rowHtml = row.innerHTML || '';
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({ id: idMatch[1], name, quality, slot: currentCategory });
items.push({
id, name, quality, slot: currentCategory,
level: itemLevel,
locked: isLocked, equipped: isEquipped,
pinned: isPinned, stored: isStored,
canSell: !!sellBtn, canSalvage: !!salvBtn,
row,
});
});
if (items.length === 0) return;
// ── Grade each item ──
const graded = items.map(item => ({
...item,
grade: gradeEquipment(item.name, item.slot),
}));
// ── Sorting: equipped/pinned first, then by quality descending, then by level ──
const qwords = KB.qualities;
const best = (arr) => arr.sort((a, b) => qwords.indexOf(b.quality) - qwords.indexOf(a.quality))[0];
const weapons = items.filter(i => !i.slot.includes('Armor') && !i.slot.includes('Shield'));
const armors = items.filter(i => i.slot.includes('Armor') || i.slot.includes('Shoes'));
const bestWeapon = best(weapons);
const bestArmor = best(armors);
const qColor = (q) => { const i = qwords.indexOf(q); return i >= 4 ? '#0f0' : i >= 2 ? '#fdcb00' : '#f80'; };
graded.sort((a, b) => {
if (a.equipped && !b.equipped) return -1;
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;
});
let body = '';
if (bestWeapon) body += `<div><span style="color:${qColor(bestWeapon.quality)}">${bestWeapon.quality}</span> <b>${bestWeapon.name}</b> ← best</div>`;
if (bestArmor) body += `<div><span style="color:${qColor(bestArmor.quality)}">${bestArmor.quality}</span> <b>${bestArmor.name}</b> ← best</div>`;
// ── Summary counts ──
const keepCount = graded.filter(i => i.grade.action === 'keep').length;
const sellCount = graded.filter(i => i.grade.action === 'sell').length;
const salvCount = graded.filter(i => i.grade.action === 'salvage').length;
const equippedCount = graded.filter(i => i.equipped).length;
body += '<div style="border-top:1px solid #374151;margin:6px 0"></div>';
// ── Build bulk action buttons ──
function bulkSell() {
const toSell = graded.filter(i => i.grade.action === 'sell' && i.canSell && !i.equipped && !i.locked);
if (toSell.length === 0) return;
toSell.forEach(item => {
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:#888"><th style="text-align:left;padding:1px 2px">Q</th><th style="text-align:left;padding:1px 2px">Item</th><th style="text-align:right;padding:1px 2px">Action</th></tr>';
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>';
for (const item of items) {
const sameSlot = items.filter(i => i.slot === item.slot);
const isBest = item === bestWeapon || item === bestArmor;
let action;
if (isBest) action = '✅';
else if (item.quality === 'Crude' && sameSlot.length > 1) action = '💰';
else action = '📦';
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">${item.name.slice(0, 30)}</td>
<td style="padding:1px 2px;text-align:right">${action}</td></tr>`;
<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:#888">Go to Character → click empty slot → select item to equip</div>';
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.id = 'hv-armory-panel';
panel.style.cssText = css({
position: 'fixed',
top: '60px',
@ -2725,13 +2779,15 @@ function enhanceArmory() {
borderRadius: '8px',
fontSize: '10px',
fontFamily: 'monospace',
maxWidth: '320px',
maxWidth: '350px',
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">
<b style="color:#fdcb00;font-size:10px">🛡 Equipment (${items.length})</b>
<b style="color:#fdcb00;font-size:10px">🛡 Armory (${items.length})</b>
<span id="hv-armory-toggle" style="color:#888;font-size:12px">${collapsed ? '▶' : '▼'}</span>
</div>
<div id="hv-armory-body" style="padding:0 8px 6px;display:${collapsed ? 'none' : 'block'}">${body}</div>`;
@ -2746,6 +2802,13 @@ function enhanceArmory() {
};
document.body.appendChild(panel);
// ── Wire bulk buttons ──
const sellBulk = document.getElementById('hv-armory-bulk-sell');
const salvBulk = document.getElementById('hv-armory-bulk-salvage');
if (sellBulk) sellBulk.onclick = bulkSell;
if (salvBulk) salvBulk.onclick = bulkSalvage;
}
// ═══════════════════════════════════════════════════════════════════════
@ -2960,7 +3023,7 @@ function init() {
} else {
// Non-battle page enhancements
if (STATE.page === 'itemshop') { enhanceItemShop(); autoCheckTask('buy-health'); }
if (STATE.page === 'equipshop') { enhanceEquipShop(); enhanceEquipShopWithAdvice(); }
if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); }
if (STATE.page === 'shrine') enhanceShrine();
if (STATE.page === 'training') { enhanceTraining(); autoCheckTask('training'); }
if (STATE.page === 'monsterlab') { enhanceMonsterLab(); autoCheckTask('feed'); }

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name HV Unified
// @namespace hvunified
// @version 0.12.0
// @version 0.13.0
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
// @author GaboGG + Hermes
// @match *://*.hentaiverse.org/*
@ -17,7 +17,7 @@
// CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.12.0';
const VERSION = '0.13.0';
const CFG = {
// — Battle automation
@ -1769,54 +1769,6 @@ function enhanceItemShop() {
});
}
// ── Equip Shop: quick sell/salvage buttons ──
function enhanceEquipShop() {
if (!CFG.autoSell) return;
const m = document.getElementById('mainpane');
if (!m || document.getElementById('hv-quick-sell')) return;
const r = document.createElement('div');
r.id = 'hv-quick-sell';
r.style.cssText = 'margin:6px;display:flex;gap:6px;flex-wrap:wrap';
['Crude', 'Fair', 'Average', 'Superior'].forEach(q => {
const b = document.createElement('input');
b.type = 'button';
b.value = `🔻 Sell ≤${q}`;
b.style.cssText = 'padding:3px 8px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:11px';
b.onclick = () => {
$$('tr', m).forEach(row => {
const t = (row.textContent || '');
const qi = ['Crude', 'Fair', 'Average', 'Superior'].indexOf(q);
if (['Crude', 'Fair', 'Average', 'Superior'].some((ql, i) => i <= qi && t.includes(ql)) && t.includes('Sell')) {
const sb = row.querySelector('input[value="Sell"]');
if (sb) sb.click();
}
});
};
r.appendChild(b);
});
const s = document.createElement('input');
s.type = 'button';
s.value = '♻ Salvage ≤Average';
s.style.cssText = 'padding:3px 8px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:11px';
s.onclick = () => {
$$('tr', m).forEach(row => {
const t = (row.textContent || '');
if ((t.includes('Crude') || t.includes('Fair') || t.includes('Average')) && t.includes('Salvage')) {
const sb = row.querySelector('input[value="Salvage"]');
if (sb) sb.click();
}
});
};
r.appendChild(s);
const ta = m.querySelector('div');
if (ta) ta.insertBefore(r, ta.firstChild);
}
// ── Equip advice: KEEP/SELL tags ──
function evaluateEquipment(text) {
@ -2654,66 +2606,168 @@ function enhanceAbilities() {
}
// ═══════════════════════════════════════════════════════════════════════
// ARMORY — equipment management panel
// ARMORY — smart equipment inventory management
// ═══════════════════════════════════════════════════════════════════════
// Analyse equipment quality and assign a grade
function gradeEquipment(name) {
const q = KB.qualities.find(q => name.includes(q));
if (!q) return { action: 'keep', label: '❓', color: '#888', reason: 'Unknown' };
const qi = KB.qualities.indexOf(q);
// Magnificent+ (index >= 5): always keep
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: 'Superior' };
// Average/Fair (1-2): salvage for materials
if (qi >= 1) return { action: 'salvage', label: '♻ Salvage', color: '#888', reason: q };
// Crude (0): sell
return { action: 'sell', label: '💰 Sell', color: '#f80', reason: 'Crude' };
}
function enhanceArmory() {
const main = document.getElementById('mainpane');
if (!main || document.getElementById('hv-armory')) return;
if (!main || document.getElementById('hv-armory-panel')) return;
// ── Parse equipment from the page ──
const items = [];
let currentCategory = '';
$$('tr', main).forEach(row => {
// Category label rows (Weapon, Armor sections)
if (row.className === 'eqtplabel') {
currentCategory = (row.textContent || '').trim();
return;
}
// Find the equip item link/ID
const links = row.querySelectorAll('a[href*="set_equip"]');
if (links.length === 0) return;
// Item has an onmouseover for tooltip
const omo = row.getAttribute('onmouseover') || '';
const idMatch = omo.match(/hover_equip\((\d+)\)/);
if (!idMatch) return;
const id = idMatch[1];
const label = row.querySelector('label');
if (!label) return;
const name = (label.textContent || '').trim();
const name = label ? (label.textContent || '').trim() : '';
// Detect level — HV renders Lv with CSS font in a .btm1 inside the row
const levelDiv = row.querySelector('.btm1 label, .btm1');
const levelText = levelDiv ? readCSSText(levelDiv) : '';
const lvMatch = levelText.match(/lv\.?\s*(\d+)/i);
const itemLevel = lvMatch ? parseInt(lvMatch[1]) : 0;
// Detect status indicators
const rowHtml = row.innerHTML || '';
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({ id: idMatch[1], name, quality, slot: currentCategory });
items.push({
id, name, quality, slot: currentCategory,
level: itemLevel,
locked: isLocked, equipped: isEquipped,
pinned: isPinned, stored: isStored,
canSell: !!sellBtn, canSalvage: !!salvBtn,
row,
});
});
if (items.length === 0) return;
// ── Grade each item ──
const graded = items.map(item => ({
...item,
grade: gradeEquipment(item.name, item.slot),
}));
// ── Sorting: equipped/pinned first, then by quality descending, then by level ──
const qwords = KB.qualities;
const best = (arr) => arr.sort((a, b) => qwords.indexOf(b.quality) - qwords.indexOf(a.quality))[0];
const weapons = items.filter(i => !i.slot.includes('Armor') && !i.slot.includes('Shield'));
const armors = items.filter(i => i.slot.includes('Armor') || i.slot.includes('Shoes'));
const bestWeapon = best(weapons);
const bestArmor = best(armors);
const qColor = (q) => { const i = qwords.indexOf(q); return i >= 4 ? '#0f0' : i >= 2 ? '#fdcb00' : '#f80'; };
graded.sort((a, b) => {
if (a.equipped && !b.equipped) return -1;
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;
});
let body = '';
if (bestWeapon) body += `<div><span style="color:${qColor(bestWeapon.quality)}">${bestWeapon.quality}</span> <b>${bestWeapon.name}</b> ← best</div>`;
if (bestArmor) body += `<div><span style="color:${qColor(bestArmor.quality)}">${bestArmor.quality}</span> <b>${bestArmor.name}</b> ← best</div>`;
// ── Summary counts ──
const keepCount = graded.filter(i => i.grade.action === 'keep').length;
const sellCount = graded.filter(i => i.grade.action === 'sell').length;
const salvCount = graded.filter(i => i.grade.action === 'salvage').length;
const equippedCount = graded.filter(i => i.equipped).length;
body += '<div style="border-top:1px solid #374151;margin:6px 0"></div>';
// ── Build bulk action buttons ──
function bulkSell() {
const toSell = graded.filter(i => i.grade.action === 'sell' && i.canSell && !i.equipped && !i.locked);
if (toSell.length === 0) return;
toSell.forEach(item => {
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:#888"><th style="text-align:left;padding:1px 2px">Q</th><th style="text-align:left;padding:1px 2px">Item</th><th style="text-align:right;padding:1px 2px">Action</th></tr>';
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>';
for (const item of items) {
const sameSlot = items.filter(i => i.slot === item.slot);
const isBest = item === bestWeapon || item === bestArmor;
let action;
if (isBest) action = '✅';
else if (item.quality === 'Crude' && sameSlot.length > 1) action = '💰';
else action = '📦';
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">${item.name.slice(0, 30)}</td>
<td style="padding:1px 2px;text-align:right">${action}</td></tr>`;
<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:#888">Go to Character → click empty slot → select item to equip</div>';
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.id = 'hv-armory-panel';
panel.style.cssText = css({
position: 'fixed',
top: '60px',
@ -2725,13 +2779,15 @@ function enhanceArmory() {
borderRadius: '8px',
fontSize: '10px',
fontFamily: 'monospace',
maxWidth: '320px',
maxWidth: '350px',
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">
<b style="color:#fdcb00;font-size:10px">🛡 Equipment (${items.length})</b>
<b style="color:#fdcb00;font-size:10px">🛡 Armory (${items.length})</b>
<span id="hv-armory-toggle" style="color:#888;font-size:12px">${collapsed ? '▶' : '▼'}</span>
</div>
<div id="hv-armory-body" style="padding:0 8px 6px;display:${collapsed ? 'none' : 'block'}">${body}</div>`;
@ -2746,6 +2802,13 @@ function enhanceArmory() {
};
document.body.appendChild(panel);
// ── Wire bulk buttons ──
const sellBulk = document.getElementById('hv-armory-bulk-sell');
const salvBulk = document.getElementById('hv-armory-bulk-salvage');
if (sellBulk) sellBulk.onclick = bulkSell;
if (salvBulk) salvBulk.onclick = bulkSalvage;
}
// ═══════════════════════════════════════════════════════════════════════
@ -2960,7 +3023,7 @@ function init() {
} else {
// Non-battle page enhancements
if (STATE.page === 'itemshop') { enhanceItemShop(); autoCheckTask('buy-health'); }
if (STATE.page === 'equipshop') { enhanceEquipShop(); enhanceEquipShopWithAdvice(); }
if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); }
if (STATE.page === 'shrine') enhanceShrine();
if (STATE.page === 'training') { enhanceTraining(); autoCheckTask('training'); }
if (STATE.page === 'monsterlab') { enhanceMonsterLab(); autoCheckTask('feed'); }

View file

@ -1,64 +1,166 @@
// ═══════════════════════════════════════════════════════════════════════
// ARMORY — equipment management panel
// ARMORY — smart equipment inventory management
// ═══════════════════════════════════════════════════════════════════════
// Analyse equipment quality and assign a grade
function gradeEquipment(name) {
const q = KB.qualities.find(q => name.includes(q));
if (!q) return { action: 'keep', label: '❓', color: '#888', reason: 'Unknown' };
const qi = KB.qualities.indexOf(q);
// Magnificent+ (index >= 5): always keep
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: 'Superior' };
// Average/Fair (1-2): salvage for materials
if (qi >= 1) return { action: 'salvage', label: '♻ Salvage', color: '#888', reason: q };
// Crude (0): sell
return { action: 'sell', label: '💰 Sell', color: '#f80', reason: 'Crude' };
}
function enhanceArmory() {
const main = document.getElementById('mainpane');
if (!main || document.getElementById('hv-armory')) return;
if (!main || document.getElementById('hv-armory-panel')) return;
// ── Parse equipment from the page ──
const items = [];
let currentCategory = '';
$$('tr', main).forEach(row => {
// Category label rows (Weapon, Armor sections)
if (row.className === 'eqtplabel') {
currentCategory = (row.textContent || '').trim();
return;
}
// Find the equip item link/ID
const links = row.querySelectorAll('a[href*="set_equip"]');
if (links.length === 0) return;
// Item has an onmouseover for tooltip
const omo = row.getAttribute('onmouseover') || '';
const idMatch = omo.match(/hover_equip\((\d+)\)/);
if (!idMatch) return;
const id = idMatch[1];
const label = row.querySelector('label');
if (!label) return;
const name = (label.textContent || '').trim();
const name = label ? (label.textContent || '').trim() : '';
// Detect level — HV renders Lv with CSS font in a .btm1 inside the row
const levelDiv = row.querySelector('.btm1 label, .btm1');
const levelText = levelDiv ? readCSSText(levelDiv) : '';
const lvMatch = levelText.match(/lv\.?\s*(\d+)/i);
const itemLevel = lvMatch ? parseInt(lvMatch[1]) : 0;
// Detect status indicators
const rowHtml = row.innerHTML || '';
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({ id: idMatch[1], name, quality, slot: currentCategory });
items.push({
id, name, quality, slot: currentCategory,
level: itemLevel,
locked: isLocked, equipped: isEquipped,
pinned: isPinned, stored: isStored,
canSell: !!sellBtn, canSalvage: !!salvBtn,
row,
});
});
if (items.length === 0) return;
// ── Grade each item ──
const graded = items.map(item => ({
...item,
grade: gradeEquipment(item.name, item.slot),
}));
// ── Sorting: equipped/pinned first, then by quality descending, then by level ──
const qwords = KB.qualities;
const best = (arr) => arr.sort((a, b) => qwords.indexOf(b.quality) - qwords.indexOf(a.quality))[0];
const weapons = items.filter(i => !i.slot.includes('Armor') && !i.slot.includes('Shield'));
const armors = items.filter(i => i.slot.includes('Armor') || i.slot.includes('Shoes'));
const bestWeapon = best(weapons);
const bestArmor = best(armors);
const qColor = (q) => { const i = qwords.indexOf(q); return i >= 4 ? '#0f0' : i >= 2 ? '#fdcb00' : '#f80'; };
graded.sort((a, b) => {
if (a.equipped && !b.equipped) return -1;
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;
});
let body = '';
if (bestWeapon) body += `<div><span style="color:${qColor(bestWeapon.quality)}">${bestWeapon.quality}</span> <b>${bestWeapon.name}</b> ← best</div>`;
if (bestArmor) body += `<div><span style="color:${qColor(bestArmor.quality)}">${bestArmor.quality}</span> <b>${bestArmor.name}</b> ← best</div>`;
// ── Summary counts ──
const keepCount = graded.filter(i => i.grade.action === 'keep').length;
const sellCount = graded.filter(i => i.grade.action === 'sell').length;
const salvCount = graded.filter(i => i.grade.action === 'salvage').length;
const equippedCount = graded.filter(i => i.equipped).length;
body += '<div style="border-top:1px solid #374151;margin:6px 0"></div>';
// ── Build bulk action buttons ──
function bulkSell() {
const toSell = graded.filter(i => i.grade.action === 'sell' && i.canSell && !i.equipped && !i.locked);
if (toSell.length === 0) return;
toSell.forEach(item => {
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:#888"><th style="text-align:left;padding:1px 2px">Q</th><th style="text-align:left;padding:1px 2px">Item</th><th style="text-align:right;padding:1px 2px">Action</th></tr>';
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>';
for (const item of items) {
const sameSlot = items.filter(i => i.slot === item.slot);
const isBest = item === bestWeapon || item === bestArmor;
let action;
if (isBest) action = '✅';
else if (item.quality === 'Crude' && sameSlot.length > 1) action = '💰';
else action = '📦';
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">${item.name.slice(0, 30)}</td>
<td style="padding:1px 2px;text-align:right">${action}</td></tr>`;
<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:#888">Go to Character → click empty slot → select item to equip</div>';
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.id = 'hv-armory-panel';
panel.style.cssText = css({
position: 'fixed',
top: '60px',
@ -70,13 +172,15 @@ function enhanceArmory() {
borderRadius: '8px',
fontSize: '10px',
fontFamily: 'monospace',
maxWidth: '320px',
maxWidth: '350px',
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">
<b style="color:#fdcb00;font-size:10px">🛡 Equipment (${items.length})</b>
<b style="color:#fdcb00;font-size:10px">🛡 Armory (${items.length})</b>
<span id="hv-armory-toggle" style="color:#888;font-size:12px">${collapsed ? '▶' : '▼'}</span>
</div>
<div id="hv-armory-body" style="padding:0 8px 6px;display:${collapsed ? 'none' : 'block'}">${body}</div>`;
@ -91,4 +195,11 @@ function enhanceArmory() {
};
document.body.appendChild(panel);
// ── Wire bulk buttons ──
const sellBulk = document.getElementById('hv-armory-bulk-sell');
const salvBulk = document.getElementById('hv-armory-bulk-salvage');
if (sellBulk) sellBulk.onclick = bulkSell;
if (salvBulk) salvBulk.onclick = bulkSalvage;
}

View file

@ -18,7 +18,7 @@ function init() {
} else {
// Non-battle page enhancements
if (STATE.page === 'itemshop') { enhanceItemShop(); autoCheckTask('buy-health'); }
if (STATE.page === 'equipshop') { enhanceEquipShop(); enhanceEquipShopWithAdvice(); }
if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); }
if (STATE.page === 'shrine') enhanceShrine();
if (STATE.page === 'training') { enhanceTraining(); autoCheckTask('training'); }
if (STATE.page === 'monsterlab') { enhanceMonsterLab(); autoCheckTask('feed'); }

View file

@ -90,54 +90,6 @@ function enhanceItemShop() {
});
}
// ── Equip Shop: quick sell/salvage buttons ──
function enhanceEquipShop() {
if (!CFG.autoSell) return;
const m = document.getElementById('mainpane');
if (!m || document.getElementById('hv-quick-sell')) return;
const r = document.createElement('div');
r.id = 'hv-quick-sell';
r.style.cssText = 'margin:6px;display:flex;gap:6px;flex-wrap:wrap';
['Crude', 'Fair', 'Average', 'Superior'].forEach(q => {
const b = document.createElement('input');
b.type = 'button';
b.value = `🔻 Sell ≤${q}`;
b.style.cssText = 'padding:3px 8px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:11px';
b.onclick = () => {
$$('tr', m).forEach(row => {
const t = (row.textContent || '');
const qi = ['Crude', 'Fair', 'Average', 'Superior'].indexOf(q);
if (['Crude', 'Fair', 'Average', 'Superior'].some((ql, i) => i <= qi && t.includes(ql)) && t.includes('Sell')) {
const sb = row.querySelector('input[value="Sell"]');
if (sb) sb.click();
}
});
};
r.appendChild(b);
});
const s = document.createElement('input');
s.type = 'button';
s.value = '♻ Salvage ≤Average';
s.style.cssText = 'padding:3px 8px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:11px';
s.onclick = () => {
$$('tr', m).forEach(row => {
const t = (row.textContent || '');
if ((t.includes('Crude') || t.includes('Fair') || t.includes('Average')) && t.includes('Salvage')) {
const sb = row.querySelector('input[value="Salvage"]');
if (sb) sb.click();
}
});
};
r.appendChild(s);
const ta = m.querySelector('div');
if (ta) ta.insertBefore(r, ta.firstChild);
}
// ── Equip advice: KEEP/SELL tags ──
function evaluateEquipment(text) {