diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index 5171948..2cd5654 100644 --- a/scripts/hv-unified.user.js +++ b/scripts/hv-unified.user.js @@ -605,6 +605,8 @@ const KB = { { lvl: 60, name: 'Haste' }, { lvl: 70, name: 'Weaken' }, { lvl: 130, name: 'Imperil' }, + { lvl: 140, name: 'Heartseeker' }, + { lvl: 175, name: 'Arcane Focus' }, { lvl: 220, name: 'Full-Cure' }, ], @@ -639,6 +641,7 @@ const KB = { { lvl: 70, tip: 'Weaken: -50% enemy damage. Use items, then Cure.' }, { lvl: 100, tip: 'Level 100! T2 spells available. Accuracy target: 150%.' }, { lvl: 130, tip: 'Imperil — THE most important debuff. Always cast first.' }, + { lvl: 140, tip: 'Heartseeker — +25% physical damage, +10% crit. Self-buff, priority over Arcane Focus for 2H.' }, { lvl: 300, tip: 'Master tier. Optimize for speed. Run PFUDOR.' }, ], }; @@ -676,13 +679,16 @@ function shouldBuff(name, minTurns) { } // Known buffs with their priority, icon name, spell name, and refresh threshold +// NOTE: Heartseeker and Arcane Focus are mutually exclusive. For a 2H (physical) build, +// Heartseeker is preferred (+25% phys dmg, +10% crit). Arcane Focus is for magic builds. const BUFF_PRIORITY = [ - { icon: 'haste', spell: 'Haste', minTurns: 2 }, - { icon: 'protection', spell: 'Protection', minTurns: 2 }, - { icon: 'sparklife', spell: 'Spark of Life', minTurns: 1 }, // icon: sparklife.png - { icon: 'regen', spell: 'Regen', minTurns: 2 }, - { icon: 'absorb', spell: 'Absorb', minTurns: 2 }, - { icon: 'shadowveil', spell: 'Shadow Veil', minTurns: 3 }, + { icon: 'haste', spell: 'Haste', minTurns: 4 }, // 100% uptime: refresh well before expiry + { icon: 'protection', spell: 'Protection', minTurns: 4 }, + { icon: 'sparklife', spell: 'Spark of Life', minTurns: 3 }, // icon: sparklife.png + { icon: 'heartseeker', spell: 'Heartseeker', minTurns: 6 }, // Long duration, refresh at 6 to avoid gap + { icon: 'regen', spell: 'Regen', minTurns: 4 }, + { icon: 'absorb', spell: 'Absorb', minTurns: 4 }, + { icon: 'shadowveil', spell: 'Shadow Veil', minTurns: 4 }, ]; // Known important monster names (bosses, legendaries, ultimates) @@ -788,18 +794,67 @@ function getBestDamageSpell() { function kickstartChanneling() { if (STATE.channeling) return null; - // Priority: cast or refresh a buff with a good MP cost for proc chance - // Regen has 17 MP = best cost/proc ratio among buffs we always want active + // Priority: cast or refresh a buff with a good MP cost for proc chance. + // Higher MP cost = higher channeling proc chance. + // Regen/Absorb/Heartseeker have the best cost/proc ratio among buffs. if (STATE.mp > 0.20) { - // Try buffs in priority order — Regen is best for sustain + channel proc + // Collect all buffs that need refreshing, sorted by MP cost descending + const candidates = []; for (const b of BUFF_PRIORITY) { const dur = buffDuration(b.icon); - // Only refresh if expiring soon (< 5 turns) or missing if ((dur === 0 || dur < 5) && STATE.mp > 0.15 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; + const sp = findSpell(b.spell); + candidates.push({ ...b, mpCost: sp ? sp.mp : 0 }); } } + if (candidates.length > 0) { + candidates.sort((a, b) => b.mpCost - a.mpCost); + return { type: 'spell', name: candidates[0].spell, selfTarget: true }; + } + } + + return null; +} + +// ── Channeling consumption — pick the buff that benefits most from being free ── +// Channeling gives: 1 MP cost + 50% longer duration. +// The best target is the buff where: +// - MP cost is high (more savings from the free cast) +// - Remaining duration is low (the 50% extension won't be wasted) +// Score formula: mp_cost / (dur + 1) +// - Expiring now (dur=0): cost × 1.0 — best candidate +// - Expiring soon (dur=5): cost × 0.17 — good +// - Fresh (dur=80): cost × 0.01 — barely worth refreshing +// No arbitrary cutoffs — every buff is a candidate, scored continuously. +function findBestChannelingTarget() { + if (!STATE.channeling || STATE.mp < 0.10) return null; + + const candidates = []; + for (const b of BUFF_PRIORITY) { + if (!hasSpell(b.spell)) continue; + const dur = buffDuration(b.icon); + if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; + // Skip completely fresh Heartseeker (> 50 turns means just cast) + if (b.icon === 'heartseeker' && dur > 50) continue; + const sp = findSpell(b.spell); + const mpCost = sp ? sp.mp : 0; + // Score: higher MP cost + lower remaining turns = better channeling target + const score = mpCost / (dur + 1); + candidates.push({ ...b, mpCost, dur, score }); + } + + if (candidates.length > 0) { + candidates.sort((a, b) => b.score - a.score); + return { type: 'spell', name: candidates[0].spell, selfTarget: true }; + } + + // Fallback: cheap damage spell rather than waste the charge on basic attack + const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']); + if (cheap) { + const t = findWeakestMonster(); + if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) + return { type: 'spell', name: cheap, target: t }; } return null; @@ -931,23 +986,10 @@ function strategyNovice() { if (k) return k; } - // If channeling is charged, consume it on a useful spell - if (STATE.channeling && STATE.mp > 0.10) { - for (const b of BUFF_PRIORITY) { - if (hasSpell(b.spell)) { - const dur = buffDuration(b.icon); - if (dur === 0 || dur < 10) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; - } - } - } - const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']); - if (cheap) { - const t = findWeakestMonster(); - if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) - return { type: 'spell', name: cheap, target: t }; - } + // If channeling is charged, consume it on the highest-MP buff that needs refreshing + { + const ch = findBestChannelingTarget(); + if (ch) return ch; } const t = findWeakestMonster(); @@ -1066,23 +1108,10 @@ function strategyAdept() { if (k) return k; } - // Use channeling charge on a spell - if (STATE.channeling && STATE.mp > 0.10) { - for (const b of BUFF_PRIORITY) { - if (hasSpell(b.spell)) { - const dur = buffDuration(b.icon); - if (dur === 0 || dur < 10) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; - } - } - } - const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']); - if (cheap) { - const t = findWeakestMonster(); - if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) - return { type: 'spell', name: cheap, target: t }; - } + // Use channeling charge on the highest-MP buff that needs refreshing + { + const ch = findBestChannelingTarget(); + if (ch) return ch; } const t = findWeakestMonster(); @@ -1213,23 +1242,10 @@ function strategyVeteran() { if (k) return k; } - // Consume channeling on a spell - if (STATE.channeling && STATE.mp > 0.10) { - for (const b of BUFF_PRIORITY) { - if (hasSpell(b.spell)) { - const dur = buffDuration(b.icon); - if (dur === 0 || dur < 10) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; - } - } - } - const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']); - if (cheap) { - const t = findWeakestMonster(); - if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) - return { type: 'spell', name: cheap, target: t }; - } + // Consume channeling on the highest-MP buff that needs refreshing + { + const ch = findBestChannelingTarget(); + if (ch) return ch; } const t = findWeakestMonster(); diff --git a/scripts/latest.user.js b/scripts/latest.user.js index 5171948..2cd5654 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -605,6 +605,8 @@ const KB = { { lvl: 60, name: 'Haste' }, { lvl: 70, name: 'Weaken' }, { lvl: 130, name: 'Imperil' }, + { lvl: 140, name: 'Heartseeker' }, + { lvl: 175, name: 'Arcane Focus' }, { lvl: 220, name: 'Full-Cure' }, ], @@ -639,6 +641,7 @@ const KB = { { lvl: 70, tip: 'Weaken: -50% enemy damage. Use items, then Cure.' }, { lvl: 100, tip: 'Level 100! T2 spells available. Accuracy target: 150%.' }, { lvl: 130, tip: 'Imperil — THE most important debuff. Always cast first.' }, + { lvl: 140, tip: 'Heartseeker — +25% physical damage, +10% crit. Self-buff, priority over Arcane Focus for 2H.' }, { lvl: 300, tip: 'Master tier. Optimize for speed. Run PFUDOR.' }, ], }; @@ -676,13 +679,16 @@ function shouldBuff(name, minTurns) { } // Known buffs with their priority, icon name, spell name, and refresh threshold +// NOTE: Heartseeker and Arcane Focus are mutually exclusive. For a 2H (physical) build, +// Heartseeker is preferred (+25% phys dmg, +10% crit). Arcane Focus is for magic builds. const BUFF_PRIORITY = [ - { icon: 'haste', spell: 'Haste', minTurns: 2 }, - { icon: 'protection', spell: 'Protection', minTurns: 2 }, - { icon: 'sparklife', spell: 'Spark of Life', minTurns: 1 }, // icon: sparklife.png - { icon: 'regen', spell: 'Regen', minTurns: 2 }, - { icon: 'absorb', spell: 'Absorb', minTurns: 2 }, - { icon: 'shadowveil', spell: 'Shadow Veil', minTurns: 3 }, + { icon: 'haste', spell: 'Haste', minTurns: 4 }, // 100% uptime: refresh well before expiry + { icon: 'protection', spell: 'Protection', minTurns: 4 }, + { icon: 'sparklife', spell: 'Spark of Life', minTurns: 3 }, // icon: sparklife.png + { icon: 'heartseeker', spell: 'Heartseeker', minTurns: 6 }, // Long duration, refresh at 6 to avoid gap + { icon: 'regen', spell: 'Regen', minTurns: 4 }, + { icon: 'absorb', spell: 'Absorb', minTurns: 4 }, + { icon: 'shadowveil', spell: 'Shadow Veil', minTurns: 4 }, ]; // Known important monster names (bosses, legendaries, ultimates) @@ -788,18 +794,67 @@ function getBestDamageSpell() { function kickstartChanneling() { if (STATE.channeling) return null; - // Priority: cast or refresh a buff with a good MP cost for proc chance - // Regen has 17 MP = best cost/proc ratio among buffs we always want active + // Priority: cast or refresh a buff with a good MP cost for proc chance. + // Higher MP cost = higher channeling proc chance. + // Regen/Absorb/Heartseeker have the best cost/proc ratio among buffs. if (STATE.mp > 0.20) { - // Try buffs in priority order — Regen is best for sustain + channel proc + // Collect all buffs that need refreshing, sorted by MP cost descending + const candidates = []; for (const b of BUFF_PRIORITY) { const dur = buffDuration(b.icon); - // Only refresh if expiring soon (< 5 turns) or missing if ((dur === 0 || dur < 5) && STATE.mp > 0.15 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; + const sp = findSpell(b.spell); + candidates.push({ ...b, mpCost: sp ? sp.mp : 0 }); } } + if (candidates.length > 0) { + candidates.sort((a, b) => b.mpCost - a.mpCost); + return { type: 'spell', name: candidates[0].spell, selfTarget: true }; + } + } + + return null; +} + +// ── Channeling consumption — pick the buff that benefits most from being free ── +// Channeling gives: 1 MP cost + 50% longer duration. +// The best target is the buff where: +// - MP cost is high (more savings from the free cast) +// - Remaining duration is low (the 50% extension won't be wasted) +// Score formula: mp_cost / (dur + 1) +// - Expiring now (dur=0): cost × 1.0 — best candidate +// - Expiring soon (dur=5): cost × 0.17 — good +// - Fresh (dur=80): cost × 0.01 — barely worth refreshing +// No arbitrary cutoffs — every buff is a candidate, scored continuously. +function findBestChannelingTarget() { + if (!STATE.channeling || STATE.mp < 0.10) return null; + + const candidates = []; + for (const b of BUFF_PRIORITY) { + if (!hasSpell(b.spell)) continue; + const dur = buffDuration(b.icon); + if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; + // Skip completely fresh Heartseeker (> 50 turns means just cast) + if (b.icon === 'heartseeker' && dur > 50) continue; + const sp = findSpell(b.spell); + const mpCost = sp ? sp.mp : 0; + // Score: higher MP cost + lower remaining turns = better channeling target + const score = mpCost / (dur + 1); + candidates.push({ ...b, mpCost, dur, score }); + } + + if (candidates.length > 0) { + candidates.sort((a, b) => b.score - a.score); + return { type: 'spell', name: candidates[0].spell, selfTarget: true }; + } + + // Fallback: cheap damage spell rather than waste the charge on basic attack + const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']); + if (cheap) { + const t = findWeakestMonster(); + if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) + return { type: 'spell', name: cheap, target: t }; } return null; @@ -931,23 +986,10 @@ function strategyNovice() { if (k) return k; } - // If channeling is charged, consume it on a useful spell - if (STATE.channeling && STATE.mp > 0.10) { - for (const b of BUFF_PRIORITY) { - if (hasSpell(b.spell)) { - const dur = buffDuration(b.icon); - if (dur === 0 || dur < 10) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; - } - } - } - const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']); - if (cheap) { - const t = findWeakestMonster(); - if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) - return { type: 'spell', name: cheap, target: t }; - } + // If channeling is charged, consume it on the highest-MP buff that needs refreshing + { + const ch = findBestChannelingTarget(); + if (ch) return ch; } const t = findWeakestMonster(); @@ -1066,23 +1108,10 @@ function strategyAdept() { if (k) return k; } - // Use channeling charge on a spell - if (STATE.channeling && STATE.mp > 0.10) { - for (const b of BUFF_PRIORITY) { - if (hasSpell(b.spell)) { - const dur = buffDuration(b.icon); - if (dur === 0 || dur < 10) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; - } - } - } - const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']); - if (cheap) { - const t = findWeakestMonster(); - if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) - return { type: 'spell', name: cheap, target: t }; - } + // Use channeling charge on the highest-MP buff that needs refreshing + { + const ch = findBestChannelingTarget(); + if (ch) return ch; } const t = findWeakestMonster(); @@ -1213,23 +1242,10 @@ function strategyVeteran() { if (k) return k; } - // Consume channeling on a spell - if (STATE.channeling && STATE.mp > 0.10) { - for (const b of BUFF_PRIORITY) { - if (hasSpell(b.spell)) { - const dur = buffDuration(b.icon); - if (dur === 0 || dur < 10) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; - } - } - } - const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']); - if (cheap) { - const t = findWeakestMonster(); - if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) - return { type: 'spell', name: cheap, target: t }; - } + // Consume channeling on the highest-MP buff that needs refreshing + { + const ch = findBestChannelingTarget(); + if (ch) return ch; } const t = findWeakestMonster(); diff --git a/src/config.js b/src/config.js index dbecee7..20f3a9c 100644 --- a/src/config.js +++ b/src/config.js @@ -2,7 +2,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.12.0'; +const VERSION = '0.13.0'; const CFG = { // — Battle automation diff --git a/src/header.user.js b/src/header.user.js index 85fa4b0..d5bac86 100644 --- a/src/header.user.js +++ b/src/header.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.12.0 +// @version 0.13.0 // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @author GaboGG + Hermes // @match *://*.hentaiverse.org/* diff --git a/src/knowledge-base.js b/src/knowledge-base.js index cffb041..085d905 100644 --- a/src/knowledge-base.js +++ b/src/knowledge-base.js @@ -44,6 +44,8 @@ const KB = { { lvl: 60, name: 'Haste' }, { lvl: 70, name: 'Weaken' }, { lvl: 130, name: 'Imperil' }, + { lvl: 140, name: 'Heartseeker' }, + { lvl: 175, name: 'Arcane Focus' }, { lvl: 220, name: 'Full-Cure' }, ], @@ -78,6 +80,7 @@ const KB = { { lvl: 70, tip: 'Weaken: -50% enemy damage. Use items, then Cure.' }, { lvl: 100, tip: 'Level 100! T2 spells available. Accuracy target: 150%.' }, { lvl: 130, tip: 'Imperil — THE most important debuff. Always cast first.' }, + { lvl: 140, tip: 'Heartseeker — +25% physical damage, +10% crit. Self-buff, priority over Arcane Focus for 2H.' }, { lvl: 300, tip: 'Master tier. Optimize for speed. Run PFUDOR.' }, ], }; diff --git a/src/strategy-engine.js b/src/strategy-engine.js index 9e37c72..051f8d9 100644 --- a/src/strategy-engine.js +++ b/src/strategy-engine.js @@ -31,13 +31,16 @@ function shouldBuff(name, minTurns) { } // Known buffs with their priority, icon name, spell name, and refresh threshold +// NOTE: Heartseeker and Arcane Focus are mutually exclusive. For a 2H (physical) build, +// Heartseeker is preferred (+25% phys dmg, +10% crit). Arcane Focus is for magic builds. const BUFF_PRIORITY = [ - { icon: 'haste', spell: 'Haste', minTurns: 2 }, - { icon: 'protection', spell: 'Protection', minTurns: 2 }, - { icon: 'sparklife', spell: 'Spark of Life', minTurns: 1 }, // icon: sparklife.png - { icon: 'regen', spell: 'Regen', minTurns: 2 }, - { icon: 'absorb', spell: 'Absorb', minTurns: 2 }, - { icon: 'shadowveil', spell: 'Shadow Veil', minTurns: 3 }, + { icon: 'haste', spell: 'Haste', minTurns: 4 }, // 100% uptime: refresh well before expiry + { icon: 'protection', spell: 'Protection', minTurns: 4 }, + { icon: 'sparklife', spell: 'Spark of Life', minTurns: 3 }, // icon: sparklife.png + { icon: 'heartseeker', spell: 'Heartseeker', minTurns: 6 }, // Long duration, refresh at 6 to avoid gap + { icon: 'regen', spell: 'Regen', minTurns: 4 }, + { icon: 'absorb', spell: 'Absorb', minTurns: 4 }, + { icon: 'shadowveil', spell: 'Shadow Veil', minTurns: 4 }, ]; // Known important monster names (bosses, legendaries, ultimates) @@ -143,18 +146,67 @@ function getBestDamageSpell() { function kickstartChanneling() { if (STATE.channeling) return null; - // Priority: cast or refresh a buff with a good MP cost for proc chance - // Regen has 17 MP = best cost/proc ratio among buffs we always want active + // Priority: cast or refresh a buff with a good MP cost for proc chance. + // Higher MP cost = higher channeling proc chance. + // Regen/Absorb/Heartseeker have the best cost/proc ratio among buffs. if (STATE.mp > 0.20) { - // Try buffs in priority order — Regen is best for sustain + channel proc + // Collect all buffs that need refreshing, sorted by MP cost descending + const candidates = []; for (const b of BUFF_PRIORITY) { const dur = buffDuration(b.icon); - // Only refresh if expiring soon (< 5 turns) or missing if ((dur === 0 || dur < 5) && STATE.mp > 0.15 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; + const sp = findSpell(b.spell); + candidates.push({ ...b, mpCost: sp ? sp.mp : 0 }); } } + if (candidates.length > 0) { + candidates.sort((a, b) => b.mpCost - a.mpCost); + return { type: 'spell', name: candidates[0].spell, selfTarget: true }; + } + } + + return null; +} + +// ── Channeling consumption — pick the buff that benefits most from being free ── +// Channeling gives: 1 MP cost + 50% longer duration. +// The best target is the buff where: +// - MP cost is high (more savings from the free cast) +// - Remaining duration is low (the 50% extension won't be wasted) +// Score formula: mp_cost / (dur + 1) +// - Expiring now (dur=0): cost × 1.0 — best candidate +// - Expiring soon (dur=5): cost × 0.17 — good +// - Fresh (dur=80): cost × 0.01 — barely worth refreshing +// No arbitrary cutoffs — every buff is a candidate, scored continuously. +function findBestChannelingTarget() { + if (!STATE.channeling || STATE.mp < 0.10) return null; + + const candidates = []; + for (const b of BUFF_PRIORITY) { + if (!hasSpell(b.spell)) continue; + const dur = buffDuration(b.icon); + if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; + // Skip completely fresh Heartseeker (> 50 turns means just cast) + if (b.icon === 'heartseeker' && dur > 50) continue; + const sp = findSpell(b.spell); + const mpCost = sp ? sp.mp : 0; + // Score: higher MP cost + lower remaining turns = better channeling target + const score = mpCost / (dur + 1); + candidates.push({ ...b, mpCost, dur, score }); + } + + if (candidates.length > 0) { + candidates.sort((a, b) => b.score - a.score); + return { type: 'spell', name: candidates[0].spell, selfTarget: true }; + } + + // Fallback: cheap damage spell rather than waste the charge on basic attack + const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']); + if (cheap) { + const t = findWeakestMonster(); + if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) + return { type: 'spell', name: cheap, target: t }; } return null; @@ -286,23 +338,10 @@ function strategyNovice() { if (k) return k; } - // If channeling is charged, consume it on a useful spell - if (STATE.channeling && STATE.mp > 0.10) { - for (const b of BUFF_PRIORITY) { - if (hasSpell(b.spell)) { - const dur = buffDuration(b.icon); - if (dur === 0 || dur < 10) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; - } - } - } - const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']); - if (cheap) { - const t = findWeakestMonster(); - if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) - return { type: 'spell', name: cheap, target: t }; - } + // If channeling is charged, consume it on the highest-MP buff that needs refreshing + { + const ch = findBestChannelingTarget(); + if (ch) return ch; } const t = findWeakestMonster(); @@ -421,23 +460,10 @@ function strategyAdept() { if (k) return k; } - // Use channeling charge on a spell - if (STATE.channeling && STATE.mp > 0.10) { - for (const b of BUFF_PRIORITY) { - if (hasSpell(b.spell)) { - const dur = buffDuration(b.icon); - if (dur === 0 || dur < 10) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; - } - } - } - const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']); - if (cheap) { - const t = findWeakestMonster(); - if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) - return { type: 'spell', name: cheap, target: t }; - } + // Use channeling charge on the highest-MP buff that needs refreshing + { + const ch = findBestChannelingTarget(); + if (ch) return ch; } const t = findWeakestMonster(); @@ -568,23 +594,10 @@ function strategyVeteran() { if (k) return k; } - // Consume channeling on a spell - if (STATE.channeling && STATE.mp > 0.10) { - for (const b of BUFF_PRIORITY) { - if (hasSpell(b.spell)) { - const dur = buffDuration(b.icon); - if (dur === 0 || dur < 10) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; - } - } - } - const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']); - if (cheap) { - const t = findWeakestMonster(); - if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) - return { type: 'spell', name: cheap, target: t }; - } + // Consume channeling on the highest-MP buff that needs refreshing + { + const ch = findBestChannelingTarget(); + if (ch) return ch; } const t = findWeakestMonster();