v0.13.22 - Cache base MP costs at battle start (dynamic, not hardcoded)

- Removed SPELL_COSTS hardcoded table
- Base MP costs are now read from the magic pane's onmouseover
  tooltip at battle init and cached in STATE._baseMpCosts
- Cache is populated during parseBattleState() and persists
  for the entire battle, avoiding channeling's 1-MP display
- Cache resets on new battle, so level-ups or stat changes
  are picked up when starting a new arena
This commit is contained in:
GaboGG 2026-07-26 15:08:23 -04:00
parent 80fc77600a
commit 31799a7261
8 changed files with 78 additions and 45 deletions

View file

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name HV Unified // @name HV Unified
// @namespace hvunified // @namespace hvunified
// @version 0.13.21 // @version 0.13.22
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
// @author GaboGG + Hermes // @author GaboGG + Hermes
// @match *://*.hentaiverse.org/* // @match *://*.hentaiverse.org/*
@ -17,7 +17,7 @@
// CONFIG — default settings // CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.13.21'; const VERSION = '0.13.22';
const CFG = { const CFG = {
// — Battle automation // — Battle automation
@ -79,6 +79,8 @@ const STATE = {
_mysticUsed: false, // Track if Mystic Gem has been used this battle _mysticUsed: false, // Track if Mystic Gem has been used this battle
_baseMpCosts: {}, // Cached base MP costs for buff spells (read from magic pane at battle start)
maxBuffDur: {}, // Tracks highest duration seen per buff icon maxBuffDur: {}, // Tracks highest duration seen per buff icon
hoverTarget: -1, hoverTarget: -1,
@ -433,7 +435,13 @@ function parseBattleState() {
const omo = el.getAttribute('onmouseover') || ''; const omo = el.getAttribute('onmouseover') || '';
if (!omo) return; if (!omo) return;
const m = omo.match(/battle\.set_infopane_spell\('([^']+)',\s*'[^']*',\s*'[^']*',\s*(\d+),\s*(\d+),\s*(\d+)\)/); const m = omo.match(/battle\.set_infopane_spell\('([^']+)',\s*'[^']*',\s*'[^']*',\s*(\d+),\s*(\d+),\s*(\d+)\)/);
if (m) STATE.spellsKnown.push({ n: m[1], mp: parseInt(m[2]), chr: parseInt(m[3]), cd: parseInt(m[4]) }); if (m) {
STATE.spellsKnown.push({ n: m[1], mp: parseInt(m[2]), chr: parseInt(m[3]), cd: parseInt(m[4]) });
// Cache the base MP cost (from magic pane, unaffected by channeling/Conservation)
if (!STATE._baseMpCosts[m[1]]) {
STATE._baseMpCosts[m[1]] = parseInt(m[2]);
}
}
}); });
} }
@ -915,19 +923,21 @@ function findBestChannelingTarget() {
const dur = buffDuration(b.icon); const dur = buffDuration(b.icon);
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
const sp = findSpell(b.spell); const sp = findSpell(b.spell);
// Read base MP cost from the magic pane tooltip (always the full base cost, // Use cached base MP cost (snapshotted at battle start from the magic pane)
// unaffected by channeling's 1-MP display or Mana Conservation) // This avoids the channeling 1-MP display issue and Mana Conservation fluctuation
let baseCost = 0; let mpCost = STATE._baseMpCosts[b.spell] || 0;
if (mpCost === 0 && sp) {
// Not cached yet — read from magic pane directly
const paneEl = $$('.btsd[onmouseover*="' + b.spell.replace(/'/g, "\\'") + '"]', const paneEl = $$('.btsd[onmouseover*="' + b.spell.replace(/'/g, "\\'") + '"]',
document.getElementById('pane_magic'))[0]; document.getElementById('pane_magic'))[0];
if (paneEl) { if (paneEl) {
const costM = (paneEl.getAttribute('onmouseover') || '') const costM = (paneEl.getAttribute('onmouseover') || '')
.match(/set_infopane_spell\('[^']+',\s*'[^']*',\s*'[^']*',\s*(\d+)/); .match(/set_infopane_spell\('[^']+',\s*'[^']*',\s*'[^']*',\s*(\d+)/);
if (costM) baseCost = parseInt(costM[1]); if (costM) mpCost = parseInt(costM[1]);
}
if (mpCost === 0) mpCost = sp.mp; // last resort: use what parseBattleState gave us
STATE._baseMpCosts[b.spell] = mpCost;
} }
let mpCost = sp ? sp.mp : 0;
// Use base cost from DOM instead of Mana Conservation-reduced cost
if (baseCost > 0) mpCost = baseCost;
// Skip if we can't afford the base cost — game pre-checks even when // Skip if we can't afford the base cost — game pre-checks even when
// channeling makes the effective cost 1 MP // channeling makes the effective cost 1 MP
const maxMp = parseInt((document.getElementById('vrhd') || {}).textContent) || 414; const maxMp = parseInt((document.getElementById('vrhd') || {}).textContent) || 414;
@ -3317,6 +3327,7 @@ function initializeBattle() {
STATE.page = 'battle'; STATE.page = 'battle';
STATE.inBattle = true; STATE.inBattle = true;
STATE._mysticUsed = false; // Reset Mystic Gem tracking on new battle STATE._mysticUsed = false; // Reset Mystic Gem tracking on new battle
STATE._baseMpCosts = {}; // Reset base cost cache
parseBattleState(); parseBattleState();
// Force tier update from cached level (battle page may not have level_readout) // Force tier update from cached level (battle page may not have level_readout)

View file

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name HV Unified // @name HV Unified
// @namespace hvunified // @namespace hvunified
// @version 0.13.21 // @version 0.13.22
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
// @author GaboGG + Hermes // @author GaboGG + Hermes
// @match *://*.hentaiverse.org/* // @match *://*.hentaiverse.org/*
@ -17,7 +17,7 @@
// CONFIG — default settings // CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.13.21'; const VERSION = '0.13.22';
const CFG = { const CFG = {
// — Battle automation // — Battle automation
@ -79,6 +79,8 @@ const STATE = {
_mysticUsed: false, // Track if Mystic Gem has been used this battle _mysticUsed: false, // Track if Mystic Gem has been used this battle
_baseMpCosts: {}, // Cached base MP costs for buff spells (read from magic pane at battle start)
maxBuffDur: {}, // Tracks highest duration seen per buff icon maxBuffDur: {}, // Tracks highest duration seen per buff icon
hoverTarget: -1, hoverTarget: -1,
@ -433,7 +435,13 @@ function parseBattleState() {
const omo = el.getAttribute('onmouseover') || ''; const omo = el.getAttribute('onmouseover') || '';
if (!omo) return; if (!omo) return;
const m = omo.match(/battle\.set_infopane_spell\('([^']+)',\s*'[^']*',\s*'[^']*',\s*(\d+),\s*(\d+),\s*(\d+)\)/); const m = omo.match(/battle\.set_infopane_spell\('([^']+)',\s*'[^']*',\s*'[^']*',\s*(\d+),\s*(\d+),\s*(\d+)\)/);
if (m) STATE.spellsKnown.push({ n: m[1], mp: parseInt(m[2]), chr: parseInt(m[3]), cd: parseInt(m[4]) }); if (m) {
STATE.spellsKnown.push({ n: m[1], mp: parseInt(m[2]), chr: parseInt(m[3]), cd: parseInt(m[4]) });
// Cache the base MP cost (from magic pane, unaffected by channeling/Conservation)
if (!STATE._baseMpCosts[m[1]]) {
STATE._baseMpCosts[m[1]] = parseInt(m[2]);
}
}
}); });
} }
@ -915,19 +923,21 @@ function findBestChannelingTarget() {
const dur = buffDuration(b.icon); const dur = buffDuration(b.icon);
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
const sp = findSpell(b.spell); const sp = findSpell(b.spell);
// Read base MP cost from the magic pane tooltip (always the full base cost, // Use cached base MP cost (snapshotted at battle start from the magic pane)
// unaffected by channeling's 1-MP display or Mana Conservation) // This avoids the channeling 1-MP display issue and Mana Conservation fluctuation
let baseCost = 0; let mpCost = STATE._baseMpCosts[b.spell] || 0;
if (mpCost === 0 && sp) {
// Not cached yet — read from magic pane directly
const paneEl = $$('.btsd[onmouseover*="' + b.spell.replace(/'/g, "\\'") + '"]', const paneEl = $$('.btsd[onmouseover*="' + b.spell.replace(/'/g, "\\'") + '"]',
document.getElementById('pane_magic'))[0]; document.getElementById('pane_magic'))[0];
if (paneEl) { if (paneEl) {
const costM = (paneEl.getAttribute('onmouseover') || '') const costM = (paneEl.getAttribute('onmouseover') || '')
.match(/set_infopane_spell\('[^']+',\s*'[^']*',\s*'[^']*',\s*(\d+)/); .match(/set_infopane_spell\('[^']+',\s*'[^']*',\s*'[^']*',\s*(\d+)/);
if (costM) baseCost = parseInt(costM[1]); if (costM) mpCost = parseInt(costM[1]);
}
if (mpCost === 0) mpCost = sp.mp; // last resort: use what parseBattleState gave us
STATE._baseMpCosts[b.spell] = mpCost;
} }
let mpCost = sp ? sp.mp : 0;
// Use base cost from DOM instead of Mana Conservation-reduced cost
if (baseCost > 0) mpCost = baseCost;
// Skip if we can't afford the base cost — game pre-checks even when // Skip if we can't afford the base cost — game pre-checks even when
// channeling makes the effective cost 1 MP // channeling makes the effective cost 1 MP
const maxMp = parseInt((document.getElementById('vrhd') || {}).textContent) || 414; const maxMp = parseInt((document.getElementById('vrhd') || {}).textContent) || 414;
@ -3317,6 +3327,7 @@ function initializeBattle() {
STATE.page = 'battle'; STATE.page = 'battle';
STATE.inBattle = true; STATE.inBattle = true;
STATE._mysticUsed = false; // Reset Mystic Gem tracking on new battle STATE._mysticUsed = false; // Reset Mystic Gem tracking on new battle
STATE._baseMpCosts = {}; // Reset base cost cache
parseBattleState(); parseBattleState();
// Force tier update from cached level (battle page may not have level_readout) // Force tier update from cached level (battle page may not have level_readout)

View file

@ -91,7 +91,13 @@ function parseBattleState() {
const omo = el.getAttribute('onmouseover') || ''; const omo = el.getAttribute('onmouseover') || '';
if (!omo) return; if (!omo) return;
const m = omo.match(/battle\.set_infopane_spell\('([^']+)',\s*'[^']*',\s*'[^']*',\s*(\d+),\s*(\d+),\s*(\d+)\)/); const m = omo.match(/battle\.set_infopane_spell\('([^']+)',\s*'[^']*',\s*'[^']*',\s*(\d+),\s*(\d+),\s*(\d+)\)/);
if (m) STATE.spellsKnown.push({ n: m[1], mp: parseInt(m[2]), chr: parseInt(m[3]), cd: parseInt(m[4]) }); if (m) {
STATE.spellsKnown.push({ n: m[1], mp: parseInt(m[2]), chr: parseInt(m[3]), cd: parseInt(m[4]) });
// Cache the base MP cost (from magic pane, unaffected by channeling/Conservation)
if (!STATE._baseMpCosts[m[1]]) {
STATE._baseMpCosts[m[1]] = parseInt(m[2]);
}
}
}); });
} }

View file

@ -2,7 +2,7 @@
// CONFIG — default settings // CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.13.21'; const VERSION = '0.13.22';
const CFG = { const CFG = {
// — Battle automation // — Battle automation

View file

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name HV Unified // @name HV Unified
// @namespace hvunified // @namespace hvunified
// @version 0.13.21 // @version 0.13.22
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
// @author GaboGG + Hermes // @author GaboGG + Hermes
// @match *://*.hentaiverse.org/* // @match *://*.hentaiverse.org/*

View file

@ -50,6 +50,7 @@ function initializeBattle() {
STATE.page = 'battle'; STATE.page = 'battle';
STATE.inBattle = true; STATE.inBattle = true;
STATE._mysticUsed = false; // Reset Mystic Gem tracking on new battle STATE._mysticUsed = false; // Reset Mystic Gem tracking on new battle
STATE._baseMpCosts = {}; // Reset base cost cache
parseBattleState(); parseBattleState();
// Force tier update from cached level (battle page may not have level_readout) // Force tier update from cached level (battle page may not have level_readout)

View file

@ -15,6 +15,8 @@ const STATE = {
_mysticUsed: false, // Track if Mystic Gem has been used this battle _mysticUsed: false, // Track if Mystic Gem has been used this battle
_baseMpCosts: {}, // Cached base MP costs for buff spells (read from magic pane at battle start)
maxBuffDur: {}, // Tracks highest duration seen per buff icon maxBuffDur: {}, // Tracks highest duration seen per buff icon
hoverTarget: -1, hoverTarget: -1,

View file

@ -238,19 +238,21 @@ function findBestChannelingTarget() {
const dur = buffDuration(b.icon); const dur = buffDuration(b.icon);
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
const sp = findSpell(b.spell); const sp = findSpell(b.spell);
// Read base MP cost from the magic pane tooltip (always the full base cost, // Use cached base MP cost (snapshotted at battle start from the magic pane)
// unaffected by channeling's 1-MP display or Mana Conservation) // This avoids the channeling 1-MP display issue and Mana Conservation fluctuation
let baseCost = 0; let mpCost = STATE._baseMpCosts[b.spell] || 0;
if (mpCost === 0 && sp) {
// Not cached yet — read from magic pane directly
const paneEl = $$('.btsd[onmouseover*="' + b.spell.replace(/'/g, "\\'") + '"]', const paneEl = $$('.btsd[onmouseover*="' + b.spell.replace(/'/g, "\\'") + '"]',
document.getElementById('pane_magic'))[0]; document.getElementById('pane_magic'))[0];
if (paneEl) { if (paneEl) {
const costM = (paneEl.getAttribute('onmouseover') || '') const costM = (paneEl.getAttribute('onmouseover') || '')
.match(/set_infopane_spell\('[^']+',\s*'[^']*',\s*'[^']*',\s*(\d+)/); .match(/set_infopane_spell\('[^']+',\s*'[^']*',\s*'[^']*',\s*(\d+)/);
if (costM) baseCost = parseInt(costM[1]); if (costM) mpCost = parseInt(costM[1]);
}
if (mpCost === 0) mpCost = sp.mp; // last resort: use what parseBattleState gave us
STATE._baseMpCosts[b.spell] = mpCost;
} }
let mpCost = sp ? sp.mp : 0;
// Use base cost from DOM instead of Mana Conservation-reduced cost
if (baseCost > 0) mpCost = baseCost;
// Skip if we can't afford the base cost — game pre-checks even when // Skip if we can't afford the base cost — game pre-checks even when
// channeling makes the effective cost 1 MP // channeling makes the effective cost 1 MP
const maxMp = parseInt((document.getElementById('vrhd') || {}).textContent) || 414; const maxMp = parseInt((document.getElementById('vrhd') || {}).textContent) || 414;