52 Commits
Author SHA1 Message Date
Alex 74c3050163 Merge pull request #31 from alexpolo1/feat/prices
prices: diablo2.io scraper + discord report + FG date fix
2026-08-28 14:47:10 +02:00
alexpolo1andClaude Opus 5 fd889a4df7 tools(spellbook): detect grid state, template-match the hotkey labels
Three fixes from running it against the live game:

1. It blind-pressed the open key. The grid was ALREADY open, so the press
   CLOSED it and the scan read grass. Now it detects the grid via its bottom
   hint and toggles only when needed, restoring the state it found. The hint
   match is fuzzy because Tesseract renders it "PRESS FI-F8 TO BIRD A SKIN"
   (BIND->BIRD, SKILL->SKIN).

2. The tooltip ROI was fixed, but the tooltip renders ABOVE the hovered cell
   and moves with the row, so it was reading the game world. Now taken relative
   to the cell. Skill identification is a fuzzy match against a known list
   rather than a demand for clean OCR: real reads included "XI BLESSED HAMMER"
   and one frame OCR'd Fist of the Heavens as "LNMEPULE".

3. Hotkey labels are read by TEMPLATE MATCH, not OCR. They are ~20x12px of
   white glyph over whatever icon is behind them; brightness thresholding
   cannot separate the two when the icon is also bright (F4's swirl, the row-4
   weapons) and OCR managed 4/8. A top-hat isolates small bright features
   regardless of background: 8/8 with zero false positives.

   Matching a same-SIZED crop is 1px-brittle — int() rounding in the grid
   geometry lands a pixel off the measured centre and the score collapses from
   ~1.0 to ~0.4. Exactly the three cells where truncation differed failed. A
   4px search slack fixes it; 8/8 held at every threshold 0.60-0.78.

Adds assets/templates/skill_binds/f1..f8.png, cut from a frame with all eight
labels visible.

Verified live: correctly reported conviction=f5 as casting TELEPORT and
concentration=f8 as casting CONVICTION — both confirmed by hand beforehand —
and found Concentration sitting unbound.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-28 10:42:36 +02:00
alexpolo1andClaude Opus 5 2feb13f44a tools: add spellbook mapper — read skill binds from the game, compare to config
Answers "which key is each skill actually on?" with screenshots and clicks, no
external service. For each cell of the bind grid it hovers, OCRs the tooltip
for the skill name, OCRs the icon's upper-right corner for the bound F-key, and
optionally saves the icon as a template with that corner blanked.

The corner is excluded from the saved template on purpose. D2R draws the hotkey
label there, so an icon captured with it only matches while the skill stays on
that key — rebind it and the template silently stops matching, looking like
template rot rather than a bind change. Same pixels, read separately as data.

Why it exists: on 2026-08-28 an Enigma put Teleport on F5, displacing
Conviction, while config still said conviction=f5 — every attack-aura cast
would have teleported the character mid-fight. F7 was Vengeance, not Holy Bolt;
F8 was Conviction, not Concentration. The startup preflight reported this
correctly and it was dismissed as a marginal template.

Two things learned building it, both encoded here:

- The tooltip renders ABOVE the hovered cell and moves with the row, so a fixed
  ROI reads the game world. The first version returned "YEW Y" and "PET". The
  band is now taken relative to the cell.
- Identification is a fuzzy match against a known-skill list rather than a
  demand for clean OCR. Real reads included "XI BLESSED HAMMER" and "HOLY
  SHIELD L", and one frame OCR'd Fist of the Heavens as "LNMEPULE" while the
  full text still contained the name. Validated 7/7 offline against saved
  frames.

It refuses to compare when it clearly could not read the grid (<3 skills or 0
bound keys) and exits 2. The first version scanned a closed grid, identified
nothing, and then reported all seven configured keys as unbound — presenting
its own blindness as findings.

Exits 1 on a real mismatch so it can gate a run. Menu entry:
    python tools/testbed.py spellbook --assets

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-28 10:27:07 +02:00
alexpolo1 5b386391df prices: diablo2.io scraper + discord report + FG date fix + refreshed data
New tools:
- tools/diablo2io_price_scraper.py: scrapes public diablo2.io trade
  listings (browsetrades.php), extracts item/WTS/WTB/desc/price
  mentions, merges into daily_prices.json as third source
- tools/discord_price_report.py: posts daily price summary embed to
  Discord webhook (top FG, top trade, movers vs last report)
- tools/discord_price_scraper.py: Discord channel price scraper
  (needs bot token; not wired into pipeline yet)

Fix:
- tools/improve_fg_estimates.py: parse post date from page HTML
  instead of file mtime (fixes day-bucketing on re-downloads)

Data (2026-08-28):
- d2jsp: 808 topics, 21 with parseable dates
- FG: day_6 (Cham 7.5), day_7 (Aldur 20, Gul 65, Ist 70, Anni 575)
- Traderie: 500 listings, 157 items
- diablo2.io: 150 listings, 28 items
- Pushed 229 prices to .96, prices.alw.dk rebuilt 08:14
2026-08-28 10:19:02 +02:00
alexpolo1 c26f249268 fix(input): snap cursor to exact target before click (SetCursorPos can land 1-2px off)
D2R UI hitboxes are tight; a 1px offset on the Join Game tab click was
enough to miss the button. Final SetCursorPos pass after the existing
verification check.
2026-08-25 00:26:44 +02:00
alexpolo1andClaude Opus 5 ce930f2a76 feat(tools): add launch_d2r — direct exe launch with a Play-button fallback
Launching D2R by hand kept failing in two different ways, both silent:

1. D2R.exe direct is fast but can come up with "Cannot Connect to Server" when
   the client has no Battle.net session.
2. The launcher's Play button always yields an authenticated client, but a
   hardcoded coordinate for it clicks the DESKTOP whenever the launcher has
   moved, been minimised to tray, or is DPI-scaled — which opened unrelated
   applications rather than reporting a failure.

So: try the exe, fall back to Play, and find Play by COLOUR rather than a fixed
point. It is the large saturated-blue block in the launcher; sampled live it is
HSV ~(104, 255, 122), and the value channel being that low is why a naive
"bright blue" threshold matches nothing.

show_launcher() also restores/maximises the window first, since the button
cannot be found while the launcher is hidden in the tray.

Never passes params.ini launch_options: those resolve to "-mod profile -txt",
and -mod puts D2R in offline mode where ladder does not exist.

client_size() imports utils.misc for its side effect of setting per-monitor DPI
awareness. Without it GetClientRect returns logical pixels, so a correct
1280x720 client reads as 1024x576 under 125% scaling and looks like a
resolution fault that is not there.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-23 08:30:39 +02:00
alexpolo1andClaude Opus 5 1d8a6657e2 fix(melee_hunt): make move/scan/attack a fixed rotation, not a fallback
Movement was the else-branch of "did the scan find anything", and motion
detection almost always finds something, so roamed was 0 in every single run.
She never relocated — she churned on one spot re-detecting the same movement,
which is why XP crawled (140 xp in 6 minutes) and why she appeared to ignore
the rest of the map.

Make the rotation unconditional: move N steps (--move-clicks, default 3), then
scan, then fight until clear or --max-engagements, then repeat.

Also stop her running past targets. At range she now force-moves toward the
target instead of left-clicking: a left-click that lands on ground rather than
on the monster is a MOVE order, so any small offset in the motion-blob centre
turned an attack into a walk-past. Only inside --melee-dist does she click, and
then with stand-still held so a swing can never be reinterpreted as movement.

Adds --max-engagements, which the rotation referenced but was never defined as
an argument — that raised AttributeError mid-run and aborted the session.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-22 20:09:20 +02:00
alexpolo1andClaude Opus 5 81c8d65375 fix(set_controls_keyo): also cover builds without skill_preflight rules
get_build_skill_checks() only knows hammerdin/fohdin/blizz_sorc. For any other
build (basic, basic_ranged, ...) it returns an empty list, so the tool fell back
to [char] keys alone and never checked the build's own hotkeys — e.g. a
basic_ranged character reported only town_portal, leaving right_attack and
buff_1 unverified.

When no preflight rules exist, read the section named after the build type
instead. Only applies when the rule-based lookup found nothing, so hammerdin and
blizz_sorc are unaffected.

Verified across three builds against real .keyo files:
  hammerdin    (profile1)   10/10 ok
  blizz_sorc   (ding)        7/7  ok
  basic_ranged (ding_level)  3/3  ok  — was 1/3

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-22 19:44:46 +02:00
alexpolo1andClaude Opus 5 28b7c210d8 fix(set_controls_keyo): verify the active build's skills, not a hardcoded list
SKILL_KEYS was a fixed Paladin list read via Config().char.get(). Build skills
live in their own sections ([hammerdin], [blizz_sorc], [sorceress]), not [char],
so that lookup returned empty for all of them and they were never checked.

In practice the tool only ever verified 3 keys — teleport, battle_orders and
battle_command — for every build. A hammerdin's blessed_hammer/concentration/
redemption/vigor/conviction/holy_shield went unverified, and a sorc got no
build coverage at all (blizzard, ice_blast, static_field, energy_shield,
telekinesis all silently skipped).

Derive the list from skill_preflight.get_build_skill_checks() instead, which
already resolves each build's skills out of the right config section, and keep
[char]-level keys (teleport, town_portal) alongside. battle_orders/
battle_command are now gated on cta_available so a character without a Call to
Arms doesn't report two permanent false "unbound" entries.

Also print the build alongside the file so it is obvious what is being checked.

Verified against two real .keyo files:
  hammerdin (fistman) — 10/10 keys ok, was reporting only 3
  blizz_sorc (ding)   —  7/7 keys ok, was reporting only 1

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-22 19:44:46 +02:00
alexpolo1andClaude Opus 5 aa48c29df8 fix(set_controls_keyo): select .keyo by char_name, not the profile name
find_keyo() matched the .keyo filename against general["name"], which is the
bot profile ("profile1"), not the character ("fistman"). No file ever starts
with the profile name, so it silently fell through to files[0] — the
alphabetically first .keyo in Saved Games.

In practice that meant `testbed.py keyo` verified Burr114743261.keyo while the
bot itself read Fistman211469871.keyo (key_detector correctly uses char_name),
reporting 3 unbound keys for a character that isn't being played. Running
--fix would have WRITTEN those binds into the wrong character's file.

Now keys off char_name (falling back to name), and normalises both sides
before prefix-matching, since D2R suffixes the file with an account id
("Fistman211469871.keyo") — the same approach key_detector._find_key_file
already uses.

Also stop falling back to an arbitrary character when char_name is set but
matches nothing: raise with the list of files found instead. Silently
verifying/writing someone else's bindings is worse than failing.

Verified: testbed.py keyo now reports the correct file and
"Controls layer matches params.ini".

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-22 19:44:46 +02:00
alexpolo1andClaude Opus 5 f1b0f6439c feat(tools): add melee_hunt — motion-based monster scan with staff attacks
target_detect.get_visible_targets cannot see ordinary monsters. FILTER_RANGES
holds two HSV bands only — "poison" (hue 38-70) and "frozen" (hue 110-120) —
so it finds monsters already tinted by Poison Nova or Holy Freeze and nothing
else. Measured against a live Fallen and zombie in Blood Moor: 0 targets at
every radius from 300 to 1280. That is why run_level, run_cold_plains and
firebolt_roam all roam without ever attacking on a fresh character.

Scan by motion instead: D2 monsters animate continuously, so differencing two
frames ~180ms apart lights them up against static terrain. The HUD and a radius
around the character are masked out to drop her own animation. Verified live —
38 engagements in 2 minutes and a confirmed Fallen kill.

Attacks with plain left-click, which walks to and hits the target: correct for
a clvl 1 sorceress with a staff and no skills (a level 1 character has zero
skill points, so there is nothing to cast).

Includes potion handling via the same meters.get_health the bot's health_manager
uses: drinks a healing potion at or below --heal-at (default 0.30). When nothing
is found it takes --move-clicks steps (default 3) before rescanning, rather than
rescanning after every single step.

Also carries tools/manual_drive.py onto this branch — it was committed only on
feat/manual-drive-seasonal-char, so it vanished from the working tree here.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-22 19:20:58 +02:00
alexpolo1andClaude Opus 5 fc9a7bfae1 fix(misc): make focus_d2r_window actually win the foreground
Windows refuses SetForegroundWindow from a process that does not already own
the foreground. The refusal is silent-ish and was swallowed, so the window never
came forward and every subsequent key and click went to whatever window WAS
focused. Measured on a live client: pressing the inventory key changed 0.89 mean
pixel value (i.e. nothing happened); after the fix, 16.44.

Attach our input queue to the current foreground thread for the duration of the
call — the documented way round the foreground lock — and return True only if
D2R actually ended up foreground, instead of returning True for "no exception
was raised".

Also harden tools/firebolt_roam.py with two preflights, because both failure
modes are silent — the character just stands there:
  - refuse to start if D2R cannot be brought to the foreground
  - refuse to start if the attack hotkey does not change the right-skill icon,
    i.e. no skill is bound to it (--skip-skill-check overrides)

Verified live: with nothing bound to F1 the tool now exits with an actionable
message rather than roaming silently casting nothing.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-22 09:20:45 +02:00
alexpolo1andClaude Opus 5 fd82f0ff05 feat(tools): add firebolt_roam — standalone roam-and-cast for levelling
src/run/cold_plains.py needs a waypoint, the town manager and the bot state
machine. None of that is usable for a clvl 1 character standing in Blood Moor,
which has no waypoint at all.

This starts from wherever the character already is: scan with target_detect ->
cast the configured skill at the nearest target -> roam if nothing is visible.
No waypoints, no town, no state machine.

    python tools/firebolt_roam.py --key f1 --minutes 10

The skill is a plain hotkey, so Fire Bolt now and Fireball at clvl 12 is the
same command. Stop key (default F12) is polled between every cast, and there is
a hard --minutes budget.

Explicitly NOT included: potion, chicken or death handling. Documented in the
module docstring — it will keep casting while dying, so it wants supervision.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-22 09:00:55 +02:00
alexpolo1 c04c60ead7 release: v0.9.1 — production cleanup
- Restore FIRST-RUN.md onboarding guide
- Pin environment.yml to requirements.txt (was loose 24 packages)
- Remove debug screenshots from git (debug_*.png -> .gitignore)
- Remove .hermes/ dev plans from git (already in .gitignore)
- Archive botty_next/ test harness to alexpolo1/my-botty-tools
- Archive docs/legacy-go/ to alexpolo1/my-botty-tools
- Move 14 dev tools from repo root to tools/ directory
- Extend .gitignore to prevent dev artifacts re-entering root
- Promote stable branch with all bug fixes from main
- Bump version 0.8.4 -> 0.9.1
2026-08-08 09:59:31 +02:00
alexpolo1 6012934e66 Add chipped gem conversion runner 2026-06-21 16:06:58 +02:00
alexpolo1andClaude Sonnet 4.6 f31c30f388 security: untrack cached d2jsp scrapes, parametrize cookie helper
- git rm --cached data/d2jsp_pages (102 files, already gitignored) — saved
  authenticated HTML embedded the live d2jsp msec session token
- apply_manual_cookies.py: read member_id/msec from env vars instead of
  hardcoding the real session token

History purge of these blobs follows in the same cleanup.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-20 10:33:50 +02:00
alexpolo1andClaude Sonnet 4.6 b9f6635f8f security: stop tracking cookies.txt, ignore secrets, drop personal paths
Pre-public cleanup:
- Remove cookies.txt from tracking (held live d2jsp session cookies
  member_id + msec) and delete the local copy
- .gitignore: cookies.txt, cookies_temp*, *.cookies, config/custom.ini
- fg_scrape_pipeline.sh: replace hardcoded /c/Users/alex/Downloads path
  and /c/Python313/python with a script-relative cd and $PYTHON from PATH

NOTE: cookies.txt still exists in git history (commit e3d6605) — a
history purge + cookie rotation is still required before going public.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-20 10:28:47 +02:00
Alex 5c687773b1 Click-and-go install: self-bootstrapping install.bat + tesserocr DLL fix (#2)
Merges fix/stash-full-guard-and-testbed into main. See PR #2 for full description.
2026-06-20 09:27:12 +02:00
alexpolo1 6a8f1ed5d9 add fiskenersej character profile + pickit profile cycler (F10) 2026-06-12 12:08:21 +02:00
alexpolo1andClaude Fable 5 40e4dd8a4f Skill hotkeys select the RIGHT slot - fix cast flow, prove auto-binding live
Live finding (fresh game, 2026-06-12): D2R skill hotkeys select onto the
RIGHT skill slot, not the left as the old comments assumed. The left slot
permanently holds Blessed Hammer. Consequence: _cast_hammers pressing the
hammer hotkey after activating an aura was REPLACING the aura on the right
slot every cast cycle - the true root cause of fights running without
Concentration. _cast_hammers no longer touches the hammer hotkey: select
aura (lands on right, stays active), hold stand-still, spam left-click.
Verified live: full Diablo kill at 09:04, ~55s from last seal to kill.

Auto skill binding proven end-to-end (tools/set_binds_from_params.py):
blessed_hammer/concentration/redemption/vigor/holy_shield/teleport all
bound via the in-game picker and visually verified (6/7 OK; conviction
correctly reported missing - not skilled on this char).

- capture tool + preflight verify now watch the RIGHT slot
- fresh skill slot templates + clean PICKER_* cell templates captured at
  current settings (blessed_hammer, concentration, redemption, vigor,
  holy_shield, teleport)
- removed bogus conviction.png (had captured vigor's icon)

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-12 09:13:52 +02:00
alexpolo1andClaude Fable 5 77b35df26b Two-layer keybind verification/setting driven by params.ini
Layer 1 (Controls page / .keyo): tools/set_controls_keyo.py parses the
character keyo (same binary format as utils/key_detector) and verifies every
skill hotkey in params.ini is bound to a skill slot; --fix writes missing keys
into free skill slots with a backup (D2R must be closed). Picks the configured
character file. Verified: fistman controls match params.

Layer 2 (skill assignment / picker): enabled the existing skill_hotkey_setter
machinery for the hammerdin build:
- skill_preflight: hammerdin build rules (blessed_hammer left/required,
  concentration right/required, redemption/vigor/conviction/holy_shield/
  teleport right/optional) + PALADIN_TEMPLATE_ALIASES, merged TEMPLATE_ALIASES.
- skill_hotkey_setter: paladin picker template aliases with slot-icon fallback.
- tools/set_binds_from_params.py: end-to-end runner - opens the in-game picker,
  binds each skill to its params hotkey, verifies via slot icon, reports
  match/mismatch per skill.
- tools/capture_skill_hotkeys.py now saves crops straight into
  assets/templates/ui/skills/ so captures immediately become live templates
  for both the preflight and the picker search.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-12 08:30:43 +02:00
alexpolo1andClaude Fable 5 63248a9437 Hammerdin: hold Concentration through trash clears; add deps doc + keybind tool
- hammerdin.py _cast_hammers: skip aura/hammer re-selection when the slot
  already holds the wanted skill (tracked via _active_skill, invalidated by
  pre_move/_weapon_switch/cast_buffs and all raw redemption/vigor hotkey
  sends). Visible effect: aura stays on right-click and hammers fly from
  plain left-click spam instead of constant F1/F3/F8 churn.
- 35 mid-clear attack casts switched from Redemption to Concentration
  (damage aura while fighting); Redemption still pulses between packs and
  after kills for corpse cleanup.
- DEPENDENCIES.md: full verified working-state snapshot (env paths, package
  versions, tesseract wiring, D2R settings, DPI specifics, safety nets).
- tools/capture_skill_hotkeys.py: presses each params.ini skill hotkey
  in-game and saves labeled left-slot icon crops - verifies binds match the
  character and builds skill-icon templates for future aura checks.
- CLAUDE.md: remove stale main.exe mention (no exe build exists; bat runs
  current source directly).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-12 00:18:23 +02:00
alex f1d6c6bd1c ci: align validation environment with Windows 11 2026-06-07 18:31:10 +02:00
alex 86e3cccbc9 feat: detect Windows profile for install and input 2026-06-07 18:26:16 +02:00
alex 8ccdca7946 fix: restore validation suite 2026-06-07 18:17:44 +02:00
alex f59ffd5d1f fix: raise stash page limit to 20 for ROTW support, fix merc_icon ROI, add red resurrect button detection 2026-06-01 09:27:42 +02:00
alex 7d1f03beb7 chore: commit all changes 2026-05-31 12:51:21 +02:00
alex 7d6314b575 Save all config, data, and tools state 2026-05-30 20:21:15 +02:00
alex 21c33a3dc9 Add D2R Keys (Terror, Destruction, Hate, 3x3 Set) to daily price tracker 2026-05-30 19:55:10 +02:00
alex 16a4d121c0 feat: log rotation with Discord notifications + failure reason tracking
- Add utils/log_rotation.py: safe_imwrite() auto-rotates screenshot dirs
  when they exceed configured file count or size limits
- Add [log_rotation] config section in params.ini (pickit/info/items
  max_files, max_mb, discord_notify_rotation)
- Replace 40 cv2.imwrite() calls with safe_imwrite() across 13 files
- Add failure reason tracking: bot.py catches run exceptions, stores
  reason in game_stats, Discord message includes the error
- Add Discord notification when log rotation deletes old files
- Prevents disk-full crashes that stopped botty on May 30
2026-05-30 09:32:24 +02:00
alex ed65691257 Add post links to sellers: extract post anchors, store direct links to seller posts 2026-05-27 19:57:51 +02:00
alex ecf91683b7 Add query_fg_prices.py: CLI tool to query FG price database 2026-05-27 16:04:11 +02:00
alex 29e6cc3c99 Add query_fg_prices.py: CLI tool to query FG price database 2026-05-27 15:56:22 +02:00
alex 9c00ca5de7 Fix DB update script: correct prev_scrape query 2026-05-27 15:16:24 +02:00
alex dbee47e152 Add database update script for FG price tracker 2026-05-27 15:15:46 +02:00
alex 512acb02b9 Fix username extraction: correct regex for user.php links and split indexing 2026-05-27 15:12:30 +02:00
alex 93771198f1 Add username extraction: per-seller cheapest prices in output and DB 2026-05-27 13:42:36 +02:00
alex 5cfa91fe64 Major scraper overhaul: multi-page pagination, 80+ item patterns, multi-item detection 2026-05-27 11:23:32 +02:00
alex bc649ed863 Fix FG report: normalize item names, estimate FG from item_kept events 2026-05-27 09:38:14 +02:00
alex ef29136e16 Add FG daily earnings report tool (8h interval) 2026-05-27 08:50:57 +02:00
alex 51319fd11f Fix d2jsp scraper: Cloudflare bypass, relative URL handling, URL dedup, Python 3.10 compat 2026-05-27 01:13:10 +02:00
alex e3d6605595 Update FG price tracker, game stats, and add d2jsp collection tooling 2026-05-27 01:08:45 +02:00
alex 19f4b8a553 Harden offline FG parser and add d2jsp collection tooling 2026-05-26 13:04:28 +02:00
alex c96fcdf545 Improve dependency checker with concrete remediation steps 2026-05-25 15:13:37 +02:00
alex b77d62144a Add requirements.txt package validation to dependency checker 2026-05-25 15:11:58 +02:00
alex e573a28e8a Add Windows dependency health-check script and launcher 2026-05-25 15:10:23 +02:00
alexandClaude Sonnet 4.6 dc224bec5c Fix CTA spam, traverse_nodes_fixed crash, and add skill preflight/setter
- config: cta_available=0 (user has no CTA weapon; was causing 5 failed
  weapon-swap attempts per run)
- pather: traverse_nodes_fixed now allows can_teleport_with_charges chars
  through instead of raising ValueError; when charges deplete mid-path
  char.move() falls back to walking gracefully
- utils: add skill_preflight.py (visual hotkey verification, all blizz_sorc
  skills use side=right) and skill_hotkey_setter.py (automated picker binding
  with step-by-step logging)
- test: add test_skill_preflight.py (3 passing tests)
- assets: add sorc skill icon templates for preflight matching
- tools: add capture_sorc_skill_icons.py and set_sorc_skill_hotkeys.py
- key_detector: extend VK map with numpad/F-key/symbol codes; add
  validate_key_bindings() and parse_key_file(); fix char_name lookup

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 19:26:00 +02:00
alex 77d84680c6 fix CI: remove broken pytest-pythonpath, add test→build pipeline, fix test asset guards 2026-05-22 23:24:41 +02:00
alex 9bbc753a16 feat: complete key auto-detection with binary .key parser
- Parse D2R binary .key format (10-byte entries, action=1=skill, action=0=non-skill)
- Detect skill slot bindings and validate against params.ini
- Scan Saved Games for any .key/.keyo file (handles battle tag names)
- Config loads cleanly, warns on skill/non-skill mismatches
2026-05-19 18:32:18 +02:00
alex 0af8a54116 feat: auto-detect D2R key bindings from .key/.keyo file
- Read character .key file from Saved Games / D2R install dir / APPDATA
- Auto-fill empty hotkeys in [char] section (inventory, belt, potions, etc.)
- Match skill slots to configured skills for validation
- Normalize key variants (left alt ~ alt, left shift ~ shift)
- params.ini values always take priority over detected bindings
- Warn on genuine mismatches between config and .key file
2026-05-19 16:46:24 +02:00