diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index 9e3d8db..a11cc43 100644 --- a/scripts/hv-unified.user.js +++ b/scripts/hv-unified.user.js @@ -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); } } diff --git a/scripts/latest.user.js b/scripts/latest.user.js index 9e3d8db..a11cc43 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -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); } } diff --git a/src/config.js b/src/config.js index a00cb66..34bbb8f 100644 --- a/src/config.js +++ b/src/config.js @@ -2,7 +2,7 @@ // CONFIG — default settings // ═══════════════════════════════════════════════════════════════════════ -const VERSION = '0.17.0'; +const VERSION = '0.17.1'; const CFG = { // — Battle strategy advisory diff --git a/src/header.user.js b/src/header.user.js index 679d05b..d1c927c 100644 --- a/src/header.user.js +++ b/src/header.user.js @@ -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/* diff --git a/src/init.js b/src/init.js index 72f55ee..51ab6a2 100644 --- a/src/init.js +++ b/src/init.js @@ -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); } } diff --git a/src/jpx-bridge.js b/src/jpx-bridge.js index 673cd45..877242d 100644 --- a/src/jpx-bridge.js +++ b/src/jpx-bridge.js @@ -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; }