diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index e7b16e8..32e16de 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.32 +// @version 0.13.33 // @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.32'; +const VERSION = '0.13.33'; const CFG = { // — Battle automation @@ -82,7 +82,7 @@ const STATE = { _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, interruptHover: false, @@ -126,6 +126,14 @@ try { 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 ── 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 // ═══════════════════════════════════════════════════════════════════════ @@ -523,6 +536,7 @@ function parseBattleState() { // Track the max duration seen for this buff (for fill-level calculation) if (turns < 999 && turns > (STATE.maxBuffDur[name] || 0)) { STATE.maxBuffDur[name] = turns; + saveMaxBuffDur(); // persist across page loads } }); } @@ -991,13 +1005,7 @@ 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); - // 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 + const emptiness = maxDur > 0 ? Math.min(1, (maxDur - dur) / maxDur) : 1; // Score: emptiness × mp_cost — an empty expensive buff is best const score = emptiness * mpCost; 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'); // 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) - // 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 }; + if (candidates[0].score >= 30) { + return { type: 'spell', name: candidates[0].spell, selfTarget: true }; } } diff --git a/scripts/latest.user.js b/scripts/latest.user.js index e7b16e8..32e16de 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.13.32 +// @version 0.13.33 // @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.32'; +const VERSION = '0.13.33'; const CFG = { // — Battle automation @@ -82,7 +82,7 @@ const STATE = { _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, interruptHover: false, @@ -126,6 +126,14 @@ try { 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 ── 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 // ═══════════════════════════════════════════════════════════════════════ @@ -523,6 +536,7 @@ function parseBattleState() { // Track the max duration seen for this buff (for fill-level calculation) if (turns < 999 && turns > (STATE.maxBuffDur[name] || 0)) { STATE.maxBuffDur[name] = turns; + saveMaxBuffDur(); // persist across page loads } }); } @@ -991,13 +1005,7 @@ 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); - // 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 + const emptiness = maxDur > 0 ? Math.min(1, (maxDur - dur) / maxDur) : 1; // Score: emptiness × mp_cost — an empty expensive buff is best const score = emptiness * mpCost; 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'); // 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) - // 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 }; + if (candidates[0].score >= 30) { + return { type: 'spell', name: candidates[0].spell, selfTarget: true }; } } diff --git a/src/battle-parser.js b/src/battle-parser.js index f787343..248a00c 100644 --- a/src/battle-parser.js +++ b/src/battle-parser.js @@ -177,6 +177,7 @@ function parseBattleState() { // Track the max duration seen for this buff (for fill-level calculation) if (turns < 999 && turns > (STATE.maxBuffDur[name] || 0)) { STATE.maxBuffDur[name] = turns; + saveMaxBuffDur(); // persist across page loads } }); } diff --git a/src/config.js b/src/config.js index b7308d7..992db18 100644 --- a/src/config.js +++ b/src/config.js @@ -2,7 +2,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.13.32'; +const VERSION = '0.13.33'; const CFG = { // — Battle automation diff --git a/src/header.user.js b/src/header.user.js index e5db92b..374906f 100644 --- a/src/header.user.js +++ b/src/header.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.13.32 +// @version 0.13.33 // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @author GaboGG + Hermes // @match *://*.hentaiverse.org/* diff --git a/src/state.js b/src/state.js index adedaaf..cd51e6c 100644 --- a/src/state.js +++ b/src/state.js @@ -18,7 +18,7 @@ const STATE = { _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, interruptHover: false, @@ -62,6 +62,14 @@ try { 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 ── function loadConfig() { @@ -146,3 +154,8 @@ function restoreCachedDifficulty() { } 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) {} +} diff --git a/src/strategy-engine.js b/src/strategy-engine.js index f8d0896..19c77ec 100644 --- a/src/strategy-engine.js +++ b/src/strategy-engine.js @@ -291,13 +291,7 @@ 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); - // 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 + const emptiness = maxDur > 0 ? Math.min(1, (maxDur - dur) / maxDur) : 1; // Score: emptiness × mp_cost — an empty expensive buff is best const score = emptiness * mpCost; 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'); // 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) - // 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 }; + if (candidates[0].score >= 30) { + return { type: 'spell', name: candidates[0].spell, selfTarget: true }; } }