v0.17.1 - jpx bridge v2: listen to jpx state events

- Listen for jpx_ctrlWidget_update (detail.active) — jpx's own state signal
- Re-trigger M when jpx reports inactive + monsters present + not battle over
- Fallback 2s poll in case the event is missed on page reload
- Cooldown guard (2.5s) to avoid double-trigger
- Stops on maxRounds / low HP / battle finish
This commit is contained in:
GaboGG 2026-08-04 15:56:16 -04:00
parent 5941b1d236
commit 68b44e022a
6 changed files with 240 additions and 156 deletions

View file

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name HV Unified // @name HV Unified
// @namespace hvunified // @namespace hvunified
// @version 0.17.0 // @version 0.17.1
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils) // @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
// @author GaboGG + Hermes // @author GaboGG + Hermes
// @match *://*.hentaiverse.org/* // @match *://*.hentaiverse.org/*
@ -18,7 +18,7 @@
// CONFIG — default settings // CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.17.0'; const VERSION = '0.17.1';
const CFG = { const CFG = {
// — Battle strategy advisory // — Battle strategy advisory
@ -1550,15 +1550,22 @@ function strategyVeteran() {
// jpx (by design) runs ONE round per M press. We re-trigger it after each // jpx (by design) runs ONE round per M press. We re-trigger it after each
// round completes, with safety guards. We NEVER re-implement battle logic — // round completes, with safety guards. We NEVER re-implement battle logic —
// jpx executes; we bridge rounds. // jpx executes; we bridge rounds.
//
// v2: listens to jpx's OWN state signal (jpx_ctrlWidget_update event with
// detail.active) instead of guessing from the DOM. When jpx reports it went
// inactive (round ended, its reDoBattle reset the flag), and a new round is
// loaded with monsters, we re-press M to start the next auto round.
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
let jpxChainStarted = false; // chain engaged for current battle let jpxChainEnabled = false; // chain engaged for current battle
let jpxChainRounds = 0; // rounds chained this battle let jpxChainRounds = 0; // rounds chained this battle
let jpxChainTimer = null; let jpxChainTimer = null;
let jpxLastActive = null; // last known isActiveBattle from jpx
let jpxChainCooldown = 0; // timestamp guard to avoid double-trigger
function jpxPresent() { function jpxPresent() {
return !!(document.getElementById('ctrl-widget') || return !!(document.getElementById('ctrl-widget') ||
document.getElementById('homosex') === null && document.querySelector('.ctrl-widget, #ctrl-widget')); document.querySelector('#ctrl-widget, .ctrl-widget'));
} }
// Trigger jpx's auto-battle toggle (equivalent to pressing M). // Trigger jpx's auto-battle toggle (equivalent to pressing M).
@ -1583,74 +1590,95 @@ function jpxTriggerAuto() {
} }
} }
// Is a round currently active (monsters present)? // Are there living monsters right now (a round is in progress)?
function jpxRoundActive() { function jpxMonstersPresent() {
const monsters = document.querySelectorAll('.btm1'); return document.querySelectorAll('.btm1').length > 0;
const btcp = document.getElementById('btcp');
// Round is active when monsters exist and the continue button is NOT visible
return monsters.length > 0 && !(btcp && btcp.style.visibility !== 'hidden' && btcp.style.display !== 'none');
} }
// Check if the continue/next-round button is up (round ended) // Is the battle over (finish button / no continue button)?
function jpxRoundEnded() { function jpxBattleOver() {
if (document.querySelector('img[src$="finishbattle.png"]')) return true;
const btcp = document.getElementById('btcp'); const btcp = document.getElementById('btcp');
if (btcp) { if (btcp) return false; // continue button exists → not over
const vis = btcp.style.visibility; return !jpxMonstersPresent();
const disp = btcp.style.display;
// jpx hides it (visibility:hidden) after clicking; visible = round ended
if (vis !== 'hidden' && disp !== 'none') return true;
}
// Fallback: finish-battle button present means battle over (not round)
if (document.querySelector('img[src$="finishbattle.png"]')) return false;
return false;
} }
// Try to start the chain (called after battle initializes) // Start the chain (called after battle initializes)
function jpxChainStart() { function jpxChainStart() {
if (!CFG.autoChainJpx) return; if (!CFG.autoChainJpx) return;
if (jpxChainStarted) return; if (jpxChainEnabled) return;
if (!jpxPresent()) return; if (!jpxPresent()) return;
jpxChainStarted = true; jpxChainEnabled = true;
jpxChainRounds = 0; jpxChainRounds = 0;
jpxChainCooldown = Date.now();
// Start the first auto round (equivalent to pressing M once) // Start the first auto round (equivalent to pressing M once)
setTimeout(jpxTriggerAuto, 800); setTimeout(jpxTriggerAuto, 800);
console.log('%c[HV] ⛓ jpx auto-chain engaged (re-triggering M each round)', 'color:#0f0'); console.log('%c[HV] ⛓ jpx auto-chain engaged (re-triggering M each round)', 'color:#0f0');
} }
// Called periodically while in battle — re-trigger when a round ends // Called when jpx reports a state change via jpx_ctrlWidget_update
function jpxChainTick() { function jpxChainOnState(detail) {
if (!CFG.autoChainJpx) return; if (!CFG.autoChainJpx) return;
if (!jpxChainStarted) return; if (!jpxChainEnabled) return;
if (!jpxPresent()) return; if (!jpxPresent()) return;
// Stop conditions const active = !!detail?.active;
if (CFG.chainMaxRounds > 0 && jpxChainRounds >= CFG.chainMaxRounds) { jpxLastActive = active;
jpxChainStop('max rounds reached');
return;
}
if (STATE.hp !== undefined && STATE.hp < CFG.chainMinHP) {
jpxChainStop('HP below ' + Math.round(CFG.chainMinHP * 100) + '%');
return;
}
if (jpxRoundEnded()) { // jpx just went inactive → a round ended (or battle ended)
// Round ended, continue button visible → re-trigger auto for next round if (active === false) {
// Stop conditions
if (CFG.chainMaxRounds > 0 && jpxChainRounds >= CFG.chainMaxRounds) {
jpxChainStop('max rounds reached');
return;
}
if (STATE.hp !== undefined && STATE.hp < CFG.chainMinHP) {
jpxChainStop('HP below ' + Math.round(CFG.chainMinHP * 100) + '%');
return;
}
if (jpxBattleOver()) {
jpxChainStop('battle finished');
return;
}
// Re-trigger for the next round — with a small delay so jpx finishes
// its re-init (reDoBattle resets state) before we press M again.
// Guard against double-firing within 2.5s.
const now = Date.now();
if (now - jpxChainCooldown < 2500) return;
jpxChainCooldown = now;
jpxChainRounds++; jpxChainRounds++;
jpxTriggerAuto(); setTimeout(jpxTriggerAuto, 600);
console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0'); console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0');
} }
} }
function jpxChainStop(reason) { function jpxChainStop(reason) {
if (!jpxChainStarted) return; if (!jpxChainEnabled) return;
jpxChainStarted = false; jpxChainEnabled = false;
console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80'); console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80');
} }
function jpxChainReset() { function jpxChainReset() {
jpxChainStarted = false; jpxChainEnabled = false;
jpxChainRounds = 0; jpxChainRounds = 0;
jpxChainCooldown = 0;
}
// One-time wiring: listen for jpx's state events
let jpxBridgeWired = false;
function jpxBridgeWire() {
if (jpxBridgeWired) return;
if (!window.__hvJpxListener) {
window.__hvJpxListener = (e) => {
// Only act if this is the right profile (persistent vs isekai) — jpx
// sends suffix in detail; accept either to be safe.
jpxChainOnState(e.detail || {});
};
window.addEventListener('jpx_ctrlWidget_update', window.__hvJpxListener);
}
jpxBridgeWired = true;
} }
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
@ -3871,16 +3899,16 @@ function initializeBattle() {
// jpx bridge: engage the round-chaining loop // jpx bridge: engage the round-chaining loop
if (CFG.autoChainJpx) { if (CFG.autoChainJpx) {
jpxChainStart(); jpxBridgeWire(); // listen for jpx state events
jpxChainStart(); // start first auto round
if (!jpxChainTimer) { if (!jpxChainTimer) {
jpxChainTimer = setInterval(jpxChainTick, 1500); jpxChainTimer = setInterval(() => {
} // Fallback poll: if jpx reports inactive but we missed the event
// Watch the continue button for round-end → next round trigger // (page reload etc.), check monsters + continue button
const btcp = document.getElementById('btcp'); if (jpxChainEnabled && jpxLastActive === false && jpxMonstersPresent() && !jpxBattleOver()) {
if (btcp && !btcp.dataset.hvObserved) { jpxChainOnState({ active: false });
btcp.dataset.hvObserved = '1'; }
const bobs = new MutationObserver(() => jpxChainTick()); }, 2000);
bobs.observe(btcp, { attributes: true, attributeFilter: ['style', 'class', 'visibility', 'display'] });
} }
} }

View file

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name HV Unified // @name HV Unified
// @namespace hvunified // @namespace hvunified
// @version 0.17.0 // @version 0.17.1
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils) // @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
// @author GaboGG + Hermes // @author GaboGG + Hermes
// @match *://*.hentaiverse.org/* // @match *://*.hentaiverse.org/*
@ -18,7 +18,7 @@
// CONFIG — default settings // CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.17.0'; const VERSION = '0.17.1';
const CFG = { const CFG = {
// — Battle strategy advisory // — Battle strategy advisory
@ -1550,15 +1550,22 @@ function strategyVeteran() {
// jpx (by design) runs ONE round per M press. We re-trigger it after each // jpx (by design) runs ONE round per M press. We re-trigger it after each
// round completes, with safety guards. We NEVER re-implement battle logic — // round completes, with safety guards. We NEVER re-implement battle logic —
// jpx executes; we bridge rounds. // jpx executes; we bridge rounds.
//
// v2: listens to jpx's OWN state signal (jpx_ctrlWidget_update event with
// detail.active) instead of guessing from the DOM. When jpx reports it went
// inactive (round ended, its reDoBattle reset the flag), and a new round is
// loaded with monsters, we re-press M to start the next auto round.
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
let jpxChainStarted = false; // chain engaged for current battle let jpxChainEnabled = false; // chain engaged for current battle
let jpxChainRounds = 0; // rounds chained this battle let jpxChainRounds = 0; // rounds chained this battle
let jpxChainTimer = null; let jpxChainTimer = null;
let jpxLastActive = null; // last known isActiveBattle from jpx
let jpxChainCooldown = 0; // timestamp guard to avoid double-trigger
function jpxPresent() { function jpxPresent() {
return !!(document.getElementById('ctrl-widget') || return !!(document.getElementById('ctrl-widget') ||
document.getElementById('homosex') === null && document.querySelector('.ctrl-widget, #ctrl-widget')); document.querySelector('#ctrl-widget, .ctrl-widget'));
} }
// Trigger jpx's auto-battle toggle (equivalent to pressing M). // Trigger jpx's auto-battle toggle (equivalent to pressing M).
@ -1583,74 +1590,95 @@ function jpxTriggerAuto() {
} }
} }
// Is a round currently active (monsters present)? // Are there living monsters right now (a round is in progress)?
function jpxRoundActive() { function jpxMonstersPresent() {
const monsters = document.querySelectorAll('.btm1'); return document.querySelectorAll('.btm1').length > 0;
const btcp = document.getElementById('btcp');
// Round is active when monsters exist and the continue button is NOT visible
return monsters.length > 0 && !(btcp && btcp.style.visibility !== 'hidden' && btcp.style.display !== 'none');
} }
// Check if the continue/next-round button is up (round ended) // Is the battle over (finish button / no continue button)?
function jpxRoundEnded() { function jpxBattleOver() {
if (document.querySelector('img[src$="finishbattle.png"]')) return true;
const btcp = document.getElementById('btcp'); const btcp = document.getElementById('btcp');
if (btcp) { if (btcp) return false; // continue button exists → not over
const vis = btcp.style.visibility; return !jpxMonstersPresent();
const disp = btcp.style.display;
// jpx hides it (visibility:hidden) after clicking; visible = round ended
if (vis !== 'hidden' && disp !== 'none') return true;
}
// Fallback: finish-battle button present means battle over (not round)
if (document.querySelector('img[src$="finishbattle.png"]')) return false;
return false;
} }
// Try to start the chain (called after battle initializes) // Start the chain (called after battle initializes)
function jpxChainStart() { function jpxChainStart() {
if (!CFG.autoChainJpx) return; if (!CFG.autoChainJpx) return;
if (jpxChainStarted) return; if (jpxChainEnabled) return;
if (!jpxPresent()) return; if (!jpxPresent()) return;
jpxChainStarted = true; jpxChainEnabled = true;
jpxChainRounds = 0; jpxChainRounds = 0;
jpxChainCooldown = Date.now();
// Start the first auto round (equivalent to pressing M once) // Start the first auto round (equivalent to pressing M once)
setTimeout(jpxTriggerAuto, 800); setTimeout(jpxTriggerAuto, 800);
console.log('%c[HV] ⛓ jpx auto-chain engaged (re-triggering M each round)', 'color:#0f0'); console.log('%c[HV] ⛓ jpx auto-chain engaged (re-triggering M each round)', 'color:#0f0');
} }
// Called periodically while in battle — re-trigger when a round ends // Called when jpx reports a state change via jpx_ctrlWidget_update
function jpxChainTick() { function jpxChainOnState(detail) {
if (!CFG.autoChainJpx) return; if (!CFG.autoChainJpx) return;
if (!jpxChainStarted) return; if (!jpxChainEnabled) return;
if (!jpxPresent()) return; if (!jpxPresent()) return;
// Stop conditions const active = !!detail?.active;
if (CFG.chainMaxRounds > 0 && jpxChainRounds >= CFG.chainMaxRounds) { jpxLastActive = active;
jpxChainStop('max rounds reached');
return;
}
if (STATE.hp !== undefined && STATE.hp < CFG.chainMinHP) {
jpxChainStop('HP below ' + Math.round(CFG.chainMinHP * 100) + '%');
return;
}
if (jpxRoundEnded()) { // jpx just went inactive → a round ended (or battle ended)
// Round ended, continue button visible → re-trigger auto for next round if (active === false) {
// Stop conditions
if (CFG.chainMaxRounds > 0 && jpxChainRounds >= CFG.chainMaxRounds) {
jpxChainStop('max rounds reached');
return;
}
if (STATE.hp !== undefined && STATE.hp < CFG.chainMinHP) {
jpxChainStop('HP below ' + Math.round(CFG.chainMinHP * 100) + '%');
return;
}
if (jpxBattleOver()) {
jpxChainStop('battle finished');
return;
}
// Re-trigger for the next round — with a small delay so jpx finishes
// its re-init (reDoBattle resets state) before we press M again.
// Guard against double-firing within 2.5s.
const now = Date.now();
if (now - jpxChainCooldown < 2500) return;
jpxChainCooldown = now;
jpxChainRounds++; jpxChainRounds++;
jpxTriggerAuto(); setTimeout(jpxTriggerAuto, 600);
console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0'); console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0');
} }
} }
function jpxChainStop(reason) { function jpxChainStop(reason) {
if (!jpxChainStarted) return; if (!jpxChainEnabled) return;
jpxChainStarted = false; jpxChainEnabled = false;
console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80'); console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80');
} }
function jpxChainReset() { function jpxChainReset() {
jpxChainStarted = false; jpxChainEnabled = false;
jpxChainRounds = 0; jpxChainRounds = 0;
jpxChainCooldown = 0;
}
// One-time wiring: listen for jpx's state events
let jpxBridgeWired = false;
function jpxBridgeWire() {
if (jpxBridgeWired) return;
if (!window.__hvJpxListener) {
window.__hvJpxListener = (e) => {
// Only act if this is the right profile (persistent vs isekai) — jpx
// sends suffix in detail; accept either to be safe.
jpxChainOnState(e.detail || {});
};
window.addEventListener('jpx_ctrlWidget_update', window.__hvJpxListener);
}
jpxBridgeWired = true;
} }
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
@ -3871,16 +3899,16 @@ function initializeBattle() {
// jpx bridge: engage the round-chaining loop // jpx bridge: engage the round-chaining loop
if (CFG.autoChainJpx) { if (CFG.autoChainJpx) {
jpxChainStart(); jpxBridgeWire(); // listen for jpx state events
jpxChainStart(); // start first auto round
if (!jpxChainTimer) { if (!jpxChainTimer) {
jpxChainTimer = setInterval(jpxChainTick, 1500); jpxChainTimer = setInterval(() => {
} // Fallback poll: if jpx reports inactive but we missed the event
// Watch the continue button for round-end → next round trigger // (page reload etc.), check monsters + continue button
const btcp = document.getElementById('btcp'); if (jpxChainEnabled && jpxLastActive === false && jpxMonstersPresent() && !jpxBattleOver()) {
if (btcp && !btcp.dataset.hvObserved) { jpxChainOnState({ active: false });
btcp.dataset.hvObserved = '1'; }
const bobs = new MutationObserver(() => jpxChainTick()); }, 2000);
bobs.observe(btcp, { attributes: true, attributeFilter: ['style', 'class', 'visibility', 'display'] });
} }
} }

View file

@ -2,7 +2,7 @@
// CONFIG — default settings // CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.17.0'; const VERSION = '0.17.1';
const CFG = { const CFG = {
// — Battle strategy advisory // — Battle strategy advisory

View file

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name HV Unified // @name HV Unified
// @namespace hvunified // @namespace hvunified
// @version 0.17.0 // @version 0.17.1
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils) // @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
// @author GaboGG + Hermes // @author GaboGG + Hermes
// @match *://*.hentaiverse.org/* // @match *://*.hentaiverse.org/*

View file

@ -69,16 +69,16 @@ function initializeBattle() {
// jpx bridge: engage the round-chaining loop // jpx bridge: engage the round-chaining loop
if (CFG.autoChainJpx) { if (CFG.autoChainJpx) {
jpxChainStart(); jpxBridgeWire(); // listen for jpx state events
jpxChainStart(); // start first auto round
if (!jpxChainTimer) { if (!jpxChainTimer) {
jpxChainTimer = setInterval(jpxChainTick, 1500); jpxChainTimer = setInterval(() => {
} // Fallback poll: if jpx reports inactive but we missed the event
// Watch the continue button for round-end → next round trigger // (page reload etc.), check monsters + continue button
const btcp = document.getElementById('btcp'); if (jpxChainEnabled && jpxLastActive === false && jpxMonstersPresent() && !jpxBattleOver()) {
if (btcp && !btcp.dataset.hvObserved) { jpxChainOnState({ active: false });
btcp.dataset.hvObserved = '1'; }
const bobs = new MutationObserver(() => jpxChainTick()); }, 2000);
bobs.observe(btcp, { attributes: true, attributeFilter: ['style', 'class', 'visibility', 'display'] });
} }
} }

View file

@ -3,15 +3,22 @@
// jpx (by design) runs ONE round per M press. We re-trigger it after each // jpx (by design) runs ONE round per M press. We re-trigger it after each
// round completes, with safety guards. We NEVER re-implement battle logic — // round completes, with safety guards. We NEVER re-implement battle logic —
// jpx executes; we bridge rounds. // jpx executes; we bridge rounds.
//
// v2: listens to jpx's OWN state signal (jpx_ctrlWidget_update event with
// detail.active) instead of guessing from the DOM. When jpx reports it went
// inactive (round ended, its reDoBattle reset the flag), and a new round is
// loaded with monsters, we re-press M to start the next auto round.
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
let jpxChainStarted = false; // chain engaged for current battle let jpxChainEnabled = false; // chain engaged for current battle
let jpxChainRounds = 0; // rounds chained this battle let jpxChainRounds = 0; // rounds chained this battle
let jpxChainTimer = null; let jpxChainTimer = null;
let jpxLastActive = null; // last known isActiveBattle from jpx
let jpxChainCooldown = 0; // timestamp guard to avoid double-trigger
function jpxPresent() { function jpxPresent() {
return !!(document.getElementById('ctrl-widget') || return !!(document.getElementById('ctrl-widget') ||
document.getElementById('homosex') === null && document.querySelector('.ctrl-widget, #ctrl-widget')); document.querySelector('#ctrl-widget, .ctrl-widget'));
} }
// Trigger jpx's auto-battle toggle (equivalent to pressing M). // Trigger jpx's auto-battle toggle (equivalent to pressing M).
@ -36,72 +43,93 @@ function jpxTriggerAuto() {
} }
} }
// Is a round currently active (monsters present)? // Are there living monsters right now (a round is in progress)?
function jpxRoundActive() { function jpxMonstersPresent() {
const monsters = document.querySelectorAll('.btm1'); return document.querySelectorAll('.btm1').length > 0;
const btcp = document.getElementById('btcp');
// Round is active when monsters exist and the continue button is NOT visible
return monsters.length > 0 && !(btcp && btcp.style.visibility !== 'hidden' && btcp.style.display !== 'none');
} }
// Check if the continue/next-round button is up (round ended) // Is the battle over (finish button / no continue button)?
function jpxRoundEnded() { function jpxBattleOver() {
if (document.querySelector('img[src$="finishbattle.png"]')) return true;
const btcp = document.getElementById('btcp'); const btcp = document.getElementById('btcp');
if (btcp) { if (btcp) return false; // continue button exists → not over
const vis = btcp.style.visibility; return !jpxMonstersPresent();
const disp = btcp.style.display;
// jpx hides it (visibility:hidden) after clicking; visible = round ended
if (vis !== 'hidden' && disp !== 'none') return true;
}
// Fallback: finish-battle button present means battle over (not round)
if (document.querySelector('img[src$="finishbattle.png"]')) return false;
return false;
} }
// Try to start the chain (called after battle initializes) // Start the chain (called after battle initializes)
function jpxChainStart() { function jpxChainStart() {
if (!CFG.autoChainJpx) return; if (!CFG.autoChainJpx) return;
if (jpxChainStarted) return; if (jpxChainEnabled) return;
if (!jpxPresent()) return; if (!jpxPresent()) return;
jpxChainStarted = true; jpxChainEnabled = true;
jpxChainRounds = 0; jpxChainRounds = 0;
jpxChainCooldown = Date.now();
// Start the first auto round (equivalent to pressing M once) // Start the first auto round (equivalent to pressing M once)
setTimeout(jpxTriggerAuto, 800); setTimeout(jpxTriggerAuto, 800);
console.log('%c[HV] ⛓ jpx auto-chain engaged (re-triggering M each round)', 'color:#0f0'); console.log('%c[HV] ⛓ jpx auto-chain engaged (re-triggering M each round)', 'color:#0f0');
} }
// Called periodically while in battle — re-trigger when a round ends // Called when jpx reports a state change via jpx_ctrlWidget_update
function jpxChainTick() { function jpxChainOnState(detail) {
if (!CFG.autoChainJpx) return; if (!CFG.autoChainJpx) return;
if (!jpxChainStarted) return; if (!jpxChainEnabled) return;
if (!jpxPresent()) return; if (!jpxPresent()) return;
// Stop conditions const active = !!detail?.active;
if (CFG.chainMaxRounds > 0 && jpxChainRounds >= CFG.chainMaxRounds) { jpxLastActive = active;
jpxChainStop('max rounds reached');
return;
}
if (STATE.hp !== undefined && STATE.hp < CFG.chainMinHP) {
jpxChainStop('HP below ' + Math.round(CFG.chainMinHP * 100) + '%');
return;
}
if (jpxRoundEnded()) { // jpx just went inactive → a round ended (or battle ended)
// Round ended, continue button visible → re-trigger auto for next round if (active === false) {
// Stop conditions
if (CFG.chainMaxRounds > 0 && jpxChainRounds >= CFG.chainMaxRounds) {
jpxChainStop('max rounds reached');
return;
}
if (STATE.hp !== undefined && STATE.hp < CFG.chainMinHP) {
jpxChainStop('HP below ' + Math.round(CFG.chainMinHP * 100) + '%');
return;
}
if (jpxBattleOver()) {
jpxChainStop('battle finished');
return;
}
// Re-trigger for the next round — with a small delay so jpx finishes
// its re-init (reDoBattle resets state) before we press M again.
// Guard against double-firing within 2.5s.
const now = Date.now();
if (now - jpxChainCooldown < 2500) return;
jpxChainCooldown = now;
jpxChainRounds++; jpxChainRounds++;
jpxTriggerAuto(); setTimeout(jpxTriggerAuto, 600);
console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0'); console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0');
} }
} }
function jpxChainStop(reason) { function jpxChainStop(reason) {
if (!jpxChainStarted) return; if (!jpxChainEnabled) return;
jpxChainStarted = false; jpxChainEnabled = false;
console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80'); console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80');
} }
function jpxChainReset() { function jpxChainReset() {
jpxChainStarted = false; jpxChainEnabled = false;
jpxChainRounds = 0; jpxChainRounds = 0;
jpxChainCooldown = 0;
}
// One-time wiring: listen for jpx's state events
let jpxBridgeWired = false;
function jpxBridgeWire() {
if (jpxBridgeWired) return;
if (!window.__hvJpxListener) {
window.__hvJpxListener = (e) => {
// Only act if this is the right profile (persistent vs isekai) — jpx
// sends suffix in detail; accept either to be safe.
jpxChainOnState(e.detail || {});
};
window.addEventListener('jpx_ctrlWidget_update', window.__hvJpxListener);
}
jpxBridgeWired = true;
} }