hv-unified/references/monsterbation-battle-patterns.md
GaboGG 402db6bb2f Initial commit: HV Unified v0.11.0 structure
- 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)
2026-07-20 19:30:26 -04:00

967 lines
45 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 SP
- `Durations()` [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 `override` based 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 of `cfg.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
```javascript
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
```javascript
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
```javascript
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 containers
- `div[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:
1. The game's spell icons work on a two-step model: mouseover selects the spell (shows targeting reticle), click confirms
2. The dummy element's `onclick` is set to the spell's `onmouseover` text
3. `dummy.click()` triggers that onmouseover behavior without needing actual mouse movement
4. Then `spell.click()` actually selects the spell
5. Finally `monsters[target].click()` completes the targeting
### 2.5 Default Rotation Configuration
```javascript
// 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
```javascript
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
```javascript
Bind(KEY_CODE, MODIFIER, ACTION)
// OR
Bind(KEY_CODE, ACTION) // Modifier defaults to NoMod
```
Implementation [lines 709-714]:
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
// 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
```javascript
// 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
```javascript
// 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):
```javascript
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)
```javascript
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
```javascript
// 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
```javascript
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
```javascript
var isekai = document.URL.indexOf('isekai') > -1 ? 'i' : ''; // [line 732]
```
All localStorage keys are suffixed with the isekai flag:
- `HVcursor` vs `HVcursori`
- `HVtrackdrops` vs `HVtrackdropsi`
- 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:
```javascript
[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)
```javascript
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
```javascript
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 `onmouseover` attribute via `regexp.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 by `Gems()` function
- Usable highlighting: potions on the quickbar get the `usable` CSS class when the player's MP/SP is low enough for full potion value, triggering blink animation
### 5.4 Cooldowns Display
```javascript
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 `#csp` element
- `alertBackground=false`: colours `#pane_vitals` and spirit stance button individually
**Buff expiry alerts** (in Durations()):
- Tests `cfg.alertBuffs` regex against effect icon filenames
- Triggers when any matching buff has < 2 turns remaining
- Colour: `cfg.colours.expiring` (lightblue)
- Also triggers `interruptAlert` when `cfg.stopOnBuffsExpiring`
**Channelling detection:** If `channeling.png` icon is present in player effects, background changes to `cfg.colours.channelling` (aquamarine).
**Alert priority chain (visual):**
1. Spark/low vitals colour (highest priority)
2. Buffs expiring colour
3. Channelling colour
4. OC full colour (spirit button only)
5. 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
```javascript
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
```javascript
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()
```javascript
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 on `cfg.deleteDropLog`
- `HVcombatlog` either removed or saved based on `cfg.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
```javascript
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
```javascript
// 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
```javascript
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
1. **Intercept the "Next Round" button:** The `#btcp` element's `onclick` is replaced with an AJAX handler
2. **Fetch the current page URL via XMLHttpRequest:** This returns the HTML for the next round
3. **Replace entire document.body** with the new page's body via `innerHTML`
4. **Re-initialize the game engine:** Inject a `<script>` that calls `battle = new Battle()` this re-creates the game's JavaScript state on the new page without a full page load
5. **Clear stray intervals:** `clearInterval(i)` loop clears any timers that might have been set by the previous page's code
6. **Dispatch DOMContentLoaded:** So other scripts (like jpx) see the new page as freshly loaded
7. **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
```javascript
// settings (line 66):
logPasteover: false // add last turn of previous round to new round log. requires ajaxRound
```
Implementation in Enhance() [line 970-972]:
```javascript
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
```javascript
// 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):
```javascript
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
```javascript
// 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 > div` width
- **Spark of Life:** Presence of `fallenshield.png` without `bar_dgreen.png`
- **Buffs/Debuffs:** Parsing `onmouseover` attributes on effect icon `<img>` tags
- **Spell cooldowns:** Parsing `onmouseover` on quickbar buttons, cross-referencing with `timelog.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:
```javascript
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
```javascript
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
```javascript
// 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 `mouseover` event (guarded by `hovering` flag)
- Key presses fire bound actions once per `keydown` (guarded by `release`/`done`)
- The `MutationObserver` waits 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.ajaxRound` auto-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.noPopup` with `!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.
===========================================================================