Root cause: both buttons were inserted inside the armory <form>.
HTML <button> defaults to type=submit — clicking submitted the form,
the page reloaded, and the panel (which renders 300ms later) was
destroyed. The split-second flash was the panel before the reload.
Fix: type='button' on both buttons. Panel now stays up.
- Gear panel: wrapped autoScrapeGear() and buildGearPanel() in try/catch.
An exception during scrape (new store-scan loop) killed the onclick
handler before the panel was scheduled — panel never appeared.
- Removed renderProgressPanel() call (Daily Activities window) per user
request — not used.
Bug: difficulties.indexOf(difficulty.toUpperCase()) — the array has
'Nintendo' (mixed case) but toUpperCase() gives 'NINTENDO' which
doesn't match, so currentIdx = -1 and -1 < suggestedIdx always true.
The suggestion kept firing even when already on Nintendo.
(IWBTH worked by luck since uppercase == itself.)
Fix: case-insensitive findIndex comparison on both sides.
New src/gear-analysis.js — ports analyze_gear.py scoring to JS:
- gearScoreArmor() / gearScoreWeapon() — same weights as Python
- gearDetectSlot() / gearIsTwoHandWeapon() / gearUsable()
- analyzeGear() — scores equipped vs armory vs buy per slot
UI on Armory pages (above equiplist):
- 🔍 Analyze Gear — rescrapes current page, shows per-slot panel
with top 4 candidates, scores, prices, and upgrade highlights
- 🔄 Clear Buy + Rescan — wipes stale buy data, rescrapes, analyzes
scrapeBuyPage() now also scavenges the full dynjs_eqstore for items
not visible in the current filter tab — one Purchase visit captures
the entire store (429+ items), no need to click through tabs.
Console API: HV.analyzeGear(), HV.gearPanel(), HV.clearBuy()
- guidance.js now suggests Nintendo (4x) instead of IWBTH (5x) for
the Veteran tier (150-300). You're dropping down to clear arenas
while the current gear catches up.
- Updated the reason text to '10x EXP. Better drops than Hell, while
still comfortable.'
Bug: spirit toggle ON → Imperil intercepts → toggle OFF → never fires skill.
The 3-state flag for spirit burst worked, but debuffs (Imperil at step 6)
came before weapon skills (step 11) in the priority chain. Each time the
flag advanced past 1, Imperil fired instead of Rending Blow, consuming
the turn and causing spirit to toggle off without ever using the skill.
Fix: debuffs now check _spiritForSkill === 0 before firing. When spirit
burst is pending (flag >= 1), skip Imperil/Weaken so the weapon skills
section fires uninterrupted.
- Step 0e (emergency weapon skills): requires STATE.monsters.length >= 3.
With 1-2 enemies, basic attack and tank the hits — better to farm OC.
- Step 11 (normal weapon skills): isHighThreat trigger also requires >= 3.
If OC >= 60 naturally, skills still fire regardless of enemy count.
- Great Cleave on rares still works at lower OC since rares are worth
killing fast regardless of group size.
- Rending Blow now uses findOptimalDominoTarget() which scores monsters
by how many alive neighbors are within 2 slots left/right. This
ensures RB hits max targets instead of wasting half the AoE on
edge monsters (previously used findDangerousMonster which ignores
positioning).
- Removed STATE.hp > 0.40 guard from step 2c OC-building check. When
Spark of Life procs, hp=0 in the DOM but the player is still alive.
The old guard blocked OC building in this state, causing Imperil
to fire instead of basic attacking to build OC.
New step 2c: when 5+ monsters, Rending Blow is ready, and OC < 60,
skip Imperil/Absorb/buff loops and basic attack to build OC.
In your log, RB was ready (sk:RRGR) from turn 1 at OC=20, but the
script spent 20 turns on Imperil x3 + Absorb (999!) + Curx3 +
items before finally firing RB at turn 21 when monsters were at
100+ SP. Now it basic-attacks aggressively to hit OC=60 faster,
then fires RB as soon as threshold is reached.
Log line now shows:
- spBars: 0:29A 1:28A 2:59A 3:52A 4:34A 5:43A 6:29A
(A=alive onclick, D=dead/no onclick — so you know which SP bars
are actionable vs leftover from dead mobs)
- sk: RC CC RC (R=Rending Blow Ready/C=Cooldown, S=Shatter, G=Great Cleave)
— first char of skill name + R/C for ready/cooldown
- h5 s3 m4 (heal=5, spirit=3, mana=4 — item counts per type)
— so you can see consumable status at a glance
- Threat threshold raised from 70% (84px) to 80% (96px) — stops
wasting OC on Rending Blow when enemies are at 89 SP (31 points
from actually specialing). In the log, RB fired at OC=50 on 89SP
mobs which wasn't an emergency.
- Single-monster stalling now triggers regardless of enemy SP level.
Previously blocked by !hasHighThreat — if the last enemy had high
SP, stalling was skipped and the script killed immediately, wasting
the stall opportunity for skill cooldown recovery.
- New stalling logic: when exactly 1 normal enemy remains (not rare,
not high threat), refresh any buff below 15 turns instead of attacking
- This naturally delays the killing blow by 1 turn, letting Rending Blow
and Shatter Strike cooldowns tick down before the round ends
- Result: next round starts with skills ready + higher buff uptime
- Edge case: one-tapping the last enemy would end the round with skills
still on cooldown — now avoided
- The toggle-off (state 2 -> OFF) was inside the monsters.length >= 7 check
- If Rending Blow killed enough enemies to drop below 7, the else block
reset _spiritForSkill = 0 without toggling spirit off, leaving it ON
- Now toggle-off guard runs independently of monster count
- Also resets flag if spirit gets toggled externally
- Raised heal threshold from 60%@7+ to 65%@5+ (earlier healing in big groups)
- Cure threshold also dynamic: 65% when 5+ mobs, normal otherwise
- Secondary weapon skill dump restricted to 3+ enemies (no OC waste on cleanup)
- Great Cleave already limited to rares — kept as-is
- Simulation confirmed +7 rounds deeper (R30->R37 avg) from OC conservation
- Source: agy_oc_output.txt
AGY built a full 60-round arena simulator (hv-battle-sim.py) using
the user's actual gear/stats and ran 10 iterations:
Original: 0/10 survived (0%)
Improved: 10/10 survived (100%)
Critical changes from simulation findings:
- Health items + Cure moved to TOP of priority chain (before skills)
- Dynamic HP threshold: 60% when 7+ mobs, normal threshold otherwise
- SP threshold raised from 55% to 60% for Spark safety buffer
- Mana maintenance raised to prevent spell lockout
- AoE crowd control prioritized for 4+ mobs
Full report in agy_sim_output.txt
- Bug 1: Purchase tab URL is 'screen=purchase', not 'screen=buy'
- Bug 2: dynjs_eqstore is declared with 'const', not 'var', so it doesn't
create a window property. getHVEquipStore() now also checks bare names
which works with @grant none and the IIFE scope chain
- Fixed price parsing: purchase table has 2 td cells (label + price),
previously required >3 cells so prices were always empty
- Names with 🔺 prefix are cleaned before storage
- scrapeBuyPage() scrapes equipment for sale from Bazaar Buy tab
(URL: ss=am&screen=buy or ss=bi), marks as source='buy'
- Stores merge with existing: buy items don't overwrite equipped/armory
- Auto-detect routing handles buy page URL patterns
- analyze_gear.py updated with 3-source output:
🟢 Equipped = currently worn
📦 Stored = in armory inventory
🛒 For Sale = purchasable from Bazaar
- Shows level limits (Lv155 + 15 = Lv170 cap), prices where available
- Re-scrape after visiting Buy tab to include market items
- When 7+ enemies and OC >= 60: toggles Spirit Stance ON before casting
Rending Blow, then toggles OFF after one skill
- Uses 3-state flag: press1=toggleON, press2=castSkill, press3=toggleOFF
- Only triggers on normal (non-panic) Rending Blow — step 0e emergency
skill still fires without spirit to avoid delay
- Resets on battle init and when conditions change
- skillOC default lowered from 75 to 60 — with 300ms Q debounce, it took
~6 presses to go from 50 to 70 OC (too slow against IWBTH enemies)
- Threat threshold lowered from 85% (102px) to 70% (84px) — triggers
earlier when monsters have high SP bars
- Added single-monster emergency: if ANY enemy >90% SP, fire skills
even without 2+ monsters (catches the lone special attacker)
- Step 0d high-threat check now fires for solitary imminent threats too
- Previously only scraped items visible in the current filter tab
(had to visit each tab: New, Two-Handed, Light, Heavy, etc.)
- Now also scans the full dynjs_equip store for ALL items in your
account (305 total) — every filter tab's worth of data at once
- Category guessed from tooltip type (Cloth, Light, Heavy, Weapon, etc.)
- One visit to the Armory page + HV.scrapeGear() = complete inventory
- 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