v0.14.1 - Fix cooldown checks: spells on cooldown no longer selected

- Added hasReadySpell() that checks DOM for onclick attribute (off cooldown)
- Buff loop (all tiers) now uses hasReadySpell instead of hasSpell
- Cure/Full-Cure now checks cooldown before casting
- Channeling target selection checks cooldown
- castInitialBuffs checks cooldown
- Imperil/Weaken debuffs check cooldown
- hasSpell() kept for knowledge checks (do we know this spell at all)
- Prevents silent failures where on-cooldown spells are selected but
  can't actually be cast (btsd[onclick] fails to find them)
This commit is contained in:
GaboGG 2026-07-27 14:05:25 -04:00
parent c9ad92e28d
commit 7ae6e4855f
5 changed files with 66 additions and 45 deletions

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name HV Unified
// @namespace hvunified
// @version 0.14.0
// @version 0.14.1
// @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.14.0';
const VERSION = '0.14.1';
const CFG = {
// — Battle automation
@ -733,6 +733,13 @@ function hasSpell(name) {
return STATE.spellsKnown.some(s => s.n === name);
}
// Check if a spell is not on cooldown (has onclick in the DOM)
function hasReadySpell(name) {
const el = $$('.btsd, .btqs').find(e =>
(e.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${name}'`));
return el ? el.hasAttribute('onclick') : false;
}
function findSpell(name) {
return STATE.spellsKnown.find(s => s.n === name);
}
@ -939,7 +946,7 @@ function castInitialBuffs() {
// Collect all missing/expiring buffs with their MP cost
const candidates = [];
for (const b of BUFF_PRIORITY) {
if (!hasSpell(b.spell)) continue;
if (!hasReadySpell(b.spell)) continue;
const dur = buffDuration(b.icon);
if (dur === 0 || dur < 3) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
@ -1009,7 +1016,7 @@ function findBestChannelingTarget() {
const candidates = [];
for (const b of BUFF_PRIORITY) {
if (!hasSpell(b.spell)) continue;
if (!hasReadySpell(b.spell)) continue;
const dur = buffDuration(b.icon);
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
const sp = findSpell(b.spell);
@ -1126,7 +1133,7 @@ function strategyNovice() {
// 2. Cure
if (STATE.hp < CFG.cureHP) {
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
const c = hasReadySpell('Full-Cure') ? 'Full-Cure' : hasReadySpell('Cure') ? 'Cure' : null;
if (c) return { type: 'spell', name: c, selfTarget: true };
}
@ -1142,7 +1149,7 @@ function strategyNovice() {
if (ch) return ch;
}
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
return { type: 'spell', name: b.spell, selfTarget: true };
}
@ -1255,7 +1262,7 @@ function strategyAdept() {
// 2. Cure
if (STATE.hp < CFG.cureHP) {
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
const c = hasReadySpell('Full-Cure') ? 'Full-Cure' : hasReadySpell('Cure') ? 'Cure' : null;
if (c) return { type: 'spell', name: c, selfTarget: true };
}
@ -1283,7 +1290,7 @@ function strategyAdept() {
if (ch) return ch;
}
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
return { type: 'spell', name: b.spell, selfTarget: true };
}
@ -1311,12 +1318,12 @@ function strategyAdept() {
// 4. Debuffs — Imperil on 2+ mobs or bosses; Weaken on high-threat
if (CFG.autoDebuff && STATE.mp > 0.20) {
if (hasSpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) {
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 (hasSpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) {
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' };
@ -1407,7 +1414,7 @@ function strategyVeteran() {
// 2. Cure
if (STATE.hp < CFG.cureHP) {
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
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)}` };
}
@ -1434,14 +1441,14 @@ function strategyVeteran() {
if (ch) return ch;
}
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
return { type: 'spell', name: b.spell, selfTarget: true, _reason: 'buff' };
}
}
}
// 4. Mana/spirit items — AFTER buffs (Veteran)
// 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 };
@ -1466,13 +1473,13 @@ function strategyVeteran() {
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 (hasSpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) {
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 (hasSpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) {
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' };

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name HV Unified
// @namespace hvunified
// @version 0.14.0
// @version 0.14.1
// @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.14.0';
const VERSION = '0.14.1';
const CFG = {
// — Battle automation
@ -733,6 +733,13 @@ function hasSpell(name) {
return STATE.spellsKnown.some(s => s.n === name);
}
// Check if a spell is not on cooldown (has onclick in the DOM)
function hasReadySpell(name) {
const el = $$('.btsd, .btqs').find(e =>
(e.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${name}'`));
return el ? el.hasAttribute('onclick') : false;
}
function findSpell(name) {
return STATE.spellsKnown.find(s => s.n === name);
}
@ -939,7 +946,7 @@ function castInitialBuffs() {
// Collect all missing/expiring buffs with their MP cost
const candidates = [];
for (const b of BUFF_PRIORITY) {
if (!hasSpell(b.spell)) continue;
if (!hasReadySpell(b.spell)) continue;
const dur = buffDuration(b.icon);
if (dur === 0 || dur < 3) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
@ -1009,7 +1016,7 @@ function findBestChannelingTarget() {
const candidates = [];
for (const b of BUFF_PRIORITY) {
if (!hasSpell(b.spell)) continue;
if (!hasReadySpell(b.spell)) continue;
const dur = buffDuration(b.icon);
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
const sp = findSpell(b.spell);
@ -1126,7 +1133,7 @@ function strategyNovice() {
// 2. Cure
if (STATE.hp < CFG.cureHP) {
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
const c = hasReadySpell('Full-Cure') ? 'Full-Cure' : hasReadySpell('Cure') ? 'Cure' : null;
if (c) return { type: 'spell', name: c, selfTarget: true };
}
@ -1142,7 +1149,7 @@ function strategyNovice() {
if (ch) return ch;
}
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
return { type: 'spell', name: b.spell, selfTarget: true };
}
@ -1255,7 +1262,7 @@ function strategyAdept() {
// 2. Cure
if (STATE.hp < CFG.cureHP) {
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
const c = hasReadySpell('Full-Cure') ? 'Full-Cure' : hasReadySpell('Cure') ? 'Cure' : null;
if (c) return { type: 'spell', name: c, selfTarget: true };
}
@ -1283,7 +1290,7 @@ function strategyAdept() {
if (ch) return ch;
}
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
return { type: 'spell', name: b.spell, selfTarget: true };
}
@ -1311,12 +1318,12 @@ function strategyAdept() {
// 4. Debuffs — Imperil on 2+ mobs or bosses; Weaken on high-threat
if (CFG.autoDebuff && STATE.mp > 0.20) {
if (hasSpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) {
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 (hasSpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) {
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' };
@ -1407,7 +1414,7 @@ function strategyVeteran() {
// 2. Cure
if (STATE.hp < CFG.cureHP) {
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
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)}` };
}
@ -1434,14 +1441,14 @@ function strategyVeteran() {
if (ch) return ch;
}
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
return { type: 'spell', name: b.spell, selfTarget: true, _reason: 'buff' };
}
}
}
// 4. Mana/spirit items — AFTER buffs (Veteran)
// 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 };
@ -1466,13 +1473,13 @@ function strategyVeteran() {
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 (hasSpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) {
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 (hasSpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) {
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' };

View file

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

View file

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

View file

@ -8,6 +8,13 @@ function hasSpell(name) {
return STATE.spellsKnown.some(s => s.n === name);
}
// Check if a spell is not on cooldown (has onclick in the DOM)
function hasReadySpell(name) {
const el = $$('.btsd, .btqs').find(e =>
(e.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${name}'`));
return el ? el.hasAttribute('onclick') : false;
}
function findSpell(name) {
return STATE.spellsKnown.find(s => s.n === name);
}
@ -214,7 +221,7 @@ function castInitialBuffs() {
// Collect all missing/expiring buffs with their MP cost
const candidates = [];
for (const b of BUFF_PRIORITY) {
if (!hasSpell(b.spell)) continue;
if (!hasReadySpell(b.spell)) continue;
const dur = buffDuration(b.icon);
if (dur === 0 || dur < 3) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
@ -284,7 +291,7 @@ function findBestChannelingTarget() {
const candidates = [];
for (const b of BUFF_PRIORITY) {
if (!hasSpell(b.spell)) continue;
if (!hasReadySpell(b.spell)) continue;
const dur = buffDuration(b.icon);
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
const sp = findSpell(b.spell);
@ -401,7 +408,7 @@ function strategyNovice() {
// 2. Cure
if (STATE.hp < CFG.cureHP) {
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
const c = hasReadySpell('Full-Cure') ? 'Full-Cure' : hasReadySpell('Cure') ? 'Cure' : null;
if (c) return { type: 'spell', name: c, selfTarget: true };
}
@ -417,7 +424,7 @@ function strategyNovice() {
if (ch) return ch;
}
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
return { type: 'spell', name: b.spell, selfTarget: true };
}
@ -530,7 +537,7 @@ function strategyAdept() {
// 2. Cure
if (STATE.hp < CFG.cureHP) {
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
const c = hasReadySpell('Full-Cure') ? 'Full-Cure' : hasReadySpell('Cure') ? 'Cure' : null;
if (c) return { type: 'spell', name: c, selfTarget: true };
}
@ -558,7 +565,7 @@ function strategyAdept() {
if (ch) return ch;
}
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
return { type: 'spell', name: b.spell, selfTarget: true };
}
@ -586,12 +593,12 @@ function strategyAdept() {
// 4. Debuffs — Imperil on 2+ mobs or bosses; Weaken on high-threat
if (CFG.autoDebuff && STATE.mp > 0.20) {
if (hasSpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) {
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 (hasSpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) {
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' };
@ -682,7 +689,7 @@ function strategyVeteran() {
// 2. Cure
if (STATE.hp < CFG.cureHP) {
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
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)}` };
}
@ -709,14 +716,14 @@ function strategyVeteran() {
if (ch) return ch;
}
for (const b of BUFF_PRIORITY) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasSpell(b.spell)) {
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.10 && hasReadySpell(b.spell)) {
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
return { type: 'spell', name: b.spell, selfTarget: true, _reason: 'buff' };
}
}
}
// 4. Mana/spirit items — AFTER buffs (Veteran)
// 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 };
@ -741,13 +748,13 @@ function strategyVeteran() {
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 (hasSpell('Imperil') && (anyRareMonster() || STATE.monsters.length >= 2)) {
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 (hasSpell('Weaken') && (anyRareMonster() || hasHighThreat(80) || STATE.monsters.length >= 3)) {
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' };