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==
|
||||
// @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') {
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// CONFIG — default settings
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const VERSION = '0.14.39';
|
||||
const VERSION = '0.14.40';
|
||||
|
||||
const CFG = {
|
||||
// — Battle automation
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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/*
|
||||
|
|
|
|||
Loading…
Reference in a new issue