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
|
items.js
|
||||||
knowledge-base.js
|
knowledge-base.js
|
||||||
strategy-engine.js
|
strategy-engine.js
|
||||||
|
jpx-bridge.js
|
||||||
action-executor.js
|
action-executor.js
|
||||||
hover-system.js
|
hover-system.js
|
||||||
keybindings.js
|
keybindings.js
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.16.3
|
// @version 0.17.0
|
||||||
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
|
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
|
||||||
// @author GaboGG + Hermes
|
// @author GaboGG + Hermes
|
||||||
// @match *://*.hentaiverse.org/*
|
// @match *://*.hentaiverse.org/*
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.16.3';
|
const VERSION = '0.17.0';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle strategy advisory
|
// — Battle strategy advisory
|
||||||
|
|
@ -45,6 +45,11 @@ const CFG = {
|
||||||
showGuidance: true,
|
showGuidance: true,
|
||||||
showEquipAdvice: true,
|
showEquipAdvice: true,
|
||||||
autoDifficulty: 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;
|
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
|
// ACTION EXECUTOR — carry out recommended actions
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -1772,6 +1885,11 @@ function openSettings() {
|
||||||
${mkTog('Advisor panel', 'showGuidance')}
|
${mkTog('Advisor panel', 'showGuidance')}
|
||||||
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
|
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
|
||||||
${mkTog('Config button', 'cfgButton')}
|
${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">
|
<div style="margin-top:12px;text-align:center;color:#666;font-size:10px">
|
||||||
Changes apply immediately. Press <b>,</b> to open settings.
|
Changes apply immediately. Press <b>,</b> to open settings.
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
@ -3618,6 +3736,9 @@ window.HV = {
|
||||||
tier: () => STATE.tier,
|
tier: () => STATE.tier,
|
||||||
stats: () => JSON.parse(localStorage[SP + 'stats'] || '{"battles":0,"credits":0,"drops":0}'),
|
stats: () => JSON.parse(localStorage[SP + 'stats'] || '{"battles":0,"credits":0,"drops":0}'),
|
||||||
settings: () => toggleSettings(),
|
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: () => {
|
advice: () => {
|
||||||
const s = detectFightingStyle();
|
const s = detectFightingStyle();
|
||||||
return { style: s, attrs: getAttrAdvice(s), spells: getSpellAdvice(), difficulty: getDifficultyAdvice() };
|
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 });
|
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
|
// Auto-scrape gear data on character/armory pages
|
||||||
const url = window.location.href || '';
|
const url = window.location.href || '';
|
||||||
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
|
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.16.3
|
// @version 0.17.0
|
||||||
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
|
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
|
||||||
// @author GaboGG + Hermes
|
// @author GaboGG + Hermes
|
||||||
// @match *://*.hentaiverse.org/*
|
// @match *://*.hentaiverse.org/*
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.16.3';
|
const VERSION = '0.17.0';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle strategy advisory
|
// — Battle strategy advisory
|
||||||
|
|
@ -45,6 +45,11 @@ const CFG = {
|
||||||
showGuidance: true,
|
showGuidance: true,
|
||||||
showEquipAdvice: true,
|
showEquipAdvice: true,
|
||||||
autoDifficulty: 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;
|
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
|
// ACTION EXECUTOR — carry out recommended actions
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -1772,6 +1885,11 @@ function openSettings() {
|
||||||
${mkTog('Advisor panel', 'showGuidance')}
|
${mkTog('Advisor panel', 'showGuidance')}
|
||||||
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
|
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
|
||||||
${mkTog('Config button', 'cfgButton')}
|
${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">
|
<div style="margin-top:12px;text-align:center;color:#666;font-size:10px">
|
||||||
Changes apply immediately. Press <b>,</b> to open settings.
|
Changes apply immediately. Press <b>,</b> to open settings.
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
@ -3618,6 +3736,9 @@ window.HV = {
|
||||||
tier: () => STATE.tier,
|
tier: () => STATE.tier,
|
||||||
stats: () => JSON.parse(localStorage[SP + 'stats'] || '{"battles":0,"credits":0,"drops":0}'),
|
stats: () => JSON.parse(localStorage[SP + 'stats'] || '{"battles":0,"credits":0,"drops":0}'),
|
||||||
settings: () => toggleSettings(),
|
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: () => {
|
advice: () => {
|
||||||
const s = detectFightingStyle();
|
const s = detectFightingStyle();
|
||||||
return { style: s, attrs: getAttrAdvice(s), spells: getSpellAdvice(), difficulty: getDifficultyAdvice() };
|
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 });
|
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
|
// Auto-scrape gear data on character/armory pages
|
||||||
const url = window.location.href || '';
|
const url = window.location.href || '';
|
||||||
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
|
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// CONFIG — default settings
|
// CONFIG — default settings
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const VERSION = '0.16.3';
|
const VERSION = '0.17.0';
|
||||||
|
|
||||||
const CFG = {
|
const CFG = {
|
||||||
// — Battle strategy advisory
|
// — Battle strategy advisory
|
||||||
|
|
@ -29,5 +29,10 @@ const CFG = {
|
||||||
showGuidance: true,
|
showGuidance: true,
|
||||||
showEquipAdvice: true,
|
showEquipAdvice: true,
|
||||||
autoDifficulty: 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==
|
// ==UserScript==
|
||||||
// @name HV Unified
|
// @name HV Unified
|
||||||
// @namespace hvunified
|
// @namespace hvunified
|
||||||
// @version 0.16.3
|
// @version 0.17.0
|
||||||
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
|
// @description HV Unified — Bridge & Strategy/Gear Advisor for HentaiVerse (complements Monsterbation & HVUtils)
|
||||||
// @author GaboGG + Hermes
|
// @author GaboGG + Hermes
|
||||||
// @match *://*.hentaiverse.org/*
|
// @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 });
|
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
|
// Auto-scrape gear data on character/armory pages
|
||||||
const url = window.location.href || '';
|
const url = window.location.href || '';
|
||||||
if (url.includes('ss=eq') || url.includes('ss=ch') || url.includes('ss=am')) {
|
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,
|
tier: () => STATE.tier,
|
||||||
stats: () => JSON.parse(localStorage[SP + 'stats'] || '{"battles":0,"credits":0,"drops":0}'),
|
stats: () => JSON.parse(localStorage[SP + 'stats'] || '{"battles":0,"credits":0,"drops":0}'),
|
||||||
settings: () => toggleSettings(),
|
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: () => {
|
advice: () => {
|
||||||
const s = detectFightingStyle();
|
const s = detectFightingStyle();
|
||||||
return { style: s, attrs: getAttrAdvice(s), spells: getSpellAdvice(), difficulty: getDifficultyAdvice() };
|
return { style: s, attrs: getAttrAdvice(s), spells: getSpellAdvice(), difficulty: getDifficultyAdvice() };
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,11 @@ function openSettings() {
|
||||||
${mkTog('Advisor panel', 'showGuidance')}
|
${mkTog('Advisor panel', 'showGuidance')}
|
||||||
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
|
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
|
||||||
${mkTog('Config button', 'cfgButton')}
|
${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">
|
<div style="margin-top:12px;text-align:center;color:#666;font-size:10px">
|
||||||
Changes apply immediately. Press <b>,</b> to open settings.
|
Changes apply immediately. Press <b>,</b> to open settings.
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue