From 641363b5b042bc223ab9eeef02732a046ad26905 Mon Sep 17 00:00:00 2001 From: GaboGG Date: Fri, 31 Jul 2026 10:50:18 -0400 Subject: [PATCH] v0.14.40 - Gear panel: armor/weapon filters + fix level detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- scripts/hv-unified.user.js | 97 +++++++++++++++++++++++++++++++++++--- scripts/latest.user.js | 97 +++++++++++++++++++++++++++++++++++--- src/config.js | 2 +- src/gear-analysis.js | 93 ++++++++++++++++++++++++++++++++++-- src/header.user.js | 2 +- 5 files changed, 273 insertions(+), 18 deletions(-) diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index b562840..d2eda97 100644 --- a/scripts/hv-unified.user.js +++ b/scripts/hv-unified.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.14.39 +// @version 0.14.40 // @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.39'; +const VERSION = '0.14.40'; const CFG = { // — Battle automation @@ -3801,6 +3801,15 @@ function gearUsable(item, playerLv) { } 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'); if (el) { const m = (el.textContent || '').match(/(\d+)/); @@ -3809,6 +3818,21 @@ function gearPlayerLevel() { 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 ── function analyzeGear() { const db = getGearDB(); @@ -3816,11 +3840,27 @@ function analyzeGear() { const armory = db.filter(e => e.source === 'armory' && e.stats); const buy = db.filter(e => e.source === 'buy' && e.stats); const playerLv = gearPlayerLevel(); + const filter = getGearFilter(); // Find equipped slot map (slotType from character page) const slotMap = {}; 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 slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']; @@ -3830,16 +3870,21 @@ function analyzeGear() { 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 + 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 { const det = gearDetectSlot(item); if (det !== slot) 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); cands.push({ item, source, score }); }; @@ -3852,7 +3897,7 @@ function analyzeGear() { results.push({ slot, current, cands }); }); - return { playerLv, results }; + return { playerLv, filter, results }; } // ── UI ── @@ -3882,6 +3927,42 @@ function buildGearPanel() { title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd'; 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(); results.forEach(({ slot, current, cands }) => { @@ -3917,9 +3998,13 @@ function buildGearPanel() { // Highlight best option const best = cands[0]; 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 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'; block.appendChild(rec); } else if (best && best.source === 'equipped') { diff --git a/scripts/latest.user.js b/scripts/latest.user.js index b562840..d2eda97 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.14.39 +// @version 0.14.40 // @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.39'; +const VERSION = '0.14.40'; const CFG = { // — Battle automation @@ -3801,6 +3801,15 @@ function gearUsable(item, playerLv) { } 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'); if (el) { const m = (el.textContent || '').match(/(\d+)/); @@ -3809,6 +3818,21 @@ function gearPlayerLevel() { 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 ── function analyzeGear() { const db = getGearDB(); @@ -3816,11 +3840,27 @@ function analyzeGear() { const armory = db.filter(e => e.source === 'armory' && e.stats); const buy = db.filter(e => e.source === 'buy' && e.stats); const playerLv = gearPlayerLevel(); + const filter = getGearFilter(); // Find equipped slot map (slotType from character page) const slotMap = {}; 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 slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']; @@ -3830,16 +3870,21 @@ function analyzeGear() { 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 + 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 { const det = gearDetectSlot(item); if (det !== slot) 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); cands.push({ item, source, score }); }; @@ -3852,7 +3897,7 @@ function analyzeGear() { results.push({ slot, current, cands }); }); - return { playerLv, results }; + return { playerLv, filter, results }; } // ── UI ── @@ -3882,6 +3927,42 @@ function buildGearPanel() { title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd'; 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(); results.forEach(({ slot, current, cands }) => { @@ -3917,9 +3998,13 @@ function buildGearPanel() { // Highlight best option const best = cands[0]; 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 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'; block.appendChild(rec); } else if (best && best.source === 'equipped') { diff --git a/src/config.js b/src/config.js index f67ae7e..8a91abb 100644 --- a/src/config.js +++ b/src/config.js @@ -2,7 +2,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.14.39'; +const VERSION = '0.14.40'; const CFG = { // — Battle automation diff --git a/src/gear-analysis.js b/src/gear-analysis.js index 577999c..b4875ec 100644 --- a/src/gear-analysis.js +++ b/src/gear-analysis.js @@ -93,6 +93,15 @@ function gearUsable(item, playerLv) { } 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'); if (el) { const m = (el.textContent || '').match(/(\d+)/); @@ -101,6 +110,21 @@ function gearPlayerLevel() { 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 ── function analyzeGear() { const db = getGearDB(); @@ -108,11 +132,27 @@ function analyzeGear() { const armory = db.filter(e => e.source === 'armory' && e.stats); const buy = db.filter(e => e.source === 'buy' && e.stats); const playerLv = gearPlayerLevel(); + const filter = getGearFilter(); // Find equipped slot map (slotType from character page) const slotMap = {}; 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 slots = ['Mainhand', 'Head', 'Body', 'Hands', 'Legs', 'Feet']; @@ -122,16 +162,21 @@ function analyzeGear() { 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 + 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 { const det = gearDetectSlot(item); if (det !== slot) 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); cands.push({ item, source, score }); }; @@ -144,7 +189,7 @@ function analyzeGear() { results.push({ slot, current, cands }); }); - return { playerLv, results }; + return { playerLv, filter, results }; } // ── UI ── @@ -174,6 +219,42 @@ function buildGearPanel() { title.style.cssText = 'font-weight:bold;margin-bottom:6px;color:#ffd'; 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(); results.forEach(({ slot, current, cands }) => { @@ -209,9 +290,13 @@ function buildGearPanel() { // Highlight best option const best = cands[0]; 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 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'; block.appendChild(rec); } else if (best && best.source === 'equipped') { diff --git a/src/header.user.js b/src/header.user.js index f6ffb8d..7e06d3f 100644 --- a/src/header.user.js +++ b/src/header.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.14.39 +// @version 0.14.40 // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @author GaboGG + Hermes // @match *://*.hentaiverse.org/*