diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index b3db691..39fc5f2 100644 --- a/scripts/hv-unified.user.js +++ b/scripts/hv-unified.user.js @@ -1,8 +1,8 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.15.4 -// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse +// @version 0.16.0 +// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils) // @author GaboGG + Hermes // @match *://*.hentaiverse.org/* // @match *://alt.hentaiverse.org/* @@ -13,21 +13,18 @@ (function() { 'use strict'; + // ═══════════════════════════════════════════════════════════════════════ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.15.4'; +const VERSION = '0.16.0'; const CFG = { - // — Battle automation - hotkey: 'KeyQ', - hotkeyMod: '', - hoverEnabled: true, + // — Battle strategy advisory autoBuff: true, autoDebuff: true, autoCure: true, - autoSpirit: false, // Manual spirit stance only (auto-disable at 65% SP for Spark) // — Thresholds (simulation-backed) cureHP: 0.45, // Cast Cure below this HP @@ -35,33 +32,22 @@ const CFG = { cureRegenHP: 0.65, // Cast Regen below this HP manaGemMP: 0.45, // Use mana gems below this MP manaPotionMP: 0.25, // Use mana potions at this MP - spiritPotionSP: 0.60, // Use spirit gems/potions below this SP (Spark costs 50%) - spiritStanceOC: 80, // Manual spirit stance only (auto-disable at 65% SP for Spark) - skillOC: 60, // Use weapon skills at this OC (lower = faster, key on IWBTH) + skillOC: 60, // Use weapon skills at this OC useAttackSpells: false, // Don't use attack spells (let channel proc naturally) // — Out of battle autoSell: true, sellQuality: 'Average', - bulkShrine: true, - reTimer: true, trainingQueue: true, - // — UI - showCooldowns: true, - showMonsterHP: true, - showDurations: true, - alertColours: true, - showMonsterNumbers: true, + // — UI & Guidance cfgButton: true, showGuidance: true, showEquipAdvice: true, autoDifficulty: true, - - // — Combat style - useAttackSpells: true, }; + // ═══════════════════════════════════════════════════════════════════════ // STATE — global runtime state // ═══════════════════════════════════════════════════════════════════════ @@ -1170,28 +1156,12 @@ function strategyNovice() { return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' }; } - // 4. Mana/spirit items — AFTER buffs (Novice) + // 4. Mana items — AFTER buffs (Novice) if (STATE.mp < CFG.manaGemMP) { const gem = hasUsableGem('mana'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } - // Spirit gem (P-slot) — auto-consume when available - const pGem = document.getElementById('ikey_p'); - const pGemId = pGem ? (pGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; - if (pGemId && pGemId[1] === '10007') { - if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true }; - } - // Spirit draughts/potions — only when SP drops below threshold - if (STATE.sp < CFG.spiritPotionSP) { - // Check non-P-slot spirit items only - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') { - return { type: 'item', id: slot, selfTarget: true }; - } - } - } + // 5. Weapon skills — these are better than basic attacks const skillOC = CFG.skillOC || 80; @@ -1255,20 +1225,6 @@ function strategyAdept() { if (gem && !STATE.channeling && !STATE._mysticUsed) return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' }; - // 0b. Spark of Life SP safety - if (STATE.sp < 0.55) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; - } - } - - // 0c. Spirit Stance safety auto-disable - if (STATE.spiritStance && STATE.sp < 0.60) { - return { type: 'toggle_spirit', _reason: 'sp_safety' }; - } - // 1. Health items — safe to use early if (STATE.hp < CFG.cureItemHP) { const gem = hasUsableGem('heal'); @@ -1281,15 +1237,6 @@ function strategyAdept() { if (c) return { type: 'spell', name: c, selfTarget: true }; } - // 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so - // Spark can actually save us. - if (STATE.sp < 0.55) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true }; - } - } // 3. Buffs // On first round, cast cheapest-first to proc channeling @@ -1318,25 +1265,12 @@ function strategyAdept() { return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' }; } - // 4. Mana/spirit items — AFTER buffs (Adept) + // 4. Mana items — AFTER buffs (Adept) if (STATE.mp < CFG.manaGemMP) { const gem = hasUsableGem('mana'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } - // Spirit gem (P-slot) — auto-consume when available - const apGem = document.getElementById('ikey_p'); - const apGemId = apGem ? (apGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; - if (apGemId && apGemId[1] === '10007') { - if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true }; - } - // Spirit draughts/potions — only at SP < threshold - if (STATE.sp < CFG.spiritPotionSP) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true }; - } - } + // 4. Debuffs — conservative: Imperil on bosses/rares or 5+ mobs // Weaken only on high-threat or bosses. Estoc handles <5. @@ -1433,45 +1367,6 @@ function strategyVeteran() { if (gem && !STATE.channeling && !STATE._mysticUsed) return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' }; - // 0b. Spark of Life SP safety — Spark costs 50% SP when triggered. - // If SP < 60%, consume a spirit item before anything else. - if (STATE.sp < 0.60) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; - } - } - - // 0c. Spirit Stance safety auto-disable — if left on, drains 10% SP/turn - if (STATE.spiritStance && STATE.sp < 0.60) { - return { type: 'toggle_spirit', _reason: 'sp_safety' }; - } - - // 0d. Spirit Stance for burst clear — toggle on for 7+ mob, off after one skill - // Gated by CFG.autoSpirit (default OFF — manual toggle only) - const is2H = STATE.skillsKnown.includes('Great Cleave') || STATE.skillsKnown.includes('Rending Blow'); - if (CFG.autoSpirit && is2H && STATE.monsters && STATE.monsters.length >= 7 && STATE.oc >= (CFG.skillOC || 60)) { - if (STATE._spiritForSkill >= 1 && STATE.spiritStance) { - if (STATE._spiritForSkill === 1) { - STATE._spiritForSkill = 2; - } else if (STATE._spiritForSkill === 2) { - STATE._spiritForSkill = 0; - return { type: 'toggle_spirit', _reason: 'spirit_off_7' }; - } - } else if (!STATE.spiritStance && STATE._spiritForSkill === 0) { - STATE._spiritForSkill = 1; - return { type: 'toggle_spirit', _reason: 'spirit_on_7' }; - } - } - // Toggle-off guard: if flag says skill was cast, toggle off even if mob count dropped - if (STATE._spiritForSkill === 2 && STATE.spiritStance) { - STATE._spiritForSkill = 0; - return { type: 'toggle_spirit', _reason: 'spirit_off_7' }; - } - // Reset flag if spirit got turned off externally - if (!STATE.spiritStance) STATE._spiritForSkill = 0; - // 0e. HIGH-THREAT WEAPON SKILLS — only when 3+ enemies // Skip if ≤2 enemies — basic attacks build OC for next wave instead. const SINGLE_THREAT = Math.round(120 * 90 / 100); @@ -1496,16 +1391,6 @@ function strategyVeteran() { } } - // 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so - // Spark can actually save us. Use spirit draughts/potions proactively. - if (STATE.sp < 0.55) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; - } - } - // 2c. OC-building priority: when 5+ monsters, OC < 60, and AoE skills ready, // skip debuffs/non-essential buffs and basic attack to build OC fast. const needsOC = STATE.monsters && STATE.monsters.length >= 4 @@ -1514,26 +1399,21 @@ function strategyVeteran() { && document.querySelector(`.btqs[onmouseover*="set_infopane_spell('Rending Blow'"], .btsd[onmouseover*="set_infopane_spell('Rending Blow'"]`) ?.hasAttribute('onclick'); if (needsOC) { - // No hp/mp guard — if Spark procced, hp=0 but we still need OC const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : findOptimalDominoTarget(); if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) return { type: 'attack', target: t, _reason: 'oc_build' }; } // 3. Buffs - // On first round, cast cheapest-first to proc channeling if (CFG.autoBuff) { { const ib = castInitialBuffs(); if (ib) return ib; } - // If channeling is active, prioritize most expensive buff for best value if (STATE.channeling && STATE.mp > 0.10) { const ch = findBestChannelingTarget(); if (ch) return ch; } - // Collect available buff candidates and sort by cost - // No channeling → cheapest first (proc chance), channeling → most expensive (MP value) const buffCandidates = []; for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) { @@ -1548,35 +1428,19 @@ function strategyVeteran() { } } - // 4. Mana/spirit items — AFTER buffs (Novice) + // 4. Mana items if (STATE.mp < CFG.manaGemMP) { const gem = hasUsableGem('mana'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } - // Spirit gem (P-slot) — use immediately - const vpGem = document.getElementById('ikey_p'); - const vpGemId = vpGem ? (vpGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; - if (vpGemId && vpGemId[1] === '10007') { - if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true }; - } - // Spirit draughts/potions — only at SP < threshold - if (STATE.sp < CFG.spiritPotionSP) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true }; - } - } - // 4. Debuffs — only when NOT in spirit burst mode (flag >= 1 means skill pending) - if (STATE._spiritForSkill === 0 && CFG.autoDebuff && STATE.mp > 0.35) { - // Imperil: bosses only, or 5+ mobs in Grindfest (Estoc handles <5) + // 4. Debuffs + if (CFG.autoDebuff && STATE.mp > 0.35) { if (hasReadySpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 5)) { const b = findStrongestMonster(); if (b >= 0 && !checkMonsterDebuff(b, 'imperil')) return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' }; } - // Weaken: only on monsters about to use special attacks (high SP) or bosses if (hasReadySpell('Weaken') && (anyRareMonster() || hasHighThreat(THREAT_PX))) { const b = findDangerousMonster(); if (b >= 0 && !checkMonsterDebuff(b, 'weaken')) @@ -1585,20 +1449,16 @@ function strategyVeteran() { } // 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar) - // If 2+ enemies have >85% SP bar, they're about to special — emergency const isHighThreat = hasHighThreat(THREAT_PX); if (isHighThreat) console.log('%c[HV] ⚠ HIGH THREAT: 2+ monsters with SP >85%', 'color:#f44'); const askillOC3 = CFG.skillOC || 80; if (STATE.oc >= askillOC3 || (isHighThreat && STATE.monsters.length >= 3)) { if (isHighThreat && STATE.oc < askillOC3) console.log('%c[HV] ⚠ emergency: using skill at OC=' + STATE.oc.toFixed(0), 'color:#f80'); - // Emergency: 2+ monsters about to special — spend OC to kill them fast - // Rending Blow: AoE armor pen vs 3+ enemies. Target center monster for max 5-target splash. if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) { const t = isHighThreat ? findDangerousMonster() : findOptimalDominoTarget(); if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' }; } - // Shatter Strike: AoE stun vs 3+ enemies with armor break if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) { const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor')); if (hasArmorBreak) { @@ -1606,12 +1466,10 @@ function strategyVeteran() { if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t, _reason: 'stun' }; } } - // Great Cleave: boss/rare fights or single-target threat if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = isHighThreat ? findDangerousMonster() : findStrongestMonster(); if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t, _reason: isHighThreat ? 'threat' : 'boss' }; } - // If high threat and 3+ enemies, use single-target skills on the most dangerous if (isHighThreat && STATE.oc >= (CFG.skillOC || 60) && STATE.monsters.length >= 3) { const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow', 'Vital Strike', 'Shield Bash', 'Concussive Strike']; @@ -1624,6 +1482,7 @@ function strategyVeteran() { } } + // 5. Spirit gem auto-consume const spGem = document.getElementById('ikey_p'); const spGemId = spGem ? (spGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; @@ -1693,16 +1552,14 @@ function executeAction(a) { case 'spell': result = a.selfTarget ? castSelfSpell(a.name) : castTargetSpell(a.name, a.target); break; case 'skill': result = useSkill(a.name, a.target); break; case 'item': result = useItem(a.id); break; - case 'toggle_spirit': result = toggleSpiritStance(); break; } if (result) { - // After dispatching an action, stagger the debounce so we don't queue - // another action while the game processes this one (typically ~500-1000ms) STATE._lastActionTime = Date.now(); } return result; } + function attackMonster(i) { if (i < 0 || i >= STATE.monsters.length) return false; const m = STATE.monsters[i]; @@ -1794,54 +1651,26 @@ function useSkill(n, i) { return true; } -function toggleSpiritStance() { - const t = document.getElementById('ckey_spirit'); - if (!t) return false; - t.click(); - return true; -} // ═══════════════════════════════════════════════════════════════════════ -// HOVER SYSTEM — mouse-over monster actions +// HOVER SYSTEM — REMOVED (Handled by Monsterbation / MB) // ═══════════════════════════════════════════════════════════════════════ -function setupHover() { - if (!CFG.hoverEnabled) return; +function setupHover() {} - STATE.monsters.forEach((m, i) => { - if (!m || m.dataset.hvHover) return; - m.dataset.hvHover = '1'; - - const area = m.querySelector('.btm6') || m; - area.addEventListener('mouseenter', () => { - STATE.hoverTarget = i; - if (!STATE.interruptHover && !STATE.interruptAlert) { - const ac = getSmartAction(); - if (ac) executeAction(ac); - } - }); - area.addEventListener('mouseleave', () => { STATE.hoverTarget = -1; }); - }); - - // RiddleMaster blocks hover - if (document.getElementById('riddlemaster')) { - STATE.interruptHover = true; - } -} // ═══════════════════════════════════════════════════════════════════════ // KEYBINDINGS — keyboard shortcuts for battle // ═══════════════════════════════════════════════════════════════════════ let keybindingsSetup = false; -const Q_DEBOUNCE_MS = 300; // Ignore Q repeats faster than this function setupKeybindings() { if (keybindingsSetup) return; keybindingsSetup = true; document.addEventListener('keydown', function(e) { - // Settings — comma key + // Settings — comma key (non-conflicting with Monsterbation) if (e.keyCode === KEYS.COMMA) { if (isBattlePage()) { e.preventDefault(); @@ -1850,226 +1679,24 @@ function setupKeybindings() { return; } } - - // Main action hotkey (default: Q) - if (e.code === CFG.hotkey && !e.ctrlKey && !e.altKey && !e.metaKey) { - if (!isInputFocused() && isBattlePage()) { - // Debounce: ignore key-repeats until game processes the action - const now = Date.now(); - if (now - (STATE._lastActionTime || 0) < Q_DEBOUNCE_MS) return; - STATE._lastActionTime = now; - - console.log('%c[HV] Q pressed — checking action...', 'color:#888'); - parseBattleState(); - // Debug: show full battle state - const activeBuffs = Object.entries(STATE.buffs) - .filter(([k, v]) => v > 0 && k !== 'regen' || v > 0) - .map(([k, v]) => `${k}=${v}`).join(' '); - // Monster SP bars (threat levels) with alive/dead markers - const threatLevels = STATE.monsters.map((m, i) => { - const sp = STATE.monsterSp[i] || 0; - const alive = m && m.hasAttribute && m.hasAttribute('onclick') ? 'A' : 'D'; - return `${i}:${sp}${alive}`; - }).join(' '); - // Skill cooldowns — check DOM for opacity/onclick on skill elements - const skillCDs = ['Rending Blow', 'Shatter Strike', 'Great Cleave'] - .filter(s => STATE.skillsKnown.includes(s)) - .map(s => { - const el = document.querySelector(`.btqs[onmouseover*="set_infopane_spell('${s}'"], .btsd[onmouseover*="set_infopane_spell('${s}'"]`); - const ready = el && el.hasAttribute('onclick'); - return `${s.charAt(0)}${ready ? 'R' : 'C'}`; - }).join(''); - const curMp = parseInt((document.getElementById('vrm') || {}).textContent) || 0; - const curHp = parseInt((document.getElementById('vrhd') || {}).textContent) || 0; - // Item counts from itemsKnown - const itemTypes = {}; - Object.values(STATE.itemsKnown).forEach(info => { - const def = ITEMS[parseInt(info)]; - if (def) itemTypes[def.t] = (itemTypes[def.t] || 0) + 1; - }); - const itemStr = ['heal','spirit','mana'].filter(t => itemTypes[t]).map(t => `${t.charAt(0)}${itemTypes[t]}`).join(' '); - console.log( - `%c[HV] ch=${STATE.channeling} hp=${curHp} mp=${curMp}(${(STATE.mp*100).toFixed(0)}%) ` + - `sp=${(STATE.sp*100).toFixed(0)}% oc=${STATE.oc.toFixed(0)} ` + - `mon:${STATE.monsters.length} sk:${skillCDs || '--'} ` + - `${itemStr} ` + - `buffs: ${activeBuffs} | spBars: ${threatLevels}`, - 'color:#888' - ); - const a = getRecommendedAction(); - if (a) { - e.preventDefault(); - // Show explicit action details - let label = ''; - if (a.type === 'spell') { - label = a.name + (a.target >= 0 ? ' → monster ' + a.target : ' (self)'); - } else if (a.type === 'skill') { - label = a.name + ' → monster ' + a.target; - } else if (a.type === 'item') { - const itemDef = ITEMS[parseInt(STATE.itemsKnown[a.id] || '0')]; - label = (itemDef ? itemDef.n : a.id) + (a.selfTarget ? ' (self)' : ''); - } else if (a.type === 'attack') { - label = 'monster ' + a.target; - } else { - label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id; - } - console.log(`%c[HV] ▶ ${a.type} → ${label}${a._reason ? ' (' + a._reason + ')' : ''}`, 'color:#0f0'); - executeAction(a); - } - } - } - - // Hover toggle (H) - if (e.code === 'KeyH' && !e.ctrlKey && !e.altKey && !e.metaKey && !isInputFocused()) { - STATE.interruptHover = !STATE.interruptHover; - console.log(`%c[HV] Hover ${STATE.interruptHover ? 'OFF' : 'ON'}`, 'color:#f80'); - } - - // Emergency heal (C) — always cast Cure/Full-Cure regardless of strategy - if (e.code === 'KeyC' && !e.ctrlKey && !e.altKey && !e.metaKey && !isInputFocused()) { - const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null; - if (c) { - console.log(`%c[HV] 🚑 Emergency: casting ${c}`, 'color:#f44'); - castSelfSpell(c); - } - } }); - console.log('%c⌨ [HV] Keys: Q=action H=hover C=cure ,=settings', 'color:#0f0;font-size:11px'); + console.log('%c⌨ [HV] Key: ,=settings (Battle input managed by Monsterbation)', 'color:#0f0;font-size:11px'); } + // ═══════════════════════════════════════════════════════════════════════ -// UI OVERLAYS — cooldowns, durations, alerts, monster numbers, monster HP +// UI OVERLAYS — REMOVED (Handled by Monsterbation / MB) // ═══════════════════════════════════════════════════════════════════════ -function updateCooldownDisplay() { - if (!CFG.showCooldowns) return; - $$('.hv-cd-overlay').forEach(e => e.remove()); - $$('.btqb').forEach(el => { - const img = el.querySelector('img'); - if (!img) return; - const src = (img.src || '').toLowerCase(); - for (const [k, v] of Object.entries(STATE.cooldowns)) { - if (v <= 0) continue; - if (src.includes(k.toLowerCase()) || src.includes(k.replace(/_/g, ''))) { - const o = document.createElement('div'); - o.className = 'hv-cd-overlay'; - o.style.cssText = 'position:absolute;top:0;right:0;background:rgba(0,0,0,0.7);color:#f00;font-size:9px;font-weight:bold;padding:0 2px;border-radius:2px;pointer-events:none'; - o.textContent = v; - el.style.position = 'relative'; - el.appendChild(o); - break; - } - } - }); -} +function updateCooldownDisplay() {} +function updateDurationDisplay() {} +function updateAlertColours() {} +function addMonsterNumbers() {} +function updateMonsterHPDisplay() {} +function saveMonsterData() {} +function refreshUI() {} -function updateDurationDisplay() { - if (!CFG.showDurations) return; - $$('.hv-dur-overlay').forEach(e => e.remove()); - const pane = document.getElementById('pane_effects'); - if (!pane) return; - $$('img[onmouseover]', pane).forEach(img => { - const omo = img.getAttribute('onmouseover') || ''; - const dur = omo.match(/(\d+)\s*turns?\s*rem/); - if (!dur) return; - const t = parseInt(dur[1]); - const st = omo.match(/(\d+)\s*stacks?/i); - const o = document.createElement('div'); - o.className = 'hv-dur-overlay'; - o.style.cssText = `position:absolute;bottom:-2px;left:50%;transform:translateX(-50%);background:${t < 3 ? '#f44' : '#44f'};color:#fff;font-size:8px;font-weight:bold;padding:0 2px;border-radius:2px;pointer-events:none;z-index:1`; - o.textContent = st ? `${t}x${parseInt(st[1])}` : t; - img.parentElement.style.position = 'relative'; - img.parentElement.appendChild(o); - }); -} - -function updateAlertColours() { - if (!CFG.alertColours) return; - let bg = '#EDEBDF'; - STATE.interruptAlert = false; - - if (!$('img[src$="bar_dgreen.png"]') && $('img[src$="fallenshield.png"]')) { - bg = 'magenta'; - STATE.interruptAlert = true; - } else if (STATE.hp < 0.25) { - bg = '#ff4488'; - STATE.interruptAlert = true; - } else if (STATE.mp < 0.15) { - bg = '#4444aa'; - } else if (Object.values(STATE.buffs).some(d => d < 2 && d > 0)) { - bg = '#88ccff'; - } - - const v = document.getElementById('pane_vitals'); - if (v) v.style.background = bg; -} - -function addMonsterNumbers() { - if (!CFG.showMonsterNumbers) return; - STATE.monsters.forEach((m, i) => { - if (!m || !m.querySelector || m.querySelector('.hv-monster-num')) return; - const b = m.querySelector('.btm1'); - if (!b) return; - const s = document.createElement('span'); - s.className = 'hv-monster-num'; - s.style.cssText = 'font-weight:bold;margin-right:3px;color:#999'; - s.textContent = (i + 1); - b.insertBefore(s, b.firstChild); - }); -} - -function updateMonsterHPDisplay() { - if (!CFG.showMonsterHP) return; - STATE.monsters.forEach((m, i) => { - if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return; - const n = m.querySelector('.btm1'); - if (!n) return; - const nm = (n.textContent || '').replace(/^\d+\s*/, '').trim(); - const md = STATE.monsterData[nm]; - if (!md || !md.hp || md.seen < 3) return; - const hpBar = m.querySelector('img[src$="nbargreen.png"]'); - if (!hpBar) return; - const r = clamp(parseInt(hpBar.style.width || '') / 120, 0.01, 1); - const txt = `HP: ${Math.max(1, Math.round(r * md.hp)).toLocaleString()} / ${md.hp.toLocaleString()}`; - const ex = m.querySelector('.hv-monster-hp'); - if (ex) { ex.textContent = txt; return; } - const d = document.createElement('div'); - d.className = 'hv-monster-hp'; - d.style.cssText = 'display:inline-block;position:relative;left:204px;top:-20px;font-size:9px;color:#888'; - d.textContent = txt; - m.appendChild(d); - }); -} - -function saveMonsterData() { - STATE.monsters.forEach((m, i) => { - if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return; - const n = m.querySelector('.btm1'); - if (!n) return; - const nm = (n.textContent || '').replace(/^\d+\s*/, '').trim(); - if (!nm) return; - const hpBar = m.querySelector('img[src$="nbargreen.png"]'); - if (hpBar) { - const r = clamp(parseInt(hpBar.style.width || '') / 120, 0.01, 1); - if (!STATE.monsterData[nm]) STATE.monsterData[nm] = { hp: 0, seen: 0 }; - STATE.monsterData[nm].hp = Math.max( - STATE.monsterData[nm].hp, - Math.round(STATE.monsterData[nm].hp * 0.7 + 1000 / r * 0.3) - ); - STATE.monsterData[nm].seen++; - } - }); - try { localStorage[SP + 'monsters'] = JSON.stringify(STATE.monsterData); } catch (e) {} -} - -function refreshUI() { - if (CFG.showDurations) updateDurationDisplay(); - if (CFG.showCooldowns) updateCooldownDisplay(); - if (CFG.alertColours) updateAlertColours(); - if (CFG.showMonsterNumbers) addMonsterNumbers(); - if (CFG.showMonsterHP) updateMonsterHPDisplay(); -} // ═══════════════════════════════════════════════════════════════════════ // SETTINGS PANEL — in-battle configuration UI @@ -2124,41 +1751,29 @@ function openSettings() { p.innerHTML = `
- ⚙ HV Unified v${VERSION} + ⚙ HV Unified v${VERSION} (Bridge & Advisor)
- Battle - ${mkTog('Auto-buff (Haste, Protection, etc.)', 'autoBuff')} - ${mkTog('Auto-debuff (Imperil, Weaken)', 'autoDebuff')} - ${mkTog('Auto-cure when HP low', 'autoCure')} - ${mkTog('Auto Spirit Stance', 'autoSpirit')} - ${mkTog('Hover-to-attack mode', 'hoverEnabled')} + Strategy Advisory + ${mkTog('Auto-buff recommendations', 'autoBuff')} + ${mkTog('Auto-debuff recommendations', 'autoDebuff')} + ${mkTog('Auto-cure recommendations', 'autoCure')} ${mkTog('Auto-difficulty suggestion', 'autoDifficulty')} - ${mkTog('Use attack spells', 'useAttackSpells')} + ${mkTog('Use attack spells in advice', 'useAttackSpells')} ${mkNum('Cure HP threshold', 'cureHP')} ${mkNum('Item HP threshold', 'cureItemHP')} ${mkNum('Mana gem MP threshold', 'manaGemMP')} ${mkNum('Mana potion MP threshold', 'manaPotionMP')} - ${mkNum('Spirit Stance OC%', 'spiritStanceOC', 5)} + ${mkNum('Skill OC% threshold', 'skillOC', 5)}
- Out of Battle - ${mkTog('Equip quick sell buttons', 'autoSell')} - ${mkTog('Bulk Shrine buttons', 'bulkShrine')} - ${mkTog('RE Timer', 'reTimer')} + Out of Battle & Guidance ${mkTog('Training queue info', 'trainingQueue')} ${mkTog('Advisor panel', 'showGuidance')} ${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')} -
- UI - ${mkTog('Cooldown timers', 'showCooldowns')} - ${mkTog('Monster HP numbers', 'showMonsterHP')} - ${mkTog('Buff duration counters', 'showDurations')} - ${mkTog('Alert colours', 'alertColours')} - ${mkTog('Monster numbers', 'showMonsterNumbers')} ${mkTog('Config button', 'cfgButton')}
- Changes apply immediately. Press , in battle to open settings. + Changes apply immediately. Press , to open settings.
`; document.body.appendChild(p); @@ -2177,6 +1792,7 @@ function openSettings() { }); } + // ═══════════════════════════════════════════════════════════════════════ // OUT-OF-BATTLE — shop enhancements, shrine, training, etc. // ═══════════════════════════════════════════════════════════════════════ @@ -2309,38 +1925,7 @@ function enhanceEquipShopWithAdvice() { // ── Shrine: bulk selection ── function enhanceShrine() { - if (!CFG.bulkShrine) return; - const m = document.getElementById('mainpane'); - if (!m || document.getElementById('hv-bulk-shrine')) return; - - const r = document.createElement('div'); - r.id = 'hv-bulk-shrine'; - r.style.cssText = 'margin:6px;display:flex;gap:6px'; - - const b1 = document.createElement('input'); - b1.type = 'button'; - b1.value = '⛩️ Select Non-Figurines'; - b1.style.cssText = 'padding:4px 10px;cursor:pointer;background:#fdcb00;color:#000;border:none;border-radius:3px;font-size:12px'; - b1.onclick = () => { - $$('tr', m).forEach(row => { - const t = (row.textContent || ''); - if (!t.includes('Figurine') && !t.includes('Collectable')) { - const cb = row.querySelector('input[type="checkbox"]'); - if (cb) cb.checked = true; - } - }); - }; - - const b2 = document.createElement('input'); - b2.type = 'button'; - b2.value = '✗ Clear'; - b2.style.cssText = 'padding:4px 10px;cursor:pointer;background:#666;color:#fff;border:none;border-radius:3px;font-size:12px'; - b2.onclick = () => { $$('input[type="checkbox"]', m).forEach(cb => cb.checked = false); }; - - r.appendChild(b1); - r.appendChild(b2); - const ta = m.querySelector('div'); - if (ta) ta.insertBefore(r, ta.firstChild); + // Shrine enhancements are owned by HVUT. } // ── Training: priority guide ── @@ -2359,103 +1944,11 @@ function enhanceTraining() { if (ta) ta.insertBefore(d, ta.firstChild); } -// ── Monster Lab: floating info panel + feed buttons ── - -let _mlDragOffset = { x: 0, y: 0 }; - function enhanceMonsterLab() { - // Create floating draggable panel - if (document.getElementById('hv-monster')) return; - - const panel = document.createElement('div'); - panel.id = 'hv-monster'; - panel.style.cssText = css({ - position: 'fixed', - top: '80px', - right: '4px', - zIndex: '9999', - background: '#111827', - color: '#d1d5db', - padding: '8px 10px', - borderRadius: '8px', - fontSize: '10px', - fontFamily: 'monospace', - maxWidth: '320px', - boxShadow: '0 0 15px rgba(0,0,0,0.6)', - border: '1px solid #374151', - cursor: 'move', - userSelect: 'none', - }); - - const body = `
- 🧬 Monster Lab - -
-
-
- - -
-
- New to ML? Pick a Beast-class (Dog/Cow/Pig) for STR/END crystals.
- Click + drag header to move. -
-
`; - - panel.innerHTML = body; - document.body.appendChild(panel); - - // ── Feed All ── - panel.querySelector('#hv-feed-all').onclick = () => { - const m = document.getElementById('mainpane'); - if (m) $$('input[value="Feed"]', m).forEach(b => b.click()); - }; - - // ── Happy Pills All ── - panel.querySelector('#hv-pill-all').onclick = () => { - const m = document.getElementById('mainpane'); - if (m) { - const pills = $$('option', m).filter(o => (o.textContent || '').includes('Happy Pill')); - if (pills.length > 0) { - pills[0].selected = true; - $$('input[value="Feed"]', m).forEach(b => b.click()); - } - } - }; - - // ── Toggle ── - panel.querySelector('#hv-monster-toggle').onclick = () => { - const body = panel.querySelector('#hv-monster-body'); - const isHidden = body.style.display === 'none'; - body.style.display = isHidden ? 'block' : 'none'; - panel.querySelector('#hv-monster-toggle').textContent = isHidden ? '−' : '+'; - }; - - // ── Make draggable ── - const header = panel.querySelector('#hv-monster-header'); - if (header) { - header.addEventListener('mousedown', (e) => { - _mlDragOffset.x = e.clientX - panel.getBoundingClientRect().left; - _mlDragOffset.y = e.clientY - panel.getBoundingClientRect().top; - const onMove = (ev) => { - panel.style.left = (ev.clientX - _mlDragOffset.x) + 'px'; - panel.style.right = 'auto'; - panel.style.top = (ev.clientY - _mlDragOffset.y) + 'px'; - }; - const onUp = () => { - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onUp); - }; - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onUp); - }); - } - - // Remove the old inline bar if it exists - const oldBar = document.getElementById('hv-lab-enhance'); - if (oldBar) oldBar.remove(); + // Monster Lab enhancements are owned by HVUT. } + // ── Config button (bottom-right) ── function addConfigButton() { @@ -3131,268 +2624,11 @@ function gradeEquipment(name) { } function enhanceArmory() { - const equipList = document.getElementById('equiplist'); - if (!equipList || document.getElementById('hv-armory-bar')) return; - - // ── Parse current equipment in the list ── - const items = []; - let currentCategory = ''; - - $$('tr', equipList).forEach(row => { - if (row.className === 'eqtplabel') { - currentCategory = (row.textContent || '').trim(); - return; - } - - const cb = row.querySelector('input[name="eqids[]"]'); - if (!cb) return; - - const id = cb.value; - const label = row.querySelector('label'); - const name = label ? (label.textContent || '').trim() : ''; - - // Status icons in the label text - const equipped = name.includes('🗡'); - const locked = name.includes('🔒'); - const pinned = name.includes('📌'); - const stored = name.includes('📦'); - const protected_ = name.includes('🛡'); - - // Strip status icons for clean name - const cleanName = name.replace(/[🗡🔒📌📦🛡️]/g, '').trim(); - - items.push({ - id, name: cleanName, slot: currentCategory, - equipped, locked, pinned, stored, protected: protected_, - checkbox: cb, row, - grade: gradeEquipment(cleanName), - }); - }); - - if (items.length === 0) return; - - // ── Detect which armory tab we're on ── - let screen = 'organize'; - const url = window.location.href || ''; - const sm = url.match(/screen=(\w+)/); - if (sm) screen = sm[1]; - - // ── Counts ── - const sellCount = items.filter(i => i.grade.action === 'sell' && !i.equipped && !i.locked).length; - const salvCount = items.filter(i => i.grade.action === 'salvage' && !i.equipped && !i.locked).length; - const keepCount = items.filter(i => i.grade.action === 'keep').length; - - // ── Build the toolbar ── - const toolbar = document.createElement('div'); - toolbar.id = 'hv-armory-bar'; - toolbar.style.cssText = css({ - display: 'flex', - gap: '4px', - flexWrap: 'wrap', - padding: '4px 6px', - marginBottom: '2px', - background: '#1a1a2e', - borderRadius: '4px', - fontSize: '10px', - fontFamily: 'monospace', - alignItems: 'center', - }); - - // Summary - const summary = document.createElement('span'); - summary.style.cssText = 'color:#888;margin-right:6px'; - summary.textContent = `✅${keepCount} ♻${salvCount} 💰${sellCount}`; - toolbar.appendChild(summary); - - // ── Smart auto-select: check boxes based on grade ── - function autoSelect(action) { - items.forEach(item => { - if (item.grade.action !== action) return; - if (item.equipped || item.locked) return; - item.checkbox.checked = true; - }); - // Trigger the game's update function if available - if (typeof update_selected_count === 'function') update_selected_count(); - } - - // ── Select by quality ── - function selectByQuality(below) { - const idx = KB.qualities.indexOf(below); - items.forEach(item => { - const qi = KB.qualities.indexOf(KB.qualities.find(q => item.name.includes(q))); - if (qi < 0) return; - if (qi <= idx && !item.equipped && !item.locked) { - item.checkbox.checked = true; - } - }); - if (typeof update_selected_count === 'function') update_selected_count(); - } - - function clearAll() { - items.forEach(item => item.checkbox.checked = false); - if (typeof update_selected_count === 'function') update_selected_count(); - } - - // Buttons — only show relevant ones for current screen - if (screen === 'sell') { - const b = document.createElement('input'); - b.type = 'button'; - b.value = `💰 Select Crude (${sellCount})`; - b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:10px'; - b.onclick = () => autoSelect('sell'); - toolbar.appendChild(b); - - const b2 = document.createElement('input'); - b2.type = 'button'; - b2.value = '💰 Select ≤Fair'; - b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#a00;color:#fff;border:none;border-radius:3px;font-size:10px'; - b2.onclick = () => selectByQuality('Fair'); - toolbar.appendChild(b2); - } - - if (screen === 'salvage') { - const b = document.createElement('input'); - b.type = 'button'; - b.value = `♻ Select Salvage (${salvCount})`; - b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px'; - b.onclick = () => autoSelect('salvage'); - toolbar.appendChild(b); - - const b2 = document.createElement('input'); - b2.type = 'button'; - b2.value = '♻ Select ≤Average'; - b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px'; - b2.onclick = () => selectByQuality('Average'); - toolbar.appendChild(b2); - - // "Sell Salvaged Equipment" toggle is already native - } - - if (screen === 'organize') { - const b = document.createElement('input'); - b.type = 'button'; - b.value = '📌 Pin Equipped'; - b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#2a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px'; - b.onclick = () => { - // Select all equipped items so they can be pinned - items.forEach(item => { if (item.equipped) item.checkbox.checked = true; }); - if (typeof update_selected_count === 'function') update_selected_count(); - }; - toolbar.appendChild(b); - } - - // ── Keep Top 5 per category (score-based), select the rest ── - // Categories: 1H, 2H, Staff, Shield + 5 armor slots × (Cloth, Light, Heavy) - function qualityIndex(name) { - const q = KB.qualities.find(qq => name.includes(qq)); - return q ? KB.qualities.indexOf(q) : 0; - } - - function keepTop5AndSelect(action) { - const catMap = new Map(); - - items.forEach(item => { - if (item.equipped || item.locked || item.pinned) return; - const data = getHVEquipData(item.id); - const stats = data && data.d ? parseEquipHTML(data.d) : null; - if (!stats) return; // skip unscraped items — leave untouched - - const type = (stats.type || ''); - let cat = '?'; - if (type.includes('Two-handed')) cat = '2H'; - else if (type.includes('One-handed')) cat = '1H'; - else if (type.includes('Staff')) cat = 'Staff'; - else if (type.includes('Shield')) cat = 'Shield'; - else if (type.includes('Cloth')) cat = 'Cloth'; - else if (type.includes('Light')) cat = 'Light'; - else if (type.includes('Heavy')) cat = 'Heavy'; - - // For armor, append the slot (Head/Body/Hands/Legs/Feet) - if (['Cloth', 'Light', 'Heavy'].includes(cat)) { - const slot = gearDetectSlot({ name: item.name, category: cat }); - if (!slot) return; - cat += ' ' + slot; - } - - const isWeaponCat = cat.startsWith('1H') || cat.startsWith('2H') || cat === 'Staff' || cat === 'Shield'; - const quality = qualityIndex(item.name); - const score = isWeaponCat - ? gearScoreWeapon({ name: item.name, stats, quality }) - : gearScoreArmor({ name: item.name, stats, quality }); - - if (!catMap.has(cat)) catMap.set(cat, []); - catMap.get(cat).push({ item, score }); - }); - - // Keep top 10% per category (min 5), select the rest - let selected = 0; - catMap.forEach((list, cat) => { - list.sort((a, b) => b.score - a.score); - const keepCount = Math.max(5, Math.ceil(list.length * 0.10)); - const keep = list.slice(0, keepCount); - const dump = list.slice(keepCount); - dump.forEach(({ item }) => { - item.checkbox.checked = true; - selected++; - }); - console.log(`%c[HV] 🗂 ${cat}: ${list.length} items → keep ${keep.length} (top ${keep.map(k => k.score.toFixed(0)).join('/')}), select ${dump.length}`, 'color:#0f0'); - }); - - if (typeof update_selected_count === 'function') update_selected_count(); - return selected; - } - - // ── Keep Top 10% button — available on sell and salvage screens ── - if (screen === 'sell' || screen === 'salvage') { - const b3 = document.createElement('input'); - b3.type = 'button'; - b3.value = `⭐ Keep Top 10% / Select ${screen === 'sell' ? 'Sell' : 'Salvage'} Rest`; - b3.style.cssText = 'padding:2px 6px;cursor:pointer;background:#7a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px'; - b3.title = 'Keeps the top 10% (min 5) highest-scoring items in each category (1H, 2H, Staff, Shield, and each armor slot per Cloth/Light/Heavy). Selects everything else.'; - b3.onclick = () => { - const n = keepTop5AndSelect(screen); - console.log(`%c[HV] ⭐ Keep Top 10%: ${n} items selected for ${screen}`, 'color:#0f0'); - }; - toolbar.appendChild(b3); - } - - // Clear button always available - const clear = document.createElement('input'); - clear.type = 'button'; - clear.value = '✗ Clear'; - clear.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px'; - clear.onclick = clearAll; - toolbar.appendChild(clear); - - // Select All / Invert Selection (useful on any screen) - const selAll = document.createElement('input'); - selAll.type = 'button'; - selAll.value = '☐ All'; - selAll.style.cssText = 'padding:2px 6px;cursor:pointer;background:#3a4a6a;color:#fff;border:none;border-radius:3px;font-size:10px'; - selAll.onclick = () => { - items.forEach(item => { item.checkbox.checked = true; }); - if (typeof update_selected_count === 'function') update_selected_count(); - }; - toolbar.appendChild(selAll); - - const invert = document.createElement('input'); - invert.type = 'button'; - invert.value = '⊞ Invert'; - invert.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px'; - invert.onclick = () => { - items.forEach(item => { item.checkbox.checked = !item.checkbox.checked; }); - if (typeof update_selected_count === 'function') update_selected_count(); - }; - toolbar.appendChild(invert); - - // ── Insert into page ── - const eqSelect = document.getElementById('equipselect_outer') || - document.querySelector('#equipselect_left, #armory_right > div'); - if (equipList && equipList.parentNode) { - equipList.parentNode.insertBefore(toolbar, equipList); - } + // Physical bulk select/sell/salvage/shrine buttons in Armory are owned by HVUT. + // HV Unified provides advisory scoring via HV.analyzeGear() and gear-analysis panel. } + // ═══════════════════════════════════════════════════════════════════════ // GEAR SCRAPER — capture full equipment details for analysis // ═══════════════════════════════════════════════════════════════════════ @@ -4250,172 +3486,22 @@ function enhanceGearAnalysis() { } // ═══════════════════════════════════════════════════════════════════════ -// BATTLE LOGGER — saves raw battle log lines to localStorage in real time +// BATTLE LOGGER — REMOVED (Handled by Monsterbation combat log) // ═══════════════════════════════════════════════════════════════════════ -let _lastLogCount = 0; -const LOG_KEY = SP + 'battleLog'; +function initBattleLog() {} +function logRound() {} +function getBattleLog() { return null; } +function getBattleSummary() { return null; } -function initBattleLog() { - _lastLogCount = 0; - // Clear previous log - try { localStorage.removeItem(LOG_KEY); } catch (e) {} -} - -function logRound() { - const log = document.getElementById('textlog'); - if (!log) return; - - const allRows = log.querySelectorAll('tr'); - if (allRows.length <= _lastLogCount) return; - - // Get only the NEW rows since last capture - const newLines = []; - for (let i = _lastLogCount; i < allRows.length; i++) { - const text = (allRows[i].textContent || '').trim(); - if (text) newLines.push(text); - } - _lastLogCount = allRows.length; - - if (newLines.length === 0) return; - - // Read existing log from localStorage, append new lines, save back - let existing = []; - try { - const saved = localStorage.getItem(LOG_KEY); - if (saved) existing = JSON.parse(saved); - } catch (e) {} - - existing.push(...newLines); - - try { - localStorage.setItem(LOG_KEY, JSON.stringify(existing)); - } catch (e) { - // localStorage full — keep only last 500 lines - try { - const trimmed = existing.slice(-500); - localStorage.setItem(LOG_KEY, JSON.stringify(trimmed)); - } catch (e2) {} - } -} - -function getBattleLog() { - try { - const saved = localStorage.getItem(LOG_KEY); - return saved ? JSON.parse(saved) : null; - } catch (e) { - return null; - } -} - -function getBattleSummary() { - const lines = getBattleLog(); - if (!lines || lines.length === 0) return null; - - let kills = 0, dmgDealt = 0, dmgTaken = 0, healed = 0; - const spells = {}, skills = {}, items = {}; - - for (const line of lines) { - const mKill = line.match(/(.+) has been defeated\./); - if (mKill && !line.includes('gains the effect')) kills++; - - const mDmg = line.match(/causing (\d+) points/); - if (mDmg) dmgDealt += parseInt(mDmg[1]); - - const mTaken = line.match(/take (\d+) (\w+)/); - if (mTaken) dmgTaken += parseInt(mTaken[1]); - - const mRegen = line.match(/Regen restores (\d+)/); - if (mRegen) healed += parseInt(mRegen[1]); - const mPot = line.match(/Regeneration restores (\d+)/); - if (mPot) healed += parseInt(mPot[1]); - const mCure = line.match(/healed for (\d+)/); - if (mCure) healed += parseInt(mCure[1]); - - const mSpell = line.match(/You cast (\w[\w\s-]+)\./); - if (mSpell) spells[mSpell[1]] = (spells[mSpell[1]] || 0) + 1; - - const mSkill = line.match(/You use (\w[\w\s-]+)\./); - if (mSkill && !mSkill[1].includes('Draught') && !mSkill[1].includes('Potion') && !mSkill[1].includes('Elixir') && !mSkill[1].includes('Gem')) { - skills[mSkill[1]] = (skills[mSkill[1]] || 0) + 1; - } - - const mItem = line.match(/You use (.+?)\.$/); - if (mItem && (mItem[1].includes('Draught') || mItem[1].includes('Potion') || mItem[1].includes('Elixir') || mItem[1].includes('Gem'))) { - items[mItem[1]] = (items[mItem[1]] || 0) + 1; - } - } - - return { - totalLines: lines.length, - kills, - dmgDealt, - dmgTaken, - healed, - spells, - skills, - items, - }; -} // ═══════════════════════════════════════════════════════════════════════ -// RE TIMER — Random Encounter countdown +// RE TIMER — REMOVED (Handled by HVUtils / HVUT) // ═══════════════════════════════════════════════════════════════════════ -let reTimerEl = null; +function setupRETimer() {} +function updateRETimer() {} -function setupRETimer() { - if (!CFG.reTimer) return; - if (document.getElementById('hv-re-timer')) return; - - reTimerEl = document.createElement('div'); - reTimerEl.id = 'hv-re-timer'; - reTimerEl.style.cssText = css({ - position: 'fixed', - top: '2px', - left: '50%', - transform: 'translateX(-50%)', - zIndex: '99999', - background: '#1a1a2e', - color: '#0f0', - padding: '2px 10px', - borderRadius: '0 0 6px 6px', - fontSize: '10px', - fontFamily: 'monospace', - opacity: '0.9', - }); - reTimerEl.textContent = 'RE: --:--'; - document.body.appendChild(reTimerEl); - updateRETimer(); - setInterval(updateRETimer, 30000); -} - -function updateRETimer() { - if (!reTimerEl) return; - const now = Date.now(); - const dt = document.body.textContent || ''; - - // Detect dawn - if (dt.includes('Dawn of a New Day') || dt.includes('A new day dawns')) { - try { localStorage[SP + 'lastDawn'] = JSON.stringify(now); } catch (e) {} - } - - const ld = JSON.parse(localStorage[SP + 'lastDawn'] || now); - const ms = (now - ld) / 60000; - const nr = 30 - (ms % 30); - - if (nr < 1) { - reTimerEl.textContent = '⚡ RE READY!'; - reTimerEl.style.color = '#f00'; - reTimerEl.style.fontWeight = 'bold'; - } else { - const mn = Math.floor(nr); - const sc = Math.floor((nr - mn) * 60); - reTimerEl.textContent = `RE: ${String(mn).padStart(2, '0')}:${String(sc).padStart(2, '0')}`; - reTimerEl.style.color = '#0f0'; - reTimerEl.style.fontWeight = 'normal'; - } -} // ── Settings page: auto-sell/salvage defaults ── @@ -4553,44 +3639,34 @@ window.HV = { clearBuy: () => { const db = getGearDB(); saveGearDB(db.filter(e => e.source !== 'buy')); return 'Buy data cleared'; }, }; -// ═══════════════════════════════════════════════════════════════════════ -// INIT — bootstrap everything -// ═══════════════════════════════════════════════════════════════════════ - function init() { loadConfig(); detectLevel(); STATE.page = detectPage(); + // Community script presence detection + STATE.hasMB = !!(window.Monsterbation || document.getElementById('mb_vitals') || document.querySelector('.mb_vital')); + STATE.hasHVUT = !!(window.HVUT || document.querySelector('.hvut-main') || document.getElementById('hvut_menu')); + // Always-on features - setupRETimer(); addConfigButton(); if (STATE.page === 'battle') { initializeBattle(); autoCheckTask('battle'); } else { - // Non-battle page enhancements + // Non-battle page enhancements & advisory tools if (STATE.page === 'itemshop') { enhanceItemShop(); autoCheckTask('buy-health'); } - if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); } + if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); enhanceGearAnalysis(); } if (STATE.page === 'shrine') enhanceShrine(); if (STATE.page === 'training') { enhanceTraining(); autoCheckTask('training'); } - if (STATE.page === 'monsterlab') { enhanceMonsterLab(); autoCheckTask('feed'); } if (STATE.page === 'arena') { autoCheckTask('arenas'); autoCheckTask('first-blood'); } if (STATE.page === 'character') showGuidancePanel(); if (STATE.page === 'settings') enhanceSettings(); if (STATE.page === 'armory') { enhanceArmory(); enhanceGearAnalysis(); } - if (STATE.page === 'equipshop') enhanceGearAnalysis(); if (STATE.page === 'abilities') enhanceAbilities(); - if (STATE.page === 'monster') enhanceMonsterLab(); } - // Dawn initialization - if (!localStorage[SP + 'lastDawn']) { - try { localStorage[SP + 'lastDawn'] = JSON.stringify(Date.now()); } catch (e) {} - } - updateRETimer(); - // Watch for dynamic page transitions (arena/grindfest → battle) const bodyObserver = new MutationObserver(() => { if (!STATE.inBattle && isBattlePage()) { @@ -4604,62 +3680,35 @@ function initializeBattle() { if (STATE.battleInitialized && STATE.page === 'battle') return; STATE.page = 'battle'; STATE.inBattle = true; - STATE._mysticUsed = false; // Reset Mystic Gem tracking on new battle - STATE._spiritForSkill = 0; // Reset spirit burst toggle - STATE._baseMpCosts = {}; // Reset base cost cache parseBattleState(); - // Force tier update from cached level (battle page may not have level_readout) if (STATE.level > 1) updateTier(); - setupHover(); setupKeybindings(); - addMonsterNumbers(); - refreshUI(); addConfigButton(); showDifficultyBanner(); - // Watch battle log + // Passive battle state parser (updates STATE without interfering with MB UI/clicks) const log = document.getElementById('textlog'); if (log && !log.dataset.hvObserved) { log.dataset.hvObserved = '1'; const obs = new MutationObserver(() => { parseBattleState(); - saveMonsterData(); - logRound(); // Log this round's data for analysis - if (CFG.hoverEnabled && !STATE.interruptHover && !STATE.interruptAlert && STATE.hoverTarget >= 0) { - const a = getSmartAction(); - if (a) executeAction(a); - } - refreshUI(); updateConfigButton(); }); obs.observe(log, { childList: true, subtree: true, characterData: true }); } - // Watch vitals pane const vitals = document.getElementById('pane_vitals'); if (vitals && !vitals.dataset.hvObserved) { vitals.dataset.hvObserved = '1'; const vobs = new MutationObserver(() => { parseBattleState(); - logRound(); // Also log on vitals changes (catches end-of-round) - refreshUI(); updateConfigButton(); - // Check if battle ended (completion pane appeared) - if (document.getElementById('pane_completion')) { - // Battle ended — handled by log observer - } }); vobs.observe(vitals, { childList: true, subtree: true, attributes: true }); } - // RiddleMaster detection - if (document.getElementById('riddlemaster')) { - STATE.interruptHover = true; - } - - // Auto-scrape gear data on character/armory pages (reads from HV's in-memory store) - // Wait for dynjs_equip to load (external script, may load after document-end) + // Auto-scrape gear data on character/armory pages const url = window.location.href || ''; if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) { let attempts = 0; @@ -4670,9 +3719,8 @@ function initializeBattle() { return; } if (++attempts < 10) { - setTimeout(tryScrape, 500); // retry every 500ms up to 5 seconds + setTimeout(tryScrape, 500); } else { - // Fallback: scrape names/IDs without stats autoScrapeGear(); } }; @@ -4690,10 +3738,11 @@ if (document.readyState === 'loading') { init(); } -console.log('%c🛡️ HV Unified v' + VERSION + '%c | Q=action H=hover C=cure ,=settings', +console.log('%c🛡️ HV Unified v' + VERSION + ' Bridge & Advisory Mode%c | ,=settings', 'color:#fdcb00;font-weight:bold', ''); -console.log('%c HV.advice() for guidance | HV.difficulty() for difficulty check', +console.log('%c HV.analyzeGear() for gear scoring | HV.advice() for build guidance | HV.difficulty() for difficulty check', 'color:#888;font-size:10px'); })(); + diff --git a/scripts/latest.user.js b/scripts/latest.user.js index b3db691..39fc5f2 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -1,8 +1,8 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.15.4 -// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse +// @version 0.16.0 +// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils) // @author GaboGG + Hermes // @match *://*.hentaiverse.org/* // @match *://alt.hentaiverse.org/* @@ -13,21 +13,18 @@ (function() { 'use strict'; + // ═══════════════════════════════════════════════════════════════════════ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.15.4'; +const VERSION = '0.16.0'; const CFG = { - // — Battle automation - hotkey: 'KeyQ', - hotkeyMod: '', - hoverEnabled: true, + // — Battle strategy advisory autoBuff: true, autoDebuff: true, autoCure: true, - autoSpirit: false, // Manual spirit stance only (auto-disable at 65% SP for Spark) // — Thresholds (simulation-backed) cureHP: 0.45, // Cast Cure below this HP @@ -35,33 +32,22 @@ const CFG = { cureRegenHP: 0.65, // Cast Regen below this HP manaGemMP: 0.45, // Use mana gems below this MP manaPotionMP: 0.25, // Use mana potions at this MP - spiritPotionSP: 0.60, // Use spirit gems/potions below this SP (Spark costs 50%) - spiritStanceOC: 80, // Manual spirit stance only (auto-disable at 65% SP for Spark) - skillOC: 60, // Use weapon skills at this OC (lower = faster, key on IWBTH) + skillOC: 60, // Use weapon skills at this OC useAttackSpells: false, // Don't use attack spells (let channel proc naturally) // — Out of battle autoSell: true, sellQuality: 'Average', - bulkShrine: true, - reTimer: true, trainingQueue: true, - // — UI - showCooldowns: true, - showMonsterHP: true, - showDurations: true, - alertColours: true, - showMonsterNumbers: true, + // — UI & Guidance cfgButton: true, showGuidance: true, showEquipAdvice: true, autoDifficulty: true, - - // — Combat style - useAttackSpells: true, }; + // ═══════════════════════════════════════════════════════════════════════ // STATE — global runtime state // ═══════════════════════════════════════════════════════════════════════ @@ -1170,28 +1156,12 @@ function strategyNovice() { return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' }; } - // 4. Mana/spirit items — AFTER buffs (Novice) + // 4. Mana items — AFTER buffs (Novice) if (STATE.mp < CFG.manaGemMP) { const gem = hasUsableGem('mana'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } - // Spirit gem (P-slot) — auto-consume when available - const pGem = document.getElementById('ikey_p'); - const pGemId = pGem ? (pGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; - if (pGemId && pGemId[1] === '10007') { - if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true }; - } - // Spirit draughts/potions — only when SP drops below threshold - if (STATE.sp < CFG.spiritPotionSP) { - // Check non-P-slot spirit items only - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') { - return { type: 'item', id: slot, selfTarget: true }; - } - } - } + // 5. Weapon skills — these are better than basic attacks const skillOC = CFG.skillOC || 80; @@ -1255,20 +1225,6 @@ function strategyAdept() { if (gem && !STATE.channeling && !STATE._mysticUsed) return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' }; - // 0b. Spark of Life SP safety - if (STATE.sp < 0.55) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; - } - } - - // 0c. Spirit Stance safety auto-disable - if (STATE.spiritStance && STATE.sp < 0.60) { - return { type: 'toggle_spirit', _reason: 'sp_safety' }; - } - // 1. Health items — safe to use early if (STATE.hp < CFG.cureItemHP) { const gem = hasUsableGem('heal'); @@ -1281,15 +1237,6 @@ function strategyAdept() { if (c) return { type: 'spell', name: c, selfTarget: true }; } - // 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so - // Spark can actually save us. - if (STATE.sp < 0.55) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true }; - } - } // 3. Buffs // On first round, cast cheapest-first to proc channeling @@ -1318,25 +1265,12 @@ function strategyAdept() { return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' }; } - // 4. Mana/spirit items — AFTER buffs (Adept) + // 4. Mana items — AFTER buffs (Adept) if (STATE.mp < CFG.manaGemMP) { const gem = hasUsableGem('mana'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } - // Spirit gem (P-slot) — auto-consume when available - const apGem = document.getElementById('ikey_p'); - const apGemId = apGem ? (apGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; - if (apGemId && apGemId[1] === '10007') { - if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true }; - } - // Spirit draughts/potions — only at SP < threshold - if (STATE.sp < CFG.spiritPotionSP) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true }; - } - } + // 4. Debuffs — conservative: Imperil on bosses/rares or 5+ mobs // Weaken only on high-threat or bosses. Estoc handles <5. @@ -1433,45 +1367,6 @@ function strategyVeteran() { if (gem && !STATE.channeling && !STATE._mysticUsed) return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' }; - // 0b. Spark of Life SP safety — Spark costs 50% SP when triggered. - // If SP < 60%, consume a spirit item before anything else. - if (STATE.sp < 0.60) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; - } - } - - // 0c. Spirit Stance safety auto-disable — if left on, drains 10% SP/turn - if (STATE.spiritStance && STATE.sp < 0.60) { - return { type: 'toggle_spirit', _reason: 'sp_safety' }; - } - - // 0d. Spirit Stance for burst clear — toggle on for 7+ mob, off after one skill - // Gated by CFG.autoSpirit (default OFF — manual toggle only) - const is2H = STATE.skillsKnown.includes('Great Cleave') || STATE.skillsKnown.includes('Rending Blow'); - if (CFG.autoSpirit && is2H && STATE.monsters && STATE.monsters.length >= 7 && STATE.oc >= (CFG.skillOC || 60)) { - if (STATE._spiritForSkill >= 1 && STATE.spiritStance) { - if (STATE._spiritForSkill === 1) { - STATE._spiritForSkill = 2; - } else if (STATE._spiritForSkill === 2) { - STATE._spiritForSkill = 0; - return { type: 'toggle_spirit', _reason: 'spirit_off_7' }; - } - } else if (!STATE.spiritStance && STATE._spiritForSkill === 0) { - STATE._spiritForSkill = 1; - return { type: 'toggle_spirit', _reason: 'spirit_on_7' }; - } - } - // Toggle-off guard: if flag says skill was cast, toggle off even if mob count dropped - if (STATE._spiritForSkill === 2 && STATE.spiritStance) { - STATE._spiritForSkill = 0; - return { type: 'toggle_spirit', _reason: 'spirit_off_7' }; - } - // Reset flag if spirit got turned off externally - if (!STATE.spiritStance) STATE._spiritForSkill = 0; - // 0e. HIGH-THREAT WEAPON SKILLS — only when 3+ enemies // Skip if ≤2 enemies — basic attacks build OC for next wave instead. const SINGLE_THREAT = Math.round(120 * 90 / 100); @@ -1496,16 +1391,6 @@ function strategyVeteran() { } } - // 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so - // Spark can actually save us. Use spirit draughts/potions proactively. - if (STATE.sp < 0.55) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; - } - } - // 2c. OC-building priority: when 5+ monsters, OC < 60, and AoE skills ready, // skip debuffs/non-essential buffs and basic attack to build OC fast. const needsOC = STATE.monsters && STATE.monsters.length >= 4 @@ -1514,26 +1399,21 @@ function strategyVeteran() { && document.querySelector(`.btqs[onmouseover*="set_infopane_spell('Rending Blow'"], .btsd[onmouseover*="set_infopane_spell('Rending Blow'"]`) ?.hasAttribute('onclick'); if (needsOC) { - // No hp/mp guard — if Spark procced, hp=0 but we still need OC const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : findOptimalDominoTarget(); if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) return { type: 'attack', target: t, _reason: 'oc_build' }; } // 3. Buffs - // On first round, cast cheapest-first to proc channeling if (CFG.autoBuff) { { const ib = castInitialBuffs(); if (ib) return ib; } - // If channeling is active, prioritize most expensive buff for best value if (STATE.channeling && STATE.mp > 0.10) { const ch = findBestChannelingTarget(); if (ch) return ch; } - // Collect available buff candidates and sort by cost - // No channeling → cheapest first (proc chance), channeling → most expensive (MP value) const buffCandidates = []; for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) { @@ -1548,35 +1428,19 @@ function strategyVeteran() { } } - // 4. Mana/spirit items — AFTER buffs (Novice) + // 4. Mana items if (STATE.mp < CFG.manaGemMP) { const gem = hasUsableGem('mana'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } - // Spirit gem (P-slot) — use immediately - const vpGem = document.getElementById('ikey_p'); - const vpGemId = vpGem ? (vpGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; - if (vpGemId && vpGemId[1] === '10007') { - if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true }; - } - // Spirit draughts/potions — only at SP < threshold - if (STATE.sp < CFG.spiritPotionSP) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true }; - } - } - // 4. Debuffs — only when NOT in spirit burst mode (flag >= 1 means skill pending) - if (STATE._spiritForSkill === 0 && CFG.autoDebuff && STATE.mp > 0.35) { - // Imperil: bosses only, or 5+ mobs in Grindfest (Estoc handles <5) + // 4. Debuffs + if (CFG.autoDebuff && STATE.mp > 0.35) { if (hasReadySpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 5)) { const b = findStrongestMonster(); if (b >= 0 && !checkMonsterDebuff(b, 'imperil')) return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' }; } - // Weaken: only on monsters about to use special attacks (high SP) or bosses if (hasReadySpell('Weaken') && (anyRareMonster() || hasHighThreat(THREAT_PX))) { const b = findDangerousMonster(); if (b >= 0 && !checkMonsterDebuff(b, 'weaken')) @@ -1585,20 +1449,16 @@ function strategyVeteran() { } // 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar) - // If 2+ enemies have >85% SP bar, they're about to special — emergency const isHighThreat = hasHighThreat(THREAT_PX); if (isHighThreat) console.log('%c[HV] ⚠ HIGH THREAT: 2+ monsters with SP >85%', 'color:#f44'); const askillOC3 = CFG.skillOC || 80; if (STATE.oc >= askillOC3 || (isHighThreat && STATE.monsters.length >= 3)) { if (isHighThreat && STATE.oc < askillOC3) console.log('%c[HV] ⚠ emergency: using skill at OC=' + STATE.oc.toFixed(0), 'color:#f80'); - // Emergency: 2+ monsters about to special — spend OC to kill them fast - // Rending Blow: AoE armor pen vs 3+ enemies. Target center monster for max 5-target splash. if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) { const t = isHighThreat ? findDangerousMonster() : findOptimalDominoTarget(); if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' }; } - // Shatter Strike: AoE stun vs 3+ enemies with armor break if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) { const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor')); if (hasArmorBreak) { @@ -1606,12 +1466,10 @@ function strategyVeteran() { if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t, _reason: 'stun' }; } } - // Great Cleave: boss/rare fights or single-target threat if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = isHighThreat ? findDangerousMonster() : findStrongestMonster(); if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t, _reason: isHighThreat ? 'threat' : 'boss' }; } - // If high threat and 3+ enemies, use single-target skills on the most dangerous if (isHighThreat && STATE.oc >= (CFG.skillOC || 60) && STATE.monsters.length >= 3) { const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow', 'Vital Strike', 'Shield Bash', 'Concussive Strike']; @@ -1624,6 +1482,7 @@ function strategyVeteran() { } } + // 5. Spirit gem auto-consume const spGem = document.getElementById('ikey_p'); const spGemId = spGem ? (spGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; @@ -1693,16 +1552,14 @@ function executeAction(a) { case 'spell': result = a.selfTarget ? castSelfSpell(a.name) : castTargetSpell(a.name, a.target); break; case 'skill': result = useSkill(a.name, a.target); break; case 'item': result = useItem(a.id); break; - case 'toggle_spirit': result = toggleSpiritStance(); break; } if (result) { - // After dispatching an action, stagger the debounce so we don't queue - // another action while the game processes this one (typically ~500-1000ms) STATE._lastActionTime = Date.now(); } return result; } + function attackMonster(i) { if (i < 0 || i >= STATE.monsters.length) return false; const m = STATE.monsters[i]; @@ -1794,54 +1651,26 @@ function useSkill(n, i) { return true; } -function toggleSpiritStance() { - const t = document.getElementById('ckey_spirit'); - if (!t) return false; - t.click(); - return true; -} // ═══════════════════════════════════════════════════════════════════════ -// HOVER SYSTEM — mouse-over monster actions +// HOVER SYSTEM — REMOVED (Handled by Monsterbation / MB) // ═══════════════════════════════════════════════════════════════════════ -function setupHover() { - if (!CFG.hoverEnabled) return; +function setupHover() {} - STATE.monsters.forEach((m, i) => { - if (!m || m.dataset.hvHover) return; - m.dataset.hvHover = '1'; - - const area = m.querySelector('.btm6') || m; - area.addEventListener('mouseenter', () => { - STATE.hoverTarget = i; - if (!STATE.interruptHover && !STATE.interruptAlert) { - const ac = getSmartAction(); - if (ac) executeAction(ac); - } - }); - area.addEventListener('mouseleave', () => { STATE.hoverTarget = -1; }); - }); - - // RiddleMaster blocks hover - if (document.getElementById('riddlemaster')) { - STATE.interruptHover = true; - } -} // ═══════════════════════════════════════════════════════════════════════ // KEYBINDINGS — keyboard shortcuts for battle // ═══════════════════════════════════════════════════════════════════════ let keybindingsSetup = false; -const Q_DEBOUNCE_MS = 300; // Ignore Q repeats faster than this function setupKeybindings() { if (keybindingsSetup) return; keybindingsSetup = true; document.addEventListener('keydown', function(e) { - // Settings — comma key + // Settings — comma key (non-conflicting with Monsterbation) if (e.keyCode === KEYS.COMMA) { if (isBattlePage()) { e.preventDefault(); @@ -1850,226 +1679,24 @@ function setupKeybindings() { return; } } - - // Main action hotkey (default: Q) - if (e.code === CFG.hotkey && !e.ctrlKey && !e.altKey && !e.metaKey) { - if (!isInputFocused() && isBattlePage()) { - // Debounce: ignore key-repeats until game processes the action - const now = Date.now(); - if (now - (STATE._lastActionTime || 0) < Q_DEBOUNCE_MS) return; - STATE._lastActionTime = now; - - console.log('%c[HV] Q pressed — checking action...', 'color:#888'); - parseBattleState(); - // Debug: show full battle state - const activeBuffs = Object.entries(STATE.buffs) - .filter(([k, v]) => v > 0 && k !== 'regen' || v > 0) - .map(([k, v]) => `${k}=${v}`).join(' '); - // Monster SP bars (threat levels) with alive/dead markers - const threatLevels = STATE.monsters.map((m, i) => { - const sp = STATE.monsterSp[i] || 0; - const alive = m && m.hasAttribute && m.hasAttribute('onclick') ? 'A' : 'D'; - return `${i}:${sp}${alive}`; - }).join(' '); - // Skill cooldowns — check DOM for opacity/onclick on skill elements - const skillCDs = ['Rending Blow', 'Shatter Strike', 'Great Cleave'] - .filter(s => STATE.skillsKnown.includes(s)) - .map(s => { - const el = document.querySelector(`.btqs[onmouseover*="set_infopane_spell('${s}'"], .btsd[onmouseover*="set_infopane_spell('${s}'"]`); - const ready = el && el.hasAttribute('onclick'); - return `${s.charAt(0)}${ready ? 'R' : 'C'}`; - }).join(''); - const curMp = parseInt((document.getElementById('vrm') || {}).textContent) || 0; - const curHp = parseInt((document.getElementById('vrhd') || {}).textContent) || 0; - // Item counts from itemsKnown - const itemTypes = {}; - Object.values(STATE.itemsKnown).forEach(info => { - const def = ITEMS[parseInt(info)]; - if (def) itemTypes[def.t] = (itemTypes[def.t] || 0) + 1; - }); - const itemStr = ['heal','spirit','mana'].filter(t => itemTypes[t]).map(t => `${t.charAt(0)}${itemTypes[t]}`).join(' '); - console.log( - `%c[HV] ch=${STATE.channeling} hp=${curHp} mp=${curMp}(${(STATE.mp*100).toFixed(0)}%) ` + - `sp=${(STATE.sp*100).toFixed(0)}% oc=${STATE.oc.toFixed(0)} ` + - `mon:${STATE.monsters.length} sk:${skillCDs || '--'} ` + - `${itemStr} ` + - `buffs: ${activeBuffs} | spBars: ${threatLevels}`, - 'color:#888' - ); - const a = getRecommendedAction(); - if (a) { - e.preventDefault(); - // Show explicit action details - let label = ''; - if (a.type === 'spell') { - label = a.name + (a.target >= 0 ? ' → monster ' + a.target : ' (self)'); - } else if (a.type === 'skill') { - label = a.name + ' → monster ' + a.target; - } else if (a.type === 'item') { - const itemDef = ITEMS[parseInt(STATE.itemsKnown[a.id] || '0')]; - label = (itemDef ? itemDef.n : a.id) + (a.selfTarget ? ' (self)' : ''); - } else if (a.type === 'attack') { - label = 'monster ' + a.target; - } else { - label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id; - } - console.log(`%c[HV] ▶ ${a.type} → ${label}${a._reason ? ' (' + a._reason + ')' : ''}`, 'color:#0f0'); - executeAction(a); - } - } - } - - // Hover toggle (H) - if (e.code === 'KeyH' && !e.ctrlKey && !e.altKey && !e.metaKey && !isInputFocused()) { - STATE.interruptHover = !STATE.interruptHover; - console.log(`%c[HV] Hover ${STATE.interruptHover ? 'OFF' : 'ON'}`, 'color:#f80'); - } - - // Emergency heal (C) — always cast Cure/Full-Cure regardless of strategy - if (e.code === 'KeyC' && !e.ctrlKey && !e.altKey && !e.metaKey && !isInputFocused()) { - const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null; - if (c) { - console.log(`%c[HV] 🚑 Emergency: casting ${c}`, 'color:#f44'); - castSelfSpell(c); - } - } }); - console.log('%c⌨ [HV] Keys: Q=action H=hover C=cure ,=settings', 'color:#0f0;font-size:11px'); + console.log('%c⌨ [HV] Key: ,=settings (Battle input managed by Monsterbation)', 'color:#0f0;font-size:11px'); } + // ═══════════════════════════════════════════════════════════════════════ -// UI OVERLAYS — cooldowns, durations, alerts, monster numbers, monster HP +// UI OVERLAYS — REMOVED (Handled by Monsterbation / MB) // ═══════════════════════════════════════════════════════════════════════ -function updateCooldownDisplay() { - if (!CFG.showCooldowns) return; - $$('.hv-cd-overlay').forEach(e => e.remove()); - $$('.btqb').forEach(el => { - const img = el.querySelector('img'); - if (!img) return; - const src = (img.src || '').toLowerCase(); - for (const [k, v] of Object.entries(STATE.cooldowns)) { - if (v <= 0) continue; - if (src.includes(k.toLowerCase()) || src.includes(k.replace(/_/g, ''))) { - const o = document.createElement('div'); - o.className = 'hv-cd-overlay'; - o.style.cssText = 'position:absolute;top:0;right:0;background:rgba(0,0,0,0.7);color:#f00;font-size:9px;font-weight:bold;padding:0 2px;border-radius:2px;pointer-events:none'; - o.textContent = v; - el.style.position = 'relative'; - el.appendChild(o); - break; - } - } - }); -} +function updateCooldownDisplay() {} +function updateDurationDisplay() {} +function updateAlertColours() {} +function addMonsterNumbers() {} +function updateMonsterHPDisplay() {} +function saveMonsterData() {} +function refreshUI() {} -function updateDurationDisplay() { - if (!CFG.showDurations) return; - $$('.hv-dur-overlay').forEach(e => e.remove()); - const pane = document.getElementById('pane_effects'); - if (!pane) return; - $$('img[onmouseover]', pane).forEach(img => { - const omo = img.getAttribute('onmouseover') || ''; - const dur = omo.match(/(\d+)\s*turns?\s*rem/); - if (!dur) return; - const t = parseInt(dur[1]); - const st = omo.match(/(\d+)\s*stacks?/i); - const o = document.createElement('div'); - o.className = 'hv-dur-overlay'; - o.style.cssText = `position:absolute;bottom:-2px;left:50%;transform:translateX(-50%);background:${t < 3 ? '#f44' : '#44f'};color:#fff;font-size:8px;font-weight:bold;padding:0 2px;border-radius:2px;pointer-events:none;z-index:1`; - o.textContent = st ? `${t}x${parseInt(st[1])}` : t; - img.parentElement.style.position = 'relative'; - img.parentElement.appendChild(o); - }); -} - -function updateAlertColours() { - if (!CFG.alertColours) return; - let bg = '#EDEBDF'; - STATE.interruptAlert = false; - - if (!$('img[src$="bar_dgreen.png"]') && $('img[src$="fallenshield.png"]')) { - bg = 'magenta'; - STATE.interruptAlert = true; - } else if (STATE.hp < 0.25) { - bg = '#ff4488'; - STATE.interruptAlert = true; - } else if (STATE.mp < 0.15) { - bg = '#4444aa'; - } else if (Object.values(STATE.buffs).some(d => d < 2 && d > 0)) { - bg = '#88ccff'; - } - - const v = document.getElementById('pane_vitals'); - if (v) v.style.background = bg; -} - -function addMonsterNumbers() { - if (!CFG.showMonsterNumbers) return; - STATE.monsters.forEach((m, i) => { - if (!m || !m.querySelector || m.querySelector('.hv-monster-num')) return; - const b = m.querySelector('.btm1'); - if (!b) return; - const s = document.createElement('span'); - s.className = 'hv-monster-num'; - s.style.cssText = 'font-weight:bold;margin-right:3px;color:#999'; - s.textContent = (i + 1); - b.insertBefore(s, b.firstChild); - }); -} - -function updateMonsterHPDisplay() { - if (!CFG.showMonsterHP) return; - STATE.monsters.forEach((m, i) => { - if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return; - const n = m.querySelector('.btm1'); - if (!n) return; - const nm = (n.textContent || '').replace(/^\d+\s*/, '').trim(); - const md = STATE.monsterData[nm]; - if (!md || !md.hp || md.seen < 3) return; - const hpBar = m.querySelector('img[src$="nbargreen.png"]'); - if (!hpBar) return; - const r = clamp(parseInt(hpBar.style.width || '') / 120, 0.01, 1); - const txt = `HP: ${Math.max(1, Math.round(r * md.hp)).toLocaleString()} / ${md.hp.toLocaleString()}`; - const ex = m.querySelector('.hv-monster-hp'); - if (ex) { ex.textContent = txt; return; } - const d = document.createElement('div'); - d.className = 'hv-monster-hp'; - d.style.cssText = 'display:inline-block;position:relative;left:204px;top:-20px;font-size:9px;color:#888'; - d.textContent = txt; - m.appendChild(d); - }); -} - -function saveMonsterData() { - STATE.monsters.forEach((m, i) => { - if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return; - const n = m.querySelector('.btm1'); - if (!n) return; - const nm = (n.textContent || '').replace(/^\d+\s*/, '').trim(); - if (!nm) return; - const hpBar = m.querySelector('img[src$="nbargreen.png"]'); - if (hpBar) { - const r = clamp(parseInt(hpBar.style.width || '') / 120, 0.01, 1); - if (!STATE.monsterData[nm]) STATE.monsterData[nm] = { hp: 0, seen: 0 }; - STATE.monsterData[nm].hp = Math.max( - STATE.monsterData[nm].hp, - Math.round(STATE.monsterData[nm].hp * 0.7 + 1000 / r * 0.3) - ); - STATE.monsterData[nm].seen++; - } - }); - try { localStorage[SP + 'monsters'] = JSON.stringify(STATE.monsterData); } catch (e) {} -} - -function refreshUI() { - if (CFG.showDurations) updateDurationDisplay(); - if (CFG.showCooldowns) updateCooldownDisplay(); - if (CFG.alertColours) updateAlertColours(); - if (CFG.showMonsterNumbers) addMonsterNumbers(); - if (CFG.showMonsterHP) updateMonsterHPDisplay(); -} // ═══════════════════════════════════════════════════════════════════════ // SETTINGS PANEL — in-battle configuration UI @@ -2124,41 +1751,29 @@ function openSettings() { p.innerHTML = `
- ⚙ HV Unified v${VERSION} + ⚙ HV Unified v${VERSION} (Bridge & Advisor)
- Battle - ${mkTog('Auto-buff (Haste, Protection, etc.)', 'autoBuff')} - ${mkTog('Auto-debuff (Imperil, Weaken)', 'autoDebuff')} - ${mkTog('Auto-cure when HP low', 'autoCure')} - ${mkTog('Auto Spirit Stance', 'autoSpirit')} - ${mkTog('Hover-to-attack mode', 'hoverEnabled')} + Strategy Advisory + ${mkTog('Auto-buff recommendations', 'autoBuff')} + ${mkTog('Auto-debuff recommendations', 'autoDebuff')} + ${mkTog('Auto-cure recommendations', 'autoCure')} ${mkTog('Auto-difficulty suggestion', 'autoDifficulty')} - ${mkTog('Use attack spells', 'useAttackSpells')} + ${mkTog('Use attack spells in advice', 'useAttackSpells')} ${mkNum('Cure HP threshold', 'cureHP')} ${mkNum('Item HP threshold', 'cureItemHP')} ${mkNum('Mana gem MP threshold', 'manaGemMP')} ${mkNum('Mana potion MP threshold', 'manaPotionMP')} - ${mkNum('Spirit Stance OC%', 'spiritStanceOC', 5)} + ${mkNum('Skill OC% threshold', 'skillOC', 5)}
- Out of Battle - ${mkTog('Equip quick sell buttons', 'autoSell')} - ${mkTog('Bulk Shrine buttons', 'bulkShrine')} - ${mkTog('RE Timer', 'reTimer')} + Out of Battle & Guidance ${mkTog('Training queue info', 'trainingQueue')} ${mkTog('Advisor panel', 'showGuidance')} ${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')} -
- UI - ${mkTog('Cooldown timers', 'showCooldowns')} - ${mkTog('Monster HP numbers', 'showMonsterHP')} - ${mkTog('Buff duration counters', 'showDurations')} - ${mkTog('Alert colours', 'alertColours')} - ${mkTog('Monster numbers', 'showMonsterNumbers')} ${mkTog('Config button', 'cfgButton')}
- Changes apply immediately. Press , in battle to open settings. + Changes apply immediately. Press , to open settings.
`; document.body.appendChild(p); @@ -2177,6 +1792,7 @@ function openSettings() { }); } + // ═══════════════════════════════════════════════════════════════════════ // OUT-OF-BATTLE — shop enhancements, shrine, training, etc. // ═══════════════════════════════════════════════════════════════════════ @@ -2309,38 +1925,7 @@ function enhanceEquipShopWithAdvice() { // ── Shrine: bulk selection ── function enhanceShrine() { - if (!CFG.bulkShrine) return; - const m = document.getElementById('mainpane'); - if (!m || document.getElementById('hv-bulk-shrine')) return; - - const r = document.createElement('div'); - r.id = 'hv-bulk-shrine'; - r.style.cssText = 'margin:6px;display:flex;gap:6px'; - - const b1 = document.createElement('input'); - b1.type = 'button'; - b1.value = '⛩️ Select Non-Figurines'; - b1.style.cssText = 'padding:4px 10px;cursor:pointer;background:#fdcb00;color:#000;border:none;border-radius:3px;font-size:12px'; - b1.onclick = () => { - $$('tr', m).forEach(row => { - const t = (row.textContent || ''); - if (!t.includes('Figurine') && !t.includes('Collectable')) { - const cb = row.querySelector('input[type="checkbox"]'); - if (cb) cb.checked = true; - } - }); - }; - - const b2 = document.createElement('input'); - b2.type = 'button'; - b2.value = '✗ Clear'; - b2.style.cssText = 'padding:4px 10px;cursor:pointer;background:#666;color:#fff;border:none;border-radius:3px;font-size:12px'; - b2.onclick = () => { $$('input[type="checkbox"]', m).forEach(cb => cb.checked = false); }; - - r.appendChild(b1); - r.appendChild(b2); - const ta = m.querySelector('div'); - if (ta) ta.insertBefore(r, ta.firstChild); + // Shrine enhancements are owned by HVUT. } // ── Training: priority guide ── @@ -2359,103 +1944,11 @@ function enhanceTraining() { if (ta) ta.insertBefore(d, ta.firstChild); } -// ── Monster Lab: floating info panel + feed buttons ── - -let _mlDragOffset = { x: 0, y: 0 }; - function enhanceMonsterLab() { - // Create floating draggable panel - if (document.getElementById('hv-monster')) return; - - const panel = document.createElement('div'); - panel.id = 'hv-monster'; - panel.style.cssText = css({ - position: 'fixed', - top: '80px', - right: '4px', - zIndex: '9999', - background: '#111827', - color: '#d1d5db', - padding: '8px 10px', - borderRadius: '8px', - fontSize: '10px', - fontFamily: 'monospace', - maxWidth: '320px', - boxShadow: '0 0 15px rgba(0,0,0,0.6)', - border: '1px solid #374151', - cursor: 'move', - userSelect: 'none', - }); - - const body = `
- 🧬 Monster Lab - -
-
-
- - -
-
- New to ML? Pick a Beast-class (Dog/Cow/Pig) for STR/END crystals.
- Click + drag header to move. -
-
`; - - panel.innerHTML = body; - document.body.appendChild(panel); - - // ── Feed All ── - panel.querySelector('#hv-feed-all').onclick = () => { - const m = document.getElementById('mainpane'); - if (m) $$('input[value="Feed"]', m).forEach(b => b.click()); - }; - - // ── Happy Pills All ── - panel.querySelector('#hv-pill-all').onclick = () => { - const m = document.getElementById('mainpane'); - if (m) { - const pills = $$('option', m).filter(o => (o.textContent || '').includes('Happy Pill')); - if (pills.length > 0) { - pills[0].selected = true; - $$('input[value="Feed"]', m).forEach(b => b.click()); - } - } - }; - - // ── Toggle ── - panel.querySelector('#hv-monster-toggle').onclick = () => { - const body = panel.querySelector('#hv-monster-body'); - const isHidden = body.style.display === 'none'; - body.style.display = isHidden ? 'block' : 'none'; - panel.querySelector('#hv-monster-toggle').textContent = isHidden ? '−' : '+'; - }; - - // ── Make draggable ── - const header = panel.querySelector('#hv-monster-header'); - if (header) { - header.addEventListener('mousedown', (e) => { - _mlDragOffset.x = e.clientX - panel.getBoundingClientRect().left; - _mlDragOffset.y = e.clientY - panel.getBoundingClientRect().top; - const onMove = (ev) => { - panel.style.left = (ev.clientX - _mlDragOffset.x) + 'px'; - panel.style.right = 'auto'; - panel.style.top = (ev.clientY - _mlDragOffset.y) + 'px'; - }; - const onUp = () => { - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onUp); - }; - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onUp); - }); - } - - // Remove the old inline bar if it exists - const oldBar = document.getElementById('hv-lab-enhance'); - if (oldBar) oldBar.remove(); + // Monster Lab enhancements are owned by HVUT. } + // ── Config button (bottom-right) ── function addConfigButton() { @@ -3131,268 +2624,11 @@ function gradeEquipment(name) { } function enhanceArmory() { - const equipList = document.getElementById('equiplist'); - if (!equipList || document.getElementById('hv-armory-bar')) return; - - // ── Parse current equipment in the list ── - const items = []; - let currentCategory = ''; - - $$('tr', equipList).forEach(row => { - if (row.className === 'eqtplabel') { - currentCategory = (row.textContent || '').trim(); - return; - } - - const cb = row.querySelector('input[name="eqids[]"]'); - if (!cb) return; - - const id = cb.value; - const label = row.querySelector('label'); - const name = label ? (label.textContent || '').trim() : ''; - - // Status icons in the label text - const equipped = name.includes('🗡'); - const locked = name.includes('🔒'); - const pinned = name.includes('📌'); - const stored = name.includes('📦'); - const protected_ = name.includes('🛡'); - - // Strip status icons for clean name - const cleanName = name.replace(/[🗡🔒📌📦🛡️]/g, '').trim(); - - items.push({ - id, name: cleanName, slot: currentCategory, - equipped, locked, pinned, stored, protected: protected_, - checkbox: cb, row, - grade: gradeEquipment(cleanName), - }); - }); - - if (items.length === 0) return; - - // ── Detect which armory tab we're on ── - let screen = 'organize'; - const url = window.location.href || ''; - const sm = url.match(/screen=(\w+)/); - if (sm) screen = sm[1]; - - // ── Counts ── - const sellCount = items.filter(i => i.grade.action === 'sell' && !i.equipped && !i.locked).length; - const salvCount = items.filter(i => i.grade.action === 'salvage' && !i.equipped && !i.locked).length; - const keepCount = items.filter(i => i.grade.action === 'keep').length; - - // ── Build the toolbar ── - const toolbar = document.createElement('div'); - toolbar.id = 'hv-armory-bar'; - toolbar.style.cssText = css({ - display: 'flex', - gap: '4px', - flexWrap: 'wrap', - padding: '4px 6px', - marginBottom: '2px', - background: '#1a1a2e', - borderRadius: '4px', - fontSize: '10px', - fontFamily: 'monospace', - alignItems: 'center', - }); - - // Summary - const summary = document.createElement('span'); - summary.style.cssText = 'color:#888;margin-right:6px'; - summary.textContent = `✅${keepCount} ♻${salvCount} 💰${sellCount}`; - toolbar.appendChild(summary); - - // ── Smart auto-select: check boxes based on grade ── - function autoSelect(action) { - items.forEach(item => { - if (item.grade.action !== action) return; - if (item.equipped || item.locked) return; - item.checkbox.checked = true; - }); - // Trigger the game's update function if available - if (typeof update_selected_count === 'function') update_selected_count(); - } - - // ── Select by quality ── - function selectByQuality(below) { - const idx = KB.qualities.indexOf(below); - items.forEach(item => { - const qi = KB.qualities.indexOf(KB.qualities.find(q => item.name.includes(q))); - if (qi < 0) return; - if (qi <= idx && !item.equipped && !item.locked) { - item.checkbox.checked = true; - } - }); - if (typeof update_selected_count === 'function') update_selected_count(); - } - - function clearAll() { - items.forEach(item => item.checkbox.checked = false); - if (typeof update_selected_count === 'function') update_selected_count(); - } - - // Buttons — only show relevant ones for current screen - if (screen === 'sell') { - const b = document.createElement('input'); - b.type = 'button'; - b.value = `💰 Select Crude (${sellCount})`; - b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:10px'; - b.onclick = () => autoSelect('sell'); - toolbar.appendChild(b); - - const b2 = document.createElement('input'); - b2.type = 'button'; - b2.value = '💰 Select ≤Fair'; - b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#a00;color:#fff;border:none;border-radius:3px;font-size:10px'; - b2.onclick = () => selectByQuality('Fair'); - toolbar.appendChild(b2); - } - - if (screen === 'salvage') { - const b = document.createElement('input'); - b.type = 'button'; - b.value = `♻ Select Salvage (${salvCount})`; - b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px'; - b.onclick = () => autoSelect('salvage'); - toolbar.appendChild(b); - - const b2 = document.createElement('input'); - b2.type = 'button'; - b2.value = '♻ Select ≤Average'; - b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px'; - b2.onclick = () => selectByQuality('Average'); - toolbar.appendChild(b2); - - // "Sell Salvaged Equipment" toggle is already native - } - - if (screen === 'organize') { - const b = document.createElement('input'); - b.type = 'button'; - b.value = '📌 Pin Equipped'; - b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#2a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px'; - b.onclick = () => { - // Select all equipped items so they can be pinned - items.forEach(item => { if (item.equipped) item.checkbox.checked = true; }); - if (typeof update_selected_count === 'function') update_selected_count(); - }; - toolbar.appendChild(b); - } - - // ── Keep Top 5 per category (score-based), select the rest ── - // Categories: 1H, 2H, Staff, Shield + 5 armor slots × (Cloth, Light, Heavy) - function qualityIndex(name) { - const q = KB.qualities.find(qq => name.includes(qq)); - return q ? KB.qualities.indexOf(q) : 0; - } - - function keepTop5AndSelect(action) { - const catMap = new Map(); - - items.forEach(item => { - if (item.equipped || item.locked || item.pinned) return; - const data = getHVEquipData(item.id); - const stats = data && data.d ? parseEquipHTML(data.d) : null; - if (!stats) return; // skip unscraped items — leave untouched - - const type = (stats.type || ''); - let cat = '?'; - if (type.includes('Two-handed')) cat = '2H'; - else if (type.includes('One-handed')) cat = '1H'; - else if (type.includes('Staff')) cat = 'Staff'; - else if (type.includes('Shield')) cat = 'Shield'; - else if (type.includes('Cloth')) cat = 'Cloth'; - else if (type.includes('Light')) cat = 'Light'; - else if (type.includes('Heavy')) cat = 'Heavy'; - - // For armor, append the slot (Head/Body/Hands/Legs/Feet) - if (['Cloth', 'Light', 'Heavy'].includes(cat)) { - const slot = gearDetectSlot({ name: item.name, category: cat }); - if (!slot) return; - cat += ' ' + slot; - } - - const isWeaponCat = cat.startsWith('1H') || cat.startsWith('2H') || cat === 'Staff' || cat === 'Shield'; - const quality = qualityIndex(item.name); - const score = isWeaponCat - ? gearScoreWeapon({ name: item.name, stats, quality }) - : gearScoreArmor({ name: item.name, stats, quality }); - - if (!catMap.has(cat)) catMap.set(cat, []); - catMap.get(cat).push({ item, score }); - }); - - // Keep top 10% per category (min 5), select the rest - let selected = 0; - catMap.forEach((list, cat) => { - list.sort((a, b) => b.score - a.score); - const keepCount = Math.max(5, Math.ceil(list.length * 0.10)); - const keep = list.slice(0, keepCount); - const dump = list.slice(keepCount); - dump.forEach(({ item }) => { - item.checkbox.checked = true; - selected++; - }); - console.log(`%c[HV] 🗂 ${cat}: ${list.length} items → keep ${keep.length} (top ${keep.map(k => k.score.toFixed(0)).join('/')}), select ${dump.length}`, 'color:#0f0'); - }); - - if (typeof update_selected_count === 'function') update_selected_count(); - return selected; - } - - // ── Keep Top 10% button — available on sell and salvage screens ── - if (screen === 'sell' || screen === 'salvage') { - const b3 = document.createElement('input'); - b3.type = 'button'; - b3.value = `⭐ Keep Top 10% / Select ${screen === 'sell' ? 'Sell' : 'Salvage'} Rest`; - b3.style.cssText = 'padding:2px 6px;cursor:pointer;background:#7a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px'; - b3.title = 'Keeps the top 10% (min 5) highest-scoring items in each category (1H, 2H, Staff, Shield, and each armor slot per Cloth/Light/Heavy). Selects everything else.'; - b3.onclick = () => { - const n = keepTop5AndSelect(screen); - console.log(`%c[HV] ⭐ Keep Top 10%: ${n} items selected for ${screen}`, 'color:#0f0'); - }; - toolbar.appendChild(b3); - } - - // Clear button always available - const clear = document.createElement('input'); - clear.type = 'button'; - clear.value = '✗ Clear'; - clear.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px'; - clear.onclick = clearAll; - toolbar.appendChild(clear); - - // Select All / Invert Selection (useful on any screen) - const selAll = document.createElement('input'); - selAll.type = 'button'; - selAll.value = '☐ All'; - selAll.style.cssText = 'padding:2px 6px;cursor:pointer;background:#3a4a6a;color:#fff;border:none;border-radius:3px;font-size:10px'; - selAll.onclick = () => { - items.forEach(item => { item.checkbox.checked = true; }); - if (typeof update_selected_count === 'function') update_selected_count(); - }; - toolbar.appendChild(selAll); - - const invert = document.createElement('input'); - invert.type = 'button'; - invert.value = '⊞ Invert'; - invert.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px'; - invert.onclick = () => { - items.forEach(item => { item.checkbox.checked = !item.checkbox.checked; }); - if (typeof update_selected_count === 'function') update_selected_count(); - }; - toolbar.appendChild(invert); - - // ── Insert into page ── - const eqSelect = document.getElementById('equipselect_outer') || - document.querySelector('#equipselect_left, #armory_right > div'); - if (equipList && equipList.parentNode) { - equipList.parentNode.insertBefore(toolbar, equipList); - } + // Physical bulk select/sell/salvage/shrine buttons in Armory are owned by HVUT. + // HV Unified provides advisory scoring via HV.analyzeGear() and gear-analysis panel. } + // ═══════════════════════════════════════════════════════════════════════ // GEAR SCRAPER — capture full equipment details for analysis // ═══════════════════════════════════════════════════════════════════════ @@ -4250,172 +3486,22 @@ function enhanceGearAnalysis() { } // ═══════════════════════════════════════════════════════════════════════ -// BATTLE LOGGER — saves raw battle log lines to localStorage in real time +// BATTLE LOGGER — REMOVED (Handled by Monsterbation combat log) // ═══════════════════════════════════════════════════════════════════════ -let _lastLogCount = 0; -const LOG_KEY = SP + 'battleLog'; +function initBattleLog() {} +function logRound() {} +function getBattleLog() { return null; } +function getBattleSummary() { return null; } -function initBattleLog() { - _lastLogCount = 0; - // Clear previous log - try { localStorage.removeItem(LOG_KEY); } catch (e) {} -} - -function logRound() { - const log = document.getElementById('textlog'); - if (!log) return; - - const allRows = log.querySelectorAll('tr'); - if (allRows.length <= _lastLogCount) return; - - // Get only the NEW rows since last capture - const newLines = []; - for (let i = _lastLogCount; i < allRows.length; i++) { - const text = (allRows[i].textContent || '').trim(); - if (text) newLines.push(text); - } - _lastLogCount = allRows.length; - - if (newLines.length === 0) return; - - // Read existing log from localStorage, append new lines, save back - let existing = []; - try { - const saved = localStorage.getItem(LOG_KEY); - if (saved) existing = JSON.parse(saved); - } catch (e) {} - - existing.push(...newLines); - - try { - localStorage.setItem(LOG_KEY, JSON.stringify(existing)); - } catch (e) { - // localStorage full — keep only last 500 lines - try { - const trimmed = existing.slice(-500); - localStorage.setItem(LOG_KEY, JSON.stringify(trimmed)); - } catch (e2) {} - } -} - -function getBattleLog() { - try { - const saved = localStorage.getItem(LOG_KEY); - return saved ? JSON.parse(saved) : null; - } catch (e) { - return null; - } -} - -function getBattleSummary() { - const lines = getBattleLog(); - if (!lines || lines.length === 0) return null; - - let kills = 0, dmgDealt = 0, dmgTaken = 0, healed = 0; - const spells = {}, skills = {}, items = {}; - - for (const line of lines) { - const mKill = line.match(/(.+) has been defeated\./); - if (mKill && !line.includes('gains the effect')) kills++; - - const mDmg = line.match(/causing (\d+) points/); - if (mDmg) dmgDealt += parseInt(mDmg[1]); - - const mTaken = line.match(/take (\d+) (\w+)/); - if (mTaken) dmgTaken += parseInt(mTaken[1]); - - const mRegen = line.match(/Regen restores (\d+)/); - if (mRegen) healed += parseInt(mRegen[1]); - const mPot = line.match(/Regeneration restores (\d+)/); - if (mPot) healed += parseInt(mPot[1]); - const mCure = line.match(/healed for (\d+)/); - if (mCure) healed += parseInt(mCure[1]); - - const mSpell = line.match(/You cast (\w[\w\s-]+)\./); - if (mSpell) spells[mSpell[1]] = (spells[mSpell[1]] || 0) + 1; - - const mSkill = line.match(/You use (\w[\w\s-]+)\./); - if (mSkill && !mSkill[1].includes('Draught') && !mSkill[1].includes('Potion') && !mSkill[1].includes('Elixir') && !mSkill[1].includes('Gem')) { - skills[mSkill[1]] = (skills[mSkill[1]] || 0) + 1; - } - - const mItem = line.match(/You use (.+?)\.$/); - if (mItem && (mItem[1].includes('Draught') || mItem[1].includes('Potion') || mItem[1].includes('Elixir') || mItem[1].includes('Gem'))) { - items[mItem[1]] = (items[mItem[1]] || 0) + 1; - } - } - - return { - totalLines: lines.length, - kills, - dmgDealt, - dmgTaken, - healed, - spells, - skills, - items, - }; -} // ═══════════════════════════════════════════════════════════════════════ -// RE TIMER — Random Encounter countdown +// RE TIMER — REMOVED (Handled by HVUtils / HVUT) // ═══════════════════════════════════════════════════════════════════════ -let reTimerEl = null; +function setupRETimer() {} +function updateRETimer() {} -function setupRETimer() { - if (!CFG.reTimer) return; - if (document.getElementById('hv-re-timer')) return; - - reTimerEl = document.createElement('div'); - reTimerEl.id = 'hv-re-timer'; - reTimerEl.style.cssText = css({ - position: 'fixed', - top: '2px', - left: '50%', - transform: 'translateX(-50%)', - zIndex: '99999', - background: '#1a1a2e', - color: '#0f0', - padding: '2px 10px', - borderRadius: '0 0 6px 6px', - fontSize: '10px', - fontFamily: 'monospace', - opacity: '0.9', - }); - reTimerEl.textContent = 'RE: --:--'; - document.body.appendChild(reTimerEl); - updateRETimer(); - setInterval(updateRETimer, 30000); -} - -function updateRETimer() { - if (!reTimerEl) return; - const now = Date.now(); - const dt = document.body.textContent || ''; - - // Detect dawn - if (dt.includes('Dawn of a New Day') || dt.includes('A new day dawns')) { - try { localStorage[SP + 'lastDawn'] = JSON.stringify(now); } catch (e) {} - } - - const ld = JSON.parse(localStorage[SP + 'lastDawn'] || now); - const ms = (now - ld) / 60000; - const nr = 30 - (ms % 30); - - if (nr < 1) { - reTimerEl.textContent = '⚡ RE READY!'; - reTimerEl.style.color = '#f00'; - reTimerEl.style.fontWeight = 'bold'; - } else { - const mn = Math.floor(nr); - const sc = Math.floor((nr - mn) * 60); - reTimerEl.textContent = `RE: ${String(mn).padStart(2, '0')}:${String(sc).padStart(2, '0')}`; - reTimerEl.style.color = '#0f0'; - reTimerEl.style.fontWeight = 'normal'; - } -} // ── Settings page: auto-sell/salvage defaults ── @@ -4553,44 +3639,34 @@ window.HV = { clearBuy: () => { const db = getGearDB(); saveGearDB(db.filter(e => e.source !== 'buy')); return 'Buy data cleared'; }, }; -// ═══════════════════════════════════════════════════════════════════════ -// INIT — bootstrap everything -// ═══════════════════════════════════════════════════════════════════════ - function init() { loadConfig(); detectLevel(); STATE.page = detectPage(); + // Community script presence detection + STATE.hasMB = !!(window.Monsterbation || document.getElementById('mb_vitals') || document.querySelector('.mb_vital')); + STATE.hasHVUT = !!(window.HVUT || document.querySelector('.hvut-main') || document.getElementById('hvut_menu')); + // Always-on features - setupRETimer(); addConfigButton(); if (STATE.page === 'battle') { initializeBattle(); autoCheckTask('battle'); } else { - // Non-battle page enhancements + // Non-battle page enhancements & advisory tools if (STATE.page === 'itemshop') { enhanceItemShop(); autoCheckTask('buy-health'); } - if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); } + if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); enhanceGearAnalysis(); } if (STATE.page === 'shrine') enhanceShrine(); if (STATE.page === 'training') { enhanceTraining(); autoCheckTask('training'); } - if (STATE.page === 'monsterlab') { enhanceMonsterLab(); autoCheckTask('feed'); } if (STATE.page === 'arena') { autoCheckTask('arenas'); autoCheckTask('first-blood'); } if (STATE.page === 'character') showGuidancePanel(); if (STATE.page === 'settings') enhanceSettings(); if (STATE.page === 'armory') { enhanceArmory(); enhanceGearAnalysis(); } - if (STATE.page === 'equipshop') enhanceGearAnalysis(); if (STATE.page === 'abilities') enhanceAbilities(); - if (STATE.page === 'monster') enhanceMonsterLab(); } - // Dawn initialization - if (!localStorage[SP + 'lastDawn']) { - try { localStorage[SP + 'lastDawn'] = JSON.stringify(Date.now()); } catch (e) {} - } - updateRETimer(); - // Watch for dynamic page transitions (arena/grindfest → battle) const bodyObserver = new MutationObserver(() => { if (!STATE.inBattle && isBattlePage()) { @@ -4604,62 +3680,35 @@ function initializeBattle() { if (STATE.battleInitialized && STATE.page === 'battle') return; STATE.page = 'battle'; STATE.inBattle = true; - STATE._mysticUsed = false; // Reset Mystic Gem tracking on new battle - STATE._spiritForSkill = 0; // Reset spirit burst toggle - STATE._baseMpCosts = {}; // Reset base cost cache parseBattleState(); - // Force tier update from cached level (battle page may not have level_readout) if (STATE.level > 1) updateTier(); - setupHover(); setupKeybindings(); - addMonsterNumbers(); - refreshUI(); addConfigButton(); showDifficultyBanner(); - // Watch battle log + // Passive battle state parser (updates STATE without interfering with MB UI/clicks) const log = document.getElementById('textlog'); if (log && !log.dataset.hvObserved) { log.dataset.hvObserved = '1'; const obs = new MutationObserver(() => { parseBattleState(); - saveMonsterData(); - logRound(); // Log this round's data for analysis - if (CFG.hoverEnabled && !STATE.interruptHover && !STATE.interruptAlert && STATE.hoverTarget >= 0) { - const a = getSmartAction(); - if (a) executeAction(a); - } - refreshUI(); updateConfigButton(); }); obs.observe(log, { childList: true, subtree: true, characterData: true }); } - // Watch vitals pane const vitals = document.getElementById('pane_vitals'); if (vitals && !vitals.dataset.hvObserved) { vitals.dataset.hvObserved = '1'; const vobs = new MutationObserver(() => { parseBattleState(); - logRound(); // Also log on vitals changes (catches end-of-round) - refreshUI(); updateConfigButton(); - // Check if battle ended (completion pane appeared) - if (document.getElementById('pane_completion')) { - // Battle ended — handled by log observer - } }); vobs.observe(vitals, { childList: true, subtree: true, attributes: true }); } - // RiddleMaster detection - if (document.getElementById('riddlemaster')) { - STATE.interruptHover = true; - } - - // Auto-scrape gear data on character/armory pages (reads from HV's in-memory store) - // Wait for dynjs_equip to load (external script, may load after document-end) + // Auto-scrape gear data on character/armory pages const url = window.location.href || ''; if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) { let attempts = 0; @@ -4670,9 +3719,8 @@ function initializeBattle() { return; } if (++attempts < 10) { - setTimeout(tryScrape, 500); // retry every 500ms up to 5 seconds + setTimeout(tryScrape, 500); } else { - // Fallback: scrape names/IDs without stats autoScrapeGear(); } }; @@ -4690,10 +3738,11 @@ if (document.readyState === 'loading') { init(); } -console.log('%c🛡️ HV Unified v' + VERSION + '%c | Q=action H=hover C=cure ,=settings', +console.log('%c🛡️ HV Unified v' + VERSION + ' Bridge & Advisory Mode%c | ,=settings', 'color:#fdcb00;font-weight:bold', ''); -console.log('%c HV.advice() for guidance | HV.difficulty() for difficulty check', +console.log('%c HV.analyzeGear() for gear scoring | HV.advice() for build guidance | HV.difficulty() for difficulty check', 'color:#888;font-size:10px'); })(); + diff --git a/src/action-executor.js b/src/action-executor.js index ed03b47..3e0745b 100644 --- a/src/action-executor.js +++ b/src/action-executor.js @@ -10,16 +10,14 @@ function executeAction(a) { case 'spell': result = a.selfTarget ? castSelfSpell(a.name) : castTargetSpell(a.name, a.target); break; case 'skill': result = useSkill(a.name, a.target); break; case 'item': result = useItem(a.id); break; - case 'toggle_spirit': result = toggleSpiritStance(); break; } if (result) { - // After dispatching an action, stagger the debounce so we don't queue - // another action while the game processes this one (typically ~500-1000ms) STATE._lastActionTime = Date.now(); } return result; } + function attackMonster(i) { if (i < 0 || i >= STATE.monsters.length) return false; const m = STATE.monsters[i]; @@ -111,9 +109,3 @@ function useSkill(n, i) { return true; } -function toggleSpiritStance() { - const t = document.getElementById('ckey_spirit'); - if (!t) return false; - t.click(); - return true; -} diff --git a/src/armory.js b/src/armory.js index d5badd8..af6fac6 100644 --- a/src/armory.js +++ b/src/armory.js @@ -24,264 +24,7 @@ function gradeEquipment(name) { } function enhanceArmory() { - const equipList = document.getElementById('equiplist'); - if (!equipList || document.getElementById('hv-armory-bar')) return; - - // ── Parse current equipment in the list ── - const items = []; - let currentCategory = ''; - - $$('tr', equipList).forEach(row => { - if (row.className === 'eqtplabel') { - currentCategory = (row.textContent || '').trim(); - return; - } - - const cb = row.querySelector('input[name="eqids[]"]'); - if (!cb) return; - - const id = cb.value; - const label = row.querySelector('label'); - const name = label ? (label.textContent || '').trim() : ''; - - // Status icons in the label text - const equipped = name.includes('🗡'); - const locked = name.includes('🔒'); - const pinned = name.includes('📌'); - const stored = name.includes('📦'); - const protected_ = name.includes('🛡'); - - // Strip status icons for clean name - const cleanName = name.replace(/[🗡🔒📌📦🛡️]/g, '').trim(); - - items.push({ - id, name: cleanName, slot: currentCategory, - equipped, locked, pinned, stored, protected: protected_, - checkbox: cb, row, - grade: gradeEquipment(cleanName), - }); - }); - - if (items.length === 0) return; - - // ── Detect which armory tab we're on ── - let screen = 'organize'; - const url = window.location.href || ''; - const sm = url.match(/screen=(\w+)/); - if (sm) screen = sm[1]; - - // ── Counts ── - const sellCount = items.filter(i => i.grade.action === 'sell' && !i.equipped && !i.locked).length; - const salvCount = items.filter(i => i.grade.action === 'salvage' && !i.equipped && !i.locked).length; - const keepCount = items.filter(i => i.grade.action === 'keep').length; - - // ── Build the toolbar ── - const toolbar = document.createElement('div'); - toolbar.id = 'hv-armory-bar'; - toolbar.style.cssText = css({ - display: 'flex', - gap: '4px', - flexWrap: 'wrap', - padding: '4px 6px', - marginBottom: '2px', - background: '#1a1a2e', - borderRadius: '4px', - fontSize: '10px', - fontFamily: 'monospace', - alignItems: 'center', - }); - - // Summary - const summary = document.createElement('span'); - summary.style.cssText = 'color:#888;margin-right:6px'; - summary.textContent = `✅${keepCount} ♻${salvCount} 💰${sellCount}`; - toolbar.appendChild(summary); - - // ── Smart auto-select: check boxes based on grade ── - function autoSelect(action) { - items.forEach(item => { - if (item.grade.action !== action) return; - if (item.equipped || item.locked) return; - item.checkbox.checked = true; - }); - // Trigger the game's update function if available - if (typeof update_selected_count === 'function') update_selected_count(); - } - - // ── Select by quality ── - function selectByQuality(below) { - const idx = KB.qualities.indexOf(below); - items.forEach(item => { - const qi = KB.qualities.indexOf(KB.qualities.find(q => item.name.includes(q))); - if (qi < 0) return; - if (qi <= idx && !item.equipped && !item.locked) { - item.checkbox.checked = true; - } - }); - if (typeof update_selected_count === 'function') update_selected_count(); - } - - function clearAll() { - items.forEach(item => item.checkbox.checked = false); - if (typeof update_selected_count === 'function') update_selected_count(); - } - - // Buttons — only show relevant ones for current screen - if (screen === 'sell') { - const b = document.createElement('input'); - b.type = 'button'; - b.value = `💰 Select Crude (${sellCount})`; - b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:10px'; - b.onclick = () => autoSelect('sell'); - toolbar.appendChild(b); - - const b2 = document.createElement('input'); - b2.type = 'button'; - b2.value = '💰 Select ≤Fair'; - b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#a00;color:#fff;border:none;border-radius:3px;font-size:10px'; - b2.onclick = () => selectByQuality('Fair'); - toolbar.appendChild(b2); - } - - if (screen === 'salvage') { - const b = document.createElement('input'); - b.type = 'button'; - b.value = `♻ Select Salvage (${salvCount})`; - b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px'; - b.onclick = () => autoSelect('salvage'); - toolbar.appendChild(b); - - const b2 = document.createElement('input'); - b2.type = 'button'; - b2.value = '♻ Select ≤Average'; - b2.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px'; - b2.onclick = () => selectByQuality('Average'); - toolbar.appendChild(b2); - - // "Sell Salvaged Equipment" toggle is already native - } - - if (screen === 'organize') { - const b = document.createElement('input'); - b.type = 'button'; - b.value = '📌 Pin Equipped'; - b.style.cssText = 'padding:2px 6px;cursor:pointer;background:#2a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px'; - b.onclick = () => { - // Select all equipped items so they can be pinned - items.forEach(item => { if (item.equipped) item.checkbox.checked = true; }); - if (typeof update_selected_count === 'function') update_selected_count(); - }; - toolbar.appendChild(b); - } - - // ── Keep Top 5 per category (score-based), select the rest ── - // Categories: 1H, 2H, Staff, Shield + 5 armor slots × (Cloth, Light, Heavy) - function qualityIndex(name) { - const q = KB.qualities.find(qq => name.includes(qq)); - return q ? KB.qualities.indexOf(q) : 0; - } - - function keepTop5AndSelect(action) { - const catMap = new Map(); - - items.forEach(item => { - if (item.equipped || item.locked || item.pinned) return; - const data = getHVEquipData(item.id); - const stats = data && data.d ? parseEquipHTML(data.d) : null; - if (!stats) return; // skip unscraped items — leave untouched - - const type = (stats.type || ''); - let cat = '?'; - if (type.includes('Two-handed')) cat = '2H'; - else if (type.includes('One-handed')) cat = '1H'; - else if (type.includes('Staff')) cat = 'Staff'; - else if (type.includes('Shield')) cat = 'Shield'; - else if (type.includes('Cloth')) cat = 'Cloth'; - else if (type.includes('Light')) cat = 'Light'; - else if (type.includes('Heavy')) cat = 'Heavy'; - - // For armor, append the slot (Head/Body/Hands/Legs/Feet) - if (['Cloth', 'Light', 'Heavy'].includes(cat)) { - const slot = gearDetectSlot({ name: item.name, category: cat }); - if (!slot) return; - cat += ' ' + slot; - } - - const isWeaponCat = cat.startsWith('1H') || cat.startsWith('2H') || cat === 'Staff' || cat === 'Shield'; - const quality = qualityIndex(item.name); - const score = isWeaponCat - ? gearScoreWeapon({ name: item.name, stats, quality }) - : gearScoreArmor({ name: item.name, stats, quality }); - - if (!catMap.has(cat)) catMap.set(cat, []); - catMap.get(cat).push({ item, score }); - }); - - // Keep top 10% per category (min 5), select the rest - let selected = 0; - catMap.forEach((list, cat) => { - list.sort((a, b) => b.score - a.score); - const keepCount = Math.max(5, Math.ceil(list.length * 0.10)); - const keep = list.slice(0, keepCount); - const dump = list.slice(keepCount); - dump.forEach(({ item }) => { - item.checkbox.checked = true; - selected++; - }); - console.log(`%c[HV] 🗂 ${cat}: ${list.length} items → keep ${keep.length} (top ${keep.map(k => k.score.toFixed(0)).join('/')}), select ${dump.length}`, 'color:#0f0'); - }); - - if (typeof update_selected_count === 'function') update_selected_count(); - return selected; - } - - // ── Keep Top 10% button — available on sell and salvage screens ── - if (screen === 'sell' || screen === 'salvage') { - const b3 = document.createElement('input'); - b3.type = 'button'; - b3.value = `⭐ Keep Top 10% / Select ${screen === 'sell' ? 'Sell' : 'Salvage'} Rest`; - b3.style.cssText = 'padding:2px 6px;cursor:pointer;background:#7a5a2a;color:#fff;border:none;border-radius:3px;font-size:10px'; - b3.title = 'Keeps the top 10% (min 5) highest-scoring items in each category (1H, 2H, Staff, Shield, and each armor slot per Cloth/Light/Heavy). Selects everything else.'; - b3.onclick = () => { - const n = keepTop5AndSelect(screen); - console.log(`%c[HV] ⭐ Keep Top 10%: ${n} items selected for ${screen}`, 'color:#0f0'); - }; - toolbar.appendChild(b3); - } - - // Clear button always available - const clear = document.createElement('input'); - clear.type = 'button'; - clear.value = '✗ Clear'; - clear.style.cssText = 'padding:2px 6px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:10px'; - clear.onclick = clearAll; - toolbar.appendChild(clear); - - // Select All / Invert Selection (useful on any screen) - const selAll = document.createElement('input'); - selAll.type = 'button'; - selAll.value = '☐ All'; - selAll.style.cssText = 'padding:2px 6px;cursor:pointer;background:#3a4a6a;color:#fff;border:none;border-radius:3px;font-size:10px'; - selAll.onclick = () => { - items.forEach(item => { item.checkbox.checked = true; }); - if (typeof update_selected_count === 'function') update_selected_count(); - }; - toolbar.appendChild(selAll); - - const invert = document.createElement('input'); - invert.type = 'button'; - invert.value = '⊞ Invert'; - invert.style.cssText = 'padding:2px 6px;cursor:pointer;background:#444;color:#fff;border:none;border-radius:3px;font-size:10px'; - invert.onclick = () => { - items.forEach(item => { item.checkbox.checked = !item.checkbox.checked; }); - if (typeof update_selected_count === 'function') update_selected_count(); - }; - toolbar.appendChild(invert); - - // ── Insert into page ── - const eqSelect = document.getElementById('equipselect_outer') || - document.querySelector('#equipselect_left, #armory_right > div'); - if (equipList && equipList.parentNode) { - equipList.parentNode.insertBefore(toolbar, equipList); - } + // Physical bulk select/sell/salvage/shrine buttons in Armory are owned by HVUT. + // HV Unified provides advisory scoring via HV.analyzeGear() and gear-analysis panel. } + diff --git a/src/battle-logger.js b/src/battle-logger.js index 2039333..408faa7 100644 --- a/src/battle-logger.js +++ b/src/battle-logger.js @@ -1,108 +1,9 @@ // ═══════════════════════════════════════════════════════════════════════ -// BATTLE LOGGER — saves raw battle log lines to localStorage in real time +// BATTLE LOGGER — REMOVED (Handled by Monsterbation combat log) // ═══════════════════════════════════════════════════════════════════════ -let _lastLogCount = 0; -const LOG_KEY = SP + 'battleLog'; +function initBattleLog() {} +function logRound() {} +function getBattleLog() { return null; } +function getBattleSummary() { return null; } -function initBattleLog() { - _lastLogCount = 0; - // Clear previous log - try { localStorage.removeItem(LOG_KEY); } catch (e) {} -} - -function logRound() { - const log = document.getElementById('textlog'); - if (!log) return; - - const allRows = log.querySelectorAll('tr'); - if (allRows.length <= _lastLogCount) return; - - // Get only the NEW rows since last capture - const newLines = []; - for (let i = _lastLogCount; i < allRows.length; i++) { - const text = (allRows[i].textContent || '').trim(); - if (text) newLines.push(text); - } - _lastLogCount = allRows.length; - - if (newLines.length === 0) return; - - // Read existing log from localStorage, append new lines, save back - let existing = []; - try { - const saved = localStorage.getItem(LOG_KEY); - if (saved) existing = JSON.parse(saved); - } catch (e) {} - - existing.push(...newLines); - - try { - localStorage.setItem(LOG_KEY, JSON.stringify(existing)); - } catch (e) { - // localStorage full — keep only last 500 lines - try { - const trimmed = existing.slice(-500); - localStorage.setItem(LOG_KEY, JSON.stringify(trimmed)); - } catch (e2) {} - } -} - -function getBattleLog() { - try { - const saved = localStorage.getItem(LOG_KEY); - return saved ? JSON.parse(saved) : null; - } catch (e) { - return null; - } -} - -function getBattleSummary() { - const lines = getBattleLog(); - if (!lines || lines.length === 0) return null; - - let kills = 0, dmgDealt = 0, dmgTaken = 0, healed = 0; - const spells = {}, skills = {}, items = {}; - - for (const line of lines) { - const mKill = line.match(/(.+) has been defeated\./); - if (mKill && !line.includes('gains the effect')) kills++; - - const mDmg = line.match(/causing (\d+) points/); - if (mDmg) dmgDealt += parseInt(mDmg[1]); - - const mTaken = line.match(/take (\d+) (\w+)/); - if (mTaken) dmgTaken += parseInt(mTaken[1]); - - const mRegen = line.match(/Regen restores (\d+)/); - if (mRegen) healed += parseInt(mRegen[1]); - const mPot = line.match(/Regeneration restores (\d+)/); - if (mPot) healed += parseInt(mPot[1]); - const mCure = line.match(/healed for (\d+)/); - if (mCure) healed += parseInt(mCure[1]); - - const mSpell = line.match(/You cast (\w[\w\s-]+)\./); - if (mSpell) spells[mSpell[1]] = (spells[mSpell[1]] || 0) + 1; - - const mSkill = line.match(/You use (\w[\w\s-]+)\./); - if (mSkill && !mSkill[1].includes('Draught') && !mSkill[1].includes('Potion') && !mSkill[1].includes('Elixir') && !mSkill[1].includes('Gem')) { - skills[mSkill[1]] = (skills[mSkill[1]] || 0) + 1; - } - - const mItem = line.match(/You use (.+?)\.$/); - if (mItem && (mItem[1].includes('Draught') || mItem[1].includes('Potion') || mItem[1].includes('Elixir') || mItem[1].includes('Gem'))) { - items[mItem[1]] = (items[mItem[1]] || 0) + 1; - } - } - - return { - totalLines: lines.length, - kills, - dmgDealt, - dmgTaken, - healed, - spells, - skills, - items, - }; -} diff --git a/src/config.js b/src/config.js index f6cb234..1544526 100644 --- a/src/config.js +++ b/src/config.js @@ -2,17 +2,13 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.15.4'; +const VERSION = '0.16.0'; const CFG = { - // — Battle automation - hotkey: 'KeyQ', - hotkeyMod: '', - hoverEnabled: true, + // — Battle strategy advisory autoBuff: true, autoDebuff: true, autoCure: true, - autoSpirit: false, // Manual spirit stance only (auto-disable at 65% SP for Spark) // — Thresholds (simulation-backed) cureHP: 0.45, // Cast Cure below this HP @@ -20,29 +16,18 @@ const CFG = { cureRegenHP: 0.65, // Cast Regen below this HP manaGemMP: 0.45, // Use mana gems below this MP manaPotionMP: 0.25, // Use mana potions at this MP - spiritPotionSP: 0.60, // Use spirit gems/potions below this SP (Spark costs 50%) - spiritStanceOC: 80, // Manual spirit stance only (auto-disable at 65% SP for Spark) - skillOC: 60, // Use weapon skills at this OC (lower = faster, key on IWBTH) + skillOC: 60, // Use weapon skills at this OC useAttackSpells: false, // Don't use attack spells (let channel proc naturally) // — Out of battle autoSell: true, sellQuality: 'Average', - bulkShrine: true, - reTimer: true, trainingQueue: true, - // — UI - showCooldowns: true, - showMonsterHP: true, - showDurations: true, - alertColours: true, - showMonsterNumbers: true, + // — UI & Guidance cfgButton: true, showGuidance: true, showEquipAdvice: true, autoDifficulty: true, - - // — Combat style - useAttackSpells: true, }; + diff --git a/src/header.user.js b/src/header.user.js index c31d9b5..cee5f02 100644 --- a/src/header.user.js +++ b/src/header.user.js @@ -1,8 +1,8 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.15.4 -// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse +// @version 0.16.0 +// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils) // @author GaboGG + Hermes // @match *://*.hentaiverse.org/* // @match *://alt.hentaiverse.org/* @@ -12,3 +12,4 @@ (function() { 'use strict'; + diff --git a/src/hover-system.js b/src/hover-system.js index 2f42cde..fa035e9 100644 --- a/src/hover-system.js +++ b/src/hover-system.js @@ -1,27 +1,6 @@ // ═══════════════════════════════════════════════════════════════════════ -// HOVER SYSTEM — mouse-over monster actions +// HOVER SYSTEM — REMOVED (Handled by Monsterbation / MB) // ═══════════════════════════════════════════════════════════════════════ -function setupHover() { - if (!CFG.hoverEnabled) return; +function setupHover() {} - STATE.monsters.forEach((m, i) => { - if (!m || m.dataset.hvHover) return; - m.dataset.hvHover = '1'; - - const area = m.querySelector('.btm6') || m; - area.addEventListener('mouseenter', () => { - STATE.hoverTarget = i; - if (!STATE.interruptHover && !STATE.interruptAlert) { - const ac = getSmartAction(); - if (ac) executeAction(ac); - } - }); - area.addEventListener('mouseleave', () => { STATE.hoverTarget = -1; }); - }); - - // RiddleMaster blocks hover - if (document.getElementById('riddlemaster')) { - STATE.interruptHover = true; - } -} diff --git a/src/init.js b/src/init.js index f7c9afc..b6fac67 100644 --- a/src/init.js +++ b/src/init.js @@ -1,41 +1,31 @@ -// ═══════════════════════════════════════════════════════════════════════ -// INIT — bootstrap everything -// ═══════════════════════════════════════════════════════════════════════ - function init() { loadConfig(); detectLevel(); STATE.page = detectPage(); + // Community script presence detection + STATE.hasMB = !!(window.Monsterbation || document.getElementById('mb_vitals') || document.querySelector('.mb_vital')); + STATE.hasHVUT = !!(window.HVUT || document.querySelector('.hvut-main') || document.getElementById('hvut_menu')); + // Always-on features - setupRETimer(); addConfigButton(); if (STATE.page === 'battle') { initializeBattle(); autoCheckTask('battle'); } else { - // Non-battle page enhancements + // Non-battle page enhancements & advisory tools if (STATE.page === 'itemshop') { enhanceItemShop(); autoCheckTask('buy-health'); } - if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); } + if (STATE.page === 'equipshop') { enhanceEquipShopWithAdvice(); enhanceGearAnalysis(); } if (STATE.page === 'shrine') enhanceShrine(); if (STATE.page === 'training') { enhanceTraining(); autoCheckTask('training'); } - if (STATE.page === 'monsterlab') { enhanceMonsterLab(); autoCheckTask('feed'); } if (STATE.page === 'arena') { autoCheckTask('arenas'); autoCheckTask('first-blood'); } if (STATE.page === 'character') showGuidancePanel(); if (STATE.page === 'settings') enhanceSettings(); if (STATE.page === 'armory') { enhanceArmory(); enhanceGearAnalysis(); } - if (STATE.page === 'equipshop') enhanceGearAnalysis(); if (STATE.page === 'abilities') enhanceAbilities(); - if (STATE.page === 'monster') enhanceMonsterLab(); } - // Dawn initialization - if (!localStorage[SP + 'lastDawn']) { - try { localStorage[SP + 'lastDawn'] = JSON.stringify(Date.now()); } catch (e) {} - } - updateRETimer(); - // Watch for dynamic page transitions (arena/grindfest → battle) const bodyObserver = new MutationObserver(() => { if (!STATE.inBattle && isBattlePage()) { @@ -49,62 +39,35 @@ function initializeBattle() { if (STATE.battleInitialized && STATE.page === 'battle') return; STATE.page = 'battle'; STATE.inBattle = true; - STATE._mysticUsed = false; // Reset Mystic Gem tracking on new battle - STATE._spiritForSkill = 0; // Reset spirit burst toggle - STATE._baseMpCosts = {}; // Reset base cost cache parseBattleState(); - // Force tier update from cached level (battle page may not have level_readout) if (STATE.level > 1) updateTier(); - setupHover(); setupKeybindings(); - addMonsterNumbers(); - refreshUI(); addConfigButton(); showDifficultyBanner(); - // Watch battle log + // Passive battle state parser (updates STATE without interfering with MB UI/clicks) const log = document.getElementById('textlog'); if (log && !log.dataset.hvObserved) { log.dataset.hvObserved = '1'; const obs = new MutationObserver(() => { parseBattleState(); - saveMonsterData(); - logRound(); // Log this round's data for analysis - if (CFG.hoverEnabled && !STATE.interruptHover && !STATE.interruptAlert && STATE.hoverTarget >= 0) { - const a = getSmartAction(); - if (a) executeAction(a); - } - refreshUI(); updateConfigButton(); }); obs.observe(log, { childList: true, subtree: true, characterData: true }); } - // Watch vitals pane const vitals = document.getElementById('pane_vitals'); if (vitals && !vitals.dataset.hvObserved) { vitals.dataset.hvObserved = '1'; const vobs = new MutationObserver(() => { parseBattleState(); - logRound(); // Also log on vitals changes (catches end-of-round) - refreshUI(); updateConfigButton(); - // Check if battle ended (completion pane appeared) - if (document.getElementById('pane_completion')) { - // Battle ended — handled by log observer - } }); vobs.observe(vitals, { childList: true, subtree: true, attributes: true }); } - // RiddleMaster detection - if (document.getElementById('riddlemaster')) { - STATE.interruptHover = true; - } - - // Auto-scrape gear data on character/armory pages (reads from HV's in-memory store) - // Wait for dynjs_equip to load (external script, may load after document-end) + // Auto-scrape gear data on character/armory pages const url = window.location.href || ''; if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) { let attempts = 0; @@ -115,9 +78,8 @@ function initializeBattle() { return; } if (++attempts < 10) { - setTimeout(tryScrape, 500); // retry every 500ms up to 5 seconds + setTimeout(tryScrape, 500); } else { - // Fallback: scrape names/IDs without stats autoScrapeGear(); } }; @@ -135,9 +97,10 @@ if (document.readyState === 'loading') { init(); } -console.log('%c🛡️ HV Unified v' + VERSION + '%c | Q=action H=hover C=cure ,=settings', +console.log('%c🛡️ HV Unified v' + VERSION + ' Bridge & Advisory Mode%c | ,=settings', 'color:#fdcb00;font-weight:bold', ''); -console.log('%c HV.advice() for guidance | HV.difficulty() for difficulty check', +console.log('%c HV.analyzeGear() for gear scoring | HV.advice() for build guidance | HV.difficulty() for difficulty check', 'color:#888;font-size:10px'); })(); + diff --git a/src/keybindings.js b/src/keybindings.js index a3357fd..3611d4c 100644 --- a/src/keybindings.js +++ b/src/keybindings.js @@ -3,14 +3,13 @@ // ═══════════════════════════════════════════════════════════════════════ let keybindingsSetup = false; -const Q_DEBOUNCE_MS = 300; // Ignore Q repeats faster than this function setupKeybindings() { if (keybindingsSetup) return; keybindingsSetup = true; document.addEventListener('keydown', function(e) { - // Settings — comma key + // Settings — comma key (non-conflicting with Monsterbation) if (e.keyCode === KEYS.COMMA) { if (isBattlePage()) { e.preventDefault(); @@ -19,90 +18,8 @@ function setupKeybindings() { return; } } - - // Main action hotkey (default: Q) - if (e.code === CFG.hotkey && !e.ctrlKey && !e.altKey && !e.metaKey) { - if (!isInputFocused() && isBattlePage()) { - // Debounce: ignore key-repeats until game processes the action - const now = Date.now(); - if (now - (STATE._lastActionTime || 0) < Q_DEBOUNCE_MS) return; - STATE._lastActionTime = now; - - console.log('%c[HV] Q pressed — checking action...', 'color:#888'); - parseBattleState(); - // Debug: show full battle state - const activeBuffs = Object.entries(STATE.buffs) - .filter(([k, v]) => v > 0 && k !== 'regen' || v > 0) - .map(([k, v]) => `${k}=${v}`).join(' '); - // Monster SP bars (threat levels) with alive/dead markers - const threatLevels = STATE.monsters.map((m, i) => { - const sp = STATE.monsterSp[i] || 0; - const alive = m && m.hasAttribute && m.hasAttribute('onclick') ? 'A' : 'D'; - return `${i}:${sp}${alive}`; - }).join(' '); - // Skill cooldowns — check DOM for opacity/onclick on skill elements - const skillCDs = ['Rending Blow', 'Shatter Strike', 'Great Cleave'] - .filter(s => STATE.skillsKnown.includes(s)) - .map(s => { - const el = document.querySelector(`.btqs[onmouseover*="set_infopane_spell('${s}'"], .btsd[onmouseover*="set_infopane_spell('${s}'"]`); - const ready = el && el.hasAttribute('onclick'); - return `${s.charAt(0)}${ready ? 'R' : 'C'}`; - }).join(''); - const curMp = parseInt((document.getElementById('vrm') || {}).textContent) || 0; - const curHp = parseInt((document.getElementById('vrhd') || {}).textContent) || 0; - // Item counts from itemsKnown - const itemTypes = {}; - Object.values(STATE.itemsKnown).forEach(info => { - const def = ITEMS[parseInt(info)]; - if (def) itemTypes[def.t] = (itemTypes[def.t] || 0) + 1; - }); - const itemStr = ['heal','spirit','mana'].filter(t => itemTypes[t]).map(t => `${t.charAt(0)}${itemTypes[t]}`).join(' '); - console.log( - `%c[HV] ch=${STATE.channeling} hp=${curHp} mp=${curMp}(${(STATE.mp*100).toFixed(0)}%) ` + - `sp=${(STATE.sp*100).toFixed(0)}% oc=${STATE.oc.toFixed(0)} ` + - `mon:${STATE.monsters.length} sk:${skillCDs || '--'} ` + - `${itemStr} ` + - `buffs: ${activeBuffs} | spBars: ${threatLevels}`, - 'color:#888' - ); - const a = getRecommendedAction(); - if (a) { - e.preventDefault(); - // Show explicit action details - let label = ''; - if (a.type === 'spell') { - label = a.name + (a.target >= 0 ? ' → monster ' + a.target : ' (self)'); - } else if (a.type === 'skill') { - label = a.name + ' → monster ' + a.target; - } else if (a.type === 'item') { - const itemDef = ITEMS[parseInt(STATE.itemsKnown[a.id] || '0')]; - label = (itemDef ? itemDef.n : a.id) + (a.selfTarget ? ' (self)' : ''); - } else if (a.type === 'attack') { - label = 'monster ' + a.target; - } else { - label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id; - } - console.log(`%c[HV] ▶ ${a.type} → ${label}${a._reason ? ' (' + a._reason + ')' : ''}`, 'color:#0f0'); - executeAction(a); - } - } - } - - // Hover toggle (H) - if (e.code === 'KeyH' && !e.ctrlKey && !e.altKey && !e.metaKey && !isInputFocused()) { - STATE.interruptHover = !STATE.interruptHover; - console.log(`%c[HV] Hover ${STATE.interruptHover ? 'OFF' : 'ON'}`, 'color:#f80'); - } - - // Emergency heal (C) — always cast Cure/Full-Cure regardless of strategy - if (e.code === 'KeyC' && !e.ctrlKey && !e.altKey && !e.metaKey && !isInputFocused()) { - const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null; - if (c) { - console.log(`%c[HV] 🚑 Emergency: casting ${c}`, 'color:#f44'); - castSelfSpell(c); - } - } }); - console.log('%c⌨ [HV] Keys: Q=action H=hover C=cure ,=settings', 'color:#0f0;font-size:11px'); + console.log('%c⌨ [HV] Key: ,=settings (Battle input managed by Monsterbation)', 'color:#0f0;font-size:11px'); } + diff --git a/src/out-of-battle.js b/src/out-of-battle.js index e1b70b5..4b26612 100644 --- a/src/out-of-battle.js +++ b/src/out-of-battle.js @@ -130,38 +130,7 @@ function enhanceEquipShopWithAdvice() { // ── Shrine: bulk selection ── function enhanceShrine() { - if (!CFG.bulkShrine) return; - const m = document.getElementById('mainpane'); - if (!m || document.getElementById('hv-bulk-shrine')) return; - - const r = document.createElement('div'); - r.id = 'hv-bulk-shrine'; - r.style.cssText = 'margin:6px;display:flex;gap:6px'; - - const b1 = document.createElement('input'); - b1.type = 'button'; - b1.value = '⛩️ Select Non-Figurines'; - b1.style.cssText = 'padding:4px 10px;cursor:pointer;background:#fdcb00;color:#000;border:none;border-radius:3px;font-size:12px'; - b1.onclick = () => { - $$('tr', m).forEach(row => { - const t = (row.textContent || ''); - if (!t.includes('Figurine') && !t.includes('Collectable')) { - const cb = row.querySelector('input[type="checkbox"]'); - if (cb) cb.checked = true; - } - }); - }; - - const b2 = document.createElement('input'); - b2.type = 'button'; - b2.value = '✗ Clear'; - b2.style.cssText = 'padding:4px 10px;cursor:pointer;background:#666;color:#fff;border:none;border-radius:3px;font-size:12px'; - b2.onclick = () => { $$('input[type="checkbox"]', m).forEach(cb => cb.checked = false); }; - - r.appendChild(b1); - r.appendChild(b2); - const ta = m.querySelector('div'); - if (ta) ta.insertBefore(r, ta.firstChild); + // Shrine enhancements are owned by HVUT. } // ── Training: priority guide ── @@ -180,103 +149,11 @@ function enhanceTraining() { if (ta) ta.insertBefore(d, ta.firstChild); } -// ── Monster Lab: floating info panel + feed buttons ── - -let _mlDragOffset = { x: 0, y: 0 }; - function enhanceMonsterLab() { - // Create floating draggable panel - if (document.getElementById('hv-monster')) return; - - const panel = document.createElement('div'); - panel.id = 'hv-monster'; - panel.style.cssText = css({ - position: 'fixed', - top: '80px', - right: '4px', - zIndex: '9999', - background: '#111827', - color: '#d1d5db', - padding: '8px 10px', - borderRadius: '8px', - fontSize: '10px', - fontFamily: 'monospace', - maxWidth: '320px', - boxShadow: '0 0 15px rgba(0,0,0,0.6)', - border: '1px solid #374151', - cursor: 'move', - userSelect: 'none', - }); - - const body = `
- 🧬 Monster Lab - -
-
-
- - -
-
- New to ML? Pick a Beast-class (Dog/Cow/Pig) for STR/END crystals.
- Click + drag header to move. -
-
`; - - panel.innerHTML = body; - document.body.appendChild(panel); - - // ── Feed All ── - panel.querySelector('#hv-feed-all').onclick = () => { - const m = document.getElementById('mainpane'); - if (m) $$('input[value="Feed"]', m).forEach(b => b.click()); - }; - - // ── Happy Pills All ── - panel.querySelector('#hv-pill-all').onclick = () => { - const m = document.getElementById('mainpane'); - if (m) { - const pills = $$('option', m).filter(o => (o.textContent || '').includes('Happy Pill')); - if (pills.length > 0) { - pills[0].selected = true; - $$('input[value="Feed"]', m).forEach(b => b.click()); - } - } - }; - - // ── Toggle ── - panel.querySelector('#hv-monster-toggle').onclick = () => { - const body = panel.querySelector('#hv-monster-body'); - const isHidden = body.style.display === 'none'; - body.style.display = isHidden ? 'block' : 'none'; - panel.querySelector('#hv-monster-toggle').textContent = isHidden ? '−' : '+'; - }; - - // ── Make draggable ── - const header = panel.querySelector('#hv-monster-header'); - if (header) { - header.addEventListener('mousedown', (e) => { - _mlDragOffset.x = e.clientX - panel.getBoundingClientRect().left; - _mlDragOffset.y = e.clientY - panel.getBoundingClientRect().top; - const onMove = (ev) => { - panel.style.left = (ev.clientX - _mlDragOffset.x) + 'px'; - panel.style.right = 'auto'; - panel.style.top = (ev.clientY - _mlDragOffset.y) + 'px'; - }; - const onUp = () => { - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onUp); - }; - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onUp); - }); - } - - // Remove the old inline bar if it exists - const oldBar = document.getElementById('hv-lab-enhance'); - if (oldBar) oldBar.remove(); + // Monster Lab enhancements are owned by HVUT. } + // ── Config button (bottom-right) ── function addConfigButton() { diff --git a/src/re-timer.js b/src/re-timer.js index 89aed29..cbb3f37 100644 --- a/src/re-timer.js +++ b/src/re-timer.js @@ -1,58 +1,7 @@ // ═══════════════════════════════════════════════════════════════════════ -// RE TIMER — Random Encounter countdown +// RE TIMER — REMOVED (Handled by HVUtils / HVUT) // ═══════════════════════════════════════════════════════════════════════ -let reTimerEl = null; +function setupRETimer() {} +function updateRETimer() {} -function setupRETimer() { - if (!CFG.reTimer) return; - if (document.getElementById('hv-re-timer')) return; - - reTimerEl = document.createElement('div'); - reTimerEl.id = 'hv-re-timer'; - reTimerEl.style.cssText = css({ - position: 'fixed', - top: '2px', - left: '50%', - transform: 'translateX(-50%)', - zIndex: '99999', - background: '#1a1a2e', - color: '#0f0', - padding: '2px 10px', - borderRadius: '0 0 6px 6px', - fontSize: '10px', - fontFamily: 'monospace', - opacity: '0.9', - }); - reTimerEl.textContent = 'RE: --:--'; - document.body.appendChild(reTimerEl); - updateRETimer(); - setInterval(updateRETimer, 30000); -} - -function updateRETimer() { - if (!reTimerEl) return; - const now = Date.now(); - const dt = document.body.textContent || ''; - - // Detect dawn - if (dt.includes('Dawn of a New Day') || dt.includes('A new day dawns')) { - try { localStorage[SP + 'lastDawn'] = JSON.stringify(now); } catch (e) {} - } - - const ld = JSON.parse(localStorage[SP + 'lastDawn'] || now); - const ms = (now - ld) / 60000; - const nr = 30 - (ms % 30); - - if (nr < 1) { - reTimerEl.textContent = '⚡ RE READY!'; - reTimerEl.style.color = '#f00'; - reTimerEl.style.fontWeight = 'bold'; - } else { - const mn = Math.floor(nr); - const sc = Math.floor((nr - mn) * 60); - reTimerEl.textContent = `RE: ${String(mn).padStart(2, '0')}:${String(sc).padStart(2, '0')}`; - reTimerEl.style.color = '#0f0'; - reTimerEl.style.fontWeight = 'normal'; - } -} diff --git a/src/settings-panel.js b/src/settings-panel.js index 17e2576..4193118 100644 --- a/src/settings-panel.js +++ b/src/settings-panel.js @@ -51,41 +51,29 @@ function openSettings() { p.innerHTML = `
- ⚙ HV Unified v${VERSION} + ⚙ HV Unified v${VERSION} (Bridge & Advisor)
- Battle - ${mkTog('Auto-buff (Haste, Protection, etc.)', 'autoBuff')} - ${mkTog('Auto-debuff (Imperil, Weaken)', 'autoDebuff')} - ${mkTog('Auto-cure when HP low', 'autoCure')} - ${mkTog('Auto Spirit Stance', 'autoSpirit')} - ${mkTog('Hover-to-attack mode', 'hoverEnabled')} + Strategy Advisory + ${mkTog('Auto-buff recommendations', 'autoBuff')} + ${mkTog('Auto-debuff recommendations', 'autoDebuff')} + ${mkTog('Auto-cure recommendations', 'autoCure')} ${mkTog('Auto-difficulty suggestion', 'autoDifficulty')} - ${mkTog('Use attack spells', 'useAttackSpells')} + ${mkTog('Use attack spells in advice', 'useAttackSpells')} ${mkNum('Cure HP threshold', 'cureHP')} ${mkNum('Item HP threshold', 'cureItemHP')} ${mkNum('Mana gem MP threshold', 'manaGemMP')} ${mkNum('Mana potion MP threshold', 'manaPotionMP')} - ${mkNum('Spirit Stance OC%', 'spiritStanceOC', 5)} + ${mkNum('Skill OC% threshold', 'skillOC', 5)}
- Out of Battle - ${mkTog('Equip quick sell buttons', 'autoSell')} - ${mkTog('Bulk Shrine buttons', 'bulkShrine')} - ${mkTog('RE Timer', 'reTimer')} + Out of Battle & Guidance ${mkTog('Training queue info', 'trainingQueue')} ${mkTog('Advisor panel', 'showGuidance')} ${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')} -
- UI - ${mkTog('Cooldown timers', 'showCooldowns')} - ${mkTog('Monster HP numbers', 'showMonsterHP')} - ${mkTog('Buff duration counters', 'showDurations')} - ${mkTog('Alert colours', 'alertColours')} - ${mkTog('Monster numbers', 'showMonsterNumbers')} ${mkTog('Config button', 'cfgButton')}
- Changes apply immediately. Press , in battle to open settings. + Changes apply immediately. Press , to open settings.
`; document.body.appendChild(p); @@ -103,3 +91,4 @@ function openSettings() { }); }); } + diff --git a/src/strategy-engine.js b/src/strategy-engine.js index c931b2d..990ee05 100644 --- a/src/strategy-engine.js +++ b/src/strategy-engine.js @@ -444,28 +444,12 @@ function strategyNovice() { return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' }; } - // 4. Mana/spirit items — AFTER buffs (Novice) + // 4. Mana items — AFTER buffs (Novice) if (STATE.mp < CFG.manaGemMP) { const gem = hasUsableGem('mana'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } - // Spirit gem (P-slot) — auto-consume when available - const pGem = document.getElementById('ikey_p'); - const pGemId = pGem ? (pGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; - if (pGemId && pGemId[1] === '10007') { - if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true }; - } - // Spirit draughts/potions — only when SP drops below threshold - if (STATE.sp < CFG.spiritPotionSP) { - // Check non-P-slot spirit items only - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') { - return { type: 'item', id: slot, selfTarget: true }; - } - } - } + // 5. Weapon skills — these are better than basic attacks const skillOC = CFG.skillOC || 80; @@ -529,20 +513,6 @@ function strategyAdept() { if (gem && !STATE.channeling && !STATE._mysticUsed) return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' }; - // 0b. Spark of Life SP safety - if (STATE.sp < 0.55) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; - } - } - - // 0c. Spirit Stance safety auto-disable - if (STATE.spiritStance && STATE.sp < 0.60) { - return { type: 'toggle_spirit', _reason: 'sp_safety' }; - } - // 1. Health items — safe to use early if (STATE.hp < CFG.cureItemHP) { const gem = hasUsableGem('heal'); @@ -555,15 +525,6 @@ function strategyAdept() { if (c) return { type: 'spell', name: c, selfTarget: true }; } - // 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so - // Spark can actually save us. - if (STATE.sp < 0.55) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true }; - } - } // 3. Buffs // On first round, cast cheapest-first to proc channeling @@ -592,25 +553,12 @@ function strategyAdept() { return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' }; } - // 4. Mana/spirit items — AFTER buffs (Adept) + // 4. Mana items — AFTER buffs (Adept) if (STATE.mp < CFG.manaGemMP) { const gem = hasUsableGem('mana'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } - // Spirit gem (P-slot) — auto-consume when available - const apGem = document.getElementById('ikey_p'); - const apGemId = apGem ? (apGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; - if (apGemId && apGemId[1] === '10007') { - if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true }; - } - // Spirit draughts/potions — only at SP < threshold - if (STATE.sp < CFG.spiritPotionSP) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true }; - } - } + // 4. Debuffs — conservative: Imperil on bosses/rares or 5+ mobs // Weaken only on high-threat or bosses. Estoc handles <5. @@ -707,45 +655,6 @@ function strategyVeteran() { if (gem && !STATE.channeling && !STATE._mysticUsed) return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' }; - // 0b. Spark of Life SP safety — Spark costs 50% SP when triggered. - // If SP < 60%, consume a spirit item before anything else. - if (STATE.sp < 0.60) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; - } - } - - // 0c. Spirit Stance safety auto-disable — if left on, drains 10% SP/turn - if (STATE.spiritStance && STATE.sp < 0.60) { - return { type: 'toggle_spirit', _reason: 'sp_safety' }; - } - - // 0d. Spirit Stance for burst clear — toggle on for 7+ mob, off after one skill - // Gated by CFG.autoSpirit (default OFF — manual toggle only) - const is2H = STATE.skillsKnown.includes('Great Cleave') || STATE.skillsKnown.includes('Rending Blow'); - if (CFG.autoSpirit && is2H && STATE.monsters && STATE.monsters.length >= 7 && STATE.oc >= (CFG.skillOC || 60)) { - if (STATE._spiritForSkill >= 1 && STATE.spiritStance) { - if (STATE._spiritForSkill === 1) { - STATE._spiritForSkill = 2; - } else if (STATE._spiritForSkill === 2) { - STATE._spiritForSkill = 0; - return { type: 'toggle_spirit', _reason: 'spirit_off_7' }; - } - } else if (!STATE.spiritStance && STATE._spiritForSkill === 0) { - STATE._spiritForSkill = 1; - return { type: 'toggle_spirit', _reason: 'spirit_on_7' }; - } - } - // Toggle-off guard: if flag says skill was cast, toggle off even if mob count dropped - if (STATE._spiritForSkill === 2 && STATE.spiritStance) { - STATE._spiritForSkill = 0; - return { type: 'toggle_spirit', _reason: 'spirit_off_7' }; - } - // Reset flag if spirit got turned off externally - if (!STATE.spiritStance) STATE._spiritForSkill = 0; - // 0e. HIGH-THREAT WEAPON SKILLS — only when 3+ enemies // Skip if ≤2 enemies — basic attacks build OC for next wave instead. const SINGLE_THREAT = Math.round(120 * 90 / 100); @@ -770,16 +679,6 @@ function strategyVeteran() { } } - // 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so - // Spark can actually save us. Use spirit draughts/potions proactively. - if (STATE.sp < 0.55) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; - } - } - // 2c. OC-building priority: when 5+ monsters, OC < 60, and AoE skills ready, // skip debuffs/non-essential buffs and basic attack to build OC fast. const needsOC = STATE.monsters && STATE.monsters.length >= 4 @@ -788,26 +687,21 @@ function strategyVeteran() { && document.querySelector(`.btqs[onmouseover*="set_infopane_spell('Rending Blow'"], .btsd[onmouseover*="set_infopane_spell('Rending Blow'"]`) ?.hasAttribute('onclick'); if (needsOC) { - // No hp/mp guard — if Spark procced, hp=0 but we still need OC const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : findOptimalDominoTarget(); if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) return { type: 'attack', target: t, _reason: 'oc_build' }; } // 3. Buffs - // On first round, cast cheapest-first to proc channeling if (CFG.autoBuff) { { const ib = castInitialBuffs(); if (ib) return ib; } - // If channeling is active, prioritize most expensive buff for best value if (STATE.channeling && STATE.mp > 0.10) { const ch = findBestChannelingTarget(); if (ch) return ch; } - // Collect available buff candidates and sort by cost - // No channeling → cheapest first (proc chance), channeling → most expensive (MP value) const buffCandidates = []; for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) { @@ -822,35 +716,19 @@ function strategyVeteran() { } } - // 4. Mana/spirit items — AFTER buffs (Novice) + // 4. Mana items if (STATE.mp < CFG.manaGemMP) { const gem = hasUsableGem('mana'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } - // Spirit gem (P-slot) — use immediately - const vpGem = document.getElementById('ikey_p'); - const vpGemId = vpGem ? (vpGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; - if (vpGemId && vpGemId[1] === '10007') { - if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true }; - } - // Spirit draughts/potions — only at SP < threshold - if (STATE.sp < CFG.spiritPotionSP) { - for (const [slot, info] of Object.entries(STATE.itemsKnown)) { - if (slot === 'p') continue; - const def = ITEMS[parseInt(info)]; - if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true }; - } - } - // 4. Debuffs — only when NOT in spirit burst mode (flag >= 1 means skill pending) - if (STATE._spiritForSkill === 0 && CFG.autoDebuff && STATE.mp > 0.35) { - // Imperil: bosses only, or 5+ mobs in Grindfest (Estoc handles <5) + // 4. Debuffs + if (CFG.autoDebuff && STATE.mp > 0.35) { if (hasReadySpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 5)) { const b = findStrongestMonster(); if (b >= 0 && !checkMonsterDebuff(b, 'imperil')) return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' }; } - // Weaken: only on monsters about to use special attacks (high SP) or bosses if (hasReadySpell('Weaken') && (anyRareMonster() || hasHighThreat(THREAT_PX))) { const b = findDangerousMonster(); if (b >= 0 && !checkMonsterDebuff(b, 'weaken')) @@ -859,20 +737,16 @@ function strategyVeteran() { } // 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar) - // If 2+ enemies have >85% SP bar, they're about to special — emergency const isHighThreat = hasHighThreat(THREAT_PX); if (isHighThreat) console.log('%c[HV] ⚠ HIGH THREAT: 2+ monsters with SP >85%', 'color:#f44'); const askillOC3 = CFG.skillOC || 80; if (STATE.oc >= askillOC3 || (isHighThreat && STATE.monsters.length >= 3)) { if (isHighThreat && STATE.oc < askillOC3) console.log('%c[HV] ⚠ emergency: using skill at OC=' + STATE.oc.toFixed(0), 'color:#f80'); - // Emergency: 2+ monsters about to special — spend OC to kill them fast - // Rending Blow: AoE armor pen vs 3+ enemies. Target center monster for max 5-target splash. if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) { const t = isHighThreat ? findDangerousMonster() : findOptimalDominoTarget(); if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' }; } - // Shatter Strike: AoE stun vs 3+ enemies with armor break if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) { const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor')); if (hasArmorBreak) { @@ -880,12 +754,10 @@ function strategyVeteran() { if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t, _reason: 'stun' }; } } - // Great Cleave: boss/rare fights or single-target threat if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = isHighThreat ? findDangerousMonster() : findStrongestMonster(); if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t, _reason: isHighThreat ? 'threat' : 'boss' }; } - // If high threat and 3+ enemies, use single-target skills on the most dangerous if (isHighThreat && STATE.oc >= (CFG.skillOC || 60) && STATE.monsters.length >= 3) { const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow', 'Vital Strike', 'Shield Bash', 'Concussive Strike']; @@ -898,6 +770,7 @@ function strategyVeteran() { } } + // 5. Spirit gem auto-consume const spGem = document.getElementById('ikey_p'); const spGemId = spGem ? (spGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; diff --git a/src/ui-overlays.js b/src/ui-overlays.js index a0c66be..6a80013 100644 --- a/src/ui-overlays.js +++ b/src/ui-overlays.js @@ -1,132 +1,12 @@ // ═══════════════════════════════════════════════════════════════════════ -// UI OVERLAYS — cooldowns, durations, alerts, monster numbers, monster HP +// UI OVERLAYS — REMOVED (Handled by Monsterbation / MB) // ═══════════════════════════════════════════════════════════════════════ -function updateCooldownDisplay() { - if (!CFG.showCooldowns) return; - $$('.hv-cd-overlay').forEach(e => e.remove()); - $$('.btqb').forEach(el => { - const img = el.querySelector('img'); - if (!img) return; - const src = (img.src || '').toLowerCase(); - for (const [k, v] of Object.entries(STATE.cooldowns)) { - if (v <= 0) continue; - if (src.includes(k.toLowerCase()) || src.includes(k.replace(/_/g, ''))) { - const o = document.createElement('div'); - o.className = 'hv-cd-overlay'; - o.style.cssText = 'position:absolute;top:0;right:0;background:rgba(0,0,0,0.7);color:#f00;font-size:9px;font-weight:bold;padding:0 2px;border-radius:2px;pointer-events:none'; - o.textContent = v; - el.style.position = 'relative'; - el.appendChild(o); - break; - } - } - }); -} +function updateCooldownDisplay() {} +function updateDurationDisplay() {} +function updateAlertColours() {} +function addMonsterNumbers() {} +function updateMonsterHPDisplay() {} +function saveMonsterData() {} +function refreshUI() {} -function updateDurationDisplay() { - if (!CFG.showDurations) return; - $$('.hv-dur-overlay').forEach(e => e.remove()); - const pane = document.getElementById('pane_effects'); - if (!pane) return; - $$('img[onmouseover]', pane).forEach(img => { - const omo = img.getAttribute('onmouseover') || ''; - const dur = omo.match(/(\d+)\s*turns?\s*rem/); - if (!dur) return; - const t = parseInt(dur[1]); - const st = omo.match(/(\d+)\s*stacks?/i); - const o = document.createElement('div'); - o.className = 'hv-dur-overlay'; - o.style.cssText = `position:absolute;bottom:-2px;left:50%;transform:translateX(-50%);background:${t < 3 ? '#f44' : '#44f'};color:#fff;font-size:8px;font-weight:bold;padding:0 2px;border-radius:2px;pointer-events:none;z-index:1`; - o.textContent = st ? `${t}x${parseInt(st[1])}` : t; - img.parentElement.style.position = 'relative'; - img.parentElement.appendChild(o); - }); -} - -function updateAlertColours() { - if (!CFG.alertColours) return; - let bg = '#EDEBDF'; - STATE.interruptAlert = false; - - if (!$('img[src$="bar_dgreen.png"]') && $('img[src$="fallenshield.png"]')) { - bg = 'magenta'; - STATE.interruptAlert = true; - } else if (STATE.hp < 0.25) { - bg = '#ff4488'; - STATE.interruptAlert = true; - } else if (STATE.mp < 0.15) { - bg = '#4444aa'; - } else if (Object.values(STATE.buffs).some(d => d < 2 && d > 0)) { - bg = '#88ccff'; - } - - const v = document.getElementById('pane_vitals'); - if (v) v.style.background = bg; -} - -function addMonsterNumbers() { - if (!CFG.showMonsterNumbers) return; - STATE.monsters.forEach((m, i) => { - if (!m || !m.querySelector || m.querySelector('.hv-monster-num')) return; - const b = m.querySelector('.btm1'); - if (!b) return; - const s = document.createElement('span'); - s.className = 'hv-monster-num'; - s.style.cssText = 'font-weight:bold;margin-right:3px;color:#999'; - s.textContent = (i + 1); - b.insertBefore(s, b.firstChild); - }); -} - -function updateMonsterHPDisplay() { - if (!CFG.showMonsterHP) return; - STATE.monsters.forEach((m, i) => { - if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return; - const n = m.querySelector('.btm1'); - if (!n) return; - const nm = (n.textContent || '').replace(/^\d+\s*/, '').trim(); - const md = STATE.monsterData[nm]; - if (!md || !md.hp || md.seen < 3) return; - const hpBar = m.querySelector('img[src$="nbargreen.png"]'); - if (!hpBar) return; - const r = clamp(parseInt(hpBar.style.width || '') / 120, 0.01, 1); - const txt = `HP: ${Math.max(1, Math.round(r * md.hp)).toLocaleString()} / ${md.hp.toLocaleString()}`; - const ex = m.querySelector('.hv-monster-hp'); - if (ex) { ex.textContent = txt; return; } - const d = document.createElement('div'); - d.className = 'hv-monster-hp'; - d.style.cssText = 'display:inline-block;position:relative;left:204px;top:-20px;font-size:9px;color:#888'; - d.textContent = txt; - m.appendChild(d); - }); -} - -function saveMonsterData() { - STATE.monsters.forEach((m, i) => { - if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return; - const n = m.querySelector('.btm1'); - if (!n) return; - const nm = (n.textContent || '').replace(/^\d+\s*/, '').trim(); - if (!nm) return; - const hpBar = m.querySelector('img[src$="nbargreen.png"]'); - if (hpBar) { - const r = clamp(parseInt(hpBar.style.width || '') / 120, 0.01, 1); - if (!STATE.monsterData[nm]) STATE.monsterData[nm] = { hp: 0, seen: 0 }; - STATE.monsterData[nm].hp = Math.max( - STATE.monsterData[nm].hp, - Math.round(STATE.monsterData[nm].hp * 0.7 + 1000 / r * 0.3) - ); - STATE.monsterData[nm].seen++; - } - }); - try { localStorage[SP + 'monsters'] = JSON.stringify(STATE.monsterData); } catch (e) {} -} - -function refreshUI() { - if (CFG.showDurations) updateDurationDisplay(); - if (CFG.showCooldowns) updateCooldownDisplay(); - if (CFG.alertColours) updateAlertColours(); - if (CFG.showMonsterNumbers) addMonsterNumbers(); - if (CFG.showMonsterHP) updateMonsterHPDisplay(); -}