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:
parent
28457202ba
commit
329e9403b4
7 changed files with 624 additions and 1176 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.14.13
|
// @version 0.14.14
|
||||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||||
// @author GaboGG + Hermes
|
// @author GaboGG + Hermes
|
||||||
// @match *://*.hentaiverse.org/*
|
// @match *://*.hentaiverse.org/*
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.14.13';
|
const VERSION = '0.14.14';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -3244,61 +3244,26 @@ function enhanceArmory() {
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// GEAR SCRAPER — capture full equipment details for analysis
|
// GEAR SCRAPER — capture full equipment details for analysis
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// Usage:
|
// V2: Uses window.dynjs_equip / dynjs_eqstore directly from HV's JS
|
||||||
// 1. Visit Character Equipment page (?s=Character&ss=eq) — equipped gear
|
// memory store — no hover simulation or popup box delays needed.
|
||||||
// 2. Visit Armory page (?s=Bazaar&ss=am) — inventory listing
|
// Every item's full tooltip HTML is pre-loaded on page load.
|
||||||
// 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()
|
|
||||||
|
|
||||||
const GEAR_DB_KEY = SP + 'geardb';
|
const GEAR_DB_KEY = SP + 'geardb';
|
||||||
|
|
||||||
// Forced scrape from console with diagnostics
|
// ── Access HV's in-memory equipment store ──
|
||||||
function debugScrapeGear() {
|
function getHVEquipStore() {
|
||||||
const url = window.location.href || '';
|
const w = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
|
||||||
console.log(`%c[HV] 🔍 Gear Debug: URL=${url}`, 'color:#f80');
|
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;
|
||||||
// Check for #eqsb
|
return null;
|
||||||
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
|
function getHVEquipData(itemId) {
|
||||||
const equiplist = document.getElementById('equiplist');
|
const store = getHVEquipStore();
|
||||||
if (equiplist) {
|
return (store && store[itemId]) ? store[itemId] : null;
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Public API ──
|
||||||
function getGearDB() {
|
function getGearDB() {
|
||||||
try { return JSON.parse(localStorage[GEAR_DB_KEY] || '[]'); } catch (e) { return []; }
|
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');
|
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');
|
console.log(`%c[HV] 📦 Gear DB: ${db.length} total (${equipped.length} equipped, ${armory.length} armory)`, 'color:#0f0');
|
||||||
equipped.forEach(item => {
|
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 => {
|
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 ──
|
// ── Parse tooltip HTML string into structured stats ──
|
||||||
// The #popup_box div appears on hover with a detailed stat breakdown:
|
// HV stores the full .eq HTML in dynjs_equip[ID].d
|
||||||
// <div class="eq">
|
function parseEquipHTML(htmlStr) {
|
||||||
// <div class="eqt">Light Armor & Level 115 & Tradeable</div>
|
if (!htmlStr) return null;
|
||||||
// <div class="eqr">Condition: 55% & Energy: N/A</div>
|
try {
|
||||||
// <div class="eqc">Burden: 7.1 & Interference: 2.1</div>
|
const doc = new DOMParser().parseFromString(htmlStr, 'text/html');
|
||||||
// <div class="ex">...</div>
|
const eq = doc.querySelector('.eq');
|
||||||
// <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;
|
if (!eq) return null;
|
||||||
|
|
||||||
const stats = {};
|
const stats = {};
|
||||||
|
|
||||||
// Type line: "Light Armor Level 115 Tradeable"
|
// Header: type, level, binding
|
||||||
const eqt = eq.querySelector('.eqt');
|
const eqt = eq.querySelector('.eqt');
|
||||||
if (eqt) {
|
if (eqt) {
|
||||||
const eqtText = eqt.textContent || '';
|
const t = eqt.textContent || '';
|
||||||
const typeMatch = eqtText.match(/^([A-Za-z\s]+?)(?:\s{2,}|$)/);
|
const typeM = t.match(/^([A-Za-z\s]+?)(?:\s{2,}|$)/);
|
||||||
if (typeMatch) stats.type = typeMatch[1].trim();
|
if (typeM) stats.type = typeM[1].trim();
|
||||||
const lvMatch = eqtText.match(/Level\s*(\d+)/i);
|
const lvM = t.match(/Level\s*(\d+|Unassigned)/i);
|
||||||
if (lvMatch) stats.level = lvMatch[1];
|
if (lvM) stats.level = lvM[1];
|
||||||
const tradeMatch = eqtText.match(/(Tradeable|Soulbound|Blessed)/);
|
const bindM = t.match(/(Tradeable|Soulbound|Blessed)/i);
|
||||||
if (tradeMatch) stats.bind = tradeMatch[1];
|
if (bindM) stats.bind = bindM[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Condition / Energy
|
// Condition / Energy
|
||||||
const eqr = eq.querySelector('.eqr');
|
const eqr = eq.querySelector('.eqr');
|
||||||
if (eqr) {
|
if (eqr) {
|
||||||
const condMatch = (eqr.textContent || '').match(/Condition:\s*(\d+)%/i);
|
const r = eqr.textContent || '';
|
||||||
if (condMatch) stats.condition = condMatch[1];
|
const cM = r.match(/Condition:\s*(\d+)%/i);
|
||||||
const engMatch = (eqr.textContent || '').match(/Energy:\s*([^\s]+)/i);
|
if (cM) stats.condition = parseInt(cM[1]);
|
||||||
if (engMatch) stats.energy = engMatch[1];
|
const eM = r.match(/Energy:\s*([^\s]+)/i);
|
||||||
|
if (eM) stats.energy = eM[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Burden / Interference
|
// Burden / Interference
|
||||||
const eqc = eq.querySelector('.eqc');
|
const eqc = eq.querySelector('.eqc');
|
||||||
if (eqc) {
|
if (eqc) {
|
||||||
const burMatch = (eqc.textContent || '').match(/Burden:\s*([\d.]+)/i);
|
const c = eqc.textContent || '';
|
||||||
if (burMatch) stats.burden = burMatch[1];
|
const bM = c.match(/Burden:\s*([\d.]+)/i);
|
||||||
const intMatch = (eqc.textContent || '').match(/Interference:\s*([\d.]+)/i);
|
if (bM) stats.burden = parseFloat(bM[1]);
|
||||||
if (intMatch) stats.interference = intMatch[1];
|
const iM = c.match(/Interference:\s*([\d.]+)/i);
|
||||||
|
if (iM) stats.interference = parseFloat(iM[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats from .ex (main stat block)
|
// 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');
|
const ex = eq.querySelector('.ex');
|
||||||
if (ex) {
|
if (ex) ex.querySelectorAll(':scope > div').forEach(parseRow);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extra stat groups (.ep)
|
// Extra stat groups (.ep)
|
||||||
eq.querySelectorAll('.ep').forEach(group => {
|
eq.querySelectorAll('.ep').forEach(g => {
|
||||||
const groupTitle = group.querySelector(':scope > div:first-child');
|
g.querySelectorAll(':scope > div:not(:first-child)').forEach(parseRow);
|
||||||
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 };
|
return stats;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scrape Character Equipment page (?s=Character&ss=eq) ──
|
// ── Scrape Character Equipment page ──
|
||||||
// DOM structure:
|
const SLOT_TYPES = ['Mainhand', 'Offhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet'];
|
||||||
// #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)
|
|
||||||
function scrapeCharacterEquip() {
|
function scrapeCharacterEquip() {
|
||||||
const scraped = [];
|
const scraped = [];
|
||||||
|
|
||||||
// Read each equipment slot in #eqsb
|
|
||||||
const slots = document.querySelectorAll('#eqsb > .eqb');
|
const slots = document.querySelectorAll('#eqsb > .eqb');
|
||||||
|
|
||||||
slots.forEach(slot => {
|
slots.forEach((slot, i) => {
|
||||||
// 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];
|
|
||||||
const nameDiv = slot.querySelector(':scope > div[onmouseover]');
|
const nameDiv = slot.querySelector(':scope > div[onmouseover]');
|
||||||
const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : '';
|
const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : '';
|
||||||
const idMatch = omo.match(/equips\.set\((\d+)/);
|
const idM = omo.match(/equips\.set\((\d+)/);
|
||||||
|
const itemId = idM ? idM[1] : '';
|
||||||
|
const name = nameDiv ? (nameDiv.textContent || '').trim() : '';
|
||||||
|
const disabled = slot.classList.contains('eqdisabled');
|
||||||
|
|
||||||
if (idMatch && typeof equips !== 'undefined' && equips.set) {
|
const obj = {
|
||||||
try {
|
id: itemId, name, disabled,
|
||||||
equips.set(parseInt(idMatch[1]), 'slot_pane', 250, 130);
|
slotType: SLOT_TYPES[i] || '',
|
||||||
// Wait for popup to populate
|
source: 'character',
|
||||||
setTimeout(() => {
|
scrapedAt: Date.now(),
|
||||||
const tooltip = parseTooltipPopup();
|
};
|
||||||
if (tooltip && tooltip.name && tooltip.name !== 'Popup Box') {
|
|
||||||
// Store in localStorage directly
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scraped.push(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scraped.length > 0) {
|
||||||
const db = getGearDB();
|
const db = getGearDB();
|
||||||
const match = db.find(e => e.id === idMatch[1]);
|
saveGearDB([...db.filter(e => e.source !== 'character'), ...scraped]);
|
||||||
if (match) {
|
console.log(`%c[HV] 🗡 Character: ${scraped.length} slots (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0');
|
||||||
match.stats = tooltip.stats;
|
|
||||||
saveGearDB(db);
|
|
||||||
}
|
}
|
||||||
results.push(tooltip);
|
return scraped;
|
||||||
console.log(`%c[HV] [${idx}] ${tooltip.name}`, 'color:#888');
|
|
||||||
}
|
|
||||||
idx++;
|
|
||||||
next();
|
|
||||||
}, 300);
|
|
||||||
return;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
idx++;
|
|
||||||
next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
// ── Scrape Armory page ──
|
||||||
return 'Capturing tooltips... check console';
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Auto-detect and scrape current page ──
|
// ── 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() {
|
function autoScrapeGear() {
|
||||||
const url = window.location.href || '';
|
const url = window.location.href || '';
|
||||||
|
if (url.includes('ss=eq') || url.includes('ss=ch')) return scrapeCharacterEquip();
|
||||||
// Character equipment page
|
if (url.includes('ss=am') && !url.includes('screen=modify')) return scrapeArmory();
|
||||||
if (url.includes('ss=eq')) {
|
if (url.includes('ss=am') && url.includes('screen=modify')) return scrapeModifyDetail();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scrape equipment modify detail page ──
|
// ── Modify detail page ──
|
||||||
function scrapeModifyDetail() {
|
function scrapeModifyDetail() {
|
||||||
const mainPane = document.getElementById('mainpane');
|
const mainPane = document.getElementById('mainpane');
|
||||||
if (!mainPane) return null;
|
if (!mainPane) return null;
|
||||||
|
const idM = window.location.href.match(/eqids?\[\]=(\d+)/);
|
||||||
// Try to get item ID from URL
|
const itemId = idM ? idM[1] : '';
|
||||||
const idMatch = window.location.href.match(/eqids?\[\]=(\d+)/);
|
|
||||||
const itemId = idMatch ? idMatch[1] : '';
|
|
||||||
|
|
||||||
// Item name from the page title area
|
|
||||||
const nameEl = mainPane.querySelector('h3, .itemname, .eqname');
|
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
|
// Parse right-side tooltip if present
|
||||||
const stats = {};
|
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 => {
|
mainPane.querySelectorAll('table.gry, table.grl').forEach(tbl => {
|
||||||
tbl.querySelectorAll('tr').forEach(row => {
|
tbl.querySelectorAll('tr').forEach(row => {
|
||||||
const cells = row.querySelectorAll('td');
|
const cells = row.querySelectorAll('td');
|
||||||
if (cells.length >= 2) {
|
if (cells.length >= 2) {
|
||||||
const key = (cells[0].textContent || '').trim().replace(':', '');
|
const k = (cells[0].textContent || '').trim().replace(':', '');
|
||||||
const val = (cells[1].textContent || '').trim();
|
const v = (cells[1].textContent || '').trim();
|
||||||
if (key && val) stats[key] = val;
|
if (k && v) stats[k] = v;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = mainPane.textContent || '';
|
const text = mainPane.textContent || '';
|
||||||
const durMatch = text.match(/Durability:\s*(\d+)\s*\/\s*(\d+)/i);
|
const durM = text.match(/Durability:\s*(\d+)\s*\/\s*(\d+)/i);
|
||||||
const potMatch = text.match(/(?:Potency|Item World)\s*:\s*([^\n<]+)/i);
|
const potM = text.match(/(?:Potency|Item World)\s*:\s*([^\n<]+)/i);
|
||||||
|
|
||||||
const item = {
|
// Enrich from memory
|
||||||
id: itemId,
|
if (itemId) {
|
||||||
name: name,
|
const data = getHVEquipData(itemId);
|
||||||
stats: stats,
|
if (data) {
|
||||||
durability: durMatch ? `${durMatch[1]}/${durMatch[2]}` : '',
|
if (!name && data.t) name = data.t;
|
||||||
potency: potMatch ? potMatch[1].trim() : '',
|
if (data.d) stats = Object.assign(parseEquipHTML(data.d) || {}, stats);
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3983,7 +3805,6 @@ window.HV = {
|
||||||
gearSummary: () => logGearSummary(),
|
gearSummary: () => logGearSummary(),
|
||||||
scrapeGear: () => autoScrapeGear(),
|
scrapeGear: () => autoScrapeGear(),
|
||||||
gearDebug: () => debugScrapeGear(),
|
gearDebug: () => debugScrapeGear(),
|
||||||
captureTooltips: () => captureAllTooltips(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -4090,15 +3911,10 @@ function initializeBattle() {
|
||||||
STATE.interruptHover = true;
|
STATE.interruptHover = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-scrape gear data on character/armory pages
|
// Auto-scrape gear data on character/armory pages (reads from HV's in-memory store)
|
||||||
if (window.location.href.includes('ss=eq')) {
|
const url = window.location.href || '';
|
||||||
// Character equipment page: trigger tooltip popups for full stats
|
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
|
||||||
setTimeout(() => {
|
setTimeout(autoScrapeGear, 300);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
STATE.battleInitialized = true;
|
STATE.battleInitialized = true;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.14.13
|
// @version 0.14.14
|
||||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||||
// @author GaboGG + Hermes
|
// @author GaboGG + Hermes
|
||||||
// @match *://*.hentaiverse.org/*
|
// @match *://*.hentaiverse.org/*
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.14.13';
|
const VERSION = '0.14.14';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -3244,61 +3244,26 @@ function enhanceArmory() {
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// GEAR SCRAPER — capture full equipment details for analysis
|
// GEAR SCRAPER — capture full equipment details for analysis
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// Usage:
|
// V2: Uses window.dynjs_equip / dynjs_eqstore directly from HV's JS
|
||||||
// 1. Visit Character Equipment page (?s=Character&ss=eq) — equipped gear
|
// memory store — no hover simulation or popup box delays needed.
|
||||||
// 2. Visit Armory page (?s=Bazaar&ss=am) — inventory listing
|
// Every item's full tooltip HTML is pre-loaded on page load.
|
||||||
// 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()
|
|
||||||
|
|
||||||
const GEAR_DB_KEY = SP + 'geardb';
|
const GEAR_DB_KEY = SP + 'geardb';
|
||||||
|
|
||||||
// Forced scrape from console with diagnostics
|
// ── Access HV's in-memory equipment store ──
|
||||||
function debugScrapeGear() {
|
function getHVEquipStore() {
|
||||||
const url = window.location.href || '';
|
const w = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
|
||||||
console.log(`%c[HV] 🔍 Gear Debug: URL=${url}`, 'color:#f80');
|
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;
|
||||||
// Check for #eqsb
|
return null;
|
||||||
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
|
function getHVEquipData(itemId) {
|
||||||
const equiplist = document.getElementById('equiplist');
|
const store = getHVEquipStore();
|
||||||
if (equiplist) {
|
return (store && store[itemId]) ? store[itemId] : null;
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Public API ──
|
||||||
function getGearDB() {
|
function getGearDB() {
|
||||||
try { return JSON.parse(localStorage[GEAR_DB_KEY] || '[]'); } catch (e) { return []; }
|
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');
|
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');
|
console.log(`%c[HV] 📦 Gear DB: ${db.length} total (${equipped.length} equipped, ${armory.length} armory)`, 'color:#0f0');
|
||||||
equipped.forEach(item => {
|
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 => {
|
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 ──
|
// ── Parse tooltip HTML string into structured stats ──
|
||||||
// The #popup_box div appears on hover with a detailed stat breakdown:
|
// HV stores the full .eq HTML in dynjs_equip[ID].d
|
||||||
// <div class="eq">
|
function parseEquipHTML(htmlStr) {
|
||||||
// <div class="eqt">Light Armor & Level 115 & Tradeable</div>
|
if (!htmlStr) return null;
|
||||||
// <div class="eqr">Condition: 55% & Energy: N/A</div>
|
try {
|
||||||
// <div class="eqc">Burden: 7.1 & Interference: 2.1</div>
|
const doc = new DOMParser().parseFromString(htmlStr, 'text/html');
|
||||||
// <div class="ex">...</div>
|
const eq = doc.querySelector('.eq');
|
||||||
// <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;
|
if (!eq) return null;
|
||||||
|
|
||||||
const stats = {};
|
const stats = {};
|
||||||
|
|
||||||
// Type line: "Light Armor Level 115 Tradeable"
|
// Header: type, level, binding
|
||||||
const eqt = eq.querySelector('.eqt');
|
const eqt = eq.querySelector('.eqt');
|
||||||
if (eqt) {
|
if (eqt) {
|
||||||
const eqtText = eqt.textContent || '';
|
const t = eqt.textContent || '';
|
||||||
const typeMatch = eqtText.match(/^([A-Za-z\s]+?)(?:\s{2,}|$)/);
|
const typeM = t.match(/^([A-Za-z\s]+?)(?:\s{2,}|$)/);
|
||||||
if (typeMatch) stats.type = typeMatch[1].trim();
|
if (typeM) stats.type = typeM[1].trim();
|
||||||
const lvMatch = eqtText.match(/Level\s*(\d+)/i);
|
const lvM = t.match(/Level\s*(\d+|Unassigned)/i);
|
||||||
if (lvMatch) stats.level = lvMatch[1];
|
if (lvM) stats.level = lvM[1];
|
||||||
const tradeMatch = eqtText.match(/(Tradeable|Soulbound|Blessed)/);
|
const bindM = t.match(/(Tradeable|Soulbound|Blessed)/i);
|
||||||
if (tradeMatch) stats.bind = tradeMatch[1];
|
if (bindM) stats.bind = bindM[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Condition / Energy
|
// Condition / Energy
|
||||||
const eqr = eq.querySelector('.eqr');
|
const eqr = eq.querySelector('.eqr');
|
||||||
if (eqr) {
|
if (eqr) {
|
||||||
const condMatch = (eqr.textContent || '').match(/Condition:\s*(\d+)%/i);
|
const r = eqr.textContent || '';
|
||||||
if (condMatch) stats.condition = condMatch[1];
|
const cM = r.match(/Condition:\s*(\d+)%/i);
|
||||||
const engMatch = (eqr.textContent || '').match(/Energy:\s*([^\s]+)/i);
|
if (cM) stats.condition = parseInt(cM[1]);
|
||||||
if (engMatch) stats.energy = engMatch[1];
|
const eM = r.match(/Energy:\s*([^\s]+)/i);
|
||||||
|
if (eM) stats.energy = eM[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Burden / Interference
|
// Burden / Interference
|
||||||
const eqc = eq.querySelector('.eqc');
|
const eqc = eq.querySelector('.eqc');
|
||||||
if (eqc) {
|
if (eqc) {
|
||||||
const burMatch = (eqc.textContent || '').match(/Burden:\s*([\d.]+)/i);
|
const c = eqc.textContent || '';
|
||||||
if (burMatch) stats.burden = burMatch[1];
|
const bM = c.match(/Burden:\s*([\d.]+)/i);
|
||||||
const intMatch = (eqc.textContent || '').match(/Interference:\s*([\d.]+)/i);
|
if (bM) stats.burden = parseFloat(bM[1]);
|
||||||
if (intMatch) stats.interference = intMatch[1];
|
const iM = c.match(/Interference:\s*([\d.]+)/i);
|
||||||
|
if (iM) stats.interference = parseFloat(iM[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats from .ex (main stat block)
|
// 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');
|
const ex = eq.querySelector('.ex');
|
||||||
if (ex) {
|
if (ex) ex.querySelectorAll(':scope > div').forEach(parseRow);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extra stat groups (.ep)
|
// Extra stat groups (.ep)
|
||||||
eq.querySelectorAll('.ep').forEach(group => {
|
eq.querySelectorAll('.ep').forEach(g => {
|
||||||
const groupTitle = group.querySelector(':scope > div:first-child');
|
g.querySelectorAll(':scope > div:not(:first-child)').forEach(parseRow);
|
||||||
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 };
|
return stats;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scrape Character Equipment page (?s=Character&ss=eq) ──
|
// ── Scrape Character Equipment page ──
|
||||||
// DOM structure:
|
const SLOT_TYPES = ['Mainhand', 'Offhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet'];
|
||||||
// #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)
|
|
||||||
function scrapeCharacterEquip() {
|
function scrapeCharacterEquip() {
|
||||||
const scraped = [];
|
const scraped = [];
|
||||||
|
|
||||||
// Read each equipment slot in #eqsb
|
|
||||||
const slots = document.querySelectorAll('#eqsb > .eqb');
|
const slots = document.querySelectorAll('#eqsb > .eqb');
|
||||||
|
|
||||||
slots.forEach(slot => {
|
slots.forEach((slot, i) => {
|
||||||
// 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];
|
|
||||||
const nameDiv = slot.querySelector(':scope > div[onmouseover]');
|
const nameDiv = slot.querySelector(':scope > div[onmouseover]');
|
||||||
const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : '';
|
const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : '';
|
||||||
const idMatch = omo.match(/equips\.set\((\d+)/);
|
const idM = omo.match(/equips\.set\((\d+)/);
|
||||||
|
const itemId = idM ? idM[1] : '';
|
||||||
|
const name = nameDiv ? (nameDiv.textContent || '').trim() : '';
|
||||||
|
const disabled = slot.classList.contains('eqdisabled');
|
||||||
|
|
||||||
if (idMatch && typeof equips !== 'undefined' && equips.set) {
|
const obj = {
|
||||||
try {
|
id: itemId, name, disabled,
|
||||||
equips.set(parseInt(idMatch[1]), 'slot_pane', 250, 130);
|
slotType: SLOT_TYPES[i] || '',
|
||||||
// Wait for popup to populate
|
source: 'character',
|
||||||
setTimeout(() => {
|
scrapedAt: Date.now(),
|
||||||
const tooltip = parseTooltipPopup();
|
};
|
||||||
if (tooltip && tooltip.name && tooltip.name !== 'Popup Box') {
|
|
||||||
// Store in localStorage directly
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scraped.push(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scraped.length > 0) {
|
||||||
const db = getGearDB();
|
const db = getGearDB();
|
||||||
const match = db.find(e => e.id === idMatch[1]);
|
saveGearDB([...db.filter(e => e.source !== 'character'), ...scraped]);
|
||||||
if (match) {
|
console.log(`%c[HV] 🗡 Character: ${scraped.length} slots (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0');
|
||||||
match.stats = tooltip.stats;
|
|
||||||
saveGearDB(db);
|
|
||||||
}
|
}
|
||||||
results.push(tooltip);
|
return scraped;
|
||||||
console.log(`%c[HV] [${idx}] ${tooltip.name}`, 'color:#888');
|
|
||||||
}
|
|
||||||
idx++;
|
|
||||||
next();
|
|
||||||
}, 300);
|
|
||||||
return;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
idx++;
|
|
||||||
next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
// ── Scrape Armory page ──
|
||||||
return 'Capturing tooltips... check console';
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Auto-detect and scrape current page ──
|
// ── 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() {
|
function autoScrapeGear() {
|
||||||
const url = window.location.href || '';
|
const url = window.location.href || '';
|
||||||
|
if (url.includes('ss=eq') || url.includes('ss=ch')) return scrapeCharacterEquip();
|
||||||
// Character equipment page
|
if (url.includes('ss=am') && !url.includes('screen=modify')) return scrapeArmory();
|
||||||
if (url.includes('ss=eq')) {
|
if (url.includes('ss=am') && url.includes('screen=modify')) return scrapeModifyDetail();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scrape equipment modify detail page ──
|
// ── Modify detail page ──
|
||||||
function scrapeModifyDetail() {
|
function scrapeModifyDetail() {
|
||||||
const mainPane = document.getElementById('mainpane');
|
const mainPane = document.getElementById('mainpane');
|
||||||
if (!mainPane) return null;
|
if (!mainPane) return null;
|
||||||
|
const idM = window.location.href.match(/eqids?\[\]=(\d+)/);
|
||||||
// Try to get item ID from URL
|
const itemId = idM ? idM[1] : '';
|
||||||
const idMatch = window.location.href.match(/eqids?\[\]=(\d+)/);
|
|
||||||
const itemId = idMatch ? idMatch[1] : '';
|
|
||||||
|
|
||||||
// Item name from the page title area
|
|
||||||
const nameEl = mainPane.querySelector('h3, .itemname, .eqname');
|
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
|
// Parse right-side tooltip if present
|
||||||
const stats = {};
|
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 => {
|
mainPane.querySelectorAll('table.gry, table.grl').forEach(tbl => {
|
||||||
tbl.querySelectorAll('tr').forEach(row => {
|
tbl.querySelectorAll('tr').forEach(row => {
|
||||||
const cells = row.querySelectorAll('td');
|
const cells = row.querySelectorAll('td');
|
||||||
if (cells.length >= 2) {
|
if (cells.length >= 2) {
|
||||||
const key = (cells[0].textContent || '').trim().replace(':', '');
|
const k = (cells[0].textContent || '').trim().replace(':', '');
|
||||||
const val = (cells[1].textContent || '').trim();
|
const v = (cells[1].textContent || '').trim();
|
||||||
if (key && val) stats[key] = val;
|
if (k && v) stats[k] = v;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = mainPane.textContent || '';
|
const text = mainPane.textContent || '';
|
||||||
const durMatch = text.match(/Durability:\s*(\d+)\s*\/\s*(\d+)/i);
|
const durM = text.match(/Durability:\s*(\d+)\s*\/\s*(\d+)/i);
|
||||||
const potMatch = text.match(/(?:Potency|Item World)\s*:\s*([^\n<]+)/i);
|
const potM = text.match(/(?:Potency|Item World)\s*:\s*([^\n<]+)/i);
|
||||||
|
|
||||||
const item = {
|
// Enrich from memory
|
||||||
id: itemId,
|
if (itemId) {
|
||||||
name: name,
|
const data = getHVEquipData(itemId);
|
||||||
stats: stats,
|
if (data) {
|
||||||
durability: durMatch ? `${durMatch[1]}/${durMatch[2]}` : '',
|
if (!name && data.t) name = data.t;
|
||||||
potency: potMatch ? potMatch[1].trim() : '',
|
if (data.d) stats = Object.assign(parseEquipHTML(data.d) || {}, stats);
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3983,7 +3805,6 @@ window.HV = {
|
||||||
gearSummary: () => logGearSummary(),
|
gearSummary: () => logGearSummary(),
|
||||||
scrapeGear: () => autoScrapeGear(),
|
scrapeGear: () => autoScrapeGear(),
|
||||||
gearDebug: () => debugScrapeGear(),
|
gearDebug: () => debugScrapeGear(),
|
||||||
captureTooltips: () => captureAllTooltips(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -4090,15 +3911,10 @@ function initializeBattle() {
|
||||||
STATE.interruptHover = true;
|
STATE.interruptHover = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-scrape gear data on character/armory pages
|
// Auto-scrape gear data on character/armory pages (reads from HV's in-memory store)
|
||||||
if (window.location.href.includes('ss=eq')) {
|
const url = window.location.href || '';
|
||||||
// Character equipment page: trigger tooltip popups for full stats
|
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
|
||||||
setTimeout(() => {
|
setTimeout(autoScrapeGear, 300);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
STATE.battleInitialized = true;
|
STATE.battleInitialized = true;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.14.13';
|
const VERSION = '0.14.14';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,26 @@
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// GEAR SCRAPER — capture full equipment details for analysis
|
// GEAR SCRAPER — capture full equipment details for analysis
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// Usage:
|
// V2: Uses window.dynjs_equip / dynjs_eqstore directly from HV's JS
|
||||||
// 1. Visit Character Equipment page (?s=Character&ss=eq) — equipped gear
|
// memory store — no hover simulation or popup box delays needed.
|
||||||
// 2. Visit Armory page (?s=Bazaar&ss=am) — inventory listing
|
// Every item's full tooltip HTML is pre-loaded on page load.
|
||||||
// 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()
|
|
||||||
|
|
||||||
const GEAR_DB_KEY = SP + 'geardb';
|
const GEAR_DB_KEY = SP + 'geardb';
|
||||||
|
|
||||||
// Forced scrape from console with diagnostics
|
// ── Access HV's in-memory equipment store ──
|
||||||
function debugScrapeGear() {
|
function getHVEquipStore() {
|
||||||
const url = window.location.href || '';
|
const w = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
|
||||||
console.log(`%c[HV] 🔍 Gear Debug: URL=${url}`, 'color:#f80');
|
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;
|
||||||
// Check for #eqsb
|
return null;
|
||||||
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
|
function getHVEquipData(itemId) {
|
||||||
const equiplist = document.getElementById('equiplist');
|
const store = getHVEquipStore();
|
||||||
if (equiplist) {
|
return (store && store[itemId]) ? store[itemId] : null;
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Public API ──
|
||||||
function getGearDB() {
|
function getGearDB() {
|
||||||
try { return JSON.parse(localStorage[GEAR_DB_KEY] || '[]'); } catch (e) { return []; }
|
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');
|
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');
|
console.log(`%c[HV] 📦 Gear DB: ${db.length} total (${equipped.length} equipped, ${armory.length} armory)`, 'color:#0f0');
|
||||||
equipped.forEach(item => {
|
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 => {
|
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 ──
|
// ── Parse tooltip HTML string into structured stats ──
|
||||||
// The #popup_box div appears on hover with a detailed stat breakdown:
|
// HV stores the full .eq HTML in dynjs_equip[ID].d
|
||||||
// <div class="eq">
|
function parseEquipHTML(htmlStr) {
|
||||||
// <div class="eqt">Light Armor & Level 115 & Tradeable</div>
|
if (!htmlStr) return null;
|
||||||
// <div class="eqr">Condition: 55% & Energy: N/A</div>
|
try {
|
||||||
// <div class="eqc">Burden: 7.1 & Interference: 2.1</div>
|
const doc = new DOMParser().parseFromString(htmlStr, 'text/html');
|
||||||
// <div class="ex">...</div>
|
const eq = doc.querySelector('.eq');
|
||||||
// <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;
|
if (!eq) return null;
|
||||||
|
|
||||||
const stats = {};
|
const stats = {};
|
||||||
|
|
||||||
// Type line: "Light Armor Level 115 Tradeable"
|
// Header: type, level, binding
|
||||||
const eqt = eq.querySelector('.eqt');
|
const eqt = eq.querySelector('.eqt');
|
||||||
if (eqt) {
|
if (eqt) {
|
||||||
const eqtText = eqt.textContent || '';
|
const t = eqt.textContent || '';
|
||||||
const typeMatch = eqtText.match(/^([A-Za-z\s]+?)(?:\s{2,}|$)/);
|
const typeM = t.match(/^([A-Za-z\s]+?)(?:\s{2,}|$)/);
|
||||||
if (typeMatch) stats.type = typeMatch[1].trim();
|
if (typeM) stats.type = typeM[1].trim();
|
||||||
const lvMatch = eqtText.match(/Level\s*(\d+)/i);
|
const lvM = t.match(/Level\s*(\d+|Unassigned)/i);
|
||||||
if (lvMatch) stats.level = lvMatch[1];
|
if (lvM) stats.level = lvM[1];
|
||||||
const tradeMatch = eqtText.match(/(Tradeable|Soulbound|Blessed)/);
|
const bindM = t.match(/(Tradeable|Soulbound|Blessed)/i);
|
||||||
if (tradeMatch) stats.bind = tradeMatch[1];
|
if (bindM) stats.bind = bindM[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Condition / Energy
|
// Condition / Energy
|
||||||
const eqr = eq.querySelector('.eqr');
|
const eqr = eq.querySelector('.eqr');
|
||||||
if (eqr) {
|
if (eqr) {
|
||||||
const condMatch = (eqr.textContent || '').match(/Condition:\s*(\d+)%/i);
|
const r = eqr.textContent || '';
|
||||||
if (condMatch) stats.condition = condMatch[1];
|
const cM = r.match(/Condition:\s*(\d+)%/i);
|
||||||
const engMatch = (eqr.textContent || '').match(/Energy:\s*([^\s]+)/i);
|
if (cM) stats.condition = parseInt(cM[1]);
|
||||||
if (engMatch) stats.energy = engMatch[1];
|
const eM = r.match(/Energy:\s*([^\s]+)/i);
|
||||||
|
if (eM) stats.energy = eM[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Burden / Interference
|
// Burden / Interference
|
||||||
const eqc = eq.querySelector('.eqc');
|
const eqc = eq.querySelector('.eqc');
|
||||||
if (eqc) {
|
if (eqc) {
|
||||||
const burMatch = (eqc.textContent || '').match(/Burden:\s*([\d.]+)/i);
|
const c = eqc.textContent || '';
|
||||||
if (burMatch) stats.burden = burMatch[1];
|
const bM = c.match(/Burden:\s*([\d.]+)/i);
|
||||||
const intMatch = (eqc.textContent || '').match(/Interference:\s*([\d.]+)/i);
|
if (bM) stats.burden = parseFloat(bM[1]);
|
||||||
if (intMatch) stats.interference = intMatch[1];
|
const iM = c.match(/Interference:\s*([\d.]+)/i);
|
||||||
|
if (iM) stats.interference = parseFloat(iM[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats from .ex (main stat block)
|
// 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');
|
const ex = eq.querySelector('.ex');
|
||||||
if (ex) {
|
if (ex) ex.querySelectorAll(':scope > div').forEach(parseRow);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extra stat groups (.ep)
|
// Extra stat groups (.ep)
|
||||||
eq.querySelectorAll('.ep').forEach(group => {
|
eq.querySelectorAll('.ep').forEach(g => {
|
||||||
const groupTitle = group.querySelector(':scope > div:first-child');
|
g.querySelectorAll(':scope > div:not(:first-child)').forEach(parseRow);
|
||||||
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 };
|
return stats;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scrape Character Equipment page (?s=Character&ss=eq) ──
|
// ── Scrape Character Equipment page ──
|
||||||
// DOM structure:
|
const SLOT_TYPES = ['Mainhand', 'Offhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet'];
|
||||||
// #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)
|
|
||||||
function scrapeCharacterEquip() {
|
function scrapeCharacterEquip() {
|
||||||
const scraped = [];
|
const scraped = [];
|
||||||
|
|
||||||
// Read each equipment slot in #eqsb
|
|
||||||
const slots = document.querySelectorAll('#eqsb > .eqb');
|
const slots = document.querySelectorAll('#eqsb > .eqb');
|
||||||
|
|
||||||
slots.forEach(slot => {
|
slots.forEach((slot, i) => {
|
||||||
// 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];
|
|
||||||
const nameDiv = slot.querySelector(':scope > div[onmouseover]');
|
const nameDiv = slot.querySelector(':scope > div[onmouseover]');
|
||||||
const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : '';
|
const omo = nameDiv ? (nameDiv.getAttribute('onmouseover') || '') : '';
|
||||||
const idMatch = omo.match(/equips\.set\((\d+)/);
|
const idM = omo.match(/equips\.set\((\d+)/);
|
||||||
|
const itemId = idM ? idM[1] : '';
|
||||||
|
const name = nameDiv ? (nameDiv.textContent || '').trim() : '';
|
||||||
|
const disabled = slot.classList.contains('eqdisabled');
|
||||||
|
|
||||||
if (idMatch && typeof equips !== 'undefined' && equips.set) {
|
const obj = {
|
||||||
try {
|
id: itemId, name, disabled,
|
||||||
equips.set(parseInt(idMatch[1]), 'slot_pane', 250, 130);
|
slotType: SLOT_TYPES[i] || '',
|
||||||
// Wait for popup to populate
|
source: 'character',
|
||||||
setTimeout(() => {
|
scrapedAt: Date.now(),
|
||||||
const tooltip = parseTooltipPopup();
|
};
|
||||||
if (tooltip && tooltip.name && tooltip.name !== 'Popup Box') {
|
|
||||||
// Store in localStorage directly
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scraped.push(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scraped.length > 0) {
|
||||||
const db = getGearDB();
|
const db = getGearDB();
|
||||||
const match = db.find(e => e.id === idMatch[1]);
|
saveGearDB([...db.filter(e => e.source !== 'character'), ...scraped]);
|
||||||
if (match) {
|
console.log(`%c[HV] 🗡 Character: ${scraped.length} slots (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0');
|
||||||
match.stats = tooltip.stats;
|
|
||||||
saveGearDB(db);
|
|
||||||
}
|
}
|
||||||
results.push(tooltip);
|
return scraped;
|
||||||
console.log(`%c[HV] [${idx}] ${tooltip.name}`, 'color:#888');
|
|
||||||
}
|
|
||||||
idx++;
|
|
||||||
next();
|
|
||||||
}, 300);
|
|
||||||
return;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
idx++;
|
|
||||||
next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
// ── Scrape Armory page ──
|
||||||
return 'Capturing tooltips... check console';
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Auto-detect and scrape current page ──
|
// ── 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() {
|
function autoScrapeGear() {
|
||||||
const url = window.location.href || '';
|
const url = window.location.href || '';
|
||||||
|
if (url.includes('ss=eq') || url.includes('ss=ch')) return scrapeCharacterEquip();
|
||||||
// Character equipment page
|
if (url.includes('ss=am') && !url.includes('screen=modify')) return scrapeArmory();
|
||||||
if (url.includes('ss=eq')) {
|
if (url.includes('ss=am') && url.includes('screen=modify')) return scrapeModifyDetail();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scrape equipment modify detail page ──
|
// ── Modify detail page ──
|
||||||
function scrapeModifyDetail() {
|
function scrapeModifyDetail() {
|
||||||
const mainPane = document.getElementById('mainpane');
|
const mainPane = document.getElementById('mainpane');
|
||||||
if (!mainPane) return null;
|
if (!mainPane) return null;
|
||||||
|
const idM = window.location.href.match(/eqids?\[\]=(\d+)/);
|
||||||
// Try to get item ID from URL
|
const itemId = idM ? idM[1] : '';
|
||||||
const idMatch = window.location.href.match(/eqids?\[\]=(\d+)/);
|
|
||||||
const itemId = idMatch ? idMatch[1] : '';
|
|
||||||
|
|
||||||
// Item name from the page title area
|
|
||||||
const nameEl = mainPane.querySelector('h3, .itemname, .eqname');
|
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
|
// Parse right-side tooltip if present
|
||||||
const stats = {};
|
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 => {
|
mainPane.querySelectorAll('table.gry, table.grl').forEach(tbl => {
|
||||||
tbl.querySelectorAll('tr').forEach(row => {
|
tbl.querySelectorAll('tr').forEach(row => {
|
||||||
const cells = row.querySelectorAll('td');
|
const cells = row.querySelectorAll('td');
|
||||||
if (cells.length >= 2) {
|
if (cells.length >= 2) {
|
||||||
const key = (cells[0].textContent || '').trim().replace(':', '');
|
const k = (cells[0].textContent || '').trim().replace(':', '');
|
||||||
const val = (cells[1].textContent || '').trim();
|
const v = (cells[1].textContent || '').trim();
|
||||||
if (key && val) stats[key] = val;
|
if (k && v) stats[k] = v;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = mainPane.textContent || '';
|
const text = mainPane.textContent || '';
|
||||||
const durMatch = text.match(/Durability:\s*(\d+)\s*\/\s*(\d+)/i);
|
const durM = text.match(/Durability:\s*(\d+)\s*\/\s*(\d+)/i);
|
||||||
const potMatch = text.match(/(?:Potency|Item World)\s*:\s*([^\n<]+)/i);
|
const potM = text.match(/(?:Potency|Item World)\s*:\s*([^\n<]+)/i);
|
||||||
|
|
||||||
const item = {
|
// Enrich from memory
|
||||||
id: itemId,
|
if (itemId) {
|
||||||
name: name,
|
const data = getHVEquipData(itemId);
|
||||||
stats: stats,
|
if (data) {
|
||||||
durability: durMatch ? `${durMatch[1]}/${durMatch[2]}` : '',
|
if (!name && data.t) name = data.t;
|
||||||
potency: potMatch ? potMatch[1].trim() : '',
|
if (data.d) stats = Object.assign(parseEquipHTML(data.d) || {}, stats);
|
||||||
source: 'modify',
|
}
|
||||||
scrapedAt: Date.now(),
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (item.id) {
|
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 db = getGearDB();
|
||||||
const idx = db.findIndex(e => e.id === item.id && e.source === 'modify');
|
const idx = db.findIndex(e => e.id === itemId);
|
||||||
if (idx >= 0) db[idx] = item;
|
if (idx >= 0) Object.assign(db[idx], item);
|
||||||
else db.push(item);
|
else db.push(item);
|
||||||
saveGearDB(db);
|
saveGearDB(db);
|
||||||
console.log(`%c[HV] Gear: scraped modify detail for ${item.name || itemId}`, 'color:#0f0');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.14.13
|
// @version 0.14.14
|
||||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||||
// @author GaboGG + Hermes
|
// @author GaboGG + Hermes
|
||||||
// @match *://*.hentaiverse.org/*
|
// @match *://*.hentaiverse.org/*
|
||||||
|
|
|
||||||
13
src/init.js
13
src/init.js
|
|
@ -102,15 +102,10 @@ function initializeBattle() {
|
||||||
STATE.interruptHover = true;
|
STATE.interruptHover = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-scrape gear data on character/armory pages
|
// Auto-scrape gear data on character/armory pages (reads from HV's in-memory store)
|
||||||
if (window.location.href.includes('ss=eq')) {
|
const url = window.location.href || '';
|
||||||
// Character equipment page: trigger tooltip popups for full stats
|
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
|
||||||
setTimeout(() => {
|
setTimeout(autoScrapeGear, 300);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
STATE.battleInitialized = true;
|
STATE.battleInitialized = true;
|
||||||
|
|
|
||||||
|
|
@ -25,5 +25,4 @@ window.HV = {
|
||||||
gearSummary: () => logGearSummary(),
|
gearSummary: () => logGearSummary(),
|
||||||
scrapeGear: () => autoScrapeGear(),
|
scrapeGear: () => autoScrapeGear(),
|
||||||
gearDebug: () => debugScrapeGear(),
|
gearDebug: () => debugScrapeGear(),
|
||||||
captureTooltips: () => captureAllTooltips(),
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue