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:
parent
949aab915f
commit
467e844aae
6 changed files with 144 additions and 60 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// CONFIG — default settings
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const VERSION = '0.13.33';
|
||||
const VERSION = '0.13.34';
|
||||
|
||||
const CFG = {
|
||||
// — Battle automation
|
||||
|
|
|
|||
|
|
@ -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/*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue