Channeling works: proc on mana-costing spell -> next spell costs 1 MP and is 50% stronger/longer. The charge lasts 5 ticks (15 with Mystic Gem) then wears off and is wasted. Previously the script would basic attack with channeling charged, wasting the bonus. Now: - If channeling is active and nothing more urgent, still cast a buff (refresh anything under 10 turns) or a cheap damage spell - Never basic attack while channeling is charged - Novice/Adept/Veteran all have this protection
563 lines
22 KiB
JavaScript
563 lines
22 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════════
|
|
// STRATEGY ENGINE — tier-based action recommendation
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
// ── Spell helpers ──
|
|
|
|
function hasSpell(name) {
|
|
return STATE.spellsKnown.some(s => s.n === name);
|
|
}
|
|
|
|
function findSpell(name) {
|
|
return STATE.spellsKnown.find(s => s.n === name);
|
|
}
|
|
|
|
function findDmgSpell(list) {
|
|
const s = STATE.spellsKnown.find(sp => list.includes(sp.n));
|
|
return s ? s.n : null;
|
|
}
|
|
|
|
// ── Buff helpers ──
|
|
|
|
// Returns the buff duration from STATE.buffs, or 0 if not present
|
|
function buffDuration(name) {
|
|
return STATE.buffs[name] || 0;
|
|
}
|
|
|
|
// Should we cast this buff? Only if missing OR about to expire (< threshold turns)
|
|
function shouldBuff(name, minTurns) {
|
|
const dur = buffDuration(name);
|
|
return dur === 0 || dur < minTurns;
|
|
}
|
|
|
|
// Known buffs with their priority, icon name, spell name, and refresh threshold
|
|
const BUFF_PRIORITY = [
|
|
{ icon: 'haste', spell: 'Haste', minTurns: 2 },
|
|
{ icon: 'protection', spell: 'Protection', minTurns: 2 },
|
|
{ icon: 'spark_of_life', spell: 'Spark of Life', minTurns: 1 },
|
|
{ icon: 'regen', spell: 'Regen', minTurns: 2 },
|
|
{ icon: 'absorb', spell: 'Absorb', minTurns: 2 },
|
|
// Shadow Veil: icon key = lowercased spell name without spaces
|
|
{ icon: 'shadowveil', spell: 'Shadow Veil', minTurns: 3 },
|
|
];
|
|
|
|
// Known important monster names (bosses, legendaries, ultimates)
|
|
const RARE_MONSTERS = [
|
|
'manbearpig', 'white bunneh', 'mithra', 'dalek',
|
|
'konata', 'mikuru asahina', 'ryouko asakura', 'yuki nagato',
|
|
'skuld', 'urd', 'verdandi', 'yggdrasil',
|
|
'rhaegal', 'viserion', 'drogon',
|
|
'real life', 'invisible pink unicorn', 'flying spaghetti monster',
|
|
'recycled boss rush', 'bottomless dungeon', 'new game +',
|
|
'achievement grind', 'time trial mode', 'hardcore mode',
|
|
];
|
|
|
|
function isRareMonster(idx) {
|
|
const m = STATE.monsters[idx];
|
|
if (!m) {
|
|
// Fallback: check battle log names
|
|
return STATE.battleMonsterNames.some(name =>
|
|
RARE_MONSTERS.some(r => name.includes(r))
|
|
);
|
|
}
|
|
|
|
// Boss/rare monsters have distinct visual cues:
|
|
// 1. Gold border: style="border-color:#BD7400"
|
|
// 2. Gold background on label: style="background:#E6CCA3"
|
|
// 3. SP bar present (third btm5 child with nbarred.png)
|
|
const style = m.getAttribute('style') || '';
|
|
const inner = m.innerHTML || '';
|
|
|
|
// Check for gold border (boss indicator)
|
|
if (style.includes('BD7400') || style.includes('E6CCA3')) return true;
|
|
|
|
// Check for SP bar (only bosses have all 3 bars: HP, MP, SP)
|
|
if (inner.includes('nbarred.png')) return true;
|
|
|
|
// Fallback: check name from DOM
|
|
const n = m.querySelector('.btm3');
|
|
if (!n) return false;
|
|
const name = (n.textContent || '').replace(/^\d+\s*/, '').trim().toLowerCase();
|
|
// Use CSS text parser for boss names rendered in font
|
|
const cssName = readCSSText(n);
|
|
return RARE_MONSTERS.some(r => name.includes(r) || cssName.includes(r));
|
|
}
|
|
|
|
function anyRareMonster() {
|
|
return STATE.monsters.some((m, i) => isRareMonster(i));
|
|
}
|
|
|
|
function findWeakestMonster() {
|
|
let best = -1, bestW = Infinity;
|
|
STATE.monsters.forEach((m, i) => {
|
|
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
|
const hp = m.querySelector('img[src$="nbargreen.png"]');
|
|
const w = hp ? parseInt(hp.style.width || '') : 120;
|
|
if (w < bestW && w > 0) { bestW = w; best = i; }
|
|
});
|
|
return best >= 0 ? best : 0;
|
|
}
|
|
|
|
function findStrongestMonster() {
|
|
let best = -1, bestW = -1;
|
|
STATE.monsters.forEach((m, i) => {
|
|
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
|
const hp = m.querySelector('img[src$="nbargreen.png"]');
|
|
const w = hp ? parseInt(hp.style.width || '') : 120;
|
|
if (w > bestW) { bestW = w; best = i; }
|
|
});
|
|
return best >= 0 ? best : 0;
|
|
}
|
|
|
|
function checkMonsterDebuff(idx, db) {
|
|
const m = STATE.monsters[idx];
|
|
if (!m) return false;
|
|
const s = m.querySelector('.btm6');
|
|
if (!s) return false;
|
|
return s.innerHTML.toLowerCase().includes(db.toLowerCase() + '.png') ||
|
|
s.innerHTML.toLowerCase().includes('wpn_' + db.toLowerCase());
|
|
}
|
|
|
|
// ── Damage spell tier list ──
|
|
|
|
const SPELL_T3 = ['Ragnarok', 'Paradise Lost', 'Flames of Loki', 'Fimbulvetr', 'Wrath of Thor', 'Storms of Njord'];
|
|
const SPELL_T2 = ['Disintegrate', 'Banishment', 'Inferno', 'Blizzard', 'Chained Lightning', 'Downburst'];
|
|
const SPELL_T1 = ['Corruption', 'Smite', 'Fiery Blast', 'Freeze', 'Shockblast', 'Gale'];
|
|
|
|
function getBestDamageSpell() {
|
|
if (STATE.monsters.length >= 2) {
|
|
const aoe = findDmgSpell(SPELL_T3.concat(SPELL_T2));
|
|
if (aoe) return aoe;
|
|
}
|
|
return findDmgSpell(SPELL_T3.concat(SPELL_T2, SPELL_T1));
|
|
}
|
|
|
|
// ── Channeling maintenance ──
|
|
//
|
|
// 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: 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 };
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ── Core strategy: forum-corrected item-before-spell priority ──
|
|
//
|
|
// Priority order:
|
|
// 1. Mystic Gem if channeling is down (free OC gen)
|
|
// 2. Health/Mana/Spirit gems proactively (don't hoard)
|
|
// 3. Cure if HP critical
|
|
// 4. Buffs (Protection, Regen, Haste)
|
|
// 5. Debuffs (Imperil, Weaken)
|
|
// 6. Spirit Stance at OC 60-80% (don't waste OC on skills before stance)
|
|
// 7. Weapon skills only at high OC (80+) to let OC build
|
|
// 8. Damage spells
|
|
// 9. Basic attack builds OC naturally
|
|
|
|
function getRecommendedAction() {
|
|
parseBattleState();
|
|
if (STATE.tier === 'novice') return strategyNovice();
|
|
if (STATE.tier === 'adept') return strategyAdept();
|
|
return strategyVeteran(); // Veteran+ use the same engine
|
|
}
|
|
|
|
// Returns non-null ONLY when there's something smarter than basic attack
|
|
function getSmartAction() {
|
|
const a = getRecommendedAction();
|
|
if (!a || a.type === 'attack') return null;
|
|
return a;
|
|
}
|
|
|
|
// ── Novice (1-50): survival first ──
|
|
|
|
function strategyNovice() {
|
|
// 0. Mystic Gem for channeling — free OC
|
|
// (channeling status is tracked from the battle log)
|
|
const gem = hasUsableGem('channel');
|
|
if (gem && !STATE.channeling)
|
|
return { type: 'item', id: gem.id, selfTarget: true };
|
|
|
|
// 1. Gems proactively — don't hoard
|
|
if (STATE.hp < CFG.cureItemHP) {
|
|
const gem = hasUsableGem('heal');
|
|
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
|
}
|
|
// Mana gem — use when MP < 50%
|
|
if (STATE.mp < CFG.manaGemMP) {
|
|
const gem = hasUsableGem('mana');
|
|
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
|
}
|
|
// Spirit gem — use when SP is low (keeps Spirit Stance available)
|
|
if (STATE.sp < CFG.spiritPotionSP) {
|
|
const gem = hasUsableGem('spirit');
|
|
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
|
}
|
|
|
|
// 2. Cure only if items aren't available and HP is critical
|
|
if (STATE.hp < CFG.cureHP) {
|
|
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
|
if (c) return { type: 'spell', name: c, selfTarget: true };
|
|
}
|
|
|
|
// 3. Buffs — cast if missing or about to expire
|
|
for (const b of BUFF_PRIORITY) {
|
|
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) {
|
|
// Regen is hp-dependent: only cast if HP is below threshold
|
|
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
|
|
return { type: 'spell', name: b.spell, selfTarget: true };
|
|
}
|
|
}
|
|
|
|
// 5. Spirit Stance at OC 60 — but only if we have SP to sustain it
|
|
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
|
return { type: 'toggle_spirit' };
|
|
|
|
// 6. Weapon skills only at OC thresholds with specific conditions
|
|
const skillOC = CFG.skillOC || 80;
|
|
if (STATE.oc >= skillOC) {
|
|
// Great Cleave: boss/rare fights only
|
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
|
const t = findStrongestMonster();
|
|
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
|
}
|
|
// Rending Blow: AoE armor pen vs 5+ enemies
|
|
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
|
const t = findStrongestMonster();
|
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
|
}
|
|
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
|
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
|
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
|
if (hasArmorBreak) {
|
|
const t = findStrongestMonster();
|
|
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
|
}
|
|
}
|
|
}
|
|
|
|
// 6. Damage spells (reserve MP for Cure)
|
|
const dmg = CFG.useAttackSpells ? findDmgSpell(SPELL_T1) : null;
|
|
if (dmg && STATE.mp > 0.25) {
|
|
const cure = findSpell('Cure') || findSpell('Full-Cure');
|
|
const cureCost = cure ? (cure.mp / 100) * (STATE.level || 1) : 4;
|
|
const mpMax = STATE.mp * 100;
|
|
const reserve = Math.max(0.15, cureCost / mpMax);
|
|
if (STATE.mp - reserve > 0.25) {
|
|
const t = findWeakestMonster();
|
|
if (t >= 0) return { type: 'spell', name: dmg, target: t };
|
|
}
|
|
}
|
|
|
|
// 7. Channeling kickstart or basic attack
|
|
{
|
|
const k = kickstartChanneling();
|
|
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 };
|
|
}
|
|
}
|
|
|
|
const t = findWeakestMonster();
|
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
|
return { type: 'attack', target: t };
|
|
return null;
|
|
}
|
|
|
|
// ── Adept (50-150): building power ──
|
|
|
|
function strategyAdept() {
|
|
// 0. Mystic Gem for channeling — free OC
|
|
const gem = hasUsableGem('channel');
|
|
if (gem && !STATE.channeling)
|
|
return { type: 'item', id: gem.id, selfTarget: true };
|
|
|
|
// 1. Gems proactively — don't hoard
|
|
if (STATE.hp < CFG.cureItemHP) {
|
|
const gem = hasUsableGem('heal');
|
|
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
|
}
|
|
if (STATE.mp < CFG.manaGemMP) {
|
|
const gem = hasUsableGem('mana');
|
|
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
|
}
|
|
// Spirit gem — use when SP is low
|
|
if (STATE.sp < CFG.spiritPotionSP) {
|
|
const gem = hasUsableGem('spirit');
|
|
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
|
}
|
|
|
|
// 2. Cure
|
|
if (STATE.hp < CFG.cureHP) {
|
|
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
|
if (c) return { type: 'spell', name: c, selfTarget: true };
|
|
}
|
|
|
|
// 3. Buffs — prioritize by remaining duration (lowest = most urgent)
|
|
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;
|
|
return { type: 'spell', name: b.spell, selfTarget: true };
|
|
}
|
|
}
|
|
|
|
// 4. Debuffs — only on important fights (rare/boss/ultimate monsters)
|
|
// In random mob fights, just attack through them
|
|
if (anyRareMonster() || STATE.monsters.length <= 2) {
|
|
if (hasSpell('Imperil')) {
|
|
const b = findStrongestMonster();
|
|
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
|
return { type: 'spell', name: 'Imperil', target: b };
|
|
}
|
|
if (hasSpell('Weaken')) {
|
|
const b = findStrongestMonster();
|
|
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
|
return { type: 'spell', name: 'Weaken', target: b };
|
|
}
|
|
}
|
|
|
|
// 5. Weapon skills — before spirit stance
|
|
const skillOC_N = CFG.skillOC || 75;
|
|
if (STATE.oc >= skillOC_N) {
|
|
// Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest)
|
|
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
|
const t = findStrongestMonster();
|
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
|
}
|
|
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
|
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
|
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
|
if (hasArmorBreak) {
|
|
const t = findStrongestMonster();
|
|
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
|
}
|
|
}
|
|
// Great Cleave: boss/rare fights only (single target, lower priority than AoE)
|
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
|
const t = findStrongestMonster();
|
|
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
|
}
|
|
}
|
|
|
|
// 6. Spirit Stance at OC 60 — only if we have enough SP to sustain it
|
|
// Costs 1 SP + 10% OC per round (wiki formula)
|
|
// Minimum 40% SP = ~20 rounds of sustain before we run out
|
|
// This prevents wasting a turn toggling on/off when SP is low
|
|
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.40 && !STATE._spiritCooldown) {
|
|
STATE._spiritCooldown = 3;
|
|
return { type: 'toggle_spirit' };
|
|
}
|
|
// Toggle off when SP is critically low
|
|
if (STATE.sp < 0.03 && STATE.spiritStance && !STATE._spiritCooldown) {
|
|
STATE._spiritCooldown = 3;
|
|
return { type: 'toggle_spirit' };
|
|
}
|
|
if (STATE._spiritCooldown > 0) STATE._spiritCooldown--;
|
|
|
|
// 7. Damage spells
|
|
const dmg = CFG.useAttackSpells ? getBestDamageSpell() : null;
|
|
if (dmg && STATE.mp > 0.2) {
|
|
const t = STATE.monsters.length > 1 ? findWeakestMonster() : findStrongestMonster();
|
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
|
return { type: 'spell', name: dmg, target: t };
|
|
}
|
|
|
|
// 8. Channeling kickstart or basic attack
|
|
{
|
|
const k = kickstartChanneling();
|
|
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 };
|
|
}
|
|
}
|
|
|
|
const t = findWeakestMonster();
|
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
|
return { type: 'attack', target: t };
|
|
return null;
|
|
}
|
|
|
|
// ── Veteran (150-300) / Master (300+): full rotation ──
|
|
|
|
function strategyVeteran() {
|
|
// 0. Mystic Gem for channeling
|
|
const gem = hasUsableGem('channel');
|
|
if (gem && !STATE.channeling)
|
|
return { type: 'item', id: gem.id, selfTarget: true };
|
|
|
|
// 1. Gems proactively — don't hoard, use at reasonable thresholds
|
|
if (STATE.hp < 0.60) {
|
|
const gem = hasUsableGem('heal');
|
|
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
|
}
|
|
if (STATE.mp < CFG.manaGemMP) {
|
|
const gem = hasUsableGem('mana');
|
|
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
|
}
|
|
if (STATE.sp < CFG.spiritPotionSP) {
|
|
const gem = hasUsableGem('spirit');
|
|
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
|
}
|
|
|
|
// 2. Cure
|
|
if (STATE.hp < CFG.cureHP) {
|
|
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
|
if (c) return { type: 'spell', name: c, selfTarget: true };
|
|
}
|
|
|
|
// 3. Buffs — prioritize by remaining duration
|
|
if (CFG.autoBuff) {
|
|
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;
|
|
return { type: 'spell', name: b.spell, selfTarget: true };
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Debuffs — only on important fights (rare/boss)
|
|
if (CFG.autoDebuff && STATE.mp > 0.2 && (anyRareMonster() || STATE.monsters.length <= 2)) {
|
|
if (hasSpell('Imperil')) {
|
|
const b = findStrongestMonster();
|
|
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
|
return { type: 'spell', name: 'Imperil', target: b };
|
|
}
|
|
if (hasSpell('Weaken')) {
|
|
const b = findStrongestMonster();
|
|
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
|
return { type: 'spell', name: 'Weaken', target: b };
|
|
}
|
|
}
|
|
|
|
// 5. Spirit Stance at OC 60
|
|
if (CFG.autoSpirit && STATE.oc >= CFG.spiritStanceOC && !STATE.spiritStance && STATE.sp > 0.10)
|
|
return { type: 'toggle_spirit' };
|
|
if (STATE.sp < 0.03 && STATE.spiritStance)
|
|
return { type: 'toggle_spirit' };
|
|
|
|
// 6. Weapon skills only at OC thresholds (let OC build via basic attacks)
|
|
const askillOC3 = CFG.skillOC || 80;
|
|
if (STATE.oc >= askillOC3) {
|
|
// Great Cleave: boss/rare fights only
|
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
|
const t = findStrongestMonster();
|
|
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
|
}
|
|
// Rending Blow + Shatter Strike: AoE vs 5+ enemies
|
|
if (STATE.monsters.length >= 5) {
|
|
if (STATE.skillsKnown.includes('Rending Blow')) {
|
|
const t = findStrongestMonster();
|
|
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
|
}
|
|
if (STATE.skillsKnown.includes('Shatter Strike')) {
|
|
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
|
if (hasArmorBreak) {
|
|
const t = findStrongestMonster();
|
|
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
|
}
|
|
}
|
|
}
|
|
// Other weapon skills (non-2H): generic use
|
|
if (!STATE.skillsKnown.includes('Great Cleave')) {
|
|
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
|
|
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
|
|
for (const sk of ps) {
|
|
if (STATE.skillsKnown.includes(sk)) {
|
|
const t = findStrongestMonster();
|
|
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 7. Damage spells
|
|
const dmg = CFG.useAttackSpells ? getBestDamageSpell() : null;
|
|
if (dmg && STATE.mp > 0.15) {
|
|
const t = STATE.monsters.length > 1 ? findWeakestMonster() : 0;
|
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
|
return { type: 'spell', name: dmg, target: t };
|
|
}
|
|
|
|
// 8. Channeling kickstart or basic attack
|
|
{
|
|
const k = kickstartChanneling();
|
|
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 };
|
|
}
|
|
}
|
|
|
|
const t = findWeakestMonster();
|
|
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
|
return { type: 'attack', target: t };
|
|
return null;
|
|
}
|