v0.14.33 - In-game gear analysis module
New src/gear-analysis.js — ports analyze_gear.py scoring to JS: - gearScoreArmor() / gearScoreWeapon() — same weights as Python - gearDetectSlot() / gearIsTwoHandWeapon() / gearUsable() - analyzeGear() — scores equipped vs armory vs buy per slot UI on Armory pages (above equiplist): - 🔍 Analyze Gear — rescrapes current page, shows per-slot panel with top 4 candidates, scores, prices, and upgrade highlights - 🔄 Clear Buy + Rescan — wipes stale buy data, rescrapes, analyzes scrapeBuyPage() now also scavenges the full dynjs_eqstore for items not visible in the current filter tab — one Purchase visit captures the entire store (429+ items), no need to click through tabs. Console API: HV.analyzeGear(), HV.gearPanel(), HV.clearBuy()
This commit is contained in:
parent
6ac31faa1f
commit
166539e845
9 changed files with 990 additions and 9 deletions
|
|
@ -32,6 +32,7 @@ FILES=(
|
|||
abilities.js
|
||||
armory.js
|
||||
gear-scraper.js
|
||||
gear-analysis.js
|
||||
battle-logger.js
|
||||
re-timer.js
|
||||
settings-page.js
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.14.32
|
||||
// @version 0.14.33
|
||||
// @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.32';
|
||||
const VERSION = '0.14.33';
|
||||
|
||||
const CFG = {
|
||||
// — Battle automation
|
||||
|
|
@ -3615,6 +3615,33 @@ function scrapeBuyPage() {
|
|||
saveGearDB([...existing, ...scraped]);
|
||||
console.log(`%c[HV] 🛒 Buy: ${scraped.length} items (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0');
|
||||
}
|
||||
|
||||
// Second, scan the full store for items NOT in the visible list
|
||||
// The Purchase page loads ALL items into dynjs_eqstore regardless of
|
||||
// the filter tab — one visit captures the whole store.
|
||||
const store = getHVEquipStore();
|
||||
if (store) {
|
||||
const visibleIds = new Set(scraped.map(s => s.id));
|
||||
const missing = Object.entries(store).filter(([id, data]) => !visibleIds.has(id) && data && (data.t || data.d));
|
||||
if (missing.length > 0) {
|
||||
missing.forEach(([id, data]) => {
|
||||
const stats = data.d ? parseEquipHTML(data.d) : null;
|
||||
let category = 'All Items';
|
||||
if (stats && stats.type) {
|
||||
if (stats.type.includes('Cloth')) category = 'Cloth Armor';
|
||||
else if (stats.type.includes('Light')) category = 'Light Armor';
|
||||
else if (stats.type.includes('Heavy')) category = 'Heavy Armor';
|
||||
else if (stats.type.includes('Shield')) category = 'Shield';
|
||||
else if (stats.type.includes('Staff')) category = 'Staff';
|
||||
else category = 'Weapon';
|
||||
}
|
||||
scraped.push({ id, name: data.t || 'Unknown', category, quality: data.q, stats, source: 'buy', scrapedAt: Date.now() });
|
||||
});
|
||||
const db2 = getGearDB();
|
||||
saveGearDB([...db2.filter(e => e.source !== 'buy'), ...scraped]);
|
||||
console.log(`%c[HV] 🛒 Buy: +${missing.length} from store (total ${scraped.length})`, 'color:#0f0');
|
||||
}
|
||||
}
|
||||
return scraped;
|
||||
}
|
||||
|
||||
|
|
@ -3679,6 +3706,302 @@ function scrapeModifyDetail() {
|
|||
return item;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GEAR ANALYSIS — in-game gear comparison (port of analyze_gear.py)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Adds a toolbar button on Armory pages. Clicking it:
|
||||
// 1. Clears old buy data (optional)
|
||||
// 2. Rescrapes the current page (store or inventory)
|
||||
// 3. Scores all items and shows per-slot recommendations
|
||||
// Sources: 🟢 equipped (character), 📦 stored (armory), 🛒 purchasable (buy)
|
||||
|
||||
function _num(v) {
|
||||
if (v == null) return 0;
|
||||
const s = String(v).replace('+', '').replace('%', '').trim();
|
||||
const n = parseFloat(s);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
// ── Scoring (mirrors analyze_gear.py) ──
|
||||
function gearScoreArmor(item) {
|
||||
const st = item.stats || {};
|
||||
let s = 0;
|
||||
s += _num(st['Physical Mitigation']) * 3;
|
||||
s += _num(st['Evade']) * 2;
|
||||
s += _num(st['Strength']) * 2;
|
||||
s += _num(st['Dexterity']) * 2;
|
||||
s += _num(st['Agility']) * 1.5;
|
||||
s += _num(st['Endurance']);
|
||||
s += _num(st['Crushing']);
|
||||
s += _num(st['Slashing']);
|
||||
s += _num(st['Piercing']);
|
||||
s -= _num(st['burden']) * 0.5;
|
||||
s += (item.quality || 0) * 5;
|
||||
return s;
|
||||
}
|
||||
|
||||
function gearScoreWeapon(item) {
|
||||
const st = item.stats || {};
|
||||
const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase();
|
||||
const quality = item.quality || 0;
|
||||
let s = 0;
|
||||
s += _num(st['Attack Accuracy']) * 2;
|
||||
s += _num(st['Attack Crit Damage']) * 60;
|
||||
s += _num(st['Strength']) * 2;
|
||||
s += _num(st['Dexterity']) * 1.5;
|
||||
s += _num(st['Parry']) * 0.5;
|
||||
s += _num(st['Block']) * 0.5;
|
||||
s += _num(st['Agility']) * 0.5;
|
||||
s -= _num(st['burden']) * 0.5;
|
||||
// Quality multiplier (q6=Magnificent, q5=Exquisite, q4=Superior)
|
||||
s *= 0.8 + (quality * 0.12);
|
||||
// Weapon type bonus
|
||||
if (name.includes('estoc')) s *= 1.25;
|
||||
else if (name.includes('longsword')) s *= 1.10;
|
||||
else if (name.includes('great mace')) s *= 0.90;
|
||||
else if (name.includes('club')) s *= 0.85;
|
||||
else if (name.includes('axe')) s *= 0.90;
|
||||
return s;
|
||||
}
|
||||
|
||||
// ── Slot detection ──
|
||||
const GEAR_SLOT_KEYWORDS = {
|
||||
'Head': ['helmet','cap','goggles','mask','hood','hat','circlet','crown','visor'],
|
||||
'Body': ['breastplate','cuirass','robe','tunic','chest','gi','jacket','vest','hauberk'],
|
||||
'Hands': ['gauntlets','gloves','mitts','bracers','handguards','fists'],
|
||||
'Legs': ['leggings','greaves','pants','trousers','loincloth','kilt','breeches','chaps'],
|
||||
'Feet': ['boots','sabatons','shoes','slippers','sandals'],
|
||||
};
|
||||
|
||||
function gearDetectSlot(item) {
|
||||
const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase();
|
||||
for (const [slot, kws] of Object.entries(GEAR_SLOT_KEYWORDS)) {
|
||||
for (const kw of kws) {
|
||||
if (name.includes(kw)) return slot;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function gearIsTwoHandWeapon(item) {
|
||||
const st = item.stats || {};
|
||||
const itype = st.type || '';
|
||||
if (itype.includes('Armor') || itype.includes('Shield') || itype.includes('Staff')) return false;
|
||||
const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase();
|
||||
if (['estoc','longsword','great mace','scythe','axe','club'].some(k => name.includes(k))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function gearUsable(item, playerLv) {
|
||||
const lv = (item.stats || {}).level;
|
||||
if (!lv || lv === 'Unassigned') return true;
|
||||
const n = parseInt(lv);
|
||||
if (isNaN(n)) return true;
|
||||
return n <= (playerLv || 155) + 15;
|
||||
}
|
||||
|
||||
function gearPlayerLevel() {
|
||||
const el = document.getElementById('level_readout');
|
||||
if (el) {
|
||||
const m = (el.textContent || '').match(/(\d+)/);
|
||||
if (m) return parseInt(m[1]);
|
||||
}
|
||||
return 155;
|
||||
}
|
||||
|
||||
// ── Main analysis ──
|
||||
function analyzeGear() {
|
||||
const db = getGearDB();
|
||||
const equipped = db.filter(e => e.source === 'character' && e.id);
|
||||
const armory = db.filter(e => e.source === 'armory' && e.stats);
|
||||
const buy = db.filter(e => e.source === 'buy' && e.stats);
|
||||
const playerLv = gearPlayerLevel();
|
||||
|
||||
// Find equipped slot map (slotType from character page)
|
||||
const slotMap = {};
|
||||
equipped.forEach(e => { if (e.slotType) slotMap[e.slotType] = e; });
|
||||
|
||||
const results = [];
|
||||
const slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet'];
|
||||
|
||||
slots.forEach(slot => {
|
||||
const current = slotMap[slot];
|
||||
const cands = [];
|
||||
|
||||
const consider = (item, source) => {
|
||||
if (!item || !item.stats) return;
|
||||
if (!gearUsable(item, playerLv)) return;
|
||||
const isWeapon = slot === 'Mainhand';
|
||||
if (isWeapon) {
|
||||
if (!gearIsTwoHandWeapon(item)) return;
|
||||
if (!item.name || !item.stats['Attack Accuracy']) return; // needs stats
|
||||
} else {
|
||||
const det = gearDetectSlot(item);
|
||||
if (det !== slot) return;
|
||||
if (!item.stats['Physical Mitigation'] && !item.stats['Evade']) return;
|
||||
}
|
||||
const score = isWeapon ? gearScoreWeapon(item) : gearScoreArmor(item);
|
||||
cands.push({ item, source, score });
|
||||
};
|
||||
|
||||
if (current) consider(current, 'equipped');
|
||||
armory.forEach(i => consider(i, 'armory'));
|
||||
buy.forEach(i => consider(i, 'buy'));
|
||||
|
||||
cands.sort((a, b) => b.score - a.score);
|
||||
results.push({ slot, current, cands });
|
||||
});
|
||||
|
||||
return { playerLv, results };
|
||||
}
|
||||
|
||||
// ── UI ──
|
||||
function buildGearPanel() {
|
||||
// Remove old panel
|
||||
const old = document.getElementById('hv-gear-panel');
|
||||
if (old) old.remove();
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'hv-gear-panel';
|
||||
panel.style.cssText = css({
|
||||
position: 'fixed', right: '8px', top: '48px', zIndex: 99999,
|
||||
width: '420px', maxHeight: '80vh', overflowY: 'auto',
|
||||
background: '#12121e', border: '1px solid #333', borderRadius: '6px',
|
||||
padding: '8px', fontSize: '11px', fontFamily: 'monospace',
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,.6)',
|
||||
});
|
||||
|
||||
const close = document.createElement('button');
|
||||
close.textContent = '✕';
|
||||
close.style.cssText = 'float:right;cursor:pointer;background:#333;border:none;color:#fff;border-radius:3px;padding:2px 6px';
|
||||
close.onclick = () => panel.remove();
|
||||
panel.appendChild(close);
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.textContent = '🛡️ Gear Analysis';
|
||||
title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd';
|
||||
panel.appendChild(title);
|
||||
|
||||
const { results } = analyzeGear();
|
||||
|
||||
results.forEach(({ slot, current, cands }) => {
|
||||
const block = document.createElement('div');
|
||||
block.style.cssText = 'margin-bottom:10px;border-top:1px solid #222;padding-top:6px';
|
||||
|
||||
const head = document.createElement('div');
|
||||
head.textContent = `${slot}: ${current ? current.name : '—'}`;
|
||||
head.style.cssText = 'color:#9cf;font-weight:bold;margin-bottom:2px';
|
||||
block.appendChild(head);
|
||||
|
||||
if (!cands.length) {
|
||||
const none = document.createElement('div');
|
||||
none.textContent = ' (no data)';
|
||||
none.style.cssText = 'color:#666';
|
||||
block.appendChild(none);
|
||||
panel.appendChild(block);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show top 4 candidates
|
||||
cands.slice(0, 4).forEach((c, i) => {
|
||||
const icon = c.source === 'equipped' ? '🟢' : c.source === 'armory' ? '📦' : '🛒';
|
||||
const row = document.createElement('div');
|
||||
const price = c.item.price ? ` 💰${c.item.price}` : '';
|
||||
const isCurrent = c.source === 'equipped';
|
||||
row.textContent = ` ${i + 1}. [${c.score.toFixed(0)}] ${icon} ${c.item.name}${isCurrent ? ' ⬅' : ''}${price}`;
|
||||
row.style.cssText = isCurrent ? 'color:#8f8' : 'color:#ccc';
|
||||
if (i === 0 && !isCurrent) row.style.color = '#ff0';
|
||||
block.appendChild(row);
|
||||
});
|
||||
|
||||
// Highlight best option
|
||||
const best = cands[0];
|
||||
if (best && best.source !== 'equipped' && current) {
|
||||
const rec = document.createElement('div');
|
||||
const icon = best.source === 'armory' ? '📦 Stored' : '🛒 For Sale';
|
||||
rec.textContent = ` ✅ Upgrade: ${icon} ${best.item.name} (${best.score.toFixed(0)} vs ${cands.find(c => c.source === 'equipped')?.score.toFixed(0) || '?'})`;
|
||||
rec.style.cssText = 'color:#0f0;margin-top:2px';
|
||||
block.appendChild(rec);
|
||||
} else if (best && best.source === 'equipped') {
|
||||
const ok = document.createElement('div');
|
||||
ok.textContent = ' ✅ Already best';
|
||||
ok.style.cssText = 'color:#0f0';
|
||||
block.appendChild(ok);
|
||||
}
|
||||
|
||||
panel.appendChild(block);
|
||||
});
|
||||
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
|
||||
function enhanceGearAnalysis() {
|
||||
// Only on Bazaar pages with equiplist
|
||||
if (!document.getElementById('equiplist')) return;
|
||||
if (document.getElementById('hv-gear-btn')) return;
|
||||
|
||||
const url = window.location.href || '';
|
||||
const isBuy = url.includes('screen=purchase') || url.includes('ss=bi');
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'hv-gear-btn';
|
||||
bar.style.cssText = css({
|
||||
display: 'inline-flex', gap: '4px', margin: '2px 0',
|
||||
});
|
||||
|
||||
// Analyze button
|
||||
const analyzeBtn = document.createElement('button');
|
||||
analyzeBtn.textContent = '🔍 Analyze Gear';
|
||||
analyzeBtn.style.cssText = css({
|
||||
padding: '3px 8px', background: '#2a2a4a', color: '#fff',
|
||||
border: '1px solid #555', borderRadius: '3px', cursor: 'pointer',
|
||||
fontSize: '11px', fontFamily: 'monospace',
|
||||
});
|
||||
analyzeBtn.onclick = () => {
|
||||
autoScrapeGear();
|
||||
setTimeout(buildGearPanel, 300);
|
||||
};
|
||||
bar.appendChild(analyzeBtn);
|
||||
|
||||
// Rescan button (clears buy data then rescrapes)
|
||||
const rescanBtn = document.createElement('button');
|
||||
rescanBtn.textContent = '🔄 Clear Buy + Rescan';
|
||||
rescanBtn.style.cssText = css({
|
||||
padding: '3px 8px', background: '#3a2a2a', color: '#fff',
|
||||
border: '1px solid #555', borderRadius: '3px', cursor: 'pointer',
|
||||
fontSize: '11px', fontFamily: 'monospace',
|
||||
});
|
||||
rescanBtn.onclick = () => {
|
||||
// Clear old buy data
|
||||
const db = getGearDB();
|
||||
saveGearDB(db.filter(e => e.source !== 'buy'));
|
||||
console.log('%c[HV] 🧹 Cleared old buy data', 'color:#f80');
|
||||
// Rescan current page
|
||||
const scraped = autoScrapeGear();
|
||||
const count = scraped.length || 0;
|
||||
console.log(`%c[HV] 🔄 Rescanned: ${count} items`, 'color:#0f0');
|
||||
// If on buy page, also scan store after short delay
|
||||
if (isBuy) {
|
||||
setTimeout(() => {
|
||||
autoScrapeGear();
|
||||
buildGearPanel();
|
||||
}, 500);
|
||||
} else {
|
||||
setTimeout(buildGearPanel, 300);
|
||||
}
|
||||
};
|
||||
bar.appendChild(rescanBtn);
|
||||
|
||||
// Insert into the page (next to the armory toolbar if present)
|
||||
const equiplist = document.getElementById('equiplist');
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = 'margin-bottom:4px';
|
||||
container.appendChild(bar);
|
||||
equiplist.parentNode.insertBefore(container, equiplist);
|
||||
|
||||
console.log('%c[HV] 🛡️ Gear analysis enabled — click 🔍 or 🔄', 'color:#0f0');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// BATTLE LOGGER — saves raw battle log lines to localStorage in real time
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -3977,6 +4300,10 @@ window.HV = {
|
|||
scrapeGear: () => autoScrapeGear(),
|
||||
gearDebug: () => debugScrapeGear(),
|
||||
gearStore: () => { const s = getHVEquipStore(); return s ? `Loaded: ${Object.keys(s).length} items` : 'Not loaded yet'; },
|
||||
// Gear analysis
|
||||
analyzeGear: () => analyzeGear(),
|
||||
gearPanel: () => buildGearPanel(),
|
||||
clearBuy: () => { const db = getGearDB(); saveGearDB(db.filter(e => e.source !== 'buy')); return 'Buy data cleared'; },
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -4006,7 +4333,7 @@ function init() {
|
|||
if (STATE.page === 'arena') { autoCheckTask('arenas'); autoCheckTask('first-blood'); }
|
||||
if (STATE.page === 'character') showGuidancePanel();
|
||||
if (STATE.page === 'settings') enhanceSettings();
|
||||
if (STATE.page === 'armory') enhanceArmory();
|
||||
if (STATE.page === 'armory') { enhanceArmory(); enhanceGearAnalysis(); }
|
||||
if (STATE.page === 'abilities') enhanceAbilities();
|
||||
if (STATE.page === 'monster') enhanceMonsterLab();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.14.32
|
||||
// @version 0.14.33
|
||||
// @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.32';
|
||||
const VERSION = '0.14.33';
|
||||
|
||||
const CFG = {
|
||||
// — Battle automation
|
||||
|
|
@ -3615,6 +3615,33 @@ function scrapeBuyPage() {
|
|||
saveGearDB([...existing, ...scraped]);
|
||||
console.log(`%c[HV] 🛒 Buy: ${scraped.length} items (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0');
|
||||
}
|
||||
|
||||
// Second, scan the full store for items NOT in the visible list
|
||||
// The Purchase page loads ALL items into dynjs_eqstore regardless of
|
||||
// the filter tab — one visit captures the whole store.
|
||||
const store = getHVEquipStore();
|
||||
if (store) {
|
||||
const visibleIds = new Set(scraped.map(s => s.id));
|
||||
const missing = Object.entries(store).filter(([id, data]) => !visibleIds.has(id) && data && (data.t || data.d));
|
||||
if (missing.length > 0) {
|
||||
missing.forEach(([id, data]) => {
|
||||
const stats = data.d ? parseEquipHTML(data.d) : null;
|
||||
let category = 'All Items';
|
||||
if (stats && stats.type) {
|
||||
if (stats.type.includes('Cloth')) category = 'Cloth Armor';
|
||||
else if (stats.type.includes('Light')) category = 'Light Armor';
|
||||
else if (stats.type.includes('Heavy')) category = 'Heavy Armor';
|
||||
else if (stats.type.includes('Shield')) category = 'Shield';
|
||||
else if (stats.type.includes('Staff')) category = 'Staff';
|
||||
else category = 'Weapon';
|
||||
}
|
||||
scraped.push({ id, name: data.t || 'Unknown', category, quality: data.q, stats, source: 'buy', scrapedAt: Date.now() });
|
||||
});
|
||||
const db2 = getGearDB();
|
||||
saveGearDB([...db2.filter(e => e.source !== 'buy'), ...scraped]);
|
||||
console.log(`%c[HV] 🛒 Buy: +${missing.length} from store (total ${scraped.length})`, 'color:#0f0');
|
||||
}
|
||||
}
|
||||
return scraped;
|
||||
}
|
||||
|
||||
|
|
@ -3679,6 +3706,302 @@ function scrapeModifyDetail() {
|
|||
return item;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GEAR ANALYSIS — in-game gear comparison (port of analyze_gear.py)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Adds a toolbar button on Armory pages. Clicking it:
|
||||
// 1. Clears old buy data (optional)
|
||||
// 2. Rescrapes the current page (store or inventory)
|
||||
// 3. Scores all items and shows per-slot recommendations
|
||||
// Sources: 🟢 equipped (character), 📦 stored (armory), 🛒 purchasable (buy)
|
||||
|
||||
function _num(v) {
|
||||
if (v == null) return 0;
|
||||
const s = String(v).replace('+', '').replace('%', '').trim();
|
||||
const n = parseFloat(s);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
// ── Scoring (mirrors analyze_gear.py) ──
|
||||
function gearScoreArmor(item) {
|
||||
const st = item.stats || {};
|
||||
let s = 0;
|
||||
s += _num(st['Physical Mitigation']) * 3;
|
||||
s += _num(st['Evade']) * 2;
|
||||
s += _num(st['Strength']) * 2;
|
||||
s += _num(st['Dexterity']) * 2;
|
||||
s += _num(st['Agility']) * 1.5;
|
||||
s += _num(st['Endurance']);
|
||||
s += _num(st['Crushing']);
|
||||
s += _num(st['Slashing']);
|
||||
s += _num(st['Piercing']);
|
||||
s -= _num(st['burden']) * 0.5;
|
||||
s += (item.quality || 0) * 5;
|
||||
return s;
|
||||
}
|
||||
|
||||
function gearScoreWeapon(item) {
|
||||
const st = item.stats || {};
|
||||
const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase();
|
||||
const quality = item.quality || 0;
|
||||
let s = 0;
|
||||
s += _num(st['Attack Accuracy']) * 2;
|
||||
s += _num(st['Attack Crit Damage']) * 60;
|
||||
s += _num(st['Strength']) * 2;
|
||||
s += _num(st['Dexterity']) * 1.5;
|
||||
s += _num(st['Parry']) * 0.5;
|
||||
s += _num(st['Block']) * 0.5;
|
||||
s += _num(st['Agility']) * 0.5;
|
||||
s -= _num(st['burden']) * 0.5;
|
||||
// Quality multiplier (q6=Magnificent, q5=Exquisite, q4=Superior)
|
||||
s *= 0.8 + (quality * 0.12);
|
||||
// Weapon type bonus
|
||||
if (name.includes('estoc')) s *= 1.25;
|
||||
else if (name.includes('longsword')) s *= 1.10;
|
||||
else if (name.includes('great mace')) s *= 0.90;
|
||||
else if (name.includes('club')) s *= 0.85;
|
||||
else if (name.includes('axe')) s *= 0.90;
|
||||
return s;
|
||||
}
|
||||
|
||||
// ── Slot detection ──
|
||||
const GEAR_SLOT_KEYWORDS = {
|
||||
'Head': ['helmet','cap','goggles','mask','hood','hat','circlet','crown','visor'],
|
||||
'Body': ['breastplate','cuirass','robe','tunic','chest','gi','jacket','vest','hauberk'],
|
||||
'Hands': ['gauntlets','gloves','mitts','bracers','handguards','fists'],
|
||||
'Legs': ['leggings','greaves','pants','trousers','loincloth','kilt','breeches','chaps'],
|
||||
'Feet': ['boots','sabatons','shoes','slippers','sandals'],
|
||||
};
|
||||
|
||||
function gearDetectSlot(item) {
|
||||
const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase();
|
||||
for (const [slot, kws] of Object.entries(GEAR_SLOT_KEYWORDS)) {
|
||||
for (const kw of kws) {
|
||||
if (name.includes(kw)) return slot;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function gearIsTwoHandWeapon(item) {
|
||||
const st = item.stats || {};
|
||||
const itype = st.type || '';
|
||||
if (itype.includes('Armor') || itype.includes('Shield') || itype.includes('Staff')) return false;
|
||||
const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase();
|
||||
if (['estoc','longsword','great mace','scythe','axe','club'].some(k => name.includes(k))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function gearUsable(item, playerLv) {
|
||||
const lv = (item.stats || {}).level;
|
||||
if (!lv || lv === 'Unassigned') return true;
|
||||
const n = parseInt(lv);
|
||||
if (isNaN(n)) return true;
|
||||
return n <= (playerLv || 155) + 15;
|
||||
}
|
||||
|
||||
function gearPlayerLevel() {
|
||||
const el = document.getElementById('level_readout');
|
||||
if (el) {
|
||||
const m = (el.textContent || '').match(/(\d+)/);
|
||||
if (m) return parseInt(m[1]);
|
||||
}
|
||||
return 155;
|
||||
}
|
||||
|
||||
// ── Main analysis ──
|
||||
function analyzeGear() {
|
||||
const db = getGearDB();
|
||||
const equipped = db.filter(e => e.source === 'character' && e.id);
|
||||
const armory = db.filter(e => e.source === 'armory' && e.stats);
|
||||
const buy = db.filter(e => e.source === 'buy' && e.stats);
|
||||
const playerLv = gearPlayerLevel();
|
||||
|
||||
// Find equipped slot map (slotType from character page)
|
||||
const slotMap = {};
|
||||
equipped.forEach(e => { if (e.slotType) slotMap[e.slotType] = e; });
|
||||
|
||||
const results = [];
|
||||
const slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet'];
|
||||
|
||||
slots.forEach(slot => {
|
||||
const current = slotMap[slot];
|
||||
const cands = [];
|
||||
|
||||
const consider = (item, source) => {
|
||||
if (!item || !item.stats) return;
|
||||
if (!gearUsable(item, playerLv)) return;
|
||||
const isWeapon = slot === 'Mainhand';
|
||||
if (isWeapon) {
|
||||
if (!gearIsTwoHandWeapon(item)) return;
|
||||
if (!item.name || !item.stats['Attack Accuracy']) return; // needs stats
|
||||
} else {
|
||||
const det = gearDetectSlot(item);
|
||||
if (det !== slot) return;
|
||||
if (!item.stats['Physical Mitigation'] && !item.stats['Evade']) return;
|
||||
}
|
||||
const score = isWeapon ? gearScoreWeapon(item) : gearScoreArmor(item);
|
||||
cands.push({ item, source, score });
|
||||
};
|
||||
|
||||
if (current) consider(current, 'equipped');
|
||||
armory.forEach(i => consider(i, 'armory'));
|
||||
buy.forEach(i => consider(i, 'buy'));
|
||||
|
||||
cands.sort((a, b) => b.score - a.score);
|
||||
results.push({ slot, current, cands });
|
||||
});
|
||||
|
||||
return { playerLv, results };
|
||||
}
|
||||
|
||||
// ── UI ──
|
||||
function buildGearPanel() {
|
||||
// Remove old panel
|
||||
const old = document.getElementById('hv-gear-panel');
|
||||
if (old) old.remove();
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'hv-gear-panel';
|
||||
panel.style.cssText = css({
|
||||
position: 'fixed', right: '8px', top: '48px', zIndex: 99999,
|
||||
width: '420px', maxHeight: '80vh', overflowY: 'auto',
|
||||
background: '#12121e', border: '1px solid #333', borderRadius: '6px',
|
||||
padding: '8px', fontSize: '11px', fontFamily: 'monospace',
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,.6)',
|
||||
});
|
||||
|
||||
const close = document.createElement('button');
|
||||
close.textContent = '✕';
|
||||
close.style.cssText = 'float:right;cursor:pointer;background:#333;border:none;color:#fff;border-radius:3px;padding:2px 6px';
|
||||
close.onclick = () => panel.remove();
|
||||
panel.appendChild(close);
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.textContent = '🛡️ Gear Analysis';
|
||||
title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd';
|
||||
panel.appendChild(title);
|
||||
|
||||
const { results } = analyzeGear();
|
||||
|
||||
results.forEach(({ slot, current, cands }) => {
|
||||
const block = document.createElement('div');
|
||||
block.style.cssText = 'margin-bottom:10px;border-top:1px solid #222;padding-top:6px';
|
||||
|
||||
const head = document.createElement('div');
|
||||
head.textContent = `${slot}: ${current ? current.name : '—'}`;
|
||||
head.style.cssText = 'color:#9cf;font-weight:bold;margin-bottom:2px';
|
||||
block.appendChild(head);
|
||||
|
||||
if (!cands.length) {
|
||||
const none = document.createElement('div');
|
||||
none.textContent = ' (no data)';
|
||||
none.style.cssText = 'color:#666';
|
||||
block.appendChild(none);
|
||||
panel.appendChild(block);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show top 4 candidates
|
||||
cands.slice(0, 4).forEach((c, i) => {
|
||||
const icon = c.source === 'equipped' ? '🟢' : c.source === 'armory' ? '📦' : '🛒';
|
||||
const row = document.createElement('div');
|
||||
const price = c.item.price ? ` 💰${c.item.price}` : '';
|
||||
const isCurrent = c.source === 'equipped';
|
||||
row.textContent = ` ${i + 1}. [${c.score.toFixed(0)}] ${icon} ${c.item.name}${isCurrent ? ' ⬅' : ''}${price}`;
|
||||
row.style.cssText = isCurrent ? 'color:#8f8' : 'color:#ccc';
|
||||
if (i === 0 && !isCurrent) row.style.color = '#ff0';
|
||||
block.appendChild(row);
|
||||
});
|
||||
|
||||
// Highlight best option
|
||||
const best = cands[0];
|
||||
if (best && best.source !== 'equipped' && current) {
|
||||
const rec = document.createElement('div');
|
||||
const icon = best.source === 'armory' ? '📦 Stored' : '🛒 For Sale';
|
||||
rec.textContent = ` ✅ Upgrade: ${icon} ${best.item.name} (${best.score.toFixed(0)} vs ${cands.find(c => c.source === 'equipped')?.score.toFixed(0) || '?'})`;
|
||||
rec.style.cssText = 'color:#0f0;margin-top:2px';
|
||||
block.appendChild(rec);
|
||||
} else if (best && best.source === 'equipped') {
|
||||
const ok = document.createElement('div');
|
||||
ok.textContent = ' ✅ Already best';
|
||||
ok.style.cssText = 'color:#0f0';
|
||||
block.appendChild(ok);
|
||||
}
|
||||
|
||||
panel.appendChild(block);
|
||||
});
|
||||
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
|
||||
function enhanceGearAnalysis() {
|
||||
// Only on Bazaar pages with equiplist
|
||||
if (!document.getElementById('equiplist')) return;
|
||||
if (document.getElementById('hv-gear-btn')) return;
|
||||
|
||||
const url = window.location.href || '';
|
||||
const isBuy = url.includes('screen=purchase') || url.includes('ss=bi');
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'hv-gear-btn';
|
||||
bar.style.cssText = css({
|
||||
display: 'inline-flex', gap: '4px', margin: '2px 0',
|
||||
});
|
||||
|
||||
// Analyze button
|
||||
const analyzeBtn = document.createElement('button');
|
||||
analyzeBtn.textContent = '🔍 Analyze Gear';
|
||||
analyzeBtn.style.cssText = css({
|
||||
padding: '3px 8px', background: '#2a2a4a', color: '#fff',
|
||||
border: '1px solid #555', borderRadius: '3px', cursor: 'pointer',
|
||||
fontSize: '11px', fontFamily: 'monospace',
|
||||
});
|
||||
analyzeBtn.onclick = () => {
|
||||
autoScrapeGear();
|
||||
setTimeout(buildGearPanel, 300);
|
||||
};
|
||||
bar.appendChild(analyzeBtn);
|
||||
|
||||
// Rescan button (clears buy data then rescrapes)
|
||||
const rescanBtn = document.createElement('button');
|
||||
rescanBtn.textContent = '🔄 Clear Buy + Rescan';
|
||||
rescanBtn.style.cssText = css({
|
||||
padding: '3px 8px', background: '#3a2a2a', color: '#fff',
|
||||
border: '1px solid #555', borderRadius: '3px', cursor: 'pointer',
|
||||
fontSize: '11px', fontFamily: 'monospace',
|
||||
});
|
||||
rescanBtn.onclick = () => {
|
||||
// Clear old buy data
|
||||
const db = getGearDB();
|
||||
saveGearDB(db.filter(e => e.source !== 'buy'));
|
||||
console.log('%c[HV] 🧹 Cleared old buy data', 'color:#f80');
|
||||
// Rescan current page
|
||||
const scraped = autoScrapeGear();
|
||||
const count = scraped.length || 0;
|
||||
console.log(`%c[HV] 🔄 Rescanned: ${count} items`, 'color:#0f0');
|
||||
// If on buy page, also scan store after short delay
|
||||
if (isBuy) {
|
||||
setTimeout(() => {
|
||||
autoScrapeGear();
|
||||
buildGearPanel();
|
||||
}, 500);
|
||||
} else {
|
||||
setTimeout(buildGearPanel, 300);
|
||||
}
|
||||
};
|
||||
bar.appendChild(rescanBtn);
|
||||
|
||||
// Insert into the page (next to the armory toolbar if present)
|
||||
const equiplist = document.getElementById('equiplist');
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = 'margin-bottom:4px';
|
||||
container.appendChild(bar);
|
||||
equiplist.parentNode.insertBefore(container, equiplist);
|
||||
|
||||
console.log('%c[HV] 🛡️ Gear analysis enabled — click 🔍 or 🔄', 'color:#0f0');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// BATTLE LOGGER — saves raw battle log lines to localStorage in real time
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -3977,6 +4300,10 @@ window.HV = {
|
|||
scrapeGear: () => autoScrapeGear(),
|
||||
gearDebug: () => debugScrapeGear(),
|
||||
gearStore: () => { const s = getHVEquipStore(); return s ? `Loaded: ${Object.keys(s).length} items` : 'Not loaded yet'; },
|
||||
// Gear analysis
|
||||
analyzeGear: () => analyzeGear(),
|
||||
gearPanel: () => buildGearPanel(),
|
||||
clearBuy: () => { const db = getGearDB(); saveGearDB(db.filter(e => e.source !== 'buy')); return 'Buy data cleared'; },
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -4006,7 +4333,7 @@ function init() {
|
|||
if (STATE.page === 'arena') { autoCheckTask('arenas'); autoCheckTask('first-blood'); }
|
||||
if (STATE.page === 'character') showGuidancePanel();
|
||||
if (STATE.page === 'settings') enhanceSettings();
|
||||
if (STATE.page === 'armory') enhanceArmory();
|
||||
if (STATE.page === 'armory') { enhanceArmory(); enhanceGearAnalysis(); }
|
||||
if (STATE.page === 'abilities') enhanceAbilities();
|
||||
if (STATE.page === 'monster') enhanceMonsterLab();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// CONFIG — default settings
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const VERSION = '0.14.32';
|
||||
const VERSION = '0.14.33';
|
||||
|
||||
const CFG = {
|
||||
// — Battle automation
|
||||
|
|
|
|||
295
src/gear-analysis.js
Normal file
295
src/gear-analysis.js
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GEAR ANALYSIS — in-game gear comparison (port of analyze_gear.py)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Adds a toolbar button on Armory pages. Clicking it:
|
||||
// 1. Clears old buy data (optional)
|
||||
// 2. Rescrapes the current page (store or inventory)
|
||||
// 3. Scores all items and shows per-slot recommendations
|
||||
// Sources: 🟢 equipped (character), 📦 stored (armory), 🛒 purchasable (buy)
|
||||
|
||||
function _num(v) {
|
||||
if (v == null) return 0;
|
||||
const s = String(v).replace('+', '').replace('%', '').trim();
|
||||
const n = parseFloat(s);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
// ── Scoring (mirrors analyze_gear.py) ──
|
||||
function gearScoreArmor(item) {
|
||||
const st = item.stats || {};
|
||||
let s = 0;
|
||||
s += _num(st['Physical Mitigation']) * 3;
|
||||
s += _num(st['Evade']) * 2;
|
||||
s += _num(st['Strength']) * 2;
|
||||
s += _num(st['Dexterity']) * 2;
|
||||
s += _num(st['Agility']) * 1.5;
|
||||
s += _num(st['Endurance']);
|
||||
s += _num(st['Crushing']);
|
||||
s += _num(st['Slashing']);
|
||||
s += _num(st['Piercing']);
|
||||
s -= _num(st['burden']) * 0.5;
|
||||
s += (item.quality || 0) * 5;
|
||||
return s;
|
||||
}
|
||||
|
||||
function gearScoreWeapon(item) {
|
||||
const st = item.stats || {};
|
||||
const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase();
|
||||
const quality = item.quality || 0;
|
||||
let s = 0;
|
||||
s += _num(st['Attack Accuracy']) * 2;
|
||||
s += _num(st['Attack Crit Damage']) * 60;
|
||||
s += _num(st['Strength']) * 2;
|
||||
s += _num(st['Dexterity']) * 1.5;
|
||||
s += _num(st['Parry']) * 0.5;
|
||||
s += _num(st['Block']) * 0.5;
|
||||
s += _num(st['Agility']) * 0.5;
|
||||
s -= _num(st['burden']) * 0.5;
|
||||
// Quality multiplier (q6=Magnificent, q5=Exquisite, q4=Superior)
|
||||
s *= 0.8 + (quality * 0.12);
|
||||
// Weapon type bonus
|
||||
if (name.includes('estoc')) s *= 1.25;
|
||||
else if (name.includes('longsword')) s *= 1.10;
|
||||
else if (name.includes('great mace')) s *= 0.90;
|
||||
else if (name.includes('club')) s *= 0.85;
|
||||
else if (name.includes('axe')) s *= 0.90;
|
||||
return s;
|
||||
}
|
||||
|
||||
// ── Slot detection ──
|
||||
const GEAR_SLOT_KEYWORDS = {
|
||||
'Head': ['helmet','cap','goggles','mask','hood','hat','circlet','crown','visor'],
|
||||
'Body': ['breastplate','cuirass','robe','tunic','chest','gi','jacket','vest','hauberk'],
|
||||
'Hands': ['gauntlets','gloves','mitts','bracers','handguards','fists'],
|
||||
'Legs': ['leggings','greaves','pants','trousers','loincloth','kilt','breeches','chaps'],
|
||||
'Feet': ['boots','sabatons','shoes','slippers','sandals'],
|
||||
};
|
||||
|
||||
function gearDetectSlot(item) {
|
||||
const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase();
|
||||
for (const [slot, kws] of Object.entries(GEAR_SLOT_KEYWORDS)) {
|
||||
for (const kw of kws) {
|
||||
if (name.includes(kw)) return slot;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function gearIsTwoHandWeapon(item) {
|
||||
const st = item.stats || {};
|
||||
const itype = st.type || '';
|
||||
if (itype.includes('Armor') || itype.includes('Shield') || itype.includes('Staff')) return false;
|
||||
const name = ((item.name || '') + ' ' + (item.category || '')).toLowerCase();
|
||||
if (['estoc','longsword','great mace','scythe','axe','club'].some(k => name.includes(k))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function gearUsable(item, playerLv) {
|
||||
const lv = (item.stats || {}).level;
|
||||
if (!lv || lv === 'Unassigned') return true;
|
||||
const n = parseInt(lv);
|
||||
if (isNaN(n)) return true;
|
||||
return n <= (playerLv || 155) + 15;
|
||||
}
|
||||
|
||||
function gearPlayerLevel() {
|
||||
const el = document.getElementById('level_readout');
|
||||
if (el) {
|
||||
const m = (el.textContent || '').match(/(\d+)/);
|
||||
if (m) return parseInt(m[1]);
|
||||
}
|
||||
return 155;
|
||||
}
|
||||
|
||||
// ── Main analysis ──
|
||||
function analyzeGear() {
|
||||
const db = getGearDB();
|
||||
const equipped = db.filter(e => e.source === 'character' && e.id);
|
||||
const armory = db.filter(e => e.source === 'armory' && e.stats);
|
||||
const buy = db.filter(e => e.source === 'buy' && e.stats);
|
||||
const playerLv = gearPlayerLevel();
|
||||
|
||||
// Find equipped slot map (slotType from character page)
|
||||
const slotMap = {};
|
||||
equipped.forEach(e => { if (e.slotType) slotMap[e.slotType] = e; });
|
||||
|
||||
const results = [];
|
||||
const slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet'];
|
||||
|
||||
slots.forEach(slot => {
|
||||
const current = slotMap[slot];
|
||||
const cands = [];
|
||||
|
||||
const consider = (item, source) => {
|
||||
if (!item || !item.stats) return;
|
||||
if (!gearUsable(item, playerLv)) return;
|
||||
const isWeapon = slot === 'Mainhand';
|
||||
if (isWeapon) {
|
||||
if (!gearIsTwoHandWeapon(item)) return;
|
||||
if (!item.name || !item.stats['Attack Accuracy']) return; // needs stats
|
||||
} else {
|
||||
const det = gearDetectSlot(item);
|
||||
if (det !== slot) return;
|
||||
if (!item.stats['Physical Mitigation'] && !item.stats['Evade']) return;
|
||||
}
|
||||
const score = isWeapon ? gearScoreWeapon(item) : gearScoreArmor(item);
|
||||
cands.push({ item, source, score });
|
||||
};
|
||||
|
||||
if (current) consider(current, 'equipped');
|
||||
armory.forEach(i => consider(i, 'armory'));
|
||||
buy.forEach(i => consider(i, 'buy'));
|
||||
|
||||
cands.sort((a, b) => b.score - a.score);
|
||||
results.push({ slot, current, cands });
|
||||
});
|
||||
|
||||
return { playerLv, results };
|
||||
}
|
||||
|
||||
// ── UI ──
|
||||
function buildGearPanel() {
|
||||
// Remove old panel
|
||||
const old = document.getElementById('hv-gear-panel');
|
||||
if (old) old.remove();
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'hv-gear-panel';
|
||||
panel.style.cssText = css({
|
||||
position: 'fixed', right: '8px', top: '48px', zIndex: 99999,
|
||||
width: '420px', maxHeight: '80vh', overflowY: 'auto',
|
||||
background: '#12121e', border: '1px solid #333', borderRadius: '6px',
|
||||
padding: '8px', fontSize: '11px', fontFamily: 'monospace',
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,.6)',
|
||||
});
|
||||
|
||||
const close = document.createElement('button');
|
||||
close.textContent = '✕';
|
||||
close.style.cssText = 'float:right;cursor:pointer;background:#333;border:none;color:#fff;border-radius:3px;padding:2px 6px';
|
||||
close.onclick = () => panel.remove();
|
||||
panel.appendChild(close);
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.textContent = '🛡️ Gear Analysis';
|
||||
title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd';
|
||||
panel.appendChild(title);
|
||||
|
||||
const { results } = analyzeGear();
|
||||
|
||||
results.forEach(({ slot, current, cands }) => {
|
||||
const block = document.createElement('div');
|
||||
block.style.cssText = 'margin-bottom:10px;border-top:1px solid #222;padding-top:6px';
|
||||
|
||||
const head = document.createElement('div');
|
||||
head.textContent = `${slot}: ${current ? current.name : '—'}`;
|
||||
head.style.cssText = 'color:#9cf;font-weight:bold;margin-bottom:2px';
|
||||
block.appendChild(head);
|
||||
|
||||
if (!cands.length) {
|
||||
const none = document.createElement('div');
|
||||
none.textContent = ' (no data)';
|
||||
none.style.cssText = 'color:#666';
|
||||
block.appendChild(none);
|
||||
panel.appendChild(block);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show top 4 candidates
|
||||
cands.slice(0, 4).forEach((c, i) => {
|
||||
const icon = c.source === 'equipped' ? '🟢' : c.source === 'armory' ? '📦' : '🛒';
|
||||
const row = document.createElement('div');
|
||||
const price = c.item.price ? ` 💰${c.item.price}` : '';
|
||||
const isCurrent = c.source === 'equipped';
|
||||
row.textContent = ` ${i + 1}. [${c.score.toFixed(0)}] ${icon} ${c.item.name}${isCurrent ? ' ⬅' : ''}${price}`;
|
||||
row.style.cssText = isCurrent ? 'color:#8f8' : 'color:#ccc';
|
||||
if (i === 0 && !isCurrent) row.style.color = '#ff0';
|
||||
block.appendChild(row);
|
||||
});
|
||||
|
||||
// Highlight best option
|
||||
const best = cands[0];
|
||||
if (best && best.source !== 'equipped' && current) {
|
||||
const rec = document.createElement('div');
|
||||
const icon = best.source === 'armory' ? '📦 Stored' : '🛒 For Sale';
|
||||
rec.textContent = ` ✅ Upgrade: ${icon} ${best.item.name} (${best.score.toFixed(0)} vs ${cands.find(c => c.source === 'equipped')?.score.toFixed(0) || '?'})`;
|
||||
rec.style.cssText = 'color:#0f0;margin-top:2px';
|
||||
block.appendChild(rec);
|
||||
} else if (best && best.source === 'equipped') {
|
||||
const ok = document.createElement('div');
|
||||
ok.textContent = ' ✅ Already best';
|
||||
ok.style.cssText = 'color:#0f0';
|
||||
block.appendChild(ok);
|
||||
}
|
||||
|
||||
panel.appendChild(block);
|
||||
});
|
||||
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
|
||||
function enhanceGearAnalysis() {
|
||||
// Only on Bazaar pages with equiplist
|
||||
if (!document.getElementById('equiplist')) return;
|
||||
if (document.getElementById('hv-gear-btn')) return;
|
||||
|
||||
const url = window.location.href || '';
|
||||
const isBuy = url.includes('screen=purchase') || url.includes('ss=bi');
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'hv-gear-btn';
|
||||
bar.style.cssText = css({
|
||||
display: 'inline-flex', gap: '4px', margin: '2px 0',
|
||||
});
|
||||
|
||||
// Analyze button
|
||||
const analyzeBtn = document.createElement('button');
|
||||
analyzeBtn.textContent = '🔍 Analyze Gear';
|
||||
analyzeBtn.style.cssText = css({
|
||||
padding: '3px 8px', background: '#2a2a4a', color: '#fff',
|
||||
border: '1px solid #555', borderRadius: '3px', cursor: 'pointer',
|
||||
fontSize: '11px', fontFamily: 'monospace',
|
||||
});
|
||||
analyzeBtn.onclick = () => {
|
||||
autoScrapeGear();
|
||||
setTimeout(buildGearPanel, 300);
|
||||
};
|
||||
bar.appendChild(analyzeBtn);
|
||||
|
||||
// Rescan button (clears buy data then rescrapes)
|
||||
const rescanBtn = document.createElement('button');
|
||||
rescanBtn.textContent = '🔄 Clear Buy + Rescan';
|
||||
rescanBtn.style.cssText = css({
|
||||
padding: '3px 8px', background: '#3a2a2a', color: '#fff',
|
||||
border: '1px solid #555', borderRadius: '3px', cursor: 'pointer',
|
||||
fontSize: '11px', fontFamily: 'monospace',
|
||||
});
|
||||
rescanBtn.onclick = () => {
|
||||
// Clear old buy data
|
||||
const db = getGearDB();
|
||||
saveGearDB(db.filter(e => e.source !== 'buy'));
|
||||
console.log('%c[HV] 🧹 Cleared old buy data', 'color:#f80');
|
||||
// Rescan current page
|
||||
const scraped = autoScrapeGear();
|
||||
const count = scraped.length || 0;
|
||||
console.log(`%c[HV] 🔄 Rescanned: ${count} items`, 'color:#0f0');
|
||||
// If on buy page, also scan store after short delay
|
||||
if (isBuy) {
|
||||
setTimeout(() => {
|
||||
autoScrapeGear();
|
||||
buildGearPanel();
|
||||
}, 500);
|
||||
} else {
|
||||
setTimeout(buildGearPanel, 300);
|
||||
}
|
||||
};
|
||||
bar.appendChild(rescanBtn);
|
||||
|
||||
// Insert into the page (next to the armory toolbar if present)
|
||||
const equiplist = document.getElementById('equiplist');
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = 'margin-bottom:4px';
|
||||
container.appendChild(bar);
|
||||
equiplist.parentNode.insertBefore(container, equiplist);
|
||||
|
||||
console.log('%c[HV] 🛡️ Gear analysis enabled — click 🔍 or 🔄', 'color:#0f0');
|
||||
}
|
||||
|
|
@ -296,6 +296,33 @@ function scrapeBuyPage() {
|
|||
saveGearDB([...existing, ...scraped]);
|
||||
console.log(`%c[HV] 🛒 Buy: ${scraped.length} items (${scraped.filter(s => s.stats).length} with stats)`, 'color:#0f0');
|
||||
}
|
||||
|
||||
// Second, scan the full store for items NOT in the visible list
|
||||
// The Purchase page loads ALL items into dynjs_eqstore regardless of
|
||||
// the filter tab — one visit captures the whole store.
|
||||
const store = getHVEquipStore();
|
||||
if (store) {
|
||||
const visibleIds = new Set(scraped.map(s => s.id));
|
||||
const missing = Object.entries(store).filter(([id, data]) => !visibleIds.has(id) && data && (data.t || data.d));
|
||||
if (missing.length > 0) {
|
||||
missing.forEach(([id, data]) => {
|
||||
const stats = data.d ? parseEquipHTML(data.d) : null;
|
||||
let category = 'All Items';
|
||||
if (stats && stats.type) {
|
||||
if (stats.type.includes('Cloth')) category = 'Cloth Armor';
|
||||
else if (stats.type.includes('Light')) category = 'Light Armor';
|
||||
else if (stats.type.includes('Heavy')) category = 'Heavy Armor';
|
||||
else if (stats.type.includes('Shield')) category = 'Shield';
|
||||
else if (stats.type.includes('Staff')) category = 'Staff';
|
||||
else category = 'Weapon';
|
||||
}
|
||||
scraped.push({ id, name: data.t || 'Unknown', category, quality: data.q, stats, source: 'buy', scrapedAt: Date.now() });
|
||||
});
|
||||
const db2 = getGearDB();
|
||||
saveGearDB([...db2.filter(e => e.source !== 'buy'), ...scraped]);
|
||||
console.log(`%c[HV] 🛒 Buy: +${missing.length} from store (total ${scraped.length})`, 'color:#0f0');
|
||||
}
|
||||
}
|
||||
return scraped;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.14.32
|
||||
// @version 0.14.33
|
||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||
// @author GaboGG + Hermes
|
||||
// @match *://*.hentaiverse.org/*
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function init() {
|
|||
if (STATE.page === 'arena') { autoCheckTask('arenas'); autoCheckTask('first-blood'); }
|
||||
if (STATE.page === 'character') showGuidancePanel();
|
||||
if (STATE.page === 'settings') enhanceSettings();
|
||||
if (STATE.page === 'armory') enhanceArmory();
|
||||
if (STATE.page === 'armory') { enhanceArmory(); enhanceGearAnalysis(); }
|
||||
if (STATE.page === 'abilities') enhanceAbilities();
|
||||
if (STATE.page === 'monster') enhanceMonsterLab();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,4 +26,8 @@ window.HV = {
|
|||
scrapeGear: () => autoScrapeGear(),
|
||||
gearDebug: () => debugScrapeGear(),
|
||||
gearStore: () => { const s = getHVEquipStore(); return s ? `Loaded: ${Object.keys(s).length} items` : 'Not loaded yet'; },
|
||||
// Gear analysis
|
||||
analyzeGear: () => analyzeGear(),
|
||||
gearPanel: () => buildGearPanel(),
|
||||
clearBuy: () => { const db = getGearDB(); saveGearDB(db.filter(e => e.source !== 'buy')); return 'Buy data cleared'; },
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue