From b5a5c3df54074285c21c59da18d356c062f69f36 Mon Sep 17 00:00:00 2001 From: GaboGG Date: Mon, 27 Jul 2026 13:07:39 -0400 Subject: [PATCH] v0.13.30 - Fix spirit item detection, add threat-based targeting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix: items on cooldown (no id attr) were invisible to parser Spirit Draught/Potion on cooldown couldn't be auto-consumed - Fix: spiritPotionSP raised from 30% to 60% - New: parse monster SP bar widths into STATE.monsterSp[] - New: findDangerousMonster() — targets highest SP bar monster - New: hasHighThreat() — true if 2+ monsters >85% SP (about to special) - Weapon skills now fire on high-threat even without full OC - Basic attacks prefer dangerous monsters in all tiers --- scripts/hv-unified.user.js | 118 ++++++++++++++++++++++++++----------- scripts/latest.user.js | 118 ++++++++++++++++++++++++++----------- src/battle-parser.js | 25 ++++++-- src/config.js | 2 +- src/header.user.js | 2 +- src/state.js | 1 + src/strategy-engine.js | 88 ++++++++++++++++++--------- 7 files changed, 246 insertions(+), 108 deletions(-) diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index 490ef0e..c597e30 100644 --- a/scripts/hv-unified.user.js +++ b/scripts/hv-unified.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.13.29 +// @version 0.13.30 // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @author GaboGG + Hermes // @match *://*.hentaiverse.org/* @@ -17,7 +17,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.13.29'; +const VERSION = '0.13.30'; const CFG = { // — Battle automation @@ -89,6 +89,7 @@ const STATE = { interruptAlert: false, monsters: [], + monsterSp: [], // SP/OC bar levels per monster (0-120, high = about to special) hp: 1, mp: 1, sp: 1, @@ -426,6 +427,13 @@ function parseBattleState() { // ── Monsters ── STATE.monsters = $$('.btm1[onclick]'); + // Parse each monster's SP/OC bar level (the red bar = special attack charge) + STATE.monsterSp = []; + $$('.btm1').forEach(m => { + const spBar = m.querySelector('.btm5 img[src$="barred.png"]'); + const spPct = spBar ? (parseInt(spBar.style.width) || 0) : 0; + STATE.monsterSp.push(spPct); + }); // ── Spells from spell pane ── STATE.spellsKnown = []; @@ -485,12 +493,18 @@ function parseBattleState() { // ── Items ── STATE.itemsKnown = {}; - $$('[id^="ikey_"]').forEach(el => { - const id = el.id.replace('ikey_', ''); - const omo = el.getAttribute('onmouseover') || ''; - const m = omo.match(/battle\.set_infopane_item\((\d+)\)/); - if (m) STATE.itemsKnown[id] = m[1]; - }); + // Read all item slots, including on-cooldown ones (which lose their id) + const itemPane = document.getElementById('pane_item'); + if (itemPane) { + $$('.bti3 > div[onmouseover], .bti3 div[id^="ikey_"]', itemPane).forEach(el => { + const omo = el.getAttribute('onmouseover') || ''; + const m = omo.match(/set_infopane_item\((\d+)\)/); + if (m) { + const slot = el.id ? el.id.replace('ikey_', '') : ('auto_' + m[1]); + STATE.itemsKnown[slot] = m[1]; + } + }); + } // Buffs STATE.buffs = {}; @@ -717,6 +731,10 @@ function shouldBuff(name, 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 = [ @@ -795,6 +813,30 @@ function findStrongestMonster() { 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; +} + function checkMonsterDebuff(idx, db) { const m = STATE.monsters[idx]; if (!m) return false; @@ -1124,7 +1166,7 @@ function strategyNovice() { if (ch) return ch; } - const t = findWeakestMonster(); + 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; @@ -1216,25 +1258,26 @@ function strategyAdept() { } } - // 5. Weapon skills — before spirit stance + // 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar) + const isHighThreat = hasHighThreat(THREAT_PX); const skillOC_N = CFG.skillOC || 75; - if (STATE.oc >= skillOC_N) { + if (STATE.oc >= skillOC_N || isHighThreat) { // 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(); + const t = isHighThreat ? findDangerousMonster() : 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(); + 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 = findStrongestMonster(); + const t = isHighThreat ? findDangerousMonster() : findStrongestMonster(); if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t }; } } @@ -1259,7 +1302,7 @@ function strategyAdept() { if (ch) return ch; } - const t = findWeakestMonster(); + 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; @@ -1349,35 +1392,38 @@ function strategyVeteran() { } } - // 6. Weapon skills only at OC thresholds (let OC build via basic attacks) + // 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); + const askillOC3 = CFG.skillOC || 80; - if (STATE.oc >= askillOC3) { - // Great Cleave: boss/rare fights only + if (STATE.oc >= askillOC3 || isHighThreat) { + // Emergency: 2+ monsters about to special — spend OC to kill them fast + // Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest) + if (STATE.monsters.length >= 5 && 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 5+ enemies (needs Penetrated Armor) + 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster(); + if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t }; + } + } + // Great Cleave: boss/rare fights or single-target threat if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { - const t = findStrongestMonster(); + const t = isHighThreat ? findDangerousMonster() : 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')) { + // 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 = findStrongestMonster(); + const t = findDangerousMonster(); if (t >= 0) return { type: 'skill', name: sk, target: t }; } } diff --git a/scripts/latest.user.js b/scripts/latest.user.js index 490ef0e..c597e30 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.13.29 +// @version 0.13.30 // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @author GaboGG + Hermes // @match *://*.hentaiverse.org/* @@ -17,7 +17,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.13.29'; +const VERSION = '0.13.30'; const CFG = { // — Battle automation @@ -89,6 +89,7 @@ const STATE = { interruptAlert: false, monsters: [], + monsterSp: [], // SP/OC bar levels per monster (0-120, high = about to special) hp: 1, mp: 1, sp: 1, @@ -426,6 +427,13 @@ function parseBattleState() { // ── Monsters ── STATE.monsters = $$('.btm1[onclick]'); + // Parse each monster's SP/OC bar level (the red bar = special attack charge) + STATE.monsterSp = []; + $$('.btm1').forEach(m => { + const spBar = m.querySelector('.btm5 img[src$="barred.png"]'); + const spPct = spBar ? (parseInt(spBar.style.width) || 0) : 0; + STATE.monsterSp.push(spPct); + }); // ── Spells from spell pane ── STATE.spellsKnown = []; @@ -485,12 +493,18 @@ function parseBattleState() { // ── Items ── STATE.itemsKnown = {}; - $$('[id^="ikey_"]').forEach(el => { - const id = el.id.replace('ikey_', ''); - const omo = el.getAttribute('onmouseover') || ''; - const m = omo.match(/battle\.set_infopane_item\((\d+)\)/); - if (m) STATE.itemsKnown[id] = m[1]; - }); + // Read all item slots, including on-cooldown ones (which lose their id) + const itemPane = document.getElementById('pane_item'); + if (itemPane) { + $$('.bti3 > div[onmouseover], .bti3 div[id^="ikey_"]', itemPane).forEach(el => { + const omo = el.getAttribute('onmouseover') || ''; + const m = omo.match(/set_infopane_item\((\d+)\)/); + if (m) { + const slot = el.id ? el.id.replace('ikey_', '') : ('auto_' + m[1]); + STATE.itemsKnown[slot] = m[1]; + } + }); + } // Buffs STATE.buffs = {}; @@ -717,6 +731,10 @@ function shouldBuff(name, 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 = [ @@ -795,6 +813,30 @@ function findStrongestMonster() { 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; +} + function checkMonsterDebuff(idx, db) { const m = STATE.monsters[idx]; if (!m) return false; @@ -1124,7 +1166,7 @@ function strategyNovice() { if (ch) return ch; } - const t = findWeakestMonster(); + 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; @@ -1216,25 +1258,26 @@ function strategyAdept() { } } - // 5. Weapon skills — before spirit stance + // 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar) + const isHighThreat = hasHighThreat(THREAT_PX); const skillOC_N = CFG.skillOC || 75; - if (STATE.oc >= skillOC_N) { + if (STATE.oc >= skillOC_N || isHighThreat) { // 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(); + const t = isHighThreat ? findDangerousMonster() : 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(); + 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 = findStrongestMonster(); + const t = isHighThreat ? findDangerousMonster() : findStrongestMonster(); if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t }; } } @@ -1259,7 +1302,7 @@ function strategyAdept() { if (ch) return ch; } - const t = findWeakestMonster(); + 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; @@ -1349,35 +1392,38 @@ function strategyVeteran() { } } - // 6. Weapon skills only at OC thresholds (let OC build via basic attacks) + // 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); + const askillOC3 = CFG.skillOC || 80; - if (STATE.oc >= askillOC3) { - // Great Cleave: boss/rare fights only + if (STATE.oc >= askillOC3 || isHighThreat) { + // Emergency: 2+ monsters about to special — spend OC to kill them fast + // Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest) + if (STATE.monsters.length >= 5 && 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 5+ enemies (needs Penetrated Armor) + 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster(); + if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t }; + } + } + // Great Cleave: boss/rare fights or single-target threat if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { - const t = findStrongestMonster(); + const t = isHighThreat ? findDangerousMonster() : 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')) { + // 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 = findStrongestMonster(); + const t = findDangerousMonster(); if (t >= 0) return { type: 'skill', name: sk, target: t }; } } diff --git a/src/battle-parser.js b/src/battle-parser.js index aabcacf..f787343 100644 --- a/src/battle-parser.js +++ b/src/battle-parser.js @@ -81,6 +81,13 @@ function parseBattleState() { // ── Monsters ── STATE.monsters = $$('.btm1[onclick]'); + // Parse each monster's SP/OC bar level (the red bar = special attack charge) + STATE.monsterSp = []; + $$('.btm1').forEach(m => { + const spBar = m.querySelector('.btm5 img[src$="barred.png"]'); + const spPct = spBar ? (parseInt(spBar.style.width) || 0) : 0; + STATE.monsterSp.push(spPct); + }); // ── Spells from spell pane ── STATE.spellsKnown = []; @@ -140,12 +147,18 @@ function parseBattleState() { // ── Items ── STATE.itemsKnown = {}; - $$('[id^="ikey_"]').forEach(el => { - const id = el.id.replace('ikey_', ''); - const omo = el.getAttribute('onmouseover') || ''; - const m = omo.match(/battle\.set_infopane_item\((\d+)\)/); - if (m) STATE.itemsKnown[id] = m[1]; - }); + // Read all item slots, including on-cooldown ones (which lose their id) + const itemPane = document.getElementById('pane_item'); + if (itemPane) { + $$('.bti3 > div[onmouseover], .bti3 div[id^="ikey_"]', itemPane).forEach(el => { + const omo = el.getAttribute('onmouseover') || ''; + const m = omo.match(/set_infopane_item\((\d+)\)/); + if (m) { + const slot = el.id ? el.id.replace('ikey_', '') : ('auto_' + m[1]); + STATE.itemsKnown[slot] = m[1]; + } + }); + } // Buffs STATE.buffs = {}; diff --git a/src/config.js b/src/config.js index b2a1be8..4f1b700 100644 --- a/src/config.js +++ b/src/config.js @@ -2,7 +2,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.13.29'; +const VERSION = '0.13.30'; const CFG = { // — Battle automation diff --git a/src/header.user.js b/src/header.user.js index 1ac42cb..f6985bb 100644 --- a/src/header.user.js +++ b/src/header.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name HV Unified // @namespace hvunified -// @version 0.13.29 +// @version 0.13.30 // @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse // @author GaboGG + Hermes // @match *://*.hentaiverse.org/* diff --git a/src/state.js b/src/state.js index 784c07d..adedaaf 100644 --- a/src/state.js +++ b/src/state.js @@ -25,6 +25,7 @@ const STATE = { interruptAlert: false, monsters: [], + monsterSp: [], // SP/OC bar levels per monster (0-120, high = about to special) hp: 1, mp: 1, sp: 1, diff --git a/src/strategy-engine.js b/src/strategy-engine.js index b5d8f4d..56a09f6 100644 --- a/src/strategy-engine.js +++ b/src/strategy-engine.js @@ -31,6 +31,10 @@ function shouldBuff(name, 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 = [ @@ -109,6 +113,30 @@ function findStrongestMonster() { 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; +} + function checkMonsterDebuff(idx, db) { const m = STATE.monsters[idx]; if (!m) return false; @@ -438,7 +466,7 @@ function strategyNovice() { if (ch) return ch; } - const t = findWeakestMonster(); + 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; @@ -530,25 +558,26 @@ function strategyAdept() { } } - // 5. Weapon skills — before spirit stance + // 5. Weapon skills — prioritize high-threat monsters (high SP/OC bar) + const isHighThreat = hasHighThreat(THREAT_PX); const skillOC_N = CFG.skillOC || 75; - if (STATE.oc >= skillOC_N) { + if (STATE.oc >= skillOC_N || isHighThreat) { // 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(); + const t = isHighThreat ? findDangerousMonster() : 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(); + 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 = findStrongestMonster(); + const t = isHighThreat ? findDangerousMonster() : findStrongestMonster(); if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t }; } } @@ -573,7 +602,7 @@ function strategyAdept() { if (ch) return ch; } - const t = findWeakestMonster(); + 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; @@ -663,35 +692,38 @@ function strategyVeteran() { } } - // 6. Weapon skills only at OC thresholds (let OC build via basic attacks) + // 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); + const askillOC3 = CFG.skillOC || 80; - if (STATE.oc >= askillOC3) { - // Great Cleave: boss/rare fights only + if (STATE.oc >= askillOC3 || isHighThreat) { + // Emergency: 2+ monsters about to special — spend OC to kill them fast + // Rending Blow: AoE armor pen vs 5+ enemies (highest priority in Grindfest) + if (STATE.monsters.length >= 5 && 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 5+ enemies (needs Penetrated Armor) + 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 = isHighThreat ? findDangerousMonster() : findStrongestMonster(); + if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t }; + } + } + // Great Cleave: boss/rare fights or single-target threat if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { - const t = findStrongestMonster(); + const t = isHighThreat ? findDangerousMonster() : 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')) { + // 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 = findStrongestMonster(); + const t = findDangerousMonster(); if (t >= 0) return { type: 'skill', name: sk, target: t }; } }