v0.14.14 - V2 gear scraper: reads from HV in-memory JS store (no hovers)

- 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
This commit is contained in:
GaboGG 2026-07-27 15:46:36 -04:00
parent 28457202ba
commit 329e9403b4
7 changed files with 624 additions and 1176 deletions

View file

@ -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:
// <div class="eq">
// <div class="eqt">Light Armor & Level 115 & Tradeable</div>
// <div class="eqr">Condition: 55% & Energy: N/A</div>
// <div class="eqc">Burden: 7.1 & Interference: 2.1</div>
// <div class="ex">...</div>
// <div class="ep ep3">Primary Attributes</div>
// </div>
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;

View file

@ -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:
// <div class="eq">
// <div class="eqt">Light Armor & Level 115 & Tradeable</div>
// <div class="eqr">Condition: 55% & Energy: N/A</div>
// <div class="eqc">Burden: 7.1 & Interference: 2.1</div>
// <div class="ex">...</div>
// <div class="ep ep3">Primary Attributes</div>
// </div>
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;

View file

@ -2,7 +2,7 @@
// CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.14.13';
const VERSION = '0.14.14';
const CFG = {
// — Battle automation

View file

@ -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:
// <div class="eq">
// <div class="eqt">Light Armor & Level 115 & Tradeable</div>
// <div class="eqr">Condition: 55% & Energy: N/A</div>
// <div class="eqc">Burden: 7.1 & Interference: 2.1</div>
// <div class="ex">...</div>
// <div class="ep ep3">Primary Attributes</div>
// </div>
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;
}

View file

@ -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/*

View file

@ -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;

View file

@ -25,5 +25,4 @@ window.HV = {
gearSummary: () => logGearSummary(),
scrapeGear: () => autoScrapeGear(),
gearDebug: () => debugScrapeGear(),
captureTooltips: () => captureAllTooltips(),
};