// ═══════════════════════════════════════════════════════════════════════ // STRATEGY ENGINE — tier-based action recommendation // ═══════════════════════════════════════════════════════════════════════ // ── Spell helpers ── function hasSpell(name) { return STATE.spellsKnown.some(s => s.n === name); } // Check if a spell is not on cooldown (has onclick in the DOM AND inner img isn't dimmed) function hasReadySpell(name) { const el = $$('.btsd, .btqs').find(e => (e.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${name}'`)); if (!el) return false; if (!el.hasAttribute('onclick')) return false; // Double-check: the inner img might have opacity:0.5 (cooldown tint) // even if the parent has onclick from a previous parse const img = el.querySelector('img.btqi, img'); if (img && img.style.opacity === '0.5') return false; return true; } 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 // Threat detection: SP bar max is 120px, monsters >85% are about to special const THREAT_PX = Math.round(120 * 85 / 100); // ~102px // 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: 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) 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" // NOTE: At high levels/difficulties, normal monsters also get SP bars, // so we can't use nbarred.png as a boss indicator anymore. const style = m.getAttribute('style') || ''; const inner = m.innerHTML || ''; // Check for gold border (boss indicator — still reliable) if (style.includes('BD7400') || style.includes('E6CCA3')) 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; } // Find the monster with the highest SP/OC bar (most dangerous — about to special) // The red bar (nbarred.png) has a max width of 120px // Returns the index, or 0 if none found function findDangerousMonster() { let best = -1, bestSP = -1; STATE.monsters.forEach((m, i) => { if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return; const sp = STATE.monsterSp[i] || 0; if (sp > bestSP) { bestSP = sp; best = i; } }); return best >= 0 ? best : 0; } // Check if there are 2+ monsters with SP bar > threshold (imminent special attacks) function hasHighThreat(threshold) { let count = 0; STATE.monsters.forEach((m, i) => { if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return; const sp = STATE.monsterSp[i] || 0; if (sp > threshold) count++; }); return count >= 2; } // Find optimal target for Domino Strike splash (2H passive) — center-aligned // For 2H builds, Domino Strike splashes up to 2 enemies on each side of the target. // Attacking edge monsters wastes 50% of the splash potential. function findOptimalDominoTarget() { const total = STATE.monsters.length; if (total <= 2) return findWeakestMonster(); let best = -1; let bestScore = -1; STATE.monsters.forEach((m, i) => { if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return; let splash = 0; if (i - 1 >= 0 && STATE.monsters[i - 1] && STATE.monsters[i - 1].hasAttribute('onclick')) splash++; if (i - 2 >= 0 && STATE.monsters[i - 2] && STATE.monsters[i - 2].hasAttribute('onclick')) splash++; if (i + 1 < total && STATE.monsters[i + 1] && STATE.monsters[i + 1].hasAttribute('onclick')) splash++; if (i + 2 < total && STATE.monsters[i + 2] && STATE.monsters[i + 2].hasAttribute('onclick')) splash++; if (splash > bestScore) { bestScore = splash; 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. // 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 (!hasReadySpell(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; // Log which mode we're in if (STATE.channeling) { // Channeling is charged — cast the MOST expensive buff for best value candidates.sort((a, b) => b.mpCost - a.mpCost); return { type: 'spell', name: candidates[0].spell, selfTarget: true, _reason: 'initCh' }; } 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, _reason: 'init' }; } } function kickstartChanneling() { if (STATE.channeling) return null; // 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) { // Collect all buffs that need refreshing, sorted by MP cost descending const candidates = []; for (const b of BUFF_PRIORITY) { const dur = buffDuration(b.icon); if ((dur === 0 || dur < 5) && STATE.mp > 0.15 && hasSpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; const sp = findSpell(b.spell); candidates.push({ ...b, mpCost: sp ? sp.mp : 0 }); } } if (candidates.length > 0) { // Cheapest first to maximize channeling proc chance candidates.sort((a, b) => a.mpCost - b.mpCost); return { type: 'spell', name: candidates[0].spell, selfTarget: true }; } } return null; } // ── Channeling consumption — pick the best value from fill level × cost ── // Channeling gives: 1 MP cost + 50% longer duration. // The best target is the buff where: // - It's nearly empty (close to expiring relative to its max duration) // - It costs a lot of MP (more savings from the free cast) // // Score = emptiess_ratio × mp_cost // = (max_dur - current) / max_dur × mp_cost // // A 100-MP buff with 90 turns max at 1 turn remaining: (90-1)/90 * 100 = 98.9 // A 40-MP buff with 50 turns max at 25 remaining: (50-25)/50 * 40 = 20.0 // A 140-MP buff with 80 turns max at 71 remaining: (80-71)/80 * 140 = 15.75 // A freshly cast buff always scores near 0. // // max_dur is tracked dynamically per buff in STATE.maxBuffDur. // It updates each time we see a new high, learning your actual duration. function findBestChannelingTarget() { if (!STATE.channeling || STATE.mp < 0.10) return null; const candidates = []; for (const b of BUFF_PRIORITY) { if (!hasReadySpell(b.spell)) continue; const dur = buffDuration(b.icon); if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; const sp = findSpell(b.spell); // Use cached base MP cost (snapshotted at battle start from the magic pane) // This avoids the channeling 1-MP display issue and Mana Conservation fluctuation let mpCost = STATE._baseMpCosts[b.spell] || 0; if (mpCost === 0 && sp) { // Not cached yet — read from magic pane directly const paneEl = $$('.btsd[onmouseover*="' + b.spell.replace(/'/g, "\\'") + '"]', document.getElementById('pane_magic'))[0]; if (paneEl) { const costM = (paneEl.getAttribute('onmouseover') || '') .match(/set_infopane_spell\('[^']+',\s*'[^']*',\s*'[^']*',\s*(\d+)/); if (costM) mpCost = parseInt(costM[1]); } if (mpCost === 0) mpCost = sp.mp; // last resort: use what parseBattleState gave us STATE._baseMpCosts[b.spell] = mpCost; } // Skip if we can't afford the base cost — game pre-checks even when // channeling makes the effective cost 1 MP const curMp = parseInt((document.getElementById('vrm') || {}).textContent) || 0; const estMaxMp = (STATE.mp > 0 && curMp > 0) ? Math.round(curMp / STATE.mp) : 414; if (mpCost > 0 && curMp > 0 && mpCost > curMp) continue; // Fill level: how empty is this buff relative to its max observed duration let maxDur = STATE.maxBuffDur[b.icon] || 0; // If max matches current (first observation, no learning yet), assume min 30 turns if (maxDur <= dur) maxDur = Math.max(dur + 1, 30); const emptiness = maxDur > 0 ? Math.min(1, (maxDur - dur) / maxDur) : 1; // Score: emptiness × mp_cost — an empty expensive buff is best const score = emptiness * mpCost; candidates.push({ ...b, mpCost, dur, score, emptiness, maxDur }); } if (candidates.length > 0) { candidates.sort((a, b) => b.score - a.score); console.log(`%c[HV] ch score: ${candidates[0].spell}=${candidates[0].score.toFixed(1)} (dur=${candidates[0].dur}, max=${candidates[0].maxDur}, mp=${candidates[0].mpCost}, empty=${(candidates[0].emptiness*100).toFixed(0)}%)`, 'color:#bb0'); // Only use a buff if it actually needs refreshing (score >= 30 means // e.g. 30% emptiness on a 100-MP spell, or 60% on a 50-MP spell) if (candidates[0].score >= 30) { return { type: 'spell', name: candidates[0].spell, selfTarget: true, _reason: `ch score ${candidates[0].score.toFixed(0)}` }; } } // Fallback: for physical 2H builds, don't waste channeling on weak damage spells. // Channeling improves the next spell, but Fiery Blast does negligible damage at IWBTH. // Better to use the charge on a basic attack that procs Domino Strike and Penetrated Armor. const isPhysical2H = STATE.skillsKnown.includes('Great Cleave') || STATE.skillsKnown.includes('Rending Blow'); if (isPhysical2H) { return null; // Let channeling expire naturally — basic attacks do more than Fiery Blast } // Mage fallback: use any cheap spell for mage builds const cheap = findDmgSpell(['Fiery Blast', 'Freeze', 'Shockblast', 'Gale', 'Smite', 'Corruption', 'Inferno', 'Blizzard', 'Chained Lightning', 'Downburst', 'Banishment', 'Disintegrate']); if (cheap) { const t = findWeakestMonster(); if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) return { type: 'spell', name: cheap, target: t }; } // Last resort: any spell we know that costs MP for (const sp of STATE.spellsKnown) { if (sp.mp > 0 && sp.mp < (STATE.mp * 100)) { const t = findWeakestMonster(); if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) return { type: 'spell', name: sp.n, target: t }; } } 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 — only use once per battle // (channeling status is tracked from the battle log) const gem = hasUsableGem('channel'); if (gem && !STATE.channeling && !STATE._mysticUsed) return { type: 'item', id: gem.id, selfTarget: true }; // 1. Health items — safe to use early if (STATE.hp < CFG.cureItemHP) { const gem = hasUsableGem('heal'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } // 2. Cure if (STATE.hp < CFG.cureHP) { const c = hasReadySpell('Full-Cure') ? 'Full-Cure' : hasReadySpell('Cure') ? 'Cure' : null; if (c) return { type: 'spell', name: c, selfTarget: true }; } // 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; } // If channeling is active, prioritize most expensive buff for best value if (STATE.channeling && STATE.mp > 0.10) { const ch = findBestChannelingTarget(); if (ch) return ch; } // Collect available buff candidates and sort by cost const buffCandidates = []; for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; const sp = findSpell(b.spell); buffCandidates.push({ ...b, mpCost: sp ? sp.mp : 0 }); } } if (buffCandidates.length > 0) { buffCandidates.sort((a, b) => STATE.channeling ? b.mpCost - a.mpCost : a.mpCost - b.mpCost); return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' }; } // 4. Mana/spirit items — AFTER buffs (Novice) if (STATE.mp < CFG.manaGemMP) { const gem = hasUsableGem('mana'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } // Spirit gem (P-slot) — auto-consume when available const pGem = document.getElementById('ikey_p'); const pGemId = pGem ? (pGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; if (pGemId && pGemId[1] === '10007') { if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true }; } // Spirit draughts/potions — only when SP drops below threshold if (STATE.sp < CFG.spiritPotionSP) { // Check non-P-slot spirit items only for (const [slot, info] of Object.entries(STATE.itemsKnown)) { if (slot === 'p') continue; const def = ITEMS[parseInt(info)]; if (def && def.t === 'spirit') { return { type: 'item', id: slot, selfTarget: true }; } } } // 5. Weapon skills — these are better than basic attacks 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 3+ enemies (needs Penetrated Armor from Rending Blow) if (STATE.monsters.length >= 3 && 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 the highest-MP buff that needs refreshing { const ch = findBestChannelingTarget(); if (ch) return ch; } const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : 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 — only use once per battle const gem = hasUsableGem('channel'); if (gem && !STATE.channeling && !STATE._mysticUsed) return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' }; // 0b. Spark of Life SP safety if (STATE.sp < 0.55) { for (const [slot, info] of Object.entries(STATE.itemsKnown)) { if (slot === 'p') continue; const def = ITEMS[parseInt(info)]; if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; } } // 0c. Spirit Stance safety auto-disable if (STATE.spiritStance && STATE.sp < 0.60) { return { type: 'toggle_spirit', _reason: 'sp_safety' }; } // 1. Health items — safe to use early if (STATE.hp < CFG.cureItemHP) { const gem = hasUsableGem('heal'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } // 2. Cure if (STATE.hp < CFG.cureHP) { const c = hasReadySpell('Full-Cure') ? 'Full-Cure' : hasReadySpell('Cure') ? 'Cure' : null; if (c) return { type: 'spell', name: c, selfTarget: true }; } // 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so // Spark can actually save us. if (STATE.sp < 0.55) { for (const [slot, info] of Object.entries(STATE.itemsKnown)) { if (slot === 'p') continue; const def = ITEMS[parseInt(info)]; if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true }; } } // 3. Buffs // On first round, cast cheapest-first to proc channeling { const ib = castInitialBuffs(); if (ib) return ib; } // If channeling is active, prioritize most expensive buff for best value // Note: channeling reduces cost to ~1, but the game pre-checks base MP, // If channeling is active, prioritize most expensive buff for best value if (STATE.channeling && STATE.mp > 0.10) { const ch = findBestChannelingTarget(); if (ch) return ch; } // Collect available buff candidates and sort by cost const buffCandidates = []; for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; const sp = findSpell(b.spell); buffCandidates.push({ ...b, mpCost: sp ? sp.mp : 0 }); } } if (buffCandidates.length > 0) { buffCandidates.sort((a, b) => STATE.channeling ? b.mpCost - a.mpCost : a.mpCost - b.mpCost); return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' }; } // 4. Mana/spirit items — AFTER buffs (Adept) if (STATE.mp < CFG.manaGemMP) { const gem = hasUsableGem('mana'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } // Spirit gem (P-slot) — auto-consume when available const apGem = document.getElementById('ikey_p'); const apGemId = apGem ? (apGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; if (apGemId && apGemId[1] === '10007') { if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true }; } // Spirit draughts/potions — only at SP < threshold if (STATE.sp < CFG.spiritPotionSP) { for (const [slot, info] of Object.entries(STATE.itemsKnown)) { if (slot === 'p') continue; const def = ITEMS[parseInt(info)]; if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true }; } } // 4. Debuffs — Imperil on 2+ mobs or bosses; Weaken on high-threat if (CFG.autoDebuff && STATE.mp > 0.20) { if (hasReadySpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) { const b = findStrongestMonster(); if (b >= 0 && !checkMonsterDebuff(b, 'imperil')) return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' }; } if (hasReadySpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) { const b = findDangerousMonster(); if (b >= 0 && !checkMonsterDebuff(b, 'weaken')) return { type: 'spell', name: 'Weaken', target: b, _reason: 'weaken' }; } } // 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar) const isHighThreat = hasHighThreat(THREAT_PX); if (isHighThreat) console.log('%c[HV] ⚠ HIGH THREAT (Adept): 2+ monsters with SP >85%', 'color:#f44'); const skillOC_N = CFG.skillOC || 75; if (STATE.oc >= skillOC_N || isHighThreat) { if (isHighThreat && STATE.oc < skillOC_N) console.log('%c[HV] ⚠ emergency skill at OC=' + STATE.oc.toFixed(0), 'color:#f80'); // Rending Blow: AoE armor pen vs 3+ enemies (reduced from 5) if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) { const t = isHighThreat ? findDangerousMonster() : findStrongestMonster(); if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t }; } // Shatter Strike: AoE stun vs 3+ enemies (needs Penetrated Armor from Rending Blow) if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) { const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor')); if (hasArmorBreak) { const t = isHighThreat ? findDangerousMonster() : 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster(); if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t }; } } // 5. 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 the highest-MP buff that needs refreshing { const ch = findBestChannelingTarget(); if (ch) return ch; } const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : (STATE.monsters.length > 2 ? findOptimalDominoTarget() : findWeakestMonster()); if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) return { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' }; return null; } // ── Veteran (150-300) / Master (300+): full rotation ── function strategyVeteran() { // 0. Mystic Gem for channeling — only use once per battle const gem = hasUsableGem('channel'); if (gem && !STATE.channeling && !STATE._mysticUsed) return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' }; // 0b. Spark of Life SP safety — Spark costs 50% SP when triggered. // If SP < 55%, consume a spirit item before anything else. // Also suppress Spark recast if SP is too low to survive a proc. if (STATE.sp < 0.55) { for (const [slot, info] of Object.entries(STATE.itemsKnown)) { if (slot === 'p') continue; const def = ITEMS[parseInt(info)]; if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; } } // 0c. Spirit Stance safety auto-disable — if left on, drains 10% SP/turn if (STATE.spiritStance && STATE.sp < 0.60) { return { type: 'toggle_spirit', _reason: 'sp_safety' }; } // 1. Health items — safe to use early if (STATE.hp < CFG.cureItemHP) { const gem = hasUsableGem('heal'); if (gem) return { type: 'item', id: gem.id, selfTarget: true, _reason: `hp${(STATE.hp*100).toFixed(0)}` }; } // 2. Cure if (STATE.hp < CFG.cureHP) { const c = hasReadySpell('Full-Cure') ? 'Full-Cure' : hasReadySpell('Cure') ? 'Cure' : null; if (c) return { type: 'spell', name: c, selfTarget: true, _reason: `hp${(STATE.hp*100).toFixed(0)}` }; } // 2b. Spirit items — Spark of Life costs 50% SP. Keep SP above 55% so // Spark can actually save us. Use spirit draughts/potions proactively. if (STATE.sp < 0.55) { for (const [slot, info] of Object.entries(STATE.itemsKnown)) { if (slot === 'p') continue; const def = ITEMS[parseInt(info)]; if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; } } // 3. Buffs // On first round, cast cheapest-first to proc channeling if (CFG.autoBuff) { { const ib = castInitialBuffs(); if (ib) return ib; } // If channeling is active, prioritize most expensive buff for best value if (STATE.channeling && STATE.mp > 0.10) { const ch = findBestChannelingTarget(); if (ch) return ch; } // Collect available buff candidates and sort by cost // No channeling → cheapest first (proc chance), channeling → most expensive (MP value) const buffCandidates = []; for (const b of BUFF_PRIORITY) { if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) { if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue; const sp = findSpell(b.spell); buffCandidates.push({ ...b, mpCost: sp ? sp.mp : 0 }); } } if (buffCandidates.length > 0) { buffCandidates.sort((a, b) => STATE.channeling ? b.mpCost - a.mpCost : a.mpCost - b.mpCost); return { type: 'spell', name: buffCandidates[0].spell, selfTarget: true, _reason: STATE.channeling ? 'chBuff' : 'buff' }; } } // 4. Mana/spirit items — AFTER buffs (Novice) if (STATE.mp < CFG.manaGemMP) { const gem = hasUsableGem('mana'); if (gem) return { type: 'item', id: gem.id, selfTarget: true }; } // Spirit gem (P-slot) — use immediately const vpGem = document.getElementById('ikey_p'); const vpGemId = vpGem ? (vpGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; if (vpGemId && vpGemId[1] === '10007') { if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true }; } // Spirit draughts/potions — only at SP < threshold if (STATE.sp < CFG.spiritPotionSP) { for (const [slot, info] of Object.entries(STATE.itemsKnown)) { if (slot === 'p') continue; const def = ITEMS[parseInt(info)]; if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true }; } } // 4. Debuffs — Imperil on any 2+ mobs or bosses; Weaken on high-threat enemies // On IWBTH, non-boss elites hit hard — debuffs are worth the MP cost if (CFG.autoDebuff && STATE.mp > 0.20) { // Imperil: reduce physical mitigation by 25-50% (stacks with Penetrated Armor) // Use on any tanky enemy when facing 2+ monsters or a rare if (hasReadySpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) { const b = findStrongestMonster(); if (b >= 0 && !checkMonsterDebuff(b, 'imperil')) return { type: 'spell', name: 'Imperil', target: b, _reason: 'imperil' }; } // Weaken: reduce incoming damage by 50% — use on high-threat or 3+ mobs if (hasReadySpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) { const b = findDangerousMonster(); if (b >= 0 && !checkMonsterDebuff(b, 'weaken')) return { type: 'spell', name: 'Weaken', target: b, _reason: 'weaken' }; } } // 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar) // If 2+ enemies have >85% SP bar, they're about to special — emergency const isHighThreat = hasHighThreat(THREAT_PX); if (isHighThreat) console.log('%c[HV] ⚠ HIGH THREAT: 2+ monsters with SP >85%', 'color:#f44'); const askillOC3 = CFG.skillOC || 80; if (STATE.oc >= askillOC3 || isHighThreat) { if (isHighThreat && STATE.oc < askillOC3) console.log('%c[HV] ⚠ emergency: using skill at OC=' + STATE.oc.toFixed(0), 'color:#f80'); // Emergency: 2+ monsters about to special — spend OC to kill them fast // Rending Blow: AoE armor pen vs 3+ enemies (reduced from 5 for IWBTH survivability) if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Rending Blow')) { const t = isHighThreat ? findDangerousMonster() : findStrongestMonster(); if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' }; } // Shatter Strike: AoE stun vs 3+ enemies with armor break if (STATE.monsters.length >= 3 && STATE.skillsKnown.includes('Shatter Strike')) { const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor')); if (hasArmorBreak) { const t = isHighThreat ? findDangerousMonster() : findStrongestMonster(); if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t, _reason: 'stun' }; } } // Great Cleave: boss/rare fights or single-target threat if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = isHighThreat ? findDangerousMonster() : findStrongestMonster(); if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t, _reason: isHighThreat ? 'threat' : 'boss' }; } // If high threat and no AoE skill available, use any weapon skill on the most dangerous if (isHighThreat && STATE.oc >= (CFG.skillOC || 80)) { 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 = findDangerousMonster(); if (t >= 0) return { type: 'skill', name: sk, target: t, _reason: 'threat' }; } } } } // 5. Spirit gem auto-consume const spGem = document.getElementById('ikey_p'); const spGemId = spGem ? (spGem.getAttribute('onmouseover') || '').match(/set_infopane_item\((\d+)\)/) : null; if (spGemId && spGemId[1] === '10007') { if (hasUsableGem('spirit')) return { type: 'item', id: 'p', selfTarget: true, _reason: 'gem' }; } // Spirit potions — only at SP < threshold (Spark needs 50%) if (STATE.sp < CFG.spiritPotionSP) { for (const [slot, info] of Object.entries(STATE.itemsKnown)) { if (slot === 'p') continue; const def = ITEMS[parseInt(info)]; if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` }; } } // 6. 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, _reason: 'dmg' }; } // 7. Channeling kickstart or basic attack { const k = kickstartChanneling(); if (k) return k; } // Consume channeling on the highest-MP buff that needs refreshing { const ch = findBestChannelingTarget(); if (ch) return ch; } const t = hasHighThreat(THREAT_PX) ? findDangerousMonster() : (STATE.monsters.length > 2 ? findOptimalDominoTarget() : findWeakestMonster()); if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick')) return { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' }; return null; }