- 23 source files in src/ (build via scripts/build.sh) - Forum-sourced player knowledge in references/ - DESIGN.md with architecture and corrections - References to existing scripts (Monsterbation, jpx, HV Utils)
45 KiB
Monsterbation 1.4.1.2 — Battle Interaction Patterns Deep Dive
Complementary Analysis to hv-scripts-analysis.md
===========================================================================
1. HOVER ATTACK SYSTEM — EXACT FLOW
1.1 The Complete Lifecycle (mouse enter → monster click)
MOUSE ENTER monster area
↓
Monsters() [line 1318] attaches event listeners:
- mouseout → ClearTarget on .btm{Cfg.hoverArea}
- mouseover → SetTarget(i) on .btm{Cfg.hoverArea}
- mousedown → HandleClick(i) on full .btm1
- contextmenu → preventDefault on full .btm1
- wheel → HandleWheel(i) on full .btm1
↓
SET TARGET(i) [line 1378] fires:
target = i; // Set the global target variable
THEN: if hover enabled, no interrupt, no alert, monster alive:
→ Hover();
↓
HOVER() [line 1359]:
if (hovering) return; // Guard: one hover per event cycle
hovering = true;
// Priority chain for action selection:
if (override) → override(); // from mouseEngage/monsterBar
else if (shiftHeld) → cfg.hoverShiftAction();
else if (ctrlHeld) → cfg.hoverCtrlAction();
else if (altHeld) → cfg.hoverAltAction();
else → cfg.hoverAction();
// Inject one-time impulse action after configured action:
if (impulse) {
impulse(); // Executes the impulse
done = true; // Prevents re-trigger
impulse = false; // Clears the impulse
}
monsters[target].click(); // CRITICAL: always clicks the monster
// This is the actual turn submission!
↓
SERVER processes turn → new HTML page loads → loop repeats
1.2 Key Design Decisions
Monster click ALWAYS fires last. This is the single most important architectural pattern: the monsters[target].click() on line 1376 is the final action in Hover(). Every spell cast, item use, or toggle happens before it. The monster click triggers the page's built-in onclick handler, which submits the turn to the server.
Hovering flag prevents re-entry. The hovering boolean (line 1360) prevents Hover() from being called recursively. It's reset to false in Observe() (line 1130) after each MutationObserver-triggered turn cycle.
hoverArea config (line 135-136, 870-871): Controls which sub-element of .btm1 triggers the mouseover:
- 1: whole monster box
- 2: monster icon
- 3: monster name
- 4: monster vitals/HP bar
- 6: monster status effects area
1.3 Interrupt System
Two global booleans control whether hover fires:
| Flag | Set By | Meaning |
|---|---|---|
interruptHover |
ToggleHover() or cfg.startRoundWithHover |
User manually toggled hover on/off |
interruptAlert |
Alerts() + Durations() |
Spark, low HP/MP/SP, or buffs expiring |
InterruptAlert is set per-turn in:
Alerts()[line 1196-1245]: Checks spark (fallenshield.png without bar_dgreen.png), low HP, low MP, low SPDurations()[line 1247-1293]: Checks alertBuffs regex against effect icons with < 2 turns remaining
The minSP auto formula [line 1002]: 0.5 - 0.5 * spboost / (spboost + 100) — dynamically scales based on Spirit Tank upgrades.
1.4 Modifier Key Hover Overrides
When a modifier key is held during hover, the action changes:
shiftHeld + cfg.hoverShiftAction → evaluated during Hover() via handleKeys()
ctrlHeld + cfg.hoverCtrlAction → syncs on keydown/keyup
altHeld + cfg.hoverAltAction → same mechanism
The modifier state is tracked via handleKeys() [line 670] and handleKeyup() [line 686] which update shiftHeld, ctrlHeld, altHeld on every key event. This is separate from the modifier-based keybinding system.
1.5 Mouse Engage Mode
When cfg.mouseEngage = true [line 989-991]:
- mousedown sets
overridebased on which mouse button: left→cfg.clickLeft, middle→cfg.clickMiddle, right→cfg.clickRight - mouseup clears override and sets
release = true - Hover() then calls
override()instead ofcfg.hoverAction
1.6 Hover Autoresume
On keyup [line 686-695]: If cfg.hoverAutoresume is true, clears interruptHover and re-fires Hover() if conditions permit. This enables the "hold key to modify, release to resume" workflow.
===========================================================================
2. SPELL ROTATION SYSTEM
2.1 Strongest() — The Core Combinator
function Strongest(actions) {
return function() {
var n = actions.length;
while (n-- > 0)
actions[n](); // Executes from LAST to FIRST
};
}
Critical detail: Strongest iterates backwards (from last to first). This means:
- FOR TARGETED SPELLS: put most desired action first in array — it gets called last (closest to monster click)
- FOR UNTARGETED SPELLS/ITEMS: put most desired last in array — it gets called first (before any targeting)
Why? Because targeted spells set up state (via the dummy element trick) that the monster click then resolves. The LAST spell run in the loop is the one whose state is active when monsters[target].click() fires. Untargeted actions complete immediately and don't need the monster click.
2.2 Impulse() — One-Shot Injection
function Impulse(action) {
return function() {
if (done) return; // Only fires once per turn cycle
impulse = action; // Stores for later execution in Hover()
if (interruptHover || interruptAlert || !monsters[target] || !monsters[target].hasAttribute('onclick')) {
action(); // Immediate execution if hover is inactive
done = true;
impulse = false;
}
};
}
The Impulse pattern: If hover is active and healthy, the action is stored in the impulse variable and waits for the next Hover() call. If hover is stopped or no target, it fires immediately. The done flag is reset when release is set (on mouseup/keyup), allowing one impulse per user interaction.
2.3 How Spell Icons Are Found in the DOM
function Cast(name) {
return function() {
var spell;
// Guard: don't recast the currently active spell
if (document.getElementsByClassName('btii')[0].innerHTML != name &&
// Find spell icon by its onmouseover text containing the spell name
(spell = document.querySelector('.bts > div[onclick][onmouseover*="\\\'' + name + '\\\'"]'))) {
// DUMMY ELEMENT TRICK:
dummy.setAttribute('onclick', spell.getAttribute('onmouseover'));
dummy.click(); // Triggers the spell's onmouseover → sets targeting mode
spell.click(); // Clicks the actual spell icon → selects it
}
};
}
Selector breakdown: .bts > div[onclick][onmouseover*="'SpellName'"]
.bts= battle spell containersdiv[onclick]= only clickable divs (available spells)[onmouseover*="'SpellName'"]= substring match on the onmouseover attribute
The onmouseover attribute on spell icons contains text like: ('Imperil', 3) — indicating the spell name and turn cost. The regex index \'' is the game's way of representing the spell name in the attribute.
Guard against recasting: Line 336 checks document.getElementsByClassName('btii')[0].innerHTML != name — this is the "currently selected spell" indicator at the top of the battle page. If Imperil is already queued, it won't try to cast it again, preventing wasteful clicks.
2.4 The Dummy Element Trick
The dummy element (line 735) is a detached <div> created once at script init. Its purpose is to bridge between the game's mouseover-click expectations:
- The game's spell icons work on a two-step model: mouseover selects the spell (shows targeting reticle), click confirms
- The dummy element's
onclickis set to the spell'sonmouseovertext dummy.click()triggers that onmouseover behavior without needing actual mouse movement- Then
spell.click()actually selects the spell - Finally
monsters[target].click()completes the targeting
2.5 Default Rotation Configuration
// From settings (lines 123-126):
hoverAction: "Nothing", // Default: plain attack
hoverShiftAction: "Strongest([Cast('Ragnarok'), ...])" // Shift: dark spells
hoverCtrlAction: "Strongest([Cast('Paradise Lost'), ...])" // Ctrl: holy spells
hoverAltAction: "Strongest([Cast('Flames of Loki'), ...])" // Alt: fire spells
The action strings are eval()'d at init (line 1007), converting string representations into actual function objects stored in cfg.hoverAction etc.
2.6 Use() — Item Consumption
function Use(id) {
return function() {
var item;
if ((item = document.getElementById('ikey_' + id))) {
dummy.setAttribute('onclick', item.getAttribute('onmouseover'));
dummy.click();
item.click();
}
};
}
Items are found by their DOM ID: ikey_1 through ikey_15 for regular items, ikey_s1-ikey_s6 for scrolls, ikey_n1-ikey_n6 for infusions, and ikey_p for the power gem. The 'p' special ID in Use('p') maps to the gem.
===========================================================================
3. KEYBINDING SYSTEM
3.1 Bind() Function Signature
Bind(KEY_CODE, MODIFIER, ACTION)
// OR
Bind(KEY_CODE, ACTION) // Modifier defaults to NoMod
Implementation [lines 709-714]:
function Bind(key, mod, command) {
if (!command) {
command = mod;
mod = NoMod;
}
if (command) {
bindings.push(new Keybind(key, mod, command));
}
}
The third-argument-optional pattern: if only two arguments are passed, the second is treated as the action and modifier defaults to NoMod.
3.2 Keybind Object
function Keybind(key, mod, action) {
this.keyCode = key; // JavaScript keyCode integer
this.modifier = mod; // Function: takes event, returns bool
this.action = action; // Function: the action to execute
}
3.3 Modifier Key Functions
NoMod(e) → !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey
Shift(e) → e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey
Ctrl(e) → e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
Alt(e) → e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey
CtrlShift(e) → !e.altKey && e.shiftKey && e.ctrlKey && !e.metaKey
AltShift(e) → !e.ctrlKey && e.altKey && e.shiftKey && !e.metaKey
CtrlAlt(e) → !e.shiftKey && e.ctrlKey && e.altKey && !e.metaKey
CtrlAltShift(e) → e.shiftKey && e.altKey && e.ctrlKey && !e.metaKey
Any(e) → !e.metaKey // ALL modifier combos except meta
All modifier functions explicitly check !e.metaKey to avoid interfering with OS-level shortcuts.
3.4 Key Code Constants
KEY_A=65 through KEY_Z=90
KEY_0=48 through KEY_9=57
KEY_SPACE=32, KEY_ENTER=13, KEY_PAGEUP=33, KEY_PAGEDOWN=34,
KEY_END=35, KEY_HOME=36, KEY_LEFT/UP/RIGHT/DOWN=37/38/39/40
KEY_F1=112 through KEY_F12=123
KEY_COMMA=188, KEY_PERIOD=190, KEY_SLASH/FORWARDSLASH=191
KEY_GRAVE/TILDE=192, KEY_LBRACKET=219, KEY_BACKSLASH=220
KEY_SEMI=186, KEY_RBRACKET=221, KEY_APOSTROPHE=222
KEY_SHIFT=16, KEY_CTRL=17, KEY_ALT=18
Note: KEY_SLASH and KEY_FORWARDSLASH are the same key (191). KEY_GRAVE and KEY_TILDE are the same (192). This means you can't bind backtick and tilde to different actions.
3.5 Event Handling Flow
// On page load, Enhance() registers:
document.addEventListener('keydown', handleKeys, true);
document.addEventListener('keyup', handleKeyup, true);
// handleKeys [line 670]:
function handleKeys(e) {
if (release) { done = false; release = false; } // Reset impulse guard
saveKeyDown(); // Save original onkeydown
shiftHeld = e.shiftKey; // Update global modifier state
ctrlHeld = e.ctrlKey;
altHeld = e.altKey;
// Linear scan through bindings array:
for (var i = 0; i < bindings.length; i++) {
bind = bindings[i];
if (e.keyCode == bind.keyCode && bind.modifier(e)) {
bind.action(); // Execute and RETURN — stops original keydown
return;
}
}
loadKeyDown(); // Restore original onkeydown if no binding matched
}
Key insight: saveKeyDown() and loadKeyDown() save and restore the page's original document.onkeydown handler. This ensures that non-bound keys (like typing in chat) still work. When a binding matches, the original handler is NOT restored — the action fires instead.
The saveKeyDown() function [line 698] injects a <script> tag that runs: var oldkeydown = document.onkeydown ? document.onkeydown : oldkeydown; document.onkeydown = null;. This nullification blocks the page's built-in keyboard handler (which would normally submit a turn for Space/Enter).
loadKeyDown() restores it with document.onkeydown = oldkeydown;.
3.6 Default Bindings
// Healing
Bind(KEY_SPACE, Any, Strongest([Cast('Cure'), HoverAction(Cast('Cure'), true)]));
Bind(KEY_A, Strongest([Use(4), Cast('Full-Cure'), Cast('Cure')])); // Normal
Bind(KEY_A, Shift, Strongest([Use(7), Use(4), ...])); // Shift+health
Bind(KEY_A, Ctrl/Alt, same); // Ctrl/Alt
// Scrolls/items
Bind(KEY_X, Strongest([Use('s1'), Use('s4'), Use('s2'), Use(2), Use(1)]));
Bind(KEY_X, Shift/Ctrl/Alt, variants with infusions);
// Buff spells
Bind(KEY_C, Any, Cast('Regen'));
Bind(KEY_V, Any, Cast(damage)); // damage = 'Arcane Focus' or 'Heartseeker' (line 746)
// Impulse items
Bind(KEY_Q, Impulse(Use(5))); // Q = one-shot item 5
Bind(KEY_W, Any, Impulse(Use(3)));
Bind(KEY_E, Impulse(Use(6)));
// Hover toggle
Bind(KEY_Z, Any, ToggleHover);
Bind(KEY_S, Any, Impulse(Toggle('Spirit')));
// Monster targeting (Imperil specific monsters)
Bind(KEY_1, Any, Strongest([TargetMonster(1), Cast('Imperil')])); // 1 = monster B
Bind(KEY_2, Any, Strongest([TargetMonster(4), Cast('Imperil')])); // 2 = monster E
Bind(KEY_3, Any, Strongest([TargetMonster(7), Cast('Imperil')])); // 3 = monster H
// Settings
Bind(KEY_P, Settings);
3.7 Valid Action Types
| Action | Signature | Description |
|---|---|---|
Cast('Spell Name') |
Finds spell icon by onmouseover substring | Case-insensitive spell name |
Use('ID') |
Finds item by element ID ikey_ID |
'p' for gem, 1-15 items, s1-s6 scrolls, n1-n6 infusions |
Toggle('Type') |
Finds checkbox by ckey_type |
Attack, Focus, Defend, Spirit |
Nothing |
No-op | Unbind a key or plain attack when used with HoverAction |
TargetMonster(N) |
Clicks monster N | 0-based index (A=0, B=1, ..., J=9) |
NextRound |
Clicks btcp + finishbattle button | Advances to next battle |
Strongest([a1,a2,...]) |
Backwards-iterating combinator | Last action wins for targeted, first for untargeted |
HoverAction(action, alert?) |
Performs action on hover target | Second param: true = respect alert interrupts |
Impulse(action) |
One-shot injection into hover rotation | Only fires once per turn cycle |
ToggleHover |
Toggles interruptHover flag | Pause/resume hover play |
Drops |
Calls ShowDrops(false) | Display drop log |
CursorUp/Down |
Move targeting cursor | Boundary-clamped |
CursorTarget |
Click monster at cursor | Use with Strongest for conditional |
CursorHover |
Engage hover at cursor position | |
ClearTarget |
Sets target=false | Stop hover targeting |
Settings |
Opens configuration interface |
3.8 release/done Guard System
// On keydown [line 671]:
if (release) { done = false; release = false; }
// On keyup [line 691]:
release = true;
This two-flag system prevents Impulse actions from firing twice on a single keystroke: done blocks re-execution until release has been set (on keyup), and on the next keydown, done is cleared. This means holding a key only fires the impulse once.
===========================================================================
4. PROFILE / PERSONA / SET SYSTEM
4.1 Data Structure
cfg
├── settings (top-level defaults from settings object)
├── persona[0..8].settings (per-persona overrides)
│ └── set[0..6].settings (per-equipment-set overrides within persona)
└── isekai (separate tree for isekai mode)
├── settings
└── persona[0..8].settings
└── set[0..6].settings
Structure from settings (lines 282-320):
cfg = {
name: '[persistent]',
persona: [
{ name: 'persona 1', settings: {}, set: [
{ name: 'set 1', settings: {} },
...7 sets per persona
]},
...9 personas
],
isekai: {
name: '[isekai]', settings: {},
persona: [...] // mirror structure
}
}
4.2 Config Resolution Order (LoadCfg)
function LoadCfg(p, s, i) { // p=persona index, s=set index, i=isekai flag
// Priority (highest first):
// 1. isekai.persona[p].set[s].settings[setting] // if i && p && s
// 2. persistent.persona[p].set[s].settings[setting] // if !i && p && s (or inherit)
// 3. isekai.persona[p].settings[setting] // if i && p
// 4. persistent.persona[p].settings[setting] // if !i && p (or inherit)
// 5. isekai.settings[setting] // if i
// 6. localStorage.HVmbcfg[setting] // saved config
// 7. settings[setting] // script defaults
}
The isekaiInherit flag (line 17, default true): When in isekai mode, if a setting doesn't exist in the isekai profile, it falls through to the persistent persona's equivalent. This means Isekai profiles can inherit everything from persistent, only overriding what differs.
4.3 Profile Storage
// Profile tracking in localStorage:
localStorage.HVmbp = JSON.stringify({
p: <persona index> or 0 for base,
ip: <isekai persona> or 0 for base,
s1..s9: <set index per persona> or 0,
is1..is9: <isekai set index per persona> or 0
});
// Config storage:
localStorage.HVmbcfg = JSON.stringify(cfg);
4.4 Auto-Switching Mechanism
function ProfileSwitch() { // [line 2240]
if (!cfg.profileAutoswitch) return;
// Detect current persona from page DOM:
var choice;
if ((choice = document.querySelector('[name="persona_set"] [selected]'))) {
profile[(isekai ? 'i' : '') + 'p'] = choice.value;
}
// Detect current equipment set from page DOM:
if ((choice = document.querySelector('[src*="equip/set"][src$="_on.png"]'))) {
profile[(isekai ? 'i' : '') + 's' + profile[...]] = parseInt(choice.src.match(regexp.number));
}
// Persist change:
if (JSON.stringify(profile) != localStorage.HVmbp) {
localStorage.HVmbp = JSON.stringify(profile);
}
}
Trigger: OutOfCombat() [line 2224] calls ProfileSwitch() whenever the user is NOT on a battle page. This means switching persona or equipment set on the character page automatically updates the profile pointer. The next time they enter battle, LoadCfg() reads the updated profile and applies the matching settings.
Battle-time switching: The CfgButton (gear icon during battle) and the SettingsLink (under Character on main page) both show a dropdown menu of configured persona → set trees. Selecting one updates profile in localStorage and calls location.href = location.href for in-battle switching (full page reload to re-apply).
4.5 Isekai Detection
var isekai = document.URL.indexOf('isekai') > -1 ? 'i' : ''; // [line 732]
All localStorage keys are suffixed with the isekai flag:
HVcursorvsHVcursoriHVtrackdropsvsHVtrackdropsi- etc.
This ensures persistent and isekai game modes don't collide in storage.
===========================================================================
5. UI SYSTEM
5.1 cfgInterface (Settings Panel)
The settings interface (Settings() function, line 388) renders a full configuration form into #mainpane. It uses a declarative settingsData array (lines 776-963) where each entry is:
[name, type, label, helpText, width]
// Types: 'h'=header, 'b'=boolean/checkbox, 'i'=integer, 'f'=float,
// 's'=string, 't'=textarea, 'a'=array, 'o'=object
The form dynamically reads from the resolved config chain. Settings that are inherited (unchanged from parent) are shown at 50% opacity. User changes go directly into the appropriate settings object (cfg.persona[p].set[s].settings etc.) using the Change() closure.
Profile selector: A <select> at the bottom shows the persona/set hierarchy with isekai as a separate subtree. The auto checkbox toggles automatic profile switching.
JSON dump mode: A "dump" button renders the entire config as a JSON textarea that can be edited directly — useful for bulk changes or sharing configs.
5.2 CfgButton (In-Battle Gear Icon)
function CfgButton() { // [line 1704]
var div = document.createElement('div');
div.id = 'cfgbutton';
div.innerHTML = '\u2699'; // Unicode gear symbol
div.onclick = Settings;
document.body.appendChild(div);
// Builds dropdown menu of persona/set tree...
}
The gear icon appears at position: absolute; top: 686px; left: 1220px (or adjusted for condenseLeft). Hovering it reveals a dropdown (#mbprofile) with the profile tree. Selecting a profile triggers a location.reload() so the new config takes effect.
5.3 Quickbar Extension
cfg.quickbarExtend: array of IDs
// 0 = space, 1 = gem, string = spell/skill/item ID
The ExtendQuickbar() function (line 1429) creates additional quickbar buttons:
- Spell icons: finds the DOM element by ID, extracts the icon name from the
onmouseoverattribute viaregexp.spellicon(, '(\w+)'), maps to/y/a/{name}.png - Item icons: parses item name (works with the "default font" obfuscation), maps known item name substrings to icon filenames (e.g., 'ealth' → healthpot.png, 'ana' → manapot.png, 'pirit' → spiritpot.png)
- Gem (ID=1): uses
gem[]array populated byGems()function - Usable highlighting: potions on the quickbar get the
usableCSS class when the player's MP/SP is low enough for full potion value, triggering blink animation
5.4 Cooldowns Display
function ShowCooldowns() { // [line 1631]
var buttons = quickbar.querySelectorAll('.btqs[onmouseover]:not([onclick])');
// For each button WITHOUT onclick (on cooldown):
// Parse onmouseover for spell info: regexp.spellinfo = /\('([\w\s-]+)'.*, (\d+)\)/
// Check timelog.lastuse[spellName] vs timelog.turn
// Display remaining cooldown turns as an overlay div.cooldown
}
The cooldown system works by tracking which spell was used on which turn (timelog.lastuse), then comparing against the spell's cooldown from the onmouseover attribute. The formula [line 1637]: cooldown = spellCooldown - currentTurn + lastUsedTurn.
The cooldown number is overlaid on the quickbar button with z-index: 3 and style color: black; font-size: 20px; font-weight: bold.
5.5 Alert Colours System
Configured via cfg.colours object (lines 41-62) and cfg.alertColours + cfg.alertBackground flags.
Alert conditions checked in Alerts() [line 1196]:
| Condition | Trigger | Colour |
|---|---|---|
| Spark of Life | fallenshield.png present but no bar_dgreen.png |
cfg.colours.spark (magenta) |
| Low HP | HP bar width ≤ threshold × bar width | cfg.colours.lowhp (deeppink) |
| Low MP | MP ratio ≤ cfg.minMP | cfg.colours.lowmp (darkslateblue) |
| Low SP | SP ratio ≤ cfg.minSP | cfg.colours.lowsp (indigo) |
| OC Full | Overcharge bar ≥ 100% | cfg.colours.ocfull (mediumspringgreen) |
Background target:
alertBackground=true: colours the full#cspelementalertBackground=false: colours#pane_vitalsand spirit stance button individually
Buff expiry alerts (in Durations()):
- Tests
cfg.alertBuffsregex against effect icon filenames - Triggers when any matching buff has < 2 turns remaining
- Colour:
cfg.colours.expiring(lightblue) - Also triggers
interruptAlertwhencfg.stopOnBuffsExpiring
Channelling detection: If channeling.png icon is present in player effects, background changes to cfg.colours.channelling (aquamarine).
Alert priority chain (visual):
- Spark/low vitals colour (highest priority)
- Buffs expiring colour
- Channelling colour
- OC full colour (spirit button only)
- Default colour
5.6 Additional UI Features
Durations display: Overlays turn counts on effect icons as .effect_duration divs. Stack count shown either as border thickness (stackBorder=true) or as "xN" text.
Log colours: cfg.logColours adds CSS classes to battle log rows based on regex matching:
.miss(evade/block/parry),.damage,.item,.attack,.spell,.recovery,.effect,.spirit,.proficiency
Turn dividers: cfg.turnDividers inserts <hr> between turns in the battle log.
Round counter: cfg.showRound displays "Round N / Total" in the battle area. cfg.bigRoundCounter shows it large in the top-right.
Monster HP display: cfg.showMonsterHP calculates and displays HP numbers next to monsters using monsterData.hp[i] parsed from the battle log and the current bar width ratio.
Monster shortening bars: cfg.shortenHPbars scales monster HP bar widths relative to the monster with highest max HP in the round.
Monster numbers: cfg.monsterNumbers replaces monster letter icons with numbers (1-10).
Monster highlighting: cfg.monsterKeywords highlights monsters matching a regex by setting their background to cfg.colours.monster.
Monster status colours: Stunned (wpn_stun.png) → cfg.colours.stun, Imperilled (imperil.png) → cfg.colours.imperil, Both → cfg.colours.stunimperil.
===========================================================================
6. OUT-OF-BATTLE FEATURES (CrunkJuice Integration)
6.1 The OutOfCombat() Function
function OutOfCombat() { // [line 2224]
DeleteLog();
ProfileSwitch();
SettingsLink();
}
Called only when NOT on a battle page (no #textlog or #riddlemaster). This is the entry point for all out-of-combat functionality. Note: CrunkJuice is a separate companion script — Monsterbation itself only has these three out-of-battle functions. The full non-combat automation (ED confirm, sell all, monster feeding, etc.) lives in CrunkJuice.
6.2 Settings Link on Character Page
function SettingsLink() { // [line 2250]
// Inserts "Monsterbation Settings" link under the Character sidebar
// Uses the game's custom font system (c5m, c5o, c5n, c5s, etc.)
// to spell out "MONSTERBATION SETTINGS" in the default font
// Also builds the profile dropdown menu
}
Default font trick: The game uses a CSS-based obfuscation where letter classes like .c5m render as specific characters. Monsterbation constructs the settings link text using these classes (lines 2264-2285) so it matches the game's visual style.
Profile dropdown: Same persona → set → isekai tree as the in-battle CfgButton, but positioned under the settings link instead of the gear icon. Selection updates localStorage.HVmbp.
6.3 DeleteLog()
function DeleteLog() { // [line 2230]
// Clears temporary localStorage when navigating away:
localStorage.removeItem('HVmonsterData' + isekai);
localStorage.removeItem('HVtimelog' + isekai);
localStorage.removeItem('HVvitals' + isekai);
localStorage.removeItem('HVcursor' + isekai);
// Configurable deletion of drop log and combat log
// 0: never, 1: when leaving battle section, 2: at end of battle
}
6.4 CrunkJuice Features (Separate Script, NOT in this file)
Based on the changelog and analysis document, CrunkJuice provides:
- ED confirm (Energy Drink confirmation dialog)
- Faster "sell all" button
- Monster morale/hunger display
- Feed pills/crystals to all monsters
- Monster database search
- RE timer/counter
- Quality filter in bazaar
- Arena page auto-open
These are NOT in the Monsterbation source — they're in a separate script that users run alongside Monsterbation.
===========================================================================
7. localStorage USAGE PATTERNS
7.1 Complete localStorage Keyspace
| Key | Type | Purpose | Persistence |
|---|---|---|---|
HVmbcfg |
JSON string | Full configuration (merged settings + persona tree) | Permanent (saved from Settings panel) |
HVmbp |
JSON string | Current profile selection {p, ip, s1..s9, is1..is9} | Permanent (auto-saved) |
HVmonsterData{i} |
JSON string | Parsed monster IDs, names, HP values, highlights | Temporary (cleared: battle end, navigation) |
HVtimelog{i} |
JSON string | Turn counter, action counter, round number, spell last-use timestamps | Temporary + stored via beforeunload |
HVcombatlog{i} |
JSON string | Full combat stats (damage dealt/taken by element, miss/evade/parry/etc.) | Semi-persistent (configurable: 0/1/2) |
HVtrackdrops{i} |
JSON string | Drop tracking (Crystals, Equips, Mats, Artifacts, etc.) | Semi-persistent (configurable: 0/1/2) |
HVvitals{i} |
JSON string | Maximum HP/MP/SP values seen | Semi-persistent |
HVcursor{i} |
Integer | Last cursor position (0-9 for monster targeting) | Temporary (cleared: battle end, navigation) |
The {i} suffix is 'i' for isekai mode, '' for persistent.
7.2 Write Timing
On every turn (in Observe() [line 1095]): MonsterData and combat stats are kept in memory; no localStorage write (performance-critical path).
On page unload (StoreTmp(), beforeunload event [line 2401]): Temporary state is flushed to localStorage. This ensures monster HP data, timers, and combat stats survive a page refresh or browser crash.
On battle end (in Observe() at finishbattle check):
HVmonsterData,HVtimelog,HVvitals,HVcursor→ removed (battle is over)HVtrackdrops→ either removed or saved based oncfg.deleteDropLogHVcombatlog→ either removed or saved based oncfg.deleteCombatLog- Dispatches
CustomEvent("battleEnd")with timelog, combatlog, droplog data
On navigation away (in DeleteLog() [line 2230]): Conditional cleanup based on deleteDropLog and deleteCombatLog — setting 1 deletes when URL no longer contains "Battle".
On profile change (CfgButton click / SettingsLink click): HVmbp updated immediately.
On settings save (Save() in Settings panel): HVmbcfg written; HVmbcfg can also be removed (Reset button).
7.3 The beforeunload Trick
window.addEventListener('beforeunload', StoreTmp); // [line 2401]
The beforeunload handler fires when the page is about to be replaced (new battle round via normal navigation) or closed. It saves all in-memory state to localStorage so the next page load can restore it. This is how monster data persists across rounds without re-parsing the full battle log on each turn.
7.4 Data Isolation Between Persistent and Isekai
All keys use the isekai suffix ('' or 'i'), ensuring that playing in persistent mode and isekai mode don't interfere with each other's stats, cursor positions, or monster data. The profile tracking (HVmbp) uses separate fields within the same JSON object: p vs ip, s1 vs is1.
7.5 Storage Size Considerations
The HVcombatlog can grow large on long battles (Arenas, Grindfests, Item World). It tracks per-element damage for four categories (pdealt, mdealt, ptaken, mtaken) with sub-categories for spirit shield absorption. The deleteCombatLog setting (0/1/2) controls cleanup to manage storage usage.
===========================================================================
8. JPX INTEGRATION (AJAX Round Advance)
8.1 The ajaxRound Feature
// settings (line 74):
ajaxRound: true, // advance to next round using ajax
// set to false if you use other scripts that do not support this
8.2 Implementation
function NoPopup() { // [line 2187]
if (!(btcp = document.getElementById('btcp'))) return;
if (cfg.ajaxRound) {
btcp.onclick = function() {
var x = new XMLHttpRequest();
x.onreadystatechange = function() {
if (x.readyState == XMLHttpRequest.DONE) {
if (x.status == 200) {
var doc = (new DOMParser()).parseFromString(x.responseText, 'text/html');
document.body.innerHTML = doc.body.innerHTML;
// Re-inject battle scripts:
var script = document.createElement('script');
script.type = 'text/javascript';
if (doc.getElementById('riddlemaster')) {
// Special handling for RiddleMaster pages
script.innerHTML = doc.getElementsByTagName('script')[2].innerHTML
.replace('e("riddleanswer").value = "?";', '')
.replace('e("riddleform").submit();', '');
} else {
script.innerHTML = 'var t = setTimeout(function(){}, 0); for (var i = t; i > 0' +
(cfg.ajaxIntervals ? ' && i > t - ' + cfg.ajaxIntervals : '') +
'; i--) clearInterval(i); battle = new Battle();';
}
document.getElementById('mainpane').appendChild(script);
var event = new Event('DOMContentLoaded');
document.dispatchEvent(event);
}
// Error handling: alert on failure
}
};
x.open('GET', document.location.href, true);
x.send();
};
}
// Fallback: normal click if ajax not enabled
if (cfg.noPopup && ...) {
btcp.click(); // Normal form submit → full page reload
}
}
8.3 How AJAX Round Advance Works
- Intercept the "Next Round" button: The
#btcpelement'sonclickis replaced with an AJAX handler - Fetch the current page URL via XMLHttpRequest: This returns the HTML for the next round
- Replace entire document.body with the new page's body via
innerHTML - Re-initialize the game engine: Inject a
<script>that callsbattle = new Battle()— this re-creates the game's JavaScript state on the new page without a full page load - Clear stray intervals:
clearInterval(i)loop clears any timers that might have been set by the previous page's code - Dispatch DOMContentLoaded: So other scripts (like jpx) see the new page as freshly loaded
- RiddleMaster special case: When a riddle appears between rounds, the script removes the auto-submit code so the riddle doesn't auto-answer with "?"
Critical for inter-script compatibility (line 74 comment): "set to false if you use other scripts that do not support this". When Monsterbation replaces the entire body via AJAX without a full page reload, other userscripts that run at document-start might not re-trigger. Scripts that expect @run-at document-start or DOMContentLoaded need to handle this. The document.dispatchEvent(new Event('DOMContentLoaded')) on line 2208-2209 is the compatibility bridge — it fires a synthetic DOMContentLoaded event that other scripts can listen for.
8.4 The jpx Compatibility Note
The comment at line 74 — "supports jpx AJAX round advance" — means that when cfg.ajaxRound = true, jpx should be able to detect the new round via the synthetic DOMContentLoaded event. The ajaxIntervals setting (line 75, default 100) controls a timing parameter: cfg.ajaxIntervals = 100 means the interval clearing loop on line 2205 runs for (var i = t; i > 0 && i > t - 100; i--) — clearing only the 100 most recent intervals rather than ALL intervals. Higher values clear more but may cause more flashing; 0 clears none.
8.5 logPasteover Feature
// settings (line 66):
logPasteover: false // add last turn of previous round to new round log. requires ajaxRound
Implementation in Enhance() [line 970-972]:
if (cfg.logPasteover && turn) {
log.firstChild.innerHTML += '<tr><td class="tls"></td></tr>' + turn;
FormatLog();
}
When using AJAX rounds, the battle log is replaced each round. logPasteover preserves the last turn's log entries from the previous round and appends them to the new round's log, providing continuity. The turn variable is extracted in ProcessLog() [line 1549] and persists because it's on the global scope.
===========================================================================
9. BATTLE LOG PARSING — THE OBSERVE SYSTEM
9.1 MutationObserver Pattern
// In Enhance() [line 994]:
var obs = new MutationObserver(Observe);
obs.observe(log.firstChild, {childList: true});
Monsterbation watches the battle log's first child for DOM changes. When the server returns a new turn, the game updates the log HTML, the MutationObserver fires Observe(), and Monsterbation processes the new state.
9.2 Observe() Flow
Observe() called [line 1095]:
↓
Check for finishbattle.png → if present: battle is OVER
→ ProcessLog(), FormatLog(), TrackDrops(), Profbar()
→ ShowDrops(true), ShowUsage(), ShowDamage()
→ Dispatch CustomEvent("battleEnd")
→ Clean localStorage, auto-dismiss popup
↓
If still in battle:
→ hovering = false // Reset hover guard
→ Gems() // Re-parse gem state
→ Alerts() // Check HP/MP/SP/Spark
→ Durations() // Update effect timers
→ Monsters() // Re-attach event listeners to new monsters
→ Confirm() // Add ED/flee confirmation
→ ExtendQuickbar() // Rebuild extended quickbar
→ ProcessLog() // Parse combat stats from new log line
→ ShowCooldowns() // Update cooldown overlays
→ MaxVitals() // Track max HP/MP/SP
→ FormatLog() // Apply log colours
→ TrackDrops() // Parse drops from log
→ Profbar() // Update proficiency sidebar
→ NoPopup() // Set up AJAX round advance
9.3 ProcessLog() Regex Engine
The battle log parsing uses a comprehensive set of named regex patterns (lines 754-774):
regexp.turn = /(.+?)<tr><td class="tls">/ // Extract full turn text
regexp.action = />([^<>]+)<\/td><\/tr>... // "You cast Imperil" etc.
regexp.use = /You (cast|use) ([\w\s-]+)/ // Identify spell/item use
regexp.damage = /[^<>]+damage( \(.../ // Extract damage lines
regexp.type = /for (\d+) (\w+) damage/ // "for 12345 fire damage"
regexp.crit = /(You crit| crits | blasts )/ // Critical hit detection
regexp.miss = /(You evade|You block|...)/ // Miss/evade/parry/resist
9.4 Speed Tracking
// In ProcessLog() [line 1552]:
if (!timelog.startTime && cfg.trackSpeed) timelog.startTime = Date.now();
// ... counts timelog.turn and timelog.action ...
// At battle end (ShowDrops):
var speed = (timelog.action * 60000 / (Date.now() - timelog.startTime)).toFixed(1);
Calculates actions-per-minute from the wall clock, displayed at battle end.
===========================================================================
10. ARCHITECTURAL PATTERNS SUMMARY
10.1 The "Click Elements, Not POST" Principle
Every action submission goes through DOM element clicking:
- Spells:
document.querySelector('.bts > div[onclick][onmouseover*="SpellName"]')→ dummy trick → click - Items:
document.getElementById('ikey_X')→ dummy trick → click - Toggles:
document.getElementById('ckey_spirit')→ dummy trick → click - Monster attacks:
monsters[i].click()directly - Next round:
document.getElementById('btcp').click()or AJAX intercept
NO raw HTTP requests for gameplay actions — the only HTTP request is the AJAX round advance, which fetches the current URL (mirroring a normal page load).
10.2 The "Parse DOM, Not HTTP" Principle
All game state is extracted from the DOM:
- Monster alive/dead:
monsters[i].hasAttribute('onclick') - HP/MP/SP: Bar image widths divided by 414 (or 207 for isekai)
- Overcharge: Orange bar width or
#vcp > divwidth - Spark of Life: Presence of
fallenshield.pngwithoutbar_dgreen.png - Buffs/Debuffs: Parsing
onmouseoverattributes on effect icon<img>tags - Spell cooldowns: Parsing
onmouseoveron quickbar buttons, cross-referencing withtimelog.lastuse - Monster data: Parsing the battle log's initial HTML for MID and HP values
- Drops: Regex parsing the end-of-battle textlog
10.3 The "Global MutationObserver" Pattern
Instead of polling or hooking into the game's JavaScript (which changes between updates), Monsterbation watches the battle log's DOM for changes. This is more reliable than trying to hook into the game's internal Battle class, which could change structure between HV updates.
10.4 The "Eval String Config" Pattern
Certain config values are stored as strings and eval()'d at init:
cfg.hoverAction = eval("Strongest([Cast('Ragnarok'), ...])");
cfg.clickRight = eval("Strongest([Cast('FUS RO DAH'), ...])");
cfg.bind = eval("Bind(KEY_SPACE, Any, ...); Bind(KEY_Z, ...); ...");
This allows users to write arbitrary JavaScript in their config strings, but the script includes validation to prevent Use() inside HoverAction() (the "Fearsome powers thrust Laputa into orbit" error on lines 420-422).
10.5 The "RiddleMaster as Exit Condition" Pattern
function Riddlemaster() { // [line 1146]
var bot;
if (!cfg.clickableRiddlemaster || !(bot = document.getElementById('riddlebot'))) return;
// Creates clickable A/B/C answer buttons on the riddle
// Also increments a "horse" counter in timelog for tracking riddle frequency
}
RiddleMaster detection is a core compliance mechanism. When a riddle appears, speed tracking counts it (timelog.horse++), and the script can add clickable answer buttons. But it does NOT auto-solve — user must click.
10.6 The Global State Machine
// Critical state variables (line 732-745):
var target = false; // Current hover target monster index
var interruptHover = undefined; // Hover enabled/disabled (bool)
var interruptAlert = false; // Emergency alert active (bool)
var hovering = false; // Guard against re-entrant Hover()
var override = false; // Mouse-engage override action
var impulse = false; // Impulse one-shot action
var done = false; // Impulse already-fired guard
var release = false; // Key/mouse release flag
var shiftHeld = false; // Global shift key state
var ctrlHeld = false; // Global ctrl key state
var altHeld = false; // Global alt key state
var cursor = -1; // Targeting cursor position
These globals implement a simple state machine governing all user interactions. The states interact according to strict precedence rules in Hover() and handleKeys(), creating deterministic behavior from concurrent inputs.
===========================================================================
11. RULES COMPLIANCE IMPLICATIONS
11.1 One User Input = One Turn
Monsterbation's architecture inherently enforces this rule:
- Each
monsters[target].click()= one turn submission - Hover fires once per
mouseoverevent (guarded byhoveringflag) - Key presses fire bound actions once per
keydown(guarded byrelease/done) - The
MutationObserverwaits for the server to respond before re-enabling interaction
11.2 What the Script Deliberately Does NOT Do
- NO auto-start: No code to click arena/battle entrance buttons
- NO auto-feed: Monster feeding is in CrunkJuice (separate script), not here
- NO auto-solve RiddleMaster: Only adds clickable A/B/C buttons, does not pick answers
- NO multi-action: Each user action produces exactly one server round-trip
- NO raw HTTP POSTs: All actions go through DOM clicks
11.3 Grey Areas
- AJAX round advance:
cfg.ajaxRoundauto-fetches the next round without user clicking "Next Round." The comment at line 74 explicitly says "set to false if you use other scripts that do not support this" — the feature was controversial enough to warrant an off switch and inter-script compatibility warning. cfg.noPopupwith!cfg.stopAtBattleEnd: Automatically dismisses the end-of-battle popup and enters next round (line 1125-1128). This bridges one battle to the next without user input, though the user must still perform the first action of the new battle.cfg.stopOnEquipDrop: The one concession — stops auto-dismissal when valuable equipment drops.
===========================================================================