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:
parent
91d53e0f1c
commit
d60233d205
7 changed files with 867 additions and 6 deletions
|
|
@ -31,6 +31,7 @@ FILES=(
|
|||
progress-tracker.js
|
||||
abilities.js
|
||||
armory.js
|
||||
gear-scraper.js
|
||||
battle-logger.js
|
||||
re-timer.js
|
||||
settings-page.js
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.14.6
|
||||
// @version 0.14.7
|
||||
// @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.6';
|
||||
const VERSION = '0.14.7';
|
||||
|
||||
const CFG = {
|
||||
// — 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
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -3639,6 +3921,11 @@ function initializeBattle() {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.14.6
|
||||
// @version 0.14.7
|
||||
// @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.6';
|
||||
const VERSION = '0.14.7';
|
||||
|
||||
const CFG = {
|
||||
// — 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
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -3639,6 +3921,11 @@ function initializeBattle() {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// CONFIG — default settings
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const VERSION = '0.14.6';
|
||||
const VERSION = '0.14.7';
|
||||
|
||||
const CFG = {
|
||||
// — Battle automation
|
||||
|
|
|
|||
281
src/gear-scraper.js
Normal file
281
src/gear-scraper.js
Normal 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 || ''}`);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.14.6
|
||||
// @version 0.14.7
|
||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||
// @author GaboGG + Hermes
|
||||
// @match *://*.hentaiverse.org/*
|
||||
|
|
|
|||
|
|
@ -102,6 +102,11 @@ function initializeBattle() {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue