diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index 6ec9ec1..c2128c6 100644 --- a/scripts/hv-unified.user.js +++ b/scripts/hv-unified.user.js @@ -76,6 +76,7 @@ const STATE = { battleMode: '', inBattle: false, battleInitialized: false, + battleRound: 0, // Incremented each round for first-round logic hoverTarget: -1, interruptHover: false, @@ -791,6 +792,50 @@ function getBestDamageSpell() { // Best trigger spells: Regen/Absorb (17 MP = ~6% proc chance each cast) // Cheaper spells have proportionally lower proc chance. +// Checks if we're in the initial buff-up phase (most buffs are missing) +function isFirstRoundBuffs() { + let missing = 0, total = 0; + for (const b of BUFF_PRIORITY) { + if (!hasSpell(b.spell)) continue; + total++; + if (buffDuration(b.icon) === 0) missing++; + } + return total > 0 && missing >= total * 0.6; // 60%+ of our buffs are down +} + +// Round 1 strategy: cast buffs from cheapest to most expensive. +// - Cheaper spells first = more casts before channeling = more proc chances +// - If channeling procs mid-sequence, switch to most expensive remaining buff +// Returns the next buff to cast, or null if all buffs are up. +function castInitialBuffs() { + if (!isFirstRoundBuffs()) return null; + if (STATE.mp < 0.20) return null; + + // Collect all missing/expiring buffs with their MP cost + const candidates = []; + for (const b of BUFF_PRIORITY) { + if (!hasSpell(b.spell)) continue; + const dur = buffDuration(b.icon); + if (dur === 0 || dur < 3) { + if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; + const sp = findSpell(b.spell); + candidates.push({ ...b, mpCost: sp ? sp.mp : 999, dur }); + } + } + + if (candidates.length === 0) return null; + + if (STATE.channeling) { + // Channeling is charged — cast the MOST expensive buff for best value + candidates.sort((a, b) => b.mpCost - a.mpCost); + } else { + // No channeling — cast the CHEAPEST buff first for more proc attempts + candidates.sort((a, b) => a.mpCost - b.mpCost); + } + + return { type: 'spell', name: candidates[0].spell, selfTarget: true }; +} + function kickstartChanneling() { if (STATE.channeling) return null; @@ -909,6 +954,11 @@ function strategyNovice() { } // 3. Buffs — cast if missing or about to expire + // On first round, cast cheapest-first to proc channeling + { + const ib = castInitialBuffs(); + if (ib) return ib; + } for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; @@ -1019,6 +1069,11 @@ function strategyAdept() { } // 3. Buffs + // On first round, cast cheapest-first to proc channeling + { + const ib = castInitialBuffs(); + if (ib) return ib; + } for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; @@ -1141,7 +1196,12 @@ function strategyVeteran() { } // 3. Buffs + // On first round, cast cheapest-first to proc channeling if (CFG.autoBuff) { + { + const ib = castInitialBuffs(); + if (ib) return ib; + } for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; diff --git a/scripts/latest.user.js b/scripts/latest.user.js index 6ec9ec1..c2128c6 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -76,6 +76,7 @@ const STATE = { battleMode: '', inBattle: false, battleInitialized: false, + battleRound: 0, // Incremented each round for first-round logic hoverTarget: -1, interruptHover: false, @@ -791,6 +792,50 @@ function getBestDamageSpell() { // Best trigger spells: Regen/Absorb (17 MP = ~6% proc chance each cast) // Cheaper spells have proportionally lower proc chance. +// Checks if we're in the initial buff-up phase (most buffs are missing) +function isFirstRoundBuffs() { + let missing = 0, total = 0; + for (const b of BUFF_PRIORITY) { + if (!hasSpell(b.spell)) continue; + total++; + if (buffDuration(b.icon) === 0) missing++; + } + return total > 0 && missing >= total * 0.6; // 60%+ of our buffs are down +} + +// Round 1 strategy: cast buffs from cheapest to most expensive. +// - Cheaper spells first = more casts before channeling = more proc chances +// - If channeling procs mid-sequence, switch to most expensive remaining buff +// Returns the next buff to cast, or null if all buffs are up. +function castInitialBuffs() { + if (!isFirstRoundBuffs()) return null; + if (STATE.mp < 0.20) return null; + + // Collect all missing/expiring buffs with their MP cost + const candidates = []; + for (const b of BUFF_PRIORITY) { + if (!hasSpell(b.spell)) continue; + const dur = buffDuration(b.icon); + if (dur === 0 || dur < 3) { + if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; + const sp = findSpell(b.spell); + candidates.push({ ...b, mpCost: sp ? sp.mp : 999, dur }); + } + } + + if (candidates.length === 0) return null; + + if (STATE.channeling) { + // Channeling is charged — cast the MOST expensive buff for best value + candidates.sort((a, b) => b.mpCost - a.mpCost); + } else { + // No channeling — cast the CHEAPEST buff first for more proc attempts + candidates.sort((a, b) => a.mpCost - b.mpCost); + } + + return { type: 'spell', name: candidates[0].spell, selfTarget: true }; +} + function kickstartChanneling() { if (STATE.channeling) return null; @@ -909,6 +954,11 @@ function strategyNovice() { } // 3. Buffs — cast if missing or about to expire + // On first round, cast cheapest-first to proc channeling + { + const ib = castInitialBuffs(); + if (ib) return ib; + } for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; @@ -1019,6 +1069,11 @@ function strategyAdept() { } // 3. Buffs + // On first round, cast cheapest-first to proc channeling + { + const ib = castInitialBuffs(); + if (ib) return ib; + } for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; @@ -1141,7 +1196,12 @@ function strategyVeteran() { } // 3. Buffs + // On first round, cast cheapest-first to proc channeling if (CFG.autoBuff) { + { + const ib = castInitialBuffs(); + if (ib) return ib; + } for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; diff --git a/src/state.js b/src/state.js index b6984b9..4b023ad 100644 --- a/src/state.js +++ b/src/state.js @@ -12,6 +12,7 @@ const STATE = { battleMode: '', inBattle: false, battleInitialized: false, + battleRound: 0, // Incremented each round for first-round logic hoverTarget: -1, interruptHover: false, diff --git a/src/strategy-engine.js b/src/strategy-engine.js index 051f8d9..54890b6 100644 --- a/src/strategy-engine.js +++ b/src/strategy-engine.js @@ -143,6 +143,50 @@ function getBestDamageSpell() { // Best trigger spells: Regen/Absorb (17 MP = ~6% proc chance each cast) // Cheaper spells have proportionally lower proc chance. +// Checks if we're in the initial buff-up phase (most buffs are missing) +function isFirstRoundBuffs() { + let missing = 0, total = 0; + for (const b of BUFF_PRIORITY) { + if (!hasSpell(b.spell)) continue; + total++; + if (buffDuration(b.icon) === 0) missing++; + } + return total > 0 && missing >= total * 0.6; // 60%+ of our buffs are down +} + +// Round 1 strategy: cast buffs from cheapest to most expensive. +// - Cheaper spells first = more casts before channeling = more proc chances +// - If channeling procs mid-sequence, switch to most expensive remaining buff +// Returns the next buff to cast, or null if all buffs are up. +function castInitialBuffs() { + if (!isFirstRoundBuffs()) return null; + if (STATE.mp < 0.20) return null; + + // Collect all missing/expiring buffs with their MP cost + const candidates = []; + for (const b of BUFF_PRIORITY) { + if (!hasSpell(b.spell)) continue; + const dur = buffDuration(b.icon); + if (dur === 0 || dur < 3) { + if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; + const sp = findSpell(b.spell); + candidates.push({ ...b, mpCost: sp ? sp.mp : 999, dur }); + } + } + + if (candidates.length === 0) return null; + + if (STATE.channeling) { + // Channeling is charged — cast the MOST expensive buff for best value + candidates.sort((a, b) => b.mpCost - a.mpCost); + } else { + // No channeling — cast the CHEAPEST buff first for more proc attempts + candidates.sort((a, b) => a.mpCost - b.mpCost); + } + + return { type: 'spell', name: candidates[0].spell, selfTarget: true }; +} + function kickstartChanneling() { if (STATE.channeling) return null; @@ -261,6 +305,11 @@ function strategyNovice() { } // 3. Buffs — cast if missing or about to expire + // On first round, cast cheapest-first to proc channeling + { + const ib = castInitialBuffs(); + if (ib) return ib; + } for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; @@ -371,6 +420,11 @@ function strategyAdept() { } // 3. Buffs + // On first round, cast cheapest-first to proc channeling + { + const ib = castInitialBuffs(); + if (ib) return ib; + } for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; @@ -493,7 +547,12 @@ function strategyVeteran() { } // 3. Buffs + // On first round, cast cheapest-first to proc channeling if (CFG.autoBuff) { + { + const ib = castInitialBuffs(); + if (ib) return ib; + } for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;