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 += ''; + body += '' + + '' + + '' + + ''; - 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 += ``; + } + + const statusIcon = item.equipped ? '🗡' : item.locked ? '🔒' : item.stored ? '📦' : item.pinned ? '📌' : ''; + const nameShort = item.name.length > 28 ? item.name.slice(0, 27) + '…' : item.name; body += ` - - `; + + + `; } body += '
QItemAction
QItemLvAction
${item.slot}
${item.quality[0]}${item.name.slice(0, 30)}${action}
${statusIcon} ${nameShort}${item.level || '?'}${item.grade.label}
'; - 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 = `
- 🛡️ Equipment (${items.length}) + 🛡️ Armory (${items.length}) ${collapsed ? '▶' : '▼'}
${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 += ''; + body += '' + + '' + + '' + + ''; - 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 += ``; + } + + const statusIcon = item.equipped ? '🗡' : item.locked ? '🔒' : item.stored ? '📦' : item.pinned ? '📌' : ''; + const nameShort = item.name.length > 28 ? item.name.slice(0, 27) + '…' : item.name; body += ` - - `; + + + `; } body += '
QItemAction
QItemLvAction
${item.slot}
${item.quality[0]}${item.name.slice(0, 30)}${action}
${statusIcon} ${nameShort}${item.level || '?'}${item.grade.label}
'; - 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 = `
- 🛡️ Equipment (${items.length}) + 🛡️ Armory (${items.length}) ${collapsed ? '▶' : '▼'}
${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 += ''; + body += '' + + '' + + '' + + ''; - 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 += ``; + } + + const statusIcon = item.equipped ? '🗡' : item.locked ? '🔒' : item.stored ? '📦' : item.pinned ? '📌' : ''; + const nameShort = item.name.length > 28 ? item.name.slice(0, 27) + '…' : item.name; body += ` - - `; + + + `; } body += '
QItemAction
QItemLvAction
${item.slot}
${item.quality[0]}${item.name.slice(0, 30)}${action}
${statusIcon} ${nameShort}${item.level || '?'}${item.grade.label}
'; - 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 = `
- 🛡️ Equipment (${items.length}) + 🛡️ Armory (${items.length}) ${collapsed ? '▶' : '▼'}
${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) {