v0.14.40 - Gear panel: armor/weapon filters + fix level detection
- Added filter dropdowns to the panel: Armor (Light/Heavy/Cloth/All) and Weapon (2H/Estoc/Longsword/Great Mace/Club/Axe/Scythe). Persisted in localStorage. Default: Light armor, 2H weapons. Filters exclude non-matching items from armory/buy candidates (equipped item always shown regardless). - Fixed gearPlayerLevel(): now reads STATE.level first (battle parser caches it), then the hv-cfg-btn label (Lv195), then level_readout. Previously fell back to 155 when the level readout used CSS fonts — this excluded the Lv171 equipped Estoc from candidates, causing 'vs ?' in the upgrade line. - Upgrade line now always shows a real number (scores equipped directly if it missed candidates). Also scrapes correctly on equipment pages: clicking Analyze there refreshes equipped slots from dynjs_equip store.
This commit is contained in:
parent
7442d7c512
commit
641363b5b0
5 changed files with 273 additions and 18 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.14.39
|
// @version 0.14.40
|
||||||
// @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.39';
|
const VERSION = '0.14.40';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -3801,6 +3801,15 @@ function gearUsable(item, playerLv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function gearPlayerLevel() {
|
function gearPlayerLevel() {
|
||||||
|
// Prefer battle-parser cached level (STATE.level)
|
||||||
|
if (STATE && STATE.level) return STATE.level;
|
||||||
|
// Parse from the HV cfg button (e.g. "Lv195")
|
||||||
|
const btn = document.getElementById('hv-cfg-btn');
|
||||||
|
if (btn) {
|
||||||
|
const m = (btn.textContent || '').match(/Lv(\d+)/i);
|
||||||
|
if (m) return parseInt(m[1]);
|
||||||
|
}
|
||||||
|
// Fallback: level_readout (may be CSS-font with no text)
|
||||||
const el = document.getElementById('level_readout');
|
const el = document.getElementById('level_readout');
|
||||||
if (el) {
|
if (el) {
|
||||||
const m = (el.textContent || '').match(/(\d+)/);
|
const m = (el.textContent || '').match(/(\d+)/);
|
||||||
|
|
@ -3809,6 +3818,21 @@ function gearPlayerLevel() {
|
||||||
return 155;
|
return 155;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Filter state (persisted) ──
|
||||||
|
const GEAR_FILTER_KEY = SP + 'gearFilter';
|
||||||
|
|
||||||
|
function getGearFilter() {
|
||||||
|
try {
|
||||||
|
const f = JSON.parse(localStorage[GEAR_FILTER_KEY] || 'null');
|
||||||
|
if (f && f.armor && f.weapon) return f;
|
||||||
|
} catch (e) {}
|
||||||
|
return { armor: 'Light', weapon: '2H' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGearFilter(f) {
|
||||||
|
try { localStorage[GEAR_FILTER_KEY] = JSON.stringify(f); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Main analysis ──
|
// ── Main analysis ──
|
||||||
function analyzeGear() {
|
function analyzeGear() {
|
||||||
const db = getGearDB();
|
const db = getGearDB();
|
||||||
|
|
@ -3816,11 +3840,27 @@ function analyzeGear() {
|
||||||
const armory = db.filter(e => e.source === 'armory' && e.stats);
|
const armory = db.filter(e => e.source === 'armory' && e.stats);
|
||||||
const buy = db.filter(e => e.source === 'buy' && e.stats);
|
const buy = db.filter(e => e.source === 'buy' && e.stats);
|
||||||
const playerLv = gearPlayerLevel();
|
const playerLv = gearPlayerLevel();
|
||||||
|
const filter = getGearFilter();
|
||||||
|
|
||||||
// Find equipped slot map (slotType from character page)
|
// Find equipped slot map (slotType from character page)
|
||||||
const slotMap = {};
|
const slotMap = {};
|
||||||
equipped.forEach(e => { if (e.slotType) slotMap[e.slotType] = e; });
|
equipped.forEach(e => { if (e.slotType) slotMap[e.slotType] = e; });
|
||||||
|
|
||||||
|
// Does this item match the armor type filter (Cloth/Light/Heavy/All)?
|
||||||
|
function matchesArmorType(item) {
|
||||||
|
if (filter.armor === 'All') return true;
|
||||||
|
const type = (item.stats && item.stats.type) || '';
|
||||||
|
const cat = (item.category || '') + ' ' + (item.name || '');
|
||||||
|
return type.includes(filter.armor) || cat.includes(filter.armor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does this weapon match the weapon class filter (Estoc/Longsword/.../2H)?
|
||||||
|
function matchesWeaponClass(item) {
|
||||||
|
if (filter.weapon === '2H') return true;
|
||||||
|
const name = (item.name || '').toLowerCase();
|
||||||
|
return name.includes(filter.weapon.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
const slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet'];
|
const slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet'];
|
||||||
|
|
||||||
|
|
@ -3830,16 +3870,21 @@ function analyzeGear() {
|
||||||
|
|
||||||
const consider = (item, source) => {
|
const consider = (item, source) => {
|
||||||
if (!item || !item.stats) return;
|
if (!item || !item.stats) return;
|
||||||
if (!gearUsable(item, playerLv)) return;
|
|
||||||
const isWeapon = slot === 'Mainhand';
|
const isWeapon = slot === 'Mainhand';
|
||||||
if (isWeapon) {
|
if (isWeapon) {
|
||||||
if (!gearIsTwoHandWeapon(item)) return;
|
if (!gearIsTwoHandWeapon(item)) return;
|
||||||
if (!item.name || !item.stats['Attack Accuracy']) return; // needs stats
|
if (!item.name || !item.stats['Attack Accuracy']) return;
|
||||||
|
// Apply weapon class filter — but always include the equipped item
|
||||||
|
if (source !== 'equipped' && !matchesWeaponClass(item)) return;
|
||||||
} else {
|
} else {
|
||||||
const det = gearDetectSlot(item);
|
const det = gearDetectSlot(item);
|
||||||
if (det !== slot) return;
|
if (det !== slot) return;
|
||||||
if (!item.stats['Physical Mitigation'] && !item.stats['Evade']) return;
|
if (!item.stats['Physical Mitigation'] && !item.stats['Evade']) return;
|
||||||
|
// Apply armor type filter — but always include the equipped item
|
||||||
|
if (source !== 'equipped' && !matchesArmorType(item)) return;
|
||||||
}
|
}
|
||||||
|
// Level filter — always include the equipped item (it's what you have on)
|
||||||
|
if (source !== 'equipped' && !gearUsable(item, playerLv)) return;
|
||||||
const score = isWeapon ? gearScoreWeapon(item) : gearScoreArmor(item);
|
const score = isWeapon ? gearScoreWeapon(item) : gearScoreArmor(item);
|
||||||
cands.push({ item, source, score });
|
cands.push({ item, source, score });
|
||||||
};
|
};
|
||||||
|
|
@ -3852,7 +3897,7 @@ function analyzeGear() {
|
||||||
results.push({ slot, current, cands });
|
results.push({ slot, current, cands });
|
||||||
});
|
});
|
||||||
|
|
||||||
return { playerLv, results };
|
return { playerLv, filter, results };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── UI ──
|
// ── UI ──
|
||||||
|
|
@ -3882,6 +3927,42 @@ function buildGearPanel() {
|
||||||
title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd';
|
title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd';
|
||||||
panel.appendChild(title);
|
panel.appendChild(title);
|
||||||
|
|
||||||
|
// ── Filters row ──
|
||||||
|
const filterRow = document.createElement('div');
|
||||||
|
filterRow.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:8px;font-size:10px;color:#aaa';
|
||||||
|
const filter = getGearFilter();
|
||||||
|
|
||||||
|
filterRow.appendChild(Object.assign(document.createElement('span'), { textContent: 'Armor:' }));
|
||||||
|
const armorSel = document.createElement('select');
|
||||||
|
armorSel.style.cssText = 'background:#222;color:#fff;border:1px solid #444;border-radius:3px;font-size:10px;font-family:monospace';
|
||||||
|
['Light', 'Heavy', 'Cloth', 'All'].forEach(t => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = t; opt.textContent = t;
|
||||||
|
if (t === filter.armor) opt.selected = true;
|
||||||
|
armorSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
filterRow.appendChild(armorSel);
|
||||||
|
|
||||||
|
filterRow.appendChild(Object.assign(document.createElement('span'), { textContent: 'Weapon:' }));
|
||||||
|
const weaponSel = document.createElement('select');
|
||||||
|
weaponSel.style.cssText = 'background:#222;color:#fff;border:1px solid #444;border-radius:3px;font-size:10px;font-family:monospace';
|
||||||
|
['2H', 'Estoc', 'Longsword', 'Great Mace', 'Club', 'Axe', 'Scythe'].forEach(t => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = t; opt.textContent = t;
|
||||||
|
if (t === filter.weapon) opt.selected = true;
|
||||||
|
weaponSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
filterRow.appendChild(weaponSel);
|
||||||
|
panel.appendChild(filterRow);
|
||||||
|
|
||||||
|
// Rebuild panel when filter changes
|
||||||
|
function applyFilter() {
|
||||||
|
setGearFilter({ armor: armorSel.value, weapon: weaponSel.value });
|
||||||
|
buildGearPanel();
|
||||||
|
}
|
||||||
|
armorSel.onchange = applyFilter;
|
||||||
|
weaponSel.onchange = applyFilter;
|
||||||
|
|
||||||
const { results } = analyzeGear();
|
const { results } = analyzeGear();
|
||||||
|
|
||||||
results.forEach(({ slot, current, cands }) => {
|
results.forEach(({ slot, current, cands }) => {
|
||||||
|
|
@ -3917,9 +3998,13 @@ function buildGearPanel() {
|
||||||
// Highlight best option
|
// Highlight best option
|
||||||
const best = cands[0];
|
const best = cands[0];
|
||||||
if (best && best.source !== 'equipped' && current) {
|
if (best && best.source !== 'equipped' && current) {
|
||||||
|
const equippedCand = cands.find(c => c.source === 'equipped');
|
||||||
|
// If equipped somehow missing from candidates, score it directly
|
||||||
|
const eqScore = equippedCand ? equippedCand.score
|
||||||
|
: (current.stats ? (slot === 'Mainhand' ? gearScoreWeapon(current) : gearScoreArmor(current)) : 0);
|
||||||
const rec = document.createElement('div');
|
const rec = document.createElement('div');
|
||||||
const icon = best.source === 'armory' ? '📦 Stored' : '🛒 For Sale';
|
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.textContent = ` ✅ Upgrade: ${icon} ${best.item.name} (${best.score.toFixed(0)} vs ${eqScore.toFixed(0)})`;
|
||||||
rec.style.cssText = 'color:#0f0;margin-top:2px';
|
rec.style.cssText = 'color:#0f0;margin-top:2px';
|
||||||
block.appendChild(rec);
|
block.appendChild(rec);
|
||||||
} else if (best && best.source === 'equipped') {
|
} else if (best && best.source === 'equipped') {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.14.39
|
// @version 0.14.40
|
||||||
// @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.39';
|
const VERSION = '0.14.40';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -3801,6 +3801,15 @@ function gearUsable(item, playerLv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function gearPlayerLevel() {
|
function gearPlayerLevel() {
|
||||||
|
// Prefer battle-parser cached level (STATE.level)
|
||||||
|
if (STATE && STATE.level) return STATE.level;
|
||||||
|
// Parse from the HV cfg button (e.g. "Lv195")
|
||||||
|
const btn = document.getElementById('hv-cfg-btn');
|
||||||
|
if (btn) {
|
||||||
|
const m = (btn.textContent || '').match(/Lv(\d+)/i);
|
||||||
|
if (m) return parseInt(m[1]);
|
||||||
|
}
|
||||||
|
// Fallback: level_readout (may be CSS-font with no text)
|
||||||
const el = document.getElementById('level_readout');
|
const el = document.getElementById('level_readout');
|
||||||
if (el) {
|
if (el) {
|
||||||
const m = (el.textContent || '').match(/(\d+)/);
|
const m = (el.textContent || '').match(/(\d+)/);
|
||||||
|
|
@ -3809,6 +3818,21 @@ function gearPlayerLevel() {
|
||||||
return 155;
|
return 155;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Filter state (persisted) ──
|
||||||
|
const GEAR_FILTER_KEY = SP + 'gearFilter';
|
||||||
|
|
||||||
|
function getGearFilter() {
|
||||||
|
try {
|
||||||
|
const f = JSON.parse(localStorage[GEAR_FILTER_KEY] || 'null');
|
||||||
|
if (f && f.armor && f.weapon) return f;
|
||||||
|
} catch (e) {}
|
||||||
|
return { armor: 'Light', weapon: '2H' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGearFilter(f) {
|
||||||
|
try { localStorage[GEAR_FILTER_KEY] = JSON.stringify(f); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Main analysis ──
|
// ── Main analysis ──
|
||||||
function analyzeGear() {
|
function analyzeGear() {
|
||||||
const db = getGearDB();
|
const db = getGearDB();
|
||||||
|
|
@ -3816,11 +3840,27 @@ function analyzeGear() {
|
||||||
const armory = db.filter(e => e.source === 'armory' && e.stats);
|
const armory = db.filter(e => e.source === 'armory' && e.stats);
|
||||||
const buy = db.filter(e => e.source === 'buy' && e.stats);
|
const buy = db.filter(e => e.source === 'buy' && e.stats);
|
||||||
const playerLv = gearPlayerLevel();
|
const playerLv = gearPlayerLevel();
|
||||||
|
const filter = getGearFilter();
|
||||||
|
|
||||||
// Find equipped slot map (slotType from character page)
|
// Find equipped slot map (slotType from character page)
|
||||||
const slotMap = {};
|
const slotMap = {};
|
||||||
equipped.forEach(e => { if (e.slotType) slotMap[e.slotType] = e; });
|
equipped.forEach(e => { if (e.slotType) slotMap[e.slotType] = e; });
|
||||||
|
|
||||||
|
// Does this item match the armor type filter (Cloth/Light/Heavy/All)?
|
||||||
|
function matchesArmorType(item) {
|
||||||
|
if (filter.armor === 'All') return true;
|
||||||
|
const type = (item.stats && item.stats.type) || '';
|
||||||
|
const cat = (item.category || '') + ' ' + (item.name || '');
|
||||||
|
return type.includes(filter.armor) || cat.includes(filter.armor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does this weapon match the weapon class filter (Estoc/Longsword/.../2H)?
|
||||||
|
function matchesWeaponClass(item) {
|
||||||
|
if (filter.weapon === '2H') return true;
|
||||||
|
const name = (item.name || '').toLowerCase();
|
||||||
|
return name.includes(filter.weapon.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
const slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet'];
|
const slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet'];
|
||||||
|
|
||||||
|
|
@ -3830,16 +3870,21 @@ function analyzeGear() {
|
||||||
|
|
||||||
const consider = (item, source) => {
|
const consider = (item, source) => {
|
||||||
if (!item || !item.stats) return;
|
if (!item || !item.stats) return;
|
||||||
if (!gearUsable(item, playerLv)) return;
|
|
||||||
const isWeapon = slot === 'Mainhand';
|
const isWeapon = slot === 'Mainhand';
|
||||||
if (isWeapon) {
|
if (isWeapon) {
|
||||||
if (!gearIsTwoHandWeapon(item)) return;
|
if (!gearIsTwoHandWeapon(item)) return;
|
||||||
if (!item.name || !item.stats['Attack Accuracy']) return; // needs stats
|
if (!item.name || !item.stats['Attack Accuracy']) return;
|
||||||
|
// Apply weapon class filter — but always include the equipped item
|
||||||
|
if (source !== 'equipped' && !matchesWeaponClass(item)) return;
|
||||||
} else {
|
} else {
|
||||||
const det = gearDetectSlot(item);
|
const det = gearDetectSlot(item);
|
||||||
if (det !== slot) return;
|
if (det !== slot) return;
|
||||||
if (!item.stats['Physical Mitigation'] && !item.stats['Evade']) return;
|
if (!item.stats['Physical Mitigation'] && !item.stats['Evade']) return;
|
||||||
|
// Apply armor type filter — but always include the equipped item
|
||||||
|
if (source !== 'equipped' && !matchesArmorType(item)) return;
|
||||||
}
|
}
|
||||||
|
// Level filter — always include the equipped item (it's what you have on)
|
||||||
|
if (source !== 'equipped' && !gearUsable(item, playerLv)) return;
|
||||||
const score = isWeapon ? gearScoreWeapon(item) : gearScoreArmor(item);
|
const score = isWeapon ? gearScoreWeapon(item) : gearScoreArmor(item);
|
||||||
cands.push({ item, source, score });
|
cands.push({ item, source, score });
|
||||||
};
|
};
|
||||||
|
|
@ -3852,7 +3897,7 @@ function analyzeGear() {
|
||||||
results.push({ slot, current, cands });
|
results.push({ slot, current, cands });
|
||||||
});
|
});
|
||||||
|
|
||||||
return { playerLv, results };
|
return { playerLv, filter, results };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── UI ──
|
// ── UI ──
|
||||||
|
|
@ -3882,6 +3927,42 @@ function buildGearPanel() {
|
||||||
title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd';
|
title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd';
|
||||||
panel.appendChild(title);
|
panel.appendChild(title);
|
||||||
|
|
||||||
|
// ── Filters row ──
|
||||||
|
const filterRow = document.createElement('div');
|
||||||
|
filterRow.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:8px;font-size:10px;color:#aaa';
|
||||||
|
const filter = getGearFilter();
|
||||||
|
|
||||||
|
filterRow.appendChild(Object.assign(document.createElement('span'), { textContent: 'Armor:' }));
|
||||||
|
const armorSel = document.createElement('select');
|
||||||
|
armorSel.style.cssText = 'background:#222;color:#fff;border:1px solid #444;border-radius:3px;font-size:10px;font-family:monospace';
|
||||||
|
['Light', 'Heavy', 'Cloth', 'All'].forEach(t => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = t; opt.textContent = t;
|
||||||
|
if (t === filter.armor) opt.selected = true;
|
||||||
|
armorSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
filterRow.appendChild(armorSel);
|
||||||
|
|
||||||
|
filterRow.appendChild(Object.assign(document.createElement('span'), { textContent: 'Weapon:' }));
|
||||||
|
const weaponSel = document.createElement('select');
|
||||||
|
weaponSel.style.cssText = 'background:#222;color:#fff;border:1px solid #444;border-radius:3px;font-size:10px;font-family:monospace';
|
||||||
|
['2H', 'Estoc', 'Longsword', 'Great Mace', 'Club', 'Axe', 'Scythe'].forEach(t => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = t; opt.textContent = t;
|
||||||
|
if (t === filter.weapon) opt.selected = true;
|
||||||
|
weaponSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
filterRow.appendChild(weaponSel);
|
||||||
|
panel.appendChild(filterRow);
|
||||||
|
|
||||||
|
// Rebuild panel when filter changes
|
||||||
|
function applyFilter() {
|
||||||
|
setGearFilter({ armor: armorSel.value, weapon: weaponSel.value });
|
||||||
|
buildGearPanel();
|
||||||
|
}
|
||||||
|
armorSel.onchange = applyFilter;
|
||||||
|
weaponSel.onchange = applyFilter;
|
||||||
|
|
||||||
const { results } = analyzeGear();
|
const { results } = analyzeGear();
|
||||||
|
|
||||||
results.forEach(({ slot, current, cands }) => {
|
results.forEach(({ slot, current, cands }) => {
|
||||||
|
|
@ -3917,9 +3998,13 @@ function buildGearPanel() {
|
||||||
// Highlight best option
|
// Highlight best option
|
||||||
const best = cands[0];
|
const best = cands[0];
|
||||||
if (best && best.source !== 'equipped' && current) {
|
if (best && best.source !== 'equipped' && current) {
|
||||||
|
const equippedCand = cands.find(c => c.source === 'equipped');
|
||||||
|
// If equipped somehow missing from candidates, score it directly
|
||||||
|
const eqScore = equippedCand ? equippedCand.score
|
||||||
|
: (current.stats ? (slot === 'Mainhand' ? gearScoreWeapon(current) : gearScoreArmor(current)) : 0);
|
||||||
const rec = document.createElement('div');
|
const rec = document.createElement('div');
|
||||||
const icon = best.source === 'armory' ? '📦 Stored' : '🛒 For Sale';
|
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.textContent = ` ✅ Upgrade: ${icon} ${best.item.name} (${best.score.toFixed(0)} vs ${eqScore.toFixed(0)})`;
|
||||||
rec.style.cssText = 'color:#0f0;margin-top:2px';
|
rec.style.cssText = 'color:#0f0;margin-top:2px';
|
||||||
block.appendChild(rec);
|
block.appendChild(rec);
|
||||||
} else if (best && best.source === 'equipped') {
|
} else if (best && best.source === 'equipped') {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.14.39';
|
const VERSION = '0.14.40';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,15 @@ function gearUsable(item, playerLv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function gearPlayerLevel() {
|
function gearPlayerLevel() {
|
||||||
|
// Prefer battle-parser cached level (STATE.level)
|
||||||
|
if (STATE && STATE.level) return STATE.level;
|
||||||
|
// Parse from the HV cfg button (e.g. "Lv195")
|
||||||
|
const btn = document.getElementById('hv-cfg-btn');
|
||||||
|
if (btn) {
|
||||||
|
const m = (btn.textContent || '').match(/Lv(\d+)/i);
|
||||||
|
if (m) return parseInt(m[1]);
|
||||||
|
}
|
||||||
|
// Fallback: level_readout (may be CSS-font with no text)
|
||||||
const el = document.getElementById('level_readout');
|
const el = document.getElementById('level_readout');
|
||||||
if (el) {
|
if (el) {
|
||||||
const m = (el.textContent || '').match(/(\d+)/);
|
const m = (el.textContent || '').match(/(\d+)/);
|
||||||
|
|
@ -101,6 +110,21 @@ function gearPlayerLevel() {
|
||||||
return 155;
|
return 155;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Filter state (persisted) ──
|
||||||
|
const GEAR_FILTER_KEY = SP + 'gearFilter';
|
||||||
|
|
||||||
|
function getGearFilter() {
|
||||||
|
try {
|
||||||
|
const f = JSON.parse(localStorage[GEAR_FILTER_KEY] || 'null');
|
||||||
|
if (f && f.armor && f.weapon) return f;
|
||||||
|
} catch (e) {}
|
||||||
|
return { armor: 'Light', weapon: '2H' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGearFilter(f) {
|
||||||
|
try { localStorage[GEAR_FILTER_KEY] = JSON.stringify(f); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Main analysis ──
|
// ── Main analysis ──
|
||||||
function analyzeGear() {
|
function analyzeGear() {
|
||||||
const db = getGearDB();
|
const db = getGearDB();
|
||||||
|
|
@ -108,11 +132,27 @@ function analyzeGear() {
|
||||||
const armory = db.filter(e => e.source === 'armory' && e.stats);
|
const armory = db.filter(e => e.source === 'armory' && e.stats);
|
||||||
const buy = db.filter(e => e.source === 'buy' && e.stats);
|
const buy = db.filter(e => e.source === 'buy' && e.stats);
|
||||||
const playerLv = gearPlayerLevel();
|
const playerLv = gearPlayerLevel();
|
||||||
|
const filter = getGearFilter();
|
||||||
|
|
||||||
// Find equipped slot map (slotType from character page)
|
// Find equipped slot map (slotType from character page)
|
||||||
const slotMap = {};
|
const slotMap = {};
|
||||||
equipped.forEach(e => { if (e.slotType) slotMap[e.slotType] = e; });
|
equipped.forEach(e => { if (e.slotType) slotMap[e.slotType] = e; });
|
||||||
|
|
||||||
|
// Does this item match the armor type filter (Cloth/Light/Heavy/All)?
|
||||||
|
function matchesArmorType(item) {
|
||||||
|
if (filter.armor === 'All') return true;
|
||||||
|
const type = (item.stats && item.stats.type) || '';
|
||||||
|
const cat = (item.category || '') + ' ' + (item.name || '');
|
||||||
|
return type.includes(filter.armor) || cat.includes(filter.armor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does this weapon match the weapon class filter (Estoc/Longsword/.../2H)?
|
||||||
|
function matchesWeaponClass(item) {
|
||||||
|
if (filter.weapon === '2H') return true;
|
||||||
|
const name = (item.name || '').toLowerCase();
|
||||||
|
return name.includes(filter.weapon.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
const slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet'];
|
const slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet'];
|
||||||
|
|
||||||
|
|
@ -122,16 +162,21 @@ function analyzeGear() {
|
||||||
|
|
||||||
const consider = (item, source) => {
|
const consider = (item, source) => {
|
||||||
if (!item || !item.stats) return;
|
if (!item || !item.stats) return;
|
||||||
if (!gearUsable(item, playerLv)) return;
|
|
||||||
const isWeapon = slot === 'Mainhand';
|
const isWeapon = slot === 'Mainhand';
|
||||||
if (isWeapon) {
|
if (isWeapon) {
|
||||||
if (!gearIsTwoHandWeapon(item)) return;
|
if (!gearIsTwoHandWeapon(item)) return;
|
||||||
if (!item.name || !item.stats['Attack Accuracy']) return; // needs stats
|
if (!item.name || !item.stats['Attack Accuracy']) return;
|
||||||
|
// Apply weapon class filter — but always include the equipped item
|
||||||
|
if (source !== 'equipped' && !matchesWeaponClass(item)) return;
|
||||||
} else {
|
} else {
|
||||||
const det = gearDetectSlot(item);
|
const det = gearDetectSlot(item);
|
||||||
if (det !== slot) return;
|
if (det !== slot) return;
|
||||||
if (!item.stats['Physical Mitigation'] && !item.stats['Evade']) return;
|
if (!item.stats['Physical Mitigation'] && !item.stats['Evade']) return;
|
||||||
|
// Apply armor type filter — but always include the equipped item
|
||||||
|
if (source !== 'equipped' && !matchesArmorType(item)) return;
|
||||||
}
|
}
|
||||||
|
// Level filter — always include the equipped item (it's what you have on)
|
||||||
|
if (source !== 'equipped' && !gearUsable(item, playerLv)) return;
|
||||||
const score = isWeapon ? gearScoreWeapon(item) : gearScoreArmor(item);
|
const score = isWeapon ? gearScoreWeapon(item) : gearScoreArmor(item);
|
||||||
cands.push({ item, source, score });
|
cands.push({ item, source, score });
|
||||||
};
|
};
|
||||||
|
|
@ -144,7 +189,7 @@ function analyzeGear() {
|
||||||
results.push({ slot, current, cands });
|
results.push({ slot, current, cands });
|
||||||
});
|
});
|
||||||
|
|
||||||
return { playerLv, results };
|
return { playerLv, filter, results };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── UI ──
|
// ── UI ──
|
||||||
|
|
@ -174,6 +219,42 @@ function buildGearPanel() {
|
||||||
title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd';
|
title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd';
|
||||||
panel.appendChild(title);
|
panel.appendChild(title);
|
||||||
|
|
||||||
|
// ── Filters row ──
|
||||||
|
const filterRow = document.createElement('div');
|
||||||
|
filterRow.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:8px;font-size:10px;color:#aaa';
|
||||||
|
const filter = getGearFilter();
|
||||||
|
|
||||||
|
filterRow.appendChild(Object.assign(document.createElement('span'), { textContent: 'Armor:' }));
|
||||||
|
const armorSel = document.createElement('select');
|
||||||
|
armorSel.style.cssText = 'background:#222;color:#fff;border:1px solid #444;border-radius:3px;font-size:10px;font-family:monospace';
|
||||||
|
['Light', 'Heavy', 'Cloth', 'All'].forEach(t => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = t; opt.textContent = t;
|
||||||
|
if (t === filter.armor) opt.selected = true;
|
||||||
|
armorSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
filterRow.appendChild(armorSel);
|
||||||
|
|
||||||
|
filterRow.appendChild(Object.assign(document.createElement('span'), { textContent: 'Weapon:' }));
|
||||||
|
const weaponSel = document.createElement('select');
|
||||||
|
weaponSel.style.cssText = 'background:#222;color:#fff;border:1px solid #444;border-radius:3px;font-size:10px;font-family:monospace';
|
||||||
|
['2H', 'Estoc', 'Longsword', 'Great Mace', 'Club', 'Axe', 'Scythe'].forEach(t => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = t; opt.textContent = t;
|
||||||
|
if (t === filter.weapon) opt.selected = true;
|
||||||
|
weaponSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
filterRow.appendChild(weaponSel);
|
||||||
|
panel.appendChild(filterRow);
|
||||||
|
|
||||||
|
// Rebuild panel when filter changes
|
||||||
|
function applyFilter() {
|
||||||
|
setGearFilter({ armor: armorSel.value, weapon: weaponSel.value });
|
||||||
|
buildGearPanel();
|
||||||
|
}
|
||||||
|
armorSel.onchange = applyFilter;
|
||||||
|
weaponSel.onchange = applyFilter;
|
||||||
|
|
||||||
const { results } = analyzeGear();
|
const { results } = analyzeGear();
|
||||||
|
|
||||||
results.forEach(({ slot, current, cands }) => {
|
results.forEach(({ slot, current, cands }) => {
|
||||||
|
|
@ -209,9 +290,13 @@ function buildGearPanel() {
|
||||||
// Highlight best option
|
// Highlight best option
|
||||||
const best = cands[0];
|
const best = cands[0];
|
||||||
if (best && best.source !== 'equipped' && current) {
|
if (best && best.source !== 'equipped' && current) {
|
||||||
|
const equippedCand = cands.find(c => c.source === 'equipped');
|
||||||
|
// If equipped somehow missing from candidates, score it directly
|
||||||
|
const eqScore = equippedCand ? equippedCand.score
|
||||||
|
: (current.stats ? (slot === 'Mainhand' ? gearScoreWeapon(current) : gearScoreArmor(current)) : 0);
|
||||||
const rec = document.createElement('div');
|
const rec = document.createElement('div');
|
||||||
const icon = best.source === 'armory' ? '📦 Stored' : '🛒 For Sale';
|
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.textContent = ` ✅ Upgrade: ${icon} ${best.item.name} (${best.score.toFixed(0)} vs ${eqScore.toFixed(0)})`;
|
||||||
rec.style.cssText = 'color:#0f0;margin-top:2px';
|
rec.style.cssText = 'color:#0f0;margin-top:2px';
|
||||||
block.appendChild(rec);
|
block.appendChild(rec);
|
||||||
} else if (best && best.source === 'equipped') {
|
} else if (best && best.source === 'equipped') {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.14.39
|
// @version 0.14.40
|
||||||
// @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