hv-unified/src/jpx-bridge.js
GaboGG 30bade19bd v0.17.2 - Fix chainState ref, lazy jpx detection, diag visibility
- Fix: chainState/chain used renamed var (jpxChainEnabled vs jpxChainStarted)
- Fix: jpxChainStart lazy-retries (10x/600ms) since jpx creates #ctrl-widget lazily
- Remove duplicate jpxChainStart definition
- HV.diag() now reports scripts.jpx + jpxBridge state for field diagnosis
2026-08-04 15:59:08 -04:00

144 lines
5.5 KiB
JavaScript

// ═══════════════════════════════════════════════════════════════════════
// 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;
}