diff --git a/scripts/build.sh b/scripts/build.sh index f83940a..81ca7f4 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -32,6 +32,7 @@ FILES=( abilities.js armory.js gear-scraper.js + gear-analysis.js battle-logger.js re-timer.js settings-page.js diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index 95b7f32..f547d58 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.14.32 +// @version 0.14.33 // @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.14.32'; +const VERSION = '0.14.33'; const CFG = { // — Battle automation @@ -3615,6 +3615,33 @@ function scrapeBuyPage() { saveGearDB([...existing, ...scraped]); console.log(`%c[HV] 🛒 Buy: ${scraped.length} items (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0'); } + + // Second, scan the full store for items NOT in the visible list + // The Purchase page loads ALL items into dynjs_eqstore regardless of + // the filter tab — one visit captures the whole store. + const store = getHVEquipStore(); + if (store) { + const visibleIds = new Set(scraped.map(s => s.id)); + const missing = Object.entries(store).filter(([id, data]) => !visibleIds.has(id) && data && (data.t || data.d)); + if (missing.length > 0) { + missing.forEach(([id, data]) => { + const stats = data.d ? parseEquipHTML(data.d) : null; + let category = 'All Items'; + if (stats && stats.type) { + if (stats.type.includes('Cloth')) category = 'Cloth Armor'; + else if (stats.type.includes('Light')) category = 'Light Armor'; + else if (stats.type.includes('Heavy')) category = 'Heavy Armor'; + else if (stats.type.includes('Shield')) category = 'Shield'; + else if (stats.type.includes('Staff')) category = 'Staff'; + else category = 'Weapon'; + } + scraped.push({ id, name: data.t || 'Unknown', category, quality: data.q, stats, source: 'buy', scrapedAt: Date.now() }); + }); + const db2 = getGearDB(); + saveGearDB([...db2.filter(e => e.source !== 'buy'), ...scraped]); + console.log(`%c[HV] 🛒 Buy: +${missing.length} from store (total ${scraped.length})`, 'color:#0f0'); + } + } return scraped; } @@ -3679,6 +3706,302 @@ function scrapeModifyDetail() { return item; } +// ═══════════════════════════════════════════════════════════════════════ +// GEAR ANALYSIS — in-game gear comparison (port of analyze_gear.py) +// ═══════════════════════════════════════════════════════════════════════ +// Adds a toolbar button on Armory pages. Clicking it: +// 1. Clears old buy data (optional) +// 2. Rescrapes the current page (store or inventory) +// 3. Scores all items and shows per-slot recommendations +// Sources: 🟢 equipped (character), 📦 stored (armory), 🛒 purchasable (buy) + +function _num(v) { + if (v == null) return 0; + const s = String(v).replace('+', '').replace('%', '').trim(); + const n = parseFloat(s); + return isNaN(n) ? 0 : n; +} + +// ── Scoring (mirrors analyze_gear.py) ── +function gearScoreArmor(item) { + const st = item.stats || {}; + let s = 0; + s += _num(st['Physical Mitigation']) * 3; + s += _num(st['Evade']) * 2; + s += _num(st['Strength']) * 2; + s += _num(st['Dexterity']) * 2; + s += _num(st['Agility']) * 1.5; + s += _num(st['Endurance']); + s += _num(st['Crushing']); + s += _num(st['Slashing']); + s += _num(st['Piercing']); + s -= _num(st['burden']) * 0.5; + s += (item.quality || 0) * 5; + return s; +} + +function gearScoreWeapon(item) { + const st = item.stats || {}; + const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase(); + const quality = item.quality || 0; + let s = 0; + s += _num(st['Attack Accuracy']) * 2; + s += _num(st['Attack Crit Damage']) * 60; + s += _num(st['Strength']) * 2; + s += _num(st['Dexterity']) * 1.5; + s += _num(st['Parry']) * 0.5; + s += _num(st['Block']) * 0.5; + s += _num(st['Agility']) * 0.5; + s -= _num(st['burden']) * 0.5; + // Quality multiplier (q6=Magnificent, q5=Exquisite, q4=Superior) + s *= 0.8 + (quality * 0.12); + // Weapon type bonus + if (name.includes('estoc')) s *= 1.25; + else if (name.includes('longsword')) s *= 1.10; + else if (name.includes('great mace')) s *= 0.90; + else if (name.includes('club')) s *= 0.85; + else if (name.includes('axe')) s *= 0.90; + return s; +} + +// ── Slot detection ── +const GEAR_SLOT_KEYWORDS = { + 'Head': ['helmet','cap','goggles','mask','hood','hat','circlet','crown','visor'], + 'Body': ['breastplate','cuirass','robe','tunic','chest','gi','jacket','vest','hauberk'], + 'Hands': ['gauntlets','gloves','mitts','bracers','handguards','fists'], + 'Legs': ['leggings','greaves','pants','trousers','loincloth','kilt','breeches','chaps'], + 'Feet': ['boots','sabatons','shoes','slippers','sandals'], +}; + +function gearDetectSlot(item) { + const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase(); + for (const [slot, kws] of Object.entries(GEAR_SLOT_KEYWORDS)) { + for (const kw of kws) { + if (name.includes(kw)) return slot; + } + } + return null; +} + +function gearIsTwoHandWeapon(item) { + const st = item.stats || {}; + const itype = st.type || ''; + if (itype.includes('Armor') || itype.includes('Shield') || itype.includes('Staff')) return false; + const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase(); + if (['estoc','longsword','great mace','scythe','axe','club'].some(k => name.includes(k))) return true; + return false; +} + +function gearUsable(item, playerLv) { + const lv = (item.stats || {}).level; + if (!lv || lv === 'Unassigned') return true; + const n = parseInt(lv); + if (isNaN(n)) return true; + return n <= (playerLv || 155) + 15; +} + +function gearPlayerLevel() { + const el = document.getElementById('level_readout'); + if (el) { + const m = (el.textContent || '').match(/(\d+)/); + if (m) return parseInt(m[1]); + } + return 155; +} + +// ── Main analysis ── +function analyzeGear() { + const db = getGearDB(); + const equipped = db.filter(e => e.source === 'character' && e.id); + const armory = db.filter(e => e.source === 'armory' && e.stats); + const buy = db.filter(e => e.source === 'buy' && e.stats); + const playerLv = gearPlayerLevel(); + + // Find equipped slot map (slotType from character page) + const slotMap = {}; + equipped.forEach(e => { if (e.slotType) slotMap[e.slotType] = e; }); + + const results = []; + const slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']; + + slots.forEach(slot => { + const current = slotMap[slot]; + const cands = []; + + const consider = (item, source) => { + if (!item || !item.stats) return; + if (!gearUsable(item, playerLv)) return; + const isWeapon = slot === 'Mainhand'; + if (isWeapon) { + if (!gearIsTwoHandWeapon(item)) return; + if (!item.name || !item.stats['Attack Accuracy']) return; // needs stats + } else { + const det = gearDetectSlot(item); + if (det !== slot) return; + if (!item.stats['Physical Mitigation'] && !item.stats['Evade']) return; + } + const score = isWeapon ? gearScoreWeapon(item) : gearScoreArmor(item); + cands.push({ item, source, score }); + }; + + if (current) consider(current, 'equipped'); + armory.forEach(i => consider(i, 'armory')); + buy.forEach(i => consider(i, 'buy')); + + cands.sort((a, b) => b.score - a.score); + results.push({ slot, current, cands }); + }); + + return { playerLv, results }; +} + +// ── UI ── +function buildGearPanel() { + // Remove old panel + const old = document.getElementById('hv-gear-panel'); + if (old) old.remove(); + + const panel = document.createElement('div'); + panel.id = 'hv-gear-panel'; + panel.style.cssText = css({ + position: 'fixed', right: '8px', top: '48px', zIndex: 99999, + width: '420px', maxHeight: '80vh', overflowY: 'auto', + background: '#12121e', border: '1px solid #333', borderRadius: '6px', + padding: '8px', fontSize: '11px', fontFamily: 'monospace', + boxShadow: '0 4px 16px rgba(0,0,0,.6)', + }); + + const close = document.createElement('button'); + close.textContent = '✕'; + close.style.cssText = 'float:right;cursor:pointer;background:#333;border:none;color:#fff;border-radius:3px;padding:2px 6px'; + close.onclick = () => panel.remove(); + panel.appendChild(close); + + const title = document.createElement('div'); + title.textContent = '🛡️ Gear Analysis'; + title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd'; + panel.appendChild(title); + + const { results } = analyzeGear(); + + results.forEach(({ slot, current, cands }) => { + const block = document.createElement('div'); + block.style.cssText = 'margin-bottom:10px;border-top:1px solid #222;padding-top:6px'; + + const head = document.createElement('div'); + head.textContent = `${slot}: ${current ? current.name : '—'}`; + head.style.cssText = 'color:#9cf;font-weight:bold;margin-bottom:2px'; + block.appendChild(head); + + if (!cands.length) { + const none = document.createElement('div'); + none.textContent = ' (no data)'; + none.style.cssText = 'color:#666'; + block.appendChild(none); + panel.appendChild(block); + return; + } + + // Show top 4 candidates + cands.slice(0, 4).forEach((c, i) => { + const icon = c.source === 'equipped' ? '🟢' : c.source === 'armory' ? '📦' : '🛒'; + const row = document.createElement('div'); + const price = c.item.price ? ` 💰${c.item.price}` : ''; + const isCurrent = c.source === 'equipped'; + row.textContent = ` ${i + 1}. [${c.score.toFixed(0)}] ${icon} ${c.item.name}${isCurrent ? ' ⬅' : ''}${price}`; + row.style.cssText = isCurrent ? 'color:#8f8' : 'color:#ccc'; + if (i === 0 && !isCurrent) row.style.color = '#ff0'; + block.appendChild(row); + }); + + // Highlight best option + const best = cands[0]; + if (best && best.source !== 'equipped' && current) { + const rec = document.createElement('div'); + const icon = best.source === 'armory' ? '📦 Stored' : '🛒 For Sale'; + rec.textContent = ` ✅ Upgrade: ${icon} ${best.item.name} (${best.score.toFixed(0)} vs ${cands.find(c => c.source === 'equipped')?.score.toFixed(0) || '?'})`; + rec.style.cssText = 'color:#0f0;margin-top:2px'; + block.appendChild(rec); + } else if (best && best.source === 'equipped') { + const ok = document.createElement('div'); + ok.textContent = ' ✅ Already best'; + ok.style.cssText = 'color:#0f0'; + block.appendChild(ok); + } + + panel.appendChild(block); + }); + + document.body.appendChild(panel); +} + +function enhanceGearAnalysis() { + // Only on Bazaar pages with equiplist + if (!document.getElementById('equiplist')) return; + if (document.getElementById('hv-gear-btn')) return; + + const url = window.location.href || ''; + const isBuy = url.includes('screen=purchase') || url.includes('ss=bi'); + + const bar = document.createElement('div'); + bar.id = 'hv-gear-btn'; + bar.style.cssText = css({ + display: 'inline-flex', gap: '4px', margin: '2px 0', + }); + + // Analyze button + const analyzeBtn = document.createElement('button'); + analyzeBtn.textContent = '🔍 Analyze Gear'; + analyzeBtn.style.cssText = css({ + padding: '3px 8px', background: '#2a2a4a', color: '#fff', + border: '1px solid #555', borderRadius: '3px', cursor: 'pointer', + fontSize: '11px', fontFamily: 'monospace', + }); + analyzeBtn.onclick = () => { + autoScrapeGear(); + setTimeout(buildGearPanel, 300); + }; + bar.appendChild(analyzeBtn); + + // Rescan button (clears buy data then rescrapes) + const rescanBtn = document.createElement('button'); + rescanBtn.textContent = '🔄 Clear Buy + Rescan'; + rescanBtn.style.cssText = css({ + padding: '3px 8px', background: '#3a2a2a', color: '#fff', + border: '1px solid #555', borderRadius: '3px', cursor: 'pointer', + fontSize: '11px', fontFamily: 'monospace', + }); + rescanBtn.onclick = () => { + // Clear old buy data + const db = getGearDB(); + saveGearDB(db.filter(e => e.source !== 'buy')); + console.log('%c[HV] 🧹 Cleared old buy data', 'color:#f80'); + // Rescan current page + const scraped = autoScrapeGear(); + const count = scraped.length || 0; + console.log(`%c[HV] 🔄 Rescanned: ${count} items`, 'color:#0f0'); + // If on buy page, also scan store after short delay + if (isBuy) { + setTimeout(() => { + autoScrapeGear(); + buildGearPanel(); + }, 500); + } else { + setTimeout(buildGearPanel, 300); + } + }; + bar.appendChild(rescanBtn); + + // Insert into the page (next to the armory toolbar if present) + const equiplist = document.getElementById('equiplist'); + const container = document.createElement('div'); + container.style.cssText = 'margin-bottom:4px'; + container.appendChild(bar); + equiplist.parentNode.insertBefore(container, equiplist); + + console.log('%c[HV] 🛡️ Gear analysis enabled — click 🔍 or 🔄', 'color:#0f0'); +} + // ═══════════════════════════════════════════════════════════════════════ // BATTLE LOGGER — saves raw battle log lines to localStorage in real time // ═══════════════════════════════════════════════════════════════════════ @@ -3977,6 +4300,10 @@ window.HV = { scrapeGear: () => autoScrapeGear(), gearDebug: () => debugScrapeGear(), gearStore: () => { const s = getHVEquipStore(); return s ? `Loaded: ${Object.keys(s).length} items` : 'Not loaded yet'; }, + // Gear analysis + analyzeGear: () => analyzeGear(), + gearPanel: () => buildGearPanel(), + clearBuy: () => { const db = getGearDB(); saveGearDB(db.filter(e => e.source !== 'buy')); return 'Buy data cleared'; }, }; // ═══════════════════════════════════════════════════════════════════════ @@ -4006,7 +4333,7 @@ function init() { if (STATE.page === 'arena') { autoCheckTask('arenas'); autoCheckTask('first-blood'); } if (STATE.page === 'character') showGuidancePanel(); if (STATE.page === 'settings') enhanceSettings(); - if (STATE.page === 'armory') enhanceArmory(); + if (STATE.page === 'armory') { enhanceArmory(); enhanceGearAnalysis(); } if (STATE.page === 'abilities') enhanceAbilities(); if (STATE.page === 'monster') enhanceMonsterLab(); } diff --git a/scripts/latest.user.js b/scripts/latest.user.js index 95b7f32..f547d58 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.14.32 +// @version 0.14.33 // @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.14.32'; +const VERSION = '0.14.33'; const CFG = { // — Battle automation @@ -3615,6 +3615,33 @@ function scrapeBuyPage() { saveGearDB([...existing, ...scraped]); console.log(`%c[HV] 🛒 Buy: ${scraped.length} items (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0'); } + + // Second, scan the full store for items NOT in the visible list + // The Purchase page loads ALL items into dynjs_eqstore regardless of + // the filter tab — one visit captures the whole store. + const store = getHVEquipStore(); + if (store) { + const visibleIds = new Set(scraped.map(s => s.id)); + const missing = Object.entries(store).filter(([id, data]) => !visibleIds.has(id) && data && (data.t || data.d)); + if (missing.length > 0) { + missing.forEach(([id, data]) => { + const stats = data.d ? parseEquipHTML(data.d) : null; + let category = 'All Items'; + if (stats && stats.type) { + if (stats.type.includes('Cloth')) category = 'Cloth Armor'; + else if (stats.type.includes('Light')) category = 'Light Armor'; + else if (stats.type.includes('Heavy')) category = 'Heavy Armor'; + else if (stats.type.includes('Shield')) category = 'Shield'; + else if (stats.type.includes('Staff')) category = 'Staff'; + else category = 'Weapon'; + } + scraped.push({ id, name: data.t || 'Unknown', category, quality: data.q, stats, source: 'buy', scrapedAt: Date.now() }); + }); + const db2 = getGearDB(); + saveGearDB([...db2.filter(e => e.source !== 'buy'), ...scraped]); + console.log(`%c[HV] 🛒 Buy: +${missing.length} from store (total ${scraped.length})`, 'color:#0f0'); + } + } return scraped; } @@ -3679,6 +3706,302 @@ function scrapeModifyDetail() { return item; } +// ═══════════════════════════════════════════════════════════════════════ +// GEAR ANALYSIS — in-game gear comparison (port of analyze_gear.py) +// ═══════════════════════════════════════════════════════════════════════ +// Adds a toolbar button on Armory pages. Clicking it: +// 1. Clears old buy data (optional) +// 2. Rescrapes the current page (store or inventory) +// 3. Scores all items and shows per-slot recommendations +// Sources: 🟢 equipped (character), 📦 stored (armory), 🛒 purchasable (buy) + +function _num(v) { + if (v == null) return 0; + const s = String(v).replace('+', '').replace('%', '').trim(); + const n = parseFloat(s); + return isNaN(n) ? 0 : n; +} + +// ── Scoring (mirrors analyze_gear.py) ── +function gearScoreArmor(item) { + const st = item.stats || {}; + let s = 0; + s += _num(st['Physical Mitigation']) * 3; + s += _num(st['Evade']) * 2; + s += _num(st['Strength']) * 2; + s += _num(st['Dexterity']) * 2; + s += _num(st['Agility']) * 1.5; + s += _num(st['Endurance']); + s += _num(st['Crushing']); + s += _num(st['Slashing']); + s += _num(st['Piercing']); + s -= _num(st['burden']) * 0.5; + s += (item.quality || 0) * 5; + return s; +} + +function gearScoreWeapon(item) { + const st = item.stats || {}; + const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase(); + const quality = item.quality || 0; + let s = 0; + s += _num(st['Attack Accuracy']) * 2; + s += _num(st['Attack Crit Damage']) * 60; + s += _num(st['Strength']) * 2; + s += _num(st['Dexterity']) * 1.5; + s += _num(st['Parry']) * 0.5; + s += _num(st['Block']) * 0.5; + s += _num(st['Agility']) * 0.5; + s -= _num(st['burden']) * 0.5; + // Quality multiplier (q6=Magnificent, q5=Exquisite, q4=Superior) + s *= 0.8 + (quality * 0.12); + // Weapon type bonus + if (name.includes('estoc')) s *= 1.25; + else if (name.includes('longsword')) s *= 1.10; + else if (name.includes('great mace')) s *= 0.90; + else if (name.includes('club')) s *= 0.85; + else if (name.includes('axe')) s *= 0.90; + return s; +} + +// ── Slot detection ── +const GEAR_SLOT_KEYWORDS = { + 'Head': ['helmet','cap','goggles','mask','hood','hat','circlet','crown','visor'], + 'Body': ['breastplate','cuirass','robe','tunic','chest','gi','jacket','vest','hauberk'], + 'Hands': ['gauntlets','gloves','mitts','bracers','handguards','fists'], + 'Legs': ['leggings','greaves','pants','trousers','loincloth','kilt','breeches','chaps'], + 'Feet': ['boots','sabatons','shoes','slippers','sandals'], +}; + +function gearDetectSlot(item) { + const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase(); + for (const [slot, kws] of Object.entries(GEAR_SLOT_KEYWORDS)) { + for (const kw of kws) { + if (name.includes(kw)) return slot; + } + } + return null; +} + +function gearIsTwoHandWeapon(item) { + const st = item.stats || {}; + const itype = st.type || ''; + if (itype.includes('Armor') || itype.includes('Shield') || itype.includes('Staff')) return false; + const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase(); + if (['estoc','longsword','great mace','scythe','axe','club'].some(k => name.includes(k))) return true; + return false; +} + +function gearUsable(item, playerLv) { + const lv = (item.stats || {}).level; + if (!lv || lv === 'Unassigned') return true; + const n = parseInt(lv); + if (isNaN(n)) return true; + return n <= (playerLv || 155) + 15; +} + +function gearPlayerLevel() { + const el = document.getElementById('level_readout'); + if (el) { + const m = (el.textContent || '').match(/(\d+)/); + if (m) return parseInt(m[1]); + } + return 155; +} + +// ── Main analysis ── +function analyzeGear() { + const db = getGearDB(); + const equipped = db.filter(e => e.source === 'character' && e.id); + const armory = db.filter(e => e.source === 'armory' && e.stats); + const buy = db.filter(e => e.source === 'buy' && e.stats); + const playerLv = gearPlayerLevel(); + + // Find equipped slot map (slotType from character page) + const slotMap = {}; + equipped.forEach(e => { if (e.slotType) slotMap[e.slotType] = e; }); + + const results = []; + const slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']; + + slots.forEach(slot => { + const current = slotMap[slot]; + const cands = []; + + const consider = (item, source) => { + if (!item || !item.stats) return; + if (!gearUsable(item, playerLv)) return; + const isWeapon = slot === 'Mainhand'; + if (isWeapon) { + if (!gearIsTwoHandWeapon(item)) return; + if (!item.name || !item.stats['Attack Accuracy']) return; // needs stats + } else { + const det = gearDetectSlot(item); + if (det !== slot) return; + if (!item.stats['Physical Mitigation'] && !item.stats['Evade']) return; + } + const score = isWeapon ? gearScoreWeapon(item) : gearScoreArmor(item); + cands.push({ item, source, score }); + }; + + if (current) consider(current, 'equipped'); + armory.forEach(i => consider(i, 'armory')); + buy.forEach(i => consider(i, 'buy')); + + cands.sort((a, b) => b.score - a.score); + results.push({ slot, current, cands }); + }); + + return { playerLv, results }; +} + +// ── UI ── +function buildGearPanel() { + // Remove old panel + const old = document.getElementById('hv-gear-panel'); + if (old) old.remove(); + + const panel = document.createElement('div'); + panel.id = 'hv-gear-panel'; + panel.style.cssText = css({ + position: 'fixed', right: '8px', top: '48px', zIndex: 99999, + width: '420px', maxHeight: '80vh', overflowY: 'auto', + background: '#12121e', border: '1px solid #333', borderRadius: '6px', + padding: '8px', fontSize: '11px', fontFamily: 'monospace', + boxShadow: '0 4px 16px rgba(0,0,0,.6)', + }); + + const close = document.createElement('button'); + close.textContent = '✕'; + close.style.cssText = 'float:right;cursor:pointer;background:#333;border:none;color:#fff;border-radius:3px;padding:2px 6px'; + close.onclick = () => panel.remove(); + panel.appendChild(close); + + const title = document.createElement('div'); + title.textContent = '🛡️ Gear Analysis'; + title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd'; + panel.appendChild(title); + + const { results } = analyzeGear(); + + results.forEach(({ slot, current, cands }) => { + const block = document.createElement('div'); + block.style.cssText = 'margin-bottom:10px;border-top:1px solid #222;padding-top:6px'; + + const head = document.createElement('div'); + head.textContent = `${slot}: ${current ? current.name : '—'}`; + head.style.cssText = 'color:#9cf;font-weight:bold;margin-bottom:2px'; + block.appendChild(head); + + if (!cands.length) { + const none = document.createElement('div'); + none.textContent = ' (no data)'; + none.style.cssText = 'color:#666'; + block.appendChild(none); + panel.appendChild(block); + return; + } + + // Show top 4 candidates + cands.slice(0, 4).forEach((c, i) => { + const icon = c.source === 'equipped' ? '🟢' : c.source === 'armory' ? '📦' : '🛒'; + const row = document.createElement('div'); + const price = c.item.price ? ` 💰${c.item.price}` : ''; + const isCurrent = c.source === 'equipped'; + row.textContent = ` ${i + 1}. [${c.score.toFixed(0)}] ${icon} ${c.item.name}${isCurrent ? ' ⬅' : ''}${price}`; + row.style.cssText = isCurrent ? 'color:#8f8' : 'color:#ccc'; + if (i === 0 && !isCurrent) row.style.color = '#ff0'; + block.appendChild(row); + }); + + // Highlight best option + const best = cands[0]; + if (best && best.source !== 'equipped' && current) { + const rec = document.createElement('div'); + const icon = best.source === 'armory' ? '📦 Stored' : '🛒 For Sale'; + rec.textContent = ` ✅ Upgrade: ${icon} ${best.item.name} (${best.score.toFixed(0)} vs ${cands.find(c => c.source === 'equipped')?.score.toFixed(0) || '?'})`; + rec.style.cssText = 'color:#0f0;margin-top:2px'; + block.appendChild(rec); + } else if (best && best.source === 'equipped') { + const ok = document.createElement('div'); + ok.textContent = ' ✅ Already best'; + ok.style.cssText = 'color:#0f0'; + block.appendChild(ok); + } + + panel.appendChild(block); + }); + + document.body.appendChild(panel); +} + +function enhanceGearAnalysis() { + // Only on Bazaar pages with equiplist + if (!document.getElementById('equiplist')) return; + if (document.getElementById('hv-gear-btn')) return; + + const url = window.location.href || ''; + const isBuy = url.includes('screen=purchase') || url.includes('ss=bi'); + + const bar = document.createElement('div'); + bar.id = 'hv-gear-btn'; + bar.style.cssText = css({ + display: 'inline-flex', gap: '4px', margin: '2px 0', + }); + + // Analyze button + const analyzeBtn = document.createElement('button'); + analyzeBtn.textContent = '🔍 Analyze Gear'; + analyzeBtn.style.cssText = css({ + padding: '3px 8px', background: '#2a2a4a', color: '#fff', + border: '1px solid #555', borderRadius: '3px', cursor: 'pointer', + fontSize: '11px', fontFamily: 'monospace', + }); + analyzeBtn.onclick = () => { + autoScrapeGear(); + setTimeout(buildGearPanel, 300); + }; + bar.appendChild(analyzeBtn); + + // Rescan button (clears buy data then rescrapes) + const rescanBtn = document.createElement('button'); + rescanBtn.textContent = '🔄 Clear Buy + Rescan'; + rescanBtn.style.cssText = css({ + padding: '3px 8px', background: '#3a2a2a', color: '#fff', + border: '1px solid #555', borderRadius: '3px', cursor: 'pointer', + fontSize: '11px', fontFamily: 'monospace', + }); + rescanBtn.onclick = () => { + // Clear old buy data + const db = getGearDB(); + saveGearDB(db.filter(e => e.source !== 'buy')); + console.log('%c[HV] 🧹 Cleared old buy data', 'color:#f80'); + // Rescan current page + const scraped = autoScrapeGear(); + const count = scraped.length || 0; + console.log(`%c[HV] 🔄 Rescanned: ${count} items`, 'color:#0f0'); + // If on buy page, also scan store after short delay + if (isBuy) { + setTimeout(() => { + autoScrapeGear(); + buildGearPanel(); + }, 500); + } else { + setTimeout(buildGearPanel, 300); + } + }; + bar.appendChild(rescanBtn); + + // Insert into the page (next to the armory toolbar if present) + const equiplist = document.getElementById('equiplist'); + const container = document.createElement('div'); + container.style.cssText = 'margin-bottom:4px'; + container.appendChild(bar); + equiplist.parentNode.insertBefore(container, equiplist); + + console.log('%c[HV] 🛡️ Gear analysis enabled — click 🔍 or 🔄', 'color:#0f0'); +} + // ═══════════════════════════════════════════════════════════════════════ // BATTLE LOGGER — saves raw battle log lines to localStorage in real time // ═══════════════════════════════════════════════════════════════════════ @@ -3977,6 +4300,10 @@ window.HV = { scrapeGear: () => autoScrapeGear(), gearDebug: () => debugScrapeGear(), gearStore: () => { const s = getHVEquipStore(); return s ? `Loaded: ${Object.keys(s).length} items` : 'Not loaded yet'; }, + // Gear analysis + analyzeGear: () => analyzeGear(), + gearPanel: () => buildGearPanel(), + clearBuy: () => { const db = getGearDB(); saveGearDB(db.filter(e => e.source !== 'buy')); return 'Buy data cleared'; }, }; // ═══════════════════════════════════════════════════════════════════════ @@ -4006,7 +4333,7 @@ function init() { if (STATE.page === 'arena') { autoCheckTask('arenas'); autoCheckTask('first-blood'); } if (STATE.page === 'character') showGuidancePanel(); if (STATE.page === 'settings') enhanceSettings(); - if (STATE.page === 'armory') enhanceArmory(); + if (STATE.page === 'armory') { enhanceArmory(); enhanceGearAnalysis(); } if (STATE.page === 'abilities') enhanceAbilities(); if (STATE.page === 'monster') enhanceMonsterLab(); } diff --git a/src/config.js b/src/config.js index 84e6aa1..b24256f 100644 --- a/src/config.js +++ b/src/config.js @@ -2,7 +2,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.14.32'; +const VERSION = '0.14.33'; const CFG = { // — Battle automation diff --git a/src/gear-analysis.js b/src/gear-analysis.js new file mode 100644 index 0000000..f7e1e2b --- /dev/null +++ b/src/gear-analysis.js @@ -0,0 +1,295 @@ +// ═══════════════════════════════════════════════════════════════════════ +// GEAR ANALYSIS — in-game gear comparison (port of analyze_gear.py) +// ═══════════════════════════════════════════════════════════════════════ +// Adds a toolbar button on Armory pages. Clicking it: +// 1. Clears old buy data (optional) +// 2. Rescrapes the current page (store or inventory) +// 3. Scores all items and shows per-slot recommendations +// Sources: 🟢 equipped (character), 📦 stored (armory), 🛒 purchasable (buy) + +function _num(v) { + if (v == null) return 0; + const s = String(v).replace('+', '').replace('%', '').trim(); + const n = parseFloat(s); + return isNaN(n) ? 0 : n; +} + +// ── Scoring (mirrors analyze_gear.py) ── +function gearScoreArmor(item) { + const st = item.stats || {}; + let s = 0; + s += _num(st['Physical Mitigation']) * 3; + s += _num(st['Evade']) * 2; + s += _num(st['Strength']) * 2; + s += _num(st['Dexterity']) * 2; + s += _num(st['Agility']) * 1.5; + s += _num(st['Endurance']); + s += _num(st['Crushing']); + s += _num(st['Slashing']); + s += _num(st['Piercing']); + s -= _num(st['burden']) * 0.5; + s += (item.quality || 0) * 5; + return s; +} + +function gearScoreWeapon(item) { + const st = item.stats || {}; + const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase(); + const quality = item.quality || 0; + let s = 0; + s += _num(st['Attack Accuracy']) * 2; + s += _num(st['Attack Crit Damage']) * 60; + s += _num(st['Strength']) * 2; + s += _num(st['Dexterity']) * 1.5; + s += _num(st['Parry']) * 0.5; + s += _num(st['Block']) * 0.5; + s += _num(st['Agility']) * 0.5; + s -= _num(st['burden']) * 0.5; + // Quality multiplier (q6=Magnificent, q5=Exquisite, q4=Superior) + s *= 0.8 + (quality * 0.12); + // Weapon type bonus + if (name.includes('estoc')) s *= 1.25; + else if (name.includes('longsword')) s *= 1.10; + else if (name.includes('great mace')) s *= 0.90; + else if (name.includes('club')) s *= 0.85; + else if (name.includes('axe')) s *= 0.90; + return s; +} + +// ── Slot detection ── +const GEAR_SLOT_KEYWORDS = { + 'Head': ['helmet','cap','goggles','mask','hood','hat','circlet','crown','visor'], + 'Body': ['breastplate','cuirass','robe','tunic','chest','gi','jacket','vest','hauberk'], + 'Hands': ['gauntlets','gloves','mitts','bracers','handguards','fists'], + 'Legs': ['leggings','greaves','pants','trousers','loincloth','kilt','breeches','chaps'], + 'Feet': ['boots','sabatons','shoes','slippers','sandals'], +}; + +function gearDetectSlot(item) { + const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase(); + for (const [slot, kws] of Object.entries(GEAR_SLOT_KEYWORDS)) { + for (const kw of kws) { + if (name.includes(kw)) return slot; + } + } + return null; +} + +function gearIsTwoHandWeapon(item) { + const st = item.stats || {}; + const itype = st.type || ''; + if (itype.includes('Armor') || itype.includes('Shield') || itype.includes('Staff')) return false; + const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase(); + if (['estoc','longsword','great mace','scythe','axe','club'].some(k => name.includes(k))) return true; + return false; +} + +function gearUsable(item, playerLv) { + const lv = (item.stats || {}).level; + if (!lv || lv === 'Unassigned') return true; + const n = parseInt(lv); + if (isNaN(n)) return true; + return n <= (playerLv || 155) + 15; +} + +function gearPlayerLevel() { + const el = document.getElementById('level_readout'); + if (el) { + const m = (el.textContent || '').match(/(\d+)/); + if (m) return parseInt(m[1]); + } + return 155; +} + +// ── Main analysis ── +function analyzeGear() { + const db = getGearDB(); + const equipped = db.filter(e => e.source === 'character' && e.id); + const armory = db.filter(e => e.source === 'armory' && e.stats); + const buy = db.filter(e => e.source === 'buy' && e.stats); + const playerLv = gearPlayerLevel(); + + // Find equipped slot map (slotType from character page) + const slotMap = {}; + equipped.forEach(e => { if (e.slotType) slotMap[e.slotType] = e; }); + + const results = []; + const slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']; + + slots.forEach(slot => { + const current = slotMap[slot]; + const cands = []; + + const consider = (item, source) => { + if (!item || !item.stats) return; + if (!gearUsable(item, playerLv)) return; + const isWeapon = slot === 'Mainhand'; + if (isWeapon) { + if (!gearIsTwoHandWeapon(item)) return; + if (!item.name || !item.stats['Attack Accuracy']) return; // needs stats + } else { + const det = gearDetectSlot(item); + if (det !== slot) return; + if (!item.stats['Physical Mitigation'] && !item.stats['Evade']) return; + } + const score = isWeapon ? gearScoreWeapon(item) : gearScoreArmor(item); + cands.push({ item, source, score }); + }; + + if (current) consider(current, 'equipped'); + armory.forEach(i => consider(i, 'armory')); + buy.forEach(i => consider(i, 'buy')); + + cands.sort((a, b) => b.score - a.score); + results.push({ slot, current, cands }); + }); + + return { playerLv, results }; +} + +// ── UI ── +function buildGearPanel() { + // Remove old panel + const old = document.getElementById('hv-gear-panel'); + if (old) old.remove(); + + const panel = document.createElement('div'); + panel.id = 'hv-gear-panel'; + panel.style.cssText = css({ + position: 'fixed', right: '8px', top: '48px', zIndex: 99999, + width: '420px', maxHeight: '80vh', overflowY: 'auto', + background: '#12121e', border: '1px solid #333', borderRadius: '6px', + padding: '8px', fontSize: '11px', fontFamily: 'monospace', + boxShadow: '0 4px 16px rgba(0,0,0,.6)', + }); + + const close = document.createElement('button'); + close.textContent = '✕'; + close.style.cssText = 'float:right;cursor:pointer;background:#333;border:none;color:#fff;border-radius:3px;padding:2px 6px'; + close.onclick = () => panel.remove(); + panel.appendChild(close); + + const title = document.createElement('div'); + title.textContent = '🛡️ Gear Analysis'; + title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd'; + panel.appendChild(title); + + const { results } = analyzeGear(); + + results.forEach(({ slot, current, cands }) => { + const block = document.createElement('div'); + block.style.cssText = 'margin-bottom:10px;border-top:1px solid #222;padding-top:6px'; + + const head = document.createElement('div'); + head.textContent = `${slot}: ${current ? current.name : '—'}`; + head.style.cssText = 'color:#9cf;font-weight:bold;margin-bottom:2px'; + block.appendChild(head); + + if (!cands.length) { + const none = document.createElement('div'); + none.textContent = ' (no data)'; + none.style.cssText = 'color:#666'; + block.appendChild(none); + panel.appendChild(block); + return; + } + + // Show top 4 candidates + cands.slice(0, 4).forEach((c, i) => { + const icon = c.source === 'equipped' ? '🟢' : c.source === 'armory' ? '📦' : '🛒'; + const row = document.createElement('div'); + const price = c.item.price ? ` 💰${c.item.price}` : ''; + const isCurrent = c.source === 'equipped'; + row.textContent = ` ${i + 1}. [${c.score.toFixed(0)}] ${icon} ${c.item.name}${isCurrent ? ' ⬅' : ''}${price}`; + row.style.cssText = isCurrent ? 'color:#8f8' : 'color:#ccc'; + if (i === 0 && !isCurrent) row.style.color = '#ff0'; + block.appendChild(row); + }); + + // Highlight best option + const best = cands[0]; + if (best && best.source !== 'equipped' && current) { + const rec = document.createElement('div'); + const icon = best.source === 'armory' ? '📦 Stored' : '🛒 For Sale'; + rec.textContent = ` ✅ Upgrade: ${icon} ${best.item.name} (${best.score.toFixed(0)} vs ${cands.find(c => c.source === 'equipped')?.score.toFixed(0) || '?'})`; + rec.style.cssText = 'color:#0f0;margin-top:2px'; + block.appendChild(rec); + } else if (best && best.source === 'equipped') { + const ok = document.createElement('div'); + ok.textContent = ' ✅ Already best'; + ok.style.cssText = 'color:#0f0'; + block.appendChild(ok); + } + + panel.appendChild(block); + }); + + document.body.appendChild(panel); +} + +function enhanceGearAnalysis() { + // Only on Bazaar pages with equiplist + if (!document.getElementById('equiplist')) return; + if (document.getElementById('hv-gear-btn')) return; + + const url = window.location.href || ''; + const isBuy = url.includes('screen=purchase') || url.includes('ss=bi'); + + const bar = document.createElement('div'); + bar.id = 'hv-gear-btn'; + bar.style.cssText = css({ + display: 'inline-flex', gap: '4px', margin: '2px 0', + }); + + // Analyze button + const analyzeBtn = document.createElement('button'); + analyzeBtn.textContent = '🔍 Analyze Gear'; + analyzeBtn.style.cssText = css({ + padding: '3px 8px', background: '#2a2a4a', color: '#fff', + border: '1px solid #555', borderRadius: '3px', cursor: 'pointer', + fontSize: '11px', fontFamily: 'monospace', + }); + analyzeBtn.onclick = () => { + autoScrapeGear(); + setTimeout(buildGearPanel, 300); + }; + bar.appendChild(analyzeBtn); + + // Rescan button (clears buy data then rescrapes) + const rescanBtn = document.createElement('button'); + rescanBtn.textContent = '🔄 Clear Buy + Rescan'; + rescanBtn.style.cssText = css({ + padding: '3px 8px', background: '#3a2a2a', color: '#fff', + border: '1px solid #555', borderRadius: '3px', cursor: 'pointer', + fontSize: '11px', fontFamily: 'monospace', + }); + rescanBtn.onclick = () => { + // Clear old buy data + const db = getGearDB(); + saveGearDB(db.filter(e => e.source !== 'buy')); + console.log('%c[HV] 🧹 Cleared old buy data', 'color:#f80'); + // Rescan current page + const scraped = autoScrapeGear(); + const count = scraped.length || 0; + console.log(`%c[HV] 🔄 Rescanned: ${count} items`, 'color:#0f0'); + // If on buy page, also scan store after short delay + if (isBuy) { + setTimeout(() => { + autoScrapeGear(); + buildGearPanel(); + }, 500); + } else { + setTimeout(buildGearPanel, 300); + } + }; + bar.appendChild(rescanBtn); + + // Insert into the page (next to the armory toolbar if present) + const equiplist = document.getElementById('equiplist'); + const container = document.createElement('div'); + container.style.cssText = 'margin-bottom:4px'; + container.appendChild(bar); + equiplist.parentNode.insertBefore(container, equiplist); + + console.log('%c[HV] 🛡️ Gear analysis enabled — click 🔍 or 🔄', 'color:#0f0'); +} diff --git a/src/gear-scraper.js b/src/gear-scraper.js index 7012e7e..83b9f37 100644 --- a/src/gear-scraper.js +++ b/src/gear-scraper.js @@ -296,6 +296,33 @@ function scrapeBuyPage() { saveGearDB([...existing, ...scraped]); console.log(`%c[HV] 🛒 Buy: ${scraped.length} items (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0'); } + + // Second, scan the full store for items NOT in the visible list + // The Purchase page loads ALL items into dynjs_eqstore regardless of + // the filter tab — one visit captures the whole store. + const store = getHVEquipStore(); + if (store) { + const visibleIds = new Set(scraped.map(s => s.id)); + const missing = Object.entries(store).filter(([id, data]) => !visibleIds.has(id) && data && (data.t || data.d)); + if (missing.length > 0) { + missing.forEach(([id, data]) => { + const stats = data.d ? parseEquipHTML(data.d) : null; + let category = 'All Items'; + if (stats && stats.type) { + if (stats.type.includes('Cloth')) category = 'Cloth Armor'; + else if (stats.type.includes('Light')) category = 'Light Armor'; + else if (stats.type.includes('Heavy')) category = 'Heavy Armor'; + else if (stats.type.includes('Shield')) category = 'Shield'; + else if (stats.type.includes('Staff')) category = 'Staff'; + else category = 'Weapon'; + } + scraped.push({ id, name: data.t || 'Unknown', category, quality: data.q, stats, source: 'buy', scrapedAt: Date.now() }); + }); + const db2 = getGearDB(); + saveGearDB([...db2.filter(e => e.source !== 'buy'), ...scraped]); + console.log(`%c[HV] 🛒 Buy: +${missing.length} from store (total ${scraped.length})`, 'color:#0f0'); + } + } return scraped; } diff --git a/src/header.user.js b/src/header.user.js index 55ed274..023514d 100644 --- a/src/header.user.js +++ b/src/header.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.14.32 +// @version 0.14.33 // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @author GaboGG + Hermes // @match *://*.hentaiverse.org/* diff --git a/src/init.js b/src/init.js index dba6f6c..b93fa39 100644 --- a/src/init.js +++ b/src/init.js @@ -25,7 +25,7 @@ function init() { if (STATE.page === 'arena') { autoCheckTask('arenas'); autoCheckTask('first-blood'); } if (STATE.page === 'character') showGuidancePanel(); if (STATE.page === 'settings') enhanceSettings(); - if (STATE.page === 'armory') enhanceArmory(); + if (STATE.page === 'armory') { enhanceArmory(); enhanceGearAnalysis(); } if (STATE.page === 'abilities') enhanceAbilities(); if (STATE.page === 'monster') enhanceMonsterLab(); } diff --git a/src/public-api.js b/src/public-api.js index 8202fb8..fdaa4b8 100644 --- a/src/public-api.js +++ b/src/public-api.js @@ -26,4 +26,8 @@ window.HV = { scrapeGear: () => autoScrapeGear(), gearDebug: () => debugScrapeGear(), gearStore: () => { const s = getHVEquipStore(); return s ? `Loaded: ${Object.keys(s).length} items` : 'Not loaded yet'; }, + // Gear analysis + analyzeGear: () => analyzeGear(), + gearPanel: () => buildGearPanel(), + clearBuy: () => { const db = getGearDB(); saveGearDB(db.filter(e => e.source !== 'buy')); return 'Buy data cleared'; }, };