v0.13.33 - Persist maxBuffDur to localStorage (no more warm-up)
- maxBuffDur now saved to localStorage['hvunified_maxBuffDur'] on every new high observation and loaded on STATE init - Survives page reloads: Heartseeker's 180-turn max stays learned - Removed the unreliable-max fallback hack — no longer needed - First Q press of a fresh session has accurate fill-level scores for all buffs that were observed in previous sessions
This commit is contained in:
parent
b8e2e945ec
commit
949aab915f
7 changed files with 60 additions and 48 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.13.32
|
// @version 0.13.33
|
||||||
// @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.32';
|
const VERSION = '0.13.33';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -82,7 +82,7 @@ const STATE = {
|
||||||
|
|
||||||
_baseMpCosts: {}, // Cached base MP costs for buff spells (read from magic pane at battle start)
|
_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 (persisted to localStorage)
|
||||||
|
|
||||||
hoverTarget: -1,
|
hoverTarget: -1,
|
||||||
interruptHover: false,
|
interruptHover: false,
|
||||||
|
|
@ -126,6 +126,14 @@ try {
|
||||||
STATE.monsterData = {};
|
STATE.monsterData = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load max buff durations from localStorage (learned across sessions)
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage[SP + 'maxBuffDur'] || '{}');
|
||||||
|
for (const [k, v] of Object.entries(saved)) {
|
||||||
|
STATE.maxBuffDur[k] = v;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
// ── helpers ──
|
// ── helpers ──
|
||||||
|
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
|
|
@ -211,6 +219,11 @@ function restoreCachedDifficulty() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save max buff durations to localStorage (persist learned values across restarts)
|
||||||
|
function saveMaxBuffDur() {
|
||||||
|
try { localStorage[SP + 'maxBuffDur'] = JSON.stringify(STATE.maxBuffDur); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// UTILS — DOM shortcuts, constants, helpers
|
// UTILS — DOM shortcuts, constants, helpers
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -523,6 +536,7 @@ function parseBattleState() {
|
||||||
// Track the max duration seen for this buff (for fill-level calculation)
|
// Track the max duration seen for this buff (for fill-level calculation)
|
||||||
if (turns < 999 && turns > (STATE.maxBuffDur[name] || 0)) {
|
if (turns < 999 && turns > (STATE.maxBuffDur[name] || 0)) {
|
||||||
STATE.maxBuffDur[name] = turns;
|
STATE.maxBuffDur[name] = turns;
|
||||||
|
saveMaxBuffDur(); // persist across page loads
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -991,13 +1005,7 @@ function findBestChannelingTarget() {
|
||||||
let maxDur = STATE.maxBuffDur[b.icon] || 0;
|
let maxDur = STATE.maxBuffDur[b.icon] || 0;
|
||||||
// If max matches current (first observation, no learning yet), assume min 30 turns
|
// If max matches current (first observation, no learning yet), assume min 30 turns
|
||||||
if (maxDur <= dur) maxDur = Math.max(dur + 1, 30);
|
if (maxDur <= dur) maxDur = Math.max(dur + 1, 30);
|
||||||
// Check if maxBuffDur is unreliable: if the gap between max and current
|
const emptiness = maxDur > 0 ? Math.min(1, (maxDur - dur) / maxDur) : 1;
|
||||||
// is tiny (<10% of max), we haven't seen the true cap yet. In that case,
|
|
||||||
// just use the simple shouldBuff check instead.
|
|
||||||
const isReliable = maxDur > dur + 10;
|
|
||||||
const emptiness = isReliable
|
|
||||||
? Math.min(1, (maxDur - dur) / maxDur)
|
|
||||||
: (dur <= b.minTurns ? 0.5 : 0); // treat as half-empty if past minTurns
|
|
||||||
// Score: emptiness × mp_cost — an empty expensive buff is best
|
// Score: emptiness × mp_cost — an empty expensive buff is best
|
||||||
const score = emptiness * mpCost;
|
const score = emptiness * mpCost;
|
||||||
candidates.push({ ...b, mpCost, dur, score, emptiness, maxDur });
|
candidates.push({ ...b, mpCost, dur, score, emptiness, maxDur });
|
||||||
|
|
@ -1008,12 +1016,8 @@ function findBestChannelingTarget() {
|
||||||
console.log(`%c[HV] ch score: ${candidates[0].spell}=${candidates[0].score.toFixed(1)} (dur=${candidates[0].dur}, max=${candidates[0].maxDur}, mp=${candidates[0].mpCost}, empty=${(candidates[0].emptiness*100).toFixed(0)}%)`, 'color:#bb0');
|
console.log(`%c[HV] ch score: ${candidates[0].spell}=${candidates[0].score.toFixed(1)} (dur=${candidates[0].dur}, max=${candidates[0].maxDur}, mp=${candidates[0].mpCost}, empty=${(candidates[0].emptiness*100).toFixed(0)}%)`, 'color:#bb0');
|
||||||
// Only use a buff if it actually needs refreshing (score >= 30 means
|
// Only use a buff if it actually needs refreshing (score >= 30 means
|
||||||
// e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell)
|
// e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell)
|
||||||
// When maxBuffDur is unreliable (gap < 10), skip the threshold for
|
if (candidates[0].score >= 30) {
|
||||||
// buffs that have ticked past their minTurns — they genuinely need refresh
|
return { type: 'spell', name: candidates[0].spell, selfTarget: true };
|
||||||
const top = candidates[0];
|
|
||||||
const gapOk = top.maxDur - top.dur > 10;
|
|
||||||
if (top.score >= 30 || (!gapOk && top.dur <= BUFF_PRIORITY.find(b => b.icon === top.icon)?.minTurns)) {
|
|
||||||
return { type: 'spell', name: top.spell, selfTarget: true };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.13.32
|
// @version 0.13.33
|
||||||
// @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.32';
|
const VERSION = '0.13.33';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
@ -82,7 +82,7 @@ const STATE = {
|
||||||
|
|
||||||
_baseMpCosts: {}, // Cached base MP costs for buff spells (read from magic pane at battle start)
|
_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 (persisted to localStorage)
|
||||||
|
|
||||||
hoverTarget: -1,
|
hoverTarget: -1,
|
||||||
interruptHover: false,
|
interruptHover: false,
|
||||||
|
|
@ -126,6 +126,14 @@ try {
|
||||||
STATE.monsterData = {};
|
STATE.monsterData = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load max buff durations from localStorage (learned across sessions)
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage[SP + 'maxBuffDur'] || '{}');
|
||||||
|
for (const [k, v] of Object.entries(saved)) {
|
||||||
|
STATE.maxBuffDur[k] = v;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
// ── helpers ──
|
// ── helpers ──
|
||||||
|
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
|
|
@ -211,6 +219,11 @@ function restoreCachedDifficulty() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save max buff durations to localStorage (persist learned values across restarts)
|
||||||
|
function saveMaxBuffDur() {
|
||||||
|
try { localStorage[SP + 'maxBuffDur'] = JSON.stringify(STATE.maxBuffDur); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// UTILS — DOM shortcuts, constants, helpers
|
// UTILS — DOM shortcuts, constants, helpers
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -523,6 +536,7 @@ function parseBattleState() {
|
||||||
// Track the max duration seen for this buff (for fill-level calculation)
|
// Track the max duration seen for this buff (for fill-level calculation)
|
||||||
if (turns < 999 && turns > (STATE.maxBuffDur[name] || 0)) {
|
if (turns < 999 && turns > (STATE.maxBuffDur[name] || 0)) {
|
||||||
STATE.maxBuffDur[name] = turns;
|
STATE.maxBuffDur[name] = turns;
|
||||||
|
saveMaxBuffDur(); // persist across page loads
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -991,13 +1005,7 @@ function findBestChannelingTarget() {
|
||||||
let maxDur = STATE.maxBuffDur[b.icon] || 0;
|
let maxDur = STATE.maxBuffDur[b.icon] || 0;
|
||||||
// If max matches current (first observation, no learning yet), assume min 30 turns
|
// If max matches current (first observation, no learning yet), assume min 30 turns
|
||||||
if (maxDur <= dur) maxDur = Math.max(dur + 1, 30);
|
if (maxDur <= dur) maxDur = Math.max(dur + 1, 30);
|
||||||
// Check if maxBuffDur is unreliable: if the gap between max and current
|
const emptiness = maxDur > 0 ? Math.min(1, (maxDur - dur) / maxDur) : 1;
|
||||||
// is tiny (<10% of max), we haven't seen the true cap yet. In that case,
|
|
||||||
// just use the simple shouldBuff check instead.
|
|
||||||
const isReliable = maxDur > dur + 10;
|
|
||||||
const emptiness = isReliable
|
|
||||||
? Math.min(1, (maxDur - dur) / maxDur)
|
|
||||||
: (dur <= b.minTurns ? 0.5 : 0); // treat as half-empty if past minTurns
|
|
||||||
// Score: emptiness × mp_cost — an empty expensive buff is best
|
// Score: emptiness × mp_cost — an empty expensive buff is best
|
||||||
const score = emptiness * mpCost;
|
const score = emptiness * mpCost;
|
||||||
candidates.push({ ...b, mpCost, dur, score, emptiness, maxDur });
|
candidates.push({ ...b, mpCost, dur, score, emptiness, maxDur });
|
||||||
|
|
@ -1008,12 +1016,8 @@ function findBestChannelingTarget() {
|
||||||
console.log(`%c[HV] ch score: ${candidates[0].spell}=${candidates[0].score.toFixed(1)} (dur=${candidates[0].dur}, max=${candidates[0].maxDur}, mp=${candidates[0].mpCost}, empty=${(candidates[0].emptiness*100).toFixed(0)}%)`, 'color:#bb0');
|
console.log(`%c[HV] ch score: ${candidates[0].spell}=${candidates[0].score.toFixed(1)} (dur=${candidates[0].dur}, max=${candidates[0].maxDur}, mp=${candidates[0].mpCost}, empty=${(candidates[0].emptiness*100).toFixed(0)}%)`, 'color:#bb0');
|
||||||
// Only use a buff if it actually needs refreshing (score >= 30 means
|
// Only use a buff if it actually needs refreshing (score >= 30 means
|
||||||
// e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell)
|
// e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell)
|
||||||
// When maxBuffDur is unreliable (gap < 10), skip the threshold for
|
if (candidates[0].score >= 30) {
|
||||||
// buffs that have ticked past their minTurns — they genuinely need refresh
|
return { type: 'spell', name: candidates[0].spell, selfTarget: true };
|
||||||
const top = candidates[0];
|
|
||||||
const gapOk = top.maxDur - top.dur > 10;
|
|
||||||
if (top.score >= 30 || (!gapOk && top.dur <= BUFF_PRIORITY.find(b => b.icon === top.icon)?.minTurns)) {
|
|
||||||
return { type: 'spell', name: top.spell, selfTarget: true };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,7 @@ function parseBattleState() {
|
||||||
// Track the max duration seen for this buff (for fill-level calculation)
|
// Track the max duration seen for this buff (for fill-level calculation)
|
||||||
if (turns < 999 && turns > (STATE.maxBuffDur[name] || 0)) {
|
if (turns < 999 && turns > (STATE.maxBuffDur[name] || 0)) {
|
||||||
STATE.maxBuffDur[name] = turns;
|
STATE.maxBuffDur[name] = turns;
|
||||||
|
saveMaxBuffDur(); // persist across page loads
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.13.32';
|
const VERSION = '0.13.33';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle automation
|
// — Battle automation
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.13.32
|
// @version 0.13.33
|
||||||
// @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/*
|
||||||
|
|
|
||||||
15
src/state.js
15
src/state.js
|
|
@ -18,7 +18,7 @@ const STATE = {
|
||||||
|
|
||||||
_baseMpCosts: {}, // Cached base MP costs for buff spells (read from magic pane at battle start)
|
_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 (persisted to localStorage)
|
||||||
|
|
||||||
hoverTarget: -1,
|
hoverTarget: -1,
|
||||||
interruptHover: false,
|
interruptHover: false,
|
||||||
|
|
@ -62,6 +62,14 @@ try {
|
||||||
STATE.monsterData = {};
|
STATE.monsterData = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load max buff durations from localStorage (learned across sessions)
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage[SP + 'maxBuffDur'] || '{}');
|
||||||
|
for (const [k, v] of Object.entries(saved)) {
|
||||||
|
STATE.maxBuffDur[k] = v;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
// ── helpers ──
|
// ── helpers ──
|
||||||
|
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
|
|
@ -146,3 +154,8 @@ function restoreCachedDifficulty() {
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save max buff durations to localStorage (persist learned values across restarts)
|
||||||
|
function saveMaxBuffDur() {
|
||||||
|
try { localStorage[SP + 'maxBuffDur'] = JSON.stringify(STATE.maxBuffDur); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -291,13 +291,7 @@ function findBestChannelingTarget() {
|
||||||
let maxDur = STATE.maxBuffDur[b.icon] || 0;
|
let maxDur = STATE.maxBuffDur[b.icon] || 0;
|
||||||
// If max matches current (first observation, no learning yet), assume min 30 turns
|
// If max matches current (first observation, no learning yet), assume min 30 turns
|
||||||
if (maxDur <= dur) maxDur = Math.max(dur + 1, 30);
|
if (maxDur <= dur) maxDur = Math.max(dur + 1, 30);
|
||||||
// Check if maxBuffDur is unreliable: if the gap between max and current
|
const emptiness = maxDur > 0 ? Math.min(1, (maxDur - dur) / maxDur) : 1;
|
||||||
// is tiny (<10% of max), we haven't seen the true cap yet. In that case,
|
|
||||||
// just use the simple shouldBuff check instead.
|
|
||||||
const isReliable = maxDur > dur + 10;
|
|
||||||
const emptiness = isReliable
|
|
||||||
? Math.min(1, (maxDur - dur) / maxDur)
|
|
||||||
: (dur <= b.minTurns ? 0.5 : 0); // treat as half-empty if past minTurns
|
|
||||||
// Score: emptiness × mp_cost — an empty expensive buff is best
|
// Score: emptiness × mp_cost — an empty expensive buff is best
|
||||||
const score = emptiness * mpCost;
|
const score = emptiness * mpCost;
|
||||||
candidates.push({ ...b, mpCost, dur, score, emptiness, maxDur });
|
candidates.push({ ...b, mpCost, dur, score, emptiness, maxDur });
|
||||||
|
|
@ -308,12 +302,8 @@ function findBestChannelingTarget() {
|
||||||
console.log(`%c[HV] ch score: ${candidates[0].spell}=${candidates[0].score.toFixed(1)} (dur=${candidates[0].dur}, max=${candidates[0].maxDur}, mp=${candidates[0].mpCost}, empty=${(candidates[0].emptiness*100).toFixed(0)}%)`, 'color:#bb0');
|
console.log(`%c[HV] ch score: ${candidates[0].spell}=${candidates[0].score.toFixed(1)} (dur=${candidates[0].dur}, max=${candidates[0].maxDur}, mp=${candidates[0].mpCost}, empty=${(candidates[0].emptiness*100).toFixed(0)}%)`, 'color:#bb0');
|
||||||
// Only use a buff if it actually needs refreshing (score >= 30 means
|
// Only use a buff if it actually needs refreshing (score >= 30 means
|
||||||
// e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell)
|
// e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell)
|
||||||
// When maxBuffDur is unreliable (gap < 10), skip the threshold for
|
if (candidates[0].score >= 30) {
|
||||||
// buffs that have ticked past their minTurns — they genuinely need refresh
|
return { type: 'spell', name: candidates[0].spell, selfTarget: true };
|
||||||
const top = candidates[0];
|
|
||||||
const gapOk = top.maxDur - top.dur > 10;
|
|
||||||
if (top.score >= 30 || (!gapOk && top.dur <= BUFF_PRIORITY.find(b => b.icon === top.icon)?.minTurns)) {
|
|
||||||
return { type: 'spell', name: top.spell, selfTarget: true };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue