- 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
144 lines
5.5 KiB
JavaScript
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;
|
|
}
|