v0.14.19 - Add Bazaar Buy page scraper + 3-source analysis
- 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
This commit is contained in:
parent
94ab29a82c
commit
f2e8ce536a
6 changed files with 232 additions and 81 deletions
137
analyze_gear.py
137
analyze_gear.py
|
|
@ -1,6 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Analyze HV gear database for 2H physical build optimization."""
|
"""Analyze HV gear database for 2H physical build optimization.
|
||||||
import json, sys
|
Sources: equipped (character), armory (inventory), buy (bazaar listings).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
def get_num(val):
|
def get_num(val):
|
||||||
if not val: return 0
|
if not val: return 0
|
||||||
|
|
@ -23,7 +25,6 @@ def score_armor(item):
|
||||||
s += item.get('quality',0) * 5
|
s += item.get('quality',0) * 5
|
||||||
return s
|
return s
|
||||||
|
|
||||||
# Map item names to equipment slots by keyword
|
|
||||||
SLOT_KEYWORDS = {
|
SLOT_KEYWORDS = {
|
||||||
'Head': ['helmet','cap','goggles','mask','hood','hat','circlet','crown','visor'],
|
'Head': ['helmet','cap','goggles','mask','hood','hat','circlet','crown','visor'],
|
||||||
'Body': ['breastplate','cuirass','robe','tunic','chest','gi','jacket','vest','hauberk'],
|
'Body': ['breastplate','cuirass','robe','tunic','chest','gi','jacket','vest','hauberk'],
|
||||||
|
|
@ -33,30 +34,22 @@ SLOT_KEYWORDS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
def detect_slot(item):
|
def detect_slot(item):
|
||||||
"""Guess equipment slot from item name."""
|
|
||||||
name = (item.get('name','') + ' ' + item.get('category','')).lower()
|
name = (item.get('name','') + ' ' + item.get('category','')).lower()
|
||||||
for slot, keywords in SLOT_KEYWORDS.items():
|
for slot, keywords in SLOT_KEYWORDS.items():
|
||||||
for kw in keywords:
|
for kw in keywords:
|
||||||
if kw in name:
|
if kw in name:
|
||||||
return slot
|
return slot
|
||||||
stats = item.get('stats',{})
|
|
||||||
# Fallback: use proportional scoring
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def is_twohand_weapon(item):
|
def is_twohand_weapon(item):
|
||||||
"""Check if item is a 2H weapon (not shield, not armor, not 1H)."""
|
|
||||||
stats = item.get('stats',{})
|
stats = item.get('stats',{})
|
||||||
itype = stats.get('type','')
|
itype = stats.get('type','')
|
||||||
# Exclude armor types
|
if 'Armor' in itype or 'Shield' in itype or 'Staff' in itype:
|
||||||
if 'Armor' in itype or 'Shield' in itype:
|
|
||||||
return False
|
return False
|
||||||
# Check for 2H weapon keywords
|
|
||||||
name_lower = (item.get('name','') + ' ' + item.get('category','')).lower()
|
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']):
|
if any(k in name_lower for k in ['estoc','longsword','great mace','scythe','axe','club']):
|
||||||
return True
|
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():
|
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():
|
if 'physical mitigation' not in str(stats).lower():
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
@ -74,86 +67,88 @@ def score_weapon(item):
|
||||||
s += item.get('quality',0) * 5
|
s += item.get('quality',0) * 5
|
||||||
return s
|
return s
|
||||||
|
|
||||||
# Filter items by usable level (within 20 levels of player level 153)
|
def usable(item, lv=155):
|
||||||
def usable(item):
|
|
||||||
stats = item.get('stats',{})
|
stats = item.get('stats',{})
|
||||||
lv = stats.get('level','0')
|
level = stats.get('level','0')
|
||||||
if lv == 'Unassigned': return True
|
if level == 'Unassigned': return True
|
||||||
try: lv = int(lv)
|
try: return int(level) <= lv + 15
|
||||||
except: return True
|
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:
|
with open('references/gear.json') as f:
|
||||||
gear = json.load(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')]
|
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')]
|
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}
|
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']:
|
for slot_type in ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']:
|
||||||
current = slot_map.get(slot_type)
|
current = slot_map.get(slot_type)
|
||||||
|
print(f"\n{'='*55}")
|
||||||
if not current:
|
if not current:
|
||||||
print(f"\n🛡️ {slot_type}: No equipped item")
|
print(f"🛡️ {slot_type}: EMPTY")
|
||||||
continue
|
continue
|
||||||
print(f"\n━━━ {slot_type} ━━━")
|
|
||||||
cur_stats = current.get('stats',{})
|
|
||||||
print(f" 🗡 {current['name']} (q{current.get('quality','?')})")
|
|
||||||
|
|
||||||
candidates = []
|
cur_stats = current.get('stats',{})
|
||||||
for item in armory:
|
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',{})
|
stats = item.get('stats',{})
|
||||||
if not usable(item): continue
|
if not usable(item, 155): continue
|
||||||
if slot_type == 'Mainhand':
|
if slot_type == 'Mainhand':
|
||||||
if not is_twohand_weapon(item): continue
|
if not is_twohand_weapon(item): continue
|
||||||
sfunc = score_weapon
|
sfunc = score_weapon
|
||||||
else:
|
else:
|
||||||
if stats.get('type') != 'Light Armor': continue
|
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
|
if detect_slot(item) != slot_type: continue
|
||||||
sfunc = score_weapon if slot_type == 'Mainhand' else score_armor
|
sfunc = score_armor
|
||||||
candidates.append((sfunc(item), item))
|
all_items.append((sfunc(item), item))
|
||||||
candidates.sort(key=lambda x: -x[0])
|
|
||||||
if not candidates: continue
|
all_items.sort(key=lambda x: -x[0])
|
||||||
top = candidates[0]
|
|
||||||
if top[1]['name'] != current['name']:
|
if all_items:
|
||||||
print(f"\n❌ {slot_type}: {current['name']}")
|
print(f"\n 📊 TOP 5 ALTERNATIVES (ranked by score):")
|
||||||
print(f"✅ → {top[1]['name']} (q{top[1].get('quality','?')}, score {top[0]:.1f})")
|
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:
|
else:
|
||||||
print(f"\n✅ {slot_type}: {current['name']} — already best (score {top[0]:.1f})")
|
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})")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.14.18
|
// @version 0.14.19
|
||||||
// @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.18';
|
const VERSION = '0.14.19';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -3504,12 +3504,64 @@ function debugScrapeGear() {
|
||||||
return { url, storeCount, slots, rows };
|
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 ──
|
// ── Auto-detect ──
|
||||||
function autoScrapeGear() {
|
function autoScrapeGear() {
|
||||||
const url = window.location.href || '';
|
const url = window.location.href || '';
|
||||||
if (url.includes('ss=eq') || url.includes('ss=ch')) return scrapeCharacterEquip();
|
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=am') && url.includes('screen=modify')) return scrapeModifyDetail();
|
||||||
|
if (url.includes('ss=bi')) return scrapeBuyPage();
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.14.18
|
// @version 0.14.19
|
||||||
// @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.18';
|
const VERSION = '0.14.19';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -3504,12 +3504,64 @@ function debugScrapeGear() {
|
||||||
return { url, storeCount, slots, rows };
|
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 ──
|
// ── Auto-detect ──
|
||||||
function autoScrapeGear() {
|
function autoScrapeGear() {
|
||||||
const url = window.location.href || '';
|
const url = window.location.href || '';
|
||||||
if (url.includes('ss=eq') || url.includes('ss=ch')) return scrapeCharacterEquip();
|
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=am') && url.includes('screen=modify')) return scrapeModifyDetail();
|
||||||
|
if (url.includes('ss=bi')) return scrapeBuyPage();
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.14.18';
|
const VERSION = '0.14.19';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
|
||||||
|
|
@ -233,12 +233,64 @@ function debugScrapeGear() {
|
||||||
return { url, storeCount, slots, rows };
|
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 ──
|
// ── Auto-detect ──
|
||||||
function autoScrapeGear() {
|
function autoScrapeGear() {
|
||||||
const url = window.location.href || '';
|
const url = window.location.href || '';
|
||||||
if (url.includes('ss=eq') || url.includes('ss=ch')) return scrapeCharacterEquip();
|
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=am') && url.includes('screen=modify')) return scrapeModifyDetail();
|
||||||
|
if (url.includes('ss=bi')) return scrapeBuyPage();
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.14.18
|
// @version 0.14.19
|
||||||
// @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/*
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue