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 = `
- `;
-
- 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 = `
- `;
-
- 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 = `
- `;
-
- 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();
-}