v0.13.4 - Round 1 buff-up: cheapest first, channeling on most expensive

- Added castInitialBuffs() that fires when 60%+ buffs are down (first round)
  - Without channeling: casts cheapest buff first (more casts = more procs)
  - With channeling: casts most expensive remaining buff (best value for free cast)
- Added isFirstRoundBuffs() helper: checks if most buffs are missing
- Injected into all three tiers (Novice, Adept, Veteran) before the
  regular BUFF_PRIORITY loop
- Added STATE.battleRound counter for future round tracking
This commit is contained in:
GaboGG 2026-07-26 13:13:48 -04:00
parent badfcde640
commit 5f61a5c4fc
4 changed files with 180 additions and 0 deletions

View file

@ -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;

View file

@ -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;

View file

@ -12,6 +12,7 @@ const STATE = {
battleMode: '',
inBattle: false,
battleInitialized: false,
battleRound: 0, // Incremented each round for first-round logic
hoverTarget: -1,
interruptHover: false,

View file

@ -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;