- Fix: chainState/chain used renamed var (jpxChainEnabled vs jpxChainStarted)
- Fix: jpxChainStart lazy-retries (10x/600ms) since jpx creates #ctrl-widget lazily
- Remove duplicate jpxChainStart definition
- HV.diag() now reports scripts.jpx + jpxBridge state for field diagnosis
- Listen for jpx_ctrlWidget_update (detail.active) — jpx's own state signal
- Re-trigger M when jpx reports inactive + monsters present + not battle over
- Fallback 2s poll in case the event is missed on page reload
- Cooldown guard (2.5s) to avoid double-trigger
- Stops on maxRounds / low HP / battle finish
Parser fix: parseEquipHTML now captures the direct-child divs of the
tooltip that were previously skipped:
- Proc lines: 'Penetrated Armor: 21.7% chance' -> stats['Proc Penetrated Armor']
- Base damage: '+708 Piercing Damage' -> stats['Weapon Damage']
- Elemental lines: '+25 Fire Damage' / '+30 Elec Strike' -> stats['Elemental Fire']
Scorer (gearScoreWeapon + analyze_gear.py):
- Weapon Damage x 0.30 (base damage matters)
- Elemental damage x 0.25 per element
- Proc bonuses by type: CC procs (stun/freeze) x0.45, armor break
x0.35, DoT x0.25, generic x0.15
- All multiplied by the same quality multiplier afterward
So an elemental weapon with a strike proc now gets its due — the
short-margin stat winner may no longer win once damage+proc count.
Note: re-scrape the armory/store after updating so the new fields
get captured (old entries lack Weapon Damage/Proc keys).
The 7+ mob burst toggle (step 0d) ignored CFG.autoSpirit and always
fired. It enabled spirit, then other priority steps (debuffs, buffs,
items) intercepted before the weapon skill fired, wasting the toggle
cycle and OC. Also the off-toggle didn't always fire.
Now: autoSpirit=false (default) → no auto-enable at all. Manual
spirit toggle only. The settings panel toggle ('Auto Spirit Stance')
already existed — it now actually gates the behavior.
Root cause: gearDetectSlot used substring matching. The Body keyword
'gi' (for gi/jacket) appears inside the word 'leggings' (l-e-g-g-i-n-g-s),
so EVERY leggings item was classified as Body:
- Legs slot showed '(no data)' — its equipped item was mis-detected
- Body slot showed leggings items as top candidates (176 score)
Fix: word-boundary regex match (\bgi\b) so 'gi' only matches as a
standalone word. Also protects against 'cap' in 'capacity' etc.
- Added filter dropdowns to the panel: Armor (Light/Heavy/Cloth/All)
and Weapon (2H/Estoc/Longsword/Great Mace/Club/Axe/Scythe).
Persisted in localStorage. Default: Light armor, 2H weapons.
Filters exclude non-matching items from armory/buy candidates
(equipped item always shown regardless).
- Fixed gearPlayerLevel(): now reads STATE.level first (battle
parser caches it), then the hv-cfg-btn label (Lv195), then
level_readout. Previously fell back to 155 when the level readout
used CSS fonts — this excluded the Lv171 equipped Estoc from
candidates, causing 'vs ?' in the upgrade line.
- Upgrade line now always shows a real number (scores equipped
directly if it missed candidates).
Also scrapes correctly on equipment pages: clicking Analyze there
refreshes equipped slots from dynjs_equip store.
Bug: init.js only called enhanceGearAnalysis() when STATE.page ===
'armory'. The equipment page (ss=eq) maps to STATE.page ===
'equipshop' in page-detector.js, so the button never rendered.
Fix: added the equipshop case.
- enhanceGearAnalysis now also fires on Character > Equipment pages
(id=eqsb), not just Bazaar (equiplist)
- On equipment pages only the Analyze button shows (Rescan hidden —
no store/inventory data to rescan there, but the localStorage DB
is still accessible for the comparison)
- Clicking Analyze on ss=eq rescrapes equipped slots + opens panel
with stored/purchasable comparisons from localStorage
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)