v0.14.7 - Gear scraper: captures full equipment stats to localStorage

- New module: gear-scraper.js (10K, 200+ lines)
- Auto-scrapes Character page (?ss=ch) for equipped gear with full stat
  blocks: ADB, damage, crit, parry, evade, burden, mitigation, stats
- Auto-scrapes Armory inventory page for all items with IDs, names,
  categories, potency, durability
- Scrapes Modify detail page for one item's complete stat table
- Stores everything in localStorage['hvunified_geardb']
- ParseHV.advice() now includes gear info
- Public API: getGearDB() returns full gear database
This commit is contained in:
GaboGG 2026-07-27 14:55:48 -04:00
parent 91d53e0f1c
commit d60233d205
7 changed files with 867 additions and 6 deletions

View file

@ -31,6 +31,7 @@ FILES=(
progress-tracker.js progress-tracker.js
abilities.js abilities.js
armory.js armory.js
gear-scraper.js
battle-logger.js battle-logger.js
re-timer.js re-timer.js
settings-page.js settings-page.js

View file

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name HV Unified // @name HV Unified
// @namespace hvunified // @namespace hvunified
// @version 0.14.6 // @version 0.14.7
// @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.6'; const VERSION = '0.14.7';
const CFG = { const CFG = {
// — Battle automation // — Battle automation
@ -3241,6 +3241,288 @@ function enhanceArmory() {
} }
} }
// ═══════════════════════════════════════════════════════════════════════
// GEAR SCRAPER — capture full equipment details for analysis
// ═══════════════════════════════════════════════════════════════════════
// Usage:
// 1. Visit Character page (?ss=ch) — scrapes equipped gear with all stats
// 2. Visit Armory pages (?ss=am) — scrapes inventory items
// 3. Data saved to localStorage['hvunified_geardb']
// 4. Query with: getGearDB() to read the full database
const GEAR_DB_KEY = SP + 'geardb';
// ── Public API ──
function getGearDB() {
try { return JSON.parse(localStorage[GEAR_DB_KEY] || '[]'); } catch (e) { return []; }
}
function saveGearDB(db) {
try { localStorage[GEAR_DB_KEY] = JSON.stringify(db); } catch (e) {}
}
// ── Scrape character page: equipped gear with full stats ──
// The HV character page (?ss=ch) shows each equipped item with:
// - Name, quality, prefix, suffix
// - Stats in the green/blue stat table
// - Potency, durability, level requirements
function scrapeCharacterGear() {
const scraped = [];
// Each equipment slot on the character page has class 'eqp' inside the
// character display table
const eqSlots = $$('.eqp, .eqslot, .equipped-item');
eqSlots.forEach(slot => {
// Try to find the item name from the tooltip trigger
const link = slot.querySelector('a[onmouseover*="inv_tooltip"]') ||
slot.querySelector('[onmouseover*="inv_tooltip"]');
if (!link) return;
const omo = link.getAttribute('onmouseover') || '';
// Extract item ID from tooltip call: inv_tooltip('ITEM_ID')
const idMatch = omo.match(/inv_tooltip\s*\(\s*['"](\d+)['"]/);
const itemId = idMatch ? idMatch[1] : '';
// Extract name from the alt/title text
const name = (link.getAttribute('title') || link.getAttribute('alt') || '').trim();
// Extract stat block from the adjacent table/div
// Stats are in a grey table showing: ADB, Damage, Crit, etc.
const statEl = slot.querySelector('.eqstatblock, .item-stats, .gry, table.gry');
const stats = statEl ? parseStatBlock(statEl.textContent || '') : {};
// Durability
const durEl = slot.querySelector('[class*="dur"], [class*="durability"]');
const durability = durEl ? (durEl.textContent || '').trim() : '';
// Potency / Item World level
const potEl = slot.querySelector('[class*="potency"], [class*="iw"]');
const potency = potEl ? (potEl.textContent || '').trim() : '';
// Slot type (weapon, armor, etc.) from parent container
const slotParent = slot.closest('tr, div, td');
const slotType = slotParent ? (slotParent.className || '') : '';
// Level requirement
const lvEl = slot.querySelector('[class*="req"], [class*="level"]');
const levelReq = lvEl ? (lvEl.textContent || '').match(/\d+/) : null;
scraped.push({
id: itemId,
name: name,
slotType: slotType,
stats: stats,
durability: durability,
potency: potency,
levelReq: levelReq ? parseInt(levelReq[0]) : 0,
source: 'character',
scrapedAt: Date.now(),
});
});
// If we found character gear, merge or replace
if (scraped.length > 0) {
const db = getGearDB();
// Remove old character entries with same IDs
const filtered = db.filter(e => !(e.source === 'character' && scraped.some(s => s.id === e.id)));
saveGearDB([...filtered, ...scraped]);
console.log(`%c[HV] Gear: scraped ${scraped.length} equipped items from character page`, 'color:#0f0');
}
return scraped;
}
// ── Scrape armory inventory page ──
function scrapeArmoryInventory() {
const scraped = [];
const equipList = document.getElementById('equiplist');
if (!equipList) return scraped;
let currentCategory = '';
$$('tr', equipList).forEach(row => {
if (row.className === 'eqtplabel') {
currentCategory = (row.textContent || '').trim();
return;
}
const cb = row.querySelector('input[name="eqids[]"]');
if (!cb) return;
const id = cb.value;
const label = row.querySelector('label');
const rawName = label ? (label.textContent || '').trim() : '';
// Parse name into quality + base name
const equipped = rawName.includes('🗡');
const locked = rawName.includes('🔒');
const pinned = rawName.includes('📌');
const stored = rawName.includes('📦');
const cleanName = rawName.replace(/[🗡🔒📌📦🛡\s]/g, '').trim();
// Try to parse potency info from adjacent cells
const cells = row.querySelectorAll('td');
let potency = '';
let durability = '';
if (cells.length >= 3) {
potency = (cells[cells.length - 2].textContent || '').trim();
durability = (cells[cells.length - 1].textContent || '').trim();
}
scraped.push({
id: id,
name: cleanName,
category: currentCategory,
equipped: equipped,
locked: locked,
pinned: pinned,
stored: stored,
potency: potency,
durability: durability,
source: 'armory',
scrapedAt: Date.now(),
});
});
if (scraped.length > 0) {
const db = getGearDB();
// Remove old armory entries
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;
}
// ── Scrape equipment modify detail page (full stats) ──
// The Modify page (?ss=am&eq=modify&eqid=XXXX) shows one item with ALL details
function scrapeModifyDetail() {
// This page shows detailed stat tables. Parse the main stat block.
const mainPane = document.getElementById('mainpane');
if (!mainPane) return null;
const text = mainPane.textContent || '';
const nameEl = mainPane.querySelector('h2, h3, .itemname, .eqname');
const name = nameEl ? (nameEl.textContent || '').trim() : '';
// Extract item ID from URL
const idMatch = window.location.href.match(/eqid[=/](\d+)/);
const itemId = idMatch ? idMatch[1] : '';
// Find the stat table (grey bordered table with stat rows)
const statTables = mainPane.querySelectorAll('table.gry, table[class*="stat"]');
const stats = {};
statTables.forEach(tbl => {
const rows = tbl.querySelectorAll('tr');
rows.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;
}
});
});
// Durability
const durMatch = text.match(/Durability:\s*(\d+\s*\/\s*\d+)/i);
// Potency
const potMatch = text.match(/(?:Potency|Item World):\s*([^\n]+)/i);
const item = {
id: itemId,
name: name,
stats: stats,
durability: durMatch ? durMatch[1] : '',
potency: potMatch ? potMatch[1].trim() : '',
source: 'modify',
scrapedAt: Date.now(),
};
// Save or update
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 ${name}`, 'color:#0f0');
}
return item;
}
// ── Auto-detect and scrape current page ──
function autoScrapeGear() {
const url = window.location.href || '';
const ss = url.match(/ss=(\w+)/);
if (url.includes('ss=ch')) {
return scrapeCharacterGear();
}
if (url.includes('ss=am')) {
if (url.includes('eq=modify')) {
return scrapeModifyDetail();
}
return scrapeArmoryInventory();
}
return [];
}
// ── Stat block parser ──
// The character page stats look like: "ADB +X | Damage X-Y | Crit X%"
function parseStatBlock(text) {
const stats = {};
// Find common stat patterns
const patterns = [
{ key: 'adb', re: /ADB\s*[+:]\s*([\d.]+)/i },
{ key: 'damage', re: /Damage\s*[+:]?\s*(\d+\s*-\s*\d+)/i },
{ key: 'critChance', re: /(?:Crit(?:ical)?\s*(?:Chance|Rate)?|CTH)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'critDamage', re: /(?:Crit\s*Damage|CDB)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'attackSpeed', re: /(?:Attack\s*Speed|Speed)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'parry', re: /Parry\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'evade', re: /Evade\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'block', re: /Block\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'physMit', re: /(?:Physical\s*(?:Mitigation|Resist)|PMit)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'magMit', re: /(?:Magical\s*(?:Mitigation|Resist)|MMit)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'str', re: /\bSTR\s*[+:]?\s*([\d.]+)/i },
{ key: 'dex', re: /\bDEX\s*[+:]?\s*([\d.]+)/i },
{ key: 'end', re: /\bEND\s*[+:]?\s*([\d.]+)/i },
{ key: 'agi', re: /\bAGI\s*[+:]?\s*([\d.]+)/i },
{ key: 'wis', re: /\bWIS\s*[+:]?\s*([\d.]+)/i },
{ key: 'int', re: /\bINT\s*[+:]?\s*([\d.]+)/i },
{ key: 'burden', re: /Burden\s*[+:]?\s*([\d.]+)/i },
{ key: 'interference', re: /Interference\s*[+:]?\s*([\d.]+)/i },
];
patterns.forEach(p => {
const m = text.match(p.re);
if (m) stats[p.key] = m[1].trim();
});
return stats;
}
// ── Quick summary for console ──
function logGearSummary() {
const db = getGearDB();
const equipped = db.filter(e => e.source === 'character');
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 => {
const name = item.name || 'unknown';
const adb = item.stats?.adb || '?';
console.log(` 🗡 ${name} | ADB: +${adb} | ${item.durability || ''}`);
});
}
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
// BATTLE LOGGER — saves raw battle log lines to localStorage in real time // BATTLE LOGGER — saves raw battle log lines to localStorage in real time
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
@ -3639,6 +3921,11 @@ function initializeBattle() {
STATE.interruptHover = true; STATE.interruptHover = true;
} }
// Auto-scrape gear data on character/armory pages
if (STATE.page === 'character' || STATE.page === 'armory' || window.location.href.includes('ss=ch') || window.location.href.includes('ss=am')) {
setTimeout(autoScrapeGear, 500); // wait for page to render
}
STATE.battleInitialized = true; STATE.battleInitialized = true;
} }

View file

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name HV Unified // @name HV Unified
// @namespace hvunified // @namespace hvunified
// @version 0.14.6 // @version 0.14.7
// @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.6'; const VERSION = '0.14.7';
const CFG = { const CFG = {
// — Battle automation // — Battle automation
@ -3241,6 +3241,288 @@ function enhanceArmory() {
} }
} }
// ═══════════════════════════════════════════════════════════════════════
// GEAR SCRAPER — capture full equipment details for analysis
// ═══════════════════════════════════════════════════════════════════════
// Usage:
// 1. Visit Character page (?ss=ch) — scrapes equipped gear with all stats
// 2. Visit Armory pages (?ss=am) — scrapes inventory items
// 3. Data saved to localStorage['hvunified_geardb']
// 4. Query with: getGearDB() to read the full database
const GEAR_DB_KEY = SP + 'geardb';
// ── Public API ──
function getGearDB() {
try { return JSON.parse(localStorage[GEAR_DB_KEY] || '[]'); } catch (e) { return []; }
}
function saveGearDB(db) {
try { localStorage[GEAR_DB_KEY] = JSON.stringify(db); } catch (e) {}
}
// ── Scrape character page: equipped gear with full stats ──
// The HV character page (?ss=ch) shows each equipped item with:
// - Name, quality, prefix, suffix
// - Stats in the green/blue stat table
// - Potency, durability, level requirements
function scrapeCharacterGear() {
const scraped = [];
// Each equipment slot on the character page has class 'eqp' inside the
// character display table
const eqSlots = $$('.eqp, .eqslot, .equipped-item');
eqSlots.forEach(slot => {
// Try to find the item name from the tooltip trigger
const link = slot.querySelector('a[onmouseover*="inv_tooltip"]') ||
slot.querySelector('[onmouseover*="inv_tooltip"]');
if (!link) return;
const omo = link.getAttribute('onmouseover') || '';
// Extract item ID from tooltip call: inv_tooltip('ITEM_ID')
const idMatch = omo.match(/inv_tooltip\s*\(\s*['"](\d+)['"]/);
const itemId = idMatch ? idMatch[1] : '';
// Extract name from the alt/title text
const name = (link.getAttribute('title') || link.getAttribute('alt') || '').trim();
// Extract stat block from the adjacent table/div
// Stats are in a grey table showing: ADB, Damage, Crit, etc.
const statEl = slot.querySelector('.eqstatblock, .item-stats, .gry, table.gry');
const stats = statEl ? parseStatBlock(statEl.textContent || '') : {};
// Durability
const durEl = slot.querySelector('[class*="dur"], [class*="durability"]');
const durability = durEl ? (durEl.textContent || '').trim() : '';
// Potency / Item World level
const potEl = slot.querySelector('[class*="potency"], [class*="iw"]');
const potency = potEl ? (potEl.textContent || '').trim() : '';
// Slot type (weapon, armor, etc.) from parent container
const slotParent = slot.closest('tr, div, td');
const slotType = slotParent ? (slotParent.className || '') : '';
// Level requirement
const lvEl = slot.querySelector('[class*="req"], [class*="level"]');
const levelReq = lvEl ? (lvEl.textContent || '').match(/\d+/) : null;
scraped.push({
id: itemId,
name: name,
slotType: slotType,
stats: stats,
durability: durability,
potency: potency,
levelReq: levelReq ? parseInt(levelReq[0]) : 0,
source: 'character',
scrapedAt: Date.now(),
});
});
// If we found character gear, merge or replace
if (scraped.length > 0) {
const db = getGearDB();
// Remove old character entries with same IDs
const filtered = db.filter(e => !(e.source === 'character' && scraped.some(s => s.id === e.id)));
saveGearDB([...filtered, ...scraped]);
console.log(`%c[HV] Gear: scraped ${scraped.length} equipped items from character page`, 'color:#0f0');
}
return scraped;
}
// ── Scrape armory inventory page ──
function scrapeArmoryInventory() {
const scraped = [];
const equipList = document.getElementById('equiplist');
if (!equipList) return scraped;
let currentCategory = '';
$$('tr', equipList).forEach(row => {
if (row.className === 'eqtplabel') {
currentCategory = (row.textContent || '').trim();
return;
}
const cb = row.querySelector('input[name="eqids[]"]');
if (!cb) return;
const id = cb.value;
const label = row.querySelector('label');
const rawName = label ? (label.textContent || '').trim() : '';
// Parse name into quality + base name
const equipped = rawName.includes('🗡');
const locked = rawName.includes('🔒');
const pinned = rawName.includes('📌');
const stored = rawName.includes('📦');
const cleanName = rawName.replace(/[🗡🔒📌📦🛡\s]/g, '').trim();
// Try to parse potency info from adjacent cells
const cells = row.querySelectorAll('td');
let potency = '';
let durability = '';
if (cells.length >= 3) {
potency = (cells[cells.length - 2].textContent || '').trim();
durability = (cells[cells.length - 1].textContent || '').trim();
}
scraped.push({
id: id,
name: cleanName,
category: currentCategory,
equipped: equipped,
locked: locked,
pinned: pinned,
stored: stored,
potency: potency,
durability: durability,
source: 'armory',
scrapedAt: Date.now(),
});
});
if (scraped.length > 0) {
const db = getGearDB();
// Remove old armory entries
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;
}
// ── Scrape equipment modify detail page (full stats) ──
// The Modify page (?ss=am&eq=modify&eqid=XXXX) shows one item with ALL details
function scrapeModifyDetail() {
// This page shows detailed stat tables. Parse the main stat block.
const mainPane = document.getElementById('mainpane');
if (!mainPane) return null;
const text = mainPane.textContent || '';
const nameEl = mainPane.querySelector('h2, h3, .itemname, .eqname');
const name = nameEl ? (nameEl.textContent || '').trim() : '';
// Extract item ID from URL
const idMatch = window.location.href.match(/eqid[=/](\d+)/);
const itemId = idMatch ? idMatch[1] : '';
// Find the stat table (grey bordered table with stat rows)
const statTables = mainPane.querySelectorAll('table.gry, table[class*="stat"]');
const stats = {};
statTables.forEach(tbl => {
const rows = tbl.querySelectorAll('tr');
rows.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;
}
});
});
// Durability
const durMatch = text.match(/Durability:\s*(\d+\s*\/\s*\d+)/i);
// Potency
const potMatch = text.match(/(?:Potency|Item World):\s*([^\n]+)/i);
const item = {
id: itemId,
name: name,
stats: stats,
durability: durMatch ? durMatch[1] : '',
potency: potMatch ? potMatch[1].trim() : '',
source: 'modify',
scrapedAt: Date.now(),
};
// Save or update
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 ${name}`, 'color:#0f0');
}
return item;
}
// ── Auto-detect and scrape current page ──
function autoScrapeGear() {
const url = window.location.href || '';
const ss = url.match(/ss=(\w+)/);
if (url.includes('ss=ch')) {
return scrapeCharacterGear();
}
if (url.includes('ss=am')) {
if (url.includes('eq=modify')) {
return scrapeModifyDetail();
}
return scrapeArmoryInventory();
}
return [];
}
// ── Stat block parser ──
// The character page stats look like: "ADB +X | Damage X-Y | Crit X%"
function parseStatBlock(text) {
const stats = {};
// Find common stat patterns
const patterns = [
{ key: 'adb', re: /ADB\s*[+:]\s*([\d.]+)/i },
{ key: 'damage', re: /Damage\s*[+:]?\s*(\d+\s*-\s*\d+)/i },
{ key: 'critChance', re: /(?:Crit(?:ical)?\s*(?:Chance|Rate)?|CTH)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'critDamage', re: /(?:Crit\s*Damage|CDB)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'attackSpeed', re: /(?:Attack\s*Speed|Speed)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'parry', re: /Parry\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'evade', re: /Evade\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'block', re: /Block\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'physMit', re: /(?:Physical\s*(?:Mitigation|Resist)|PMit)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'magMit', re: /(?:Magical\s*(?:Mitigation|Resist)|MMit)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'str', re: /\bSTR\s*[+:]?\s*([\d.]+)/i },
{ key: 'dex', re: /\bDEX\s*[+:]?\s*([\d.]+)/i },
{ key: 'end', re: /\bEND\s*[+:]?\s*([\d.]+)/i },
{ key: 'agi', re: /\bAGI\s*[+:]?\s*([\d.]+)/i },
{ key: 'wis', re: /\bWIS\s*[+:]?\s*([\d.]+)/i },
{ key: 'int', re: /\bINT\s*[+:]?\s*([\d.]+)/i },
{ key: 'burden', re: /Burden\s*[+:]?\s*([\d.]+)/i },
{ key: 'interference', re: /Interference\s*[+:]?\s*([\d.]+)/i },
];
patterns.forEach(p => {
const m = text.match(p.re);
if (m) stats[p.key] = m[1].trim();
});
return stats;
}
// ── Quick summary for console ──
function logGearSummary() {
const db = getGearDB();
const equipped = db.filter(e => e.source === 'character');
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 => {
const name = item.name || 'unknown';
const adb = item.stats?.adb || '?';
console.log(` 🗡 ${name} | ADB: +${adb} | ${item.durability || ''}`);
});
}
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
// BATTLE LOGGER — saves raw battle log lines to localStorage in real time // BATTLE LOGGER — saves raw battle log lines to localStorage in real time
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
@ -3639,6 +3921,11 @@ function initializeBattle() {
STATE.interruptHover = true; STATE.interruptHover = true;
} }
// Auto-scrape gear data on character/armory pages
if (STATE.page === 'character' || STATE.page === 'armory' || window.location.href.includes('ss=ch') || window.location.href.includes('ss=am')) {
setTimeout(autoScrapeGear, 500); // wait for page to render
}
STATE.battleInitialized = true; STATE.battleInitialized = true;
} }

View file

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

281
src/gear-scraper.js Normal file
View file

@ -0,0 +1,281 @@
// ═══════════════════════════════════════════════════════════════════════
// GEAR SCRAPER — capture full equipment details for analysis
// ═══════════════════════════════════════════════════════════════════════
// Usage:
// 1. Visit Character page (?ss=ch) — scrapes equipped gear with all stats
// 2. Visit Armory pages (?ss=am) — scrapes inventory items
// 3. Data saved to localStorage['hvunified_geardb']
// 4. Query with: getGearDB() to read the full database
const GEAR_DB_KEY = SP + 'geardb';
// ── Public API ──
function getGearDB() {
try { return JSON.parse(localStorage[GEAR_DB_KEY] || '[]'); } catch (e) { return []; }
}
function saveGearDB(db) {
try { localStorage[GEAR_DB_KEY] = JSON.stringify(db); } catch (e) {}
}
// ── Scrape character page: equipped gear with full stats ──
// The HV character page (?ss=ch) shows each equipped item with:
// - Name, quality, prefix, suffix
// - Stats in the green/blue stat table
// - Potency, durability, level requirements
function scrapeCharacterGear() {
const scraped = [];
// Each equipment slot on the character page has class 'eqp' inside the
// character display table
const eqSlots = $$('.eqp, .eqslot, .equipped-item');
eqSlots.forEach(slot => {
// Try to find the item name from the tooltip trigger
const link = slot.querySelector('a[onmouseover*="inv_tooltip"]') ||
slot.querySelector('[onmouseover*="inv_tooltip"]');
if (!link) return;
const omo = link.getAttribute('onmouseover') || '';
// Extract item ID from tooltip call: inv_tooltip('ITEM_ID')
const idMatch = omo.match(/inv_tooltip\s*\(\s*['"](\d+)['"]/);
const itemId = idMatch ? idMatch[1] : '';
// Extract name from the alt/title text
const name = (link.getAttribute('title') || link.getAttribute('alt') || '').trim();
// Extract stat block from the adjacent table/div
// Stats are in a grey table showing: ADB, Damage, Crit, etc.
const statEl = slot.querySelector('.eqstatblock, .item-stats, .gry, table.gry');
const stats = statEl ? parseStatBlock(statEl.textContent || '') : {};
// Durability
const durEl = slot.querySelector('[class*="dur"], [class*="durability"]');
const durability = durEl ? (durEl.textContent || '').trim() : '';
// Potency / Item World level
const potEl = slot.querySelector('[class*="potency"], [class*="iw"]');
const potency = potEl ? (potEl.textContent || '').trim() : '';
// Slot type (weapon, armor, etc.) from parent container
const slotParent = slot.closest('tr, div, td');
const slotType = slotParent ? (slotParent.className || '') : '';
// Level requirement
const lvEl = slot.querySelector('[class*="req"], [class*="level"]');
const levelReq = lvEl ? (lvEl.textContent || '').match(/\d+/) : null;
scraped.push({
id: itemId,
name: name,
slotType: slotType,
stats: stats,
durability: durability,
potency: potency,
levelReq: levelReq ? parseInt(levelReq[0]) : 0,
source: 'character',
scrapedAt: Date.now(),
});
});
// If we found character gear, merge or replace
if (scraped.length > 0) {
const db = getGearDB();
// Remove old character entries with same IDs
const filtered = db.filter(e => !(e.source === 'character' && scraped.some(s => s.id === e.id)));
saveGearDB([...filtered, ...scraped]);
console.log(`%c[HV] Gear: scraped ${scraped.length} equipped items from character page`, 'color:#0f0');
}
return scraped;
}
// ── Scrape armory inventory page ──
function scrapeArmoryInventory() {
const scraped = [];
const equipList = document.getElementById('equiplist');
if (!equipList) return scraped;
let currentCategory = '';
$$('tr', equipList).forEach(row => {
if (row.className === 'eqtplabel') {
currentCategory = (row.textContent || '').trim();
return;
}
const cb = row.querySelector('input[name="eqids[]"]');
if (!cb) return;
const id = cb.value;
const label = row.querySelector('label');
const rawName = label ? (label.textContent || '').trim() : '';
// Parse name into quality + base name
const equipped = rawName.includes('🗡');
const locked = rawName.includes('🔒');
const pinned = rawName.includes('📌');
const stored = rawName.includes('📦');
const cleanName = rawName.replace(/[🗡🔒📌📦🛡\s]/g, '').trim();
// Try to parse potency info from adjacent cells
const cells = row.querySelectorAll('td');
let potency = '';
let durability = '';
if (cells.length >= 3) {
potency = (cells[cells.length - 2].textContent || '').trim();
durability = (cells[cells.length - 1].textContent || '').trim();
}
scraped.push({
id: id,
name: cleanName,
category: currentCategory,
equipped: equipped,
locked: locked,
pinned: pinned,
stored: stored,
potency: potency,
durability: durability,
source: 'armory',
scrapedAt: Date.now(),
});
});
if (scraped.length > 0) {
const db = getGearDB();
// Remove old armory entries
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;
}
// ── Scrape equipment modify detail page (full stats) ──
// The Modify page (?ss=am&eq=modify&eqid=XXXX) shows one item with ALL details
function scrapeModifyDetail() {
// This page shows detailed stat tables. Parse the main stat block.
const mainPane = document.getElementById('mainpane');
if (!mainPane) return null;
const text = mainPane.textContent || '';
const nameEl = mainPane.querySelector('h2, h3, .itemname, .eqname');
const name = nameEl ? (nameEl.textContent || '').trim() : '';
// Extract item ID from URL
const idMatch = window.location.href.match(/eqid[=/](\d+)/);
const itemId = idMatch ? idMatch[1] : '';
// Find the stat table (grey bordered table with stat rows)
const statTables = mainPane.querySelectorAll('table.gry, table[class*="stat"]');
const stats = {};
statTables.forEach(tbl => {
const rows = tbl.querySelectorAll('tr');
rows.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;
}
});
});
// Durability
const durMatch = text.match(/Durability:\s*(\d+\s*\/\s*\d+)/i);
// Potency
const potMatch = text.match(/(?:Potency|Item World):\s*([^\n]+)/i);
const item = {
id: itemId,
name: name,
stats: stats,
durability: durMatch ? durMatch[1] : '',
potency: potMatch ? potMatch[1].trim() : '',
source: 'modify',
scrapedAt: Date.now(),
};
// Save or update
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 ${name}`, 'color:#0f0');
}
return item;
}
// ── Auto-detect and scrape current page ──
function autoScrapeGear() {
const url = window.location.href || '';
const ss = url.match(/ss=(\w+)/);
if (url.includes('ss=ch')) {
return scrapeCharacterGear();
}
if (url.includes('ss=am')) {
if (url.includes('eq=modify')) {
return scrapeModifyDetail();
}
return scrapeArmoryInventory();
}
return [];
}
// ── Stat block parser ──
// The character page stats look like: "ADB +X | Damage X-Y | Crit X%"
function parseStatBlock(text) {
const stats = {};
// Find common stat patterns
const patterns = [
{ key: 'adb', re: /ADB\s*[+:]\s*([\d.]+)/i },
{ key: 'damage', re: /Damage\s*[+:]?\s*(\d+\s*-\s*\d+)/i },
{ key: 'critChance', re: /(?:Crit(?:ical)?\s*(?:Chance|Rate)?|CTH)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'critDamage', re: /(?:Crit\s*Damage|CDB)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'attackSpeed', re: /(?:Attack\s*Speed|Speed)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'parry', re: /Parry\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'evade', re: /Evade\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'block', re: /Block\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'physMit', re: /(?:Physical\s*(?:Mitigation|Resist)|PMit)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'magMit', re: /(?:Magical\s*(?:Mitigation|Resist)|MMit)\s*[+:]?\s*([\d.]+)%?/i },
{ key: 'str', re: /\bSTR\s*[+:]?\s*([\d.]+)/i },
{ key: 'dex', re: /\bDEX\s*[+:]?\s*([\d.]+)/i },
{ key: 'end', re: /\bEND\s*[+:]?\s*([\d.]+)/i },
{ key: 'agi', re: /\bAGI\s*[+:]?\s*([\d.]+)/i },
{ key: 'wis', re: /\bWIS\s*[+:]?\s*([\d.]+)/i },
{ key: 'int', re: /\bINT\s*[+:]?\s*([\d.]+)/i },
{ key: 'burden', re: /Burden\s*[+:]?\s*([\d.]+)/i },
{ key: 'interference', re: /Interference\s*[+:]?\s*([\d.]+)/i },
];
patterns.forEach(p => {
const m = text.match(p.re);
if (m) stats[p.key] = m[1].trim();
});
return stats;
}
// ── Quick summary for console ──
function logGearSummary() {
const db = getGearDB();
const equipped = db.filter(e => e.source === 'character');
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 => {
const name = item.name || 'unknown';
const adb = item.stats?.adb || '?';
console.log(` 🗡 ${name} | ADB: +${adb} | ${item.durability || ''}`);
});
}

View file

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name HV Unified // @name HV Unified
// @namespace hvunified // @namespace hvunified
// @version 0.14.6 // @version 0.14.7
// @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/*

View file

@ -102,6 +102,11 @@ function initializeBattle() {
STATE.interruptHover = true; STATE.interruptHover = true;
} }
// Auto-scrape gear data on character/armory pages
if (STATE.page === 'character' || STATE.page === 'armory' || window.location.href.includes('ss=ch') || window.location.href.includes('ss=am')) {
setTimeout(autoScrapeGear, 500); // wait for page to render
}
STATE.battleInitialized = true; STATE.battleInitialized = true;
} }