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==
// @name HV Unified
// @namespace hvunified
// @version 0.17.0
// @version 0.17.1
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
// @author GaboGG + Hermes
// @match *://*.hentaiverse.org/*
@ -18,7 +18,7 @@
// CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.17.0';
const VERSION = '0.17.1';
const CFG = {
// — Battle strategy advisory
@ -1550,15 +1550,22 @@ function strategyVeteran() {
// 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 —
// 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 jpxChainRounds = 0; // rounds chained this battle
let jpxChainEnabled = false; // chain engaged for current battle
let jpxChainRounds = 0; // rounds chained this battle
let jpxChainTimer = null;
let jpxLastActive = null; // last known isActiveBattle from jpx
let jpxChainCooldown = 0; // timestamp guard to avoid double-trigger
function jpxPresent() {
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).
@ -1583,74 +1590,95 @@ function jpxTriggerAuto() {
}
}
// Is a round currently active (monsters present)?
function jpxRoundActive() {
const monsters = document.querySelectorAll('.btm1');
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');
// Are there living monsters right now (a round is in progress)?
function jpxMonstersPresent() {
return document.querySelectorAll('.btm1').length > 0;
}
// Check if the continue/next-round button is up (round ended)
function jpxRoundEnded() {
// Is the battle over (finish button / no continue button)?
function jpxBattleOver() {
if (document.querySelector('img[src$="finishbattle.png"]')) return true;
const btcp = document.getElementById('btcp');
if (btcp) {
const vis = btcp.style.visibility;
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;
if (btcp) return false; // continue button exists → not over
return !jpxMonstersPresent();
}
// Try to start the chain (called after battle initializes)
// Start the chain (called after battle initializes)
function jpxChainStart() {
if (!CFG.autoChainJpx) return;
if (jpxChainStarted) return;
if (jpxChainEnabled) return;
if (!jpxPresent()) return;
jpxChainStarted = true;
jpxChainEnabled = true;
jpxChainRounds = 0;
jpxChainCooldown = Date.now();
// Start the first auto round (equivalent to pressing M once)
setTimeout(jpxTriggerAuto, 800);
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
function jpxChainTick() {
// Called when jpx reports a state change via jpx_ctrlWidget_update
function jpxChainOnState(detail) {
if (!CFG.autoChainJpx) return;
if (!jpxChainStarted) return;
if (!jpxChainEnabled) return;
if (!jpxPresent()) return;
// 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;
}
const active = !!detail?.active;
jpxLastActive = active;
if (jpxRoundEnded()) {
// Round ended, continue button visible → re-trigger auto for next round
// jpx just went inactive → a round ended (or battle ended)
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++;
jpxTriggerAuto();
setTimeout(jpxTriggerAuto, 600);
console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0');
}
}
function jpxChainStop(reason) {
if (!jpxChainStarted) return;
jpxChainStarted = false;
if (!jpxChainEnabled) return;
jpxChainEnabled = false;
console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80');
}
function jpxChainReset() {
jpxChainStarted = false;
jpxChainEnabled = false;
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
if (CFG.autoChainJpx) {
jpxChainStart();
jpxBridgeWire(); // listen for jpx state events
jpxChainStart(); // start first auto round
if (!jpxChainTimer) {
jpxChainTimer = setInterval(jpxChainTick, 1500);
}
// Watch the continue button for round-end → next round trigger
const btcp = document.getElementById('btcp');
if (btcp && !btcp.dataset.hvObserved) {
btcp.dataset.hvObserved = '1';
const bobs = new MutationObserver(() => jpxChainTick());
bobs.observe(btcp, { attributes: true, attributeFilter: ['style', 'class', 'visibility', 'display'] });
jpxChainTimer = setInterval(() => {
// Fallback poll: if jpx reports inactive but we missed the event
// (page reload etc.), check monsters + continue button
if (jpxChainEnabled && jpxLastActive === false && jpxMonstersPresent() && !jpxBattleOver()) {
jpxChainOnState({ active: false });
}
}, 2000);
}
}

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name HV Unified
// @namespace hvunified
// @version 0.17.0
// @version 0.17.1
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
// @author GaboGG + Hermes
// @match *://*.hentaiverse.org/*
@ -18,7 +18,7 @@
// CONFIG — default settings
// ═══════════════════════════════════════════════════════════════════════
const VERSION = '0.17.0';
const VERSION = '0.17.1';
const CFG = {
// — Battle strategy advisory
@ -1550,15 +1550,22 @@ function strategyVeteran() {
// 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 —
// 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 jpxChainRounds = 0; // rounds chained this battle
let jpxChainEnabled = false; // chain engaged for current battle
let jpxChainRounds = 0; // rounds chained this battle
let jpxChainTimer = null;
let jpxLastActive = null; // last known isActiveBattle from jpx
let jpxChainCooldown = 0; // timestamp guard to avoid double-trigger
function jpxPresent() {
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).
@ -1583,74 +1590,95 @@ function jpxTriggerAuto() {
}
}
// Is a round currently active (monsters present)?
function jpxRoundActive() {
const monsters = document.querySelectorAll('.btm1');
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');
// Are there living monsters right now (a round is in progress)?
function jpxMonstersPresent() {
return document.querySelectorAll('.btm1').length > 0;
}
// Check if the continue/next-round button is up (round ended)
function jpxRoundEnded() {
// Is the battle over (finish button / no continue button)?
function jpxBattleOver() {
if (document.querySelector('img[src$="finishbattle.png"]')) return true;
const btcp = document.getElementById('btcp');
if (btcp) {
const vis = btcp.style.visibility;
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;
if (btcp) return false; // continue button exists → not over
return !jpxMonstersPresent();
}
// Try to start the chain (called after battle initializes)
// Start the chain (called after battle initializes)
function jpxChainStart() {
if (!CFG.autoChainJpx) return;
if (jpxChainStarted) return;
if (jpxChainEnabled) return;
if (!jpxPresent()) return;
jpxChainStarted = true;
jpxChainEnabled = true;
jpxChainRounds = 0;
jpxChainCooldown = Date.now();
// Start the first auto round (equivalent to pressing M once)
setTimeout(jpxTriggerAuto, 800);
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
function jpxChainTick() {
// Called when jpx reports a state change via jpx_ctrlWidget_update
function jpxChainOnState(detail) {
if (!CFG.autoChainJpx) return;
if (!jpxChainStarted) return;
if (!jpxChainEnabled) return;
if (!jpxPresent()) return;
// 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;
}
const active = !!detail?.active;
jpxLastActive = active;
if (jpxRoundEnded()) {
// Round ended, continue button visible → re-trigger auto for next round
// jpx just went inactive → a round ended (or battle ended)
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++;
jpxTriggerAuto();
setTimeout(jpxTriggerAuto, 600);
console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0');
}
}
function jpxChainStop(reason) {
if (!jpxChainStarted) return;
jpxChainStarted = false;
if (!jpxChainEnabled) return;
jpxChainEnabled = false;
console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80');
}
function jpxChainReset() {
jpxChainStarted = false;
jpxChainEnabled = false;
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
if (CFG.autoChainJpx) {
jpxChainStart();
jpxBridgeWire(); // listen for jpx state events
jpxChainStart(); // start first auto round
if (!jpxChainTimer) {
jpxChainTimer = setInterval(jpxChainTick, 1500);
}
// Watch the continue button for round-end → next round trigger
const btcp = document.getElementById('btcp');
if (btcp && !btcp.dataset.hvObserved) {
btcp.dataset.hvObserved = '1';
const bobs = new MutationObserver(() => jpxChainTick());
bobs.observe(btcp, { attributes: true, attributeFilter: ['style', 'class', 'visibility', 'display'] });
jpxChainTimer = setInterval(() => {
// Fallback poll: if jpx reports inactive but we missed the event
// (page reload etc.), check monsters + continue button
if (jpxChainEnabled && jpxLastActive === false && jpxMonstersPresent() && !jpxBattleOver()) {
jpxChainOnState({ active: false });
}
}, 2000);
}
}

View file

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

View file

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

View file

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

View file

@ -3,15 +3,22 @@
// 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 —
// 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 jpxChainRounds = 0; // rounds chained this battle
let jpxChainEnabled = false; // chain engaged for current battle
let jpxChainRounds = 0; // rounds chained this battle
let jpxChainTimer = null;
let jpxLastActive = null; // last known isActiveBattle from jpx
let jpxChainCooldown = 0; // timestamp guard to avoid double-trigger
function jpxPresent() {
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).
@ -36,72 +43,93 @@ function jpxTriggerAuto() {
}
}
// Is a round currently active (monsters present)?
function jpxRoundActive() {
const monsters = document.querySelectorAll('.btm1');
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');
// Are there living monsters right now (a round is in progress)?
function jpxMonstersPresent() {
return document.querySelectorAll('.btm1').length > 0;
}
// Check if the continue/next-round button is up (round ended)
function jpxRoundEnded() {
// Is the battle over (finish button / no continue button)?
function jpxBattleOver() {
if (document.querySelector('img[src$="finishbattle.png"]')) return true;
const btcp = document.getElementById('btcp');
if (btcp) {
const vis = btcp.style.visibility;
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;
if (btcp) return false; // continue button exists → not over
return !jpxMonstersPresent();
}
// Try to start the chain (called after battle initializes)
// Start the chain (called after battle initializes)
function jpxChainStart() {
if (!CFG.autoChainJpx) return;
if (jpxChainStarted) return;
if (jpxChainEnabled) return;
if (!jpxPresent()) return;
jpxChainStarted = true;
jpxChainEnabled = true;
jpxChainRounds = 0;
jpxChainCooldown = Date.now();
// Start the first auto round (equivalent to pressing M once)
setTimeout(jpxTriggerAuto, 800);
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
function jpxChainTick() {
// Called when jpx reports a state change via jpx_ctrlWidget_update
function jpxChainOnState(detail) {
if (!CFG.autoChainJpx) return;
if (!jpxChainStarted) return;
if (!jpxChainEnabled) return;
if (!jpxPresent()) return;
// 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;
}
const active = !!detail?.active;
jpxLastActive = active;
if (jpxRoundEnded()) {
// Round ended, continue button visible → re-trigger auto for next round
// jpx just went inactive → a round ended (or battle ended)
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++;
jpxTriggerAuto();
setTimeout(jpxTriggerAuto, 600);
console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0');
}
}
function jpxChainStop(reason) {
if (!jpxChainStarted) return;
jpxChainStarted = false;
if (!jpxChainEnabled) return;
jpxChainEnabled = false;
console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80');
}
function jpxChainReset() {
jpxChainStarted = false;
jpxChainEnabled = false;
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;
}