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)
This commit is contained in:
commit
402db6bb2f
44 changed files with 13418 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
simulator/data/
|
||||
*.log
|
||||
196
docs/DESIGN.md
Normal file
196
docs/DESIGN.md
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
# HV Unified — Design Specification (post-analysis synthesis)
|
||||
## What we learned from 750KB of source code across 4 scripts
|
||||
|
||||
=============================================================================
|
||||
|
||||
## FILES IN KNOWLEDGE BASE
|
||||
|
||||
| File | Size | Lines | Source |
|
||||
|------|------|-------|--------|
|
||||
| Monsterbation 1.4.1.2 | 163KB | 2,402 | Forum attachment |
|
||||
| jpx 2026.07.06 | 270KB | 6,215 | Forum attachment |
|
||||
| HV Utils 4.2.3 | 416KB | 9,710 | Forum attachment |
|
||||
| monsterbation-battle-patterns.md | 46KB | 967 | Our analysis |
|
||||
| jpx-analysis.md | 34KB | - | Our analysis |
|
||||
| HVUT_4.2.3_analysis.md | 32KB | - | Our analysis |
|
||||
| **Total analyzed** | **~750KB+** | **18,327 lines** | |
|
||||
|
||||
Plus 4 community preset files for jpx rules.
|
||||
|
||||
=============================================================================
|
||||
|
||||
## KEY ARCHITECTURAL PATTERNS (cross-script)
|
||||
|
||||
### 1. DOM-clicking, NOT HTTP POSTs
|
||||
ALL scripts click existing page elements. None construct raw HTTP requests.
|
||||
The game's built-in onclick/onmouseover handlers do all the work.
|
||||
This is the single most important pattern for staying within the rules.
|
||||
|
||||
### 2. Dummy Element Trick
|
||||
Monsterbation and our script both use a hidden div to trigger game handlers:
|
||||
```js
|
||||
dummy.setAttribute('onclick', spell.getAttribute('onmouseover'));
|
||||
dummy.click(); spell.click(); monster.click();
|
||||
```
|
||||
|
||||
### 3. Page Detection by DOM, NOT URL
|
||||
jpx and HV Utils both check for specific elements rather than URL patterns:
|
||||
- `#textlog` → battle page
|
||||
- `#navbar` + specific links → bazaar/character/shrine/etc.
|
||||
- `#riddlemaster` → anti-bot popup
|
||||
|
||||
### 4. localStorage for Persistence
|
||||
All three scripts use localStorage extensively:
|
||||
- Monsterbation: `HVmbcfg`, `HVmbp`, `HVmonsterData`, etc.
|
||||
- jpx: `jpx_*` prefixed keys + IndexedDB for battle history
|
||||
- HV Utils: `hvut_*` + `hvuti_*` prefixed keys, namespaced per server
|
||||
|
||||
### 5. Battle Log MutationObserver
|
||||
jpx uses a MutationObserver on `#textlog` as the primary event loop.
|
||||
This fires on every server response (new HTML injected by the game engine).
|
||||
Our script adopts this pattern.
|
||||
|
||||
### 6. Spell Detection by onmouseover Text
|
||||
All scripts find spells by searching for divs whose onmouseover attribute
|
||||
contains the spell name. No hardcoded spell IDs needed.
|
||||
|
||||
### 7. Vitals by Bar Width Ratios
|
||||
HP/MP/SP all derived from `img.style.width` / base_width.
|
||||
Different bases for persistent (414) vs isekai (207/496/190).
|
||||
|
||||
### 8. Monster Detection by onclick Attribute
|
||||
Alive monsters have `onclick`; dead ones don't. Simple and reliable.
|
||||
|
||||
=============================================================================
|
||||
|
||||
## WHAT EACH SCRIPT EXCELS AT
|
||||
|
||||
| Feature | Monsterbation | jpx | HV Utils |
|
||||
|---------|:---:|:---:|:---:|
|
||||
| Hover-based attacks | ★★★★★ | ★★ | — |
|
||||
| Keyboard shortcuts | ★★★★ | ★★★★ | — |
|
||||
| Conditional rule engine | ★★ | ★★★★★ | — |
|
||||
| Battle statistics | ★★★ | ★★★★★ | — |
|
||||
| Buff/cooldown display | ★★★★★ | ★★★★ | — |
|
||||
| Config/profile system | ★★★★ | ★★★ | ★★ |
|
||||
| Out-of-battle shop | ★ (CrunkJuice) | ★★ | ★★★★★ |
|
||||
| Shrine automation | — | — | ★★★★★ |
|
||||
| Monster lab | ★ (CrunkJuice) | — | ★★★★ |
|
||||
| MoogleMail | — | — | ★★★★★ |
|
||||
| Training queue | — | — | ★★★★★ |
|
||||
| RE timer | ★ (CrunkJuice) | ★ | ★★★★ |
|
||||
| Market integration | — | ★★★ | ★★★★★ |
|
||||
| Equipment management | — | — | ★★★★★ |
|
||||
| Fighting style auto-detect | — | ★★★★★ | — |
|
||||
| Level-progression aware | — | — | — |
|
||||
|
||||
The gap: NO script handles the full level 1→500 progression journey.
|
||||
jpx says "Lv.300+ recommended." Monsterbation assumes you know what you're doing.
|
||||
HV Utils is all out-of-battle. Our unified script fills this gap.
|
||||
|
||||
=============================================================================
|
||||
|
||||
## IMPLEMENTATION STATUS — Phase 1 (Current)
|
||||
|
||||
### ✅ Done:
|
||||
- Page detector (battle, bazaar, arena, shrine, equipshop, etc.)
|
||||
- Battle state parser (vitals, monsters, buffs, spells, skills)
|
||||
- Level detection & tier auto-selection
|
||||
- 4 strategy tiers: Novice (1-50), Adept (50-150), Veteran (150-300), Master (300+)
|
||||
- Action system: castSpell, useItem, attackMonster, useSkill, toggleSpirit
|
||||
- Monster targeting: findWeakestMonster, findStrongestMonster
|
||||
- Debuff tracking: checkMonsterDebuff
|
||||
- Keybinding: hotkey + modifier support, toggle hover, force cure
|
||||
- Hover system: mouse enter monster → execute action
|
||||
- Monster numbering in battle
|
||||
- RE timer placeholder
|
||||
- Equipment shop: Quick Sell (≤ quality) button
|
||||
- Shrine: Bulk Shrine button
|
||||
- MutationObserver on battle log
|
||||
- Console API: window.HV.getState(), .getAction(), .execute()
|
||||
|
||||
### 🔜 Pending (Phase 2-4):
|
||||
- Cooldown display on quickbar
|
||||
- Buff duration counters
|
||||
- Alert colours (low HP, spark, expiring buffs)
|
||||
- Spirit Stance auto-management
|
||||
- Elemental weakness matching from monster DB
|
||||
- Monster HP database (track HP values to show numbers)
|
||||
- Settings panel (press , to open)
|
||||
- Profile/persona/set switching support
|
||||
- Isekai mode detection
|
||||
- Equipment shop: salvage vs sell comparison
|
||||
- Training queue with cost calculation
|
||||
- MoogleMail: search, preview
|
||||
- Monster lab: crystal feeder, morale display
|
||||
- Market price integration
|
||||
- Battle statistics & damage tracking
|
||||
- Arena completion tracking
|
||||
- PXP simulator for Item World
|
||||
- Import/export battle configs (like jpx presets)
|
||||
- Full conditional rule engine (for master tier custom rules)
|
||||
|
||||
=============================================================================
|
||||
|
||||
## DESIGN DECISIONS
|
||||
|
||||
1. **Single file, no build step** — users install directly as Tampermonkey script
|
||||
2. **Vanilla JS, no dependencies** — works in any browser with Tampermonkey
|
||||
3. **Progressive enhancement** — features activate based on level/detected capabilities
|
||||
4. **Safe-by-default** — auto-buffs only maintain, never waste MP below thresholds
|
||||
5. **Opt-in automation** — out-of-battle features have explicit buttons, not auto-fire
|
||||
6. **Rules-compliant** — one key press = one action. No multi-turn sequences.
|
||||
7. **Console API** — power users can script via window.HV
|
||||
|
||||
=============================================================================
|
||||
|
||||
## INSTALLATION
|
||||
|
||||
1. Install Tampermonkey extension in your browser
|
||||
2. Open Tampermonkey dashboard
|
||||
3. Create new script, paste hv-unified.user.js
|
||||
4. Save. Script auto-runs on hentaiverse.org
|
||||
|
||||
## USAGE
|
||||
|
||||
- **Q key**: Execute recommended action (configurable)
|
||||
- **H key**: Toggle hover mode (auto-attack on mouse-over monsters)
|
||||
- **C key**: Force cast Cure/Full-Cure
|
||||
- **Console**: `HV.getAction()` → see what the script would do next
|
||||
| Console: `HV.set('cureHP', 0.5)` → change cure threshold
|
||||
|
||||
=============================================================================
|
||||
|
||||
## FORUM-SOURCED PLAYER KNOWLEDGE (added 2026-07-20)
|
||||
|
||||
We now have direct access to the e-hentai forums. Key threads saved to references/forum-*.md:
|
||||
|
||||
### What the community actually cares about (vs what we assumed)
|
||||
|
||||
1. **Stats don't matter much** — Noni (mod, L500): "They barely even matter. Just do what feels right. Only INT isn't needed for melee, and STR isn't needed for mage." Our elaborate % allocation tables in KB are over-engineered.
|
||||
|
||||
2. **Proficiency > everything** — The single biggest differentiator between playstyle performance is weapon/armor proficiency. Not stats, not gear quality.
|
||||
|
||||
3. **Items before spells** — "Items are faster and safer than spells. Casting any spell costs time; items are instant, monsters don't attack after you use them." Our novice strategy prioritizes Cure before health items — should reverse.
|
||||
|
||||
4. **Use items first, Cure only if still low** — Player wisdom contradicts our strategy engine's priority.
|
||||
|
||||
### Endgame meta (from battle records)
|
||||
- Mage dominates speed: Holy Mage does DwD (L300 arena) in ~800 turns vs 1H Heavy in ~4500 turns
|
||||
- Fastest builds: Holy Mage > Dark Mage > Wind Mage > Cold Mage > Fire Mage > Elec Mage > 1H Mage > DW > 1H > Niten > 2H > 1H Heavy
|
||||
- 1H Heavy is the tankiest but slowest endgame build
|
||||
- Meta gear: Feather+Aether charms, Radiant phase for mages, Power Slaughter for melee
|
||||
|
||||
### IW Potency (research data)
|
||||
- 1H: Butcher > Fatality > Overpower (Overpower inefficient for 1H because counters already stun)
|
||||
- All other melee: Overpower > Butcher > Fatality
|
||||
- Overpower only affects normal attacks (not skills/counters)
|
||||
|
||||
### Accuracy cap confirmed
|
||||
- 150%+ after L200, 200% is useless
|
||||
|
||||
### Key corrections to our KB data
|
||||
- Spell unlocks (KB.lv spellUnlocks): MagNet was removed in a recent patch (jpx 20260705 removed Magnet features)
|
||||
- Imperil should be prioritized much higher — it's "THE most important debuff" per community
|
||||
- The "Nintendo" difficulty suggestion for Veteran is wrong — players run PFUDOR at L150+ for max rewards
|
||||
- Spirit Stance should activate earlier — the community consensus is OC 60-80%, not 90%
|
||||
768
references/HVUT_4.2.3_analysis.md
Normal file
768
references/HVUT_4.2.3_analysis.md
Normal file
|
|
@ -0,0 +1,768 @@
|
|||
# HV Utils 4.2.3 — Deep Structural Analysis
|
||||
|
||||
**Source:** `/home/gabogg/Downloads/HVUT_4.2.3.txt`
|
||||
**Lines:** 9,710 (416 KB)
|
||||
**Author:** sssss2
|
||||
**Date:** 2026-06-21
|
||||
**Sections:** 19 page-specific modules + 10 core shared modules
|
||||
|
||||
---
|
||||
|
||||
## 1. PAGE DETECTION
|
||||
|
||||
HV Utils uses a multi-layered detection system:
|
||||
|
||||
### Primary: URL Query Parsing (Line 212)
|
||||
```js
|
||||
const _query = Object.fromEntries(location.search.slice(1).split('&')
|
||||
.map((q) => { const [k, v = ''] = q.split('=', 2);
|
||||
return [decodeURIComponent(k), decodeURIComponent(v)];
|
||||
}));
|
||||
```
|
||||
This parses Hentaiverse's `?s=X&ss=Y&screen=Z&filter=W` format into `_query.s`, `_query.ss`, `_query.screen`, `_query.filter`.
|
||||
|
||||
### Server Detection (Line 213-217)
|
||||
```js
|
||||
const _server = {
|
||||
name: location.pathname.includes('/isekai/') ? 'isekai' : 'persistent',
|
||||
season: $id('world_text')?.textContent.match(/\d+ Season \d+/)?.[0] || '1',
|
||||
};
|
||||
```
|
||||
Also sets `_server.persistent` / `_server.isekai` for boolean checks.
|
||||
|
||||
### Page Dispatch Architecture (Lines 3819–9710)
|
||||
The script uses a chain of `if/else if` blocks testing `_query.s`, `_query.ss`, and DOM elements:
|
||||
|
||||
| Section | Condition | Module | Line |
|
||||
|---------|-----------|--------|------|
|
||||
| Character | `s=Character, ss=ch` | `_ch` | 3819 |
|
||||
| Equipment | `s=Character, ss=eq` | `_eq` | 3922 |
|
||||
| Abilities | `s=Character, ss=ab` | `_ab` | 4096 |
|
||||
| Training | `s=Character, ss=tr` | `_tr` | 4467 |
|
||||
| Item Inventory | `s=Character, ss=it` | `_it` | 4675 |
|
||||
| Settings | `s=Character, ss=se` | `_se` | 4703 |
|
||||
| Item Shop | `s=Bazaar, ss=is` | `_is` | 4806 |
|
||||
| The Shrine | `s=Bazaar, ss=ss` | `_ss` | 4832 |
|
||||
| The Market | `s=Bazaar, ss=mk` | `_mk` | 5373 |
|
||||
| Monster Lab | `s=Bazaar, ss=ml` | `_ml` | 5485 |
|
||||
| MoogleMail | `s=Bazaar, ss=mm` | `_mm` | 6869 |
|
||||
| Lottery | `s=Bazaar, ss=lt/la` | `_lt` | 8499 |
|
||||
| Battle → Arena | `s=Battle, ss=ar` | `_ar` | 8560 |
|
||||
| Battle → RoB | `s=Battle, ss=rb` | `_ar` | 8570 |
|
||||
| Battle → Tower | `s=Battle, ss=tw` | — | 8580 |
|
||||
| Battle → GrindFest | `s=Battle, ss=gr` | — | 8587 |
|
||||
| Battle → Item World | `s=Battle, ss=iw` | — | 8594 |
|
||||
| Armory (Equip Shop) | `s=Bazaar, ss=am` | `$armory` | 8609 |
|
||||
| Modify (single equip) | `s=Bazaar, ss=am, screen=modify` | `_mo` | 9669 |
|
||||
|
||||
### No-Navbar Early Exit (Lines 1144–1164)
|
||||
Before any page dispatch, the script checks `$id('navbar')`. If absent, it handles:
|
||||
- **Battle pages:** Activates RE timer in battle mode
|
||||
- **e-hentai.org gallery:** Activates RE timer in gallery mode
|
||||
- **Riddle Master:** Commented out
|
||||
Then `return` — none of the page modules execute.
|
||||
|
||||
---
|
||||
|
||||
## 2. INVENTORY/SHOP AUTOMATION
|
||||
|
||||
### Equipment Shop (`$armory`, Lines 8609–9667)
|
||||
|
||||
**Architecture:** A self-contained module on `?s=Bazaar&ss=am` pages with sub-modules:
|
||||
|
||||
#### Sidebar Buttons (Line 8752-8801)
|
||||
- **Select All** — toggles all non-protected, non-filtered checkboxes
|
||||
- **Tradeables / Pinned** — bulk selection by status
|
||||
- **Equip Code** — generate forum BBcode for selected equipment
|
||||
- **Sell / Salvage / Purchase & Salvage** — with smart selection:
|
||||
- `sell`: selects items where sell_price ≥ salvage_value, excluding protected/locked/stored
|
||||
- `salvage`: selects items where salvage_value > sell_price
|
||||
- `purchase_salvage`: selects items profitable to buy+salvage
|
||||
- **Bazaar Filters / Protect Filters** — toggle between show-all and hide-based-on-filters
|
||||
|
||||
#### Protection System (Line 9073-9089)
|
||||
- The `protect()` method checks: `eq.info.protected || eq.info.pinned || $equip.filter.equip($config.settings.equipmentShopProtectFilters, eq)`
|
||||
- Protected equipment is moved to a special `<tbody>` at the top labeled "Protected Equipment"
|
||||
- Their checkboxes are disabled from "Select All"
|
||||
- If `equipmentShopAutoProtect` is enabled, auto-submits a protect action
|
||||
|
||||
#### Bazaar Filter System (Line 9090-9104)
|
||||
- Applies `equipmentShopBazaarFilters` to hide non-valuable equipment
|
||||
- Also shows items where `salvage_value > purchase_price` (profitable to buy and salvage)
|
||||
- Hidden items get `hvut-eqp-hidden` class; category headers auto-hide when all children hidden
|
||||
|
||||
#### Salvage Value Calculation (`$armory.calc`, Line 8852-8938)
|
||||
- **`calc.materials(eq)`**: Estimates salvage materials based on quality, material type, core type, and rare status:
|
||||
- Quality 1-3 (Crude–Average): `Scrap [type]` = `min(10, ceil(sell_price / 100))`
|
||||
- Quality 4 (Superior): `Low-Grade [type]` × (1 on persistent, 3 on isekai)
|
||||
- Quality 5 (Exquisite): `Mid-Grade [type]`
|
||||
- Quality 6 (Magnificent): `High-Grade [type]`
|
||||
- Quality 7+ (Legendary/Peerless): adds `[Quality] [Weapon/Staff/Armor] Core` (×5 if rare)
|
||||
- Rare items: adds `Energy Cell`
|
||||
- **`calc.value(materials)`**: Multiplies by item prices (with optional 1% market fee deduction)
|
||||
- Shows "C" (sell price) and "V" (salvage value) columns, highlighting profitable salvage in green
|
||||
|
||||
#### Submit Confirm (Line 9184-9194)
|
||||
Three modes based on `equipmentShopConfirm`:
|
||||
- `0`: default browser confirm dialog
|
||||
- `1`: auto-check confirm checkbox
|
||||
- `2`: skip confirmation entirely
|
||||
|
||||
#### Screen-Specific Modifications (`$armory.modify`, Line 8978-9042)
|
||||
- **organize**: Adds note input fields (for `@price, $note` forum code annotations)
|
||||
- **modify**: Shows upgrade/IW levels
|
||||
- **purchase**: Shows purchase price and salvage value side-by-side; applies bazaar filter
|
||||
- **sell**: Loads salvage page to get salvage values; protects valuable items
|
||||
- **salvage**: Loads sell page to get sell prices; protects valuable items
|
||||
|
||||
#### All-In-One Integration (Line 8940-8975)
|
||||
- `$armory.integrate` loads ALL equipment categories (1H, 2H, Staff, Shield, Cloth, Light, Heavy) into a single unified table
|
||||
- Adds an "All" tab to the filter bar
|
||||
- Each category loads asynchronously via AJAX
|
||||
|
||||
### Item Shop (`_is`, Lines 4806–4828)
|
||||
- Minimal: just adds `hvut-item-{type}` CSS class to each row for color coding
|
||||
- Types: Consumable, Artifact, Trophy, Token, Crystal, MonsterFood, Material, Collectable
|
||||
|
||||
---
|
||||
|
||||
## 3. SHRINE (`_ss`, Lines 4832–5371)
|
||||
|
||||
### Bulk Offering Queue
|
||||
|
||||
**Per-Item Controls (Line 5029-5081):**
|
||||
- Each item row gets:
|
||||
- A number input for offer count
|
||||
- An "Offer" button
|
||||
- For trophies: "All" button (offers max = `floor(stock / bulk)`)
|
||||
- Displays bulk grouping info (e.g., " / 1000")
|
||||
|
||||
**Trophy Value System (Line 5050-5068):**
|
||||
- Trophies have tier-based values (1000c for T2, 2000c for T3, 4000c for T4, 5000c for T5)
|
||||
- Auto-calculates upgrade paths: combining lower-tier trophies into higher tiers increases value
|
||||
- Shows upgraded trophy tier and value
|
||||
|
||||
**Item Hiding (Line 5070-5072):**
|
||||
- Items matching `shrineHideItems` (default: `['Figurine', 'Peerless Voucher']`) get `hvut-none-item` class
|
||||
- Toggle button to show/hide these filtered items
|
||||
|
||||
**Reward Selection (Line 4911-4948):**
|
||||
- Intercepts reward selection buttons, adds visual highlight on selected reward
|
||||
- Tracks `reward_type` and `reward_slot` for each offering request
|
||||
|
||||
### Result Tracking
|
||||
|
||||
**Offering Requests (Line 5083-5371):**
|
||||
- Each offering creates an AJAX request with: `iid`, `count`, optional `reward_type`/`reward_slot`
|
||||
- Results parsed from server response messages
|
||||
- Equipment rewards filtered through `shrineFilters` (default: Peerless, Legendary, Magnificent, Exquisite)
|
||||
- Results displayed in a toggleable table showing per-item breakdowns:
|
||||
- Percentage and count for each reward type
|
||||
- Equipment names shown for quality-filtered items
|
||||
- Grouped categories (Pouches, Charms, High-Grade Materials, Bindings, Crystals, PABs)
|
||||
|
||||
**Shrine Log (Line 4889-4889):**
|
||||
- Persistent log of all offering results
|
||||
- Reset button to clear log
|
||||
|
||||
---
|
||||
|
||||
## 4. MONSTER LAB (`_ml`, Lines 5485–6867)
|
||||
|
||||
### Monster List Display
|
||||
|
||||
**Sort System (Line 5660-5685):**
|
||||
- Sorts by: index, name, class, PL, wins, kills, gains, gifts, morale, hunger
|
||||
- Configurable default via `monsterLabDefaultSort`
|
||||
- Click column headers to sort; clicking again reverses order
|
||||
|
||||
**Per-Monster Tracking (Line 5741-5836):**
|
||||
- `_ml.mobs[]` array stores: name, class, PL, wins, kills, PA values, ER values, CT values, morale, hunger, gifts log
|
||||
- `_ml.log[]` persisted to storage with: date, PL, wins, kills, PA/ER/CT arrays, 49-element gift array
|
||||
|
||||
**Morale/Hunger Display (Line 5791-5803):**
|
||||
- Parses the pixel-width of bar images: `hunger = width * 200`, `morale = width * 200`
|
||||
- Shows numeric value overlay on each bar
|
||||
|
||||
### Feed System
|
||||
|
||||
**Feed Actions (Line 5877-5909):**
|
||||
- **Click morale bar:** Feeds drugs (increases morale)
|
||||
- **Click hunger bar:** Feeds food (increases hunger)
|
||||
- **Click wins/kills:** Updates monster stats (fetches the monster page)
|
||||
- After feeding, auto-updates: PL, wins, kills, PAs, ERs, CTs, morale, hunger bars
|
||||
|
||||
**Bulk Feed (Line 5889-5891):**
|
||||
- "Update Wins/Kills" button feeds ALL monsters
|
||||
- `feedall(stat, value, food)` — conditional bulk feeding
|
||||
|
||||
### Gift Tracking
|
||||
|
||||
**Gift Summary (Line 5921-5976):**
|
||||
- Parses message box for gift notifications: "X brought you a gift!" + "Received N× Item"
|
||||
- Computes total gifts and estimated credit value using `$price.value()`
|
||||
- Shows per-material breakdown in a summary panel
|
||||
|
||||
**Monster Log (Line 5996-6023):**
|
||||
- Per-monster log showing all 49 tracked material types
|
||||
- Shows: days since first log, total gifts, total value, daily average
|
||||
- Formatted in two-column layout
|
||||
- Materials tracked: 12 grade materials + Phazon + Shade Fragment + Repurposed Actuator + Defense Matrix + 33 Bindings + World Seed
|
||||
|
||||
### Crystal/Pill Feeder (Monster Upgrader, Lines 6037-6867)
|
||||
|
||||
**Upgrade Table (Line 6096-6135):**
|
||||
- Spreadsheet-style grid: rows = monsters, columns = stats
|
||||
- **Primary Attributes (PA):** STR, DEX, AGI, END, INT, WIS → use Vigor/Finesse/Swiftness/Fortitude/Cunning/Knowledge crystals
|
||||
- **Elemental Resistances (ER):** FIRE, COLD, ELEC, WIND, HOLY, DARK → use Flames/Frost/Lightning/Tempest/Devotion/Corruption crystals
|
||||
- **Chaos Tokens (CT):** 12 stats → use Chaos Tokens
|
||||
|
||||
**Bulk Operations:**
|
||||
- Increase/Decrease all monsters' stats at once
|
||||
- Equalize (set all to highest value)
|
||||
- Per-stat buttons for each crystal type
|
||||
- Right-click to decrease, left-click to increase
|
||||
|
||||
**Crystal Stock Tracking:**
|
||||
- Loads item inventory to show available crystals
|
||||
- Shows per-crystal usage and remaining stock
|
||||
|
||||
### Power Level Calculator (Lines ~6700-6867)
|
||||
- Complex PL simulation with PA/ER/CT values
|
||||
- Input fields for custom values with slider-like controls
|
||||
- Shows PL changes per stat
|
||||
|
||||
---
|
||||
|
||||
## 5. MOOGLEMAIL (`$mail` + `_mm`, Lines 2174–2413, 6869–8498)
|
||||
|
||||
### Send Engine (`$mail`)
|
||||
|
||||
**Queue System (Line 2179-2305):**
|
||||
- `$mail.queue[]`: Array of mail chunks
|
||||
- `$mail.current`: Index of current mail being processed
|
||||
- `$mail.ready`: Semaphore preventing concurrent sends
|
||||
- Auto-chains: when one mail completes, the next automatically starts
|
||||
|
||||
**Chunking (Line 2306-2386):**
|
||||
- Splits large attachments into chunks of 10 items each
|
||||
- Auto-generates subject from first item name
|
||||
- Calculates CoD totals with optional deduction
|
||||
- Adds attachment text to body
|
||||
|
||||
**Send Process (Line 2186-2305):**
|
||||
1. Fetches MoogleMail token if not cached
|
||||
2. Removes any existing attachments
|
||||
3. Attaches items via `attach_add` API (Promise.all for concurrency)
|
||||
4. Sets CoD if applicable
|
||||
5. For persistent CoD (Isekai → Persistent): opens persistent MoogleMail, attaches credits, sets CoD there
|
||||
6. Sends message
|
||||
7. On completion: advances to next queue item, or redirects to Sent folder
|
||||
|
||||
**Error Handling (Line 2387-2398):**
|
||||
- Checks server response for error messages
|
||||
- On error: logs and discards the draft
|
||||
|
||||
### Write UI (`_mm.write`, Lines 6889-7089)
|
||||
|
||||
**Compose Interface:**
|
||||
- To: with datalist autocomplete from user list
|
||||
- Subject: auto-filled from first attachment
|
||||
- Body: textarea
|
||||
- CoD Deduction field
|
||||
- Persistent CoD checkbox (Isekai only)
|
||||
|
||||
**Attachment Panels (three tabs):**
|
||||
1. **Credits** (`_mm.credits`): Amount input with pre-set buttons (10k, 100k, 500k, 1m, etc.)
|
||||
2. **Equipment** (`_mm.equip`): List of all equipment with checkboxes, search by name/EID, protected-item warning
|
||||
3. **Items** (`_mm.item`): Filter by group (Consumables, Materials, Trophies, Crystals, Figures), search by name, bulk count/check
|
||||
|
||||
**Item Attachment (Lines 7092-7169):**
|
||||
- Search: type partial item names, separated by commas
|
||||
- Each item row: checkbox, count input, price input, auto-calculated CoD
|
||||
- "CALC" button: previews attachment text
|
||||
- "ATTACH from TEXT": parses free-form text like `100 x Health Potion @ 10`
|
||||
- "SEND" button on each row for single-item quick send
|
||||
- "SEND ALL" to send everything checked
|
||||
|
||||
**User List (Line 7020-7049):**
|
||||
- Persisted list of recipients
|
||||
- Auto-adds on send
|
||||
- Deduplication on save
|
||||
- Editable via popup
|
||||
|
||||
---
|
||||
|
||||
## 6. TRAINING (`_tr`, Lines 4467–4671)
|
||||
|
||||
### Queue Management
|
||||
|
||||
**Data Model (Line 4470-4488):**
|
||||
- `_tr.json` (stored as `hvut_tr_notif`): `{ current_name, current_level, current_end, next_name, next_level, next_id, error }`
|
||||
- Training data: id, base cost, linear cost increment, exponential factor for all 15 trainable skills
|
||||
|
||||
**Progress Tracking (Line 4548-4559):**
|
||||
- Reads `end_time` from server-rendered JavaScript
|
||||
- Shows current training name + level in bottom bar
|
||||
- Countdown timer with `HH:MM:SS` format
|
||||
|
||||
**Queue System (Lines 4491-4659):**
|
||||
- **Plan Training:** Select skill + target level, shows calculated credit cost
|
||||
- **Set:** Saves `next_name`/`next_level`/`next_id` to storage
|
||||
- When current training completes (or on page load if idle):
|
||||
1. Fetches training page
|
||||
2. Checks if `next_name`'s current level < `next_level`
|
||||
3. If yes: auto-submits `start_train=<id>` form
|
||||
4. If no: shows "Training completed!"
|
||||
- **Cancel:** Clears the notification JSON
|
||||
- **Reset Planning:** Clears both current and next
|
||||
|
||||
**Cost Calculation (Line 4611-4628):**
|
||||
- Formula: `Σ pow(base + linear × level, 1 + exp × level)` for each level from current to target
|
||||
- Uses the real game formulas for each training type
|
||||
- Shows spent credits per training and total spent
|
||||
|
||||
**Bottom Bar Integration (Line 3647-3721):**
|
||||
- `_bottom.tr` shows current training status in compact format
|
||||
- Links to training page
|
||||
- Auto-loads when timer expires
|
||||
|
||||
---
|
||||
|
||||
## 7. EQUIPMENT MANAGEMENT
|
||||
|
||||
### Upgrade Queue (`_mo.upgrade`, Lines 9669–9704)
|
||||
|
||||
On the single-equipment Modify page:
|
||||
- Parses the upgrade materials table
|
||||
- Calculates total cost using `$price.value(mats) + credits`
|
||||
- Shows clickable total cost button → opens price editor
|
||||
|
||||
### Salvage/Repair/Rebuild (via `$armory`, Lines 8609-9667)
|
||||
|
||||
**Salvage vs Sell Comparison:**
|
||||
- Calculates `salvage_value` from estimated materials × market prices
|
||||
- Compares with `sell_price` or `purchase_price`
|
||||
- Highlights profitable salvage in green (`hvut-eqp-profit`)
|
||||
- "Purchase & Salvage" action filters for profitable arbitrage
|
||||
|
||||
**Batch Operations:**
|
||||
- `$armory.submit.confirm('sell', ...)` / `$armory.submit.confirm('salvage', ...)`
|
||||
- Gets the real submit button from the page, submits multi-equip form
|
||||
|
||||
### Reforge/Soulfuse
|
||||
- Not directly implemented as separate features
|
||||
- The Armory page covers all screens (organize, modify, repair, soulbind, purchase, sell, salvage)
|
||||
|
||||
### Equipment Data (`$armory.equipdata`, Line 8621)
|
||||
- Stored as `hvut_equipdata` (persistent) / `hvuti_equipdata` (isekai)
|
||||
- Contains per-EID: price (for forum code), note (for forum code `$featured` flag)
|
||||
|
||||
---
|
||||
|
||||
## 8. ITEM WORLD (Line 8594–8600)
|
||||
|
||||
The IW implementation is relatively minimal:
|
||||
- Renders equipment list table with `$equip.list.table()`
|
||||
- Moves the equipment blurb to the action area
|
||||
- Initializes the battle panel (`$battle.init()`) for the equip select outer div
|
||||
- **No PXP simulator is present** in this version (4.2.3). The script relies on the game's built-in IW interface.
|
||||
|
||||
Note: There *is* an EXP Simulator on the Character page (`_ch.exp`, Line 3823-3901) which uses the same exponential formula as the game.
|
||||
|
||||
---
|
||||
|
||||
## 9. ARENA/BATTLE MODES (Lines 8517–8604)
|
||||
|
||||
### Compact View
|
||||
|
||||
All battle modes (Arena, Ring of Blood, Tower, GrindFest, Item World) use `$battle.init()`:
|
||||
|
||||
**Layout System:**
|
||||
- Wraps main pane in `hvut-bt-outer` class
|
||||
- Adds a 600px side panel (`#hvut-bt-div`) on the left or right (configurable)
|
||||
- Compact mode (`hvut-bt-on`): reduces main pane to 620px, side panel slides in
|
||||
- Toggle button: "Details" / "Collapse"
|
||||
|
||||
**Equipment Panel (`.hvut-bt-equip`):**
|
||||
- Shows equipped gear from current equip set
|
||||
- Per-equip row: name (link), condition% / energy%, repair button
|
||||
- Condition ≤ threshold (default 20%) shown in warning color
|
||||
- Hover on repair shows per-material breakdown for that equip
|
||||
|
||||
**Item Inventory Panel (`.hvut-bt-items`):**
|
||||
- Shows configured items from `equipPanelItemInventory`
|
||||
- Format: `name (stock / threshold)`
|
||||
- Warning if stock < threshold
|
||||
- Click item name to buy from Item Shop
|
||||
|
||||
**Repair All Panel:**
|
||||
- Calculates total materials needed
|
||||
- Checks item inventory; if insufficient, offers to buy from Item Shop (Scraps, Energy Cell)
|
||||
- Non-purchasable items (Infusions, Shards) trigger alert to buy from Market
|
||||
|
||||
### Arena-Specific
|
||||
- Splits colspan=2 rows for proper compact layout
|
||||
- Re-parents arena_list into arena_outer div
|
||||
|
||||
### Ring of Blood
|
||||
- Same split_colspan treatment
|
||||
- Includes arena_tokens in the outer
|
||||
|
||||
---
|
||||
|
||||
## 10. RE TIMER (`$re`, Lines 947–1142)
|
||||
|
||||
### Detection Mechanisms
|
||||
|
||||
**Mode Detection (Line 953):**
|
||||
```js
|
||||
$re.type = (!location.hostname.includes('hentaiverse.org') || _server.isekai) ? 'eh'
|
||||
: $id('navbar') ? 'hv'
|
||||
: $id('battle_top') ? 'ba'
|
||||
: false;
|
||||
```
|
||||
- **`eh`**: e-hentai.org gallery or Isekai server
|
||||
- **`hv`**: Normal Hentaiverse pages (has navbar)
|
||||
- **`ba`**: Battle pages (has battle_top)
|
||||
- **`false`**: Unknown/unsupported
|
||||
|
||||
### Key Management
|
||||
|
||||
- Stored as `hvut_re` (cross-profile, not namespaced): `{ date, key, count, clear }`
|
||||
- **30-minute timer:** Random Encounter refreshes every 30 minutes
|
||||
- **Auto-detection:**
|
||||
- On HV pages: checks `location.search` for `encounter=<key>`
|
||||
- On e-hentai: scrapes the event pane for the encounter link
|
||||
- **Daily reset:** If the stored date is from a different UTC day, auto-resets
|
||||
|
||||
### Notification
|
||||
|
||||
- **Countdown display:** Shows `MM:SS [count]` while timer active
|
||||
- **"Ready"**: When timer expires, button shows "Ready [count]"
|
||||
- **"Expired"**: If an encounter was generated but not cleared
|
||||
- **Beep:** Audio notification at `[0.2, 500, 0.5]` (volume, Hz, seconds) when timer expires — configurable, with test button
|
||||
- **Warning highlight:** `hvut-warn` class when an uncleared encounter exists
|
||||
|
||||
### Engagement
|
||||
|
||||
- **Click RE button:** Engages if ready/uncleared (or Ctrl+Click to force engage)
|
||||
- **Battle mode:** Shows RE status, click loads new key
|
||||
- **Gallery mode:** Opens in new tab (configurable alt.hentaiverse.org)
|
||||
- **Gallery detection:** Scrapes e-hentai.org news.php for the encounter key
|
||||
|
||||
---
|
||||
|
||||
## 11. DIFFICULTY/PERSONA/SET SELECTOR
|
||||
|
||||
### Difficulty Changer (`$dfct`, Lines 3280–3337)
|
||||
|
||||
**Location:** Top bar, shows current difficulty (Normal/Hard/Nightmare/Hell/Nintendo/IWBTH/PFUDOR)
|
||||
|
||||
**Mechanism:**
|
||||
- Hover reveals dropdown with all 7 difficulties
|
||||
- Selecting triggers:
|
||||
1. Fetch settings page
|
||||
2. Extract form data
|
||||
3. Modify `difflevel`
|
||||
4. POST updated form
|
||||
5. Update local `_player.difficulty`
|
||||
6. Persist to `hvut_ch_style`
|
||||
|
||||
**State:** Persists chosen difficulty in `ch_style.difficulty` storage
|
||||
|
||||
### Persona & Equipment Set Selector (`$persona`, Lines 3340–3594)
|
||||
|
||||
**Location:** Top bar, shows "Persona" by default, then current set name
|
||||
|
||||
**Data Model:**
|
||||
```js
|
||||
$persona.json = {
|
||||
pset: current_persona_number,
|
||||
plen: total_personas,
|
||||
pname: current_persona_name,
|
||||
eset: current_equip_set_number,
|
||||
elen: total_equip_sets,
|
||||
ename: current_set_name,
|
||||
[pset]: { name: "...", [eset]: { name: "..." }, ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Persona Switching:**
|
||||
1. Fetch `?s=Character&ss=ch` with `persona_set=<N>`
|
||||
2. Parse persona form for all persona names/IDs
|
||||
3. Check if current matches stored
|
||||
|
||||
**Equip Set Switching:**
|
||||
1. Fetch `?s=Character&ss=eq` with `equip_set=<N>`
|
||||
2. Detect active set by finding `_on.png` image
|
||||
3. Save equip config to `$config.set('equipset', ...)` — array of `{ slot, category, name, customname, eid, key }`
|
||||
|
||||
**Auto-reload:** After persona/set change, reloads current page if on Equipment, Abilities, Items, or Settings tabs
|
||||
|
||||
**Stats Parsing:**
|
||||
- Parses the stats pane to determine:
|
||||
- Fighting Style (Staff/Dualwield/Niten/Two-Handed/One-Handed/Unarmed)
|
||||
- Best spell type (highest affinity)
|
||||
- Proficiency factor
|
||||
- Magic score
|
||||
- Saves to `ch_style` for external use (e.g., monsterbation)
|
||||
|
||||
**Warnings:**
|
||||
- Shows red warning bar for: repair needed, attribute check, exhausted stamina
|
||||
- Stamina warning for: Exhausted, accuracy penalty, or below `warnLowStamina`
|
||||
|
||||
---
|
||||
|
||||
## 12. UI SYSTEM
|
||||
|
||||
### Top Navigation Bar (Lines 3030–3277)
|
||||
|
||||
**Complete replacement** of the game's `#navbar`:
|
||||
- Hidden via `#navbar { display: none; }`
|
||||
- Custom `#hvut-top` flexbox bar with:
|
||||
- **MENU dropdown** (or individual section dropdowns if integration disabled): Character, Bazaar, Battle, Armory categories in organized columns
|
||||
- **Quick links:** Configurable 2-letter link buttons (CH, EQ, AB, TR, IT, SE, IS, SS, MK, ML, MM, etc.)
|
||||
- **Stamina display** with dropdown for restorative items
|
||||
- **Level display** with EXP progress bar
|
||||
- **Difficulty** dropdown
|
||||
- **Persona/Equip Set** dropdown
|
||||
- **RE Timer** display
|
||||
- **Server** indicator with switch link
|
||||
- **Settings** gear icon
|
||||
|
||||
### Bottom Bar (Lines 3597–3816)
|
||||
|
||||
`#hvut-bottom` below the main pane:
|
||||
- **Credits** display (C:)
|
||||
- **Equipment Inventory** capacity (E: usage/capacity) with warnings near limit
|
||||
- **Training** status with countdown
|
||||
- **Lottery** displays for Weapon and Armor lotteries:
|
||||
- Shows current equipment + time until next drawing
|
||||
- Red highlight when drawing is imminent
|
||||
- Pops up alert when qualifying equipment appears (checked against `lotteryFilters`)
|
||||
|
||||
### Color Coding System (Lines 2772–2820)
|
||||
|
||||
**CSS Custom Properties** on `:root`:
|
||||
|
||||
**Font Colors:**
|
||||
- `--color-font-default`: #5C0D11 (dark red-brown)
|
||||
- `--color-font-light`: #9B4E03 (orange-brown)
|
||||
- `--color-font-highlight`: #c00 (red)
|
||||
- `--color-font-warn`: #e00 (bright red)
|
||||
- `--color-font-bonus`: #03c (blue)
|
||||
- `--color-font-invalid`: #666 (gray)
|
||||
- `--color-font-invert`: #fff (white)
|
||||
|
||||
**Background Colors:**
|
||||
- `--color-bg-default`: #EDEBDF (parchment) — main background
|
||||
- `--color-bg-back`: #E3E0D1 — secondary background
|
||||
- `--color-bg-light`: #fff — light highlight
|
||||
- `--color-bg-alpha`: #fff9 — translucent overlay
|
||||
- `--color-bg-invert`: #5C0D11 — inverted (dark)
|
||||
- `--color-bg-h1`: #edb — header background
|
||||
|
||||
**Equipment Quality Colors:**
|
||||
- Peerless: #fbb (pink), Legendary: #fd8 (gold), Magnificent: #bdf (light blue), Exquisite: #ce9 (green), Superior: #ccc (gray)
|
||||
- Applied as `hvut-equip-{Quality}` class + `background-color` on rows
|
||||
|
||||
**Item Type Colors:**
|
||||
- Consumable: #00B000 (green), Artifact: #0000FF (blue), Trophy: #461B7E (purple), Token: #254117 (dark green), Crystal: #BA05B4 (magenta), MonsterFood: #489EFF (light blue), Material: #FF0000 (red), Collectable: #0000FF (blue)
|
||||
- Applied as `hvut-item-{Type}` class on rows
|
||||
|
||||
**Warning System:**
|
||||
- `.hvut-warn`: Red text (#e00)
|
||||
- `.hvut-warn2`: Inverted (dark bg, white text)
|
||||
- `.hvut-bonus`: Blue text (#03c)
|
||||
|
||||
### Side Panel System
|
||||
- `.hvut-side`: Absolute-positioned 100px wide flex column
|
||||
- Used on: Shrine (`.hvut-ss-side`), Monster Lab (`.hvut-ml-side`), Market (`.hvut-mk-side`), Armory (`.hvut-am-side`)
|
||||
- Contains action buttons grouped with optional margins
|
||||
|
||||
---
|
||||
|
||||
## 13. STORAGE STRUCTURES
|
||||
|
||||
### Primary Storage: `GM_getValue` / `GM_setValue` (TGM/Greasemonkey)
|
||||
|
||||
All keys prefixed with `hvut_` (persistent) or `hvuti_` (isekai):
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `hvut_settings` | Object | All user settings |
|
||||
| `hvut_prices` | Object | Item price mappings (name → credits) |
|
||||
| `hvut_persona` | Object | Persona/equip set state |
|
||||
| `hvut_ch_style` | Object | Character style (difficulty, FS, spell type, prof factor) |
|
||||
| `hvut_equipset` | Array | Current equip set slots |
|
||||
| `hvut_equipdata` | Object | Equipment notes/prices (per EID) |
|
||||
| `hvut_ml_log` | Array | Monster Lab log |
|
||||
| `hvut_ss_log` | Object | Shrine log |
|
||||
| `hvut_mm_userlist` | Array | MoogleMail recipient list |
|
||||
| `hvut_ab_level` | Object | Ability levels |
|
||||
| `hvut_se_settings` | Object | Saved settings presets |
|
||||
| `hvut_tr_level` | Object | Training levels |
|
||||
|
||||
### Secondary Storage: `localStorage` (backup for critical data)
|
||||
|
||||
The `$config.ls_savelist` (line 222) defines which keys also get localStorage backup:
|
||||
- `ch_style`, `persona`, `prices`, `equipset`
|
||||
|
||||
### Cross-Profile Storage (no prefix)
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `hvut_re` | Object | Random Encounter state (`{ date, key, count, clear }`) |
|
||||
| `hvut_tr_notif` | Object | Training notification state |
|
||||
| `hvut_lt_notif` | Object | Lottery notification state |
|
||||
|
||||
### Migration System (`$config.migration`, Lines 402-511)
|
||||
- Detects old localStorage-based data (pre-4.x)
|
||||
- Upgrades: equipdata, prices, protection filters, bazaar filters, monster lab log, equip sets, shrine log
|
||||
- Cleans up old localStorage keys after migration
|
||||
- Version-specific migrations for 4.2 and 4.22
|
||||
|
||||
---
|
||||
|
||||
## 14. KEY FUNCTION NAMES AND PURPOSES
|
||||
|
||||
### Core Shared Modules
|
||||
|
||||
| Module | Line | Purpose |
|
||||
|--------|------|---------|
|
||||
| `$config` | 220 | Configuration system: get/set/del/validate/migrate |
|
||||
| `$ajax` | 837 | Rate-limited AJAX queue (300ms interval, max 4 concurrent) |
|
||||
| `$re` | 947 | Random Encounter timer and notification |
|
||||
| `$equip` | 1227 | Equipment parsing, sorting, filtering, namecode generation |
|
||||
| `$item` | 1810 | Item inventory loading, counting, shop buying |
|
||||
| `$price` | 1950 | Item price management, market integration |
|
||||
| `$mail` | 2174 | MoogleMail send queue engine |
|
||||
| `$battle` | 2416 | Battle side panel (equipment, items, repair) |
|
||||
| `$dfct` | 3280 | Difficulty changer |
|
||||
| `$persona` | 3340 | Persona/equip set changer |
|
||||
|
||||
### Page-Specific Modules
|
||||
|
||||
| Module | Line | Page |
|
||||
|--------|------|------|
|
||||
| `_ch` | 3820 | Character: EXP simulator, stats parsing |
|
||||
| `_eq` | 3923 | Equipment: popups, charms, mage stats, equip code |
|
||||
| `_ab` | 4097 | Abilities: slotbar parsing, tree parsing, ability simulator |
|
||||
| `_tr` | 4468 | Training: queue planning, cost calculation, auto-start |
|
||||
| `_it` | 4676 | Item Inventory: type coloring |
|
||||
| `_se` | 4704 | Settings: preset save/load |
|
||||
| `_is` | 4807 | Item Shop: type coloring |
|
||||
| `_ss` | 4833 | Shrine: bulk offering, reward tracking, trophy system |
|
||||
| `_mk` | 5374 | Market: price display, order checking, crystal pack |
|
||||
| `_ml` | 5486 | Monster Lab: list, feeding, upgrades, PLC |
|
||||
| `_mm` | 6870 | MoogleMail: compose UI, item/equip/credits attach |
|
||||
| `_lt` | 8500 | Lottery: notification toggle, golden ticket confirm |
|
||||
| `_ar` | 8560 | Arena/RoB: layout adjustment |
|
||||
| `$armory` | 8611 | Equipment Shop: unified list, protect/bazaar filters, salvage calc |
|
||||
| `_mo` | 9670 | Modify (single equip): upgrade cost display |
|
||||
|
||||
### Key Utility Functions
|
||||
|
||||
| Function | Line | Purpose |
|
||||
|----------|------|---------|
|
||||
| `$id` / `$qs` / `$qsa` / `$xpath` | 191-194 | DOM selectors |
|
||||
| `$doc` | 195 | HTML string → Document |
|
||||
| `$element` | 196 | Element builder with attributes, events, children |
|
||||
| `$input` | 197 | Form input builder (text/checkbox/select/number) |
|
||||
| `time_format` | 198 | ms → HH:MM:SS / HH:MM / MM:SS |
|
||||
| `date_format` | 199 | Unix timestamp → YY-MM-DD HH:MM |
|
||||
| `parse_count` | 200 | "1,234" → 1234 |
|
||||
| `parse_price` | 201 | "10m"/"500k" → number |
|
||||
| `split2` | 202 | "key:value" → ["key", "value"] |
|
||||
| `scrollIntoView` | 203 | Scroll parent to show child |
|
||||
| `confirm_event` | 204 | Add confirmation to onclick handlers |
|
||||
| `toggle_button` | 205 | Toggle button text based on CSS class |
|
||||
| `play_beep` | 206 | Web Audio API beep |
|
||||
| `popup` | 207 | Modal overlay |
|
||||
| `popup_text` | 208 | Modal with textarea |
|
||||
| `get_message` | 209 | Parse server message box |
|
||||
| `$equip.filter.test` | 1609 | Filter expression evaluator (&&, \|\|, !, $Quality+, $pab=, $prefix, $level) |
|
||||
| `$equip.namecode` | 1696 | Forum BBcode name decorator |
|
||||
| `$equip.parse.name` | 1300 | Name regex parser (quality, prefix, type, slot, suffix) |
|
||||
| `$equip.parse.elem` | 1397 | DOM element → equipment object |
|
||||
| `$equip.parse.dynjs` | 1361 | dynjs_equip data → equipment object |
|
||||
|
||||
---
|
||||
|
||||
## 15. MARKET INTEGRATION
|
||||
|
||||
### Price System (`$price`, Lines 1950–2171)
|
||||
|
||||
**Storage:** `hvut_prices` — object mapping item names to credit values
|
||||
|
||||
**Item Groups (Lines 1953-1971):**
|
||||
- **Consumables:** Health/Mana/Spirit Draughts/Potions/Elixirs, Energy Drinks, Caffeinated Candy, Infusions, Scrolls, Flower Vases, Bubble-Gum
|
||||
- **Materials:** Grade materials, Scraps, Energy Cells, Phazon, Shade Fragment, Repurposed Actuator, Defense Matrix Modulator, Bindings (33 types), Weapon/Staff/Armor Cores, Shards
|
||||
- **Trophies:** All 12 trophies
|
||||
- **Crystals:** All 12 crystal types
|
||||
- **Figures:** All 27 figurine types
|
||||
|
||||
**Isekai Filtering (Line 1982-1989):**
|
||||
- Removes: Last Elixir, Energy Drink, Caffeinated Candy, Bindings, Crystals, Figures
|
||||
- These items don't exist on Isekai server
|
||||
|
||||
**Market Data Fetching (Line 2101-2149):**
|
||||
```js
|
||||
$price.parse_market(filter, doc)
|
||||
```
|
||||
- Parses the market item list table (`#market_itemlist table`)
|
||||
- Extracts: item name, item ID, your stock, market bid, market ask, market stock
|
||||
- Caches in `$price.market[name]` = `{ itemid, stock, bid, ask, market_stock }`
|
||||
- Filter names: `co` (consumables), `ma` (materials), `tr` (trophies), `ar` (artifacts), `fi` (figures), `mo` (monster food)
|
||||
|
||||
**Market Price Update (Line 2125-2149):**
|
||||
- `$price.update_market(filter, key, save)`: Fetches current market data, extracts prices for specified key (bid/ask)
|
||||
- Fetches all filters at once if `filter` is empty
|
||||
|
||||
**Market Integration Points:**
|
||||
|
||||
1. **Market Page (`_mk`):** Shows HVUT prices in extra column, highlighting mismatched orders
|
||||
2. **Item Price Editor:** `$price.edit()` opens popup with "Bid"/"Ask" buttons to fetch market prices
|
||||
3. **Equipment Salvage Calc:** Uses material prices from `$price.get('Materials')` to value salvage
|
||||
4. **Monster Lab Gift Summary:** Uses `$price.value()` to estimate gift credit value
|
||||
5. **Monster Lab Upgrade Cost:** Uses crystal prices to show upgrade costs
|
||||
6. **Modify Page:** Shows total upgrade cost using material prices
|
||||
7. **Crystal Pack:** On Monster Food market page, calculates aggregate crystal pack bid/ask
|
||||
|
||||
**Price Editor Popup (Line 2045-2088):**
|
||||
- Textarea with `name @ price` format
|
||||
- **Save:** Validates and saves
|
||||
- **Bid/Ask:** Fetches market prices for specified key, replaces values
|
||||
- **Edit All Items:** Switches to full price list
|
||||
|
||||
**Default Prices (Line 1972-1976):**
|
||||
- Peerless Weapon/Staff/Armor Cores: 500,000 credits
|
||||
|
||||
---
|
||||
|
||||
## ARCHITECTURE SUMMARY
|
||||
|
||||
### Module Dependencies
|
||||
```
|
||||
$config (storage, settings)
|
||||
↓
|
||||
$ajax (network, 300ms/4conn rate limit)
|
||||
↓
|
||||
$equip (equipment parsing/filtering) ← $item (inventory) → $price (market)
|
||||
↓ ↓
|
||||
$re (RE timer) $battle (side panel) $mail (MM send)
|
||||
↓
|
||||
Page modules (_ch, _eq, _ab, _tr, _it, _se, _is, _ss, _mk, _ml, _mm, _lt, _ar, $armory, _mo)
|
||||
↓
|
||||
_top (top bar) + _bottom (bottom bar) — always active, uses _player and specific modules
|
||||
```
|
||||
|
||||
### Design Patterns
|
||||
- **Singleton modules:** All `$xxx` and `_xxx` objects are const-declared singletons
|
||||
- **Lazy initialization:** Most modules have `init()` called only when their page matches
|
||||
- **AJAX queue:** Central `$ajax` manages concurrency (max 4) and rate limiting (300ms interval)
|
||||
- **Filter DSL:** The `$equip.filter` system implements a custom boolean expression evaluator for equipment matching
|
||||
- **CSS variables:** All theming uses CSS custom properties, making re-skinning trivial
|
||||
- **Storage namespacing:** Persistent (`hvut_`) and Isekai (`hvuti_`) have separate storage namespaces
|
||||
- **Migration system:** Automatic data migration when version numbers don't match
|
||||
41
references/ehwiki-advanced-advice.md
Normal file
41
references/ehwiki-advanced-advice.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# EHWiki Advanced Advice — Key Points vs Our Current KB
|
||||
|
||||
**Source:** ehwiki.org/wiki/HentaiVerse_Advice_Advanced
|
||||
|
||||
## Stats Allocation (differs from our KB!)
|
||||
|
||||
Advice wiki says:
|
||||
- STR: Above level (HIGH priority) — our KB had 30-45% allocation
|
||||
- DEX: Above level (HIGH priority) — our KB had 20-30%
|
||||
- AGI: Above level (Mid-low to High, build-dependent) — our KB had flat 10%
|
||||
- END: Above level (HIGH priority) — our KB had 10-25%
|
||||
- INT: 0 to Level × ~0.7 (LOWEST priority for melee)
|
||||
- WIS: Around level (MID priority)
|
||||
|
||||
**Key difference**: Wiki says stats "barely even matter" (Noni confirmed this in forum) — proficiency and gear matter way more. Our KB's elaborate percentage allocations may be over-engineered.
|
||||
|
||||
## Accuracy
|
||||
- Target 150%+ after level 200
|
||||
- Accuracy above 200% has no use
|
||||
|
||||
## IW Potency Rankings (per weapon type)
|
||||
|
||||
| Style | #1 Potency | #2 | #3 |
|
||||
|-------|-----------|-----|-----|
|
||||
| 1H | Butcher | Fatality | Overpower |
|
||||
| 2H | Overpower | Butcher | Fatality |
|
||||
| DW | Overpower | Fatality | Butcher |
|
||||
| Niten | Overpower | Fatality | Butcher |
|
||||
| Staff | Economizer | Aether | (MDB stats) |
|
||||
|
||||
## Armor Suffix Recommendations
|
||||
- Light: Shadowdancer or Fleet (Shade), Protection (Leather)
|
||||
- Heavy: Slaughter (high level), Protection+Warding (low level)
|
||||
- Prefix: Savage (offense), Mithril (featherweight non-1H), Reinforced (leather)
|
||||
|
||||
## Mage Gearing
|
||||
- Priority: MDB > EDB ≥ Proficiencies
|
||||
- Redwood = cheapest staff wood for new mages
|
||||
- Target 0.7+ proficiency factor
|
||||
- Base set needs 140-150% HP Bonus from IW
|
||||
- Juggernaut potency: minimum 3 stacks on armor
|
||||
26
references/forum-ask-experts.md
Normal file
26
references/forum-ask-experts.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Ask the Experts Forum — Player Wisdom
|
||||
|
||||
**Source:** forums.e-hentai.org/index.php?showtopic=211064 (1307 pages)
|
||||
**Key posters:** Noni (mod, L500), Ramaki (L500), l13763824039 (L500), Atilius Draco (L431), Corruptio Ultima (L249), CapableScoutMan (L500)
|
||||
|
||||
## Stats Allocation Q&A
|
||||
|
||||
**CapableScoutMan asks:** Best stat allocation per playstyle? (1H, STR=DEX=END, WIS -10, AGI -30, INT -100)
|
||||
|
||||
**Noni's answer (mod, L500):** "Not really. They barely even matter. Just do what feels right. Many styles you can keep them even. Only INT isn't needed for melee, and STR isn't needed for mage. Personally I keep the defensive stats a bit higher at low level: DEX END AGI. Well, AGI you don't need that much as 1h heavy but it is also not useless."
|
||||
|
||||
**Noni on proficiency:** "Most likely difference in proficiency. Proficiency is important."
|
||||
|
||||
## Charm System Discussion
|
||||
|
||||
**Ramaki's guide note:** "I still do not value Critical Strike Damage very highly due to the 50% Critical Strike chance hard cap. One, which most styles cannot even approach."
|
||||
|
||||
**l13763824039 on charm availability:** "Probably you don't have that charm. All weapons can now equip the same charms, as long as you have sufficient points and charms. If you don't have a certain charm, they would not be listed."
|
||||
|
||||
## Key Takeaways
|
||||
1. Stats barely matter for performance — focus on proficiency and gear
|
||||
2. Only hard rule: don't put INT on melee, don't put STR on mage
|
||||
3. Defensive stats (DEX/END/AGI) are more valuable at low levels
|
||||
4. Charm options are filtered by which charms you actually own
|
||||
5. Critical strike hard cap is 50% chance, making crit damage less valuable
|
||||
6. Proficiency is the #1 differentiator between playstyle performance
|
||||
84
references/forum-battle-records-091.md
Normal file
84
references/forum-battle-records-091.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# HV Battle Records 0.91+ Edition — Post-Update Meta (Current as of Jul 14, 2026)
|
||||
|
||||
**Source:** forums.e-hentai.org/index.php?showtopic=296644
|
||||
**Author:** Shank (Global Mod, L500)
|
||||
|
||||
## Official Record Board (as of post #25, Jul 14 2026)
|
||||
|
||||
All on PFUDOR difficulty, level 500, persistent (not isekai).
|
||||
|
||||
### 1H
|
||||
- DwD (L300): no data
|
||||
- PGC (L400): no data
|
||||
- SPL (L500): no data
|
||||
- Grindfest: no data
|
||||
|
||||
### 2H
|
||||
- DwD: 1,660 — Noni
|
||||
- PGC: 1,799 — Noni
|
||||
- SPL: 2,091 — Noni
|
||||
- GF: no data
|
||||
|
||||
### DW
|
||||
- DwD: no data
|
||||
- PGC: no data
|
||||
- SPL: 2,790 — Selvaria Bles
|
||||
- GF: no data
|
||||
|
||||
### Niten Ichiryu
|
||||
- DwD: 945 — lololo16
|
||||
- PGC: 1,026 — lololo16
|
||||
- SPL: 1,197 — lololo16
|
||||
- GF: 6,650 — lololo16
|
||||
|
||||
### Fire/Cold Mage
|
||||
- No data posted yet
|
||||
|
||||
### Elec/Wind Mage
|
||||
- DwD: no data
|
||||
- PGC: no data
|
||||
- SPL: no data
|
||||
- GF: 3,431 — HappyAccident
|
||||
|
||||
### Holy Mage (fastest)
|
||||
- DwD: 492 — e-Stark
|
||||
- PGC: 552 — e-Stark
|
||||
- SPL: 676 — Lady_Slayer
|
||||
- GF: 3,247 — dz31899560
|
||||
|
||||
### Dark Mage
|
||||
- DwD: no data
|
||||
- PGC: no data
|
||||
- SPL: no data
|
||||
- GF: 3,756 — Kagoraphobia
|
||||
|
||||
### 1H Mage
|
||||
- No data
|
||||
|
||||
### DW Mage
|
||||
- GF: 5,824 (holy) — Lady_Slayer
|
||||
|
||||
### Notable runs
|
||||
- 4 element mage, gum vase pfest: 1,205 turns — HappyAccident
|
||||
|
||||
## Player Builds Shared
|
||||
|
||||
### Lady_Slayer — Holy Mage (record holder)
|
||||
- DD9, Tower 100
|
||||
- SPL: 734 turns (no infusion, LHOH)
|
||||
- PGC: 591 turns (LHOH + infusion, Holy Day)
|
||||
- DwD: 541 turns (PHKH + infusion, Friday)
|
||||
- Uses: LHOH (Lady's Hammer of Holy?), PHKH
|
||||
|
||||
### Noni — 2H
|
||||
- All-round records for 2H (DwD 1660, PGC 1799, SPL 2091)
|
||||
|
||||
## Key Meta Changes from 0.91 Update
|
||||
1. Item World is removed from competition (can't undo IW anymore, too slow)
|
||||
2. IW difficulty (IW26 vs IW30) isn't clearly reported by JPX — same number of floors
|
||||
3. All submissions must be PFUDOR difficulty
|
||||
4. Gum/Vase runs don't count
|
||||
5. Holy Mage is the dominant speed build: 492 turns DwD (pre-0.91 best was 811)
|
||||
6. Niten Ichiryu is the fastest melee at 945 turns DwD
|
||||
7. 2H is now viable at 1660 turns (wasn't even tracked pre-0.91)
|
||||
8. Grindfest meta: Elec/Wind Mage (3431) ≈ Holy Mage (3247) ≈ Dark Mage (3756)
|
||||
100
references/forum-battle-records.md
Normal file
100
references/forum-battle-records.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# HV Battle Records — Turn Count Reference & Endgame Meta
|
||||
|
||||
**Source:** forums.e-hentai.org/index.php?showtopic=277026
|
||||
**Author:** Nezu (mod, L500)
|
||||
|
||||
## Purpose
|
||||
Turn count records for endgame content:
|
||||
- L500 arena (Secret Pony Level / SPL)
|
||||
- L300 arena (Dance with Dragons / DwD)
|
||||
- Full PFUDOR Grindfest
|
||||
|
||||
**Disqualified:** Isekai battles, Flower Vase & Bubble Gum usage.
|
||||
|
||||
## DwD Record List (Dance with Dragons, L300 arena)
|
||||
|
||||
| Rank | Player | Style | Turns |
|
||||
|------|--------|-------|-------|
|
||||
| 1 | mathl33t | Holy Mage | 811 |
|
||||
| 2 | canthold | Holy Mage | 902 |
|
||||
| 3 | Lady_Slayer | Holy Mage | 913 |
|
||||
| 4 | kamio11 | Wind Mage | 922 |
|
||||
| 5 | what_is_name | Fire Mage | 976 |
|
||||
| 6 | ikki. | Cold Mage | 1155 |
|
||||
| 7 | mathl33t | 1H Mage | 1787 |
|
||||
| 8 | lololo16 | Dual Wield | 1842 |
|
||||
| 9 | lololo16 | 1H | 1868 |
|
||||
| 10 | Lady_Slayer | 1H Holy | 2100 |
|
||||
| 11 | Noni | 1H | 2566 |
|
||||
| 12 | Lady_Slayer | 1H | 4579 |
|
||||
|
||||
## SPL Record List (Secret Pony Level, L500 arena)
|
||||
|
||||
| Rank | Player | Style | Turns |
|
||||
|------|--------|-------|-------|
|
||||
| 1 | mathl33t | Holy Mage | 1016 |
|
||||
| 2 | canthold | Holy Mage | 1097 |
|
||||
| 3 | Noni | Dark Mage | 1120 |
|
||||
| 4 | what_is_name | Fire Mage | 1277 |
|
||||
| 5 | canthold | Elec Mage | 1291 |
|
||||
| 6 | ikki. | Cold Mage | 1601 |
|
||||
| 7 | mathl33t | 1H Mage | 2071 |
|
||||
| 8 | lololo16 | 1H | 2304 |
|
||||
| 9 | lololo16 | Dual Wield | 2418 |
|
||||
| 10 | Lady_Slayer | 1H Mage | 2846 |
|
||||
|
||||
## Grindfest Record List
|
||||
|
||||
| Rank | Player | Style | Turns |
|
||||
|------|--------|-------|-------|
|
||||
| 1 | mxy215 | Wind Mage | 5315 |
|
||||
| 2 | Lady_Slayer | Holy Mage | 6520 |
|
||||
| 3 | lololo16 | 1H | 12868 |
|
||||
|
||||
## Player Sample Builds (from posts)
|
||||
|
||||
### ikki. — Cold Mage
|
||||
- Redwood staff, Radiant 4+1
|
||||
- DD9, Tower floor 40
|
||||
- DwD: 1155 turns (7:07)
|
||||
- SPL: 1601 turns (10:27)
|
||||
|
||||
### Lady_Slayer — Holy Mage (record holder)
|
||||
- 5+0 radiants (4r1c), Feather+Aether
|
||||
- ATK 4557/562.7 divine, 719/548
|
||||
- DD9, Tower 32
|
||||
- DwD: 1020 turns (with Infusion all round, Imp SG only)
|
||||
- Later improved to 932 turns on Holy Day
|
||||
|
||||
### mathl33t — Holy Mage (record holder)
|
||||
- 5+0 radiants, Feather+Aether
|
||||
- DD8, Tower 100
|
||||
- DwD: 857 turns
|
||||
|
||||
### Noni — 1H Heavy
|
||||
- Peerless Shortsword, Peerless Shield
|
||||
- Armor forged to 20
|
||||
- DD9, Tower 86
|
||||
- DwD: 2922 turns (using Vital Strike whenever available)
|
||||
|
||||
### Lady_Slayer — 1H Heavy
|
||||
- Legendary Ethereal Axe of Slaughter (unforged)
|
||||
- Peerless Mithril Buckler of the Barrier
|
||||
- Full Legendary Power Slaughter set (Savage on body/boots)
|
||||
- All gear unforged except shield
|
||||
- DD9, Tower 32
|
||||
- DwD: 4579 turns
|
||||
|
||||
### lololo16 — 1H / Dual Wield
|
||||
- DD9, Tower 50
|
||||
- DwD: 1842 (DW), 1868 (1H)
|
||||
- SPL: 2304 (1H), 2418 (DW)
|
||||
- GF: 12868 (1H)
|
||||
|
||||
## Key Meta Observations
|
||||
1. **Mage dominates speed records** — Holy Mage is fastest (811 turns DwD), all top 6 DwD records are mages
|
||||
2. **1H Mage is competitive** but slower than pure mage
|
||||
3. **1H Heavy melee** is the slowest endgame build (4579 turns DwD) but is tanky
|
||||
4. **DW is faster than 1H** (1842 vs 1868 DwD for same player)
|
||||
5. **Mages clear Grindfest in 5315-6520 turns** while 1H takes 12868
|
||||
6. Meta gear: Feather+Aether charms, Radiant phase for mages, Power Slaughter for melee
|
||||
35
references/forum-hv-research.md
Normal file
35
references/forum-hv-research.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# HV Research Thread — Data-Driven Findings
|
||||
|
||||
**Source:** forums.e-hentai.org/index.php?showtopic=232444
|
||||
**Author:** Noni (mod, L500), research by sssss2
|
||||
|
||||
## sssss2's 1H Research (Level 468, PFUDOR Grindfest)
|
||||
|
||||
### Setup
|
||||
- Legendary Arctic Rapier of Slaughter (Butcher Lv.5, Fatality Lv.4)
|
||||
- Physical Attack: 10,842 base
|
||||
- Hit: 212.2%, Crit: 53.38% (48.2% + Heartseeker 10%)
|
||||
- Crit dmg: +83% (+68% + Heartseeker 15%)
|
||||
- Def: 82.5% phys mit, 76.5% mag mit, 66.7% block, 65.4% parry
|
||||
|
||||
### Butcher vs Fatality Analysis
|
||||
- **Butcher Lv.5**: Increases attack base damage by 2.312%
|
||||
- **Fatality Lv.5**: Increases crit damage by 10%
|
||||
- At 53.38% crit rate:
|
||||
- Butcher: Normal 2.455x, Counter 0.767x, Skill 1.432x
|
||||
- Fatality: Normal 2.454x, Counter 0.750x, Skill 1.454x
|
||||
- **Verdict**: Butcher better for normal attacks + counters, Fatality better for skills
|
||||
- "Butcher's efficiency will be reduced if I wear 5x Power Slaughter" (diminishing returns with Slaughter suffix)
|
||||
|
||||
### Overpower Analysis (Potency Tier 5)
|
||||
- Normal hit rate without Overpower: 96.45%
|
||||
- Normal hit rate with Overpower Lv.5: 97.71%
|
||||
- Overpower increases real hit rate by ~1.3%
|
||||
- "Overpower applies only to normal attacks"
|
||||
- "A counter-attack stuns monsters, which can't parry. For this reason, Overpower is less efficient than Butcher and Fatality (to one-handed style)"
|
||||
|
||||
### Spike Shield Analysis
|
||||
- Cold/Elec/Wind Strike with proper Spike Shields are better than Holy/Dark Strike (except SG arena)
|
||||
- Flame Spike Shield reduces monster's cold resistance by 25%
|
||||
- Data shows Cold Strike with Flame Spike outperforms both Dark Strike and Holy Strike
|
||||
- This is why the author kept Arctic Rapier instead of switching to Hallowed/Demonic
|
||||
83
references/forum-jpx-thread.md
Normal file
83
references/forum-jpx-thread.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# jpx Script Thread — Forum OP
|
||||
|
||||
**Source:** forums.e-hentai.org/index.php?showtopic=290507
|
||||
**Author:** 闯关弟子 (Level 500, Catgirl Camarilla)
|
||||
**Script:** jpx v2026.07.06 (270KB)
|
||||
**Downloads:** 209 (latest)
|
||||
|
||||
## Default Auto-Battle Rules
|
||||
|
||||
jpx ships with built-in defaults for:
|
||||
- **OneHanded - General**
|
||||
- **OneHanded - Tower**
|
||||
- **Staff - General**
|
||||
|
||||
Key note: "The spell types used (T1–T3) are determined based on whichever Spell Damage Bonus is highest in the player's Statistics panel. Because of this, you need to visit either 'Character' or 'Equipment' first so the script can read the Spell Damage Bonus values."
|
||||
- Max Imperil casts per round: configurable (default 3)
|
||||
|
||||
### No Default Auto-Battle Rules (but community presets exist)
|
||||
|
||||
- **1H Mage - General**: "When any value in your Spell Damage Bonus exceeds 100, the fighting style will switch to Battlecaster. For 1H, the threshold is 70 instead of 100."
|
||||
- **TwoHanded - General** (config download available)
|
||||
- **2H Mage - General**
|
||||
- **DualWielding - General** (config download)
|
||||
- **DW Mage - General**
|
||||
- **NitenIchiryu - General** (config download)
|
||||
- **NI Mage - General**
|
||||
- **Unarmed - General**
|
||||
|
||||
### Community Presets (forum users who contributed)
|
||||
- Byza — Battlecaster_General_20251222 (622 downloads)
|
||||
- l13763824039
|
||||
- Ramaki
|
||||
- kgx — Staff_General, Battlecaster_General
|
||||
- Wivers
|
||||
- CornerCactus — 2H and NitenIchiryu
|
||||
- Noni — 2H
|
||||
|
||||
## Controls
|
||||
- **M** or click bottom-right panel → Start single-round auto-battler
|
||||
- **Z** → Open battle statistics panel
|
||||
- **,** → Open settings (displayed at bottom of page)
|
||||
|
||||
## Battle Config System
|
||||
|
||||
- Battle Settings are separate for Persistent and Isekai
|
||||
- Stats Settings are shared
|
||||
|
||||
### "Supports" (resource management)
|
||||
- Require at least 1 condition to trigger
|
||||
- Examples: healing, buffing, mana/spirit management
|
||||
|
||||
### "Attacks" (combat actions)
|
||||
- Divided into: Target actions, Smart Debuff actions, Other actions
|
||||
- Default Smart Debuff target count: 3 (need Faster Weaken/Imperil/Better Silence abilities)
|
||||
- Each Target action selects the first monster meeting conditions (scan top-to-bottom)
|
||||
- Multiple consecutive Target actions: first monster matching ANY is selected
|
||||
- Once selected, remaining targets in that group are skipped → Other actions execute
|
||||
- Other actions require a target monster selected first
|
||||
|
||||
### Condition System
|
||||
Uses flexible conditions:
|
||||
- **Player**: HP/MP/SP%, OC, spirit stance, effect status, cooldowns
|
||||
- **Monster**: Name, type, class, HP/MP/SP, effects, days since update, level
|
||||
- **Battle**: World, type, difficulty, round, floor
|
||||
- All numeric conditions support [min, max] ranges
|
||||
|
||||
## Notes from Author
|
||||
- "Recommended for players who have no survival pressure (Lv.300+)"
|
||||
- recordBattleLog disabled by default (localStorage limits on GrindFest)
|
||||
- Edit configs with text editor but don't modify battleVersion
|
||||
- Other layout-modifying scripts may cause malfunction
|
||||
- Cleanup script available for removing all jpx data
|
||||
|
||||
## Recent Changelog Highlights
|
||||
|
||||
- **20260706**: New condition: Player Max Spell Type; aggregate by day unchecked by default
|
||||
- **20260705**: Empty rulesets preserved; Arena300/400/500 always use PFUDOR; Magnet features removed
|
||||
- **20260701**: Auto difficulty detection for IW; World Level display
|
||||
- **20260624**: Open Stats to ctrlWidget; 1H Mage threshold lowered 100→70
|
||||
- **20260617**: New condition: Monster Level
|
||||
- **20260608**: Fixed debuffResist counting Coalesced Mana
|
||||
- **20260531**: HP%/MP%/SP%/OC precision increased to 3 decimal places
|
||||
- **20260509**: Battle Mode rulesets separated into individual Battle Style + Battle Type dropdowns
|
||||
55
references/forum-monsterbation-thread.md
Normal file
55
references/forum-monsterbation-thread.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Monsterbation 1.4.1.2 Thread — Forum OP
|
||||
|
||||
**Source:** forums.e-hentai.org/index.php?showtopic=211039
|
||||
**Author:** sickentide (Level 500 Ponyslayer)
|
||||
**Script DLs:** 413,304
|
||||
|
||||
## OP Features List
|
||||
|
||||
- cooldowns
|
||||
- move player effects and vitals to above monsters
|
||||
- effect durations, including stacks
|
||||
- change background or quickbar colour according to alert conditions
|
||||
- log colour highlights
|
||||
- hide battle log
|
||||
- skip end of round popup, unless you want to stop at battle end or on equipment drop
|
||||
- quickbar extender, including gem icon
|
||||
- bind actions to perform on monsters to mouse buttons and wheel
|
||||
- hoverplay, both for melee and spell rotation, with option to stop under conditions like spark
|
||||
- key bindings
|
||||
- support for default and custom fonts
|
||||
- drop/exp/proficiency/damage/usage tracker
|
||||
- round/turn/speed counter
|
||||
- display max player vitals
|
||||
- display monster hp and shorten hp bars
|
||||
- monster numbers instead of letters
|
||||
- clickable riddlemaster
|
||||
- ed/flee confirm
|
||||
- ajax round advance
|
||||
- show monster info and submit scan data from/to decondelite's database
|
||||
- support for mobile devices with firefox and tampermonkey
|
||||
- shrink view to either side
|
||||
- configuration interface
|
||||
- multiple profiles with automatic switching
|
||||
- stun and imperil highlight
|
||||
|
||||
## CrunkJuice 1.3.0 Features (companion out-of-battle script)
|
||||
|
||||
- ed confirm
|
||||
- faster "sell all" button
|
||||
- morale and hunger values in monster lab
|
||||
- feed pills and crystals to all monsters, with level caps
|
||||
- hide low- and mid-grade gifts
|
||||
- search decondelite's monster database
|
||||
- re timer/counter
|
||||
- PFUDOR/IWBTH and Godslayer/Dovahkiin toggle
|
||||
- bazaar quality filter
|
||||
- open second page of arena by default
|
||||
- monster lab scroll bar
|
||||
- quick enchant
|
||||
- condensed view
|
||||
|
||||
## Author's sig (player profile)
|
||||
|
||||
2H: gundam megazord silvergun
|
||||
sexromancer: icon of dissolution wheel of suffering hell mantle clutch of ghosts roots of insolence walk among beasts
|
||||
59
references/forum-new-ask-experts.md
Normal file
59
references/forum-new-ask-experts.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# New Ask-The-Experts Thread — Beginner Guide
|
||||
|
||||
**Source:** forums.e-hentai.org/index.php?showtopic=297475
|
||||
**Author:** Noni (mod, L500 Ponyslayer)
|
||||
**Based on:** f4tal's original Do's and Don'ts, updated 2026 by mod team
|
||||
|
||||
## TOP 30 Tips Summary (actual text from the thread)
|
||||
|
||||
### 1-3: Setup
|
||||
1. Try alt.hentaiverse.org if main site has problems
|
||||
2. Read the wiki (Advice page, FAQ, Acronyms, Technical Issues)
|
||||
3. Ask questions in forum or (unofficial) Discord
|
||||
|
||||
### 4: Trading
|
||||
- Use MoogleMail for player trades
|
||||
- CoD (Cash on Delivery) requires Postage Paid hath perk (~350K credits) — buy it
|
||||
- Trade via WTS/WTB forum sections, or The Market for items
|
||||
|
||||
### 5-6: Don't Sell to Bazaar
|
||||
- "Just remember: never sell anything to the bazaar. At least until you will be proficient enough to tell important items from unimportant."
|
||||
- High-level players give free stuff to newcomers in WTS shops
|
||||
|
||||
### 7-8: Credits & GP
|
||||
- Use GP (not Credits) to download galleries — 3x more downloads
|
||||
- Visit News Page daily for free EXP, Credits, arena reset
|
||||
|
||||
### 9: Forum Bonus
|
||||
- Double EXP for posting in forum at least once per 30 days
|
||||
- "Please post that one message here — don't spam"
|
||||
|
||||
### 10: Start as Melee, Not Mage
|
||||
- "Mage equipment is very expensive and on lower levels you just don't have good spells — it is just extremely hard to play as mage right from level one (but not impossible)."
|
||||
|
||||
### 11: Don't Chase High Quality Gear
|
||||
- "Do not try to get Legendary or Peerless in first day of playing, and even in the first month."
|
||||
- Change quality gradually and adequately
|
||||
|
||||
### 12: Mix Armor but Use 3 Same for Abilities
|
||||
- 3+ Heavy → melee abilities
|
||||
- 3+ Light → melee evade abilities
|
||||
- 3+ Cloth → mage abilities
|
||||
- Rare types: Power (heavy), Shade (light), Phase (cloth)
|
||||
|
||||
### 15: Lock Equipment
|
||||
- Lock equipment you don't want to accidentally sell/salvage
|
||||
|
||||
### 16-17: Abilities & Spells
|
||||
- After upgrading abilities, ACTIVATE them (click icon → slot at top panel)
|
||||
- Spells: Offensive, Debuffs, Buffs, Cures
|
||||
- Melee should use spells too (buffs, debuffs)
|
||||
- "Items are faster and safer than spells" — items are instant, monsters don't attack after using items
|
||||
- "Whenever you have chance, use items first and only then, if your HP is still low, cast a Cure spell"
|
||||
|
||||
### 18: Spark of Life
|
||||
- Requires 50% base SP to cast
|
||||
- After it saves you, absorbs 50% base SP — don't recast immediately or you'll die
|
||||
|
||||
### 19: Item Slots
|
||||
- Don't forget to put items into item slots (Settings page)
|
||||
726
references/hentaiverse-analysis.md
Normal file
726
references/hentaiverse-analysis.md
Normal file
|
|
@ -0,0 +1,726 @@
|
|||
# HentaiVerse — Complete Game Analysis & Automation Architecture
|
||||
## Prepared for headless automation / Chrome extension / harness design
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 1. GAME ARCHITECTURE
|
||||
|
||||
HentaiVerse is a **server-authoritative browser RPG**. All logic runs server-side;
|
||||
the browser is a thin client rendering HTML pages. Key architectural facts:
|
||||
|
||||
- **URL**: https://hentaiverse.org/ (subdomain of e-hentai.org)
|
||||
- **Transport**: HTTP(S) POST/GET with session cookies from e-hentai.org
|
||||
- **State**: Pure server-side. Browser refreshes the full page per turn/round.
|
||||
- **Page structure**: The battle page is a single HTML document. Each turn
|
||||
("attack", "cast spell", "use skill", "use item") is an HTTP POST that returns
|
||||
a new page with updated state.
|
||||
- **Anti-automation**: Server-side rate limit of **4 turns per second**.
|
||||
RiddleMaster anti-bot popups appear between rounds at random intervals.
|
||||
- **Stamina**: Anti-cheating mechanic — only 99 stamina max, regenerates 1/hr.
|
||||
You cannot grind indefinitely without items or waiting.
|
||||
|
||||
### HTTP Flow (Battle)
|
||||
|
||||
```
|
||||
GET /?s=BATTLE&... ← Enter battle (from Arena/Grindfest/etc.)
|
||||
POST /?s=BATTLE&action=attack ← Click "Attack" → server processes → returns new HTML
|
||||
POST /?s=BATTLE&action=cast&spell=14&target=2 ← Cast spell on monster #2
|
||||
POST /?s=BATTLE&action=skill&skill=1 ← Use skill
|
||||
POST /?s=BATTLE&action=item&item=3&target=player ← Use item on self
|
||||
POST /?s=BATTLE&action=flee ← Flee
|
||||
POST /?s=BATTLE&action=defend ← Defend
|
||||
POST /?s=BATTLE&action=focus ← Focus
|
||||
```
|
||||
|
||||
The response HTML contains:
|
||||
- Updated monster HP/MP/status bars (parsed from HTML tables)
|
||||
- Updated player vitals
|
||||
- New battle log text
|
||||
- Buff/debuff durations
|
||||
- If monsters are dead → link to "next round" or "exit to bazaar"
|
||||
|
||||
### Key Observation
|
||||
|
||||
Because the game is **server-authoritative** and every action returns full HTML,
|
||||
an automation tool doesn't need to simulate complex state. It just needs to:
|
||||
|
||||
1. Parse the current page HTML to extract game state
|
||||
2. Decide the optimal action based on state + strategy
|
||||
3. POST the action form
|
||||
4. Parse the new page HTML
|
||||
5. Repeat
|
||||
|
||||
This is exactly how existing userscripts (jpx, Monsterbation) work.
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 2. GAMEPLAY LOOP
|
||||
|
||||
```
|
||||
LOGIN → BAZAAR → SELECT BATTLE MODE → [ENTER BATTLE] →
|
||||
ROUND 1: fight monsters → ROUND N: ... → EXIT → BAZAAR
|
||||
```
|
||||
|
||||
### Pre-Battle (Bazaar)
|
||||
- Equip gear (Character → Equipment)
|
||||
- Allocate ability/mastery points (Character → Abilities)
|
||||
- Train skills (Character → Training)
|
||||
- Shrine artifacts
|
||||
- Manage items/inventory
|
||||
- Monster Lab management
|
||||
|
||||
### Battle Loop (per round)
|
||||
```
|
||||
1. Page loads → parse enemy list, player vitals, buffs/debuffs
|
||||
2. Select action:
|
||||
- Physical Attack (target monster)
|
||||
- Cast Spell (target monster(s))
|
||||
- Use Skill (target monster or self)
|
||||
- Use Item (target self)
|
||||
- Defend / Focus / Flee
|
||||
- Toggle Spirit Stance
|
||||
3. POST action → server resolves:
|
||||
- Player attack lands/misses/crits
|
||||
- Elemental strike proc checks
|
||||
- Equipment proc checks
|
||||
- Monster attacks resolved (speed-based, can be multiple)
|
||||
- Status effect durations decremented
|
||||
- Vital regeneration ticks
|
||||
4. New HTML page loads → go to step 2 or "Next Round"
|
||||
```
|
||||
|
||||
### Post-Battle
|
||||
- Loot popup (equipment/items/credits)
|
||||
- Return to Bazaar
|
||||
- Train / shrine / sell / repair equipment
|
||||
- Repeat from Step 1 until stamina exhausted
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 3. CHARACTER SYSTEM — COMPLETE FORMULAS
|
||||
|
||||
### 3.1 Primary Attributes (6 stats)
|
||||
|
||||
| Stat | Key Effects |
|
||||
|-------|------------------------------------------------------|
|
||||
| STR | +2 phys dmg / pt, +1% Overwhelming Strikes per 100 |
|
||||
| END | +6 HP/pt, +1 phys mit/pt, +1 mag mit/pt |
|
||||
| DEX | +1 phys dmg/pt, +0.5 acc, +0.4 parry |
|
||||
| INT | +2 mag dmg/pt, +1% Coalesced Mana per 100 |
|
||||
| AGI | +0.5 phys mit/pt, +0.4 evade, +attack speed |
|
||||
| WIS | +1 MP/pt, +1 mag dmg/pt, +0.5 mag acc, +0.4 resist |
|
||||
|
||||
**EXP cost to raise**: (current_stat + 1)^(2.5475566751265^(1+(current_stat/950)))
|
||||
|
||||
### 3.2 Derived Stats — Key Formulas
|
||||
|
||||
**Physical Base Damage:**
|
||||
```
|
||||
= (log(3330 + STR*2 + DEX, 1.0003) - 27039.81)
|
||||
+ (weapon_prof * TYPE) + sum(all_equip_ADB)
|
||||
```
|
||||
Where TYPE = 5 (DW), 4 (1H), 3 (2H/Staff)
|
||||
|
||||
**Magic Base Damage:**
|
||||
```
|
||||
= (log(3330 + INT*2 + WIS, 1.0003) - 27039.81)
|
||||
+ (staff_prof * 0.5) + sum(all_equip_MDB)
|
||||
```
|
||||
|
||||
**Physical Hit Chance:**
|
||||
```
|
||||
= 80% + DEX*0.04% + prof_bonus + sum(equip_acc)
|
||||
```
|
||||
|
||||
**Magical Hit Chance:**
|
||||
```
|
||||
= 80% + WIS*0.04% + prof_bonus + sum(equip_mag_acc)
|
||||
```
|
||||
|
||||
**Physical Crit (layered):**
|
||||
```
|
||||
= 1 - Π(1 - layer_n)
|
||||
```
|
||||
Layers: base (5%), stats, prof, each equipment piece independently.
|
||||
Burden penalty: crit_chance *= Max(1 - (Max(burden-70,0) * 0.02)^1.5, 0)
|
||||
|
||||
**Magic Crit (layered):** Same structure, interference penalty instead of burden.
|
||||
|
||||
**Attack Speed:**
|
||||
```
|
||||
1 - Π(1 - layer_n) with layers from AGI, prof, each equipment piece
|
||||
Burden penalty: speed *= (1 - Min((Max(burden-40, 0) * 0.02)^1.5, 1))
|
||||
```
|
||||
|
||||
**Cast Speed (additive):**
|
||||
```
|
||||
= (1 + equip_bonus + prof_bonus)
|
||||
```
|
||||
|
||||
**Evade:**
|
||||
```
|
||||
= 1 - (1 - shadow_veil) * (burden_penalty) * (1-eq) * (1-title) * (1 - AGI*0.04%)
|
||||
```
|
||||
|
||||
**Parry:**
|
||||
```
|
||||
= 1 - Π(1 - layer_n) // layers from DEX, prof, equipment
|
||||
```
|
||||
|
||||
**Mitigation:**
|
||||
```
|
||||
Phys: 1 - (1 - eq_bonus) * (900/(900 + END + AGI/2))
|
||||
Mag: 1 - (1 - eq_bonus) * (900/(900 + END + WIS/2))
|
||||
```
|
||||
|
||||
**HP / MP / SP:**
|
||||
```
|
||||
HP = (500 + level*10 + END*6) * HP_Tank * vigorous_vitality
|
||||
MP = (10 + level + WIS) * MP_Tank * effluent_ether
|
||||
SP = (1 + sum(all_6_stats)/5) * SP_Tank * suffusive_spirit
|
||||
```
|
||||
|
||||
**Battle Regen (per "tick" = 100 time units):**
|
||||
```
|
||||
MP/tick = 5 + (WIS/25) * perk_bonus
|
||||
SP/tick = 1 + (sum_all_stats/600) * perk_bonus
|
||||
```
|
||||
|
||||
### 3.3 Action Speed & Time Units
|
||||
|
||||
```
|
||||
action_speed = 100 / (1 - cast_speed) / specific_action_speed
|
||||
/ prof_factor_multiplier * (1 + haste)
|
||||
|
||||
time_units = clamp(20, 10000 / action_speed, 500)
|
||||
```
|
||||
|
||||
**Key action base speeds:**
|
||||
- Physical attack: 1.0
|
||||
- T1 offensive spell: 1.2
|
||||
- T2 offensive spell: 1.4
|
||||
- T3 offensive spell: 1.6
|
||||
- Curative spells: ~0.2
|
||||
- Support spells: ~0.1
|
||||
|
||||
A "tick" = 100 time units. Buffs/debuffs decrement every tick.
|
||||
Faster actions = more attacks per tick before buffs expire.
|
||||
|
||||
**Server bottleneck: max 4 turns/sec** (anti-bot measure).
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 4. COMBAT SYSTEM
|
||||
|
||||
### 4.1 Damage Formula
|
||||
|
||||
**Physical damage delivered:**
|
||||
```
|
||||
damage = base_physical_dmg
|
||||
* (1 + hath_bonus) // Daemon Duality 10-50%
|
||||
* (1 + tower_bonus) // 0.1% per isekai floor
|
||||
* (1 + crit_mod) // +50% base, +equip bonuses
|
||||
* (1 + heartseeker_bonus) // 25% if active
|
||||
* (1 + spirit_stance) // 100% if active
|
||||
```
|
||||
|
||||
**Then applied to target:**
|
||||
```
|
||||
damage_taken = damage * (1 - phys_mitigation) * (1 - specific_mitigation)
|
||||
```
|
||||
|
||||
Where specific mitigation = elemental type (fire/cold/elec/wind/holy/dark) OR
|
||||
physical type (slash/pierce/crush).
|
||||
|
||||
Mitigations are multiplicative, cannot go below 0.
|
||||
|
||||
### 4.2 Elemental Strikes
|
||||
|
||||
Weapons may have elemental strikes (Fire/Cold/Elec/Wind/Holy/Dark/Void).
|
||||
Each strike deals ~50% of normal physical damage as a separate attack in the same
|
||||
turn. Counts as physical for mitigation purposes (goes through phys mit + specific
|
||||
elemental mit).
|
||||
|
||||
### 4.3 Avoidance (layered multiplicative)
|
||||
|
||||
```
|
||||
Physical avoidance = 1 - (1-evade') * (1-block') * (1-parry')
|
||||
Magical avoidance = 1 - (1-evade') * (1-block') * (1-resist')
|
||||
```
|
||||
|
||||
Where primed values account for anti-evade/anti-block/anti-parry/anti-resist
|
||||
from custom monsters.
|
||||
|
||||
**Resist rolls** (for spells): 3 independent rolls.
|
||||
- 0 rolls: 0% reduction
|
||||
- 1 roll: 50% reduction
|
||||
- 2 rolls: 75% reduction
|
||||
- 3 rolls: 90% reduction
|
||||
|
||||
### 4.4 Equipment Procs
|
||||
|
||||
Weapon-specific procs trigger on hit:
|
||||
- **Bleeding Wound** (Axe, Shortsword, Wakizashi, Longsword, Katana) — DoT
|
||||
- **Stun** (Club, Mace) — target can't act
|
||||
- **Penetrated Armor** (Rapier, Estoc) — reduces phys/mag mitigation by 25%/stack
|
||||
- **Ether Theft** (Staff) — steal MP
|
||||
|
||||
Stacking: Penetrated Armor up to 3 stacks (75%). Bleeding Wound up to 100 stacks.
|
||||
|
||||
### 4.5 Skills (Overcharge-based)
|
||||
|
||||
Skills cost Overcharge (yellow bar built by dealing/taking damage).
|
||||
Key skills by fighting style:
|
||||
|
||||
**1H + Shield:**
|
||||
- Shield Bash (25 OC, 10 CD) — Stun 5 turns
|
||||
- Vital Strike (50 OC, 10 CD) — 100 Bleeding Wound stacks on stunned enemy
|
||||
- Merciful Blow (100 OC, 10 CD) — execute <25% HP bleeding target
|
||||
|
||||
**Dual Wield:**
|
||||
- Iris Strike (50 OC, 5 CD) — Blind 100 turns
|
||||
- Backstab (50 OC, 5 CD) — 2x damage to blinded, poison
|
||||
- Frenzied Blows (75 OC, 10 CD) — 10-20 hits across 5 targets
|
||||
|
||||
**2H:**
|
||||
- Great Cleave (50 OC, 5 CD) — guaranteed crit
|
||||
- Rending Blow (50 OC, 5 CD) — 3 Penetrated Armor to 5 targets, bypasses phys mit
|
||||
- Shatter Strike (50 OC, 5 CD) — Stun 5 turns to 5 targets with Penetrated Armor
|
||||
|
||||
**Staff (Mage):**
|
||||
- Concussive Strike (50 OC, 10 CD) — Stun 5 turns, magic damage
|
||||
|
||||
### 4.6 Spirit Stance
|
||||
|
||||
- Toggle state: +100% damage dealt, +25% mana costs
|
||||
- Drains SP while active (10% of base overcharge per turn)
|
||||
- Cannot be maintained without sufficient SP
|
||||
|
||||
### 4.7 Core Spells (for automation strategy)
|
||||
|
||||
**Offensive (Elemental):**
|
||||
| Tier | Fire | Cold | Elec | Wind | Lvl | Cost |
|
||||
|------|------|------|------|------|-----|------|
|
||||
| 1 | Fiery Blast | Freeze | Shockblast | Gale | 15-45 | 6% |
|
||||
| 2 | Inferno | Blizzard | Chained Lightning | Downburst | 95-125 | 14% |
|
||||
| 3 | Flames of Loki | Fimbulvetr | Wrath of Thor | Storms of Njord | 175-205 | 21% |
|
||||
|
||||
Cost is % of player level. T2/T3 hit multiple targets (5-10 depending on abilities).
|
||||
|
||||
**Deprecating (mandatory for mages):**
|
||||
- **Imperil** (Lvl 130, 10% cost, 3 CD) — REDUCES enemy phys/mag/elemental mitigation
|
||||
- **Weaken** (Lvl 70, 10% cost, 3 CD) — REDUCES enemy damage by 50%
|
||||
- **Slow** (Lvl 10, 18% cost, 3 CD) — slows enemy actions
|
||||
- **Sleep** (Lvl 80, 22% cost, 7 CD) — incapacitates target
|
||||
- **MagNet** (Lvl 250, 22% cost, 15 CD) — prevents evade AND resist
|
||||
|
||||
**Supportive:**
|
||||
- **Haste** (Lvl 60, 30% cost, 0 CD) — +50% action speed
|
||||
- **Protection** (Lvl 10, 25% cost, 0 CD) — -25% damage taken
|
||||
- **Shadow Veil** (Lvl ~130) — base evade that ignores burden
|
||||
- **Regen** (Lvl 50, 22.5% cost, 0 CD) — HP regen/turn
|
||||
- **Cure** (Lvl 5, 20% cost, 5 CD) — instant heal
|
||||
- **Heartseeker** (Lvl 200+) — +25% phys damage
|
||||
- **Spirit Shield** (Lvl 200+) — absorb damage with SP
|
||||
|
||||
**Spell cost modified by:**
|
||||
```
|
||||
MP_cost = roundup(level * base_cost/100 * (1 + interference*0.5%)
|
||||
* (1 - mana_conservation) * (1 - spirit_stance)
|
||||
* (1 - coalesced))
|
||||
```
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 5. BATTLE MODES
|
||||
|
||||
### Arena
|
||||
- Fixed monster sets, fixed round counts
|
||||
- **Best credits/hour in early-mid game**
|
||||
- Available once/day per challenge
|
||||
- Higher level challenges have more rounds, better credit bonuses
|
||||
- Max: "Eve of Death" (Lvl 180, 70 rounds)
|
||||
- First clear bonus is higher than subsequent clears
|
||||
|
||||
### Grindfest
|
||||
- **Infinite rounds** (until you die or flee)
|
||||
- Monsters scale with rounds (harder over time)
|
||||
- 1 stamina entry cost, no per-round stamina
|
||||
- **Best EXP/proficiency grind for sustained play**
|
||||
- Drop multiplier increases at high round counts
|
||||
|
||||
### Item World
|
||||
- Level up equipment stats (potency)
|
||||
- Costs all stamina upfront based on rounds to clear
|
||||
- Quality of equipment affects difficulty
|
||||
- Higher difficulty → better potency gains
|
||||
|
||||
### Random Encounter
|
||||
- Triggered randomly while browsing E-Hentai
|
||||
- Does NOT cost stamina
|
||||
- Special loot tables
|
||||
- Scripts (HV Utils) auto-detect and notify
|
||||
|
||||
### Ring of Blood
|
||||
- Boss fights
|
||||
- 1/day attempt
|
||||
- Unique rewards
|
||||
|
||||
### The Tower (Isekai)
|
||||
- Seasonal competitive mode
|
||||
- Separate character (Isekai mode)
|
||||
- Rankings and special rewards
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 6. ECONOMY
|
||||
|
||||
### Credits (primary currency)
|
||||
- Earned from battles (Arena most reliable)
|
||||
- Spent on: training, equipment repairs, item shop, market
|
||||
|
||||
### Credits per Arena (first clear):
|
||||
| Challenge | Rounds | First Clear Credits |
|
||||
|-----------|--------|---------------------|
|
||||
| Fresh Meat (Lvl 50) | 12 | 5,000 C |
|
||||
| Endgame (Lvl 100) | 35 | 10,000 C |
|
||||
| Exile (Lvl 130) | 50 | 20,000 C |
|
||||
| To Kill a God (Lvl 165) | 65 | 35,000 C |
|
||||
| Eve of Death (Lvl 180) | 70 | 40,000 C |
|
||||
|
||||
### Hath (premium currency)
|
||||
- Earned from H@H (our other project) and donations
|
||||
- Buy Hath Perks (permanent account upgrades)
|
||||
- Exchange for Credits on Hath Exchange (currently ~3,038 C/Hath)
|
||||
|
||||
### Gallery Points (GP)
|
||||
- Earned from H@H hits (0.1 GP/hit)
|
||||
- Earned from HV battles (slower)
|
||||
- Used in bounty system
|
||||
|
||||
### Training (credit sink, permanent upgrades)
|
||||
| Training | Effect | Levels | Initial Cost |
|
||||
|----------|--------|--------|-------------|
|
||||
| Adept Learner | +1% EXP/level | 300 | 100 C |
|
||||
| Scavenger | +1% loot drop chance | 50 | 500 C |
|
||||
| Ability Boost | +1 Ability Point | 500 | 100 C |
|
||||
| Assimilator | +10% prof gain rate | 25 | 50K C |
|
||||
| Quartermaster | +5% equip drop chance | 20 | 5K C |
|
||||
| Archaeologist | +10% artifact drop chance | 10 | 25K C |
|
||||
| Luck of the Draw | +1% rare equip chance | 25 | 2K C |
|
||||
|
||||
### Items (battle consumables)
|
||||
- Energy Drink: +10 stamina (from donator daily reward)
|
||||
- Caffeinated Candy: +5 stamina (lottery)
|
||||
- Potions/Scrolls/Gems: various battle effects
|
||||
- Infusions: temporary elemental damage
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 7. AUTOMATION RULES (CRITICAL)
|
||||
|
||||
**From the official wiki ("Scripts & Tools" → "Forbidden Actions"):**
|
||||
|
||||
### EXPLICITLY FORBIDDEN (full ban):
|
||||
1. **Fully automated/unattended play** — clearing multiple battles without ANY
|
||||
manual input from the player
|
||||
2. **Automatic monster feeding** — collecting benefits without player input
|
||||
3. **Automating RiddleMaster** — including ML-based solving
|
||||
4. **Auto-starting new battles** — anything that chains battles together
|
||||
|
||||
### ALLOWED:
|
||||
1. Per-round auto-battlers that require a **human click to advance** (jpx)
|
||||
2. UI enhancements (HV Utils, Monsterbation)
|
||||
3. Statistical tracking, alerts, quality-of-life scripts
|
||||
4. Hotkey rebinding
|
||||
|
||||
**The grey area**: "per round" means a script may decide what action to take
|
||||
this round, but the player must manually click to execute or advance. The
|
||||
existing approved script jpx works exactly this way — it recommends the
|
||||
optimal action, the player clicks to execute, the script processes the result
|
||||
and recommends the next action.
|
||||
|
||||
### PENALTIES
|
||||
**Full account ban** — not just the HV game. You lose:
|
||||
- Access to e-hentai.org galleries
|
||||
- Forum account
|
||||
- All H@H clients
|
||||
- All Hath, Credits, equipment
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 8. EXISTING AUTOMATION TOOLS (patterns to study)
|
||||
|
||||
### jpx (approved, active)
|
||||
- Type: Tampermonkey userscript
|
||||
- Function: "Manually triggered per round auto-battler"
|
||||
- Pattern: Press a hotkey → script recommends action → press another hotkey →
|
||||
script submits the action → page reloads → script parses new state → repeat
|
||||
- Has: auto-battle ruleset manager, battle statistics tracker
|
||||
- URL: Listed on wiki's Scripts & Tools page (v2026.07.06)
|
||||
|
||||
### Monsterbation (approved)
|
||||
- Type: Tampermonkey userscript (v1.4.1.3)
|
||||
- Function: "All-purpose hovering script for melee and mage"
|
||||
- Pattern: Hover-based UI that overlays recommendations
|
||||
|
||||
### HV Utils (approved)
|
||||
- Type: Tampermonkey userscript (v4.2.3)
|
||||
- Function: Out-of-battle automation
|
||||
- Automates: shrine, equipment shop (auto-sell/salvage bad gear), RE detection,
|
||||
inline equipment changer
|
||||
|
||||
### HV Toolbox
|
||||
- Type: Tampermonkey userscript
|
||||
- Function: Shop management, MoogleMail, shrine
|
||||
|
||||
---
|
||||
|
||||
## 9. IMPLEMENTATION OPTIONS
|
||||
|
||||
### Option A: Tampermonkey Userscript (Recommended)
|
||||
**What:** JavaScript userscript injected into hentaiverse.org
|
||||
**How:** Leverages the browser's DOM + cookies for authenticated requests.
|
||||
**Compliance:** Requires one human click per round = fully within rules.
|
||||
|
||||
**Architecture:**
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ hentaiverse.org page (browser) │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ Tampermonkey Userscript │ │
|
||||
│ │ ┌─────────┐ ┌──────┐ ┌───────┐ │ │
|
||||
│ │ │ Parser │ │ Strat │ │Action │ │ │
|
||||
│ │ │(DOM→obj)│→│ Engine│→│ Submitter│ │
|
||||
│ │ └─────────┘ └──────┘ └──┬────┘ │ │
|
||||
│ │ │ POST │ │
|
||||
│ └────────────────────────┼───────┘ │
|
||||
│ ← HTML response ←─┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Low development effort (pure JS, ~2000-3000 lines)
|
||||
- Uses existing browser session/cookies (no auth headache)
|
||||
- Can be published as a Tampermonkey script
|
||||
- Hotkey-driven: press "F" to execute recommended action
|
||||
|
||||
**Cons:**
|
||||
- Requires a browser open (can't be fully headless)
|
||||
- Must comply with per-round click requirement
|
||||
- Cannot auto-chain battles (within rules anyway)
|
||||
|
||||
### Option B: Headless Browser + Puppeteer/Playwright
|
||||
**What:** Node.js script controlling headless Chromium with puppeteer
|
||||
**How:** Full browser automation — can click buttons, fill forms, parse HTML
|
||||
**Compliance:** DANGER ZONE — easy to accidentally cross into full automation
|
||||
|
||||
**Pros:**
|
||||
- Can run on VPS headless 24/7
|
||||
- Full DOM access
|
||||
- Can take screenshots for debugging
|
||||
|
||||
**Cons:**
|
||||
- Resource heavy (Chromium + Node.js)
|
||||
- Very easy to get banned if fully automated
|
||||
- The 4-turns/sec rate limit still applies
|
||||
- RiddleMaster will appear faster with full automation and flag you
|
||||
|
||||
### Option C: Direct HTTP Client (Python/Node.js)
|
||||
**What:** Script that directly sends HTTP POST requests to hentaiverse.org
|
||||
**How:** Parse HTML responses, POST action forms, manage cookies
|
||||
**Compliance:** HIGH RISK — indistinguishable from botting
|
||||
|
||||
**Pros:**
|
||||
- Zero overhead (no browser)
|
||||
- Can run on VPS headless
|
||||
- Fastest possible throughput
|
||||
|
||||
**Cons:**
|
||||
- Must reverse-engineer exact form fields, CSRF tokens, cookie handling
|
||||
- No JavaScript execution — some game mechanics may rely on JS
|
||||
- **Extremely high ban risk** if performed unattended
|
||||
- Cannot solve RiddleMaster without AI/OCR (which is explicitly forbidden)
|
||||
|
||||
### Option D: Hybrid — Chrome Extension
|
||||
**What:** Chrome extension that bridges between manual and headless
|
||||
**How:** Extension runs a WebSocket server; a remote harness connects and controls
|
||||
|
||||
**Pros:**
|
||||
- Browser handles auth/rendering
|
||||
- Remote harness can run on VPS
|
||||
- Clean separation of concerns
|
||||
|
||||
**Cons:**
|
||||
- Browser still needs to be open
|
||||
- Complex architecture
|
||||
- Extension review policy may flag it
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 10. RECOMMENDED STRATEGY
|
||||
|
||||
### Short-term: Tampermonkey script (Option A)
|
||||
|
||||
Build a **per-round auto-battler** modeled after jpx:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Strategy Engine (what the script decides) │
|
||||
│ │
|
||||
│ MAGE MODE (simplest to automate): │
|
||||
│ 1. Check if Imperil needed → cast │
|
||||
│ 2. Check if Weaken needed → cast │
|
||||
│ 3. Check mana → if low, use Draught/Focus │
|
||||
│ 4. Check if Haste active → if not, cast │
|
||||
│ 5. Check if Shadow Veil active → cast │
|
||||
│ 6. Cast strongest available AoE spell │
|
||||
│ Pick element based on target weakness: │
|
||||
│ - Scan each monster for lowest resist │
|
||||
│ - Match spell element to weakness │
|
||||
│ ALL BY HOTKEY: PRESS ONE KEY TO EXECUTE │
|
||||
│ │
|
||||
│ MELEE MODE: │
|
||||
│ 1. Check Overcharge → use skills if enough │
|
||||
│ 2. Attack lowest-HP monster first (sweep) │
|
||||
│ 3. Use Spirit Stance when full OC/SP │
|
||||
│ 4. Heartseeker before big skills │
|
||||
│ │
|
||||
│ ITEM MODE (optional, stamina recovery): │
|
||||
│ - Consume Energy Drink if stamina < 10 │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Mid-term: Add out-of-battle automation like HV Utils
|
||||
- Auto-sell/salvage low-quality equipment
|
||||
- Auto-shrine artifacts
|
||||
- Auto-train when credits available
|
||||
- Monster Lab feeding reminders
|
||||
|
||||
### Long-term (IF server offset goal is serious):
|
||||
Run H@H 24/7 for passive Hath (our primary project), and supplement with
|
||||
Hath→Credit exchange (3,038 Cr/Hath). This is the **legit, no-ban-risk path**
|
||||
to monetize the game's economy without botting.
|
||||
|
||||
### NEVER build:
|
||||
- Fully automated battle chaining (Arena → exit → next Arena → ...)
|
||||
- RiddleMaster auto-solver
|
||||
- Anything that runs with zero human interaction for >1 round
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 11. KEY TECHNICAL NOTES FOR IMPLEMENTATION
|
||||
|
||||
### HTML Parsing
|
||||
|
||||
The battle page structure (from wiki screenshots):
|
||||
- Top bar: stamina, credits, HP/MP/SP bars
|
||||
- Center: enemy list (table with HP bars, status icons, names)
|
||||
- Bottom: action buttons (Attack dropdown, Spell grid, Skill list, Item list)
|
||||
- Battle log: text output of last action
|
||||
|
||||
### Monster Data Extraction (from HTML tables)
|
||||
```
|
||||
<table class="monster_list">
|
||||
<tr> <!-- per monster -->
|
||||
<td>HP bar (width = current/max %)</td>
|
||||
<td>Status icons</td>
|
||||
<td>Name (text)</td>
|
||||
<td>Level/PL</td>
|
||||
</tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
### Player State Extraction
|
||||
```
|
||||
HP: parse from "Health: X / Y" text
|
||||
MP: parse from "Magic: X / Y"
|
||||
SP: parse from "Spirit: X / Y"
|
||||
Overcharge: parse from yellow bar width
|
||||
Current buffs: check status icon presence
|
||||
```
|
||||
|
||||
### Form Submission
|
||||
```javascript
|
||||
// Attack monster #2
|
||||
document.forms['battle'].elements['action'].value = 'attack';
|
||||
document.forms['battle'].elements['target'].value = '2';
|
||||
document.forms['battle'].submit();
|
||||
|
||||
// OR via fetch to avoid full page reload (if game supports AJAX):
|
||||
fetch('?s=BATTLE', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({action:'attack', target:2})
|
||||
}).then(r => r.text()).then(html => { /* re-parse */ });
|
||||
```
|
||||
|
||||
### Elemental Weakness Matching
|
||||
Monsters have 6 resistance values (Fire, Cold, Elec, Wind, Holy, Dark).
|
||||
The Scan skill reveals these. Without scanning, a script can track damage
|
||||
numbers and infer resistances:
|
||||
```
|
||||
If Fire spell did 1000 damage and Cold did 2000 → target is fire-resistant.
|
||||
Track per-monster-type for future encounters (Monster Lab data).
|
||||
```
|
||||
|
||||
### RiddleMaster Bypass (DON'T — EXPLICITLY FORBIDDEN)
|
||||
|
||||
The RiddleMaster is a CAPTCHA-like puzzle that appears between rounds at random
|
||||
intervals. The wiki explicitly states:
|
||||
|
||||
> "Any kind of attempts to automate the RiddleMaster in any way, including scripts
|
||||
> that automatically scrape riddles for the purposes of machine learning."
|
||||
|
||||
**The ONLY legitimate approach**: pause the script and require the human to solve
|
||||
it manually. Play a sound or flash the tab to get attention.
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 12. CREDIT-FARMING MATH
|
||||
|
||||
### Credits per Arena run (approximate, mid-level)
|
||||
|
||||
| Arena | Rounds | ~Time | Credits (first) | Credits (repeat) |
|
||||
|-------|--------|-------|-----------------|-----------------|
|
||||
| Fresh Meat (50) | 12 | 3-5 min | 5,000 | 1,000 |
|
||||
| Killzone (90) | 30 | 8-10 min | 9,000 | ~1,800 |
|
||||
| Endgame (100) | 35 | 10-12 min | 10,000 | ~2,000 |
|
||||
| Exile (130) | 50 | 15-20 min | 20,000 | ~4,000 |
|
||||
| To Kill a God (165) | 65 | 20-25 min | 35,000 | ~7,000 |
|
||||
|
||||
A dedicated player running ~14 arenas/day at mid-tier earns roughly
|
||||
**50,000-150,000 Credits/day** from Arenas, plus equipment/artifact drops
|
||||
worth 10-50K more.
|
||||
|
||||
### Hath vs Credits comparison
|
||||
From earlier calculation: 1 Hath = ~3,038 Credits (exchange rate).
|
||||
Our H@H client (58,000 KB/s, 400 GB) may earn 5-150 Hath/day depending on
|
||||
ramp-up. That's 15,190 - 455,700 Credits/day equivalent — **orders of magnitude
|
||||
more efficient than manually grinding HV**.
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 13. FILE STRUCTURE (Tampermonkey script template)
|
||||
|
||||
```
|
||||
/scripts/hv-auto/
|
||||
├── hv-auto.user.js ← Main userscript (manifest + bootstrap)
|
||||
├── parser.js ← HTML→gameState parser
|
||||
├── strategy.js ← Action decision engine
|
||||
├── actions.js ← Form submission helpers
|
||||
├── constants.js ← Monster DB, spell tables, damage type matrix
|
||||
├── monitor.js ← Stamina/credit/profit tracking
|
||||
└── ui.js ← Optional overlay UI
|
||||
```
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 14. BOTTOM LINE
|
||||
|
||||
H@H is your **passive income engine** (build it, forget it, cash Hath).
|
||||
HV scripts are your **quality-of-life supplements** (speed up Arena runs,
|
||||
manage inventory, train efficiently).
|
||||
|
||||
Don't risk a full account ban trying to fully automate HV. The per-round
|
||||
semi-automation path (Option A) is proven by jpx/Monsterbation, fully within
|
||||
rules, and saves 90% of the tedium.
|
||||
278
references/hv-guide.md
Normal file
278
references/hv-guide.md
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
# HentaiVerse — Complete Domination Guide (Level 1 → 500)
|
||||
## Powered by HV Unified script + H@H passive income
|
||||
|
||||
---
|
||||
|
||||
## PHASE 0: SETUP (Before your first battle)
|
||||
|
||||
### 1. Install the script
|
||||
Open Tampermonkey → Create new script → paste `/home/gabogg/hv-unified/hv-unified.user.js` → Save.
|
||||
|
||||
### 2. Understand the controls
|
||||
| Key | What |
|
||||
|-----|------|
|
||||
| **Q** | Execute recommended action (attacks, spells, heals — auto-decides) |
|
||||
| **H** | Toggle hover mode (mouse over a monster = auto-attack) |
|
||||
| **C** | Emergency heal (cast Cure/Full-Cure immediately) |
|
||||
| **,** | Open settings panel (toggle features, adjust thresholds) |
|
||||
|
||||
### 3. Your passive income engine
|
||||
H@H client 52710 is already running on the VPS. It'll take weeks to ramp up,
|
||||
but once it does, you'll earn Hath passively. Exchange Hath for Credits at
|
||||
~3,000 Cr/Hath. This IS your server-funding strategy.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1: NOVICE (Levels 1–50) — Survival First
|
||||
|
||||
### Stat allocation
|
||||
```
|
||||
STR ██████░░░░░░░░░░░░░░ 30% — Physical damage
|
||||
END █████░░░░░░░░░░░░░░░ 25% — HP + damage reduction
|
||||
DEX ████░░░░░░░░░░░░░░░░ 20% — Accuracy + Parry + Phys dmg
|
||||
AGI ██░░░░░░░░░░░░░░░░░░ 10% — Evade + Attack speed
|
||||
WIS ██░░░░░░░░░░░░░░░░░░ 10% — MP + Magic acc
|
||||
INT █░░░░░░░░░░░░░░░░░░░ 5% — Magic damage
|
||||
```
|
||||
|
||||
**Rule:** END keeps you alive. STR makes battles faster. Level them 2:1 until
|
||||
you stop dying, then shift toward STR.
|
||||
|
||||
### What to do every day
|
||||
1. **Arena "First Blood"** (2 rounds) — 100 credits, takes 30 seconds
|
||||
2. **Arena "Graduation"** (6 rounds) — 1,000 credits first clear
|
||||
3. Spend credits on **Adept Learner training** — +1% EXP per level, stacks forever
|
||||
4. Any leftover stamina → **Grindfest on Normal** — great EXP, flee at 30% HP
|
||||
|
||||
### Equipment
|
||||
- Don't worry about gear quality yet. Equip whatever drops.
|
||||
- Sell Crude/Fair items (click the Shop Sell buttons our script adds).
|
||||
- Keep Average+ items that match your weapon type.
|
||||
|
||||
### Spell priority
|
||||
| Level | Spell | Why |
|
||||
|-------|-------|-----|
|
||||
| 5 | **Cure** | Your lifeline. Press C to use. |
|
||||
| 10 | **Protection** | -25% damage taken. Cast at battle start. |
|
||||
| 15 | **Fiery Blast** | First damage spell. Use when MP > 25%. |
|
||||
| 25 | **Freeze** | Cold damage + slows enemies. |
|
||||
|
||||
### Script behavior at this tier
|
||||
- Will auto-cast Cure when HP < 35%
|
||||
- Will use any offensive spell you've unlocked
|
||||
- Defaults to basic attacks when MP is low
|
||||
- Won't waste MP on unnecessary buffs
|
||||
|
||||
### Credit earnings (estimate)
|
||||
- ~500-2,000 credits/day from early arenas
|
||||
- ~100-500 credits from equipment sales
|
||||
- **Priority:** Adept Learner to level 50, then Scavenger to level 25
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2: ADEPT (Levels 50–150) — Building Power
|
||||
|
||||
### Stat shift
|
||||
```
|
||||
STR ███████░░░░░░░░░░░░░ 35% — Physical damage (increased)
|
||||
END ████░░░░░░░░░░░░░░░░ 20% — HP (reduced — you survive better now)
|
||||
DEX ████░░░░░░░░░░░░░░░░ 20% — Accuracy + Parry
|
||||
AGI ██░░░░░░░░░░░░░░░░░░ 10% — Evade
|
||||
WIS ██░░░░░░░░░░░░░░░░░░ 10% — MP pool
|
||||
INT █░░░░░░░░░░░░░░░░░░░ 5% — Minimal
|
||||
```
|
||||
|
||||
### What to do every day
|
||||
1. **All available Arenas** — you should have 5-8 unlocked by now
|
||||
2. **Grindfest on Hard** — better drops, more EXP
|
||||
3. Training: **Adept Learner → 100**, **Scavenger → 25**, **Ability Boost → 50**
|
||||
4. Start doing **Item World** on Average+ weapons — adds potency stats
|
||||
|
||||
### Equipment goals
|
||||
- Get a weapon with **elemental strike prefix** (Fiery/Freezing/Shocking/Gale)
|
||||
- Look for **Superior+ quality** in your weapon slot first
|
||||
- Armor: prioritize **Protection suffix** or **Agile prefix** on light armor
|
||||
- Start checking equip popups — our script tags items as KEEP/SELL
|
||||
|
||||
### Key spell unlocks
|
||||
| Level | Spell | Impact |
|
||||
|-------|-------|--------|
|
||||
| 50 | **Regen** | Passive HP regen per turn. Cast once, lasts whole battle. |
|
||||
| 60 | **Haste** | +50% action speed. Most important buff in the game. |
|
||||
| 65 | **Smite** | Holy damage. Good against undead/demons. |
|
||||
| 70 | **Weaken** | Reduces enemy damage by 50%. Cast on bosses. |
|
||||
| 80 | **Sleep** | Incapacitates enemy. Cast on dangerous targets. |
|
||||
|
||||
### Script behavior at this tier
|
||||
- Auto-maintains Haste + Protection + Regen
|
||||
- Auto-casts Imperil on strongest enemy (once unlocked at 130)
|
||||
- Auto-casts Weaken on bosses
|
||||
- Uses T1 spells for damage, prioritizes AoE if multiple enemies
|
||||
|
||||
### Credit earnings (estimate)
|
||||
- ~5,000-15,000 credits/day from arenas
|
||||
- ~500-2,000 from equipment sales
|
||||
- Start saving for Ability Boost training (100 credits per level, adds up)
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3: VETERAN (Levels 150–300) — Speed & Efficiency
|
||||
|
||||
### Stat allocation (1H style)
|
||||
```
|
||||
STR ████████░░░░░░░░░░░░ 40% — Physical damage (maximize)
|
||||
DEX █████░░░░░░░░░░░░░░░ 25% — Crit + Parry
|
||||
END ███░░░░░░░░░░░░░░░░░ 15% — HP (enough to not die)
|
||||
AGI ██░░░░░░░░░░░░░░░░░░ 10% — Attack speed
|
||||
WIS █░░░░░░░░░░░░░░░░░░░ 5% — Utility
|
||||
INT ░░░░░░░░░░░░░░░░░░░░ 5% — Minimal
|
||||
```
|
||||
|
||||
For mages (Staff): INT 40%, WIS 30%, END 15%, AGI 10%.
|
||||
|
||||
### What to do every day
|
||||
1. **All 10+ Arenas** — credits from "Endgame" (10K), "Exile" (20K), "To Kill a God" (35K)
|
||||
2. **Grindfest PFUDOR** — maximum rewards, 100+ rounds
|
||||
3. Training: finish **Scavenger → 50**, **Quartermaster → 20**, start **Assimilator**
|
||||
4. **Item World** on your best weapons — aim for Butcher/Fatality/Swift Strike potencies
|
||||
|
||||
### Key unlocks
|
||||
| Level | What | Why |
|
||||
|-------|------|-----|
|
||||
| 130 | **Imperil** | Reduces ALL enemy defenses. Cast it on everything. |
|
||||
| 175 | **T3 spells** | Huge damage, hits 7-10 targets. Arena clear speed doubles. |
|
||||
| 200 | **Paradise Lost** | Best holy nuke. |
|
||||
| 220 | **Full-Cure** | Full HP restore. Emergency button. |
|
||||
| 245 | **Ragnarok** | Best dark nuke. |
|
||||
| 250 | **MagNet** | Prevents enemy evade AND resist. Essential for mages. |
|
||||
|
||||
### Equipment goals
|
||||
- Full **Exquisite+** set in your fighting style
|
||||
- Weapon with **Butcher Lv.5** (IW potency) — +25% damage
|
||||
- **Force Shield** (rare drop) if 1H — best block in game
|
||||
- Start looking for **Magnificent+** gear — our script will flag these
|
||||
|
||||
### Fighting style optimization
|
||||
| Style | Best at | Weapon |
|
||||
|-------|---------|--------|
|
||||
| **1H + Shield** | Arena clearing (fast, safe) | Rapier (Penetrated Armor) or Shortsword (high accuracy) |
|
||||
| **2H** | Boss killing (huge single hits) | Estoc (Penetrated Armor) or Katana (high crit) |
|
||||
| **DW** | Speed farming (most attacks/turn) | Wakizashi + Wakizashi |
|
||||
| **Staff** | AoE grinding (hits everything) | Willow (Holy+Dark) or Redwood (balanced elements) |
|
||||
|
||||
### Script behavior at this tier
|
||||
- Full auto-buff rotation (Haste → Protection → Shadow Veil → Spark of Life)
|
||||
- Auto Spirit Stance management (toggle when OC > 80%)
|
||||
- Auto skill usage (Frenzied Blows, Great Cleave, Shatter Strike)
|
||||
- Debuff priority: Imperil → Weaken → Slow (bosses only)
|
||||
- Damage: T3 AoE spells if multiple enemies, T2 single-target otherwise
|
||||
|
||||
### Credit earnings (estimate)
|
||||
- ~30,000-80,000 credits/day from arenas
|
||||
- ~5,000-20,000 from equipment sales (rare drops are valuable)
|
||||
- Training costs increase significantly — prioritize Ability Boost
|
||||
- Buy **Source Nexus** (1,000 Hath) if you have it — unlocks original images
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4: MASTER (Levels 300–500) — Optimization
|
||||
|
||||
### Stat allocation (1H style)
|
||||
```
|
||||
STR █████████░░░░░░░░░░░ 45% — Maximum physical damage
|
||||
DEX ██████░░░░░░░░░░░░░ 30% — Max crit + parry
|
||||
END ██░░░░░░░░░░░░░░░░░░ 10% — Minimum to survive
|
||||
AGI ██░░░░░░░░░░░░░░░░░░ 10% — Speed cap
|
||||
WIS ░░░░░░░░░░░░░░░░░░░░ 3% — Bare minimum
|
||||
INT ░░░░░░░░░░░░░░░░░░░░ 2% — Negligible
|
||||
```
|
||||
|
||||
### Daily routine
|
||||
1. **All 14+ Arenas** — ~50,000-150,000 credits/day
|
||||
2. **Grindfest PFUDOR** for proficiency farming
|
||||
3. **Item World** on peerless equipment
|
||||
4. **Tower climbing** (Isekai) for seasonal rewards
|
||||
5. Training: max everything — Ability Boost, Scavenger, Quartermaster, Assimilator
|
||||
|
||||
### Endgame goals
|
||||
- Full **Legendary/Peerless** equipment set
|
||||
- All **Hath Perks** relevant to your build (Daemon Duality, Repair Bear, Innate Arcana)
|
||||
- Top 100 Tower ranking (seasonal)
|
||||
- **Follower of Snowflake** (50,000 Hath — long-term goal, gives peerless vouchers)
|
||||
|
||||
### Money strategy
|
||||
- Exchange **Hath → Credits** on the Hath Exchange (~3,000 Cr/Hath)
|
||||
- Your H@H client generates Hath passively — this IS your income engine
|
||||
- Earned Hath can buy Source Nexus, Daemon Duality, Repair Bear — all permanent account upgrades
|
||||
- Bounty system: convert Credits + Hath → Steam gift cards to pay for VPS
|
||||
|
||||
---
|
||||
|
||||
## GENERAL TIPS
|
||||
|
||||
### Always do
|
||||
- **Every arena, every day** — they reset at Dawn (~midnight UTC). Credit income scales with arena level.
|
||||
- **Training queue** — Adept Learner first, then Scavenger, then damage dealers.
|
||||
- **Equipment check** — after every arena clear, check drops. Sell ≤Average, keep Superior+.
|
||||
- **Monster Lab** — feed monsters daily. They bring gifts (items, crystals, credits).
|
||||
|
||||
### Never do
|
||||
- **Don't sell Magnificent+ gear** — it's worth 10-100x more to players than the shop.
|
||||
- **Don't shrine figurines** — they can be sold to players for credits.
|
||||
- **Don't shrine collectables** — needed for Orbital Friendship Cannon.
|
||||
- **Don't let stamina cap at 99** — you lose regeneration. Use it or lose it.
|
||||
- **Don't neglect proficiency** — Assimilator training + Grindfest = fast prof gains.
|
||||
|
||||
### Stamina management
|
||||
- 99 max stamina, regenerates **1 per hour** (24/day)
|
||||
- Great status (60-99): +100% EXP but faster drain
|
||||
- Normal (1-59): standard
|
||||
- Exhausted (0): NO rewards from battles. Never battle at 0.
|
||||
- **Priority:** Arenas first (best credits/stamina), then Grindfest (best EXP/stamina), then Item World
|
||||
|
||||
### Hath strategy
|
||||
1. Let H@H client run 24/7 (already set up)
|
||||
2. First 1,000 Hath → **Source Nexus** (unlocks original images on galleries)
|
||||
3. Next → **Daemon Duality I-V** (permanent +10-50% damage)
|
||||
4. Then → **Repair Bear** (reduces equipment degradation) or **Innate Arcana** (auto-maintain buffs)
|
||||
5. Exchange excess Hath for Credits (3,000 Cr/Hath) to fund training
|
||||
6. Long-term: use Creds + Hath in Bounty system → Steam gift cards → offset VPS cost
|
||||
|
||||
### Credit spending priority
|
||||
1. **Training** — always have one training running
|
||||
2. **Equipment repairs** — broken gear = 0 stats
|
||||
3. **Item Shop** — mana/spirit potions for long battles
|
||||
4. **Equipment upgrades** — forge your best gear
|
||||
5. **Lottery tickets** — low priority, gambling
|
||||
|
||||
### The H@H math
|
||||
Your client at 58,000 KB/s + 400 GB cache in Europe region:
|
||||
- Expected: **5-50 Hath/day** after ramp-up (1-3 months)
|
||||
- At 3,038 Cr/Hath: **15,000-150,000 Credits/day equivalent**
|
||||
- At bounty rates: this can cover **$2-10/month** in gift cards
|
||||
- Combined with HV earnings: realistic path to **$15/month** (VPS break-even)
|
||||
|
||||
---
|
||||
|
||||
## QUICK REFERENCE CARD
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ HV UNIFIED — Quick Ref │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Q Execute recommended action │
|
||||
│ H Toggle hover mode (auto-attack) │
|
||||
│ C Emergency heal (Cure/Full-Cure) │
|
||||
│ , Open settings panel │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Console API: │
|
||||
│ HV.advice() Full guidance for your level │
|
||||
│ HV.stats() Tracked battles/credits/drops │
|
||||
│ HV.settings() Open settings UI │
|
||||
│ HV.monsters() Monster HP database │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ H@H Status: sudo systemctl status hentaiathome │
|
||||
│ H@H Logs: sudo journalctl -u hentaiathome -f │
|
||||
│ Hath page: https://e-hentai.org/hentaiathome │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
537
references/hv-scripts-analysis.md
Normal file
537
references/hv-scripts-analysis.md
Normal file
|
|
@ -0,0 +1,537 @@
|
|||
# HentaiVerse Scripts — Source Code Analysis & Architecture Patterns
|
||||
## Based on Monsterbation 1.4.1.2 (163KB, 2402 lines) + Wiki formulas
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 1. SOURCE CODE STATUS
|
||||
|
||||
| Script | Version | Status | Source |
|
||||
|--------|---------|--------|--------|
|
||||
| **Monsterbation** | 1.4.1.2 | Downloaded (163KB) | Forum attachment 157304 |
|
||||
| **CrunkJuice** | 1.3.0 | Available (same thread) | Forum attachment (companion) |
|
||||
| **jpx** | v2026.07.06 | Forum thread 290507 | GitHub-hosted? (Cloudflare blocked) |
|
||||
| **HV Utils** | 4.2.3 | Forum thread 211883 | Forum attachment 188793 (Cloudflare blocked) |
|
||||
| **HV Toolbox** | 1.0.14 | Forum thread 209070 | Forum attachment 165038 (Cloudflare blocked) |
|
||||
| **HVSTAT** | 5.7.1 | Forum thread 79552 | Forum attachment 15604 (Cloudflare blocked) |
|
||||
|
||||
**Blockers**: E-hentai forums are behind Cloudflare protection. Direct downloads from the
|
||||
VPS are blocked by JS challenge. None are on GreasyFork/GitHub (community convention is
|
||||
forum-only distribution). User needs to download these from their browser and upload to VPS.
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 2. MONSTERBATION — ARCHITECTURE DEEP DIVE
|
||||
|
||||
### 2.1 High-Level Architecture
|
||||
|
||||
```
|
||||
Monsterbation = Tampermonkey userscript injected into hentaiverse.org
|
||||
|
||||
DETECTION: Check for DOM elements #textlog or #riddlemaster → battle page
|
||||
NO HTTP requests for game state. ALL state parsed from DOM.
|
||||
|
||||
PATTERN: Page interactions = clicking existing DOM elements.
|
||||
NEVER constructs raw HTTP POSTs.
|
||||
```
|
||||
|
||||
### 2.2 Core Loop
|
||||
|
||||
```
|
||||
Page loads (battle round start)
|
||||
→ Enhance() called
|
||||
→ Parse DOM: monsters[], vitals (HP/MP/SP bars), buffs, cooldowns
|
||||
→ Set up event listeners on monsters (mouseover, mousedown, wheel, contextmenu)
|
||||
→ If hover enabled: auto-fire hover attack on target monster
|
||||
→ Wait for human interaction (keyboard/mouse)
|
||||
|
||||
Human moves mouse over monster
|
||||
→ SetTarget(i) called
|
||||
→ If hover enabled: Hover() fires
|
||||
→ Hover(): run cfg.hoverAction() → then monsters[target].click()
|
||||
→ The click triggers the page's built-in onclick handler
|
||||
→ Server processes turn → new HTML page loads → loop repeats
|
||||
```
|
||||
|
||||
### 2.3 DOM-Based State Parsing
|
||||
|
||||
**Monster detection:**
|
||||
```javascript
|
||||
monsters = document.querySelectorAll('.btm') // Battle table monsters
|
||||
// Each monster has onclick attribute when alive
|
||||
monsters[i].hasAttribute('onclick') // true = alive, false = dead
|
||||
```
|
||||
|
||||
**Vitals (HP/MP/SP):**
|
||||
```javascript
|
||||
// Parse from bar image widths (2 possible formats - isekai vs persistent)
|
||||
mp = parseInt(image.width) / 414 // MP bar fill ratio
|
||||
sp = parseInt(image.width) / 414 // SP bar fill ratio
|
||||
hp_left = parseInt(image.width) / 414 // HP bar fill ratio
|
||||
// Overcharge: bar_orange.png width
|
||||
|
||||
// Isekai uses /207 instead of /414 (different base widths)
|
||||
```
|
||||
|
||||
**Buffs/Debuffs:**
|
||||
```javascript
|
||||
// Parse from onmouseover text on effect icons
|
||||
var effects = document.getElementById('pane_effects').getElementsByTagName('img')
|
||||
effects[n].getAttribute('onmouseover') // Contains duration + stack count
|
||||
// Regex: /regexp.duration/ extracts turns remaining and stacks
|
||||
```
|
||||
|
||||
**Cooldowns:**
|
||||
```javascript
|
||||
// Quickbar icons have cooldown info in their style/attributes
|
||||
// Timer shows turns until available
|
||||
```
|
||||
|
||||
**Battle log (post-combat):**
|
||||
```javascript
|
||||
// Parses textLog for damage numbers, proficiency gains, drops
|
||||
var log = document.getElementById('textlog')
|
||||
// Regex extracts: monsters present, damage dealt, items dropped
|
||||
```
|
||||
|
||||
### 2.4 Action System (how it submits turns)
|
||||
|
||||
**Design principle: NEVER construct URLs or form POSTs. ALWAYS click page elements.**
|
||||
|
||||
```javascript
|
||||
// SPELLS: Find the spell icon by its onmouseover text containing spell name
|
||||
function Cast(name) {
|
||||
return function() {
|
||||
var spell;
|
||||
if ((spell = document.querySelector(
|
||||
'.bts > div[onclick][onmouseover*="\'' + name + '\'"]'))) {
|
||||
// Use a dummy element to trigger the spell's mouseover+click
|
||||
dummy.setAttribute('onclick', spell.getAttribute('onmouseover'));
|
||||
dummy.click();
|
||||
spell.click();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ITEMS: Find by element ID
|
||||
function Use(id) {
|
||||
return function() {
|
||||
var item;
|
||||
if ((item = document.getElementById('ikey_' + id))) {
|
||||
dummy.setAttribute('onclick', item.getAttribute('onmouseover'));
|
||||
dummy.click();
|
||||
item.click();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ATTACK: Click the monster div directly
|
||||
function TargetMonster(num) {
|
||||
return function() {
|
||||
if (monsters[num] && monsters[num].hasAttribute('onclick'))
|
||||
monsters[num].click();
|
||||
};
|
||||
}
|
||||
|
||||
// TOGGLE: Click checkbox elements by ID
|
||||
function Toggle(name) {
|
||||
return function() {
|
||||
if ((state = document.getElementById('ckey_' + name.toLowerCase())))
|
||||
state.click();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Key insight:** The game's page has HTML elements with onclick/onmouseover handlers.
|
||||
Monsterbation finds these elements and triggers their events. The server processes
|
||||
the resulting HTTP request normally. This is why it's considered "single action"
|
||||
— each click = one server round-trip = one turn.
|
||||
|
||||
### 2.5 Hover System (the killer feature)
|
||||
|
||||
```
|
||||
hoverArea: 6 = which .btmX div within monster triggers hover
|
||||
|
||||
Hover flow:
|
||||
1. Mouse enters monster area → SetTarget(i) called
|
||||
2. If hover enabled → Hover() fires
|
||||
3. Hover() → runs the configured action chain
|
||||
4. Action chain clicks spell/item/attack elements on the page
|
||||
5. The last action clicks the monster → server resolves turn
|
||||
6. Next page loads → Enhance() → parse new state → loop
|
||||
|
||||
Modifier keys:
|
||||
- No modifier: cfg.hoverAction
|
||||
- Shift: cfg.hoverShiftAction (e.g. dark spells)
|
||||
- Ctrl: cfg.hoverCtrlAction (e.g. holy spells)
|
||||
- Alt: cfg.hoverAltAction (e.g. fire spells)
|
||||
|
||||
Hover interrupts:
|
||||
- stopOnSpark (spark of life triggered)
|
||||
- stopOnUsable (consumable becomes available)
|
||||
- stopOnLowHP/MP/SP
|
||||
- stopOnBuffsExpiring (custom regex)
|
||||
- stopOnMiss
|
||||
- Impulse system: inject one-time spell into rotation
|
||||
```
|
||||
|
||||
### 2.6 Spell Rotation System
|
||||
|
||||
```javascript
|
||||
// Strongest: tries actions in order, first one that is available fires
|
||||
Strongest([Cast('Ragnarok'), Cast('Disintegrate'), Cast('Corruption')])
|
||||
|
||||
// This tries Ragnarok first. If the element doesn't exist (cooldown/spell unavailable),
|
||||
// it falls through to Disintegrate, then Corruption.
|
||||
// The action fires by finding the spell icon in the DOM and clicking it.
|
||||
|
||||
// Impulse: injects a one-time action into the rotation
|
||||
// e.g. press I while hovering to cast Imperil once, then resume normal rotation
|
||||
Bind(KEY_I, Impulse(Cast('Imperil')))
|
||||
```
|
||||
|
||||
### 2.7 Keybinding System
|
||||
|
||||
```javascript
|
||||
// Format: Bind(KEY, ON_KEYDOWN, ON_KEYUP)
|
||||
// KEY: string like 'F', 'Digit1', 'Numpad1', 'ShiftLeft', etc.
|
||||
|
||||
// Examples:
|
||||
Bind('KeyR', Strongest([Cast('Ragnarok'), Cast('Disintegrate')]));
|
||||
// Press R → cast strongest dark spell at mouse target
|
||||
|
||||
Bind('KeyH', Any, ToggleHover);
|
||||
// Release H → toggle hover play
|
||||
|
||||
Bind('KeyV', HoverAction(Nothing));
|
||||
// Hold V → attack monster at mouse cursor
|
||||
|
||||
Bind('Digit2', Strongest([ToggleHover, HoverAction(Cast('Imperil'))]));
|
||||
// Press 2 → cast Imperil (even if hovering is stopped)
|
||||
```
|
||||
|
||||
### 2.8 Profile System
|
||||
|
||||
```
|
||||
cfg.name → cfg.persona[0].name → cfg.persona[0].set[0].name
|
||||
├── settings ├── settings
|
||||
├── hoverAction ├── hoverAction
|
||||
├── bind ├── bind
|
||||
└── ... └── ...
|
||||
|
||||
Profiles auto-switch when:
|
||||
- Persona changes (cfg.profileAutoswitch)
|
||||
- Equipment set changes
|
||||
- Isekai mode detected (cfg.isekaiInherit)
|
||||
|
||||
Storage: localStorage['HVmbcfg'] for config, localStorage['HVtrackdrops'] for tracking
|
||||
```
|
||||
|
||||
### 2.9 Out-of-Battle Features (CrunkJuice, the companion script)
|
||||
|
||||
CrunkJuice handles non-combat automation:
|
||||
- ED confirm (auto-confirm that annoying 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
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 3. CORE ARCHITECTURAL PATTERNS (applicable to any new tool)
|
||||
|
||||
### Pattern 1: DOM Detection, Not HTTP
|
||||
```javascript
|
||||
// Detect battle page:
|
||||
if (document.getElementById('textlog') || document.getElementById('riddlemaster')) {
|
||||
// We're in battle
|
||||
}
|
||||
|
||||
// Detect bazaar/character page:
|
||||
if (document.getElementById('mainpane')) {
|
||||
// We're in the main game area
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Click Elements, Don't POST
|
||||
```javascript
|
||||
// WRONG (bot-like, detectable):
|
||||
fetch('?s=BATTLE', {method:'POST', body:'action=attack&target=2'});
|
||||
|
||||
// RIGHT (same as human clicking):
|
||||
document.querySelector('.btm[onclick]').click();
|
||||
document.querySelector('.bts > div[onclick]').click();
|
||||
```
|
||||
|
||||
### Pattern 3: Element Selection by Content
|
||||
```javascript
|
||||
// Find the Imperil spell icon:
|
||||
document.querySelector('.bts > div[onclick][onmouseover*="Imperil"]')
|
||||
|
||||
// Find a specific item by its game-internal ID:
|
||||
document.getElementById('ikey_3') // Item slot 3
|
||||
|
||||
// Find Spirit Stance toggle:
|
||||
document.getElementById('ckey_spirit')
|
||||
```
|
||||
|
||||
### Pattern 4: State from Style Widths
|
||||
```javascript
|
||||
// HP percentage from green bar:
|
||||
var hp_pct = parseInt(
|
||||
document.querySelector('img[src$="green.png"]').style.width
|
||||
) / 414;
|
||||
|
||||
// MP from blue bar:
|
||||
var mp_pct = parseInt(
|
||||
document.querySelector('img[src$="blue.png"]').style.width
|
||||
) / 414;
|
||||
|
||||
// Overcharge from orange bar:
|
||||
var oc_full = parseInt(
|
||||
document.querySelector('img[src$="bar_orange.png"]').style.width
|
||||
) >= 414;
|
||||
```
|
||||
|
||||
### Pattern 5: localStorage for Persistence
|
||||
```javascript
|
||||
// Config storage:
|
||||
localStorage['HVmbcfg'] = JSON.stringify(config);
|
||||
|
||||
// Monster HP database (to show HP numbers):
|
||||
localStorage['HVmonsterData'] = JSON.stringify(monsterData);
|
||||
|
||||
// Drop tracking:
|
||||
localStorage['HVtrackdrops'] = JSON.stringify(droplog);
|
||||
|
||||
// Combat stats:
|
||||
localStorage['HVcombatlog'] = JSON.stringify(combatlog);
|
||||
```
|
||||
|
||||
### Pattern 6: Dummy Element Trick
|
||||
```javascript
|
||||
// The game often requires a specific click sequence:
|
||||
// 1. Click spell icon → triggers mouseover → shows targeting mode
|
||||
// 2. Click monster → actually casts spell
|
||||
// Monsterbation handles this with a dummy element:
|
||||
|
||||
var dummy = document.createElement('div');
|
||||
dummy.setAttribute('onclick', spell.getAttribute('onmouseover'));
|
||||
dummy.click(); // Sets up targeting
|
||||
spell.click(); // Actually selects spell
|
||||
monster.click(); // Fires the spell at target
|
||||
```
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 4. EXISTING TOOL GAPS — WHERE TO IMPROVE
|
||||
|
||||
### Gap 1: No automated Battle Mode Selection
|
||||
Current tools require manually picking Arena/Grindfest/Item World.
|
||||
A tool could:
|
||||
- Parse available arenas, check which ones are uncleared that day
|
||||
- Sort by credit/minute efficiency based on player's clear speed
|
||||
- One-click "do today's best arena"
|
||||
|
||||
### Gap 2: No Smart Mana/SP Management
|
||||
Current hover systems use threshold-based interrupts (stop at X% mana).
|
||||
A smarter approach:
|
||||
- Calculate exact mana cost of next spell (including interference, spirit stance, etc.)
|
||||
- Predict regeneration rate over time
|
||||
- Decide: cast now vs wait 1 tick vs use mana potion
|
||||
- Pre-cast buffs when MP is high, conserve when MP is low
|
||||
|
||||
### Gap 3: No Per-Monster Adaptive Targeting
|
||||
Current tools target whoever the mouse is over, or use "Strongest spell rotation" blindly.
|
||||
A fully informed targeting system would:
|
||||
- Scan all monsters for resistances (from Monster Lab database)
|
||||
- Match elemental weakness to available spells
|
||||
- Prioritize: debuff needs → lowest HP (sweep) → highest threat
|
||||
- Account for monster status effects (don't waste Imperil on already-imperiled target)
|
||||
|
||||
### Gap 4: No Auto-Equipment Optimizer for Battle
|
||||
Current tools don't read equipment loadouts and suggest optimal gear.
|
||||
Could:
|
||||
- Parse equipment stats for current fighting style
|
||||
- Auto-equip best set for the battle mode (Arena vs GF vs IW)
|
||||
- Warn about broken equipment before battle
|
||||
|
||||
### Gap 5: Fragmented Tool Ecosystem
|
||||
Monsterbation (battle) + CrunkJuice (out-of-battle) + HV Utils (shop/shrine) + HV Toolbox (MoogleMail) + jpx (auto-battler rules) = 5+ separate scripts.
|
||||
A unified tool with modular architecture would:
|
||||
- Share state between modules (drops in battle → auto-sell in shop)
|
||||
- Single config/profile system
|
||||
- Consistent keybindings and UI
|
||||
|
||||
### Gap 6: No VPS-Ready Remote Monitoring
|
||||
None of the tools offer headless monitoring. Could:
|
||||
- WebSocket bridge: browser extension sends state to VPS
|
||||
- VPS logs: daily earnings, Hath balance, stamina status
|
||||
- Push notifications: "Your stamina is full" or "Arena reset is ready"
|
||||
|
||||
### Gap 7: Training Queue
|
||||
Current tools handle training manually. Could:
|
||||
- Queue training purchases with credit thresholds
|
||||
- Auto-buy Scavenger/Quartermaster/Archaeologist when credits exceed threshold
|
||||
- Prioritize Ability Boost when leveling
|
||||
|
||||
### Gap 8: Monster Lab Auto-Management
|
||||
CrunkJuice has basic feeding. Could extend to:
|
||||
- Optimal crystal allocation (which monster gets which crystal type)
|
||||
- Morale management (pill when below threshold)
|
||||
- Auto-name/auto-level tracking
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 5. BUILD PLAN — UNIFIED BATTLE ASSISTANT (Tampermonkey)
|
||||
|
||||
### Architecture
|
||||
```
|
||||
hv-unified.user.js
|
||||
├── engine/
|
||||
│ ├── detector.js ← Detect which page we're on (battle/bazaar/monsterlab/etc.)
|
||||
│ ├── parser.js ← DOM→gameState (shared parser for all modules)
|
||||
│ ├── submitter.js ← Action submitter (click elements, dummy trick)
|
||||
│ ├── storage.js ← localStorage wrapper (config, tracking, monster DB)
|
||||
│ └── config.js ← Profile system, keybinding parser
|
||||
├── battle/
|
||||
│ ├── vitals.js ← HP/MP/SP/OC monitoring
|
||||
│ ├── buffs.js ← Buff duration tracking, alert conditions
|
||||
│ ├── cooldowns.js ← Spell/skill/item cooldown display
|
||||
│ ├── monsters.js ← Monster state (HP, status, resistances)
|
||||
│ ├── hover.js ← Hover system (target selection, action execution)
|
||||
│ ├── strategy.js ← Action decision logic (spell rotation, target priority)
|
||||
│ ├── spells.js ← Spell selection (find icons by name, check mana cost)
|
||||
│ ├── skills.js ← Skill selection (overcharge management)
|
||||
│ └── items.js ← Item usage (potions, scrolls, infusions)
|
||||
├── bazaar/
|
||||
│ ├── arena.js ← Arena detection, completion tracking
|
||||
│ ├── shrine.js ← Bulk shrine artifacts
|
||||
│ ├── shop.js ← Auto-sell/salvage low-quality gear
|
||||
│ ├── training.js ← Training queue management
|
||||
│ └── monsterlab.js ← Auto-feeding, crystal allocation
|
||||
├── tracking/
|
||||
│ ├── drops.js ← Drop logging and statistics
|
||||
│ ├── damage.js ← Damage dealt/taken tracking
|
||||
│ ├── proficiency.js ← Proficiency gain tracking
|
||||
│ └── credits.js ← Credit earning rate calculation
|
||||
└── ui/
|
||||
├── quickbar.js ← Extended quickbar with cooldowns
|
||||
├── overlay.js ← Settings panel, profile switcher
|
||||
├── alerts.js ← Visual/audio alerts (low HP, spark, buffs expiring)
|
||||
└── keybind.js ← Keybinding system
|
||||
```
|
||||
|
||||
### Strategy Engine — The Core Innovation
|
||||
|
||||
Rather than Monsterbation's simple "cast all spells in rotation at whatever target",
|
||||
build a **conditional rule engine**:
|
||||
|
||||
```javascript
|
||||
const strategy = {
|
||||
// Priority-ordered rules. First matching rule fires.
|
||||
rules: [
|
||||
{
|
||||
name: 'Emergency heal',
|
||||
condition: () => state.hp < 0.25,
|
||||
action: () => Use('health_potion') || Cast('Full-Cure') || Cast('Cure'),
|
||||
},
|
||||
{
|
||||
name: 'Maintain Imperil',
|
||||
condition: () => !monsterHasEffect(currentTarget, 'imperil'),
|
||||
action: () => Cast('Imperil'),
|
||||
},
|
||||
{
|
||||
name: 'Maintain Weaken',
|
||||
condition: () => !monsterHasEffect(currentTarget, 'weaken') && state.mp > 0.3,
|
||||
action: () => Cast('Weaken'),
|
||||
},
|
||||
{
|
||||
name: 'Maintain buffs',
|
||||
condition: () => !hasBuff('haste') || !hasBuff('shadow_veil'),
|
||||
action: () => castMissingBuffs(),
|
||||
},
|
||||
{
|
||||
name: 'Use Spirit Stance',
|
||||
condition: () => state.oc >= 100 && !spiritStanceActive && state.sp > 0.25,
|
||||
action: () => Toggle('spirit'),
|
||||
},
|
||||
{
|
||||
name: 'Use skill at full OC',
|
||||
condition: () => state.oc >= 200 && !skillOnCooldown('frenzied_blows'),
|
||||
action: () => Skill('Frenzied Blows'),
|
||||
},
|
||||
{
|
||||
name: 'AOE spell',
|
||||
condition: () => aliveMonsters > 1 && canCast('Inferno'),
|
||||
action: () => Cast('Inferno'),
|
||||
},
|
||||
{
|
||||
name: 'Single target: match weakness',
|
||||
condition: () => true, // fallback
|
||||
action: () => castBestSpellAgainst(currentTarget),
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Elemental Weakness Matching
|
||||
|
||||
Build a monster database:
|
||||
```javascript
|
||||
const monsterDB = {
|
||||
// key: monster name pattern
|
||||
'Giant': { weak: 'Cold', resist: 'Fire', hp_scaling: 1.2 },
|
||||
'Dragon': { weak: 'Elec', resist: 'Fire', hp_scaling: 1.5 },
|
||||
// ... populated from Monster Lab scans + community data
|
||||
};
|
||||
|
||||
function castBestSpellAgainst(monster) {
|
||||
const info = findMonsterInDB(monster.name);
|
||||
if (info && info.weak) {
|
||||
return Cast(getHighestTierSpell(info.weak));
|
||||
}
|
||||
// Fallback: use neutral/highest damage spell
|
||||
return Cast('Ragnarok');
|
||||
}
|
||||
```
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 6. RULES COMPLIANCE CHECKLIST
|
||||
|
||||
The script MUST:
|
||||
- [x] Require human input per combat round (hotkey press or mouse hover)
|
||||
- [x] NOT auto-advance between rounds without input
|
||||
- [x] NOT auto-start new battles
|
||||
- [x] NOT auto-feed monsters
|
||||
- [x] NOT auto-solve RiddleMaster
|
||||
- [x] NOT combine multiple actions into one user input
|
||||
- [x] Stop on RiddleMaster detection and alert user
|
||||
- [x] Parse game state only for recommendations, not for triggering actions
|
||||
|
||||
For out-of-battle features:
|
||||
- [ ] Auto-sell/salvage: ALLOWED if user initiates (HV Utils pattern)
|
||||
- [ ] Auto-training queue: GREY AREA — training is manual, queuing probably OK
|
||||
- [ ] Auto-shrine: ALLOWED if user clicks the button (bulk shrine OK)
|
||||
- [ ] Monster feeding: NOT ALLOWED if automatic (must require button press)
|
||||
- [ ] Auto-arena selection: GREY AREA — suggesting which arena is OK, auto-entering is NOT
|
||||
|
||||
=============================================================================
|
||||
|
||||
## 7. NEXT STEPS
|
||||
|
||||
1. **User downloads remaining scripts** from forums (browser → upload to VPS):
|
||||
- jpx from https://forums.e-hentai.org/index.php?showtopic=290507
|
||||
- HV Utils from https://forums.e-hentai.org/index.php?showtopic=211883
|
||||
- HV Toolbox from https://forums.e-hentai.org/index.php?showtopic=209070
|
||||
|
||||
2. **Analyze jpx's ruleset system** — it has the most advanced conditional logic for auto-battle, and understanding its rule format would inform our strategy engine design
|
||||
|
||||
3. **Extract Monster Lab data** — build a JSON database of monster weaknesses from the existing script databases (decondelite's server at nibl.co.uk)
|
||||
|
||||
4. **Build MVP**: start with battle parser + strategy engine + hover system, integrating the best patterns from all existing tools
|
||||
|
||||
5. **Test in a low-level account** before running on your main to verify rules compliance
|
||||
47
references/hvc.js
Normal file
47
references/hvc.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
function api_call(h,g,d){h.open("POST",MAIN_URL+"json");h.setRequestHeader("Content-Type","application/json");h.withCredentials=!0;h.onreadystatechange=d;h.send(JSON.stringify(g))}function api_response(h){if(4==h.readyState)if(200==h.status)if(h=JSON.parse(h.responseText),void 0!=h.login)top.location.href=login_url;else return h;else alert("Server communication failed: "+h.status+" ("+h.responseText+")"),document.location+="";return!1}
|
||||
function number_format(h){return h.toString().replace(/\B(?=(\d{3})+(?!\d))/g," ")}var e=function(h){return document.getElementById(h)};
|
||||
function Common(){function h(a,f,l){a=a.getElementsByTagName("DIV");f=["f2l"+f,"f2r"+f,"f4l"+f,"f4r"+f];var r=["f2l"+l,"f2r"+l,"f4l"+l,"f4r"+l];l="a"==l?"#0030CB":"#5C0D11";for(var n=0;n<a.length;n++)for(var w=0;w<f.length;w++)a[n].className=a[n].className.replace(f[w],r[w]),a[n].style.color=l}this.goto_arena=function(){document.location=MAIN_URL+"?s=Battle&ss=ar"};this.goto_ring=function(){document.location=MAIN_URL+"?s=Battle&ss=rb"};this.goto_grindfest=function(){document.location=MAIN_URL+"?s=Battle&ss=gr"};
|
||||
this.goto_tower=function(){document.location=MAIN_URL+"?s=Battle&ss=tw"};this.findPos=function(a){var f=0,l=0;if(a.offsetParent){do f+=a.offsetLeft,l+=a.offsetTop;while(a=a.offsetParent)}return[f,l]};this.findPosWithScroll=function(a){var f=0,l=0;if(a.offsetParent){do f+=a.offsetLeft+(a.scrollLeft?a.scrollLeft:0),l+=a.offsetTop+(a.scrollTop?a.scrollTop:0);while(a=a.offsetParent)}return[f,l]};this.findScrollOffset=function(a){var f=0,l=0;if(a.offsetParent){do f+=a.scrollLeft?a.scrollLeft:0,l+=a.scrollTop?
|
||||
a.scrollTop:0;while(a=a.offsetParent)}return[f,l]};this.getCursorPosition=function(a){a=a||window.event;var f={x:0,y:0};if(a.pageX||a.pageY)f.x=a.pageX,f.y=a.pageY;else{var l=document.documentElement,r=document.body;f.x=a.clientX+(l.scrollLeft||r.scrollLeft)-(l.clientLeft||0);f.y=a.clientY+(l.scrollTop||r.scrollTop)-(l.clientTop||0)}return f};this.decimalround=function(a,f){return Math.round(a*Math.pow(10,f))/Math.pow(10,f)};this.suppress_popups=!1;this.show_popup_box=function(a,f,l,r,n,w,B,D,H,F){if(!this.suppress_popups){var G=
|
||||
e("popup_box"),J=[0,0],I=[0,0],L=0;void 0!=w&&(L=w.offsetWidth,J=common.findPosWithScroll(w),""!=n&&(n=e(n),I[0]=n.scrollLeft,I[1]=n.scrollTop));G.style.left=("right"==B?J[0]-I[0]+L+a:J[0]-I[0]-a-l)+"px";G.style.top=J[1]-I[1]+f+"px";G.style.width=l+"px";G.style.height=r+"px";G.innerHTML="<div>"+D+"</div><div>"+H+"</div><div>"+F+"</div>";G.style.visibility="visible"}};this.hide_popup_box=function(){e("popup_box").removeAttribute("style")};this.show_itemc_box=function(a,f,l,r,n,w){this.show_popup_box(a,
|
||||
f,398,75,l,r,n,dynjs_itemc[w].n,dynjs_itemc[w].q,"Consumable")};this.show_itemr_box=function(a,f,l,r,n,w,B,D){this.show_popup_box(a,f,398,85,l,r,n,w,B,D)};var g=void 0,d=0,m=0,q=0,u=function(){var a=g.scrollTop;g.scrollTop=0<d?Math.min(g.scrollTop+q,m):Math.max(g.scrollTop-q,m);a!=g.scrollTop?setTimeout(u,1):(g=void 0,m=d=0)};this.scrollpane_up=function(a,f,l){void 0==g&&(g=e(a),d=-1,m=Math.max(0,g.scrollTop-f),q=void 0!=l?1E3:25,u())};this.scrollpane_down=function(a,f,l){void 0==g&&(g=e(a),d=1,m=
|
||||
g.scrollTop+f,q=void 0!=l?1E3:25,u())};this.hookEvent=function(a,f,l){"string"==typeof a&&(a=e(a));null!=a&&(a.addEventListener?("mousewheel"==f&&a.addEventListener("DOMMouseScroll",l,!1),a.addEventListener(f,l,!1)):a.attachEvent&&a.attachEvent("on"+f,l))};this.unhookEvent=function(a,f,l){"string"==typeof a&&(a=e(a));null!=a&&(a.removeEventListener?("mousewheel"==f&&a.removeEventListener("DOMMouseScroll",l,!1),a.removeEventListener(f,l,!1)):a.detachEvent&&a.detachEvent("on"+f,l))};this.cancelEvent=
|
||||
function(a){a=a?a:window.event;a.stopPropagation&&a.stopPropagation();a.preventDefault&&a.preventDefault();a.cancelBubble=!0;a.cancel=!0;return a.returnValue=!1};this.number_format=function(a){x=(a+"").split(".");x1=x[0];x2=1<x.length?"."+x[1]:"";for(a=/(\d+)(\d{3})/;a.test(x1);)x1=x1.replace(a,"$1,$2");return x1+x2};var v=[9,5,10,10,10,10,10,10,9,9,4,4,5,10,11,8,8,8,11,11,4,4,4];this.get_dynamic_digit_string=function(a){a=this.number_format(a);for(var f="",l=0,r=a.length-1;0<=r;r--){var n=","==a.charAt(r)?
|
||||
11:"."==a.charAt(r)?10:"+"==a.charAt(r)?15:":"==a.charAt(r)?22:"-"==a.charAt(r)?16:parseInt(a.charAt(r));f=f+'<div style="float:right; height:12px; width:'+(v[n]+1)+"px; background:transparent url("+IMG_URL+"font/12b.png) 0px -"+12*n+'px"></div>';l+=v[n]}return'<div style="position:relative; display:inline; height:12px; width:'+l+'px">'+f+"</div>"};this.apply_select=function(a){h(a,"b","a");a.style.color="#0030CB"};this.apply_unselect=function(a){h(a,"a","b");a.removeAttribute("style")};var y=void 0;
|
||||
this.text_select=function(a){var f=y!=a;this.text_unselect();f&&(this.apply_select(a),y=a)};this.text_unselect=function(){void 0!=y&&(this.apply_unselect(y),y=void 0)}}var common=new Common;
|
||||
function ItemShop(){function h(){y=!0;1>d||1>m?y=!1:"shop_pane"==g&&m*u>current_credits&&(y=!1);e("accept_button").src=IMG_URL+"shops/accept"+(y?"":"_d")+".png";e("cost_field").value=u;e("sum_field").value=(m*u).toLocaleString("en")}var g=void 0,d=0,m=0,q=void 0,u=0,v=0,y=!1;this.set_item=function(a,f,l,r,n){f==d&&a==g&&(a=void 0,r=l=f=0,n=void 0);g=a;d=f;u=r;q=f&&!n?dynjs_itemc[f].n:n;v=l;this.set_count(0<f?1:0);h()};this.set_count=function(a){m=Math.max(0,Math.min(a,v));e("count_field").value=m;
|
||||
h()};this.increase_count=function(a){this.set_count(1==m&&1<a?a:m+a)};this.read_count=function(){m=Math.max(0,parseInt(e("count_field").value));0<d&&m>v&&this.set_count(v);h()};this.commit_transaction=function(){y&&confirm("Are you sure you wish to "+("shop_pane"==g?"purchase":"sell")+" "+m+' "'+q.replace("'","'")+'" for '+common.number_format(m*u)+" credits ?")&&(e("select_mode").value=g,e("select_item").value=d,e("select_count").value=m,e("shopform").submit())}}
|
||||
function Snowflake(){var h=e("shrine_info"),g=e("shrine_artifact"),d=e("shrine_trophy"),m=e("shrine_collectible"),q=e("shrine_offertext"),u=0,v=0,y=0,a="",f="",l=void 0,r=!1;this.set_shrine_item=function(n,w,B,D){n==u&&(B=w=n=0,D=void 0);u=n;v=w;y=B;l=D;r=!0;n=0;1>u?(e("accept_equip").style.display="none",r=!1,n=1):2E4<=u&&3E4>u?(r=!0,n=2):3E4<=u&&4E4>u?(r=y<=v,n=3):7E4<=u&&8E4>u&&(r=!0,n=4);if(3==n)for(e("accept_equip").style.display="",e("accept_reward").style.display="none",w=e("accept_equip").querySelectorAll(".accept_equip"),
|
||||
B=0;B<w.length;B++)w[B].disabled=r?"":"disabled";else e("accept_equip").style.display="none",e("accept_reward").style.display=1<n?"":"none",e("accept_reward").disabled=r?"":"disabled";h.style.display=1==n?"":"none";g.style.display=2==n?"":"none";d.style.display=3==n?"":"none";m.style.display=4==n?"":"none";q.innerHTML=0==u?"":y>v?"You have "+v+" / "+y+" items required for this offering.":"Offer "+y+"x "+l+" for :"};this.submit_shrine_reward=function(n,w){a=n;f=w;this.commit_transaction()};this.commit_transaction=
|
||||
function(){r&&confirm("Are you sure you wish to offer Snowflake "+(1<y?y+"x":"a")+" "+l.replace("'","'")+" ?")&&(e("select_item").value=u,e("select_reward_type").value=a,e("select_reward_slot").value=f,e("shopform").submit())}}
|
||||
function MoogleMail(){var h=0,g=void 0;this.set_mooglemail_item=function(d,m){if(void 0==m||"1"!=m.getAttribute("data-locked"))d==h&&(d=0),0==d?(common.text_unselect(),h=0):(void 0!=m&&common.text_select(m),h=d,"equip"==g&&this.apply_attachment())};this.set_mooglemail_pane=function(d){this.set_mooglemail_item(0);g=g==d?void 0:d;e("mmail_attachinfo").style.display=void 0==g?"":"none";e("mmail_attachitem").style.display="item"==g?"":"none";e("mmail_attachequip").style.display="equip"==g?"":"none";e("mmail_attachcredits").style.display=
|
||||
"credits"==g?"":"none";e("mmail_attachhath").style.display="hath"==g?"":"none"};this.apply_attachment=function(){if(void 0!=g){var d="equip"==g?1:Math.max(1,parseInt(e("count_"+g).value));0<d&&(e("action").value="attach_add",e("select_item").value=h,e("select_count").value=d,e("select_pane").value=g,e("mailform").submit())}};this.check_apply_attachment=function(d){13==d.keyCode&&this.apply_attachment()};this.mmail_send=function(){var d="";0<attach_count&&(d="You have attached "+attach_count+(1==attach_count?
|
||||
" item":" items")+(0<attach_cod?", and the CoD is set to "+attach_cod+" credits, kupo!":", but you have not set a CoD, kupo! The attachments will be a gift, kupo!"));0<send_cost&&(d+=" Sending it will cost you "+send_cost+" credits, kupo!");confirm(d+" Are you sure you wish to send this message, kupo?")&&(e("action").value="send",e("mailform").submit())};this.mmail_save=function(){e("action").value="save";e("mailform").submit()};this.mmail_discard=function(){confirm("Are you sure you wish to discard this message, kupo?")&&
|
||||
(e("action").value="discard",e("mailform").submit())};this.remove_attachment=function(d){0<mail_state&&0<attach_cod&&!confirm("Removing the attachments will deduct "+attach_cod+" Credits from your account, kupo! Are you sure?")||(e("action").value="attach_remove",e("action_value").value=d,e("mailform").submit())};this.return_mail=function(){confirm("This will return the message to the sender, kupo! Are you sure?")&&(e("action").value="return_message",e("mailform").submit())};this.check_set_cod=function(d){13==
|
||||
d.keyCode&&this.set_cod()};this.set_cod=function(){e("action").value="attach_cod";e("action_value").value=Math.max(0,parseInt(e("newcod").value));e("mailform").submit()}}
|
||||
function Equips(){var h=0,g=void 0;this.set=function(d,m,q,u){var v="undefined"==typeof dynjs_eqstore||"undefined"==typeof dynjs_eqstore[d]?dynjs_equip:dynjs_eqstore;h=d;g=v[d].k;common.show_popup_box(q,u,360,360,m,void 0,"right",v[d].t,v[d].d,"")};this.unset=function(){h=0;g=void 0;common.hide_popup_box()};this.pop_equipwindow=function(){return 0<h?(window.open(MAIN_URL+"equip/"+h+"/"+g,"_pu"+(Math.random()+"").replace(/0\./,""),"toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=450,height=520,left="+
|
||||
(screen.width-450)/2+",top="+(screen.height-520)/2),!0):!1};this.lock=function(d,m){var q=new XMLHttpRequest;api_call(q,{type:"simple",method:"lockequip",uid,token:simple_token,eid:d,lock:"il"==m.className?0:1},function(){var u=api_response(q);if(0!=u&&void 0!=u.eid){m.className=u.locked?"il":"iu";var v=e("e"+d),y=v.getAttribute("data-locked");if(y){if("0"===y&&v.getAttribute("style")&&v.onclick)v.onclick();v.setAttribute("data-locked",u.locked)}}})};document.onkeypress=function(d){d.shiftKey||d.altKey||
|
||||
"c"==String.fromCharCode(window.event?d.keyCode:d.which)&&equips.pop_equipwindow()&&common.cancelEvent(d)}}
|
||||
function Training(){this.start_training=function(m){e("start_train").value=m;e("trainform").submit()};this.cancel_training=function(){e("cancel_train").value=1;e("trainform").submit()};var h=e("train_progbar"),g=e("train_progcnt");if("undefined"!=typeof reload_to)var d=setInterval(function(){var m=Date.now()/1E3+time_skew,q=m<end_time?q=100-100*(end_time-m)/total_time:100,u=Math.floor(q),v=Math.floor(100*(q-u));g.innerHTML=u+"."+(10>v?"0":"")+v;h.style.width=4*q+"px";m>=end_time&&(document.location=
|
||||
reload_to,clearInterval(d))},ticktime)}
|
||||
function ItemSelector(){var h=0,g=!1;this.set_item=function(q){h==q?this.commit_slot(0):h=q};var d=void 0,m=void 0;this.hover_slot=function(q){0!=h&&(void 0!=d&&this.unhover_slot(),common.suppress_popups=!0,d=q,m=q.innerHTML,q.innerHTML=dynjs_itemc[h].n,q.className="ss")};this.unhover_slot=function(){void 0!=d&&(d.innerHTML=m,d.removeAttribute("class"),d=void 0,common.suppress_popups=!1)};this.commit_slot=function(q){g||(g=!0,e("slot").value=q,e("item").value=h,e("selectionform").submit())}}
|
||||
function MonsterLab(){this.create_monster=function(h){e("selected_patk").value=h;e("create_form").submit()}}
|
||||
function Battle(){var h=e("infopane"),g=[void 0,void 0],d=[void 0,void 0],m="log",q=void 0,u=void 0,v=void 0,y=1,a=0,f=void 0,l=e("ta_monster_1"),r=e("ta_monster_2"),n=!1,w=function(b){void 0!=h&&(h.innerHTML=b)};this.set_infopane=function(b){switch(b){case "Attack":var k="Damages a single enemy. Depending on your equipped weapon, this can place certain status effects on the affected monster. To attack, click here, then click your target. Simply clicking an enemy will also perform a normal attack.";break;
|
||||
case "Skillbook":k="Use special skills and magic. To use offensive spells and skills, first click it, then click your target. To use it on yourself, click it twice.";break;case "Items":k="Use various consumable items that can replenish your vitals or augment your power in various ways.";break;case "Spirit":k="Toggle Spirit Channeling.";break;case "Defend":k="Increases your defensive capabilities for the next turn.";break;case "Focus":k="Reduces the chance that your next spell will be resisted. Your defenses and evade chances are lowered for the next turn.";
|
||||
break;default:k="Choose from the Battle Actions highlighted above, and use them to defeat your enemies listed to the right. When all enemies are reduced to zero Health, you win. If your Health reaches zero, you are defeated."}w('<div class="btii">'+b+"</div><div>"+k+"</div>")};this.set_infopane_spell=function(b,k,t,z,C,A){var E="";if(0<z||0<C)E="Requires ",0<z&&(E+=z+" Magic Points"),0<z&&0<C&&(E+=z+" and "),0<C&&(E+=C+" Charge"+(1==C?"":"s")),E+=" to use.";0<A&&(E+=" Cooldown: "+A+" turns.");w('<div class="btii">'+
|
||||
b+'</div><div style="position:relative"><div style="float:left; width:601px"><div style="padding-bottom:3px; padding-right:3px">'+k+'</div><div><span style="font-weight:bold">'+E+'</span></div></div><div style="float:left; width:32px; height:32px; position:relative"><img src="'+IMG_URL+"a/"+t+'.png" style="border:0px; margin:0px; padding:0px; position:absolute; left:3px; top:4px; z-index:3" /><img src="'+IMG_URL+'ab/b.png" style="border:0px; margin:0px; padding:0px; position:absolute; left:-5px; top:-4px; z-index:3" /></div></div>')};
|
||||
this.set_infopane_effect=function(b,k,t){w('<div class="btii">'+b+'</div><div style="padding-bottom:3px">'+k+'</div><div><span style="font-weight:bold">'+("autocast"==t?"Expires if magic is depleted to below 10%":"permanent"==t?"Permanent until triggered":"decaying"==t?"Decays by 20% per turn":"Expires in "+t+" turn"+(1==t?"":"s"))+".</span></div>")};this.set_infopane_item=function(b){w('<div class="btii">'+dynjs_itemc[b].n+'</div><div style="padding-bottom:3px">'+dynjs_itemc[b].q+"</div>")};this.lock_action=
|
||||
function(b,k,t,z){if(!n)if(d[k]==b&&"skill"!=t?(g[k]=void 0,d[k]=void 0):(g[k]=h.innerHTML,d[k]=b),0==k&&(g[1]=void 0,d[1]=void 0),1==k)q!=t&&(this.clear_actions(),q=t,b.src=IMG_URL+"battle/"+q+"_s.png",this.set_mode(t)),this.set_selected_subaction(b,z);else switch(q==t?"skill"==t?this.set_selected_subaction(void 0):q=void 0:(this.clear_actions(),q=t),b.src=IMG_URL+"battle/"+t+"_"+(void 0==q?"n":"s")+".png",this.set_mode(t),t){case "attack":this.toggle_default_pane();break;case "skill":this.toggle_magic_pane();
|
||||
break;case "items":this.toggle_item_pane();break;default:this.touch_and_go()}};this.clear_actions=function(){if(void 0!=q){if("spirit"!=q){var b="magic"==q?"skill":q;e("ckey_"+b).src=IMG_URL+"battle/"+b+"_n.png"}q=void 0}};this.clear_infopane=function(){void 0!=g[1]?w(g[1]):void 0!=g[0]?w(g[0]):this.set_infopane("Battle Time")};var B=function(b){e("pane_"+b).style.display="none";m=void 0};this.toggle_pane=function(b){b==m?this.toggle_default_pane():(B(m),e("pane_"+b).style.display="",m=b)};this.toggle_default_pane=
|
||||
function(){"log"!=m&&(B(m),e("pane_log").style.display="",m=m="log")};this.toggle_magic_pane=function(){"skill"==m?this.toggle_pane("magic"):"magic"==m?this.toggle_pane("skill"):this.toggle_pane(default_magic_pane)};this.toggle_skill_pane=function(){this.toggle_pane("skill")};this.toggle_item_pane=function(){this.toggle_pane("item")};var D="attack",H=0,F=0;this.set_mode=function(b){D=D==b&&"magic"!=b?"attack":b};this.reset_skill=function(){F=H=0;this.set_selected_subaction(void 0)};this.set_hostile_skill=
|
||||
function(b){F=F==b?0:b};this.set_friendly_skill=function(b){F==b?(H=0,this.touch_and_go()):F=b};this.hover_target=function(b){if(void 0==v){var k=common.findPosWithScroll(b),t=common.findPosWithScroll(e("battle_right"));k[0]-=t[0];k[1]-=t[1];l.style.left=k[0]-7+"px";l.style.top=k[1]+b.offsetHeight/2-3+"px";r.style.left=k[0]+b.offsetWidth+2+"px";r.style.top=k[1]+b.offsetHeight/2-3+"px";l.style.visibility="visible";r.style.visibility="visible"}};this.unhover_target=function(){void 0==v&&(l.removeAttribute("style"),
|
||||
r.removeAttribute("style"))};this.commit_target=function(b){n||void 0!=v||(H=v=b,this.touch_and_go())};var G=void 0,J=void 0,I=void 0;this.touch_and_go=function(){n||void 0!=f||(G=D,J=H,I=F,f=new XMLHttpRequest,api_call(f,{type:"battle",method:"action",token:battle_token,mode:D,target:H,skill:F},this.process_action))};this.recast=function(){n||void 0==G||(D=G,H=J,F=I,this.touch_and_go())};for(var L=0,O=!1,M="pane_completion pane_effects pane_action pane_vitals pane_quickbar table_skills table_magic pane_item pane_monster".split(" "),
|
||||
Q=[],P=0;P<M.length;P++){var R=M[P];Q[R]=e(R)}this.process_action=function(){var b=api_response(f);if(0!=b){if(void 0!=b.error)this.battle_continue();else if(void 0!=b.reload)this.battle_continue();else{for(var k=0;k<M.length;k++){var t=M[k];void 0!=b[t]&&(Q[t].innerHTML=b[t])}has_debug&&(e("debugpane").innerHTML=b.debugpane);if(void 0!=b.textlog){t=e("textlog");t.insertRow(0).insertCell(0).className="tls";var z=b.textlog.length;for(k=0;k<z;k++){var C=t.insertRow(0).insertCell(0);C.className="tl"+
|
||||
(void 0!=b.textlog[k].c?b.textlog[k].c:"");C.innerHTML=b.textlog[k].t}for(L+=z;100<L;)t.deleteRow(-1),--L}void 0==b.pane_completion?(e("pane_completion").innerHTML="",n=!1):n=!0;e("expbar").style.width=b.exp+"px";1234==b.exp?(O=!0,e("expbar").src=IMG_URL+"bar_yellow.png"):O&&(O=!1,e("expbar").src=IMG_URL+"bar_blue.png");do_healthflash=b.healthflash;v=void 0;battle.unhover_target();battle.reset_skill();battle.set_mode("attack");a=1;l.style.opacity=a;r.style.opacity=a;battle.toggle_default_pane();g[0]=
|
||||
void 0;g[1]=void 0;battle.clear_infopane();battle.clear_actions()}f=void 0}};var S=!1;this.battle_continue=function(){S||(S=!0,document.location+="")};this.set_selected_subaction=function(b,k){void 0==b?(u=void 0,common.text_unselect()):u==k?this.set_selected_subaction(void 0):common.text_select(b)};var N=0;this.start_flash_loop=function(){setInterval(function(){a=common.decimalround(Math.min(1,Math.max(0,a+.07*y)),2);if(0==a||1==a)y*=-1;void 0!=v&&(l.style.opacity=a,r.style.opacity=a);do_healthflash?
|
||||
(N=Math.floor(80+150*a),e(vital_prefix+"vbh").style.backgroundColor="rgb("+N+",50,50)"):0!=N&&(e(vital_prefix+"vbh").style.backgroundColor="",N=0);for(var b=0,k;k=e("effect_expire_"+ ++b);)k.style.opacity=.8-a/2},20)};document.onkeydown=function(b){b=b||window.event;if(b.target)var k=b.target;else b.srcElement&&(k=b.srcElement);3==k.nodeType&&(k=k.parentNode);if("INPUT"!=k.tagName&&"TEXTAREA"!=k.tagName){k=b.keyCode?b.keyCode:b.which;var t=String.fromCharCode(k),z=void 0,C=void 0,A=-1;switch(k){case 48:case 96:A=
|
||||
0;break;case 49:case 97:A=1;break;case 50:case 98:A=2;break;case 51:case 99:A=3;break;case 52:case 100:A=4;break;case 53:case 101:A=5;break;case 54:case 102:A=6;break;case 55:case 103:A=7;break;case 56:case 104:A=8;break;case 57:case 105:A=9}var E=!1;if(b.altKey){if(0<=A&&10>A){0==A&&(A=10);E=!0;var K=e("qb"+A);K&&(K.onmouseover(),K.onclick())}}else{if(13==k||32==k)z="btcp";else if(-1<A)z="mkey_"+A;else if(112<=k&&123>=k||"G"==t||"P"==t)"item"!=m&&(z="ckey_items"),C="G"==t||"P"==t?"ikey_p":b.ctrlKey?
|
||||
"ikey_n"+(k-111):b.shiftKey?"ikey_s"+(k-111):"ikey_"+(k-111);else if(!b.ctrlKey&&!b.shiftKey)switch(t){case "Q":z="ckey_attack";break;case "W":z="ckey_skill";break;case "E":z="ckey_items";break;case "S":z="ckey_spirit";break;case "D":z="ckey_defend";break;case "F":z="ckey_focus";break;case "R":z="recast"}if(z||C)E=!0;if(z)if("recast"==z)battle.recast();else if(K=e(z))K.onclick();if(C&&(K=e(C)))K.onclick()}if(E)return b.cancelBubble=!0,b.returnValue=!1,b.stopPropagation&&(b.stopPropagation(),b.preventDefault()),
|
||||
!1}};this.clear_infopane();this.start_flash_loop();this.set_infopane("Battle Time")}function at_show_aux(h,g){h=e(h);g=e(g);for(var d="x"==g.at_position?h.offsetWidth+0:0;h;h=h.offsetParent)d+=h.offsetLeft;g.style.position="absolute";g.style.top="27px";g.style.left=d+"px";g.style.visibility="visible"}function at_show(){p=e(this.at_parent);c=e(this.at_child);at_show_aux(p.id,c.id)}function at_hide(){c=e(this.at_child);e(c.id).style.visibility="hidden"}
|
||||
function at_click(){p=e(this.at_parent);c=e(this.at_child);"visible"!=c.style.visibility?at_show_aux(p.id,c.id):c.style.visibility="hidden";return!1}
|
||||
function at_attach(h,g,d,m,q){p=e(h);c=e(g);p.at_parent=p.id;c.at_parent=p.id;p.at_child=c.id;c.at_child=c.id;p.at_position=m;c.at_position=m;c.style.position="absolute";c.style.visibility="hidden";switch(d){case "click":p.onclick=at_click;p.onmouseout=at_hide;c.onmouseover=at_show;c.onmouseout=at_hide;break;case "hover":p.onmouseover=at_show,p.onmouseout=at_hide,c.onmouseover=at_show,c.onmouseout=at_hide}};
|
||||
37
references/hveqc.js
Normal file
37
references/hveqc.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
function get_eqdata(a){if("undefined"!=typeof dynjs_eqstore&&"undefined"!=typeof dynjs_eqstore[a])return dynjs_eqstore[a];if("undefined"!=typeof dynjs_equip&&"undefined"!=typeof dynjs_equip[a])return dynjs_equip[a]}
|
||||
function rehover_last_checked(){if(0<last_checked_eqid){if(!e("e"+last_checked_eqid).checked)for(i=last_checked_eqid=0;i<equipform.elements.length;i++)if("eqids[]"==equipform.elements[i].name&&equipform.elements[i].checked){last_checked_eqid=equipform.elements[i].value;break}0<last_checked_eqid&&hover_equip(last_checked_eqid)}}
|
||||
function hover_equip(a){if("undefined"!=typeof last_hover_eqid){last_hover_eqid||(infotext=equipinfo.innerHTML);var c=get_eqdata(a);if("undefined"!=typeof c){curr_hover_eqid=last_hover_eqid=a;last_hover_key=c.k;if("undefined"!=typeof showequipped_eqid)equipinfo.innerHTML=showequipped_eqid==a?"":'<div class="showequip"><div></div><div>'+c.d+"</div></div>";else{var b=void 0;const g=new URLSearchParams(window.location.search);g.has("filter")&&(b=g.get("filter"));b="purchase"!=title?"?s=Bazaar&ss=am&screen=modify"+
|
||||
(b?"&filter="+b:"")+"&eqids[]="+a:null;equipinfo.innerHTML="undefined"==typeof c.n?'<div class="showequip"><div>'+(b?'<a href="'+b+'">'+c.t+"</a>":c.t)+"</div><div>"+c.d+"</div></div>":'<div class="showequip"><div><div>'+(b?'<a href="'+b+'">'+c.t+"</a>":c.t)+"</div><div>("+c.n+")</div></div><div>"+c.d+"</div></div>"}update_iteminfo()}if("sacrifice"==title&&eqitems[a]){c=0;for(f in eqitems[a].s)c+=eqitems[a].s[f].boost;var f='<p style="font-weight:normal">The selected equipment will be sacrified, and fused with your:</p><p><a href="'+
|
||||
backurl+'">'+fusename+'</a></p><p style="font-weight:normal">This has the following effects and base costs:</p>';f+="<p>+"+c+" Base Stat Rolls</p>";for(let g in eqitems[a].m)if(60201>g||60239<g)f+="<p>"+eqitems[a].m[g]+"x "+itemdata[g].n+" ("+itemdata[g].c+")</p>";f+="<p"+(eqitems[a].c>credit_balance?' class="eqred"':"")+">"+number_format(eqitems[a].c)+" Credits</p>";e("itemlist").innerHTML=f}}}function unhover_equip(a){curr_hover_eqid==a&&(curr_hover_eqid=0,update_iteminfo())}
|
||||
function reset_infopane(){"undefined"!=typeof infotext&&(curr_hover_eqid&&unhover_equip(curr_hover_eqid),void 0!=hoverow&&(hoverow.removeAttribute("data-hover"),hoverow=void 0),equipinfo.innerHTML=infotext,itemlist.innerHTML="")}
|
||||
function update_iteminfo(){if("undefined"!=typeof eqitems){var a=show_items=show_credits=!1,c=single_header=void 0;"repair"==title&&(show_items=!0,a=!document.getElementById("replace_charms").checked,c="Total Repair Cost");"salvage"==title&&(show_items=!0,show_credits=document.getElementById("sell_salvage").checked,eqcredits.style.display=show_credits?"":"none",c="Total Salvage");"itemworld"==title&&(show_items=!0,c="Required Items");"soulbind"==title&&(show_items=!0,c="Required Items");if("sell"==
|
||||
title||"purchase"==title)show_credits=!0;confirm_info=void 0;var b=[],f=[],g=0;for(i=0;i<equipform.elements.length;i++)"eqids[]"==equipform.elements[i].name&&equipform.elements[i].checked&&(eqid=equipform.elements[i].value,show_items&&"undefined"!=typeof eqitems[eqid]&&Object.keys(eqitems[eqid].m).forEach(d=>{let l;b[d]=(null!=(l=b[d])?l:0)+eqitems[eqid].m[d]}),show_credits&&(g+=eqitems[eqid].c),"undefined"!=typeof eqitems[eqid].p?(confirm_info=eqitems[eqid].p,confirm_info.eqname=eqitems[eqid].t):
|
||||
"sacrifice"==title&&(confirm_info=eqitems[eqid]));show_items&&0<curr_hover_eqid&&"undefined"!=typeof eqitems[curr_hover_eqid]&&Object.keys(eqitems[curr_hover_eqid].m).forEach(d=>{void 0==b[d]&&(b[d]=0);f[d]=eqitems[curr_hover_eqid].m[d]});var h=[],k=0<curr_hover_eqid&&document.getElementById("e"+curr_hover_eqid).checked;block_submit=!1;if(0<b.length){h.push('<tr><th colspan="3">'+c+":</th></tr>");for(let d in b)a&&61900<=d&&64999>=d||(c=("repair"==title||"soulbind"==title)&&itemdata[d].c<b[d]||"itemworld"==
|
||||
title&&itemdata[d].c<(curr_hover_eqid?f[d]:b[d]),multiselect?h.push("<tr"+(c?' style="color:red"':"")+"><td>"+b[d]+"x</td><td>"+itemdata[d].n+"</td><td>"+(hide_delta||k||!curr_hover_eqid||void 0==f[d]?"":" ("+(k?"-":"+")+f[d]+")")+"</td></tr>"):h.push("<tr"+(c?' style="color:red"':"")+"><td>"+(curr_hover_eqid?f[d]:b[d])+"x "+itemdata[d].n+"</td></tr>"),c&&(block_submit=!0))}show_items&&(itemlist.innerHTML=0<h.length?"<table>"+h.join("")+"</table>":"");show_credits&&(eqcrsum.innerHTML=g+" Credits",
|
||||
eqcrsum.style.color="purchase"==title&&credit_balance<g?"red":"",eqcrdiff.innerHTML=curr_hover_eqid&&!k?" ("+(k?"-":"+")+eqitems[curr_hover_eqid].c+")":"")}}let unprotect_eqid=0,unprotect_rowelem=void 0;
|
||||
function confirm_unprotect(a,c,b,f){e("e"+c).disabled&&(unprotect_eqid=c,unprotect_rowelem=a,e("confirm_body").innerHTML="<p>Are you sure you want to make this protected equipment selectable?</p><p><strong>"+b+"</strong></p>"+(f?"<p>Obtained: <strong>"+f+"</strong></p>":"")+'<button id="confirm_button" type="button" onclick="unprotect_equip()">Confirm Select</button>',confirm_open())}
|
||||
function unprotect_equip(){unprotect_eqid&&(unprotect_rowelem.removeAttribute("data-eqprotect"),++selectable_count,update_multiselector(),e("e"+unprotect_eqid).disabled=!1,select_equip(unprotect_eqid,void 0,unprotect_rowelem),unprotect_eqid=0);confirm_close()}
|
||||
function select_equip(a,c,b){if(e("e"+a).disabled)return!1;if(!multiselect)for(i=0;i<equipform.elements.length;i++)"eqids[]"==equipform.elements[i].name&&equipform.elements[i].checked&&(equipform.elements[i].checked=!1);last_hover_eqid!=a&&void 0!=b&&(hide_delta=!0,hover_equip(a),void 0!=hoverow&&hoverow.removeAttribute("data-hover"),hoverow=b,hoverow.setAttribute("data-hover",1));b=document.getElementById("e"+a);const f=b.checked?!1:"checked";if(multiselect&&void 0!=c&&c.shiftKey&&0<last_clicked_eqid&&
|
||||
a!=last_clicked_eqid&&f==last_statechange)for(c=!1,i=0;i<equipform.elements.length;i++)if("eqids[]"==equipform.elements[i].name&&!equipform.elements[i].disabled){let g=equipform.elements[i].value==a||equipform.elements[i].value==last_clicked_eqid;!c&&g&&(c=!0,g=!1);c&&(equipform.elements[i].checked=f);c&&g&&(c=!1)}b.checked=f;last_clicked_eqid=a;(last_statechange=f)&&(last_checked_eqid=a);update_selected_count();return!1}let selected_count=0,selected_soulbound=0,selected_rarity=[];
|
||||
function update_selected_count(){selected_soulbound=selected_count=0;selected_rarity=[];for(i=0;i<equipform.elements.length;i++)if("eqids[]"==equipform.elements[i].name&&equipform.elements[i].checked){++selected_count;let a=get_eqdata(parseInt(equipform.elements[i].value));if(void 0!=a){let c;selected_rarity[a.q]=(null!=(c=selected_rarity[a.q])?c:0)+1;let b;null!=(b=a.s)&&b&&++selected_soulbound}}1>selected_count&&(last_clicked_eqid=0);multiselect&&update_multiselector();update_iteminfo();document.getElementById("equipsubmit").disabled=
|
||||
!block_submit&&0<selected_count?"":"disabled"}function update_multiselector(){multiselect&&(e("equipcount").innerHTML='<input type="checkbox" onclick="select_all()"'+(selected_count<selectable_count?"":'checked="checked"')+" /><span></span> Selected "+selected_count+" of "+selectable_count+" matching equipment available to "+title)}
|
||||
function select_all(){last_clicked_eqid=0;let a=selected_count<selectable_count?"checked":!1;for(i=0;i<equipform.elements.length;i++)"eqids[]"!=equipform.elements[i].name||equipform.elements[i].disabled||(equipform.elements[i].checked=a);update_selected_count()}
|
||||
function confirm_action(a,c,b,f,g){if(!a.disabled){a="<div>"+b.replace("%SELECTCOUNT%","<strong>"+selected_count+"</strong>")+"</div>";b=!1;if(confirm_info&&(console.log(JSON.stringify(confirm_info,null,2)),"itemworld"==title&&(a+="\n<p><strong>"+confirm_info.eqname+'</strong></p>\n<table id="iwinfo">\n\t<tr><td>World Level:</td><td>'+confirm_info.iwlvl+"</td></tr>\n\t<tr><td>Battle Rounds:</td><td>"+confirm_info.rounds+"</td></tr>\n\t<tr><td>Monster LVL:</td><td>"+confirm_info.monsterlevel+"</td></tr>\n\t<tr><td>Difficulty:</td><td>"+
|
||||
confirm_info.diffname+(0<confirm_info.diffboost?" +"+confirm_info.diffboost+"%":"")+"</td></tr>\n\t<tr><td>Entry Cost:</td><td>"+confirm_info.req_worldseeds+" World Seed"+(1==confirm_info.req_worldseeds?"":"s")+" ("+confirm_info.has_worldseeds+")</td></tr>\n</table>"),"sacrifice"==title)){a+="\n<p><strong>"+confirm_info.t+'</strong></p>\n<table id="fuseinfo">';let k=[];for(let d in confirm_info.s){let l=confirm_info.s[d].bndid,m=itemdata[l].c<confirm_info.m[l];k.push("<tr><td>"+confirm_info.s[d].label+
|
||||
"</td><td>"+confirm_info.s[d].value+"</td><td>+"+confirm_info.s[d].boost+"</td><td"+(m?' class="eqred"':"")+">"+confirm_info.m[l]+"x "+itemdata[l].n+" ("+itemdata[l].c+")</td></tr>");m&&(b=!0)}a+=k.join("");a+="\n</table>";for(let d in confirm_info.m)if(60201>d||60239<d)a+="<p>"+confirm_info.m[d]+"x "+itemdata[d].n+" ("+itemdata[d].c+")</p>";a+="<p"+(confirm_info.c>credit_balance?' class="eqred"':"")+">"+number_format(confirm_info.c)+" Credits</p>";credit_balance<confirm_info.c&&(b=!0)}if(f){var h;
|
||||
f=null!=(h=selected_rarity[7])?h:0;let k,d;h=(null!=(k=selected_rarity[8])?k:0)+(null!=(d=selected_rarity[9])?d:0);multiselect?(0<selected_soulbound&&(a+="<p><strong>You have selected "+(1<selected_soulbound?selected_soulbound:"a")+" SOULBOUND equipment.</strong></p>"),0<f&&(a+="<p><strong>You have selected "+(1<f?f:"a")+" LEGENDARY equipment.</strong></p>"),0<h&&(a+="<p><strong>You have selected "+(1<h?h:"a")+" PEERLESS equipment.</strong></p>")):0<selected_soulbound+f+h&&(a+="<p><strong>You have selected a "+
|
||||
(0<selected_soulbound?"SOULBOUND ":"")+(0<f?"LEGENDARY ":"")+(0<h?"PEERLESS ":"")+"equipment.</strong></p>");a=(b?a+"<p>You are missing some required resources.</p>":a+"<p>Check both safety boxes to continue.</p>")+('\n<table>\n\t<tr>\n\t\t<td><label class="lc"><input id="cfs1" type="checkbox" onchange="confirm_safety()"'+(b?' disabled="disabled"':"")+'><span></span></label></td>\n\t\t<td><input id="confirm_button" type="submit" value="'+c+'" disabled="disabled"'+(g?' formaction="'+g+'"':"")+' /></td>\n\t\t<td><label class="lc"><input id="cfs2" type="checkbox" onchange="confirm_safety()"'+
|
||||
(b?' disabled="disabled"':"")+"><span></span></label></td>\n\t</tr>\n</table>")}else a+='<input id="confirm_button" id="equipsubmit" type="submit" value="'+c+'"'+(g?' formaction="'+g+'"':"")+(b?' disabled="disabled"':"")+" />";e("confirm_body").innerHTML=a;confirm_open()}}function confirm_safety(){e("confirm_button").disabled=e("cfs1").checked&&e("cfs2").checked?"":"disabled"}
|
||||
function recalc_cp(a){var c=parseInt(a.getAttribute("data-cp"));"charmtype"==a.name?(sel_cp_charm=c,sel_charmtype=parseInt(a.value),charmname=a.getAttribute("data-name"),disabled_charm=a.getAttribute("data-disabled"),charm_stats[sel_charmtype]&&(e("eqstats").innerHTML=charm_stats[sel_charmtype])):(sel_cp_pouch=c,sel_pouchtype=parseInt(a.value),pouchname=a.getAttribute("data-name"),disabled_pouch=a.getAttribute("data-disabled"));if(a=sel_charmtype&&(disabled_charm||disabled_pouch)){var b;c=null!=(b=
|
||||
disabled_charm)?b:disabled_pouch;e("cdreason").innerHTML="("+c+")"}else e("cdreason").innerHTML=" ";b=exl_cp+sel_cp_charm+sel_cp_pouch;e("cpreadout").innerHTML="Charm Points: "+b+" / "+max_cp;e("cpreadout").style.color=b>max_cp?"#FF0000":"";can_submit=!a&&(0==sel_charmtype||b<=max_cp)&&(cur_charm_worn||cur_charm_broken||cur_pouch_broken||cur_charmtype!=sel_charmtype||0<sel_charmtype&&cur_pouchtype!=sel_pouchtype);e("setcharm").disabled=can_submit?"":"disabled";!a&&b>max_cp&&(e("cdreason").innerHTML=
|
||||
"(Insufficient Charm Points)");b="Attach Charm";!sel_charmtype&&0<cur_charmtype?b="Destroy Charm":sel_charmtype&&sel_charmtype==cur_charmtype&&(cur_charm_worn||cur_charm_broken?b="Replace Charm":cur_pouchtype!=sel_pouchtype?b="Replace Pouch":cur_pouch_broken&&(b="Repair Pouch"));e("setcharm").innerHTML=b}
|
||||
function pop_setcharm(){if(can_submit){let b=[];if(cur_charm_worn||cur_charm_broken||cur_charmtype!=sel_charmtype)for(var a in charm_mats[sel_charmtype])b.push("<tr><td>"+charm_mats[sel_charmtype][a].c+"x</td><td>"+charm_mats[sel_charmtype][a].n+"</td></tr>");if((!cur_charm_broken||0<sel_pouchtype)&&(cur_pouch_broken||cur_pouchtype!=sel_pouchtype))for(var c in pouch_mats[sel_pouchtype])b.push("<tr><td>"+pouch_mats[sel_pouchtype][c].c+"x</td><td>"+pouch_mats[sel_pouchtype][c].n+"</td></tr>");a=cur_charmtype?
|
||||
"Replace":"Attach";c="attach a new charm";sel_charmtype?(cur_charmtype==sel_charmtype&&(c=cur_charm_worn||cur_charm_broken?"replace the "+(cur_charm_broken?"torn":"worn")+" charm":cur_pouch_broken?"replace the destroyed pouch":"replace the intact pouch",a="Replace"),e("confirm_body").innerHTML="<p>Are you sure you want to "+c+" in <strong>Slot "+charmslot+"</strong>:</p><p><strong>"+charmname+"</strong> with a <strong>"+pouchname+"</strong></p>",0<b.length&&(e("confirm_body").innerHTML+='<p>by spending the following materials:</p><table id="charmats">'+
|
||||
b.join("")+"</table>")):(e("confirm_body").innerHTML="<p>Are you sure you want to <strong>DESTROY</strong> the charm and pouch in <strong>Slot "+charmslot+"</strong>?</p>",a="Destroy");0<sel_charmtype&&(!cur_charmtype||cur_charm_worn||cur_charm_broken||cur_charmtype==sel_charmtype||(e("confirm_body").innerHTML+="<p>The existing charm will be <strong>DESTROYED</strong>.</p>"),cur_pouch_broken||(cur_pouchtype!=sel_pouchtype?0<cur_charmtype&&(e("confirm_body").innerHTML+="<p>The existing pouch will be <strong>DESTROYED</strong>.</p>"):
|
||||
0<sel_pouchtype&&(e("confirm_body").innerHTML+="<p>The existing pouch will be reused.</p>")));e("confirm_body").innerHTML+='<button id="confirm_button" type="submit" name="action" value="setcharm">Confirm '+a+"</button>";confirm_open()}}let is_confirm_open=!1,is_confirm_text=!1;
|
||||
function pop_rename(a,c){is_confirm_text=!0;e("confirm_body").innerHTML="<p>Enter a new customized name for your</p><p><strong>"+a+'</strong></p><input name="eqname" type="text" maxlength="50" placeholder="'+a+'" value="'+c+'" /><p>Enter a blank name to revert to the default name. Customized names are always removed if the equipment is sold or attached to a MoogleMail.</p><button id="confirm_button" type="submit" name="action" value="rename">Rename Equipment</button>';confirm_open()}
|
||||
function pop_upgrade(){e("confirm_body").innerHTML='<p>Are you sure you want to spend the requisite materials and credits to upgrade this equipment? Credits and Cores cannot be refunded.</p><table class="upgrmats">'+e("upgrmats").innerHTML+'</table><button id="confirm_button" type="submit" name="action" value="upgrade">Confirm Upgrade</button>';confirm_open()}
|
||||
function pop_unequip(){e("confirm_body").innerHTML='<p>Are you sure you want to force unequip this item from all equipment sets in all personas? This may also unequip other gear that depends on it.</p><button id="confirm_button" type="submit" name="action" value="unequip">Confirm Unequip</button>';confirm_open()}
|
||||
function pop_equipwindow(){"undefined"!=typeof last_hover_eqid&&0<last_hover_eqid&&window.open(MAIN_URL+"equip/"+last_hover_eqid+"/"+last_hover_key,"_pu"+(Math.random()+"").replace(/0\./,""),"toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=450,height=520,left="+(screen.width-450)/2+",top="+(screen.height-520)/2)}function confirm_open(){is_confirm_open=!0;e("confirm_outer").style.visibility=""}
|
||||
function confirm_close(){is_confirm_text=is_confirm_open=!1;e("confirm_outer").style.visibility="hidden"}
|
||||
document.onkeydown=function(a){a.shiftKey||a.altKey||(is_confirm_open?(("Enter"==a.key||!is_confirm_text&&" "==a.key)&&e("confirm_button").click(),"Escape"==a.key&&confirm_close()):("c"==a.key&&(pop_equipwindow(),common.cancelEvent(a)),"x"==a.key&&"purchase"!=title&&last_hover_eqid&&(a=(new URLSearchParams(window.location.search)).get("filter"),document.location="?s=Bazaar&ss=am&screen=modify"+(a?"&filter="+a:"")+"&eqids[]="+last_hover_eqid)))};
|
||||
689
references/jpx-analysis.md
Normal file
689
references/jpx-analysis.md
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
# Deep-Structural Analysis of jpx HentaiVerse Auto-Battler Userscript
|
||||
|
||||
**File:** `/home/gabogg/Downloads/jpx20260706.txt`
|
||||
**Version:** 2026.07.06
|
||||
**Size:** 270,752 bytes, 6,215 lines
|
||||
**Language:** Vanilla JavaScript (no framework), single-file userscript
|
||||
|
||||
---
|
||||
|
||||
## 1. ARCHITECTURE
|
||||
|
||||
### 1.1 Initialization Flow
|
||||
|
||||
The entire script is a single-file userscript with a declarative bootstrap:
|
||||
|
||||
1. **Userscript Header** (lines 1-11): `@run-at document-end`, matches `*.hentaiverse.org/*`, excludes `/equip/*` and `/isekai/equip/*`. No GM_ grants needed — works purely via DOM.
|
||||
|
||||
2. **Module Bootstrap** (lines 463-465): Three singleton-style modules are immediately invoked:
|
||||
- `jpxPanelManager()` — creates/configures the control widget overlay
|
||||
- `jpxMarket()` — market price fetcher/cacher
|
||||
- `jpxUtils()` — utility library
|
||||
|
||||
Each follows the pattern `function jpxThing() { const ns = jpxThing; if (ns._init) return ns; ... ns._init = true; return ns; }` — single-init guard via `_init` flag on the function object itself.
|
||||
|
||||
3. **Entry Point** (line 6215): `initDo()` is called at file's end.
|
||||
|
||||
4. **`initDo()`** (line 1242): The router function that:
|
||||
- Injects CSS via `<style id="jpx">`
|
||||
- Registers `beforeunload` handler (`storeTmp`) for crash-safe state persistence
|
||||
- Registers `pointerdown` handler to dismiss multi-select popup panels
|
||||
- Registers `keydown` handler with throttled `actionManager` (75ms throttle)
|
||||
- Initializes I18N (`initDoI18n`)
|
||||
- **Page Detection** via `document.querySelector('#textlog')`:
|
||||
- **If `#textlog` exists AND `doInitDoBattle` is false** → `initDoBattle()` — we're in a battle
|
||||
- **If `#riddlemaster` exists** → `riddleRecorder()` — RiddleMaster encounter
|
||||
- **If no `#textlog` (Lobby)** → reads player info (level, stamina, difficulty, persona, spell damage bonus) from DOM selectors and stores in localStorage
|
||||
|
||||
### 1.2 Hooking Into the Game
|
||||
|
||||
The script hooks into the game at three levels:
|
||||
|
||||
1. **MutationObserver on `#textlog`** (line 1541): A `MutationObserver` watches `log.firstChild` for `childList` changes. Every time new battle log text is added, `preProcessLog()` fires (throttled at 200ms, trailing). This is the primary event loop.
|
||||
|
||||
2. **DOM event listeners**:
|
||||
- `keydown` on `document` (capture phase, line 1255) — for hotkeys and key-bound battle actions
|
||||
- `pointerdown` on `document` (line 1249) — for dismissing multi-select panels
|
||||
- `mousemove` on `window` (line 1366) — sets `jpxPanelManager.ready = true`
|
||||
- `DOMContentLoaded` for `reDoBattle()` on round transitions (line 1369)
|
||||
- `beforeunload` on `window` (line 1248) — calls `storeTmp()` to persist state
|
||||
|
||||
3. **Custom AJAX round advancement** (lines 1623-1716): When `ajaxRound` is enabled, the script intercepts the "Continue" button (`#btcp`) onclick, replaces it with an async XHR-based flow that fetches the next round HTML, handles RiddleMaster encounters inline, and re-initializes the game's `window.Battle` object via `document.dispatchEvent(new Event('DOMContentLoaded'))`.
|
||||
|
||||
### 1.3 Page/Context Detection
|
||||
|
||||
The script detects its environment via DOM selectors with no URL parsing beyond the `isekaiSuffix` (line 475):
|
||||
|
||||
| Condition | State | Action |
|
||||
|-----------|-------|--------|
|
||||
| `#textlog` exists + `!doInitDoBattle` | In battle | `initDoBattle()` |
|
||||
| `#riddlemaster` exists | RiddleMaster | `riddleRecorder()` |
|
||||
| No `#textlog`, Bazaar AM modify or Battle IW | Lobby (IW equipment screen) | Observe `#equipform` for world level/difficulty |
|
||||
| No `#textlog`, `s=Battle` query | Lobby (battle selection) | Read level, stamina, persona, spell damage bonus |
|
||||
| Default | Lobby (other) | No action |
|
||||
|
||||
---
|
||||
|
||||
## 2. RULE ENGINE
|
||||
|
||||
### 2.1 Configuration Structure
|
||||
|
||||
Battle configuration is stored in `cfgBattle` (line 13), merged from:
|
||||
- `defaultCfgBattle` (lines 14-229) — hardcoded defaults for all fighting styles
|
||||
- `localStorage['jpx_cfgBattle' + isekaiSuffix]` — user overrides
|
||||
- Merged by `mergeCfg()` (line 5034) with version-aware reset logic
|
||||
|
||||
The config schema for each battle mode (`{Style}_{Type}`) has three sections:
|
||||
- `supports` — array of support actions (healing, buffs, items) evaluated first
|
||||
- `attacks` — array of offensive actions evaluated second
|
||||
- `kb_*` (key-bindings) — direct hotkey-triggered action sequences stored as dynamic keys
|
||||
|
||||
The supported battle mode keys follow BATTLE_MODES (line 657):
|
||||
```
|
||||
Style × Variant: 10 styles × 6 types = 60 possible modes
|
||||
Styles: OneHanded, 1H_Mage, TwoHanded, 2H_Mage, DualWielding, DW_Mage, NitenIchiryu, NI_Mage, Staff, Unarmed
|
||||
Types: General, Arena, Encounter, Colosseum, Battle1000, Item, Tower
|
||||
```
|
||||
|
||||
### 2.2 Action Types
|
||||
|
||||
Each action is an object with a `type` discriminator and optional `conditions` array:
|
||||
|
||||
| Type | Description | Parameters |
|
||||
|------|-------------|------------|
|
||||
| `stop` | Halts auto-battle with custom message | `customMessage`, `conditions` |
|
||||
| `spellSupport` | Casts a supportive spell | `name` (from `SPELLS_SUPPORT`), `conditions` |
|
||||
| `item` | Uses an inventory item | `name` (from `ITEMS`), `conditions` |
|
||||
| `toggle` | Toggles Spirit Stance/Defend/Focus | `name`, `toggled` (boolean), `conditions` |
|
||||
| `target` | Selects a target monster for subsequent actions | `priorityRule` (from `PRIORITY_RULES`), `conditions` |
|
||||
| `smartDebuff` | Casts AoE debuffs with spatial targeting | `name`, `targetCount` (1-3), `bottomUp`, `tailSkip`, `maxAtFirst`, `minMonstersLeft`, `conditions` |
|
||||
| `spellDebuff` | Casts single-target debuff on selected monster | `name`, `conditions` |
|
||||
| `spellDamage` | Casts offensive spell on selected monster | `name` (can be `T1`/`T2`/`T3` aliases), `conditions` |
|
||||
| `skill` | Uses a weapon skill | `name` (from `SKILLS`), `conditions` |
|
||||
| `normalAttack` | Basic attack on selected monster | None |
|
||||
|
||||
### 2.3 Condition System
|
||||
|
||||
#### Condition Categories
|
||||
|
||||
**General Conditions** (`conditionsGeneral`, lines 947-979): Checked against global/player state:
|
||||
- `world` — Persistent vs Isekai
|
||||
- `pLevel` — Player level range
|
||||
- `pMaxSpellType` — Player's strongest spell element
|
||||
- `battleTypes` — Battle type(s) (Arena, Encounter, etc.)
|
||||
- `difficulty` — Difficulty array
|
||||
- `roundCurrent`, `roundLeft`, `roundTotal` — Round position
|
||||
- `floor` — Tower floor range
|
||||
- `pActionCooldown` — Cooldown range of specific actions (skill/spell/item)
|
||||
- `pActionCounts` — Uses-per-round range of specific actions
|
||||
- `pHP`, `pMP`, `pSP` — Player resource percentages
|
||||
- `pOC` — Overcharge percentage
|
||||
- `pSpiritStatus` — Spirit Stance active?
|
||||
- `pEffects` — Player has effects with turns in range
|
||||
- `pIgnoredEffects` — Player does NOT have these effects
|
||||
- `pEffectStacks` — Player effect stack counts
|
||||
- `monsters`, `activeMonsters`, `defeatedMonsters` — Monster counts
|
||||
- `bosses`, `activeBosses`, `defeatedBosses` — Boss counts
|
||||
- `mLevel` — Monster level range
|
||||
- `mWithoutEffects` — Count of monsters without specific effects
|
||||
|
||||
**Target Conditions** (`conditionsTarget`, lines 980-999): Checked per-monster with offset/matched mechanics:
|
||||
- `tName` — Regex-matchable monster name
|
||||
- `tTypes` — Monster type (Normal, Rare, Legendary, etc.)
|
||||
- `tClasses` — Monster class from MonsterDB (Arthropod, Avion, etc.)
|
||||
- `tPowerLevel` — Monster power level range
|
||||
- `tIndex` — Monster position (0-based, negative from end)
|
||||
- `tHP`, `tMP`, `tSP` — Monster resource percentages
|
||||
- `tEffects` — Monster has specific effects
|
||||
- `tIgnoredEffects` — Monster does NOT have specific effects
|
||||
- `tEffectStacks` — Monster effect stacks
|
||||
- `tDaysSinceUpdate` — Days since MonsterDB last updated entry
|
||||
|
||||
Each target condition carries `offset` (range relative to target's index) and `matched` (count of monsters in the offset range that must satisfy the condition — defaults to `[1,1]` = exactly 1).
|
||||
|
||||
#### Condition Evaluation (`checkConditions`, line 2579)
|
||||
|
||||
```javascript
|
||||
function checkConditions(conditions, target, checkGlobal, checkTarget)
|
||||
```
|
||||
- Iterates through all conditions, short-circuiting on first failure (AND logic)
|
||||
- Global conditions: checked directly against global state objects
|
||||
- Target conditions: evaluated with offset-based multi-monster checking
|
||||
- `successCount` increments for each active monster in `[target.index + offset[0], target.index + offset[1]]` that satisfies the condition
|
||||
- Final check: `jpxUtils.inRange(successCount, matched)`
|
||||
|
||||
The conditions system is lazily initialized via `initConditions()` (line 2445) which creates handler maps. Results are cached in `conditionsObj` per evaluation cycle (cleared in `preRender()`).
|
||||
|
||||
### 2.4 Action Execution (`actionManager`, line 2734)
|
||||
|
||||
The action manager processes action sequences with a sophisticated targeting phase:
|
||||
|
||||
```
|
||||
actionManager(actions):
|
||||
targetPhase = false
|
||||
targetMonster = null
|
||||
targetLocked = false
|
||||
|
||||
for each action in actions:
|
||||
if action.disabled → skip
|
||||
|
||||
if action.type == 'target':
|
||||
targetPhase = true
|
||||
if not targetLocked:
|
||||
result = handler(action, null) // evaluates target conditions globally, returns matching monster or null
|
||||
if result found:
|
||||
targetMonster = result
|
||||
targetLocked = true // Lock in the first matching target
|
||||
|
||||
else if targetPhase and no targetMonster → skip (no target to act on)
|
||||
|
||||
else if handler(action, targetMonster) succeeds → return true (action taken)
|
||||
targetLocked = false // Unlock for next target rule
|
||||
|
||||
return false // No action was possible
|
||||
```
|
||||
|
||||
Key behaviors:
|
||||
- **Target rules cascade**: Once a target rule finds a monster, `targetLocked` prevents re-evaluation until an action is successfully executed
|
||||
- **Smart Debuff skips target phase**: `smartDebuff` actions are checked globally (conditions must pass without target), then internally handle their own targeting
|
||||
- **Non-target actions after target rules require a locked target**, but "stop"/"item"/"spellSupport"/"toggle" can execute without a target if they appear before any target rule
|
||||
|
||||
### 2.5 Smart Debuff System (`doSpellsDebuffGoNext`, line 1810)
|
||||
|
||||
This is the most advanced targeting system in the script. It handles multi-target AoE debuff placement:
|
||||
|
||||
**Parameters:**
|
||||
- `name` — Spell name
|
||||
- `targetCount` — 1, 2, or 3 monsters to hit
|
||||
- `bottomUp` — Iterate from monster J→A (true) or A→J (false)
|
||||
- `tailSkip` — Skip last N monsters (positive) or only consider first N (negative)
|
||||
- `maxAtFirst` — Maximum casts when ≤1 monster defeated
|
||||
- `minMonstersLeft` — Threshold for infinite casting when many monsters remain
|
||||
|
||||
**Algorithm:**
|
||||
1. Check cooldown and action count limits
|
||||
2. Calculate `startIndex`/`endIndex` range from `activeMonsters` bounds
|
||||
3. `tailSkip`: if positive, trim from end; if negative, limit to first N from start
|
||||
4. First pass (targetCount ≥ 3): Find 3 consecutive alive + undebuffed monsters → cast on middle
|
||||
5. Second pass (targetCount ≥ 2): Find 2 consecutive alive + undebuffed → cast on edge-appropriate monster
|
||||
6. Third pass (targetCount ≥ 3, relaxed): Find 3 consecutive where middle is alive → cast on middle
|
||||
7. Fourth pass (targetCount = 1): Any single undebuffed monster → cast
|
||||
|
||||
**`isUnDebuffed` memoization** (line 1843): Results cached in `undebuffedObj` to avoid re-evaluating conditions for the same index.
|
||||
|
||||
### 2.6 Priority Rules for Target Selection
|
||||
|
||||
Six rules (line 658):
|
||||
- **Top Down** — Default, monster A first
|
||||
- **Bottom Up** — Monster J first
|
||||
- **Current HP Low to High** — Sort by absolute HP
|
||||
- **Current HP High to Low** — Sort by absolute HP descending
|
||||
- **Current HP Percent Low to High** — Sort by HP percentage
|
||||
- **Current HP Percent High to Low** — Sort by HP percentage descending
|
||||
|
||||
Results are cached in `monstersObj.sorted` per priority rule per cycle.
|
||||
|
||||
---
|
||||
|
||||
## 3. DOM PARSING
|
||||
|
||||
### 3.1 Monster State (`getMonsters`, line 2191)
|
||||
|
||||
Parses all `.btm1` elements to build monster objects:
|
||||
|
||||
```javascript
|
||||
monster = {
|
||||
index, // 0-9
|
||||
click(), // clicks the monster DOM element
|
||||
level, // from .btm2 div text or HVClasses
|
||||
name, // from .btm3 div text or HVClasses
|
||||
type, // matched against bossTypes map (Normal/Rare/Legendary/etc.)
|
||||
hpPercentage, // from .btm4 green bar width / 120
|
||||
mpPercentage, // from .btm4 blue bar width / 120
|
||||
spPercentage, // from .btm4 red bar width / 120
|
||||
isAlive, // .btm1 has onclick attribute
|
||||
effectObj: { name: { turns, stack } }, // from .btm6 img tooltips
|
||||
monster_btm1 // reference to DOM element
|
||||
}
|
||||
```
|
||||
|
||||
Returns: `{ monsters, activeMonsters, bosses, activeBosses }`
|
||||
|
||||
### 3.2 Player Vitals (`getVitals`, line 2091)
|
||||
|
||||
Two code paths:
|
||||
- **With OC bar** (Arena/Tower): `widthHP=414, widthMP/SP/OC=414`, reads `#dvrhb/#dvrhd`, `#dvrm`, `#dvrs`
|
||||
- **Without OC bar** (Encounter/Grindfest): `widthHP=496, widthMP/SP=207`, reads `#vrhb/#vrhd`, `#vrm`, `#vrs`
|
||||
|
||||
OC calculation:
|
||||
- With bar: `10 * parseInt(ocBar.style.width) / 414`
|
||||
- Without bar: Count `<div>` elements in `#vcp` HTML (each `div` = 0.5 OC after the first), handle half-segment via `vcr` class
|
||||
|
||||
Returns: `{ oc, hpPercentage, mpPercentage, spPercentage, hpCurrent, mpCurrent, spCurrent, hpMax, mpMax, spMax }`
|
||||
|
||||
### 3.3 Action Cooldowns (`getActionCooldowns`, line 1950)
|
||||
|
||||
Dual scanner:
|
||||
- **Spell cooldowns** (`.bts > div[onmouseover]`): Parse `onmouseover` tooltip with `regExp.spellInfo` → extract name and cooldown. If element has `onclick`, cooldown = 0 (ready). Otherwise: `lastUse + initCooldown - currentTurn`
|
||||
- **Item cooldowns** (`.bti3 > div`): Parse with `regExp.itemInfo` → map item ID to name via `itemMap`. Fixed 40-turn cooldown.
|
||||
|
||||
Cooldown is `'-'` if still on cooldown, `0` if ready.
|
||||
|
||||
### 3.4 Player Effects (`getEffectDuration`, line 2145)
|
||||
|
||||
Parses `#pane_effects` images and `.btm6 > img` elements:
|
||||
- Extract from `onmouseover` tooltip using `regExp.spellMatch` with named groups: `name`, `stack`, `description`, `turns`
|
||||
- Builds `playerEffectsObj[name] = { turns, stack }`
|
||||
- When `render=true`: creates duration overlay divs with color coding (<4 turns aquamarine, <9 lavender, auto/permanent special display)
|
||||
|
||||
### 3.5 Monster Effects Tracking (`updateMonsterEffects`, line 2291)
|
||||
|
||||
Handles the game's limitation of only showing 5 status effects per monster:
|
||||
- **5 effects visible**: Reset saved state completely
|
||||
- **6 effects visible**: The 6th is hidden behind a scroll — the script:
|
||||
1. Calculates hidden turn deltas by comparing saved vs visible effect turns
|
||||
2. Parses battle log for effect gains/expirations (`regExp.effectGain`, `regExp.effectExpired`, `regExp.effectWear`, etc.)
|
||||
3. Applies turn decrement to hidden effects
|
||||
4. Special handling for elemental effects (Searing Skin, Freezing Limbs, etc.) and Coalesced Mana
|
||||
5. Prunes effects that would have expired
|
||||
6. Renders hidden effect icons into the `.btm6` DOM
|
||||
|
||||
### 3.6 Monster Info (MonsterDB Integration, line 2404)
|
||||
|
||||
Polls `window.HVMonsterDB.getCurrentMonstersInformation()` with 250ms timeout for:
|
||||
- `monsterClass` — e.g., "Arthropod", "Daimon"
|
||||
- `attack` — attack type
|
||||
- `plvl` — power level
|
||||
- `lastUpdate` — timestamp for `tDaysSinceUpdate` condition
|
||||
|
||||
Stored in `allMonsterInfo` keyed by `mkey_{0-9}`.
|
||||
|
||||
### 3.7 Spell/Item Casting (`cast` / `use`, lines 1928-1948)
|
||||
|
||||
**cast(name):** Searches `.bts > div[onclick][onmouseover*="'name'"]` for spell buttons. If current selected spell differs, fires `onmouseover` (to select) then `onclick` (to cast) via a dummy div.
|
||||
|
||||
**use(name):** Looks up item ID from `itemMap`, finds matching div in `.bti3 > div[onclick][onmouseover]` containing that ID, fires `onmouseover` + `onclick`.
|
||||
|
||||
### 3.8 Spirit Status (`getSpiritStatus`, line 2139)
|
||||
|
||||
Checks if `#ckey_spirit` outerHTML contains `spirit_a.png` (active) or not (inactive).
|
||||
|
||||
---
|
||||
|
||||
## 4. FIGHTING STYLES
|
||||
|
||||
### 4.1 Auto-Detection (`initDoBattle`, line 1364)
|
||||
|
||||
Detection is based on which skills appear in the spell quickbar:
|
||||
|
||||
| Available Skill | Style (low spell damage) | Style (high spell damage) | Threshold |
|
||||
|----------------|--------------------------|---------------------------|-----------|
|
||||
| Shield Bash | `OneHanded` | `1H_Mage` | 70 |
|
||||
| Great Cleave | `TwoHanded` | `2H_Mage` | 100 |
|
||||
| Iris Strike | `DualWielding` | `DW_Mage` | 100 |
|
||||
| Skyward Sword | `NitenIchiryu` | `NI_Mage` | 100 |
|
||||
| Concussive Strike | `Staff` | `Staff` | N/A |
|
||||
| None of above | `Unarmed` | `Unarmed` | N/A |
|
||||
|
||||
The threshold compares `spellDamageBonus.maxValue` (from `localStorage['jpx_spellDamageBonus']`) — the player's maximum spell damage bonus value. Above threshold = mage variant.
|
||||
|
||||
### 4.2 Battle Mode Resolution (`getBattleMode`, line 4173)
|
||||
|
||||
```javascript
|
||||
getBattleMode(defaultBattleStyle = 'Unarmed'):
|
||||
modeKey = `${battleStyle}_${battleType}` // e.g., "OneHanded_Arena"
|
||||
|
||||
if cfgBattle[modeKey] has non-empty supports, attacks, or key-bindings:
|
||||
return modeKey // Specific mode exists
|
||||
|
||||
return `${battleStyle}_General` // Fall back to general mode
|
||||
```
|
||||
|
||||
This means: check for a battle-type-specific config first, fall back to general.
|
||||
|
||||
### 4.3 Battle Type Detection (`initDoBattle`, lines 1410-1428)
|
||||
|
||||
Parses `#textlog` innerHTML with `regExp.battleTypeLog`:
|
||||
- `arena challenge` + NOT `Round 1 / 1` → `Arena`
|
||||
- `random encounter` → `Encounter`
|
||||
- `arena challenge` + `Round 1 / 1` → `Colosseum`
|
||||
- `Grindfest` → `Battle1000`
|
||||
- `Item World` → `Item`
|
||||
- `The Tower` → `Tower` (also extracts floor number)
|
||||
|
||||
### 4.4 Style-Specific Configurations (Default)
|
||||
|
||||
The bundled defaults show the script author's own configurations:
|
||||
|
||||
- **OneHanded_General**: Full support chain (Spirit→Health→Mana gems/potions/elixirs, Mystic Gem, Heartseeker, Regen, Draughts), attacks use smartDebuff (Weaken for Arena400/500), target rules by priority, Scan first round, skills (OFC, Merciful Blow, Vital Strike), ending with normalAttack
|
||||
- **OneHanded_Tower**: Complex multi-stage smart debuff chain (Sleep→Weaken→Silence→Imperil) triggered by floor/round thresholds
|
||||
- **Staff_General**: Mage-focused supports (Arcane Focus instead of Heartseeker), smartDebuff chain (Weaken→Silence→Imperil), target priority for Coalesced Mana monsters, T3→T2→T1 spell priority
|
||||
- Other styles (2H, DW, Niten, Unarmed, mage variants): Empty default configs (lines 126-173, 221-228) — users configure them
|
||||
|
||||
---
|
||||
|
||||
## 5. BATTLE MODES
|
||||
|
||||
### 5.1 Enumeration (LINE 656-657)
|
||||
|
||||
```javascript
|
||||
BATTLE_TYPES = ['Arena', 'Encounter', 'Colosseum', 'Battle1000', 'Item', 'Tower']
|
||||
BATTLE_STYLES = ['OneHanded', '1H_Mage', 'TwoHanded', '2H_Mage', 'DualWielding', 'DW_Mage', 'NitenIchiryu', 'NI_Mage', 'Staff', 'Unarmed']
|
||||
BATTLE_MODES = Cartesian product BATTLE_STYLES × ['General', ...BATTLE_TYPES]
|
||||
```
|
||||
|
||||
### 5.2 Mode-Specific Behavior
|
||||
|
||||
| Battle Type | Special Behavior |
|
||||
|-------------|-----------------|
|
||||
| **Arena** | Sets difficulty to PFUDOR if ≥90 rounds. Uses roundInfo from localStorage for persistence |
|
||||
| **Encounter** | Round info not tracked. Stamina cost = 0 |
|
||||
| **Colosseum** | Single round. Stamina cost = 0 |
|
||||
| **Battle1000** (Grindfest) | Stamina cost includes +1 entry fee |
|
||||
| **Item** (Item World) | Reads worldLevel from localStorage (populated in lobby from IW equipment screen). Stamina cost formula with great/normal rates |
|
||||
| **Tower** | Auto-derives difficulty from floor number (1-6 Normal, 7-13 Hard, ..., 34-39 IWBTH, 40+ PFUDOR). Caches floor in localStorage |
|
||||
|
||||
### 5.3 Difficulty Map (line 574)
|
||||
|
||||
```javascript
|
||||
{ Normal: 1, Hard: 2, Nightmare: 4, Hell: 7, Nintendo: 10, IWBTH: 15, PFUDOR: 20 }
|
||||
```
|
||||
|
||||
### 5.4 Stamina Cost Calculation (`getStaminaCost`, line 3172)
|
||||
|
||||
- Great cost: 0.03 (Persistent) or 0.06 (Isekai) per round
|
||||
- Normal cost: 0.02 (Persistent) or 0.04 (Isekai) per round
|
||||
- Great rounds capped by `floor(max(0, stamina - 60) / greatCost)` (60 stamina reserved for normal-cost rounds)
|
||||
- Battle1000 adds +1 entry fee
|
||||
- Daily quota tracking via `staminaRecords` in localStorage per date
|
||||
|
||||
---
|
||||
|
||||
## 6. KEYBINDINGS & UI
|
||||
|
||||
### 6.1 Hotkey System
|
||||
|
||||
**Global Keybinds** (line 912):
|
||||
```javascript
|
||||
KEYBINDS = {
|
||||
openBattleRecords: { key: 'z', ctrl: false },
|
||||
toggleActive: { key: 'm', ctrl: false },
|
||||
openSettings: { key: ',', ctrl: false }
|
||||
}
|
||||
```
|
||||
|
||||
User-customizable, stored in `localStorage['jpx_userKeybinds' + isekaiSuffix]`.
|
||||
|
||||
**Battle Action Keybindings**: Stored as `kb_Ctrl+Shift+A` style keys in each battle mode config. Processed in `onKeyDown()`:
|
||||
1. Check for global keybind match (non-repeat only)
|
||||
2. If inactive and `#textlog` exists and monsters are alive, check for mode-specific keybinding
|
||||
3. Repeat keys use throttled `actionManager` (75ms), initial press uses direct `actionManager`
|
||||
|
||||
**Key Capture** (`jpxUtils.captureKeyCombo`, line 6060): Uses `AbortController` for clean cancellation. Ignores modifier-only keypresses. Fires on first non-modifier keydown or pointerdown.
|
||||
|
||||
### 6.2 Control Widget (`ctrlWidget`)
|
||||
|
||||
Created by `jpxPanelManager.createCtrlWidget('battle')`:
|
||||
- Positioned absolutely at top-right of battle screen (responsive via media query for landscape)
|
||||
- Shows configurable rows (`ctrlWidgetRows`): Active status, Ready state, Network delay, Battle style, Battle type, Battle mode, Round
|
||||
- Background color signals state: green (`#4f4`) = active, pink (`#fef`) = inactive, yellow (`#ff5`) = warning
|
||||
- Click toggles auto-battle
|
||||
- Sub-buttons: "Open Stats" → opens battle records in new window; "Open Settings" → opens settings panel
|
||||
- Optional `mouseEnter` trigger for auto-battle activation
|
||||
- Dispatches `jpx_ctrlWidget_update` CustomEvent for inter-script communication
|
||||
|
||||
### 6.3 Settings Panel (press `,`)
|
||||
|
||||
`renderSettings()` creates a full in-page settings panel with:
|
||||
- **Two tabs**: Battle Settings, Stats Settings
|
||||
- **Battle tab**: Keybind remapping UI, full schema-driven config editor for all battle modes, export/import/reset current mode
|
||||
- **Stats tab**: Dark mode toggle, combat/revenue row picker, stats column picker, IndexedDB export/import
|
||||
- **Schema-driven rendering**: `renderSchema()` with `fieldRenderers` registry supporting: heading, constant, boolean, text, number, rangeNumber, dropdown, array (with drag-and-drop reordering), fieldPicker (dual-list), object, keyBasedObjectArray (for keybindings), conditionsArray
|
||||
|
||||
### 6.4 Toast Notifications
|
||||
|
||||
`jpxUtils.createToast(content, duration)` — fixed-position toast at bottom-right with fade animation.
|
||||
|
||||
---
|
||||
|
||||
## 7. STORAGE
|
||||
|
||||
### 7.1 localStorage (prefix: `jpx_`, suffixed with `_isekai` for Isekai)
|
||||
|
||||
| Key | Data | Purpose |
|
||||
|-----|------|---------|
|
||||
| `cfgBattle` | Full battle config | User rules, passed through mergeCfg |
|
||||
| `cfgStats` | Stats display config | Combat/revenue display preferences |
|
||||
| `userKeybinds` | Custom keybindings | Overrides for global hotkeys |
|
||||
| `spellDamageBonus` | `{maxType, maxValue}` | Spell element optimization |
|
||||
| `worldLevel` | Number | IW world level |
|
||||
| `difficulty` | String | Current difficulty |
|
||||
| `playerLevel` | Number | Player level |
|
||||
| `persona` | String | Active persona |
|
||||
| `stamina` | Number | Current stamina |
|
||||
| `battleType` | String | Last battle type |
|
||||
| `towerFloor` | Number | Current tower floor |
|
||||
| `roundInfo` | `{current, total}` | Round tracking across page loads |
|
||||
| `monsterData` | Array of `{id, name, level, maxHP}` | Monster data for HP calculation |
|
||||
| `battleLogRecord` | Array of strings | Raw battle log (if enabled) |
|
||||
| `timeRecords` | `{action, turn, riddle, lastUse}` | Per-battle timing |
|
||||
| `combatRecords` | Nested damage/result stats | Per-battle combat data |
|
||||
| `revenueRecords` | Nested drop/currency stats | Per-battle revenue tracking |
|
||||
| `staminaRecords` | `{lastUpdate, staminaCost}` | Daily stamina quota tracking |
|
||||
| `priceData` | Market prices | Cached once per day |
|
||||
|
||||
### 7.2 IndexedDB
|
||||
|
||||
**Database**: `jpx`, version 1
|
||||
**Object Store**: `battleRecords` with keyPath `timestamp`, index on `date`
|
||||
|
||||
Used for persistent battle history. Records contain: world, timestamp, date, playerLevel, difficulty, persona, battleType, worldLevel, towerFloor, roundInfo, result, deltaSeconds, deltaTime, turns, tps, riddle, combatRecords, revenueRecords.
|
||||
|
||||
**Operations**:
|
||||
- `openDB()` — Open/create database
|
||||
- `storeBattleRecords()` — Save after battle completion
|
||||
- `getBattleRecordsRender()` — Query with filters (aggregate by day, world, battleType, difficulty, result, round range)
|
||||
- `exportIndexedDB()` / `importIndexedDB()` — Full DB export/import with optional merge
|
||||
|
||||
### 7.3 State Management
|
||||
|
||||
- `storeTmp()` (beforeunload): Persists monsterData, battleLogRecord, timeRecords, combatRecords, revenueRecords — but only if battle is not complete (no finish button)
|
||||
- On battle completion: `localStorage.removeItem()` clears all temp records
|
||||
- Version-aware config migration via `mergeCfg()` comparing `battleVersion`/`statsVersion`
|
||||
|
||||
---
|
||||
|
||||
## 8. COMPATIBILITY
|
||||
|
||||
### 8.1 Monsterbation (Monster DB Script) Integration
|
||||
|
||||
The script integrates with `window.HVMonsterDB` (Monsterbation's monster database):
|
||||
- `monsterDBReady()` (line 2404): Polls `window.HVMonsterDB.getCurrentMonstersInformation()` with 250ms timeout
|
||||
- Used for: `monsterClass`, `attack` type, `plvl` (power level), `lastUpdate` date
|
||||
- `tClasses`, `tPowerLevel`, `tDaysSinceUpdate` conditions depend on this data
|
||||
- `showMonsterInfo` option displays monster class, attack type, and power level on each monster
|
||||
- Falls back gracefully (`'?'` display) when MonsterDB is unavailable
|
||||
|
||||
### 8.2 Inter-Script Communication
|
||||
|
||||
`jpx_ctrlWidget_update` CustomEvent is dispatched by `jpxPanelManager.dispatchState()` (line 5614) with:
|
||||
```javascript
|
||||
{ active: isActiveBattle, background: color, suffix: isekaiSuffix, timestamp }
|
||||
```
|
||||
This is the designated hook for other userscripts to react to jpx state changes.
|
||||
|
||||
### 8.3 AJAX Round Compatibility
|
||||
|
||||
The `ajaxRound` feature has a config flag with explicit warning: "Disable it if other scripts don't support it." When enabled, it replaces standard page navigation with XHR fetches, which may break other scripts that rely on full page loads.
|
||||
|
||||
### 8.4 DOM Content Loaded Simulation
|
||||
|
||||
On AJAX round advancement, the script fires `document.dispatchEvent(new Event('DOMContentLoaded'))` (line 1680) to retrigger other scripts' initialization. It also re-creates `window.battle = new window.Battle()` and calls `clearInterval(window.timer)`.
|
||||
|
||||
---
|
||||
|
||||
## 9. KEY FUNCTIONS
|
||||
|
||||
### Initialization & Lifecycle
|
||||
| Function | Line | Purpose |
|
||||
|----------|------|---------|
|
||||
| `initDo()` | 1242 | Router: detects page context and dispatches to battle/lobby/riddle flows |
|
||||
| `initDoBattle()` | 1364 | Battle initialization: load configs, detect style/type, parse initial state |
|
||||
| `reDoBattle()` | 1457 | Re-initialization on round transition / DOMContentLoaded |
|
||||
| `preDoBattle()` | 1476 | Parse round info, proficiency, monster data; setup MutationObserver |
|
||||
| `preProcessLog()` | 1547 | MutationObserver callback: processes new log entries |
|
||||
| `preRender()` | 1580 | Prepares all state objects (cooldowns, vitals, monsters, effects) for decision |
|
||||
| `goNext()` | 1614 | Main loop: checks if active, handles end-of-round, triggers smartBattle |
|
||||
| `storeTmp()` | 5469 | beforeunload handler: persists temp state |
|
||||
|
||||
### Battle Logic
|
||||
| Function | Line | Purpose |
|
||||
|----------|------|---------|
|
||||
| `smartBattle()` | 1792 | Evaluates supports then attacks; returns whether action was taken |
|
||||
| `actionManager()` | 2734 | Processes action sequences with target locking |
|
||||
| `checkConditions()` | 2579 | Universal condition evaluator |
|
||||
| `initConditions()` | 2445 | Builds general/target handler maps |
|
||||
| `doSpellsDebuffGoNext()` | 1810 | Smart debuff spatial targeting algorithm |
|
||||
| `doSpellGoNext()` | 1897 | Cast spell on specific monster |
|
||||
| `doAttackGoNext()` | 1907 | Normal attack on specific monster |
|
||||
| `doToggleGoNext()` | 1916 | Toggle Spirit/Defend/Focus |
|
||||
| `cast()` | 1928 | Cast named spell via DOM |
|
||||
| `use()` | 1937 | Use named item via DOM |
|
||||
|
||||
### DOM Parsing
|
||||
| Function | Line | Purpose |
|
||||
|----------|------|---------|
|
||||
| `getActionCooldowns()` | 1950 | Parse spell/item cooldowns from quickbar DOM; render quickbar extensions |
|
||||
| `getVitals()` | 2091 | Parse HP/MP/SP/OC from bar elements |
|
||||
| `getSpiritStatus()` | 2139 | Check if Spirit Stance is active |
|
||||
| `getEffectDuration()` | 2145 | Parse player/monster effect durations from tooltips; render overlays |
|
||||
| `getMonsters()` | 2191 | Build monster state objects from `.btm1` elements |
|
||||
| `updateMonsterEffects()` | 2291 | Track hidden (6th+) monster status effects |
|
||||
| `updateMonsterInfo()` | 2428 | Poll MonsterDB for monster class/power/attack data |
|
||||
| `monsterDBReady()` | 2404 | Async poller for MonsterDB availability |
|
||||
|
||||
### Battle Recording
|
||||
| Function | Line | Purpose |
|
||||
|----------|------|---------|
|
||||
| `battleRecorder()` | 2767 | Orchestrates all recording subsystems |
|
||||
| `battleLogRecorder()` | 2789 | Records raw battle log text |
|
||||
| `timeRecorder()` | 2823 | Tracks actions, turns, last use timestamps |
|
||||
| `riddleRecorder()` | 2830 | Counts RiddleMaster solves |
|
||||
| `combatRecorder()` | 2839 | Comprehensive damage/result tracking with type classification |
|
||||
| `revenueRecorder()` | 3012 | Tracks EXP, credits, drops by category and quality |
|
||||
|
||||
### Battle Results Display
|
||||
| Function | Line | Purpose |
|
||||
|----------|------|---------|
|
||||
| `battleRecordPlayer()` | 3127 | Orchestrates end-of-battle record display and DB storage |
|
||||
| `battleLogPlayer()` | 3290 | Creates downloadable battle log blob |
|
||||
| `timeRecordPlayer()` | 3301 | Creates time/riddle/spark summary div |
|
||||
| `combatRecordPlayer()` | 3313 | Creates combat stats table (damage, results, crit stacks, debuff resists) |
|
||||
| `combatRecordPlayer_Use()` | 3438 | Creates action usage summary table |
|
||||
| `revenueRecordPlayer()` | 3478 | Creates revenue table with drop/use/balance/profit |
|
||||
| `newWindowRecordPlayer()` | 3605 | Renders mid-battle stats in popup window |
|
||||
|
||||
### IndexedDB & Stats
|
||||
| Function | Line | Purpose |
|
||||
|----------|------|---------|
|
||||
| `openDB()` | 3672 | Open/create IndexedDB |
|
||||
| `storeBattleRecords()` | 3695 | Save battle record |
|
||||
| `openBattleRecords()` | 3711 | Open stats window with filters |
|
||||
| `getBattleRecordsRender()` | 3799 | Query/filter/aggregate battle records |
|
||||
| `exportIndexedDB()` | 3851 | Full DB export |
|
||||
| `importIndexedDB()` | 3873 | DB import with merge support |
|
||||
| `filterData()` | 3908 | Apply filters to records |
|
||||
| `generateAggregate()` | 3918 | Aggregate data by day (Total/Average) |
|
||||
| `createFilter()` | 4028 | Build filter UI |
|
||||
| `getFilters()` | 4080 | Read filter values from UI |
|
||||
| `renderDynamicTable()` | 4107 | Render stats table with color thresholds |
|
||||
|
||||
### Settings & Schema
|
||||
| Function | Line | Purpose |
|
||||
|----------|------|---------|
|
||||
| `renderSettings()` | 5094 | Open/close settings panel |
|
||||
| `switchTab()` | 5265 | Switch between battle/stats tabs |
|
||||
| `renderBattleTab()` | 5288 | Battle settings tab with keybinds + full config editor |
|
||||
| `renderStatsTab()` | 5381 | Stats settings tab with DB export/import |
|
||||
| `renderSchema()` | 4189 | Schema-driven form renderer |
|
||||
| `renderField()` | 4989 | Field dispatcher to type-specific renderers |
|
||||
| `resolveSchema()` | 4994 | Resolve discriminator-based schemas |
|
||||
| `createEmptyObject()` | 5000 | Generate default object from schema |
|
||||
| `getUniqueId()` | 5030 | Generate unique DOM IDs |
|
||||
| `mergeCfg()` | 5034 | Merge stored config with defaults, run patches/migrations |
|
||||
|
||||
### Configuration
|
||||
| Function | Line | Purpose |
|
||||
|----------|------|---------|
|
||||
| `getBattleMode()` | 4173 | Resolve active battle mode key (e.g., "OneHanded_Arena" → "OneHanded_General") |
|
||||
|
||||
### Keybinding
|
||||
| Function | Line | Purpose |
|
||||
|----------|------|---------|
|
||||
| `onKeyDown()` | 1746 | Global keydown handler |
|
||||
| `toggleActive()` | 1782 | Toggle auto-battle on/off |
|
||||
|
||||
### I18N
|
||||
| Function | Line | Purpose |
|
||||
|----------|------|---------|
|
||||
| `initDoI18n()` | 5491 | Merge built-in I18N with external `jpxI18N` override |
|
||||
| `t()` | 5505 | Translation function with dot-path lookup and templating |
|
||||
|
||||
### Modules (singleton pattern)
|
||||
| Module | Line | Purpose |
|
||||
|--------|------|---------|
|
||||
| `jpxPanelManager()` | 5526 | Control widget creation, content update, state dispatch |
|
||||
| `jpxMarket()` | 5632 | Market price fetching, caching, default prices |
|
||||
| `jpxUtils()` | 5797 | 25 utility functions: throttle, time formatting, sorting, type checks, DOM helpers, XHR, key capture, HV class parsing, record factories |
|
||||
|
||||
### Utility Functions (jpxUtils)
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `throttle(fn, ms, trailing)` | Rate-limit function calls |
|
||||
| `secondsToTime(s, ms)` | Convert seconds to HH:MM:SS |
|
||||
| `daysSince(dateStr)` | Days since UTC date |
|
||||
| `getSortedArray(arr, fn, asc)` | Sort array by computed value |
|
||||
| `lowerFirst(str)` | Lowercase first character |
|
||||
| `titleCase(str)` | Title-case with camelCase splitting |
|
||||
| `sentenceCase(str)` | Sentence-case |
|
||||
| `matchAny(str, ...regexps)` | Test multiple regexes, return first match |
|
||||
| `parseValue(val)` | Parse string to number if numeric |
|
||||
| `isEmpty(obj)` | Check if object has no own properties |
|
||||
| `inRange(value, [min, max])` | Range check |
|
||||
| `getValueByPath(obj, keys)` | Dot-path object access |
|
||||
| `deepMerge(target, source)` | Recursive object merge |
|
||||
| `getSortedKeys(order, keys)` | Sort keys by predefined order |
|
||||
| `inc(obj, key, step)` | Increment object property |
|
||||
| `createButton(container, opts)` | Create styled button with toast feedback |
|
||||
| `createToast(content, dur)` | Show toast notification |
|
||||
| `stringifyLimited(obj, level)` | Pretty-print JSON with depth limit |
|
||||
| `toRegExp(input)` | Parse regex string or create literal regex |
|
||||
| `captureKeyCombo(onComplete, onAbort)` | Listen for single key combo |
|
||||
| `formatKeyCombo(input, sep)` | Format key combo object/string for display |
|
||||
| `parseHVClasses(container)` | Decode HentaiVerse obfuscated class names |
|
||||
| `createTimeRecords()` | Factory for empty time records |
|
||||
| `createCombatRecords()` | Factory for empty combat records |
|
||||
| `createRevenueRecords()` | Factory for empty revenue records |
|
||||
| `xhrGet(urlArray, interval)` | Staggered XHR requests with Promise.allSettled |
|
||||
|
||||
---
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
- **Total lines**: 6,215
|
||||
- **Named functions**: ~45
|
||||
- **Utility functions**: 25
|
||||
- **Regular expressions**: ~55 defined in `regExp` object
|
||||
- **Supported fighting styles**: 10 (5 physical + 5 mage variants)
|
||||
- **Supported battle types**: 6 + "General" fallback
|
||||
- **Total battle mode combinations**: 60
|
||||
- **Action types**: 10
|
||||
- **General condition keys**: 25
|
||||
- **Target condition keys**: 12
|
||||
- **Priority rules**: 6
|
||||
- **IndexedDB object store**: 1 (battleRecords)
|
||||
- **localStorage keys**: ~20+
|
||||
- **Singleton modules**: 3 (PanelManager, Market, Utils)
|
||||
- **Default market prices**: ~80 items
|
||||
- **Monster boss types**: 30+ across 6 tiers
|
||||
- **I18N entries**: ~150+
|
||||
967
references/monsterbation-battle-patterns.md
Normal file
967
references/monsterbation-battle-patterns.md
Normal file
|
|
@ -0,0 +1,967 @@
|
|||
# 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.
|
||||
|
||||
===========================================================================
|
||||
64
scripts/build.sh
Executable file
64
scripts/build.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/bin/bash
|
||||
# Build HV Unified from src/* into a single hv-unified.user.js
|
||||
# Usage: ./scripts/build.sh [--watch]
|
||||
# No dependencies — pure cat concatenation.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
SRC_DIR="$PROJECT_DIR/src"
|
||||
OUTPUT="$PROJECT_DIR/scripts/hv-unified.user.js"
|
||||
|
||||
# Ordered list of source files
|
||||
FILES=(
|
||||
header.user.js
|
||||
config.js
|
||||
state.js
|
||||
utils.js
|
||||
page-detector.js
|
||||
battle-parser.js
|
||||
items.js
|
||||
knowledge-base.js
|
||||
strategy-engine.js
|
||||
action-executor.js
|
||||
hover-system.js
|
||||
keybindings.js
|
||||
ui-overlays.js
|
||||
settings-panel.js
|
||||
out-of-battle.js
|
||||
guidance.js
|
||||
progress-tracker.js
|
||||
abilities.js
|
||||
armory.js
|
||||
battle-logger.js
|
||||
re-timer.js
|
||||
public-api.js
|
||||
init.js
|
||||
)
|
||||
|
||||
echo "🛡️ Building HV Unified..."
|
||||
echo " Source: $SRC_DIR"
|
||||
echo " Output: $OUTPUT"
|
||||
echo ""
|
||||
|
||||
# Clear output
|
||||
> "$OUTPUT"
|
||||
|
||||
# Concatenate with clear separators
|
||||
for file in "${FILES[@]}"; do
|
||||
src_path="$SRC_DIR/$file"
|
||||
if [ ! -f "$src_path" ]; then
|
||||
echo " ⚠️ MISSING: $file (skipping)"
|
||||
continue
|
||||
fi
|
||||
echo " ├── $file"
|
||||
cat "$src_path" >> "$OUTPUT"
|
||||
echo "" >> "$OUTPUT"
|
||||
done
|
||||
|
||||
# Count lines
|
||||
LINES=$(wc -l < "$OUTPUT")
|
||||
SIZE=$(du -h "$OUTPUT" | cut -f1)
|
||||
echo ""
|
||||
echo " ✅ Built: $LINES lines, $SIZE"
|
||||
2882
scripts/hv-unified.user.js
Normal file
2882
scripts/hv-unified.user.js
Normal file
File diff suppressed because it is too large
Load diff
2882
scripts/latest.user.js
Normal file
2882
scripts/latest.user.js
Normal file
File diff suppressed because it is too large
Load diff
169
src/abilities.js
Normal file
169
src/abilities.js
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// ABILITIES — spell & skill unlock guide for abilities page
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function enhanceAbilities() {
|
||||
if (document.getElementById('hv-abilities')) return;
|
||||
|
||||
const lv = STATE.level || 1;
|
||||
|
||||
// Detect current tree
|
||||
const url = window.location.href || '';
|
||||
const treeMap = {
|
||||
'tree=general': 'General',
|
||||
'tree=onehanded': 'One-Handed',
|
||||
'tree=twohanded': 'Two-Handed',
|
||||
'tree=dualwield': 'Dual Wield',
|
||||
'tree=niten': 'Niten',
|
||||
'tree=staff': 'Staff',
|
||||
'tree=cloth': 'Cloth',
|
||||
'tree=light': 'Light',
|
||||
'tree=heavy': 'Heavy',
|
||||
'tree=deprecating1': 'Deprecating 1',
|
||||
'tree=deprecating2': 'Deprecating 2',
|
||||
'tree=supportive1': 'Supportive 1',
|
||||
'tree=supportive2': 'Supportive 2',
|
||||
'tree=elemental': 'Elemental',
|
||||
'tree=forbidden': 'Forbidden',
|
||||
'tree=divine': 'Divine',
|
||||
};
|
||||
let tree = 'General';
|
||||
for (const [k, v] of Object.entries(treeMap)) {
|
||||
if (url.includes(k)) { tree = v; break; }
|
||||
}
|
||||
|
||||
// Read AP from CSS-digit counter
|
||||
let ap = 0;
|
||||
$$('#ability_top [class*="f4b"]').some(div => {
|
||||
const txt = div.textContent || '';
|
||||
if (txt.includes('Ability') || txt.includes('Mastery')) {
|
||||
const d = readCSSDigits(div);
|
||||
if (d !== null) ap = d;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Parse all visible abilities
|
||||
const abilities = [];
|
||||
$$('[id^="slot_"][onmouseover]').forEach(el => {
|
||||
const mo = el.getAttribute('onmouseover') || '';
|
||||
const m = mo.match(/overability\((\d+),\s*'([^']+)',\s*'([^']*)',\s*'([^']*)',\s*'([^']*)'/);
|
||||
if (!m) return;
|
||||
|
||||
const name = m[2];
|
||||
const status = m[4];
|
||||
const nextHtml = m[5];
|
||||
const lvlMatch = nextHtml.match(/Level\s*(\d+)/i);
|
||||
const lvlReq = lvlMatch ? parseInt(lvlMatch[1]) : 0;
|
||||
const apMatch = nextHtml.match(/(\d+)\s*Ability\s*Points?/i);
|
||||
const apCost = apMatch ? parseInt(apMatch[1]) : 0;
|
||||
const tierMatch = status.match(/Tier\s*(\d+)/i);
|
||||
const curTier = tierMatch ? parseInt(tierMatch[1]) : 0;
|
||||
const acquired = status !== 'Not Acquired' && status !== 'Locked';
|
||||
|
||||
abilities.push({ name, status, acquired, curTier, lvlReq, apCost, el });
|
||||
});
|
||||
|
||||
// Free slots
|
||||
const freeSlots = $$('#ability_top [id^="slot_"]').filter(el => {
|
||||
return (el.style.backgroundImage || '').includes('t/0.png');
|
||||
}).length;
|
||||
|
||||
// Build panel
|
||||
let body = `<b>💎 AP: ${ap}</b><br>`;
|
||||
if (freeSlots > 0) {
|
||||
body += `<div style="color:#f88;font-size:9px">⚠ ${freeSlots} empty slots — assign abilities!</div>`;
|
||||
}
|
||||
|
||||
body += `<div style="border-top:1px solid #374151;margin:4px 0;padding:4px 0"><b>🌳 ${tree}</b>`;
|
||||
|
||||
const affordable = abilities.filter(a => !a.acquired && lv >= a.lvlReq && ap >= a.apCost);
|
||||
const lockedByLevel = abilities.filter(a => !a.acquired && lv < a.lvlReq);
|
||||
const owned = abilities.filter(a => a.acquired);
|
||||
|
||||
if (owned.length > 0) body += `<div style="color:#8f8;font-size:9px">✅ ${owned.length} owned</div>`;
|
||||
if (affordable.length > 0) {
|
||||
const best = affordable.sort((a, b) => a.apCost - b.apCost)[0];
|
||||
body += `<div style="color:#0f0;font-size:9px">⬆ <b>Buy: ${best.name}</b> (${best.apCost} AP)</div>`;
|
||||
}
|
||||
if (lockedByLevel.length > 0) {
|
||||
const next = lockedByLevel.sort((a, b) => a.lvlReq - b.lvlReq)[0];
|
||||
body += `<div style="color:#fdcb00;font-size:9px">🔒 ${next.name} @ Lv${next.lvlReq}</div>`;
|
||||
}
|
||||
body += `</div>`;
|
||||
|
||||
// Full table
|
||||
if (abilities.length > 0) {
|
||||
body += `<div style="border-top:1px solid #374151;margin:4px 0;padding:4px 0;font-size:9px">
|
||||
<table style="width:100%;border-collapse:collapse">
|
||||
<tr style="color:#888"><th style="text-align:left;padding:1px 4px">Ability</th>
|
||||
<th style="padding:1px 4px">AP</th><th style="padding:1px 4px">Lv</th>
|
||||
<th style="text-align:right;padding:1px 4px">Status</th></tr>`;
|
||||
|
||||
const sorted = [].concat(
|
||||
affordable.sort((a, b) => a.apCost - b.apCost),
|
||||
owned,
|
||||
lockedByLevel.sort((a, b) => a.lvlReq - b.lvlReq),
|
||||
);
|
||||
|
||||
const seen = new Set();
|
||||
for (const a of sorted) {
|
||||
const k = a.name + a.curTier;
|
||||
if (seen.has(k)) continue;
|
||||
seen.add(k);
|
||||
const color = a.acquired ? '#8f8'
|
||||
: (lv >= a.lvlReq && ap >= a.apCost) ? '#0f0'
|
||||
: lv >= a.lvlReq ? '#fdcb00' : '#888';
|
||||
const lbl = a.acquired ? `T${a.curTier}`
|
||||
: (lv >= a.lvlReq && ap >= a.apCost) ? '⬆ Buy'
|
||||
: lv >= a.lvlReq ? '🔒AP' : `Lv${a.lvlReq}`;
|
||||
body += `<tr><td style="padding:1px 4px;color:${color}">${a.name}</td>
|
||||
<td style="padding:1px 4px;color:#888">${a.apCost}</td>
|
||||
<td style="padding:1px 4px;color:#888">${a.lvlReq}</td>
|
||||
<td style="padding:1px 4px;text-align:right;color:${color}">${lbl}</td></tr>`;
|
||||
}
|
||||
body += `</table></div>`;
|
||||
}
|
||||
|
||||
// Global priority
|
||||
body += `<div style="border-top:1px solid #374151;margin:4px 0;padding:4px 0;font-size:9px;color:#888">
|
||||
<b>🗺 Priority:</b> General (Tanks) → Supportive → Weapon → Elemental → Deprecating<br>
|
||||
Visit each tree tab for detailed recommendations.
|
||||
</div>`;
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'hv-abilities';
|
||||
panel.style.cssText = css({
|
||||
position: 'fixed',
|
||||
top: '60px',
|
||||
right: '4px',
|
||||
zIndex: '9995',
|
||||
background: '#111827',
|
||||
color: '#d1d5db',
|
||||
padding: '0',
|
||||
borderRadius: '8px',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'monospace',
|
||||
maxWidth: '320px',
|
||||
boxShadow: '0 0 15px rgba(0,0,0,0.6)',
|
||||
border: '1px solid #374151',
|
||||
});
|
||||
|
||||
const collapsed = localStorage[SP + 'abCollapsed'] === '1';
|
||||
panel.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 8px;cursor:pointer" id="hv-ab-header">
|
||||
<b style="color:#fdcb00;font-size:10px">📖 Abilities (Lv${lv})</b>
|
||||
<span id="hv-ab-toggle" style="color:#888;font-size:12px">${collapsed ? '▶' : '▼'}</span>
|
||||
</div>
|
||||
<div id="hv-ab-body" style="padding:0 8px 6px;display:${collapsed ? 'none' : 'block'}">${body}</div>`;
|
||||
|
||||
panel.querySelector('#hv-ab-header').onclick = () => {
|
||||
const b = document.getElementById('hv-ab-body');
|
||||
const t = document.getElementById('hv-ab-toggle');
|
||||
const h = b.style.display === 'none';
|
||||
b.style.display = h ? 'block' : 'none';
|
||||
t.textContent = h ? '▼' : '▶';
|
||||
localStorage[SP + 'abCollapsed'] = h ? '0' : '1';
|
||||
};
|
||||
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
101
src/action-executor.js
Normal file
101
src/action-executor.js
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// ACTION EXECUTOR — carry out recommended actions
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function executeAction(a) {
|
||||
if (!a) return false;
|
||||
switch (a.type) {
|
||||
case 'attack': return attackMonster(a.target);
|
||||
case 'spell': return a.selfTarget ? castSelfSpell(a.name) : castTargetSpell(a.name, a.target);
|
||||
case 'skill': return useSkill(a.name, a.target);
|
||||
case 'item': return useItem(a.id);
|
||||
case 'toggle_spirit': return toggleSpiritStance();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function attackMonster(i) {
|
||||
if (i < 0 || i >= STATE.monsters.length) return false;
|
||||
const m = STATE.monsters[i];
|
||||
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return false;
|
||||
m.click();
|
||||
return true;
|
||||
}
|
||||
|
||||
function castSelfSpell(n) {
|
||||
const s = $$('.btsd[onclick]').find(el =>
|
||||
(el.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${n}'`));
|
||||
if (!s) return false;
|
||||
s.click();
|
||||
return true;
|
||||
}
|
||||
|
||||
function castTargetSpell(n, i) {
|
||||
const s = $$('.btsd[onclick]').find(el =>
|
||||
(el.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${n}'`));
|
||||
if (!s) return false;
|
||||
s.click();
|
||||
if (i >= 0 && i < STATE.monsters.length) {
|
||||
setTimeout(() => {
|
||||
const m = STATE.monsters[i];
|
||||
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
||||
}, 50);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function useItem(id) {
|
||||
const it = document.getElementById('ikey_' + id);
|
||||
if (!it) return false;
|
||||
dummy.setAttribute('onclick', it.getAttribute('onmouseover'));
|
||||
dummy.click();
|
||||
it.click();
|
||||
return true;
|
||||
}
|
||||
|
||||
function useSkill(n, i) {
|
||||
let el = null;
|
||||
|
||||
// Try quickbar first
|
||||
for (const e of $$('#quickbar > .btqs[onclick]')) {
|
||||
if ((e.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${n}'`)) {
|
||||
el = e; break;
|
||||
}
|
||||
}
|
||||
// Try spellbook
|
||||
if (!el) {
|
||||
el = $$('.btsd[onclick]').find(e =>
|
||||
(e.getAttribute('onmouseover') || '').includes(`set_infopane_spell('${n}'`));
|
||||
}
|
||||
// Fallback: onclick match
|
||||
if (!el) {
|
||||
for (const e of $$('.btqb[onclick]')) {
|
||||
if ((e.getAttribute('onclick') || '').includes(n)) { el = e; break; }
|
||||
}
|
||||
}
|
||||
// Fallback: image match
|
||||
if (!el) {
|
||||
for (const e of $$('.btqb img')) {
|
||||
if ((e.src || '').toLowerCase().includes(n.toLowerCase().replace(/\s+/g, '_'))) {
|
||||
el = e.parentElement; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!el) return false;
|
||||
el.click();
|
||||
if (i >= 0 && i < STATE.monsters.length) {
|
||||
setTimeout(() => {
|
||||
const m = STATE.monsters[i];
|
||||
if (m && m.hasAttribute && m.hasAttribute('onclick')) m.click();
|
||||
}, 50);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function toggleSpiritStance() {
|
||||
const t = document.getElementById('ckey_spirit');
|
||||
if (!t) return false;
|
||||
t.click();
|
||||
return true;
|
||||
}
|
||||
94
src/armory.js
Normal file
94
src/armory.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// ARMORY — equipment management panel
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function enhanceArmory() {
|
||||
const main = document.getElementById('mainpane');
|
||||
if (!main || document.getElementById('hv-armory')) return;
|
||||
|
||||
const items = [];
|
||||
let currentCategory = '';
|
||||
|
||||
$$('tr', main).forEach(row => {
|
||||
if (row.className === 'eqtplabel') {
|
||||
currentCategory = (row.textContent || '').trim();
|
||||
return;
|
||||
}
|
||||
const omo = row.getAttribute('onmouseover') || '';
|
||||
const idMatch = omo.match(/hover_equip\((\d+)\)/);
|
||||
if (!idMatch) return;
|
||||
const label = row.querySelector('label');
|
||||
if (!label) return;
|
||||
const name = (label.textContent || '').trim();
|
||||
const quality = KB.qualities.find(q => name.includes(q)) || '?';
|
||||
items.push({ id: idMatch[1], name, quality, slot: currentCategory });
|
||||
});
|
||||
|
||||
if (items.length === 0) return;
|
||||
|
||||
const qwords = KB.qualities;
|
||||
const best = (arr) => arr.sort((a, b) => qwords.indexOf(b.quality) - qwords.indexOf(a.quality))[0];
|
||||
const weapons = items.filter(i => !i.slot.includes('Armor') && !i.slot.includes('Shield'));
|
||||
const armors = items.filter(i => i.slot.includes('Armor') || i.slot.includes('Shoes'));
|
||||
const bestWeapon = best(weapons);
|
||||
const bestArmor = best(armors);
|
||||
const qColor = (q) => { const i = qwords.indexOf(q); return i >= 4 ? '#0f0' : i >= 2 ? '#fdcb00' : '#f80'; };
|
||||
|
||||
let body = '';
|
||||
if (bestWeapon) body += `<div><span style="color:${qColor(bestWeapon.quality)}">${bestWeapon.quality}</span> <b>${bestWeapon.name}</b> ← best</div>`;
|
||||
if (bestArmor) body += `<div><span style="color:${qColor(bestArmor.quality)}">${bestArmor.quality}</span> <b>${bestArmor.name}</b> ← best</div>`;
|
||||
|
||||
body += '<div style="border-top:1px solid #374151;margin:6px 0"></div>';
|
||||
body += '<table style="width:100%;font-size:9px;border-collapse:collapse">';
|
||||
body += '<tr style="color:#888"><th style="text-align:left;padding:1px 2px">Q</th><th style="text-align:left;padding:1px 2px">Item</th><th style="text-align:right;padding:1px 2px">Action</th></tr>';
|
||||
|
||||
for (const item of items) {
|
||||
const sameSlot = items.filter(i => i.slot === item.slot);
|
||||
const isBest = item === bestWeapon || item === bestArmor;
|
||||
let action;
|
||||
if (isBest) action = '✅';
|
||||
else if (item.quality === 'Crude' && sameSlot.length > 1) action = '💰';
|
||||
else action = '📦';
|
||||
body += `<tr><td style="padding:1px 2px;color:${qColor(item.quality)}">${item.quality[0]}</td>
|
||||
<td style="padding:1px 2px">${item.name.slice(0, 30)}</td>
|
||||
<td style="padding:1px 2px;text-align:right">${action}</td></tr>`;
|
||||
}
|
||||
body += '</table>';
|
||||
body += '<div style="border-top:1px solid #374151;margin:6px 0;padding:4px 0;font-size:9px;color:#888">Go to Character → click empty slot → select item to equip</div>';
|
||||
|
||||
const collapsed = localStorage[SP + 'armoryCollapsed'] === '1';
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'hv-armory';
|
||||
panel.style.cssText = css({
|
||||
position: 'fixed',
|
||||
top: '60px',
|
||||
right: '4px',
|
||||
zIndex: '9995',
|
||||
background: '#111827',
|
||||
color: '#d1d5db',
|
||||
padding: '0',
|
||||
borderRadius: '8px',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'monospace',
|
||||
maxWidth: '320px',
|
||||
boxShadow: '0 0 15px rgba(0,0,0,0.6)',
|
||||
border: '1px solid #374151',
|
||||
});
|
||||
|
||||
panel.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 8px;cursor:pointer" id="hv-armory-header">
|
||||
<b style="color:#fdcb00;font-size:10px">🛡️ Equipment (${items.length})</b>
|
||||
<span id="hv-armory-toggle" style="color:#888;font-size:12px">${collapsed ? '▶' : '▼'}</span>
|
||||
</div>
|
||||
<div id="hv-armory-body" style="padding:0 8px 6px;display:${collapsed ? 'none' : 'block'}">${body}</div>`;
|
||||
|
||||
panel.querySelector('#hv-armory-header').onclick = () => {
|
||||
const b = document.getElementById('hv-armory-body');
|
||||
const t = document.getElementById('hv-armory-toggle');
|
||||
const h = b.style.display === 'none';
|
||||
b.style.display = h ? 'block' : 'none';
|
||||
t.textContent = h ? '▼' : '▶';
|
||||
localStorage[SP + 'armoryCollapsed'] = h ? '0' : '1';
|
||||
};
|
||||
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
108
src/battle-logger.js
Normal file
108
src/battle-logger.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// BATTLE LOGGER — saves raw battle log lines to localStorage in real time
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
let _lastLogCount = 0;
|
||||
const LOG_KEY = SP + 'battleLog';
|
||||
|
||||
function initBattleLog() {
|
||||
_lastLogCount = 0;
|
||||
// Clear previous log
|
||||
try { localStorage.removeItem(LOG_KEY); } catch (e) {}
|
||||
}
|
||||
|
||||
function logRound() {
|
||||
const log = document.getElementById('textlog');
|
||||
if (!log) return;
|
||||
|
||||
const allRows = log.querySelectorAll('tr');
|
||||
if (allRows.length <= _lastLogCount) return;
|
||||
|
||||
// Get only the NEW rows since last capture
|
||||
const newLines = [];
|
||||
for (let i = _lastLogCount; i < allRows.length; i++) {
|
||||
const text = (allRows[i].textContent || '').trim();
|
||||
if (text) newLines.push(text);
|
||||
}
|
||||
_lastLogCount = allRows.length;
|
||||
|
||||
if (newLines.length === 0) return;
|
||||
|
||||
// Read existing log from localStorage, append new lines, save back
|
||||
let existing = [];
|
||||
try {
|
||||
const saved = localStorage.getItem(LOG_KEY);
|
||||
if (saved) existing = JSON.parse(saved);
|
||||
} catch (e) {}
|
||||
|
||||
existing.push(...newLines);
|
||||
|
||||
try {
|
||||
localStorage.setItem(LOG_KEY, JSON.stringify(existing));
|
||||
} catch (e) {
|
||||
// localStorage full — keep only last 500 lines
|
||||
try {
|
||||
const trimmed = existing.slice(-500);
|
||||
localStorage.setItem(LOG_KEY, JSON.stringify(trimmed));
|
||||
} catch (e2) {}
|
||||
}
|
||||
}
|
||||
|
||||
function getBattleLog() {
|
||||
try {
|
||||
const saved = localStorage.getItem(LOG_KEY);
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getBattleSummary() {
|
||||
const lines = getBattleLog();
|
||||
if (!lines || lines.length === 0) return null;
|
||||
|
||||
let kills = 0, dmgDealt = 0, dmgTaken = 0, healed = 0;
|
||||
const spells = {}, skills = {}, items = {};
|
||||
|
||||
for (const line of lines) {
|
||||
const mKill = line.match(/(.+) has been defeated\./);
|
||||
if (mKill && !line.includes('gains the effect')) kills++;
|
||||
|
||||
const mDmg = line.match(/causing (\d+) points/);
|
||||
if (mDmg) dmgDealt += parseInt(mDmg[1]);
|
||||
|
||||
const mTaken = line.match(/take (\d+) (\w+)/);
|
||||
if (mTaken) dmgTaken += parseInt(mTaken[1]);
|
||||
|
||||
const mRegen = line.match(/Regen restores (\d+)/);
|
||||
if (mRegen) healed += parseInt(mRegen[1]);
|
||||
const mPot = line.match(/Regeneration restores (\d+)/);
|
||||
if (mPot) healed += parseInt(mPot[1]);
|
||||
const mCure = line.match(/healed for (\d+)/);
|
||||
if (mCure) healed += parseInt(mCure[1]);
|
||||
|
||||
const mSpell = line.match(/You cast (\w[\w\s-]+)\./);
|
||||
if (mSpell) spells[mSpell[1]] = (spells[mSpell[1]] || 0) + 1;
|
||||
|
||||
const mSkill = line.match(/You use (\w[\w\s-]+)\./);
|
||||
if (mSkill && !mSkill[1].includes('Draught') && !mSkill[1].includes('Potion') && !mSkill[1].includes('Elixir') && !mSkill[1].includes('Gem')) {
|
||||
skills[mSkill[1]] = (skills[mSkill[1]] || 0) + 1;
|
||||
}
|
||||
|
||||
const mItem = line.match(/You use (.+?)\.$/);
|
||||
if (mItem && (mItem[1].includes('Draught') || mItem[1].includes('Potion') || mItem[1].includes('Elixir') || mItem[1].includes('Gem'))) {
|
||||
items[mItem[1]] = (items[mItem[1]] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalLines: lines.length,
|
||||
kills,
|
||||
dmgDealt,
|
||||
dmgTaken,
|
||||
healed,
|
||||
spells,
|
||||
skills,
|
||||
items,
|
||||
};
|
||||
}
|
||||
168
src/battle-parser.js
Normal file
168
src/battle-parser.js
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// BATTLE PARSER — read game state from the battle DOM
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function parseBarRatio(sel, base, ctx) {
|
||||
const e = (ctx || document).querySelector(sel);
|
||||
return e ? clamp(parseInt(e.style.width || 0) / base, 0, 1) : 1;
|
||||
}
|
||||
|
||||
function parseBattleState() {
|
||||
STATE.inBattle = true;
|
||||
STATE.isekai = !!document.getElementById('vcp');
|
||||
|
||||
// ── Parse monster names from battle log (plain text) ──
|
||||
STATE.battleMonsterNames = [];
|
||||
// Also track channeling status from log
|
||||
STATE.channeling = false;
|
||||
const log = document.getElementById('textlog');
|
||||
if (log) {
|
||||
log.querySelectorAll('tr').forEach(row => {
|
||||
const text = (row.textContent || '');
|
||||
const m = text.match(/Spawned Monster \w: MID=\d+ \(([^)]+)\)/);
|
||||
if (m) STATE.battleMonsterNames.push(m[1].toLowerCase());
|
||||
// Detect channeling status from log messages
|
||||
if (text.includes('The effect Channeling')) {
|
||||
STATE.channeling = text.includes('gains the effect');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Level & difficulty ──
|
||||
const container = document.querySelector('#level_readout > div');
|
||||
if (container) {
|
||||
const text = (container.innerText || container.textContent || '').trim();
|
||||
const textMatch = text.match(/(\w+)\s+Lv\.?\s*(\d+)/);
|
||||
if (textMatch) {
|
||||
STATE.difficulty = textMatch[1];
|
||||
STATE.level = parseInt(textMatch[2]) || 1;
|
||||
} else {
|
||||
// CSS-font rendering for overworld/battle font
|
||||
const cssText = readCSSText(container);
|
||||
const cssMatch = cssText.match(/(\w+)\s+lv\.\s*(\d+)/i);
|
||||
if (cssMatch) {
|
||||
STATE.difficulty = cssMatch[1].charAt(0).toUpperCase() + cssMatch[1].slice(1);
|
||||
cacheDifficulty();
|
||||
}
|
||||
const d = readCSSDigits(container);
|
||||
if (d !== null && d > 0) STATE.level = d;
|
||||
}
|
||||
}
|
||||
if (!STATE.level || STATE.level < 1) {
|
||||
try {
|
||||
const s = parseInt(localStorage[SP + 'playerLevel']);
|
||||
if (s > 0) STATE.level = s;
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!STATE.level || STATE.level < 1) STATE.level = 1;
|
||||
try { localStorage[SP + 'playerLevel'] = STATE.level; } catch (e) {}
|
||||
updateTier();
|
||||
restoreCachedDifficulty();
|
||||
|
||||
// ── Vitals ──
|
||||
const vitalsPane = document.getElementById('pane_vitals');
|
||||
const isk = STATE.isekai;
|
||||
STATE.hp = parseBarRatio('img[src$="green.png"]', isk ? 496 : 414, vitalsPane);
|
||||
STATE.mp = parseBarRatio('img[src$="bar_blue.png"]', isk ? 207 : 414, vitalsPane);
|
||||
STATE.sp = parseBarRatio('img[src$="bar_red.png"]', isk ? 207 : 414, vitalsPane);
|
||||
|
||||
// ── Overcharge ──
|
||||
const vcp = document.getElementById('vcp');
|
||||
STATE.oc = vcp
|
||||
? clamp(parseInt((vcp.querySelector('div') || {}).style?.width || 0) / 190, 0, 1) * 100
|
||||
: 0;
|
||||
|
||||
// ── Spirit Stance ──
|
||||
STATE.spiritStance = ((document.getElementById('ckey_spirit') || {}).src || '').includes('_s.png');
|
||||
|
||||
// ── Monsters ──
|
||||
STATE.monsters = $$('.btm1[onclick]');
|
||||
|
||||
// ── Spells from spell pane ──
|
||||
STATE.spellsKnown = [];
|
||||
const magicPane = document.getElementById('pane_magic');
|
||||
if (magicPane) {
|
||||
$$('.btsd[onclick]', magicPane).forEach(el => {
|
||||
const omo = el.getAttribute('onmouseover') || '';
|
||||
const m = omo.match(/battle\.set_infopane_spell\('([^']+)',\s*'[^']*',\s*'[^']*',\s*(\d+),\s*(\d+),\s*(\d+)\)/);
|
||||
if (m) STATE.spellsKnown.push({ n: m[1], mp: parseInt(m[2]), chr: parseInt(m[3]), cd: parseInt(m[4]) });
|
||||
});
|
||||
}
|
||||
|
||||
// ── Skills from skill pane (Flee, Scan, etc.) ──
|
||||
const skillPane = document.getElementById('pane_skill');
|
||||
if (skillPane) {
|
||||
$$('.btsd[onclick]', skillPane).forEach(el => {
|
||||
const omo = el.getAttribute('onmouseover') || '';
|
||||
const m = omo.match(/battle\.set_infopane_spell\('([^']+)'/);
|
||||
if (m) STATE.spellsKnown.push({ n: m[1], mp: 0, chr: 0, cd: 0 });
|
||||
});
|
||||
}
|
||||
|
||||
// ── Skills/spells from quickbar ──
|
||||
const WEAPON_SKILLS = [
|
||||
'Great Cleave', 'Frenzied Blows', 'Shield Bash', 'Iris Strike',
|
||||
'Skyward Sword', 'Shatter Strike', 'Rending Blow', 'Backstab',
|
||||
'Merciful Blow', 'Vital Strike', 'Concussive Strike',
|
||||
];
|
||||
|
||||
STATE.skillsKnown = [];
|
||||
$$('#quickbar > .btqs[onclick]').forEach(el => {
|
||||
const omo = el.getAttribute('onmouseover') || '';
|
||||
const m = omo.match(/battle\.set_infopane_spell\('([^']+)'/);
|
||||
if (m) {
|
||||
const name = m[1];
|
||||
if (WEAPON_SKILLS.includes(name)) {
|
||||
STATE.skillsKnown.push(name);
|
||||
} else if (!STATE.spellsKnown.some(s => s.n === name)) {
|
||||
const costMatch = omo.match(/battle\.set_infopane_spell\('([^']+)',\s*'[^']*',\s*'[^']*',\s*(\d+),\s*(\d+),\s*(\d+)\)/);
|
||||
STATE.spellsKnown.push({
|
||||
n: name,
|
||||
mp: costMatch ? parseInt(costMatch[2]) : 0,
|
||||
chr: costMatch ? parseInt(costMatch[3]) : 0,
|
||||
cd: costMatch ? parseInt(costMatch[4]) : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Items ──
|
||||
STATE.itemsKnown = {};
|
||||
$$('[id^="ikey_"]').forEach(el => {
|
||||
const id = el.id.replace('ikey_', '');
|
||||
const omo = el.getAttribute('onmouseover') || '';
|
||||
const m = omo.match(/battle\.set_infopane_item\((\d+)\)/);
|
||||
if (m) STATE.itemsKnown[id] = m[1];
|
||||
});
|
||||
|
||||
// Buffs
|
||||
STATE.buffs = {};
|
||||
const pane = document.getElementById('pane_effects');
|
||||
if (pane) {
|
||||
$$('img[onmouseover]', pane).forEach(img => {
|
||||
const omo = img.getAttribute('onmouseover') || '';
|
||||
const dur = omo.match(/(\d+)\s*turns?\s*rem/);
|
||||
// 'permanent' means the buff lasts indefinitely (Absorb, autocast effects)
|
||||
const isPermanent = omo.includes("'permanent'");
|
||||
const name = (img.src || '').split('/').pop().replace('.png', '');
|
||||
STATE.buffs[name] = isPermanent ? 999 : (dur ? parseInt(dur[1]) : 50);
|
||||
});
|
||||
}
|
||||
// Spark of Life check
|
||||
if ($('img[src$="fallenshield.png"]') && !$('img[src$="bar_dgreen.png"]'))
|
||||
STATE.buffs['spark_of_life'] = 0;
|
||||
|
||||
// ── Cooldowns ──
|
||||
STATE.cooldowns = {};
|
||||
$$('.btqb').forEach(el => {
|
||||
const txt = (el.getAttribute('onmouseover') || el.getAttribute('title') || '');
|
||||
const cd = txt.match(/Cooldown:\s*(\d+)/);
|
||||
if (cd) {
|
||||
const key = (el.querySelector('img')?.src || '').split('/').pop()?.replace('.png', '') || '?';
|
||||
STATE.cooldowns[key] = parseInt(cd[1]);
|
||||
}
|
||||
});
|
||||
|
||||
STATE.difficulty = STATE.difficulty || 'Normal';
|
||||
STATE.battleInitialized = true;
|
||||
}
|
||||
46
src/config.js
Normal file
46
src/config.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// CONFIG — default settings
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const VERSION = '0.11.0';
|
||||
|
||||
const CFG = {
|
||||
// — Battle automation
|
||||
hotkey: 'KeyQ',
|
||||
hotkeyMod: '',
|
||||
hoverEnabled: true,
|
||||
autoBuff: true,
|
||||
autoDebuff: true,
|
||||
autoCure: true,
|
||||
autoSpirit: false,
|
||||
|
||||
// — Thresholds
|
||||
cureHP: 0.35, // Cast Cure below this HP
|
||||
cureItemHP: 0.65, // Use health items at this HP (aggressive, credits abundant)
|
||||
cureRegenHP: 0.75, // Cast Regen below this HP
|
||||
manaGemMP: 0.60, // Use mana gems below this MP (proactive)
|
||||
manaPotionMP: 0.30, // Use mana potions at critical
|
||||
spiritPotionSP: 0.40, // Use spirit gems/potions below this SP
|
||||
spiritStanceOC: 60, // Activate Spirit Stance at this OC
|
||||
|
||||
// — Out of battle
|
||||
autoSell: true,
|
||||
sellQuality: 'Average',
|
||||
bulkShrine: true,
|
||||
reTimer: true,
|
||||
trainingQueue: true,
|
||||
|
||||
// — UI
|
||||
showCooldowns: true,
|
||||
showMonsterHP: true,
|
||||
showDurations: true,
|
||||
alertColours: true,
|
||||
showMonsterNumbers: true,
|
||||
cfgButton: true,
|
||||
showGuidance: true,
|
||||
showEquipAdvice: true,
|
||||
autoDifficulty: true,
|
||||
|
||||
// — Combat style
|
||||
useAttackSpells: true,
|
||||
};
|
||||
304
src/guidance.js
Normal file
304
src/guidance.js
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GUIDANCE — character page advisor panel with attribute recommendations
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function detectFightingStyle() {
|
||||
if (STATE.skillsKnown.includes('Shield Bash')) return '1H';
|
||||
if (STATE.skillsKnown.includes('Great Cleave')) return '2H';
|
||||
if (STATE.skillsKnown.includes('Iris Strike')) return 'DW';
|
||||
if (STATE.skillsKnown.includes('Skyward Sword')) return 'Niten';
|
||||
if (STATE.skillsKnown.includes('Concussive Strike')) return 'Staff';
|
||||
return '1H';
|
||||
}
|
||||
|
||||
function getAttrAdvice(style) {
|
||||
const a = KB.statAllocation[STATE.tier]?.[style] || KB.statAllocation[STATE.tier]?.Staff || { INT: 35, WIS: 25, END: 20, AGI: 10, DEX: 5, STR: 5 };
|
||||
const notes = { STR: 'Phys dmg', DEX: 'Acc+Parry', END: 'HP+Mit', INT: 'Magic dmg', WIS: 'MP+Acc', AGI: 'Evade+Spd' };
|
||||
return Object.entries(a).map(([s, p]) => {
|
||||
const bar = '█'.repeat(Math.round(p / 5)) + '░'.repeat(20 - Math.round(p / 5));
|
||||
return `${s.padEnd(4)} ${bar} ${p}% — ${notes[s] || ''}`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
function getSpellAdvice() {
|
||||
const lv = STATE.level;
|
||||
const up = KB.spellUnlocks.filter(s => s.lvl > lv && s.lvl <= lv + 20);
|
||||
const ju = KB.spellUnlocks.filter(s => s.lvl <= lv && s.lvl >= lv - 5);
|
||||
const lines = [];
|
||||
if (ju.length) lines.push('⚠ Recent: ' + ju.map(s => s.name).join(', '));
|
||||
if (up.length) lines.push('🔮 Soon: ' + up.map(s => `${s.name}@${s.lvl}`).join(', '));
|
||||
if (!lines.length) lines.push('All major spells unlocked.');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function getDifficultyAdvice() {
|
||||
const tier = STATE.tier;
|
||||
const difficulty = STATE.difficulty || 'Normal';
|
||||
|
||||
// Difficulty order (higher index = harder)
|
||||
const difficulties = ['Normal', 'Hard', 'Nightmare', 'Hell', 'Nintendo', 'IWBTH', 'PFUDOR'];
|
||||
|
||||
// Forum-corrected: PFUDOR from Veteran onward
|
||||
let suggested, reason;
|
||||
switch (tier) {
|
||||
case 'novice':
|
||||
suggested = 'Normal';
|
||||
reason = 'Survival first. Use items before spells.';
|
||||
break;
|
||||
case 'adept':
|
||||
suggested = 'Hard';
|
||||
reason = 'Better drops + EXP. You have enough HP now.';
|
||||
break;
|
||||
case 'veteran':
|
||||
suggested = 'PFUDOR';
|
||||
reason = '20x EXP. You can handle PFUDOR at L150+.';
|
||||
break;
|
||||
case 'master':
|
||||
suggested = 'PFUDOR';
|
||||
reason = 'Max rewards. Always PFUDOR.';
|
||||
break;
|
||||
default:
|
||||
suggested = 'Normal';
|
||||
reason = '';
|
||||
}
|
||||
|
||||
const currentIdx = difficulties.indexOf(difficulty);
|
||||
const suggestedIdx = difficulties.indexOf(suggested);
|
||||
|
||||
// Only warn if current difficulty is LOWER than suggested
|
||||
// (don't nag if you're already on a higher difficulty)
|
||||
if (currentIdx < suggestedIdx && CFG.autoDifficulty) {
|
||||
return { current: difficulty, suggested, reason, upgrade: true,
|
||||
message: `⚡ Suggested: ${suggested} (currently ${difficulty})\n ${reason}` };
|
||||
}
|
||||
return { current: difficulty, suggested, reason, upgrade: false, message: '' };
|
||||
}
|
||||
|
||||
// ── Attribute guidance ──
|
||||
|
||||
function getCurrentAttributes() {
|
||||
try {
|
||||
if (typeof attr_current !== 'undefined' && attr_current) {
|
||||
const delta = (typeof attr_delta !== 'undefined' && attr_delta) ? attr_delta : {};
|
||||
return {
|
||||
STR: (attr_current.str || 0) + (delta.str || 0),
|
||||
DEX: (attr_current.dex || 0) + (delta.dex || 0),
|
||||
AGI: (attr_current.agi || 0) + (delta.agi || 0),
|
||||
END: (attr_current.end || 0) + (delta.end || 0),
|
||||
INT: (attr_current.int || 0) + (delta.int || 0),
|
||||
WIS: (attr_current.wis || 0) + (delta.wis || 0),
|
||||
};
|
||||
}
|
||||
} catch (e) {}
|
||||
return { STR: 0, DEX: 0, AGI: 0, END: 0, INT: 0, WIS: 0 };
|
||||
}
|
||||
|
||||
function getNextAttributeAdvice(style) {
|
||||
const current = getCurrentAttributes();
|
||||
const total = Object.values(current).reduce((a, b) => a + b, 0);
|
||||
if (total === 0) return null;
|
||||
|
||||
const target = KB.statAllocation[STATE.tier]?.[style] || KB.statAllocation[STATE.tier]?.Staff;
|
||||
if (!target) return null;
|
||||
|
||||
let worstStat = null, worstDeficit = -Infinity;
|
||||
const stats = ['STR', 'DEX', 'AGI', 'END', 'INT', 'WIS'];
|
||||
|
||||
for (const stat of stats) {
|
||||
const currentPct = total > 0 ? (current[stat] / total) * 100 : 0;
|
||||
const targetPct = target[stat] || 0;
|
||||
const deficit = targetPct - currentPct;
|
||||
if (deficit > worstDeficit) { worstDeficit = deficit; worstStat = stat; }
|
||||
}
|
||||
|
||||
if (!worstStat || worstDeficit <= 2) {
|
||||
const priority = KB.statPriority[style] || KB.statPriority['1H'];
|
||||
return { stat: priority[0], reason: 'Balanced. Push primary damage stat.',
|
||||
target: target[priority[0]], current: Math.round((current[priority[0]] / total) * 100), deficit: 0 };
|
||||
}
|
||||
|
||||
const reasons = {
|
||||
STR: 'More physical damage.',
|
||||
DEX: 'Better accuracy, crit, parry.',
|
||||
AGI: 'Faster attacks, higher evade.',
|
||||
END: 'More HP.',
|
||||
INT: 'Stronger magic.',
|
||||
WIS: 'More MP, better resist.',
|
||||
};
|
||||
|
||||
return { stat: worstStat, reason: reasons[worstStat] || '',
|
||||
target: target[worstStat], current: Math.round((current[worstStat] / total) * 100),
|
||||
deficit: Math.round(worstDeficit) };
|
||||
}
|
||||
|
||||
function highlightAttributeButton(advice) {
|
||||
if (!advice) return;
|
||||
const statKey = advice.stat.toLowerCase();
|
||||
const incBtn = document.getElementById(statKey + '_inc');
|
||||
if (!incBtn) return;
|
||||
incBtn.style.outline = '2px solid #fdcb00';
|
||||
incBtn.style.outlineOffset = '2px';
|
||||
incBtn.title = `Level ${advice.stat} next! ${advice.reason}`;
|
||||
}
|
||||
|
||||
// ── Guidance panel ──
|
||||
|
||||
function showGuidancePanel() {
|
||||
if (!CFG.showGuidance) return;
|
||||
if (document.getElementById('hv-guidance')) return;
|
||||
|
||||
const style = detectFightingStyle();
|
||||
const diff = STATE.difficulty || 'Normal';
|
||||
const da = getDifficultyAdvice();
|
||||
const nextAttr = getNextAttributeAdvice(style);
|
||||
const collapsed = localStorage[SP + 'guideCollapsed'] === '1';
|
||||
|
||||
const p = document.createElement('div');
|
||||
p.id = 'hv-guidance';
|
||||
p.style.cssText = css({
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
zIndex: '99996',
|
||||
background: '#111827',
|
||||
color: '#d1d5db',
|
||||
padding: '0',
|
||||
borderRadius: '8px',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'monospace',
|
||||
maxWidth: '600px',
|
||||
minWidth: '320px',
|
||||
boxShadow: '0 0 30px rgba(0,0,0,0.8)',
|
||||
border: '1px solid #374151',
|
||||
cursor: 'move',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
// Restore saved position
|
||||
const savedPos = JSON.parse(localStorage[SP + 'guidePos'] || '{}');
|
||||
if (savedPos.left && savedPos.top) {
|
||||
p.style.left = savedPos.left + 'px';
|
||||
p.style.top = savedPos.top + 'px';
|
||||
p.style.transform = 'none';
|
||||
}
|
||||
|
||||
let nextHtml = '';
|
||||
if (nextAttr) {
|
||||
nextHtml = `<div style="background:#1f2937;padding:10px;margin:6px 0;border-radius:4px;border-left:3px solid #fdcb00;font-size:14px">
|
||||
<b style="color:#fdcb00">👉 Next: +${nextAttr.stat}</b>
|
||||
<span style="color:#9ca3af;margin-left:8px">${nextAttr.reason}</span>
|
||||
<div style="margin-top:4px;font-size:11px;color:#888">
|
||||
Target: <b style="color:#fdcb00">${nextAttr.target}%</b> |
|
||||
Now: <b style="color:#${nextAttr.deficit > 10 ? 'f44' : '8f8'}">${nextAttr.current}%</b>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
p.innerHTML = `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;cursor:move" id="hv-guide-header">
|
||||
<b style="color:#fdcb00;font-size:12px">🧭 Advisor — Lv${STATE.level} (${STATE.tier.toUpperCase()})</b>
|
||||
<div style="display:flex;gap:6px">
|
||||
<span id="hv-guide-minimize" style="cursor:pointer;color:#888;font-size:14px">${collapsed ? '📌' : '🗕'}</span>
|
||||
<span id="hv-guide-close" style="cursor:pointer;color:#f44;font-size:14px">✕</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="hv-guide-body" style="padding:0 10px 10px;display:${collapsed ? 'none' : 'block'}">
|
||||
${nextAttr ? nextHtml : ''}
|
||||
<div style="background:#1f2937;padding:8px;margin:6px 0;border-radius:4px;border-left:3px solid #fdcb00">
|
||||
<b>📈 Attributes</b><pre style="margin:4px 0;color:#9ca3af;font-size:10px;line-height:1.3">${getAttrAdvice(style)}</pre>
|
||||
</div>
|
||||
<div style="background:#1f2937;padding:8px;margin:6px 0;border-radius:4px;border-left:3px solid #8b5cf6">
|
||||
<b>✨ Spells</b><pre style="margin:4px 0;color:#9ca3af;font-size:10px">${getSpellAdvice()}</pre>
|
||||
</div>
|
||||
<div style="background:#1f2937;padding:8px;margin:6px 0;border-radius:4px;border-left:3px solid ${da.upgrade ? '#f59e0b' : '#10b981'}">
|
||||
<b>⚔ Difficulty</b><div style="color:#9ca3af;font-size:10px">Current: <b>${diff}</b>${da.upgrade ? ` → Suggested: <b style="color:#fdcb00">${da.suggested}</b> — ${da.reason}` : ' ✓ Optimal'}</div>
|
||||
</div>
|
||||
<div style="background:#1f2937;padding:8px;margin:6px 0;border-radius:4px;border-left:3px solid #ef4444">
|
||||
<b>🎯 Training</b><div style="color:#9ca3af;font-size:10px">Adept Learner → Scavenger → Ability Boost → Quartermaster</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(p);
|
||||
|
||||
// Draggable
|
||||
let dragging = false, startX, startY, origX, origY;
|
||||
const header = document.getElementById('hv-guide-header');
|
||||
const onDrag = (e) => {
|
||||
if (!dragging) return;
|
||||
p.style.left = (origX + e.clientX - startX) + 'px';
|
||||
p.style.top = (origY + e.clientY - startY) + 'px';
|
||||
p.style.transform = 'none';
|
||||
};
|
||||
|
||||
header.addEventListener('mousedown', e => {
|
||||
if (e.target.id === 'hv-guide-minimize' || e.target.id === 'hv-guide-close') return;
|
||||
dragging = true;
|
||||
const rect = p.getBoundingClientRect();
|
||||
startX = e.clientX; startY = e.clientY;
|
||||
origX = rect.left; origY = rect.top;
|
||||
document.addEventListener('mousemove', onDrag);
|
||||
document.addEventListener('mouseup', () => {
|
||||
dragging = false;
|
||||
document.removeEventListener('mousemove', onDrag);
|
||||
const rect = p.getBoundingClientRect();
|
||||
try { localStorage[SP + 'guidePos'] = JSON.stringify({ left: rect.left, top: rect.top }); } catch (e) {}
|
||||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.getElementById('hv-guide-minimize').onclick = () => {
|
||||
const body = document.getElementById('hv-guide-body');
|
||||
const hidden = body.style.display === 'none';
|
||||
body.style.display = hidden ? 'block' : 'none';
|
||||
document.getElementById('hv-guide-minimize').textContent = hidden ? '🗕' : '📌';
|
||||
localStorage[SP + 'guideCollapsed'] = hidden ? '0' : '1';
|
||||
};
|
||||
|
||||
document.getElementById('hv-guide-close').onclick = () => p.remove();
|
||||
|
||||
if (nextAttr) highlightAttributeButton(nextAttr);
|
||||
|
||||
// Recalculate on + button clicks
|
||||
$$('[id$="_inc"]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
setTimeout(() => {
|
||||
$$('[id$="_inc"]').forEach(b => { b.style.outline = ''; b.title = ''; });
|
||||
const ny = getNextAttributeAdvice(detectFightingStyle());
|
||||
if (ny) highlightAttributeButton(ny);
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Difficulty banner in battle ──
|
||||
|
||||
function showDifficultyBanner() {
|
||||
if (!CFG.autoDifficulty) return;
|
||||
if (!isBattlePage()) return;
|
||||
if (document.getElementById('hv-diff-banner')) return;
|
||||
|
||||
const advice = getDifficultyAdvice();
|
||||
if (!advice.upgrade) return;
|
||||
|
||||
const banner = document.createElement('div');
|
||||
banner.id = 'hv-diff-banner';
|
||||
banner.style.cssText = css({
|
||||
position: 'fixed',
|
||||
top: '30px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: '99998',
|
||||
background: '#1a1a2e',
|
||||
color: '#fdcb00',
|
||||
padding: '6px 16px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'monospace',
|
||||
border: '1px solid #fdcb00',
|
||||
whiteSpace: 'pre-line',
|
||||
});
|
||||
banner.textContent = advice.message;
|
||||
document.body.appendChild(banner);
|
||||
setTimeout(() => { const b = document.getElementById('hv-diff-banner'); if (b) b.remove(); }, 5000);
|
||||
}
|
||||
14
src/header.user.js
Normal file
14
src/header.user.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// ==UserScript==
|
||||
// @name HV Unified
|
||||
// @namespace hvunified
|
||||
// @version 0.11.0
|
||||
// @description HV Unified — battle automation, guidance, and UI enhancements for HentaiVerse
|
||||
// @author GaboGG + Hermes
|
||||
// @match *://*.hentaiverse.org/*
|
||||
// @match *://alt.hentaiverse.org/*
|
||||
// @grant none
|
||||
// @run-at document-end
|
||||
// ==/UserScript==
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
27
src/hover-system.js
Normal file
27
src/hover-system.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// HOVER SYSTEM — mouse-over monster actions
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function setupHover() {
|
||||
if (!CFG.hoverEnabled) return;
|
||||
|
||||
STATE.monsters.forEach((m, i) => {
|
||||
if (!m || m.dataset.hvHover) return;
|
||||
m.dataset.hvHover = '1';
|
||||
|
||||
const area = m.querySelector('.btm6') || m;
|
||||
area.addEventListener('mouseenter', () => {
|
||||
STATE.hoverTarget = i;
|
||||
if (!STATE.interruptHover && !STATE.interruptAlert) {
|
||||
const ac = getSmartAction();
|
||||
if (ac) executeAction(ac);
|
||||
}
|
||||
});
|
||||
area.addEventListener('mouseleave', () => { STATE.hoverTarget = -1; });
|
||||
});
|
||||
|
||||
// RiddleMaster blocks hover
|
||||
if (document.getElementById('riddlemaster')) {
|
||||
STATE.interruptHover = true;
|
||||
}
|
||||
}
|
||||
117
src/init.js
Normal file
117
src/init.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// INIT — bootstrap everything
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function init() {
|
||||
loadConfig();
|
||||
detectLevel();
|
||||
STATE.page = detectPage();
|
||||
|
||||
// Always-on features
|
||||
setupRETimer();
|
||||
addConfigButton();
|
||||
renderProgressPanel();
|
||||
|
||||
if (STATE.page === 'battle') {
|
||||
initializeBattle();
|
||||
autoCheckTask('battle');
|
||||
} else {
|
||||
// Non-battle page enhancements
|
||||
if (STATE.page === 'itemshop') { enhanceItemShop(); autoCheckTask('buy-health'); }
|
||||
if (STATE.page === 'equipshop') { enhanceEquipShop(); enhanceEquipShopWithAdvice(); }
|
||||
if (STATE.page === 'shrine') enhanceShrine();
|
||||
if (STATE.page === 'training') { enhanceTraining(); autoCheckTask('training'); }
|
||||
if (STATE.page === 'monsterlab') { enhanceMonsterLab(); autoCheckTask('feed'); }
|
||||
if (STATE.page === 'arena') { autoCheckTask('arenas'); autoCheckTask('first-blood'); }
|
||||
if (STATE.page === 'character') showGuidancePanel();
|
||||
if (STATE.page === 'armory') enhanceArmory();
|
||||
if (STATE.page === 'abilities') enhanceAbilities();
|
||||
}
|
||||
|
||||
// Dawn initialization
|
||||
if (!localStorage[SP + 'lastDawn']) {
|
||||
try { localStorage[SP + 'lastDawn'] = JSON.stringify(Date.now()); } catch (e) {}
|
||||
}
|
||||
updateRETimer();
|
||||
|
||||
// Watch for dynamic page transitions (arena/grindfest → battle)
|
||||
const bodyObserver = new MutationObserver(() => {
|
||||
if (!STATE.inBattle && isBattlePage()) {
|
||||
initializeBattle();
|
||||
}
|
||||
});
|
||||
bodyObserver.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
function initializeBattle() {
|
||||
if (STATE.battleInitialized && STATE.page === 'battle') return;
|
||||
STATE.page = 'battle';
|
||||
STATE.inBattle = true;
|
||||
|
||||
parseBattleState();
|
||||
// Force tier update from cached level (battle page may not have level_readout)
|
||||
if (STATE.level > 1) updateTier();
|
||||
setupHover();
|
||||
setupKeybindings();
|
||||
addMonsterNumbers();
|
||||
refreshUI();
|
||||
addConfigButton();
|
||||
showDifficultyBanner();
|
||||
|
||||
// Watch battle log
|
||||
const log = document.getElementById('textlog');
|
||||
if (log && !log.dataset.hvObserved) {
|
||||
log.dataset.hvObserved = '1';
|
||||
const obs = new MutationObserver(() => {
|
||||
parseBattleState();
|
||||
saveMonsterData();
|
||||
logRound(); // Log this round's data for analysis
|
||||
if (CFG.hoverEnabled && !STATE.interruptHover && !STATE.interruptAlert && STATE.hoverTarget >= 0) {
|
||||
const a = getSmartAction();
|
||||
if (a) executeAction(a);
|
||||
}
|
||||
refreshUI();
|
||||
updateConfigButton();
|
||||
});
|
||||
obs.observe(log, { childList: true, subtree: true, characterData: true });
|
||||
}
|
||||
|
||||
// Watch vitals pane
|
||||
const vitals = document.getElementById('pane_vitals');
|
||||
if (vitals && !vitals.dataset.hvObserved) {
|
||||
vitals.dataset.hvObserved = '1';
|
||||
const vobs = new MutationObserver(() => {
|
||||
parseBattleState();
|
||||
logRound(); // Also log on vitals changes (catches end-of-round)
|
||||
refreshUI();
|
||||
updateConfigButton();
|
||||
// Check if battle ended (completion pane appeared)
|
||||
if (document.getElementById('pane_completion') && battleLog && !battleLog.summary) {
|
||||
finalizeBattleLog();
|
||||
}
|
||||
});
|
||||
vobs.observe(vitals, { childList: true, subtree: true, attributes: true });
|
||||
}
|
||||
|
||||
// RiddleMaster detection
|
||||
if (document.getElementById('riddlemaster')) {
|
||||
STATE.interruptHover = true;
|
||||
}
|
||||
|
||||
STATE.battleInitialized = true;
|
||||
}
|
||||
|
||||
// ── Bootstrap ──
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
console.log('%c🛡️ HV Unified v' + VERSION + '%c | Q=action H=hover C=cure ,=settings',
|
||||
'color:#fdcb00;font-weight:bold', '');
|
||||
console.log('%c HV.advice() for guidance | HV.difficulty() for difficulty check',
|
||||
'color:#888;font-size:10px');
|
||||
|
||||
})();
|
||||
53
src/items.js
Normal file
53
src/items.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// ITEMS — item ID mapping
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const ITEMS = {
|
||||
// Gems
|
||||
10005: { n: 'Health Gem', t: 'heal' },
|
||||
10006: { n: 'Mana Gem', t: 'mana' },
|
||||
10007: { n: 'Spirit Gem', t: 'spirit' },
|
||||
10008: { n: 'Mystic Gem', t: 'channel' },
|
||||
|
||||
// Health potions
|
||||
11191: { n: 'Health Draught', t: 'heal', cost: 25 },
|
||||
11195: { n: 'Health Potion', t: 'heal', cost: 50 },
|
||||
11199: { n: 'Health Elixir', t: 'heal', cost: 500 },
|
||||
|
||||
// Mana potions
|
||||
11291: { n: 'Mana Draught', t: 'mana', cost: 50 },
|
||||
11295: { n: 'Mana Potion', t: 'mana', cost: 100 },
|
||||
11299: { n: 'Mana Elixir', t: 'mana', cost: 1000 },
|
||||
|
||||
// Spirit potions
|
||||
11391: { n: 'Spirit Draught', t: 'spirit', cost: 50 },
|
||||
11395: { n: 'Spirit Potion', t: 'spirit', cost: 100 },
|
||||
11399: { n: 'Spirit Elixir', t: 'spirit', cost: 1000 },
|
||||
|
||||
// Crystals
|
||||
50001: { n: 'Crystal of Vigor', s: 'STR' },
|
||||
50002: { n: 'Crystal of Finesse', s: 'DEX' },
|
||||
50003: { n: 'Crystal of Swiftness', s: 'AGI' },
|
||||
50004: { n: 'Crystal of Fortitude', s: 'END' },
|
||||
50005: { n: 'Crystal of Cunning', s: 'INT' },
|
||||
50006: { n: 'Crystal of Knowledge', s: 'WIS' },
|
||||
};
|
||||
|
||||
function getItemType(id) {
|
||||
const item = ITEMS[parseInt(id)];
|
||||
return item ? item.t : null;
|
||||
}
|
||||
|
||||
function hasUsableGem(type) {
|
||||
for (const [id, info] of Object.entries(STATE.itemsKnown)) {
|
||||
const d = ITEMS[parseInt(info)];
|
||||
if (!d || d.t !== type) continue;
|
||||
// P slot (gem) only valid if it has content inside it
|
||||
if (id === 'p') {
|
||||
const p = document.getElementById('ikey_p');
|
||||
if (!p || !p.querySelector('div')) continue;
|
||||
}
|
||||
return { id, item: d };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
53
src/keybindings.js
Normal file
53
src/keybindings.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// KEYBINDINGS — keyboard shortcuts for battle
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
let keybindingsSetup = false;
|
||||
|
||||
function setupKeybindings() {
|
||||
if (keybindingsSetup) return;
|
||||
keybindingsSetup = true;
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
// Settings — comma key
|
||||
if (e.keyCode === KEYS.COMMA) {
|
||||
if (isBattlePage()) {
|
||||
e.preventDefault();
|
||||
toggleSettings();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Main action hotkey (default: Q)
|
||||
if (e.code === CFG.hotkey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
if (!isInputFocused() && isBattlePage()) {
|
||||
console.log('%c[HV] Q pressed — checking action...', 'color:#888');
|
||||
parseBattleState();
|
||||
const a = getRecommendedAction();
|
||||
if (a) {
|
||||
e.preventDefault();
|
||||
const label = a.target >= 0 ? 'monster ' + a.target : a.name || a.id;
|
||||
console.log(`%c[HV] ▶ ${a.type} → ${label}`, 'color:#0f0');
|
||||
executeAction(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hover toggle (H)
|
||||
if (e.code === 'KeyH' && !e.ctrlKey && !e.altKey && !e.metaKey && !isInputFocused()) {
|
||||
STATE.interruptHover = !STATE.interruptHover;
|
||||
console.log(`%c[HV] Hover ${STATE.interruptHover ? 'OFF' : 'ON'}`, 'color:#f80');
|
||||
}
|
||||
|
||||
// Emergency heal (C) — always cast Cure/Full-Cure regardless of strategy
|
||||
if (e.code === 'KeyC' && !e.ctrlKey && !e.altKey && !e.metaKey && !isInputFocused()) {
|
||||
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
||||
if (c) {
|
||||
console.log(`%c[HV] 🚑 Emergency: casting ${c}`, 'color:#f44');
|
||||
castSelfSpell(c);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('%c⌨ [HV] Keys: Q=action H=hover C=cure ,=settings', 'color:#0f0;font-size:11px');
|
||||
}
|
||||
83
src/knowledge-base.js
Normal file
83
src/knowledge-base.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// KNOWLEDGE BASE — game data, spell unlocks, milestones, stat advice
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const KB = {
|
||||
qualities: ['Crude', 'Fair', 'Average', 'Superior', 'Exquisite', 'Magnificent', 'Legendary', 'Peerless'],
|
||||
|
||||
// Stat priority per weapon type (order = importance)
|
||||
statPriority: {
|
||||
'1H': ['STR', 'END', 'DEX', 'AGI', 'WIS', 'INT'],
|
||||
'2H': ['STR', 'DEX', 'END', 'AGI', 'WIS', 'INT'],
|
||||
'DW': ['STR', 'DEX', 'AGI', 'END', 'WIS', 'INT'],
|
||||
'Niten': ['STR', 'DEX', 'AGI', 'END', 'WIS', 'INT'],
|
||||
'Staff': ['INT', 'WIS', 'END', 'AGI', 'DEX', 'STR'],
|
||||
},
|
||||
|
||||
// Forum note: stats "barely even matter". These are rough guidelines, not precise.
|
||||
statAllocation: {
|
||||
novice: {
|
||||
'1H': { STR: 30, END: 25, DEX: 20, AGI: 10, WIS: 10, INT: 5 },
|
||||
'Staff': { INT: 30, WIS: 25, END: 25, AGI: 10, DEX: 5, STR: 5 },
|
||||
},
|
||||
adept: {
|
||||
'1H': { STR: 35, END: 20, DEX: 20, AGI: 10, WIS: 10, INT: 5 },
|
||||
'Staff': { INT: 35, WIS: 25, END: 20, AGI: 10, DEX: 5, STR: 5 },
|
||||
},
|
||||
veteran: {
|
||||
'1H': { STR: 40, DEX: 25, END: 15, AGI: 10, WIS: 5, INT: 5 },
|
||||
'Staff': { INT: 40, WIS: 30, END: 15, AGI: 10, DEX: 3, STR: 2 },
|
||||
},
|
||||
master: {
|
||||
'1H': { STR: 45, DEX: 30, END: 10, AGI: 10, WIS: 3, INT: 2 },
|
||||
'Staff': { INT: 45, WIS: 35, END: 10, AGI: 5, DEX: 3, STR: 2 },
|
||||
},
|
||||
},
|
||||
|
||||
// Spell unlock levels (forum-corrected: no MagNet)
|
||||
spellUnlocks: [
|
||||
{ lvl: 5, name: 'Cure' },
|
||||
{ lvl: 10, name: 'Protection' },
|
||||
{ lvl: 15, name: 'Fiery Blast' },
|
||||
{ lvl: 25, name: 'Freeze' },
|
||||
{ lvl: 50, name: 'Regen' },
|
||||
{ lvl: 60, name: 'Haste' },
|
||||
{ lvl: 70, name: 'Weaken' },
|
||||
{ lvl: 130, name: 'Imperil' },
|
||||
{ lvl: 220, name: 'Full-Cure' },
|
||||
],
|
||||
|
||||
// IW Potency rankings (forum research data)
|
||||
iwPotency: {
|
||||
'1H': ['Butcher', 'Fatality', 'Overpower'],
|
||||
'2H': ['Overpower', 'Butcher', 'Fatality'],
|
||||
'DW': ['Overpower', 'Fatality', 'Butcher'],
|
||||
'Niten': ['Overpower', 'Fatality', 'Butcher'],
|
||||
'Staff': ['Economizer', 'Aether', 'Juggernaut'],
|
||||
},
|
||||
|
||||
// Difficulty EXP multipliers
|
||||
difficultyMult: {
|
||||
'Normal': 1,
|
||||
'Hard': 2,
|
||||
'Nightmare': 4,
|
||||
'Hell': 7,
|
||||
'Nintendo': 10,
|
||||
'IWBTH': 15,
|
||||
'PFUDOR': 20,
|
||||
},
|
||||
|
||||
// Milestone tips
|
||||
milestones: [
|
||||
{ lvl: 1, tip: 'Do Arena "First Blood". Use items before spells.' },
|
||||
{ lvl: 5, tip: 'Cure unlocked! Keep Health Draughts.' },
|
||||
{ lvl: 10, tip: 'Protection unlocked — set as autocast.' },
|
||||
{ lvl: 15, tip: 'First damage spell: Fiery Blast.' },
|
||||
{ lvl: 50, tip: 'Regen + Adept tier. Train Adept Learner.' },
|
||||
{ lvl: 60, tip: 'Haste unlocked — +50% action speed.' },
|
||||
{ lvl: 70, tip: 'Weaken: -50% enemy damage. Use items, then Cure.' },
|
||||
{ lvl: 100, tip: 'Level 100! T2 spells available. Accuracy target: 150%.' },
|
||||
{ lvl: 130, tip: 'Imperil — THE most important debuff. Always cast first.' },
|
||||
{ lvl: 300, tip: 'Master tier. Optimize for speed. Run PFUDOR.' },
|
||||
],
|
||||
};
|
||||
298
src/out-of-battle.js
Normal file
298
src/out-of-battle.js
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// OUT-OF-BATTLE — shop enhancements, shrine, training, etc.
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── Item Shop: quick-buy essentials + crystal labels ──
|
||||
|
||||
function enhanceItemShop() {
|
||||
if (document.getElementById('hv-itemshop-enh')) return;
|
||||
|
||||
const main = document.getElementById('mainpane');
|
||||
if (!main) { setTimeout(enhanceItemShop, 200); return; }
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'hv-itemshop-enh';
|
||||
bar.style.cssText = css({
|
||||
position: 'fixed',
|
||||
bottom: '30px',
|
||||
right: '4px',
|
||||
zIndex: '9995',
|
||||
background: '#111827',
|
||||
color: '#d1d5db',
|
||||
padding: '0',
|
||||
borderRadius: '8px',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'monospace',
|
||||
maxWidth: '320px',
|
||||
boxShadow: '0 0 15px rgba(0,0,0,0.6)',
|
||||
border: '1px solid #374151',
|
||||
});
|
||||
|
||||
// Inventory counts from left pane
|
||||
const inventory = {};
|
||||
$$('#item_pane .itemlist tr', main).forEach(row => {
|
||||
const itemDiv = row.querySelector('[id^="item_"]');
|
||||
const countTd = row.querySelector('td:last-child');
|
||||
if (itemDiv && countTd) {
|
||||
const id = parseInt(itemDiv.id.replace('item_', ''));
|
||||
inventory[id] = parseInt(countTd.textContent) || 0;
|
||||
}
|
||||
});
|
||||
|
||||
const essentials = [
|
||||
{ id: 11191, label: 'Health Draught (25c)', warn: 2 },
|
||||
{ id: 11195, label: 'Health Potion (50c)', warn: 1 },
|
||||
{ id: 11291, label: 'Mana Draught (50c)', warn: 1 },
|
||||
{ id: 11295, label: 'Mana Potion (100c)', warn: 0 },
|
||||
];
|
||||
|
||||
let bodyHtml = '';
|
||||
for (const e of essentials) {
|
||||
const inInv = inventory[e.id] || 0;
|
||||
const urgent = inInv <= e.warn;
|
||||
bodyHtml += `<div style="display:flex;justify-content:space-between;padding:2px 0;cursor:pointer"
|
||||
data-item="${e.id}" class="hv-shop-buy">
|
||||
<span style="color:${urgent ? '#f88' : '#8f8'}">${urgent ? '⬆ ' : '✓ '}${e.label}</span>
|
||||
<span style="color:#888">have: ${inInv}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
bodyHtml += '<div style="border-top:1px solid #374151;margin:4px 0;padding:4px 0;font-size:9px;color:#888">' +
|
||||
'💎 Crystals: <span style="color:#fdcb00">Vigor=STR</span> <span style="color:#0f0">Finesse=DEX</span> ' +
|
||||
'<span style="color:#8af">Swift=AGI</span> <span style="color:#f80">Fort=END</span> ' +
|
||||
'<span style="color:#f0f">Cunn=INT</span> <span style="color:#0ff">Know=WIS</span></div>';
|
||||
|
||||
const collapsed = localStorage[SP + 'shopCollapsed'] === '1';
|
||||
bar.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center;
|
||||
padding:6px 8px;cursor:pointer" id="hv-shop-header">
|
||||
<b style="color:#fdcb00;font-size:10px">🛒 Shop</b>
|
||||
<span id="hv-shop-toggle" style="color:#888;font-size:12px">${collapsed ? '▶' : '▼'}</span>
|
||||
</div>
|
||||
<div id="hv-shop-body" style="padding:0 8px 6px;display:${collapsed ? 'none' : 'block'}">${bodyHtml}</div>`;
|
||||
|
||||
bar.querySelector('#hv-shop-header').onclick = () => {
|
||||
const body = document.getElementById('hv-shop-body');
|
||||
const toggle = document.getElementById('hv-shop-toggle');
|
||||
const isHidden = body.style.display === 'none';
|
||||
body.style.display = isHidden ? 'block' : 'none';
|
||||
toggle.textContent = isHidden ? '▼' : '▶';
|
||||
localStorage[SP + 'shopCollapsed'] = isHidden ? '0' : '1';
|
||||
};
|
||||
|
||||
document.body.appendChild(bar);
|
||||
|
||||
bar.addEventListener('click', e => {
|
||||
const row = e.target.closest('.hv-shop-buy');
|
||||
if (!row) return;
|
||||
const id = row.dataset.item;
|
||||
const shopItem = document.querySelector('#shop_pane #item_' + id);
|
||||
if (shopItem) shopItem.click();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Equip Shop: quick sell/salvage buttons ──
|
||||
|
||||
function enhanceEquipShop() {
|
||||
if (!CFG.autoSell) return;
|
||||
const m = document.getElementById('mainpane');
|
||||
if (!m || document.getElementById('hv-quick-sell')) return;
|
||||
|
||||
const r = document.createElement('div');
|
||||
r.id = 'hv-quick-sell';
|
||||
r.style.cssText = 'margin:6px;display:flex;gap:6px;flex-wrap:wrap';
|
||||
|
||||
['Crude', 'Fair', 'Average', 'Superior'].forEach(q => {
|
||||
const b = document.createElement('input');
|
||||
b.type = 'button';
|
||||
b.value = `🔻 Sell ≤${q}`;
|
||||
b.style.cssText = 'padding:3px 8px;cursor:pointer;background:#d50c2d;color:#fff;border:none;border-radius:3px;font-size:11px';
|
||||
b.onclick = () => {
|
||||
$$('tr', m).forEach(row => {
|
||||
const t = (row.textContent || '');
|
||||
const qi = ['Crude', 'Fair', 'Average', 'Superior'].indexOf(q);
|
||||
if (['Crude', 'Fair', 'Average', 'Superior'].some((ql, i) => i <= qi && t.includes(ql)) && t.includes('Sell')) {
|
||||
const sb = row.querySelector('input[value="Sell"]');
|
||||
if (sb) sb.click();
|
||||
}
|
||||
});
|
||||
};
|
||||
r.appendChild(b);
|
||||
});
|
||||
|
||||
const s = document.createElement('input');
|
||||
s.type = 'button';
|
||||
s.value = '♻ Salvage ≤Average';
|
||||
s.style.cssText = 'padding:3px 8px;cursor:pointer;background:#555;color:#fff;border:none;border-radius:3px;font-size:11px';
|
||||
s.onclick = () => {
|
||||
$$('tr', m).forEach(row => {
|
||||
const t = (row.textContent || '');
|
||||
if ((t.includes('Crude') || t.includes('Fair') || t.includes('Average')) && t.includes('Salvage')) {
|
||||
const sb = row.querySelector('input[value="Salvage"]');
|
||||
if (sb) sb.click();
|
||||
}
|
||||
});
|
||||
};
|
||||
r.appendChild(s);
|
||||
|
||||
const ta = m.querySelector('div');
|
||||
if (ta) ta.insertBefore(r, ta.firstChild);
|
||||
}
|
||||
|
||||
// ── Equip advice: KEEP/SELL tags ──
|
||||
|
||||
function evaluateEquipment(text) {
|
||||
const t = (text || '').toLowerCase();
|
||||
for (const q of KB.qualities) {
|
||||
if (t.includes(q.toLowerCase())) {
|
||||
const qi = KB.qualities.indexOf(q);
|
||||
if (qi >= 5) return { verdict: 'keep', reason: 'Magnificent+ — keep' };
|
||||
if (qi <= 2) return { verdict: 'sell', reason: 'Low quality — sell' };
|
||||
return { verdict: 'keep', reason: 'Usable' };
|
||||
}
|
||||
}
|
||||
return { verdict: 'keep', reason: '' };
|
||||
}
|
||||
|
||||
function enhanceEquipShopWithAdvice() {
|
||||
if (!CFG.showEquipAdvice) return;
|
||||
const m = document.getElementById('mainpane');
|
||||
if (!m) return;
|
||||
|
||||
$$('tr', m).forEach(row => {
|
||||
if (row.querySelector('.hv-equip-advice')) return;
|
||||
const t = row.textContent || '';
|
||||
if (!t.includes('Sell') && !t.includes('Salvage')) return;
|
||||
|
||||
const ev = evaluateEquipment(t);
|
||||
const d = document.createElement('span');
|
||||
d.className = 'hv-equip-advice';
|
||||
const col = ev.verdict === 'keep' ? '#0f0' : '#f80';
|
||||
d.style.cssText = `margin-left:6px;font-size:9px;color:${col};font-weight:bold`;
|
||||
d.textContent = ev.verdict === 'keep' ? '✓ KEEP' : '💰 SELL';
|
||||
d.title = ev.reason;
|
||||
const td = row.querySelector('td:last-child');
|
||||
if (td) td.appendChild(d);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Shrine: bulk selection ──
|
||||
|
||||
function enhanceShrine() {
|
||||
if (!CFG.bulkShrine) return;
|
||||
const m = document.getElementById('mainpane');
|
||||
if (!m || document.getElementById('hv-bulk-shrine')) return;
|
||||
|
||||
const r = document.createElement('div');
|
||||
r.id = 'hv-bulk-shrine';
|
||||
r.style.cssText = 'margin:6px;display:flex;gap:6px';
|
||||
|
||||
const b1 = document.createElement('input');
|
||||
b1.type = 'button';
|
||||
b1.value = '⛩️ Select Non-Figurines';
|
||||
b1.style.cssText = 'padding:4px 10px;cursor:pointer;background:#fdcb00;color:#000;border:none;border-radius:3px;font-size:12px';
|
||||
b1.onclick = () => {
|
||||
$$('tr', m).forEach(row => {
|
||||
const t = (row.textContent || '');
|
||||
if (!t.includes('Figurine') && !t.includes('Collectable')) {
|
||||
const cb = row.querySelector('input[type="checkbox"]');
|
||||
if (cb) cb.checked = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const b2 = document.createElement('input');
|
||||
b2.type = 'button';
|
||||
b2.value = '✗ Clear';
|
||||
b2.style.cssText = 'padding:4px 10px;cursor:pointer;background:#666;color:#fff;border:none;border-radius:3px;font-size:12px';
|
||||
b2.onclick = () => { $$('input[type="checkbox"]', m).forEach(cb => cb.checked = false); };
|
||||
|
||||
r.appendChild(b1);
|
||||
r.appendChild(b2);
|
||||
const ta = m.querySelector('div');
|
||||
if (ta) ta.insertBefore(r, ta.firstChild);
|
||||
}
|
||||
|
||||
// ── Training: priority guide ──
|
||||
|
||||
function enhanceTraining() {
|
||||
if (!CFG.trainingQueue) return;
|
||||
const m = document.getElementById('mainpane');
|
||||
if (!m || document.getElementById('hv-training-enh')) return;
|
||||
|
||||
const d = document.createElement('div');
|
||||
d.id = 'hv-training-enh';
|
||||
d.style.cssText = 'margin:8px;padding:8px;background:#1a2a1a;border-radius:4px;font-size:11px;color:#8f8';
|
||||
d.innerHTML = '<b>📋 Training Priority</b>: Adept Learner → Scavenger → Ability Boost → Quartermaster<br>' +
|
||||
'<small style="color:#888">Train cheapest available. AL to Lv100+, then damage.</small>';
|
||||
const ta = m.querySelector('div');
|
||||
if (ta) ta.insertBefore(d, ta.firstChild);
|
||||
}
|
||||
|
||||
// ── Monster Lab: feed all ──
|
||||
|
||||
function enhanceMonsterLab() {
|
||||
const m = document.getElementById('mainpane');
|
||||
if (!m || document.getElementById('hv-lab-enhance')) return;
|
||||
|
||||
const r = document.createElement('div');
|
||||
r.id = 'hv-lab-enhance';
|
||||
r.style.cssText = 'margin:8px;padding:8px;display:flex;gap:8px';
|
||||
|
||||
const f = document.createElement('input');
|
||||
f.type = 'button';
|
||||
f.value = '🍖 Feed All';
|
||||
f.style.cssText = 'padding:4px 10px;cursor:pointer;background:#2a5a2a;color:#fff;border:none;border-radius:3px;font-size:11px';
|
||||
f.onclick = () => { $$('input[value="Feed"]', m).forEach(b => b.click()); };
|
||||
|
||||
const p = document.createElement('input');
|
||||
p.type = 'button';
|
||||
p.value = '💊 Happy Pills All';
|
||||
p.style.cssText = 'padding:4px 10px;cursor:pointer;background:#5a3a2a;color:#fff;border:none;border-radius:3px;font-size:11px';
|
||||
p.onclick = () => {
|
||||
const pills = $$('option', m).filter(o => (o.textContent || '').includes('Happy Pill'));
|
||||
if (pills.length > 0) {
|
||||
pills[0].selected = true;
|
||||
$$('input[value="Feed"]', m).forEach(b => b.click());
|
||||
}
|
||||
};
|
||||
|
||||
r.appendChild(f);
|
||||
r.appendChild(p);
|
||||
const ta = m.querySelector('div');
|
||||
if (ta) ta.insertBefore(r, ta.firstChild);
|
||||
}
|
||||
|
||||
// ── Config button (bottom-right) ──
|
||||
|
||||
function addConfigButton() {
|
||||
if (!CFG.cfgButton) return;
|
||||
if (document.getElementById('hv-cfg-btn')) return updateConfigButton();
|
||||
|
||||
const b = document.createElement('div');
|
||||
b.id = 'hv-cfg-btn';
|
||||
b.style.cssText = css({
|
||||
position: 'fixed',
|
||||
bottom: '4px',
|
||||
right: '4px',
|
||||
zIndex: '9998',
|
||||
background: '#1a1a2e',
|
||||
color: '#fdcb00',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'monospace',
|
||||
cursor: 'pointer',
|
||||
opacity: '0.7',
|
||||
});
|
||||
b.textContent = `⚙ HV v${VERSION} | ${STATE.tier.toUpperCase()} | Lv${STATE.level}`;
|
||||
b.title = 'Click for settings, press , in battle';
|
||||
b.onclick = toggleSettings;
|
||||
document.body.appendChild(b);
|
||||
}
|
||||
|
||||
function updateConfigButton() {
|
||||
const b = document.getElementById('hv-cfg-btn');
|
||||
if (!b) return;
|
||||
const diff = STATE.difficulty || 'Normal';
|
||||
b.textContent = `⚙ HV v${VERSION} | ${STATE.tier.toUpperCase()} | Lv${STATE.level} | ${diff}`;
|
||||
}
|
||||
99
src/page-detector.js
Normal file
99
src/page-detector.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// PAGE DETECTOR — detect current page and player level
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function detectPage() {
|
||||
// Battle page detection by DOM elements (most reliable)
|
||||
if (document.getElementById('textlog') ||
|
||||
document.getElementById('riddlemaster') ||
|
||||
document.getElementById('pane_vitals') ||
|
||||
document.getElementById('battle_root')) {
|
||||
return 'battle';
|
||||
}
|
||||
|
||||
// URL-based page detection
|
||||
const url = window.location.href || '';
|
||||
const pages = {
|
||||
'ss=ch': 'character',
|
||||
'ss=eq': 'equipshop',
|
||||
'ss=is': 'itemshop',
|
||||
'ss=am': 'armory',
|
||||
'ss=ar': 'arena',
|
||||
'ss=gr': 'grindfest',
|
||||
'ss=iw': 'itemworld',
|
||||
'ss=ml': 'monsterlab',
|
||||
'ss=sh': 'shrine',
|
||||
'ss=mm': 'mooglemail',
|
||||
'ss=tr': 'training',
|
||||
'ss=rb': 'ring',
|
||||
'ss=re': 'repair',
|
||||
'ss=up': 'upgrade',
|
||||
'ss=en': 'enchant',
|
||||
'ss=sv': 'salvage',
|
||||
'ss=rf': 'reforge',
|
||||
'ss=sf': 'soulfuse',
|
||||
'ss=ab': 'abilities',
|
||||
'ss=it': 'invitems',
|
||||
'ss=se': 'settings',
|
||||
'ss=ss': 'spiritshop',
|
||||
'ss=mk': 'market',
|
||||
'ss=lt': 'lottery',
|
||||
'ss=la': 'lastarena',
|
||||
};
|
||||
for (const [param, page] of Object.entries(pages)) {
|
||||
if (url.includes(param)) return page;
|
||||
}
|
||||
|
||||
return 'bazaar';
|
||||
}
|
||||
|
||||
function isBattlePage() {
|
||||
return !!(document.getElementById('textlog') ||
|
||||
document.getElementById('riddlemaster') ||
|
||||
document.getElementById('pane_vitals'));
|
||||
}
|
||||
|
||||
function detectLevel() {
|
||||
const container = document.querySelector('#level_readout > div');
|
||||
if (!container) {
|
||||
try {
|
||||
const s = parseInt(localStorage[SP + 'playerLevel']);
|
||||
if (s > 0) STATE.level = s;
|
||||
} catch (e) {}
|
||||
return;
|
||||
}
|
||||
|
||||
// Battle page format: innerText = "Normal Lv.10"
|
||||
const text = (container.innerText || container.textContent || '').trim();
|
||||
const textMatch = text.match(/(\w+)\s+Lv\.?\s*(\d+)/);
|
||||
if (textMatch) {
|
||||
STATE.difficulty = textMatch[1];
|
||||
STATE.level = parseInt(textMatch[2]) || 1;
|
||||
updateTier();
|
||||
return;
|
||||
}
|
||||
|
||||
// CSS-font rendering (overworld page): "Nightmare Lv.52"
|
||||
const cssText = readCSSText(container);
|
||||
const cssMatch = cssText.match(/(\w+)\s+lv\.\s*(\d+)/i);
|
||||
if (cssMatch) {
|
||||
STATE.difficulty = cssMatch[1].charAt(0).toUpperCase() + cssMatch[1].slice(1);
|
||||
cacheDifficulty();
|
||||
}
|
||||
|
||||
// CSS-digit rendering for level number
|
||||
const digits = readCSSDigits(container);
|
||||
if (digits !== null && digits > 0) {
|
||||
STATE.level = digits;
|
||||
updateTier();
|
||||
}
|
||||
|
||||
if (STATE.level > 1) {
|
||||
try { localStorage[SP + 'playerLevel'] = STATE.level; } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTier() {
|
||||
const lv = STATE.level || 1;
|
||||
STATE.tier = lv >= 300 ? 'master' : lv >= 150 ? 'veteran' : lv >= 50 ? 'adept' : 'novice';
|
||||
}
|
||||
138
src/progress-tracker.js
Normal file
138
src/progress-tracker.js
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// PROGRESS TRACKER — daily task checklist on all pages
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function buildTaskList() {
|
||||
const today = new Date().toDateString();
|
||||
const saved = JSON.parse(localStorage[SP + 'tasks'] || '{"_date":"","done":[],"stats":{}}');
|
||||
if (saved._date !== today) { saved._date = today; saved.done = []; saved.stats = {}; }
|
||||
|
||||
const tasks = [];
|
||||
const tier = STATE.tier;
|
||||
const lv = STATE.level;
|
||||
|
||||
// Core tasks (all tiers)
|
||||
tasks.push({ id: 'arenas', icon: '⚔', text: 'Clear all available Arenas', tier: 'all', urgent: true });
|
||||
tasks.push({ id: 'battle', icon: '👊', text: 'Complete at least one battle', tier: 'all', urgent: false });
|
||||
tasks.push({ id: 'feed', icon: '🍖', text: 'Feed monsters in Monster Lab', tier: 'all', urgent: false });
|
||||
tasks.push({ id: 'training', icon: '🎓', text: 'Start a new Training session', tier: 'all', urgent: true });
|
||||
|
||||
// Novice-specific
|
||||
if (tier === 'novice') {
|
||||
tasks.push({ id: 'items', icon: '🧪', text: 'Keep Health/Mana Draughts stocked', detail: 'Items > spells', tier: 'novice', urgent: true });
|
||||
tasks.push({ id: 'first-blood', icon: '⚔', text: 'Clear "First Blood" arena daily', detail: '100 credits, 2 rounds', tier: 'novice', urgent: true });
|
||||
tasks.push({ id: 'normal-diff', icon: '⚠', text: 'Stay on Normal difficulty', tier: 'novice', urgent: false });
|
||||
}
|
||||
|
||||
// Adept
|
||||
if (tier === 'adept') {
|
||||
tasks.push({ id: 'hard-mode', icon: '⬆', text: 'Switch to Hard difficulty', tier: 'adept', urgent: false });
|
||||
tasks.push({ id: 'scavenger', icon: '🎓', text: 'Train Scavenger to Lv25', tier: 'adept', urgent: false });
|
||||
}
|
||||
|
||||
// Veteran
|
||||
if (tier === 'veteran') {
|
||||
tasks.push({ id: 'pfudor', icon: '💀', text: 'Run Grindfest on PFUDOR', tier: 'veteran', urgent: false });
|
||||
tasks.push({ id: 'spirit-stance', icon: '✨', text: 'Use Spirit Stance when OC > 70%', tier: 'veteran', urgent: false });
|
||||
}
|
||||
|
||||
// Master
|
||||
if (tier === 'master') {
|
||||
tasks.push({ id: 'all-arenas', icon: '🏆', text: 'Clear all 17 arenas daily', tier: 'master', urgent: true });
|
||||
tasks.push({ id: 'tower', icon: '🗼', text: 'Progress in the Tower', tier: 'master', urgent: false });
|
||||
}
|
||||
|
||||
// Tips
|
||||
tasks.push({ id: 'dawn', icon: '🌅', text: 'Battles reset at Dawn (~midnight UTC)', tier: 'all', urgent: false, tip: true });
|
||||
tasks.push({ id: 'hath', icon: '💰', text: 'H@H generates Hath passively', tier: 'all', urgent: false, tip: true });
|
||||
|
||||
return { tasks, saved };
|
||||
}
|
||||
|
||||
function renderProgressPanel() {
|
||||
if (document.getElementById('hv-progress')) return;
|
||||
|
||||
const { tasks, saved } = buildTaskList();
|
||||
const done = saved.done || [];
|
||||
const doneCount = done.length;
|
||||
const totalCount = tasks.filter(t => !t.tip).length;
|
||||
const collapsed = localStorage[SP + 'progressCollapsed'] === '1';
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'hv-progress';
|
||||
panel.style.cssText = css({
|
||||
position: 'fixed',
|
||||
top: '60px',
|
||||
right: '4px',
|
||||
zIndex: '9997',
|
||||
background: '#111827',
|
||||
color: '#d1d5db',
|
||||
padding: '8px 10px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'monospace',
|
||||
maxWidth: '320px',
|
||||
boxShadow: '0 0 15px rgba(0,0,0,0.6)',
|
||||
border: '1px solid #374151',
|
||||
maxHeight: '70vh',
|
||||
overflowY: 'auto',
|
||||
});
|
||||
|
||||
panel.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;cursor:pointer" id="hv-progress-header">
|
||||
<b style="color:#fdcb00;font-size:11px">📋 Today: ${doneCount}/${totalCount}</b>
|
||||
<span id="hv-progress-toggle" style="color:#888;font-size:14px">${collapsed ? '▶' : '▼'}</span>
|
||||
</div>
|
||||
<div id="hv-progress-body" style="display:${collapsed ? 'none' : 'block'}">${
|
||||
tasks.map(t => {
|
||||
const isDone = done.includes(t.id);
|
||||
const style = isDone ? 'text-decoration:line-through;color:#666'
|
||||
: t.urgent ? 'color:#fdcb00'
|
||||
: t.tip ? 'color:#888;font-style:italic'
|
||||
: 'color:#9ca3af';
|
||||
const cb = isDone ? '☑' : '☐';
|
||||
return `<div style="padding:2px 0;${style};cursor:${t.tip ? 'default' : 'pointer'}"
|
||||
data-task="${t.id}" class="hv-task-item">
|
||||
${cb} ${t.icon} ${t.text}
|
||||
${t.detail ? `<br><span style="margin-left:18px;font-size:9px;color:#666">↳ ${t.detail}</span>` : ''}
|
||||
</div>`;
|
||||
}).join('')
|
||||
}</div>`;
|
||||
|
||||
document.body.appendChild(panel);
|
||||
|
||||
document.getElementById('hv-progress-header').onclick = () => {
|
||||
const body = document.getElementById('hv-progress-body');
|
||||
const toggle = document.getElementById('hv-progress-toggle');
|
||||
const hidden = body.style.display === 'none';
|
||||
body.style.display = hidden ? 'block' : 'none';
|
||||
toggle.textContent = hidden ? '▼' : '▶';
|
||||
localStorage[SP + 'progressCollapsed'] = hidden ? '0' : '1';
|
||||
};
|
||||
|
||||
panel.querySelectorAll('.hv-task-item').forEach(el => {
|
||||
if (el.dataset.task === 'dawn' || el.dataset.task === 'hath') return;
|
||||
el.addEventListener('click', () => {
|
||||
const id = el.dataset.task;
|
||||
const s = JSON.parse(localStorage[SP + 'tasks'] || '{}');
|
||||
if (!s.done) s.done = [];
|
||||
const idx = s.done.indexOf(id);
|
||||
if (idx >= 0) s.done.splice(idx, 1);
|
||||
else s.done.push(id);
|
||||
localStorage[SP + 'tasks'] = JSON.stringify(s);
|
||||
el.remove();
|
||||
panel.remove();
|
||||
renderProgressPanel();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function autoCheckTask(taskId) {
|
||||
const saved = JSON.parse(localStorage[SP + 'tasks'] || '{}');
|
||||
if (!saved.done) saved.done = [];
|
||||
if (!saved.done.includes(taskId)) {
|
||||
saved.done.push(taskId);
|
||||
localStorage[SP + 'tasks'] = JSON.stringify(saved);
|
||||
const panel = document.getElementById('hv-progress');
|
||||
if (panel) { panel.remove(); renderProgressPanel(); }
|
||||
}
|
||||
}
|
||||
23
src/public-api.js
Normal file
23
src/public-api.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// PUBLIC API — window.HV for console power users
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
window.HV = {
|
||||
state: () => STATE,
|
||||
config: () => CFG,
|
||||
set: (k, v) => setConfig(k, v),
|
||||
action: () => getRecommendedAction(),
|
||||
execute: (a) => executeAction(a || getRecommendedAction()),
|
||||
tier: () => STATE.tier,
|
||||
stats: () => JSON.parse(localStorage[SP + 'stats'] || '{"battles":0,"credits":0,"drops":0}'),
|
||||
settings: () => toggleSettings(),
|
||||
advice: () => {
|
||||
const s = detectFightingStyle();
|
||||
return { style: s, attrs: getAttrAdvice(s), spells: getSpellAdvice(), difficulty: getDifficultyAdvice() };
|
||||
},
|
||||
difficulty: () => getDifficultyAdvice(),
|
||||
// Battle log analysis
|
||||
log: () => getBattleLog(),
|
||||
summary: () => getBattleSummary(),
|
||||
saveLog: () => {}, // Auto-saves in real-time now
|
||||
};
|
||||
58
src/re-timer.js
Normal file
58
src/re-timer.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// RE TIMER — Random Encounter countdown
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
let reTimerEl = null;
|
||||
|
||||
function setupRETimer() {
|
||||
if (!CFG.reTimer) return;
|
||||
if (document.getElementById('hv-re-timer')) return;
|
||||
|
||||
reTimerEl = document.createElement('div');
|
||||
reTimerEl.id = 'hv-re-timer';
|
||||
reTimerEl.style.cssText = css({
|
||||
position: 'fixed',
|
||||
top: '2px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: '99999',
|
||||
background: '#1a1a2e',
|
||||
color: '#0f0',
|
||||
padding: '2px 10px',
|
||||
borderRadius: '0 0 6px 6px',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'monospace',
|
||||
opacity: '0.9',
|
||||
});
|
||||
reTimerEl.textContent = 'RE: --:--';
|
||||
document.body.appendChild(reTimerEl);
|
||||
updateRETimer();
|
||||
setInterval(updateRETimer, 30000);
|
||||
}
|
||||
|
||||
function updateRETimer() {
|
||||
if (!reTimerEl) return;
|
||||
const now = Date.now();
|
||||
const dt = document.body.textContent || '';
|
||||
|
||||
// Detect dawn
|
||||
if (dt.includes('Dawn of a New Day') || dt.includes('A new day dawns')) {
|
||||
try { localStorage[SP + 'lastDawn'] = JSON.stringify(now); } catch (e) {}
|
||||
}
|
||||
|
||||
const ld = JSON.parse(localStorage[SP + 'lastDawn'] || now);
|
||||
const ms = (now - ld) / 60000;
|
||||
const nr = 30 - (ms % 30);
|
||||
|
||||
if (nr < 1) {
|
||||
reTimerEl.textContent = '⚡ RE READY!';
|
||||
reTimerEl.style.color = '#f00';
|
||||
reTimerEl.style.fontWeight = 'bold';
|
||||
} else {
|
||||
const mn = Math.floor(nr);
|
||||
const sc = Math.floor((nr - mn) * 60);
|
||||
reTimerEl.textContent = `RE: ${String(mn).padStart(2, '0')}:${String(sc).padStart(2, '0')}`;
|
||||
reTimerEl.style.color = '#0f0';
|
||||
reTimerEl.style.fontWeight = 'normal';
|
||||
}
|
||||
}
|
||||
105
src/settings-panel.js
Normal file
105
src/settings-panel.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// SETTINGS PANEL — in-battle configuration UI
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function toggleSettings() {
|
||||
if (STATE.settingsVisible) { closeSettings(); } else { openSettings(); }
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
const p = document.getElementById('hv-settings-panel');
|
||||
if (p) p.remove();
|
||||
STATE.settingsVisible = false;
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
if (document.getElementById('hv-settings-panel')) return;
|
||||
STATE.settingsVisible = true;
|
||||
|
||||
const p = document.createElement('div');
|
||||
p.id = 'hv-settings-panel';
|
||||
p.style.cssText = css({
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '520px',
|
||||
maxHeight: '80vh',
|
||||
overflowY: 'auto',
|
||||
background: '#1a1a2e',
|
||||
color: '#ccc',
|
||||
zIndex: '99999',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
boxShadow: '0 0 30px rgba(0,0,0,0.8)',
|
||||
});
|
||||
|
||||
const mkTog = (l, k) =>
|
||||
`<label style="display:flex;align-items:center;margin:3px 0;cursor:pointer">
|
||||
<input type="checkbox" ${CFG[k] ? 'checked' : ''} data-key="${k}" style="margin-right:6px">
|
||||
<span>${l}</span>
|
||||
</label>`;
|
||||
|
||||
const mkNum = (l, k, st) =>
|
||||
`<div style="margin:3px 0;display:flex;align-items:center">
|
||||
<span style="width:190px;flex-shrink:0">${l}</span>
|
||||
<input type="number" value="${CFG[k]}" data-key="${k}" step="${st || 0.05}"
|
||||
style="width:55px;background:#333;color:#ccc;border:1px solid #555;border-radius:3px;padding:2px 4px">
|
||||
</div>`;
|
||||
|
||||
p.innerHTML = `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<b style="color:#fdcb00;font-size:14px">⚙ HV Unified v${VERSION}</b>
|
||||
<button id="hv-settings-close" style="background:#d50c2d;color:#fff;border:none;border-radius:4px;padding:4px 10px;cursor:pointer">✕</button>
|
||||
</div>
|
||||
<div style="border-top:1px solid #444;margin:8px 0"></div>
|
||||
<b style="color:#0f0">Battle</b>
|
||||
${mkTog('Auto-buff (Haste, Protection, etc.)', 'autoBuff')}
|
||||
${mkTog('Auto-debuff (Imperil, Weaken)', 'autoDebuff')}
|
||||
${mkTog('Auto-cure when HP low', 'autoCure')}
|
||||
${mkTog('Auto Spirit Stance', 'autoSpirit')}
|
||||
${mkTog('Hover-to-attack mode', 'hoverEnabled')}
|
||||
${mkTog('Auto-difficulty suggestion', 'autoDifficulty')}
|
||||
${mkTog('Use attack spells', 'useAttackSpells')}
|
||||
${mkNum('Cure HP threshold', 'cureHP')}
|
||||
${mkNum('Item HP threshold', 'cureItemHP')}
|
||||
${mkNum('Mana gem MP threshold', 'manaGemMP')}
|
||||
${mkNum('Mana potion MP threshold', 'manaPotionMP')}
|
||||
${mkNum('Spirit Stance OC%', 'spiritStanceOC', 5)}
|
||||
<div style="border-top:1px solid #444;margin:8px 0"></div>
|
||||
<b style="color:#0f0">Out of Battle</b>
|
||||
${mkTog('Equip quick sell buttons', 'autoSell')}
|
||||
${mkTog('Bulk Shrine buttons', 'bulkShrine')}
|
||||
${mkTog('RE Timer', 'reTimer')}
|
||||
${mkTog('Training queue info', 'trainingQueue')}
|
||||
${mkTog('Advisor panel', 'showGuidance')}
|
||||
${mkTog('Equip KEEP/SELL tags', 'showEquipAdvice')}
|
||||
<div style="border-top:1px solid #444;margin:8px 0"></div>
|
||||
<b style="color:#0f0">UI</b>
|
||||
${mkTog('Cooldown timers', 'showCooldowns')}
|
||||
${mkTog('Monster HP numbers', 'showMonsterHP')}
|
||||
${mkTog('Buff duration counters', 'showDurations')}
|
||||
${mkTog('Alert colours', 'alertColours')}
|
||||
${mkTog('Monster numbers', 'showMonsterNumbers')}
|
||||
${mkTog('Config button', 'cfgButton')}
|
||||
<div style="margin-top:12px;text-align:center;color:#666;font-size:10px">
|
||||
Changes apply immediately. Press <b>,</b> in battle to open settings.
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(p);
|
||||
|
||||
document.getElementById('hv-settings-close').onclick = closeSettings;
|
||||
|
||||
$$('input', p).forEach(inp => {
|
||||
inp.addEventListener('change', () => {
|
||||
const k = inp.dataset.key;
|
||||
if (!k || CFG[k] === undefined) return;
|
||||
if (inp.type === 'checkbox') CFG[k] = inp.checked;
|
||||
else CFG[k] = parseFloat(inp.value) || CFG[k];
|
||||
saveConfig();
|
||||
refreshUI();
|
||||
});
|
||||
});
|
||||
}
|
||||
140
src/state.js
Normal file
140
src/state.js
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// STATE — global runtime state
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const SP = 'hvunified_';
|
||||
|
||||
const STATE = {
|
||||
page: null,
|
||||
level: 1,
|
||||
tier: 'novice',
|
||||
isekai: false,
|
||||
battleMode: '',
|
||||
inBattle: false,
|
||||
battleInitialized: false,
|
||||
|
||||
hoverTarget: -1,
|
||||
interruptHover: false,
|
||||
interruptAlert: false,
|
||||
|
||||
monsters: [],
|
||||
hp: 1,
|
||||
mp: 1,
|
||||
sp: 1,
|
||||
oc: 0,
|
||||
spiritStance: false,
|
||||
|
||||
buffs: {},
|
||||
spellsKnown: [],
|
||||
skillsKnown: [],
|
||||
itemsKnown: {},
|
||||
cooldowns: {},
|
||||
|
||||
battleMonsterNames: [],
|
||||
channeling: false,
|
||||
monsterData: {},
|
||||
difficulty: 'Normal',
|
||||
|
||||
battleStats: {
|
||||
damageDealt: 0,
|
||||
damageTaken: 0,
|
||||
turns: 0,
|
||||
drops: 0,
|
||||
credits: 0,
|
||||
},
|
||||
|
||||
round: 0,
|
||||
settingsVisible: false,
|
||||
};
|
||||
|
||||
// Load monster data from localStorage
|
||||
try {
|
||||
STATE.monsterData = JSON.parse(localStorage[SP + 'monsters'] || '{}');
|
||||
} catch (e) {
|
||||
STATE.monsterData = {};
|
||||
}
|
||||
|
||||
// ── helpers ──
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage[SP + 'cfg'] || '{}');
|
||||
for (const [k, v] of Object.entries(saved)) {
|
||||
if (CFG[k] !== undefined) CFG[k] = v;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
try { localStorage[SP + 'cfg'] = JSON.stringify(CFG); } catch (e) {}
|
||||
}
|
||||
|
||||
function setConfig(key, value) {
|
||||
if (CFG[key] !== undefined) {
|
||||
CFG[key] = value;
|
||||
saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
function readCSSDigits(container) {
|
||||
if (!container) return null;
|
||||
const digits = [];
|
||||
container.querySelectorAll('div').forEach(c => {
|
||||
const m = (c.className || '').match(/c4(\d)\b/);
|
||||
if (m) digits.push(m[1]);
|
||||
});
|
||||
if (digits.length === 0) return null;
|
||||
const parsed = parseInt(digits.join(''));
|
||||
return isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
// CSS font character map: c5a=a, c5b=b, ..., c5z=z
|
||||
// c59 = space, c4a = period, c40-c49 = 0-9
|
||||
// Note: no \\b word boundary — JS handles boundaries differently in class names
|
||||
function readCSSText(container) {
|
||||
if (!container) return '';
|
||||
let text = '';
|
||||
container.querySelectorAll('div').forEach(c => {
|
||||
const cls = c.className || '';
|
||||
// c5a-c5z → letters a-z
|
||||
const letterMatch = cls.match(/c5([a-z])\b/);
|
||||
if (letterMatch) {
|
||||
text += letterMatch[1];
|
||||
return;
|
||||
}
|
||||
// c59 → space
|
||||
if (/c59\b/.test(cls)) {
|
||||
text += ' ';
|
||||
return;
|
||||
}
|
||||
// c4a → period
|
||||
if (/c4a\b/.test(cls)) {
|
||||
text += '.';
|
||||
return;
|
||||
}
|
||||
// c40-c49 → digits 0-9
|
||||
const digitMatch = cls.match(/c4([0-9])\b/);
|
||||
if (digitMatch) {
|
||||
text += digitMatch[1];
|
||||
return;
|
||||
}
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
// Save difficulty from last overworld page read
|
||||
function cacheDifficulty() {
|
||||
if (STATE.difficulty && STATE.difficulty !== 'Normal') {
|
||||
try { localStorage[SP + 'difficulty'] = STATE.difficulty; } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Load cached difficulty (for battle pages where difficulty isn't in DOM)
|
||||
function restoreCachedDifficulty() {
|
||||
if (STATE.difficulty === 'Normal' || !STATE.difficulty) {
|
||||
try {
|
||||
const cached = localStorage[SP + 'difficulty'];
|
||||
if (cached) STATE.difficulty = cached;
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
498
src/strategy-engine.js
Normal file
498
src/strategy-engine.js
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// STRATEGY ENGINE — tier-based action recommendation
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── Spell helpers ──
|
||||
|
||||
function hasSpell(name) {
|
||||
return STATE.spellsKnown.some(s => s.n === name);
|
||||
}
|
||||
|
||||
function findSpell(name) {
|
||||
return STATE.spellsKnown.find(s => s.n === name);
|
||||
}
|
||||
|
||||
function findDmgSpell(list) {
|
||||
const s = STATE.spellsKnown.find(sp => list.includes(sp.n));
|
||||
return s ? s.n : null;
|
||||
}
|
||||
|
||||
// ── Buff helpers ──
|
||||
|
||||
// Returns the buff duration from STATE.buffs, or 0 if not present
|
||||
function buffDuration(name) {
|
||||
return STATE.buffs[name] || 0;
|
||||
}
|
||||
|
||||
// Should we cast this buff? Only if missing OR about to expire (< threshold turns)
|
||||
function shouldBuff(name, minTurns) {
|
||||
const dur = buffDuration(name);
|
||||
return dur === 0 || dur < minTurns;
|
||||
}
|
||||
|
||||
// Known buffs with their priority, icon name, spell name, and refresh threshold
|
||||
const BUFF_PRIORITY = [
|
||||
{ icon: 'haste', spell: 'Haste', minTurns: 2 },
|
||||
{ icon: 'protection', spell: 'Protection', minTurns: 2 },
|
||||
{ icon: 'spark_of_life', spell: 'Spark of Life', minTurns: 1 },
|
||||
{ icon: 'shadow_veil', spell: 'Shadow Veil', minTurns: 2 },
|
||||
{ icon: 'regen', spell: 'Regen', minTurns: 2 },
|
||||
{ icon: 'absorb', spell: 'Absorb', minTurns: 2 },
|
||||
];
|
||||
|
||||
// Known important monster names (bosses, legendaries, ultimates)
|
||||
const RARE_MONSTERS = [
|
||||
'manbearpig', 'white bunneh', 'mithra', 'dalek',
|
||||
'konata', 'mikuru asahina', 'ryouko asakura', 'yuki nagato',
|
||||
'skuld', 'urd', 'verdandi', 'yggdrasil',
|
||||
'rhaegal', 'viserion', 'drogon',
|
||||
'real life', 'invisible pink unicorn', 'flying spaghetti monster',
|
||||
'recycled boss rush', 'bottomless dungeon', 'new game +',
|
||||
'achievement grind', 'time trial mode', 'hardcore mode',
|
||||
];
|
||||
|
||||
function isRareMonster(idx) {
|
||||
const m = STATE.monsters[idx];
|
||||
if (!m) {
|
||||
// Fallback: check battle log names
|
||||
return STATE.battleMonsterNames.some(name =>
|
||||
RARE_MONSTERS.some(r => name.includes(r))
|
||||
);
|
||||
}
|
||||
|
||||
// Boss/rare monsters have distinct visual cues:
|
||||
// 1. Gold border: style="border-color:#BD7400"
|
||||
// 2. Gold background on label: style="background:#E6CCA3"
|
||||
// 3. SP bar present (third btm5 child with nbarred.png)
|
||||
const style = m.getAttribute('style') || '';
|
||||
const inner = m.innerHTML || '';
|
||||
|
||||
// Check for gold border (boss indicator)
|
||||
if (style.includes('BD7400') || style.includes('E6CCA3')) return true;
|
||||
|
||||
// Check for SP bar (only bosses have all 3 bars: HP, MP, SP)
|
||||
if (inner.includes('nbarred.png')) return true;
|
||||
|
||||
// Fallback: check name from DOM
|
||||
const n = m.querySelector('.btm3');
|
||||
if (!n) return false;
|
||||
const name = (n.textContent || '').replace(/^\d+\s*/, '').trim().toLowerCase();
|
||||
// Use CSS text parser for boss names rendered in font
|
||||
const cssName = readCSSText(n);
|
||||
return RARE_MONSTERS.some(r => name.includes(r) || cssName.includes(r));
|
||||
}
|
||||
|
||||
function anyRareMonster() {
|
||||
return STATE.monsters.some((m, i) => isRareMonster(i));
|
||||
}
|
||||
|
||||
function findWeakestMonster() {
|
||||
let best = -1, bestW = Infinity;
|
||||
STATE.monsters.forEach((m, i) => {
|
||||
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||
const hp = m.querySelector('img[src$="nbargreen.png"]');
|
||||
const w = hp ? parseInt(hp.style.width || '') : 120;
|
||||
if (w < bestW && w > 0) { bestW = w; best = i; }
|
||||
});
|
||||
return best >= 0 ? best : 0;
|
||||
}
|
||||
|
||||
function findStrongestMonster() {
|
||||
let best = -1, bestW = -1;
|
||||
STATE.monsters.forEach((m, i) => {
|
||||
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||
const hp = m.querySelector('img[src$="nbargreen.png"]');
|
||||
const w = hp ? parseInt(hp.style.width || '') : 120;
|
||||
if (w > bestW) { bestW = w; best = i; }
|
||||
});
|
||||
return best >= 0 ? best : 0;
|
||||
}
|
||||
|
||||
function checkMonsterDebuff(idx, db) {
|
||||
const m = STATE.monsters[idx];
|
||||
if (!m) return false;
|
||||
const s = m.querySelector('.btm6');
|
||||
if (!s) return false;
|
||||
return s.innerHTML.toLowerCase().includes(db.toLowerCase() + '.png') ||
|
||||
s.innerHTML.toLowerCase().includes('wpn_' + db.toLowerCase());
|
||||
}
|
||||
|
||||
// ── Damage spell tier list ──
|
||||
|
||||
const SPELL_T3 = ['Ragnarok', 'Paradise Lost', 'Flames of Loki', 'Fimbulvetr', 'Wrath of Thor', 'Storms of Njord'];
|
||||
const SPELL_T2 = ['Disintegrate', 'Banishment', 'Inferno', 'Blizzard', 'Chained Lightning', 'Downburst'];
|
||||
const SPELL_T1 = ['Corruption', 'Smite', 'Fiery Blast', 'Freeze', 'Shockblast', 'Gale'];
|
||||
|
||||
function getBestDamageSpell() {
|
||||
if (STATE.monsters.length >= 2) {
|
||||
const aoe = findDmgSpell(SPELL_T3.concat(SPELL_T2));
|
||||
if (aoe) return aoe;
|
||||
}
|
||||
return findDmgSpell(SPELL_T3.concat(SPELL_T2, SPELL_T1));
|
||||
}
|
||||
|
||||
// ── Channeling maintenance ──
|
||||
|
||||
// Cheapest mana-costing spells to trigger channeling when it wears off
|
||||
const CHANNEL_TRIGGERS = ['Fiery Blast', 'Freeze', 'Shockblast', 'Gale'];
|
||||
|
||||
function kickstartChanneling() {
|
||||
if (STATE.channeling) return null;
|
||||
|
||||
// Priority 1: refresh any buff that's about to expire (< 15 turns)
|
||||
// This is more useful than wasting MP on attack spells
|
||||
for (const b of BUFF_PRIORITY) {
|
||||
const dur = buffDuration(b.icon);
|
||||
if (dur > 0 && dur < 15 && STATE.mp > 0.15 && hasSpell(b.spell)) {
|
||||
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
|
||||
return { type: 'spell', name: b.spell, selfTarget: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: cast a buff that's missing entirely
|
||||
for (const b of BUFF_PRIORITY) {
|
||||
if (buffDuration(b.icon) === 0 && STATE.mp > 0.15 && hasSpell(b.spell)) {
|
||||
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
|
||||
return { type: 'spell', name: b.spell, selfTarget: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: cheapest attack spell to trigger channeling (only if attack spells enabled)
|
||||
if (CFG.useAttackSpells) {
|
||||
const spell = findDmgSpell(CHANNEL_TRIGGERS);
|
||||
if (spell && STATE.mp > 0.10) {
|
||||
const t = findWeakestMonster();
|
||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||
return { type: 'spell', name: spell, target: t };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Core strategy: forum-corrected item-before-spell priority ──
|
||||
//
|
||||
// Priority order:
|
||||
// 1. Mystic Gem if channeling is down (free OC gen)
|
||||
// 2. Health/Mana/Spirit gems proactively (don't hoard)
|
||||
// 3. Cure if HP critical
|
||||
// 4. Buffs (Protection, Regen, Haste)
|
||||
// 5. Debuffs (Imperil, Weaken)
|
||||
// 6. Spirit Stance at OC 60-80% (don't waste OC on skills before stance)
|
||||
// 7. Weapon skills only at high OC (80+) to let OC build
|
||||
// 8. Damage spells
|
||||
// 9. Basic attack builds OC naturally
|
||||
|
||||
function getRecommendedAction() {
|
||||
parseBattleState();
|
||||
if (STATE.tier === 'novice') return strategyNovice();
|
||||
if (STATE.tier === 'adept') return strategyAdept();
|
||||
return strategyVeteran(); // Veteran+ use the same engine
|
||||
}
|
||||
|
||||
// Returns non-null ONLY when there's something smarter than basic attack
|
||||
function getSmartAction() {
|
||||
const a = getRecommendedAction();
|
||||
if (!a || a.type === 'attack') return null;
|
||||
return a;
|
||||
}
|
||||
|
||||
// ── Novice (1-50): survival first ──
|
||||
|
||||
function strategyNovice() {
|
||||
// 0. Mystic Gem for channeling — free OC
|
||||
// (channeling status is tracked from the battle log)
|
||||
const gem = hasUsableGem('channel');
|
||||
if (gem && !STATE.channeling)
|
||||
return { type: 'item', id: gem.id, selfTarget: true };
|
||||
|
||||
// 1. Gems proactively — don't hoard
|
||||
if (STATE.hp < CFG.cureItemHP) {
|
||||
const gem = hasUsableGem('heal');
|
||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
||||
}
|
||||
// Mana gem — use when MP < 50%
|
||||
if (STATE.mp < CFG.manaGemMP) {
|
||||
const gem = hasUsableGem('mana');
|
||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
||||
}
|
||||
// Spirit gem — use when SP is low (keeps Spirit Stance available)
|
||||
if (STATE.sp < CFG.spiritPotionSP) {
|
||||
const gem = hasUsableGem('spirit');
|
||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
||||
}
|
||||
|
||||
// 2. Cure only if items aren't available and HP is critical
|
||||
if (STATE.hp < CFG.cureHP) {
|
||||
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
||||
if (c) return { type: 'spell', name: c, selfTarget: true };
|
||||
}
|
||||
|
||||
// 3. Buffs — cast if missing or about to expire
|
||||
for (const b of BUFF_PRIORITY) {
|
||||
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) {
|
||||
// Regen is hp-dependent: only cast if HP is below threshold
|
||||
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
|
||||
return { type: 'spell', name: b.spell, selfTarget: true };
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Spirit Stance at OC 60 — but only if we have SP to sustain it
|
||||
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
||||
return { type: 'toggle_spirit' };
|
||||
|
||||
// 6. Weapon skills only at OC 80+ with specific conditions
|
||||
if (STATE.oc >= 80) {
|
||||
// Great Cleave: boss/rare fights only
|
||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
||||
}
|
||||
// Rending Blow: AoE armor pen vs 5+ enemies
|
||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
||||
}
|
||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||
if (hasArmorBreak) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Damage spells (reserve MP for Cure)
|
||||
const dmg = CFG.useAttackSpells ? findDmgSpell(SPELL_T1) : null;
|
||||
if (dmg && STATE.mp > 0.25) {
|
||||
const cure = findSpell('Cure') || findSpell('Full-Cure');
|
||||
const cureCost = cure ? (cure.mp / 100) * (STATE.level || 1) : 4;
|
||||
const mpMax = STATE.mp * 100;
|
||||
const reserve = Math.max(0.15, cureCost / mpMax);
|
||||
if (STATE.mp - reserve > 0.25) {
|
||||
const t = findWeakestMonster();
|
||||
if (t >= 0) return { type: 'spell', name: dmg, target: t };
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Channeling kickstart or basic attack — builds OC naturally
|
||||
{
|
||||
const k = kickstartChanneling();
|
||||
if (k) return k;
|
||||
}
|
||||
const t = findWeakestMonster();
|
||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||
return { type: 'attack', target: t };
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Adept (50-150): building power ──
|
||||
|
||||
function strategyAdept() {
|
||||
// 0. Mystic Gem for channeling — free OC
|
||||
const gem = hasUsableGem('channel');
|
||||
if (gem && !STATE.channeling)
|
||||
return { type: 'item', id: gem.id, selfTarget: true };
|
||||
|
||||
// 1. Gems proactively — don't hoard
|
||||
if (STATE.hp < CFG.cureItemHP) {
|
||||
const gem = hasUsableGem('heal');
|
||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
||||
}
|
||||
if (STATE.mp < CFG.manaGemMP) {
|
||||
const gem = hasUsableGem('mana');
|
||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
||||
}
|
||||
// Spirit gem — use when SP is low
|
||||
if (STATE.sp < CFG.spiritPotionSP) {
|
||||
const gem = hasUsableGem('spirit');
|
||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
||||
}
|
||||
|
||||
// 2. Cure
|
||||
if (STATE.hp < CFG.cureHP) {
|
||||
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
||||
if (c) return { type: 'spell', name: c, selfTarget: true };
|
||||
}
|
||||
|
||||
// 3. Buffs — prioritize by remaining duration (lowest = most urgent)
|
||||
for (const b of BUFF_PRIORITY) {
|
||||
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) {
|
||||
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
|
||||
return { type: 'spell', name: b.spell, selfTarget: true };
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Debuffs — only on important fights (rare/boss/ultimate monsters)
|
||||
// In random mob fights, just attack through them
|
||||
if (anyRareMonster() || STATE.monsters.length <= 2) {
|
||||
if (hasSpell('Imperil')) {
|
||||
const b = findStrongestMonster();
|
||||
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
||||
return { type: 'spell', name: 'Imperil', target: b };
|
||||
}
|
||||
if (hasSpell('Weaken')) {
|
||||
const b = findStrongestMonster();
|
||||
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
||||
return { type: 'spell', name: 'Weaken', target: b };
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Spirit Stance at OC 60 — but only if we have SP to sustain it
|
||||
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
||||
return { type: 'toggle_spirit' };
|
||||
|
||||
// 6. Weapon skills only at OC 80+ with specific conditions
|
||||
// (checked regardless of spirit stance — if stance failed due to low SP,
|
||||
// still try to use the OC on skills)
|
||||
if (STATE.oc >= 80) {
|
||||
// Great Cleave: boss/rare fights only
|
||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
||||
}
|
||||
// Rending Blow: AoE armor pen vs 5+ enemies
|
||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Rending Blow')) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
||||
}
|
||||
// Shatter Strike: AoE stun vs 5+ enemies (needs Penetrated Armor from Rending Blow)
|
||||
if (STATE.monsters.length >= 5 && STATE.skillsKnown.includes('Shatter Strike')) {
|
||||
// Check if at least one monster has Penetrated Armor
|
||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||
if (hasArmorBreak) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Damage spells
|
||||
const dmg = CFG.useAttackSpells ? getBestDamageSpell() : null;
|
||||
if (dmg && STATE.mp > 0.2) {
|
||||
const t = STATE.monsters.length > 1 ? findWeakestMonster() : findStrongestMonster();
|
||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||
return { type: 'spell', name: dmg, target: t };
|
||||
}
|
||||
|
||||
// 8. Channeling kickstart or basic attack
|
||||
{
|
||||
const k = kickstartChanneling();
|
||||
if (k) return k;
|
||||
}
|
||||
const t = findWeakestMonster();
|
||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||
return { type: 'attack', target: t };
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Veteran (150-300) / Master (300+): full rotation ──
|
||||
|
||||
function strategyVeteran() {
|
||||
// 0. Mystic Gem for channeling
|
||||
const gem = hasUsableGem('channel');
|
||||
if (gem && !STATE.channeling)
|
||||
return { type: 'item', id: gem.id, selfTarget: true };
|
||||
|
||||
// 1. Gems proactively — don't hoard, use at reasonable thresholds
|
||||
if (STATE.hp < 0.60) {
|
||||
const gem = hasUsableGem('heal');
|
||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
||||
}
|
||||
if (STATE.mp < CFG.manaGemMP) {
|
||||
const gem = hasUsableGem('mana');
|
||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
||||
}
|
||||
if (STATE.sp < CFG.spiritPotionSP) {
|
||||
const gem = hasUsableGem('spirit');
|
||||
if (gem) return { type: 'item', id: gem.id, selfTarget: true };
|
||||
}
|
||||
|
||||
// 2. Cure
|
||||
if (STATE.hp < CFG.cureHP) {
|
||||
const c = hasSpell('Full-Cure') ? 'Full-Cure' : hasSpell('Cure') ? 'Cure' : null;
|
||||
if (c) return { type: 'spell', name: c, selfTarget: true };
|
||||
}
|
||||
|
||||
// 3. Buffs — prioritize by remaining duration
|
||||
if (CFG.autoBuff) {
|
||||
for (const b of BUFF_PRIORITY) {
|
||||
if (shouldBuff(b.icon, b.minTurns) && STATE.mp > 0.2 && hasSpell(b.spell)) {
|
||||
if (b.icon === 'regen' && STATE.hp >= CFG.cureRegenHP) continue;
|
||||
return { type: 'spell', name: b.spell, selfTarget: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Debuffs — only on important fights (rare/boss)
|
||||
if (CFG.autoDebuff && STATE.mp > 0.2 && (anyRareMonster() || STATE.monsters.length <= 2)) {
|
||||
if (hasSpell('Imperil')) {
|
||||
const b = findStrongestMonster();
|
||||
if (b >= 0 && !checkMonsterDebuff(b, 'imperil'))
|
||||
return { type: 'spell', name: 'Imperil', target: b };
|
||||
}
|
||||
if (hasSpell('Weaken')) {
|
||||
const b = findStrongestMonster();
|
||||
if (b >= 0 && !checkMonsterDebuff(b, 'weaken'))
|
||||
return { type: 'spell', name: 'Weaken', target: b };
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Spirit Stance at OC 60
|
||||
if (CFG.autoSpirit && STATE.oc >= CFG.spiritStanceOC && !STATE.spiritStance && STATE.sp > 0.10)
|
||||
return { type: 'toggle_spirit' };
|
||||
if (STATE.sp < 0.03 && STATE.spiritStance)
|
||||
return { type: 'toggle_spirit' };
|
||||
|
||||
// 6. Weapon skills only at OC 80+ (let OC build via basic attacks)
|
||||
if (STATE.oc >= 80) {
|
||||
// Great Cleave: boss/rare fights only
|
||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Great Cleave', target: t };
|
||||
}
|
||||
// Rending Blow + Shatter Strike: AoE vs 5+ enemies
|
||||
if (STATE.monsters.length >= 5) {
|
||||
if (STATE.skillsKnown.includes('Rending Blow')) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Rending Blow', target: t };
|
||||
}
|
||||
if (STATE.skillsKnown.includes('Shatter Strike')) {
|
||||
const hasArmorBreak = STATE.monsters.some((m, i) => checkMonsterDebuff(i, 'penetrated_armor'));
|
||||
if (hasArmorBreak) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: 'Shatter Strike', target: t };
|
||||
}
|
||||
}
|
||||
}
|
||||
// Other weapon skills (non-2H): generic use
|
||||
if (!STATE.skillsKnown.includes('Great Cleave')) {
|
||||
const ps = ['Frenzied Blows', 'Skyward Sword', 'Iris Strike', 'Merciful Blow',
|
||||
'Vital Strike', 'Shield Bash', 'Concussive Strike'];
|
||||
for (const sk of ps) {
|
||||
if (STATE.skillsKnown.includes(sk)) {
|
||||
const t = findStrongestMonster();
|
||||
if (t >= 0) return { type: 'skill', name: sk, target: t };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Damage spells
|
||||
const dmg = CFG.useAttackSpells ? getBestDamageSpell() : null;
|
||||
if (dmg && STATE.mp > 0.15) {
|
||||
const t = STATE.monsters.length > 1 ? findWeakestMonster() : 0;
|
||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||
return { type: 'spell', name: dmg, target: t };
|
||||
}
|
||||
|
||||
// 8. Channeling kickstart or basic attack
|
||||
{
|
||||
const k = kickstartChanneling();
|
||||
if (k) return k;
|
||||
}
|
||||
const t = findWeakestMonster();
|
||||
if (t >= 0 && STATE.monsters[t] && STATE.monsters[t].hasAttribute('onclick'))
|
||||
return { type: 'attack', target: t };
|
||||
return null;
|
||||
}
|
||||
132
src/ui-overlays.js
Normal file
132
src/ui-overlays.js
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// UI OVERLAYS — cooldowns, durations, alerts, monster numbers, monster HP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateCooldownDisplay() {
|
||||
if (!CFG.showCooldowns) return;
|
||||
$$('.hv-cd-overlay').forEach(e => e.remove());
|
||||
$$('.btqb').forEach(el => {
|
||||
const img = el.querySelector('img');
|
||||
if (!img) return;
|
||||
const src = (img.src || '').toLowerCase();
|
||||
for (const [k, v] of Object.entries(STATE.cooldowns)) {
|
||||
if (v <= 0) continue;
|
||||
if (src.includes(k.toLowerCase()) || src.includes(k.replace(/_/g, ''))) {
|
||||
const o = document.createElement('div');
|
||||
o.className = 'hv-cd-overlay';
|
||||
o.style.cssText = 'position:absolute;top:0;right:0;background:rgba(0,0,0,0.7);color:#f00;font-size:9px;font-weight:bold;padding:0 2px;border-radius:2px;pointer-events:none';
|
||||
o.textContent = v;
|
||||
el.style.position = 'relative';
|
||||
el.appendChild(o);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateDurationDisplay() {
|
||||
if (!CFG.showDurations) return;
|
||||
$$('.hv-dur-overlay').forEach(e => e.remove());
|
||||
const pane = document.getElementById('pane_effects');
|
||||
if (!pane) return;
|
||||
$$('img[onmouseover]', pane).forEach(img => {
|
||||
const omo = img.getAttribute('onmouseover') || '';
|
||||
const dur = omo.match(/(\d+)\s*turns?\s*rem/);
|
||||
if (!dur) return;
|
||||
const t = parseInt(dur[1]);
|
||||
const st = omo.match(/(\d+)\s*stacks?/i);
|
||||
const o = document.createElement('div');
|
||||
o.className = 'hv-dur-overlay';
|
||||
o.style.cssText = `position:absolute;bottom:-2px;left:50%;transform:translateX(-50%);background:${t < 3 ? '#f44' : '#44f'};color:#fff;font-size:8px;font-weight:bold;padding:0 2px;border-radius:2px;pointer-events:none;z-index:1`;
|
||||
o.textContent = st ? `${t}x${parseInt(st[1])}` : t;
|
||||
img.parentElement.style.position = 'relative';
|
||||
img.parentElement.appendChild(o);
|
||||
});
|
||||
}
|
||||
|
||||
function updateAlertColours() {
|
||||
if (!CFG.alertColours) return;
|
||||
let bg = '#EDEBDF';
|
||||
STATE.interruptAlert = false;
|
||||
|
||||
if (!$('img[src$="bar_dgreen.png"]') && $('img[src$="fallenshield.png"]')) {
|
||||
bg = 'magenta';
|
||||
STATE.interruptAlert = true;
|
||||
} else if (STATE.hp < 0.25) {
|
||||
bg = '#ff4488';
|
||||
STATE.interruptAlert = true;
|
||||
} else if (STATE.mp < 0.15) {
|
||||
bg = '#4444aa';
|
||||
} else if (Object.values(STATE.buffs).some(d => d < 2 && d > 0)) {
|
||||
bg = '#88ccff';
|
||||
}
|
||||
|
||||
const v = document.getElementById('pane_vitals');
|
||||
if (v) v.style.background = bg;
|
||||
}
|
||||
|
||||
function addMonsterNumbers() {
|
||||
if (!CFG.showMonsterNumbers) return;
|
||||
STATE.monsters.forEach((m, i) => {
|
||||
if (!m || !m.querySelector || m.querySelector('.hv-monster-num')) return;
|
||||
const b = m.querySelector('.btm1');
|
||||
if (!b) return;
|
||||
const s = document.createElement('span');
|
||||
s.className = 'hv-monster-num';
|
||||
s.style.cssText = 'font-weight:bold;margin-right:3px;color:#999';
|
||||
s.textContent = (i + 1);
|
||||
b.insertBefore(s, b.firstChild);
|
||||
});
|
||||
}
|
||||
|
||||
function updateMonsterHPDisplay() {
|
||||
if (!CFG.showMonsterHP) return;
|
||||
STATE.monsters.forEach((m, i) => {
|
||||
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||
const n = m.querySelector('.btm1');
|
||||
if (!n) return;
|
||||
const nm = (n.textContent || '').replace(/^\d+\s*/, '').trim();
|
||||
const md = STATE.monsterData[nm];
|
||||
if (!md || !md.hp || md.seen < 3) return;
|
||||
const hpBar = m.querySelector('img[src$="nbargreen.png"]');
|
||||
if (!hpBar) return;
|
||||
const r = clamp(parseInt(hpBar.style.width || '') / 120, 0.01, 1);
|
||||
const txt = `HP: ${Math.max(1, Math.round(r * md.hp)).toLocaleString()} / ${md.hp.toLocaleString()}`;
|
||||
const ex = m.querySelector('.hv-monster-hp');
|
||||
if (ex) { ex.textContent = txt; return; }
|
||||
const d = document.createElement('div');
|
||||
d.className = 'hv-monster-hp';
|
||||
d.style.cssText = 'display:inline-block;position:relative;left:204px;top:-20px;font-size:9px;color:#888';
|
||||
d.textContent = txt;
|
||||
m.appendChild(d);
|
||||
});
|
||||
}
|
||||
|
||||
function saveMonsterData() {
|
||||
STATE.monsters.forEach((m, i) => {
|
||||
if (!m || !m.hasAttribute || !m.hasAttribute('onclick')) return;
|
||||
const n = m.querySelector('.btm1');
|
||||
if (!n) return;
|
||||
const nm = (n.textContent || '').replace(/^\d+\s*/, '').trim();
|
||||
if (!nm) return;
|
||||
const hpBar = m.querySelector('img[src$="nbargreen.png"]');
|
||||
if (hpBar) {
|
||||
const r = clamp(parseInt(hpBar.style.width || '') / 120, 0.01, 1);
|
||||
if (!STATE.monsterData[nm]) STATE.monsterData[nm] = { hp: 0, seen: 0 };
|
||||
STATE.monsterData[nm].hp = Math.max(
|
||||
STATE.monsterData[nm].hp,
|
||||
Math.round(STATE.monsterData[nm].hp * 0.7 + 1000 / r * 0.3)
|
||||
);
|
||||
STATE.monsterData[nm].seen++;
|
||||
}
|
||||
});
|
||||
try { localStorage[SP + 'monsters'] = JSON.stringify(STATE.monsterData); } catch (e) {}
|
||||
}
|
||||
|
||||
function refreshUI() {
|
||||
if (CFG.showDurations) updateDurationDisplay();
|
||||
if (CFG.showCooldowns) updateCooldownDisplay();
|
||||
if (CFG.alertColours) updateAlertColours();
|
||||
if (CFG.showMonsterNumbers) addMonsterNumbers();
|
||||
if (CFG.showMonsterHP) updateMonsterHPDisplay();
|
||||
}
|
||||
31
src/utils.js
Normal file
31
src/utils.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// UTILS — DOM shortcuts, constants, helpers
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const $ = (s, c) => (c || document).querySelector(s);
|
||||
const $$ = (s, c) => [...(c || document).querySelectorAll(s)];
|
||||
const clamp = (v, l, h) => Math.max(l, Math.min(h, v));
|
||||
const dummy = document.createElement('div');
|
||||
|
||||
const KEYS = {
|
||||
A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, H: 72,
|
||||
I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79, P: 80,
|
||||
Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87, X: 88, Y: 89, Z: 90,
|
||||
D0: 48, D1: 49, D2: 50, D3: 51, D4: 52, D5: 53, D6: 54, D7: 55, D8: 56, D9: 57,
|
||||
SPACE: 32, RETURN: 13, ESCAPE: 27, COMMA: 188, PERIOD: 190, UP: 38, DOWN: 40,
|
||||
};
|
||||
|
||||
function isInputFocused() {
|
||||
const el = document.activeElement;
|
||||
if (!el) return false;
|
||||
const t = el.tagName;
|
||||
return t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT' || el.isContentEditable;
|
||||
}
|
||||
|
||||
// ── CSS style builder shorthand ──
|
||||
|
||||
function css(styles) {
|
||||
return Object.entries(styles)
|
||||
.map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}:${v}`)
|
||||
.join(';');
|
||||
}
|
||||
Loading…
Reference in a new issue