diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index fa8683a..c6ad7da 100644 --- a/scripts/hv-unified.user.js +++ b/scripts/hv-unified.user.js @@ -29,16 +29,16 @@ const CFG = { autoCure: true, autoSpirit: false, - // — Thresholds (benchmark-optimized) + // — Thresholds (simulation-backed) cureHP: 0.45, // Cast Cure below this HP cureItemHP: 0.50, // Use health items at this HP cureRegenHP: 0.65, // Cast Regen below this HP - manaGemMP: 0.70, // Use mana gems below this MP (aggressive, stay rich) - manaPotionMP: 0.50, // Use mana potions at this MP + manaGemMP: 0.45, // Use mana gems below this MP + manaPotionMP: 0.25, // Use mana potions at this MP spiritPotionSP: 0.30, // Use spirit gems/potions below this SP spiritStanceOC: 80, // Activate Spirit Stance late (save SP for skills) skillOC: 75, // Use weapon skills at this OC - useAttackSpells: true, // Use cheap spells for channeling kickstart + useAttackSpells: false, // Don't use attack spells (let channel proc naturally) // — Out of battle autoSell: true, @@ -778,40 +778,33 @@ function getBestDamageSpell() { } // ── Channeling maintenance ── - -// Cheapest mana-costing spells to trigger channeling when it wears off -const CHANNEL_TRIGGERS = ['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']; +// +// Channeling: procs randomly when casting any mana-costing spell. +// Proc chance = spell_cost / (base_mana * 1.2) +// Cannot proc from spells cast while already Channeling. +// Effect: next spell costs 1 MP and is 50% stronger (or 50% longer for buffs) +// Duration: 5 ticks (15 with Mystic Gem in P-slot) +// +// Best trigger spells: Regen/Absorb (17 MP = ~6% proc chance each cast) +// Cheaper spells have proportionally lower proc chance. function kickstartChanneling() { if (STATE.channeling) return null; - // Priority 1: refresh any buff that's about to expire (< 15 turns) - // This is more useful than wasting MP on attack spells - for (const b of BUFF_PRIORITY) { - const dur = buffDuration(b.icon); - if (dur > 0 && dur < 15 && STATE.mp > 0.15 && hasSpell(b.spell)) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; + // 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 + if (STATE.mp > 0.20) { + // Try buffs in priority order — Regen is best for sustain + channel proc + 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 }; + } } } - // Priority 2: cast a buff that's missing entirely - for (const b of BUFF_PRIORITY) { - if (buffDuration(b.icon) === 0 && STATE.mp > 0.15 && hasSpell(b.spell)) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; - } - } - - // Priority 3: cheapest attack spell to trigger channeling (only if attack spells enabled) - if (CFG.useAttackSpells) { - const spell = findDmgSpell(CHANNEL_TRIGGERS); - if (spell && STATE.mp > 0.10) { - const t = findWeakestMonster(); - if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) - return { type: 'spell', name: spell, target: t }; - } - } return null; } diff --git a/scripts/latest.user.js b/scripts/latest.user.js index fa8683a..c6ad7da 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -29,16 +29,16 @@ const CFG = { autoCure: true, autoSpirit: false, - // — Thresholds (benchmark-optimized) + // — Thresholds (simulation-backed) cureHP: 0.45, // Cast Cure below this HP cureItemHP: 0.50, // Use health items at this HP cureRegenHP: 0.65, // Cast Regen below this HP - manaGemMP: 0.70, // Use mana gems below this MP (aggressive, stay rich) - manaPotionMP: 0.50, // Use mana potions at this MP + manaGemMP: 0.45, // Use mana gems below this MP + manaPotionMP: 0.25, // Use mana potions at this MP spiritPotionSP: 0.30, // Use spirit gems/potions below this SP spiritStanceOC: 80, // Activate Spirit Stance late (save SP for skills) skillOC: 75, // Use weapon skills at this OC - useAttackSpells: true, // Use cheap spells for channeling kickstart + useAttackSpells: false, // Don't use attack spells (let channel proc naturally) // — Out of battle autoSell: true, @@ -778,40 +778,33 @@ function getBestDamageSpell() { } // ── Channeling maintenance ── - -// Cheapest mana-costing spells to trigger channeling when it wears off -const CHANNEL_TRIGGERS = ['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']; +// +// Channeling: procs randomly when casting any mana-costing spell. +// Proc chance = spell_cost / (base_mana * 1.2) +// Cannot proc from spells cast while already Channeling. +// Effect: next spell costs 1 MP and is 50% stronger (or 50% longer for buffs) +// Duration: 5 ticks (15 with Mystic Gem in P-slot) +// +// Best trigger spells: Regen/Absorb (17 MP = ~6% proc chance each cast) +// Cheaper spells have proportionally lower proc chance. function kickstartChanneling() { if (STATE.channeling) return null; - // Priority 1: refresh any buff that's about to expire (< 15 turns) - // This is more useful than wasting MP on attack spells - for (const b of BUFF_PRIORITY) { - const dur = buffDuration(b.icon); - if (dur > 0 && dur < 15 && STATE.mp > 0.15 && hasSpell(b.spell)) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; + // 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 + if (STATE.mp > 0.20) { + // Try buffs in priority order — Regen is best for sustain + channel proc + 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 }; + } } } - // Priority 2: cast a buff that's missing entirely - for (const b of BUFF_PRIORITY) { - if (buffDuration(b.icon) === 0 && STATE.mp > 0.15 && hasSpell(b.spell)) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; - } - } - - // Priority 3: cheapest attack spell to trigger channeling (only if attack spells enabled) - if (CFG.useAttackSpells) { - const spell = findDmgSpell(CHANNEL_TRIGGERS); - if (spell && STATE.mp > 0.10) { - const t = findWeakestMonster(); - if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) - return { type: 'spell', name: spell, target: t }; - } - } return null; } diff --git a/src/config.js b/src/config.js index f8909c3..27faed8 100644 --- a/src/config.js +++ b/src/config.js @@ -14,16 +14,16 @@ const CFG = { autoCure: true, autoSpirit: false, - // — Thresholds (benchmark-optimized) + // — Thresholds (simulation-backed) cureHP: 0.45, // Cast Cure below this HP cureItemHP: 0.50, // Use health items at this HP cureRegenHP: 0.65, // Cast Regen below this HP - manaGemMP: 0.70, // Use mana gems below this MP (aggressive, stay rich) - manaPotionMP: 0.50, // Use mana potions at this MP + manaGemMP: 0.45, // Use mana gems below this MP + manaPotionMP: 0.25, // Use mana potions at this MP spiritPotionSP: 0.30, // Use spirit gems/potions below this SP spiritStanceOC: 80, // Activate Spirit Stance late (save SP for skills) skillOC: 75, // Use weapon skills at this OC - useAttackSpells: true, // Use cheap spells for channeling kickstart + useAttackSpells: false, // Don't use attack spells (let channel proc naturally) // — Out of battle autoSell: true, diff --git a/src/strategy-engine.js b/src/strategy-engine.js index 2036303..bda2152 100644 --- a/src/strategy-engine.js +++ b/src/strategy-engine.js @@ -133,40 +133,33 @@ function getBestDamageSpell() { } // ── Channeling maintenance ── - -// Cheapest mana-costing spells to trigger channeling when it wears off -const CHANNEL_TRIGGERS = ['Fiery Blast', 'Freeze', 'Shockblast', 'Gale']; +// +// Channeling: procs randomly when casting any mana-costing spell. +// Proc chance = spell_cost / (base_mana * 1.2) +// Cannot proc from spells cast while already Channeling. +// Effect: next spell costs 1 MP and is 50% stronger (or 50% longer for buffs) +// Duration: 5 ticks (15 with Mystic Gem in P-slot) +// +// Best trigger spells: Regen/Absorb (17 MP = ~6% proc chance each cast) +// Cheaper spells have proportionally lower proc chance. function kickstartChanneling() { if (STATE.channeling) return null; - // Priority 1: refresh any buff that's about to expire (< 15 turns) - // This is more useful than wasting MP on attack spells - for (const b of BUFF_PRIORITY) { - const dur = buffDuration(b.icon); - if (dur > 0 && dur < 15 && STATE.mp > 0.15 && hasSpell(b.spell)) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; + // 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 + if (STATE.mp > 0.20) { + // Try buffs in priority order — Regen is best for sustain + channel proc + 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 }; + } } } - // Priority 2: cast a buff that's missing entirely - for (const b of BUFF_PRIORITY) { - if (buffDuration(b.icon) === 0 && STATE.mp > 0.15 && hasSpell(b.spell)) { - if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; - return { type: 'spell', name: b.spell, selfTarget: true }; - } - } - - // Priority 3: cheapest attack spell to trigger channeling (only if attack spells enabled) - if (CFG.useAttackSpells) { - const spell = findDmgSpell(CHANNEL_TRIGGERS); - if (spell && STATE.mp > 0.10) { - const t = findWeakestMonster(); - if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) - return { type: 'spell', name: spell, target: t }; - } - } return null; }