From b8e2e945ec0422f5f96ce43ea9adf08c82ec9214 Mon Sep 17 00:00:00 2001 From: GaboGG Date: Mon, 27 Jul 2026 13:22:15 -0400 Subject: [PATCH] v0.13.32 - Fix channeling waste when maxBuffDur hasn't learned true cap - When maxBuffDur gap (max - current) is < 10, the cap is unreliable (first observation mid-battle, not at full duration) - In that case, skip the fill-level formula entirely - Only suggest a buff for channeling if it actually needs refresh (dur <= minTurns from BUFF_PRIORITY) - Once maxBuffDur learns the true cap (after a fresh cast), the normal fill-level scoring takes over correctly --- scripts/hv-unified.user.js | 22 ++++++++++++++++------ scripts/latest.user.js | 22 ++++++++++++++++------ src/config.js | 2 +- src/guidance.js | 2 +- src/header.user.js | 2 +- src/strategy-engine.js | 16 +++++++++++++--- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index 9637290..e7b16e8 100644 --- a/scripts/hv-unified.user.js +++ b/scripts/hv-unified.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.13.31 +// @version 0.13.32 // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @author GaboGG + Hermes // @match *://*.hentaiverse.org/* @@ -17,7 +17,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.13.31'; +const VERSION = '0.13.32'; const CFG = { // — Battle automation @@ -991,7 +991,13 @@ function findBestChannelingTarget() { let maxDur = STATE.maxBuffDur[b.icon] || 0; // If max matches current (first observation, no learning yet), assume min 30 turns if (maxDur <= dur) maxDur = Math.max(dur + 1, 30); - const emptiness = maxDur > 0 ? Math.min(1, (maxDur - dur) / maxDur) : 1; + // Check if maxBuffDur is unreliable: if the gap between max and current + // 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 const score = emptiness * mpCost; candidates.push({ ...b, mpCost, dur, score, emptiness, maxDur }); @@ -1002,8 +1008,12 @@ 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'); // 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) - if (candidates[0].score >= 30) { - return { type: 'spell', name: candidates[0].spell, selfTarget: true }; + // When maxBuffDur is unreliable (gap < 10), skip the threshold for + // buffs that have ticked past their minTurns — they genuinely need refresh + 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 }; } } @@ -2294,7 +2304,7 @@ function getDifficultyAdvice() { reason = ''; } - const currentIdx = difficulties.indexOf(difficulty); + const currentIdx = difficulties.indexOf(difficulty.toUpperCase()); const suggestedIdx = difficulties.indexOf(suggested); // Only warn if current difficulty is LOWER than suggested diff --git a/scripts/latest.user.js b/scripts/latest.user.js index 9637290..e7b16e8 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.13.31 +// @version 0.13.32 // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @author GaboGG + Hermes // @match *://*.hentaiverse.org/* @@ -17,7 +17,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.13.31'; +const VERSION = '0.13.32'; const CFG = { // — Battle automation @@ -991,7 +991,13 @@ function findBestChannelingTarget() { let maxDur = STATE.maxBuffDur[b.icon] || 0; // If max matches current (first observation, no learning yet), assume min 30 turns if (maxDur <= dur) maxDur = Math.max(dur + 1, 30); - const emptiness = maxDur > 0 ? Math.min(1, (maxDur - dur) / maxDur) : 1; + // Check if maxBuffDur is unreliable: if the gap between max and current + // 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 const score = emptiness * mpCost; candidates.push({ ...b, mpCost, dur, score, emptiness, maxDur }); @@ -1002,8 +1008,12 @@ 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'); // 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) - if (candidates[0].score >= 30) { - return { type: 'spell', name: candidates[0].spell, selfTarget: true }; + // When maxBuffDur is unreliable (gap < 10), skip the threshold for + // buffs that have ticked past their minTurns — they genuinely need refresh + 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 }; } } @@ -2294,7 +2304,7 @@ function getDifficultyAdvice() { reason = ''; } - const currentIdx = difficulties.indexOf(difficulty); + const currentIdx = difficulties.indexOf(difficulty.toUpperCase()); const suggestedIdx = difficulties.indexOf(suggested); // Only warn if current difficulty is LOWER than suggested diff --git a/src/config.js b/src/config.js index eaa0f36..b7308d7 100644 --- a/src/config.js +++ b/src/config.js @@ -2,7 +2,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.13.31'; +const VERSION = '0.13.32'; const CFG = { // — Battle automation diff --git a/src/guidance.js b/src/guidance.js index 0b3ddef..bfd4289 100644 --- a/src/guidance.js +++ b/src/guidance.js @@ -62,7 +62,7 @@ function getDifficultyAdvice() { reason = ''; } - const currentIdx = difficulties.indexOf(difficulty); + const currentIdx = difficulties.indexOf(difficulty.toUpperCase()); const suggestedIdx = difficulties.indexOf(suggested); // Only warn if current difficulty is LOWER than suggested diff --git a/src/header.user.js b/src/header.user.js index 0babdae..e5db92b 100644 --- a/src/header.user.js +++ b/src/header.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.13.31 +// @version 0.13.32 // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @author GaboGG + Hermes // @match *://*.hentaiverse.org/* diff --git a/src/strategy-engine.js b/src/strategy-engine.js index 19c77ec..f8d0896 100644 --- a/src/strategy-engine.js +++ b/src/strategy-engine.js @@ -291,7 +291,13 @@ function findBestChannelingTarget() { let maxDur = STATE.maxBuffDur[b.icon] || 0; // If max matches current (first observation, no learning yet), assume min 30 turns if (maxDur <= dur) maxDur = Math.max(dur + 1, 30); - const emptiness = maxDur > 0 ? Math.min(1, (maxDur - dur) / maxDur) : 1; + // Check if maxBuffDur is unreliable: if the gap between max and current + // 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 const score = emptiness * mpCost; candidates.push({ ...b, mpCost, dur, score, emptiness, maxDur }); @@ -302,8 +308,12 @@ 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'); // 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) - if (candidates[0].score >= 30) { - return { type: 'spell', name: candidates[0].spell, selfTarget: true }; + // When maxBuffDur is unreliable (gap < 10), skip the threshold for + // buffs that have ticked past their minTurns — they genuinely need refresh + 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 }; } }