From 329e9403b4f423d1ee1da4a5ff8fb05a5e065a45 Mon Sep 17 00:00:00 2001 From: GaboGG Date: Mon, 27 Jul 2026 15:46:36 -0400 Subject: [PATCH] v0.14.14 - V2 gear scraper: reads from HV in-memory JS store (no hovers) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Previously: tried equips.set() + 300ms delays + popup_box polling which was fragile, async, and often saved stats to wrong slots - Now: reads window.dynjs_equip / dynjs_eqstore which contains ALL item data pre-loaded on page load (name, quality, full tooltip HTML) - Uses DOMParser to parse the embedded .eq HTML into structured stats (type, level, condition, burden, ADB, mit, attributes, etc.) - Zero hover, zero delays, zero popup_box interaction — instant stats - Works on Character Equipment page AND Armory pages simultaneously - Removed triggerAllTooltips() and captureAllTooltips() — obsolete --- scripts/hv-unified.user.js | 600 +++++++++++++------------------------ scripts/latest.user.js | 600 +++++++++++++------------------------ src/config.js | 2 +- src/gear-scraper.js | 582 +++++++++++++---------------------- src/header.user.js | 2 +- src/init.js | 13 +- src/public-api.js | 1 - 7 files changed, 624 insertions(+), 1176 deletions(-) diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index bad70d4..3a488c2 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.13 +// @version 0.14.14 // @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.13'; +const VERSION = '0.14.14'; const CFG = { // — Battle automation @@ -3244,61 +3244,26 @@ function enhanceArmory() { // ═══════════════════════════════════════════════════════════════════════ // GEAR SCRAPER — capture full equipment details for analysis // ═══════════════════════════════════════════════════════════════════════ -// Usage: -// 1. Visit Character Equipment page (?s=Character&ss=eq) — equipped gear -// 2. Visit Armory page (?s=Bazaar&ss=am) — inventory listing -// 3. Visit Modify page (?s=Bazaar&ss=am&screen=modify&eqids[]=ID) — full stats -// 4. Data saved to localStorage['hvunified_geardb'] -// 5. Query with: HV.gear() or HV.gearSummary() +// V2: Uses window.dynjs_equip / dynjs_eqstore directly from HV's JS +// memory store — no hover simulation or popup box delays needed. +// Every item's full tooltip HTML is pre-loaded on page load. const GEAR_DB_KEY = SP + 'geardb'; -// Forced scrape from console with diagnostics -function debugScrapeGear() { - const url = window.location.href || ''; - console.log(`%c[HV] 🔍 Gear Debug: URL=${url}`, 'color:#f80'); - - // Check for #eqsb - const eqsb = document.getElementById('eqsb'); - if (eqsb) { - const slots = eqsb.querySelectorAll(':scope > .eqb'); - console.log(`%c[HV] #eqsb found: ${slots.length} equipment slots`, 'color:#0f0'); - slots.forEach((s, i) => { - const omo = s.getAttribute('onmouseover') || ''; - // The onmouseover is on the inner name div, not the .eqb parent - const nameDiv = s.querySelector(':scope > div[onmouseover]'); - const innerOmo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : ''; - const name = nameDiv ? (nameDiv.textContent || '').trim() : 'no name'; - console.log(` [${i}] ${name} | onmouseover: ${(innerOmo || omo).substring(0, 80)}`); - }); - } else { - console.log(`%c[HV] ❌ #eqsb NOT FOUND`, 'color:#f44'); - } - - // Check for #equiplist - const equiplist = document.getElementById('equiplist'); - if (equiplist) { - const rows = equiplist.querySelectorAll('table tr'); - console.log(`%c[HV] #equiplist found: ${rows.length} rows`, 'color:#0f0'); - } else { - console.log(`%c[HV] ❌ #equiplist NOT FOUND`, 'color:#f44'); - } - - // Check popup_box - const popup = document.getElementById('popup_box'); - if (popup) { - const text = (popup.textContent || '').substring(0, 100); - console.log(`%c[HV] #popup_box: "${text}..."`, 'color:#888'); - } else { - console.log(`%c[HV] ❌ #popup_box NOT FOUND`, 'color:#f44'); - } - - // Check for equips global - console.log(`%c[HV] typeof equips = ${typeof equips}`, 'color:#888'); - - return { url, hasEqsb: !!eqsb, hasEquiplist: !!equiplist, hasPopup: !!popup }; +// ── Access HV's in-memory equipment store ── +function getHVEquipStore() { + const w = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; + if (w.dynjs_eqstore && Object.keys(w.dynjs_eqstore).length) return w.dynjs_eqstore; + if (w.dynjs_equip && Object.keys(w.dynjs_equip).length) return w.dynjs_equip; + return null; } +function getHVEquipData(itemId) { + const store = getHVEquipStore(); + return (store && store[itemId]) ? store[itemId] : null; +} + +// ── Public API ── function getGearDB() { try { return JSON.parse(localStorage[GEAR_DB_KEY] || '[]'); } catch (e) { return []; } } @@ -3311,378 +3276,235 @@ function logGearSummary() { const armory = db.filter(e => e.source === 'armory'); console.log(`%c[HV] 📦 Gear DB: ${db.length} total (${equipped.length} equipped, ${armory.length} armory)`, 'color:#0f0'); equipped.forEach(item => { - console.log(` 🗡 ${item.name} | ID: ${item.id} | ${item.slotType || ''}`); + console.log(` 🗡 [${item.slotType}] ${item.name} (ID: ${item.id}) — ${item.stats ? Object.keys(item.stats).length + ' stats' : 'no stats'}`); }); armory.forEach(item => { - console.log(` 📦 ${item.name} | ID: ${item.id} | ${item.category || ''}`); + console.log(` 📦 [${item.category}] ${item.name} (ID: ${item.id}) — ${item.stats ? Object.keys(item.stats).length + ' stats' : 'no stats'}`); }); } -// ── Parse the tooltip popup box for full item stats ── -// The #popup_box div appears on hover with a detailed stat breakdown: -//
-//
Light Armor & Level 115 & Tradeable
-//
Condition: 55% & Energy: N/A
-//
Burden: 7.1 & Interference: 2.1
-//
...
-//
Primary Attributes
-//
-function parseTooltipPopup() { - const popup = document.getElementById('popup_box'); - if (!popup) return null; - const text = popup.textContent || ''; - - // Extract item name (first div child text, before nested eq) - const nameDiv = popup.querySelector(':scope > div:first-child'); - const name = nameDiv ? (nameDiv.textContent || '').trim() : ''; - - const eq = popup.querySelector('.eq'); - if (!eq) return null; - - const stats = {}; - - // Type line: "Light Armor Level 115 Tradeable" - const eqt = eq.querySelector('.eqt'); - if (eqt) { - const eqtText = eqt.textContent || ''; - const typeMatch = eqtText.match(/^([A-Za-z\s]+?)(?:\s{2,}|$)/); - if (typeMatch) stats.type = typeMatch[1].trim(); - const lvMatch = eqtText.match(/Level\s*(\d+)/i); - if (lvMatch) stats.level = lvMatch[1]; - const tradeMatch = eqtText.match(/(Tradeable|Soulbound|Blessed)/); - if (tradeMatch) stats.bind = tradeMatch[1]; - } - - // Condition / Energy - const eqr = eq.querySelector('.eqr'); - if (eqr) { - const condMatch = (eqr.textContent || '').match(/Condition:\s*(\d+)%/i); - if (condMatch) stats.condition = condMatch[1]; - const engMatch = (eqr.textContent || '').match(/Energy:\s*([^\s]+)/i); - if (engMatch) stats.energy = engMatch[1]; - } - - // Burden / Interference - const eqc = eq.querySelector('.eqc'); - if (eqc) { - const burMatch = (eqc.textContent || '').match(/Burden:\s*([\d.]+)/i); - if (burMatch) stats.burden = burMatch[1]; - const intMatch = (eqc.textContent || '').match(/Interference:\s*([\d.]+)/i); - if (intMatch) stats.interference = intMatch[1]; - } - - // Stats from .ex (main stat block) - const ex = eq.querySelector('.ex'); - if (ex) { - ex.querySelectorAll(':scope > div').forEach(stat => { - const label = stat.querySelector(':scope > div:first-child'); - const value = stat.querySelector(':scope > div:nth-child(2) span, :scope > div:nth-child(2)'); - if (label && value) { - const key = (label.textContent || '').trim().replace(/[^A-Za-z\s]/g, '').trim(); - const val = (value.textContent || '').trim(); - if (key && val) stats[key] = val; +// ── Parse tooltip HTML string into structured stats ── +// HV stores the full .eq HTML in dynjs_equip[ID].d +function parseEquipHTML(htmlStr) { + if (!htmlStr) return null; + try { + const doc = new DOMParser().parseFromString(htmlStr, 'text/html'); + const eq = doc.querySelector('.eq'); + if (!eq) return null; + const stats = {}; + + // Header: type, level, binding + const eqt = eq.querySelector('.eqt'); + if (eqt) { + const t = eqt.textContent || ''; + const typeM = t.match(/^([A-Za-z\s]+?)(?:\s{2,}|$)/); + if (typeM) stats.type = typeM[1].trim(); + const lvM = t.match(/Level\s*(\d+|Unassigned)/i); + if (lvM) stats.level = lvM[1]; + const bindM = t.match(/(Tradeable|Soulbound|Blessed)/i); + if (bindM) stats.bind = bindM[1]; + } + + // Condition / Energy + const eqr = eq.querySelector('.eqr'); + if (eqr) { + const r = eqr.textContent || ''; + const cM = r.match(/Condition:\s*(\d+)%/i); + if (cM) stats.condition = parseInt(cM[1]); + const eM = r.match(/Energy:\s*([^\s]+)/i); + if (eM) stats.energy = eM[1]; + } + + // Burden / Interference + const eqc = eq.querySelector('.eqc'); + if (eqc) { + const c = eqc.textContent || ''; + const bM = c.match(/Burden:\s*([\d.]+)/i); + if (bM) stats.burden = parseFloat(bM[1]); + const iM = c.match(/Interference:\s*([\d.]+)/i); + if (iM) stats.interference = parseFloat(iM[1]); + } + + // Parse label/value pairs from stat divs + const parseRow = (div) => { + const label = div.querySelector(':scope > div:first-child'); + const val = div.querySelector(':scope > div:nth-child(2)'); + if (!label || !val) return; + const key = (label.textContent || '').trim(); + const raw = (val.textContent || '').trim(); + if (key && raw) { + stats[key] = raw; + const title = div.getAttribute('title') || ''; + const baseM = title.match(/Base:\s*(\d+)/i); + if (baseM) stats[key + ' Base'] = parseInt(baseM[1]); } + }; + + // Main stats (.ex) + const ex = eq.querySelector('.ex'); + if (ex) ex.querySelectorAll(':scope > div').forEach(parseRow); + + // Extra stat groups (.ep) + eq.querySelectorAll('.ep').forEach(g => { + g.querySelectorAll(':scope > div:not(:first-child)').forEach(parseRow); }); + + return stats; + } catch (e) { + return null; } - - // Extra stat groups (.ep) - eq.querySelectorAll('.ep').forEach(group => { - const groupTitle = group.querySelector(':scope > div:first-child'); - const groupName = groupTitle ? (groupTitle.textContent || '').trim() : ''; - group.querySelectorAll(':scope > div:not(:first-child)').forEach(stat => { - const label = stat.querySelector(':scope > div:first-child'); - const value = stat.querySelector(':scope > div:nth-child(2) span, :scope > div:nth-child(2)'); - if (label && value) { - const key = (label.textContent || '').trim().replace(/[^A-Za-z\s]/g, '').trim(); - const val = (value.textContent || '').trim(); - if (key && val) stats[key] = val; - } - }); - }); - - return { name, stats }; } -// ── Scrape Character Equipment page (?s=Character&ss=eq) ── -// DOM structure: -// #eqsb > .eqb (equipment slots) -// .eqb div: "Exquisite Estoc of Slaughter" (name) -// .eqb onmouseover="equips.set(ITEM_ID,'slot_pane',...)" (ID) -// #popup_box (tooltip with full stats, populated by hovering) +// ── Scrape Character Equipment page ── +const SLOT_TYPES = ['Mainhand', 'Offhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']; + function scrapeCharacterEquip() { const scraped = []; - - // Read each equipment slot in #eqsb const slots = document.querySelectorAll('#eqsb > .eqb'); - slots.forEach(slot => { - // onmouseover is on the inner name div, not the .eqb parent - const nameDiv = slot.querySelector(':scope > div[onmouseover]'); - const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : slot.getAttribute('onmouseover') || ''; - const idMatch = omo.match(/equips\.set\((\d+)/); - const itemId = idMatch ? idMatch[1] : ''; - - // Name is the text content of the name div - const name = nameDiv ? (nameDiv.textContent || '').trim() : ''; - - // Check if slot is disabled - const disabled = slot.classList.contains('eqdisabled'); - - // Determine slot type from parent structure - const slotType = ''; - - scraped.push({ - id: itemId, - name: name, - slotType: slotType, - disabled: disabled, - source: 'character', - scrapedAt: Date.now(), - }); - }); - - if (scraped.length > 0) { - const db = getGearDB(); - const filtered = db.filter(e => !(e.source === 'character')); - saveGearDB([...filtered, ...scraped]); - console.log(`%c[HV] Gear: scraped ${scraped.length} equipped items (${scraped.filter(s => s.stats).length} with tooltip stats)`, 'color:#0f0'); - } - return scraped; -} - -// ── Scrape Armory Organize page (?s=Bazaar&ss=am&screen=organize) ── -// DOM structure: -// #equiplist > table > tbody > tr -// tr.eqtplabel: category name -// tr[onmouseover]: equipment row with: -// onmouseover="hover_equip(ITEM_ID)" -// input[name="eqids[]"] value="ITEM_ID" -// label text: item name -// #equipinfo > .showequip > .eq: tooltip for selected item -function scrapeArmory() { - const scraped = []; - const equipList = document.getElementById('equiplist'); - if (!equipList) return scraped; - - let currentCategory = ''; - - const rows = equipList.querySelectorAll('table tr'); - rows.forEach(row => { - // Category header - if (row.classList.contains('eqtplabel')) { - currentCategory = (row.textContent || '').trim(); - return; - } - - // Skip select-all header row - if (row.classList.contains('eqselall')) return; - - // Item ID from onmouseover - const omo = row.getAttribute('onmouseover') || ''; - const idMatch = omo.match(/hover_equip\((\d+)\)/); - const itemId = idMatch ? idMatch[1] : ''; - - // Item name from label - const label = row.querySelector('label'); - let name = label ? (label.textContent || '').trim() : ''; - // Strip checkbox indicator - const cb = label ? label.querySelector('input') : null; - if (cb) { - name = (label.textContent || '').replace(cb.outerHTML, '').trim(); - } - - // Checkbox value as fallback ID - const checkbox = row.querySelector('input[name="eqids[]"]'); - const cbId = checkbox ? checkbox.value : ''; - - if (!name && !itemId) return; - - scraped.push({ - id: itemId || cbId, - name: name || 'Unknown', - category: currentCategory, - source: 'armory', - scrapedAt: Date.now(), - }); - }); - - // Read the right-side info pane for the currently-hovered item's stats - const equipInfo = document.getElementById('equipinfo'); - if (equipInfo) { - const showEquip = equipInfo.querySelector('.showequip'); - if (showEquip) { - const link = showEquip.querySelector('a'); - const infoName = link ? (link.textContent || '').trim() : ''; - const eq = showEquip.querySelector('.eq'); - if (eq) { - const infoText = eq.textContent || ''; - // Find matching item and attach raw stats - const matchIdx = scraped.findIndex(s => infoName.includes(s.name) || s.name.includes(infoName)); - if (matchIdx >= 0) { - scraped[matchIdx].infoText = infoText; - // Parse condition - const condMatch = infoText.match(/Condition:\s*(\d+)%/i); - if (condMatch) scraped[matchIdx].condition = parseInt(condMatch[1]); - } - } - } - } - - if (scraped.length > 0) { - const db = getGearDB(); - const filtered = db.filter(e => e.source !== 'armory'); - saveGearDB([...filtered, ...scraped]); - console.log(`%c[HV] Gear: scraped ${scraped.length} armory items`, 'color:#0f0'); - } - return scraped; -} - -// ── Trigger hover on each character slot to populate tooltip ── -// HV's equips.set(id, 'slot_pane', w, h) populates #popup_box with stats. -// We call it for each item in sequence to capture all tooltips. -function triggerAllTooltips() { - const slots = document.querySelectorAll('#eqsb > .eqb'); - const results = []; - - slots.forEach(slot => { - const nameDiv = slot.querySelector(':scope > div[onmouseover]'); - const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : slot.getAttribute('onmouseover') || ''; - const idMatch = omo.match(/equips\.set\((\d+)/); - if (idMatch && typeof equips !== 'undefined' && equips.set) { - try { - equips.set(parseInt(idMatch[1]), 'slot_pane', 250, 130); - } catch (e) {} - } - }); - - return results; -} - -// Manual: hover each item and capture tooltip stats with delays -// Run this from console on the character equipment page for full stats -function captureAllTooltips(delay = 400) { - const slots = document.querySelectorAll('#eqsb > .eqb'); - const results = []; - let idx = 0; - - function next() { - if (idx >= slots.length) { - console.log(`%c[HV] Captured ${results.length} tooltips`, 'color:#0f0'); - // DON'T re-scrape — that would wipe the tooltip stats we just saved - logGearSummary(); - return; - } - - const slot = slots[idx]; + slots.forEach((slot, i) => { const nameDiv = slot.querySelector(':scope > div[onmouseover]'); const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : ''; - const idMatch = omo.match(/equips\.set\((\d+)/); - - if (idMatch && typeof equips !== 'undefined' && equips.set) { - try { - equips.set(parseInt(idMatch[1]), 'slot_pane', 250, 130); - // Wait for popup to populate - setTimeout(() => { - const tooltip = parseTooltipPopup(); - if (tooltip && tooltip.name && tooltip.name !== 'Popup Box') { - // Store in localStorage directly - const db = getGearDB(); - const match = db.find(e => e.id === idMatch[1]); - if (match) { - match.stats = tooltip.stats; - saveGearDB(db); - } - results.push(tooltip); - console.log(`%c[HV] [${idx}] ${tooltip.name}`, 'color:#888'); - } - idx++; - next(); - }, 300); - return; - } catch (e) {} + const idM = omo.match(/equips\.set\((\d+)/); + const itemId = idM ? idM[1] : ''; + const name = nameDiv ? (nameDiv.textContent || '').trim() : ''; + const disabled = slot.classList.contains('eqdisabled'); + + const obj = { + id: itemId, name, disabled, + slotType: SLOT_TYPES[i] || '', + source: 'character', + scrapedAt: Date.now(), + }; + + // Pull full stats from HV's in-memory store — instant, no hover + if (itemId) { + const data = getHVEquipData(itemId); + if (data) { + if (data.t) obj.name = data.t; + if (data.q != null) obj.quality = data.q; + if (data.d) obj.stats = parseEquipHTML(data.d); + } } - idx++; - next(); + scraped.push(obj); + }); + + if (scraped.length > 0) { + const db = getGearDB(); + saveGearDB([...db.filter(e => e.source !== 'character'), ...scraped]); + console.log(`%c[HV] 🗡 Character: ${scraped.length} slots (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0'); } - - next(); - return 'Capturing tooltips... check console'; + return scraped; } -// ── Auto-detect and scrape current page ── +// ── Scrape Armory page ── +function scrapeArmory() { + const scraped = []; + const el = document.getElementById('equiplist'); + if (!el) return scraped; + let cat = ''; + + el.querySelectorAll('table tr').forEach(row => { + if (row.classList.contains('eqtplabel')) { cat = (row.textContent || '').trim(); return; } + if (row.classList.contains('eqselall')) return; + const omo = row.getAttribute('onmouseover') || ''; + const idM = omo.match(/hover_equip\((\d+)\)/); + const cb = row.querySelector('input[name="eqids[]"]'); + const itemId = idM ? idM[1] : (cb ? cb.value : ''); + const label = row.querySelector('label'); + let name = label ? (label.textContent || '').trim() : ''; + if (cb && label) name = (label.textContent || '').replace(cb.outerHTML, '').trim(); + if (!itemId && !name) return; + + const obj = { id: itemId, name: name || 'Unknown', category: cat, source: 'armory', scrapedAt: Date.now() }; + if (itemId) { + const data = getHVEquipData(itemId); + if (data) { + if (data.t) obj.name = data.t; + if (data.q != null) obj.quality = data.q; + if (data.d) obj.stats = parseEquipHTML(data.d); + } + } + scraped.push(obj); + }); + + if (scraped.length > 0) { + const db = getGearDB(); + saveGearDB([...db.filter(e => e.source !== 'armory'), ...scraped]); + console.log(`%c[HV] 📦 Armory: ${scraped.length} items (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0'); + } + return scraped; +} + +// ── Debug ── +function debugScrapeGear() { + const url = window.location.href || ''; + const store = getHVEquipStore(); + const storeCount = store ? Object.keys(store).length : 0; + const eqsb = document.getElementById('eqsb'); + const slots = eqsb ? eqsb.querySelectorAll(':scope > .eqb').length : 0; + const el = document.getElementById('equiplist'); + const rows = el ? el.querySelectorAll('table tr[onmouseover]').length : 0; + console.log(`%c[HV] 🔍 URL=${url} | store=${storeCount} items | eqsb=${slots} slots | equiplist=${rows} rows`, 'color:#f80'); + return { url, storeCount, slots, rows }; +} + +// ── Auto-detect ── function autoScrapeGear() { const url = window.location.href || ''; - - // Character equipment page - if (url.includes('ss=eq')) { - return scrapeCharacterEquip(); - } - - // Character summary page — try reading the slots - if (url.includes('ss=ch')) { - return scrapeCharacterEquip(); - } - - // Armory pages - if (url.includes('ss=am')) { - if (url.includes('screen=modify')) { - return scrapeModifyDetail(); - } - return scrapeArmory(); - } - + if (url.includes('ss=eq') || url.includes('ss=ch')) return scrapeCharacterEquip(); + if (url.includes('ss=am') && !url.includes('screen=modify')) return scrapeArmory(); + if (url.includes('ss=am') && url.includes('screen=modify')) return scrapeModifyDetail(); return []; } -// ── Scrape equipment modify detail page ── +// ── Modify detail page ── function scrapeModifyDetail() { const mainPane = document.getElementById('mainpane'); if (!mainPane) return null; - - // Try to get item ID from URL - const idMatch = window.location.href.match(/eqids?\[\]=(\d+)/); - const itemId = idMatch ? idMatch[1] : ''; - - // Item name from the page title area + const idM = window.location.href.match(/eqids?\[\]=(\d+)/); + const itemId = idM ? idM[1] : ''; const nameEl = mainPane.querySelector('h3, .itemname, .eqname'); - const name = nameEl ? (nameEl.textContent || '').trim() : ''; + let name = nameEl ? (nameEl.textContent || '').trim() : ''; + let stats = {}; - // Read all stat tables - const stats = {}; + // Parse right-side tooltip if present + const eqDiv = mainPane.querySelector('#equipmodify_right .eq, #equipinfo .eq'); + if (eqDiv) stats = parseEquipHTML(eqDiv.outerHTML) || {}; + + // Parse modify tables mainPane.querySelectorAll('table.gry, table.grl').forEach(tbl => { tbl.querySelectorAll('tr').forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length >= 2) { - const key = (cells[0].textContent || '').trim().replace(':', ''); - const val = (cells[1].textContent || '').trim(); - if (key && val) stats[key] = val; + const k = (cells[0].textContent || '').trim().replace(':', ''); + const v = (cells[1].textContent || '').trim(); + if (k && v) stats[k] = v; } }); }); const text = mainPane.textContent || ''; - const durMatch = text.match(/Durability:\s*(\d+)\s*\/\s*(\d+)/i); - const potMatch = text.match(/(?:Potency|Item World)\s*:\s*([^\n<]+)/i); + const durM = text.match(/Durability:\s*(\d+)\s*\/\s*(\d+)/i); + const potM = text.match(/(?:Potency|Item World)\s*:\s*([^\n<]+)/i); - const item = { - id: itemId, - name: name, - stats: stats, - durability: durMatch ? `${durMatch[1]}/${durMatch[2]}` : '', - potency: potMatch ? potMatch[1].trim() : '', - source: 'modify', - scrapedAt: Date.now(), - }; - - if (item.id) { - const db = getGearDB(); - const idx = db.findIndex(e => e.id === item.id && e.source === 'modify'); - if (idx >= 0) db[idx] = item; - else db.push(item); - saveGearDB(db); - console.log(`%c[HV] Gear: scraped modify detail for ${item.name || itemId}`, 'color:#0f0'); + // Enrich from memory + if (itemId) { + const data = getHVEquipData(itemId); + if (data) { + if (!name && data.t) name = data.t; + if (data.d) stats = Object.assign(parseEquipHTML(data.d) || {}, stats); + } } + const item = { id: itemId, name, stats, durability: durM ? `${durM[1]}/${durM[2]}` : '', potency: potM ? potM[1].trim() : '', source: 'modify', scrapedAt: Date.now() }; + if (itemId) { + const db = getGearDB(); + const idx = db.findIndex(e => e.id === itemId); + if (idx >= 0) Object.assign(db[idx], item); + else db.push(item); + saveGearDB(db); + } return item; } @@ -3983,7 +3805,6 @@ window.HV = { gearSummary: () => logGearSummary(), scrapeGear: () => autoScrapeGear(), gearDebug: () => debugScrapeGear(), - captureTooltips: () => captureAllTooltips(), }; // ═══════════════════════════════════════════════════════════════════════ @@ -4090,15 +3911,10 @@ function initializeBattle() { STATE.interruptHover = true; } - // Auto-scrape gear data on character/armory pages - if (window.location.href.includes('ss=eq')) { - // Character equipment page: trigger tooltip popups for full stats - setTimeout(() => { - triggerAllTooltips(); // populate popup_box for each item - setTimeout(autoScrapeGear, 300); // then scrape - }, 500); - } else if (window.location.href.includes('ss=ch') || window.location.href.includes('ss=am')) { - setTimeout(autoScrapeGear, 500); + // Auto-scrape gear data on character/armory pages (reads from HV's in-memory store) + const url = window.location.href || ''; + if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) { + setTimeout(autoScrapeGear, 300); } STATE.battleInitialized = true; diff --git a/scripts/latest.user.js b/scripts/latest.user.js index bad70d4..3a488c2 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.14.13 +// @version 0.14.14 // @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.13'; +const VERSION = '0.14.14'; const CFG = { // — Battle automation @@ -3244,61 +3244,26 @@ function enhanceArmory() { // ═══════════════════════════════════════════════════════════════════════ // GEAR SCRAPER — capture full equipment details for analysis // ═══════════════════════════════════════════════════════════════════════ -// Usage: -// 1. Visit Character Equipment page (?s=Character&ss=eq) — equipped gear -// 2. Visit Armory page (?s=Bazaar&ss=am) — inventory listing -// 3. Visit Modify page (?s=Bazaar&ss=am&screen=modify&eqids[]=ID) — full stats -// 4. Data saved to localStorage['hvunified_geardb'] -// 5. Query with: HV.gear() or HV.gearSummary() +// V2: Uses window.dynjs_equip / dynjs_eqstore directly from HV's JS +// memory store — no hover simulation or popup box delays needed. +// Every item's full tooltip HTML is pre-loaded on page load. const GEAR_DB_KEY = SP + 'geardb'; -// Forced scrape from console with diagnostics -function debugScrapeGear() { - const url = window.location.href || ''; - console.log(`%c[HV] 🔍 Gear Debug: URL=${url}`, 'color:#f80'); - - // Check for #eqsb - const eqsb = document.getElementById('eqsb'); - if (eqsb) { - const slots = eqsb.querySelectorAll(':scope > .eqb'); - console.log(`%c[HV] #eqsb found: ${slots.length} equipment slots`, 'color:#0f0'); - slots.forEach((s, i) => { - const omo = s.getAttribute('onmouseover') || ''; - // The onmouseover is on the inner name div, not the .eqb parent - const nameDiv = s.querySelector(':scope > div[onmouseover]'); - const innerOmo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : ''; - const name = nameDiv ? (nameDiv.textContent || '').trim() : 'no name'; - console.log(` [${i}] ${name} | onmouseover: ${(innerOmo || omo).substring(0, 80)}`); - }); - } else { - console.log(`%c[HV] ❌ #eqsb NOT FOUND`, 'color:#f44'); - } - - // Check for #equiplist - const equiplist = document.getElementById('equiplist'); - if (equiplist) { - const rows = equiplist.querySelectorAll('table tr'); - console.log(`%c[HV] #equiplist found: ${rows.length} rows`, 'color:#0f0'); - } else { - console.log(`%c[HV] ❌ #equiplist NOT FOUND`, 'color:#f44'); - } - - // Check popup_box - const popup = document.getElementById('popup_box'); - if (popup) { - const text = (popup.textContent || '').substring(0, 100); - console.log(`%c[HV] #popup_box: "${text}..."`, 'color:#888'); - } else { - console.log(`%c[HV] ❌ #popup_box NOT FOUND`, 'color:#f44'); - } - - // Check for equips global - console.log(`%c[HV] typeof equips = ${typeof equips}`, 'color:#888'); - - return { url, hasEqsb: !!eqsb, hasEquiplist: !!equiplist, hasPopup: !!popup }; +// ── Access HV's in-memory equipment store ── +function getHVEquipStore() { + const w = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; + if (w.dynjs_eqstore && Object.keys(w.dynjs_eqstore).length) return w.dynjs_eqstore; + if (w.dynjs_equip && Object.keys(w.dynjs_equip).length) return w.dynjs_equip; + return null; } +function getHVEquipData(itemId) { + const store = getHVEquipStore(); + return (store && store[itemId]) ? store[itemId] : null; +} + +// ── Public API ── function getGearDB() { try { return JSON.parse(localStorage[GEAR_DB_KEY] || '[]'); } catch (e) { return []; } } @@ -3311,378 +3276,235 @@ function logGearSummary() { const armory = db.filter(e => e.source === 'armory'); console.log(`%c[HV] 📦 Gear DB: ${db.length} total (${equipped.length} equipped, ${armory.length} armory)`, 'color:#0f0'); equipped.forEach(item => { - console.log(` 🗡 ${item.name} | ID: ${item.id} | ${item.slotType || ''}`); + console.log(` 🗡 [${item.slotType}] ${item.name} (ID: ${item.id}) — ${item.stats ? Object.keys(item.stats).length + ' stats' : 'no stats'}`); }); armory.forEach(item => { - console.log(` 📦 ${item.name} | ID: ${item.id} | ${item.category || ''}`); + console.log(` 📦 [${item.category}] ${item.name} (ID: ${item.id}) — ${item.stats ? Object.keys(item.stats).length + ' stats' : 'no stats'}`); }); } -// ── Parse the tooltip popup box for full item stats ── -// The #popup_box div appears on hover with a detailed stat breakdown: -//
-//
Light Armor & Level 115 & Tradeable
-//
Condition: 55% & Energy: N/A
-//
Burden: 7.1 & Interference: 2.1
-//
...
-//
Primary Attributes
-//
-function parseTooltipPopup() { - const popup = document.getElementById('popup_box'); - if (!popup) return null; - const text = popup.textContent || ''; - - // Extract item name (first div child text, before nested eq) - const nameDiv = popup.querySelector(':scope > div:first-child'); - const name = nameDiv ? (nameDiv.textContent || '').trim() : ''; - - const eq = popup.querySelector('.eq'); - if (!eq) return null; - - const stats = {}; - - // Type line: "Light Armor Level 115 Tradeable" - const eqt = eq.querySelector('.eqt'); - if (eqt) { - const eqtText = eqt.textContent || ''; - const typeMatch = eqtText.match(/^([A-Za-z\s]+?)(?:\s{2,}|$)/); - if (typeMatch) stats.type = typeMatch[1].trim(); - const lvMatch = eqtText.match(/Level\s*(\d+)/i); - if (lvMatch) stats.level = lvMatch[1]; - const tradeMatch = eqtText.match(/(Tradeable|Soulbound|Blessed)/); - if (tradeMatch) stats.bind = tradeMatch[1]; - } - - // Condition / Energy - const eqr = eq.querySelector('.eqr'); - if (eqr) { - const condMatch = (eqr.textContent || '').match(/Condition:\s*(\d+)%/i); - if (condMatch) stats.condition = condMatch[1]; - const engMatch = (eqr.textContent || '').match(/Energy:\s*([^\s]+)/i); - if (engMatch) stats.energy = engMatch[1]; - } - - // Burden / Interference - const eqc = eq.querySelector('.eqc'); - if (eqc) { - const burMatch = (eqc.textContent || '').match(/Burden:\s*([\d.]+)/i); - if (burMatch) stats.burden = burMatch[1]; - const intMatch = (eqc.textContent || '').match(/Interference:\s*([\d.]+)/i); - if (intMatch) stats.interference = intMatch[1]; - } - - // Stats from .ex (main stat block) - const ex = eq.querySelector('.ex'); - if (ex) { - ex.querySelectorAll(':scope > div').forEach(stat => { - const label = stat.querySelector(':scope > div:first-child'); - const value = stat.querySelector(':scope > div:nth-child(2) span, :scope > div:nth-child(2)'); - if (label && value) { - const key = (label.textContent || '').trim().replace(/[^A-Za-z\s]/g, '').trim(); - const val = (value.textContent || '').trim(); - if (key && val) stats[key] = val; +// ── Parse tooltip HTML string into structured stats ── +// HV stores the full .eq HTML in dynjs_equip[ID].d +function parseEquipHTML(htmlStr) { + if (!htmlStr) return null; + try { + const doc = new DOMParser().parseFromString(htmlStr, 'text/html'); + const eq = doc.querySelector('.eq'); + if (!eq) return null; + const stats = {}; + + // Header: type, level, binding + const eqt = eq.querySelector('.eqt'); + if (eqt) { + const t = eqt.textContent || ''; + const typeM = t.match(/^([A-Za-z\s]+?)(?:\s{2,}|$)/); + if (typeM) stats.type = typeM[1].trim(); + const lvM = t.match(/Level\s*(\d+|Unassigned)/i); + if (lvM) stats.level = lvM[1]; + const bindM = t.match(/(Tradeable|Soulbound|Blessed)/i); + if (bindM) stats.bind = bindM[1]; + } + + // Condition / Energy + const eqr = eq.querySelector('.eqr'); + if (eqr) { + const r = eqr.textContent || ''; + const cM = r.match(/Condition:\s*(\d+)%/i); + if (cM) stats.condition = parseInt(cM[1]); + const eM = r.match(/Energy:\s*([^\s]+)/i); + if (eM) stats.energy = eM[1]; + } + + // Burden / Interference + const eqc = eq.querySelector('.eqc'); + if (eqc) { + const c = eqc.textContent || ''; + const bM = c.match(/Burden:\s*([\d.]+)/i); + if (bM) stats.burden = parseFloat(bM[1]); + const iM = c.match(/Interference:\s*([\d.]+)/i); + if (iM) stats.interference = parseFloat(iM[1]); + } + + // Parse label/value pairs from stat divs + const parseRow = (div) => { + const label = div.querySelector(':scope > div:first-child'); + const val = div.querySelector(':scope > div:nth-child(2)'); + if (!label || !val) return; + const key = (label.textContent || '').trim(); + const raw = (val.textContent || '').trim(); + if (key && raw) { + stats[key] = raw; + const title = div.getAttribute('title') || ''; + const baseM = title.match(/Base:\s*(\d+)/i); + if (baseM) stats[key + ' Base'] = parseInt(baseM[1]); } + }; + + // Main stats (.ex) + const ex = eq.querySelector('.ex'); + if (ex) ex.querySelectorAll(':scope > div').forEach(parseRow); + + // Extra stat groups (.ep) + eq.querySelectorAll('.ep').forEach(g => { + g.querySelectorAll(':scope > div:not(:first-child)').forEach(parseRow); }); + + return stats; + } catch (e) { + return null; } - - // Extra stat groups (.ep) - eq.querySelectorAll('.ep').forEach(group => { - const groupTitle = group.querySelector(':scope > div:first-child'); - const groupName = groupTitle ? (groupTitle.textContent || '').trim() : ''; - group.querySelectorAll(':scope > div:not(:first-child)').forEach(stat => { - const label = stat.querySelector(':scope > div:first-child'); - const value = stat.querySelector(':scope > div:nth-child(2) span, :scope > div:nth-child(2)'); - if (label && value) { - const key = (label.textContent || '').trim().replace(/[^A-Za-z\s]/g, '').trim(); - const val = (value.textContent || '').trim(); - if (key && val) stats[key] = val; - } - }); - }); - - return { name, stats }; } -// ── Scrape Character Equipment page (?s=Character&ss=eq) ── -// DOM structure: -// #eqsb > .eqb (equipment slots) -// .eqb div: "Exquisite Estoc of Slaughter" (name) -// .eqb onmouseover="equips.set(ITEM_ID,'slot_pane',...)" (ID) -// #popup_box (tooltip with full stats, populated by hovering) +// ── Scrape Character Equipment page ── +const SLOT_TYPES = ['Mainhand', 'Offhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']; + function scrapeCharacterEquip() { const scraped = []; - - // Read each equipment slot in #eqsb const slots = document.querySelectorAll('#eqsb > .eqb'); - slots.forEach(slot => { - // onmouseover is on the inner name div, not the .eqb parent - const nameDiv = slot.querySelector(':scope > div[onmouseover]'); - const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : slot.getAttribute('onmouseover') || ''; - const idMatch = omo.match(/equips\.set\((\d+)/); - const itemId = idMatch ? idMatch[1] : ''; - - // Name is the text content of the name div - const name = nameDiv ? (nameDiv.textContent || '').trim() : ''; - - // Check if slot is disabled - const disabled = slot.classList.contains('eqdisabled'); - - // Determine slot type from parent structure - const slotType = ''; - - scraped.push({ - id: itemId, - name: name, - slotType: slotType, - disabled: disabled, - source: 'character', - scrapedAt: Date.now(), - }); - }); - - if (scraped.length > 0) { - const db = getGearDB(); - const filtered = db.filter(e => !(e.source === 'character')); - saveGearDB([...filtered, ...scraped]); - console.log(`%c[HV] Gear: scraped ${scraped.length} equipped items (${scraped.filter(s => s.stats).length} with tooltip stats)`, 'color:#0f0'); - } - return scraped; -} - -// ── Scrape Armory Organize page (?s=Bazaar&ss=am&screen=organize) ── -// DOM structure: -// #equiplist > table > tbody > tr -// tr.eqtplabel: category name -// tr[onmouseover]: equipment row with: -// onmouseover="hover_equip(ITEM_ID)" -// input[name="eqids[]"] value="ITEM_ID" -// label text: item name -// #equipinfo > .showequip > .eq: tooltip for selected item -function scrapeArmory() { - const scraped = []; - const equipList = document.getElementById('equiplist'); - if (!equipList) return scraped; - - let currentCategory = ''; - - const rows = equipList.querySelectorAll('table tr'); - rows.forEach(row => { - // Category header - if (row.classList.contains('eqtplabel')) { - currentCategory = (row.textContent || '').trim(); - return; - } - - // Skip select-all header row - if (row.classList.contains('eqselall')) return; - - // Item ID from onmouseover - const omo = row.getAttribute('onmouseover') || ''; - const idMatch = omo.match(/hover_equip\((\d+)\)/); - const itemId = idMatch ? idMatch[1] : ''; - - // Item name from label - const label = row.querySelector('label'); - let name = label ? (label.textContent || '').trim() : ''; - // Strip checkbox indicator - const cb = label ? label.querySelector('input') : null; - if (cb) { - name = (label.textContent || '').replace(cb.outerHTML, '').trim(); - } - - // Checkbox value as fallback ID - const checkbox = row.querySelector('input[name="eqids[]"]'); - const cbId = checkbox ? checkbox.value : ''; - - if (!name && !itemId) return; - - scraped.push({ - id: itemId || cbId, - name: name || 'Unknown', - category: currentCategory, - source: 'armory', - scrapedAt: Date.now(), - }); - }); - - // Read the right-side info pane for the currently-hovered item's stats - const equipInfo = document.getElementById('equipinfo'); - if (equipInfo) { - const showEquip = equipInfo.querySelector('.showequip'); - if (showEquip) { - const link = showEquip.querySelector('a'); - const infoName = link ? (link.textContent || '').trim() : ''; - const eq = showEquip.querySelector('.eq'); - if (eq) { - const infoText = eq.textContent || ''; - // Find matching item and attach raw stats - const matchIdx = scraped.findIndex(s => infoName.includes(s.name) || s.name.includes(infoName)); - if (matchIdx >= 0) { - scraped[matchIdx].infoText = infoText; - // Parse condition - const condMatch = infoText.match(/Condition:\s*(\d+)%/i); - if (condMatch) scraped[matchIdx].condition = parseInt(condMatch[1]); - } - } - } - } - - if (scraped.length > 0) { - const db = getGearDB(); - const filtered = db.filter(e => e.source !== 'armory'); - saveGearDB([...filtered, ...scraped]); - console.log(`%c[HV] Gear: scraped ${scraped.length} armory items`, 'color:#0f0'); - } - return scraped; -} - -// ── Trigger hover on each character slot to populate tooltip ── -// HV's equips.set(id, 'slot_pane', w, h) populates #popup_box with stats. -// We call it for each item in sequence to capture all tooltips. -function triggerAllTooltips() { - const slots = document.querySelectorAll('#eqsb > .eqb'); - const results = []; - - slots.forEach(slot => { - const nameDiv = slot.querySelector(':scope > div[onmouseover]'); - const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : slot.getAttribute('onmouseover') || ''; - const idMatch = omo.match(/equips\.set\((\d+)/); - if (idMatch && typeof equips !== 'undefined' && equips.set) { - try { - equips.set(parseInt(idMatch[1]), 'slot_pane', 250, 130); - } catch (e) {} - } - }); - - return results; -} - -// Manual: hover each item and capture tooltip stats with delays -// Run this from console on the character equipment page for full stats -function captureAllTooltips(delay = 400) { - const slots = document.querySelectorAll('#eqsb > .eqb'); - const results = []; - let idx = 0; - - function next() { - if (idx >= slots.length) { - console.log(`%c[HV] Captured ${results.length} tooltips`, 'color:#0f0'); - // DON'T re-scrape — that would wipe the tooltip stats we just saved - logGearSummary(); - return; - } - - const slot = slots[idx]; + slots.forEach((slot, i) => { const nameDiv = slot.querySelector(':scope > div[onmouseover]'); const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : ''; - const idMatch = omo.match(/equips\.set\((\d+)/); - - if (idMatch && typeof equips !== 'undefined' && equips.set) { - try { - equips.set(parseInt(idMatch[1]), 'slot_pane', 250, 130); - // Wait for popup to populate - setTimeout(() => { - const tooltip = parseTooltipPopup(); - if (tooltip && tooltip.name && tooltip.name !== 'Popup Box') { - // Store in localStorage directly - const db = getGearDB(); - const match = db.find(e => e.id === idMatch[1]); - if (match) { - match.stats = tooltip.stats; - saveGearDB(db); - } - results.push(tooltip); - console.log(`%c[HV] [${idx}] ${tooltip.name}`, 'color:#888'); - } - idx++; - next(); - }, 300); - return; - } catch (e) {} + const idM = omo.match(/equips\.set\((\d+)/); + const itemId = idM ? idM[1] : ''; + const name = nameDiv ? (nameDiv.textContent || '').trim() : ''; + const disabled = slot.classList.contains('eqdisabled'); + + const obj = { + id: itemId, name, disabled, + slotType: SLOT_TYPES[i] || '', + source: 'character', + scrapedAt: Date.now(), + }; + + // Pull full stats from HV's in-memory store — instant, no hover + if (itemId) { + const data = getHVEquipData(itemId); + if (data) { + if (data.t) obj.name = data.t; + if (data.q != null) obj.quality = data.q; + if (data.d) obj.stats = parseEquipHTML(data.d); + } } - idx++; - next(); + scraped.push(obj); + }); + + if (scraped.length > 0) { + const db = getGearDB(); + saveGearDB([...db.filter(e => e.source !== 'character'), ...scraped]); + console.log(`%c[HV] 🗡 Character: ${scraped.length} slots (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0'); } - - next(); - return 'Capturing tooltips... check console'; + return scraped; } -// ── Auto-detect and scrape current page ── +// ── Scrape Armory page ── +function scrapeArmory() { + const scraped = []; + const el = document.getElementById('equiplist'); + if (!el) return scraped; + let cat = ''; + + el.querySelectorAll('table tr').forEach(row => { + if (row.classList.contains('eqtplabel')) { cat = (row.textContent || '').trim(); return; } + if (row.classList.contains('eqselall')) return; + const omo = row.getAttribute('onmouseover') || ''; + const idM = omo.match(/hover_equip\((\d+)\)/); + const cb = row.querySelector('input[name="eqids[]"]'); + const itemId = idM ? idM[1] : (cb ? cb.value : ''); + const label = row.querySelector('label'); + let name = label ? (label.textContent || '').trim() : ''; + if (cb && label) name = (label.textContent || '').replace(cb.outerHTML, '').trim(); + if (!itemId && !name) return; + + const obj = { id: itemId, name: name || 'Unknown', category: cat, source: 'armory', scrapedAt: Date.now() }; + if (itemId) { + const data = getHVEquipData(itemId); + if (data) { + if (data.t) obj.name = data.t; + if (data.q != null) obj.quality = data.q; + if (data.d) obj.stats = parseEquipHTML(data.d); + } + } + scraped.push(obj); + }); + + if (scraped.length > 0) { + const db = getGearDB(); + saveGearDB([...db.filter(e => e.source !== 'armory'), ...scraped]); + console.log(`%c[HV] 📦 Armory: ${scraped.length} items (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0'); + } + return scraped; +} + +// ── Debug ── +function debugScrapeGear() { + const url = window.location.href || ''; + const store = getHVEquipStore(); + const storeCount = store ? Object.keys(store).length : 0; + const eqsb = document.getElementById('eqsb'); + const slots = eqsb ? eqsb.querySelectorAll(':scope > .eqb').length : 0; + const el = document.getElementById('equiplist'); + const rows = el ? el.querySelectorAll('table tr[onmouseover]').length : 0; + console.log(`%c[HV] 🔍 URL=${url} | store=${storeCount} items | eqsb=${slots} slots | equiplist=${rows} rows`, 'color:#f80'); + return { url, storeCount, slots, rows }; +} + +// ── Auto-detect ── function autoScrapeGear() { const url = window.location.href || ''; - - // Character equipment page - if (url.includes('ss=eq')) { - return scrapeCharacterEquip(); - } - - // Character summary page — try reading the slots - if (url.includes('ss=ch')) { - return scrapeCharacterEquip(); - } - - // Armory pages - if (url.includes('ss=am')) { - if (url.includes('screen=modify')) { - return scrapeModifyDetail(); - } - return scrapeArmory(); - } - + if (url.includes('ss=eq') || url.includes('ss=ch')) return scrapeCharacterEquip(); + if (url.includes('ss=am') && !url.includes('screen=modify')) return scrapeArmory(); + if (url.includes('ss=am') && url.includes('screen=modify')) return scrapeModifyDetail(); return []; } -// ── Scrape equipment modify detail page ── +// ── Modify detail page ── function scrapeModifyDetail() { const mainPane = document.getElementById('mainpane'); if (!mainPane) return null; - - // Try to get item ID from URL - const idMatch = window.location.href.match(/eqids?\[\]=(\d+)/); - const itemId = idMatch ? idMatch[1] : ''; - - // Item name from the page title area + const idM = window.location.href.match(/eqids?\[\]=(\d+)/); + const itemId = idM ? idM[1] : ''; const nameEl = mainPane.querySelector('h3, .itemname, .eqname'); - const name = nameEl ? (nameEl.textContent || '').trim() : ''; + let name = nameEl ? (nameEl.textContent || '').trim() : ''; + let stats = {}; - // Read all stat tables - const stats = {}; + // Parse right-side tooltip if present + const eqDiv = mainPane.querySelector('#equipmodify_right .eq, #equipinfo .eq'); + if (eqDiv) stats = parseEquipHTML(eqDiv.outerHTML) || {}; + + // Parse modify tables mainPane.querySelectorAll('table.gry, table.grl').forEach(tbl => { tbl.querySelectorAll('tr').forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length >= 2) { - const key = (cells[0].textContent || '').trim().replace(':', ''); - const val = (cells[1].textContent || '').trim(); - if (key && val) stats[key] = val; + const k = (cells[0].textContent || '').trim().replace(':', ''); + const v = (cells[1].textContent || '').trim(); + if (k && v) stats[k] = v; } }); }); const text = mainPane.textContent || ''; - const durMatch = text.match(/Durability:\s*(\d+)\s*\/\s*(\d+)/i); - const potMatch = text.match(/(?:Potency|Item World)\s*:\s*([^\n<]+)/i); + const durM = text.match(/Durability:\s*(\d+)\s*\/\s*(\d+)/i); + const potM = text.match(/(?:Potency|Item World)\s*:\s*([^\n<]+)/i); - const item = { - id: itemId, - name: name, - stats: stats, - durability: durMatch ? `${durMatch[1]}/${durMatch[2]}` : '', - potency: potMatch ? potMatch[1].trim() : '', - source: 'modify', - scrapedAt: Date.now(), - }; - - if (item.id) { - const db = getGearDB(); - const idx = db.findIndex(e => e.id === item.id && e.source === 'modify'); - if (idx >= 0) db[idx] = item; - else db.push(item); - saveGearDB(db); - console.log(`%c[HV] Gear: scraped modify detail for ${item.name || itemId}`, 'color:#0f0'); + // Enrich from memory + if (itemId) { + const data = getHVEquipData(itemId); + if (data) { + if (!name && data.t) name = data.t; + if (data.d) stats = Object.assign(parseEquipHTML(data.d) || {}, stats); + } } + const item = { id: itemId, name, stats, durability: durM ? `${durM[1]}/${durM[2]}` : '', potency: potM ? potM[1].trim() : '', source: 'modify', scrapedAt: Date.now() }; + if (itemId) { + const db = getGearDB(); + const idx = db.findIndex(e => e.id === itemId); + if (idx >= 0) Object.assign(db[idx], item); + else db.push(item); + saveGearDB(db); + } return item; } @@ -3983,7 +3805,6 @@ window.HV = { gearSummary: () => logGearSummary(), scrapeGear: () => autoScrapeGear(), gearDebug: () => debugScrapeGear(), - captureTooltips: () => captureAllTooltips(), }; // ═══════════════════════════════════════════════════════════════════════ @@ -4090,15 +3911,10 @@ function initializeBattle() { STATE.interruptHover = true; } - // Auto-scrape gear data on character/armory pages - if (window.location.href.includes('ss=eq')) { - // Character equipment page: trigger tooltip popups for full stats - setTimeout(() => { - triggerAllTooltips(); // populate popup_box for each item - setTimeout(autoScrapeGear, 300); // then scrape - }, 500); - } else if (window.location.href.includes('ss=ch') || window.location.href.includes('ss=am')) { - setTimeout(autoScrapeGear, 500); + // Auto-scrape gear data on character/armory pages (reads from HV's in-memory store) + const url = window.location.href || ''; + if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) { + setTimeout(autoScrapeGear, 300); } STATE.battleInitialized = true; diff --git a/src/config.js b/src/config.js index 35a7cea..f88d8b1 100644 --- a/src/config.js +++ b/src/config.js @@ -2,7 +2,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.14.13'; +const VERSION = '0.14.14'; const CFG = { // — Battle automation diff --git a/src/gear-scraper.js b/src/gear-scraper.js index 0419551..aefddd6 100644 --- a/src/gear-scraper.js +++ b/src/gear-scraper.js @@ -1,61 +1,26 @@ // ═══════════════════════════════════════════════════════════════════════ // GEAR SCRAPER — capture full equipment details for analysis // ═══════════════════════════════════════════════════════════════════════ -// Usage: -// 1. Visit Character Equipment page (?s=Character&ss=eq) — equipped gear -// 2. Visit Armory page (?s=Bazaar&ss=am) — inventory listing -// 3. Visit Modify page (?s=Bazaar&ss=am&screen=modify&eqids[]=ID) — full stats -// 4. Data saved to localStorage['hvunified_geardb'] -// 5. Query with: HV.gear() or HV.gearSummary() +// V2: Uses window.dynjs_equip / dynjs_eqstore directly from HV's JS +// memory store — no hover simulation or popup box delays needed. +// Every item's full tooltip HTML is pre-loaded on page load. const GEAR_DB_KEY = SP + 'geardb'; -// Forced scrape from console with diagnostics -function debugScrapeGear() { - const url = window.location.href || ''; - console.log(`%c[HV] 🔍 Gear Debug: URL=${url}`, 'color:#f80'); - - // Check for #eqsb - const eqsb = document.getElementById('eqsb'); - if (eqsb) { - const slots = eqsb.querySelectorAll(':scope > .eqb'); - console.log(`%c[HV] #eqsb found: ${slots.length} equipment slots`, 'color:#0f0'); - slots.forEach((s, i) => { - const omo = s.getAttribute('onmouseover') || ''; - // The onmouseover is on the inner name div, not the .eqb parent - const nameDiv = s.querySelector(':scope > div[onmouseover]'); - const innerOmo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : ''; - const name = nameDiv ? (nameDiv.textContent || '').trim() : 'no name'; - console.log(` [${i}] ${name} | onmouseover: ${(innerOmo || omo).substring(0, 80)}`); - }); - } else { - console.log(`%c[HV] ❌ #eqsb NOT FOUND`, 'color:#f44'); - } - - // Check for #equiplist - const equiplist = document.getElementById('equiplist'); - if (equiplist) { - const rows = equiplist.querySelectorAll('table tr'); - console.log(`%c[HV] #equiplist found: ${rows.length} rows`, 'color:#0f0'); - } else { - console.log(`%c[HV] ❌ #equiplist NOT FOUND`, 'color:#f44'); - } - - // Check popup_box - const popup = document.getElementById('popup_box'); - if (popup) { - const text = (popup.textContent || '').substring(0, 100); - console.log(`%c[HV] #popup_box: "${text}..."`, 'color:#888'); - } else { - console.log(`%c[HV] ❌ #popup_box NOT FOUND`, 'color:#f44'); - } - - // Check for equips global - console.log(`%c[HV] typeof equips = ${typeof equips}`, 'color:#888'); - - return { url, hasEqsb: !!eqsb, hasEquiplist: !!equiplist, hasPopup: !!popup }; +// ── Access HV's in-memory equipment store ── +function getHVEquipStore() { + const w = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; + if (w.dynjs_eqstore && Object.keys(w.dynjs_eqstore).length) return w.dynjs_eqstore; + if (w.dynjs_equip && Object.keys(w.dynjs_equip).length) return w.dynjs_equip; + return null; } +function getHVEquipData(itemId) { + const store = getHVEquipStore(); + return (store && store[itemId]) ? store[itemId] : null; +} + +// ── Public API ── function getGearDB() { try { return JSON.parse(localStorage[GEAR_DB_KEY] || '[]'); } catch (e) { return []; } } @@ -68,377 +33,234 @@ function logGearSummary() { const armory = db.filter(e => e.source === 'armory'); console.log(`%c[HV] 📦 Gear DB: ${db.length} total (${equipped.length} equipped, ${armory.length} armory)`, 'color:#0f0'); equipped.forEach(item => { - console.log(` 🗡 ${item.name} | ID: ${item.id} | ${item.slotType || ''}`); + console.log(` 🗡 [${item.slotType}] ${item.name} (ID: ${item.id}) — ${item.stats ? Object.keys(item.stats).length + ' stats' : 'no stats'}`); }); armory.forEach(item => { - console.log(` 📦 ${item.name} | ID: ${item.id} | ${item.category || ''}`); + console.log(` 📦 [${item.category}] ${item.name} (ID: ${item.id}) — ${item.stats ? Object.keys(item.stats).length + ' stats' : 'no stats'}`); }); } -// ── Parse the tooltip popup box for full item stats ── -// The #popup_box div appears on hover with a detailed stat breakdown: -//
-//
Light Armor & Level 115 & Tradeable
-//
Condition: 55% & Energy: N/A
-//
Burden: 7.1 & Interference: 2.1
-//
...
-//
Primary Attributes
-//
-function parseTooltipPopup() { - const popup = document.getElementById('popup_box'); - if (!popup) return null; - const text = popup.textContent || ''; - - // Extract item name (first div child text, before nested eq) - const nameDiv = popup.querySelector(':scope > div:first-child'); - const name = nameDiv ? (nameDiv.textContent || '').trim() : ''; - - const eq = popup.querySelector('.eq'); - if (!eq) return null; - - const stats = {}; - - // Type line: "Light Armor Level 115 Tradeable" - const eqt = eq.querySelector('.eqt'); - if (eqt) { - const eqtText = eqt.textContent || ''; - const typeMatch = eqtText.match(/^([A-Za-z\s]+?)(?:\s{2,}|$)/); - if (typeMatch) stats.type = typeMatch[1].trim(); - const lvMatch = eqtText.match(/Level\s*(\d+)/i); - if (lvMatch) stats.level = lvMatch[1]; - const tradeMatch = eqtText.match(/(Tradeable|Soulbound|Blessed)/); - if (tradeMatch) stats.bind = tradeMatch[1]; - } - - // Condition / Energy - const eqr = eq.querySelector('.eqr'); - if (eqr) { - const condMatch = (eqr.textContent || '').match(/Condition:\s*(\d+)%/i); - if (condMatch) stats.condition = condMatch[1]; - const engMatch = (eqr.textContent || '').match(/Energy:\s*([^\s]+)/i); - if (engMatch) stats.energy = engMatch[1]; - } - - // Burden / Interference - const eqc = eq.querySelector('.eqc'); - if (eqc) { - const burMatch = (eqc.textContent || '').match(/Burden:\s*([\d.]+)/i); - if (burMatch) stats.burden = burMatch[1]; - const intMatch = (eqc.textContent || '').match(/Interference:\s*([\d.]+)/i); - if (intMatch) stats.interference = intMatch[1]; - } - - // Stats from .ex (main stat block) - const ex = eq.querySelector('.ex'); - if (ex) { - ex.querySelectorAll(':scope > div').forEach(stat => { - const label = stat.querySelector(':scope > div:first-child'); - const value = stat.querySelector(':scope > div:nth-child(2) span, :scope > div:nth-child(2)'); - if (label && value) { - const key = (label.textContent || '').trim().replace(/[^A-Za-z\s]/g, '').trim(); - const val = (value.textContent || '').trim(); - if (key && val) stats[key] = val; +// ── Parse tooltip HTML string into structured stats ── +// HV stores the full .eq HTML in dynjs_equip[ID].d +function parseEquipHTML(htmlStr) { + if (!htmlStr) return null; + try { + const doc = new DOMParser().parseFromString(htmlStr, 'text/html'); + const eq = doc.querySelector('.eq'); + if (!eq) return null; + const stats = {}; + + // Header: type, level, binding + const eqt = eq.querySelector('.eqt'); + if (eqt) { + const t = eqt.textContent || ''; + const typeM = t.match(/^([A-Za-z\s]+?)(?:\s{2,}|$)/); + if (typeM) stats.type = typeM[1].trim(); + const lvM = t.match(/Level\s*(\d+|Unassigned)/i); + if (lvM) stats.level = lvM[1]; + const bindM = t.match(/(Tradeable|Soulbound|Blessed)/i); + if (bindM) stats.bind = bindM[1]; + } + + // Condition / Energy + const eqr = eq.querySelector('.eqr'); + if (eqr) { + const r = eqr.textContent || ''; + const cM = r.match(/Condition:\s*(\d+)%/i); + if (cM) stats.condition = parseInt(cM[1]); + const eM = r.match(/Energy:\s*([^\s]+)/i); + if (eM) stats.energy = eM[1]; + } + + // Burden / Interference + const eqc = eq.querySelector('.eqc'); + if (eqc) { + const c = eqc.textContent || ''; + const bM = c.match(/Burden:\s*([\d.]+)/i); + if (bM) stats.burden = parseFloat(bM[1]); + const iM = c.match(/Interference:\s*([\d.]+)/i); + if (iM) stats.interference = parseFloat(iM[1]); + } + + // Parse label/value pairs from stat divs + const parseRow = (div) => { + const label = div.querySelector(':scope > div:first-child'); + const val = div.querySelector(':scope > div:nth-child(2)'); + if (!label || !val) return; + const key = (label.textContent || '').trim(); + const raw = (val.textContent || '').trim(); + if (key && raw) { + stats[key] = raw; + const title = div.getAttribute('title') || ''; + const baseM = title.match(/Base:\s*(\d+)/i); + if (baseM) stats[key + ' Base'] = parseInt(baseM[1]); } + }; + + // Main stats (.ex) + const ex = eq.querySelector('.ex'); + if (ex) ex.querySelectorAll(':scope > div').forEach(parseRow); + + // Extra stat groups (.ep) + eq.querySelectorAll('.ep').forEach(g => { + g.querySelectorAll(':scope > div:not(:first-child)').forEach(parseRow); }); + + return stats; + } catch (e) { + return null; } - - // Extra stat groups (.ep) - eq.querySelectorAll('.ep').forEach(group => { - const groupTitle = group.querySelector(':scope > div:first-child'); - const groupName = groupTitle ? (groupTitle.textContent || '').trim() : ''; - group.querySelectorAll(':scope > div:not(:first-child)').forEach(stat => { - const label = stat.querySelector(':scope > div:first-child'); - const value = stat.querySelector(':scope > div:nth-child(2) span, :scope > div:nth-child(2)'); - if (label && value) { - const key = (label.textContent || '').trim().replace(/[^A-Za-z\s]/g, '').trim(); - const val = (value.textContent || '').trim(); - if (key && val) stats[key] = val; - } - }); - }); - - return { name, stats }; } -// ── Scrape Character Equipment page (?s=Character&ss=eq) ── -// DOM structure: -// #eqsb > .eqb (equipment slots) -// .eqb div: "Exquisite Estoc of Slaughter" (name) -// .eqb onmouseover="equips.set(ITEM_ID,'slot_pane',...)" (ID) -// #popup_box (tooltip with full stats, populated by hovering) +// ── Scrape Character Equipment page ── +const SLOT_TYPES = ['Mainhand', 'Offhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']; + function scrapeCharacterEquip() { const scraped = []; - - // Read each equipment slot in #eqsb const slots = document.querySelectorAll('#eqsb > .eqb'); - slots.forEach(slot => { - // onmouseover is on the inner name div, not the .eqb parent - const nameDiv = slot.querySelector(':scope > div[onmouseover]'); - const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : slot.getAttribute('onmouseover') || ''; - const idMatch = omo.match(/equips\.set\((\d+)/); - const itemId = idMatch ? idMatch[1] : ''; - - // Name is the text content of the name div - const name = nameDiv ? (nameDiv.textContent || '').trim() : ''; - - // Check if slot is disabled - const disabled = slot.classList.contains('eqdisabled'); - - // Determine slot type from parent structure - const slotType = ''; - - scraped.push({ - id: itemId, - name: name, - slotType: slotType, - disabled: disabled, - source: 'character', - scrapedAt: Date.now(), - }); - }); - - if (scraped.length > 0) { - const db = getGearDB(); - const filtered = db.filter(e => !(e.source === 'character')); - saveGearDB([...filtered, ...scraped]); - console.log(`%c[HV] Gear: scraped ${scraped.length} equipped items (${scraped.filter(s => s.stats).length} with tooltip stats)`, 'color:#0f0'); - } - return scraped; -} - -// ── Scrape Armory Organize page (?s=Bazaar&ss=am&screen=organize) ── -// DOM structure: -// #equiplist > table > tbody > tr -// tr.eqtplabel: category name -// tr[onmouseover]: equipment row with: -// onmouseover="hover_equip(ITEM_ID)" -// input[name="eqids[]"] value="ITEM_ID" -// label text: item name -// #equipinfo > .showequip > .eq: tooltip for selected item -function scrapeArmory() { - const scraped = []; - const equipList = document.getElementById('equiplist'); - if (!equipList) return scraped; - - let currentCategory = ''; - - const rows = equipList.querySelectorAll('table tr'); - rows.forEach(row => { - // Category header - if (row.classList.contains('eqtplabel')) { - currentCategory = (row.textContent || '').trim(); - return; - } - - // Skip select-all header row - if (row.classList.contains('eqselall')) return; - - // Item ID from onmouseover - const omo = row.getAttribute('onmouseover') || ''; - const idMatch = omo.match(/hover_equip\((\d+)\)/); - const itemId = idMatch ? idMatch[1] : ''; - - // Item name from label - const label = row.querySelector('label'); - let name = label ? (label.textContent || '').trim() : ''; - // Strip checkbox indicator - const cb = label ? label.querySelector('input') : null; - if (cb) { - name = (label.textContent || '').replace(cb.outerHTML, '').trim(); - } - - // Checkbox value as fallback ID - const checkbox = row.querySelector('input[name="eqids[]"]'); - const cbId = checkbox ? checkbox.value : ''; - - if (!name && !itemId) return; - - scraped.push({ - id: itemId || cbId, - name: name || 'Unknown', - category: currentCategory, - source: 'armory', - scrapedAt: Date.now(), - }); - }); - - // Read the right-side info pane for the currently-hovered item's stats - const equipInfo = document.getElementById('equipinfo'); - if (equipInfo) { - const showEquip = equipInfo.querySelector('.showequip'); - if (showEquip) { - const link = showEquip.querySelector('a'); - const infoName = link ? (link.textContent || '').trim() : ''; - const eq = showEquip.querySelector('.eq'); - if (eq) { - const infoText = eq.textContent || ''; - // Find matching item and attach raw stats - const matchIdx = scraped.findIndex(s => infoName.includes(s.name) || s.name.includes(infoName)); - if (matchIdx >= 0) { - scraped[matchIdx].infoText = infoText; - // Parse condition - const condMatch = infoText.match(/Condition:\s*(\d+)%/i); - if (condMatch) scraped[matchIdx].condition = parseInt(condMatch[1]); - } - } - } - } - - if (scraped.length > 0) { - const db = getGearDB(); - const filtered = db.filter(e => e.source !== 'armory'); - saveGearDB([...filtered, ...scraped]); - console.log(`%c[HV] Gear: scraped ${scraped.length} armory items`, 'color:#0f0'); - } - return scraped; -} - -// ── Trigger hover on each character slot to populate tooltip ── -// HV's equips.set(id, 'slot_pane', w, h) populates #popup_box with stats. -// We call it for each item in sequence to capture all tooltips. -function triggerAllTooltips() { - const slots = document.querySelectorAll('#eqsb > .eqb'); - const results = []; - - slots.forEach(slot => { - const nameDiv = slot.querySelector(':scope > div[onmouseover]'); - const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : slot.getAttribute('onmouseover') || ''; - const idMatch = omo.match(/equips\.set\((\d+)/); - if (idMatch && typeof equips !== 'undefined' && equips.set) { - try { - equips.set(parseInt(idMatch[1]), 'slot_pane', 250, 130); - } catch (e) {} - } - }); - - return results; -} - -// Manual: hover each item and capture tooltip stats with delays -// Run this from console on the character equipment page for full stats -function captureAllTooltips(delay = 400) { - const slots = document.querySelectorAll('#eqsb > .eqb'); - const results = []; - let idx = 0; - - function next() { - if (idx >= slots.length) { - console.log(`%c[HV] Captured ${results.length} tooltips`, 'color:#0f0'); - // DON'T re-scrape — that would wipe the tooltip stats we just saved - logGearSummary(); - return; - } - - const slot = slots[idx]; + slots.forEach((slot, i) => { const nameDiv = slot.querySelector(':scope > div[onmouseover]'); const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : ''; - const idMatch = omo.match(/equips\.set\((\d+)/); - - if (idMatch && typeof equips !== 'undefined' && equips.set) { - try { - equips.set(parseInt(idMatch[1]), 'slot_pane', 250, 130); - // Wait for popup to populate - setTimeout(() => { - const tooltip = parseTooltipPopup(); - if (tooltip && tooltip.name && tooltip.name !== 'Popup Box') { - // Store in localStorage directly - const db = getGearDB(); - const match = db.find(e => e.id === idMatch[1]); - if (match) { - match.stats = tooltip.stats; - saveGearDB(db); - } - results.push(tooltip); - console.log(`%c[HV] [${idx}] ${tooltip.name}`, 'color:#888'); - } - idx++; - next(); - }, 300); - return; - } catch (e) {} + const idM = omo.match(/equips\.set\((\d+)/); + const itemId = idM ? idM[1] : ''; + const name = nameDiv ? (nameDiv.textContent || '').trim() : ''; + const disabled = slot.classList.contains('eqdisabled'); + + const obj = { + id: itemId, name, disabled, + slotType: SLOT_TYPES[i] || '', + source: 'character', + scrapedAt: Date.now(), + }; + + // Pull full stats from HV's in-memory store — instant, no hover + if (itemId) { + const data = getHVEquipData(itemId); + if (data) { + if (data.t) obj.name = data.t; + if (data.q != null) obj.quality = data.q; + if (data.d) obj.stats = parseEquipHTML(data.d); + } } - idx++; - next(); + scraped.push(obj); + }); + + if (scraped.length > 0) { + const db = getGearDB(); + saveGearDB([...db.filter(e => e.source !== 'character'), ...scraped]); + console.log(`%c[HV] 🗡 Character: ${scraped.length} slots (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0'); } - - next(); - return 'Capturing tooltips... check console'; + return scraped; } -// ── Auto-detect and scrape current page ── +// ── Scrape Armory page ── +function scrapeArmory() { + const scraped = []; + const el = document.getElementById('equiplist'); + if (!el) return scraped; + let cat = ''; + + el.querySelectorAll('table tr').forEach(row => { + if (row.classList.contains('eqtplabel')) { cat = (row.textContent || '').trim(); return; } + if (row.classList.contains('eqselall')) return; + const omo = row.getAttribute('onmouseover') || ''; + const idM = omo.match(/hover_equip\((\d+)\)/); + const cb = row.querySelector('input[name="eqids[]"]'); + const itemId = idM ? idM[1] : (cb ? cb.value : ''); + const label = row.querySelector('label'); + let name = label ? (label.textContent || '').trim() : ''; + if (cb && label) name = (label.textContent || '').replace(cb.outerHTML, '').trim(); + if (!itemId && !name) return; + + const obj = { id: itemId, name: name || 'Unknown', category: cat, source: 'armory', scrapedAt: Date.now() }; + if (itemId) { + const data = getHVEquipData(itemId); + if (data) { + if (data.t) obj.name = data.t; + if (data.q != null) obj.quality = data.q; + if (data.d) obj.stats = parseEquipHTML(data.d); + } + } + scraped.push(obj); + }); + + if (scraped.length > 0) { + const db = getGearDB(); + saveGearDB([...db.filter(e => e.source !== 'armory'), ...scraped]); + console.log(`%c[HV] 📦 Armory: ${scraped.length} items (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0'); + } + return scraped; +} + +// ── Debug ── +function debugScrapeGear() { + const url = window.location.href || ''; + const store = getHVEquipStore(); + const storeCount = store ? Object.keys(store).length : 0; + const eqsb = document.getElementById('eqsb'); + const slots = eqsb ? eqsb.querySelectorAll(':scope > .eqb').length : 0; + const el = document.getElementById('equiplist'); + const rows = el ? el.querySelectorAll('table tr[onmouseover]').length : 0; + console.log(`%c[HV] 🔍 URL=${url} | store=${storeCount} items | eqsb=${slots} slots | equiplist=${rows} rows`, 'color:#f80'); + return { url, storeCount, slots, rows }; +} + +// ── Auto-detect ── function autoScrapeGear() { const url = window.location.href || ''; - - // Character equipment page - if (url.includes('ss=eq')) { - return scrapeCharacterEquip(); - } - - // Character summary page — try reading the slots - if (url.includes('ss=ch')) { - return scrapeCharacterEquip(); - } - - // Armory pages - if (url.includes('ss=am')) { - if (url.includes('screen=modify')) { - return scrapeModifyDetail(); - } - return scrapeArmory(); - } - + if (url.includes('ss=eq') || url.includes('ss=ch')) return scrapeCharacterEquip(); + if (url.includes('ss=am') && !url.includes('screen=modify')) return scrapeArmory(); + if (url.includes('ss=am') && url.includes('screen=modify')) return scrapeModifyDetail(); return []; } -// ── Scrape equipment modify detail page ── +// ── Modify detail page ── function scrapeModifyDetail() { const mainPane = document.getElementById('mainpane'); if (!mainPane) return null; - - // Try to get item ID from URL - const idMatch = window.location.href.match(/eqids?\[\]=(\d+)/); - const itemId = idMatch ? idMatch[1] : ''; - - // Item name from the page title area + const idM = window.location.href.match(/eqids?\[\]=(\d+)/); + const itemId = idM ? idM[1] : ''; const nameEl = mainPane.querySelector('h3, .itemname, .eqname'); - const name = nameEl ? (nameEl.textContent || '').trim() : ''; + let name = nameEl ? (nameEl.textContent || '').trim() : ''; + let stats = {}; - // Read all stat tables - const stats = {}; + // Parse right-side tooltip if present + const eqDiv = mainPane.querySelector('#equipmodify_right .eq, #equipinfo .eq'); + if (eqDiv) stats = parseEquipHTML(eqDiv.outerHTML) || {}; + + // Parse modify tables mainPane.querySelectorAll('table.gry, table.grl').forEach(tbl => { tbl.querySelectorAll('tr').forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length >= 2) { - const key = (cells[0].textContent || '').trim().replace(':', ''); - const val = (cells[1].textContent || '').trim(); - if (key && val) stats[key] = val; + const k = (cells[0].textContent || '').trim().replace(':', ''); + const v = (cells[1].textContent || '').trim(); + if (k && v) stats[k] = v; } }); }); const text = mainPane.textContent || ''; - const durMatch = text.match(/Durability:\s*(\d+)\s*\/\s*(\d+)/i); - const potMatch = text.match(/(?:Potency|Item World)\s*:\s*([^\n<]+)/i); + const durM = text.match(/Durability:\s*(\d+)\s*\/\s*(\d+)/i); + const potM = text.match(/(?:Potency|Item World)\s*:\s*([^\n<]+)/i); - const item = { - id: itemId, - name: name, - stats: stats, - durability: durMatch ? `${durMatch[1]}/${durMatch[2]}` : '', - potency: potMatch ? potMatch[1].trim() : '', - source: 'modify', - scrapedAt: Date.now(), - }; - - if (item.id) { - const db = getGearDB(); - const idx = db.findIndex(e => e.id === item.id && e.source === 'modify'); - if (idx >= 0) db[idx] = item; - else db.push(item); - saveGearDB(db); - console.log(`%c[HV] Gear: scraped modify detail for ${item.name || itemId}`, 'color:#0f0'); + // Enrich from memory + if (itemId) { + const data = getHVEquipData(itemId); + if (data) { + if (!name && data.t) name = data.t; + if (data.d) stats = Object.assign(parseEquipHTML(data.d) || {}, stats); + } } + const item = { id: itemId, name, stats, durability: durM ? `${durM[1]}/${durM[2]}` : '', potency: potM ? potM[1].trim() : '', source: 'modify', scrapedAt: Date.now() }; + if (itemId) { + const db = getGearDB(); + const idx = db.findIndex(e => e.id === itemId); + if (idx >= 0) Object.assign(db[idx], item); + else db.push(item); + saveGearDB(db); + } return item; } diff --git a/src/header.user.js b/src/header.user.js index f63a798..ab74ed5 100644 --- a/src/header.user.js +++ b/src/header.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.14.13 +// @version 0.14.14 // @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 f9ecd04..1d8a2d5 100644 --- a/src/init.js +++ b/src/init.js @@ -102,15 +102,10 @@ function initializeBattle() { STATE.interruptHover = true; } - // Auto-scrape gear data on character/armory pages - if (window.location.href.includes('ss=eq')) { - // Character equipment page: trigger tooltip popups for full stats - setTimeout(() => { - triggerAllTooltips(); // populate popup_box for each item - setTimeout(autoScrapeGear, 300); // then scrape - }, 500); - } else if (window.location.href.includes('ss=ch') || window.location.href.includes('ss=am')) { - setTimeout(autoScrapeGear, 500); + // Auto-scrape gear data on character/armory pages (reads from HV's in-memory store) + const url = window.location.href || ''; + if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) { + setTimeout(autoScrapeGear, 300); } STATE.battleInitialized = true; diff --git a/src/public-api.js b/src/public-api.js index 46fd81f..fd99792 100644 --- a/src/public-api.js +++ b/src/public-api.js @@ -25,5 +25,4 @@ window.HV = { gearSummary: () => logGearSummary(), scrapeGear: () => autoScrapeGear(), gearDebug: () => debugScrapeGear(), - captureTooltips: () => captureAllTooltips(), };