From d60233d20582d1d06ca34da4defaebcd18a46577 Mon Sep 17 00:00:00 2001 From: GaboGG Date: Mon, 27 Jul 2026 14:55:48 -0400 Subject: [PATCH] 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 --- scripts/build.sh | 1 + scripts/hv-unified.user.js | 291 ++++++++++++++++++++++++++++++++++++- scripts/latest.user.js | 291 ++++++++++++++++++++++++++++++++++++- src/config.js | 2 +- src/gear-scraper.js | 281 +++++++++++++++++++++++++++++++++++ src/header.user.js | 2 +- src/init.js | 5 + 7 files changed, 867 insertions(+), 6 deletions(-) create mode 100644 src/gear-scraper.js diff --git a/scripts/build.sh b/scripts/build.sh index fdc6dcc..f83940a 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -31,6 +31,7 @@ FILES=( progress-tracker.js abilities.js armory.js + gear-scraper.js battle-logger.js re-timer.js settings-page.js diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index da19a1c..12938ea 100644 --- a/scripts/hv-unified.user.js +++ b/scripts/hv-unified.user.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; } diff --git a/scripts/latest.user.js b/scripts/latest.user.js index da19a1c..12938ea 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.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; } diff --git a/src/config.js b/src/config.js index 8fb44a7..695f1f1 100644 --- a/src/config.js +++ b/src/config.js @@ -2,7 +2,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.14.6'; +const VERSION = '0.14.7'; const CFG = { // — Battle automation diff --git a/src/gear-scraper.js b/src/gear-scraper.js new file mode 100644 index 0000000..2ca0a24 --- /dev/null +++ b/src/gear-scraper.js @@ -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 || ''}`); + }); +} diff --git a/src/header.user.js b/src/header.user.js index a60c57a..9f97c95 100644 --- a/src/header.user.js +++ b/src/header.user.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/* diff --git a/src/init.js b/src/init.js index f08bf83..9e65a73 100644 --- a/src/init.js +++ b/src/init.js @@ -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; }