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(),
};