- captureAllTooltips() iterates each equipment slot, calls equips.set()
to trigger the tooltip popup, waits 300ms for the DOM to update,
then reads and saves the full stat block from #popup_box
- Catches 'Popup Box' default text vs real tooltip
- Saves stats directly to localStorage gear DB
- Exposed as HV.captureTooltips() for console use
- For best results: visit Character Equipment page, run HV.captureTooltips()
- .eqb parent has onclick and structure, but the onmouseover with
equips.set() and the item name text are on a CHILD div within .eqb
- Fixed scrapeCharacterEquip() and triggerAllTooltips() to query
:scope > div[onmouseover] inside each .eqb slot
- Fixed debugScrapeGear() diagnostic to show inner div content
- New debugScrapeGear() function checks DOM structure:
- Whether #eqsb exists and its slot count
- Whether #equiplist exists
- Whether #popup_box exists and its content
- Whether equips global is available
- Exposed as HV.gearDebug() for console use
- Character Equipment page (?s=Character&ss=eq): reads #eqsb > .eqb
divs with onmouseover='equips.set(ID,...)' for item names and IDs
- triggerAllTooltips() calls equips.set() for each item to populate
the #popup_box tooltip, then parses full stat blocks (ADB, damage,
mitigation, attributes, burden, interference, condition)
- Armory Organize page: reads #equiplist table rows with
onmouseover='hover_equip(ID)' and checkbox values
- Reads right-side #equipinfo preview for condition and stats
- Modify detail page: reads full stat tables
- Added to window.HV public API: gear(), gearSummary(), scrapeGear()
- Now accessible from console as HV.gear(), HV.gearSummary()
- Also keep getGearDB() and logGearSummary() directly available
- New module: gear-scraper.js (10K, 200+ lines)
- Auto-scrapes Character page (?ss=ch) for equipped gear with full stat
blocks: ADB, damage, crit, parry, evade, burden, mitigation, stats
- Auto-scrapes Armory inventory page for all items with IDs, names,
categories, potency, durability
- Scrapes Modify detail page for one item's complete stat table
- Stores everything in localStorage['hvunified_geardb']
- ParseHV.advice() now includes gear info
- Public API: getGearDB() returns full gear database
- Added step 0d in Veteran: if 2+ monsters have SP >85%, fire weapon
skills BEFORE buffs, items, debuffs, or anything else
- Only needs OC >= 20 (one skill's worth, builds fast from basic attacks)
- Rending Blow > Shatter Strike > Great Cleave, targeting most dangerous monster
- Formerly: weapon skills were at step 11, after all buffs and items.
Enemies got multiple turns to special attack during the buff-up phase.
Now dangerous monsters get shut down immediately.
- Imperil no longer casts on every 2-mob wave (Estoc's native Penetrated
Armor handles armor-breaking for <5 targets)
- Imperil threshold raised from MP>20% to MP>35%
- Imperil now only fires on bosses/rares OR 5+ monster waves (Grindfest)
- Weaken now only on high-threat (SP>85%) or bosses, not every 3-mob wave
- Formerly: Imperil on 2+ mobs, Weaken on 3+ mobs — drained MP fast
- Regular buff loop in all 3 tiers now collects candidates then sorts
by MP cost ascending (cheapest first) when no channeling, or
descending (most expensive first) when channeling is active
- Previously just iterated BUFF_PRIORITY declaration order (Haste →
Spark → Heartseeker → Regen → Absorb → Shadow Veil) which wasn't
cost-ordered
- _reason now shows 'buff' (cheapest first) or 'chBuff' (most expensive)
- castInitialBuffs now tags actions with _reason='init' (cheapest-first
sort, no channeling) or _reason='initCh' (most-expensive-first,
channeling already charged)
- Makes the log clearly show which buff path fired
- Some quickbar/deprecated elements retain onclick even when on cooldown
if the cooldown indicator is only visual (img opacity:0.5) not structural
- Now also checks img.btqi / img style.opacity !== '0.5'
- This catches cases where the parent div has onclick but the inner image
is visually dimmed (game cooldown)
- Added hasReadySpell() that checks DOM for onclick attribute (off cooldown)
- Buff loop (all tiers) now uses hasReadySpell instead of hasSpell
- Cure/Full-Cure now checks cooldown before casting
- Channeling target selection checks cooldown
- castInitialBuffs checks cooldown
- Imperil/Weaken debuffs check cooldown
- hasSpell() kept for knowledge checks (do we know this spell at all)
- Prevents silent failures where on-cooldown spells are selected but
can't actually be cast (btsd[onclick] fails to find them)
BUG FIXES (P0):
- Spark of Life SP safety: consume spirit items when SP < 55% BEFORE
buffs, Cure, or any other action (both Adept and Veteran)
- Spirit Stance safety auto-disable: auto-toggle OFF if SP drops below 60%
- useItem() fallback: auto_ slot IDs now lookup by onmouseover attribute
(on-cooldown items without DOM id were silently failing)
- setTimeout targeting race: removed 50ms delay in castTargetSpell/useSkill
(was causing targeting failures with fast Q-repeat)
- maxBuffDur pre-seeded with L150+ baseline values (haste=80, heartseeker=120,
sparklife=50, etc.) so fill-level scoring works from first turn
STRATEGY (P1):
- Channeling waste eliminated for 2H physical builds: no more Fiery Blast
fallback — let channeling expire on a basic attack (Domino Strike +
Penetrated Armor do more damage)
- Weapon skill thresholds lowered from 5+ to 3+ enemies (IWBTH survivability)
- Expanded debuffs: Imperil on any 2+ mobs, Weaken on high-threat targets
(not just bosses anymore)
- Domino Strike optimal targeting: findOptimalDominoTarget() picks center
monsters to max splash coverage (up to 2 targets each side)
NEW FEATURES:
- Domino Strike splash positioning algorithm added to all tiers
- Full _reason tags on every action return for decision tracing
- agy research report saved at hermes_report.md (554 lines)
- maxBuffDur now saved to localStorage['hvunified_maxBuffDur'] on every
new high observation and loaded on STATE init
- Survives page reloads: Heartseeker's 180-turn max stays learned
- Removed the unreliable-max fallback hack — no longer needed
- First Q press of a fresh session has accurate fill-level scores
for all buffs that were observed in previous sessions
- When maxBuffDur gap (max - current) is < 10, the cap is unreliable
(first observation mid-battle, not at full duration)
- In that case, skip the fill-level formula entirely
- Only suggest a buff for channeling if it actually needs refresh
(dur <= minTurns from BUFF_PRIORITY)
- Once maxBuffDur learns the true cap (after a fresh cast), the
normal fill-level scoring takes over correctly
- Q-key log now shows: HP, current MP, MP%, SP%, OC, monster count,
buff durations, and per-monster SP bar levels (spBars: 0:120 1:95 ...)
- High-threat detection logs a red '⚠ HIGH THREAT' warning
- Emergency skill usage at low OC logs an orange '⚠ emergency' warning
- Now you can paste me the console log and I'll see exact threat levels,
why skills did/didn't fire, and what the state was before each action
- Fix: items on cooldown (no id attr) were invisible to parser
Spirit Draught/Potion on cooldown couldn't be auto-consumed
- Fix: spiritPotionSP raised from 30% to 60%
- New: parse monster SP bar widths into STATE.monsterSp[]
- New: findDangerousMonster() — targets highest SP bar monster
- New: hasHighThreat() — true if 2+ monsters >85% SP (about to special)
- Weapon skills now fire on high-threat even without full OC
- Basic attacks prefer dangerous monsters in all tiers
- Raised spiritPotionSP threshold from 30% to 60%
- Added Spirit item check at step 2b in Adept and Veteran tiers:
if SP < 55%, consume Spirit Draught/Potion immediately
- Spark of Life costs 50% max SP when triggered. If SP is too low,
Spark fails and you die. Keeping SP above 55% ensures Spark always
has enough fuel to save you from a killing blow on IWBTH
- Changed Veteran suggestion from PFUDOR (20x) to IWBTH (15x)
- PFUDOR is now only suggested at Master tier (L300+)
- Since you're already on IWBTH, the banner won't show anymore
(current == suggested, so no nag)
- executeAction now sets STATE._lastActionTime after dispatching
- Q handler checks this instead of a local var
- When an action executes, the Q debounce resets so the next Q press
waits the full 300ms before attempting again
- Prevents stale-state queueing when game hasn't processed the
previous action yet (was sending 12+ 'skill' commands to the
game, though only 1 actually ran)
- Added _lastQTime debounce (300ms) to the Q key handler
- Holding Q now fires at most ~3 actions per second instead
of ~20, preventing duplicate actions from stale state reads
- The previous issue: 6 identical Haste casts, then 8 identical
Spark casts, then 12 identical Heartseeker casts — all from
state not yet updated by the game
- castInitialBuffs was sorting by mpCost descending (b.mpCost - a.mpCost)
- So it picked Spark of Life (70) before Haste (41), defeating the
whole purpose of casting cheapest-first to proc channeling
- Changed to ascending sort (a.mpCost - b.mpCost): Haste first, then
Spark, etc.
- Was using vrhd (HP element) instead of vrm (current MP)
- Now reads #vrm textContent for current MP value
- Simply compares mpCost > curMp to skip unaffordable spells
- This prevents the infinite Heartseeker loop when you
don't have enough MP even though the buff is a good target
- Removed SPELL_COSTS hardcoded table
- Base MP costs are now read from the magic pane's onmouseover
tooltip at battle init and cached in STATE._baseMpCosts
- Cache is populated during parseBattleState() and persists
for the entire battle, avoiding channeling's 1-MP display
- Cache resets on new battle, so level-ups or stat changes
are picked up when starting a new arena
- Removed hardcoded SPELL_COSTS table (was brittle)
- Now reads base MP cost directly from the magic pane's
onmouseover tooltip (always shows true base cost, unaffected
by channeling's 1-MP display or Mana Conservation)
- Added affordable check: skip spells where base cost exceeds
current MP, even though channeling would make it cost 1
- The game's native MP check (shown by "You do not have enough")
checks against base cost, not channeling-reduced cost
- Some spells show mp=1 in the DOM due to high Mana Conservation
proficiency, making fill-level scoring produce near-zero values
- Now: if mpCost < 5, look up the base cost from SPELL_COSTS table
- This ensures Heartseeker (141 MP) scores correctly even when the
DOM shows it costing 1 MP
- Magic pane query used '.btsd[onclick]' which excludes
on-cooldown spells (opacity:0.5, no onclick attribute)
- Heartseeker was invisible to hasSpell() during its cooldown
period, which is exactly when channeling procs from it
- Changed to '.btsd' with onmouseover fallback
- Logs the top candidate from findBestChannelingTarget() on every Q press
- Shows: spell, score, dur, maxDur, mpCost, emptiness%
- Next time channeling doesn't fire on a buff, the console will show
the actual computed scores so we can see what's happening
- When maxBuffDur for a buff equals its current duration (first observation
in a battle), the emptiness was 0 — making the script think all buffs
are full and skipping them
- Now: if maxDur <= current dur, assume a minimum of 30 turns as the max
- This ensures buffs like Shadow Veil at 8/8 are treated as 8/30 (73% empty)
instead of 8/8 (0% empty)
- The regex only matched 'X turns remaining' text format
- Buffs like channeling and freshly cast spells use the raw third
parameter of set_infopane_effect('Name','desc',X) without 'turns rem'
- These were defaulting to 50 turns, making all fresh buffs appear
as having 50 turns when they really had 0-5
- Added fallback to parse the last numeric argument of the function call
- Mystic Gem usage now tracked with STATE._mysticUsed flag
Prevents re-triggering the gem check after it's been consumed
(stale STATE.itemsKnown was causing infinite 'item → p' loops)
- Flag resets on new battle initialization
- Added buff durations + channeling status to Q-key console output
Shows: ch=true/false mp=X% oc=X buffs: heartseeker=47 haste=24 ...
- Removed call to undefined finalizeBattleLog() that was spamming
ReferenceErrors and potentially corrupting the MutationObserver
- Raised channeling score threshold from 5 to 30: now only picks a
buff for channeling if it's at least 30% empty on a 100-MP spell
or 60% empty on a 50-MP spell
- Spark at 30/60 turns (50% empty, score 44) now falls to cheap
damage fallback instead of wasting channeling
- Channeling detection was ONLY from battle log text entries
- When log entries scroll off (new round starts), the 'gains the effect'
message is lost and STATE.channeling stays false permanently
- Added effects pane check: if channeling.png is visible, force channeling=true
- This fixes the root cause of channeling being ignored when the log
has scrolled past the proc message
- Also bumped version properly this time
- Score = emptiness_ratio × mp_cost
- emptiness_ratio = (max_dur - current) / max_dur
- max_dur is tracked dynamically per buff in STATE.maxBuffDur
(learned from the highest duration seen per icon)
- Freshly cast buffs score ~0 regardless of MP cost
- A 100-MP buff at 1/90 turns scores 98.9
- A 40-MP buff at 25/50 turns scores 20.0
- A 140-MP buff at 71/80 turns scores 15.75
- Removed the Heartseeker >50 skip (no longer needed - fill level handles it)
- Reverted the >10 turns cutoff (wrong approach)
- Instead, added a minimum score threshold of 5 to the best candidate
- At score >= 5: a 50-MP spell with <9 turns, a 140-MP spell with <27 turns
- Below score 5: falls through to cheap damage spell fallback
- This prevents recasting fresh buffs while still considering all spells
- findBestChannelingTarget: skip buffs with >10 remaining turns
- When all buffs are fresh, falls through to cheap damage spell
instead of wasting channeling on a nearly-full duration buff
- This prevents recasting Spark of Life at 15 turns just because
its score happens to be highest among all fresh buffs
- The vitals observer check 'battleLog && !battleLog.summary' referenced
an undefined variable, throwing on every observer tick
- Simplified to just check for pane_completion existence
- This crash may have been interfering with state parsing on the observer
- config.js: 0.13.0 -> 0.13.8
- header.user.js: 0.12.0 -> 0.13.8 (was never bumped past 0.12.0)
- Now the @version tag in the userscript header matches the internal VERSION
- Reduced buff loop MP threshold from 20% to 10% in all three tiers
(prevents skipping buffs when MP is moderately low)
- Also fixed the 'battleLog is not defined' ReferenceError from the
vitals observer (removed in previous commit but was still referenced)
- Moved findBestChannelingTarget() BEFORE the BUFF_PRIORITY loop
- When Q is pressed and channeling is active, the script now picks the most
expensive buff for the channeling charge instead of wasting it on the
first buff in priority order (Haste at 32 MP instead of Heartseeker at 175)
- Reverted the vitals auto-execute (Q-key workflow, not auto-battle)
- All three tiers: channeling check fires after castInitialBuffs but
before the regular shouldBuff loop
- Added action execution to the vitals MutationObserver so the script
fires on every round tick, not only when hovering a monster
- This fixes buffs expiring and channeling being wasted when the user
isn't actively hovering a monster during auto-battle
- Removed unused STATE.battleRound
- findBestChannelingTarget() fallback: expanded damage spell list to
include T2 spells (Inferno, Blizzard, etc.) and a last-resort loop
that fires ANY offensive spell the player knows
- Prevents channeling from expiring when all buffs have high remaining
durations and the player has no T1 elemental spells
- Added castInitialBuffs() that fires when 60%+ buffs are down (first round)
- Without channeling: casts cheapest buff first (more casts = more procs)
- With channeling: casts most expensive remaining buff (best value for free cast)
- Added isFirstRoundBuffs() helper: checks if most buffs are missing
- Injected into all three tiers (Novice, Adept, Veteran) before the
regular BUFF_PRIORITY loop
- Added STATE.battleRound counter for future round tracking
- New settings-page.js: adds a toolbar above the auto-salvage table
on Character Settings (ss=se) with bulk preset buttons
- Preset 1: Sell <=Fair (value 2) / Salvage <=Average (value 3)
- Preset 2: Sell <=Crude (value 1) / Salvage <=Fair (value 2)
- Disable All: reset both to No Auto-Sell/Salvage
- Shows current state summary (Sell X | Salvage Y)
- Also fixes init.js and build.sh to include new file
- Scrap the old floating panel approach (wrong DOM selectors)
- Now injects a toolbar directly into the equipment list on the Armory page
- Parses equipment from native #equiplist table rows using correct
DOM selectors (hover_equip, select_equip, eqids[] checkboxes)
- Detects current screen (organize/sell/salvage/modify) to show relevant
bulk-select buttons per tab
- Sell tab: select Crude, select <=Fair
- Salvage tab: select salvage-grade, select <=Average
- Organize tab: select equipped (for pinning)
- Plus: Select All, Invert, Clear on every screen
- Grades by quality: Keep (Magnificent+), Keep (Superior),
Salvage (Average-Fair), Sell (Crude)
- Resets properly across page navigations (single-use id guard)
- Removed enhanceEquipShop() — broken bulk sell/salvage from equip shop page
- Rewrote enhanceArmory() with actionable equipment management panel:
- Parses all equipment from the Armory (ss=am) page
- Grades each piece by quality: Keep (Magnificent+), Keep (Superior),
Salvage (Average-Fair), Sell (Crude)
- Bulk Sell / Bulk Salvage buttons that click native game buttons
- Shows slot separators, quality colors, status icons (equipped/locked/stored)
- Sorting: equipped/pinned first, then by quality, then by level
- Summary counts for each action category
At Lv100+ / Hell difficulty, all normal monsters also have SP bars
(the red nbarred.png). This made the script think every monster was
a boss, triggering debuffs (Imperil/Weaken) on every fight and
draining MP.
Changes:
- isRareMonster() no longer checks for nbarred.png
- Only gold border (BD7400/E6CCA3) indicates a real boss
- Debuffs now only fire on gold-border bosses, not on ≤2 monsters