v0.13.34 - Explicit action names in log + _reason tags per decision

- Log now shows spell names/skill names/item names explicitly:
  '[HV] ▶ spell → Haste (self)' instead of '▶ spell → monster 0'
  '[HV] ▶ spell → Fiery Blast → monster 1 (ch fallback)'
  '[HV] ▶ item → Spirit Draught (self) (sp55)'
- Added _reason tag to every action return: 'hp60', 'sp55', 'buff',
  'ch score 81', 'threat', 'oc', 'boss', 'stun', 'mana', 'gem',
  'mystic', 'fallback', 'dmg'
- Now you can see exactly WHY each action was chosen on every turn
This commit is contained in:
GaboGG 2026-07-27 13:35:45 -04:00
parent 949aab915f
commit 467e844aae
6 changed files with 144 additions and 60 deletions

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name HV Unified
// @namespace hvunified
// @version 0.13.33
// @version 0.13.34
// @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.33';
const VERSION = '0.13.34';
const CFG = {
// — Battle automation
@ -1017,7 +1017,7 @@ function findBestChannelingTarget() {
// 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 };
return { type: 'spell', name: candidates[0].spell, selfTarget: true, _reason: `ch score ${candidates[0].score.toFixed(0)}` };
}
}
@ -1330,18 +1330,18 @@ 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 };
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
// 1. Health items — safe to use early
if (STATE.hp < 0.60) {
if (STATE.hp < CFG.cureItemHP) {
const gem = hasUsableGem('heal');
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
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 = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
if (c) return { type: 'spell', name: c, selfTarget: true };
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
@ -1350,7 +1350,7 @@ function strategyVeteran() {
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 };
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
}
}
@ -1369,7 +1369,7 @@ function strategyVeteran() {
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
return { type: 'spell', name: b.spell, selfTarget: true };
return { type: 'spell', name: b.spell, selfTarget: true, _reason: 'buff' };
}
}
}
@ -1420,20 +1420,20 @@ function strategyVeteran() {
// 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 };
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
}
// 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 };
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 };
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)) {
@ -1442,21 +1442,36 @@ function strategyVeteran() {
for (const sk of ps) {
if (STATE.skillsKnown.includes(sk)) {
const t = findDangerousMonster();
if (t >= 0) return { type: 'skill', name: sk, target: t };
if (t >= 0) return { type: 'skill', name: sk, target: t, _reason: 'threat' };
}
}
}
}
// 7. Damage spells
// 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 };
return { type: 'spell', name: dmg, target: t, _reason: 'dmg' };
}
// 8. Channeling kickstart or basic attack
// 7. Channeling kickstart or basic attack
{
const k = kickstartChanneling();
if (k) return k;
@ -1468,9 +1483,9 @@ function strategyVeteran() {
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 { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
return null;
}
@ -1665,8 +1680,21 @@ function setupKeybindings() {
const a = getRecommendedAction();
if (a) {
e.preventDefault();
const label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
console.log(`%c[HV] ▶ ${a.type}${label}`, 'color:#0f0');
// Show explicit action details
let label = '';
if (a.type === 'spell') {
label = a.name + (a.target >= 0 ? ' → monster ' + a.target : ' (self)');
} else if (a.type === 'skill') {
label = a.name + ' → monster ' + a.target;
} else if (a.type === 'item') {
const itemDef = ITEMS[parseInt(STATE.itemsKnown[a.id] || '0')];
label = (itemDef ? itemDef.n : a.id) + (a.selfTarget ? ' (self)' : '');
} else if (a.type === 'attack') {
label = 'monster ' + a.target;
} else {
label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
}
console.log(`%c[HV] ▶ ${a.type}${label}${a._reason ? ' (' + a._reason + ')' : ''}`, 'color:#0f0');
executeAction(a);
}
}

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name HV Unified
// @namespace hvunified
// @version 0.13.33
// @version 0.13.34
// @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.33';
const VERSION = '0.13.34';
const CFG = {
// — Battle automation
@ -1017,7 +1017,7 @@ function findBestChannelingTarget() {
// 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 };
return { type: 'spell', name: candidates[0].spell, selfTarget: true, _reason: `ch score ${candidates[0].score.toFixed(0)}` };
}
}
@ -1330,18 +1330,18 @@ 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 };
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
// 1. Health items — safe to use early
if (STATE.hp < 0.60) {
if (STATE.hp < CFG.cureItemHP) {
const gem = hasUsableGem('heal');
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
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 = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
if (c) return { type: 'spell', name: c, selfTarget: true };
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
@ -1350,7 +1350,7 @@ function strategyVeteran() {
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 };
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
}
}
@ -1369,7 +1369,7 @@ function strategyVeteran() {
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
return { type: 'spell', name: b.spell, selfTarget: true };
return { type: 'spell', name: b.spell, selfTarget: true, _reason: 'buff' };
}
}
}
@ -1420,20 +1420,20 @@ function strategyVeteran() {
// 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 };
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
}
// 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 };
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 };
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)) {
@ -1442,21 +1442,36 @@ function strategyVeteran() {
for (const sk of ps) {
if (STATE.skillsKnown.includes(sk)) {
const t = findDangerousMonster();
if (t >= 0) return { type: 'skill', name: sk, target: t };
if (t >= 0) return { type: 'skill', name: sk, target: t, _reason: 'threat' };
}
}
}
}
// 7. Damage spells
// 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 };
return { type: 'spell', name: dmg, target: t, _reason: 'dmg' };
}
// 8. Channeling kickstart or basic attack
// 7. Channeling kickstart or basic attack
{
const k = kickstartChanneling();
if (k) return k;
@ -1468,9 +1483,9 @@ function strategyVeteran() {
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 { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
return null;
}
@ -1665,8 +1680,21 @@ function setupKeybindings() {
const a = getRecommendedAction();
if (a) {
e.preventDefault();
const label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
console.log(`%c[HV] ▶ ${a.type}${label}`, 'color:#0f0');
// Show explicit action details
let label = '';
if (a.type === 'spell') {
label = a.name + (a.target >= 0 ? ' → monster ' + a.target : ' (self)');
} else if (a.type === 'skill') {
label = a.name + ' → monster ' + a.target;
} else if (a.type === 'item') {
const itemDef = ITEMS[parseInt(STATE.itemsKnown[a.id] || '0')];
label = (itemDef ? itemDef.n : a.id) + (a.selfTarget ? ' (self)' : '');
} else if (a.type === 'attack') {
label = 'monster ' + a.target;
} else {
label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
}
console.log(`%c[HV] ▶ ${a.type}${label}${a._reason ? ' (' + a._reason + ')' : ''}`, 'color:#0f0');
executeAction(a);
}
}

View file

@ -2,7 +2,7 @@
// CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.13.33';
const VERSION = '0.13.34';
const CFG = {
// — Battle automation

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name HV Unified
// @namespace hvunified
// @version 0.13.33
// @version 0.13.34
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
// @author GaboGG + Hermes
// @match *://*.hentaiverse.org/*

View file

@ -51,8 +51,21 @@ function setupKeybindings() {
const a = getRecommendedAction();
if (a) {
e.preventDefault();
const label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
console.log(`%c[HV] ▶ ${a.type}${label}`, 'color:#0f0');
// Show explicit action details
let label = '';
if (a.type === 'spell') {
label = a.name + (a.target >= 0 ? ' → monster ' + a.target : ' (self)');
} else if (a.type === 'skill') {
label = a.name + ' → monster ' + a.target;
} else if (a.type === 'item') {
const itemDef = ITEMS[parseInt(STATE.itemsKnown[a.id] || '0')];
label = (itemDef ? itemDef.n : a.id) + (a.selfTarget ? ' (self)' : '');
} else if (a.type === 'attack') {
label = 'monster ' + a.target;
} else {
label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
}
console.log(`%c[HV] ▶ ${a.type}${label}${a._reason ? ' (' + a._reason + ')' : ''}`, 'color:#0f0');
executeAction(a);
}
}

View file

@ -303,7 +303,7 @@ function findBestChannelingTarget() {
// 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 };
return { type: 'spell', name: candidates[0].spell, selfTarget: true, _reason: `ch score ${candidates[0].score.toFixed(0)}` };
}
}
@ -616,18 +616,18 @@ 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 };
return { type: 'item', id: gem.id, selfTarget: true, _reason: 'mystic' };
// 1. Health items — safe to use early
if (STATE.hp < 0.60) {
if (STATE.hp < CFG.cureItemHP) {
const gem = hasUsableGem('heal');
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
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 = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
if (c) return { type: 'spell', name: c, selfTarget: true };
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
@ -636,7 +636,7 @@ function strategyVeteran() {
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 };
if (def && def.t === 'spirit') return { type: 'item', id: slot, selfTarget: true, _reason: `sp${(STATE.sp*100).toFixed(0)}` };
}
}
@ -655,7 +655,7 @@ function strategyVeteran() {
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
return { type: 'spell', name: b.spell, selfTarget: true };
return { type: 'spell', name: b.spell, selfTarget: true, _reason: 'buff' };
}
}
}
@ -706,20 +706,20 @@ function strategyVeteran() {
// 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 };
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t, _reason: isHighThreat ? 'threat' : 'oc' };
}
// 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 };
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 };
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)) {
@ -728,21 +728,36 @@ function strategyVeteran() {
for (const sk of ps) {
if (STATE.skillsKnown.includes(sk)) {
const t = findDangerousMonster();
if (t >= 0) return { type: 'skill', name: sk, target: t };
if (t >= 0) return { type: 'skill', name: sk, target: t, _reason: 'threat' };
}
}
}
}
// 7. Damage spells
// 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 };
return { type: 'spell', name: dmg, target: t, _reason: 'dmg' };
}
// 8. Channeling kickstart or basic attack
// 7. Channeling kickstart or basic attack
{
const k = kickstartChanneling();
if (k) return k;
@ -754,8 +769,8 @@ function strategyVeteran() {
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 { type: 'attack', target: t, _reason: hasHighThreat(THREAT_PX) ? 'threat' : 'fallback' };
return null;
}