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
|
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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
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==
|
// ==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/*
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue