# 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 `
` 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 `