- equip-UID.js loads asynchonously after document-end, so dynjs_equip
might not exist when init() first runs
- Now retries up to 10 times (500ms apart, 5 seconds total) waiting for
the store to populate before scraping
- Added HV.gearStore() to check whether the store is loaded
- If store never loads, falls back to name/ID scrape without stats
- Previously: tried equips.set() + 300ms delays + popup_box polling
which was fragile, async, and often saved stats to wrong slots
- Now: reads window.dynjs_equip / dynjs_eqstore which contains ALL
item data pre-loaded on page load (name, quality, full tooltip HTML)
- Uses DOMParser to parse the embedded .eq HTML into structured stats
(type, level, condition, burden, ADB, mit, attributes, etc.)
- Zero hover, zero delays, zero popup_box interaction — instant stats
- Works on Character Equipment page AND Armory pages simultaneously
- Removed triggerAllTooltips() and captureAllTooltips() — obsolete
- scrapeCharacterEquip() now only reads names/IDs from DOM slots (no tooltip
popup reading — that was attaching wrong stats to wrong slots)
- captureAllTooltips() now calls logGearSummary() instead of autoScrapeGear()
so the tooltip stats aren't wiped by a re-scrape
- Workflow: visit Character Equipment page, then run HV.captureTooltips()
to populate full stats with 300ms hover delay per item
- 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
- 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
Spark of Life consumes 50% of your SP bar when triggered. If you're in
Spirit Stance with low SP, Spark fails to proc. Changes:
- No auto-activation of Spirit Stance (toggle manually with S key)
- Auto-disable if SP drops below 65% to reserve SP for Spark trigger
- Veteran still has optional auto-activation via CFG.autoSpirit but
requires SP > 65% (practically never triggers)
Channeling wiki fix:
- Procs randomly on any mana-costing spell
- Proc chance = spell_cost / (base_mana * 1.2)
- Higher cost = higher chance (Regen/Absorb at 17 MP = ~6%)
- Cannot re-proc while already channeling
- Duration: 5 ticks (15 with Mystic Gem)
- Effect: next spell costs 1 MP, 50% stronger/longer
Kickstart now only refreshes expiring buffs (missing or < 5 turns)
with decent-cost spells (Regen/Absorb). No more cheap attack spells
at 4 MP = 1.4% proc chance — that was a waste of MP.
Defaults reverted to full-conservative values since the 'mp-focused'
win was based on wrong channeling assumptions.
- 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)