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