diff --git a/scripts/build.sh b/scripts/build.sh
index 81ca7f4..c925930 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -21,6 +21,7 @@ FILES=(
items.js
knowledge-base.js
strategy-engine.js
+ jpx-bridge.js
action-executor.js
hover-system.js
keybindings.js
diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js
index 026db78..9e3d8db 100644
--- a/scripts/hv-unified.user.js
+++ b/scripts/hv-unified.user.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')}
+
+ jpx Bridge
+ ${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)}
Changes apply immediately. Press , to open settings.
`;
@@ -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')) {
diff --git a/scripts/latest.user.js b/scripts/latest.user.js
index 026db78..9e3d8db 100644
--- a/scripts/latest.user.js
+++ b/scripts/latest.user.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')}
+
+ jpx Bridge
+ ${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)}
Changes apply immediately. Press , to open settings.
`;
@@ -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')) {
diff --git a/src/config.js b/src/config.js
index 912616a..a00cb66 100644
--- a/src/config.js
+++ b/src/config.js
@@ -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
};
diff --git a/src/header.user.js b/src/header.user.js
index 02c3d8b..679d05b 100644
--- a/src/header.user.js
+++ b/src/header.user.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/*
diff --git a/src/init.js b/src/init.js
index 3227701..72f55ee 100644
--- a/src/init.js
+++ b/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')) {
diff --git a/src/jpx-bridge.js b/src/jpx-bridge.js
new file mode 100644
index 0000000..673cd45
--- /dev/null
+++ b/src/jpx-bridge.js
@@ -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;
+}
diff --git a/src/public-api.js b/src/public-api.js
index b445e51..0d3dc6f 100644
--- a/src/public-api.js
+++ b/src/public-api.js
@@ -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() };
diff --git a/src/settings-panel.js b/src/settings-panel.js
index 4193118..d793de0 100644
--- a/src/settings-panel.js
+++ b/src/settings-panel.js
@@ -72,6 +72,11 @@ function openSettings() {
${mkTog('Advisor panel', 'showGuidance')}
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
${mkTog('Config button', 'cfgButton')}
+
+ jpx Bridge
+ ${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)}
Changes apply immediately. Press , to open settings.
`;