// ═══════════════════════════════════════════════════════════════════════ // JPX BRIDGE — chain jpx's single-round auto-battler into continuous play. // 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 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.querySelector('#ctrl-widget, .ctrl-widget')); } // Lazy-start: retry until jpx's widget actually appears (it's created lazily // by jpx after its init, so our first check may be too early). function jpxChainStart() { if (!CFG.autoChainJpx) return; if (jpxChainEnabled) return; if (!jpxPresent()) { // Retry a few times over ~6s before giving up if (!jpxChainRetryCount) jpxChainRetryCount = 0; if (jpxChainRetryCount < 10) { jpxChainRetryCount++; setTimeout(jpxChainStart, 600); } return; } jpxChainRetryCount = 0; 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'); } // Trigger jpx's auto-battle toggle (equivalent to pressing M). // jpx listens for keydown on document (capture phase) and maps key 'm' to // toggleActive via userKeybinds. A synthetic KeyboardEvent works because // jpx reads e.key, not e.isTrusted. function jpxTriggerAuto() { try { const evt = new KeyboardEvent('keydown', { key: 'm', code: 'KeyM', keyCode: 77, which: 77, bubbles: true, cancelable: true, }); document.dispatchEvent(evt); return true; } catch (e) { console.warn('[HV] jpxTriggerAuto failed:', e.message); return false; } } // Are there living monsters right now (a round is in progress)? function jpxMonstersPresent() { return document.querySelectorAll('.btm1').length > 0; } // 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) return false; // continue button exists → not over return !jpxMonstersPresent(); } // Called when jpx reports a state change via jpx_ctrlWidget_update function jpxChainOnState(detail) { if (!CFG.autoChainJpx) return; if (!jpxChainEnabled) return; if (!jpxPresent()) return; const active = !!detail?.active; jpxLastActive = active; // 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++; setTimeout(jpxTriggerAuto, 600); console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0'); } } function jpxChainStop(reason) { if (!jpxChainEnabled) return; jpxChainEnabled = false; console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80'); } function jpxChainReset() { 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; }