- 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)
967 lines
45 KiB
Markdown
967 lines
45 KiB
Markdown
# 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.
|
||
|
||
===========================================================================
|