diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js
index 2cd5654..ac32581 100644
--- a/scripts/hv-unified.user.js
+++ b/scripts/hv-unified.user.js
@@ -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 += `
${bestWeapon.quality} ${bestWeapon.name} ← best
`;
- if (bestArmor) body += `${bestArmor.quality} ${bestArmor.name} ← best
`;
+ // ── 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 += '';
+ // ── 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 = `
+
+
+
`;
+
+ body += `
+ ✅ Keep: ${keepCount} | ♻ Salvage: ${salvCount} | 💰 Sell: ${sellCount} | 🗡 Equipped: ${equippedCount}
+
`;
+
+ // ── Table ──
body += '';
- body += '| Q | Item | Action |
';
+ body += '| Q | ' +
+ 'Item | ' +
+ 'Lv | ' +
+ 'Action |
';
- 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 += `| ${item.slot} |
`;
+ }
+
+ const statusIcon = item.equipped ? '🗡' : item.locked ? '🔒' : item.stored ? '📦' : item.pinned ? '📌' : '';
+ const nameShort = item.name.length > 28 ? item.name.slice(0, 27) + '…' : item.name;
body += `| ${item.quality[0]} |
- ${item.name.slice(0, 30)} |
- ${action} |
`;
+ ${statusIcon} ${nameShort} |
+ ${item.level || '?'} |
+ ${item.grade.label} | `;
}
body += '
';
- body += 'Go to Character → click empty slot → select item to equip
';
+ body += '' +
+ 'Status: 🗡Equipped 📌Pinned 🔒Locked 📦Stored
';
+
+ // ── 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 = `
${body}
`;
@@ -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'); }
diff --git a/scripts/latest.user.js b/scripts/latest.user.js
index 2cd5654..ac32581 100644
--- a/scripts/latest.user.js
+++ b/scripts/latest.user.js
@@ -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 += `${bestWeapon.quality} ${bestWeapon.name} ← best
`;
- if (bestArmor) body += `${bestArmor.quality} ${bestArmor.name} ← best
`;
+ // ── 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 += '';
+ // ── 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 = `
+
+
+
`;
+
+ body += `
+ ✅ Keep: ${keepCount} | ♻ Salvage: ${salvCount} | 💰 Sell: ${sellCount} | 🗡 Equipped: ${equippedCount}
+
`;
+
+ // ── Table ──
body += '';
- body += '| Q | Item | Action |
';
+ body += '| Q | ' +
+ 'Item | ' +
+ 'Lv | ' +
+ 'Action |
';
- 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 += `| ${item.slot} |
`;
+ }
+
+ const statusIcon = item.equipped ? '🗡' : item.locked ? '🔒' : item.stored ? '📦' : item.pinned ? '📌' : '';
+ const nameShort = item.name.length > 28 ? item.name.slice(0, 27) + '…' : item.name;
body += `| ${item.quality[0]} |
- ${item.name.slice(0, 30)} |
- ${action} |
`;
+ ${statusIcon} ${nameShort} |
+ ${item.level || '?'} |
+ ${item.grade.label} | `;
}
body += '
';
- body += 'Go to Character → click empty slot → select item to equip
';
+ body += '' +
+ 'Status: 🗡Equipped 📌Pinned 🔒Locked 📦Stored
';
+
+ // ── 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 = `
${body}
`;
@@ -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'); }
diff --git a/src/armory.js b/src/armory.js
index d84864b..a794e15 100644
--- a/src/armory.js
+++ b/src/armory.js
@@ -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 += `${bestWeapon.quality} ${bestWeapon.name} ← best
`;
- if (bestArmor) body += `${bestArmor.quality} ${bestArmor.name} ← best
`;
+ // ── 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 += '';
+ // ── 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 = `
+
+
+
`;
+
+ body += `
+ ✅ Keep: ${keepCount} | ♻ Salvage: ${salvCount} | 💰 Sell: ${sellCount} | 🗡 Equipped: ${equippedCount}
+
`;
+
+ // ── Table ──
body += '';
- body += '| Q | Item | Action |
';
+ body += '| Q | ' +
+ 'Item | ' +
+ 'Lv | ' +
+ 'Action |
';
- 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 += `| ${item.slot} |
`;
+ }
+
+ const statusIcon = item.equipped ? '🗡' : item.locked ? '🔒' : item.stored ? '📦' : item.pinned ? '📌' : '';
+ const nameShort = item.name.length > 28 ? item.name.slice(0, 27) + '…' : item.name;
body += `| ${item.quality[0]} |
- ${item.name.slice(0, 30)} |
- ${action} |
`;
+ ${statusIcon} ${nameShort} |
+ ${item.level || '?'} |
+ ${item.grade.label} | `;
}
body += '
';
- body += 'Go to Character → click empty slot → select item to equip
';
+ body += '' +
+ 'Status: 🗡Equipped 📌Pinned 🔒Locked 📦Stored
';
+
+ // ── 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 = `
${body}
`;
@@ -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;
}
diff --git a/src/init.js b/src/init.js
index 793bd2e..27c17c4 100644
--- a/src/init.js
+++ b/src/init.js
@@ -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'); }
diff --git a/src/out-of-battle.js b/src/out-of-battle.js
index a849e78..e1b70b5 100644
--- a/src/out-of-battle.js
+++ b/src/out-of-battle.js
@@ -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) {