From f2e8ce536a83e04ae1173102a2b6bfd7a8e05d99 Mon Sep 17 00:00:00 2001 From: GaboGG Date: Mon, 27 Jul 2026 20:38:09 -0400 Subject: [PATCH] v0.14.19 - Add Bazaar Buy page scraper + 3-source analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scrapeBuyPage() scrapes equipment for sale from Bazaar Buy tab (URL: ss=am&screen=buy or ss=bi), marks as source='buy' - Stores merge with existing: buy items don't overwrite equipped/armory - Auto-detect routing handles buy page URL patterns - analyze_gear.py updated with 3-source output: 🟢 Equipped = currently worn šŸ“¦ Stored = in armory inventory šŸ›’ For Sale = purchasable from Bazaar - Shows level limits (Lv155 + 15 = Lv170 cap), prices where available - Re-scrape after visiting Buy tab to include market items --- analyze_gear.py | 139 ++++++++++++++++++------------------- scripts/hv-unified.user.js | 58 +++++++++++++++- scripts/latest.user.js | 58 +++++++++++++++- src/config.js | 2 +- src/gear-scraper.js | 54 +++++++++++++- src/header.user.js | 2 +- 6 files changed, 232 insertions(+), 81 deletions(-) diff --git a/analyze_gear.py b/analyze_gear.py index 51e52ed..71c1209 100644 --- a/analyze_gear.py +++ b/analyze_gear.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 -"""Analyze HV gear database for 2H physical build optimization.""" -import json, sys +"""Analyze HV gear database for 2H physical build optimization. +Sources: equipped (character), armory (inventory), buy (bazaar listings). +""" +import json def get_num(val): if not val: return 0 @@ -23,7 +25,6 @@ def score_armor(item): s += item.get('quality',0) * 5 return s -# Map item names to equipment slots by keyword SLOT_KEYWORDS = { 'Head': ['helmet','cap','goggles','mask','hood','hat','circlet','crown','visor'], 'Body': ['breastplate','cuirass','robe','tunic','chest','gi','jacket','vest','hauberk'], @@ -33,30 +34,22 @@ SLOT_KEYWORDS = { } def detect_slot(item): - """Guess equipment slot from item name.""" name = (item.get('name','') + ' ' + item.get('category','')).lower() for slot, keywords in SLOT_KEYWORDS.items(): for kw in keywords: if kw in name: return slot - stats = item.get('stats',{}) - # Fallback: use proportional scoring return None def is_twohand_weapon(item): - """Check if item is a 2H weapon (not shield, not armor, not 1H).""" stats = item.get('stats',{}) itype = stats.get('type','') - # Exclude armor types - if 'Armor' in itype or 'Shield' in itype: + if 'Armor' in itype or 'Shield' in itype or 'Staff' in itype: return False - # Check for 2H weapon keywords name_lower = (item.get('name','') + ' ' + item.get('category','')).lower() if any(k in name_lower for k in ['estoc','longsword','great mace','scythe','axe','club']): return True - # Fallback: burden >= 14 AND has attack accuracy (not burden from armor) if get_num(stats.get('burden',0)) >= 14 and 'attack accuracy' in str(stats).lower(): - # Double check it's not armor with attack damage bonus if 'physical mitigation' not in str(stats).lower(): return True return False @@ -74,86 +67,88 @@ def score_weapon(item): s += item.get('quality',0) * 5 return s -# Filter items by usable level (within 20 levels of player level 153) -def usable(item): +def usable(item, lv=155): stats = item.get('stats',{}) - lv = stats.get('level','0') - if lv == 'Unassigned': return True - try: lv = int(lv) + level = stats.get('level','0') + if level == 'Unassigned': return True + try: return int(level) <= lv + 15 except: return True - return lv <= 170 # allow up to L170 (within reach) + +def lv_str(item): + stats = item.get('stats',{}) + return str(stats.get('level','?')) + +def src_label(item): + src = item.get('source','?') + return {'character': '🟢 Equipped', 'armory': 'šŸ“¦ Stored', 'buy': 'šŸ›’ For Sale'}.get(src, f'ā“ {src}') with open('references/gear.json') as f: gear = json.load(f) equipped = [i for i in gear if i.get('source') == 'character' and i.get('id') and not i.get('disabled')] armory = [i for i in gear if i.get('source') == 'armory' and i.get('stats')] +buy = [i for i in gear if i.get('source') == 'buy' and i.get('stats')] + +print(f"šŸ“Š Loaded: {len(equipped)} equipped, {len(armory)} stored, {len(buy)} purchasable") +print(f"šŸ”’ Level cap for equipping: Lv155 (player) + 15 = Lv170") +print() slot_map = {e.get('slotType',''): e for e in equipped} -print("=" * 70) -print("2H PHYSICAL BUILD — GEAR ANALYSIS (Lv153, Estoc, IWBTH)") -print("=" * 70) - for slot_type in ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']: current = slot_map.get(slot_type) + print(f"\n{'='*55}") if not current: - print(f"\nšŸ›”ļø {slot_type}: No equipped item") + print(f"šŸ›”ļø {slot_type}: EMPTY") continue - print(f"\n━━━ {slot_type} ━━━") - cur_stats = current.get('stats',{}) - print(f" šŸ—” {current['name']} (q{current.get('quality','?')})") - candidates = [] - for item in armory: + cur_stats = current.get('stats',{}) + cur_lv = cur_stats.get('level','?') + print(f"šŸ›”ļø {slot_type}: {current['name']} (q{current.get('quality','?')}, Lv{cur_lv})") + print(f" ═══ CURRENTLY EQUIPPED ═══") + if slot_type == 'Mainhand': + print(f" Acc:{cur_stats.get('Attack Accuracy','?')} CritDam:{cur_stats.get('Attack Crit Damage','?')} STR:{cur_stats.get('Strength','?')} Parry:{cur_stats.get('Parry','?')} Block:{cur_stats.get('Block','?')} Burden:{cur_stats.get('burden','?')}") + else: + print(f" PMit:{cur_stats.get('Physical Mitigation','?')} Evade:{cur_stats.get('Evade','?')} STR:{cur_stats.get('Strength','?')} DEX:{cur_stats.get('Dexterity','?')} AGI:{cur_stats.get('Agility','?')} Burden:{cur_stats.get('burden','?')}") + + # Show alternatives from ALL sources, sorted by score + all_items = [] + for item in armory + buy: stats = item.get('stats',{}) - if not usable(item): continue + if not usable(item, 155): continue if slot_type == 'Mainhand': if not is_twohand_weapon(item): continue sfunc = score_weapon else: if stats.get('type') != 'Light Armor': continue - # Match by slot type - detected = detect_slot(item) - if detected != slot_type: continue - sfunc = score_armor - candidates.append((sfunc(item), item)) - - candidates.sort(key=lambda x: -x[0]) - - for score, item in candidates[:5]: - stats = item.get('stats',{}) - marker = ' ⬅ EQUIPPED' if item['name'] == current['name'] else '' - if slot_type == 'Mainhand': - print(f" {score:7.1f} | {item['name']} ({item.get('quality','?')}){marker}") - print(f" Acc:{stats.get('Attack Accuracy','?')} CritDam:{stats.get('Attack Crit Damage','?')} STR:{stats.get('Strength','?')} B:{stats.get('burden','?')}") - else: - print(f" {score:7.1f} | {item['name']} (q{item.get('quality','?')}) | Lv{stats.get('level','?')}{marker}") - print(f" PMit:{stats.get('Physical Mitigation','?')} Evade:{stats.get('Evade','?')} STR:{stats.get('Strength','?')} B:{stats.get('burden','?')}") - -print("\n" + "=" * 70) -print("šŸ† UPGRADE RECOMMENDATIONS") -print("=" * 70) -for slot_type in ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']: - current = slot_map.get(slot_type) - if not current: continue - candidates = [] - for item in armory: - stats = item.get('stats',{}) - if not stats.get('type'): continue - if not usable(item): continue - if slot_type == 'Mainhand': - if not is_twohand_weapon(item): continue - else: - if stats['type'] != 'Light Armor': continue if detect_slot(item) != slot_type: continue - sfunc = score_weapon if slot_type == 'Mainhand' else score_armor - candidates.append((sfunc(item), item)) - candidates.sort(key=lambda x: -x[0]) - if not candidates: continue - top = candidates[0] - if top[1]['name'] != current['name']: - print(f"\nāŒ {slot_type}: {current['name']}") - print(f"āœ… → {top[1]['name']} (q{top[1].get('quality','?')}, score {top[0]:.1f})") - else: - print(f"\nāœ… {slot_type}: {current['name']} — already best (score {top[0]:.1f})") + sfunc = score_armor + all_items.append((sfunc(item), item)) + + all_items.sort(key=lambda x: -x[0]) + + if all_items: + print(f"\n šŸ“Š TOP 5 ALTERNATIVES (ranked by score):") + for i, (score, item) in enumerate(all_items[:5]): + src = src_label(item) + stats = item.get('stats',{}) + lv = lv_str(item) + price = item.get('price','') + price_str = f' | šŸ’°{price}' if price else '' + marker = ' ⬅ EQUIPPED' if item['name'] == current['name'] else '' + print(f" {i+1}. [{score:6.1f}] {src} | {item['name']} (q{item.get('quality','?')}, Lv{lv}){price_str}{marker}") + if slot_type == 'Mainhand': + print(f" Acc:{stats.get('Attack Accuracy','?')} CritDam:{stats.get('Attack Crit Damage','?')} STR:{stats.get('Strength','?')}") + else: + print(f" PMit:{stats.get('Physical Mitigation','?')} Evade:{stats.get('Evade','?')} STR:{stats.get('Strength','?')} DEX:{stats.get('Dexterity','?')}") + + # Check if current equipped is still best + if all_items: + best = all_items[0] + if best[1]['name'] != current['name']: + print(f"\n āŒ NOT OPTIMAL — upgrade available!") + print(f" āœ… {src_label(best[1])}: {best[1]['name']} (score {best[0]:.1f})") + if best[1].get('source') == 'buy': + print(f" šŸ’° Price: ${best[1].get('price','?')}") + else: + print(f"\n āœ… ALREADY BEST (score {best[0]:.1f})") diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index 08e6566..af885c2 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.18 +// @version 0.14.19 // @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.18'; +const VERSION = '0.14.19'; const CFG = { // — Battle automation @@ -3504,12 +3504,64 @@ function debugScrapeGear() { return { url, storeCount, slots, rows }; } +// ── Scrape Bazaar Buy page (equipment for sale by other players) ── +// URL: ?s=Bazaar&ss=am&screen=buy or ?s=Bazaar&ss=bi +function scrapeBuyPage() { + const scraped = []; + const el = document.getElementById('equiplist'); + if (!el) return scraped; + let cat = ''; + + el.querySelectorAll('table tr').forEach(row => { + if (row.classList.contains('eqtplabel')) { cat = (row.textContent || '').trim(); return; } + if (row.classList.contains('eqselall')) return; + const omo = row.getAttribute('onmouseover') || ''; + const idM = omo.match(/hover_equip\((\d+)\)/); + const cb = row.querySelector('input[name="eqids[]"]'); + const itemId = idM ? idM[1] : (cb ? cb.value : ''); + const label = row.querySelector('label'); + let name = label ? (label.textContent || '').trim() : ''; + if (cb && label) name = (label.textContent || '').replace(cb.outerHTML, '').trim(); + + // Try to read price from buy pages (often in additional columns or cell text) + let price = ''; + if (!idM && !name && !itemId) return; + const cells = row.querySelectorAll('td'); + if (cells.length > 3) { + const lastCell = cells[cells.length - 1]; + if (lastCell) price = (lastCell.textContent || '').trim(); + } + + const obj = { id: itemId, name: name || 'Unknown', category: cat, price, source: 'buy', scrapedAt: Date.now() }; + if (itemId) { + const data = getHVEquipData(itemId); + if (data) { + if (data.t) obj.name = data.t; + if (data.q != null) obj.quality = data.q; + if (data.d) obj.stats = parseEquipHTML(data.d); + } + } + scraped.push(obj); + }); + + if (scraped.length > 0) { + const db = getGearDB(); + // Don't overwrite equipped/armory items — merge buy items + const existing = db.filter(e => e.source !== 'buy'); + saveGearDB([...existing, ...scraped]); + console.log(`%c[HV] šŸ›’ Buy: ${scraped.length} items (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0'); + } + return scraped; +} + // ── Auto-detect ── function autoScrapeGear() { const url = window.location.href || ''; if (url.includes('ss=eq') || url.includes('ss=ch')) return scrapeCharacterEquip(); - if (url.includes('ss=am') && !url.includes('screen=modify')) return scrapeArmory(); + if (url.includes('ss=am') && (url.includes('screen=organize') || !url.includes('screen='))) return scrapeArmory(); + if (url.includes('ss=am') && url.includes('screen=buy')) return scrapeBuyPage(); if (url.includes('ss=am') && url.includes('screen=modify')) return scrapeModifyDetail(); + if (url.includes('ss=bi')) return scrapeBuyPage(); return []; } diff --git a/scripts/latest.user.js b/scripts/latest.user.js index 08e6566..af885c2 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.14.18 +// @version 0.14.19 // @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.18'; +const VERSION = '0.14.19'; const CFG = { // — Battle automation @@ -3504,12 +3504,64 @@ function debugScrapeGear() { return { url, storeCount, slots, rows }; } +// ── Scrape Bazaar Buy page (equipment for sale by other players) ── +// URL: ?s=Bazaar&ss=am&screen=buy or ?s=Bazaar&ss=bi +function scrapeBuyPage() { + const scraped = []; + const el = document.getElementById('equiplist'); + if (!el) return scraped; + let cat = ''; + + el.querySelectorAll('table tr').forEach(row => { + if (row.classList.contains('eqtplabel')) { cat = (row.textContent || '').trim(); return; } + if (row.classList.contains('eqselall')) return; + const omo = row.getAttribute('onmouseover') || ''; + const idM = omo.match(/hover_equip\((\d+)\)/); + const cb = row.querySelector('input[name="eqids[]"]'); + const itemId = idM ? idM[1] : (cb ? cb.value : ''); + const label = row.querySelector('label'); + let name = label ? (label.textContent || '').trim() : ''; + if (cb && label) name = (label.textContent || '').replace(cb.outerHTML, '').trim(); + + // Try to read price from buy pages (often in additional columns or cell text) + let price = ''; + if (!idM && !name && !itemId) return; + const cells = row.querySelectorAll('td'); + if (cells.length > 3) { + const lastCell = cells[cells.length - 1]; + if (lastCell) price = (lastCell.textContent || '').trim(); + } + + const obj = { id: itemId, name: name || 'Unknown', category: cat, price, source: 'buy', scrapedAt: Date.now() }; + if (itemId) { + const data = getHVEquipData(itemId); + if (data) { + if (data.t) obj.name = data.t; + if (data.q != null) obj.quality = data.q; + if (data.d) obj.stats = parseEquipHTML(data.d); + } + } + scraped.push(obj); + }); + + if (scraped.length > 0) { + const db = getGearDB(); + // Don't overwrite equipped/armory items — merge buy items + const existing = db.filter(e => e.source !== 'buy'); + saveGearDB([...existing, ...scraped]); + console.log(`%c[HV] šŸ›’ Buy: ${scraped.length} items (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0'); + } + return scraped; +} + // ── Auto-detect ── function autoScrapeGear() { const url = window.location.href || ''; if (url.includes('ss=eq') || url.includes('ss=ch')) return scrapeCharacterEquip(); - if (url.includes('ss=am') && !url.includes('screen=modify')) return scrapeArmory(); + if (url.includes('ss=am') && (url.includes('screen=organize') || !url.includes('screen='))) return scrapeArmory(); + if (url.includes('ss=am') && url.includes('screen=buy')) return scrapeBuyPage(); if (url.includes('ss=am') && url.includes('screen=modify')) return scrapeModifyDetail(); + if (url.includes('ss=bi')) return scrapeBuyPage(); return []; } diff --git a/src/config.js b/src/config.js index 6089202..c24144f 100644 --- a/src/config.js +++ b/src/config.js @@ -2,7 +2,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.14.18'; +const VERSION = '0.14.19'; const CFG = { // — Battle automation diff --git a/src/gear-scraper.js b/src/gear-scraper.js index a2c7b4c..27f4fbb 100644 --- a/src/gear-scraper.js +++ b/src/gear-scraper.js @@ -233,12 +233,64 @@ function debugScrapeGear() { return { url, storeCount, slots, rows }; } +// ── Scrape Bazaar Buy page (equipment for sale by other players) ── +// URL: ?s=Bazaar&ss=am&screen=buy or ?s=Bazaar&ss=bi +function scrapeBuyPage() { + const scraped = []; + const el = document.getElementById('equiplist'); + if (!el) return scraped; + let cat = ''; + + el.querySelectorAll('table tr').forEach(row => { + if (row.classList.contains('eqtplabel')) { cat = (row.textContent || '').trim(); return; } + if (row.classList.contains('eqselall')) return; + const omo = row.getAttribute('onmouseover') || ''; + const idM = omo.match(/hover_equip\((\d+)\)/); + const cb = row.querySelector('input[name="eqids[]"]'); + const itemId = idM ? idM[1] : (cb ? cb.value : ''); + const label = row.querySelector('label'); + let name = label ? (label.textContent || '').trim() : ''; + if (cb && label) name = (label.textContent || '').replace(cb.outerHTML, '').trim(); + + // Try to read price from buy pages (often in additional columns or cell text) + let price = ''; + if (!idM && !name && !itemId) return; + const cells = row.querySelectorAll('td'); + if (cells.length > 3) { + const lastCell = cells[cells.length - 1]; + if (lastCell) price = (lastCell.textContent || '').trim(); + } + + const obj = { id: itemId, name: name || 'Unknown', category: cat, price, source: 'buy', scrapedAt: Date.now() }; + if (itemId) { + const data = getHVEquipData(itemId); + if (data) { + if (data.t) obj.name = data.t; + if (data.q != null) obj.quality = data.q; + if (data.d) obj.stats = parseEquipHTML(data.d); + } + } + scraped.push(obj); + }); + + if (scraped.length > 0) { + const db = getGearDB(); + // Don't overwrite equipped/armory items — merge buy items + const existing = db.filter(e => e.source !== 'buy'); + saveGearDB([...existing, ...scraped]); + console.log(`%c[HV] šŸ›’ Buy: ${scraped.length} items (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0'); + } + return scraped; +} + // ── Auto-detect ── function autoScrapeGear() { const url = window.location.href || ''; if (url.includes('ss=eq') || url.includes('ss=ch')) return scrapeCharacterEquip(); - if (url.includes('ss=am') && !url.includes('screen=modify')) return scrapeArmory(); + if (url.includes('ss=am') && (url.includes('screen=organize') || !url.includes('screen='))) return scrapeArmory(); + if (url.includes('ss=am') && url.includes('screen=buy')) return scrapeBuyPage(); if (url.includes('ss=am') && url.includes('screen=modify')) return scrapeModifyDetail(); + if (url.includes('ss=bi')) return scrapeBuyPage(); return []; } diff --git a/src/header.user.js b/src/header.user.js index fd75431..171aee1 100644 --- a/src/header.user.js +++ b/src/header.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.14.18 +// @version 0.14.19 // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @author GaboGG + Hermes // @match *://*.hentaiverse.org/*