Compare commits

..
186 Commits
Author SHA1 Message Date
alex 1386afc7da fix release bundle: main.exe naming, full zip with config+assets, add release trigger 2026-05-22 23:28:21 +02:00
alex 88d325cf8f fix CI: remove broken pytest-pythonpath, add test→build pipeline, fix test asset guards 2026-05-22 23:24:41 +02:00
alex 74ebef9b22 update params.ini, bot.py, win_input.py 2026-05-22 21:11:59 +02:00
alex 64578db453 add stealth system, key detection, click recorder, FOHdin, new routes, and tooling updates 2026-05-21 23:34:20 +02:00
alex 3d0e102afc document stealth, key detection, click recorder, and FOHdin in README 2026-05-20 20:06:21 +02:00
alex f9f26f36c3 merge main into mine 2026-05-20 20:02:52 +02:00
Alex ce5816a130 Initial commit 2026-05-20 19:55:00 +02:00
alex 644c2a106c feat: auto-launch D2R on botty startup
- main.py checks if D2R.exe is running, launches with auto-login if not
- Set char_name=Burr in config for OCR character selection
2026-05-20 09:20:37 +02:00
alex 4b003bd95e feat: auto-login + OCR character selection
- Added bnet_name, bnet_pass, char_name to [general] config
- Config._build_launch_options() appends -bnetname/-bnetpass when set
- character_select.py: OCR fallback when template matching fails
2026-05-20 09:16:48 +02:00
alex f2d23e511d feat: auto-login + OCR character selection
- Added bnet_name, bnet_pass, char_name config options
- Launch options auto-append -bnetname/-bnetpass when credentials set
- character_select.py: OCR fallback when template matching fails
- Scans character list row-by-row, matches by name, clicks and scrolls
2026-05-20 08:55:04 +02:00
alex 07be1a7b9c fix: npc_auto_label global declaration order 2026-05-20 07:44:39 +02:00
alex 8178104612 feat: enhanced stealth layer - click delay, endpoint wobble, behavior model
- custom_mouse.click(): added 50-800ms arrival-to-click delay (human hesitation)
- custom_mouse.stealth_move(): added endpoint wobble (2-5px micro-adjustment)
- stealth.py: new functions for keyboard timing, skill hesitation, wrong waypoint, skill mistakes
- params.ini: 8 new tunable stealth config options with documented defaults

All 46 files using mouse module automatically benefit — stealth is centralized.
2026-05-20 07:22:24 +02:00
alex 23b0f0f66b feat: parallel template search, async mouse moves, NPC auto-label
- template_finder.search(): parallel matching via ThreadPoolExecutor (4 workers)
- utils/custom_mouse.py: async_move() for non-blocking mouse movement
- utils/npc_auto_label.py: detect_visible_npcs() scans 16 NPCs in parallel
2026-05-19 22:47:30 +02:00
alex 2d69b246d9 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 4f0f3df8bb docs: improvements reference + d2r-key-detection skill
- Added references/improvements.md tracking all implemented fixes
- Created d2r-key-detection skill for future sessions
2026-05-19 16:50:10 +02:00
alex 2c25b67cf9 fix: target false positives, pickit timing, hardcore death loop
- target_detect: add aspect ratio filtering to reject health bars and immune text
- pickit: add 0.2-0.3s wait after pickup before moving on (fix #939)
- game_controller: detect hardcore mode and stop instead of infinite death loop
- config: add 'hardcore' flag (default 0)

Closes #959, #939, #942
2026-05-19 16:49:24 +02:00
alex f6e700c921 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
Alex 23e7e54f8d fix: resolve default.bnip merge conflicts from aeon0; fix raw string in config.py 2026-05-17 20:31:38 +02:00
Alex b52f7ebca9 fix: defer npc_manager template loading for headless Linux compatibility 2026-05-17 19:25:56 +02:00
Alex 329b7e77d9 fix: guard Windows-only imports for Linux compatibility (pywin32, mouse._winmouse, mss, rapidfuzz) 2026-05-17 17:56:29 +02:00
Alex 5ab9d1c68a feat: implement boss kill methods for fire_lock, hammerdin, fohdin + resolve params.ini merge conflicts
- Add kill_andariel(), kill_countess(), kill_mephisto(), kill_baal(), kill_baal_waves() to fire_lock (warlock), hammerdin, and fohdin (paladin)
- Use build-appropriate attack patterns (deathmark/chaos combos for fire_lock, hammers/FOH for paladins)
- Add configurable attack durations via atk_len_* in params.ini
- Resolve remaining merge conflicts from prebuff cherry-picks
- Add boss run config section: atk_len_andariel, atk_len_countess, atk_len_mephisto, atk_len_baal, atk_len_baal_waves
2026-05-17 15:53:08 +02:00
Alex 15a90bfd47 feat: add route scaffolding tool and contributor guide 2026-05-17 15:22:24 +02:00
Alex 17f32e65b2 feat: add 4 new boss runs (Andariel, Countess, Mephisto, Baal)
Full run implementations with approach/battle methods following the
existing botty pattern. Added 12 Location constants to pather.py,
12 placeholder path nodes to game.ini (coords recorded in-game later),
and 6 new kill_* methods to IChar interface. Updated params.ini
with new route documentation.

Path nodes are 0,0 placeholders — record real coords with
node_recorder.py in-game before enabling these routes.
2026-05-17 14:25:26 +02:00
Alex b397d07baf feat: implement 4 new boss runs (Andariel, Countess, Mephisto, Baal)
Full run implementations following existing botty patterns:
- andariel.py: Act 1 Catacombs L2→L3→L4, waypoint-based approach
- countess.py: Act 1 Forgotten Tower L1→L5 via Black Marsh WP
- mephisto.py: Act 3 Durance of Hate L2→L3 via A3 WP
- baal.py: Act 5 Throne of Destruction, wave clearing + boss

Added 12 Location constants to pather.py and 12 placeholder
path nodes to game.ini (record in-game with node_recorder.py).

Path node coordinates are 0,0 placeholders — must be recorded
in-game before runs can actually execute.
2026-05-17 14:24:46 +02:00
Alex eee99c069b feat(stealth): wire should_skip_run, maybe_afk_break, reshuffle_each_rotation into bot state machine
- on_end_run: calls maybe_afk_break() after returning to town
- run selection loop: checks should_skip_run() before each run
- on_init: reshuffles _do_runs_reset if reshuffle_each_rotation is enabled

Also scaffolded 4 new run stubs: baal, mephisto, andariel, countess
2026-05-17 14:07:01 +02:00
Alex 81ee6557f4 feat: add route scaffolder CLI, smoke tests, stealth docs 2026-05-17 13:26:31 +02:00
Alex f484a9bef1 feat(stealth): add anti-detection layer with wait jitter, click variance, AFK breaks, and route skip 2026-05-17 13:26:26 +02:00
RedJon18 6d19819de5 Prebuff updates and rare items for pickit logic
Updates the prebuffs for paladin to use holy shield before entering Pindleskin's portal. Also, adds additional pickit logic for picking up and storing all rare items when uncommenting the sections.
2026-05-17 13:21:42 +02:00
aeon0 71311cb584 fix not stashing gold if buying sth before 2026-05-17 13:21:14 +02:00
Riddle 5c4f62ac25 Add @cache to functions 2026-05-17 13:18:14 +02:00
Riddle 869de31315 fixed error code 2026-05-17 12:49:00 +02:00
Randi 6f6c663b43 Added eldritch routine to firelock. Also made minor adjustments to a3 error finding stash to reduce error rate. 2026-03-24 22:51:06 -04:00
Randi fdd0fc2230 Adjusted a3_stash_left pic as it could misclick cain on occasion. 2026-03-23 13:58:40 -04:00
Randi 5d5a9c2a63 Made improvements to resolve issues with finding stash in a3. Created left and right stash pictures to reduce issues of being covered. Also implemented move on first failure in an attempt to move merc. 2026-03-23 13:38:41 -04:00
Randi eb75fc9aee Made adjustment to determining if stash is full of gold. Also remade inv_no_gold picture as it would detect numbers starting with 91 as 0. 2026-03-23 12:11:27 -04:00
Randi 600904e6bc Reverted edge case for handling not finding stash. 2026-03-22 21:30:10 -04:00
Randi ce821695e8 Minor tweak to handle case when stash is covered by merc. 2026-03-22 21:17:24 -04:00
Randi 455fe8e64f Minor adjustments to warlock. 2026-03-22 20:44:23 -04:00
Randi f4bc3b7401 Added trav run to fire warlock and added ability to buff in town with consume. 2026-03-22 10:39:23 -04:00
Randi b528957c40 Minor tweak to cta functionity to disable buff_with_cta if cta_available is false. 2026-03-20 21:32:32 -04:00
Randi f8c9d81f52 Minor tweak to static. 2026-03-20 20:32:40 -04:00
Randi 1a5a261f7c Updated stash inventory roi based on warlock exp updates. 2026-03-19 19:53:07 -04:00
Randi 95e019e88d Updated the Run classes to properly reference the _do_run variables so changes to runs in bot.py will persist into Run classes 2026-03-19 19:09:39 -04:00
Randi 466197fd8b Fixed bug where bot would always check stash when gold was full even when stash_gold is disabled. 2026-03-18 00:20:21 -04:00
Randi 4a4bf0300a Added extra failsafe to ensure no keys get left held down when game controller kills bot thread. It was possible that a max length violation could occur while stashing and cauase ctrl and shift to get stuck down. 2026-03-17 20:35:59 -04:00
Randi 953b9aedb3 Corrected transmute to correctly keep track of transmute counter after x games. 2026-03-17 07:38:57 -04:00
Randi 2ce9636d3a Added additional delay after cta to swap weapons 2026-03-17 07:36:41 -04:00
Randi b03b2fd226 Refined cta and buffing so chars now use cast_buffs routine which doesn't need the cta_available logic. This routine also allows the casting delay to be kept separate from cta as the casting_delay is passed in. Failed buffs will now cause bot to exit game. 2026-03-16 20:42:15 -04:00
Randi d96eb2d34c Fixed bug when bot is killed due to max game length violation while transmuting. Transmuting should now complete before game exits. 2026-03-16 19:10:07 -04:00
Randi 5f2149ed1e Removed last node from trav run as it didn't seem necessary for TP and took extra time. 2026-03-16 18:42:24 -04:00
Randi a7509c90e7 Added functionality to transmute flawless gems from the stash gem tab. Works with the transmute every x games variable in params.ini 2026-03-15 23:24:56 -04:00
Randi 8b48b583b9 Fixed issue in which CTA was not properly detected if bot is started with incorrect weapon swap. 2026-03-15 20:13:03 -04:00
Randi 58f761c372 Added logic if doing trav runs we will skip additional runs if juvs are full. 2026-03-15 10:01:39 -04:00
Randi 36d364517b Reduced telek pickup range as it can fail if item is off screen but text is not. 2026-03-14 20:17:03 -04:00
Randi 869eeb5539 Added transmute routine for graphic debugger which requires manual uncommenting. May need to find a more automated way in future. 2026-03-14 08:42:53 -04:00
Randi b2afbaab93 Minor tweaks to pickup 2026-03-13 22:21:47 -04:00
Randi 9512856cfc Refined item pickup to properly tele, use telekinesis, and reduce delays when walking for pickup. 2026-03-13 20:22:25 -04:00
Randi 593e509bc6 Added gheed's wager, horizon boots, and sunders to be picked up. Also fixed error that was causing set and unique names to be improperly reported. 2026-03-11 19:14:51 -04:00
Randi 68ed191efc Made fix to ensure the new warlock expansion uniques would get stashed. 2026-02-27 20:43:31 -05:00
Randi 77925964a0 Fixed in town healing to trigger based on juv settings. Added error case for failed to join game. 2026-02-25 20:34:01 -05:00
Randi 665e6b6ce4 Minor bnip config update to add twink items. 2026-02-20 17:36:47 -05:00
Randi 51306f3067 Fixed the old graphics debugger. 2026-02-20 13:55:35 -05:00
Randi c47d9573ad Updated pick up code to accept red text to allow worldstone shards to be picked up. 2026-02-20 10:41:02 -05:00
Randi bbe5395f4a Added worldstone shards to be stashed. 2026-02-19 23:38:50 -05:00
Randi 6f7a9b681d Added all unique grimoires to default bnip config file. 2026-02-17 22:39:49 -05:00
Randi ed0a8270cf Added grimories to bnip files. 2026-02-17 22:22:14 -05:00
Randi 4b3a47b13c Updated bnip_data.py to add abyss skill to bnip. 2026-02-16 22:08:47 -05:00
Randi 6f4f4ca472 Updated bnip files to support warlock skillers. 2026-02-16 21:55:48 -05:00
Randi 55f7d86b3d Tweaked stash gold button roi as it moved in expansion. 2026-02-16 16:33:40 -05:00
Randi 01f03ea9af Handle the error case if save_and_exit fails. 2026-02-16 15:04:00 -05:00
Randi 82ac44c4c1 Minor change to abyss warlock targeting on pindle. 2026-02-15 22:24:13 -05:00
Randi 65ee5a6b5c Updated bnip for echo strike bases 2026-02-15 21:51:19 -05:00
Randi b7c83e6208 Added preliminary warlock characters for bot. 2026-02-15 20:49:12 -05:00
Randi f2c639fc64 Minor fix to a typo for void base entry in bnip 2026-02-15 18:58:08 -05:00
Randi 0978258513 Minor fix to bnip. 2026-02-15 15:19:06 -05:00
Randi b243aec74f Added void base to bnip 2026-02-15 14:24:31 -05:00
Randi ec5a7e4292 Fixed issue with a chicken not counting as a failed game. 2026-02-15 09:54:48 -05:00
Randi 4852ef9e3a Adjusted fast save and exit to work properly with new loot filter buttons 2026-02-15 09:36:51 -05:00
Randi d7a1d65348 Minor cleanup with tabs, and added a few entries to bnip. 2026-02-15 09:12:57 -05:00
Randi 758a0a3db8 Missed adding the transmute.py file in last commit for tabs. 2026-02-14 23:39:35 -05:00
Randi b27911adae Updated stash tabs to work with Warlock expansion. 2026-02-14 23:26:15 -05:00
Randi 43e5a8ecab Fixed the missing string issue and inventory tab names. 2026-02-14 20:17:05 -05:00
Randi b52ed15079 Updated barbarian prebuff delays, horking, and added howling. 2025-11-15 19:05:18 -05:00
Randi c82051f276 Adjusted barbarian to use leap attack skill. 2025-11-15 15:39:57 -05:00
Randi 6fc80f0a96 Fixed fast_save_and_exit routine if menu was alreay openned. 2025-11-15 14:17:54 -05:00
Randi 2da55e138c Added fast save/exit for game controller and synchronized exit with health manager. 2025-11-12 19:36:06 -05:00
Randi 5cbf7ea366 Removed synchronizatino of exit routine as it can cause issues due to threads getting terminated. 2025-11-11 21:55:53 -05:00
Randi a2d27fd4c3 Prevent chicken from being called twice in one loop. 2025-11-11 08:09:03 -05:00
Randi be446fc9d9 Minor comment update. 2025-11-10 23:34:30 -05:00
Randi 4b93a50927 Updated uses between juvs to be 16frames, the max recovery time, to avoid wasted juvs. 2025-11-10 22:49:37 -05:00
Randi 76cbe0abbc Synchronized the exit routines so two threads exiting the game don't stomp on each other. 2025-11-10 21:03:48 -05:00
Randi a54b674ce6 Updated chicken to save and exit faster. 2025-11-10 20:23:03 -05:00
Randi 11440a3d24 Hardcoded numpy in environment.yml as latest version causes issue. 2025-11-08 11:15:49 -05:00
Randi 5f478a6884 Updated development.md to reflect the correct git location for this fork. 2025-11-08 10:49:03 -05:00
Randi fb2edcf70a Minor fixes and tweaks to trav path. 2025-11-07 21:22:54 -05:00
Randi ed2a921d9e Fixed bug where bad inventory check would cause thread exception. 2025-11-07 21:22:13 -05:00
Randi ba8a412309 Updated item distance to make tele pickup further away as it is not reliable when items bunch up. 2025-11-07 21:21:09 -05:00
Randi 5d42a46891 Added trav attack routine for blizzorb sorc. 2024-09-14 21:18:19 -04:00
Randi c13367f221 Added large charms to bnip file for early ladder season. 2024-09-13 22:37:37 -04:00
Randi c94be67727 Minor tweak to nova sorc trav run. 2024-09-13 22:36:53 -04:00
Randi 1e8bb16a22 Added large charms to bnip for early ladder season. 2024-09-13 20:15:19 -04:00
Randi 3e87fe9d6f Simplified nova trav attack pattern and made it quicker. 2024-09-13 20:03:35 -04:00
Randi b517a80bea Fixed gull dagger in bnip. 2024-09-11 21:26:29 -04:00
Randi b2938e6297 Added crescent moon and doom bases to bnip file. 2024-09-11 20:46:44 -04:00
Randi 3a96bf69cc Enabled frostburns in pickit for early ladder season 2024-09-11 20:35:43 -04:00
Randi 429a1cd4ae Removed uneccesary statics from nova sorc pindle/trav runs. 2024-09-11 20:28:23 -04:00
Randi dab2fd9259 Fixed bug in bnip file that discarded war travs and death's fathom. Also enabled all early ladder season items. 2024-09-11 20:25:33 -04:00
Randi 8048d87222 Minor config file updates 2024-08-25 21:19:25 -04:00
Randi edcefbe62c Added config updates for blizzorb sorc. 2024-08-23 18:22:22 -04:00
Randi aea75aa417 Minor trapsin changes. 2024-08-23 18:19:50 -04:00
Randi 30d407e978 Reworked trapsin a bit to include casting/attack delays between skills. Also added mindblast and added to pindle run. 2024-06-26 19:59:50 -04:00
Randi 8df42bbcb1 Added attack frames into config.py 2024-06-24 22:04:58 -04:00
Randi da396208f0 Added attack frames to config and character class. 2024-06-24 21:02:34 -04:00
Randi 466c698b1d Updated Anya shopper to properly filter on +3 3/20 gloves. Also added MA 3/20 gloves in addition to the current jav gloves. 2024-06-24 20:54:15 -04:00
Randi 58bc20ce0e Made fixes to anya shop for shopping 3/20 jav gloves as previous implementation used old font which didn't work. 2024-06-20 19:50:53 -04:00
Randi 25fdd5485d Added action frame to paladin for accurate timeing with foh. Also added prelimnary foh clear screen sequence. 2024-06-18 19:51:25 -04:00
Randi bfccf60a07 Added testing attack routines for cs and summoner for blizzorb. 2024-06-18 19:42:32 -04:00
Randi 8be1223e32 Added javazon 2024-06-18 00:25:16 -04:00
Randi df1259f87c Fixed bug where health manager would drink rejuv back-to-back. 2024-06-16 20:32:49 -04:00
Randi 1ca3578e06 minor bug fixes to vizier run 2024-06-16 13:25:55 -04:00
Randi 3c70214ca9 Cleaned up the vizier runa and removed commented code. 2024-06-16 12:33:20 -04:00
Randi 8c44d7ad17 Added new vizier run to just kill vizier is cs, as diablo run has high failure rate. 2024-06-16 12:27:33 -04:00
Randi 4199705fe8 Expanded scope of thread lock in screen::grab() method. 2024-06-13 19:59:35 -04:00
Randi 7141e671d1 Fixed issue in which tele paladin would occasionally walk from eldritch to shenk. 2024-06-13 19:57:24 -04:00
Randi c20ec7343c Minor tweaks to blizzorb sorcs offsets for attacks. Also added in summoner attack routine. 2024-06-12 23:45:27 -04:00
Randi 9a714b969e Made fix to pickit in which it did not properly read physical damage reduction attributes. Caused automatic discard for ss, verdungo, soe etc. 2024-06-12 23:40:09 -04:00
Randi d518389493 Added shenk and nilathak runs to blizzorb sorc. 2024-06-11 21:53:00 -04:00
Randi c6f81c70a6 Fixed bug with health manager that would cause unintended chicken on rejuv use. 2024-06-11 21:51:25 -04:00
Randi 70659b7544 Added new blizzorb sorc that specializes in comboing blizzard with frozen orb. 2024-06-11 19:32:56 -04:00
Randi aa56bbc484 Made fix to paladin.py to support wait_tp parameter in overriden pre_move() method. 2024-06-09 15:39:34 -04:00
Randi 901592a54a Fixed an issue with hammerdin overriden pre_move() method which was missing wait_tp variable. 2024-06-09 14:29:34 -04:00
Randi a51db00a15 Updated pickit to remove edge-case it which last item checked will not attempt retries for pickups. Updated health_manager to never skip chicken check. Also added chicken conditions when out of juvs, or if juvs are used in rapid succession. 2024-06-07 15:24:03 -04:00
Randi 0197298e31 Ensure foh does not have holybolt selected when walking as it causes targeting issues with merc. 2024-06-06 00:46:17 -04:00
Randi a4dada9237 Make fohdin suck less against pindle. 2024-06-05 20:34:37 -04:00
Randi 6b2f719605 Tweaked env file. 2024-05-25 13:24:34 -04:00
Randi ed70c8e688 Tried to fixed issue of failed teleport at start. Fixed issue that prevented potions from being picked up. Fixed item inspection issue with tooltip. 2024-05-23 19:33:11 -04:00
Randi f68ebf0861 Cleaned up readme and development.md with new instructions based on fork. 2024-05-20 13:29:21 -04:00
Randi 39788df29c Set params.ini to match sorc. Updated environment.yml to set dependencies on package versions such as mss 7.0.1. Updated discord_embeds.py to work with latest discord version. Updated save_exit button image. Updated char_state image and roi. 2024-05-19 21:44:00 -04:00
aeon0 f1ea45ce2a Update README.md 2022-07-01 15:06:32 +02:00
aeon0 31a0579f65 Update README.md 2022-07-01 06:47:53 +02:00
aeon0 ad8ea13444 Update README.md 2022-07-01 06:47:23 +02:00
aeon0 5df97c4a7e Update README.md 2022-07-01 06:46:51 +02:00
Legit f5538916fe Merge pull request #958 from noblesigma/master
Update location
2022-06-30 09:35:30 -05:00
noblesigma d7be49edd7 Update location 2022-06-30 07:38:55 -04:00
Riddle 6e10c549e4 Merge pull request #954 from definitelynotsosa/patch-8
Update default.bnip
2022-06-28 06:07:42 -04:00
Sosa 656e52ffff Update default.bnip
Fixing Natures Peace Ring line
2022-06-28 01:51:14 -04:00
mgleed 8c9e98ebd8 add a couple mouse waits 2022-06-27 18:35:36 -04:00
Gleed da09cb820a Feature: More descriptive pather.py output when run from src, support list of template patterns 2022-06-27 15:47:22 -04:00
Legit d45d974280 Merge pull request #949 from definitelynotsosa/patch-7
Update default.bnip
2022-06-26 16:54:50 -05:00
Sosa 613d4f0ff3 Update default.bnip
Minor fix
Corrected Magic Necrohead line
2022-06-26 17:47:17 -04:00
Riddle 8f4942a3a2 Merge pull request #948 from bottytools/bnip-incorrect-reading
fixes #936
2022-06-26 10:54:11 -04:00
Riddle ef34efdd54 Merge branch 'master' of https://github.com/aeon0/botty 2022-06-26 10:52:13 -04:00
Riddle 9886f2ee96 fixes #936 2022-06-26 10:52:07 -04:00
Riddle 3e4c0d8fa5 Merge pull request #947 from bottytools/encode-bnip-files
Add encoding to .bnip files
2022-06-26 07:36:46 -04:00
Riddle f72856a8bd Add encoding to .bnip files 2022-06-26 06:26:23 -04:00
mgleed 980a8f5036 get rid of redundant id_items param 2022-06-24 20:28:41 -04:00
Riddle 9f1a4772cd Merge pull request #926 from bottytools/nip-to-bnip
Replace nip with bnip
2022-06-24 13:37:19 -04:00
Riddle c68d92c8e1 Merge branch 'master' of https://github.com/aeon0/botty into nip-to-bnip 2022-06-24 13:36:53 -04:00
Riddle 076737672e Merge branch 'master' into nip-to-bnip 2022-06-24 13:36:31 -04:00
Gleed 689a93a3f7 Bugfix: Make character name OCR not freeze program if fails, also improved read 2022-06-23 17:01:45 -04:00
mgleed 9ed7869ddd fix typo 👀 2022-06-23 15:13:10 -04:00
Gleed c62c237442 Bugfix: template_finder.search_all() messing with input arguments (#931)
* init

* remove print
2022-06-23 14:53:58 -04:00
Legit 4e10dbb2c7 Updated staves and nip errors (#932)
Corrected white staves and other syntax errors.
2022-06-23 14:49:45 -04:00
Gleed 197ff41e69 Fix Fix: template_finder.search() tests were terminating after first "assert" 2022-06-23 13:42:39 -04:00
Wang Xiang 5089abaca6 Update default.nip Shadowplates (#927)
Add expressions for Shadowplates in Armor Bases section.
2022-06-23 07:13:59 -04:00
jobithu 8469d51f0c Merge pull request #928 from xw220/improve/diablo_sealdance
Update diablo.py _sealdance() -reviewed and approved. also implemented for PR867
2022-06-23 13:11:25 +02:00
Wang Xiang 9b2d52a39b Update diablo.py _sealdance()
I watched 10 hours of diablo run by 4 fohdins. About 30% were fail games which aborted in middle run, especially on detecting that if the seal is openned.  In almost every fail game caused by this reason, the seal was openned successfully, but the decisions were closed for 5 times and the game was aborted. By increasing the numbers of try and reduce the seal_opentemplates threshold to 0.7, the incorrect judgements on seal's open status seldom occur, and the ratio of failed games typically reduced. Now it is about 12% mostly caused by temporary high latency or getting lost after teleport to pick item.
2022-06-23 17:51:40 +08:00
jagarop d95b581dae Merge pull request #920 from definitelynotsosa/patch-5
Update default.nip
2022-06-22 09:42:54 +00:00
Riddle 759c4fff82 Remove the test321.. 2022-06-22 05:39:06 -04:00
Riddle d5bdb769b3 Replace nip with bnip 2022-06-22 05:37:54 -04:00
Riddle 8a1a15d77d Merge pull request #925 from bottytools/nip-error-update
Added better expections for bnip
2022-06-22 04:41:16 -04:00
Riddle 24d2da8e67 Added better expections for bnip 2022-06-22 04:24:04 -04:00
aeon0 6d1779ea5f fix not stashing gold if buying sth before (#922) 2022-06-22 09:59:14 +02:00
Riddle 0cbc5f759e Merge pull request #924 from bottytools/nip-type-checking
fixed liner errors for nip files
2022-06-22 02:57:01 -04:00
Riddle ce1d99bfd8 fixed liner errors 2022-06-22 02:47:54 -04:00
Riddle 257d687166 Merge pull request #923 from bottytools/nip-fix-subtraction-2
fixed
2022-06-22 02:30:27 -04:00
Riddle cfc192e1dc fixed 2022-06-22 02:28:47 -04:00
Riddle 161b9226fb Merge pull request #921 from bottytools/nip-ntipaliasstatkeyword-fix
Nip ntipaliasstatkeyword fix
2022-06-22 02:27:11 -04:00
Riddle 46b8d9bf17 Remove prints / replace ntipaliasvalue to use keyword 2022-06-22 02:17:32 -04:00
Riddle 9faa3fd25a forgot to replace in transpile.py 2022-06-22 02:10:06 -04:00
Riddle bca0471d9f removed print statements 2022-06-22 01:01:40 -04:00
Riddle 5610490db6 removed print statements 2022-06-22 01:01:23 -04:00
Riddle 5efebd0bb7 fixed missing keyword... o-o 2022-06-22 01:00:13 -04:00
Sosa 81bc9028d1 Update default.nip
Fixing white claw bases
2022-06-22 00:43:49 -04:00
Gleed 3de9f6db93 Bugfix: Pickit IndexError 2022-06-21 22:33:27 -04:00
mgleed 0f08cd8e93 bump version 2022-06-21 20:07:25 -04:00
196 changed files with 16651 additions and 2341 deletions
+71 -32
View File
@@ -1,52 +1,91 @@
name: Botty - CI
on: [pull_request]
on:
pull_request:
push:
branches: [main, mine]
release:
types: [published]
jobs:
tests:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Setup Miniconda Python 3.10
uses: actions/setup-python@v2
with:
python-version: '3.10'
# TODO: The below setp only chaches conda packages, need to cache pip seperatly
- name: Cache conda
uses: actions/cache@v2
env:
# Increase this value to reset cache if environment.yml has not changed
CACHE_NUMBER: 1
with:
path: ~/conda_pkgs_dir
key: conda-${{ env.CACHE_NUMBER }}-${{hashFiles('environment.yml') }}
- name: Cache pip
uses: actions/cache@v2
env:
# Increase this value to reset cache if environment.yml has not changed
CACHE_NUMBER: 1
with:
path: ~/pip
key: pip-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }}
- name: Install Miniconda
uses: conda-incubator/setup-miniconda@v2
uses: conda-incubator/setup-miniconda@v3
with:
python-version: '3.10'
activate-environment: botty
channel-priority: strict
environment-file: environment.yml
use-only-tar-bz2: true # Needed for caching
- name: Activate conda
use-only-tar-bz2: true
- name: Download test assets
shell: powershell
run: |
C:\Miniconda\condabin\conda.bat init powershell
set PYTHONPATH=./src
- name: Pytest & Coverage
C:\Miniconda\condabin\conda.bat activate botty
python -c "import utils.download_test_assets"
- name: Pytest
shell: powershell
run: |
C:\Miniconda\condabin\conda.bat activate botty
python -c "import sys; print(sys.version)"
coverage run --source=./src -m pytest -v -s
- name: Coverage Report Generation
python -m pytest test/smoke_test.py test/nip/ test/game_stats_test.py test/closest_non_hud_test.py test/transmute/ -v -s
- name: Coverage
shell: powershell
run: |
C:\Miniconda\condabin\conda.bat activate botty
coverage xml
build:
needs: test
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup Miniconda Python 3.10
uses: conda-incubator/setup-miniconda@v3
with:
python-version: '3.10'
activate-environment: botty
channel-priority: strict
environment-file: environment.yml
use-only-tar-bz2: true
- name: Build exe
shell: powershell
env:
BOTTY_NO_RENAME: '1'
run: |
C:\Miniconda\condabin\conda.bat activate botty
python build.py --conda_path C:\Miniconda
- name: Prepare release zip
shell: powershell
run: |
$BOTTY_DIR = Get-ChildItem -Directory -Name | Where-Object { $_ -match '^botty_v' } | Select-Object -First 1
Write-Host "Botty dir: $BOTTY_DIR"
Get-ChildItem -Path $BOTTY_DIR -Recurse | Select-Object -Property FullName, Length
$ZIP = "${BOTTY_DIR}.zip"
Compress-Archive -Path "${BOTTY_DIR}\*" -DestinationPath $ZIP -Force
Write-Host "Release zip: $ZIP"
Get-Item $ZIP | Select-Object -Property FullName, Length
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: botty-build
path: botty_v*/
retention-days: 7
- name: Upload to Release
if: github.event_name == 'release'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
BOTTY_DIR=$(ls -d botty_v* | head -1)
gh release upload "${{ github.event.release.tag_name }}" "${BOTTY_DIR}.zip" --clobber
+2 -3
View File
@@ -24,9 +24,8 @@ test/assets/
htmlcov/
coverage.xml
utils/live-view/
config/nip/*
!config/nip/.gitkeep
config/bnip/*
!config/bnip/.gitkeep
stats/
info_screenshots/
item_error_screenshots/
+87
View File
@@ -0,0 +1,87 @@
# Botty Improvements Implementation Plan
## Status Legend
- [ ] Not started
- [~] In progress
- [x] Done
- [-] Cancelled / low priority
---
## Phase 1: Key Auto-Detection (Issue #940)
Read D2R .key file and auto-fill hotkeys.
- [x] Create src/utils/key_detector.py module
- [x] VK code mapping (partial — needs review for accuracy)
- [x] Parse .key file (text format: VK action_type param)
- [x] Auto-fill empty [char] hotkeys from detected bindings
- [x] Auto-fill build-specific skill hotkeys (fohdin, hammerdin, etc.)
- [x] Wire into config.py load_data()
- [ ] REVIEW: Verify VK_MAP accuracy (D2R uses its own VK offset scheme)
- [ ] REVIEW: Skill slot-to-config matching is heuristic — may misassign
- [ ] TEST: Verify against actual D2R .key file on user's machine
## Phase 2: Target Detection False Positives (Issues #959/#964)
Health bars and "immune to X" text mistaken for targets.
- [ ] Analyze current get_visible_targets() in target_detect.py
- [ ] Add shape/size filtering: health bars are thin horizontal strips, immune text is small
- [ ] Add aspect ratio check: real targets (poison/freeze auras) are roughly circular/elliptical
- [ ] Add minimum bounding box height constraint (filter out thin text)
- [ ] Optionally: add color temperature check (immune text is yellow/gold, not blue/green)
- [ ] Test with screenshots of edge cases
## Phase 3: Pickit Timing Fix (Issue #939)
Items skipped because bot teleports away before grabbing.
- [ ] Review pickit.py _yoink_item() for timing issues
- [ ] Add configurable pickup_delay parameter (current: fixed timing)
- [ ] Add retry logic: if item still visible after pickup attempt, re-try
- [ ] Add "slow mode" for large/heavy items (framed/magic items may animate longer)
- [ ] Ensure bot doesn't teleport until pickup animation completes
- [ ] Test: verify no "Attempt to pick xyz" warnings followed by teleport
## Phase 4: Parallel Template Search (Issue #848)
Speed up template_finder.search() with threading.
- [ ] Add ThreadPoolExecutor-based search_all_parallel()
- [ ] Keep existing search() for single-template (no overhead)
- [ ] Only parallelize when searching >3 templates simultaneously
- [ ] Benchmark: measure speedup on typical 1280x720 grab
## Phase 5: Async Mouse Moves (Issue #955)
Non-blocking mouse movement.
- [ ] Add async_move() to utils/custom_mouse.py
- [ ] Run movement in background thread
- [ ] Add is_moving() / wait_for_move() synchronization
- [ ] Integrate into game_controller.py for smoother action chains
## Phase 6: Hardcore Chicken Loop Fix (Issue #942)
Prevent infinite death loops on Hardcore characters.
- [ ] Review death_manager.py chicken logic
- [ ] Add max_chicken_count config parameter (default: 3)
- [ ] If max chicken count exceeded on HC, exit gracefully instead of re-entering
- [ ] Add defensive chicken config option (chicken to TP instead of full chicken)
- [ ] Test: verify HC character exits cleanly after N deaths
## Phase 7: Auto-Label NPCs (Issue #950)
Learn vendor identities automatically during gameplay.
- [ ] During town states, detect NPC name plates via OCR
- [ ] Cross-reference detected names with known NPC list
- [ ] Auto-capture NPC templates when confidence is high
- [ ] Store learned templates in assets/npc/
- [ ] This is a long-term feature — lower priority
---
## Priority Order (implement in this order)
1. **Phase 1** - Key auto-detection (already partially done, needs review + test)
2. **Phase 3** - Pickit timing (high impact on loot collection)
3. **Phase 2** - Target detection (high impact on kill reliability)
4. **Phase 6** - Hardcore chicken fix (safety critical)
5. **Phase 4** - Parallel template search (performance)
6. **Phase 5** - Async mouse moves (quality of life)
7. **Phase 7** - Auto-label NPCs (long-term feature)
+561
View File
@@ -0,0 +1,561 @@
# Quest Framework + Den of Evil Plan
## Goal
Build a quest automation framework in botty that can interact with D2R NPCs, handle dialogue,
track quest progress, and run Den of Evil as the first quest -- all usable by a low-level FoHdin.
---
## Architecture
The quest framework is a new subsystem that plugs into the existing botty state machine.
It follows the same patterns as existing runs (approach -> battle -> return to town) but adds
NPC dialogue interaction and quest state persistence.
### New files
```
src/quest/
__init__.py # Exports
quest_manager.py # Quest state machine + persistence (JSON)
quest_dialogue.py # OCR-based NPC dialogue interaction
quest_items.py # Quest item detection/pickup
quest_combat.py # Lightweight combat wrapper (killing trash)
a1/
__init__.py
q_den_of_evil.py # Den of Evil run
```
### Modified files
```
src/npc_manager.py # Add TOWN_MAIDEN NPC constant + templates
src/pather.py # Add A1_ROARING_CANYON + DoE entrance locations
src/bot.py # Add quest state, transitions, handler
src/run/__init__.py # Export DenOfEvil
src/town/a1.py # (optional) Add can_do_den_of_evil method
config/params.ini # Add run_doe to [routes]
config/bnip/ # Add town_maiden.png template
```
---
## Phase 1: Foundation
### 1.1 `src/quest/quest_manager.py`
Purpose: Track which quests are done, persist between sessions, dispatch to quest modules.
```python
class QuestManager:
"""Manages quest state: tracks done/available quests per act, persists to JSON."""
# Quest definitions per act
QUESTS = {
"a1": ["den_of_evil"],
"a2": [], # future: radament, horadric_staff, etc.
...
}
def __init__(self):
self._state_file = "config/quest_state.json"
self._state = self._load()
def is_done(self, quest_name: str) -> bool:
return self._state.get(quest_name, False)
def mark_done(self, quest_name: str):
self._state[quest_name] = True
self._save()
def mark_all_done(self, act: str):
for q in self.QUESTS.get(act, []):
self._state[q] = True
self._save()
def next_pending(self, act: str) -> str | None:
for q in self.QUESTS.get(act, []):
if not self.is_done(q):
return q
return None
def all_done(self, act: str) -> bool:
return all(self._state.get(q, False) for q in self.QUESTS.get(act, []))
def _load(self) -> dict:
if os.path.exists(self._state_file):
with open(self._state_file) as f:
return json.load(f)
return {}
def _save(self):
with open(self._state_file, "w") as f:
json.dump(self._state, f, indent=2)
```
JSON format (config/quest_state.json):
```json
{
"den_of_evil": true,
"search_for_smith": true,
...
}
```
### 1.2 `src/quest/quest_dialogue.py`
Purpose: Talk to NPCs, read dialogue options via OCR, click the right branch.
This is the core of quest automation -- it makes the bot "converse" with NPCs.
```python
class QuestDialogue:
"""OCR-based NPC dialogue interaction for quest conversations."""
# ROI at 1280x720
DIALOGUE_TEXT_ROI = (200, 470, 680, 100) # NPC speech text
DIALOGUE_OPTIONS_ROI = (200, 560, 680, 140) # Player response buttons
DIALOGUE_CLOSE_Y = 670 # Close button area
@staticmethod
def open_dialogue(npc_name: str) -> bool:
"""Walk to NPC and open their dialogue menu."""
from npc_manager import Npc, open_npc_menu
return open_npc_menu(getattr(Npc, npc_name.upper()))
@staticmethod
def read_dialogue() -> dict:
"""OCR the current dialogue box. Returns:
{
'npc_text': str, # What the NPC said
'options': [str, ...], # Response options (may be empty if no choice)
'has_continue': bool # True if just need to click continue
}
"""
img = grab()
npc_text = ocr_roi(img, self.DIALOGUE_TEXT_ROI)
options_text = ocr_roi(img, self.DIALOGUE_OPTIONS_ROI)
# Parse options: split by line, filter out empty, return list
options = [line.strip() for line in options_text.split('\n') if line.strip()]
has_continue = len(options) == 0 or "continue" in options_text.lower()
return {
'npc_text': npc_text.strip(),
'options': options,
'has_continue': has_continue
}
@staticmethod
def click_option(option_text: str) -> bool:
"""Find and click a specific dialogue option by matching text via OCR.
Searches the options ROI for a template match of the option text."""
img = grab()
options_img = cut_roi(img, self.DIALOGUE_OPTIONS_ROI)
# Use template_finder or OCR to locate which button matches
# Then click at that position
...
@staticmethod
def continue_dialogue() -> bool:
"""Click the close/continue button to advance dialogue."""
# Click in the close button area
x, y, w, h = self.DIALOGUE_CLOSE_Y
mouse.click at center of close area
...
@staticmethod
def follow_conversation(expected_options: list[str]) -> bool:
"""Follow a multi-step conversation:
- Read NPC text
- If options present, click the expected one
- If no options, click continue
- Repeat until dialogue closes or unexpected text appears
"""
max_steps = 20 # Safety limit
for i in range(max_steps):
dialogue = self.read_dialogue()
if not dialogue['has_continue'] and dialogue['options']:
# We have a choice - click the expected option
for opt in expected_options:
if opt.lower() in ' '.join(dialogue['options']).lower():
if not self.click_option(opt):
return False
break
else:
Logger.warning(f"Unexpected dialogue options: {dialogue['options']}")
return False
else:
# Just continue
if not self.continue_dialogue():
return False
wait(1.0, 1.5)
# Check if dialogue box is still visible
if not is_visible(ScreenObjects.NPCDialogue):
return True # Done
return False # Hit max steps
```
Key design: `follow_conversation()` takes a list of expected response text. It will
match against whatever options the NPC presents and click the right one. This handles
multi-branch dialogues without hardcoding step-by-step clicks.
### 1.3 `src/quest/quest_combat.py`
Purpose: Lightweight combat for clearing trash during quests. Reuses existing char methods.
```python
class QuestCombat:
"""Combat helpers for quest areas -- reuses existing character combat logic."""
@staticmethod
def clear_area(pather: Pather, char: IChar, path_nodes: list[int],
timeout: float = 60) -> bool:
"""Walk a path while killing monsters until timeout or all nodes cleared.
This is the core of DoE: walk down, kill, walk back."""
return pather.traverse_nodes(path_nodes, char, timeout=timeout, do_combat=True)
@staticmethod
def wait_for_clear(char: IChar, timeout: float = 15) -> bool:
"""Wait until no monsters are visible (area is clear)."""
start = time.time()
while time.time() - start < timeout:
targets = get_visible_targets()
if not targets or len(targets) == 0:
return True
# Attack if enemies present
char.attack()
wait(0.5)
return False
```
### 1.4 `src/quest/quest_items.py`
Purpose: Detect and pick up quest items (gold glow detection).
```python
class QuestItems:
"""Quest item detection and management."""
@staticmethod
def detect_quest_items(img: np.ndarray) -> list[tuple[float, float]]:
"""Detect gold-glowing items on screen (quest items).
Returns list of (x, y) positions in monitor coords."""
quest_item_mask, _ = color_filter(img, Config().colors.get("gold_glow", [
(180, 140, 0), (255, 220, 80)
]))
# Find contours, return centers
...
@staticmethod
def pick_up_quest_items(char: IChar, img: np.ndarray = None) -> bool:
"""Find and pick up any quest items currently visible."""
if img is None:
img = grab()
items = self.detect_quest_items(img)
for pos in items:
char.pick_up_item(pos, item_name="Quest Item")
wait(0.5)
return len(items) > 0
```
---
## Phase 2: NPC & Location additions
### 2.1 Add Town_Maiden to `src/npc_manager.py`
```python
# In class Npc:
TOWN_MAIDEN = "town_maiden" # Act 1, Roaring Canyon
# In _build_npcs():
Npc.TOWN_MAIDEN: {
"head": "town_maiden.png", # Need to capture template
"actions": {} # No trade/identify - just dialogue
}
```
The Town Maiden sits in Roaring Canyon (eastern part of Act 1 town). She has a simple
dialogue: you talk to her to "unlock" the Den of Evil entrance, then you talk to her
again after clearing it to get the XP reward and reset it for another run.
### 2.2 Add locations to `src/pather.py`
```python
class Location:
# ... existing locations ...
# Act 1 Roaring Canyon / Den of Evil
A1_ROARING_CANYON = "a1_roaring_canyon" # Town area where Maiden is
A1_DEN_OF_EVIL_ENTRANCE = "a1_doe_entrance" # Stairs down to DoE
A1_DEN_LEVEL_1 = "a1_doe_level_1"
A1_DEN_LEVEL_2 = "a1_doe_level_2"
A1_DEN_LEVEL_3 = "a1_doe_level_3"
A1_DEN_LEVEL_4 = "a1_doe_level_4"
# (DoE has 3-5 levels depending on game version - need to confirm)
```
Path nodes will need to be added for the Roaring Canyon area and each DoE level.
These are captured via quest_debug.py by walking the path and recording waypoints.
---
## Phase 3: Den of Evil run module
### 3.1 `src/quest/a1/q_den_of_evil.py`
```python
class DenOfEvil:
"""Den of Evil run - Act 1 repeatable quest for XP.
Flow:
1. Ensure character is in Act 1
2. Walk to Roaring Canyon (Town Maiden)
3. Talk to Town Maiden (unlock entrance if needed)
4. Enter Den of Evil
5. Pre-buff (FoH + Conviction for FoHdin)
6. Walk through each level, killing trash
7. Exit back to Roaring Canyon
8. Talk to Town Maiden again for reward
9. Return to town center
"""
name = "run_doe"
# Path nodes per level (to be filled in via quest_debug.py)
LEVEL_PATHS = {
1: [], # Entrance to level 1 stairs
2: [], # Level 1 to level 2
3: [], # Level 2 to level 3
4: [], # Level 3 to level 4 (or final area)
}
def __init__(self, pather, town_manager, char, pickit, runs):
self._pather = pather
self._town_manager = town_manager
self._char = char
self._pickit = pickit
self._runs = runs
self._quest_manager = QuestManager()
self._dialogue = QuestDialogue()
def approach(self, curr_loc: Location, do_buff: bool) -> Location | bool:
"""Get to Roaring Canyon and talk to Town Maiden."""
Logger.info("Run Den of Evil")
# Ensure we're in Act 1
if TownManager.get_act_from_location(curr_loc) != Location.A1_TOWN_START:
curr_loc = self._town_manager.go_to_act(1, curr_loc)
if not curr_loc:
return False
# Walk to Roaring Canyon (Town Maiden area)
if not self._pather.traverse_nodes(
(curr_loc, Location.A1_ROARING_CANYON), self._char, force_move=True
):
return False
# Talk to Town Maiden to unlock/open the Den
if not self._dialogue.open_dialogue("town_maiden"):
return False
# Follow the conversation (expect "Oh no, not again" or similar)
if not self._dialogue.follow_conversation(["Tell me more", "I'll help you"]):
return False
# Enter the Den
if not self._pather.traverse_nodes(
(Location.A1_ROARING_CANYON, Location.A1_DEN_OF_EVIL_ENTRANCE),
self._char, force_move=True
):
return False
return Location.A1_DEN_OF_EVIL_ENTRANCE
def battle(self, do_pre_buff: bool) -> bool | tuple[Location, bool]:
"""Fight through the Den of Evil."""
# Pre-buff
if do_pre_buff:
if not self._char.pre_buff():
return False
# Clear each level
for level in sorted(self.LEVEL_PATHS.keys()):
Logger.info(f"Clearing Den of Evil level {level}")
if not self._pather.traverse_nodes(
self.LEVEL_PATHS[level], self._char, timeout=120, do_combat=True
):
Logger.error(f"Failed to clear DoE level {level}")
return False
# Pick up any quest items / loot
self._pickit.pick_up_items(self._char)
QuestItems.pick_up_quest_items(self._char)
# Walk back to Roaring Canyon
if not self._pather.traverse_nodes(
(Location.A1_DEN_OF_EVIL_ENTRANCE, Location.A1_ROARING_CANYON),
self._char, force_move=True
):
return False
# Talk to Town Maiden for reward
if not self._dialogue.open_dialogue("town_maiden"):
return False
if not self._dialogue.follow_conversation(["Yes", "Thank you"]):
Logger.warning("Failed to collect DoE reward from Town Maiden")
# Mark as done (for non-repeatable quests) or just return success
# Note: DoE is repeatable once per real-day, so we DON'T mark permanently done
# self._quest_manager.mark_done("den_of_evil") # Only if non-repeatable
return (Location.A1_ROARING_CANYON, True)
```
---
## Phase 4: Bot integration
### 4.1 `src/bot.py` changes
```python
# Add import
from quest.a1.q_den_of_evil import DenOfEvil
# In __init__:
self._do_runs["run_doe"] = Config().routes.get("run_doe")
self._doe = DenOfEvil(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
# In _states list:
# (No new state needed - DoE uses the existing pattern: town -> doe -> end_run -> town)
# In _transitions list (add):
{ 'trigger': 'run_doe', 'source': 'town', 'dest': 'doe', 'before': "on_run_doe" },
# Add 'doe' to end_run source list:
{ 'trigger': 'end_run', 'source': [..., 'doe'], 'dest': 'town', 'before': "on_end_run" },
# Add end_game source:
{ 'trigger': 'end_game', 'source': [..., 'doe'], 'dest': 'initialization', 'before': "on_end_game" },
# Add handler method:
def on_run_doe(self):
res = False
self._do_runs["run_doe"] = False
self._game_stats.update_location("DoE")
self._curr_loc = self._doe.approach(self._curr_loc, not self._pre_buffed)
if self._curr_loc:
set_pause_state(False)
res = self._doe.battle(not self._pre_buffed)
self._ending_run_helper(res)
```
### 4.2 `src/run/__init__.py` changes
```python
# No change needed if DoE lives in src/quest/ (not src/run/)
# But if we want consistency, add:
from quest.a1.q_den_of_evil import DenOfEvil
```
### 4.3 `config/params.ini` changes
```ini
[routes]
; ... existing runs ...
; run_doe (Act 1 Den of Evil - repeatable daily XP)
order=run_doe
```
### 4.4 `config/params.ini` FoHdin config
For a lvl 1 Paladin running DoE, the params.ini needs:
```ini
[char]
type=fohdin
...
[fohdin]
; FoHdin-specific config for low-level DoE runs
teleport=
; No teleport at lvl 1-9, so pathing is on foot
```
---
## Phase 5: Testing workflow
### What needs user input (I cannot see D2R):
1. **Capture Town_Maiden template:**
- Go to Roaring Canyon in Act 1
- Stand near the Town Maiden
- Run `quest_debug.py`, press F4 (NPC detection)
- Paste output so I can save the template
2. **Capture DoE path nodes:**
- Enter the Den of Evil
- Run `quest_debug.py`, press F1 at each waypoint
- Walk from entrance through each level
- Paste outputs so I can build the path arrays
3. **Capture dialogue:**
- Talk to Town Maiden (both before and after clearing)
- Run `quest_debug.py`, press F2 (dialogue OCR)
- Paste output so I can code the conversation flow
4. **Test run:**
- After I write the code, you run botty with `run_doe` in the route order
- Report what happens / paste terminal output
- I iterate based on results
### Lvl 1 Paladin specifics:
- **FoHdin requires FOH skill lvl 6 for Feign of Life passive** -- this needs 3 skill points
in FoH, meaning character level 9 minimum (or level 4 with a +1 skill weapon)
- Before reaching lvl 9, the bot can still run DoE but will be much more fragile
- Recommended: manually level Paladin to ~lvl 4-5 (short runs in Area 1 or 2) before
letting the bot solo DoE with FoH
- The bot pathing should handle the walk-through at low speed with heavy FoH spam
---
## Implementation order
1. Write `quest_manager.py` (simple JSON state tracker)
2. Write `quest_dialogue.py` (OCR-based NPC interaction)
3. Write `quest_items.py` + `quest_combat.py` (lightweight helpers)
4. Add Town_Maiden NPC to npc_manager.py
5. Write `q_den_of_evil.py` (skeleton with placeholder paths)
6. Integrate into bot.py (state, transitions, handler)
7. Update params.ini
8. **USER TESTS** -- captures templates, paths, dialogue
9. I fill in the actual path nodes and dialogue based on your captures
10. Full test run and iterate
---
## File tree after implementation
```
my-botty/
├── config/
│ ├── params.ini # Modified: +run_doe in routes
│ ├── quest_state.json # New: auto-created by QuestManager
│ └── bnip/
│ └── town_maiden.png # New: captured template
├── src/
│ ├── quest/ # New directory
│ │ ├── __init__.py
│ │ ├── quest_manager.py
│ │ ├── quest_dialogue.py
│ │ ├── quest_items.py
│ │ ├── quest_combat.py
│ │ └── a1/
│ │ ├── __init__.py
│ │ └── q_den_of_evil.py
│ ├── npc_manager.py # Modified: +TOWN_MAIDEN
│ ├── pather.py # Modified: +A1_ROARING_CANYON, +A1_DEN_* locations
│ ├── bot.py # Modified: +doe state, transitions, handler
│ └── run/__init__.py # Modified: +DenOfEvil export
```
+231
View File
@@ -0,0 +1,231 @@
# Botty Architecture
## Overview
Botty is an autonomous bot for Diablo II Resurrected (D2R) that uses computer vision and native Windows API input to run endgame content without kernel drivers. It's built in Python 3.10 and distributed as a PyInstaller onefile executable.
## Entry Points
Two standalone executables are built from the same source:
| Executable | Source | Purpose |
|---|---|---|
| `main.exe` | `src/main.py` | Main bot controller |
| `shop.exe` | `src/shopper.py` | Standalone vendor shopping |
## Threading Model
main.py spawns three concurrent threads:
```
main.py (main thread)
├── game_controller.py (bot loop thread)
│ └── bot.py (state machine thread)
├── health_manager.py (background thread)
└── death_manager.py (background thread)
```
- **Main thread**: Shows UI, registers hotkeys (F7-F12), calls `keyboard.wait()` to block
- **Game Controller**: Orchestrates the bot loop - starts/stops the bot thread, handles game recovery, tracks stats
- **Health Manager**: Polls health/mana/merc every ~1s, auto-potions, chickens when low
- **Death Manager**: Polls every ~1s for "You Have Died" screen, triggers recovery
## Core Modules
### Input Layer (`src/input_layer/`)
Native Windows API input replacement (no kernel drivers):
| File | Purpose |
|---|---|
| `win_input.py` | ctypes wrappers for `SendInput`, `GetAsyncKeyState`, `GetCursorPos` |
| `mouse_impl.py` | Humanized mouse with Bezier curves, Gaussian distortion, endpoint wobble |
| `hotkey.py` | Polling-based hotkey manager (replaces `keyboard.add_hotkey`) |
| `__init__.py` | Drop-in API: `from input_layer import keyboard, mouse` |
All key presses include stealth micro-pauses and variable press duration automatically.
### Screen Capture (`src/screen.py`)
Handles D2R window detection and screenshot capture via MSS library. Converts between coordinate systems: Monitor (top-left first monitor), Screen (per-monitor), Absolute (character center), Relative (template-matched).
### Image Recognition (`src/template_finder.py`)
Template matching via OpenCV (`cv2.matchTemplate`). Searches against pre-captured asset templates in `assets/templates/`. Returns match position and validity.
### UI Detection (`src/ui/`)
| File | Detects |
|---|---|
| `main_menu.py` | Main menu buttons (play, save & exit) |
| `character_select.py` | Character portraits and selection |
| `skills.py` | Skill bar state, skill availability |
| `meters.py` | Health/mana globes, potion charges |
| `player_bar.py` | Player status bar, XP bar |
| `view.py` | Minimap, inventory screen detection |
| `waypoint.py` | Waypoint portal selection |
| `error_screens.py` | Error dialogs, death screen |
| `loading.py` | Loading screen detection |
### Pathing (`src/pather.py`)
Pathfinding via reference template matching with relative coordinates. Each location has numbered nodes with template references. The `Location` class defines named destinations (towns, dungeons). Path definitions map (start, end) pairs to node sequences.
### Character System (`src/char/`)
Inheritance hierarchy:
```
IChar (abstract base)
├── Basic / Basic_Ranged
├── Paladin
│ ├── Hammerdin
│ └── FoHdin
├── Sorceress (base for all sorc builds)
│ ├── BlizzSorc
│ ├── BlizzorbSorc
│ ├── NovaSorc
│ ├── LightSorc
│ └── HydraSorc
├── Amazon
│ └── Javazon
├── Trapsin
├── Barbarian
├── Necro
├── Poison_Necro
├── Bone_Necro
└── Warlock
├── FireLock
├── EchoLock
└── AbyssLock
```
Each character class implements: run methods, skill usage, attack patterns, position-specific behavior.
### Run System (`src/run/`)
| File | Run |
|---|---|
| `pindle.py` | Pindle of Purity |
| `shenk_eld.py` | Shenk + Eldritch |
| `trav.py` | Travincal |
| `nihlathak.py` | Nihlathak's Temple |
| `arcane.py` | Arcane Sanctuary |
| `diablo.py` | Chaos Sanctuary + Diablo |
| `vizier.py` | Vizier (Seal Boss) |
| `baal.py` | Baal |
| `mephisto.py` | Mephisto |
| `andariel.py` | Andariel |
| `countess.py` | Countess |
| `level.py` | General leveling runs |
### Town System (`src/town/`)
`TownManager` orchestrates act-specific town routines. Each act (A1-A5) has its own module with NPC interactions, waypoint usage, and stash handling.
### Inventory (`src/inventory/`)
| File | Purpose |
|---|---|
| `belt.py` | Belt slot management, potion detection |
| `personal.py` | Personal inventory grid, item positions |
| `vendor.py` | Vendor trade window |
| `stash.py` | Personal stash, shared stash |
| `cube.py` | Horadric Cube |
| `common.py` | Shared inventory utilities |
### Item Recognition (`src/item/`)
| File | Purpose |
|---|---|
| `pickit.py` | Pickup decisions, item filtering |
| `consumables.py` | Potion and consumable identification |
BNIP parser (`src/bnip/`) handles item filter rules from `.bnip` files.
### Transmute (`src/transmute/`)
Gem transmutation: collects gems from inventory/stash, performs cube recipes, manages stash destinations.
### Messages (`src/messages/`)
| File | Purpose |
|---|---|
| `messenger.py` | Message dispatcher |
| `discord_embeds.py` | Discord webhook formatting |
| `generic_api.py` | Generic HTTP webhook support |
### Configuration (`src/config.py`)
Singleton that merges config files in priority order:
`custom.ini` > `params.ini` > `game.ini` > `shop.ini` > `transmute.ini`
Supports variable substitution via `[variables]` sections.
### Stealth (`src/utils/stealth.py`)
Three-tier stealth system:
- **Tier 1 (Input)**: Micro-pauses, click variance, key press duration, endpoint wobble
- **Tier 2 (Behavior)**: Wrong waypoint chance, skill mistake chance, skill hesitation
- **Tier 3 (Session)**: AFK breaks, run skipping, personality seed per character
### Utilities (`src/utils/`)
| File | Purpose |
|---|---|
| `misc.py` | Window management, DPI awareness, timing utilities |
| `restart.py` | Game launch/kill, D2R always-on-top |
| `auto_settings.py` | D2R game settings adjustment |
| `key_detector.py` | Auto-detect skill bindings from `.key`/`.keyo` files |
| `graphic_debugger.py` | Visual debugging overlay |
| `node_recorder.py` | Record new path nodes |
| `stealth.py` | Stealth behavior randomization |
## Data Flow
```
D2R Game Window
screen.grab() ──→ Image (numpy array)
template_finder.search() ──→ Position + Validity
ui_manager.detect_screen_object() ──→ Current UI state
bot.py (state machine) ──→ Decides next action
char.run() / pather.go_to() ──→ Movement + Interaction
input_layer.keyboard.send() / mouse.click() ──→ SendInput API
D2R Game Window (response captured on next grab)
```
## Build System
- **Build script**: `build.py` (PyInstaller with `--onefile --noconsole`)
- **Dependencies**: `pyproject.toml`
- **Distribution**: Random exe name, no console window, no kernel drivers
- **Assets**: `assets/` directory copied to build output
## Key Detection
At startup, `key_detector.py` reads the character's `.key`/`.keyo` file from `Saved Games/Diablo II Resurrected/` to auto-detect skill bindings and non-skill keys (inventory, show items, etc.). This eliminates manual key configuration.
## Coordinate Systems
| System | Origin | Used By |
|---|---|---|
| Monitor | Top-left of first monitor | `screen.grab()` |
| Screen | Per-monitor client area | UI detection |
| Absolute | Character at screen center | Pathing, target detection |
| Relative | Template match position | Inventory grid, NPC interaction |
Conversion functions in `screen.py`: `convert_monitor_to_screen()`, `convert_screen_to_abs()`, etc.
+427
View File
@@ -0,0 +1,427 @@
# Botty D2R - Improvement Ideas
Generated: 2026-05-20 | Status: Partially implemented
Based on analysis of ~100+ source files across src/, test/, config/, and assets/.
Character: FOH Paladin | Priority: Anti-cheat stealth > everything else
---
## DONE
- [x] **C8** Bug: personal.py `open()` -> `open_inventory()` (fixed)
- [x] **C9** Bug: FoHdin missing PickIt (fixed)
- [x] **C10** Bug: game_controller race condition - `game_stats` now in `__init__()` (fixed)
- [x] **M15** Bug: TARGET_ASPECT_MIN duplicate removed (fixed)
- [x] **M17** Bug: chest.py relative path -> `Path(__file__)` absolute (fixed)
- [x] **Quick win**: All 47 bare `time.sleep()` replaced with `wait()` (15 files fixed)
- [x] **Quick win**: Stealth fallback `Logger.warning()` added to mouse_impl.py (3 blocks)
- [x] **Quick win**: `requirements.txt` created (120 lines, 20+ deps)
- [x] **Quick win**: `ruff.toml` created (Python 3.10, line-length 120)
- [x] **Tool**: `asset_manager.py` created (inventory, search, key, audit, quality, similarity, capture, crop, auto_crop, validate, cleanup, batch)
---
## CRITICAL - DO FIRST
### C1. Stealth: Consolidate all timing through centralized wait()
Many places use bare `time.sleep()` instead of `utils.misc.wait()` (which has Gaussian jitter).
Every direct sleep creates a predictable timing signature detectable by anti-cheat.
Files with bare `time.sleep()`:
- health_manager.py line 14
- chest.py
- pather.py
- game_recovery.py
- npc_manager.py
- bot.py
Fix: Replace all bare `time.sleep(n)` with `wait(n, n*1.2)` for human-like jitter.
### C2. Stealth: Add variable typing rhythm
`win_input.py` `send_text()` uses fixed `0.05` per character.
Real humans type with 0.02-0.12 per character with variation.
Fix: Add per-character randomization to `send_text()`.
### C3. Stealth: Human curve complexity should adapt to distance
`mouse_impl.py` `HumanCurve` uses static parameters.
Short movements (teleport to adjacent tile) should be simpler/faster.
Long movements should have more complex arcs.
Fix: Add distance-to-complexity mapping in `HumanCurve.__init__()`.
### C4. Stealth: Variable pathing speed within single movement arc
Current implementation varies timing BETWEEN movements but not WITHIN one.
Real humans speed up and slow down during a single mouse arc.
Fix: Add per-segment timing variation in `HumanCurve` execution loop.
### C5. Stealth: Extend endpoint wobble to ALL click sequences
~~`mouse_impl.py` `endpoint_wobble()` only fires for `stealth_move()`.~~
~~Most paths use regular `mouse.move()` + `mouse.click()` with no wobble.~~
~~Fix: Make `stealth_move()` the default, or add wobble to regular click flow.~~
(Still needed - stealth fallback logging added but wobble not yet extended to all clicks.)
### C6. ~~Stealth: Screen capture timing jitter~~
~~`screen.py` uses `dxcam` with perfectly regular capture intervals (~80ms).~~
~~Anti-cheat can detect this regular polling pattern.~~
~~Fix: Add 5-10% jitter to grab timing intervals.~~
(Done - all timing now routes through `wait()` with jitter.)
### C7. ~~Stealth: Input event spacing jitter~~
~~`hotkey.py` `GetAsyncKeyState` polling runs at exactly 50Hz.~~
~~Real human keyboard polling is variable.~~
~~Fix: Add microsecond-level jitter to polling intervals.~~
(Done - all `time.sleep()` in hotkey.py replaced with `wait()` calls.)
### C8. ~~Bug: Fix personal.py open() shadowing (line 60)~~ ~~(DONE)~~
### C9. ~~Bug: Fix FoHdin missing PickIt (bot.py line 72)~~ ~~(DONE)~~
### C10. ~~Bug: Replace thread killing with cooperative shutdown~~
~~`utils.misc.kill_thread()` uses `PyThreadState_SetAsyncExc` (CPython private API).~~
~~This can leave locks in inconsistent state, cause GIL issues, or corrupt numpy arrays.~~
~~Fix: Replace with threading.Event flags for cooperative shutdown.~~
(Still needs doing - this is the most dangerous remaining bug.)
---
## HIGH PRIORITY
### H1. Stealth: Add AFK countermeasures during ALL idle states
`stealth.py` `maybe_afk_break()` only runs during run transitions.
Should also run during health monitoring idle periods and town states.
Fix: Add micro mouse adjustments during health/death manager idle loops.
### H2. Stealth: Add "thinking" pauses before major actions
Before executing major actions (entering waypoint, starting run), add 0.5-3s "deliberation" pause.
Humans plan before acting.
Fix: Add configurable pause in `bot.py` state transitions.
### H3. Stealth: Randomize action order in town
Bot always does: belt update -> stash -> waypoint (same order every time).
Real players vary their town routine.
Fix: Randomize which town tasks are performed first in `town_manager.py`.
### H4. Stealth: Add variable "reaction time" to potion drinking
When health drops, bot should NOT react instantly.
Add 100-500ms delay to potion drinking to simulate human reaction time.
Fix: Add reaction delay in `health_manager.py` before invoking heal skill.
### H5. Stealth: Mouse trail entropy / Brownian overlay
Record actual mouse path during movements and add small random perturbations
that accumulate over time. Makes movement traces look more human.
Fix: Add Brownian motion overlay in `mouse_impl.py` `HumanCurve`.
### H6. Stealth: Click position micro-jitter
Before and after every click, add a tiny 1-3 pixel random offset.
Real humans don't click at exactly the same coordinates.
Fix: Add jitter in `mouse_impl.py` click methods.
### H7. Performance: Pre-load templates into memory at startup
`template_finder.py` loads PNGs with `cv2.imread()` on each search call.
With 1111+ templates, this wastes CPU on every detection cycle.
Current: Has `@cache` on `stored_templates()` but cache is never invalidated.
Fix: Good as-is if cache works. Verify cache isn't being bypassed in hot paths.
### H8. Performance: Cache screen grabs in tight pathing loops
`pather.py` `traverse_nodes()` calls `grab(force_new=True)` for every node.
If retrying quickly, the frame won't have changed.
Fix: Cache last grab timestamp; skip if less than 16ms since last grab.
### H9. Performance: Convert BGR->HSV once, apply all color masks
`utils.misc.color_filter()` converts BGR->HSV for every filter range.
With multiple NPC templates, this creates redundant conversions.
Fix: Convert once, apply all masks from single HSV image.
### H10. Architecture: Split pather.py (750 lines)
Handles node definition, path data, traversal, offset management, AND debug main.
Split into:
- path_data.py (constants and route definitions)
- path_traversal.py (traverse_nodes logic)
- path_utils.py (node offset helpers)
### H11. Architecture: Split config.py (29,500 chars)
Loads, validates, and caches all configuration in one massive file.
Split into:
- config_loader.py (read params.ini / config.yaml)
- config_schema.py (Pydantic/dataclass validation)
- config_defaults.py (default values per section)
### H12. Architecture: Encapsulate health_manager global state
Uses module-level globals `pause_state` and `panel_check_paused` with getter/setter functions.
Should be encapsulated in `HealthManager` class instance for thread safety.
### H13. Architecture: Fix npc_manager deferred init fragility
`npcs` dict is populated in try/except at module level, silently staying empty on failures.
If templates load late, `open_npc_menu()` crashes with unhelpful KeyError.
Fix: Add explicit initialization check with clear error message.
### H14. Architecture: Add thread safety to shared state
`health_manager.py` and `death_manager.py` share `set_pause_state` without mutex/lock.
Bot uses `self._stash_mutex` for stashing but health/death managers don't.
Fix: Add threading.Lock around shared state access.
### H15. Configuration: Add schema validation to params.ini
A typo in any key name causes runtime crash deep in the call stack.
Fix: Use Pydantic or dataclasses with defaults and type hints.
### H16. Configuration: Eliminate config duplication
Default values are scattered across params.ini AND config.py with hardcoded fallbacks.
Fix: Single source of truth - either params.ini with config.py as schema only,
or config.py with params.ini as user overrides.
### H17. Configuration: Add inline config documentation
No comments explaining what each param does, valid ranges, or D2R version compatibility.
Fix: Add docstrings to config sections with examples and ranges.
### H18. Asset management: Add startup health check
If a template file is missing or corrupted, the bot crashes mid-run.
Fix: Add startup validation that checks all required templates exist and load.
### H19. Asset management: Compress PNG templates with lossless optimization
1111+ PNG files. Many can be reduced with optipng/pngcrush without quality loss.
Fix: Run `optipng -o7` on all assets/ PNGs.
### H20. Error handling: Add timeout to _do_chicken
If D2R is frozen/unresponsive, `view.fast_save_and_exit()` hangs indefinitely.
Fix: Add timeout with fallback `taskkill` on timeout.
### H21. Error handling: Add graceful degradation for missing features
If `d2r_image` fails (OCR/library issues), the bot crashes entirely.
Fix: Add fallback pathfinding mode without OCR, with clear warning.
### H22. Error handling: Add total path timeout to traverse_nodes
Per-node timeout exists but total traversal can be extremely long if many nodes barely timeout.
Fix: Add cumulative path timeout in `pather.py` `traverse_nodes()`.
### H23. Error handling: Add verbose failure logging to NPC interaction
`open_npc_menu()` returns False after 35s but gives no diagnostic about WHY.
Fix: Log which template failed, current screen state, and suggested fixes.
---
## MEDIUM PRIORITY
### M1. Code quality: Duplicate code in i_char.py move()/walk()
`move()` and `walk()` methods (lines 214-261) are nearly identical.
Walk distance adjustment logic is copied verbatim.
Fix: Extract to `_adjust_walk_position()` helper.
### M2. Code quality: Silent failures in stealth fallback logging
`mouse_impl.py` has bare `except Exception:` blocks around stealth features (lines 178-183, 209-227).
These silently fail without logging, making stealth debugging impossible.
Fix: Add `Logger.warning()` in all stealth fallback except blocks.
### M3. Code quality: Inconsistent import style
Some files use `from config import Config`, others `import template_finder`, others `from screen import grab`.
Fix: Standardize on absolute imports throughout.
### M4. Code quality: Remove `__main__` blocks from production modules
Nearly every module has a standalone test block at the bottom that imports and configures
the full environment, making `import *` unreliable.
Fix: Move to dedicated test directory.
### M5. Code quality: Add full type hints coverage
Python 3.10+ type hints exist in some places but are incomplete.
`pather.py` has 750 lines with minimal typing.
Fix: Add type annotations to all public methods and data structures.
### M6. Code quality: Standardize asset naming convention
Some use `_BACK`, some use `_SIDE_2`, some use `_0`, `_45`, `_135` angles.
Fix: `NPCNAME_ANGLE_VARIANT.png` (e.g. `akara_front_1.png`, `akara_side_45_1.png`)
### M7. Developer experience: Add requirements.txt / pyproject.toml
Dependencies are scattered: `dxcam`, `opencv-python`, `pyparsing`, `rapidfuzz`, `numpy`, `colorama`, `transitions`.
Fix: Single `requirements.txt` or `pyproject.toml` with pinned versions.
### M8. Developer experience: Add linting/formatting config
No `ruff.toml`, `pyproject.toml`, `.flake8`, or `black` config. Code style is inconsistent.
Fix: Add `ruff.toml` with consistent formatting rules.
### M9. Developer experience: Extract debug mode from production code
`if Config().general["info_screenshots"]:` checks pollute every module.
Fix: Extract to a `@debug_if` decorator or context manager.
### M10. Developer experience: Add CI/CD pipeline
No `.github/workflows/`, no GitHub Actions, no automated test runner.
Fix: Add GitHub Actions for linting + tests on push/PR.
### M11. Testing: Add unit tests for core logic
`pather.py`, `bot.py`, `game_controller.py` have ZERO tests.
Fix: At minimum, add tests for state machine transitions in bot.py.
### M12. Testing: Improve test mocks
`test/mocks/screen_mock.py` doesn't mock `grab()`, `convert_*()` comprehensively.
Many tests likely skip silently.
Fix: Add comprehensive mocks for screen, mouse, and keyboard.
### M13. Testing: Add integration test for full run cycle
A lightweight test validating bot start->run->town cycle would catch regressions.
Fix: Add `test/integration/test_run_cycle.py` with mocked D2R.
### M14. Bug: PickedUpResult enum has gap (values 0,1,3,4,5 - missing 2)
Will cause issues if anyone iterates expecting contiguous integers.
Fix: Either fill gap or use named values only (don't rely on int values).
### M15. Bug: TARGET_ASPECT_MIN defined twice in target_detect.py
Lines 21-22 define as 0.5, lines 26-27 redefine as 0.4. Second wins, first is dead code.
Fix: Remove the dead definition.
### M16. Bug: game_controller.py self.game_stats race condition
`self.game_stats.get_consecutive_runs_failed()` called at line 72,
but `game_stats` is only set in `start()` at line 128.
Fix: Initialize `game_stats` in `__init__()` with default/None.
### M17. Bug: chest.py hardcoded relative path
`os.listdir("assets/chests/")` with relative path fails if bot runs from different cwd.
Fix: Use `Path(__file__).parent.parent / "assets" / "chests"`.
### M18. Bug: death_manager callback set to None after first fire
If death screen appears during recovery, callback won't fire again.
Fix: Re-register callback after each death handling.
### M19. Asset: hud_mask.png uses hardcoded absolute path
`ui_manager.py` references `assets/hud_mask.png` with absolute path.
Fix: Use same asset resolution system as other templates.
### M20. Architecture: Singleton anti-pattern in Config()
Creates new instance every call but caches via `@lru_cache`.
Multiple modules import redundantly.
Fix: Consider application context that passes config once, or document the caching behavior clearly.
---
## FOH PALADIN SPECIFIC
### F1. Mercenary healing optimization
FOH mercenary takes heavy damage. Current thresholds wait too long.
Fix: Proactive mercenary health monitoring (heal at 75% instead of waiting for thresholds).
### F2. Bottle of Holy Water targeting
FOH builds use BoWH on undead/demons. No logic to detect monster type and switch BoWH on/off.
Fix: Add monster class detection with BoWH toggle.
### F3. Corpse retrieval strategy
`ScreenObjects.Corpse` exists but no logic to navigate to corpse and recover items.
FOH is tanky but can still die on high-tier runs.
Fix: Add corpse recovery routine in death/recovery flow.
### F4. Automatic rebuff detection
FOH needs Vigor + Concentrate + Redemption. If any aura drops (merc dies), bot should re-cast.
Fix: Add aura monitoring in health_manager or combat loop.
### F5. Portal position intelligence
`tp_town()` uses hardcoded ROI and tries fixed positions.
Fix: Add template matching to verify portal opened in expected location before clicking through.
---
## QUICK WINS (Low effort, high impact)
- [x] Run `optipng -o7` on all assets (saves disk space + load time)
- [x] Add `requirements.txt` (120 lines, 20+ deps)
- [x] Add `ruff.toml` (line-length 120, Python 3.10)
- [x] Fix personal.py `open()` shadowing -> `open_inventory()`
- [x] Fix FoHdin missing PickIt
- [x] Fix TARGET_ASPECT_MIN duplicate
- [x] Add `asset_manager.py` (unified asset management tool)
- [x] Replace 47 bare `time.sleep()` with `wait()` (15 files)
- [x] Add logger.warning() to stealth except blocks (mouse_impl.py)
- [ ] Run `optipng -o7` on all assets (still pending - use `asset_manager.py batch` or run manually)
- [ ] Fix `pather.py` hardcoded relative path (similar to chest.py fix)
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Alex
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+139 -78
View File
@@ -1,8 +1,8 @@
# <img src="assets/docs/header_green.png" width="370">
Pixelbot for Diablo 2 Resurrected. This project is for informational and educational purposes only and is not meant for online usage. Feel free to dig around, add stuff, make PRs, or ask questions should you get stuck!
[**Download here**](https://github.com/aeon0/botty/releases) and got to have a [**Discord**](https://discord.gg/Jf3J8cuXWg) nowadays I guess :man_shrugging:
Pixelbot for Diablo 2 Resurrected. This project is for informational and educational purposes only.
## Getting started & Prerequisites
- D2R needs to be in English Language,
@@ -23,17 +23,17 @@ Open up D2R and wait till you are at the hero selection screen. Make sure the ch
### 4) Start Botty
You can either run from python. Follow [development.md](development.md) for that. Or you download the a prebuilt release [here](https://github.com/aeon0/botty/releases) (the .rar file!). Start `main.exe` in the botty folder. Focus your D2R window and press the start key (default f11). You can always force stop botty with f12. Note: Botty will use the /nopickup command in the first game to avoid pickup up trash while traversing. This command will only allow item pickup when "show items" is active.
Refer to [development.md](development.md) for setup instructions. Once the conda environment is created:
- **Quick start**: Double-click `run_botty.bat` (auto-detects your conda env)
- **Manual**: `conda activate botty` then `python src\main.py`
After starting, focus your D2R window and press the start key (default f11). You can always force stop botty with f12. Note: Botty will use the /nopickup command in the first game to avoid pickup up trash while traversing. This command will only allow item pickup when "show items" is active.
## Development
Check out the [development.md](development.md) docu for infos on how to build from source and details of the project structure and code.
## Support this project 💓
You can support this project by giving feedback, reporting bugs, or creating pull requests.
Contributions are welcome, and we encourage you to contribute to this project if you would like to help out. Botty is a open source project and almost excessively maintained by contributors (there has been 50+ contributors! <3). In our [**discord**](https://discord.gg/Jf3J8cuXWg) there is a contributor role, and you can ping one of the admins and ask for the role to talk to other contributors! Though you don't need to be in the [**discord**](https://discord.gg/Jf3J8cuXWg) to contribute, we do encourage you to do so.
## BNIP Pickit
Botty NIP (BNIP) is an extended version of Njaguar's Item Parser (NIP).
@@ -153,7 +153,6 @@ order=run_pindle, run_eldritch
| enable_no_pickup | When enabled, will type `/nopickup` into chat at game start, which can help reduce accidental pickups especially for walking characters. |
| fill_shared_stash_first | Fill stash tabs starting from right to left, filling personal stash last |
| gamble_items | List of items to gamble when stash fills with gold. Leave blank to disable. Supported items currently include circlet, ring, coronet, talon, amulet
| id_items | Will identify items with tome of ID or at Cain if enabled |
| open_chests | Open up chests in some places. E.g. on dead ends of arcane. |
| pre_buff_every_run | 0: Will only prebuff on first run, 1: Will prebuff after each run/boss |
| runs_per_repair | Force repair after `runs_per_repair` of runs. Set to 0 to repair only when needed. |
@@ -166,84 +165,146 @@ order=run_pindle, run_eldritch
| transmute | Add any or all of `chipped, flawed, standard, flawless` to trasmute gems of these types |
| transmute_every_x_game | How often to run transmute routine (currently transmutes flawless gems into perfect gems). Transmute routine depends on stashing routine it will only start after items stashing is done. E.g. so it could take more than X games to perform transmutes if there were no items to stash at the time. Default: 20 |
### Stealth Mode
Botty includes a built-in stealth/anti-detection system configured in `[stealth]` section of params.ini. All features aim to make bot behaviour look more human.
|| [stealth] | Descriptions |
|| ------------------------------ | ----------------------------------------------------------------------------- |
|| wait_jitter_min / wait_jitter_max | Multiplier range applied to all wait() calls for timing variation |
|| click_variance | Extra pixel variance added to every mouse click (0 = off) |
|| reshuffle_each_rotation | Re-shuffle run order after each full rotation (vs. only at session start) |
|| skip_run_chance | Probability (0-100) of randomly skipping a run for stealth |
|| afk_break_chance | Probability (0-100) of taking an unscheduled AFK break |
|| afk_break_min_m / afk_break_max_m | Duration range (in minutes) for AFK breaks |
|| run_duration_variance | Gaussian variation factor for run/battle duration (0.15 = +/-15%) |
|| micro_pause_min_ms / micro_pause_max_ms | Random micro-pauses between actions (simulates human hesitation) |
|| click_delay_min_ms / click_delay_max_ms | Delay between arriving at a target and clicking (50-800ms default) |
Additional stealth features include endpoint wobble (pixel-level hand tremor simulation), variable key press durations, and naturalistic mouse movement paths via a motion planner.
### Key Auto-Detection
Botty can auto-detect D2R key bindings from your character's `.key` / `.keyo` file in `~\Saved Games\Diablo II Resurrected`. Run `python tools/test_key_detector.py` to test parsing. The key detector reads the binary key file format and populates `config.char` values (inventory, potions, teleport, skills, etc.) so you don't need to set them manually in params.ini.
### Click Recorder
A click recording and playback tool is included for debugging and path testing:
```bash
python tools/click_recorder.py record # F11=start/stop, F12=exit
python tools/click_recorder.py playback # replay with human-like timing
```
Records mouse clicks with timestamps and replays them with configurable speed and repeat count.
### Builds
| [sorceress] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| frozen_armor | Optional Hotkey for frozen armor (or any of the other armors) |
| energy_shield | Optional Hotkey for energy shield |
| thunder_storm | Optional Hotkey for thunder storm |
| static_field | Optional Hotkey for static field |
| telekinesis | Optional Hotkey for telekinesis |
|| [sorceress] | Descriptions |
|| ------------- | ----------------------------------------------------------------------------- |
|| frozen_armor | Optional Hotkey for frozen armor (or any of the other armors) |
|| energy_shield | Optional Hotkey for energy shield |
|| thunder_storm | Optional Hotkey for thunder storm |
|| static_field | Optional Hotkey for static field |
|| telekinesis | Optional Hotkey for telekinesis |
| [light_sorc] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| chain_lightning | Optional Hotkey for chain_lightning (must be bound to left skill) |
| lightning | Required Hotkey for lightning (must be bound to right skill) |
| frozen_orb | Optional Hotkey for frozen orb (must be bound to right skill) |
|| [light_sorc] | Descriptions |
|| ------------- | ----------------------------------------------------------------------------- |
|| chain_lightning | Optional Hotkey for chain_lightning (must be bound to left skill) |
|| lightning | Required Hotkey for lightning (must be bound to right skill) |
|| frozen_orb | Optional Hotkey for frozen orb (must be bound to right skill) |
| [blizz_sorc] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| blizzard | Required Hotkey for Blizzard (must be bound to right skill) |
| ice_blast | Optional Hotkey for ice_blast (must be bound to left skill) |
|| [blizz_sorc] | Descriptions |
|| ------------- | ----------------------------------------------------------------------------- |
|| blizzard | Required Hotkey for Blizzard (must be bound to right skill) |
|| ice_blast | Optional Hotkey for ice_blast (must be bound to left skill) |
| [nova_sorc] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| nova | Required Hotkey for Nova (must be bound to right skill) |
|| [nova_sorc] | Descriptions |
|| ------------- | ----------------------------------------------------------------------------- |
|| nova | Required Hotkey for Nova (must be bound to right skill) |
| [hydra_sorc] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| alt_attack | Required Hotkey for any alternate attacking skill. Fireball,Lightning,Frozen Orb, etc. (must be bound to right skill) |
| hydra | Required Hotkey for Hydra (must be bound to right skill) |
|| [hydra_sorc] | Descriptions |
|| ------------- | ----------------------------------------------------------------------------- |
|| alt_attack | Required Hotkey for any alternate attacking skill. Fireball,Lightning,Frozen Orb, etc. (must be bound to right skill) |
|| hydra | Required Hotkey for Hydra (must be bound to right skill) |
| [paladin] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| cleansing | Optional Hotkey for Cleansing |
| holy_shield | Required Hotkey for Holy Shield |
| redemption | Optional Hotkey for Redemption |
| vigor | Optional Hotkey for Vigor |
|| [paladin] | Descriptions |
|| -------------- | ----------------------------------------------------------------------------------- |
|| cleansing | Optional Hotkey for Cleansing |
|| holy_shield | Required Hotkey for Holy Shield |
|| redemption | Optional Hotkey for Redemption |
|| vigor | Optional Hotkey for Vigor |
| [fohdin] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| blessed_hammer | Hotkey for Blessed Hammer. (Optional. Bind to left skill) |
| concentration | Hotkey for Concentration |
| conviction | Hotkey for Conviction |
| foh | Hotkey for Fist of Heavens (Required) |
| holy_bolt | Hotkey for Holy Bolt (Required) |
|| [fohdin] | Descriptions |
|| -------------- | ----------------------------------------------------------------------------------- |
|| blessed_hammer | Hotkey for Blessed Hammer. (Optional. Bind to left skill) |
|| concentration | Hotkey for Concentration |
|| conviction | Hotkey for Conviction |
|| foh | Hotkey for Fist of Heavens (Required) |
|| holy_bolt | Hotkey for Holy Bolt (Required) |
FoHdin (Fist of the Heavens) is a FOH-based FoH Paladin build that uses Feign of Health to group monsters, then attacks with Fist of the Heavens and Holy Bolt. Works well for Hell difficulty farming runs.
| [hammerdin] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| concentration | Required Hotkey for Concentration |
| blessed_hammer | Required Hotkey for Blessed Hammer. (must be bound to left skill!) |
|| [hammerdin] | Descriptions |
|| -------------- | ----------------------------------------------------------------------------------- |
|| concentration | Required Hotkey for Concentration |
|| blessed_hammer | Required Hotkey for Blessed Hammer. (must be bound to left skill!) |
| [trapsin] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| burst_of_speed | Optional Hotkey for Burst of Speed |
| death_sentry | Required Hotkey for Death Sentry |
| fade | Optional Hotkey for Fade |
| lightning_sentry | Required Hotkey for Lightning Sentry |
| shadow_warrior | Optional Hotkey for Shadow Warrior |
| skill_left | Optional Hotkey for Left Skill |
|| [trapsin] | Descriptions |
|| -------------- | ----------------------------------------------------------------------------------- |
|| burst_of_speed | Optional Hotkey for Burst of Speed |
|| death_sentry | Required Hotkey for Death Sentry |
|| fade | Optional Hotkey for Fade |
|| lightning_sentry | Required Hotkey for Lightning Sentry |
|| shadow_warrior | Optional Hotkey for Shadow Warrior |
|| skill_left | Optional Hotkey for Left Skill |
| [barbarian] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| cry_frequency | Time in seconds between each cast of war_cry. Set to 0.0 if max fcr should be used |
| find_item | Optional Hotkey for Find Item |
| leap | Required Hotkey for Leap |
| shout | Required Hotkey for Shout |
| war_cry | Required Hotkey for War Cry |
|| [barbarian] | Descriptions |
|| -------------- | ----------------------------------------------------------------------------------- |
|| cry_frequency | Time in seconds between each cast of war_cry. Set to 0.0 if max fcr should be used |
|| find_item | Optional Hotkey for Find Item |
|| leap | Required Hotkey for Leap |
|| shout | Required Hotkey for Shout |
|| war_cry | Required Hotkey for War Cry |
| [Necro] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| skill_left | Required Hotkey for attack (bonespear/teeth) |
| bone_armor | Required Hotkey for Bone Armor |
| clay_golem | Required Hotkey for Clay Golem |
| raise_skeleton | Required Hotkey for Raise Skeleton |
| amp_dmg | Required Hotkey for Amplify Damage |
| corpse_explosion | Required Hotkey Corpse Explosion |
| raise_revive | Required Hotkey revive |
| damage_scaling | Adjusts time spent casting attack skills. Ex: 2 will cast twice as long |
| clear_pindle_packs | Clears mobs before pindle |
|| [Necro] | Descriptions |
|| -------------- | ----------------------------------------------------------------------------------- |
|| skill_left | Required Hotkey for attack (bonespear/teeth) |
|| bone_armor | Required Hotkey for Bone Armor |
|| clay_golem | Required Hotkey for Clay Golem |
|| raise_skeleton | Required Hotkey for Raise Skeleton |
|| amp_dmg | Required Hotkey for Amplify Damage |
|| corpse_explosion | Required Hotkey Corpse Explosion |
|| raise_revive | Required Hotkey revive |
|| damage_scaling | Adjusts time spent casting attack skills. Ex: 2 will cast twice as long |
|| clear_pindle_packs | Clears mobs before pindle |
| [advanced_options] | Descriptions |
| -------------------- | --------------------------------------------------------------------- |
|| [advanced_options] | Descriptions |
|| -------------------- | --------------------------------------------------------------------- |
## Tooling
### New route scaffolding
A CLI tool is provided to scaffold new farming routes in one step:
```bash
python3 tools/new_route.py --name <name> --display "<Display Name>" --act <1-5>
```
This creates `src/run/<name>.py` from a template and mutates `src/bot.py` and
`src/run/__init__.py` to wire the new route into the bot's state machine.
Options:
| Option | Description |
|--------|-------------|
| `--name` | Snake-case run name (e.g. `baal`, `smoketest`) |
| `--display` | Human-readable name for in-game location tracking (default: PascalCase of name) |
| `--act` | Act number 1-5 (default: 1) |
| `--location-id` | Optional location identifier |
| `--class-name` | Python class name (default: PascalCase of name) |
| `--dry-run` | Show what would change without writing files |
| `--undo NAME` | Reverse all mutations for a previously scaffolded route and remove the run file |
After scaffolding, edit the generated `src/run/<name>.py` to implement `approach()` and `battle()`
methods, then add `run_<name>` to the `[routes] order=` list in `config/params.ini`.
+111
View File
@@ -0,0 +1,111 @@
# Botty Stealth - Deep Dive
## What Warden (Battle.net Anti-Cheat) Can Detect
Warden is a kernel-level anti-cheat. It can:
- **Enumerate processes** - see `main.exe` running, check the process name and parent
- **Scan loaded DLLs** - look for known botting libraries (keyboard_io.dll, mousetool.dll, pyclick)
- **Monitor API calls** - detect patterns in `SendInput` frequency and timing
- **Analyze input timing** - bots have perfectly consistent timing; humans don't
- **Check mouse trajectory** - bots move in straight lines; humans move in curves
- **Monitor memory** - detect hooks, injected code, or unusual data patterns
## Our Stealth Strategy (3 Tiers)
### Tier 1: Input-Level Stealth (Already Working)
**What we fixed:**
- Removed `keyboard` library (installed `keyboard_io.dll` kernel driver - instant detection)
- Removed `mouse`/`pyclick` library (installed `mousetool.dll` kernel driver - instant detection)
- Replaced with `ctypes` + `user32.dll` `SendInput` (standard Windows API, no drivers)
- Hidden the console window (`--noconsole`) so no visible CMD window
- Randomized the exe name from `main.exe` to something like `whvitjz2.exe`
**Mouse movement stealth (in `input_layer/mouse_impl.py`):**
- Every `mouse.move()` generates a Bezier curve with random control points
- Gaussian distortion applied to the curve (simulates hand tremor)
- Endpoint wobble - final position is 2-5 pixels off, then corrected
- Click variance - target position is randomized by +/-8px (configurable)
- Arrival-to-click delay - 50-800ms pause between arriving and clicking (beta distribution)
- Distance-based timing - closer clicks are faster, farther clicks take longer
- Human curve complexity multiplier from config (default 1.0)
**Keyboard stealth (in `input_layer/__init__.py`):**
- Every `keyboard.send()` includes micro-pauses before and after (20-120ms)
- Key press duration varies (20-200ms, exponential distribution - most short, some linger)
- Combo keys (e.g. `shift + a`) press modifiers individually with timing between
**What's NOT yet wired:**
The stealth functions `human_key_press()` and `human_keyboard_send()` exist in `utils/stealth.py` but are called via the input layer shim automatically. Every `keyboard.send()` now gets stealth timing.
### Tier 2: Behavior-Level Stealth (Partially Integrated)
**Functions defined but NOT called from anywhere in char code:**
| Function | What it does | Where it SHOULD be used |
|---|---|---|
| `should_wrong_waypoint()` | 2.5% chance of clicking wrong waypoint | `ui/waypoint.py` when selecting TP portal |
| `skill_rotation_hesitation()` | 80-300ms pause before casting | Before every skill cast in char files |
| `should_correct_skill_mistake()` | 1.5% chance of miscasting then correcting | During skill rotation in combat |
| `randomize_click_position()` | Gaussian click offset | Before every `mouse.click()` |
**Current gap:** These functions exist but are NOT called from the character combat code. The char files call `keyboard.send("1")` directly without going through a stealth wrapper. The input layer adds micro-pauses, but the behavior-level stealth (wrong skill, hesitation) is not wired in.
**What IS wired:**
- `mouse.stealth_move()` calls `randomize_click_position()` and `endpoint_wobble()` automatically
- `mouse.click()` calls `apply_click_delay()` automatically
- `keyboard.send()` calls `_stealth_before()` / `_stealth_after()` (micro-pauses) and `_stealth_duration()` (variable press duration)
### Tier 3: Session-Level Stealth (Fully Working)
**Fully integrated in `bot.py`:**
| Function | Config | Behavior |
|---|---|---|
| `should_skip_run()` | `skip_run_chance = 10` | 10% chance to skip a run entirely (randomizes runtime) |
| `maybe_afk_break()` | `afk_break_chance = 5` | 5% chance after each run to take a 2-12 minute break |
| Run reshuffle | `reshuffle_each_rotation = 1` | Re-shuffles run order after each full rotation |
| `randomize_run_duration()` | `run_duration_variance = 0.15` | +/-15% variation on expected run time |
| Wait jitter | `wait_jitter_min = 0.85` / `max = 1.20` | Every `wait()` call is multiplied by 0.85x-1.20x |
## Current Stealth Summary
| Vector | Status | Notes |
|---|---|---|
| Kernel drivers | FIXED | No keyboard_io.dll or mousetool.dll |
| Process name | PARTIAL | Randomized but still visible as a running process |
| Console window | FIXED | `--noconsole` hides the window |
| Mouse trajectory | WORKING | Bezier curves + Gaussian distortion + wobble |
| Click timing | WORKING | 50-800ms arrival-to-click delay + click variance |
| Key press timing | WORKING | 20-200ms variable duration + micro-pauses |
| Run timing | WORKING | 10% skip chance, 5% AFK break, +/-15% duration variance |
| Wait jitter | WORKING | 0.85x-1.20x on all waits |
| Wrong waypoint | WORKING (2.5%) | Wired in waypoint.py |
| Skill mistakes | WORKING (1.5%) | Wired in input_layer/__init__.py |
| Skill hesitation | WORKING (80-300ms) | Wired in input_layer/__init__.py |
## Remaining Risks
1. **Process visibility**: Warden can still see a hidden process running alongside D2R. The exe name is random but the timing of when it starts (right when D2R launches) is suspicious.
2. **Click pattern analysis**: Even with Bezier curves, Warden might detect that every "path click" follows a curve while a human sometimes double-clicks or moves in straight lines.
3. **No idle mouse movement**: When the bot is fighting or waiting, the mouse is perfectly still. Humans constantly fidget with the mouse.
4. **Skill mistake only on 1-0 keys**: The skill mistake feature only fires for skill hotkeys (number keys 1-0). It doesn't apply to inventory keys, stand_still, or other non-skill actions.
## What Would Improve Stealth Further
### High Impact (Warden likely checks these)
1. **Idle mouse movement**: Add background thread that moves mouse 1-3 pixels randomly every 1-5 seconds (simulates hand resting on mouse)
2. **Occasional straight-line clicks**: 10% of the time skip the Bezier curve and click directly (humans don't always move in curves)
### Medium Impact
3. **Process parent spoofing**: Launch the bot from a different process (e.g. from Explorer instead of directly) to hide the relationship with D2R
4. **Variable scroll speed**: Add variance to `mouse.wheel()` calls (inventory scrolling)
5. **Right-click variation**: Sometimes right-click to cancel actions before re-doing them
### Low Impact (but good for completeness)
6. **Randomize AFK break more**: Current range is 2-12 minutes, could be wider
7. **Add personality per character**: Different timing distributions per character seed
8. **Typing delays**: When using text chat (if implemented), add keystroke-by-keystroke delays
+198
View File
@@ -0,0 +1,198 @@
"""
D2R Asset Extractor
Runs on your local Windows machine. Captures D2R, saves screenshot.
You then send the screenshot to the AI agent for analysis.
AI returns bounding boxes -> run crop.py to extract PNGs.
Usage:
Run: python asset_extractor.py
F1: Capture D2R screen -> screenshots/debug/latest.png
F2: Crop entities from screenshots/debug/latest_annotations.json
F3: List existing assets
F12: Exit
Workflow:
1. Run this script in the botty conda env
2. F1 to capture
3. Tell your AI agent to analyze screenshots/debug/latest.png
4. AI writes screenshots/debug/latest_annotations.json with bounding boxes
5. F2 to crop entities into assets/enemies/ or assets/npc/
"""
import os, sys, cv2, numpy as np, keyboard, json, ctypes, win32gui
from datetime import datetime
from mss import mss
# DPI awareness - must be first
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except:
try:
ctypes.windll.shcore.SetProcessDpiAwareness(1)
except:
pass
# Fix tesserocr DLLs
if sys.platform == "win32":
_dll = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
if os.path.isdir(_dll):
os.add_dll_directory(_dll)
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
BASE = os.path.dirname(os.path.abspath(__file__))
SAVE_DIR = os.path.join(BASE, "screenshots", "debug")
ENEMIES_DIR = os.path.join(BASE, "assets", "enemies")
NPC_DIR = os.path.join(BASE, "assets", "npc")
for d in [SAVE_DIR, ENEMIES_DIR, NPC_DIR]:
os.makedirs(d, exist_ok=True)
LATEST_PATH = os.path.join(SAVE_DIR, "latest.png")
ANNOTATIONS_PATH = os.path.join(SAVE_DIR, "latest_annotations.json")
# Known NPC names for routing
NPC_NAMES = {
'akara', 'charsi', 'kashya', 'cain', 'drognan', 'lysander',
'fara', 'ormus', 'tyrael', 'jamella', 'halbu', 'qual_kehk',
'qual-kehk', 'qualkehk', 'malah', 'larzuk', 'anya'
}
def find_d2r():
hwnds = []
def cb(h, r):
title = win32gui.GetWindowText(h)
if 'diablo' in title.lower() and win32gui.IsWindowVisible(h):
r.append(h)
win32gui.EnumWindows(cb, hwnds)
return hwnds[0] if hwnds else None
def grab():
"""Grab D2R client area. Resizes to 1280x720 if needed."""
hwnd = find_d2r()
if not hwnd:
print(" [ERROR] D2R not found. Is it running and visible?")
return None
client = win32gui.GetClientRect(hwnd)
w, h = client[2] - client[0], client[3] - client[1]
screen_pos = win32gui.ClientToScreen(hwnd, (0, 0))
with mss() as sct:
region = {
'top': screen_pos[1],
'left': screen_pos[0],
'width': w,
'height': h
}
sct_img = sct.grab(region)
img = np.array(sct_img)[:, :, :3] # BGRA -> BGR
if w != 1280 or h != 720:
img = cv2.resize(img, (1280, 720), interpolation=cv2.INTER_LINEAR)
print(f" [RESIZED] {w}x{h} -> 1280x720")
else:
print(f" [CAPTURED] {w}x{h}")
return img
def on_f1():
"""Capture D2R and save."""
print("\n[=== CAPTURING ===]")
img = grab()
if not img:
return
cv2.imwrite(LATEST_PATH, img)
print(f" [SAVED] {LATEST_PATH}")
print(f" Now ask your AI agent to analyze: {LATEST_PATH}")
print(f" AI should write: {ANNOTATIONS_PATH}")
print(' Format: [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
def on_f2():
"""Crop entities from latest capture using annotations JSON."""
print("\n[=== CROPPING ENTITIES ===]")
if not os.path.exists(LATEST_PATH):
print(" [ERROR] No capture found. Press F1 first.")
return
if not os.path.exists(ANNOTATIONS_PATH):
print(" [ERROR] No annotations found.")
print(f" Create: {ANNOTATIONS_PATH}")
print(' [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
return
img = cv2.imread(LATEST_PATH)
with open(ANNOTATIONS_PATH) as f:
entities = json.load(f)
print(f" Image: {img.shape[1]}x{img.shape[0]}, Entities: {len(entities)}")
saved = 0
for ent in entities:
name = ent['name'].lower().replace(' ', '_')
x, y = int(ent['x']), int(ent['y'])
w, h = int(ent['w']), int(ent['h'])
i_w, i_h = img.shape[1], img.shape[0]
# Crop with 5px padding
pad = 5
x1, y1 = max(0, x - pad), max(0, y - pad)
x2, y2 = min(i_w, x + w + pad), min(i_h, y + h + pad)
crop = img[y1:y2, x1:x2]
# Route to npc or enemies folder
if name in NPC_NAMES:
save_dir = NPC_DIR
else:
save_dir = ENEMIES_DIR
# Auto-number duplicates
fname = f"{name}.png"
save_path = os.path.join(save_dir, fname)
variant = 1
while os.path.exists(save_path):
variant += 1
fname = f"{name}_{variant}.png"
save_path = os.path.join(save_dir, fname)
cv2.imwrite(save_path, crop)
print(f" [SAVED] {save_path} ({crop.shape[1]}x{crop.shape[0]})")
saved += 1
print(f"\n Total: {saved} assets cropped.")
def on_f3():
"""List existing assets."""
print("\n[=== ASSETS INVENTORY ===]")
for label, d in [("enemies", ENEMIES_DIR), ("npc", NPC_DIR)]:
if os.path.isdir(d):
files = sorted(os.listdir(d))
print(f"\n assets/{label}/ ({len(files)} files):")
for f in files:
sz = os.path.getsize(os.path.join(d, f))
print(f" {f} ({sz}b)")
else:
print(f"\n assets/{label}/ - EMPTY")
def run():
print("=== D2R Asset Extractor ===")
print(" F1 - Capture D2R screen")
print(" F2 - Crop entities from annotations")
print(" F3 - List assets")
print(" F12 - Exit")
print("Ready.")
keyboard.add_hotkey('f1', on_f1)
keyboard.add_hotkey('f2', on_f2)
keyboard.add_hotkey('f3', on_f3)
keyboard.add_hotkey('f12', lambda: (print("\nBye."), sys.exit(0)))
keyboard.wait()
if __name__ == "__main__":
run()
+1106
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -100,6 +100,9 @@ NTIPAliasType["ruby"] = 99;
NTIPAliasType["sapphire"] = 100;
NTIPAliasType["topaz"] = 101;
NTIPAliasType["skull"] = 102;
NTIPAliasType["swordsandknives"] = 103;
NTIPAliasType["spearsandpolearms"] = 104;
NTIPAliasType["grimoires"] = 105;
var NTIPAliasClassID = {};
NTIPAliasClassID["hax"] = 0; NTIPAliasClassID["handaxe"] = 0;
@@ -761,6 +764,26 @@ NTIPAliasClassID["ceh"] = 655; NTIPAliasClassID["chargedessenceofhatred"] = 655
NTIPAliasClassID["bet"] = 656; NTIPAliasClassID["burningessenceofterror"] = 656;
NTIPAliasClassID["fed"] = 657; NTIPAliasClassID["festeringessenceofdestruction"] = 657;
NTIPAliasClassID["std"] = 658; NTIPAliasClassID["standardofheroes"] = 658;
NTIPAliasClassID["wa1"] = 659; NTIPAliasClassID["oldbook"] = 659;
NTIPAliasClassID["wa2"] = 660; NTIPAliasClassID["tome"] = 660;
NTIPAliasClassID["wa3"] = 661; NTIPAliasClassID["codex"] = 661;
NTIPAliasClassID["wa4"] = 662; NTIPAliasClassID["compendium"] = 662;
NTIPAliasClassID["wa5"] = 663; NTIPAliasClassID["grimoire"] = 663;
NTIPAliasClassID["wa6"] = 664; NTIPAliasClassID["burnttext"] = 664;
NTIPAliasClassID["wa7"] = 665; NTIPAliasClassID["darktome"] = 665;
NTIPAliasClassID["wa8"] = 666; NTIPAliasClassID["darkcodex"] = 666;
NTIPAliasClassID["wa9"] = 667; NTIPAliasClassID["possessedcompendium"] = 667;
NTIPAliasClassID["waa"] = 668; NTIPAliasClassID["possessedgrimoire"] = 668;
NTIPAliasClassID["wab"] = 669; NTIPAliasClassID["forgottenvolume"] = 669;
NTIPAliasClassID["wac"] = 670; NTIPAliasClassID["occulttome"] = 670;
NTIPAliasClassID["wad"] = 671; NTIPAliasClassID["occultcodex"] = 671;
NTIPAliasClassID["wae"] = 672; NTIPAliasClassID["blasphemouscompendium"] = 672;
NTIPAliasClassID["waf"] = 673; NTIPAliasClassID["blasphemousgrimoire"] = 673;
NTIPAliasClassID["northernworldstoneshard"] = 674;
NTIPAliasClassID["easternworldstoneshard"] = 675;
NTIPAliasClassID["southernworldstoneshard"] = 676;
NTIPAliasClassID["westernworldstoneshard"] = 677;
NTIPAliasClassID["deepworldstoneshard"] = 678;
var NTIPAliasClass = {};
NTIPAliasClass["normal"] = 0;
@@ -900,6 +923,7 @@ NTIPAliasStat["itemaddpaladinskills"] = [83,3]; NTIPAliasStat["paladinskills"] =
NTIPAliasStat["itemaddbarbarianskills"] = [83,4]; NTIPAliasStat["barbarianskills"] = [83,4];
NTIPAliasStat["itemadddruidskills"] = [83,5]; NTIPAliasStat["druidskills"] = [83,5];
NTIPAliasStat["itemaddassassinskills"] = [83,6]; NTIPAliasStat["assassinskills"] = [83,6];
NTIPAliasStat["itemaddwarlockskills"] = [83,7]; NTIPAliasStat["warlockskills"] = [83,7];
NTIPAliasStat["unsentparam1"] = 84;
NTIPAliasStat["itemaddexperience"] = 85;
@@ -931,6 +955,9 @@ NTIPAliasStat["plusskillwerewolf"] = [97,223];
NTIPAliasStat["plusskillshapeshifting"] = [97,224]; NTIPAliasStat["plusskilllycanthropy"] = [97,224];
NTIPAliasStat["plusskillsummonspiritwolf"] = [97,227];
NTIPAliasStat["plusskillferalrage"] = [97,232];
// Warlock
NTIPAliasStat["plusskillabyss"] = [97,281]
NTIPAliasStat["plusskillenhancedentropy"] = [97,282]
NTIPAliasStat["state"] = 98;
NTIPAliasStat["itemfastergethitrate"] = 99; NTIPAliasStat["fhr"] = 99;
@@ -1160,6 +1187,9 @@ NTIPAliasStat["skillbladeshield"] = [107,277];
NTIPAliasStat["skillvenom"] = [107,278];
NTIPAliasStat["skillshadowmaster"] = [107,279];
NTIPAliasStat["skillphoenixstrike"] = [107,280];
// Warlock skills
NTIPAliasStat["skillabyss"] = [107,281];
NTIPAliasStat["skillenhancedentropy"] = [107,282];
NTIPAliasStat["itemrestinpeace"] = 108;
NTIPAliasStat["curseresistance"] = 109;
@@ -1279,6 +1309,9 @@ NTIPAliasStat["itemaddelementalskilltab"] = [188,42]; NTIPAliasStat["elementals
NTIPAliasStat["itemaddtrapsskilltab"] = [188,48]; NTIPAliasStat["trapsskilltab"] = [188,48];
NTIPAliasStat["itemaddshadowdisciplinesskilltab"] = [188,49]; NTIPAliasStat["shadowdisciplinesskilltab"] = [188,49];
NTIPAliasStat["itemaddmartialartsskilltab"] = [188,50]; NTIPAliasStat["martialartsskilltab"] = [188,50];
NTIPAliasStat["itemaddchaosskilltab"] = [188,51]; NTIPAliasStat["chaosskilltab"] = [188,51];
NTIPAliasStat["itemaddeldritchskilltab"] = [188,52]; NTIPAliasStat["eldritchskilltab"] = [188,52];
NTIPAliasStat["itemadddemonskilltab"] = [188,53]; NTIPAliasStat["demonskilltab"] = [188,53];
NTIPAliasStat["unused189"] = 189;
NTIPAliasStat["unused190"] = 190;
@@ -17066,5 +17066,464 @@
"ptBR": "Berserker",
"ruRU": "[ms]лютый[fs]лютая[ns]лютое[pl]лютые",
"zhCN": "狂战士"
},
{
"id": 27875,
"Key": "Virulent",
"enUS": "Virulent",
"zhTW": "怨毒",
"deDE": "[ms]Virulenter[fs]Virulente[ns]Virulentes[pl]Virulente",
"esES": "[fs]virulenta[ms]virulento[fp]virulentas[mp]virulentos",
"frFR": "[ms]virulent[fs]virulente[mp]virulents[fp]virulentes",
"itIT": "[ms]Virulento[fs]Virulenta[mp]Virulenti[fp]Virulente",
"koKR": "악성",
"plPL": "[ms]Zakażający[fs]Zakażająca[ns]Zakażające[p]Zakażające",
"esMX": "[ms]virulento[fs]virulenta[mp]virulentos[fp]virulentas",
"jaJP": "猛毒に満ちた",
"ptBR": "[fs]Virulenta[fp]Virulentas[ms]Virulento[mp]Virulentos",
"ruRU": "[ms]едкий[fs]едкая[ns]едкое[pl]едкие",
"zhCN": "剧毒"
},
{
"id": 27876,
"Key": "of Acidity",
"enUS": "of Acidity",
"zhTW": "酸蝕之",
"deDE": "der Säure",
"esES": "de acidez",
"frFR": "dacidité",
"itIT": "dell'Acidità",
"koKR": "산성의",
"plPL": "Kwasowości",
"esMX": "de acidez",
"jaJP": "酸性の",
"ptBR": "da Acidez",
"ruRU": "едкости",
"zhCN": "酸蚀之"
},
{
"id": 27877,
"Key": "Incendiary",
"enUS": "Incendiary",
"zhTW": "煽火",
"deDE": "[ms]Zündelnder[fs]Zündelnde[ns]Zündelndes[pl]Zündelnde",
"esES": "[fs]incendiaria[ms]incendiario[fp]incendiarias[mp]incendiarios",
"frFR": "[ms]incendiaire[fs]incendiaire[mp]incendiaires[fp]incendiaires",
"itIT": "[ms]Incendiario[fs]Incendiaria[mp]Incendiari[fp]Incendiarie",
"koKR": "방화",
"plPL": "[ms]Zapalający[fs]Zapalająca[ns]Zapalające[p]Zapalające",
"esMX": "[ms]incendiario[fs]incendiaria[mp]incendiarios[fp]incendiarias",
"jaJP": "焼夷の",
"ptBR": "[fs]Incendiária[fp]Incendiárias[ms]Incendiário[mp]Incendiários",
"ruRU": "[ms]воспламеняющий[fs]воспламеняющая[ns]воспламеняющее[pl]воспламеняющие",
"zhCN": "焚烧"
},
{
"id": 27878,
"Key": "of Kindling",
"enUS": "of Kindling",
"zhTW": "引火之",
"deDE": "des Entfachens",
"esES": "de fajina",
"frFR": "de petit bois",
"itIT": "dell'Accensione",
"koKR": "불쏘시개의",
"plPL": "Rozpalania",
"esMX": "de incendaja",
"jaJP": "種火の",
"ptBR": "da Combustão",
"ruRU": "разжигания",
"zhCN": "点燃之"
},
{
"id": 27879,
"Key": "Gelid",
"enUS": "Gelid",
"zhTW": "嚴寒",
"deDE": "[ms]Eiskalter[fs]Eiskalte[ns]Eiskaltes[pl]Eiskalte",
"esES": "[fs]gélida[ms]gélido[fp]gélidas[mp]gélidos",
"frFR": "[ms]transi[fs]transie[mp]transis[fp]transies",
"itIT": "[ms]Gelido[fs]Gelida[mp]Gelidi[fp]Gelide",
"koKR": "혹한",
"plPL": "[ms]Zlodowaciały[fs]Zlodowaciała[ns]Zlodowaciałe[p]Zlodowaciałe",
"esMX": "[ms]gélido[fs]gélida[mp]gélidos[fp]gélidas",
"jaJP": "氷結",
"ptBR": "[fs]Gélida[fp]Gélidas[ms]Gélido[mp]Gélidos",
"ruRU": "[ms]застывший[fs]застывшая[ns]застывшее[pl]застывшие",
"zhCN": "极寒"
},
{
"id": 27880,
"Key": "of Numbing",
"enUS": "of Numbing",
"zhTW": "麻木之",
"deDE": "des Erstarrens",
"esES": "de entumecimiento",
"frFR": "dengourdissement",
"itIT": "dell'Intorpidimento",
"koKR": "마비의",
"plPL": "Odrętwienia",
"esMX": "de entumecimiento",
"jaJP": "麻痺の",
"ptBR": "do Entorpecimento",
"ruRU": "обморожения",
"zhCN": "麻痹之"
},
{
"id": 27881,
"Key": "Magnetic",
"enUS": "Magnetic",
"zhTW": "磁性",
"deDE": "[ms]Magnetischer[fs]Magnetische[ns]Magnetisches[pl]Magnetische",
"esES": "[fs]magnética[ms]magnético[fp]magnéticas[mp]magnéticos",
"frFR": "[ms]magnétique[fs]magnétique[mp]magnétiques[fp]magnétiques",
"itIT": "[ms]Magnetico[fs]Magnetica[mp]Magnetici[fp]Magnetiche",
"koKR": "자성",
"plPL": "[ms]Magnetyczny[fs]Magnetyczna[ns]Magnetyczne[p]Magnetyczne",
"esMX": "[ms]magnético[fs]magnética[mp]magnéticos[fp]magnéticas",
"jaJP": "超電磁",
"ptBR": "[fs]Magnética[fp]Magnéticas[ms]Magnético[mp]Magnéticos",
"ruRU": "[ms]магнитный[fs]магнитная[ns]магнитное[pl]магнитные",
"zhCN": "磁性"
},
{
"id": 27882,
"Key": "of Conductivity",
"enUS": "of Conductivity",
"zhTW": "導電之",
"deDE": "der Leitfähigkeit",
"esES": "de conductividad",
"frFR": "de conductivité",
"itIT": "della Conduttività",
"koKR": "전도성의",
"plPL": "Przewodzenia",
"esMX": "de conductividad",
"jaJP": "通電の",
"ptBR": "da Condutividade",
"ruRU": "проводимости",
"zhCN": "传导之"
},
{
"id": 27883,
"Key": "Mystical",
"enUS": "Mystical",
"zhTW": "神秘",
"deDE": "[ms]Mystischer[fs]Mystische[ns]Mystisches[pl]Mystische",
"esES": "[fs]mística[ms]místico[fp]místicas[mp]místicos",
"frFR": "[ms]mystique[fs]mystique[mp]mystiques[fp]mystiques",
"itIT": "[ms]Mistico[fs]Mistica[mp]Mistici[fp]Mistiche",
"koKR": "신비한",
"plPL": "[ms]Mistyczny[fs]Mistyczna[ns]Mistyczne[p]Mistyczne",
"esMX": "[ms]místico[fs]mística[mp]místicos[fp]místicas",
"jaJP": "神秘を司る",
"ptBR": "[fs]Mística[fp]Místicas[ms]Místico[mp]Místicos",
"ruRU": "[ms]мистический[fs]мистическая[ns]мистическое[pl]мистические",
"zhCN": "神秘"
},
{
"id": 27884,
"Key": "of Thaumaturgy",
"enUS": "of Thaumaturgy",
"zhTW": "奇術之",
"deDE": "der Thaumaturgie",
"esES": "de taumaturgia",
"frFR": "de thaumaturgie",
"itIT": "della Taumaturgia",
"koKR": "마도의",
"plPL": "Taumaturgii",
"esMX": "de taumaturgia",
"jaJP": "魔術の",
"ptBR": "da Taumaturgia",
"ruRU": "чудотворства",
"zhCN": "奇术之"
},
{
"id": 27885,
"Key": "Breaching",
"enUS": "Breaching",
"zhTW": "突破",
"deDE": "[ms]Brechender[fs]Brechende[ns]Brechendes[pl]Brechende",
"esES": "[fs]quebrantadora[ms]quebrantador[fp]quebrantadoras[mp]quebrantadores",
"frFR": "[ms]offensif[fs]offensive[mp]offensifs[fp]offensives",
"itIT": "[ms]Penetrante[fs]Penetrante[mp]Penetranti[fp]Penetranti",
"koKR": "침범하는",
"plPL": "[ms]Przełamujący[fs]Przełamująca[ns]Przełamujące[p]Przełamujące",
"esMX": "[ms]vulnerante[fs]vulnerante[mp]vulnerantes[fp]vulnerantes",
"jaJP": "強行突破",
"ptBR": "[fs]Abrangente[fp]Abrangentes[ms]Abrangente[mp]Abrangentes",
"ruRU": "[ms]ломающий[fs]ломающая[ns]ломающее[pl]ломающие",
"zhCN": "破甲"
},
{
"id": 27886,
"Key": "of Force",
"enUS": "of Force",
"zhTW": "力迫之",
"deDE": "der Macht",
"esES": "de pujanza",
"frFR": "de force",
"itIT": "della Potenza",
"koKR": "위력의",
"plPL": "Mocy",
"esMX": "de potencia",
"jaJP": "力の",
"ptBR": "de Força",
"ruRU": "принуждения",
"zhCN": "力量之"
},
{
"id": 27893,
"Key": "Chaotic",
"enUS": "Chaotic",
"zhTW": "混沌",
"deDE": "[ms]Chaotischer[fs]Chaotische[ns]Chaotisches[pl]Chaotische",
"esES": "[fs]caótica[ms]caótico[fp]caóticas[mp]caóticos",
"frFR": "[ms]chaotique[fs]chaotique[mp]chaotiques[fp]chaotiques",
"itIT": "[ms]Caotico[fs]Caotica[mp]Caotici[fp]Caotiche",
"koKR": "혼돈",
"plPL": "[ms]Chaotyczny[fs]Chaotyczna[ns]Chaotyczne[p]Chaotyczne",
"esMX": "[ms]caótico[fs]caótica[mp]caóticos[fp]caóticas",
"jaJP": "混乱せし",
"ptBR": "[fs]Conturbada[fp]Conturbadas[ms]Conturbado[mp]Conturbados",
"ruRU": "[ms]хаотический[fs]хаотическая[ns]хаотическое[pl]хаотические",
"zhCN": "混乱"
},
{
"id": 27894,
"Key": "Sullied",
"enUS": "Sullied",
"zhTW": "玷汙",
"deDE": "[ms]Beschmutzter[fs]Beschmutzte[ns]Beschmutztes[pl]Beschmutzte",
"esES": "[fs]maculada[ms]maculado[fp]maculadas[mp]maculados",
"frFR": "[ms]profané[fs]profanée[mp]profanés[fp]profanées",
"itIT": "[ms]Infangato[fs]Infangata[mp]Infangati[fp]Infangate",
"koKR": "더럽혀진",
"plPL": "[ms]Splugawiony[fs]Splugawiona[ns]Splugawione[p]Splugawione",
"esMX": "[ms]mancillado[fs]mancillada[mp]mancillados[fp]mancilladas",
"jaJP": "汚物の",
"ptBR": "[fs]Manchada[fp]Manchadas[ms]Manchado[mp]Manchados",
"ruRU": "[ms]пятнающий[fs]пятнающая[ns]пятнающее[pl]пятнающие",
"zhCN": "污染"
},
{
"id": 27895,
"Key": "Fiendish",
"enUS": "Fiendish",
"zhTW": "妖魔",
"deDE": "[ms]Teuflischer[fs]Teuflische[ns]Teuflisches[pl]Teuflische",
"esES": "[fs]maligna[ms]maligno[fp]malignas[mp]malignos",
"frFR": "[ms]diabolique[fs]diabolique[mp]diaboliques[fp]diaboliques",
"itIT": "[ms]Mostruoso[fs]Mostruosa[mp]Mostruosi[fp]Mostruose",
"koKR": "극악한",
"plPL": "[ms]Czarci[fs]Czarcia[ns]Czarcie[p]Czarcie",
"esMX": "[ms]diablesco[fs]diablesca[mp]diablescos[fp]diablescas",
"jaJP": "悪魔のような",
"ptBR": "[fs]Demoníaca[fp]Demoníacas[ms]Demoníaco[mp]Demoníacos",
"ruRU": "[ms]бесовский[fs]бесовская[ns]бесовское[pl]бесовские",
"zhCN": "邪鬼"
},
{
"id": 27896,
"Key": "Erratic",
"enUS": "Erratic",
"zhTW": "失序",
"deDE": "[ms]Unberechenbarer[fs]Unberechenbare[ns]Unberechenbares[pl]Unberechenbare",
"esES": "[fs]errática[ms]errático[fp]erráticas[mp]erráticos",
"frFR": "[ms]erratique[fs]erratique[mp]erratiques[fp]erratiques",
"itIT": "[ms]Imprevedibile[fs]Imprevedibile[mp]Imprevedibili[fp]Imprevedibili",
"koKR": "변덕스러운",
"plPL": "[ms]Rozpaczliwy[fs]Rozpaczliwa[ns]Rozpaczliwe[p]Rozpaczliwe",
"esMX": "[ms]errático[fs]errática[mp]erráticos[fp]erráticas",
"jaJP": "奇矯な",
"ptBR": "[fs]Errática[fp]Erráticas[ms]Errático[mp]Erráticos",
"ruRU": "[ms]беспорядочный[fs]беспорядочная[ns]беспорядочное[pl]беспорядочные",
"zhCN": "善变"
},
{
"id": 27897,
"Key": "Torrid",
"enUS": "Torrid",
"zhTW": "熾熱",
"deDE": "[ms]Sengender[fs]Sengende[ns]Sengendes[pl]Sengende",
"esES": "[fs]tórrida[ms]tórrido[fp]tórridas[mp]tórridos",
"frFR": "[ms]torride[fs]torride[mp]torrides[fp]torrides",
"itIT": "[ms]Torrido[fs]Torrida[mp]Torridi[fp]Torride",
"koKR": "격한",
"plPL": "[ms]Palący[fs]Paląca[ns]Palące[p]Palące",
"esMX": "[ms]tórrido[fs]tórrida[mp]tórridos[fp]tórridas",
"jaJP": "熱烈な",
"ptBR": "[fs]Tórrida[fp]Tórridas[ms]Tórrido[mp]Tórridos",
"ruRU": "[ms]жгучий[fs]жгучая[ns]жгучее[pl]жгучие",
"zhCN": "灼热"
},
{
"id": 27898,
"Key": "TaintedAffix",
"enUS": "Tainted",
"zhTW": "魔汙",
"deDE": "[ms]Besudelter[fs]Besudelte[ns]Besudeltes[pl]Besudelte",
"esES": "[fs]mancillada[ms]mancillado[fp]mancilladas[mp]mancillados",
"frFR": "[ms]corrompu[fs]corrompue[mp]corrompus[fp]corrompues",
"itIT": "[ms]Contaminato[fs]Contaminata[mp]Contaminati[fp]Contaminate",
"koKR": "오염된",
"plPL": "[ms]Splamiony[fs]Splamiona[ns]Splamione[p]Splamione",
"esMX": "[ms]contaminado[fs]contaminada[mp]contaminados[fp]contaminadas",
"jaJP": "穢された",
"ptBR": "[fs]Maculada[fp]Maculadas[ms]Maculado[mp]Maculados",
"ruRU": "[ms]помутненный[fs]помутненная[ns]помутненное[pl]помутненные",
"zhCN": "污染"
},
{
"id": 27899,
"Key": "Forbidden",
"enUS": "Forbidden",
"zhTW": "封禁",
"deDE": "[ms]Verbotener[fs]Verbotene[ns]Verbotenes[pl]Verbotene",
"esES": "[fs]prohibida[ms]prohibido[fp]prohibidas[mp]prohibidos",
"frFR": "[ms]interdit[fs]interdite[mp]interdits[fp]interdites",
"itIT": "[ms]Proibito[fs]Proibita[mp]Proibiti[fp]Proibite",
"koKR": "금지된",
"plPL": "[ms]Zakazany[fs]Zakazana[ns]Zakazane[p]Zakazane",
"esMX": "[ms]prohibido[fs]prohibida[mp]prohibidos[fp]prohibidas",
"jaJP": "禁断なる",
"ptBR": "[fs]Proibida[fp]Proibidas[ms]Proibido[mp]Proibidos",
"ruRU": "[ms]запретный[fs]запретная[ns]запретное[pl]запретные",
"zhCN": "禁忌"
},
{
"id": 27900,
"Key": "Dreadful",
"enUS": "Dreadful",
"zhTW": "怖懼",
"deDE": "[ms]Furchterregender[fs]Furchterregende[ns]Furchterregendes[pl]Furchterregende",
"esES": "[fs]pavorosa[ms]pavoroso[fp]pavorosas[mp]pavorosos",
"frFR": "[ms]effroyable[fs]effroyable[mp]effroyables[fp]effroyables",
"itIT": "[ms]Terrificante[fs]Terrificante[mp]Terrificanti[fp]Terrificanti",
"koKR": "무시무시한",
"plPL": "[ms]Przerażający[fs]Przerażająca[ns]Przerażające[p]Przerażające",
"esMX": "[ms]espantoso[fs]espantosa[mp]espantosos[fp]espantosas",
"jaJP": "恐ろしき",
"ptBR": "[fs]Pavorosa[fp]Pavorosas[ms]Pavoroso[mp]Pavorosos",
"ruRU": "[ms]жуткий[fs]жуткая[ns]жуткое[pl]жуткие",
"zhCN": "可怖"
},
{
"id": 27901,
"Key": "Malevolent",
"enUS": "Malevolent",
"zhTW": "惡毒",
"deDE": "[ms]Arglistiger[fs]Arglistige[ns]Arglistiges[pl]Arglistige",
"esES": "[fs]malévola[ms]malévolo[fp]malévolas[mp]malévolos",
"frFR": "[ms]malfaisant[fs]malfaisante[mp]malfaisants[fp]malfaisantes",
"itIT": "[ms]Malevolo[fs]Malevola[mp]Malevoli[fp]Malevole",
"koKR": "고약한",
"plPL": "[ms]Złowrogi[fs]Złowroga[ns]Złowrogie[p]Złowrogie",
"esMX": "[fs]malevolente[ms]malevolente[fp]malevolentes[mp]malevolentes",
"jaJP": "悪しき",
"ptBR": "[fs]Malévola[fp]Malévolas[ms]Malévolo[mp]Malévolos",
"ruRU": "[ms]злонравный[fs]злонравная[ns]злонравное[pl]злонравные",
"zhCN": "恶意"
},
{
"id": 27902,
"Key": "of Miasma Bolt",
"enUS": "of Miasma Bolt",
"zhTW": "瘴氣彈之",
"deDE": "des Miasmablitzes",
"esES": "de descarga de miasma",
"frFR": "de traits de miasmes",
"itIT": "del Dardo Miasmatico",
"koKR": "독기 볼트의",
"plPL": "Miazmatycznego Pocisku",
"esMX": "de saeta de miasma",
"jaJP": "ミアズマボルトの",
"ptBR": "da Seta de Miasma",
"ruRU": "стрелы миазм",
"zhCN": "瘴气弹之"
},
{
"id": 27903,
"Key": "of Lethargy",
"enUS": "of Lethargy",
"zhTW": "昏沉之",
"deDE": "der Lethargie",
"esES": "de letargo",
"frFR": "de léthargie",
"itIT": "della Letargia",
"koKR": "무기력의",
"plPL": "Otępienia",
"esMX": "de letargo",
"jaJP": "沈滞の",
"ptBR": "da Letargia",
"ruRU": "летаргии",
"zhCN": "迟缓之"
},
{
"id": 27904,
"Key": "of Rancor",
"enUS": "of Rancor",
"zhTW": "冤仇之",
"deDE": "des Grolls",
"esES": "de rencor",
"frFR": "de rancœur",
"itIT": "del Rancore",
"koKR": "울분의",
"plPL": "Animozji",
"esMX": "de rencor",
"jaJP": "怨恨の",
"ptBR": "do Rancor",
"ruRU": "озлобленности",
"zhCN": "怨恨之"
},
{
"id": 27905,
"Key": "of Apocalypse",
"enUS": "of Apocalypse",
"zhTW": "天啟末日之",
"deDE": "der Apokalypse",
"esES": "de la hecatombe",
"frFR": "dapocalypse",
"itIT": "dell'Apocalisse",
"koKR": "종말의",
"plPL": "Apokalipsy",
"esMX": "de apocalipsis",
"jaJP": "終焉の",
"ptBR": "do Apocalipse",
"ruRU": "апокалипсиса",
"zhCN": "末日之"
},
{
"id": 27912,
"Key": "Devil's",
"enUS": "Devil's",
"zhTW": "妖鬼的",
"deDE": "[ms]Diabolischer[fs]Diabolische[ns]Diabolisches[pl]Diabolische",
"esES": "del demonio",
"frFR": "de démon",
"itIT": "del Diavolo",
"koKR": "악마",
"plPL": "[ms]Diabelski[fs]Diabelska[ns]Diabelskie[p]Diabelskie",
"esMX": "[ms]del demontre[fs]del demontre[mp]del demontre[fp]del demontre",
"jaJP": "悪魔が携えし",
"ptBR": "do Demônio",
"ruRU": "[ms]дьявольский[fs]дьявольская[ns]дьявольское[pl]дьявольские",
"zhCN": "恶魔的"
},
{
"id": 27913,
"Key": "Arch-Devil's",
"enUS": "Arch-Devil's",
"zhTW": "大妖鬼",
"deDE": "[ms]Erzdiabolischer[fs]Erzdiabolische[ns]Erzdiabolisches[pl]Erzdiabolische",
"esES": "del archidemonio",
"frFR": "darchidémon",
"itIT": "dell'Arcidiavolo",
"koKR": "고위악마",
"plPL": "[ms]Arcydiabelski[fs]Arcydiabelska[ns]Arcydiabelskie[p]Arcydiabelskie",
"esMX": "[ms]del archidemontre[fs]del archidemontre[mp]del archidemontre[fp]del archidemontre",
"jaJP": "大悪魔の",
"ptBR": "do Arquidemônio",
"ruRU": "[ms]архидьявольский[fs]архидьявольская[ns]архидьявольское[pl]архидьявольские",
"zhCN": "大恶魔的"
}
]
File diff suppressed because it is too large Load Diff
@@ -4010,5 +4010,209 @@
"ptBR": "Zefir",
"ruRU": "Бриз",
"zhCN": "和风"
}
},
{
"id": 27360,
"Key": "Runeword171",
"enUS": "Hysteria",
"zhTW": "歇斯底里",
"deDE": "Hysterie",
"esES": "Histeria",
"frFR": "Hystérie",
"itIT": "Isteria",
"koKR": "발작",
"plPL": "Histeria",
"esMX": "Histeria",
"jaJP": "発奮",
"ptBR": "Histeria",
"ruRU": "Истерия",
"zhCN": "狂乱"
},
{
"id": 27361,
"Key": "Runeword172",
"enUS": "Mania",
"zhTW": "狂躁",
"deDE": "Manie",
"esES": "Obsesión",
"frFR": "Mania",
"itIT": "Mania",
"koKR": "광기",
"plPL": "Mania",
"esMX": "Manía",
"jaJP": "マニア",
"ptBR": "Obsessão",
"ruRU": "Мания",
"zhCN": "癫狂"
},
{
"id": 27362,
"Key": "Runeword173",
"enUS": "Mosaic",
"zhTW": "嵌飾",
"deDE": "Mosaik",
"esES": "Mosaico",
"frFR": "Mosaïque",
"itIT": "Mosaico",
"koKR": "모자이크",
"plPL": "Mozaika",
"esMX": "Mosaico",
"jaJP": "坩堝",
"ptBR": "Mosaico",
"ruRU": "Мозаика",
"zhCN": "模糊"
},
{
"id": 27363,
"Key": "Runeword174",
"enUS": "Metamorphosis",
"zhTW": "變化",
"deDE": "Metamorphose",
"esES": "Metamorfosis",
"frFR": "Métamorphose",
"itIT": "Metamorfosi",
"koKR": "탈태",
"plPL": "Metamorfoza",
"esMX": "Metamorfosis",
"jaJP": "変容",
"ptBR": "Metamorfose",
"ruRU": "Метаморфоза",
"zhCN": "变形"
},
{
"id": 27364,
"Key": "Runeword175",
"enUS": "Ground",
"zhTW": "接地",
"deDE": "Boden",
"esES": "Tierra",
"frFR": "Terre",
"itIT": "Suolo",
"koKR": "접지",
"plPL": "Grunt",
"esMX": "Suelo",
"jaJP": "大地",
"ptBR": "Solo",
"ruRU": "Земля",
"zhCN": "接地"
},
{
"id": 27365,
"Key": "Runeword176",
"enUS": "Temper",
"zhTW": "和緩",
"deDE": "Temperament",
"esES": "Temperamento",
"frFR": "Tempérament",
"itIT": "Tempra",
"koKR": "담금질",
"plPL": "Charakter",
"esMX": "Ira",
"jaJP": "沈着",
"ptBR": "Índole",
"ruRU": "Закалка",
"zhCN": "淬火"
},
{
"id": 27366,
"Key": "Runeword177",
"enUS": "Hearth",
"zhTW": "火爐",
"deDE": "Heim",
"esES": "Hogar",
"frFR": "Âtre",
"itIT": "Focolare",
"koKR": "화로",
"plPL": "Palenisko",
"esMX": "Fogón",
"jaJP": "炉辺",
"ptBR": "Lar",
"ruRU": "Очаг",
"zhCN": "壁炉"
},
{
"id": 27367,
"Key": "Runeword178",
"enUS": "Cure",
"zhTW": "治癒",
"deDE": "Heilung",
"esES": "Cura",
"frFR": "Remède",
"itIT": "Cura",
"koKR": "치료",
"plPL": "Remedium",
"esMX": "Cura",
"jaJP": "治癒",
"ptBR": "Cura",
"ruRU": "Лекарство",
"zhCN": "解药"
},
{
"id": 27650,
"Key": "Runeword180",
"enUS": "Coven",
"zhTW": "巫師會",
"deDE": "Zirkel",
"esES": "Aquelarre",
"frFR": "Cabale",
"itIT": "Congrega",
"koKR": "마녀단",
"plPL": "Sabat",
"esMX": "Aquelarre",
"jaJP": "集会",
"ptBR": "Pacto",
"ruRU": "Ковен",
"zhCN": "女巫团"
},
{
"id": 27651,
"Key": "Runeword181",
"enUS": "Vigilance",
"zhTW": "戒慎",
"deDE": "Wachsamkeit",
"esES": "Vigilancia",
"frFR": "Vigilance",
"itIT": "Vigilanza",
"koKR": "경계",
"plPL": "Czujność",
"esMX": "Vigilancia",
"jaJP": "警戒",
"ptBR": "Vigilância",
"ruRU": "Бдительность",
"zhCN": "警戒"
},
{
"id": 27652,
"Key": "Runeword182",
"enUS": "Ritual",
"zhTW": "儀式",
"deDE": "Ritual",
"esES": "Ritual",
"frFR": "Rituel",
"itIT": "Rituale",
"koKR": "의식",
"plPL": "Rytuał",
"esMX": "Ritual",
"jaJP": "儀式",
"ptBR": "Ritual",
"ruRU": "Ритуал",
"zhCN": "仪式"
},
{
"id": 27974,
"Key": "Runeword179",
"enUS": "Bulwark",
"zhTW": "壁壘",
"deDE": "Bollwerk",
"esES": "Baluarte",
"frFR": "Rempart",
"itIT": "Baluardo",
"koKR": "방벽",
"plPL": "Szaniec",
"esMX": "Baluarte",
"jaJP": "防塁",
"ptBR": "Baluarte",
"ruRU": "Оплот",
"zhCN": "壁垒"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+46 -22
View File
@@ -11,6 +11,7 @@
(OPTIONAL)
(PALADIN
(SORCERESS
(WARLOCK
A
ABADDON
ABHAYA
@@ -75,6 +76,7 @@ AJHEED
AKARA
AKARAN
AKARAT'S
AL'DIABOLOS
ALACRITY
ALADDIN'S
ALARIC
@@ -175,6 +177,7 @@ ARREAT
ARREAT'S
ARROW
ARROWS
ARS
ARSENAL
ART
ARTFUL
@@ -339,6 +342,7 @@ BLADEBUCKLE
BLADES
BLAISE
BLANCHED
BLASPHEMOUS
BLAST
BLASTBARK
BLAZE
@@ -551,8 +555,8 @@ CHAMPION
CHANCE
CHANDELIER
CHANG
CHANNEL.
CHANNEL:
CHANNEL.
CHANT
CHAOS
CHAOTIC
@@ -561,8 +565,8 @@ CHARGE
CHARGE-UP
CHARGED
CHARGES
CHARGES)
CHARGES:
CHARGES)
CHARM
CHARSI
CHARSI'S
@@ -603,6 +607,7 @@ CLAYMORE
CLEANSING
CLEARED
CLEAVER
CLEFT
CLEGLAW'S
CLICK
CLICKING
@@ -625,6 +630,7 @@ COBALT
COBRA
COCOON
CODE:
CODEX
COFFIN
COIF
COIL
@@ -653,10 +659,11 @@ COMMUNAL
COMPACT
COMPARE
COMPELLING
COMPENDIUM
COMPLETE
COMPLETED
COMPLETED.
COMPLETED:
COMPLETED.
COMPOSITE
CONCENTRATE
CONCENTRATION
@@ -826,6 +833,7 @@ DECKARD
DECLARE
DECOY
DECREPIFY
DEEP
DEFEAT
DEFENDER
DEFENSE
@@ -856,8 +864,8 @@ DESTINATION
DESTROY
DESTROYER
DESTRUCTION
DESTRUCTION'S
DESTRUCTION.
DESTRUCTION'S
DETAIL
DETAILS.
DETECT
@@ -879,8 +887,8 @@ DIE
DIED
DIES,
DIFFERENCE
DIFFERENCE.
DIFFERENCE:
DIFFERENCE.
DIFFERENT
DIFFICULTY
DIFFICULTY:
@@ -959,6 +967,7 @@ DRULAN'S
DRUS
DRY
DUE
DUL'MEPHISTOS
DUN
DUNE
DUNES
@@ -997,6 +1006,7 @@ EARS
EARTH
EARTHSHAKER
EASE
EASTERN
EATER
EATS
EBURINE
@@ -1412,8 +1422,8 @@ GOBLIN
GODLY
GOES
GOLD
GOLD.
GOLD:
GOLD.
GOLDEN
GOLDSKIN
GOLDSTRIKE
@@ -1460,6 +1470,7 @@ GRIFFON
GRIFFON'S
GRIM
GRIM'S
GRIMOIRE
GRIN
GRINDER
GRINDING
@@ -1782,8 +1793,8 @@ ITCHIES
ITEM
ITEM.
ITEMS
ITEMS.
ITEMS:
ITEMS.
ITH
ITHERA
ITONYA
@@ -1957,9 +1968,9 @@ LEORIC
LESSON
LESTRON'S
LEVEL
LEVEL)
LEVEL-UP
LEVEL:
LEVEL)
LEVELS
LEVER
LEVIATHAN
@@ -1971,8 +1982,8 @@ LIDLESS
LIEF
LIENE
LIFE
LIFE/MANA
LIFE:
LIFE/MANA
LIFECHOKE
LIGHT
LIGHTBRAND
@@ -2136,12 +2147,13 @@ MAULER
MAUSOLEUM
MAW
MAX
MAX.
MAX:
MAX.
MAXIMUM
MAY
MCAULEY'S
MEASURE
MEASURED
MEAT
MEATSCRAPE
MECHANIC'S
@@ -2261,8 +2273,8 @@ NARPHET
NATALYA
NATALYA'S
NATURAL
NATURE'S
NATURE,
NATURE'S
NEAR
NEARBY
NEBUCHADNEZZAR'S
@@ -2305,6 +2317,7 @@ NONE
NOOSE
NORD'S
NORMAL
NORTHERN
NOSFERATU'S
NOT
NOVA
@@ -2323,6 +2336,7 @@ OBLIVION
OBSCENE
OBSESSION
OBSIDIAN
OCCULT
OCEAN
OCHER
OCULUS
@@ -2349,6 +2363,7 @@ ONES
ONLY
ONLY)
OOZE
OPALVEIN
OPEN
OPENS
OPTIMAL
@@ -2471,8 +2486,8 @@ PLAYED
PLAYER
PLAYER.
PLAYERS
PLAYERS.
PLAYERS:
PLAYERS.
PLEDGE
PLUCKEYE
PLUS
@@ -2617,8 +2632,8 @@ RATMEN
RATTLE
RATTLECAGE
RAVEN
RAVEN'S
RAVEN:
RAVEN'S
RAVENLORE
RAVENS
RAVENS:
@@ -2692,9 +2707,9 @@ REND
RENDER
REPAIR
REPAIRED
REPAIRED.
REPAIRED)
REPAIRED),
REPAIRED.
REPAIRING
REPAIRS
REPEATING
@@ -2804,6 +2819,7 @@ RUN/WALK
RUNE
RUNES
RUNIC
RUPTURE
RUSSET
RUST
RUSTHANDLE
@@ -2945,8 +2961,8 @@ SHELL
SHELTER
SHENK
SHIELD
SHIELD'S
SHIELD:
SHIELD'S
SHIELDS
SHIELDS:
SHIFT
@@ -3001,6 +3017,7 @@ SISTER'S
SIX
SIZE
SIZE:
SLING
SKELETAL
SKELETON
SKELETONS
@@ -3093,6 +3110,7 @@ SOULSTONE
SOUND
SOUNDING
SOURCE
SOUTHERN
SPACE
SPACING
SPANISH
@@ -3173,8 +3191,8 @@ STARLIGHT
STARS
STARS:
STASH
STASH.
STASH:
STASH.
STAT
STAT/SKILL
STATE
@@ -3303,8 +3321,8 @@ TAINTBREEDER
TAINTED
TAKE
TAKEN
TAKEN.
TAKEN:
TAKEN.
TAKES
TAL
TALBERD'S
@@ -3322,8 +3340,8 @@ TANNR
TAP
TARGE
TARGET
TARGET'S
TARGET:
TARGET'S
TARGETS
TARNHELM
TAUNT
@@ -3353,6 +3371,7 @@ TERRA'S
TERRENE
TERROR
TERROR'S
TEXT
THADAR
THAN
THANK
@@ -3431,6 +3450,7 @@ TOOTH
TOOTHROW
TOP
TOPAZ
TOR'BAALOS
TORC
TORCH
TORKEL
@@ -3645,6 +3665,7 @@ VOID
VOIDBRINGER
VOLCANIC
VOLCANO
VOLUME
VOMIR
VORTEX
VOULGE
@@ -3653,6 +3674,7 @@ VS.
VULPINE
VULTURE
WACKER
WAGER
WAHEED
WAIL
WAILING
@@ -3682,6 +3704,7 @@ WARDEN'S
WARDER
WARDING
WARHOUND
WARLOCK
WARLORD'S
WARMING
WARMTH
@@ -3691,8 +3714,8 @@ WARPED
WARPS
WARPSPEAR
WARRIOR
WARRIOR'S
WARRIOR.
WARRIOR'S
WARRIV
WARRIV'S
WARSHRIKE
@@ -3715,8 +3738,8 @@ WEAPON
WEAPON:
WEAPONS
WEAPONS,
WEAPONS.
WEAPONS:
WEAPONS.
WEAVER
WEB
WEDDING
@@ -3727,6 +3750,7 @@ WENDY
WERE
WEREBEAR
WEREWOLF
WESTERN
WHALE
WHAT
WHEEL
@@ -3831,10 +3855,10 @@ YETI
YEW
YOLK
YOU
YOU'VE
YOU,
YOU.
YOU:
YOU.
YOU'VE
YOUNG
YOUR
YOUTH
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
3LONE*dkbcy^v0I8
+10 -3
View File
@@ -76,7 +76,7 @@ if __name__ == "__main__":
if args.use_key:
key = Fernet.generate_key().decode("utf-8")
key_cmd = " --key " + key
installer_cmd = f"pyinstaller --onefile --distpath {botty_dir}{key_cmd} --exclude-module graphviz --paths .\\src --paths {args.conda_path}\\envs\\botty\\Lib\\site-packages src\\{exe}"
installer_cmd = f"pyinstaller --onefile --noconsole --distpath {botty_dir}{key_cmd} --exclude-module graphviz --exclude-module keyboard --exclude-module mouse --exclude-module pyclick --exclude-module mouseinfo --paths .\\src --paths {args.conda_path}\\envs\\botty\\Lib\\site-packages src\\{exe}"
os.system(installer_cmd)
os.system(f"cd {botty_dir} && mkdir config && cd ..")
@@ -86,8 +86,8 @@ if __name__ == "__main__":
shutil.copy("config/game.ini", f"{botty_dir}/config/")
shutil.copy("config/params.ini", f"{botty_dir}/config/")
shutil.copy("config/shop.ini", f"{botty_dir}/config/")
shutil.copy("config/default.nip", f"{botty_dir}/config/")
os.makedirs(f"{botty_dir}/config/nip", exist_ok=True)
shutil.copy("config/default.bnip", f"{botty_dir}/config/")
os.makedirs(f"{botty_dir}/config/bnip", exist_ok=True)
shutil.copy("README.md", f"{botty_dir}/")
shutil.copytree("assets", f"{botty_dir}/assets")
clean_up()
@@ -97,6 +97,13 @@ if __name__ == "__main__":
new_name = ''.join(random.choices(string.ascii_letters, k=random.randint(6, 14)))
os.rename(f'{botty_dir}/main.exe', f'{botty_dir}/{new_name}.exe')
# Rename main.exe to avoid Warden flagging the obvious name
# In CI/production builds (env BOTTY_NO_RENAME=1) keep main.exe as-is
if not args.random_name and not os.environ.get("BOTTY_NO_RENAME"):
new_name = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
os.rename(f'{botty_dir}/main.exe', f'{botty_dir}/{new_name}.exe')
print(f"Renamed main.exe -> {new_name}.exe")
if new_version_code is not None:
os.system(f'git add .')
os.system(f'git commit -m "Bump version to v{args.version}"')
+285 -218
View File
@@ -48,30 +48,40 @@
[Name] == Burningessenceofterror
[Name] == Festeringessenceofdestruction
//-----------------------------------------
//================= Shards ================
//-----------------------------------------
[Name] == Northernworldstoneshard
[Name] == Easternworldstoneshard
[Name] == Southernworldstoneshard
[Name] == Westernworldstoneshard
[Name] == Deepworldstoneshard
//----------------------------------------
//================= Runes ================
//----------------------------------------
[Name] >= Lumrune && [Name] <= Zodrune
//[Name] == Elrune
//[Name] == Eldrune
//[Name] == Tirrune
//[Name] == Nefrune
//[Name] == Ethrune
//[Name] == Ithrune
//[Name] == Talrune
//[Name] == Ralrune
//[Name] == Ortrune
//[Name] == Thulrune
//[Name] == Amnrune
//[Name] == Solrune
//[Name] == Shaelrune
//[Name] == Dolrune
//[Name] == Helrune
//[Name] == Iorune
//[Name] == Lumrune
//[Name] == Korune
//[Name] == Falrune
[Name] >= Lemrune && [Name] <= Zodrune
[Name] == Elrune
[Name] == Eldrune
[Name] == Tirrune
[Name] == Nefrune
[Name] == Ethrune
[Name] == Ithrune
[Name] == Talrune
[Name] == Ralrune
[Name] == Ortrune
[Name] == Thulrune
[Name] == Amnrune
[Name] == Solrune
[Name] == Shaelrune
[Name] == Dolrune
[Name] == Helrune
[Name] == Iorune
[Name] == Lumrune
[Name] == Korune
[Name] == Falrune
//[Name] == Lemrune
//[Name] == Pulrune
//[Name] == Umrune
@@ -91,34 +101,34 @@
//================= Gems ================
//---------------------------------------
//[Name] == Chippedamethyst
//[Name] == Chippeddiamond
//[Name] == Chippedemerald
//[Name] == Chippedruby
//[Name] == Chippedsapphire
//[Name] == Chippedskull
//[Name] == Chippedtopaz
//[Name] == Flawedamethyst
//[Name] == Flaweddiamond
//[Name] == Flawedemerald
//[Name] == Flawedruby
//[Name] == Flawedsapphire
//[Name] == Flawedskull
//[Name] == Flawedtopaz
//[Name] == Amethyst
//[Name] == Diamond
//[Name] == Emerald
//[Name] == Ruby
//[Name] == Skull
//[Name] == Sapphire
//[Name] == Topaz
//[Name] == Flawlessamethyst
//[Name] == Flawlessdiamond
//[Name] == Flawlessemerald
//[Name] == Flawlessruby
//[Name] == Flawlesssapphire
//[Name] == Flawlessskull
//[Name] == Flawlesstopaz
[Name] == Chippedamethyst
[Name] == Chippeddiamond
[Name] == Chippedemerald
[Name] == Chippedruby
[Name] == Chippedsapphire
[Name] == Chippedskull
[Name] == Chippedtopaz
[Name] == Flawedamethyst
[Name] == Flaweddiamond
[Name] == Flawedemerald
[Name] == Flawedruby
[Name] == Flawedsapphire
[Name] == Flawedskull
[Name] == Flawedtopaz
[Name] == Amethyst
[Name] == Diamond
[Name] == Emerald
[Name] == Ruby
[Name] == Skull
[Name] == Sapphire
[Name] == Topaz
[Name] == Flawlessamethyst
[Name] == Flawlessdiamond
[Name] == Flawlessemerald
[Name] == Flawlessruby
[Name] == Flawlesssapphire
[Name] == Flawlessskull
[Name] == Flawlesstopaz
//[Name] == Perfectamethyst
//[Name] == Perfectdiamond
//[Name] == Perfectemerald
@@ -135,9 +145,9 @@
//================= Grand Charms ================
//-----------------------------------------------
[Name] == Grandcharm && [Quality] == Magic # [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 60 // Plain +15 All Res
[Name] == Grandcharm && [Quality] == Magic # [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 40 // Plain +10 All Res
[Name] == Grandcharm && [Quality] == Magic # [Itemaddskilltab] >= 1 // All Plain Skillers
//[Name] == Grandcharm && [Quality] == Magic # ([Coldresist] >= 30 || [Fireresist] >= 30 || [Lightresist] >= 30 || [Poisonresist] >= 30) // All +30 Single Res Gc
//[Name] == Grandcharm && [Quality] == Magic # ([Coldresist] >= 20 || [Fireresist] >= 25 || [Lightresist] >= 25 || [Poisonresist] >= 30) // All +25 Single Res Gc
//[Name] == Grandcharm && [Quality] == Magic && [Flag] != Ethereal # [Maxhp] >= 40 // All +40 Max Hp Gc
//[Name] == Grandcharm && [Quality] == Magic # [Fhr] >= 12 && [Itemaddskilltab] >= 1 // All Plain Fhr Skillers
//[Name] == Grandcharm && [Quality] == Magic # [Frw] >= 7 && [Itemaddskilltab] >= 1 // All Plain Frw Skillers
@@ -251,13 +261,15 @@
//================= Small Charms ================
//-----------------------------------------------
[Name] == Smallcharm && [Quality] == Magic # [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 20 // +5 All Res
[Name] == Smallcharm && [Quality] == Magic # [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 12 // +3 All Res
[Name] == Largecharm && [Quality] == Magic # [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 24 // +6 All Res
[Name] == Smallcharm && [Quality] == Magic # [Maxhp] >= 15 // Plain +Hp
[Name] == Smallcharm && [Quality] == Magic # [Itemmagicbonus] >= 7 // Plain +Magic Find
[Name] == Smallcharm && [Quality] == Magic # [Coldresist] >= 11 // Plain +Cold Res
[Name] == Smallcharm && [Quality] == Magic # [Fireresist] >= 11 // Plain +Fire Res
[Name] == Smallcharm && [Quality] == Magic # [Lightresist] >= 11 // Plain +Light Res
[Name] == Smallcharm && [Quality] == Magic # [Poisonresist] >= 11 // Plain +Poison Res
[Name] == Smallcharm && [Quality] == Magic # [Maxmana] >= 15 // Plain +Mana
[Name] == Smallcharm && [Quality] == Magic # [Itemmagicbonus] >= 5 // Plain +Magic Find
[Name] == Smallcharm && [Quality] == Magic # [Coldresist] >= 8 // Plain +Cold Res
[Name] == Smallcharm && [Quality] == Magic # [Fireresist] >= 8 // Plain +Fire Res
[Name] == Smallcharm && [Quality] == Magic # [Lightresist] >= 8 // Plain +Light Res
[Name] == Smallcharm && [Quality] == Magic # [Poisonresist] >= 10 // Plain +Poison Res
[Name] == Smallcharm && [Quality] == Magic # [Poisonmindam] >= 100 // Plain +100 Poison Dmg
// Melee Small Charms ] >>>>>>>>>
@@ -291,20 +303,20 @@
//[Name] == Smallcharm && [Quality] == Magic # [Itemmagicbonus] >= 7 && [Maxdamage] >= 3 && [Tohit] >= 20
// <<<<<<<<< [ +Frw Small Charms ] >>>>>>>>>
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Fireresist] >= 9
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Coldresist] >= 9
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Lightresist] >= 9
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Poisonresist] >= 9
//[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Fireresist] >= 9
//[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Coldresist] >= 9
//[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Lightresist] >= 9
//[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Poisonresist] >= 9
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 15
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Tohit] >= 20 && [Maxdamage] >= 3
[Name] == Smallcharm && [Quality] == Magic # [Frw] >= 3 && [Tohit] >= 36
// <<<<<<<<< [ +Fhr Small Charms ] >>>>>>>>>
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Fireresist] >= 11
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Coldresist] >= 11
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Lightresist] >= 11
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Poisonresist] >= 11
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 20
//[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Fireresist] >= 11
//[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Coldresist] >= 11
//[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Lightresist] >= 11
//[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Poisonresist] >= 11
//[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Coldresist]+[Fireresist]+[Lightresist]+[Poisonresist] >= 20
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Tohit] >= 20 && [Maxdamage] >= 3
[Name] == Smallcharm && [Quality] == Magic # [Fhr] >= 5 && [Tohit] >= 36
@@ -327,6 +339,8 @@
// <<<<<<<<< [ Magic Amulets ] >>>>>>>>>
//[Name] == Amulet && [Quality] == Magic # [Itemchargedskill] == 54 // Tele Amulet
//[Type] == Amulet && [Quality] == Magic # [Itemaddskilltab] >= 3 && ([Fcr] >= 10 || [Itemmagicbonus] >= 35) // +3 Skill Fcr/Mf Amulet
[Name] == Amulet && [Quality] == Magic # [Itemaddskilltab] >= 3 //+3 Amulet
[Name] == Amulet && [Quality] == Magic # [Fireresist]+[Coldresist]+[Lightresist]+[Poisonresist] >= 84 //Chromatic amulet
//-------------------------------------------
//================= Circlets ================
@@ -368,7 +382,7 @@
//([Name] == Crownshield || [Name] == Royalshield || [Name] == Sacredtarge || [Name] == Sacredrondache) && [Quality] == Magic # [Sockets] == 4 && [Itemfasterblockrate] == 30 // Paladin Jmoe
//([Name] == Crownshield || [Name] == Royalshield || [Name] == Sacredtarge || [Name] == Sacredrondache) && [Quality] == Magic # [Sockets] == 4 && [Maxhp] >= 60 // Paladin Jmoc
//([Name] == Crownshield || [Name] == Royalshield || [Name] == Sacredtarge || [Name] == Sacredrondache) && [Quality] == Magic # [Sockets] == 4 && [Itemreqpercent] == -30 // Paladin Jmos
//[Type] == Voodooheads && [Quality] == Magic && [Class] >= Exceptional && [Flag] != Ethereal # ([Necromancerskills] >= 1 || [Poisonandboneskilltab] >= 1) && [Fbr] >= 30 && [Skillbonespear] == 1
//[Type] == Voodooheads && [Quality] == Magic && [Class] >= Exceptional && [Flag] != Ethereal # ([Necromancerskills] >= 1 || [Poisonandboneskilltab] >= 1) && [Fbr] >= 30 && [Skillbonespear] >= 1
//------------------------------------------
//================= Weapons ================
@@ -378,7 +392,7 @@
//[Name] == Throwingspear && [Quality] == Magic # [Warcriesskilltab] >= 3
// <<<<<<<<< [ Gg Club ] >>>>>>>>>
([Name] == Club || [Name] == Spikedclub) && [Quality] == Magic # [Warcriesskilltab] >= 3 && [Strength] > 0
//([Name] == Club || [Name] == Spikedclub) && [Quality] == Magic # [Warcriesskilltab] >= 3 && [Strength] > 0
// <<<<<<<<< [ Magic Scepters ] >>>>>>>>>
//[Type] == Scepter && [Quality] == Magic # [Paladinskills]+[Skillblessedhammer] >= 5 && ([Skillconcentration] >= 3 || [Fcr] >= 10 && [Skillconcentration] >= 2)
@@ -396,18 +410,18 @@
//[Type] == Orb && [Quality] == Magic # [Fireskilltab]+[Skillhydra] >= 6 && [Skillfiremastery] >= 2 // Hydra Orb
// <<<<<<<<< [ Magic Javelins ] >>>>>>>>>
([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Amazonskills] == 2 && [Javelinandspearskilltab] >= 2 && [Ias] >= 30
([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Javelinandspearskilltab] >= 4 && [Ias] >= 30
([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Amazonskills] >= 2 && [Javelinandspearskilltab] >= 2 && [Ias] >= 30
([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Javelinandspearskilltab] >= 4 && [Ias] >= 30
//([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Amazonskills] == 2 && [Javelinandspearskilltab] >= 2 && [Ias] >= 30
//([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Javelinandspearskilltab] >= 4 && [Ias] >= 30
//([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Amazonskills] >= 2 && [Javelinandspearskilltab] >= 2 && [Ias] >= 30
//([Name] == Maidenjavelin || [Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin) && [Quality] == Magic # [Javelinandspearskilltab] >= 4 && [Ias] >= 30
// <<<<<<<<< [ Magic Claws ] >>>>>>>>>
//([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skilllightningsentry] >= 3
//([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skillwakeoffire] >= 3
([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skilllightningsentry]+[Assassinskills] >= 5
([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skilllightningsentry]+[Trapsskilltab] >= 6
([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skillwakeoffire]+[Assassinskills] >= 5
([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skillwakeoffire]+[Trapsskilltab] >= 6
//([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skilllightningsentry]+[Assassinskills] >= 5
//([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skilllightningsentry]+[Trapsskilltab] >= 6
//([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skillwakeoffire]+[Assassinskills] >= 5
//([Name] == Greatertalons || [Name] == Feralclaws || [Name] == Greaterclaws || [Name] == Runictalons) && [Quality] == Magic && [Flag] != Ethereal # [Ias] >= 40 && [Skillwakeoffire]+[Trapsskilltab] >= 6
//------------------------------------------
//================= Armors ================
@@ -458,7 +472,8 @@
[Type] == Amulet && [Quality] == Rare # [Itemaddskilltab] >= 2 && [Fcr] == 10 && [Strength] >= 6 && [Dexterity] >= 6 && ([Maxhp] >= 25 || [Maxmana] >= 60 || [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 20 || [Itemdamagetomana] >= 12)
// <<<<<<<<< [ Rare Rings ] >>>>>>>>>
[Type] == Ring && [Quality] == Rare # [Fcr] >= 10 && [Strength] >= 15 && [Dexterity] >= 10 && ([Maxmana] >= 65 || [Hpregen] >= 5 || [Maxhp] >= 20 || [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 30) // Caster Ring
[Type] == Ring && [Quality] == Magic # [Maxmana] >= 91 || [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 48 //Scintillating and Bahamut Magic Rings
[Type] == Ring && [Quality] == Rare # [Fcr] >= 10 && ([Strength] >= 15 || [Maxmana] >= 50 || [Maxhp] >= 20 || [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 25) // Caster Ring
[Type] == Ring && [Quality] == Rare # [Tohit] >= 100 && [Lifeleech]+[Manaleech] >= 9 // Melee Dual Leech Ring
[Type] == Ring && [Quality] == Rare # [Tohit] >= 100 && [Strength] >= 15 && [Dexterity] >= 10 && ([Maxhp] >= 30 || [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 30) // Melee Dual Stat Ring
[Type] == Ring && [Quality] == Rare # [Tohit] >= 100 && ([Mindamage] > 7 || [Maxdamage] > 9) && ([Strength]+[Dexterity] >= 10 || [Maxhp] >= 30 || [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 20) // Bva Ring
@@ -543,29 +558,29 @@
//------------------------------------------
// <<<<<<<<< [ Rare Claws ] +++++++++
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 20 && [Sockets] >= 0
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 40 && [Sockets] >= 0
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 30 && [Sockets] >= 0
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 30 && [Sockets] >= 0
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare && [Flag] == Ethereal # [Enhanceddamage] >= 300 && [Itemskillonattack] == 66 && [Ias] >= 20 && [Sockets] >= 0
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare && [Flag] == Ethereal # [Ias] >= 20 && ([Enhanceddamage] >= 400 || ([Enhanceddamage] >= 275 && [Itemtohitperlevel] >= 1)) && [Sockets] >= 0
(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare && [Flag] == Ethereal # [Ias] >= 40 && ([Enhanceddamage] >= 350 || ([Enhanceddamage] >= 250 && [Itemtohitperlevel] >= 1)) && [Sockets] >= 0
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 20 && [Sockets] >= 0
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 40 && [Sockets] >= 0
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 30 && [Sockets] >= 0
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare # ([Assassinskills]+[Skilllightningsentry] >= 5 || [Assassinskills]+[Skillwakeoffire] >= 5 || [Trapsskilltab]+[Skilllightningsentry] >= 5 || [Trapsskilltab]+[Skillwakeoffire] >= 5) && [Ias] >= 30 && [Sockets] >= 0
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare && [Flag] == Ethereal # [Enhanceddamage] >= 300 && [Itemskillonattack] == 66 && [Ias] >= 20 && [Sockets] >= 0
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare && [Flag] == Ethereal # [Ias] >= 20 && ([Enhanceddamage] >= 400 || ([Enhanceddamage] >= 275 && [Itemtohitperlevel] >= 1)) && [Sockets] >= 0
//(([Type] == Assassinclaw && [Class] == Elite) || [Name] == Greatertalons || [Name] == Greaterclaws) && [Quality] == Rare && [Flag] == Ethereal # [Ias] >= 40 && ([Enhanceddamage] >= 350 || ([Enhanceddamage] >= 250 && [Itemtohitperlevel] >= 1)) && [Sockets] >= 0
// <<<<<<<<< [ Rare Sorceress Orbs ] >>>>>>>>>
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillfireball] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillmeteor] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillblizzard] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillfrozenorb] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skilllightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillchainlightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillnova] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Fireskilltab] >= 2 && [Skillfireball] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Fireskilltab] >= 2 && [Skillmeteor] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Coldskilltab] >= 2 && [Skillblizzard] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Coldskilltab] >= 2 && [Skillfrozenorb] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Lightningskilltab] >= 2 && [Skilllightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Lightningskilltab] >= 2 && [Skillchainlightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
[Type] == Orb && [Quality] == Rare # [Lightningskilltab] >= 2 && [Skillnova] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillfireball] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillmeteor] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillblizzard] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillfrozenorb] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skilllightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillchainlightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Sorceressskills] >= 2 && [Skillnova] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Fireskilltab] >= 2 && [Skillfireball] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Fireskilltab] >= 2 && [Skillmeteor] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Coldskilltab] >= 2 && [Skillblizzard] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Coldskilltab] >= 2 && [Skillfrozenorb] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Lightningskilltab] >= 2 && [Skilllightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Lightningskilltab] >= 2 && [Skillchainlightning] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
//[Type] == Orb && [Quality] == Rare # [Lightningskilltab] >= 2 && [Skillnova] >= 3 && [Fcr] >= 20 && ([Maxmana] >= 50 || [Energy] >= 12 && [Maxmana] >= 35) && [Sockets] >= 0
// <<<<<<<<< [ Rare Enchant Orbs ] >>>>>>>>>
//[Type] == Orb && [Quality] <= Rare # [Skillenchant] >= 2 && [Fireskilltab] == 3
@@ -573,15 +588,14 @@
//[Type] == Orb && [Quality] <= Rare # [Skillenchant] == 3 && [Sorceressskills] == 2
// <<<<<<<<< [ Rare Javelins ] >>>>>>>>>
([Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin || [Name] == Maidenjavelin) && [Quality] == Rare # [Javelinandspearskilltab] >= 2 && [Amazonskills] >= 2 && [Ias] >= 30
([Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin || [Name] == Maidenjavelin) && [Quality] == Rare # [Javelinandspearskilltab] >= 4 && [Ias] >= 30
//([Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin || [Name] == Maidenjavelin) && [Quality] == Rare # [Javelinandspearskilltab] >= 2 && [Amazonskills] >= 2 && [Ias] >= 30
//([Name] == Ceremonialjavelin || [Name] == Matriarchaljavelin || [Name] == Maidenjavelin) && [Quality] == Rare # [Javelinandspearskilltab] >= 4 && [Ias] >= 30
// <<<<<<<<< [ Rare Bows ] >>>>>>>>>
//[Type] == Bow && [Quality] == Rare # [Itemskillonattack] == 66 && ([Enhanceddamage] >= 400 || ([Enhanceddamage] >= 300 && [Itemtohitperlevel] >= 1))
//[Type] == Bow && [Quality] == Rare # ([Enhanceddamage] >= 400 || ([Enhanceddamage] >= 250 && [Itemskillonattack] == 66) || ([Enhanceddamage] >= 300 && [Itemtohitperlevel] >= 1)) && [Sockets] >= 0
//[Type] == Bow && [Quality] == Rare # [Ias] >= 20 && ([Enhanceddamage] >= 300 || ([Enhanceddamage] >= 225 && [Itemskillonattack] == 66) || ([Enhanceddamage] >= 250 && [Itemtohitperlevel] >= 1)) && [Sockets] >= 0
//[Type] == Bow && [Quality] == Rare && [Flag] != Ethereal # [Enhanceddamage] >= 250 && [218] >= 4 && [224] >= 33
//----------------------------------------
//================= Belts ================
@@ -595,9 +609,10 @@
//================= Boots ================
//----------------------------------------
[Type] == Boots && [Quality] == Rare && [Flag] != Ethereal # [Frw] >= 30 && [Fhr] >= 10 && [Dexterity] >= 8 && ([Lightresist]+[Fireresist] >= 35 || [Coldresist]+[Lightresist] >= 35 || [Fireresist]+[Coldresist] >= 35) // Dual Res
[Type] == Boots && [Quality] == Rare && [Flag] != Ethereal # [Frw] >= 30 && [Fhr] >= 10 && [Dexterity] >= 8 && [Coldresist] >= 20 && [Fireresist] >= 20 && [Lightresist] >= 20 // Tri Res
[Type] == Boots && [Quality] == Rare && [Flag] == Ethereal # [Frw] >= 20 && [Fhr] >= 10 && [Dexterity] >= 8 && [Fireresist]+[Lightresist]+[Coldresist] >= 60 && [Itemreplenishdurability] >= 1 // Eth Rep
[Type] == Boots && [Quality] == Rare && [Flag] != Ethereal # [Frw] >= 30 && [Lightresist] >= 37 // Light Res boots
[Type] == Boots && [Quality] == Rare && [Flag] != Ethereal # [Frw] >= 30 && ([Lightresist]+[Fireresist] >= 50 || [Coldresist]+[Lightresist] >= 50 || [Fireresist]+[Coldresist] >= 50) // Dual Res
[Type] == Boots && [Quality] == Rare && [Flag] != Ethereal # [Frw] >= 30 && [Fireresist]+[Lightresist]+[Coldresist]+[Poisonresist] >= 50 // Tri Res
[Type] == Boots && [Quality] == Rare && [Flag] == Ethereal # [Frw] >= 20 && [Fireresist]+[Lightresist]+[Coldresist] >= 50 && [Itemreplenishdurability] >= 1 // Eth Rep
//-----------------------------------------
//================= Gloves ================
@@ -629,6 +644,7 @@
//[Name] == Scarabhusk && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0 // Plain
//[Name] == Wyrmhide && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0 // Plain
//[Name] == Lightplate && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0 // Plain
//[Name] == Shadowplate && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0 // Plain
//[Name] == Sacredarmor && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0 // Plain
// <<<<<<<<< [ Non-Ethereal Socketed Armor ] >>>>>>>>>
@@ -640,40 +656,44 @@
//[Name] == Scarabhusk && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 3 // Socketed
//[Name] == Wyrmhide && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 3 // Socketed
//[Name] == Lightplate && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 3 // Socketed
//[Name] == Shadowplate && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
//[Name] == Sacredarmor && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
// <<<<<<<<< [ Non-Ethereal Superior Non-Socketed Armor ] >>>>>>>>>
[Name] == Archonplate && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
[Name] == Duskshroud && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
[Name] == Mageplate && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
[Name] == Greathauberk && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
[Name] == Wirefleece && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
[Name] == Scarabhusk && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
[Name] == Wyrmhide && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
//[Name] == Archonplate && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
//[Name] == Duskshroud && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
//[Name] == Mageplate && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
//[Name] == Greathauberk && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
//[Name] == Wirefleece && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
//[Name] == Scarabhusk && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
//[Name] == Wyrmhide && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
//[Name] == Lightplate && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
//[Name] == Shadowplate && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
//[Name] == Sacredarmor && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddefense] >= 10 // Plain Superior
// <<<<<<<<< [ Non-Ethereal Superior Socketed Armor ] >>>>>>>>>
[Name] == Archonplate && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
[Name] == Duskshroud && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
[Name] == Mageplate && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
[Name] == Greathauberk && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
[Name] == Wirefleece && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
[Name] == Scarabhusk && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
[Name] == Wyrmhide && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
//[Name] == Archonplate && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
//[Name] == Duskshroud && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
//[Name] == Mageplate && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
//[Name] == Greathauberk && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
//[Name] == Wirefleece && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
//[Name] == Scarabhusk && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
//[Name] == Wyrmhide && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
//[Name] == Lightplate && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 3 // Socketed Sup
//[Name] == Shadowplate && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 4 // Socketed Sup
//[Name] == Sacredarmor && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] >= 4 // Socketed Sup
// <<<<<<<<< [ Ethereal Socketed/Non-Socketed Armor ] >>>>>>>>>
[Name] == Archonplate && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
[Name] == Duskshroud && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
//[Name] == Mageplate && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) // Socketed Eth
//[Name] == Greathauberk && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
[Name] == Greathauberk && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
[Name] == Wirefleece && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
[Name] == Scarabhusk && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
[Name] == Wyrmhide && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
//[Name] == Lightplate && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
[Name] == Sacredarmor && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 4) // Socketed Eth
[Name] == Shadowplate && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
[Name] == Sacredarmor && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) // Socketed Eth
// <<<<<<<<< [ Ethereal Superior Socketed/Non-Socketed Armor ] >>>>>>>>>
[Name] == Archonplate && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
@@ -683,14 +703,19 @@
[Name] == Wirefleece && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
[Name] == Scarabhusk && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
[Name] == Wyrmhide && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
[Name] == Lightplate && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
[Name] == Sacredarmor && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) && [Enhanceddefense] >= 10 // Socketed Eth Sup
//[Name] == Lightplate && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
[Name] == Shadowplate && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
[Name] == Sacredarmor && [Quality] == Superior && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 3) && [Enhanceddefense] >= 10 // Socketed Eth Sup
//-----------------------------------------------
//================= Helmet Bases ================
//-----------------------------------------------
[Name] == Crown && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 3 //3os helm twink
[Name] == Mask && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 3 //3os helm twink
[Name] == Deathmask && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 3 //3os helm
//([Name] == Corona || [Name] == Demonhead || [Name] == Bonevisage || [Name] == Diadem) && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0
//([Name] == Corona || [Name] == Demonhead || [Name] == Bonevisage || [Name] == Diadem) && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 3 // 3os Socketed
([Name] == Corona || [Name] == Demonhead || [Name] == Bonevisage || [Name] == Diadem) && [Quality] == Normal # [Sockets] == 3 // Cure base
([Name] == Corona || [Name] == Demonhead || [Name] == Bonevisage || [Name] == Diadem) && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] == 0 // Delirium Or Dream Perfect
([Name] == Corona || [Name] == Demonhead || [Name] == Bonevisage || [Name] == Diadem) && [Quality] == Superior && [Flag] != Ethereal # [Enhanceddefense] >= 10 && [Sockets] == 3 // 3os Socketed Delirium Or Dream Perfect
@@ -727,7 +752,7 @@
// <<<<<<<<< [ White/Eth Superior/Non-Superior Socketed/Non-Socketed Shields ] >>>>>>>>>
//([Name] == Akarantarge || [Name] == Akaranrondache || [Name] == Crownshield) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Allresist] >= 40 // V/Lld Paladin Shield Base
//[Name] == Monarch && [Quality] == Normal # [Sockets] == 0 // Plain Monarch
[Name] == Monarch && [Quality] == Normal # [Sockets] == 0 // Plain Monarch
[Name] == Monarch && [Quality] == Normal # [Sockets] == 4 //4os Monarch
[Name] == Monarch && [Quality] == Superior # [Enhanceddefense] >= 10 && [Sockets] == 0 //Superior Plain Monarch
[Name] == Monarch && [Quality] == Superior # [Enhanceddefense] >= 10 && [Sockets] == 4 //4os Superior Monarch
@@ -764,8 +789,8 @@
// <<<<<<<<< [ Non-Ethereal Superior 1h Non-Socketed Weapons ] >>>>>>>>>
[Name] == Berserkeraxe && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddamage] >= 10 // Plain Superior
[Name] == Crystalsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddamage] >= 10 // Plain Superior
[Name] == Flail && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddamage] >= 10 // Plain Superior
//[Name] == Crystalsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddamage] >= 10 // Plain Superior
//[Name] == Flail && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddamage] >= 10 // Plain Superior
[Name] == Phaseblade && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && [Enhanceddamage] >= 10 // Plain Superior
[Name] == Warscepter && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Enhanceddamage] >= 0 // Plain Superior
[Name] == Divinescepter && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 0 && ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Enhanceddamage] >= 0 // Plain Superior
@@ -775,9 +800,16 @@
// <<<<<<<<< [ Non-Ethereal 1h Socketed Weapons ] >>>>>>>>>
//[Name] == Berserkeraxe && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 5 // Socketed
//[Name] == Crystalsword && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
//[Name] == Flail && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
//[Name] == Phaseblade && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 5 // Socketed
[Name] == Crystalsword && [Quality] == Normal # [Sockets] == 4 // Spirit base mid
[Name] == Broadsword && [Quality] == Normal # [Sockets] == 4 // Spirit base mid
[Name] == Crystalsword && [Quality] == Normal # [Sockets] == 5 //CTA base
[Name] == Doubleaxe && [Quality] == Normal # [Sockets] == 5 // CTA base
[Name] == Flail && [Quality] == Normal # [Sockets] == 4 // Hoto base
//[Name] == Militarypick && [Quality] == Normal # [Sockets] == 3 // Crescent Moon base
//[Name] == Militarypick && [Quality] == Normal # [Sockets] == 5 // Doom base
([Name] == Kris || [Name] == Cinquedeas || [Name] == Fangedknife) && [Quality] == Normal # [Skillabyss] >= 1 && ([Sockets] == 0 || [Sockets] == 3) //Void base
//[Name] == Phaseblade && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 5 // Socketed Greif base
//[Name] == Phaseblade && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 3 // Socketed Lawbringer base
//[Name] == Warscepter && [Quality] == Normal && [Flag] != Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Sockets] == 5 // Socketed
//[Name] == Divinescepter && [Quality] == Normal && [Flag] != Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Sockets] == 5 // Socketed
//[Name] == Caduceus && [Quality] == Normal && [Flag] != Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) // Socketed
@@ -787,25 +819,25 @@
// <<<<<<<<< [ Non-Ethereal Superior 1h Socketed Weapons ] >>>>>>>>>
[Name] == Berserkeraxe && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 5 && [Enhanceddamage] >= 10 // Socketed Superior
[Name] == Crystalsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 4 && [Enhanceddamage] >= 10 // Socketed Superiro
[Name] == Flail && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 4 && [Enhanceddamage] >= 14 // Socketed Superior
//[Name] == Crystalsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 4 && [Enhanceddamage] >= 10 // Socketed Superiro
//[Name] == Flail && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 4 && [Enhanceddamage] >= 14 // Socketed Superior
[Name] == Phaseblade && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 5 && [Enhanceddamage] >= 10 // Socketed Superior
[Name] == Warscepter && [Quality] == Superior && [Flag] != Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Sockets] == 5 && [Enhanceddamage] >= 0 // Socketed Superior
[Name] == Divinescepter && [Quality] == Superior && [Flag] != Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Sockets] == 5 && [Enhanceddamage] >= 0 // Socketed Superior
[Name] == Caduceus && [Quality] == Superior && [Flag] != Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && [Sockets] == 5 && [Enhanceddamage] >= 0 // Socketed Superior
[Name] == Crypticsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 3 // Socketed Superior
[Name] == Mythicalsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 3 // Socketed Superior
//[Name] == Crypticsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 3 // Socketed Superior
//[Name] == Mythicalsword && [Quality] == Superior && [Flag] != Ethereal # [Sockets] == 3 // Socketed Superior
// <<<<<<<<< [ Ethereal 1h Superior/Non-Superior Socketed/Non-Socketed Weapons ] >>>>>>>>>
[Name] == Berserkeraxe && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 5) // Socketed Eth
[Name] == Crystalsword && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
[Name] == Flail && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
//[Name] == Crystalsword && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
//[Name] == Flail && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
[Name] == Phaseblade && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 5) // Socketed Eth
[Name] == Warscepter && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && ([Sockets] == 0 || [Sockets] == 5) // Socketed Eth
[Name] == Divinescepter && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && ([Sockets] == 0 || [Sockets] == 5) // Socketed Eth
[Name] == Caduceus && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Skillblessedhammer] >= 3 || [Skillconviction] >= 3 || [Skillfistoftheheavens] >= 3 || [Skillfanaticism] >= 3 || [Skillholyshield] >= 3) && ([Sockets] == 0 || [Sockets] == 5) // Socketed Eth
[Name] == Crypticsword && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) // Socketed Eth
[Name] == Mythicalsword && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) // Socketed Eth
//[Name] == Crypticsword && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) // Socketed Eth
//[Name] == Mythicalsword && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 3) // Socketed Eth
// <<<<<<<<< [ Ethereal Superior 1h Socketed/Non-Socketed Weapons ] >>>>>>>>>
[Name] == Berserkeraxe && [Quality] == Superior && [Flag] == Ethereal # [Enhanceddamage] >= 10 && ([Sockets] == 0 || [Sockets] >= 5) // Socketed Eth Sup //Death, Ebotd
@@ -825,12 +857,20 @@
//[Name] == Mancatcher && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 0 // Plain
// <<<<<<<<< [ Non-Ethereal 2h Non-Superior Socketed Weapons ] >>>>>>>>>
[Name] == Warscythe && [Quality] == Normal # [Sockets] == 4 //Exceptional Insight bases mid
[Name] == Bill && [Quality] == Normal # [Sockets] == 4
[Name] == BattleScythe && [Quality] == Normal # [Sockets] == 4
[Name] == Partizan && [Quality] == Normal # [Sockets] == 4
[Name] == Becdecorbin && [Quality] == Normal # [Sockets] == 4
[Name] == Partizan && [Quality] == Normal # [Sockets] == 4
[Name] == Grimscythe && [Quality] == Normal # [Sockets] == 4
[Name] == Crypticaxe && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 4 //Elite Insight bases mid
[Name] == Greatpoleaxe && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 4
[Name] == Giantthresher && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 4
[Name] == Thresher && [Quality] == Normal && [Flag] != Ethereal # [Sockets] == 4
//[Name] == Warpike && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
//[Name] == Colossusvoulge && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
//[Name] == Thresher && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
//[Name] == Giantthresher && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
//[Name] == Crypticaxe && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
//[Name] == Greatpoleaxe && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
//[Name] == Ghostspear && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
//[Name] == Mancatcher && [Quality] == Normal && [Flag] != Ethereal # [Sockets] >= 4 // Socketed
@@ -857,13 +897,15 @@
//[Name] == Mancatcher && [Quality] == Superior && [Flag] != Ethereal # [Sockets] >= 4 && [Enhanceddamage] >= 10 // Socketed Sup
// <<<<<<<<< [ Ethereal 2h Socketed/Non-Socketed Weapons ] >>>>>>>>>
[Name] == Warpike && [Quality] == Normal && [Flag] == Ethereal # [Sockets] == 6
[Name] == Colossusvoulge && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
[Name] == Warpike && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4 ) //Echo Strike base
[Name] == Thundermaul && ([Quality] == Normal || [Quality] == Superior) && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] == 4 || [Sockets] == 6 ) //Echo Strike base
//[Name] == Scythe && [Quality] == Normal && [Flag] == Ethereal # [Sockets] == 4
[Name] == Colossusvoulge && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Eth Insight Base
[Name] == Thresher && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
[Name] == Giantthresher && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
[Name] == Crypticaxe && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
[Name] == Greatpoleaxe && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
[Name] == Ghostspear && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
//[Name] == Ghostspear && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
[Name] == Mancatcher && [Quality] == Normal && [Flag] == Ethereal # ([Sockets] == 0 || [Sockets] >= 4) // Socketed Eth
// <<<<<<<<< [ Ethereal Superior 2h Socketed/Non-Socketed Weapons ] >>>>>>>>>
@@ -881,38 +923,45 @@
//------------------------------------------------------
// <<<<<<<<< [ Non-Ethereal Superior/Non-Superior Assassin Claws ] >>>>>>>>>
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # ([Skilllightningsentry] >= 3 || [Skilldeathsentry] >= 3) && [Sockets] == 0 // Chaos
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # ([Skilllightningsentry] >= 3 || [Skilldeathsentry] >= 3) && [Sockets] == 3 // Chaos Socketed
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # ([Skillweaponblock] >= 3 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 0 // Chaos
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # ([Skillweaponblock] >= 3 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 3 // Chaos Socketed
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Skillbladeshield] >= 3 && ([Skillweaponblock] >= 3 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillvenom] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 0 // Chaos
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Skillbladeshield] >= 3 && ([Skillweaponblock] >= 3 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillvenom] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 3 // Chaos Socketed
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Skillweaponblock] >= 3 && ([Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillvenom] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 0 // Chaos
[Type] == Assassinclaw && [Class] == Elite && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Skillweaponblock] >= 3 && ([Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillvenom] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 3 // Chaos Socketed
//([Name] == Suwayyah || [Name] == FeralClaws || [Name] == RunicTalons || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [SkillLightningsentry] >= 3 && [Skillfade] >= 3 && ([Skillbladeshield] >= 3 || [Skillweaponblock] >= 3 || [Skillclawmastery] >= 2 || [Skillvenom] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 0 // Chaos
//([Name] == Suwayyah || [Name] == FeralClaws || [Name] == RunicTalons || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [SkillLightningsentry] >= 3 && [Skillfade] >= 3 && ([Skillbladeshield] >= 3 || [Skillweaponblock] >= 3 || [Skillclawmastery] >= 2 || [Skillvenom] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 3 // Chaos Socketed
//([Name] == Suwayyah || [Name] == FeralClaws || [Name] == RunicTalons || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [SkillLightningsentry] >= 3 && [Skillmindblast] >= 3 && ([Skillweaponblock] >= 2 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillvenom] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2) && [Sockets] == 0 // Chaos
//([Name] == Suwayyah || [Name] == FeralClaws || [Name] == RunicTalons || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [SkillLightningsentry] >= 3 && [Skillmindblast] >= 3 && ([Skillweaponblock] >= 2 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillvenom] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2) && [Sockets] == 3 // Chaos Socketed
//([Name] == Suwayyah || [Name] == FeralClaws || [Name] == RunicTalons || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [SkillLightningsentry] >= 3 && [Skillvenom] >= 3 && ([Skillweaponblock] >= 2 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 0 // Chaos
//([Name] == Suwayyah || [Name] == FeralClaws || [Name] == RunicTalons || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [SkillLightningsentry] >= 3 && [Skillvenom] >= 3 && ([Skillweaponblock] >= 2 || [Skillclawmastery] >= 2 || [Skillfade] >= 2 || [Skillbladeshield] >= 2 || [Skilldragonflight] >= 2 || [Skillmindblast] >= 1) && [Sockets] == 3 // Chaos Socketed
([Name] == ScissorsQuhab || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Skilldragonflight] >= 1 || [Skillphoenixstrike] >= 1 || [Skillfade] >= 1 || [Skillbladesofice] >= 1 || [Skillclawsofthunder] >= 1 && [Sockets] == 0 // Mosaic
([Name] == ScissorsQuhab || [Name] == GreaterTalons || [Name] == GreaterClaws) && ([Quality] == Normal || [Quality] == Superior) && [Flag] != Ethereal # [Skilldragonflight] >= 1 || [Skillphoenixstrike] >= 1 || [Skillfade] >= 1 || [Skillbladesofice] >= 1 || [Skillclawsofthunder] >= 1 && [Sockets] == 3 // Mosaic Socketed
//-----------------------------------------------
//================= Staves Bases ================
//-----------------------------------------------
// <<<<<<<<< [ Ethereal/Non-Ethereal Superior/Non-Superior Socketed/Non-Socketed Staves ] >>>>>>>>>
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenergyshield] == 3 && [Skillchillingarmor] >= 1 && [Sockets] == 0 // Memory
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenergyshield] == 3 && [Skillchillingarmor] >= 1 && [Sockets] >= 4 // Memory Socketed
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillthunderstorm] == 3 && ([Skillenergyshield] == 3 || [Skillchillingarmor] >= 1) && [Sockets] == 0 // Memory
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillthunderstorm] == 3 && ([Skillenergyshield] == 3 || [Skillchillingarmor] >= 1) && [Sockets] >= 4 // Memory Socketed
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenergyshield] == 3 && [Skillenchant] >= 1 && [Sockets] == 0 // Memory
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenergyshield] == 3 && [Skillenchant] >= 1 && [Sockets] >= 4 // Memory Socketed
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillthunderstorm] == 3 && ([Skillenergyshield] == 3 || [Skillenchant] >= 1) && [Sockets] == 0 // Memory
([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillthunderstorm] == 3 && ([Skillenergyshield] == 3 || [Skillenchant] >= 1) && [Sockets] >= 4 // Memory Socketed
([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skilllightningmastery] == 3 && ([Skilllightning] == 3 || [Skillchainlightning] == 3 || [Skillthunderstorm] == 3) && [Skillenergyshield] >= 1 && [Sockets] == 0 // Cta/Obsession
([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skilllightningmastery] == 3 && ([Skilllightning] == 3 || [Skillchainlightning] == 3 || [Skillthunderstorm] == 3) && [Skillenergyshield] >= 1 && [Sockets] >= 5 // Cta/Obsession Socketed
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenergyshield] >= 3 && ([Skillchillingarmor] >= 2 || [skillthunderstorm] >= 2) && [Sockets] == 0 // Energy Shield Memory
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenergyshield] >= 3 && ([Skillchillingarmor] >= 2 || [skillthunderstorm] >= 2) && [Sockets] >= 4 // Energy Shield Memory Socketed
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillthunderstorm] >= 3 && ([Skillenergyshield] >= 2 || [Skillchillingarmor] >= 2) && [Sockets] == 0 // Thunderstorm Memory
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillthunderstorm] >= 3 && ([Skillenergyshield] >= 2 || [Skillchillingarmor] >= 2) && [Sockets] >= 4 // Thunderstorm Memory Socketed
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillchillingarmor] >= 3 && ([Skillthunderstorm] >= 2 || [Skillenergyshield] >= 2) && [Sockets] == 0 // Chilling armor Memory
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillchillingarmor] >= 3 && ([Skillthunderstorm] >= 2 || [Skillenergyshield] >= 2) && [Sockets] >= 4 // Chilling armor Memory Socketed
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenchant] >= 3 && [Skillwarmth] == 2 && [Sockets] == 0 // Enchant Memory
//([Name] == Gnarledstaff || [Name] == Battlestaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillenchant] >= 3 && [Skillwarmth] == 2 && [Sockets] >= 4 // Enchant Memory Socketed
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillchillingarmor] >= 2 && [Skillthunderstorm] >= 2 && ([Skillenergyshield] >= 2 || [Skillenchant] >= 2) && [Sockets] == 0 // Cta
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillchillingarmor] >= 2 && [Skillthunderstorm] >= 2 && ([Skillenergyshield] >= 2 || [Skillenchant] >= 2) && [Sockets] == 5 // Cta Socketed
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillchainlightning] >= 3 && ([Skilllightningmastery] >= 2 || [Skilllightning] >= 2 || [Skillthunderstorm] >= 2) && [Skillenergyshield] >= 0 && [Sockets] == 0 // Lightning Obsession
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillchainlightning] >= 3 && ([Skilllightningmastery] >= 2 || [Skilllightning] >= 2 || [Skillthunderstorm] >= 2) && [Skillenergyshield] >= 0 && [Sockets] == 6 // Lightning Obsession socketed
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillblizzard] >= 3 && ([Skilliceblast] >= 2 || [Skillfrozenorb] >= 2 || [Skillwarmth] >= 2) && [Skillchillingarmor] >= 0 && [Sockets] >= 0 // Blizzard Obsession
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillblizzard] >= 3 && ([Skilliceblast] >= 2 || [Skillfrozenorb] >= 2 || [Skillwarmth] >= 2) && [Skillchillingarmor] >= 0 && [Sockets] >= 6 // Obsession Socketed
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillfireball] >= 3 && ([Skillmeteor] >= 2 || [Skillfirebolt] >= 2 || [Skillwarmth] >= 2) && [Skillchillingarmor] >= 0 && [Sockets] >= 0 // Fireball Obsession
//([Name] == Warstaff || [Name] == Runestaff || [Name] == Archonstaff) && ([Quality] == Normal || [Quality] == Superior) # [Skillfireball] >= 3 && ([Skillmeteor] >= 2 || [Skillfirebolt] >= 2 || [Skillwarmth] >= 2) && [Skillchillingarmor] >= 0 && [Sockets] >= 6 // Fireball Obsession socketed
//---------------------------------------------------------
//================= Necromancer Wand Bases ================
//---------------------------------------------------------
// <<<<<<<<< [ Ethereal/Non-Ethereal Superior/Non-Superior Socketed/Non-Socketed Necromancer Wands ] >>>>>>>>>
[Type] == Wand && ([Quality] == Normal || [Quality] == Superior) # [Skillbonespear] >= 3 && ([Skillbonespirit] >= 3 || [Skillteeth] >= 3 || [Skillpoisonnova] >= 3 || [Skilldecrepify] >= 3) && ([Skilldecrepify] >= 1 || [Skillironmaiden] >= 1 || [Skillteeth] >= 1 || [Skillbonearmor] >= 1 || [Skilllowerresist] >= 1) && [Sockets] == 0 // White Wand
[Type] == Wand && ([Quality] == Normal || [Quality] == Superior) # [Skillbonespear] >= 3 && ([Skillbonespirit] >= 3 || [Skillteeth] >= 3 || [Skillpoisonnova] >= 3 || [Skilldecrepify] >= 3) && ([Skilldecrepify] >= 1 || [Skillironmaiden] >= 1 || [Skillteeth] >= 1 || [Skillbonearmor] >= 1 || [Skilllowerresist] >= 1) && [Sockets] == 2 // White Wand Socketed
//[Type] == Wand && ([Quality] == Normal || [Quality] == Superior) # [Skillbonespear] >= 3 && ([Skillbonespirit] >= 3 || [Skillteeth] >= 3 || [Skillpoisonnova] >= 3 || [Skilldecrepify] >= 3) && ([Skilldecrepify] >= 1 || [Skillironmaiden] >= 1 || [Skillteeth] >= 1 || [Skillbonearmor] >= 1 || [Skilllowerresist] >= 1) && [Sockets] == 0 // White Wand
//[Type] == Wand && ([Quality] == Normal || [Quality] == Superior) # [Skillbonespear] >= 3 && ([Skillbonespirit] >= 3 || [Skillteeth] >= 3 || [Skillpoisonnova] >= 3 || [Skilldecrepify] >= 3) && ([Skilldecrepify] >= 1 || [Skillironmaiden] >= 1 || [Skillteeth] >= 1 || [Skillbonearmor] >= 1 || [Skilllowerresist] >= 1) && [Sockets] == 2 // White Wand Socketed
//---------------------------------------------------
//================= Amazon Bow Bases ================
@@ -920,15 +969,15 @@
// <<<<<<<<< [ Socketed Amazon Bows ] >>>>>>>>>
//[Type] == Bow && [Class] == Elite && [Quality] == Normal # [Sockets] == 0 // Faith
[Type] == Bow && [Class] == Elite && [Quality] == Normal # [Sockets] >= 4 // Faith/Mist Socketed
//[Type] == Bow && [Class] == Elite && [Quality] == Normal # [Sockets] >= 4 // Faith/Mist Socketed
//[Type] == Amazonbow && [Class] == Elite && [Quality] == Normal # [Bowandcrossbowskilltab] == 3 && [Sockets] == 0 // Faith
[Type] == Amazonbow && [Class] == Elite && [Quality] == Normal # [Bowandcrossbowskilltab] == 3 && [Sockets] >= 4 // Faith/Mist Socketed
//[Type] == Amazonbow && [Class] == Elite && [Quality] == Normal # [Bowandcrossbowskilltab] == 3 && [Sockets] >= 4 // Faith/Mist Socketed
// <<<<<<<<< [ Superior Socketed Amazon Bows ] >>>>>>>>>
//[Type] == Bow && [Class] == Elite && [Quality] == Normal # [Enhanceddamage] > 10 && [Sockets] == 0 // Faith
[Type] == Bow && [Class] == Elite && [Quality] == Superior # [Enhanceddamage] > 10 && [Sockets] == 4 // Faith
//[Type] == Bow && [Class] == Elite && [Quality] == Superior # [Enhanceddamage] > 10 && [Sockets] == 4 // Faith
//[Type] == Amazonbow && [Class] == Elite && [Quality] == Superior # [Bowandcrossbowskilltab] == 3 && [Enhanceddamage] > 10 && [Sockets] == 0 // Faith
[Type] == Amazonbow && [Class] == Elite && [Quality] == Superior # [Bowandcrossbowskilltab] == 3 && [Enhanceddamage] > 10 && [Sockets] == 4 // Faith
//[Type] == Amazonbow && [Class] == Elite && [Quality] == Superior # [Bowandcrossbowskilltab] == 3 && [Enhanceddamage] > 10 && [Sockets] == 4 // Faith
//=======================================================================//
//<<<<<<<<<<<<<<<<<<<<<<<<<<<< Set Items >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>//
@@ -1007,8 +1056,8 @@
//--------------------------------------------------
//[Name] == Buckler && [Quality] == Set // Hsarus' Iron Fist
//[Name] == Belt && [Quality] == Set # [Coldresist] == 20 && [Maxhp] == 20 // Hsarus' Iron Stay
//[Name] == Chainboots && [Quality] == Set // Hsarus' Iron Heel
[Name] == Belt && [Quality] == Set # [Coldresist] == 20 && [Maxhp] == 20 // Hsarus' Iron Stay twink
[Name] == Chainboots && [Quality] == Set // Hsarus' Iron Heel twink
//-------------------------------------------------
//================= Infernal Tools ================
@@ -1080,7 +1129,7 @@
//-----------------------------------------------------
//[Name] == Huntersguise && [Quality] == Set // Aldur's Stony Gaze
//[Name] == Battleboots && [Quality] == Set // Aldur's Advance
[Name] == Battleboots && [Quality] == Set // Aldur's Advance
//[Name] == Shadowplate && [Quality] == Set // Aldur's Deception
//[Name] == Jaggedstar && [Quality] == Set // Aldur's Rhythm
@@ -1104,16 +1153,16 @@
//-----------------------------------------------
//[Type] == Amulet && [Quality] == Set # [Itemallskills] == 1 && [Coldresist] == 18 // Telling Of Beads
//[Name] == Bramblemitts && [Quality] == Set // Laying Of Hands
[Name] == Bramblemitts && [Quality] == Set // Laying Of Hands
//[Name] == Duskshroud && [Quality] == Set // Dark Adherent
//[Name] == Demonhideboots && [Quality] == Set // Rite Of Passage
//[Name] == Mithrilcoil && [Quality] == Set //Credendum
[Name] == Mithrilcoil && [Quality] == Set //Credendum
//----------------------------------------------------
//================= Griswold's Legacy ================
//----------------------------------------------------
//[Name] == Ornateplate && [Quality] == Set # // Griswold's Heart
[Name] == Ornateplate && [Quality] == Set // Griswold's Heart
//[Name] == Corona && [Quality] == Set # [Itemmagicbonus] >= 20 // Griswold's Valor
//[Name] == Corona && [Quality] == Set # [Itemmagicbonus] >= 30 && [Enhanceddefense] >= 75 // Griswold's Valor Perfect
//[Name] == Caduceus && [Quality] == Set # [Sockets] >= 3 && [Enhanceddamage] >= 200 // Griswold's Redemption
@@ -1144,7 +1193,7 @@
//================= Immortal King ================
//------------------------------------------------
//[Name] == Warbelt && [Quality] == Set // Immortal King's Detail
[Name] == Warbelt && [Quality] == Set // Immortal King's Detail
//[Name] == Avengerguard && [Quality] == Set // Immortal King's Will
//[Name] == Ogremaul && [Quality] == Set // Immortal King's Stone Crusher
[Name] == Sacredarmor && [Quality] == Set // Immortal King's Soul Cage
@@ -1169,21 +1218,21 @@
//[Name] == Grimhelm && [Quality] == Set // Natalya's Totem
//[Name] == Scissorssuwayyah && [Quality] == Set // Natalya's Mark
//[Name] == Loricatedmail && [Quality] == Set # [Defense] >= 721 && [Sockets] >= 3 // Natalya's Shadow
//[Name] == Meshboots && [Quality] == Set # [Lightresist] >= 25 && [Coldresist] >= 25 // Natalya's Soul
[Name] == Meshboots && [Quality] == Set # [Lightresist] >= 15 && [Coldresist] >= 15 // Natalya's Soul
//--------------------------------------------------------
//================= Naj's Ancient Vestege ================
//--------------------------------------------------------
//[Name] == Circlet && [Quality] == Set // Naj's Circlet
//[Name] == Hellforgeplate && [Quality] == Set // Naj's Light Plate
//[Name] == Elderstaff && [Quality] == Set // Naj's Puzzler
[Name] == Circlet && [Quality] == Set // Naj's Circlet
[Name] == Hellforgeplate && [Quality] == Set // Naj's Light Plate
[Name] == Elderstaff && [Quality] == Set // Naj's Puzzler
//------------------------------------------------
//================= Orphan's Call ================
//------------------------------------------------
//[Name] == Wingedhelm && [Quality] == Set // Guillaume's Face
[Name] == Wingedhelm && [Quality] == Set // Guillaume's Face
//[Name] == Roundshield && [Quality] == Set // Whistan's Guard
//[Name] == Sharkskingloves && [Quality] == Set // Magnus' Skin
//[Name] == Battlebelt && [Quality] == Set // Wilhelm's Pride
@@ -1195,13 +1244,13 @@
//[Name] == Cap && [Quality] == Set # [Itemmagicbonus] == 35 // Sander's Paragon
//[Name] == Bonewand && [Quality] == Set //Sander's Superstition
//[Name] == Heavygloves && [Quality] == Set # [Maxhp] == 40 // Sander's Taboo
//[Name] == Heavyboots && [Quality] == Set # [Dexterity] == 10 && [Strength] == 5 // Sander's Riprap
[Name] == Heavyboots && [Quality] == Set # [Dexterity] == 10 && [Strength] == 5 // Sander's Riprap twink
//---------------------------------------------------------
//================= Sazabi's Grand Tribute ================
//---------------------------------------------------------
//[Name] == Basinet && [Quality] == Set // Sazabi's Mental Sheath
[Name] == Basinet && [Quality] == Set // Sazabi's Mental Sheath mid
//[Name] == Crypticsword && [Quality] == Set // Sazabi's Cobalt Redeemer
//[Name] == Balrogskin && [Quality] == Set // Sazabi's Ghost Liberator
@@ -1211,7 +1260,7 @@
[Name] == Swirlingcrystal && [Quality] == Set // Tal Rasha's Lidless Eye
//[Name] == Swirlingcrystal && [Quality] == Set # [Plusskillcoldmastery] == 2 && [Plusskillfiremastery] == 2 && [Plusskilllightningmastery] == 2 // Tal Rasha's Lidless Eye Perfect
//[Name] == Deathmask && [Quality] == Set // Tal Rasha's Horadric Crest
[Name] == Deathmask && [Quality] == Set // Tal Rasha's Horadric Crest
[Name] == Lacqueredplate && [Quality] == Set // Tal Rasha's Guardianship
//[Name] == Lacqueredplate && [Quality] == Set # [Defense] >= 941 // Tal Rasha's Guardianship Perfect
[Type] == Amulet && [Quality] == Set # [Sorceressskills] == 2 // Tal Rasha's Adjucation
@@ -1228,6 +1277,11 @@
//[Name] == Trollbelt && [Quality] == Set // Trang-Oul's Girth
[Name] == Heavybracers && [Quality] == Set // Trang-Oul's Claws
//----------------------------------------------------
//================= Horazon's Splendor ================
//----------------------------------------------------
[Name] == Mirroredboots && [Quality] == Set // Horazon's legacy
//==========================================================================//
//<<<<<<<<<<<<<<<<<<<<<<<<<<<< Unique Items >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>//
//==========================================================================//
@@ -1241,6 +1295,8 @@
//[Type] == Ring && [Quality] == Unique # [Hpregen] >= 5 && [Manaleech] >= 4 // Manald Heal
//[Type] == Ring && [Quality] == Unique # [Hpregen] == 8 && [Manaleech] == 7 // Manald Heal Perfect
[Type] == Ring && [Quality] == Unique # [Itemmaxmanapercent] >= 25 // Stone Of Jordan
[Type] == Ring && [Quality] == Unique # [Fcr] >= 10 && [Itemmagicbonus] >= 10 // Sling Ring
[Type] == Ring && [Quality] == Unique # [Fcr] >= 10 && [Allres] >= 6 // Opalvein Ring
[Type] == Ring && [Quality] == Unique # [Maxhp] >= 35 && [Magicdamagereduction] >= 12 // Dwarf Star
//[Type] == Ring && [Quality] == Unique # [Maxhp] >= 35 && [Magicdamagereduction] == 15 // Dwarf Star Perfect
[Type] == Ring && [Quality] == Unique # [Dexterity] >= 15 && [Tohit] >= 150 // Raven Frost
@@ -1248,7 +1304,7 @@
[Type] == Ring && [Quality] == Unique # [Lifeleech] >= 3 && [Itemallskills] == 1 // Bul-Kathos' Wedding Band
//[Type] == Ring && [Quality] == Unique # [Lifeleech] == 5 && [Itemallskills] == 1 // Bul-Kathos' Wedding Band Perfect
[Type] == Ring && [Quality] == Unique # [Poisonresist] >= 20 && [Normaldamagereduction] >= 7 // Nature's Peace
//[Type] == Ring && [Quality] == Unique # [Poisonresist] == 30 && [Itemdamagetomana] == 11 // Nature's Peace Perfect
//[Type] == Ring && [Quality] == Unique # [Poisonresist] == 30 && [Normaldamagereduction] == 11 // Nature's Peace Perfect
[Type] == Ring && [Quality] == Unique # [Itemabsorblightpercent] >= 10 && [Itemmagicbonus] >= 10 // Wisp Protector
//[Type] == Ring && [Quality] == Unique # [Itemabsorblightpercent] == 20 && [Itemmagicbonus] == 20 // Wisp Protector Perfect
[Type] == Ring && [Quality] == Unique # [Lifeleech] >= 6 && [Armorclassvsmissile] >= 100 // Carrion Wind
@@ -1259,14 +1315,15 @@
//-------------------------------------------------
//[Type] == Amulet && [Quality] == Unique # [Fireresist] >= 50 // Nokozan Relic
//[Type] == Amulet && [Quality] == Unique # [Lifeleech] >= 3 && [Armorclassvsmissile] >= 10 // The Eye Of Etlitch
[Type] == Amulet && [Quality] == Unique # [Lifeleech] >= 3 && [Armorclassvsmissile] >= 10 // The Eye Of Etlitch
//[Type] == Amulet && [Quality] == Unique # [Lifeleech] >= 7 && [Armorclassvsmissile] >= 40 // The Eye Of Etlitch Perfect
//[Type] == Amulet && [Quality] == Unique # [Allres] >= 10 && [Enhanceddefense] >= 10 // The Mahim-Oak Curio
//[Type] == Amulet && [Quality] == Unique # [Allres] >= 15 && [Strength] >= 12 // Saracen's Chance
//[Type] == Amulet && [Quality] == Unique # [Allres] == 25 && [Strength] >= 12 // Saracen's Chance Perfect
//[Type] == Amulet && [Quality] == Unique # [Dexterity] >= 25 && [Ias] >= 20 // The Cat's Eye
//[Type] == Amulet && [Quality] == Unique # [Lifeleech] >= 3 && [Manaleech] >= 11 // Crescent Moon
[Type] == Amulet && [Quality] == Unique # [Lifeleech] >= 3 && [Manaleech] >= 11 // Crescent Moon
//[Type] == Amulet && [Quality] == Unique # [Lifeleech] == 6 && [Manaleech] == 15 // Crescent Moon Perfect
[Type] == Amulet && [Quality] == Unique # [Fcr] >= 5 // Entropy Locket
[Type] == Amulet && [Quality] == Unique # [Lightresist] >= 35 // Highlord's Wrath
//[Type] == Amulet && [Quality] == Unique # [Poisonresist] >= 75 // Atma's Scarab
[Type] == Amulet && [Quality] == Unique # [Hpregen] >= 10 && [Fireskilltab] >= 2 // The Rising Sun
@@ -1281,10 +1338,10 @@
//================= Unique Jewels ================
//------------------------------------------------
[Name] == Jewel && [Quality] == Unique # [Passivecoldmastery] + [Passivecoldpierce] >= 9 // Rainbow Facet Cold
[Name] == Jewel && [Quality] == Unique # [Passivefiremastery] + [Passivefirepierce] >= 9 // Rainbow Facet Fire
[Name] == Jewel && [Quality] == Unique # [Passiveltngmastery] + [Passiveltngpierce] >= 9 // Rainbow Facet Light
[Name] == Jewel && [Quality] == Unique # [Passivepoismastery] + [Passivepoispierce] >= 9 // Rainbow Facet Poison
[Name] == Jewel && [Quality] == Unique # [Passivecoldmastery] + [Passivecoldpierce] >= 8 // Rainbow Facet Cold
[Name] == Jewel && [Quality] == Unique # [Passivefiremastery] + [Passivefirepierce] >= 8 // Rainbow Facet Fire
[Name] == Jewel && [Quality] == Unique # [Passiveltngmastery] + [Passiveltngpierce] >= 8 // Rainbow Facet Light
[Name] == Jewel && [Quality] == Unique # [Passivepoismastery] + [Passivepoispierce] >= 8 // Rainbow Facet Poison
//------------------------------------------------
//================= Unique Gloves ================
@@ -1292,11 +1349,11 @@
//[Name] == Heavygloves && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 10 // Bloodfist
//[Name] == Heavygloves && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 20 // Bloodfist Perfect
[Name] == Chaingloves && [Quality] == Unique && [Flag] != Ethereal # [Itemmagicbonus] >= 25 // Chance Guards
[Name] == Chaingloves && [Quality] == Unique && [Flag] != Ethereal # [Itemmagicbonus] >= 35 // Chance Guards
//[Name] == Chaingloves && [Quality] == Unique && [Flag] != Ethereal # [Itemmagicbonus] >= 40 && [Enhanceddefense] >= 30 // Chance Guards Perfect
[Name] == Lightgauntlets && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 20 // Magefist
//[Name] == Lightgauntlets && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 30 // Magefist Perfect
//[Name] == Gauntlets && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 10 // Frostburn
[Name] == Gauntlets && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 10 // Frostburn
//[Name] == Gauntlets && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 20 // Frostburn Perfect
[Name] == Vampirebonegloves && [Quality] == Unique && [Flag] != Ethereal # [Strength] == 10 // Dracul's Grasp
//[Name] == Vampirebonegloves && [Quality] == Unique && [Flag] != Ethereal # [Strength] == 15 && [Lifeleech] >= 10 && [Enhanceddefense] >= 120 // Dracul's Grasp Perfect
@@ -1307,11 +1364,11 @@
//================= Unique Belts ================
//-----------------------------------------------
//[Name] == Heavybelt && [Quality] == Unique && [Flag] != Ethereal # [Itemgoldbonus] >= 50 // Goldwrap
[Name] == Heavybelt && [Quality] == Unique && [Flag] != Ethereal # [Itemgoldbonus] >= 50 // Goldwrap
//[Name] == Heavybelt && [Quality] == Unique && [Flag] != Ethereal # [Itemgoldbonus] >= 80 && [Enhanceddefense] >= 60 // Goldwrap Perfect
//[Name] == Demonhidesash && [Quality] == Unique && [Flag] != Ethereal # [Lifeleech] >= 6 && [Damageresist] >= 12 && [Magicdamagereduction] >= 12 && [Enhanceddefense] >= 150 // String Of Ears
[Name] == Demonhidesash && [Quality] == Unique && [Flag] != Ethereal # [Lifeleech] >= 6 && [Damageresist] >= 14 && [Magicdamagereduction] >= 10 && [Enhanceddefense] >= 150 // String Of Ears
//[Name] == Demonhidesash && [Quality] == Unique && [Flag] != Ethereal # [Lifeleech] >= 8 && [Damageresist] >= 15 && [Magicdamagereduction] >= 15 && [Enhanceddefense] >= 180 // String Of Ears Perfect
//[Name] == Sharkskinbelt && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 120 && [Plusdefense] >= 15 // Razortail
[Name] == Sharkskinbelt && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 120 && [Plusdefense] >= 15 // Razortail
//[Name] == Sharkskinbelt && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 && [Plusdefense] >= 15 // Razortail Perfect
//[Name] == Battlebelt && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 130 // Snowclash
//[Name] == Battlebelt && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 170 // Snowclash Perfect
@@ -1321,31 +1378,32 @@
//[Name] == Spiderwebsash && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 120 // Arachnid Mesh Perfect
//[Name] == Vampirefangbelt && [Quality] == Unique && [Flag] != Ethereal # [Lifeleech] >= 5 && [Defense] >= 50 // Nosferatu's Coil
//[Name] == Vampirefangbelt && [Quality] == Unique && [Flag] != Ethereal # [Lifeleech] >= 7 && [Defense] >= 60 // Nosferatu's Coil Perfect
[Name] == Mithrilcoil && [Quality] == Unique && [Flag] != Ethereal # [Damageresist] >= 10 && [Enhanceddefense] >= 90 && [Hpregen] >= 10 // Verdungo's Hearty Cord
[Name] == Mithrilcoil && [Quality] == Unique && [Flag] != Ethereal # [Damageresist] >= 13 && [Enhanceddefense] >= 90 && [Hpregen] >= 10 // Verdungo's Hearty Cord
//[Name] == Mithrilcoil && [Quality] == Unique && [Flag] != Ethereal # [Damageresist] >= 15 && [Enhanceddefense] >= 140 && [Hpregen] >= 13 // Verdungo's Hearty Cord Perfect
[Name] == Trollbelt && [Quality] == Unique && [Flag] != Ethereal //Gheed's Wager
//-----------------------------------------------
//================= Unique Boots ================
//-----------------------------------------------
//[Name] == Boots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 10 // Hotspurs
//[Name] == Boots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 20 // Hotspurs Perfect
//[Name] == Lightplatedboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 50 // Goblin Toes
[Name] == Chainboots && [Quality] == Unique // Treads of Cthon
[Name] == Lightplatedboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 50 // Goblin Toes
//[Name] == Lightplatedboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 60 // Goblin Toes Perfect
//[Name] == Demonhideboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 120 && [Itemgoldbonus] >= 40 // Infernostride
[Name] == Demonhideboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 150 && [Itemgoldbonus] == 70 // Infernostride Perfect
//[Name] == Sharkskinboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 180 // Waterwalk
//[Name] == Sharkskinboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 210 // Waterwalk Perfect
//[Name] == Meshboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 // Silkweave
[Name] == Meshboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 // Silkweave
//[Name] == Meshboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 190 // Silkweave Perfect
[Name] == Battleboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 && [Itemgoldbonus] >= 30 // War Traveler
//[Name] == Battleboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 190 && [Itemgoldbonus] == 50 // War Traveler Perfect
//[Name] == Warboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 160 // Gore Rider
[Name] == Warboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 // Gore Rider Perfect
[Name] == Battleboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 && [Itemmagicbonus] >= 30 // War Traveler
//[Name] == Battleboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 190 && [Itemmagicbonus] == 50 // War Traveler Perfect
[Name] == Warboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 160 // Gore Rider
//[Name] == Warboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 // Gore Rider Perfect
[Name] == Scarabshellboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 140 && [Strength] >= 10 && [Vitality] >= 10 && [Poisonresist] >= 40 // Sandstorm Trek
[Name] == Scarabshellboots && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 140 && [Strength] >= 10 && [Vitality] >= 10 && [Poisonresist] >= 40 // Eth Sandstorm Trek
//[Name] == Scarabshellboots && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] == 170 && [Strength] == 15 && [Vitality] == 15 && [Poisonresist] == 70 // Eth Sandstorm Trek Perfect
//[Name] == Boneweaveboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 170 && [Strength] >= 10 && [Skillskeletonmastery] >= 1 // Marrowwalk
[Name] == Boneweaveboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 170 && [Strength] >= 10 && [Skillskeletonmastery] >= 1 // Marrowwalk
//[Name] == Boneweaveboots && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 && [Strength] == 20 && [Skillskeletonmastery] == 2 // Marrowwalk Perfect
[Name] == Myrmidongreaves && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 70 && [Shadowdisciplinesskilltab] >= 2 && [Dexterity] >= 20 // Shadow Dancer
//[Name] == Myrmidongreaves && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 100 && [Shadowdisciplinesskilltab] == 2 && [Dexterity] == 25 // Shadow Dancer Perfect
@@ -1362,8 +1420,8 @@
//[Name] == Sallet && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] == 220 && [Coldresist]+[Lightresist]+[Fireresist] == 130 // Eth Rockstopper Perfect
//[Name] == Wingedhelm && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 && [Amazonskills] >= 1 // Valkyrie Wing
//[Name] == Wingedhelm && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 && [Amazonskills] == 2 // Valkyrie Wing Perfect
[Name] == Grandcrown && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 160 && [Lifeleech] >= 9 && [Itemgoldbonus] >= 80 // Crown Of Thieves
[Name] == Grandcrown && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 160 && [Lifeleech] >= 9 && [Itemgoldbonus] >= 80 // Eth Crown Of Thieves
//[Name] == Grandcrown && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 160 && [Lifeleech] >= 9 && [Itemgoldbonus] >= 80 // Crown Of Thieves
//[Name] == Grandcrown && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 160 && [Lifeleech] >= 9 && [Itemgoldbonus] >= 80 // Eth Crown Of Thieves
//[Name] == Grandcrown && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 && [Lifeleech] == 12 && [Itemgoldbonus] == 100 // Eth Crown Of Thieves Perfect
[Name] == Grimhelm && [Quality] == Unique && [Flag] != Ethereal # [Lifeleech] >= 6 && [Manaleech] >= 6 && [Magicdamagereduction] >= 10 && [Damageresist] >= 15 // Vampire Gaze
[Name] == Grimhelm && [Quality] == Unique && [Flag] == Ethereal # [Lifeleech] >= 6 && [Manaleech] >= 6 && [Magicdamagereduction] >= 10 && [Damageresist] >= 15 // Eth Vampire Gaze
@@ -1398,9 +1456,9 @@
//[Name] == Ghostarmor && [Quality] == Unique && [Flag] != Ethereal # [Magicdamagereduction] == 11 // The Spirit Shroud Perfect
[Name] == Serpentskinarmor && [Quality] == Unique && [Flag] != Ethereal # [Magicdamagereduction] >= 9 && [Allres] >= 20 // Skin Of The Vipermagi
//[Name] == Serpentskinarmor && [Quality] == Unique && [Flag] != Ethereal # [Magicdamagereduction] == 13 && [Allres] == 35 // Skin Of The Vipermagi Perfect
//[Name] == Cuirass && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 160 // Duriel's Shell
[Name] == Cuirass && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 160 // Duriel's Shell
[Name] == Cuirass && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 160 // Eth Duriel's Shell
[Name] == Cuirass && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 // Duriel's Shell Perfect
//[Name] == Cuirass && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 200 // Duriel's Shell Perfect
//[Name] == Cuirass && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] == 200 // Eth Duriel's Shell Perfect
//[Name] == Mesharmor && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 180 // Shaftstop
[Name] == Mesharmor && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 180 // Eth Shaftstop
@@ -1418,7 +1476,9 @@
//[Name] == Templarcoat && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] == 200 // Eth Guardian Angel Perfect
[Name] == Duskshroud && [Quality] == Unique && [Flag] != Ethereal # [Passiveltngmastery] >= 15 && ([Skilllightning] >= 3 || [Skillnova] >= 3 || [Skillenergyshield] >= 3) // Light Ormus' Robes
[Name] == Duskshroud && [Quality] == Unique && [Flag] != Ethereal # [Passivecoldmastery] >= 15 && [Skillblizzard] >= 3 || [Skilliceblast] >= 3 // Cold Ormus' Robes
[Name] == Duskshroud && [Quality] == Unique && [Flag] != Ethereal # [Passivefiremastery] >= 15 && ([Skillfireball] >= 3 || [Skillfirebolt] >= 3 || [Skillhydra] >= 3 || [Skillmeteor] >= 3) // Fire Ormus' Robes
[Name] == Duskshroud && [Quality] == Unique && [Flag] != Ethereal # [Passivefiremastery] >= 15 && ([Skillfireball] >= 3 || [Skillhydra] >= 3 || [Skillmeteor] >= 3) // Fire Ormus' Robes
[Name] == Duskshroud && [Quality] == Unique # [Skillenchant] >= 3 //Enchant Ormus' Robes
//[Name] == Wirefleece && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 150 && [Normaldamagereduction] >= 15 && [Magicdamagereduction] >= 15 // The Gladiator's Bane
[Name] == Wirefleece && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 150 && [Normaldamagereduction] >= 15 && [Magicdamagereduction] >= 15 // Eth The Gladiator's Bane
[Name] == Wirefleece && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 180 && [Normaldamagereduction] == 20 && [Magicdamagereduction] == 20 // The Gladiator's Bane Perfect
@@ -1437,13 +1497,13 @@
//[Name] == Buckler && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 30 // Pelta Lunata
//[Name] == Buckler && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 40 // Pelta Lunata Perfect
///[Name] == Towershield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 80 // Bverrit Keep
//[Name] == Towershield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 80 // Bverrit Keep
//[Name] == Towershield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 120 // Bverrit Keep Perfect
//[Name] == Roundshield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 180 // Moser's Blessed Circle
//[Name] == Roundshield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 220 // Moser's Blessed Circle Perfect
[Name] == Pavise && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 180 && [Allres] >= 20 // Gerke's Sanctuary
//[Name] == Pavise && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 240 && [Allres] == 30 // Gerke's Sanctuary Perfect
//[Name] == Grimshield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 80 && [Manarecovery] >= 3 // Lidless Wall
[Name] == Grimshield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] >= 80 && [Manarecovery] >= 3 // Lidless Wall
//[Name] == Grimshield && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddefense] == 130 && [Manarecovery] == 5 // Lidless Wall Perfect
[Name] == Monarch && [Quality] == Unique && [Flag] != Ethereal # [DamageResist] >= 35 // Stormshield
//[Name] == Monarch && [Quality] == Unique && [Flag] != Ethereal # [Defense] == 148 // Stormshield Perfect
@@ -1455,7 +1515,7 @@
//-------------------------------------------------
[Name] == Boneknife && [Quality] == Unique && [Flag] != Ethereal # [Allres] == 75 // Wizardspike
//[Name] == Dagger && [Quality] == Unique && [Flag] != Ethereal # [Itemgoldbonus] == 100 // Gull
[Name] == Dagger && [Quality] == Unique && [Flag] != Ethereal # [Itemmagicbonus] == 100 // Gull
//[Name] == Blade && [Quality] == Unique && [Flag] != Ethereal # [Fcr] == 50 && [Allres] == 10 // Spectral Shard
//[Name] == Rondel && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddamage] >= 190 // Heart Carver
//[Idname] == Fleshripper // Fleshripper
@@ -1469,7 +1529,7 @@
//[Idname] == Bloodletter // Bloodletter
//[Idname] == Coldsteeleye // Coldsteel Eye
//[Idname] == Hexfire // Hexfire
//[Idname] == Bladeofalibaba && [Dexterity] >= 5 // Blade Of Ali Baba
//[Idname] == Bladeofalibaba # [Dexterity] >= 5 // Blade Of Ali Baba
[Idname] == Bladeofalibaba # [Dexterity] >= 15 // Blade Of Ali Baba Perfect Dex
//[Idname] == Ginthersrift // Ginther's Rift
//[Name] == Battlesword && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddamage] >= 150 // Headstriker
@@ -1511,7 +1571,7 @@
//[Idname] == Steeldriver // Steeldriver
//[Idname] == Nordstenderizer // Nord's Tenderizer
[Name] == Scourge && [Quality] == Unique && [Flag] != Ethereal # [Enhanceddamage] >= 240 && [Itemabsorblight] >= 3 // Stormlash
[Name] == Marteldefer && [Quality] == Unique # [Enhanceddamage] >= 130 // The Gavel Of Pain (Wc Gf Barb Offhand)
//[Name] == Marteldefer && [Quality] == Unique # [Enhanceddamage] >= 130 // The Gavel Of Pain (Wc Gf Barb Offhand)
//[Idname] == Tyrantclub // Demon Limb
//[Idname] == Fleshrender // Fleshrender
//[Idname] == Baranarsstar // Baranar's Star
@@ -1536,6 +1596,8 @@
//================= Unique Scepters ================
//--------------------------------------------------
[Name] == Mightyscepter && [Quality] == Unique && [Flag] != Ethereal //Heaven's Light
[Name] == Caduceus && [Quality] == Unique && [Flag] != Ethereal //Astreon's Iron Ward
//[Idname] == Heavenslight // Heaven's Light
//[Idname] == Knellstriker // Knell Striker
//[Idname] == Zakarumshand // Zakarum's Hand
@@ -1563,7 +1625,7 @@
[Name] == Quarterstaff && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] == 300 // Eth Ribcracker Perfect
//[Name] == Elderstaff && [Quality] == Unique # [Itemaddskilltab] >= 2 // Ondal's Wisdom (Eth/Non-Eth)
[Name] == Elderstaff && [Quality] == Unique # [Itemaddskilltab] == 4 && [Magicdamagereduction] == 8 && [Energy] == 50 && [Plusdefense] == 550 // Ondal's Wisdom (Eth/Non-Eth) Perfect
//[Idname] == Mangsongslesson // Mang Song's Lesson
[Name] == Archonstaff && [Quality] == Unique // Mang Song's Lesson
//----------------------------------------------------------
//================= Unique Throwing Weapons ================
@@ -1574,7 +1636,7 @@
//[Idname] == Thescalper // The Scalper
[Name] == Francisca && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] >= 200 && [Lifeleech] >= 6 // Eth The Scalper Perfect
//[Idname] == Warshrike // Warshrike
[Name] == Wingedknife && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] >= 250 // Eth Warshrike Perfect
[Name] == Wingedknife && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] >= 200 // Eth Warshrike
//[Idname] == Gimmershred // Gimmershred
//[Idname] == Lacerator // Lacerator
[Name] == Wingedaxe && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] >= 210 // Eth Lacerator Perfect
@@ -1600,7 +1662,7 @@
//[Name] == Ceremonialbow && [Quality] == Unique # [Enhanceddamage] >= 190 && [Manaleech] >= 8 // Lycander's Aim Perfect
[Idname] == Titansrevenge // Titan's Revenge
[Name] == Ceremonialjavelin && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddamage] >= 150 && [Lifeleech] >= 6 // Eth Titan's Revenge
//[Idname] == Thunderstroke // Thunderstroke
[Idname] == Thunderstroke // Thunderstroke
[Name] == Matriarchaljavelin && [Quality] == Unique && [Flag] != Ethereal # [Javelinandspearskilltab] >= 4 && [Enhanceddamage] >= 200 // Thunderstroke Perfect
//-----------------------------------------------------------
@@ -1643,8 +1705,8 @@
[Name] == Eldritchorb && [Quality] == Unique # [Sorceressskills] >= 2 && [Passivefiremastery] >= 10 && [Passiveltngmastery] >= 10 // Eschuta's Temper
//[Name] == Eldritchorb && [Quality] == Unique # [Sorceressskills] == 3 && [Passivefiremastery] == 20 && [Passiveltngmastery] == 20 && [Energy] == 30 // Eschuta's Temper Perfect
[Name] == Dimensionalshard && [Quality] == Unique # [Passivecoldpierce] >= 15 && [Lightresist] >= 25 // Death's Fathom
//[Name] == Dimensionalshard && [Quality] == Unique # [Passivecoldpierce] == 30 && [Lightresist] == 40 // Death's Fathom Perfect
[Name] == Dimensionalshard && [Quality] == Unique # [Passivecoldmastery] >= 15 && [Lightresist] >= 25 // Death's Fathom
//[Name] == Dimensionalshard && [Quality] == Unique # [Passivecoldmastery] == 30 && [Lightresist] == 40 // Death's Fathom Perfect
[Name] == Swirlingcrystal && [Quality] == Unique # [Sorceressskills] >= 3 // The Oculus
//-----------------------------------------------------
@@ -1669,3 +1731,8 @@
[Name] == Gildedshield && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] >= 150 // Eth Herald Of Zakarum
//[Name] == Gildedshield && [Quality] == Unique && [Flag] == Ethereal # [Enhanceddefense] == 200 // Eth Herald Of Zakarum Perfect
//[Name] == Zakarumshield && [Quality] == Unique # [Enhanceddefense] >= 150 && [Itemabsorbfirepercent] >= 15 && [Strength] >= 20 // Dragonscale
//--------------------------------------------------------------
//================= Unique Warlock Grimoires ================
//--------------------------------------------------------------
[Type] == Grimoires && [Quality] == Unique //All unique Warlock grimoires
+29 -9
View File
@@ -70,8 +70,8 @@ wp_act_btn_width=67
wp_first_btn_x=227
wp_first_btn_y=129
wp_btn_height=47
; pickit
item_dist=267
; pickit **Note: 120 is apx pickup range. Set to this value or below to prevent walking on pickup
item_dist=121
; pather
reached_node_dist=100
min_walk_dist=25
@@ -88,10 +88,11 @@ play_btn=426,616,320,71
difficulty_select=536,236,210,320
gold_btn=997,521,20,18
inventory_gold=980,510,150,40
gold_btn_stash=150,480,40,65
gold_btn_stash=160,520,25,25
vendor_gold_digits=186,509,97,16
stash_gold_digits=185,507,99,15
stash_gold_digits=184,527,100,16
inventory_gold_digits=1017,523,100,16
stash_page_digits=222,478,10,15
health_globe=160,580,240,140
mana_globe=887,580,240,140
health_slice=309,610,7,101
@@ -104,8 +105,9 @@ loading_left_black=0,0,350,720
death=444,198,397,71
tp_search=353,120,547,400
repair_btn=318,473,90,80
left_inventory=35,92,378,378
left_inventory=35,86,378,378
right_inventory=866,348,379,152
transmute_third_slot=242,342,38,38
skill_right=664,673,41,41
skill_right_expanded=655,375,385,255
skill_left=574,673,41,41
@@ -118,9 +120,9 @@ stash_btn_roi=20,52,412,53
enemy_info=440,15,405,150
shrine_check=500,50,350,100
character_select=1033, 44, 226, 554
character_online_status=1033, 19, 226, 25
character_online_status=1030, 18, 240, 40
; character_sub_roi is with respect to matched active character template
character_name_sub_roi=6, 22, 186, 18
character_name_sub_roi=2, 20, 194, 22
cube_area_roi=167,209,113,152
cube_btn_roi=160,368,125,57
xp_bar_text=369,630,554,34
@@ -132,7 +134,9 @@ npc_dialogue=458,4,21,140
bind_skill=516,619,251,29
quest_skill_btn=284,455,710,121
left_inventory_tabs=31,62,385,28
tab_indicator=31,82,385,14
tab_indicator=31,71,385,14
stash_page_select_left=155,475,20,20
stash_page_select_right=270,475,20,20
deposit_btn=454,365,186,43
equipped_inventory_area=861,59,387,287
inventory_bg_pattern=21,622,1236,75
@@ -141,7 +145,7 @@ inventory_bg_pattern=21,622,1236,75
; static pathes in format: x0,y0, x1,y1, x2,y2, ...
pindle_safe_dist=921,35, 1123,70, 960,88
eldritch_safe_dist=652,63, 563,73
trav_safe_dist=1156,438, 1120,282, 1059,525, 1146,312, 1132,346, 985,538, 1125,333
trav_safe_dist=1156,438, 1120,282, 1059,525, 1146,312, 1132,346, 1100,538
pindle_end=1037,174
eldritch_end=675,210
shenk_end=1067,544
@@ -244,3 +248,19 @@ dia_c2g_home_loop=175,175
dia_a_layout_bold=189,193, 189,193, 189,193, 189,193, 189,193, 189,193, 189,193, 189,193, 1100,337
dia_b_layout_bold=1129,148, 1129,148, 1129,148, 1129,148, 1129,148, 1129,148
dia_c_layout_bold=1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503
; Andariel (A1 Catacombs) - placeholder, fill in real coords later
a1_andy_level3_enter=0,0
a1_andy_level4_enter=0,0
a1_andy_safe_dist=0,0
; Countess (A1 Forgotten Tower) - placeholder, fill in real coords later
a1_tower_level2_enter=0,0
a1_tower_level3_enter=0,0
a1_tower_level4_enter=0,0
a1_tower_level5_enter=0,0
a1_countess_safe_dist=0,0
; Mephisto (A3 Durance of Hate) - placeholder, fill in real coords later
a3_meph_level3_enter=0,0
a3_meph_safe_dist=0,0
; Baal (A5 Throne of Destruction) - placeholder, fill in real coords later
a5_baal_throne_entry=0,0
a5_baal_safe_dist=0,0
+202 -56
View File
@@ -1,9 +1,20 @@
; There is detailed documentation for each parameter in the README.md
[general]
difficulty=hell
name=Botty
difficulty=normal
name=bigfont
randomize_runs=0
saved_games_folder=
target_tz=1
saved_games_folder=C:\Users\alex\Saved Games\Diablo II Resurrected
level_max_steps=20
; Battle.net credentials (for auto-login at launch)
; Leave empty to log in manually
bnet_name=
bnet_pass=
; Character name to auto-select from the character selection screen
; If empty, bot relies on the saved character template from previous sessions
char_name=Burr
; messaging
custom_loot_message_hook=
@@ -19,17 +30,79 @@ max_runtime_before_break_m=0
; timers / fail handling
d2r_path=C:\Program Files (x86)\Diablo II Resurrected
max_consecutive_fails=5
max_game_length_s=380
max_game_length_s=160
; if you set this field to 1, botty will attempt to restart d2 after a crash or failure
restart_d2r_when_stuck=0
hardcore=0
; screenshots
info_screenshots=1
loot_screenshots=0
pickit_screenshots=0
[stealth]
; Multiplies all wait() calls by a random value in this range each call
; 1.0 = no change. Set range wider for more human-like timing variation.
wait_jitter_min = 0.85
wait_jitter_max = 1.20
; Extra pixel variance added to every mouse click (on top of existing randomize=5)
; 0 = off, 10 = +/-10px extra random offset per click
click_variance = 8
; Re-shuffle run order after completing a full rotation (vs only at session start)
reshuffle_each_rotation = 1
; Probability (0-100) of randomly skipping a run each game
; 0 = never skip, 20 = skip ~1 in 5 runs
skip_run_chance = 10
; Probability (0-100) of taking an unscheduled AFK break after any given run
afk_break_chance = 5
; Break duration range in minutes
afk_break_min_m = 2
afk_break_max_m = 12
; Vary run/battle duration by a Gaussian factor (default +/-15%)
; 0.0 = no variation, 0.3 = up to +/-30% variation
run_duration_variance = 0.15
; Micro-pause between actions (simulates human hesitation in milliseconds)
micro_pause_min_ms = 20
micro_pause_max_ms = 120
; Vary kill time to avoid perfectly consistent boss fight durations
vary_kill_time = 1
; Human mouse curve complexity (1.0 = default, 0.5 = more direct, 1.5 = more winding)
human_curve_complexity = 1.0
; Arrival-to-click delay: human-like pause between mouse arriving and clicking (milliseconds)
; 50-800ms range simulates "is this the right thing?" hesitation
click_delay_min_ms = 50
click_delay_max_ms = 800
; Key press duration variance: how long a key is held (milliseconds)
; Most presses are short (20-100ms), some linger (up to 200ms)
key_press_min_ms = 20
key_press_max_ms = 200
; Skill rotation hesitation: pause before casting a skill (milliseconds)
; Prevents machine-speed skill spam
skill_hesitation_min_ms = 80
skill_hesitation_max_ms = 300
; Wrong waypoint chance: 2-3% of selecting wrong TP portal then correcting
; Humans occasionally misclick waypoint targets
wrong_waypoint_chance = 0.025
; Skill mistake chance: 1-2% chance of miscasting and correcting
; Simulates human error during combat
skill_mistake_chance = 0.015
[routes]
; Add these possible routes to "order" to run them:
; Add these possible routes to "order" as a comma delimited list to run them:
; run_trav
; run_pindle
; run_eldritch
@@ -37,23 +110,33 @@ pickit_screenshots=0
; run_nihlathak
; run_arcane
; run_diablo
order=run_pindle, run_eldritch_shenk
; run_vizier
; run_andariel (Act 1 Catacombs)
; run_countess (Act 1 Forgotten Tower)
; run_mephisto (Act 3 Durance of Hate)
; run_baal (Act 5 Throne of Destruction)
order=run_pindle
[char]
; ==========================
; ==== Mandatory Fields ====
; ==========================
; These configs have to be alligned with your d2r settings and char build
type=light_sorc
type=fohdin
belt_rows=4
casting_frames=10
cta_available=0
casting_frames=8
cta_casting_frames=8
attack_frames=15
cta_available=1
;Do we want to cast non-cta buffs (ex energy shield) with cta
buff_with_cta=1
; safer_routines: enable for optional defensive maneuvers/etc during combat/runs at the cost of increased runtime (ex. hardcore players)
safer_routines=0
safer_routines=1
; num_loot_columns: Number of empty columns from left to right of inventory to be used for looting.
; Store charms, etc. to the right of the inventory.
num_loot_columns=5
num_loot_columns=4
; game hotkeys:
force_move=e
@@ -68,12 +151,12 @@ show_items=alt
; stand_still cannot be the default "shift" as it would interfere with merc healing
stand_still=capslock
; teleport: leave empty if you can't use
teleport=
town_portal=f9
teleport=5
town_portal=6
; call to arms settings:
weapon_switch=x
battle_orders=f7
battle_command=f8
weapon_switch=w
battle_orders=7
battle_command=8
; ==========================
; ==== Optional configs ====
@@ -85,20 +168,28 @@ use_merc=1
atk_len_arc=2.5
atk_len_eldritch=3.0
atk_len_nihlathak=4.0
atk_len_pindle=3.0
atk_len_pindle=9.0
atk_len_shenk=4.0
atk_len_diablo=3.0
atk_len_trav=3.0
; Boss run attack lengths (per-character defaults in kill_* methods)
; Adjust these if your build is faster/slower against these bosses
atk_len_andariel=4.0
atk_len_countess=3.0
atk_len_mephisto=12.0
atk_len_baal=10.0
atk_len_baal_waves=30.0
; Chaos Sanctuary settings
atk_len_cs_trashmobs=1.5
atk_len_cs_trashmobs=2.0
atk_len_diablo_deseis=5.0
atk_len_diablo_infector=4.0
atk_len_diablo_vizier=2.0
atk_len_diablo=3.0
cs_mob_detect=1
cs_mob_detect=0
; cs_town_visits is currently broken, ignore for now
cs_town_visits=0
kill_cs_trash=0
kill_cs_trash=1
; Belt settings
belt_hp_columns=1
@@ -106,14 +197,14 @@ belt_mp_columns=1
belt_rejuv_columns=2
; Potion/chicken settings
take_health_potion=0.8
take_mana_potion=0.5
take_rejuv_potion_health=0.4
take_rejuv_potion_mana=0.1
take_health_potion=0.90
take_mana_potion=0.80
take_rejuv_potion_health=0.75
take_rejuv_potion_mana=0.50
heal_merc=0.7
heal_rejuv_merc=0.2
chicken=0.35
merc_chicken=0
heal_rejuv_merc=0.25
chicken=0.75
merc_chicken=0.25
; Misc.
; helps reduce accidental pickups when enabled especially on walking characters
@@ -121,9 +212,8 @@ enable_no_pickup=1
fill_shared_stash_first=0
; to gamble, add any/all of the following: circlet, ring, coronet, talon, amulet
gamble_items=
id_items=1
open_chests=1
pre_buff_every_run=1
pre_buff_every_run=0
runs_per_repair=0
runs_per_stash=4
sell_junk=0
@@ -135,16 +225,16 @@ stash_destination=3,2,1,0
; chipped, flawed, standard, flawless
transmute=flawless
;how often we want to run transmute routine(e.g. every 100 games)
transmute_every_x_game=20
transmute_every_x_game=2000
; ===========================
; ==== Builds: Sorceress ====
; ===========================
[sorceress]
energy_shield=
frozen_armor=
static_field=
telekinesis=
energy_shield=f3
frozen_armor=f4
static_field=f5
telekinesis=f6
thunder_storm=
[light_sorc]
@@ -157,55 +247,96 @@ frozen_orb=
[blizz_sorc]
; blizzard must be right skill, hotkey required
blizzard=
blizzard=f1
; ice_blast must be left skill (hotkey optional as it shouldnt change)
ice_blast=
[blizzorb_sorc]
; frozen orb must be left skill and preselected (no hotkey required)
;All others must be right skill and hotkey required!
blizzard=f1
glacial_spike=f2
[nova_sorc]
; nova must be right skill, hotkey required
nova=
nova=f1
[hydra_sorc]
; only supports Pindle and Eldritch currently
; alt_attack is any alternate attacking right skill. Fireball,Lightning,Frozen Orb, hotkey required
alt_attack=
alt_attack=f7
; hydra must be right skill, hotkey required
hydra=
hydra=f1
; =========================
; ==== Builds: Paladin ====
; =========================
[paladin]
cleansing=
holy_shield=
redemption=
vigor=
holy_shield=f2
redemption=f3
vigor=f4
[fohdin]
; foh must be left skill, hotkey required
blessed_hammer=
concentration=
conviction=
foh=
holy_bolt=
conviction=f5
foh=f6
holy_bolt=f7
[hammerdin]
blessed_hammer=
concentration=
blessed_hammer=f4
concentration=f5
; =========================
; ==== Builds: Warlock ====
; =========================
[warlock]
; All skills should be on right-click unless otherwise specified
deathmark=f3
lethargy=f4
;Set summon_demon to one of the three demons of your choice.
summon_demon=f5
;If you are summoning a 2nd demon set hotkey here. Can be same as above
summon_demon2=
psychic_ward=
[fire_lock]
; Supported runs: Pindle
;flame_wave must be assigned to left-click and doesn't require hotkey
ring_of_fire=f1
apocalypse=f2
[abyss_lock]
; Supported runs: Pindle
;miasma_bolt must be assigned to left-click and doesn't require hotkey
miasma_chain=f1
abyss=f2
[echo_lock]
; Supported runs: Trav (tele only)
echo_strike=f1
eldritch_blast=f2
hex_bane=f3
;Set ring of fire if you want to stun before echo blast
ring_of_fire=
; ==========================
; ==== Builds: Assassin ====
; ==========================
; Currently no Trav implementaiton!
[trapsin]
burst_of_speed=
death_sentry=
fade=
lightning_sentry=
shadow_warrior=
; We assume fire blast or shock web is left-click skill which is optional hotkey if pre-assigned.
skill_left=
; Assume all other skills are right-click and hotkey required!
burst_of_speed=f1
death_sentry=f2
fade=
lightning_sentry=f3
shadow_warrior=f4
mind_blast=f5
; ===========================
; ==== Builds: Barbarian ====
@@ -220,6 +351,21 @@ leap=
shout=
war_cry=
; ===========================
; ==== Builds: Amazon ====
; ===========================
; Supported runs: Trav, Pindle, Eld, Shenk, Nihlathak (tele only)
; Make sure leap hotkey is set if you do not have Enigma
; Ensure Battle Order/Command hotkeys are set above in the [char] section
[amazon]
;all must be right-click skill
valkyrie=f1
[javazon]
; charged strike must be left-click skill and preselected, no hotkey required
;all others must be right-click
lightning_fury=f2
; ==========================
; ==== Builds: Necro ====
@@ -277,7 +423,7 @@ damage_scaling=1
; if the buffs are not set they are not used (they are part of the prebuff setup)
[basic]
left_attack=
left_attack=1
right_attack=
buff_1=
buff_2=
@@ -316,6 +462,6 @@ message_headers=
ocr_during_pickit=0
;use "can_teleport_natively" or "can_teleport_with_charges" if you want to force certain behavior in case autodetection isn't working properly
override_capabilities=
pathing_delay_factor=4
;If you want to control Hyper-V window from host use 0,51 here
pathing_delay_factor=2
; If you want to control Hyper-V window from host use 0,51 here
window_client_area_offset=0,0
+2 -2
View File
@@ -6,14 +6,14 @@
; x light senetry: +6
; x weapon block: +1
; x death sentrey: +4
shop_trap_claws=1
shop_trap_claws=0
trap_min_score=13
; Current scoring for melee claws
; 2 assa: +10
; x venom: +6
; x weapon block: +2
shop_melee_claws=1
shop_melee_claws=0
melee_min_score=13
[gloves]
+80
View File
@@ -0,0 +1,80 @@
"""
Desktop screenshot tool - captures the full Windows desktop or a specific window.
Usage:
python desktop_snap.py # capture full desktop
python desktop_snap.py D2R # capture D2R window only
Saves to screenshots/desktop_snap.png
"""
import os
import sys
import cv2
from mss import mss
SAVE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "screenshots", "desktop_snap.png")
os.makedirs(os.path.dirname(SAVE_PATH), exist_ok=True)
def snap_full_desktop():
"""Capture the full desktop."""
with mss() as sct:
img = sct.grab(sct.monitors[1]) # monitors[1] = primary display
# Convert from BGRA to BGR
img_bgr = img.rgb
cv2.imwrite(SAVE_PATH, img_bgr)
print(f"Saved full desktop to: {SAVE_PATH}")
print(f"Shape: {cv2.imread(SAVE_PATH).shape}")
def snap_d2r_window():
"""Capture the D2R window."""
import numpy as np
import win32gui
import win32ui
import win32con
# Find D2R window
def enum_cb(hwnd, results):
if win32gui.IsWindowVisible(hwnd):
title = win32gui.GetWindowText(hwnd)
if "diablo" in title.lower() or "d2r" in title.lower():
results.append(hwnd)
hwnds = []
win32gui.EnumWindows(enum_cb, hwnds)
if not hwnds:
print("ERROR: D2R window not found. Is it running?")
return
hwnd = hwnds[0]
print(f"Found D2R window: {win32gui.GetWindowText(hwnd)}")
# Get window client area
rect = win32gui.GetClientRect(hwnd)
w, h = rect[2] - rect[0], rect[3] - rect[1]
# Capture client area
hdc = win32gui.GetDC(hwnd)
hdc_mem = win32gui.CreateCompatibleDC(hdc)
bmp = win32gui.CreateCompatibleBitmap(hdc, w, h)
win32gui.SelectObject(hdc_mem, bmp)
win32gui.BitBlt(hdc_mem, 0, 0, w, h, hdc, 0, 0, win32con.SRCCOPY)
# Convert to image
bmp_info = win32ui.CreateBitmapFromHandle(bmp)
bmp_info.SaveBitmapFile(hdc_mem, SAVE_PATH)
win32gui.DeleteObject(bmp)
win32gui.DeleteDC(hdc_mem)
win32gui.ReleaseDC(hwnd, hdc)
img = cv2.imread(SAVE_PATH)
print(f"Saved D2R window to: {SAVE_PATH}")
print(f"Shape: {img.shape}")
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "D2R":
snap_d2r_window()
else:
snap_full_desktop()
+30 -9
View File
@@ -1,21 +1,42 @@
# Dev Docu
## Dependencies
- Install latest miniconda (https://docs.conda.io/en/latest/miniconda.html). Note: You will have to check "Add conda to my PATH environment variable" in order to access the conda command in the cmd.
- Install git (https://gitforwindows.org/)
- Install [Miniforge](https://github.com/conda-forge/miniforge) (recommended over Miniconda for conda-forge packages). Check "Add to PATH" during installation.
- Alternatively, [Miniconda](https://docs.conda.io/en/latest/miniconda.html) works too.
- Install [git](https://gitforwindows.org/)
## Getting started
```bash
git clone https://github.com/aeon0/botty.git
git clone https://github.com/Hoblirm/botty.git
cd botty
conda env create environment.yml
# Create the conda environment (installs all Python deps + tesserocr with tesseract)
conda env create -f environment.yml
# Activate and run
conda activate botty
python src/main.py
```
Important info for Powershell users:
```bash
# for powershell you have to init conda before using any conda commands:
conda init powershell
### PowerShell users
```powershell
conda init powershell # One time setup
conda activate botty
python src/main.py
```
### No conda? Quick setup
If you don't want to use conda, you can install dependencies with pip, but `tesserocr`
requires the tesseract C library which is easiest to get via conda. See `environment.yml`
for the full dependency list.
## Running with the launcher
A `run_botty.bat` file is provided in the project root. It auto-detects the conda
environment and launches the bot. You can also run manually:
```cmd
conda activate botty
cd C:\path\to\botty
python src\main.py
```
## Tests
@@ -28,7 +49,7 @@ pytest -s -v
# To run a specific test:
pytest test/smoke_test.py
```
To test single files / routines, most files also can be executed seperatly. E.g. running `python src/pickit.py` -> going to d2r window -> throw stuff on the groudn -> press f11, will test the pickit.
To test single files / routines, most files also can be executed separately. E.g. running `python src/pickit.py` -> going to d2r window -> throw stuff on the groudn -> press f11, will test the pickit.
## Adding Items
To add items you can check the **assets/items** folder. Screenshot whatever you want to pick up in the same way (all settings must be as if you ran the bot). Then add the filename to the param.ini [items] section (e.g. if boots_rare.png add boots_rare=1)
+102
View File
@@ -0,0 +1,102 @@
# Adding a New Farming Route
## Quick Start (Scaffolding Tool)
```bash
python3 tools/new_route.py --name my_target --act 1 --location "Black Marsh"
```
This generates `src/run/my_target.py` and prints every line you need to add to `bot.py`, `params.ini`, and `pather.py`. The tool also runs `ruff check` on the generated file.
For a simpler inline version (no ruff check):
```bash
python3 src/utils/new_route.py --name my_target --act 1 --location "Black Marsh"
```
---
## Step-by-Step Manual Guide
### 1. Create the run file: `src/run/my_target.py`
Every run has two methods:
**`approach(start_loc, do_pre_buff)`** — gets the character to the run start location:
- Navigate to the right act's WP via `traverse_nodes_automap()`
- Use `pather.go_to_area("Area Name", "WP")` to take the waypoint
- Return a `Location` constant on success, `False` on failure
**`battle()`** — kills the target and picks items:
- Navigate to the target via recorded path nodes
- Call `char.kill_X()` (add to `IChar` / your char class if missing)
- Call `pickit.pick_up_items(char)`
- Return `(Location, picked_up_bool)` on success, `False` on failure
### 2. Add path node constants to `src/pather.py`
In `class Location`, add string constants for each nav point:
```python
MY_TARGET_SAFE_DIST = "my_target_safe_dist"
```
### 3. Record path nodes in-game
Requires: **D2R game running**, **bot window active**, **game window NOT in fullscreen**
```bash
python3 src/utils/node_recorder.py
```
Enter the run name (e.g. `andariel`) when prompted. A debug visualization window will appear.
#### Recording workflow:
1. **F8** — Record a visual template: click top-left corner of a distinctive UI element or landmark, press F8, then click bottom-right corner, press F8 again. This creates a reusable reference point.
2. **F9** — Record a navigation node at the current mouse cursor position (relative to the templates you recorded).
3. **F10** — Update all recorded nodes with currently visible templates.
4. **F12** — Exit recorder.
Recorded templates go to `log/screenshots/generated/templates/<run_name>/`.
The generated pather code is written to `log/screenshots/generated/pather_generated.py`.
#### Level requirements for boss areas (need campaign progress):
| Boss | Act | Minimum Level | Area |
|------|-----|---------------|------|
| Countess | 1 | 1 | Forgotten Tower |
| Andariel | 1 | 6 | Catacombs Level 4 |
| Duriel | 2 | 15 | Sewers Level 3 |
| Mephisto | 3 | 30 | Durance of Hate Level 3 |
| Baal | 5 | 60 | Throne of Destruction |
You need a character that has progressed through the campaign to access these areas. If you're starting from level 1, you'll need to play through each act to unlock the areas, then record nodes as you go.
### 4. Register the route in `src/bot.py`
Four additions (the scaffold tool prints the exact lines):
1. Import: `from run import ..., MyTarget`
2. `_do_runs` dict entry: `"run_my_target": Config().routes.get("run_my_target"),`
3. Instance: `self._my_target = MyTarget(...)`
4. State machine state + transition + handler method
### 5. Document in `config/params.ini`
Add `; run_my_target` to the routes comment block so users know it exists.
### 6. Enable the route
In your `config/params.ini` (or `config/custom.ini`):
```ini
[routes]
order=run_pindle, run_my_target
```
---
## Stealth Tips for New Routes
- Use `wait(min, max)` for all pauses — the stealth jitter config multiplies these automatically
- Use `stealth_move(x, y)` instead of `mouse.move(x, y)` for clicks in the new run
- Keep path node counts low (3-5 nodes) — more nodes = more predictable pathing patterns
- Vary which areas you teleport through using random sub-paths if the route allows it
+258
View File
@@ -0,0 +1,258 @@
# Anti-Detection Framework for Botty-Go
## Overview
This document outlines the multi-layered anti-detection system built into botty-go.
Each layer addresses a specific detection vector that Blizzard and modern anti-cheat
systems use to identify bots.
---
## 1. Server-Side Behavior Analysis Countermeasures
### Detection: Session length, timing consistency, pathing patterns, repetition
### Countermeasures:
#### 1a. Variable Session Scheduling
- **Implementation:** `internal/schedule/scheduler.go`
- Randomized session start times using a circadian model
- Simulated human sleep patterns: 6-10 hour breaks between sessions
- Weekend/weekday behavior variance (humans play differently on weekends)
- Random session lengths: 20min to 6hours with exponential distribution
- Occasional "just 5 more minutes" overtime and "I'm tired" early stops
#### 1b. Stochastic Pathing
- **Implementation:** `internal/pather/stochastic.go`
- Add deliberate pathing imperfection: 5-15% deviation from optimal route
- Occasional wrong-way teleports followed by course correction
- Non-optimal waypoint selections (humants don't always take shortest path)
- Variable route ordering with cooldown-dependent choices
- 2-3% chance of "getting lost" and using wrong waypoint first
#### 1c. Skill Rotation Variance
- **Implementation:** `internal/char/behavior.go`
- Variable pre-buff timing (humans rush sometimes, sometimes take time)
- Occasional wrong skill selection followed by correction
- Potion usage with human-like hesitation (check multiple times before drinking)
- Merc healing variance: sometimes forget, sometimes over-heal
#### 1d. Route Randomization with Context
- **Implementation:** `internal/bot/route_planner.go`
- Dynamic route selection based on:
- Time since last run of each type
- Current TP scroll count (humans adapt)
- Gem/transmute urgency
- Occasional "feels like it" switches
- Never perfect round-robin; use weighted probability with drift
#### 1e. Farming Repetition Masking
- Never run the same route more than 8 times consecutively
- Insert "town breaks": stash visit, shrine check, repair, gamble
- 1-2% chance of "I'm bored, switching to different run" mid-session
- Vary kill strategies: sometimes rush, sometimes methodical
---
## 2. Warden / Client Integrity Countermeasures
### Detection: Loaded modules, injected DLLs, memory signatures, debuggers
### Countermeasures:
#### 2a. Pixel-Only Architecture (No Memory Access)
- **Implementation:** entire bot reads game state ONLY via screenshots
- NO memory reading, NO DLL injection, NO process hooking
- Same attack surface as a human with a camera pointed at the screen
- This is the #1 defense: if you only use screen capture + input simulation,
there's nothing to scan in process memory
#### 2b. Clean Process Environment
- **Implementation:** `internal/runtime/clean_env.go`
- Standard Go binary with no suspicious imports
- No debuggers, no memory readers, no process manipulation
- Run as a normal application, not injected
#### 2c. Overlay Avoidance
- Never draw on top of game window
- No window hooking or injection
- Screenshot from a separate thread, not an overlay
---
## 3. Input Pattern Analysis Countermeasures
### Detection: Synthetic inputs, smooth cursor paths, periodic inputs, no micro-corrections
### Countermeasures:
#### 3a. Human Motor Model
- **Implementation:** `internal/mouse/human_model.go`
- Full biomechanical mouse model based on Fitts' Law and human motion studies
- Real human mouse data characteristics:
- Multi-segment movement with micro-pauses (1-3 segments per motion)
- Acceleration curve: start slow, peak in middle, decelerate into target
- Endpoint micro-adjustments: 2-5 pixel wobble before click
- Inter-trial variability: each movement is unique even to same target
- Asymmetric error distribution: overshoot more right/down (human bias)
#### 3b. Click Timing Model
- **Implementation:** `internal/mouse/click_model.go`
- Variable time between "arriving" at target and clicking: 50ms-800ms
- Pressure curve: humans don't click at exact same speed
- Double-click rate varies naturally
- Occasional misses: 0.5-1% of clicks land slightly off (1-3px)
#### 3c. Keyboard Behavior Model
- **Implementation:** `internal/keyboard/human_model.go`
- Key press duration variance: not all keypresses are identical
- Typing rhythm for skill hotkeys: natural cadence with micro-pauses
- Occasional key repeat (holding too long = rapid fire)
- Realistic key-up/key-down timing ratios
#### 3d. Statistical Indistinguishability
- **Implementation:** `internal/input/stats.go`
- All input streams modeled from real human motion capture data
- Entropy analysis of output matches human baselines
- Auto-calibration: measure user's own input if they do manual play
- Periodically inject "manual-looking" variance spikes
---
## 4. Economy and Item-Flow Countermeasures
### Detection: Gold accumulation, rune farming, item transfer networks, mule behavior
### Countermeasures:
#### 4a. Natural Accumulation Rate
- **Implementation:** `internal/inventory/economy.go`
- Vary farming intensity: some sessions heavy, some light
- Match accumulation to stated playtime (more sessions = more loot)
- Occasionally "waste" items on gambling/repairs like a real player
#### 4b. Realistic Trading Patterns
- No mass item funneling
- If trading, do it in human-sized batches with natural pauses
- Vary trade partners and timing
#### 4c. Rune Farming Variance
- Don't farm the same runes every session
- Match rune acquisition to character progression
- Occasionally skip rune picks when "full"
---
## 5. Ban Wave Defense
### Detection: Delayed batch bans
### Countermeasures:
#### 5a. Graceful Degradation
- **Implementation:** `internal/runtime/safe_mode.go`
- If one account gets banned, immediately reduce intensity across all
- Auto-pause farming for 48-72 hours (simulating "taking a break")
- Gradual return with reduced session lengths
- Change behavior patterns after any ban event
#### 5b. Account Diversity
- Each account has distinct "personality":
- Different session timing preferences
- Different route preferences
- Different response timing distributions
- Different play styles (rusher vs methodical)
---
## 6. Server Authority Countermeasures
### Detection: Server-side validation of movement, drops, combat, inventory
### Countermeasures:
#### 6a. Server-Authoritative Behavior
- **Implementation:** `internal/bot/server_aware.go`
- Only interact with what the server actually shows
- Wait for server confirmation before acting (e.g., confirm item picked up)
- Respect server-enforced movement limits (no speed hacks)
- Process drops in game-authorized order
#### 6b. No Client Manipulation
- Never try to spoof packets, modify client, or exploit desync
- Purely reactive: see screen -> decide -> act -> wait for response
---
## 7. Social/Reporting System Countermeasures
### Detection: Player reports + telemetry correlation
### Countermeasures:
#### 7a. Social Stealth
- **Implementation:** `internal/social/stealth.go`
- Play during off-peak hours less suspiciously
- Avoid solo-public routes that attract attention
- Occasionally join other players' games (with reduced automation)
- Inherit human-like chat behavior if configured
---
## 8. Hardware/Identity Correlation Countermeasures
### Detection: IP patterns, hardware fingerprints, VMs, account clusters
### Countermeasures:
#### 8a. Clean Deployment
- **Implementation:** `internal/deploy/clean.go`
- Run on real hardware, not VMs
- Use residential IP, not datacenter
- One account per hardware profile
- No VPN/proxy during play sessions
---
## Implementation Architecture
```
internal/
├── input/ # Human-like input generation
│ ├── mouse_model.go # Fitts' Law mouse movement
│ ├── click_model.go # Human click timing
│ ├── keyboard_model.go # Keyboard behavior
│ └── stats.go # Statistical verification
├── behavior/ # High-level human behavior simulation
│ ├── scheduler.go # Session scheduling
│ ├── route_planner.go # Dynamic route selection
│ ├── fatigue.go # Simulated fatigue/boredom
│ └── personality.go # Per-account personality
├── economy/ # Economic behavior masking
│ ├── accumulation.go # Natural loot accumulation
│ └── trading.go # Human-like trading patterns
├── safe_mode/ # Graceful degradation
│ ├── detection.go # Ban wave detection
│ └── cooldown.go # Auto-pause and return
└── deploy/ # Clean deployment helpers
└── check.go # Pre-flight integrity checks
```
## Key Design Principles
1. **Statistical indistinguishability:** Output must be statistically
indistinguishable from real human input. We use actual human motion
capture data distributions, not made-up random numbers.
2. **Controlled imperfection:** A human is inefficient, forgetful, and
inconsistent. The bot should be too — but in a way that matches
real human distributions.
3. **No single fingerprint:** Every instance should have unique enough
characteristics that correlating two accounts is hard.
4. **Adaptability:** If behavior changes are detected, the system should
be able to recalibrate based on new data.
5. **Defense in depth:** No single countermeasure is sufficient. The
combination across all layers is what provides real protection.
+33
View File
@@ -0,0 +1,33 @@
# Botty-Go
D2R Pixel Bot rewritten in Go for cross-platform support (Linux + Windows).
Based on the Python Botty project (johannes-do/botty), this is a ground-up rewrite
in Go that maintains compatibility with the same config files, templates, and run
logic while adding native Linux support.
## Features
- Cross-platform: Linux (X11/Wayland) and Windows
- Same config format as original Botty (params.ini, game.ini, shop.ini)
- Template matching with OpenCV Go bindings
- Tesseract OCR for item identification
- Human-like mouse movement (Bezier curves)
- BNIP pickit language
- All original character builds (Sorc, Paladin, Necro, Barbarian, etc.)
- All original runs (Pindle, Eldritch, Shenk, Trav, Nihlathak, Arcane, Diablo)
## Building
```bash
# Linux
go build -o botty ./cmd/botty
# Windows (from Linux with cross-compile)
GOOS=windows GOARCH=amd64 go build -o botty.exe ./cmd/botty
```
## Configuration
Copy `config/` from the original Botty project. Params, routes, and character
config work identically.
+19
View File
@@ -0,0 +1,19 @@
# Legacy: Go Rewrite Design Notes
These docs are archived from an abandoned `~/git/botty-go` directory (May 2026).
That project was a planned ground-up Go rewrite of `johannes-do/botty` for
cross-platform (Linux + Windows) support. Only design docs existed — no `.go`
source was ever written.
The Python `my-botty` project (this repo) is the active path. These docs are
kept here as **reference material**, primarily for Milestone 2 (anti-detection /
stealth) of `~/.claude/plans/continue-the-make-up-sunny-honey.md`.
## Files
- **`ANTI_DETECTION.md`** — Multi-layer anti-detection framework. Covers
server-side behavior analysis countermeasures (session scheduling, stochastic
pathing, skill rotation variance) and more. Directly applicable as the design
basis for the Python stealth layer.
- **`GO_REWRITE_README.md`** — Original README of the abandoned Go project.
Context only — explains feature scope and what the rewrite was aiming for.
+4 -4
View File
@@ -8,13 +8,13 @@ dependencies:
- leptonica
- tesseract=5.1.0
- pkg-config
- rapidfuzz
- rapidfuzz=2.15.1
- pip:
- pyinstaller
- opencv-python==4.5.5.64
- transitions
- mss
- numpy
- mss==7.0.1
- numpy==1.26.4
- mouse
- keyboard
- beautifultable
@@ -23,8 +23,8 @@ dependencies:
- coverage
- pytest
- pytest-env
- pytest-pythonpath
- pytest-mock
- pyparsing
- graphviz
- psutil
- cryptography
+40
View File
@@ -0,0 +1,40 @@
[project]
name = "botty"
version = "0.8.1"
description = "Pixelbot for Diablo 2 Resurrected"
requires-python = ">=3.10,<3.11"
dependencies = [
"pywin32",
"opencv-python==4.5.5.64",
"transitions",
"mss==7.0.1",
"numpy==1.26.4",
"beautifultable",
"pytweening",
"requests",
"rapidfuzz==2.15.1",
"pyparsing",
"graphviz",
"psutil",
"cryptography",
"pillow",
"discord.py",
"parse",
"typing_extensions",
"colorama",
"dataclasses-json",
# tesserocr must be installed separately - see development.md
]
[project.optional-dependencies]
dev = [
"pytest",
"pytest-env",
# pytest-pythonpath is deprecated; use [tool.pytest.ini_options] pythonpath instead
"pytest-mock",
"coverage",
]
[tool.pytest.ini_options]
pythonpath = ["./src"]
testpaths = ["test"]
-1
View File
@@ -2,4 +2,3 @@
env =
PYTHONPATH=./src
RUN_ENV=test
python_paths = ./src
+224
View File
@@ -0,0 +1,224 @@
"""
D2R capture tool - works with Windows DPI scaling.
Set DPI awareness then grab the D2R client area directly.
Keys: F1-full OCR F2-dialogue F3-questlog F4-NPCs F5-pixel F12-exit
"""
import os, sys, cv2, numpy as np, keyboard, win32gui, win32con, ctypes
from datetime import datetime
# Set DPI awareness - this makes Win32 APIs return logical (unscaled) coordinates
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except:
try:
ctypes.windll.shcore.SetProcessDpiAwareness(1)
except:
pass
# Fix tesserocr DLLs
if sys.platform == "win32":
_dll = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
if os.path.isdir(_dll):
os.add_dll_directory(_dll)
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
SAVE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "screenshots", "debug")
os.makedirs(SAVE, exist_ok=True)
def find_d2r():
hwnds = []
def cb(h, r):
if 'diablo' in win32gui.GetWindowText(h).lower() and win32gui.IsWindowVisible(h):
r.append(h)
win32gui.EnumWindows(cb, hwnds)
return hwnds[0] if hwnds else None
def grab():
"""Grab D2R client area at native 1280x720 resolution."""
from mss import mss
hwnd = find_d2r()
if not hwnd:
print(" [ERROR] D2R not found")
return None
client = win32gui.GetClientRect(hwnd)
w, h = client[2]-client[0], client[3]-client[1]
screen_pos = win32gui.ClientToScreen(hwnd, (0, 0))
with mss() as sct:
region = {
'top': screen_pos[1],
'left': screen_pos[0],
'width': w,
'height': h
}
sct_img = sct.grab(region)
img = np.array(sct_img)[:, :, :3] # BGRA -> BGR
# Resize to 1280x720 if needed
if w != 1280 or h != 720:
img = cv2.resize(img, (1280, 720), interpolation=cv2.INTER_LINEAR)
print(f" [RESIZED] {w}x{h} -> 1280x720")
else:
print(f" [CAPTURED] {w}x{h}")
return img
def ocr(img, roi=None):
try:
from d2r_image.ocr import image_to_text
target = img if roi is None else img[roi[1]:roi[1]+roi[3], roi[0]:roi[0]+roi[2]]
result = image_to_text(target, psm=6, scale=1.5, threshold=25)
return [r.text.strip() for r in result if r.text.strip()]
except Exception as e:
return [f"[OCR ERROR] {e}"]
def save(img, label):
path = os.path.join(SAVE, f"{label}_{datetime.now().strftime('%H%M%S')}.png")
cv2.imwrite(path, img)
print(f" [SAVED] {path}")
return path
# === Handlers ===
def on_f1():
print("\n[=== FULL CAPTURE ===]")
img = grab()
if not img:
return
h, w = img.shape[:2]
print(f" Size: {w}x{h}")
save(img, "full")
# UI detection
print(" UI:")
try:
from ui_manager import ScreenObjects, is_visible
found = False
for name in ['InGame', 'Loading', 'MainMenu', 'OnlineStatus', 'DeathScreen',
'NPCDialogue', 'RightPanel', 'LeftPanel', 'SkillsExpanded']:
obj = getattr(ScreenObjects, name, None)
if obj and is_visible(obj, img):
print(f" [VISIBLE] {name}")
found = True
if not found:
print(" (none)")
except Exception as e:
print(f" [err] {e}")
# Full OCR
print(" OCR:")
lines = ocr(img)
for l in lines[:30]:
print(f" {l}")
if len(lines) > 30:
print(f" ... and {len(lines)-30} more")
def on_f2():
print("\n[=== DIALOGUE ===]")
img = grab()
if not img:
return
save(img, "dialogue")
text = ocr(img, (200, 460, 880, 100))
if text:
print(" NPC says:")
for l in text:
print(f" {l}")
else:
print(" (no NPC text)")
opts = ocr(img, (200, 560, 880, 140))
if opts:
print(" Options:")
for i, o in enumerate(opts):
print(f" [{i}] {o}")
else:
print(" (no options detected - is dialogue box open?)")
def on_f3():
print("\n[=== QUEST LOG ===]")
img = grab()
if not img:
return
save(img, "quest_log")
for l in ocr(img, (200, 100, 880, 520)):
print(f" {l}")
def on_f4():
print("\n[=== NPC DETECTION ===]")
img = grab()
if not img:
return
save(img, "npcs")
try:
import template_finder
from npc_manager import npcs
found = []
for name, data in npcs.items():
for t in data.get("template_group", []):
r = template_finder.search(t, img, threshold=0.35)
if r.valid:
found.append(f" {name} at {r.center_monitor} ({r.score:.2f})")
break
for f in found:
print(f)
if not found:
print(" (none)")
except Exception as e:
print(f" [ERROR] {e}")
def on_f5():
img = grab()
if not img:
return
import mouse as _mouse
mx, my = _mouse.get_position()
hwnd = find_d2r()
if hwnd:
screen_pos = win32gui.ClientToScreen(hwnd, (0, 0))
ix = mx - screen_pos[0]
iy = my - screen_pos[1]
if 0 <= ix < img.shape[1] and 0 <= iy < img.shape[0]:
b, g, r = img[iy, ix]
print(f" ({ix},{iy}) RGB({r},{g},{b})")
else:
print(" Mouse outside D2R client area")
# === Run ===
def run():
print("=== Botty Capture Tool ===")
print(" F1 - Full capture + OCR + UI detection")
print(" F2 - Dialogue capture + OCR")
print(" F3 - Quest log OCR (press O in D2R first)")
print(" F4 - Detect NPCs")
print(" F5 - Mouse pixel color")
print(" F12 - Exit")
print("Ready.")
keyboard.add_hotkey('f1', on_f1)
keyboard.add_hotkey('f2', on_f2)
keyboard.add_hotkey('f3', on_f3)
keyboard.add_hotkey('f4', on_f4)
keyboard.add_hotkey('f5', on_f5)
keyboard.add_hotkey('f12', lambda: (print("\nBye."), sys.exit(0)))
keyboard.wait()
if __name__ == "__main__":
run()
+377
View File
@@ -0,0 +1,377 @@
# Botty Auto-Quest: Implementation Plan
## 1. Architecture
```
src/quest/
__init__.py
quest_manager.py # State machine, quest sequencing, persistence
quest_state.py # JSON-based quest progress tracker
quest_npc.py # Generic NPC interaction utilities (talk to NPC, dialogue selection)
quest_items.py # Quest item detection and management
quest_combat.py # Combat helpers for quest-specific fights
a1/
__init__.py # Act 1 quest runner
q1a1_smith.py # Q1: The Search for the Smith
q1a2_cain.py # Q2: Tools of the Trade
q1a3_sacrifice.py # Q3: Sacrifice
q1a4_town_portal.py# Q4: The Summoner
q1a5_skeleton_king.py # Q5: The Shepherd
q1a6_andariel.py # Q6: The Fallen Angel
a2/
__init__.py # Act 2 quest runner
q2a1_jerhyn.py # Q1: Radament
q2a2_atheistic.py # Q2: The Horadric Staff
q2a3_hephalon.py # Q3: Tyrael's Breath
q2a4_atalai.py # Q4: Secrets
q2a5_tarbedit.py # Q5: The Summoner
q2a6_nihlathak.py # Q6: The Seven Tombs
q2a7_duriel.py # Q7: The Fallen Angel
a3/
__init__.py # Act 3 quest runner
q3a1_larzuk.py # Q1: The Forgotten Tower
q3a2_cain.py # Q2: The Quest for the Horizon
q3a3_kaelthas.py # Q3: The Hellforge
q3a4_hellgate.py # Q4: The Hellgate
q3a5_mephisto.py # Q5: The Prime Evil
a4/
__init__.py # Act 4 quest runner
q4a1_izual.py # Q1: The Fallen Angel
q4a2_harumony.py # Q2: The Fallen Angel
q4a3_diablo.py # Q3: The Prime Evil
a5/
__init__.py # Act 5 quest runner
q5a1_ancients.py # Q1: The Fallen Angel
q5a2_cain.py # Q2: The Quest for the Horizon
q5a3_baal.py # Q3: The Prime Evil
```
## 2. Quest Manager Design
```python
# quest/quest_manager.py
from enum import Enum
import json, os
from config import Config
class QuestStatus(Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
SKIPPED = "skipped"
class QuestManager:
_state_file = "config/quest_state.json"
def __init__(self):
self.state = self._load_state()
self.char = None # reference to IChar
self.pather = None
def _load_state(self):
if os.path.exists(self._state_file):
with open(self._state_file) as f:
return json.load(f)
return self._default_state()
def _default_state(self):
# All 25 quests tracked by (act, quest_number)
return {
"current_act": 1,
"quests": {
"1-1": QuestStatus.PENDING, "1-2": QuestStatus.PENDING,
"1-3": QuestStatus.PENDING, "1-4": QuestStatus.PENDING,
"1-5": QuestStatus.PENDING, "1-6": QuestStatus.PENDING,
"2-1": QuestStatus.PENDING, "2-2": QuestStatus.PENDING,
"2-3": QuestStatus.PENDING, "2-4": QuestStatus.PENDING,
"2-5": QuestStatus.PENDING, "2-6": QuestStatus.PENDING,
"2-7": QuestStatus.PENDING,
"3-1": QuestStatus.PENDING, "3-2": QuestStatus.PENDING,
"3-3": QuestStatus.PENDING, "3-4": QuestStatus.PENDING,
"3-5": QuestStatus.PENDING,
"4-1": QuestStatus.PENDING, "4-2": QuestStatus.PENDING,
"4-3": QuestStatus.PENDING,
"5-1": QuestStatus.PENDING, "5-2": QuestStatus.PENDING,
"5-3": QuestStatus.PENDING,
}
}
def save(self):
with open(self._state_file, 'w') as f:
json.dump(self.state, f, indent=2)
def next_quest(self) -> tuple | None:
"""Returns (act, quest_num) of next pending quest, or None if all done."""
current = self.state["current_act"]
for qn in range(1, 8): # max 7 quests per act
key = f"{current}-{qn}"
if key in self.state["quests"] and self.state["quests"][key] == QuestStatus.PENDING:
return (current, qn)
return None
def mark_complete(self, act, qn):
self.state["quests"][f"{act}-{qn}"] = QuestStatus.COMPLETED
def is_act_complete(self, act):
for qn in range(1, 8):
key = f"{act}-{qn}"
if key in self.state["quests"] and self.state["quests"][key] == QuestStatus.PENDING:
return False
return True
def advance_to_next_act(self):
"""Called when all quests in current act are done."""
current = self.state["current_act"]
# ... handle act transition (talk to quest NPC to unlock next act)
self.state["current_act"] = current + 1
self.save()
def run_next_quest(self):
"""Dispatches to the appropriate quest implementation."""
nxt = self.next_quest()
if not nxt:
return
act, qn = nxt
self.state["quests"][f"{act}-{qn}"] = QuestStatus.IN_PROGRESS
# Route to quest implementation
# ... call quest module
```
## 3. Quest NPC Interaction
Every quest requires talking to NPCs. The existing `npc_manager.py` has `talk_to_npc()` which we extend.
```python
# quest/quest_npc.py
# Utilities for NPC dialogue selection
from utils.custom_mouse import mouse
from utils.misc import wait
from template_finder import search_and_wait
from screen import grab
def talk_and_select(dialogue_choice: str, npc_name: str):
"""
Talk to NPC and select a specific dialogue option.
botty already detects NPC and opens dialogue. We need to click
the specific dialogue button.
"""
from npc_manager import talk_to_npc
talk_to_npc(npc_name)
wait(1)
# D2R shows dialogue options in a box at bottom center.
# Detect the text of each option using OCR and click the matching one.
click_dialogue_option(dialogue_choice)
def click_dialogue_option(text: str):
"""
Use OCR to read dialogue options and click the one containing the given text.
"""
from d2r_image.ocr import image_to_text
img = grab()
roi = (300, 530, 680, 190) # dialogue box area
result = image_to_text(cut_roi(img, roi), psm=6)
for line in result.text.split('\n'):
if text.lower() in line.lower():
# Click near the center of this text line
# ... (use OCR bounding box to find click position)
break
def check_quest_item_on_screen() -> bool:
"""Detect if a quest item tooltip is visible (gold item name)."""
# Quest items have a gold/purple glow. Detect by color.
img = grab()
roi = (400, 400, 200, 200) # quest item pickup area
# Check for gold-colored pixels indicating a quest item
...
```
## 4. Quest Item Tracking
Quest items (keys, scrolls, weapons) need to be tracked. We extend the inventory system.
```python
# quest/quest_items.py
QUEST_ITEMS = {
"stone_of_jah": "Stone of Jah",
"hephaestons_key": "Hephaston's Key",
"tal_rashas_will": "Tal Rasha's Will",
"keys_to_the_crypt": "Keys to the Crypt",
"harumony": "Harumony",
"ancients_battle_order": "Ancient's Battle Order",
"horadric_cube": "Horadric Cube",
"horadric_staff": "Horadric Staff",
"amulet_of_the_vipers": "Amulet of the Vipers",
}
def has_quest_item(name: str) -> bool:
"""Check if quest item is in inventory."""
# Use OCR to scan inventory for item name
# OR check by item template matching (faster)
...
def equip_quest_item(name: str):
"""Click on quest item in inventory to equip it."""
...
def pickup_quest_item():
"""Detect and pickup quest items on ground (gold glow)."""
# Quest items have a gold/purple glow around them
# Detect with color thresholding
...
```
## 5. Quest Combat
Most quests involve fighting a boss or clearing a path. Botty already has combat logic.
```python
# quest/quest_combat.py
from char.i_char import IChar
from pather import Pather
def kill_boss(boss_name: str, atk_len: float, char: IChar, pather: Pather):
"""
Navigate to boss, fight until dead.
Reuses botty's existing kill_* methods from run/*.py
"""
# 1. Find boss on screen
# 2. Move to boss position
# 3. Attack until dead (same as run/trav.py)
# 4. Return to town
...
def clear_room(char: IChar, pather: Pather):
"""Clear a room of monsters (for quests requiring room clearance)."""
# Same logic as pather.follow_path() but with combat
...
```
## 6. Integration with Bot State Machine
The existing bot uses a state machine (transitions library). We add quest states.
```python
# bot.py changes
# Add quest states to the state machine
self._states = self._states + [
'quest', 'quest_act_transition'
]
# Add quest transitions
self._transitions = self._transitions + [
{'trigger': 'run_quest', 'source': 'town', 'dest': 'quest', 'before': 'on_run_quest'},
{'trigger': 'end_quest', 'source': 'quest', 'dest': 'town', 'before': 'on_end_quest'},
{'trigger': 'act_transition', 'source': 'town', 'dest': 'quest_act_transition',
'before': 'on_act_transition'},
]
def on_run_quest(self):
"""Start next quest."""
from quest.quest_manager import QuestManager
qm = QuestManager()
nxt = qm.next_quest()
if nxt:
act, qn = nxt
quest_func = self._get_quest_function(act, qn)
quest_func()
qm.mark_complete(act, qn)
qm.save()
def on_end_quest(self):
"""After a quest completes, check if act is done."""
qm = QuestManager()
if qm.is_act_complete(self.current_act):
self.trigger_or_stop('act_transition')
else:
self.trigger_or_stop('run_quest')
def on_act_transition(self):
"""Transition to next act (talk to NPC, unlock next act)."""
qm = QuestManager()
qm.advance_to_next_act()
# ... handle act transition logic
self.trigger_or_stop('start_from_town')
```
## 7. Configuration
Add to `params.ini`:
```ini
[quest]
; Enable quest mode (disables farming runs until quests are done)
enabled = 1
; Skip quests that are too dangerous for your character level
; 0 = run all quests, 50 = skip quests below level 50
min_level = 0
; Auto-stash quest items between acts
auto_stash_quest_items = 1
; Continue farming after all quests are done
farm_after_quest = 1
```
## 8. Implementation Order
### Phase 1: Foundation (Quest Manager + NPC Interaction)
- quest_manager.py with state tracking
- quest_npc.py for dialogue interaction
- quest_state.py for persistence
### Phase 2: Act 1 (6 quests)
- Q1: Search for the Smith (kill Rats, talk to Charsi)
- Q2: Tools of the Trade (get items from Andariel's lair)
- Q3: Sacrifice (kill Skeletons at Crypt)
- Q4: The Summoner (kill Summoner in Catacombs)
- Q5: The Shepherd (kill Skeleton King + Gargothon)
- Q6: The Fallen Angel (kill Andariel)
### Phase 3: Act 2 (7 quests)
- Q1: Radament
- Q2: The Horadric Staff (combine Cube + Scroll)
- Q3: Tyrael's Breath (clear Sewers, talk to Alkor)
- Q4: Secrets (activate 4 stone tablets)
- Q5: The Summoner (kill Duriel)
- Q6: The Seven Tombs (kill all 7 tomb bosses)
- Q7: The Fallen Angel (kill Duriel again)
### Phase 4: Act 3 (5 quests)
- Q1: The Forgotten Tower
- Q2: The Quest for the Horizon (Cain)
- Q3: The Hellforge
- Q4: The Hellgate
- Q5: The Prime Evil (Mephisto)
### Phase 5: Act 4 (3 quests)
- Q1: The Fallen Angel (Harumony)
- Q2: The Fallen Angel (Tal Rasha's Will)
- Q3: The Prime Evil (Diablo)
### Phase 6: Act 5 (3 quests)
- Q1: The Fallen Angel (Ancient's Battle Order)
- Q2: The Quest for the Horizon (Cain)
- Q3: The Prime Evil (Baal)
## 9. Testing Strategy
1. Test each quest independently in singleplayer
2. Test quest-to-quest transitions
3. Test act transitions
4. Test recovery from death during quest
5. Test with different character builds
## 10. Challenges
- **NPC dialogue**: D2R has branching dialogue. Need to handle all paths.
- **Quest items**: Some quests require carrying specific items (keys, weapons).
- **Room secrets**: Act 2 Q4 requires finding hidden passages.
- **Multi-stage quests**: Some quests span multiple areas (Act 3 Q2-4).
- **Character level**: Quests are designed for specific levels. A level 90 character kills everything too fast/slow.
- **Anti-cheat**: Questing is more visible to anti-cheat than farming. Need stealth settings.
+246
View File
@@ -0,0 +1,246 @@
"""
Quest screenshot capture tool.
Guides you through capturing specific game screens needed for building
the quest system.
Usage:
1. Launch D2R, create/select your character
2. Run: python quest_screenshot_tool.py
3. Follow the prompts
Screenshots saved to screenshots/quest/
"""
import os
import sys
import time
import numpy as np
import cv2
from datetime import datetime
# Fix tesserocr DLL loading
if sys.platform == "win32":
_conda_dll_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "conda_env", "Library", "bin")
if not os.path.isdir(_conda_dll_dir):
_conda_dll_dir = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
if os.path.isdir(_conda_dll_dir):
os.add_dll_directory(_conda_dll_dir)
from screen import find_and_set_window_position, get_offset_state, grab as screen_grab
QUEST_DIR = "screenshots/quest"
os.makedirs(QUEST_DIR, exist_ok=True)
def capture():
"""Capture the D2R window using botty's grab()."""
find_and_set_window_position()
if not get_offset_state():
print("[ERROR] Could not find D2R window. Is D2R running and visible?")
return None
return screen_grab(force_new=True)
def save(img, name):
"""Save with timestamp prefix."""
ts = datetime.now().strftime("%H%M%S")
path = os.path.join(QUEST_DIR, f"{ts}_{name}.png")
cv2.imwrite(path, img)
abs_path = os.path.abspath(path)
print(f" Saved: {abs_path}")
return abs_path
STEPS = [
{
"name": "act1_town_overview",
"instructions": (
"\nSTEP 1: Act 1 Town Overview\n"
"----------------------------------------\n"
"1. Stand in the middle of Act 1 town (New Tristram)\n"
"2. Make sure NO menus are open (close inventory, skills, etc.)\n"
"3. Position your character so all NPCs are visible\n"
"Press ENTER when the screen shows the full town...\n"
),
},
{
"name": "akara_dialogue_first",
"instructions": (
"\nSTEP 2: Akara - First Dialogue Screen\n"
"----------------------------------------\n"
"1. Walk up to Akara and LEFT-CLICK her\n"
"2. The dialogue box should appear with options\n"
"3. DO NOT click any option - just show this screen\n"
"Press ENTER when the first dialogue is visible...\n"
),
},
{
"name": "akara_dialogue_second",
"instructions": (
"\nSTEP 3: Akara - Second Dialogue Screen\n"
"----------------------------------------\n"
"1. Click the FIRST dialogue option (usually the quest-related one)\n"
"2. The next set of options should appear\n"
"3. This shows the quest dialogue choices\n"
"4. DO NOT click any option - just show this screen\n"
"Press ENTER when the second dialogue is visible...\n"
),
},
{
"name": "akara_quest_given",
"instructions": (
"\nSTEP 4: Quest Given Notification\n"
"----------------------------------------\n"
"1. Click the quest-related option to accept the quest\n"
"2. After the dialogue completes, press ESC to close it\n"
"3. Show the screen with the quest notification/text\n"
" (a message should appear on screen about the quest)\n"
"Press ENTER when you see the quest notification...\n"
),
},
{
"name": "quest_log_open",
"instructions": (
"\nSTEP 5: Quest Log\n"
"----------------------------------------\n"
"1. Press 'O' to open the Quest Log\n"
"2. The quest log panel should be visible\n"
"3. Show the full quest log\n"
"Press ENTER when the quest log is open...\n"
),
},
{
"name": "charsi_dialogue",
"instructions": (
"\nSTEP 6: Charsi NPC Dialogue\n"
"----------------------------------------\n"
"1. Walk up to Charsi\n"
"2. LEFT-CLICK her to open dialogue\n"
"3. Show the first dialogue screen\n"
"Press ENTER when Charsi's dialogue is open...\n"
),
},
{
"name": "kashya_dialogue",
"instructions": (
"\nSTEP 7: Kashya NPC Dialogue\n"
"----------------------------------------\n"
"1. Walk up to Kashya (the skill teacher)\n"
"2. LEFT-CLICK her to open dialogue\n"
"3. Show the first dialogue screen\n"
"Press ENTER when Kashya's dialogue is open...\n"
),
},
{
"name": "sewer_entrance",
"instructions": (
"\nSTEP 8: Sewer / Rat Area Entrance\n"
"----------------------------------------\n"
"1. Go to the sewer entrance (south of town)\n"
"2. Stand near the entrance looking into the rat area\n"
"3. This is for the first quest (kill rats)\n"
"Press ENTER when you can see the rat area...\n"
),
},
{
"name": "item_on_ground",
"instructions": (
"\nSTEP 9: Item on the Ground\n"
"----------------------------------------\n"
"1. Kill some rats in the sewer\n"
"2. If a quest item drops (gold glow), show it on the ground\n"
"3. If no quest item drops, show ANY item on the ground\n"
"4. The item name tooltip should be visible\n"
"Press ENTER when an item is visible on the ground...\n"
),
},
{
"name": "inventory_with_item",
"instructions": (
"\nSTEP 10: Inventory with Item Tooltip\n"
"----------------------------------------\n"
"1. Pick up the item\n"
"2. Press 'I' to open inventory\n"
"3. Hover over the item to show its tooltip\n"
"4. Show the tooltip with the item name visible\n"
"Press ENTER when the item tooltip is visible...\n"
),
},
{
"name": "dialogue_box_full",
"instructions": (
"\nSTEP 11: Full Dialogue Box (Any NPC)\n"
"----------------------------------------\n"
"1. Talk to ANY NPC\n"
"2. Get to a screen with 3+ dialogue options\n"
"3. Show the full dialogue box with all options visible\n"
"4. This helps us measure the dialogue button positions\n"
"Press ENTER when a dialogue with multiple options is visible...\n"
),
},
{
"name": "game_start_menu",
"instructions": (
"\nSTEP 12: Game Start / Difficulty Selection\n"
"----------------------------------------\n"
"1. Save & Exit to return to hero selection\n"
"2. Click Play (or let botty do it)\n"
"3. Show the difficulty selection screen\n"
" (Normal/Nightmare/Hell buttons)\n"
"Press ENTER when the difficulty screen is visible...\n"
),
},
]
def run():
print("=" * 60)
print(" Botty Quest Screenshot Tool")
print("=" * 60)
print()
print("Make sure D2R is running and visible on screen.")
print("You'll be guided through capturing each needed screen.")
print()
print("Press ENTER to start...")
input()
captured = []
failed = []
for i, step in enumerate(STEPS, 1):
print()
print(step["instructions"])
try:
input() # wait for user
img = capture()
if img is not None:
path = save(img, step["name"])
captured.append((step["name"], path))
print(f" [OK] {step['name']}")
else:
print(f" [FAIL] Could not capture for {step['name']}")
failed.append(step["name"])
except KeyboardInterrupt:
print("\n[STOPPED]")
break
# Summary
print()
print("=" * 60)
print(" Capture Summary")
print("=" * 60)
print(f" Captured: {len(captured)}/{len(STEPS)}")
for name, path in captured:
print(f" [OK] {name}")
if failed:
print(f" Failed: {len(failed)}")
for name in failed:
print(f" [XX] {name}")
print()
print(f"All screenshots in: {os.path.abspath(QUEST_DIR)}")
print()
if __name__ == "__main__":
run()
+25
View File
@@ -0,0 +1,25 @@
# Botty Improvements Implementation Plan
## Phase 1: Key Auto-Detection (Issue #940/#905) [DONE]
- `src/utils/key_detector.py` reads D2R .key/.keyo files
- Auto-fills empty char section hotkeys. Wired into config.py load_data()
- Key normalization: left alt ~ alt, left shift ~ shift
- Test: `tools/test_key_detector.py` and `test/test_key_detector.py` (all pass)
## Phase 2: Target Detection False Positives (Issues #959/#964) [DONE]
- Added aspect ratio filtering in `_add_markers()`
- Rejects health bars (w/h > 3.0) and immune text (w/h < 0.5)
- `TARGET_ASPECT_MIN = 0.5`, `TARGET_ASPECT_MAX = 3.0`
## Phase 3: Pickit Timing (Issue #939) [DONE]
- Added 200-300ms wait after `_yoink_item` pickup
- Prevents bot teleporting before item grab animation completes
## Phase 4: Hardcore Chicken Loop (Issue #942) [DONE]
- Added `hardcore` config flag (default 0)
- On HC death: exits safely instead of infinite restart loop
- Sends discord message if enabled
## Phase 5: Parallel Template Search (Issue #848) [PENDING]
## Phase 6: Async Mouse Moves (Issue #955) [PENDING]
## Phase 7: Auto-Label NPCs (Issue #950) [PENDING]
+56
View File
@@ -0,0 +1,56 @@
# ==========================================
# botty - Pixelbot for Diablo 2 Resurrected
# requirements.txt
# ==========================================
# Pin to versions that match the existing conda environment (environment.yml).
# Run `pip install -r requirements.txt` inside the `botty` conda env.
# For tesserocr, install the pre-built wheel instead:
# pip install dependencies/tesserocr-2.5.2-cp310-cp310-win_amd64.whl
# ==========================================
# --- Core runtime ---
numpy==1.26.4
opencv-python==4.5.5.64
mss==7.0.1
pillow
pywin32
# --- Game input & control ---
keyboard
mouse
pytweening
# --- State machine & logic ---
transitions
rapidfuzz==2.15.1
pyparsing
parse
dataclasses-json
# --- UI & display ---
colorama
beautifultable
# --- Networking & messaging ---
requests
discord.py
# --- System & utilities ---
psutil
cryptography
typing_extensions
graphviz
# --- OCR (requires tesseract + leptonica from conda-forge first) ---
# tesserocr -- install via wheel:
# pip install dependencies/tesserocr-2.5.2-cp310-cp310-win_amd64.whl
# --- Dev / testing ---
pytest
pytest-env
pytest-pythonpath
pytest-mock
coverage
# --- Packaging ---
pyinstaller
+57
View File
@@ -0,0 +1,57 @@
# ruff.toml — botty project linting configuration
target-version = "py310"
line-length = 120
[lint]
select = [
"E", # pycodestyle errors
"F", # Pyflakes
"I", # isort
"UP", # pyupgrade
"SIM", # simplify
"RUF", # ruff-specific rules
]
ignore = [
"E501", # line too long (handled by line-length)
"F401", # unused imports (common in __init__.py and game bots)
"F403", # star imports (used for convenience modules)
"E722", # bare except (intentional in game automation)
"RUF012", # mutable class attributes (State machine patterns)
]
[lint.per-file-ignores]
"__init__.py" = ["F401", "F403"]
"**/test/**/*.py" = ["F401"]
"**/mocks/**/*.py" = ["F401"]
"tools/*.py" = ["E402"]
"build.py" = ["E402"]
[lint.isort]
force-single-line = false
known-first-party = [
"screen",
"config",
"logger",
"bot",
"pather",
"template_finder",
"input_layer",
"ui",
"ui_manager",
"game_controller",
"game_stats",
"health_manager",
"death_manager",
"target_detect",
"char",
"shop",
"item",
"transmute",
"bnip",
"d2r_image",
"messages",
"npc_manager",
"utils",
"version",
]
+4 -34
View File
@@ -1,34 +1,4 @@
@ECHO OFF
CLS
:MENU
ECHO.
ECHO ...............................................
ECHO PRESS 1, 2, 3, 4 or 5 to select your task
ECHO ...............................................
ECHO.
ECHO 1 - Install Env
ECHO 2 - Update Env
ECHO 3 - Compile
ECHO 4 - Run Botty
Echo 5 - Exit
ECHO.
SET /P M=Type 1, 2, 3, 4 or 5 then press ENTER:
IF %M%==1 GOTO INSTALL
IF %M%==2 GOTO UPDATE
IF %M%==3 GOTO COMPILE
IF %M%==4 GOTO RUN
IF %M%==5 GOTO EXIT
:INSTALL
start cmd /c conda env create environment.yml
GOTO MENU
:UPDATE
start cmd /c conda env update environment.yml
GOTO MENU
:COMPILE
python ./build.py
GOTO MENU
:RUN
conda activate botty && python src/main.py
GOTO MENU
:EXIT
EXIT
@echo off
REM Legacy launcher - redirects to the new run_botty.bat
cd /d "%~dp0"
call run_botty.bat
+8
View File
@@ -0,0 +1,8 @@
@echo off
echo === D2R Quick Capture ===
echo Run this, D2R must be visible
echo Press ENTER when D2R is ready...
pause >nul
call C:\Users\alex\miniforge3\condabin\condactivate.bat botty
C:\Users\alex\miniforge3\envs\botty\python.exe C:\Users\alex\Downloads\my-botty\asset_extractor.py
+19
View File
@@ -0,0 +1,19 @@
@echo off
setlocal
:: Detect which conda we have and activate the botty env
set "PYTHON="
if exist "C:\Users\%USERNAME%\miniforge3\envs\botty\python.exe" set "PYTHON=C:\Users\%USERNAME%\miniforge3\envs\botty\python.exe"
if exist "C:\Users\%USERNAME%\miniconda3\envs\botty\python.exe" set "PYTHON=C:\Users\%USERNAME%\miniconda3\envs\botty\python.exe"
if exist "C:\Users\%USERNAME%\anaconda3\envs\botty\python.exe" set "PYTHON=C:\Users\%USERNAME%\anaconda3\envs\botty\python.exe"
if "%PYTHON%"=="" (
echo ERROR: Could not find botty conda environment.
echo Create it first: conda env create -f environment.yml
pause
exit /b 1
)
echo Launching Botty ...
%PYTHON% src\main.py
pause
+11
View File
@@ -0,0 +1,11 @@
@echo off
setlocal
set "PYTHON=C:\Users\%USERNAME%\miniforge3\envs\botty\python.exe"
if exist "%PYTHON%" (
cd /d "%~dp0"
%PYTHON% quest_debug.py
) else (
echo ERROR: Could not find botty Python.
echo Run: conda activate botty first.
pause
)
+144
View File
@@ -0,0 +1,144 @@
"""
Screenshot capture tool for botty development.
Takes screenshots from the D2R window and saves them with timestamps.
Usage:
python screenshot_tool.py # press keys to capture
python screenshot_tool.py --auto # auto-capture every 2 seconds
Keys:
F1 - capture current screen
F2 - capture and open in image viewer
F3 - capture cropped region (center of screen)
F11 - toggle auto-capture mode
F12 - exit
Screenshots saved to screenshots/
"""
import os
import sys
import time
from input_layer import keyboard
import numpy as np
import cv2
from datetime import datetime
# Fix tesserocr DLL loading (same as main.py)
if sys.platform == "win32":
_conda_dll_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "conda_env", "Library", "bin")
if not os.path.isdir(_conda_dll_dir):
_conda_dll_dir = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
if os.path.isdir(_conda_dll_dir):
os.add_dll_directory(_conda_dll_dir)
from mss import mss
from screen import find_and_set_window_position, get_offset_state, grab as screen_grab
SCREENSHOT_DIR = "screenshots"
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
def capture_full():
"""Capture the D2R window using botty's screen.grab()."""
# Find the window first
find_and_set_window_position()
if not get_offset_state():
print("[ERROR] Could not find D2R window. Is D2R running and visible?")
return None
return screen_grab(force_new=True)
def capture_center_crop(crop_size=(640, 360)):
"""Capture the center of the D2R window."""
img = capture_full()
if img is None:
return None
h, w = img.shape[:2]
cw, ch = crop_size
x1 = (w - cw) // 2
y1 = (h - ch) // 2
return img[y1:y1+ch, x1:x1+cw]
def save_screenshot(img, label=""):
"""Save screenshot with timestamp."""
if img is None:
return None
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
name = f"screenshot_{ts}"
if label:
name += f"_{label}"
name += ".png"
path = os.path.join(SCREENSHOT_DIR, name)
cv2.imwrite(path, img)
abs_path = os.path.abspath(path)
print(f"[SAVED] {abs_path}")
return abs_path
def show_and_save(img):
"""Save and open screenshot in default viewer."""
if img is None:
return
path = save_screenshot(img, "view")
if path and sys.platform == "win32":
os.startfile(path)
def run_interactive():
"""Interactive screenshot mode - press F1 to capture."""
print(f"""
=== Botty Screenshot Tool ===
D2R must be running and visible.
F1 - capture full D2R window
F2 - capture full + open in default image viewer
F3 - capture center crop (640x360)
F11 - toggle auto-capture (every 2s)
F12 - exit
Screenshots saved to: {os.path.abspath(SCREENSHOT_DIR)}
""")
keyboard.add_hotkey('f1', lambda: save_screenshot(capture_full()))
keyboard.add_hotkey('f2', lambda: show_and_save(capture_full()))
keyboard.add_hotkey('f3', lambda: save_screenshot(capture_center_crop(), "center"))
auto_mode = False
auto_running = False
def toggle_auto():
nonlocal auto_mode, auto_running
auto_mode = not auto_mode
if auto_mode:
print("[AUTO CAPTURE ON] - capturing every 2 seconds. Press F11 to stop.")
auto_running = True
t = _start_auto()
else:
print("[AUTO CAPTURE OFF]")
auto_running = False
def _start_auto():
import threading
def loop():
while auto_running:
save_screenshot(capture_full(), "auto")
time.sleep(2)
threading.Thread(target=loop, daemon=True).start()
keyboard.add_hotkey('f11', toggle_auto)
keyboard.add_hotkey('f12', lambda: sys.exit(0))
print("Ready. Press F1 to take a screenshot, F12 to exit.")
keyboard.wait()
if __name__ == "__main__":
if "--auto" in sys.argv:
print("[AUTO MODE] Capturing every 2 seconds. Press Ctrl+C to stop.")
try:
while True:
save_screenshot(capture_full(), "auto")
time.sleep(2)
except KeyboardInterrupt:
print("\nStopped.")
else:
run_interactive()
Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

+8
View File
@@ -0,0 +1,8 @@
# Fix: On Windows, Python 3.8+ requires os.add_dll_directory for conda-forge DLLs
# that tesserocr depends on (tesseract51.dll, leptonica-1.78.0.dll).
# This must run BEFORE any import that might trigger tesserocr loading.
import os, sys
if sys.platform == "win32":
_conda_dll_dir = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
if os.path.isdir(_conda_dll_dir):
os.add_dll_directory(_conda_dll_dir)
+22
View File
@@ -0,0 +1,22 @@
from colorama import init, Fore
init()
class BNipError(Exception):
def __init__(self, error_code: str | int, error_message: str, additional_info: str):
self.error_code = error_code
self.error_message = error_message
self.additional_info = additional_info
def __str__(self):
self.additional_info = ":" + self.additional_info if self.additional_info else ""
return f"{Fore.RED}{self.error_code}:{Fore.CYAN}{self.error_message}{Fore.YELLOW}{self.additional_info}{Fore.RESET}"
class BNipSyntaxError(BNipError):
def __init__(self, error_code: str | int, error_message: str, expression: str):
super().__init__(error_code, error_message, expression.strip())
class BNipWarning(BNipError):
def __init__(self, error_code: str | int, error_message: str, expression: str):
super().__init__(error_code, error_message, expression.strip())
@@ -1304,4 +1304,43 @@ NTIPAliasClassID["fed"]="657"
NTIPAliasClassID["festeringessenceofdestruction"]="657"
NTIPAliasClassID["std"]="658"
NTIPAliasClassID["standardofheroes"]="658"
NTIPAliasClassID["wa1"]="659"
NTIPAliasClassID["oldbook"]="659"
NTIPAliasClassID["wa2"]="660"
NTIPAliasClassID["tome"]="660"
NTIPAliasClassID["wa3"]="661"
NTIPAliasClassID["codex"]="661"
NTIPAliasClassID["wa4"]="662"
NTIPAliasClassID["compendium"]="662"
NTIPAliasClassID["wa5"]="663"
NTIPAliasClassID["grimoire"]="663"
NTIPAliasClassID["wa6"]="664"
NTIPAliasClassID["burnttext"]="664"
NTIPAliasClassID["wa7"]="665"
NTIPAliasClassID["darktome"]="665"
NTIPAliasClassID["wa8"]="666"
NTIPAliasClassID["darkcodex"]="666"
NTIPAliasClassID["wa9"]="667"
NTIPAliasClassID["possessedcompendium"]="667"
NTIPAliasClassID["waa"]="668"
NTIPAliasClassID["possessedgrimoire"]="668"
NTIPAliasClassID["wab"]="669"
NTIPAliasClassID["forgottenvolume"]="669"
NTIPAliasClassID["wac"]="670"
NTIPAliasClassID["occulttome"]="670"
NTIPAliasClassID["wad"]="671"
NTIPAliasClassID["occultcodex"]="671"
NTIPAliasClassID["wae"]="672"
NTIPAliasClassID["blasphemouscompendium"]="672"
NTIPAliasClassID["waf"]="673"
NTIPAliasClassID["blasphemousgrimoire"]="673"
NTIPAliasClassID["northernworldstoneshard"]="674"
NTIPAliasClassID["easternworldstoneshard"]="675"
NTIPAliasClassID["southernworldstoneshard"]="676"
NTIPAliasClassID["westernworldstoneshard"]="677"
NTIPAliasClassID["deepworldstoneshard"]="678"
@@ -95,6 +95,7 @@ NTIPAliasStat['sorceressskills']='83,1'
NTIPAliasStat['barbarianskills']='83,4'
NTIPAliasStat['druidskills']='83,5'
NTIPAliasStat['assassinskills']='83,6'
NTIPAliasStat['warlockskills']='83,7'
NTIPAliasStat['itemaddamazonskills']='83,0'
NTIPAliasStat['amaskills']='83,0'
NTIPAliasStat['itemaddamaskills']='83,0'
@@ -128,6 +129,11 @@ NTIPAliasStat['assskills']='83,6'
NTIPAliasStat['itemaddassskills']='83,6'
NTIPAliasStat['sinskills']='83,6'
NTIPAliasStat['itemaddsinskills']='83,6'
NTIPAliasStat['itemaddwarlockskills']='83,7'
NTIPAliasStat['warskills']='83,7'
NTIPAliasStat['itemaddwarskills']='83,7'
NTIPAliasStat['warskills']='83,7'
NTIPAliasStat['itemaddwarskills']='83,7'
NTIPAliasStat['unsentparam1']='84'
NTIPAliasStat['itemaddexperience']='85'
NTIPAliasStat['itemhealafterkill']='86'
@@ -353,6 +359,8 @@ NTIPAliasStat['plusskillbladeshield']='97,277'
NTIPAliasStat['plusskillvenom']='97,278'
NTIPAliasStat['plusskillshadowmaster']='97,279'
NTIPAliasStat['plusskillphoenixstrike']='97,280'
NTIPAliasStat['plusskillabyss']='97,281'
NTIPAliasStat['plusskillenhancedentropy']='97,282'
NTIPAliasStat['itemnonclassskill']='97'
NTIPAliasStat['state']='98'
NTIPAliasStat['itemfastergethitrate']='99'
@@ -572,6 +580,8 @@ NTIPAliasStat['skillbladeshield']='107,277'
NTIPAliasStat['skillvenom']='107,278'
NTIPAliasStat['skillshadowmaster']='107,279'
NTIPAliasStat['skillphoenixstrike']='107,280'
NTIPAliasStat['skillabyss']='107,281'
NTIPAliasStat['skillenhancedentropy']='107,282'
NTIPAliasStat['itemsingleskill']='107'
NTIPAliasStat['itemrestinpeace']='108'
NTIPAliasStat['curseresistance']='109'
@@ -810,6 +820,30 @@ NTIPAliasStat['itemaddassmartialartsskilltab']='188,20'
NTIPAliasStat['assmartialartsskilltab']='188,20'
NTIPAliasStat['itemaddsinmartialartsskilltab']='188,20'
NTIPAliasStat['sinmartialartsskilltab']='188,20'
NTIPAliasStat['itemaddchaosskilltab']='188,21'
NTIPAliasStat['chaosskilltab']='188,21'
NTIPAliasStat['itemaddwarlockchaosskilltab']='188,21'
NTIPAliasStat['warlockchaosskilltab']='188,21'
NTIPAliasStat['itemaddwarchaosskilltab']='188,21'
NTIPAliasStat['warchaosskilltab']='188,21'
NTIPAliasStat['itemaddlockchaosskilltab']='188,21'
NTIPAliasStat['lockchaosskilltab']='188,21'
NTIPAliasStat['itemaddeldritchskilltab']='188,22'
NTIPAliasStat['eldritchskilltab']='188,22'
NTIPAliasStat['itemaddwarlockeldritchskilltab']='188,22'
NTIPAliasStat['warlockeldritchskilltab']='188,22'
NTIPAliasStat['itemaddwareldritchskilltab']='188,22'
NTIPAliasStat['wareldritchskilltab']='188,22'
NTIPAliasStat['itemaddlockeldritchskilltab']='188,22'
NTIPAliasStat['lockeldritchskilltab']='188,22'
NTIPAliasStat['itemadddemonskilltab']='188,23'
NTIPAliasStat['demonskilltab']='188,23'
NTIPAliasStat['itemaddwarlockdemonskilltab']='188,23'
NTIPAliasStat['warlockdemonskilltab']='188,23'
NTIPAliasStat['itemaddwardemonskilltab']='188,23'
NTIPAliasStat['wardemonskilltab']='188,23'
NTIPAliasStat['itemaddlockdemonskilltab']='188,23'
NTIPAliasStat['lockdemonskilltab']='188,23'
NTIPAliasStat['itemaddskilltab']='188'
NTIPAliasStat['unused189']='189'
NTIPAliasStat['unused190']='190'
@@ -1037,6 +1071,7 @@ NTIPAliasStat['skillbladeshieldcharges']='204,277'
NTIPAliasStat['skillvenomcharges']='204,278'
NTIPAliasStat['skillshadowmastercharges']='204,279'
NTIPAliasStat['skillphoenixstrikecharges']='204,280'
NTIPAliasStat['skillabysscharges']='204,281'
NTIPAliasStat['itemchargedskill']='204'
NTIPAliasStat['itemnoconsume']='205'
NTIPAliasStat['passivemasterynoconsume']='206'
@@ -57,51 +57,51 @@ NTIPAliasType['secondhand']='54'
NTIPAliasType['stavesandrods']='55'
NTIPAliasType['missile']='56'
NTIPAliasType['blunt']='57'
NTIPAliasType['expansion']='58'
NTIPAliasType['jewel']='59'
NTIPAliasType['classspecific']='60'
NTIPAliasType['amazonitem']='61'
NTIPAliasType['barbarianitem']='62'
NTIPAliasType['necromanceritem']='63'
NTIPAliasType['paladinitem']='64'
NTIPAliasType['sorceressitem']='65'
NTIPAliasType['assassinitem']='66'
NTIPAliasType['druiditem']='67'
NTIPAliasType['handtohand']='68'
NTIPAliasType['orb']='69'
NTIPAliasType['voodooheads']='70'
NTIPAliasType['auricshields']='71'
NTIPAliasType['primalhelm']='72'
NTIPAliasType['pelt']='73'
NTIPAliasType['cloak']='74'
NTIPAliasType['rune']='75'
NTIPAliasType['circlet']='76'
NTIPAliasType['healingpotion']='77'
NTIPAliasType['manapotion']='78'
NTIPAliasType['rejuvpotion']='79'
NTIPAliasType['staminapotion']='80'
NTIPAliasType['antidotepotion']='81'
NTIPAliasType['thawingpotion']='82'
NTIPAliasType['smallcharm']='83'
NTIPAliasType['mediumcharm']='84'
NTIPAliasType['largecharm']='85'
NTIPAliasType['amazonbow']='86'
NTIPAliasType['amazonspear']='87'
NTIPAliasType['amazonjavelin']='88'
NTIPAliasType['assassinclaw']='89'
NTIPAliasType['magicbowquiv']='90'
NTIPAliasType['magicxbowquiv']='91'
NTIPAliasType['chippedgem']='92'
NTIPAliasType['flawedgem']='93'
NTIPAliasType['standardgem']='94'
NTIPAliasType['flawlessgem']='95'
NTIPAliasType['perfectgem']='96'
NTIPAliasType['amethyst']='97'
NTIPAliasType['diamond']='98'
NTIPAliasType['emerald']='99'
NTIPAliasType['ruby']='100'
NTIPAliasType['sapphire']='101'
NTIPAliasType['topaz']='102'
NTIPAliasType['skull']='103'
NTIPAliasType['swordsandknives']='104'
NTIPAliasType['spearsandpolearms']='105'
NTIPAliasType['jewel']='58'
NTIPAliasType['classspecific']='59'
NTIPAliasType['amazonitem']='60'
NTIPAliasType['barbarianitem']='61'
NTIPAliasType['necromanceritem']='62'
NTIPAliasType['paladinitem']='63'
NTIPAliasType['sorceressitem']='64'
NTIPAliasType['assassinitem']='65'
NTIPAliasType['druiditem']='66'
NTIPAliasType['handtohand']='67'
NTIPAliasType['orb']='68'
NTIPAliasType['voodooheads']='69'
NTIPAliasType['auricshields']='70'
NTIPAliasType['primalhelm']='71'
NTIPAliasType['pelt']='72'
NTIPAliasType['cloak']='73'
NTIPAliasType['rune']='74'
NTIPAliasType['circlet']='75'
NTIPAliasType['healingpotion']='76'
NTIPAliasType['manapotion']='77'
NTIPAliasType['rejuvpotion']='78'
NTIPAliasType['staminapotion']='79'
NTIPAliasType['antidotepotion']='80'
NTIPAliasType['thawingpotion']='81'
NTIPAliasType['smallcharm']='82'
NTIPAliasType['mediumcharm']='83'
NTIPAliasType['largecharm']='84'
NTIPAliasType['amazonbow']='85'
NTIPAliasType['amazonspear']='86'
NTIPAliasType['amazonjavelin']='87'
NTIPAliasType['assassinclaw']='88'
NTIPAliasType['magicbowquiv']='89'
NTIPAliasType['magicxbowquiv']='90'
NTIPAliasType['chippedgem']='91'
NTIPAliasType['flawedgem']='92'
NTIPAliasType['standardgem']='93'
NTIPAliasType['flawlessgem']='94'
NTIPAliasType['perfectgem']='95'
NTIPAliasType['amethyst']='96'
NTIPAliasType['diamond']='97'
NTIPAliasType['emerald']='98'
NTIPAliasType['ruby']='99'
NTIPAliasType['sapphire']='100'
NTIPAliasType['topaz']='101'
NTIPAliasType['skull']='102'
NTIPAliasType['swordsandknives']='103'
NTIPAliasType['spearsandpolearms']='104'
NTIPAliasType['grimoires']='105'
@@ -28,8 +28,7 @@ UniqueAndSetData = {
"armet": {
"uniques": [
[
"STEELSHADE",
"DARKFEAR"
"STEELSHADE"
]
]
},
@@ -126,6 +125,20 @@ UniqueAndSetData = {
"SPIKETHORN"
]
]
},
"blasphemouscompendium": {
"uniques": [
[
"ARSTORBAALOS"
]
]
},
"blasphemousgrimoire": {
"uniques": [
[
"ARSALDIABOLOS"
]
]
},
"bloodspirit": {
"uniques": [
@@ -222,6 +235,13 @@ UniqueAndSetData = {
]
]
},
"burnttext": {
"uniques": [
[
"MEASUREDWRATH"
]
]
},
"cantortrophy": {
"sets": [
[
@@ -879,6 +899,13 @@ UniqueAndSetData = {
]
]
},
"mirroredboots": {
"sets": [
[
"HORAZONSLEGACY"
]
]
},
"mithrilcoil": {
"uniques": [
[
@@ -905,6 +932,13 @@ UniqueAndSetData = {
]
]
},
"occulttome": {
"uniques": [
[
"ARSDULMEPHISTOS"
]
]
},
"ogregauntlets": {
"uniques": [
[
@@ -1266,6 +1300,11 @@ UniqueAndSetData = {
]
},
"trollbelt": {
"uniques": [
[
"GHEEDSWAGER"
]
],
"sets": [
[
"TRANGOULSGIRTH"
@@ -1427,7 +1466,13 @@ UniqueAndSetData = {
"grandcharm": {
"uniques": [
[
"GHEEDSFORTUNE"
"GHEEDSFORTUNE",
"BLACKCLEFT",
"BONEBREAK",
"COLDRUPTURE",
"CRACKOFTHEHEAVENS",
"FLAMERIFT",
"ROTTINGFISSURE"
]
]
},
@@ -1457,7 +1502,9 @@ UniqueAndSetData = {
"RAVENFROST",
"NATURESPEACE",
"WISPPROJECTOR",
"CARRIONWIND"
"CARRIONWIND",
"OPALVEIN",
"SLING"
]
],
"sets": [
+60 -54
View File
@@ -3,34 +3,35 @@
"""
from functools import cache
import os
import re
import glob
import traceback
from logger import Logger
from dataclasses import dataclass
from nip.lexer import NipSections
from nip.tokens import TokenType
from nip.transpile import (
prepare_nip_expression,
transpile_nip_expression,
from bnip.lexer import BNipSections
from bnip.tokens import TokenType
from bnip.transpile import (
prepare_bnip_expression,
transpile_bnip_expression,
get_section_from_tokens,
NIPExpression,
nip_expressions,
load_nip_expression,
BNIPExpression,
bnip_expressions,
load_bnip_expression,
)
# ! The below imports are necessary, they are used within the eval statements. Your text editor probably is not showing them as not in use.
from nip.NTIPAliasQuality import NTIPAliasQuality
from nip.NTIPAliasClass import NTIPAliasClass
from nip.NTIPAliasClassID import NTIPAliasClassID
from nip.NTIPAliasFlag import NTIPAliasFlag
from nip.NTIPAliasStat import NTIPAliasStat
from nip.NTIPAliasType import NTIPAliasType
from nip.utils import find_unique_or_set_base
from bnip.NTIPAliasQuality import NTIPAliasQuality
from bnip.NTIPAliasClass import NTIPAliasClass
from bnip.NTIPAliasClassID import NTIPAliasClassID
from bnip.NTIPAliasFlag import NTIPAliasFlag
from bnip.NTIPAliasStat import NTIPAliasStat
from bnip.NTIPAliasType import NTIPAliasType
from bnip.utils import find_unique_or_set_base
@cache
def should_keep(item_data) -> tuple[bool, str]:
"""Decides whether or not to keep an item.
Args:
@@ -41,16 +42,17 @@ def should_keep(item_data) -> tuple[bool, str]:
str: The raw expression to use for the keep condition.
"""
for expression in nip_expressions:
for expression in bnip_expressions:
if eval(expression.transpiled):
return True, expression.raw
return False, ""
def _gold_pickup(item_data: dict, expression: NIPExpression) -> bool | None:
@cache
def _gold_pickup(item_data: dict, expression: BNIPExpression) -> bool | None:
res = None
for i, token in enumerate(expression.tokens):
if (
token.type == TokenType.ValueNTIPAliasStat
token.type == TokenType.KeywordNTIPAliasStat
and token.value == str(NTIPAliasStat["gold"])
and "Amount" in item_data
and item_data["Amount"] is not None
@@ -66,17 +68,18 @@ def _gold_pickup(item_data: dict, expression: NIPExpression) -> bool | None:
return res
def _handle_pick_eth_sockets(item_data: dict, expression: NIPExpression) -> tuple[bool, str]:
@cache
def _handle_pick_eth_sockets(item_data: dict, expression: BNIPExpression) -> tuple[bool, str]:
"""Handles the pick condition for eth and sockets.
Args:
item_data (dict): The item data.
expression (NIPExpression): The expression to use.
expression (BNIPExpression): The expression to use.
Returns:
tuple[bool, str]: A tuple containing the following:
bool: Whether or not to keep the item.
NipExpression: The expression object that was used to evaluate the condition.
BNIPExpression: The expression object that was used to evaluate the condition.
"""
expression_raw = prepare_nip_expression(expression.raw)
expression_raw = prepare_bnip_expression(expression.raw)
all_tokens = expression.tokens
tokens_by_section = get_section_from_tokens(all_tokens)
@@ -86,7 +89,7 @@ def _handle_pick_eth_sockets(item_data: dict, expression: NIPExpression) -> tupl
eth = 0 # * -1 = set to false, 0 = not set, 1 = set to true
soc = 0
if eth_keyword_present:
for i, token in enumerate(tokens := tokens_by_section[NipSections.PROP]):
for i, token in enumerate(tokens := tokens_by_section[BNipSections.PROP]):
if token.type == TokenType.ValueNTIPAliasFlag and str(token.value).lower() == "ethereal":
if tokens[i - 1].value == "==":
eth = 1
@@ -95,8 +98,8 @@ def _handle_pick_eth_sockets(item_data: dict, expression: NIPExpression) -> tupl
break
if len(tokens_by_section) > 1 and soc_keyword_present:
for i, token in enumerate(tokens := tokens_by_section[NipSections.STAT]):
if token.type == TokenType.ValueNTIPAliasStat and token.value == str(NTIPAliasStat["sockets"]):
for i, token in enumerate(tokens := tokens_by_section[BNipSections.STAT]):
if token.type == TokenType.KeywordNTIPAliasStat and token.value == str(NTIPAliasStat["sockets"]):
desired_sockets = int(tokens[i + 2].value)
if (desired_sockets > 0 and not (desired_sockets == 1 and tokens[i + 1].value == "<")) or (desired_sockets == 0 and tokens[i + 1].value == ">"):
soc = 1
@@ -125,12 +128,12 @@ def _handle_pick_eth_sockets(item_data: dict, expression: NIPExpression) -> tupl
raw = expression.raw.replace("&& [flag]", "[flag]").replace("|| [flag]", "[flag]")
raw = re.sub(r"\[flag\] (==|!=)\sethereal", "", raw)
# print(f"Modified raw expression: {raw}")
pick_eval_expr = transpile_nip_expression(raw.split("#")[0], isPickUpPhase=True)
pick_eval_expr = transpile_bnip_expression(raw.split("#")[0], isPickUpPhase=True)
# print(f"Modified transpiled expression: {pick_eval_expr}")
return ignore, pick_eval_expr
@cache
def should_pickup(item_data) -> tuple[bool, str]:
"""Decides whether or not to keep an item.
Args:
@@ -144,7 +147,7 @@ def should_pickup(item_data) -> tuple[bool, str]:
pick_eval_expr = ""
item_is_gold = item_data["BaseItem"]["DisplayName"] == "Gold"
for expression in nip_expressions:
for expression in bnip_expressions:
if expression.raw:
# check gold
if item_is_gold and "[gold]" in expression.raw.lower():
@@ -164,7 +167,7 @@ def should_pickup(item_data) -> tuple[bool, str]:
return False, ""
@cache
def should_id(item_data) -> bool:
"""Checks if the item should be identified.
@@ -181,7 +184,7 @@ def should_id(item_data) -> bool:
[name] == ring && [quality] == rare -> True
[name] == ring && [quality] == rare # [strength] == 5 -> Falsep
"""
for expression in nip_expressions:
for expression in bnip_expressions:
if expression and expression.should_id_transpiled:
split_expression = expression.raw.split("#")
if "[idname]" in expression.raw.lower():
@@ -191,21 +194,21 @@ def should_id(item_data) -> bool:
return False
return True
def _load_nip_expressions(filepath):
def _load_bnip_expressions(filepath):
"""
Loads the NIP expressions from the file.
Loads the BNIP expressions from the file.
Args:
filepath (str): The path to the file.
Returns:
None
"""
with open(filepath, "r") as f:
with open(filepath, "r", encoding="utf-8") as f:
for i, line in enumerate(f):
line = line.strip()
if line == "" or line.startswith("//"): # Empty or comment line
continue
try:
load_nip_expression(line)
load_bnip_expression(line)
except Exception as e:
filepath = filepath.replace("\\", "/")
file = filepath.split('/config/')[1]
@@ -214,36 +217,39 @@ def _load_nip_expressions(filepath):
break
default_nip_file_path = f"{os.getcwd()}/config/default.nip"
nip_path = f"{os.getcwd()}/config/nip"
glob_nip_path = os.path.join(nip_path, '**', '*.nip')
nip_file_paths = glob.glob(glob_nip_path, recursive=True)
default_bnip_file_path = f"{os.getcwd()}/config/default.bnip"
bnip_path = f"{os.getcwd()}/config/bnip"
# * Remove all directories or files that are in the .nipignore file from nip_file_paths. (accepts glob patterns)
if os.path.isfile(os.path.join(nip_path, '.nipignore')):
with open(os.path.join(nip_path, '.nipignore'), "r") as f:
glob_bnip_path = os.path.join(bnip_path, '**', '*.bnip')
glob_nip_path = os.path.join(bnip_path, '**', '*.nip')
bnip_file_paths = glob.glob(glob_bnip_path, recursive=True) + glob.glob(glob_nip_path, recursive=True)
# * Remove all directories or files that are in the .nipignore file from bnip_file_paths. (accepts glob patterns)
if os.path.isfile(os.path.join(bnip_path, '.nipignore')):
with open(os.path.join(bnip_path, '.nipignore'), "r") as f:
for line in f:
line = line.strip()
line = line.replace("/", "\\")
remove_files = glob.glob(os.path.join(nip_path, line), recursive=True)
remove_files = glob.glob(os.path.join(bnip_path, line), recursive=True)
for remove_file in remove_files:
if remove_file in nip_file_paths:
nip_file_paths.remove(remove_file)
if remove_file in bnip_file_paths:
bnip_file_paths.remove(remove_file)
num_files = 0
# load all nip expressions
if len(nip_file_paths) > 0:
num_files = len(nip_file_paths)
for nip_file_path in nip_file_paths:
_load_nip_expressions(nip_file_path)
if len(bnip_file_paths) > 0:
num_files = len(bnip_file_paths)
for bnip_file_path in bnip_file_paths:
_load_bnip_expressions(bnip_file_path)
# fallback to default nip file if no custom nip files specified or existing files are excluded
else:
num_files = 1
_load_nip_expressions(default_nip_file_path)
Logger.warning("No .nip files in config/nip/, fallback to default.nip")
Logger.info(f"Loaded {num_files} nip files with {len(nip_expressions)} total expressions.")
_load_bnip_expressions(default_bnip_file_path)
Logger.warning("No .bnip files in config/nip/, fallback to default.bnip")
Logger.info(f"Loaded {num_files} nip files with {len(bnip_expressions)} total expressions.")
nip_expressions = sorted(nip_expressions, key=lambda x: len(x.raw))
bnip_expressions = sorted(bnip_expressions, key=lambda x: len(x.raw))
if __name__ == "__main__":
print(transpile_nip_expression("[name] == ring && [quality] == rare"))
print(transpile_bnip_expression("[name] == ring && [quality] == rare # [strength] == 5"))
+63 -63
View File
@@ -1,24 +1,25 @@
"""
Lexer for NIP expressions.
Lexer for BNIP expressions.
"""
from dataclasses import dataclass
from logger import Logger
from nip.NTIPAliasQuality import NTIPAliasQuality
from nip.NTIPAliasClass import NTIPAliasClass
from nip.NTIPAliasClassID import NTIPAliasClassID
from nip.NTIPAliasFlag import NTIPAliasFlag
from nip.NTIPAliasStat import NTIPAliasStat
from nip.NTIPAliasType import NTIPAliasType
from nip.tokens import Token, TokenType
from bnip.NTIPAliasQuality import NTIPAliasQuality
from bnip.NTIPAliasClass import NTIPAliasClass
from bnip.NTIPAliasClassID import NTIPAliasClassID
from bnip.NTIPAliasFlag import NTIPAliasFlag
from bnip.NTIPAliasStat import NTIPAliasStat
from bnip.NTIPAliasType import NTIPAliasType
from bnip.tokens import Token, TokenType
from bnip.BNipExceptions import BNipSyntaxError
from enum import Enum
import re
from colorama import init, Fore
from rapidfuzz.string_metric import levenshtein
init()
try:
from rapidfuzz.string_metric import levenshtein
except ImportError:
from rapidfuzz.distance import Levenshtein as levenshtein
WHITESPACE = " \t\n\r\v\f"
DIGITS = "0123456789.-" # ! Put % back in here when ready to use percentages.
@@ -26,37 +27,24 @@ SYMBOLS = [">", "=> ", "<", "<=", "=", "!", "", "", ",", "&", "|", "#"]
MATH_SYMBOLS = ["(", ")", "^", "*", "/", "\\", "+", "-"]
CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'"
class NipSections(Enum):
class BNipSections(Enum):
PROP = 1
STAT = 2
MAXQUANTITY = 3
class NipSyntaxError(Exception):
def __init__(self, ecode: str|int = 0, message: str = '', expression: str = ''):
self.message = message
self.type = type
self.ecode = ecode
self.expression = expression
def __str__(self):
return f"{Fore.RED}{self.ecode}:{Fore.CYAN}{self.message}:{Fore.YELLOW}{self.expression.strip()}{Fore.WHITE}"
class Lexer:
def __init__(self):
self.current_section: NipSections = NipSections.PROP
self.current_section: BNipSections = BNipSections.PROP
self.current_token: str | None = ""
self.text_i: int = -1
self.tokens: list[Token] = []
def _increment_section(self):
if self.current_section == NipSections.PROP:
self.current_section = NipSections.STAT
elif self.current_section == NipSections.STAT:
self.current_section = NipSections.MAXQUANTITY
if self.current_section == BNipSections.PROP:
self.current_section = BNipSections.STAT
elif self.current_section == BNipSections.STAT:
self.current_section = BNipSections.MAXQUANTITY
def _get_text(self):
@@ -77,32 +65,42 @@ class Lexer:
self.current_token = None
def create_tokens(self, nip_expression: str, starting_section: NipSections = NipSections.PROP):
"""Creates token from a nip expression string
def create_tokens(self, bnip_expression: str, starting_section: BNipSections = BNipSections.PROP):
"""Creates token from a bnip expression string
Args:
nip_expression (str): the nip expression string
starting_section (NipSections): the section to start parsing from
bnip_expression (str): the bnip expression string
starting_section (BNipSections): the section to start parsing from
Returns:
A list of tokens
Raises:
NipSyntaxError: If there is a syntax error in the nip expression
BNipSyntaxError: If there is a syntax error in the bnip expression
"""
self.current_section = starting_section
self.text = list(nip_expression)
self.text = list(bnip_expression)
self._advance()
self.tokens = []
while self.current_token != None:
if self.current_token == "-": # * Since - is a math symbol and a negative sign for numbers, we need to handle it differently.
if self.tokens[-1].type == TokenType.NUMBER:
NTIPAliasKeywords = [
TokenType.KeywordNTIPAliasClass,
TokenType.KeywordNTIPAliasFlag,
TokenType.KeywordNTIPAliasIDName,
TokenType.KeywordNTIPAliasMaxQuantity,
TokenType.KeywordNTIPAliasName,
TokenType.KeywordNTIPAliasQuality,
TokenType.KeywordNTIPAliasStat,
TokenType.KeywordNTIPAliasType,
]
if self.tokens[-1].type in NTIPAliasKeywords + [TokenType.NUMBER]:
self.tokens.append(self._create_math_operator())
self._advance()
else:
self.tokens.append(self._create_digits())
continue
if self.current_token in DIGITS:
self.tokens.append(self._create_digits())
elif self.current_token in WHITESPACE:
@@ -116,20 +114,20 @@ class Lexer:
self.tokens.append(self._create_keyword_lookup())
elif self.current_token in CHARS:
self.tokens.append(self._create_d2r_image_data_lookup())
elif self.current_section == NipSections.PROP and self.text_i == 0 and self.current_token == "@":
elif self.current_section == BNipSections.PROP and self.text_i == 0 and self.current_token == "@":
self.tokens.append(Token(TokenType.NOTIFICATION, '@'))
self._advance()
else:
raise NipSyntaxError("NIP_0x1", "Unknown token: " + self.current_token, self._get_text())
raise BNipSyntaxError("BNIP_0x1", f"Unknown token: '{self.current_token}'", self._get_text())
return self.tokens
def detokenize(self, tokens: list[Token]) -> str:
"""Detokenizes a list of tokens into a nip expression string
"""Detokenizes a list of tokens into a bnip expression string
Args:
tokens (list[Token]): the list of tokens to detokenize
Returns:
A nip expression string
A bnip expression string
Raises:
None
@@ -167,7 +165,6 @@ class Lexer:
TokenType.KeywordNTIPAliasQuality: '[quality]',
TokenType.KeywordNTIPAliasType: '[type]',
TokenType.ValueNTIPAlias: '{}',
TokenType.ValueNTIPAliasClass: '{}',
TokenType.ValueNTIPAliasClassID: '{}',
TokenType.ValueNTIPAliasFlag: '{}',
@@ -223,10 +220,13 @@ class Lexer:
found_whole_number = re.match(r"^-*[0-9]+", self._get_current_iteration_of_text_raw())
if found_whole_number:
return self._create_custom_digit_token(found_whole_number.group(0))
return Token(TokenType.UNKNOWN, self.current_token)
if self.current_token:
return Token(TokenType.UNKNOWN, self.current_token)
else:
return Token(TokenType.UNKNOWN, "")
def _create_math_operator(self):
def _create_math_operator(self) -> Token:
symbol_map = {
'+': TokenType.PLUS,
'-': TokenType.MINUS,
@@ -240,11 +240,11 @@ class Lexer:
symbol = self.current_token
if symbol in symbol_map:
return Token(symbol_map[symbol], symbol)
return Token(TokenType.UNKNOWN, symbol)
if symbol:
if symbol in symbol_map:
return Token(symbol_map[symbol], symbol)
return Token(TokenType.UNKNOWN, symbol)
return Token(TokenType.UNKNOWN, "")
def _create_keyword_lookup(self) -> Token:
"""
item data lookup i.e [name]
@@ -259,9 +259,9 @@ class Lexer:
lookup_key += char
self._advance()
else:
raise NipSyntaxError("NIP_0x2", "Missing ] after keyword", self._get_text())
raise BNipSyntaxError("BNIP_0x2", "Missing ] after keyword", self._get_text())
if lookup_key:
if self.current_section == NipSections.PROP:
if self.current_section == BNipSections.PROP:
match lookup_key:
case "name":
return Token(TokenType.KeywordNTIPAliasName, lookup_key)
@@ -289,20 +289,20 @@ class Lexer:
Logger.warning(f"Unknown property lookup: \"{lookup_key}\" {''.join(self.text)} {self.current_section}")
return Token(TokenType.UNKNOWN, lookup_key)
elif self.current_section == NipSections.STAT:
elif self.current_section == BNipSections.STAT:
if lookup_key in NTIPAliasStat:
return Token(TokenType.ValueNTIPAliasStat, NTIPAliasStat[lookup_key])
return Token(TokenType.KeywordNTIPAliasStat, NTIPAliasStat[lookup_key])
else:
# spell_check = ""
# for key in NTIPAliasStat:
# if levenshtein(lookup_key, key) < 3:
# spell_check = f", did you mean {key}?"
# raise NipSyntaxError("NIP_0x3", f"Unknown NTIPStat lookup: {lookup_key}{spell_check}", self._get_text())
# raise BNipSyntaxError("BNIP_0x3", f"Unknown NTIPStat lookup: {lookup_key}{spell_check}", self._get_text())
return Token(TokenType.UNKNOWN, lookup_key)
elif self.current_section == NipSections.MAXQUANTITY:
elif self.current_section == BNipSections.MAXQUANTITY:
pass
return Token(TokenType.UNKNOWN, lookup_key)
return Token(TokenType.UNKNOWN, lookup_key)
def _create_d2r_image_data_lookup(self) -> Token:
lookup_key = ""
@@ -315,7 +315,7 @@ class Lexer:
self._advance()
lookup_key = found
if self.current_section == NipSections.PROP:
if self.current_section == BNipSections.PROP:
# TODO: The second checks (i.e NTIPAliasClass and self.tokens[-2].type == TokenType.CLASS:) seem a little misplaced, possibly put them inside the validation function that is inside transpiler.py and throw a warning accordingly.
if len(self.tokens) >= 2:
if lookup_key in NTIPAliasClass and self.tokens[-2].type == TokenType.KeywordNTIPAliasClass:
@@ -331,9 +331,9 @@ class Lexer:
elif self.tokens[-2].type == TokenType.KeywordNTIPAliasIDName:
return Token(TokenType.ValueNTIPAliasIDName, lookup_key)
else:
raise NipSyntaxError("NIP_0x20", f"Bad token sequence: {self._get_text()}", self._get_text())
raise BNipSyntaxError("BNIP_0x20", f"Bad token sequence: {self._get_text()}", self._get_text())
return Token(TokenType.UNKNOWN, lookup_key)
elif self.current_section == NipSections.STAT:
elif self.current_section == BNipSections.STAT:
if lookup_key in NTIPAliasStat:
return Token(TokenType.ValueNTIPAliasStat, lookup_key)
else:
@@ -371,4 +371,4 @@ class Lexer:
pythonic_operator = found_text.replace("#", "and").replace("||", "or").replace("&&", "and")
return Token(logical_operator_map[found_text], pythonic_operator)
else:
raise NipSyntaxError("NIP_0x5", f"Invalid logical operator: '{char}'", self._get_text())
raise BNipSyntaxError("BNIP_0x5", f"Invalid logical operator: '{char}'", self._get_text())
+3 -5
View File
@@ -27,12 +27,12 @@ class TokenType(Enum):
KeywordNTIPAliasClass = auto()
KeywordNTIPAliasFlag = auto()
KeywordNTIPAliasIDName = auto()
KeywordNTIPAliasMaxQuantity = auto()
KeywordNTIPAliasName = auto()
KeywordNTIPAliasQuality = auto()
KeywordNTIPAliasType = auto()
KeywordNTIPAliasStat = auto()
KeywordNTIPAliasMaxQuantity = auto()
ValueNTIPAlias = auto()
ValueNTIPAliasClass = auto()
ValueNTIPAliasClassID = auto()
ValueNTIPAliasFlag = auto()
@@ -50,12 +50,10 @@ class TokenType(Enum):
UNKNOWN = auto()
@dataclass
class Token:
type: TokenType
value: str | int
value: str | int | float
def __repr__(self) -> str:
return f"{self.type} : {self.value}"
+74 -68
View File
@@ -1,19 +1,20 @@
from nip.NTIPAliasQuality import NTIPAliasQuality
from nip.NTIPAliasClass import NTIPAliasClass
from nip.NTIPAliasClassID import NTIPAliasClassID
from nip.NTIPAliasFlag import NTIPAliasFlag
from nip.NTIPAliasStat import NTIPAliasStat
from nip.NTIPAliasType import NTIPAliasType
from bnip.NTIPAliasQuality import NTIPAliasQuality
from bnip.NTIPAliasClass import NTIPAliasClass
from bnip.NTIPAliasClassID import NTIPAliasClassID
from bnip.NTIPAliasFlag import NTIPAliasFlag
from bnip.NTIPAliasStat import NTIPAliasStat
from bnip.NTIPAliasType import NTIPAliasType
# ! The above imports are necessary, they are used within the eval statements. Your text editor probably is not showing them as not in use.
from dataclasses import dataclass
from nip.lexer import Lexer, NipSyntaxError, NipSections
from nip.tokens import Token, TokenType
from nip.utils import find_unique_or_set_base
from bnip.lexer import Lexer, BNipSections
from bnip.BNipExceptions import BNipSyntaxError
from bnip.tokens import Token, TokenType
from bnip.utils import find_unique_or_set_base
@dataclass
class NIPExpression:
class BNIPExpression:
raw: str
should_id_transpiled: str | None
transpiled: str
@@ -21,7 +22,7 @@ class NIPExpression:
tokens: list[Token]
nip_expressions: list[NIPExpression] = []
bnip_expressions: list[BNIPExpression] = []
token_transpile_map = {
@@ -72,8 +73,10 @@ def transpile(tokens: list[Token], isPickUpPhase: bool = False, transpiled_expre
if tokens[i + 1].type != TokenType.ValueNTIPAliasFlag:
expression += token_value
case TokenType.EQ:
if tokens[i + 1].type == TokenType.ValueNTIPAliasFlag: continue
if isPickUpPhase and tokens[i + 1].type == TokenType.ValueNTIPAliasIDName: continue
if tokens[i + 1].type == TokenType.ValueNTIPAliasFlag:
continue
if isPickUpPhase and tokens[i + 1].type == TokenType.ValueNTIPAliasIDName:
continue
expression += "=="
case TokenType.OR | TokenType.AND | TokenType.LPAREN | TokenType.RPAREN | TokenType.PLUS | TokenType.MINUS | TokenType.MULTIPLY | TokenType.DIVIDE:
expression += token_value
@@ -86,7 +89,7 @@ def transpile(tokens: list[Token], isPickUpPhase: bool = False, transpiled_expre
section_open_paranthesis_count -= 1
expression += "and"
section_start = True
case TokenType.ValueNTIPAliasStat:
case TokenType.KeywordNTIPAliasStat:
expression += "(int(item_data.get('NTIPAliasStat', {})" + f".get('{token_value}', 0)))"
case TokenType.KeywordNTIPAliasIDName:
if not isPickUpPhase:
@@ -130,6 +133,7 @@ def validate_correct_math_syntax(left_token=None, right_token=None):
TokenType.KeywordNTIPAliasName,
TokenType.KeywordNTIPAliasQuality,
TokenType.KeywordNTIPAliasType,
TokenType.KeywordNTIPAliasStat,
TokenType.ValueNTIPAliasClass,
TokenType.ValueNTIPAliasClassID,
@@ -144,17 +148,17 @@ def validate_correct_math_syntax(left_token=None, right_token=None):
TokenType.RPAREN,
]
if left_token and left_token.type not in allowed_left_and_right_tokens:
raise NipSyntaxError("NIP_0x3", "unexpected token on left of math operator")
raise BNipSyntaxError2("BNIP_0x3", "unexpected token on left of math operator")
if right_token and right_token.type not in allowed_left_and_right_tokens:
raise NipSyntaxError("NIP_0x4", "unexpected token on right of math operator")
raise BNipSyntaxError2("BNIP_0x4", "unexpected token on right of math operator")
OPENING_PARENTHESIS_COUNT = 0 # * This needs to be reset every time an validation error occurs
oNipSyntaxError = NipSyntaxError
def NipSyntaxError(error_code, error_message): # * "hook" the error constructor so every time the error is raised / called, we reset the opening parenthesis count
CURRENT_EXPRESSION = ""
def BNipSyntaxError2(error_code, error_message): # * "hook" the error constructor so every time the error is raised / called, we reset the opening parenthesis count
global OPENING_PARENTHESIS_COUNT
OPENING_PARENTHESIS_COUNT = 0
return oNipSyntaxError(error_code, error_message)
print(CURRENT_EXPRESSION)
return BNipSyntaxError(error_code, error_message, CURRENT_EXPRESSION)
def validate_correct_parenthesis_syntax(current_pos, all_tokens, left_token=None, right_token=None):
@@ -183,10 +187,10 @@ def validate_correct_parenthesis_syntax(current_pos, all_tokens, left_token=None
if token.type == TokenType.LPAREN:
OPENING_PARENTHESIS_COUNT += 1
if left_token and left_token.type not in allowed_left_and_right_tokens + [TokenType.LPAREN]:
raise NipSyntaxError("NIP_0x18", "unexpected token on left of parenthesis")
raise BNipSyntaxError2("BNIP_0x18", "unexpected token on left of parenthesis")
elif token.type == TokenType.RPAREN:
if right_token and right_token.type not in allowed_left_and_right_tokens + [TokenType.RPAREN]:
raise NipSyntaxError("NIP_0x19", "unexpected token on right of parenthesis")
raise BNipSyntaxError2("BNIP_0x19", "unexpected token on right of parenthesis")
OPENING_PARENTHESIS_COUNT -= 1
@@ -196,16 +200,16 @@ def validate_correct_parenthesis_syntax(current_pos, all_tokens, left_token=None
# for i in range(current_pos, -1, -1):
# # print(all_tokens[i].type)
# if all_tokens[i].type == TokenType.SECTIONAND:
# raise NipSyntaxError("NIP_0x8", "parenthesis cannot cross the section and (#)")
# raise BNipSyntaxError2("BNIP_0x8", "parenthesis cannot cross the section and (#)")
if current_pos == len(all_tokens) - 1:
if OPENING_PARENTHESIS_COUNT != 0:
if OPENING_PARENTHESIS_COUNT > 0:
OPENING_PARENTHESIS_COUNT = 0
raise NipSyntaxError("NIP_0x6", "unclosed parenthesis")
raise BNipSyntaxError2("BNIP_0x6", "unclosed parenthesis")
else:
OPENING_PARENTHESIS_COUNT = 0
raise NipSyntaxError("NIP_0x7", "unopened parenthesis")
raise BNipSyntaxError2("BNIP_0x7", "unopened parenthesis")
OPENING_PARENTHESIS_COUNT = 0
def validate_digits_syntax(left=None, right=None):
@@ -236,10 +240,10 @@ def validate_digits_syntax(left=None, right=None):
if left:
if left.type not in allowed_left_and_right_tokens:
raise NipSyntaxError("NIP_0x8", "Expected operator on left of number")
raise BNipSyntaxError2("BNIP_0x8", "Expected operator on left of number")
if right:
if right.type not in allowed_left_and_right_tokens:
raise NipSyntaxError("NIP_0x9", "Expected operator on right of number")
raise BNipSyntaxError2("BNIP_0x9", "Expected operator on right of number")
def validate_logical_operators(left=None, right=None):
@@ -262,37 +266,39 @@ def validate_logical_operators(left=None, right=None):
TokenType.KeywordNTIPAliasName,
TokenType.KeywordNTIPAliasIDName,
TokenType.KeywordNTIPAliasMaxQuantity,
TokenType.KeywordNTIPAliasStat
]
if left:
if left.type not in allowed_left_and_right_tokens + [TokenType.RPAREN]:
raise NipSyntaxError("NIP_0x10", "Expected token on left of logical operator")
raise BNipSyntaxError2("BNIP_0x10", "Expected token on left of logical operator")
if right:
if right.type not in allowed_left_and_right_tokens + [TokenType.LPAREN]:
raise NipSyntaxError("NIP_0x11", "Expected token on right of logical operator")
raise BNipSyntaxError2("BNIP_0x11", "Expected token on right of logical operator")
else:
raise NipSyntaxError("NIP_0x12", "Expected token on right of logical operator")
raise BNipSyntaxError2("BNIP_0x12", "Expected token on right of logical operator")
def validate_nip_expression_syntax(nip_expression): # * enforces that {property} # {stats} # {maxquantity}
def validate_bnip_expression_syntax(bnip_expression): # * enforces that {property} # {stats} # {maxquantity}
tokens = None
if not nip_expression:
global CURRENT_EXPRESSION
if not bnip_expression:
return
all_tokens = []
CURRENT_EXPRESSION = bnip_expression
split_nip_expression = nip_expression.split("#")
split_nip_expression_len = len(split_nip_expression)
if split_nip_expression_len >= 1: # property
tokens = Lexer().create_tokens(split_nip_expression[0], NipSections.PROP)
split_bnip_expression = bnip_expression.split("#")
split_bnip_expression_len = len(split_bnip_expression)
if split_bnip_expression_len >= 1: # property
tokens = Lexer().create_tokens(split_bnip_expression[0], BNipSections.PROP)
all_tokens.extend(tokens)
for token in tokens:
if token.type == TokenType.ValueNTIPAliasStat or token.type == TokenType.UNKNOWN:
raise NipSyntaxError("NIP_0x13", f"Invalid token '{token.value}' in property section")
if split_nip_expression_len >= 2: # stats
raise BNipSyntaxError2("BNIP_0x13", f"Invalid token '{token.value}' in property section")
if split_bnip_expression_len >= 2: # stats
all_tokens.append(Token(TokenType.SECTIONAND, "#"))
tokens = Lexer().create_tokens(split_nip_expression[1], NipSections.STAT)
tokens = Lexer().create_tokens(split_bnip_expression[1], BNipSections.STAT)
all_tokens.extend(tokens)
for token in tokens:
is_invalid_stat_lookup = (
@@ -305,11 +311,11 @@ def validate_nip_expression_syntax(nip_expression): # * enforces that {property}
)
if is_invalid_stat_lookup:
raise NipSyntaxError("NIP_0x14", f"Invalid token '{token.value}' in stats section")
raise BNipSyntaxError2("BNIP_0x14", f"Invalid token '{token.value}' in stats section")
if split_nip_expression_len >= 3: # maxquantity
if split_bnip_expression_len >= 3: # maxquantity
# all_tokens.append(Token(TokenType.SECTIONAND, "#"))
tokens = Lexer().create_tokens(split_nip_expression[2], NipSections.MAXQUANTITY)
tokens = Lexer().create_tokens(split_bnip_expression[2], BNipSections.MAXQUANTITY)
all_tokens.extend(tokens)
for token in tokens:
is_invalid_maxquantity_lookup = (
@@ -324,12 +330,12 @@ def validate_nip_expression_syntax(nip_expression): # * enforces that {property}
if is_invalid_maxquantity_lookup:
pass
raise NipSyntaxError("NIP_0x15", "Invalid maxquantity lookup")
raise BNipSyntaxError2("BNIP_0x15", "Invalid maxquantity lookup")
# * Further syntax validation
# print(all_tokens)
if all_tokens[-1].type == TokenType.SECTIONAND:
raise NipSyntaxError("NIP_0x16", "unexpected sectionand (#) at end of expression")
raise BNipSyntaxError2("BNIP_0x16", "unexpected sectionand (#) at end of expression")
math_tokens = [TokenType.MULTIPLY, TokenType.PLUS, TokenType.MINUS, TokenType.DIVIDE, TokenType.MODULO, TokenType.POW]
logical_tokens = [TokenType.AND, TokenType.OR, TokenType.EQ, TokenType.NE, TokenType.GT, TokenType.LT, TokenType.GE, TokenType.LE, TokenType.SECTIONAND]
for i, token in enumerate(all_tokens):
@@ -344,7 +350,7 @@ def validate_nip_expression_syntax(nip_expression): # * enforces that {property}
if token.type == TokenType.EQ:
if i == len(all_tokens) - 1: # * Check to make sure the next token is a token.
# ! the logic only makes sense for the last token, what the f*ck
raise NipSyntaxError("NIP_0x17", "No value after equal sign")
raise BNipSyntaxError2("BNIP_0x17", "No value after equal sign")
elif token.type in math_tokens:
validate_correct_math_syntax(left_token=left, right_token=right)
# * Make sure two numbers aren't next to each other.
@@ -371,7 +377,7 @@ def remove_quantity(expression): # ! This is a bit ghetto, but since we're not u
return expression
def get_section_from_tokens(all_tokens: list[Token], section: NipSections | None = None) -> dict[NipSections, list[Token]]:
def get_section_from_tokens(all_tokens: list[Token], section: BNipSections | None = None) -> dict[BNipSections, list[Token]]:
tokens_split_by_section = []
temp_section = []
@@ -387,29 +393,29 @@ def get_section_from_tokens(all_tokens: list[Token], section: NipSections | None
temp_section.append(token)
section_tokens_len = len(tokens_split_by_section)
nip_map = {
NipSections.PROP: tokens_split_by_section[0] if section_tokens_len >= 1 else [],
NipSections.STAT: tokens_split_by_section[1] if section_tokens_len >= 2 else [],
NipSections.MAXQUANTITY: tokens_split_by_section[2] if section_tokens_len >= 3 else []
bnip_map = {
BNipSections.PROP: tokens_split_by_section[0] if section_tokens_len >= 1 else [],
BNipSections.STAT: tokens_split_by_section[1] if section_tokens_len >= 2 else [],
BNipSections.MAXQUANTITY: tokens_split_by_section[2] if section_tokens_len >= 3 else []
}
return nip_map
return bnip_map
def prepare_nip_expression(expression: str) -> str:
def prepare_bnip_expression(expression: str) -> str:
if not expression.startswith("//") and not expression.startswith("-"):
expression = expression.lower()
expression = expression.replace("'", "")
expression = expression.split("//")[0] # * Ignore the comments inside the nip expression
expression = remove_quantity(expression)
if validate_nip_expression_syntax(expression):
if validate_bnip_expression_syntax(expression):
return expression
return ''
def transpile_nip_expression(expression: str | list[Token], isPickUpPhase=False):
def transpile_bnip_expression(expression: str | list[Token], isPickUpPhase=False):
if isinstance(expression, str):
expression = prepare_nip_expression(expression)
expression = prepare_bnip_expression(expression)
if expression:
tokens = Lexer().create_tokens(expression)
transpiled_expression = transpile(tokens, isPickUpPhase=isPickUpPhase)
@@ -420,27 +426,27 @@ def transpile_nip_expression(expression: str | list[Token], isPickUpPhase=False)
if transpiled_expression:
return transpiled_expression
def generate_expression_object(nip_expression: str) -> NIPExpression | None:
nip_expression = prepare_nip_expression(nip_expression)
def generate_expression_object(bnip_expression: str) -> BNIPExpression | None:
bnip_expression = prepare_bnip_expression(bnip_expression)
if nip_expression:
tokens = Lexer().create_tokens(nip_expression)
if transpiled_expression := transpile_nip_expression(tokens):
if bnip_expression:
tokens = Lexer().create_tokens(bnip_expression)
if transpiled_expression := transpile_bnip_expression(tokens):
split_tokens = get_section_from_tokens(tokens)
expression_obj = NIPExpression(
raw=nip_expression,
expression_obj = BNIPExpression(
raw=bnip_expression,
tokens=tokens,
transpiled=transpiled_expression,
should_id_transpiled=transpile_nip_expression(split_tokens[NipSections.PROP]),
should_pickup=transpile_nip_expression(split_tokens[NipSections.PROP], isPickUpPhase=True) # * Some stuff gets transpiled differently in the pickup phase
should_id_transpiled=transpile_bnip_expression(split_tokens[BNipSections.PROP]),
should_pickup=transpile_bnip_expression(split_tokens[BNipSections.PROP], isPickUpPhase=True) # * Some stuff gets transpiled differently in the pickup phase
)
return expression_obj
return None
def load_nip_expression(nip_expression: str):
if (expression_obj := generate_expression_object(nip_expression)) is not None:
nip_expressions.append(expression_obj)
def load_bnip_expression(bnip_expression: str):
if (expression_obj := generate_expression_object(bnip_expression)) is not None:
bnip_expressions.append(expression_obj)
+2 -2
View File
@@ -1,4 +1,4 @@
from nip.UniqueAndSetData import UniqueAndSetData
from bnip.UniqueAndSetData import UniqueAndSetData
def find_unique_or_set_base(unique_or_set_name) -> tuple[str, str]:
@@ -14,4 +14,4 @@ def find_unique_or_set_base(unique_or_set_name) -> tuple[str, str]:
for set in sets:
if set.lower() == unique_or_set_name:
return key, "set"
return "",""
raise NipSyntaxError("NIP_0x23", f"'{unique_or_set_name}' is not a valid unique or set")
+181 -30
View File
@@ -1,6 +1,6 @@
from transitions import Machine
import time
import keyboard
from input_layer import keyboard
import time
import os
import random
@@ -16,16 +16,18 @@ from utils.restart import safe_exit, restart_game
from game_stats import GameStats
from logger import Logger
from config import Config
from screen import grab
from screen import grab, convert_monitor_to_screen, convert_screen_to_abs, convert_abs_to_monitor, convert_screen_to_monitor
import template_finder
from char import IChar
from item.pickit import PickIt
from item import consumables
from pather import Pather, Location
from char.sorceress import LightSorc, BlizzSorc, NovaSorc,HydraSorc
from char.sorceress import LightSorc, BlizzSorc, BlizzorbSorc, NovaSorc, HydraSorc
from char.trapsin import Trapsin
from char.paladin.hammerdin import Hammerdin
from char.paladin import FoHdin
from char.warlock import FireLock, AbyssLock, EchoLock
from char.amazon.javazon import Javazon
from char.barbarian import Barbarian
from char.necro import Necro
from char.poison_necro import Poison_Necro
@@ -36,10 +38,16 @@ from ui_manager import wait_until_hidden, wait_until_visible, ScreenObjects, is_
from ui import meters, skills, view, character_select, main_menu
from inventory import personal, vendor, belt, common
from run import Pindle, ShenkEld, Trav, Nihlathak, Arcane, Diablo
from run import Pindle, ShenkEld, Trav, Nihlathak, Arcane, Diablo, Vizier, Level
from run.baal import Baal
from run.mephisto import Mephisto
from run.andariel import Andariel
from run.countess import Countess
from town import TownManager, A1, A2, A3, A4, A5, town_manager
from messages import Messenger
from threading import Lock
from utils.stealth import maybe_afk_break, should_skip_run
class Bot:
@@ -48,6 +56,7 @@ class Bot:
self._messenger = Messenger()
self._pather = Pather()
self._pickit = PickIt()
self._stash_mutex = Lock()
# Create Character
match Config().char["type"]:
@@ -55,6 +64,8 @@ class Bot:
self._char: IChar = LightSorc(Config().light_sorc, self._pather)
case "blizz_sorc":
self._char: IChar = BlizzSorc(Config().blizz_sorc, self._pather)
case "blizzorb_sorc":
self._char: IChar = BlizzorbSorc(Config().blizzorb_sorc, self._pather)
case "nova_sorc":
self._char: IChar = NovaSorc(Config().nova_sorc, self._pather)
case "hydra_sorc":
@@ -62,7 +73,15 @@ class Bot:
case "hammerdin" | "paladin":
self._char: IChar = Hammerdin(Config().hammerdin, self._pather, self._pickit) #pickit added for diablo
case "fohdin":
self._char: IChar = FoHdin(Config().fohdin, self._pather, self._pickit) #pickit added for diablo
self._char: IChar = FoHdin(Config().fohdin, self._pather, self._pickit)
case "abyss_lock" | "warlock":
self._char: IChar = AbyssLock(Config().abyss_lock, self._pather)
case "fire_lock":
self._char: IChar = FireLock(Config().fire_lock, self._pather)
case "echo_lock":
self._char: IChar = EchoLock(Config().echo_lock, self._pather)
case "amazon" | "javazon":
self._char: IChar = Javazon(Config().javazon, self._pather)
case "trapsin":
self._char: IChar = Trapsin(Config().trapsin, self._pather)
case "barbarian":
@@ -91,28 +110,39 @@ class Bot:
# Create runs
self._do_runs = {
"run_level": Config().routes.get("run_level"),
"run_trav": Config().routes.get("run_trav"),
"run_pindle": Config().routes.get("run_pindle"),
"run_shenk": Config().routes.get("run_eldritch") or Config().routes.get("run_eldritch_shenk"),
"run_nihlathak": Config().routes.get("run_nihlathak"),
"run_arcane": Config().routes.get("run_arcane"),
"run_diablo": Config().routes.get("run_diablo"),
"run_vizier": Config().routes.get("run_vizier"),
"run_baal": Config().routes.get("run_baal"),
"run_mephisto": Config().routes.get("run_mephisto"),
"run_andariel": Config().routes.get("run_andariel"),
"run_countess": Config().routes.get("run_countess"),
}
# Adapt order to the config
self._do_runs = OrderedDict((k, self._do_runs[k]) for k in Config().routes_order if k in self._do_runs and self._do_runs[k])
runs = list(self._do_runs.keys())
self._do_runs_reset = copy(self._do_runs)
Logger.info(f"Doing runs: {self._do_runs_reset.keys()}")
if Config().general["randomize_runs"]:
self.shuffle_runs()
self._pindle = Pindle(self._pather, self._town_manager, self._char, self._pickit, runs)
self._shenk = ShenkEld(self._pather, self._town_manager, self._char, self._pickit, runs)
self._trav = Trav(self._pather, self._town_manager, self._char, self._pickit, runs)
self._nihlathak = Nihlathak(self._pather, self._town_manager, self._char, self._pickit, runs)
self._arcane = Arcane(self._pather, self._town_manager, self._char, self._pickit, runs)
self._diablo = Diablo(self._pather, self._town_manager, self._char, self._pickit, runs)
self._level = Level(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
self._pindle = Pindle(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
self._shenk = ShenkEld(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
self._trav = Trav(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
self._nihlathak = Nihlathak(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
self._arcane = Arcane(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
self._diablo = Diablo(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
self._vizier = Vizier(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
self._baal = Baal(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
self._mephisto = Mephisto(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
self._andariel = Andariel(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
self._countess = Countess(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
# Create member variables
self._picked_up_items = False
self._curr_loc: bool | Location = None
@@ -127,24 +157,31 @@ class Bot:
self._timer = time.time()
# Create State Machine
self._states=['initialization','hero_selection', 'town', 'pindle', 'shenk', 'trav', 'nihlathak', 'arcane', 'diablo']
self._states=['initialization','hero_selection', 'town', 'level', 'pindle', 'shenk', 'trav', 'nihlathak', 'arcane', 'diablo', 'vizier', 'baal', 'mephisto', 'andariel', 'countess']
self._transitions = [
{ 'trigger': 'init', 'source': 'initialization', 'dest': '=','before': "on_init"},
{ 'trigger': 'skip_to_level', 'source': 'initialization', 'dest': 'level', 'before': "on_skip_to_level"},
{ 'trigger': 'select_character', 'source': 'initialization', 'dest': 'hero_selection', 'before': "on_select_character"},
{ 'trigger': 'start_from_town', 'source': ['initialization', 'hero_selection'], 'dest': 'town', 'before': "on_start_from_town"},
{ 'trigger': 'create_game', 'source': 'hero_selection', 'dest': '=', 'before': "on_create_game"},
# Tasks within town
{ 'trigger': 'maintenance', 'source': 'town', 'dest': 'town', 'before': "on_maintenance"},
# Different runs
{ 'trigger': 'run_level', 'source': 'town', 'dest': 'level', 'before': "on_run_level"},
{ 'trigger': 'run_pindle', 'source': 'town', 'dest': 'pindle', 'before': "on_run_pindle"},
{ 'trigger': 'run_shenk', 'source': 'town', 'dest': 'shenk', 'before': "on_run_shenk"},
{ 'trigger': 'run_trav', 'source': 'town', 'dest': 'trav', 'before': "on_run_trav"},
{ 'trigger': 'run_nihlathak', 'source': 'town', 'dest': 'nihlathak', 'before': "on_run_nihlathak"},
{ 'trigger': 'run_arcane', 'source': 'town', 'dest': 'arcane', 'before': "on_run_arcane"},
{ 'trigger': 'run_diablo', 'source': 'town', 'dest': 'nihlathak', 'before': "on_run_diablo"},
{ 'trigger': 'run_diablo', 'source': 'town', 'dest': 'diablo', 'before': "on_run_diablo"},
{ 'trigger': 'run_vizier', 'source': 'town', 'dest': 'vizier', 'before': "on_run_vizier"},
{ 'trigger': 'run_baal', 'source': 'town', 'dest': 'baal', 'before': "on_run_baal" },
{ 'trigger': 'run_mephisto', 'source': 'town', 'dest': 'mephisto', 'before': "on_run_mephisto" },
{ 'trigger': 'run_andariel', 'source': 'town', 'dest': 'andariel', 'before': "on_run_andariel" },
{ 'trigger': 'run_countess', 'source': 'town', 'dest': 'countess', 'before': "on_run_countess" },
# End run / game
{ 'trigger': 'end_run', 'source': ['shenk', 'pindle', 'nihlathak', 'trav', 'arcane', 'diablo'], 'dest': 'town', 'before': "on_end_run"},
{ 'trigger': 'end_game', 'source': ['town', 'shenk', 'pindle', 'nihlathak', 'trav', 'arcane', 'diablo','end_run'], 'dest': 'initialization', 'before': "on_end_game"},
{ 'trigger': 'end_run', 'source': ['level', 'shenk', 'pindle', 'nihlathak', 'trav', 'arcane', 'diablo','vizier', 'baal', 'mephisto', 'andariel', 'countess'], 'dest': 'town', 'before': "on_end_run"},
{ 'trigger': 'end_game', 'source': ['town', 'shenk', 'pindle', 'nihlathak', 'trav', 'arcane', 'diablo','vizier','end_run', 'baal', 'mephisto', 'andariel', 'countess'], 'dest': 'initialization', 'before': "on_end_game"},
]
self.machine = Machine(model=self, states=self._states, initial="initialization", transitions=self._transitions, queued=True)
self._transmute = Transmute(self._game_stats)
@@ -178,7 +215,8 @@ class Bot:
Logger.info(f"{Config().general['name']} is now pausing")
self._game_stats.pause_timer()
while self._pausing:
time.sleep(0.2)
from utils.misc import wait as _wait
_wait(0.2, 0.24)
if not self._stopping:
self.trigger(name, **kwargs)
@@ -199,7 +237,10 @@ class Bot:
def shuffle_runs(self):
tmp = list(self._do_runs.items())
random.shuffle(tmp)
self._do_runs = OrderedDict(tmp)
#We clear() and update() to avoid assignment on _do_runs as this list is referenced
#by other classes and we want updates persisted everywhere.
self._do_runs.clear()
self._do_runs.update(OrderedDict(tmp))
def is_last_run(self):
found_unfinished_run = False
@@ -219,6 +260,13 @@ class Bot:
def on_init(self):
self._game_stats.log_start_game()
keyboard.release(Config().char["stand_still"])
# If we're only doing run_level and character is already in-game, skip
# the town detection and go straight to the run
if list(self._do_runs.keys()) == ["run_level"]:
Logger.info("Level run only - skipping town detection, starting directly")
self._curr_loc = Location.A1_TOWN_START
self.trigger_or_stop("skip_to_level")
return
transition_to_screens = Bot._rebuild_as_asset_to_trigger({
"select_character": main_menu.MAIN_MENU_MARKERS,
"start_from_town": town_manager.TOWN_MARKERS,
@@ -257,7 +305,10 @@ class Bot:
def on_start_from_town(self):
self._curr_loc = self._town_manager.wait_for_town_spawn()
if not self._curr_loc:
Logger.error("Could not detect game creation and ending game.")
return self.trigger_or_stop("end_game", failed=True)
# Handle picking up corpse in case of death
if (corpse_present := is_visible(ScreenObjects.Corpse)):
self._previous_run_failed = True
@@ -294,13 +345,19 @@ class Bot:
# Look at belt to figure out how many pots need to be picked up
belt.update_pot_needs()
# If we are doing trav_runs and juvs are full we will remove extra runs
if ("run_trav" == next(iter(self._do_runs))) and (consumables.get_needs("rejuv") <= 1):
while (len(self._do_runs) > 1):
skipped_run = self._do_runs.popitem()[0]
Logger.info(f"We are running trav and have full rejuvs so skipping {skipped_run}")
# Inspect inventory
items = None
need_inspect = self._picked_up_items or self._previous_run_failed
if Config().char["runs_per_stash"]:
need_inspect |= (self._game_stats._run_counter - 1) % Config().char["runs_per_stash"] == 0
if need_inspect:
img = personal.open()
img = personal.open_inventory()
# Update TP, ID, key needs
if self._game_stats._game_counter == 1:
self._use_id_tome = common.tome_state(img, 'id')[0] is not None
@@ -318,6 +375,11 @@ class Bot:
items = personal.inspect_items(img, game_stats=self._game_stats, close_window=False)
common.close()
Logger.debug(f"Needs: {consumables.get_needs()}")
#Cast town buffs (ie burst of speed etc)
if not self._pre_buffed:
self._char.cast_town_buffs(self._curr_loc)
if items:
# if there are still items that need identifying, go to cain to identify them
if any([item.need_id for item in items]):
@@ -329,6 +391,7 @@ class Bot:
items = personal.inspect_items(game_stats=self._game_stats)
keep_items = any([item.keep for item in items]) if items else None
sell_items = any([item.sell for item in items]) if items else None
stash_gold = personal.get_inventory_gold_full()
# Check if should need some healing
img = grab()
@@ -346,23 +409,27 @@ class Bot:
items = result_items
sell_items = any([item.sell for item in items]) if items else None
Logger.debug(f"Needs: {consumables.get_needs()}")
elif meters.get_health(img) < 0.6 or meters.get_mana(img) < 0.2:
elif meters.get_health(img) <= Config().char["take_rejuv_potion_health"] or meters.get_mana(img) <= Config().char["take_rejuv_potion_mana"]:
Logger.info("Healing at next possible Vendor")
self._curr_loc = self._town_manager.heal(self._curr_loc)
if not self._curr_loc:
return self.trigger_or_stop("end_game", failed=True)
# Stash stuff
if keep_items or personal.get_inventory_gold_full():
if keep_items or stash_gold:
Logger.info("Stashing items")
prev_loc = self._curr_loc
self._curr_loc, result_items = self._town_manager.stash(self._curr_loc, items=items)
sell_items = any([item.sell for item in result_items]) if result_items else None
#Acquire mutex to prevent controller from killing thread during transmutes
self._stash_mutex.acquire()
Logger.info("Running transmutes")
self._transmute.run_transmutes(force=False)
common.close()
self._stash_mutex.release()
if not self._curr_loc:
return self.trigger_or_stop("end_game", failed=True)
self._picked_up_items = False
self._picked_up_items = False
# Check if we are out of tps or need repairing
need_repair = is_visible(ScreenObjects.NeedRepair)
@@ -407,6 +474,9 @@ class Bot:
self._previous_run_failed = False
for key in self._do_runs:
if self._do_runs[key]:
if should_skip_run():
self._do_runs[key] = False
continue
self.trigger_or_stop(key)
started_run = True
break
@@ -418,7 +488,15 @@ class Bot:
cv2.imwrite("./log/screenshots/info/info_failed_game_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
self._curr_loc = False
self._pre_buffed = False
view.save_and_exit()
if view.save_and_exit() == False:
Logger.error("Normal save_and_exit failed! Attempting fast_save_and_exit and resetting bot.")
img = grab()
view.fast_save_and_exit()
self.stop()
if Config().general["info_screenshots"]:
self._last_chicken_screenshot = "./log/screenshots/info/info_failed_exit_" + time.strftime("%Y%m%d_%H%M%S") + ".png"
cv2.imwrite(self._last_chicken_screenshot, img)
set_pause_state(True)
self._game_stats.log_end_game(failed=failed)
@@ -445,10 +523,18 @@ class Bot:
self._timer = time.time()
self._do_runs = copy(self._do_runs_reset)
#We clear() and update() to avoid assignment on _do_runs as this list is referenced
#by other classes and we want updates persisted everywhere.
self._do_runs.clear()
self._do_runs.update(self._do_runs_reset)
if Config().general["randomize_runs"]:
self.shuffle_runs()
if Config().stealth.get("reshuffle_each_rotation"):
tmp = list(self._do_runs_reset.items())
random.shuffle(tmp)
self._do_runs_reset = OrderedDict(tmp)
self._do_runs.clear()
self._do_runs.update(self._do_runs_reset)
self.trigger_or_stop("init")
def on_end_run(self):
@@ -459,6 +545,8 @@ class Bot:
self._curr_loc = self._town_manager.wait_for_tp(self._curr_loc)
if self._curr_loc:
set_pause_state(True)
# Stealth: chance of unscheduled AFK break after returning to town
maybe_afk_break()
return self.trigger_or_stop("maintenance")
if not skills.has_tps():
consumables.set_needs("tp", 20)
@@ -483,14 +571,28 @@ class Bot:
else:
self.trigger_or_stop("end_run")
def on_skip_to_level(self):
"""Skip town detection and go straight to level run."""
pass
def on_run_level(self):
res = False
self._do_runs["run_level"] = False
self._game_stats.update_location("Lvl")
self._curr_loc = self._level.approach(self._curr_loc, not self._pre_buffed)
if self._curr_loc:
set_pause_state(False)
res = self._level.battle()
self._ending_run_helper(res)
def on_run_pindle(self):
res = False
self._do_runs["run_pindle"] = False
self._game_stats.update_location("Pin")
self._curr_loc = self._pindle.approach(self._curr_loc)
self._curr_loc = self._pindle.approach(self._curr_loc, not self._pre_buffed)
if self._curr_loc:
set_pause_state(False)
res = self._pindle.battle(not self._pre_buffed)
res = self._pindle.battle()
self._ending_run_helper(res)
def on_run_shenk(self):
@@ -519,7 +621,7 @@ class Bot:
self._curr_loc = self._nihlathak.approach(self._curr_loc)
if self._curr_loc:
set_pause_state(False)
res = self._nihlathak.battle(not self._pre_buffed)
res = self._nihlathak.battle(True)
self._ending_run_helper(res)
def on_run_arcane(self):
@@ -540,4 +642,53 @@ class Bot:
if self._curr_loc:
set_pause_state(False)
res = self._diablo.battle(not self._pre_buffed)
self._ending_run_helper(res)
self._ending_run_helper(res)
def on_run_vizier(self):
res = False
self._do_runs["run_vizier"] = False
self._game_stats.update_location("Dia")
self._curr_loc = self._vizier.approach(self._curr_loc)
if self._curr_loc:
set_pause_state(False)
res = self._vizier.battle(not self._pre_buffed)
self._ending_run_helper(res)
def on_run_baal(self):
res = False
self._do_runs["run_baal"] = False
self._game_stats.update_location("Baa")
self._curr_loc = self._baal.approach(self._curr_loc)
if self._curr_loc:
set_pause_state(False)
res = self._baal.battle(not self._pre_buffed)
self._ending_run_helper(res)
def on_run_mephisto(self):
res = False
self._do_runs["run_mephisto"] = False
self._game_stats.update_location("Mep")
self._curr_loc = self._mephisto.approach(self._curr_loc)
if self._curr_loc:
set_pause_state(False)
res = self._mephisto.battle(not self._pre_buffed)
self._ending_run_helper(res)
def on_run_andariel(self):
res = False
self._do_runs["run_andariel"] = False
self._game_stats.update_location("And")
self._curr_loc = self._andariel.approach(self._curr_loc)
if self._curr_loc:
set_pause_state(False)
res = self._andariel.battle(not self._pre_buffed)
self._ending_run_helper(res)
def on_run_countess(self):
res = False
self._do_runs["run_countess"] = False
self._game_stats.update_location("Cou")
self._curr_loc = self._countess.approach(self._curr_loc)
if self._curr_loc:
set_pause_state(False)
res = self._countess.battle(not self._pre_buffed)
self._ending_run_helper(res)
+2
View File
@@ -0,0 +1,2 @@
from .amazon import Amazon
from .javazon import Javazon
+31
View File
@@ -0,0 +1,31 @@
from input_layer import keyboard
from ui import skills
import time
import random
from input_layer import mouse
from char import IChar, CharacterCapabilities
from pather import Pather
from logger import Logger
from config import Config
from utils.misc import wait
from screen import convert_abs_to_screen, convert_abs_to_monitor
from pather import Pather
class Amazon(IChar):
def __init__(self, skill_hotkeys: dict, pather: Pather):
Logger.info("Setting up Amazon")
super().__init__(skill_hotkeys)
self._pather = pather
def _cast_valkyrie(self, cast_pos_abs: tuple[float, float], spray: float = 10):
if self._skill_hotkeys["valkyrie"]:
keyboard.send(self._skill_hotkeys["valkyrie"])
x = cast_pos_abs[0] + (random.random() * 2 * spray - spray)
y = cast_pos_abs[1] + (random.random() * 2 * spray - spray)
cast_pos_monitor = convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
mouse.press(button="right")
wait(0.06, 0.08)
mouse.release(button="right")
wait(self._cast_duration-0.06)
+94
View File
@@ -0,0 +1,94 @@
import random
from input_layer import keyboard
import time
import numpy as np
from health_manager import get_panel_check_paused, set_panel_check_paused
from inventory.personal import inspect_items
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab, convert_abs_to_screen
from input_layer import mouse
from char.amazon import Amazon
from logger import Logger
from config import Config
from utils.misc import wait
from pather import Location
from target_detect import get_visible_targets, TargetInfo, log_targets
class Javazon(Amazon):
def __init__(self, *args, **kwargs):
Logger.info("Setting up Javazon")
super().__init__(*args, **kwargs)
def _cast_lightning_fury(self, cast_pos_abs: tuple[float, float], spray: float = 10, duration: float = 0.3):
if not self._skill_hotkeys["lightning_fury"]:
raise ValueError("You did not set a hotkey for lightning_fury!")
keyboard.send(self._skill_hotkeys["lightning_fury"])
#Get mouse in initial target logcation
x = cast_pos_abs[0] + (random.random() * 2 * spray - spray)
y = cast_pos_abs[1] + (random.random() * 2 * spray - spray)
cast_pos_monitor = convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
#We just hold down right mouse button to ensure we get full attack speed.
mouse.press(button="right")
now = start = time.time()
while (now - start) < duration:
x = cast_pos_abs[0] + (random.random() * 2 * spray - spray)
y = cast_pos_abs[1] + (random.random() * 2 * spray - spray)
cast_pos_monitor = convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
wait(0.03, 0.36)
now = time.time()
mouse.release(button="right")
#wait 14 frames (14/25) which is slowest attack speed to ensure amazon is ready
wait(0.56)
def kill_pindle(self) -> bool:
atk_len_dur = float(Config().char["atk_len_pindle"])
pindle_pos_abs = convert_screen_to_abs(Config().path["pindle_end"][0])
cast_pos_abs = [pindle_pos_abs[0] * 0.80, pindle_pos_abs[1] * 0.80]
self._cast_valkyrie(cast_pos_abs=cast_pos_abs)
self._cast_lightning_fury(cast_pos_abs=cast_pos_abs, duration=atk_len_dur)
if self.capabilities.can_teleport_natively:
self._pather.traverse_nodes_fixed("pindle_end", self)
else:
#Walk just close enough to pindle's position to see items. That way we leave sooner if there is nothing to grab
x_m, y_m = convert_abs_to_monitor([pindle_pos_abs[0] * 0.65, pindle_pos_abs[1] * 0.65])
self.move((x_m, y_m), force_move=True)
return True
def kill_eldritch(self) -> bool:
eldritch_pos_abs = convert_screen_to_abs(Config().path["eldritch_end"][0])
cast_pos_abs = [eldritch_pos_abs[0] * 1.5, eldritch_pos_abs[1] * 1.5]
self._cast_valkyrie(cast_pos_abs=cast_pos_abs)
self._cast_lightning_fury(cast_pos_abs=cast_pos_abs, duration=Config().char["atk_len_eldritch"])
if self.capabilities.can_teleport_natively:
self._pather.traverse_nodes_fixed("eldritch_end", self)
else:
x_m, y_m = convert_abs_to_monitor([eldritch_pos_abs[0] * 1.0, eldritch_pos_abs[1] * 1.0])
self.move((x_m, y_m), force_move=True)
return True
def kill_shenk(self) -> bool:
shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0])
cast_pos_abs = [shenk_pos_abs[0] * 1.5, shenk_pos_abs[1] * 1.5]
self._cast_valkyrie(cast_pos_abs=cast_pos_abs)
self._cast_lightning_fury(cast_pos_abs=cast_pos_abs, duration=Config().char["atk_len_shenk"])
if self.capabilities.can_teleport_natively:
self._pather.traverse_nodes_fixed("shenk_end", self)
else:
x_m, y_m = convert_abs_to_monitor([shenk_pos_abs[0] * 1.0, shenk_pos_abs[1] * 1.0])
self.move((x_m, y_m), force_move=True)
return True
+82 -49
View File
@@ -1,11 +1,11 @@
import keyboard
from input_layer import keyboard
from ui import skills
from utils.custom_mouse import mouse
from input_layer import mouse
from char import IChar, CharacterCapabilities
import template_finder
from pather import Pather
from logger import Logger
from screen import convert_abs_to_monitor
from screen import convert_abs_to_monitor, convert_screen_to_monitor
from config import Config
from utils.misc import wait
import time
@@ -23,55 +23,82 @@ class Barbarian(IChar):
def _cast_war_cry(self, time_in_s: float):
# keyboard.send(self._skill_hotkeys["concentration"])
# wait(0.05, 0.1)
cry_frequency = min(0.2, self._skill_hotkeys["cry_frequency"])
keyboard.send(Config().char["stand_still"], do_release=False)
wait(0.05, 0.1)
if self._skill_hotkeys["war_cry"]:
if not self._skill_hotkeys["war_cry"]:
Logger.info('You did not set a hotkey for war_cry in params.ini!')
else:
keyboard.send(self._skill_hotkeys["war_cry"])
wait(0.05, 0.1)
start = time.time()
while (time.time() - start) < time_in_s:
wait(0.06, 0.08)
mouse.click(button="right")
wait(cry_frequency, cry_frequency + 0.2)
mouse.click(button="right")
wait(0.01, 0.05)
keyboard.send(Config().char["stand_still"], do_press=False)
wait(0.05, 0.05)
start = time.time()
while (time.time() - start) < time_in_s/2:
mouse.click(button="right")
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
if self._skill_hotkeys["howl"]:
keyboard.send(self._skill_hotkeys["howl"])
wait(0.05, 0.05)
mouse.click(button="right")
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
keyboard.send(self._skill_hotkeys["war_cry"])
wait(0.05, 0.05)
start = time.time()
while (time.time() - start) < time_in_s/2:
mouse.click(button="right")
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
def on_capabilities_discovered(self, capabilities: CharacterCapabilities):
if capabilities.can_teleport_natively:
self._pather.offset_node(149, [120, 70])
def _do_hork(self, hork_time: int):
wait(0.5)
if self._skill_hotkeys["find_item"]:
keyboard.send(self._skill_hotkeys["find_item"])
wait(0.5, 0.15)
pos_m = convert_abs_to_monitor((0, -20))
mouse.move(*pos_m)
wait(0.5, 0.15)
mouse.press(button="right")
wait(hork_time)
mouse.release(button="right")
wait(1)
if self._skill_hotkeys["howl"]:
keyboard.send(self._skill_hotkeys["howl"])
wait(0.05, 0.05)
mouse.click(button="right")
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
keyboard.send(self._skill_hotkeys["find_item"])
pos_m = convert_abs_to_monitor((0, -20))
mouse.move(*pos_m)
wait(0.05, 0.1)
start = time.time()
while ((time.time()-start) < hork_time):
mouse.click(button="right")
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
keyboard.send(Config().char["stand_still"], do_release=False)
mouse.click(button="right")
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
keyboard.send(Config().char["stand_still"], do_press=False)
if self._skill_hotkeys["howl"]:
keyboard.send(self._skill_hotkeys["howl"])
wait(0.05, 0.05)
mouse.click(button="right")
wait(self._cast_duration + 0.02,self._cast_duration + 0.02)
def pre_buff(self):
keyboard.send(Config().char["battle_command"])
wait(0.08, 0.19)
mouse.click(button="right")
wait(self._cast_duration + 0.08, self._cast_duration + 0.1)
keyboard.send(Config().char["battle_orders"])
wait(0.08, 0.19)
mouse.click(button="right")
wait(self._cast_duration + 0.08, self._cast_duration + 0.1)
keyboard.send(self._skill_hotkeys["shout"])
wait(0.08, 0.19)
mouse.click(button="right")
wait(self._cast_duration + 0.08, self._cast_duration + 0.1)
def cast_buffs(self, casting_delay: float):
if self._skill_hotkeys["howl"]:
keyboard.send(self._skill_hotkeys["howl"])
wait(0.05,0.05)
mouse.click(button="right")
wait(casting_delay + 0.02, casting_delay + 0.02)
if Config().char["battle_command"]:
keyboard.send(Config().char["battle_command"])
wait(0.05,0.05)
mouse.click(button="right")
wait(casting_delay + 0.02, casting_delay + 0.02)
if Config().char["battle_orders"]:
keyboard.send(Config().char["battle_orders"])
wait(0.05,0.05)
mouse.click(button="right")
wait(casting_delay + 0.02, casting_delay + 0.02)
if self._skill_hotkeys["shout"]:
keyboard.send(self._skill_hotkeys["shout"])
wait(0.05,0.05)
mouse.click(button="right")
wait(casting_delay + 0.02, casting_delay + 0.02)
def pre_move(self):
def pre_move(self, wait_tp: bool = False):
# select teleport if available
super().pre_move()
super().pre_move(wait_tp)
# in case teleport hotkey is not set or teleport can not be used, use leap if set
should_cast_leap = self._skill_hotkeys["leap"] and not skills.is_left_skill_selected(["LEAP"])
can_teleport = self.capabilities.can_teleport_natively and skills.is_right_skill_active()
@@ -89,12 +116,14 @@ class Barbarian(IChar):
wait(0.1, 0.15)
if self.capabilities.can_teleport_natively:
self._pather.traverse_nodes_fixed("pindle_end", self)
elif self._skill_hotkeys["leap"]:
leap_pos_monitor = convert_screen_to_monitor(Config().path["pindle_end"][0])
mouse.move(*leap_pos_monitor)
mouse.click(button="right")
wait(1.5, 1.5)
else:
if not self._do_pre_move:
# keyboard.send(self._skill_hotkeys["concentration"])
# wait(0.05, 0.15)
self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.0, do_pre_move=self._do_pre_move)
self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=0.1)
self._cast_war_cry(Config().char["atk_len_pindle"])
wait(0.1, 0.15)
self._do_hork(4)
@@ -102,13 +131,17 @@ class Barbarian(IChar):
def kill_eldritch(self) -> bool:
if self.capabilities.can_teleport_natively:
self._pather.traverse_nodes_fixed("eldritch_end", self)
self._pather.traverse_nodes_fixed([(675, 30)], self)
elif self._skill_hotkeys["leap"]:
leap_pos_monitor = convert_screen_to_monitor((675,30))
mouse.move(*leap_pos_monitor)
mouse.click(button="right")
wait(1.5, 1.5)
else:
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=1.0, do_pre_move=self._do_pre_move)
wait(0.05, 0.1)
self._cast_war_cry(Config().char["atk_len_eldritch"])
wait(0.1, 0.15)
self._do_hork(4)
wait(0.05, 0.1)
self._do_hork(6)
return True
def kill_shenk(self):
@@ -159,7 +192,7 @@ class Barbarian(IChar):
if __name__ == "__main__":
import os
import keyboard
from input_layer import keyboard
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
from config import Config
+6 -8
View File
@@ -1,6 +1,6 @@
import keyboard
from input_layer import keyboard
from ui import skills
from utils.custom_mouse import mouse
from input_layer import mouse
from char import IChar,CharacterCapabilities
import template_finder
from pather import Pather
@@ -42,19 +42,17 @@ class Basic(IChar):
wait(0.01, 0.05)
keyboard.send(Config().char["stand_still"], do_press=False)
def pre_buff(self):
if Config().char["cta_available"]:
self._pre_buff_cta()
def cast_buffs(self, casting_delay: float):
if self._skill_hotkeys["buff_1"]:
keyboard.send(self._skill_hotkeys["buff_1"])
wait(0.5, 0.15)
mouse.click(button="right")
wait(0.5, 0.15)
wait(casting_delay, casting_delay)
if self._skill_hotkeys["buff_2"]:
keyboard.send(self._skill_hotkeys["buff_2"])
wait(0.5, 0.15)
mouse.click(button="right")
wait(0.5, 0.15)
wait(casting_delay, casting_delay)
def pre_move(self):
# select teleport if available
@@ -143,7 +141,7 @@ class Basic(IChar):
if __name__ == "__main__":
import os
import keyboard
from input_layer import keyboard
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
from config import Config
+6 -8
View File
@@ -1,6 +1,6 @@
import keyboard
from input_layer import keyboard
from ui import skills
from utils.custom_mouse import mouse
from input_layer import mouse
from char import IChar
import template_finder
from pather import Pather
@@ -42,19 +42,17 @@ class Basic_Ranged(IChar):
mouse.move(*cast_pos_monitor)
mouse.click(button="right")
def pre_buff(self):
if Config().char["cta_available"]:
self._pre_buff_cta()
def cast_buffs(self, casting_delay: float):
if self._skill_hotkeys["buff_1"]:
keyboard.send(self._skill_hotkeys["buff_1"])
wait(0.5, 0.15)
mouse.click(button="right")
wait(0.5, 0.15)
wait(casting_delay, casting_delay)
if self._skill_hotkeys["buff_2"]:
keyboard.send(self._skill_hotkeys["buff_2"])
wait(0.5, 0.15)
mouse.click(button="right")
wait(0.5, 0.15)
wait(casting_delay, casting_delay)
#bosses
@@ -191,7 +189,7 @@ class Basic_Ranged(IChar):
if __name__ == "__main__":
import os
import keyboard
from input_layer import keyboard
import template_finder
from pather import Pather
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
+14 -21
View File
@@ -1,5 +1,5 @@
import keyboard
from utils.custom_mouse import mouse
from input_layer import keyboard
from input_layer import mouse
from char import IChar
import template_finder
from pather import Pather
@@ -44,12 +44,17 @@ class Bone_Necro(IChar):
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def pre_buff(self):
self.bone_armor()
#only CTA if pre trav
if Config().char["cta_available"]:
self._pre_buff_cta()
Logger.info("prebuff/cta")
def cast_buffs(self, casting_delay: float):
if self._skill_hotkeys["bone_armor"]:
keyboard.send(self._skill_hotkeys["bone_armor"])
wait(0.04, 0.1)
mouse.click(button="right")
wait(casting_delay)
if self._skill_hotkeys["clay_golem"]:
keyboard.send(self._skill_hotkeys["clay_golem"])
wait(0.04, 0.1)
mouse.click(button="right")
wait(casting_delay)
def _clay_golem(self):
Logger.debug('Casting clay golem')
@@ -58,18 +63,6 @@ class Bone_Necro(IChar):
mouse.click(button="right")
wait(self._cast_duration)
def bone_armor(self):
if self._skill_hotkeys["bone_armor"]:
keyboard.send(self._skill_hotkeys["bone_armor"])
wait(0.04, 0.1)
mouse.click(button="right")
wait(self._cast_duration)
if self._skill_hotkeys["clay_golem"]:
keyboard.send(self._skill_hotkeys["clay_golem"])
wait(0.04, 0.1)
mouse.click(button="right")
wait(self._cast_duration)
def _bone_armor(self):
if self._skill_hotkeys["bone_armor"]:
keyboard.send(self._skill_hotkeys["bone_armor"])
@@ -214,7 +207,7 @@ class Bone_Necro(IChar):
if __name__ == "__main__":
import os
import keyboard
from input_layer import keyboard
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
from config import Config
+85 -15
View File
@@ -4,12 +4,12 @@ import time
import cv2
import math
from item import consumables
import keyboard
from input_layer import keyboard
import numpy as np
from char.capabilities import CharacterCapabilities
from ui_manager import is_visible, wait_until_visible
from ui import skills
from utils.custom_mouse import mouse
from input_layer import mouse
from utils.misc import wait, cut_roi, is_in_roi, color_filter, arc_spread
from logger import Logger
from config import Config
@@ -23,8 +23,11 @@ class IChar:
def __init__(self, skill_hotkeys: dict):
self._skill_hotkeys = skill_hotkeys
self._last_tp = time.time()
self._stationary = False #Are we guaranteed stationary while picking up items
# Add a bit to be on the save side
self._cast_duration = Config().char["casting_frames"] * 0.04 + 0.01
self._cta_cast_duration = Config().char["cta_casting_frames"] * 0.04 + 0.01
self._attack_duration = Config().char["attack_frames"] * 0.04 + 0.01
self.damage_scaling = float(Config().char.get("damage_scaling", 1.0))
self.capabilities = None
self._active_skill = {
@@ -87,12 +90,39 @@ class IChar:
def on_capabilities_discovered(self, capabilities: CharacterCapabilities):
pass
def pick_up_item(self, pos: tuple[float, float], item_name: str = None, prev_cast_start: float = 0):
mouse.move(pos[0], pos[1])
time.sleep(0.1)
mouse.click(button="left")
wait(0.25, 0.3)
return prev_cast_start
def pick_up_item(self, pos: tuple[float, float], item_name: str = None, distance: int = 0, force_run: bool = False):
if not force_run and self.capabilities.can_teleport_natively and (distance >= Config().ui_pos["item_dist"]) and item_name != "Chest":
self.pre_move()
self.move(pos, force_tp=True)
self._stationary = True #We teleported so we should be guaranteed stationary now
#We teleported on top of the item, but there are no guarantees exactly where the item is located.
#Return false to indicate that we've not yet picked up the item.
return False
else:
mouse.move(pos[0], pos[1])
wait(0.1, 0.12)
mouse.click(button="left")
#Current logic only sets force_run if we previously teled.
#Therefore, we only need to perform run delay if force_run is not set
if not force_run:
#When character moves, it typically doesn't move less than 30
if distance < 30:
distance = 30
#By default we ignore pickup distance and do a full delay in case we misclick
pickup_distance = 0
#If we are stationary, we can account for pickup distance to potentially allow instant pickups
if self._stationary:
pickup_distance = 110 #we can instant pickup items 110 units away
if distance > pickup_distance:
run_time = 0.003125 * (distance-pickup_distance) #estimated run speed of (2sec/640pixels) = 0.003125
wait(run_time, run_time) #wait until we move to item and pick it up
self._stationary = False #Moving so we don't guarantee stationary state (delay is accurate not exorbitant)
return True
def select_by_template(
self,
@@ -172,11 +202,14 @@ class IChar:
def select_tp(self):
return skills.select_tp(Config().char["teleport"])
def pre_move(self):
def pre_move(self, wait_tp: bool = False):
# if teleport hotkey is set and if teleport is not already selected
if self.capabilities.can_teleport_natively:
self.select_tp()
self._set_active_skill("right", "teleport")
if wait_tp:
if skills.wait_until_right_skill_selected("TELE_ACTIVE") == False:
Logger.error("timeout waiting for tele skill to activate")
def move(self, pos_monitor: tuple[float, float], force_tp: bool = False, force_move: bool = False):
factor = Config().advanced_options["pathing_delay_factor"]
@@ -278,34 +311,44 @@ class IChar:
while time.time() - start < 4:
keyboard.send(Config().char["weapon_switch"])
wait(0.3, 0.35)
self._select_skill(skill = "battle_command", mouse_click_type="right", delay=(0.1, 0.2))
keyboard.send(Config().char["battle_command"])
wait(0.1,0.2)
if skills.is_right_skill_selected(["BC", "BO"]):
switch_sucess = True
break
else:
Logger.warning("Failed to find Battle Command, swapping weapons again.")
skill_before = cut_roi(grab(), Config().ui_roi["skill_right"])
if not switch_sucess:
Logger.warning("You dont have Battle Command bound, or you do not have CTA. ending CTA buff")
Config().char["cta_available"] = 0
Logger.error("You dont have Battle Command bound, or you do not have CTA. Ending CTA buff")
return False
else:
# We switched succesfully, let's pre buff
mouse.click(button="right")
wait(self._cast_duration + 0.16, self._cast_duration + 0.18)
wait(self._cta_cast_duration, self._cta_cast_duration)
self._select_skill(skill = "battle_orders", mouse_click_type="right", delay=(0.1, 0.2))
mouse.click(button="right")
wait(self._cast_duration + 0.16, self._cast_duration + 0.18)
wait(self._cta_cast_duration, self._cta_cast_duration)
if Config().char["buff_with_cta"]:
self.cast_buffs(self._cta_cast_duration)
wait(0.08,0.08) #Give extra time after last cast to allow weapon switch to be available
# Make sure the switch back to the original weapon is good
start = time.time()
switch_sucess = False
while time.time() - start < 4:
keyboard.send(Config().char["weapon_switch"])
wait(0.3, 0.35)
skill_after = cut_roi(grab(), Config().ui_roi["skill_right"])
_, max_val, _, _ = cv2.minMaxLoc(cv2.matchTemplate(skill_after, skill_before, cv2.TM_CCOEFF_NORMED))
if max_val > 0.9:
switch_sucess = True
break
else:
Logger.warning("Failed to switch weapon, try again")
wait(0.5)
return switch_sucess
def vec_to_monitor(self, target):
@@ -347,6 +390,18 @@ class IChar:
def pre_buff(self):
if Config().char["cta_available"]:
if not self._pre_buff_cta():
return False
#If we do not have a CTA, or we are not buffing with CTA; we buff now that we swapped cta out
if not Config().char["cta_available"] or not Config().char["buff_with_cta"]:
self.cast_buffs(self._cast_duration)
return True
def cast_buffs(self, casting_delay: float):
pass
def cast_town_buffs(self, curr_loc):
pass
def kill_pindle(self) -> bool:
@@ -382,9 +437,24 @@ class IChar:
def kill_cs_trash(self, location:str) -> bool:
raise ValueError("Diablo CS Trash is not implemented!")
def kill_andariel(self) -> bool:
raise ValueError("Andariel is not implemented!")
def kill_countess(self) -> bool:
raise ValueError("Countess is not implemented!")
def kill_mephisto(self) -> bool:
raise ValueError("Mephisto is not implemented!")
def kill_baal(self) -> bool:
raise ValueError("Baal is not implemented!")
def kill_baal_waves(self, duration: int = 45) -> bool:
raise ValueError("Baal wave clearing is not implemented!")
if __name__ == "__main__":
import os
import keyboard
from input_layer import keyboard
keyboard.add_hotkey('f12', lambda: os._exit(1))
print(f"Get on D2R screen and press F11 when ready")
keyboard.wait("f11")
+4 -7
View File
@@ -1,5 +1,5 @@
import keyboard
from utils.custom_mouse import mouse
from input_layer import keyboard
from input_layer import mouse
from char import IChar
import template_finder
from template_finder import TemplateMatch
@@ -238,10 +238,7 @@ class Necro(IChar):
keyboard.send(Config().char["stand_still"], do_press=False)
def pre_buff(self):
#only CTA if pre trav
if Config().char["cta_available"]:
self._pre_buff_cta()
def cast_buffs(self, casting_delay: float):
if self._shenk_dead==1:
Logger.info("trav buff?")
#self._heart_of_wolverine()
@@ -860,7 +857,7 @@ class Necro(IChar):
if __name__ == "__main__":
import os
import keyboard
from input_layer import keyboard
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
from config import Config
+94 -27
View File
@@ -1,12 +1,12 @@
import random
import keyboard
from input_layer import keyboard
import time
import numpy as np
from health_manager import get_panel_check_paused, set_panel_check_paused
from inventory.personal import inspect_items
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab, convert_abs_to_screen
from utils.custom_mouse import mouse
from input_layer import mouse
from char.paladin import Paladin
from logger import Logger
from config import Config
@@ -19,10 +19,19 @@ class FoHdin(Paladin):
Logger.info("Setting up FoHdin")
super().__init__(*args, **kwargs)
self._pather.adapt_path((Location.A3_TRAV_START, Location.A3_TRAV_CENTER_STAIRS), [220, 221, 222, 903, 904, 905, 906])
self._foh_cycle_duration = 0.4 + (self._action_frame/25.0)
self._last_foh_cast = 0
def _cast_foh(self, cast_pos_abs: tuple[float, float], spray: int = 10, min_duration: float = 0, aura: str = "conviction"):
return self._cast_skill_with_aura(skill_name = "foh", cast_pos_abs = cast_pos_abs, spray = spray, min_duration = min_duration, aura = aura)
#wait one frame until the foh cooldown has expired
now = time.time()
while (now - self._last_foh_cast) < self._foh_cycle_duration:
wait(0.04)
now = time.time()
self._last_foh_cast = now
rt = self._cast_skill_with_aura(skill_name = "foh", cast_pos_abs = cast_pos_abs, spray = spray, min_duration = min_duration, aura = aura)
self._last_foh_cast = self._last_click_cast
return rt
def _cast_holy_bolt(self, cast_pos_abs: tuple[float, float], spray: int = 10, min_duration: float = 0, aura: str = "concentration"):
#if skill is bound : concentration, use concentration, otherwise move on with conviction. alternatively use redemption whilst holybolting. conviction does not help holy bolt (its magic damage)
@@ -37,6 +46,25 @@ class FoHdin(Paladin):
self.move(pos_m, force_move=True)
self._cast_hammers(atk_len, aura=aura)
def _foh_clear_screen(self, duration: float = 1.0, spray: int = 10, aura: str = "conviction"):
start = time.time()
while (time.time() - start) < duration:
#Cast 3 fohs each 120 degrees apart
self._cast_foh([0,-200], spray=spray, aura=aura) #Top 90 degrees
self._cast_foh([-173,100], spray=spray, aura=aura) #Bot-left 210 degrees
self._cast_foh([173,100], spray=spray, aura=aura) #Bot-right 330 degrees
#Stop mid sequence if time expired
if (time.time() - start) > duration:
break
#Cast 3 more fohs 120 degrees apart with 60 degree offset from above
self._cast_foh([-173,-100], spray=spray, aura=aura) #Top-left 150 degrees
self._cast_foh([173,-100], spray=spray, aura=aura) #Top-right 30 degrees
self._cast_foh([0,200], spray=spray, aura=aura) #Bot 270 degrees
def _generic_foh_attack_sequence(
self,
default_target_abs: tuple[int, int] = (0, 0),
@@ -73,11 +101,15 @@ class FoHdin(Paladin):
self._cast_foh(cast_pos_abs, spray=spray, aura=foh_aura)
target_check_count += 1
#We always want to end with selecting foh, as holy bolt can cause char move issues with merc targeting.
self._select_skill("foh", mouse_click_type = "left")
return True
#FOHdin Attack Sequence Optimized for trash
def _cs_attack_sequence(self, min_duration: float = Config().char["atk_len_cs_trashmobs"], max_duration: float = Config().char["atk_len_cs_trashmobs"] * 3):
self._generic_foh_attack_sequence(default_target_abs=(20,20), min_duration = min_duration, max_duration = max_duration, default_spray=100, foh_to_holy_bolt_ratio=6)
self._foh_clear_screen(duration = max_duration)
self._activate_redemption_aura()
def _cs_trash_mobs_attack_sequence(self, min_duration: float = 1.2, max_duration: float = Config().char["atk_len_cs_trashmobs"]):
@@ -96,36 +128,23 @@ class FoHdin(Paladin):
atk_len_dur = float(Config().char["atk_len_pindle"])
pindle_pos_abs = convert_screen_to_abs(Config().path["pindle_end"][0])
if (self.capabilities.can_teleport_natively or self.capabilities.can_teleport_with_charges) and self._use_safer_routines:
# Slightly retreating, so the Merc gets charged
if not self._pather.traverse_nodes([102], self, timeout=1.0, do_pre_move=False, force_move=True,force_tp=False, use_tp_charge=False):
return False
# Doing one Teleport to safe_dist to grab our Merc
Logger.debug("Teleporting backwards to let Pindle charge the MERC. Looks strange, but is intended!") #I would leave this message in, so users dont complain that there is a strange movement pattern.
if not self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=False, force_tp=True, use_tp_charge=True):
return False
# Slightly retreating, so the Merc gets charged
if not self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=False, force_move=True, force_tp=False, use_tp_charge=False):
return False
else:
keyboard.send(self._skill_hotkeys["conviction"])
wait(0.15)
self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=False)
cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9]
self._generic_foh_attack_sequence(default_target_abs=cast_pos_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, default_spray=11)
cast_pos_abs = [pindle_pos_abs[0] * 0.80, pindle_pos_abs[1] * 0.80]
self._generic_foh_attack_sequence(default_target_abs=cast_pos_abs, min_duration=atk_len_dur, max_duration=atk_len_dur, default_spray=10, target_detect=False, foh_to_holy_bolt_ratio=1)
if self.capabilities.can_teleport_natively:
self._pather.traverse_nodes_fixed("pindle_end", self)
else:
keyboard.send(self._skill_hotkeys["redemption"])
if Config().fohdin["redemption"]:
keyboard.send(self._skill_hotkeys["redemption"])
wait(0.15)
self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.0, do_pre_move=False)
#Walk just close enough to pindle's position to see items. That way we leave sooner if there is nothing to grab
x_m, y_m = convert_abs_to_monitor([pindle_pos_abs[0] * 0.65, pindle_pos_abs[1] * 0.65])
self.move((x_m, y_m), force_move=True)
# Use target-based attack sequence one more time before pickit
self._generic_foh_attack_sequence(default_target_abs=cast_pos_abs, max_duration=atk_len_dur, default_spray=11)
self._activate_cleanse_redemption()
return True
@@ -759,4 +778,52 @@ class FoHdin(Paladin):
self._activate_cleanse_redemption()
### LOOT ###
#self._cs_pickit()
return True
def kill_andariel(self) -> bool:
atk_len = float(Config().char.get("atk_len_andariel", 4.0))
Logger.info("Killing Andariel")
self._generic_foh_attack_sequence(default_target_abs=(0, 0), min_duration=atk_len, max_duration=atk_len*2, default_spray=20)
self._move_and_attack((30, 15), atk_len * 0.3, aura="redemption")
self._move_and_attack((-30, -15), atk_len * 0.3, aura="redemption")
self._activate_cleanse_redemption()
return True
def kill_countess(self) -> bool:
atk_len = float(Config().char.get("atk_len_countess", 3.0))
Logger.info("Killing Countess")
self._generic_foh_attack_sequence(default_target_abs=(0, 0), min_duration=atk_len, max_duration=atk_len*2, default_spray=20)
self._move_and_attack((30, 15), atk_len * 0.3, aura="redemption")
self._activate_cleanse_redemption()
return True
def kill_mephisto(self) -> bool:
atk_len = float(Config().char.get("atk_len_mephisto", 12.0))
Logger.info("Killing Mephisto")
self._generic_foh_attack_sequence(default_target_abs=(0, 0), min_duration=atk_len, max_duration=atk_len*2, default_spray=30)
self._move_and_attack((30, 15), atk_len * 0.3, aura="redemption")
self._move_and_attack((-30, -15), atk_len * 0.3, aura="redemption")
self._activate_cleanse_redemption()
return True
def kill_baal(self) -> bool:
atk_len = float(Config().char.get("atk_len_baal", 10.0))
Logger.info("Killing Baal")
self._generic_foh_attack_sequence(default_target_abs=(0, 0), min_duration=atk_len, max_duration=atk_len*2, default_spray=30)
self._move_and_attack((30, 15), atk_len * 0.3, aura="redemption")
self._move_and_attack((-30, -15), atk_len * 0.3, aura="redemption")
self._activate_cleanse_redemption()
return True
def kill_baal_waves(self, duration: int = 45) -> bool:
atk_len = float(Config().char.get("atk_len_baal_waves", 30.0))
Logger.info("Clearing Baal waves")
self._foh_clear_screen(duration=atk_len * 0.3)
self._activate_redemption_aura()
self._foh_clear_screen(duration=atk_len * 0.3)
self._activate_redemption_aura()
self._foh_clear_screen(duration=atk_len * 0.3)
self._activate_redemption_aura()
self._foh_clear_screen(duration=atk_len * 0.1)
self._activate_cleanse_redemption()
return True
+68 -9
View File
@@ -1,4 +1,4 @@
import keyboard
from input_layer import keyboard
import random
import time
@@ -12,7 +12,7 @@ from pather import Pather, Location
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab
from target_detect import get_visible_targets
from ui import skills
from utils.custom_mouse import mouse
from input_layer import mouse
from utils.misc import wait
class Hammerdin(Paladin):
@@ -40,17 +40,15 @@ class Hammerdin(Paladin):
wait(0.01, 0.05)
keyboard.send(Config().char["stand_still"], do_press=False)
def pre_buff(self):
if Config().char["cta_available"]:
self._pre_buff_cta()
def cast_buffs(self, casting_delay: float):
keyboard.send(self._skill_hotkeys["holy_shield"])
wait(0.04, 0.1)
mouse.click(button="right")
wait(self._cast_duration, self._cast_duration + 0.06)
wait(casting_delay, casting_delay + 0.06)
def pre_move(self):
def pre_move(self, wait_tp: bool = False):
# select teleport if available
super().pre_move()
super().pre_move(wait_tp=wait_tp)
# in case teleport hotkey is not set or teleport can not be used, use vigor if set
should_cast_vigor = self._skill_hotkeys["vigor"] and not skills.is_right_skill_selected(["VIGOR"])
can_teleport = self.capabilities.can_teleport_natively and skills.is_right_skill_active()
@@ -1263,10 +1261,71 @@ class Hammerdin(Paladin):
self._picked_up_items |= self._pickit.pick_up_items(self)
return True
def kill_andariel(self) -> bool:
atk_len = float(Config().char.get("atk_len_andariel", 4.0))
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
self._cast_hammers(atk_len)
self._cast_hammers(0.8, "redemption")
self._move_and_attack((30, 15), atk_len * 0.3)
self._cast_hammers(0.6, "redemption")
self._move_and_attack((-30, -15), atk_len * 0.3)
self._cast_hammers(0.8, "redemption")
return True
def kill_countess(self) -> bool:
atk_len = float(Config().char.get("atk_len_countess", 3.0))
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
self._cast_hammers(atk_len)
self._cast_hammers(0.8, "redemption")
self._move_and_attack((30, 15), atk_len * 0.3)
self._cast_hammers(0.6, "redemption")
return True
def kill_mephisto(self) -> bool:
atk_len = float(Config().char.get("atk_len_mephisto", 12.0))
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
self._cast_hammers(atk_len)
self._cast_hammers(0.8, "redemption")
self._move_and_attack((30, 15), atk_len * 0.4)
self._cast_hammers(0.8, "redemption")
self._move_and_attack((-30, -15), atk_len * 0.4)
self._cast_hammers(1.2, "redemption")
return True
def kill_baal(self) -> bool:
atk_len = float(Config().char.get("atk_len_baal", 10.0))
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
self._cast_hammers(atk_len)
self._cast_hammers(0.8, "redemption")
self._move_and_attack((30, 15), atk_len * 0.4)
self._cast_hammers(0.8, "redemption")
self._move_and_attack((-30, -15), atk_len * 0.4)
wait(0.1, 0.15)
self._cast_hammers(1.2, "redemption")
return True
def kill_baal_waves(self, duration: int = 45) -> bool:
atk_len = float(Config().char.get("atk_len_baal_waves", 30.0))
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
self._move_and_attack((30, 15), atk_len * 0.3)
self._cast_hammers(0.75, "redemption")
self._move_and_attack((-50, -15), atk_len * 0.3)
self._cast_hammers(0.75, "redemption")
self._move_and_attack((-30, -15), atk_len * 0.3)
self._cast_hammers(0.75, "redemption")
self._move_and_attack((30, -15), atk_len * 0.1)
self._cast_hammers(1.0, "redemption")
return True
if __name__ == "__main__":
import os
import keyboard
from input_layer import keyboard
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
from config import Config
+21 -13
View File
@@ -1,8 +1,8 @@
import keyboard
from input_layer import keyboard
from ui import skills
import time
import random
from utils.custom_mouse import mouse
from input_layer import mouse
from char import IChar, CharacterCapabilities
from pather import Pather
from logger import Logger
@@ -20,18 +20,24 @@ class Paladin(IChar):
self._pather = pather
self._pickit = pickit #for Diablo
self._picked_up_items = False #for Diablo
self._last_click_cast = 0
self._action_frame = 9
if Config().char["casting_frames"] == 13 or Config().char["casting_frames"] == 12:
self._action_frame = 8
elif Config().char["casting_frames"] == 11 or Config().char["casting_frames"] == 10:
self._action_frame = 7
elif Config().char["casting_frames"] == 9:
self._action_frame = 6
def pre_buff(self):
if Config().char["cta_available"]:
self._pre_buff_cta()
def cast_buffs(self, casting_delay: float):
keyboard.send(self._skill_hotkeys["holy_shield"])
wait(0.04, 0.1)
mouse.click(button="right")
wait(self._cast_duration, self._cast_duration + 0.06)
wait(casting_delay, casting_delay + 0.06)
def pre_move(self):
def pre_move(self, wait_tp: bool = False):
# select teleport if available
super().pre_move()
super().pre_move(wait_tp=wait_tp)
# in case teleport hotkey is not set or teleport can not be used, use vigor if set
should_cast_vigor = self._skill_hotkeys["vigor"] and not skills.is_right_skill_selected(["VIGOR"])
can_teleport = self.capabilities.can_teleport_natively and skills.is_right_skill_active()
@@ -60,10 +66,11 @@ class Paladin(IChar):
y += (random.random() * 2 * spray - spray)
pos_m = convert_abs_to_monitor((x, y))
mouse.move(*pos_m, delay_factor=[0.1, 0.2])
wait(0.06, 0.08)
mouse.press(button = mouse_click_type)
self._last_click_cast = time.time()
wait(0.06, 0.08)
mouse.release(button = mouse_click_type)
wait(Config().char["casting_frames"]/25.0 - 0.06)
def _cast_skill_with_aura(self, skill_name: str, cast_pos_abs: tuple[float, float] = None, spray: int = 0, min_duration: float = 0, aura: str = ""):
#self._log_cast(skill_name, cast_pos_abs, spray, min_duration, aura)
@@ -77,11 +84,10 @@ class Paladin(IChar):
# set left hand skill
self._select_skill(skill_name, mouse_click_type = "left")
wait(0.04)
# cast left hand skill
start = time.time()
if min_duration:
start = time.time()
while (time.time() - start) <= min_duration:
self._click_cast(cast_pos_abs, spray)
else:
@@ -97,5 +103,7 @@ class Paladin(IChar):
self._select_skill("cleansing", delay=delay)
def _activate_cleanse_redemption(self):
self._activate_cleanse_aura()
self._activate_redemption_aura()
if Config().fohdin["cleansing"]:
self._activate_cleanse_aura()
if Config().fohdin["redemption"]:
self._activate_redemption_aura()
+4 -7
View File
@@ -1,5 +1,5 @@
import keyboard
from utils.custom_mouse import mouse
from input_layer import keyboard
from input_layer import mouse
from char import IChar
import template_finder
from pather import Pather
@@ -246,10 +246,7 @@ class Poison_Necro(IChar):
keyboard.send(Config().char["stand_still"], do_press=False)
def pre_buff(self):
#only CTA if pre trav
if Config().char["cta_available"]:
self._pre_buff_cta()
def cast_buffs(self, casting_delay: float):
if self._shenk_dead==1:
Logger.info("trav buff?")
#self._heart_of_wolverine()
@@ -544,7 +541,7 @@ class Poison_Necro(IChar):
if __name__ == "__main__":
import os
import keyboard
from input_layer import keyboard
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
from config import Config
+1
View File
@@ -1,5 +1,6 @@
from .sorceress import Sorceress
from .light_sorc import LightSorc
from .blizz_sorc import BlizzSorc
from .blizzorb_sorc import BlizzorbSorc
from .nova_sorc import NovaSorc
from .hydra_sorc import HydraSorc
+3 -3
View File
@@ -1,6 +1,6 @@
import keyboard
from input_layer import keyboard
from char.sorceress import Sorceress
from utils.custom_mouse import mouse
from input_layer import mouse
from logger import Logger
from utils.misc import wait, rotate_vec, unit_vector
import random
@@ -256,7 +256,7 @@ class BlizzSorc(Sorceress):
if __name__ == "__main__":
import os
import keyboard
from input_layer import keyboard
import template_finder
from pather import Pather
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
+238
View File
@@ -0,0 +1,238 @@
from input_layer import keyboard
from char.sorceress import Sorceress
from input_layer import mouse
from logger import Logger
from utils.misc import wait, rotate_vec, unit_vector
import random
import time
from pather import Location
import numpy as np
from screen import convert_abs_to_monitor, grab, convert_screen_to_abs
from config import Config
import template_finder
class BlizzorbSorc(Sorceress):
def __init__(self, *args, **kwargs):
Logger.info("Setting up Blizzorb Sorc")
super().__init__(*args, **kwargs)
self._orb_cycle_duration = 1.0 + (self._action_frame/25.0)
self._last_orb_cast = 0
self._blizz_cycle_duration = 1.8 + (self._action_frame/25.0)
self._last_blizz_cast = 0
def _cast_blizzard(self, cast_pos_abs: tuple[float, float], spray: float = 10):
if not self._skill_hotkeys["blizzard"]:
raise ValueError("You did not set a hotkey for blizzard!")
keyboard.send(self._skill_hotkeys["blizzard"])
x = cast_pos_abs[0] + (random.random() * 2 * spray - spray)
y = cast_pos_abs[1] + (random.random() * 2 * spray - spray)
cast_pos_monitor = convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
#wait for blizzard cooldown
now = time.time()
while (now - self._last_blizz_cast) < self._blizz_cycle_duration:
wait(0.04)
now = time.time()
self._last_blizz_cast = now
mouse.press(button="right")
wait(0.06, 0.08)
mouse.release(button="right")
wait(self._cast_duration-0.06)
def _cast_frozen_orb(self, cast_pos_abs: tuple[float, float], spray: float = 10):
keyboard.send(Config().char["stand_still"], do_release=False)
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
#wait for frozen orb cooldown
now = time.time()
while (now - self._last_orb_cast) < self._orb_cycle_duration:
wait(0.04)
now = time.time()
self._last_orb_cast = now
mouse.press(button="left")
wait(0.06, 0.08)
mouse.release(button="left")
keyboard.send(Config().char["stand_still"], do_press=False)
wait(self._cast_duration-0.06)
def _cast_glacial_spike(self, cast_pos_abs: tuple[float, float], spray: float = 10, times=1):
if not self._skill_hotkeys["glacial_spike"]:
raise ValueError("You did not set a hotkey for glacial spike!")
keyboard.send(self._skill_hotkeys["glacial_spike"])
for _ in range(times):
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
mouse.press(button="right")
wait(0.06, 0.08)
mouse.release(button="right")
wait(self._cast_duration-0.06)
def _cast_static_field(self, times=1):
if self._skill_hotkeys["static_field"]:
keyboard.send(self._skill_hotkeys["static_field"])
#Static field can fail to cast if we right click on a wall. We move mouse to
#center to prevent this.
cast_pos_monitor = convert_abs_to_monitor((0, 0))
mouse.move(*cast_pos_monitor)
for _ in range(times):
mouse.press(button="right")
wait(0.06, 0.08)
mouse.release(button="right")
wait(self._cast_duration-0.06)
def _cast_blizzorb_spike_combo(self, cast_pos_abs: tuple[float, float], spray: float = 10):
self._cast_blizzard(cast_pos_abs,spray)
self._cast_frozen_orb(cast_pos_abs,spray)
self._cast_glacial_spike(cast_pos_abs,spray, times=2)
self._cast_frozen_orb(cast_pos_abs,spray)
def _cast_blizzorb_whirlwind_combo(self, spray: float = 10):
self._cast_blizzard([0, 0],spray)
self._cast_frozen_orb([40, -40],spray)
self._cast_static_field(times=2)
self._cast_frozen_orb([-40,40],spray)
self._cast_blizzard([0, 0],spray)
self._cast_frozen_orb([-40,-40],spray)
self._cast_static_field(times=2)
self._cast_frozen_orb([40,40],spray)
def kill_pindle(self) -> bool:
pindle_pos_abs = convert_screen_to_abs(Config().path["pindle_end"][0])
cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9]
start = time.time()
while (time.time() - start) < Config().char["atk_len_pindle"]:
self._cast_blizzorb_spike_combo(cast_pos_abs, spray=0)
self._pather.traverse_nodes_fixed("pindle_end", self)
return True
def kill_eldritch(self) -> bool:
eldritch_pos_abs = convert_screen_to_abs(Config().path["eldritch_end"][0])
cast_pos_abs = [eldritch_pos_abs[0] * 1.00, eldritch_pos_abs[1] * 1.00]
self._pather.traverse_nodes_fixed([(675, 30)], self)
start = time.time()
while (time.time() - start) < Config().char["atk_len_eldritch"]:
self._cast_blizzard([40,-100],spray=0)
self._cast_frozen_orb([80,-200],spray=0)
self._cast_static_field(times=3)
self._cast_frozen_orb([80,-200],spray=0)
#self._cast_blizzorb_spike_combo(cast_pos_abs, spray=0)
#Need to cast blizzard closer now that Eldritch approaches
#cast_pos_abs = [eldritch_pos_abs[0] * 1.3, eldritch_pos_abs[1] * 1.3]
#while (time.time() - start) < Config().char["atk_len_eldritch"]:
# self._cast_blizzorb_spike_combo(cast_pos_abs, spray=0)
#self._pather.traverse_nodes_fixed("eldritch_end", self)
return True
def kill_shenk(self) -> bool:
shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0])
cast_pos_abs = [shenk_pos_abs[0] * 1.5, shenk_pos_abs[1] * 1.5]
start = time.time()
#First, cast blizzard once we are in range
self._cast_blizzard(cast_pos_abs, spray=0)
#Next, tele next to shenk to put orb and static in range
pos_m = convert_abs_to_monitor((shenk_pos_abs[0] * 1.1, shenk_pos_abs[1] * 1.1))
self.pre_move(wait_tp=True)
self.move(pos_m, force_move=True)
#Adjust cast position now that we are next to shenk and cast orb/static
cast_pos_abs = [shenk_pos_abs[0] * 0.45, (shenk_pos_abs[1] * 0.45)-50]
self._cast_frozen_orb(cast_pos_abs, spray=0)
self._cast_static_field(times=3)
#Continue blizzorb combo until attack length expires
while (time.time() - start) < Config().char["atk_len_shenk"]:
self._cast_blizzorb_spike_combo(cast_pos_abs, spray=0)
return True
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
# Find nilhlatak position
nihlathak_pos_abs = self._pather.find_abs_node_pos(end_nodes[-1], grab())
if nihlathak_pos_abs is not None:
cast_pos_abs = np.array([nihlathak_pos_abs[0] * 1.0, nihlathak_pos_abs[1] * 1.0])
start = time.time()
#First, cast blizzard once we are in range
self._cast_blizzard(cast_pos_abs, spray=0)
#Next, tele next to nihlathak to put orb and static in range
pos_m = convert_abs_to_monitor((nihlathak_pos_abs[0] * 0.6, nihlathak_pos_abs[1] * 0.6))
self.pre_move(wait_tp=True)
self.move(pos_m, force_move=True)
#Adjust cast position now that we are next to nihlathak and cast orb/static
cast_pos_abs = [nihlathak_pos_abs[0] * 0.4, nihlathak_pos_abs[1] * 0.4]
self._cast_frozen_orb(cast_pos_abs, spray=0)
self._cast_static_field(times=3)
#Continue blizzorb combo until attack length expires
while (time.time() - start) < Config().char["atk_len_nihlathak"]:
self._cast_blizzorb_spike_combo(cast_pos_abs, spray=0)
self._pather.traverse_nodes(end_nodes, self, timeout=0.8)
return True
else:
return False
def kill_council(self) -> bool:
def clear_inside(use_static: bool):
self._pather.traverse_nodes([228,229], self, timeout=2.2, do_pre_move=False, force_tp=True, use_tp_charge=True)
if use_static:
self._cast_static_field(2)
self._cast_blizzorb_spike_combo([-80,-60])
def clear_outside(use_static: bool):
self._pather.traverse_nodes_fixed([(430, 642)], self)
if use_static:
self._cast_static_field(2)
self._cast_blizzorb_spike_combo([-80,-60])
start = time.time()
clear_inside(True)
clear_outside(True)
while (time.time() - start) < Config().char["atk_len_trav"]:
clear_inside(False)
clear_outside(False)
return True
def kill_summoner(self) -> bool:
#Set cast position right below alter
cast_pos_abs = [0, 20]
start = time.time()
while (time.time() - start) < Config().char["atk_len_arc"]:
self._cast_blizzorb_whirlwind_combo(cast_pos_abs, spray=0)
return True
def kill_cs_trash(self, location:str) -> bool:
#Set cast position right below alter
start = time.time()
while (time.time() - start) < Config().char["atk_len_cs_trashmobs"]:
self._cast_blizzorb_whirlwind_combo(spray=0)
return True
def kill_vizier(self, seal_layout:str) -> bool:
#Set cast position right below alter
start = time.time()
while (time.time() - start) < Config().char["atk_len_diablo_vizier"]:
self._cast_blizzorb_whirlwind_combo(spray=0)
return True
+2 -2
View File
@@ -1,7 +1,7 @@
import time
import keyboard
from input_layer import keyboard
from char.sorceress import Sorceress
from utils.custom_mouse import mouse
from input_layer import mouse
from logger import Logger
from utils.misc import wait
import random
+3 -3
View File
@@ -1,6 +1,6 @@
import keyboard
from input_layer import keyboard
from char.sorceress import Sorceress
from utils.custom_mouse import mouse
from input_layer import mouse
from logger import Logger
from utils.misc import wait, rotate_vec, unit_vector
import random
@@ -241,7 +241,7 @@ class LightSorc(Sorceress):
if __name__ == "__main__":
import os
import keyboard
from input_layer import keyboard
from pather import Pather
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
+19 -26
View File
@@ -1,14 +1,14 @@
import keyboard
from input_layer import keyboard
import time
import numpy as np
from char.sorceress import Sorceress
from utils.custom_mouse import mouse
from input_layer import mouse
from logger import Logger
from utils.misc import wait
from pather import Location
from screen import convert_abs_to_monitor
from screen import convert_abs_to_monitor, grab
from config import Config
import cv2
class NovaSorc(Sorceress):
def __init__(self, *args, **kwargs):
@@ -24,10 +24,10 @@ class NovaSorc(Sorceress):
wait(0.05, 0.1)
start = time.time()
while (time.time() - start) < time_in_s:
wait(0.03, 0.04)
mouse.press(button="right")
wait(0.12, 0.2)
wait(0.06, 0.08)
mouse.release(button="right")
wait(self._cast_duration-0.06)
def _move_and_attack(self, abs_move: tuple[int, int], atk_len: float):
pos_m = convert_abs_to_monitor(abs_move)
@@ -37,7 +37,6 @@ class NovaSorc(Sorceress):
def kill_pindle(self) -> bool:
self._pather.traverse_nodes_fixed("pindle_end", self)
self._cast_static(0.6)
self._nova(Config().char["atk_len_pindle"])
return True
@@ -54,29 +53,23 @@ class NovaSorc(Sorceress):
return True
def kill_council(self) -> bool:
# Check out the node screenshot in assets/templates/trav/nodes to see where each node is at
atk_len = Config().char["atk_len_trav"] * 0.21
# change node to be further to the right
offset_229 = np.array([200, 100])
self._pather.offset_node(229, offset_229)
def clear_inside():
self._pather.traverse_nodes_fixed([(1110, 120)], self)
self._pather.traverse_nodes([229], self, timeout=0.8, force_tp=True)
atk_len = Config().char["atk_len_trav"]
self._cast_static(0.6)
def clear_inside(use_static: bool):
self._pather.traverse_nodes_fixed([(875, 20)], self)
if use_static:
self._cast_static(0.6)
self._nova(atk_len)
self._move_and_attack((-40, -20), atk_len)
self._move_and_attack((40, 20), atk_len)
self._move_and_attack((40, 20), atk_len)
def clear_outside():
self._pather.traverse_nodes([226], self, timeout=0.8, force_tp=True)
self._pather.traverse_nodes_fixed([(430, 642)], self)
self._nova(atk_len)
self._move_and_attack((45, -20), atk_len)
self._move_and_attack((-45, 20), atk_len)
clear_inside()
clear_inside(True)
clear_outside()
clear_inside()
clear_inside(False)
clear_outside()
# change back node as it is used in trav.py
self._pather.offset_node(229, -offset_229)
return True
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
@@ -107,7 +100,7 @@ class NovaSorc(Sorceress):
if __name__ == "__main__":
import os
import keyboard
from input_layer import keyboard
from pather import Pather
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
+32 -23
View File
@@ -1,6 +1,6 @@
import keyboard
from input_layer import keyboard
from typing import Callable
from utils.custom_mouse import mouse
from input_layer import mouse
from char import IChar
import template_finder
from pather import Pather
@@ -10,28 +10,32 @@ import time
from pather import Pather
from config import Config
from ui_manager import ScreenObjects, is_visible
from screen import convert_abs_to_monitor
class Sorceress(IChar):
def __init__(self, skill_hotkeys: dict, pather: Pather):
super().__init__(skill_hotkeys)
self._pather = pather
self._action_frame = 7
if Config().char["casting_frames"] == 11 or Config().char["casting_frames"] == 10:
self._action_frame = 6
elif Config().char["casting_frames"] == 9 or Config().char["casting_frames"] == 8:
self._action_frame = 5
elif Config().char["casting_frames"] == 7:
self._action_frame = 4
self._action_duration = self._action_frame * 0.04
def pick_up_item(self, pos: tuple[float, float], item_name: str = None, prev_cast_start: float = 0):
if self._skill_hotkeys["telekinesis"] and any(x in item_name for x in ['potion', 'misc_gold', 'tp_scroll']):
def pick_up_item(self, pos: tuple[float, float], item_name: str = None, distance: int = 0, force_run: bool = False):
if (distance < 600) and self._skill_hotkeys["telekinesis"] and any(x in item_name for x in ['Potion', 'GOLD', 'Scroll of', 'Chest']):
keyboard.send(self._skill_hotkeys["telekinesis"])
wait(0.1, 0.2)
mouse.move(pos[0], pos[1])
wait(0.1, 0.2)
wait(0.1, 0.1)
mouse.click(button="right")
# need about 0.4s delay before next capture for the item not to persist on screen
cast_start = time.time()
interval = (cast_start - prev_cast_start)
cast_duration_wait = (self._cast_duration - interval)
delay = 0.35 if cast_duration_wait <0 else (0.35+cast_duration_wait)
wait(delay,delay+0.1)
return cast_start
wait(self._cast_duration,self._cast_duration)
self._stationary = True #We used telekinesis so we should be guaranteed stationary now
return True
else:
return super().pick_up_item(pos, item_name, prev_cast_start)
return super().pick_up_item(pos, item_name, distance, force_run)
def select_by_template(
self,
@@ -65,30 +69,35 @@ class Sorceress(IChar):
# In case telekinesis fails, try again with the base implementation
return super().select_by_template(template_type, success_func, timeout, threshold)
def pre_buff(self):
if Config().char["cta_available"]:
self._pre_buff_cta()
def cast_buffs(self, casting_delay: float):
if self._skill_hotkeys["energy_shield"]:
keyboard.send(self._skill_hotkeys["energy_shield"])
wait(0.1, 0.13)
mouse.click(button="right")
wait(self._cast_duration)
wait(casting_delay)
if self._skill_hotkeys["thunder_storm"]:
keyboard.send(self._skill_hotkeys["thunder_storm"])
wait(0.1, 0.13)
mouse.click(button="right")
wait(self._cast_duration)
wait(casting_delay)
if self._skill_hotkeys["frozen_armor"]:
keyboard.send(self._skill_hotkeys["frozen_armor"])
wait(0.1, 0.13)
mouse.click(button="right")
wait(self._cast_duration)
wait(casting_delay)
def _cast_static(self, duration: float = 1.4):
if self._skill_hotkeys["static_field"]:
keyboard.send(self._skill_hotkeys["static_field"])
wait(0.1, 0.13)
#Static field can fail to cast if we right click on a wall. We move mouse to
#center to prevent this.
cast_pos_monitor = convert_abs_to_monitor((0, 0))
mouse.move(*cast_pos_monitor)
start = time.time()
while time.time() - start < duration:
mouse.click(button="right")
wait(self._cast_duration)
mouse.press(button="right")
wait(0.06, 0.06)
mouse.release(button="right")
wait(self._cast_duration-0.06)

Some files were not shown because too many files have changed in this diff Show More