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:
GaboGG 2026-08-04 15:53:25 -04:00
parent 20068df55c
commit 5941b1d236
9 changed files with 414 additions and 6 deletions

View file

@ -21,6 +21,7 @@ FILES=(
items.js
knowledge-base.js
strategy-engine.js
jpx-bridge.js
action-executor.js
hover-system.js
keybindings.js

View file

@ -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')) {

View file

@ -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')) {

View file

@ -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
};

View file

@ -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/*

View file

@ -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
View 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;
}

View file

@ -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() };

View file

@ -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>`;