v0.17.0 - jpx round-chaining bridge
- New src/jpx-bridge.js: re-triggers jpx auto-battle (M key) after each round completes, chaining single-round auto-battler into continuous play - Config: autoChainJpx (default on), chainMinHP 0.25, chainMaxRounds 0 - Safety: stops below chainMinHP, at maxRounds, or when jpx absent - Settings panel: jpx Bridge section - API: HV.chain(on), HV.chainState() - Works by dispatching synthetic 'm' keydown (jpx reads e.key, not isTrusted) - Never re-implements battle logic — jpx executes, we bridge rounds
This commit is contained in:
parent
20068df55c
commit
5941b1d236
9 changed files with 414 additions and 6 deletions
|
|
@ -21,6 +21,7 @@ FILES=(
|
|||
items.js
|
||||
knowledge-base.js
|
||||
strategy-engine.js
|
||||
jpx-bridge.js
|
||||
action-executor.js
|
||||
hover-system.js
|
||||
keybindings.js
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.16.3
|
||||
// @version 0.17.0
|
||||
// @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.16.3';
|
||||
const VERSION = '0.17.0';
|
||||
|
||||
const CFG = {
|
||||
// — Battle strategy advisory
|
||||
|
|
@ -45,6 +45,11 @@ const CFG = {
|
|||
showGuidance: true,
|
||||
showEquipAdvice: true,
|
||||
autoDifficulty: true,
|
||||
|
||||
// — jpx integration (bridge)
|
||||
autoChainJpx: true, // Re-trigger jpx auto-battle (M) after each round
|
||||
chainMinHP: 0.25, // Pause chaining below this HP
|
||||
chainMaxRounds: 0, // 0 = unlimited; else stop after N rounds
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -1540,6 +1545,114 @@ function strategyVeteran() {
|
|||
return null;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 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.
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
let jpxChainStarted = false; // chain engaged for current battle
|
||||
let jpxChainRounds = 0; // rounds chained this battle
|
||||
let jpxChainTimer = null;
|
||||
|
||||
function jpxPresent() {
|
||||
return !!(document.getElementById('ctrl-widget') ||
|
||||
document.getElementById('homosex') === null && document.querySelector('.ctrl-widget, #ctrl-widget'));
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// Check if the continue/next-round button is up (round ended)
|
||||
function jpxRoundEnded() {
|
||||
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;
|
||||
}
|
||||
|
||||
// Try to start the chain (called after battle initializes)
|
||||
function jpxChainStart() {
|
||||
if (!CFG.autoChainJpx) return;
|
||||
if (jpxChainStarted) return;
|
||||
if (!jpxPresent()) return;
|
||||
|
||||
jpxChainStarted = true;
|
||||
jpxChainRounds = 0;
|
||||
// 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() {
|
||||
if (!CFG.autoChainJpx) return;
|
||||
if (!jpxChainStarted) 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;
|
||||
}
|
||||
|
||||
if (jpxRoundEnded()) {
|
||||
// Round ended, continue button visible → re-trigger auto for next round
|
||||
jpxChainRounds++;
|
||||
jpxTriggerAuto();
|
||||
console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0');
|
||||
}
|
||||
}
|
||||
|
||||
function jpxChainStop(reason) {
|
||||
if (!jpxChainStarted) return;
|
||||
jpxChainStarted = false;
|
||||
console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80');
|
||||
}
|
||||
|
||||
function jpxChainReset() {
|
||||
jpxChainStarted = false;
|
||||
jpxChainRounds = 0;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// ACTION EXECUTOR — carry out recommended actions
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -1772,6 +1885,11 @@ function openSettings() {
|
|||
${mkTog('Advisor panel', 'showGuidance')}
|
||||
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
|
||||
${mkTog('Config button', 'cfgButton')}
|
||||
<div style="border-top:1px solid #444;margin:8px 0"></div>
|
||||
<b style="color:#0f0">jpx Bridge</b>
|
||||
${mkTog('Auto-chain jpx rounds (re-press M)', 'autoChainJpx')}
|
||||
${mkNum('Min HP% to keep chaining', 'chainMinHP', 0.05)}
|
||||
${mkNum('Max chained rounds (0=∞)', 'chainMaxRounds', 1)}
|
||||
<div style="margin-top:12px;text-align:center;color:#666;font-size:10px">
|
||||
Changes apply immediately. Press <b>,</b> to open settings.
|
||||
</div>`;
|
||||
|
|
@ -3618,6 +3736,9 @@ window.HV = {
|
|||
tier: () => STATE.tier,
|
||||
stats: () => JSON.parse(localStorage[SP + 'stats'] || '{"battles":0,"credits":0,"drops":0}'),
|
||||
settings: () => toggleSettings(),
|
||||
// jpx bridge control
|
||||
chain: (on) => { CFG.autoChainJpx = !!on; saveConfig(); if (on) { jpxChainStart(); if (!jpxChainTimer) jpxChainTimer = setInterval(jpxChainTick, 1500); } else { jpxChainStop('manual'); } return 'jpx chain: ' + CFG.autoChainJpx; },
|
||||
chainState: () => ({ active: jpxChainStarted, rounds: jpxChainRounds, jpx: jpxPresent(), maxRounds: CFG.chainMaxRounds, minHP: CFG.chainMinHP }),
|
||||
advice: () => {
|
||||
const s = detectFightingStyle();
|
||||
return { style: s, attrs: getAttrAdvice(s), spells: getSpellAdvice(), difficulty: getDifficultyAdvice() };
|
||||
|
|
@ -3748,6 +3869,21 @@ function initializeBattle() {
|
|||
vobs.observe(vitals, { childList: true, subtree: true, attributes: true });
|
||||
}
|
||||
|
||||
// jpx bridge: engage the round-chaining loop
|
||||
if (CFG.autoChainJpx) {
|
||||
jpxChainStart();
|
||||
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'] });
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scrape gear data on character/armory pages
|
||||
const url = window.location.href || '';
|
||||
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.16.3
|
||||
// @version 0.17.0
|
||||
// @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.16.3';
|
||||
const VERSION = '0.17.0';
|
||||
|
||||
const CFG = {
|
||||
// — Battle strategy advisory
|
||||
|
|
@ -45,6 +45,11 @@ const CFG = {
|
|||
showGuidance: true,
|
||||
showEquipAdvice: true,
|
||||
autoDifficulty: true,
|
||||
|
||||
// — jpx integration (bridge)
|
||||
autoChainJpx: true, // Re-trigger jpx auto-battle (M) after each round
|
||||
chainMinHP: 0.25, // Pause chaining below this HP
|
||||
chainMaxRounds: 0, // 0 = unlimited; else stop after N rounds
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -1540,6 +1545,114 @@ function strategyVeteran() {
|
|||
return null;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 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.
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
let jpxChainStarted = false; // chain engaged for current battle
|
||||
let jpxChainRounds = 0; // rounds chained this battle
|
||||
let jpxChainTimer = null;
|
||||
|
||||
function jpxPresent() {
|
||||
return !!(document.getElementById('ctrl-widget') ||
|
||||
document.getElementById('homosex') === null && document.querySelector('.ctrl-widget, #ctrl-widget'));
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// Check if the continue/next-round button is up (round ended)
|
||||
function jpxRoundEnded() {
|
||||
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;
|
||||
}
|
||||
|
||||
// Try to start the chain (called after battle initializes)
|
||||
function jpxChainStart() {
|
||||
if (!CFG.autoChainJpx) return;
|
||||
if (jpxChainStarted) return;
|
||||
if (!jpxPresent()) return;
|
||||
|
||||
jpxChainStarted = true;
|
||||
jpxChainRounds = 0;
|
||||
// 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() {
|
||||
if (!CFG.autoChainJpx) return;
|
||||
if (!jpxChainStarted) 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;
|
||||
}
|
||||
|
||||
if (jpxRoundEnded()) {
|
||||
// Round ended, continue button visible → re-trigger auto for next round
|
||||
jpxChainRounds++;
|
||||
jpxTriggerAuto();
|
||||
console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0');
|
||||
}
|
||||
}
|
||||
|
||||
function jpxChainStop(reason) {
|
||||
if (!jpxChainStarted) return;
|
||||
jpxChainStarted = false;
|
||||
console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80');
|
||||
}
|
||||
|
||||
function jpxChainReset() {
|
||||
jpxChainStarted = false;
|
||||
jpxChainRounds = 0;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// ACTION EXECUTOR — carry out recommended actions
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -1772,6 +1885,11 @@ function openSettings() {
|
|||
${mkTog('Advisor panel', 'showGuidance')}
|
||||
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
|
||||
${mkTog('Config button', 'cfgButton')}
|
||||
<div style="border-top:1px solid #444;margin:8px 0"></div>
|
||||
<b style="color:#0f0">jpx Bridge</b>
|
||||
${mkTog('Auto-chain jpx rounds (re-press M)', 'autoChainJpx')}
|
||||
${mkNum('Min HP% to keep chaining', 'chainMinHP', 0.05)}
|
||||
${mkNum('Max chained rounds (0=∞)', 'chainMaxRounds', 1)}
|
||||
<div style="margin-top:12px;text-align:center;color:#666;font-size:10px">
|
||||
Changes apply immediately. Press <b>,</b> to open settings.
|
||||
</div>`;
|
||||
|
|
@ -3618,6 +3736,9 @@ window.HV = {
|
|||
tier: () => STATE.tier,
|
||||
stats: () => JSON.parse(localStorage[SP + 'stats'] || '{"battles":0,"credits":0,"drops":0}'),
|
||||
settings: () => toggleSettings(),
|
||||
// jpx bridge control
|
||||
chain: (on) => { CFG.autoChainJpx = !!on; saveConfig(); if (on) { jpxChainStart(); if (!jpxChainTimer) jpxChainTimer = setInterval(jpxChainTick, 1500); } else { jpxChainStop('manual'); } return 'jpx chain: ' + CFG.autoChainJpx; },
|
||||
chainState: () => ({ active: jpxChainStarted, rounds: jpxChainRounds, jpx: jpxPresent(), maxRounds: CFG.chainMaxRounds, minHP: CFG.chainMinHP }),
|
||||
advice: () => {
|
||||
const s = detectFightingStyle();
|
||||
return { style: s, attrs: getAttrAdvice(s), spells: getSpellAdvice(), difficulty: getDifficultyAdvice() };
|
||||
|
|
@ -3748,6 +3869,21 @@ function initializeBattle() {
|
|||
vobs.observe(vitals, { childList: true, subtree: true, attributes: true });
|
||||
}
|
||||
|
||||
// jpx bridge: engage the round-chaining loop
|
||||
if (CFG.autoChainJpx) {
|
||||
jpxChainStart();
|
||||
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'] });
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scrape gear data on character/armory pages
|
||||
const url = window.location.href || '';
|
||||
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// CONFIG — default settings
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const VERSION = '0.16.3';
|
||||
const VERSION = '0.17.0';
|
||||
|
||||
const CFG = {
|
||||
// — Battle strategy advisory
|
||||
|
|
@ -29,5 +29,10 @@ const CFG = {
|
|||
showGuidance: true,
|
||||
showEquipAdvice: true,
|
||||
autoDifficulty: true,
|
||||
|
||||
// — jpx integration (bridge)
|
||||
autoChainJpx: true, // Re-trigger jpx auto-battle (M) after each round
|
||||
chainMinHP: 0.25, // Pause chaining below this HP
|
||||
chainMaxRounds: 0, // 0 = unlimited; else stop after N rounds
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.16.3
|
||||
// @version 0.17.0
|
||||
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
|
||||
// @author GaboGG + Hermes
|
||||
// @match *://*.hentaiverse.org/*
|
||||
|
|
|
|||
15
src/init.js
15
src/init.js
|
|
@ -67,6 +67,21 @@ function initializeBattle() {
|
|||
vobs.observe(vitals, { childList: true, subtree: true, attributes: true });
|
||||
}
|
||||
|
||||
// jpx bridge: engage the round-chaining loop
|
||||
if (CFG.autoChainJpx) {
|
||||
jpxChainStart();
|
||||
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'] });
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scrape gear data on character/armory pages
|
||||
const url = window.location.href || '';
|
||||
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
|
||||
|
|
|
|||
107
src/jpx-bridge.js
Normal file
107
src/jpx-bridge.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 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.
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
let jpxChainStarted = false; // chain engaged for current battle
|
||||
let jpxChainRounds = 0; // rounds chained this battle
|
||||
let jpxChainTimer = null;
|
||||
|
||||
function jpxPresent() {
|
||||
return !!(document.getElementById('ctrl-widget') ||
|
||||
document.getElementById('homosex') === null && document.querySelector('.ctrl-widget, #ctrl-widget'));
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// Check if the continue/next-round button is up (round ended)
|
||||
function jpxRoundEnded() {
|
||||
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;
|
||||
}
|
||||
|
||||
// Try to start the chain (called after battle initializes)
|
||||
function jpxChainStart() {
|
||||
if (!CFG.autoChainJpx) return;
|
||||
if (jpxChainStarted) return;
|
||||
if (!jpxPresent()) return;
|
||||
|
||||
jpxChainStarted = true;
|
||||
jpxChainRounds = 0;
|
||||
// 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() {
|
||||
if (!CFG.autoChainJpx) return;
|
||||
if (!jpxChainStarted) 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;
|
||||
}
|
||||
|
||||
if (jpxRoundEnded()) {
|
||||
// Round ended, continue button visible → re-trigger auto for next round
|
||||
jpxChainRounds++;
|
||||
jpxTriggerAuto();
|
||||
console.log(`%c[HV] ⛓ chaining round ${jpxChainRounds}${CFG.chainMaxRounds ? '/' + CFG.chainMaxRounds : ''}`, 'color:#0f0');
|
||||
}
|
||||
}
|
||||
|
||||
function jpxChainStop(reason) {
|
||||
if (!jpxChainStarted) return;
|
||||
jpxChainStarted = false;
|
||||
console.log('%c[HV] ⛓ jpx auto-chain stopped: ' + reason, 'color:#f80');
|
||||
}
|
||||
|
||||
function jpxChainReset() {
|
||||
jpxChainStarted = false;
|
||||
jpxChainRounds = 0;
|
||||
}
|
||||
|
|
@ -11,6 +11,9 @@ window.HV = {
|
|||
tier: () => STATE.tier,
|
||||
stats: () => JSON.parse(localStorage[SP + 'stats'] || '{"battles":0,"credits":0,"drops":0}'),
|
||||
settings: () => toggleSettings(),
|
||||
// jpx bridge control
|
||||
chain: (on) => { CFG.autoChainJpx = !!on; saveConfig(); if (on) { jpxChainStart(); if (!jpxChainTimer) jpxChainTimer = setInterval(jpxChainTick, 1500); } else { jpxChainStop('manual'); } return 'jpx chain: ' + CFG.autoChainJpx; },
|
||||
chainState: () => ({ active: jpxChainStarted, rounds: jpxChainRounds, jpx: jpxPresent(), maxRounds: CFG.chainMaxRounds, minHP: CFG.chainMinHP }),
|
||||
advice: () => {
|
||||
const s = detectFightingStyle();
|
||||
return { style: s, attrs: getAttrAdvice(s), spells: getSpellAdvice(), difficulty: getDifficultyAdvice() };
|
||||
|
|
|
|||
|
|
@ -72,6 +72,11 @@ function openSettings() {
|
|||
${mkTog('Advisor panel', 'showGuidance')}
|
||||
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
|
||||
${mkTog('Config button', 'cfgButton')}
|
||||
<div style="border-top:1px solid #444;margin:8px 0"></div>
|
||||
<b style="color:#0f0">jpx Bridge</b>
|
||||
${mkTog('Auto-chain jpx rounds (re-press M)', 'autoChainJpx')}
|
||||
${mkNum('Min HP% to keep chaining', 'chainMinHP', 0.05)}
|
||||
${mkNum('Max chained rounds (0=∞)', 'chainMaxRounds', 1)}
|
||||
<div style="margin-top:12px;text-align:center;color:#666;font-size:10px">
|
||||
Changes apply immediately. Press <b>,</b> to open settings.
|
||||
</div>`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue