Compare commits

..
61 Commits
Author SHA1 Message Date
mgleed 7bc34a7b2f bump version (#720) 2022-04-20 20:01:03 -04:00
mgleedandaeon0 2dc1356cf7 Bugfixes: A2 TP stuck, also recapture some templates for d2r v2.4
* add _capture_template switch to template_finder.py _main function, replace eldritch templates

* update some trav templates

* update template_finder.py, update act 3 town templates

* update missing Eldritch templates

* add redudant eldritch templates and update nodes

* update shenk templates

* update trav assets, add redundant assets w nodes

* bugfix: a2 tp out of range of a2 templates

Co-authored-by: aeon0 <[email protected]>
2022-04-20 19:06:55 -04:00
mgleed ae0ab54efe Bugfix: Cain loop, etc. with return_to_play() function 2022-04-20 19:05:37 -04:00
mgleed bfb367ee62 Clear default launch_options param for planned upcoming stable release 2022-04-20 19:04:48 -04:00
mgleed 8d925509fa Bugfix: Default pickit errors with berserker axe and war pike 2022-04-20 09:26:40 -07:00
mgleed 811d656022 Increase padding from 1 to 2 spaces, pad gold (#711) 2022-04-20 12:16:24 +02:00
c7c0e9e1a3 Updated pot drinking logic (#710)
* Updated pot drinking logic

* fixed bugs

* simplify

* Fixed bad hp/mana check

* Fixed bad hp/mana check, (last was noob git by me)

Co-authored-by: Your Name <[email protected]>
Co-authored-by: mgleed <[email protected]>
2022-04-20 12:15:50 +02:00
aeon0andmgleed 9bc085af3a feat/restricted-mouse-clicks (#709)
* feat/restricted-mouse-clicks

* fix typos, use GoldBtnInventory ScreenObject, use personal.specific_inventory_roi()

* make smaller gold button, trim ROI, and use grayscale to search

* remove unused roi

* fix circular import by reverting to Config inventory area and adding open_inventory_area variable, adjust code throughout

* simplify

* revert to template finding due to circular import with ui_manager

* grayscale template match

Co-authored-by: mgleed <[email protected]>
2022-04-18 17:00:18 +02:00
roozhou a5d7fdaffa BugFix: Fix indicator_location_to_tab_count (#708)
1) 4 tabs instead of 3
2) Return value should be integer when failed
2022-04-17 12:47:34 +02:00
aeon0 16bf599d88 feat/run-qol (#707) 2022-04-17 12:40:53 +02:00
aeon0 59ba4409b5 Update i_char.py (#706) 2022-04-16 07:53:26 -07:00
mgleed f510965d05 init (#703) 2022-04-16 07:43:55 +02:00
aeon0 d43441358d Bugfix: Create mod folder if it doesnt exist (#701) 2022-04-14 21:47:39 -07:00
Aimware0 1ece05f7a7 Merge pull request #702 from roozhou/tooltip_fix
BugFix: Allow 2 lines of tooltip
2022-04-14 20:58:11 -04:00
roozhou ca93f3aa5d BugFix: Allow 2 lines of tooltip
Fix item_cropper failure in 2.4
2022-04-14 17:47:39 -07:00
tomoyo233 5c6e2211d6 Kill BlizzardError.exe (#688)
Kill the BlizzardError.exe process before restarting the game
2022-04-14 23:08:39 +02:00
mgleed f469d98f74 Bugfix: Item tooltip obscuring close button 2022-04-12 14:32:35 -07:00
Legit 699fcac7af Bugfix: missed pnova overhaul fixed (#694) 2022-04-06 22:54:29 -04:00
Legit c22275c1bb Bugfix: old ui overhaul time out modified for pnova necro (#693) 2022-04-06 18:38:17 -04:00
mgleed 470f42b1b9 Bugfix: renamed mod folder from ifilter to botty and narrowed rmtree (#690)
* fix

* didn't save
2022-04-03 21:13:03 -04:00
Legit 4b5b8385f8 Refactor: Condensed discord message will be the only option (#687)
* Condsensed discord message will be the only option

Condensed message will be default. removed the option from params.

* Update game_stats.py

* Update discord_embeds.py

* Update bot.py

* Update shenk_eld.py

* Update config.py

* removed a line

removed e.set_thumbnail(url=f"{self._psnURL}36L4a4994.png")

* Update README.md
2022-04-03 20:16:27 -04:00
jagarop 42655d5209 Enhancement: Stop gem pickups from logging to item count
Remove gems from loggin
2022-04-02 21:32:55 -04:00
mgleed 09a33ccf3d Feature/enhancement: Support for D2R launch options and cosmetic mods (#677)
Breaking change to param: `d2r_path`
2022-04-02 21:27:03 -04:00
egut125 fd93910941 Bugfix: Extra space (#682) 2022-04-01 13:46:54 -04:00
egut125 51de6f9f00 Bugfix: Placing list manipulation inside try block (#681) 2022-04-01 11:52:11 -04:00
egut125 d615f11608 Bugfix: Divide by zero fix (#680) 2022-03-31 22:39:10 -04:00
egut125andmgleed c6d2f3f068 Bugfix: Quick list manipulation to remove anything before experience in the list (#679)
* Quick list manipulation to remove anything before experience in the list

* update

* stupid ide

* cleanup

Co-authored-by: mgleed <[email protected]>
2022-03-31 22:38:13 -04:00
mgleed 0606adb5da Bugfix: Corpse pickup broken after Exp implementation (#675) 2022-03-31 17:16:39 -04:00
mgleed 82576282ab Enhancement: Update OCR model for large numbers 2022-03-31 17:16:09 -04:00
mgleed e6fe7ef609 Enhancement/bugfix: Refactor checks for item transferring (#673)
* init

* add confirmed action debug line
2022-03-30 19:49:13 -04:00
egut125 45e7a5ca9a Enhancement: Update messages to have better values (#671) 2022-03-30 13:06:50 -04:00
egut125 7d3710d1e8 Bugfix: Remove %exp calculation in player_bar()
Breaks on lvl 99 and doesn’t correctly calculate percent to next level anyways.
2022-03-29 08:40:02 -04:00
mgleed e354ed8074 Bugfix: custom config parsing (#668) 2022-03-28 18:01:12 -04:00
egut125andmgleed c4a5c6fe34 Feature: Runtime breaks (#665)
* New params for runtime and break time

* New params to manage session length and break time

* Starting message

* Message adjustments

* use _default_iff in config

Co-authored-by: mgleed <[email protected]>
2022-03-28 17:55:15 -04:00
egut125 2079b81df4 Feature: Leverage OCR to read exp and track in-game stats (#662)
* Leveraging OCR to read exp and track in game_stats

Start of each game and end of each run OCR exp
game_stats stores starting EXP and current EXP
game_stats summary calculates XP gained, XP per game, XP per hour

* Adding lvl tracking and xp to next level stats

* logic clean up
2022-03-28 09:25:30 -04:00
Aimware0andYour Name e64b312562 Bugfix: error handling for common belt error (#664)
Co-authored-by: Your Name <[email protected]>
2022-03-28 09:18:23 -04:00
Aimware0andYour Name cceb7905c7 Enhancement: Logger colors (#663)
Co-authored-by: Your Name <[email protected]>
2022-03-28 09:17:47 -04:00
4007087ceb Enhancement: variables can be initialized/used in config files
* Added a way to add config variables to ini files.

* Added examples

* Added examples

Co-authored-by: Your Name <[email protected]>
Co-authored-by: mgleed <[email protected]>
2022-03-28 09:17:09 -04:00
e64f1151f3 Enhancement: Use keyboard to select difficulty (#661)
* Enhancement: Use keyboard to select difficulty

* press instead of send

if somehow the select difficult menu is not available when the while True: loop is running, will still work.

* simplify, delete now unused assets

* cleanup more

Co-authored-by: jagarop <[email protected]>
Co-authored-by: mgleed <[email protected]>
2022-03-25 22:35:45 -04:00
mgleed 46202e5a69 Bugfix/enhancement: Fix ocr output wordlist check, replace difflib with Lev (#658)
* init

* remove unused difflib import

* use built-in function

* small changes
2022-03-23 16:11:57 -04:00
Legit 782001d338 Feature: Poison necro config (#597)
* Poison necro config

* Update bot.py

* added "walk"

to move summons after teleporting.

* Add files via upload

* Update params.ini

* Update params.ini

* Update poison_necro.py

* Update poison_necro.py
2022-03-23 14:28:07 -04:00
mgleed 4eeadb934f bump (#653) 2022-03-21 21:17:55 -04:00
mgleed 1f16c13300 Bump to 0.7.2 (#652) 2022-03-19 20:59:05 -04:00
mgleed ee479fb526 Bugfix: Stash tab counts not tracking (#646)
* init

* saved stuff

* fix startup

* fix

* gdi
2022-03-19 12:02:14 -04:00
Legit d05ff50f76 Enhancement: Improved eld attack sequence for Blizz sorc (#608)
* improved eld attack sequence

added an extra attack sequences and added wait timers.

* Update blizz_sorc.py
2022-03-18 17:14:41 -04:00
Aimware0 927ade6108 Bugfix: added error handling for custom.ini when missing section header (#647) 2022-03-18 17:02:12 -04:00
Steve6030 5eda34ded0 Enhancement: Tweaked light sorc attack sequence (#644) 2022-03-18 16:45:04 -04:00
mgleedandVladimir Makaev d9b9d66e3b Feature: Use OCR for gold pickit (#643)
* init

* consolidate

* Update src/config.py

Co-authored-by: Vladimir Makaev <[email protected]>

Co-authored-by: Vladimir Makaev <[email protected]>
2022-03-17 21:31:48 -04:00
mgleed 1adeb3a32a Bugfix: Some NPC's don't open to misc tab (#642)
* init

* cleanup

* clean

* cleanup
2022-03-16 21:52:29 -04:00
mgleed 1abf57eae1 Bugfix/refactor: Safer screen transitions for laggy setups (#641)
* init

* update

* WIP. cleanup code. refactor stashing routines, etc. hopefully now closer to lag-proof

* more simplification, ready for testing

* change default normalize_monitor and adjust code accordingly. testing

* undo save/exit keyboard

* forgot to save a couple

* fix gold checking
2022-03-16 16:06:12 -04:00
Legit 65797eba8f Note for num loot (#637)
created note for num loot to avoid confusion. Per Sosa.
2022-03-14 17:25:34 -04:00
mgleed 03a6e8b05c Bugfix/refactor: Arcane run doesn't turn off health_monitor when in town 2022-03-14 16:01:07 -04:00
mgleed 2c09459bde Bugfix/enhancement: Optimize waits; implement wait_for_expiration(ScreenObject) function (#629)
* init

* simplify

* the

* init

* update

* init

* fix belt open

* rework waiting functions in ui_manager.py
2022-03-13 20:19:41 -04:00
aeon0anduidn4233 7564fea4b0 Remove: Codecov (#632)
* Remove: Codecov

* test commit

* revert test commit

Co-authored-by: uidn4233 <[email protected]>
2022-03-12 22:21:14 -05:00
mgleed 95b27f099f Bugfix/refactor: Search for open panels instead of skill icons (#631)
* init

* add town logic

* add additional close asset for HELP menu

* fin

* a
2022-03-11 17:46:30 -05:00
mgleed e96c042853 Bugfix: Stash tab gold was being read based on prior stash tab image (#628)
* init

* simplify

* the

* init

* update
2022-03-11 13:53:46 -05:00
mgleed 57bf7a0f7d Bugfix: log_item_keep without game_stats (#627)
* init

* simplify

* the
2022-03-11 11:57:32 -05:00
mgleed da892d9043 Bugfix: Laggy inventory loading (#625)
* init

* undo item add

* add msg

* curse you Vladimir for reviewing too carefully
2022-03-10 17:28:22 -05:00
mgleed 7fad5e9aff Bugfixes: Quantity: parse fail handling, if in-game icon not found during return_to_play() 2022-03-10 17:07:35 -05:00
mgleed 840ffcdf0c Update version.py (#621) 2022-03-10 17:06:58 -05:00
Ezro 1078563f2d Bugfix: Prevent join for NoneType and start_detecting_window() in main.py (#624)
* Prevent join for NoneType and start_detecting_window() in main.py

* Start and stop in the debugger toggle
2022-03-09 23:15:36 -05:00
143 changed files with 49220 additions and 975 deletions
-5
View File
@@ -50,8 +50,3 @@ jobs:
run: |
C:\Miniconda\condabin\conda.bat activate botty
coverage xml
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v2
with:
verbose: true
files: ./coverage.xml
+1 -2
View File
@@ -59,7 +59,6 @@ run_shenk=0
| difficulty | Set to `normal` `nightmare` or `hell` for game difficulty. |
| message_api_type | Which api to use to send botty messages. Supports "generic_api" (basic discord), or "discord" (discord embeds with images). |
| discord_status_count | Number of games between discord status messages being sent. Leave empty for no status reports.
| discord_status_condensed | Toggles condensed view of Discord status messages. 0 Full text, 1 Condensed text.
| info_screenshots | If `1`, the bot takes a screenshot with timestamp on every stuck / chicken / timeout / inventory full event. This is 1 by Default, so remember to clean up the folder every once in a while. |
| loot_screenshots | If `1`, the bot takes a screenshot with timestamp everytime he presses `show_items` button and saves it to `loot_screenshots` folder. Remember to clear them once in a while... |
| saved_games_folder | [Optional] Defaults to `~\Saved Games\Diablo II Resurrected`. Used to store configuration settings for `f9` / auto settings. |
@@ -97,7 +96,7 @@ run_shenk=0
| battle_order | Hotkey for battle orders from cta (only needed if cta_available=1) |
| battle_command | Hotkey for battle command from cta (only needed if cta_available=1) |
| stash_gold | Bool value to stash gold each time when stashing items |
| gold_trav_only | Hacky config that will restrict gold pickup to trav only. misc_gold must be set to 1 for this to have any effect |
| min_gold_to_pick | Minimum quantity of gold to pickup (also must set misc_gold=1 in pickit config) |
| use_merc | Set to 1 for using merc. Set to 0 for not using merc (will not revive merc when dead), default = 1 |
| atk_len_arc | Attack length for hdin/sorc fighting arcane |
| atk_len_trav | Attack length for hdin fighting trav (note this atk length will be applied in 4 different spots each) |
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
{
"name": "botty",
"savepath": "../"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
comment: false
+6 -1
View File
@@ -22,6 +22,7 @@ mana_globe=117,120,20,121,255,255
blue_slot=102,194,18,138,230,54
green_slot=33,181,18,87,258,69
red_slot=161,204,28,197,240,64
tab_text=0,0,125,180,255,255
[ui_pos]
screen_width=1280
@@ -85,7 +86,7 @@ chat_line_1=12,537,391,25
save_and_exit=500,233,273,173
play_btn=426,616,320,71
difficulty_select=536,236,210,320
gold_btn=927,491,267,100
gold_btn=997,521,20,18
inventory_gold=980,510,150,40
gold_btn_stash=150,480,40,65
vendor_gold_digits=186,509,97,16
@@ -130,6 +131,10 @@ right_panel_header=830,0,450,54
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
deposit_btn=454,365,186,43
equipped_inventory_area=861,59,387,287
[path]
; static pathes in format: x0,y0, x1,y1, x2,y2, ...
+33 -12
View File
@@ -5,28 +5,30 @@ message_api_type=discord
custom_message_hook=
max_game_length_s=380
max_consecutive_fails=0
max_runtime_before_break_m=0
break_length_m=0
randomize_runs=0
difficulty=hell
info_screenshots=1
loot_screenshots=0
discord_status_count=20
discord_status_condensed=1
saved_games_folder=
d2r_path=C:\Program Files (x86)\Diablo II Resurrected\D2R.exe
d2r_path=C:\Program Files (x86)\Diablo II Resurrected
; if you set this field to 1, botty will save the top-most visible character template just after
; the online/offline tab and upon restart will attempt to find the same template to select the char
restart_d2r_when_stuck=0
[routes]
run_trav=0
run_pindle=1
run_eldritch=1
run_shenk=0
run_nihlathak=0
run_arcane=0
run_diablo=0
; Note: You can not split up or change order of eldritch and shenk as botty treats it as a single run
order=run_pindle, run_eldritch, run_shenk, run_nihlathak, run_trav, run_arcane, run_diablo
; Add these possible routes to "order" to run them:
; run_trav
; run_pindle
; run_eldritch
; run_eldritch_shenk
; run_nihlathak
; run_arcane
; run_diablo
order=run_pindle, run_eldritch_shenk
[char]
; ==========================
@@ -38,6 +40,7 @@ casting_frames=10
show_items=alt
inventory_screen=i
tp=f9
;Note: loot columns are counted from left to right. Items on the left side of inventory will be considered lootable.
num_loot_columns=5
force_move=e
;Note: stand_still can not be the default "shift" as it would interfere with merc healing
@@ -57,7 +60,7 @@ battle_command=f8
; ==== Optional configs ====
; ==========================
stash_gold=1
gold_trav_only=0
min_gold_to_pick=1000
use_merc=1
; Attack length for barbarians should be as high as 8-10 and even 10-12 for trav/shenk
atk_len_arc=2.5
@@ -202,6 +205,23 @@ corpse_explosion=
clear_pindle_pack=
heart_of_wolverine=
[poison_necro]
teleport=
poison_nova=
# Not Required
skill_left=
bone_armor=
# Can use any golem
clay_golem=
raise_skeleton=
raise_mage=
raise_revive=
# Can Use any curse
lower_res=
corpse_explosion=
clear_pindle_pack=
heart_of_wolverine=
; ===========================
; ==== Builds: Basic ====
; ===========================
@@ -259,3 +279,4 @@ hwnd_window_process=D2R\.exe
;If you want to control Hyper-V window from host use 0,51 here
window_client_area_offset=0,0
ocr_during_pickit=0
launch_options=
+37 -21
View File
@@ -37,15 +37,31 @@
;examples
; keeps small charms with resi, 7_mf or 3 max_AD and AR
; magic_small_charm=1, (and(resi, 7_mf), and(3_maximum_damage, attack_rating)
; magic_small_charm=1, (all_resist, 7_magicfind, and(3_maximum_damage, attack_rating))
; keeps all Skiller or (Life, max_AD, Life) GC
; magic_grand_charm=1, (Amazonskiller, Assasinskiller, Barbarianskiller, Druidskiller, Necromancerskiller, Paladinskiller, Sorceressskiller, (Life, maximum_damage, attack_rating))
; keeps greater talons with (2 Assa Skills + 3 lightning sentry) or (+Traps skills and + lightning sentry)
; magic_greater_talons=1, ((2_assasin_skills, 3_lightning_sentry), (traps, lightning_sentry))
; you can also create and use variables for filters you use often
; make sure it's under the [variables] section
;examples
; creates a variable called has_skiller
; has_skiller=Amazonskiller, Assasinskiller, Barbarianskiller, Druidskiller, Necromancerskiller, Paladinskiller, Sorceressskiller
; use the has_skiller variable
; magic_grand_charm=1, (has_skiller, (Life, maximum_damage, attack_rating))
[variables]
;==================================================================;
;<<<<<<<<<<<<<<<<<<<<<<<<<<<< Variables >>>>>>>>>>>>>>>>>>>>>>>>>>>>;
;==================================================================;
has_skiller=Amazonskiller, Assasinskiller, Barbarianskiller, Druidskiller, Necromancerskiller, Paladinskiller, Sorceressskiller
lightning_fire_cold_res=and(lightning_resist, fire_resist, cold_resist)
[items]
;==================================================================;
;<<<<<<<<<<<<<<<<<<<<<<<<<<<< MISC >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>;
@@ -152,7 +168,7 @@ rune_33_zod=2
;==================================================================;
;magic charms
magic_grand_charm=1, (Amazonskiller, Assasinskiller, Barbarianskiller, Druidskiller, Necromancerskiller, Paladinskiller, Sorceressskiller, (Life, maximum_damage, attack_rating))
magic_grand_charm=1, (has_skiller, (Life, maximum_damage, attack_rating))
magic_large_charm=0
magic_small_charm=1, (all_resist, 7_magicfind, 20_life, and(3_maximum_damage, attack_rating))
@@ -345,25 +361,25 @@ rare_colossus_girdle=0,, ethereal
;---------------------------------------;
;normal rare boots
rare_greaves=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_chain_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_light_plated_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_heavy_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_greaves=1, lightning_fire_cold_res, ethereal
rare_boots=1, lightning_fire_cold_res, ethereal
rare_chain_boots=1, lightning_fire_cold_res, ethereal
rare_light_plated_boots=1, lightning_fire_cold_res, ethereal
rare_heavy_boots=1, lightning_fire_cold_res, ethereal
;exceptional rare boots
rare_war_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_battle_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_mesh_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_sharkskin_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_demonhide_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_war_boots=1, lightning_fire_cold_res, ethereal
rare_battle_boots=1, lightning_fire_cold_res, ethereal
rare_mesh_boots=1, lightning_fire_cold_res, ethereal
rare_sharkskin_boots=1, lightning_fire_cold_res, ethereal
rare_demonhide_boots=1, lightning_fire_cold_res, ethereal
;elite rare boots
rare_myrmidon_greaves=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_mirrored_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_boneweave_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_scarabshell_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_wyrmhide_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
rare_myrmidon_greaves=1, lightning_fire_cold_res, ethereal
rare_mirrored_boots=1, lightning_fire_cold_res, ethereal
rare_boneweave_boots=1, lightning_fire_cold_res, ethereal
rare_scarabshell_boots=1, lightning_fire_cold_res, ethereal
rare_wyrmhide_boots=1, lightning_fire_cold_res, ethereal
;---------------------------------------;
;============= RARE GLOVES =============;
@@ -709,14 +725,14 @@ white_superior_phase_blade=0
;-----------------------------------------;
;eth/non eth/socketed weapons
gray_weapon_berserker_axe=0, (socketed_1, socketed_2, socketed_3, socketed_4)
gray_weapon_berserker_axe=0,, (socketed_1, socketed_2, socketed_3, socketed_4)
gray_weapon_crystal_sword=0,, (ethereal, socketed_1, socketed_2, socketed_3)
gray_weapon_flail=0,, (ethereal, socketed_1, socketed_2, socketed_3)
gray_weapon_phase_blade=0,, (ethereal, socketed_1, socketed_2, socketed_3, socketed_4, socketed_6)
gray_weapon_war_scepter=1,, (ethereal, socketed_1, socketed_2, socketed_3, socketed_4)
gray_divine_scepter=1,, (ethereal, socketed_1, socketed_2, socketed_3, socketed_4)
gray_caduceus=1,, (ethereal, socketed_1, socketed_2, socketed_3, socketed_4)
gray_weapon_war_pike=0, (socketed_1, socketed_2, socketed_3, socketed_4, socketed_5)
gray_weapon_war_pike=0,, (socketed_1, socketed_2, socketed_3, socketed_4, socketed_5)
;superior ethereal
gray_superior_berserker_axe=1, enhanced_damage, (socketed_1, socketed_2, socketed_3, socketed_4)
@@ -1454,4 +1470,4 @@ rare_thunder_maul=0, ethereal
rare_truncheon=0, ethereal
rare_tyrant_club=0, ethereal
rare_war_club=0, ethereal
rare_war_hammer=0, ethereal
rare_war_hammer=0, ethereal
+2
View File
@@ -31,3 +31,5 @@ dependencies:
- parse
- dependencies/tesserocr-2.5.2-cp39-cp39-win_amd64.whl
- typing_extensions
- rapidfuzz
- colorama
+79 -42
View File
@@ -5,12 +5,14 @@ import time
import os
import random
import cv2
import math
from copy import copy
from typing import Union
from collections import OrderedDict
from health_manager import set_pause_state
from transmute import Transmute
from utils.misc import wait, hms
from utils.misc import wait
from game_stats import GameStats
from logger import Logger
from config import Config
@@ -25,9 +27,10 @@ from char.trapsin import Trapsin
from char.hammerdin import Hammerdin
from char.barbarian import Barbarian
from char.necro import Necro
from char.poison_necro import Poison_Necro
from char.basic import Basic
from char.basic_ranged import Basic_Ranged
from ui_manager import wait_for_screen_object, detect_screen_object, ScreenObjects
from ui_manager import wait_until_hidden, wait_until_visible, ScreenObjects, is_visible
from ui import meters, skills, view, character_select, main_menu
from inventory import personal, vendor, belt, common, consumables
@@ -61,6 +64,8 @@ class Bot:
self._char: IChar = Trapsin(Config().trapsin, self._pather)
elif Config().char["type"] == "barbarian":
self._char: IChar = Barbarian(Config().barbarian, self._pather)
elif Config().char["type"] == "poison_necro":
self._char: IChar = Poison_Necro(Config().poison_necro, self._pather)
elif Config().char["type"] == "necro":
self._char: IChar = Necro(Config().necro, self._pather)
elif Config().char["type"] == "basic":
@@ -80,16 +85,13 @@ class Bot:
self._town_manager = TownManager(a1, a2, a3, a4, a5)
# Create runs
if Config().routes["run_shenk"] and not Config().routes["run_eldritch"]:
Logger.error("Running shenk without eldtritch is not supported. Either run none or both")
os._exit(1)
self._do_runs = {
"run_trav": Config().routes["run_trav"],
"run_pindle": Config().routes["run_pindle"],
"run_shenk": Config().routes["run_shenk"] or Config().routes["run_eldritch"],
"run_nihlathak": Config().routes["run_nihlathak"],
"run_arcane": Config().routes["run_arcane"],
"run_diablo": Config().routes["run_diablo"],
"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"),
}
# 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])
@@ -105,7 +107,6 @@ class Bot:
self._diablo = Diablo(self._pather, self._town_manager, self._char, self._pickit)
# Create member variables
self._pick_corpse = False
self._picked_up_items = False
self._curr_loc: Union[bool, Location] = None
self._use_id_tome = True
@@ -116,6 +117,7 @@ class Bot:
self._current_threads = []
self._ran_no_pickup = False
self._previous_run_failed = False
self._timer = time.time()
# Create State Machine
self._states=['initialization','hero_selection', 'town', 'pindle', 'shenk', 'trav', 'nihlathak', 'arcane', 'diablo']
@@ -218,8 +220,7 @@ class Bot:
def on_create_game(self):
# Start a game from hero selection
m = wait_for_screen_object(ScreenObjects.MainMenu)
if m.valid:
if (m := wait_until_visible(ScreenObjects.MainMenu)).valid:
if "DARK" in m.name:
keyboard.send("esc")
main_menu.start_game()
@@ -229,6 +230,19 @@ class Bot:
def on_start_from_town(self):
self._curr_loc = self._town_manager.wait_for_town_spawn()
# Handle picking up corpse in case of death
if (corpse_present := is_visible(ScreenObjects.Corpse)):
self._previous_run_failed = True
view.pickup_corpse()
wait_until_hidden(ScreenObjects.Corpse)
belt.fill_up_belt_from_inventory(Config().char["num_loot_columns"])
self._char.discover_capabilities()
if corpse_present and self._char.capabilities.can_teleport_with_charges and not self._char.select_tp():
keybind = self._char._skill_hotkeys["teleport"]
Logger.info(f"Teleport keybind is lost upon death. Rebinding teleport to '{keybind}'")
self._char.remap_right_skill_hotkey("TELE_ACTIVE", self._char._skill_hotkeys["teleport"])
# Check for the current game ip and pause if we are able to obtain the hot ip
if Config().dclone["region_ips"] != "" and Config().dclone["dclone_hotip"] != "":
cur_game_ip = get_d2r_game_ip()
@@ -250,23 +264,18 @@ class Bot:
Logger.info("Activated /nopickup")
else:
Logger.error("Failed to detect if /nopickup command was applied or not")
self._game_stats.log_exp()
self.trigger_or_stop("maintenance")
def on_maintenance(self):
# Handle picking up corpse in case of death
self._pick_corpse = detect_screen_object(ScreenObjects.Corpse).valid
if self._pick_corpse:
self._previous_run_failed = True
time.sleep(1.6)
view.pickup_corpse()
wait(1.2, 1.5)
belt.fill_up_belt_from_inventory(Config().char["num_loot_columns"])
wait(0.5)
self._char.discover_capabilities()
if self._pick_corpse and self._char.capabilities.can_teleport_with_charges and not self._char.select_tp():
keybind = self._char._skill_hotkeys["teleport"]
Logger.info(f"Teleport keybind is lost upon death. Rebinding teleport to '{keybind}'")
self._char.remap_right_skill_hotkey("TELE_ACTIVE", self._char._skill_hotkeys["teleport"])
# Pause health manager if not already paused
set_pause_state(True)
# Dismiss skill/quest/help/stats icon if they are on screen
if not view.dismiss_skills_icon():
view.return_to_play()
# Look at belt to figure out how many pots need to be picked up
belt.update_pot_needs()
@@ -281,7 +290,7 @@ class Bot:
# 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
self._use_keys = detect_screen_object(ScreenObjects.Key, img).valid
self._use_keys = is_visible(ScreenObjects.Key, img)
if (self._game_stats._run_counter - 1) % 4 == 0 or self._previous_run_failed:
consumables.update_tome_key_needs(img, item_type = 'tp')
if self._use_id_tome:
@@ -323,7 +332,6 @@ 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()}")
wait(0.5, 0.8)
elif meters.get_health(img) < 0.6 or meters.get_mana(img) < 0.2:
Logger.info("Healing at next possible Vendor")
self._curr_loc = self._town_manager.heal(self._curr_loc)
@@ -340,10 +348,9 @@ class Bot:
if not self._curr_loc:
return self.trigger_or_stop("end_game", failed=True)
self._picked_up_items = False
wait(1.0)
# Check if we are out of tps or need repairing
need_repair = detect_screen_object(ScreenObjects.NeedRepair).valid
need_repair = is_visible(ScreenObjects.NeedRepair)
need_routine_repair = False if not Config().char["runs_per_repair"] else self._game_stats._run_counter % Config().char["runs_per_repair"] == 0
need_refill_teleport = self._char.capabilities.can_teleport_with_charges and (not self._char.select_tp() or self._char.is_low_on_teleport_charges())
if need_repair or need_routine_repair or need_refill_teleport or sell_items:
@@ -360,11 +367,9 @@ class Bot:
items = result_items
if not self._curr_loc:
return self.trigger_or_stop("end_game", failed=True)
wait(1.0)
# Check if merc needs to be revived
match = detect_screen_object(ScreenObjects.MercIcon)
if not match.valid and Config().char["use_merc"]:
if not is_visible(ScreenObjects.MercIcon) and Config().char["use_merc"]:
Logger.info("Resurrect merc")
self._game_stats.log_merc_death()
self._curr_loc = self._town_manager.resurrect(self._curr_loc)
@@ -399,11 +404,34 @@ class Bot:
self._curr_loc = False
self._pre_buffed = False
view.save_and_exit()
set_pause_state(True)
self._game_stats.log_end_game(failed=failed)
if Config().general["max_runtime_before_break_m"] and Config().general["break_length_m"]:
elapsed_time = time.time() - self._timer
Logger.debug(f'Session length = {math.ceil(elapsed_time/60)} minutes, max_runtime_before_break_m {Config().general["max_runtime_before_break_m"]}.')
if elapsed_time > (Config().general["max_runtime_before_break_m"]*60):
break_msg = f'Ran for {hms(elapsed_time)}, taking a break for {hms(Config().general["break_length_m"]*60)}.'
Logger.info(break_msg)
self._messenger.send_message(break_msg)
if not self._pausing:
self.toggle_pause()
wait(Config().general["break_length_m"]*60)
break_msg = f'Break over, will now run for {hms(Config().general["max_runtime_before_break_m"]*60)}.'
Logger.info(break_msg)
self._messenger.send_message(break_msg)
if self._pausing:
self.toggle_pause()
self._timer = time.time()
self._do_runs = copy(self._do_runs_reset)
if Config().general["randomize_runs"]:
self.shuffle_runs()
wait(0.2, 0.5)
self.trigger_or_stop("init")
def on_end_run(self):
@@ -413,15 +441,18 @@ class Bot:
if success:
self._curr_loc = self._town_manager.wait_for_tp(self._curr_loc)
if self._curr_loc:
set_pause_state(True)
return self.trigger_or_stop("maintenance")
if not skills.has_tps():
consumables.set_needs("tp", 20)
set_pause_state(True)
self.trigger_or_stop("end_game", failed=True)
# All the runs go here
# ==================================
def _ending_run_helper(self, res: Union[bool, tuple[Location, bool]]):
self._game_stats._run_counter += 1
self._game_stats.log_exp()
# either fill member variables with result data or mark run as failed
failed_run = True
if res:
@@ -438,9 +469,10 @@ class Bot:
def on_run_pindle(self):
res = False
self._do_runs["run_pindle"] = False
self._game_stats.update_location("Pin" if Config().general['discord_status_condensed'] else "Pindle")
self._game_stats.update_location("Pin")
self._curr_loc = self._pindle.approach(self._curr_loc)
if self._curr_loc:
set_pause_state(False)
res = self._pindle.battle(not self._pre_buffed)
self._ending_run_helper(res)
@@ -449,41 +481,46 @@ class Bot:
self._do_runs["run_shenk"] = False
self._curr_loc = self._shenk.approach(self._curr_loc)
if self._curr_loc:
res = self._shenk.battle(Config().routes["run_shenk"], not self._pre_buffed, self._game_stats)
set_pause_state(False)
res = self._shenk.battle(Config().routes.get("run_eldritch_shenk"), not self._pre_buffed, self._game_stats)
self._ending_run_helper(res)
def on_run_trav(self):
res = False
self._do_runs["run_trav"] = False
self._game_stats.update_location("Trav" if Config().general['discord_status_condensed'] else "Travincal")
self._game_stats.update_location("Trav")
self._curr_loc = self._trav.approach(self._curr_loc)
if self._curr_loc:
set_pause_state(False)
res = self._trav.battle(not self._pre_buffed)
self._ending_run_helper(res)
def on_run_nihlathak(self):
res = False
self._do_runs["run_nihlathak"] = False
self._game_stats.update_location("Nihl" if Config().general['discord_status_condensed'] else "Nihlathak")
self._game_stats.update_location("Nihl")
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)
self._ending_run_helper(res)
def on_run_arcane(self):
res = False
self._do_runs["run_arcane"] = False
self._game_stats.update_location("Arc" if Config().general['discord_status_condensed'] else "Arcane")
self._game_stats.update_location("Arc")
self._curr_loc = self._arcane.approach(self._curr_loc)
if self._curr_loc:
set_pause_state(False)
res = self._arcane.battle(not self._pre_buffed)
self._ending_run_helper(res)
def on_run_diablo(self):
res = False
self._do_runs["run_diablo"] = False
self._game_stats.update_location("Dia" if Config().general['discord_status_condensed'] else "Diablo")
self._game_stats.update_location("Dia")
self._curr_loc = self._diablo.approach(self._curr_loc)
if self._curr_loc:
set_pause_state(False)
res = self._diablo.battle(not self._pre_buffed)
self._ending_run_helper(res)
+7 -7
View File
@@ -93,8 +93,8 @@ class Barbarian(IChar):
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, time_out=1.0, do_pre_move=self._do_pre_move)
self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, time_out=0.1)
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)
@@ -104,7 +104,7 @@ class Barbarian(IChar):
if self.capabilities.can_teleport_natively:
self._pather.traverse_nodes_fixed("eldritch_end", self)
else:
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, time_out=1.0, do_pre_move=self._do_pre_move)
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)
@@ -112,7 +112,7 @@ class Barbarian(IChar):
return True
def kill_shenk(self):
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, time_out=1.0, do_pre_move=self._do_pre_move)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_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_shenk"])
wait(0.1, 0.15)
@@ -123,14 +123,14 @@ class Barbarian(IChar):
# Check out the node screenshot in assets/templates/trav/nodes to see where each node is at
atk_len = Config().char["atk_len_trav"]
# Go inside and war cry a bit
self._pather.traverse_nodes([228, 229], self, time_out=2.5, force_tp=True)
self._pather.traverse_nodes([228, 229], self, timeout=2.5, force_tp=True)
self._cast_war_cry(atk_len)
# Move a bit back and another round
self._move_and_attack((40, 20), atk_len)
# Here we have two different attack sequences depending if tele is available or not
if self.capabilities.can_teleport_natively:
# Back to center stairs and more war cry
self._pather.traverse_nodes([226], self, time_out=2.5, force_tp=True)
self._pather.traverse_nodes([226], self, timeout=2.5, force_tp=True)
self._cast_war_cry(atk_len)
# move a bit to the top
self._move_and_attack((65, -30), atk_len)
@@ -143,7 +143,7 @@ class Barbarian(IChar):
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
# Move close to nihlathak
self._pather.traverse_nodes(end_nodes, self, time_out=0.8, do_pre_move=False)
self._pather.traverse_nodes(end_nodes, self, timeout=0.8, do_pre_move=False)
# move mouse to center (leftover from hammerdin)
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
+7 -7
View File
@@ -75,8 +75,8 @@ class Basic(IChar):
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, time_out=1.0, do_pre_move=self._do_pre_move)
self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, time_out=0.1)
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_attack_pattern(Config().char["atk_len_pindle"])
wait(0.1, 0.15)
return True
@@ -88,7 +88,7 @@ class Basic(IChar):
if not self._do_pre_move:
# keyboard.send(self._skill_hotkeys["concentration"])
# wait(0.05, 0.15)
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, time_out=1.0, do_pre_move=self._do_pre_move)
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_attack_pattern(Config().char["atk_len_eldritch"])
return True
@@ -97,7 +97,7 @@ class Basic(IChar):
# if not self._do_pre_move:
# keyboard.send(self._skill_hotkeys["concentration"])
# wait(0.05, 0.15)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, time_out=1.0, do_pre_move=self._do_pre_move)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0, do_pre_move=self._do_pre_move)
wait(0.05, 0.1)
self._cast_attack_pattern(Config().char["atk_len_shenk"])
wait(0.1, 0.15)
@@ -107,14 +107,14 @@ class Basic(IChar):
# Check out the node screenshot in assets/templates/trav/nodes to see where each node is at
atk_len = Config().char["atk_len_trav"]
# Go inside and war cry a bit
self._pather.traverse_nodes([228, 229], self, time_out=2.5, force_tp=True)
self._pather.traverse_nodes([228, 229], self, timeout=2.5, force_tp=True)
self._cast_attack_pattern(atk_len)
# Move a bit back and another round
self._move_and_attack((40, 20), atk_len)
# Here we have two different attack sequences depending if tele is available or not
if self.capabilities.can_teleport_natively:
# Back to center stairs and more war cry
self._pather.traverse_nodes([226], self, time_out=2.5, force_tp=True)
self._pather.traverse_nodes([226], self, timeout=2.5, force_tp=True)
self._cast_attack_pattern(atk_len)
# move a bit to the top
self._move_and_attack((65, -30), atk_len)
@@ -126,7 +126,7 @@ class Basic(IChar):
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
# Move close to nihlathak
self._pather.traverse_nodes(end_nodes, self, time_out=0.8, do_pre_move=False)
self._pather.traverse_nodes(end_nodes, self, timeout=0.8, do_pre_move=False)
# move mouse to center (leftover from hammerdin)
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
+6 -6
View File
@@ -72,7 +72,7 @@ class Basic_Ranged(IChar):
keyboard.send(Config().char["stand_still"], do_press=False)
wait(self._cast_duration, self._cast_duration + 0.2)
# Move to items
self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, time_out=1.4, force_tp=True)
self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.4, force_tp=True)
return True
@@ -91,7 +91,7 @@ class Basic_Ranged(IChar):
keyboard.send(Config().char["stand_still"], do_press=False)
wait(self._cast_duration, self._cast_duration + 0.2)
# Move to items
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, time_out=1.4, force_tp=True)
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=1.4, force_tp=True)
return True
def kill_shenk(self) -> bool:
@@ -111,7 +111,7 @@ class Basic_Ranged(IChar):
keyboard.send(Config().char["stand_still"], do_press=False)
wait(self._cast_duration, self._cast_duration + 0.2)
# Move to items
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, time_out=1.4, force_tp=True)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.4, force_tp=True)
return True
def kill_council(self) -> bool:
@@ -119,7 +119,7 @@ class Basic_Ranged(IChar):
# Check out the node screenshot in assets/templates/trav/nodes to see where each node is at
# Go inside cast stuff in general direction
self._pather.offset_node(229, [250, 130])
self._pather.traverse_nodes([228, 229], self, time_out=2.5, force_tp=True)
self._pather.traverse_nodes([228, 229], self, timeout=2.5, force_tp=True)
self._pather.offset_node(229, [-250, -130])
atk_pos_abs = self._pather.find_abs_node_pos(230, grab())
if atk_pos_abs is None:
@@ -149,7 +149,7 @@ class Basic_Ranged(IChar):
self._left_attack(cast_pos_abs, spray=11)
# Move outside
# Move a bit back and another round
self._pather.traverse_nodes([226], self, time_out=2.5, force_tp=True)
self._pather.traverse_nodes([226], self, timeout=2.5, force_tp=True)
cast_pos_abs = np.array([-300, -100])
for _ in range(atk_len_trav_2):
if skills.is_right_skill_active():
@@ -183,7 +183,7 @@ class Basic_Ranged(IChar):
self._left_attack(cast_pos_abs, spray=11)
# Move to items
wait(self._cast_duration, self._cast_duration + 0.2)
self._pather.traverse_nodes(end_nodes, self, time_out=0.8)
self._pather.traverse_nodes(end_nodes, self, timeout=0.8)
return True
+56 -56
View File
@@ -81,7 +81,7 @@ class Hammerdin(IChar):
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, time_out=1.0, do_pre_move=self._do_pre_move)
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._cast_hammers(Config().char["atk_len_pindle"])
wait(0.1, 0.15)
self._cast_hammers(1.6, "redemption")
@@ -95,7 +95,7 @@ class Hammerdin(IChar):
if not self._do_pre_move:
keyboard.send(self._skill_hotkeys["concentration"])
wait(0.05, 0.15)
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, time_out=1.0, do_pre_move=self._do_pre_move, force_tp=True, use_tp_charge=True)
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=1.0, do_pre_move=self._do_pre_move, force_tp=True, use_tp_charge=True)
wait(0.05, 0.1)
self._cast_hammers(Config().char["atk_len_eldritch"])
wait(0.1, 0.15)
@@ -106,7 +106,7 @@ class Hammerdin(IChar):
if not self._do_pre_move:
keyboard.send(self._skill_hotkeys["concentration"])
wait(0.05, 0.15)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, time_out=1.0, do_pre_move=self._do_pre_move, force_tp=True, use_tp_charge=True)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0, do_pre_move=self._do_pre_move, force_tp=True, use_tp_charge=True)
wait(0.05, 0.1)
self._cast_hammers(Config().char["atk_len_shenk"])
wait(0.1, 0.15)
@@ -120,14 +120,14 @@ class Hammerdin(IChar):
# Check out the node screenshot in assets/templates/trav/nodes to see where each node is at
atk_len = Config().char["atk_len_trav"]
# Go inside and hammer a bit
self._pather.traverse_nodes([228, 229], self, time_out=2.5, force_tp=True, use_tp_charge=True)
self._pather.traverse_nodes([228, 229], self, timeout=2.5, force_tp=True, use_tp_charge=True)
self._cast_hammers(atk_len)
# Move a bit back and another round
self._move_and_attack((40, 20), atk_len)
# Here we have two different attack sequences depending if tele is available or not
if self.capabilities.can_teleport_natively or self.capabilities.can_teleport_with_charges:
# Back to center stairs and more hammers
self._pather.traverse_nodes([226], self, time_out=2.5, force_tp=True, use_tp_charge=True)
self._pather.traverse_nodes([226], self, timeout=2.5, force_tp=True, use_tp_charge=True)
self._cast_hammers(atk_len)
# move a bit to the top
self._move_and_attack((65, -30), atk_len)
@@ -140,7 +140,7 @@ class Hammerdin(IChar):
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
# Move close to nihlathak
self._pather.traverse_nodes(end_nodes, self, time_out=0.8, do_pre_move=False)
self._pather.traverse_nodes(end_nodes, self, timeout=0.8, do_pre_move=False)
# move mouse to center, otherwise hammers sometimes dont fly, not sure why
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
@@ -200,7 +200,7 @@ class Hammerdin(IChar):
elif location == "rof_01": #node 603 - outside CS in ROF
### APPROACH ###
if not self._pather.traverse_nodes([603], self, time_out=3): return False #calibrate after static path
if not self._pather.traverse_nodes([603], self, timeout=3): return False #calibrate after static path
pos_m = convert_abs_to_monitor((0, 0))
### ATTACK ###
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
@@ -221,7 +221,7 @@ class Hammerdin(IChar):
elif location == "rof_02": #node 604 - inside ROF
### APPROACH ###
if not self._pather.traverse_nodes([604], self, time_out=3): return False #threshold=0.8 (ex 601)
if not self._pather.traverse_nodes([604], self, timeout=3): return False #threshold=0.8 (ex 601)
### ATTACK ###
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
@@ -298,7 +298,7 @@ class Hammerdin(IChar):
wait(0.3, 0.6)
### LOOT ###
self._picked_up_items |= self._pickit.pick_up_items(self)
if not self._pather.traverse_nodes([673], self): return False # , time_out=3): # Re-adjust itself and continues to attack
if not self._pather.traverse_nodes([673], self): return False # , timeout=3): # Re-adjust itself and continues to attack
elif location == "entrance1_02": #node 673
### APPROACH ###
@@ -317,7 +317,7 @@ class Hammerdin(IChar):
### LOOT ###
self._picked_up_items |= self._pickit.pick_up_items(self)
self._pather.traverse_nodes_fixed("diablo_entrance_1_1", self) # Moves char to postion close to node 674 continues to attack
if not self._pather.traverse_nodes([674], self): return False#, time_out=3)
if not self._pather.traverse_nodes([674], self): return False#, timeout=3)
elif location == "entrance1_03": #node 674
### APPROACH ###
@@ -336,9 +336,9 @@ class Hammerdin(IChar):
### LOOT ###
self._picked_up_items |= self._pickit.pick_up_items(self)
self._picked_up_items |= self._pickit.pick_up_items(self)
if not self._pather.traverse_nodes([675], self): return False#, time_out=3) # Re-adjust itself
if not self._pather.traverse_nodes([675], self): return False#, timeout=3) # Re-adjust itself
self._pather.traverse_nodes_fixed("diablo_entrance_1_1", self) #static path to get to be able to spot 676
if not self._pather.traverse_nodes([676], self): return False#, time_out=3)
if not self._pather.traverse_nodes([676], self): return False#, timeout=3)
elif location == "entrance1_04": #node 676- Hall3
### APPROACH ###
@@ -379,10 +379,10 @@ class Hammerdin(IChar):
elif location == "entrance2_02": #node 682
### APPROACH ###
#if not self._pather.traverse_nodes([682], self): return False # , time_out=3):
#if not self._pather.traverse_nodes([682], self): return False # , timeout=3):
self._pather.traverse_nodes_fixed("diablo_trash_b_hall2_605_right", self) #pull mobs from the right
wait (0.2, 0.5)
if not self._pather.traverse_nodes([605], self): return False#, time_out=3)
if not self._pather.traverse_nodes([605], self): return False#, timeout=3)
### ATTACK ###
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
@@ -400,13 +400,13 @@ class Hammerdin(IChar):
elif location == "entrance2_03": #node 683
### APPROACH ###
#if not self._pather.traverse_nodes([682], self): return False # , time_out=3):
#if not self._pather.traverse_nodes([682], self): return False # , timeout=3):
#self._pather.traverse_nodes_fixed("diablo_entrance2_1", self)
#if not self._pather.traverse_nodes([683], self): return False # , time_out=3):
#if not self._pather.traverse_nodes([683], self): return False # , timeout=3):
self._pather.traverse_nodes_fixed("diablo_trash_b_hall2_605_top1", self) #pull mobs from top
wait (0.2, 0.5)
self._pather.traverse_nodes_fixed("diablo_trash_b_hall2_605_top2", self) #pull mobs from top
if not self._pather.traverse_nodes([605], self): return False#, time_out=3)
if not self._pather.traverse_nodes([605], self): return False#, timeout=3)
### ATTACK ###
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
@@ -424,14 +424,14 @@ class Hammerdin(IChar):
elif location == "entrance2_04": #node 686 - Hall3
### APPROACH ###
if not self._pather.traverse_nodes([605], self): return False#, time_out=3)
#if not self._pather.traverse_nodes([683,684], self): return False#, time_out=3)
if not self._pather.traverse_nodes([605], self): return False#, timeout=3)
#if not self._pather.traverse_nodes([683,684], self): return False#, timeout=3)
#self._pather.traverse_nodes_fixed("diablo_entrance2_2", self)
#if not self._pather.traverse_nodes([685,686], self): return False#, time_out=3)
#if not self._pather.traverse_nodes([685,686], self): return False#, timeout=3)
self._pather.traverse_nodes_fixed("diablo_trash_b_hall2_605_hall3", self)
if not self._pather.traverse_nodes([609], self): return False#, time_out=3)
if not self._pather.traverse_nodes([609], self): return False#, timeout=3)
self._pather.traverse_nodes_fixed("diablo_trash_b_hall3_pull_609", self)
if not self._pather.traverse_nodes([609], self): return False#, time_out=3)
if not self._pather.traverse_nodes([609], self): return False#, timeout=3)
### ATTACK ###
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
@@ -450,9 +450,9 @@ class Hammerdin(IChar):
### LOOT ###
self._picked_up_items |= self._pickit.pick_up_items(self)
self._picked_up_items |= self._pickit.pick_up_items(self)
if not self._pather.traverse_nodes([609], self): return False#, time_out=3)
if not self._pather.traverse_nodes([609], self): return False#, timeout=3)
self._picked_up_items |= self._pickit.pick_up_items(self)
if not self._pather.traverse_nodes([609], self): return False#, time_out=3)
if not self._pather.traverse_nodes([609], self): return False#, timeout=3)
####################
# PENT TRASH TO SEAL
@@ -605,7 +605,7 @@ class Hammerdin(IChar):
elif location == "A1-L_01": #node 611 seal layout A1-L: safe_dist
### APPROACH ###
if not self._pather.traverse_nodes([611], self): return False # , time_out=3):
if not self._pather.traverse_nodes([611], self): return False # , timeout=3):
### ATTACK ###
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
@@ -623,7 +623,7 @@ class Hammerdin(IChar):
elif location == "A1-L_02": #node 612 seal layout A1-L: center
### APPROACH ###
if not self._pather.traverse_nodes([612], self): return False # , time_out=3):
if not self._pather.traverse_nodes([612], self): return False # , timeout=3):
### ATTACK ###
if self._skill_hotkeys["redemption"]:
keyboard.send(self._skill_hotkeys["redemption"])
@@ -646,7 +646,7 @@ class Hammerdin(IChar):
elif location == "A1-L_03": #node 613 seal layout A1-L: fake_seal
### APPROACH ###
if not self._pather.traverse_nodes([613], self): return False # , time_out=3):
if not self._pather.traverse_nodes([613], self): return False # , timeout=3):
### ATTACK ###
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
@@ -677,7 +677,7 @@ class Hammerdin(IChar):
elif location == "A1-L_seal2": #node 614 seal layout A1-L: boss_seal
### APPROACH ###
if not self._pather.traverse_nodes([613, 615], self): return False # , time_out=3):
if not self._pather.traverse_nodes([613, 615], self): return False # , timeout=3):
### ATTACK ###
if self._skill_hotkeys["redemption"]:
keyboard.send(self._skill_hotkeys["redemption"])
@@ -693,7 +693,7 @@ class Hammerdin(IChar):
### APPROACH ###
if not self._pather.traverse_nodes_fixed("dia_a2y_hop_622", self): return False
Logger.info("A2-Y: Hop!")
#if not self._pather.traverse_nodes([622], self): return False # , time_out=3):
#if not self._pather.traverse_nodes([622], self): return False # , timeout=3):
if not self._pather.traverse_nodes([622], self): return False
if self._skill_hotkeys["redemption"]:
keyboard.send(self._skill_hotkeys["redemption"])
@@ -743,7 +743,7 @@ class Hammerdin(IChar):
### ATTACK ###
### LOOT ###
# we loot at boss
if not self._pather.traverse_nodes([625], self): return False # , time_out=3):
if not self._pather.traverse_nodes([625], self): return False # , timeout=3):
if self._skill_hotkeys["redemption"]:
keyboard.send(self._skill_hotkeys["redemption"])
wait(0.3, 0.6)
@@ -785,7 +785,7 @@ class Hammerdin(IChar):
elif location == "B1-S_seal2": #B only has 1 seal, which is the boss seal = seal2
### APPROACH ###
if not self._pather.traverse_nodes([634], self): return False # , time_out=3):
if not self._pather.traverse_nodes([634], self): return False # , timeout=3):
### ATTACK ###
if self._skill_hotkeys["redemption"]:
keyboard.send(self._skill_hotkeys["redemption"])
@@ -821,7 +821,7 @@ class Hammerdin(IChar):
elif location == "B2-U_seal2": #B only has 1 seal, which is the boss seal = seal2
### APPROACH ###
self._pather.traverse_nodes_fixed("dia_b2u_bold_seal", self)
if not self._pather.traverse_nodes([644], self): return False # , time_out=3):
if not self._pather.traverse_nodes([644], self): return False # , timeout=3):
### ATTACK ###
### LOOT ###
# we loot at boss
@@ -860,7 +860,7 @@ class Hammerdin(IChar):
wait(0.1,0.3)
self._pather.traverse_nodes_fixed("dia_c1f_hop_fakeseal", self)
wait(0.1,0.3)
if not self._pather.traverse_nodes([655], self): return False # , time_out=3):
if not self._pather.traverse_nodes([655], self): return False # , timeout=3):
### ATTACK ###
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
@@ -875,7 +875,7 @@ class Hammerdin(IChar):
wait(0.3, 0.6)
### LOOT ###
self._picked_up_items |= self._pickit.pick_up_items(self)
if not self._pather.traverse_nodes([655], self): return False # , time_out=3):
if not self._pather.traverse_nodes([655], self): return False # , timeout=3):
if self._skill_hotkeys["redemption"]:
keyboard.send(self._skill_hotkeys["redemption"])
wait(0.3, 0.6)
@@ -883,7 +883,7 @@ class Hammerdin(IChar):
elif location == "C1-F_seal2":
### APPROACH ###
self._pather.traverse_nodes_fixed("dia_c1f_654_651", self)
if not self._pather.traverse_nodes([652], self): return False # , time_out=3):
if not self._pather.traverse_nodes([652], self): return False # , timeout=3):
### ATTACK ###
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
@@ -898,7 +898,7 @@ class Hammerdin(IChar):
wait(0.3, 0.6)
### LOOT ###
self._picked_up_items |= self._pickit.pick_up_items(self)
if not self._pather.traverse_nodes([652], self): return False # , time_out=3):
if not self._pather.traverse_nodes([652], self): return False # , timeout=3):
if self._skill_hotkeys["redemption"]:
keyboard.send(self._skill_hotkeys["redemption"])
wait(0.3, 0.6)
@@ -930,7 +930,7 @@ class Hammerdin(IChar):
elif location == "C2-G_seal1":
### APPROACH ###
#if not self._pather.traverse_nodes([663, 662], self): return False # , time_out=3): #caused 7% failed runs, replaced by static path.
#if not self._pather.traverse_nodes([663, 662], self): return False # , timeout=3): #caused 7% failed runs, replaced by static path.
self._pather.traverse_nodes_fixed("dia_c2g_lc_661", self)
### ATTACK ###
### LOOT ###
@@ -976,7 +976,7 @@ class Hammerdin(IChar):
if self._skill_hotkeys["redemption"]:
keyboard.send(self._skill_hotkeys["redemption"])
wait(0.3, 0.6)
if not self._pather.traverse_nodes([664, 665], self): return False # , time_out=3):
if not self._pather.traverse_nodes([664, 665], self): return False # , timeout=3):
else:
### APPROACH ###
@@ -1002,7 +1002,7 @@ class Hammerdin(IChar):
def kill_vizier(self, seal_layout:str) -> bool:
if seal_layout == "A1-L":
### APPROACH ###
if not self._pather.traverse_nodes([612], self): return False # , time_out=3):
if not self._pather.traverse_nodes([612], self): return False # , timeout=3):
### ATTACK ###
Logger.debug(seal_layout + ": Attacking Vizier at position 1/2")
pos_m = convert_abs_to_monitor((0, 0))
@@ -1011,7 +1011,7 @@ class Hammerdin(IChar):
self._move_and_attack((-30, -15), Config().char["atk_len_diablo_vizier"] * 0.5)
self._cast_hammers(1, "redemption")
Logger.debug(seal_layout + ": Attacking Vizier at position 2/2")
self._pather.traverse_nodes([611], self, time_out=3)
self._pather.traverse_nodes([611], self, timeout=3)
self._move_and_attack((30, 15), Config().char["atk_len_diablo_vizier"] * 0.5)
self._move_and_attack((-30, -15), Config().char["atk_len_diablo_vizier"]) # no factor, so merc is not reset by teleport and he his some time to move & kill stray bosses
self._cast_hammers(1, "redemption")
@@ -1024,16 +1024,16 @@ class Hammerdin(IChar):
wait(0.3, 1.2)
### LOOT ###
self._picked_up_items |= self._pickit.pick_up_items(self)
if not self._pather.traverse_nodes([612], self): return False # , time_out=3):
if not self._pather.traverse_nodes([612], self): return False # , timeout=3):
if self._skill_hotkeys["redemption"]:
keyboard.send(self._skill_hotkeys["redemption"])
wait(0.3, 0.6)
self._picked_up_items |= self._pickit.pick_up_items(self)
if not self._pather.traverse_nodes([612], self): return False # , time_out=3): # recalibrate after loot
if not self._pather.traverse_nodes([612], self): return False # , timeout=3): # recalibrate after loot
elif seal_layout == "A2-Y":
### APPROACH ###
if not self._pather.traverse_nodes([627, 622], self): return False # , time_out=3):
if not self._pather.traverse_nodes([627, 622], self): return False # , timeout=3):
### ATTACK ###
Logger.debug(seal_layout + ": Attacking Vizier at position 1/2")
pos_m = convert_abs_to_monitor((0, 0))
@@ -1042,7 +1042,7 @@ class Hammerdin(IChar):
self._move_and_attack((-30, -15), Config().char["atk_len_diablo_vizier"] * 0.5)
self._cast_hammers(1, "redemption")
Logger.debug(seal_layout + ": Attacking Vizier at position 2/2")
self._pather.traverse_nodes([623], self, time_out=3)
self._pather.traverse_nodes([623], self, timeout=3)
self._move_and_attack((30, 15), Config().char["atk_len_diablo_vizier"] * 0.5)
self._move_and_attack((-30, -15), Config().char["atk_len_diablo_vizier"] * 0.5)
self._cast_hammers(1, "redemption")
@@ -1064,12 +1064,12 @@ class Hammerdin(IChar):
if self._skill_hotkeys["redemption"]:
keyboard.send(self._skill_hotkeys["redemption"])
wait(0.3, 0.6)
if not self._pather.traverse_nodes([622], self): return False #, time_out=3):
if not self._pather.traverse_nodes([622], self): return False #, timeout=3):
if self._skill_hotkeys["redemption"]:
keyboard.send(self._skill_hotkeys["redemption"])
wait(0.3, 0.6)
self._picked_up_items |= self._pickit.pick_up_items(self)
if not self._pather.traverse_nodes([622], self): return False # , time_out=3): #recalibrate after loot
if not self._pather.traverse_nodes([622], self): return False # , timeout=3): #recalibrate after loot
else:
Logger.debug(seal_layout + ": Invalid location for kill_deseis("+ seal_layout +"), should not happen.")
@@ -1093,16 +1093,16 @@ class Hammerdin(IChar):
self._move_and_attack((-30, -15), Config().char["atk_len_diablo_deseis"] * 0.2)
self._cast_hammers(1, "redemption")
Logger.debug(seal_layout + ": Attacking De Seis at position 2/4")
self._pather.traverse_nodes(nodes1, self, time_out=3)
self._pather.traverse_nodes(nodes1, self, timeout=3)
self._move_and_attack((30, 15), Config().char["atk_len_diablo_deseis"] * 0.2)
self._move_and_attack((-30, -15), Config().char["atk_len_diablo_deseis"] * 0.2)
self._cast_hammers(1, "redemption")
Logger.debug(seal_layout + ": Attacking De Seis at position 3/4")
self._pather.traverse_nodes(nodes2, self, time_out=3)
self._pather.traverse_nodes(nodes2, self, timeout=3)
self._move_and_attack((0, 0), Config().char["atk_len_diablo_deseis"] * 0.5)
self._cast_hammers(1, "redemption")
Logger.debug(seal_layout + ": Attacking De Seis at position 4/4")
self._pather.traverse_nodes(nodes3, self, time_out=3)
self._pather.traverse_nodes(nodes3, self, timeout=3)
self._move_and_attack((0, 0), Config().char["atk_len_diablo_deseis"]) # no factor, so merc is not reset by teleport and he his some time to move & kill stray bosses
wait(0.1, 0.2)
self._cast_hammers(2, "redemption")
@@ -1130,16 +1130,16 @@ class Hammerdin(IChar):
self._move_and_attack((-30, -15), Config().char["atk_len_diablo_deseis"] * 0.2)
self._cast_hammers(1, "redemption")
Logger.debug(seal_layout + ": Attacking De Seis at position 2/4")
self._pather.traverse_nodes(nodes1, self, time_out=3)
self._pather.traverse_nodes(nodes1, self, timeout=3)
self._move_and_attack((30, 15), Config().char["atk_len_diablo_deseis"] * 0.2)
self._move_and_attack((-30, -15), Config().char["atk_len_diablo_deseis"] * 0.2)
self._cast_hammers(1, "redemption")
Logger.debug(seal_layout + ": Attacking De Seis at position 3/4")
self._pather.traverse_nodes(nodes2, self, time_out=3)
self._pather.traverse_nodes(nodes2, self, timeout=3)
self._move_and_attack((0, 0), Config().char["atk_len_diablo_deseis"] * 0.5)
self._cast_hammers(1, "redemption")
Logger.debug(seal_layout + ": Attacking De Seis at position 4/4")
self._pather.traverse_nodes(nodes3, self, time_out=3)
self._pather.traverse_nodes(nodes3, self, timeout=3)
self._move_and_attack((0, 0), Config().char["atk_len_diablo_deseis"]) # no factor, so merc is not reset by teleport and he his some time to move & kill stray bosses
wait(0.1, 0.2)
self._cast_hammers(2, "redemption")
@@ -1150,11 +1150,11 @@ class Hammerdin(IChar):
#if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_check_deseis_dead" + seal_layout + "_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
### LOOT ###
self._picked_up_items |= self._pickit.pick_up_items(self)
if not self._pather.traverse_nodes([641], self): return False # , time_out=3):
if not self._pather.traverse_nodes([646], self): return False # , time_out=3):
if not self._pather.traverse_nodes([641], self): return False # , timeout=3):
if not self._pather.traverse_nodes([646], self): return False # , timeout=3):
self._picked_up_items |= self._pickit.pick_up_items(self)
if not self._pather.traverse_nodes([646], self): return False # , time_out=3):
if not self._pather.traverse_nodes([640], self): return False # , time_out=3):
if not self._pather.traverse_nodes([646], self): return False # , timeout=3):
if not self._pather.traverse_nodes([640], self): return False # , timeout=3):
self._picked_up_items |= self._pickit.pick_up_items(self)
else:
+26 -11
View File
@@ -7,7 +7,7 @@ from inventory import consumables
import keyboard
import numpy as np
from char.capabilities import CharacterCapabilities
from ui_manager import wait_for_screen_object
from ui_manager import is_visible, wait_until_visible
from ui import skills
from utils.custom_mouse import mouse
from utils.misc import wait, cut_roi, is_in_roi, color_filter
@@ -30,7 +30,7 @@ class IChar:
self.capabilities = None
def _discover_capabilities(self) -> CharacterCapabilities:
override = Config().advanced_options["override_capabilities"];
override = Config().advanced_options["override_capabilities"]
if override is None:
if self._skill_hotkeys["teleport"]:
if self.select_tp():
@@ -69,7 +69,7 @@ class IChar:
self,
template_type: Union[str, List[str]],
success_func: Callable = None,
time_out: float = 8,
timeout: float = 8,
threshold: float = 0.68,
telekinesis: bool = False
) -> bool:
@@ -77,17 +77,16 @@ class IChar:
Finds any template from the template finder and interacts with it
:param template_type: Strings or list of strings of the templates that should be searched for
:param success_func: Function that will return True if the interaction is successful e.g. return True when loading screen is reached, defaults to None
:param time_out: Timeout for the whole template selection, defaults to None
:param timeout: Timeout for the whole template selection, defaults to None
:param threshold: Threshold which determines if a template is found or not. None will use default form .ini files
:return: True if success. False otherwise
"""
if type(template_type) == list and "A5_STASH" in template_type:
# sometimes waypoint is opened and stash not found because of that, check for that
match = detect_screen_object(ScreenObjects.WaypointLabel)
if match.valid:
if is_visible(ScreenObjects.WaypointLabel):
keyboard.send("esc")
start = time.time()
while time_out is None or (time.time() - start) < time_out:
while timeout is None or (time.time() - start) < timeout:
template_match = TemplateFinder().search(template_type, grab(), threshold=threshold, normalize_monitor=True)
if template_match.valid:
Logger.debug(f"Select {template_match.name} ({template_match.score*100:.1f}% confidence)")
@@ -175,6 +174,24 @@ class IChar:
keyboard.send(Config().char["force_move"])
else:
mouse.click(button="left")
def walk(self, pos_monitor: Tuple[float, float], force_tp: bool = False, force_move: bool = False):
factor = Config().advanced_options["pathing_delay_factor"]
# in case we want to walk we actually want to move a bit before the point cause d2r will always "overwalk"
pos_screen = convert_monitor_to_screen(pos_monitor)
pos_abs = convert_screen_to_abs(pos_screen)
dist = math.dist(pos_abs, (0, 0))
min_wd = max(10, Config().ui_pos["min_walk_dist"])
max_wd = random.randint(int(Config().ui_pos["max_walk_dist"] * 0.65), Config().ui_pos["max_walk_dist"])
adjust_factor = max(max_wd, min(min_wd, dist - 50)) / max(min_wd, dist)
pos_abs = [int(pos_abs[0] * adjust_factor), int(pos_abs[1] * adjust_factor)]
x, y = convert_abs_to_monitor(pos_abs)
mouse.move(x, y, randomize=5, delay_factor=[factor*0.1, factor*0.14])
wait(0.012, 0.02)
if force_move:
keyboard.send(Config().char["force_move"])
else:
mouse.click(button="left")
def tp_town(self):
# will check if tp is available and select the skill
@@ -203,16 +220,14 @@ class IChar:
mouse.click(button="right")
consumables.increment_need("tp", 1)
wait(0.8, 1.3) # takes quite a while for tp to be visible
template_match = detect_screen_object(ScreenObjects.TownPortal)
if template_match.valid:
if (template_match := detect_screen_object(ScreenObjects.TownPortal)).valid:
pos = template_match.center
pos = (pos[0], pos[1] + 30)
# Note: Template is top of portal, thus move the y-position a bit to the bottom
mouse.move(*pos, randomize=6, delay_factor=[0.9, 1.1])
wait(0.08, 0.15)
mouse.click(button="left")
match = wait_for_screen_object(ScreenObjects.Loading, 2)
if match.valid:
if wait_until_visible(ScreenObjects.Loading, 2).valid:
return True
# move mouse away to not overlay with the town portal if mouse is in center
pos_screen = convert_monitor_to_screen(mouse.get_position())
+15 -19
View File
@@ -13,7 +13,7 @@ from pather import Location, Pather
import numpy as np
import time
import os
from ui_manager import wait_for_screen_object, ScreenObjects
from ui_manager import wait_until_visible, ScreenObjects
class Necro(IChar):
def __init__(self, skill_hotkeys: dict, pather: Pather):
@@ -491,7 +491,7 @@ class Necro(IChar):
if self.capabilities.can_teleport_natively:
self._pather.traverse_nodes_fixed("eldritch_end", self)
else:
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, time_out=0.6, force_tp=True)
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=0.6, force_tp=True)
self.bone_armor()
#get some more summons out for elite packs
@@ -532,7 +532,7 @@ class Necro(IChar):
self._summon_stat()
#continue to shenk fight
self._pather.traverse_nodes(([ 146, 147, 148]), self, time_out=1.4, force_tp=True)
self._pather.traverse_nodes(([ 146, 147, 148]), self, timeout=1.4, force_tp=True)
shenk_pos_abs = self._pather.find_abs_node_pos(149, grab())
if shenk_pos_abs is None:
@@ -566,7 +566,7 @@ class Necro(IChar):
# Move to items
#wait(self._cast_duration, self._cast_duration + 0.2)
self._pather.traverse_nodes(([148,149]), self, time_out=3.4, force_tp=True)
self._pather.traverse_nodes(([148,149]), self, timeout=3.4, force_tp=True)
for _ in range(30):
self._summon_count()
if self._skeletons_count < 10:
@@ -678,11 +678,8 @@ class Necro(IChar):
wait(0.08, 0.15)
mouse.click(button="left")
Logger.debug("enter durance lv 1")
match = wait_for_screen_object(ScreenObjects.Loading, 2)
if match.valid:
return True
else:
return False
return wait_until_visible(ScreenObjects.Loading, 2).valid
else:
Logger.debug("cant find durance!, trying fixed loc")
return False
@@ -719,8 +716,7 @@ class Necro(IChar):
wait(0.08, 0.15)
mouse.click(button="left")
Logger.debug("entering trav...")
match = wait_for_screen_object(ScreenObjects.Loading, 2)
if match.valid:
if wait_until_visible(ScreenObjects.Loading, 2).valid:
return True
return False
@@ -728,7 +724,7 @@ class Necro(IChar):
def kill_council(self) -> bool:
''' kill the council '''
result = self._pather.traverse_nodes((901,902,903,904,905,906,226,228,300), self, force_move=True,time_out = 2.5,threshold=.55)
result = self._pather.traverse_nodes((901,902,903,904,905,906,226,228,300), self, force_move=True,timeout = 2.5,threshold=.55)
#this is gross but the skeletons I think cause pathing issues
while result is False:
@@ -745,22 +741,22 @@ class Necro(IChar):
if self._pather.find_abs_node_pos(904, grab()):
#try again
result = self._pather.traverse_nodes((904,905,906,226,228,300), self, force_move=True,time_out = 2.5)
result = self._pather.traverse_nodes((904,905,906,226,228,300), self, force_move=True,timeout = 2.5)
elif self._pather.find_abs_node_pos(905, grab()):
#try again
result = self._pather.traverse_nodes((905,906,226,228,300), self, force_move=True,time_out = 2.5)
result = self._pather.traverse_nodes((905,906,226,228,300), self, force_move=True,timeout = 2.5)
elif self._pather.find_abs_node_pos(906, grab()):
#try again
result = self._pather.traverse_nodes((906,226,228,300), self, force_move=True,time_out = 2.5)
result = self._pather.traverse_nodes((906,226,228,300), self, force_move=True,timeout = 2.5)
elif self._pather.find_abs_node_pos(226, grab()):
#try again
result = self._pather.traverse_nodes((226,228,300), self, force_move=True,time_out = 2.5)
result = self._pather.traverse_nodes((226,228,300), self, force_move=True,timeout = 2.5)
elif self._pather.find_abs_node_pos(901, grab()):
#try again
result = self._pather.traverse_nodes((901,902,903,904,905,906,226,228,300), self, force_move=True,time_out = 2.5)
result = self._pather.traverse_nodes((901,902,903,904,905,906,226,228,300), self, force_move=True,timeout = 2.5)
elif self._pather.find_abs_node_pos(902, grab()):
#try again
result = self._pather.traverse_nodes((902,903,904,905,906,226,228,300), self, force_move=True,time_out = 2.5)
result = self._pather.traverse_nodes((902,903,904,905,906,226,228,300), self, force_move=True,timeout = 2.5)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='amp_dmg',delay=3.0)
@@ -864,7 +860,7 @@ class Necro(IChar):
self._corpse_explosion(corpse_exp_pos, 240, cast_count=18)
#self._pather.traverse_nodes([230, 229 ,228], self, time_out=2.5, force_tp=True)
#self._pather.traverse_nodes([230, 229 ,228], self, timeout=2.5, force_tp=True)
Logger.info('\033[92m'+"atk cycle end"+'\033[0m')
+557
View File
@@ -0,0 +1,557 @@
import keyboard
from utils.custom_mouse import mouse
from char import IChar
from template_finder import TemplateFinder
from pather import Pather
from logger import Logger
from screen import grab, convert_abs_to_monitor, convert_screen_to_abs
from config import Config
from utils.misc import wait, rotate_vec, unit_vector
import random
from typing import Tuple
from pather import Location, Pather
import screen as screen
import numpy as np
import time
import os
from ui_manager import ScreenObjects
class Poison_Necro(IChar):
def __init__(self, skill_hotkeys: dict, pather: Pather):
os.system('color')
Logger.info("\033[94m<<Setting up Necro>>\033[0m")
super().__init__(skill_hotkeys)
self._pather = pather
#custom necro pathing for pindle
self._pather.adapt_path((Location.A5_PINDLE_START, Location.A5_PINDLE_SAFE_DIST), [100,101])
self._pather.adapt_path((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), [104])
#minor offsets to pindle fight locations
self._pather.offset_node(102, [15, 0])
self._pather.offset_node(103, [15, 0])
self._pather.offset_node(101, [100,-5])
#Diablo
self._pather.offset_node(644, [150, -70])
self._pather.offset_node(610620, [50, 50])
self._pather.offset_node(631, [-50, 50])
self._pather.offset_node(656, [-70, -50])
self._shenk_dead = 0
self._skeletons_count=0
self._mages_count=0
self._golem_count="none"
self._revive_count=0
def _check_shenk_death(self):
''' make sure shenk is dead checking for fireballs so we can exit combat sooner '''
roi = [640,0,640,720]
img = grab()
template_match = TemplateFinder().search(
['SHENK_DEATH_1','SHENK_DEATH_2','SHENK_DEATH_3','SHENK_DEATH_4'],
img,
threshold=0.6,
roi=roi,
use_grayscale = False
)
if template_match.valid:
self._shenk_dead=1
Logger.info('\33[31m'+"Shenks Dead, looting..."+'\033[0m')
else:
return True
def _count_revives(self):
roi = [15,14,400,45]
img = grab()
max_rev = 13
template_match = TemplateFinder().search(
['REV_BASE'],
img,
threshold=0.6,
roi=roi
)
if template_match.valid:
self._revive_count=max_rev
else:
self._revive_count=0
return True
for count in range(1,max_rev):
rev_num = "REV_"+str(count)
template_match = TemplateFinder().search(
[rev_num],
img,
threshold=0.66,
roi=roi,
use_grayscale = False
)
if template_match.valid:
self._revive_count=count
def poison_nova(self, time_in_s: float):
if not self._skill_hotkeys["poison_nova"]:
raise ValueError("You did not set poison nova hotkey!")
keyboard.send(self._skill_hotkeys["poison_nova"])
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)
mouse.release(button="right")
def _count_skeletons(self):
roi = [15,14,400,45]
img = grab()
max_skeles = 13
template_match = TemplateFinder().search(
['SKELE_BASE'],
img,
threshold=0.6,
roi=roi
)
if template_match.valid:
self._skeletons_count=max_skeles
else:
self._skeletons_count=0
return True
for count in range(1,max_skeles):
skele_num = "SKELE_"+str(count)
template_match = TemplateFinder().search(
[skele_num],
img,
threshold=0.66,
roi=roi,
use_grayscale = False
)
if template_match.valid:
self._skeletons_count=count
def _count_gol(self):
roi = [15,14,400,45]
img = grab()
template_match = TemplateFinder().search(
['CLAY'],
img,
threshold=0.6,
roi=roi
)
if template_match.valid:
self._golem_count="clay gol"
else:
self._golem_count="none"
return True
def _summon_count(self):
''' see how many summons and which golem are out '''
self._count_skeletons()
self._count_revives()
self._count_gol()
def _summon_stat(self):
''' print counts for summons '''
Logger.info('\33[31m'+"Summon status | "+str(self._skeletons_count)+"skele | "+str(self._revive_count)+" rev | "+self._golem_count+" |"+'\033[0m')
def _revive(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=12):
Logger.info('\033[94m'+"raise revive"+'\033[0m')
keyboard.send(Config().char["stand_still"], do_release=False)
for _ in range(cast_count):
if self._skill_hotkeys["raise_revive"]:
keyboard.send(self._skill_hotkeys["raise_revive"])
#Logger.info("revive -> cast")
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
nx = cast_pos_monitor[0]
ny = cast_pos_monitor[1]
if(nx>1280):
nx=1275
if(ny>720):
ny=715
if(nx<0):
nx=0
if(ny<0):
ny=0
clamp = [nx,ny]
mouse.move(*clamp)
mouse.press(button="right")
wait(0.075, 0.1)
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def _raise_skeleton(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=16):
Logger.info('\033[94m'+"raise skeleton"+'\033[0m')
keyboard.send(Config().char["stand_still"], do_release=False)
for _ in range(cast_count):
if self._skill_hotkeys["raise_skeleton"]:
keyboard.send(self._skill_hotkeys["raise_skeleton"])
#Logger.info("raise skeleton -> cast")
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
nx = cast_pos_monitor[0]
ny = cast_pos_monitor[1]
if(nx>1280):
nx=1279
if(ny>720):
ny=719
if(nx<0):
nx=0
if(ny<0):
ny=0
clamp = [nx,ny]
mouse.move(*clamp)
mouse.press(button="right")
wait(0.02, 0.05)
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def _raise_mage(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=16):
Logger.info('\033[94m'+"raise mage"+'\033[0m')
keyboard.send(Config().char["stand_still"], do_release=False)
for _ in range(cast_count):
if self._skill_hotkeys["raise_mage"]:
keyboard.send(self._skill_hotkeys["raise_mage"])
#Logger.info("raise skeleton -> cast")
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
nx = cast_pos_monitor[0]
ny = cast_pos_monitor[1]
if(nx>1280):
nx=1279
if(ny>720):
ny=719
if(nx<0):
nx=0
if(ny<0):
ny=0
clamp = [nx,ny]
mouse.move(*clamp)
mouse.press(button="right")
wait(0.02, 0.05)
mouse.release(button="right")
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()
if self._shenk_dead==1:
Logger.info("trav buff?")
#self._heart_of_wolverine()
Logger.info("prebuff/cta")
def _heart_of_wolverine(self):
Logger.info('\033[94m'+"buff ~> heart_of_wolverine"+'\033[0m')
keyboard.send(self._skill_hotkeys["heart_of_wolverine"])
wait(0.05, 0.2)
mouse.click(button="right")
wait(self._cast_duration)
def _clay_golem(self):
Logger.info('\033[94m'+"cast ~> clay golem"+'\033[0m')
keyboard.send(self._skill_hotkeys["clay_golem"])
wait(0.05, 0.2)
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"])
wait(0.04, 0.1)
mouse.click(button="right")
wait(self._cast_duration)
def _left_attack(self, cast_pos_abs: Tuple[float, float], spray: int = 10):
keyboard.send(Config().char["stand_still"], do_release=False)
if self._skill_hotkeys["skill_left"]:
keyboard.send(self._skill_hotkeys["skill_left"])
for _ in range(10):
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
mouse.press(button="left")
wait(0.25, 0.3)
mouse.release(button="left")
keyboard.send(Config().char["stand_still"], do_press=False)
def _left_attack_single(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=6):
keyboard.send(Config().char["stand_still"], do_release=False)
if self._skill_hotkeys["skill_left"]:
keyboard.send(self._skill_hotkeys["skill_left"])
for _ in range(cast_count):
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
mouse.press(button="left")
wait(0.25, 0.3)
mouse.release(button="left")
keyboard.send(Config().char["stand_still"], do_press=False)
def _amp_dmg(self, cast_pos_abs: Tuple[float, float], spray: float = 10):
if self._skill_hotkeys["amp_dmg"]:
keyboard.send(self._skill_hotkeys["amp_dmg"])
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
mouse.press(button="right")
wait(0.25, 0.35)
mouse.release(button="right")
def _lower_res(self, cast_pos_abs: Tuple[float, float], spray: float = 10):
if self._skill_hotkeys["lower_res"]:
keyboard.send(self._skill_hotkeys["lower_res"])
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
mouse.press(button="right")
wait(0.25, 0.35)
mouse.release(button="right")
def _corpse_explosion(self, cast_pos_abs: Tuple[float, float], spray: int = 10,cast_count: int = 8):
keyboard.send(Config().char["stand_still"], do_release=False)
Logger.info('\033[93m'+"corpse explosion~> random cast"+'\033[0m')
for _ in range(cast_count):
if self._skill_hotkeys["corpse_explosion"]:
keyboard.send(self._skill_hotkeys["corpse_explosion"])
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
mouse.press(button="right")
wait(0.075, 0.1)
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def _lerp(self,a: float,b: float, f:float):
return a + f * (b - a)
def _cast_circle(self, cast_dir: Tuple[float,float],cast_start_angle: float=0.0, cast_end_angle: float=90.0,cast_div: int = 10,cast_v_div: int=4,cast_spell: str='raise_skeleton',delay: float=1.0,offset: float=1.0):
Logger.info('\033[93m'+"circle cast ~>"+cast_spell+'\033[0m')
keyboard.send(Config().char["stand_still"], do_release=False)
keyboard.send(self._skill_hotkeys[cast_spell])
mouse.press(button="right")
for i in range(cast_div):
angle = self._lerp(cast_start_angle,cast_end_angle,float(i)/cast_div)
target = unit_vector(rotate_vec(cast_dir, angle))
#Logger.info("current angle ~> "+str(angle))
for j in range(cast_v_div):
circle_pos_screen = self._pather._adjust_abs_range_to_screen((target*120.0*float(j+1.0))*offset)
circle_pos_monitor = screen.convert_abs_to_monitor(circle_pos_screen)
mouse.move(*circle_pos_monitor,delay_factor=[0.3*delay, .6*delay])
#Logger.info("circle move")
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def kill_pindle(self) -> bool:
pos_m = screen.convert_abs_to_monitor((0, 30))
self.walk(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
self.poison_nova(3.0)
pos_m = screen.convert_abs_to_monitor((0, -50))
self.pre_move()
self.move(pos_m, force_move=True)
pos_m = screen.convert_abs_to_monitor((50, 0))
self.walk(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=5,cast_v_div=2,cast_spell='corpse_explosion',delay=1.1,offset=1.8)
self.poison_nova(3.0)
return True
def kill_eldritch(self) -> bool:
pos_m = screen.convert_abs_to_monitor((0, -100))
self.pre_move()
self.move(pos_m, force_move=True)
self.bone_armor()
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
self.poison_nova(2.0)
self._summon_stat()
# move a bit back
pos_m = screen.convert_abs_to_monitor((0, 50))
self.pre_move()
self.move(pos_m, force_move=True)
self.poison_nova(2.0)
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=0.6, force_tp=True)
pos_m = screen.convert_abs_to_monitor((0, 170))
self.pre_move()
self.move(pos_m, force_move=True)
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=8,cast_v_div=4,cast_spell='raise_revive',delay=1.2,offset=.8)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=8,cast_v_div=4,cast_spell='raise_skeleton',delay=1.1,offset=.8)
pos_m = screen.convert_abs_to_monitor((0, -50))
self.pre_move()
self.move(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=8,cast_v_div=4,cast_spell='raise_mage',delay=1.1,offset=1.0)
pos_m = screen.convert_abs_to_monitor((-75, 0))
self.pre_move()
self.move(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=8,cast_v_div=4,cast_spell='raise_skeleton',delay=1.1,offset=.5)
self._summon_count()
self._summon_stat()
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=0.6, force_tp=True)
return True
def kill_shenk(self) -> bool:
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0)
#pos_m = self._screen.convert_abs_to_monitor((50, 0))
#self.walk(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
self.poison_nova(3.0)
pos_m = screen.convert_abs_to_monitor((0, -50))
self.pre_move()
self.move(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=10,cast_v_div=4,cast_spell='raise_mage',delay=1.1,offset=.8)
pos_m = screen.convert_abs_to_monitor((50, 0))
self.pre_move()
self.move(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=10,cast_v_div=4,cast_spell='raise_revive',delay=1.1,offset=.8)
pos_m = screen.convert_abs_to_monitor((-20, -20))
self.pre_move()
self.move(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_skeleton',delay=1.1,offset=.8)
self._summon_count()
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=2,cast_v_div=1,cast_spell='corpse_explosion',delay=3.0,offset=1.8)
return True
def kill_council(self) -> bool:
pos_m = screen.convert_abs_to_monitor((0, -200))
self.pre_move()
self.move(pos_m, force_move=True)
self._pather.traverse_nodes([229], self, timeout=2.5, force_tp=True, use_tp_charge=True)
pos_m = screen.convert_abs_to_monitor((50, 0))
self.walk(pos_m, force_move=True)
#self._lower_res((-50, 0), spray=10)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
self.poison_nova(2.0)
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=9,cast_v_div=3,cast_spell='raise_skeleton',delay=1.2,offset=.8)
pos_m = screen.convert_abs_to_monitor((200, 50))
self.pre_move()
self.move(pos_m, force_move=True)
pos_m = screen.convert_abs_to_monitor((30, -50))
self.walk(pos_m, force_move=True)
self.poison_nova(2.0)
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=2,cast_v_div=1,cast_spell='corpse_explosion',delay=3.0,offset=1.8)
#wait(self._cast_duration, self._cast_duration +.2)
pos_m = screen.convert_abs_to_monitor((-200, 200))
self.pre_move()
self.move(pos_m, force_move=True)
pos_m = screen.convert_abs_to_monitor((-100, 200))
self.pre_move()
self.move(pos_m, force_move=True)
self._pather.traverse_nodes([226], self, timeout=2.5, force_tp=True, use_tp_charge=True)
pos_m = screen.convert_abs_to_monitor((0, 30))
self.walk(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
wait(0.5)
self.poison_nova(4.0)
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=2,cast_v_div=1,cast_spell='corpse_explosion',delay=3.0,offset=1.8)
#wait(self._cast_duration, self._cast_duration +.2)
#self.poison_nova(2.0)
pos_m = screen.convert_abs_to_monitor((50, 0))
self.pre_move()
self.move(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=5,cast_v_div=2,cast_spell='corpse_explosion',delay=0.5,offset=1.8)
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=9,cast_v_div=3,cast_spell='raise_skeleton',delay=1.2,offset=.8)
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=9,cast_v_div=3,cast_spell='raise_mage',delay=1.2,offset=.8)
pos_m = screen.convert_abs_to_monitor((-200, -200))
self.pre_move()
self.move(pos_m, force_move=True)
self._pather.traverse_nodes([229], self, timeout=2.5, force_tp=True, use_tp_charge=True)
pos_m = screen.convert_abs_to_monitor((20, -50))
self.walk(pos_m, force_move=True)
self.poison_nova(2.0)
pos_m = screen.convert_abs_to_monitor((50, 0))
self.pre_move()
self.move(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=5,cast_v_div=2,cast_spell='corpse_explosion',delay=3.0,offset=1.8)
pos_m = screen.convert_abs_to_monitor((-30, -20))
self.pre_move()
self.move(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_skeleton',delay=1.2,offset=.8)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_mage',delay=1.2,offset=.8)
return True
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
# Move close to nihlathak
self._pather.traverse_nodes(end_nodes, self, timeout=0.8, do_pre_move=True)
pos_m = screen.convert_abs_to_monitor((20, 20))
self.walk(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
self.poison_nova(3.0)
pos_m = screen.convert_abs_to_monitor((50, 0))
self.pre_move()
self.move(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=7200,cast_div=2,cast_v_div=2,cast_spell='corpse_explosion',delay=3.0,offset=1.8)
wait(self._cast_duration, self._cast_duration +.2)
self.poison_nova(3.0)
return True
def kill_summoner(self) -> bool:
# Attack
pos_m = screen.convert_abs_to_monitor((0, 30))
self.walk(pos_m, force_move=True)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
wait(0.5)
self.poison_nova(3.0)
pos_m = screen.convert_abs_to_monitor((50, 0))
self.pre_move()
self.move(pos_m, force_move=True)
wait(self._cast_duration, self._cast_duration + 0.2)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_mage',delay=1.2,offset=.8)
return True
if __name__ == "__main__":
import os
import keyboard
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
from config import Config
from char import Necro
pather = Pather()
char = Necro(Config().necro, Config().char, pather)
+17 -10
View File
@@ -73,26 +73,33 @@ class BlizzSorc(Sorceress):
self.move(pos_m, force_move=True)
self._blizzard((-50, -50), spray=10)
self._cast_static()
wait(0.75)
#move down
pos_m = convert_abs_to_monitor((0, 85))
self.pre_move()
self.move(pos_m, force_move=True)
wait(0.70)
self._blizzard((-170, -350), spray=10)
self._cast_static()
#move down
wait(0.75)
pos_m = convert_abs_to_monitor((0, 75))
self.pre_move()
self.move(pos_m, force_move=True)
self._blizzard((100, -300), spray=10)
self._cast_static()
wait(0.75)
pos_m = convert_abs_to_monitor((0, 55))
self.pre_move()
self.move(pos_m, force_move=True)
wait(1.0)
self._blizzard((-50, -130), spray=10)
self._cast_static()
wait(3.0)
pos_m = convert_abs_to_monitor((0, -100))
self.pre_move()
self.move(pos_m, force_move=True)
self._blizzard((-50, -130), spray=10)
self._cast_static()
wait(1.75)
self._pather.traverse_nodes_fixed("eldritch_end", self)
return True
@@ -101,7 +108,7 @@ class BlizzSorc(Sorceress):
self.pre_move()
self.move(pos_m, force_move=True)
#lower left posistion
self._pather.traverse_nodes([151], self, time_out=2.5, force_tp=False)
self._pather.traverse_nodes([151], self, timeout=2.5, force_tp=False)
self._cast_static()
self._blizzard((-250, 100), spray=10)
self._ice_blast((60, 70), spray=60)
@@ -130,14 +137,14 @@ class BlizzSorc(Sorceress):
self._cast_static()
self._blizzard((100, -50), spray=10)
# Move to items
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, time_out=1.4, force_tp=True)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.4, force_tp=True)
return True
def kill_council(self) -> bool:
# Move inside to the right
self._pather.traverse_nodes_fixed([(1110, 120)], self)
self._pather.offset_node(300, (80, -110))
self._pather.traverse_nodes([300], self, time_out=5.5, force_tp=True)
self._pather.traverse_nodes([300], self, timeout=5.5, force_tp=True)
self._pather.offset_node(300, (-80, 110))
# Attack to the left
self._blizzard((-150, 10), spray=80)
@@ -154,7 +161,7 @@ class BlizzSorc(Sorceress):
wait(0.5)
# Move to far left
self._pather.offset_node(301, (-80, -50))
self._pather.traverse_nodes([301], self, time_out=2.5, force_tp=True)
self._pather.traverse_nodes([301], self, timeout=2.5, force_tp=True)
self._pather.offset_node(301, (80, 50))
# Attack to RIGHT
self._blizzard((100, 150), spray=80)
@@ -167,7 +174,7 @@ class BlizzSorc(Sorceress):
for p in [(450, 100), (-190, 200)]:
pos_m = convert_abs_to_monitor(p)
self.move(pos_m, force_move=True)
self._pather.traverse_nodes([304], self, time_out=2.5, force_tp=True)
self._pather.traverse_nodes([304], self, timeout=2.5, force_tp=True)
# Attack to center of stairs
self._blizzard((-175, -200), spray=30)
self._ice_blast((30, -60), spray=30)
@@ -176,7 +183,7 @@ class BlizzSorc(Sorceress):
wait(1.0)
# Move back inside
self._pather.traverse_nodes_fixed([(1110, 15)], self)
self._pather.traverse_nodes([300], self, time_out=2.5, force_tp=False)
self._pather.traverse_nodes([300], self, timeout=2.5, force_tp=False)
# Attack to center
self._blizzard((-100, 0), spray=10)
self._cast_static()
@@ -208,7 +215,7 @@ class BlizzSorc(Sorceress):
self._cast_static()
self._ice_blast((-30, 50), spray=10)
# Move outside since the trav.py expects to start searching for items there if char can teleport
self._pather.traverse_nodes([226], self, time_out=2.5, force_tp=True)
self._pather.traverse_nodes([226], self, timeout=2.5, force_tp=True)
return True
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
@@ -233,7 +240,7 @@ class BlizzSorc(Sorceress):
self._blizzard(cast_pos_abs, spray=15)
# Move to items
wait(1.3)
self._pather.traverse_nodes(end_nodes, self, time_out=0.8)
self._pather.traverse_nodes(end_nodes, self, timeout=0.8)
return True
def kill_summoner(self) -> bool:
+22 -20
View File
@@ -57,9 +57,9 @@ class LightSorc(Sorceress):
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]
self._lightning(cast_pos_abs, spray=11)
for _ in range(int(Config().char["atk_len_pindle"])):
self._chain_lightning(cast_pos_abs, spray=11)
self._lightning(cast_pos_abs, spray=11)
wait(self._cast_duration, self._cast_duration + 0.2)
# Move to items
self._pather.traverse_nodes_fixed("pindle_end", self)
@@ -68,9 +68,9 @@ class LightSorc(Sorceress):
def kill_eldritch(self) -> bool:
eld_pos_abs = convert_screen_to_abs(Config().path["eldritch_end"][0])
cast_pos_abs = [eld_pos_abs[0] * 0.9, eld_pos_abs[1] * 0.9]
self._lightning(cast_pos_abs, spray=50)
for _ in range(int(Config().char["atk_len_eldritch"])):
self._chain_lightning(cast_pos_abs, spray=90)
self._lightning(cast_pos_abs, spray=50)
# Move to items
wait(self._cast_duration, self._cast_duration + 0.2)
self._pather.traverse_nodes_fixed("eldritch_end", self)
@@ -81,36 +81,37 @@ class LightSorc(Sorceress):
if shenk_pos_abs is None:
shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0])
cast_pos_abs = [shenk_pos_abs[0] * 0.9, shenk_pos_abs[1] * 0.9]
for _ in range(int(Config().char["atk_len_shenk"] * 0.5)):
self._chain_lightning(cast_pos_abs, spray=90)
pos_m = convert_abs_to_monitor((150, 50))
self.pre_move()
self.move(pos_m, force_move=True)
shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0])
cast_pos_abs = [shenk_pos_abs[0] * 0.9, shenk_pos_abs[1] * 0.9]
for _ in range(int(Config().char["atk_len_shenk"] * 0.5)):
self._chain_lightning(cast_pos_abs, spray=90)
self._lightning(cast_pos_abs, spray=60)
for _ in range(int(Config().char["atk_len_shenk"] * 0.5)):
self._chain_lightning(cast_pos_abs, spray=90)
pos_m = convert_abs_to_monitor((150, 50))
self.pre_move()
self.move(pos_m, force_move=True)
shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0])
cast_pos_abs = [shenk_pos_abs[0] * 0.9, shenk_pos_abs[1] * 0.9]
self._lightning(cast_pos_abs, spray=60)
for _ in range(int(Config().char["atk_len_shenk"] * 0.5)):
self._chain_lightning(cast_pos_abs, spray=90)
pos_m = convert_abs_to_monitor((150, 50))
self.pre_move()
self.move(pos_m, force_move=True)
shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0])
cast_pos_abs = [shenk_pos_abs[0] * 0.9, shenk_pos_abs[1] * 0.9]
self._lightning(cast_pos_abs, spray=60)
for _ in range(int(Config().char["atk_len_shenk"])):
self._chain_lightning(cast_pos_abs, spray=90)
self._lightning(cast_pos_abs, spray=60)
self.pre_move()
self.move(pos_m, force_move=True)
# Move to items
wait(self._cast_duration, self._cast_duration + 0.2)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, time_out=1.4, force_tp=True)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.4, force_tp=True)
return True
def kill_council(self) -> bool:
# Move inside to the right
self._pather.traverse_nodes_fixed([(1110, 120)], self)
self._pather.offset_node(300, (80, -110))
self._pather.traverse_nodes([300], self, time_out=1.0, force_tp=True)
self._pather.traverse_nodes([300], self, timeout=1.0, force_tp=True)
self._pather.offset_node(300, (-80, 110))
wait(0.5)
self._frozen_orb((-150, -10), spray=10)
@@ -126,7 +127,7 @@ class LightSorc(Sorceress):
self.move(pos_m, force_move=True)
wait(0.5)
self._pather.offset_node(226, (-80, 60))
self._pather.traverse_nodes([226], self, time_out=1.0, force_tp=True)
self._pather.traverse_nodes([226], self, timeout=1.0, force_tp=True)
self._pather.offset_node(226, (80, -60))
wait(0.5)
self._frozen_orb((-150, -130), spray=10)
@@ -134,7 +135,7 @@ class LightSorc(Sorceress):
self._chain_lightning((-170, -150), spray=20)
wait(0.5)
self._pather.traverse_nodes_fixed([(1110, 15)], self)
self._pather.traverse_nodes([300], self, time_out=1.0, force_tp=True)
self._pather.traverse_nodes([300], self, timeout=1.0, force_tp=True)
pos_m = convert_abs_to_monitor((300, 150))
self.pre_move()
self.move(pos_m, force_move=True)
@@ -149,7 +150,7 @@ class LightSorc(Sorceress):
self.pre_move()
self.move(pos_m, force_move=True)
self._pather.offset_node(304, (0, -80))
self._pather.traverse_nodes([304], self, time_out=1.0, force_tp=True)
self._pather.traverse_nodes([304], self, timeout=1.0, force_tp=True)
self._pather.offset_node(304, (0, 80))
wait(0.5)
self._frozen_orb((175, -170), spray=40)
@@ -184,7 +185,7 @@ class LightSorc(Sorceress):
self.pre_move()
self.move(pos_m, force_move=True)
# Move outside since the trav.py expects to start searching for items there if char can teleport
self._pather.traverse_nodes([226], self, time_out=2.5, force_tp=True)
self._pather.traverse_nodes([226], self, timeout=2.5, force_tp=True)
return True
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
@@ -204,6 +205,7 @@ class LightSorc(Sorceress):
if nihlathak_pos_abs is not None:
cast_pos_abs = np.array([nihlathak_pos_abs[0] * 0.9, nihlathak_pos_abs[1] * 0.9])
self._lightning(cast_pos_abs, spray=60)
self._chain_lightning(cast_pos_abs, delay, 90)
# Do some tele "dancing" after each sequence
if i < atk_len - 1:
@@ -220,7 +222,7 @@ class LightSorc(Sorceress):
# Move to items
wait(self._cast_duration, self._cast_duration + 0.2)
self._pather.traverse_nodes(end_nodes, self, time_out=0.8)
self._pather.traverse_nodes(end_nodes, self, timeout=0.8)
return True
def kill_summoner(self) -> bool:
@@ -229,8 +231,8 @@ class LightSorc(Sorceress):
pos_m = convert_abs_to_monitor((-20, 20))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
for _ in range(int(Config().char["atk_len_arc"])):
self._chain_lightning(cast_pos_abs, spray=11)
self._lightning(cast_pos_abs, spray=11)
self._chain_lightning(cast_pos_abs, spray=11)
wait(self._cast_duration, self._cast_duration + 0.2)
return True
+4 -4
View File
@@ -48,7 +48,7 @@ class NovaSorc(Sorceress):
return True
def kill_shenk(self) -> bool:
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, time_out=1.0)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0)
self._cast_static(0.6)
self._nova(Config().char["atk_len_shenk"])
return True
@@ -61,13 +61,13 @@ class NovaSorc(Sorceress):
self._pather.offset_node(229, offset_229)
def clear_inside():
self._pather.traverse_nodes_fixed([(1110, 120)], self)
self._pather.traverse_nodes([229], self, time_out=0.8, force_tp=True)
self._pather.traverse_nodes([229], self, timeout=0.8, force_tp=True)
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, time_out=0.8, force_tp=True)
self._pather.traverse_nodes([226], self, timeout=0.8, force_tp=True)
self._nova(atk_len)
self._move_and_attack((45, -20), atk_len)
self._move_and_attack((-45, 20), atk_len)
@@ -82,7 +82,7 @@ class NovaSorc(Sorceress):
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
atk_len = Config().char["atk_len_nihlathak"] * 0.3
# Move close to nihlathak
self._pather.traverse_nodes(end_nodes, self, time_out=0.8, do_pre_move=False)
self._pather.traverse_nodes(end_nodes, self, timeout=0.8, do_pre_move=False)
# move mouse to center
pos_m = convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
+6 -7
View File
@@ -10,7 +10,7 @@ import time
from typing import Tuple
from pather import Pather
from config import Config
from ui_manager import detect_screen_object, ScreenObjects
from ui_manager import ScreenObjects, is_visible
class Sorceress(IChar):
def __init__(self, skill_hotkeys: dict, pather: Pather):
@@ -38,20 +38,19 @@ class Sorceress(IChar):
self,
template_type: Union[str, List[str]],
success_func: Callable = None,
time_out: float = 8,
timeout: float = 8,
threshold: float = 0.68,
telekinesis: bool = False
) -> bool:
# In case telekinesis is False or hotkey is not set, just call the base implementation
if not self._skill_hotkeys["telekinesis"] or not telekinesis:
return super().select_by_template(template_type, success_func, time_out, threshold)
return super().select_by_template(template_type, success_func, timeout, threshold)
if type(template_type) == list and "A5_STASH" in template_type:
# sometimes waypoint is opened and stash not found because of that, check for that
match = detect_screen_object(ScreenObjects.WaypointLabel)
if match.valid:
if is_visible(ScreenObjects.WaypointLabel):
keyboard.send("esc")
start = time.time()
while time_out is None or (time.time() - start) < time_out:
while timeout is None or (time.time() - start) < timeout:
template_match = TemplateFinder().search(template_type, grab(), threshold=threshold, normalize_monitor=True)
if template_match.valid:
keyboard.send(self._skill_hotkeys["telekinesis"])
@@ -65,7 +64,7 @@ class Sorceress(IChar):
if success_func is None or success_func():
return True
# In case telekinesis fails, try again with the base implementation
return super().select_by_template(template_type, success_func, time_out, threshold)
return super().select_by_template(template_type, success_func, timeout, threshold)
def pre_buff(self):
if Config().char["cta_available"]:
+3 -3
View File
@@ -96,7 +96,7 @@ class Trapsin(IChar):
if self.capabilities.can_teleport_natively:
self._pather.traverse_nodes_fixed("eldritch_end", self)
else:
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, time_out=0.6, force_tp=True)
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=0.6, force_tp=True)
return True
def kill_shenk(self) -> bool:
@@ -110,7 +110,7 @@ class Trapsin(IChar):
self._left_attack(cast_pos_abs, 90)
# Move to items
wait(self._cast_duration, self._cast_duration + 0.2)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, time_out=1.4, force_tp=True)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.4, force_tp=True)
return True
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
@@ -132,7 +132,7 @@ class Trapsin(IChar):
self.move(pos_m)
# Move to items
wait(self._cast_duration, self._cast_duration + 0.2)
self._pather.traverse_nodes(end_nodes, self, time_out=0.8)
self._pather.traverse_nodes(end_nodes, self, timeout=0.8)
return True
+2 -2
View File
@@ -23,12 +23,12 @@ class Chest:
chest = filename[:-4].upper()
self._templates.append(chest)
def open_up_chests(self, time_out: float = 8.0, threshold: float = 0.73) -> bool:
def open_up_chests(self, timeout: float = 8.0, threshold: float = 0.73) -> bool:
Logger.debug("Open chests")
templates = self._templates
found_chest = True
start = time.time()
while time.time() - start < time_out:
while time.time() - start < timeout:
template_match = TemplateFinder().search(templates, grab(), roi=Config().ui_roi["reduce_to_center"], threshold=threshold, use_grayscale=True, best_match=True, normalize_monitor=True)
# search for at least 1.5 second, if no chest found, break
if not template_match.valid:
+118 -76
View File
@@ -3,10 +3,8 @@ import string
import threading
import numpy as np
import os
import re
from dataclasses import dataclass
from logger import Logger
config_lock = threading.Lock()
@@ -23,13 +21,9 @@ class ItemProps:
class Config:
data_loaded = False
# all configparser objects
_config = None
_game_config = None
_pickit_config = None
_shop_config = None
_transmute_config = None
_custom = None
configs = {}
# config data
general = {}
advanced_options = {}
@@ -50,6 +44,7 @@ class Config:
hammerdin = {}
trapsin = {}
barbarian = {}
poison_necro = {}
necro = {}
basic = {}
basic_ranged = {}
@@ -72,16 +67,28 @@ class Config:
return default
def _select_val(self, section: str, key: str = None):
if section in self._custom and key in self._custom[section]:
return self._custom[section][key]
elif section in self._config:
return self._config[section][key]
elif section in self._pickit_config:
return self._pickit_config[section][key]
elif section in self._shop_config:
return self._shop_config[section][key]
found_in = ""
if section in self.configs["custom"]["parser"] and key in self.configs["custom"]["parser"][section]:
found_val = self.configs["custom"]["parser"][section][key]
found_in = "custom"
elif section in self.configs["config"]["parser"]:
found_val = self.configs["config"]["parser"][section][key]
found_in = "config"
elif section in self.configs["pickit"]["parser"]:
found_val = self.configs["pickit"]["parser"][section][key]
found_in = "pickit"
elif section in self.configs["shop"]["parser"]:
found_val = self.configs["shop"]["parser"][section][key]
found_in = "shop"
else:
return self._game_config[section][key]
found_val = self.configs["game"]["parser"][section][key]
found_in = "game"
for var_name in self.configs[found_in]["vars"]: # set variable.
if var_name in found_val:
var_val = self.configs[found_in]["vars"][var_name]
found_val = found_val.replace(var_name, var_val)
return found_val
def parse_item_config_string(self, key: str = None) -> ItemProps:
string = self._select_val("items", key).upper()
@@ -170,32 +177,52 @@ class Config:
self.items["misc_gold"].pickit_type = 1
def load_data(self):
self._config = configparser.ConfigParser()
self._config.read('config/params.ini')
self._game_config = configparser.ConfigParser()
self._game_config.read('config/game.ini')
self._pickit_config = configparser.ConfigParser()
self._pickit_config.read('config/pickit.ini')
self._shop_config = configparser.ConfigParser()
self._shop_config.read('config/shop.ini')
self._custom = configparser.ConfigParser()
Logger.debug("Loading config")
self.configs = {
"config": {"parser": configparser.ConfigParser(), "vars": {}},
"game": {"parser": configparser.ConfigParser(), "vars": {}},
"pickit": {"parser": configparser.ConfigParser(), "vars": {}},
"shop": {"parser": configparser.ConfigParser(), "vars": {}},
"transmute": {"parser": configparser.ConfigParser(), "vars": {}},
"custom": {"parser": configparser.ConfigParser(), "vars": {}},
}
self.configs["config"]["parser"].read('config/params.ini')
self.configs["game"]["parser"].read('config/game.ini')
self.configs["pickit"]["parser"].read('config/pickit.ini')
self.configs["shop"]["parser"].read('config/shop.ini')
self.configs["transmute"]["parser"].read('config/transmute.ini')
if os.environ.get('RUN_ENV') != "test" and os.path.exists('config/custom.ini'):
self._custom.read('config/custom.ini')
try:
self.configs["custom"]["parser"].read('config/custom.ini')
except configparser.MissingSectionHeaderError:
Logger.error("custom.ini missing section header, defaulting to params.ini")
for config_name in self.configs:
config = self.configs[config_name]
try:
for var in config["parser"]["variables"]:
config["vars"][var] = config["parser"]["variables"][var] # set var name to var value
except KeyError: # "" header section was not found for this config file.
pass
self.general = {
"saved_games_folder": self._select_val("general", "saved_games_folder"),
"name": self._select_val("general", "name"),
"max_game_length_s": float(self._select_val("general", "max_game_length_s")),
"max_consecutive_fails": int(self._select_val("general", "max_consecutive_fails")),
"max_runtime_before_break_m": float(_default_iff(self._select_val("general", "max_runtime_before_break_m"), '', 0)),
"break_length_m": float(_default_iff(self._select_val("general", "break_length_m"), '', 0)),
"randomize_runs": bool(int(self._select_val("general", "randomize_runs"))),
"difficulty": self._select_val("general", "difficulty"),
"message_api_type": self._select_val("general", "message_api_type"),
"custom_message_hook": self._select_val("general", "custom_message_hook"),
"discord_status_count": False if not self._select_val("general", "discord_status_count") else int(self._select_val("general", "discord_status_count")),
"discord_status_condensed": bool(int(self._select_val("general", "discord_status_condensed"))),
"info_screenshots": bool(int(self._select_val("general", "info_screenshots"))),
"loot_screenshots": bool(int(self._select_val("general", "loot_screenshots"))),
"d2r_path": self._select_val("general", "d2r_path"),
"d2r_path": _default_iff(self._select_val("general", "d2r_path"), "", "C:\Program Files (x86)\Diablo II Resurrected"),
"restart_d2r_when_stuck": bool(int(self._select_val("general", "restart_d2r_when_stuck"))),
}
@@ -206,11 +233,13 @@ class Config:
}
self.routes = {}
order_str = self._select_val("routes", "order").replace("run_eldritch", "run_shenk")
order_str = self._select_val("routes", "order")
self.routes_order = [x.strip() for x in order_str.split(",")]
del self._config["routes"]["order"]
for key in self._config["routes"]:
self.routes[key] = bool(int(self._select_val("routes", key)))
del self.configs["config"]["parser"]["routes"]["order"]
for key in self.routes_order:
self.routes[key] = True
# Botty only knows "run_shenk" but in orders we split run_eldritch and run_eldritch_shenk
self.routes_order = ["run_shenk" if x in ["run_eldritch", "run_eldritch_shenk"] else x for x in self.routes_order]
self.char = {
"type": self._select_val("char", "type"),
@@ -238,7 +267,7 @@ class Config:
"belt_hp_columns": int(self._select_val("char", "belt_hp_columns")),
"belt_mp_columns": int(self._select_val("char", "belt_mp_columns")),
"stash_gold": bool(int(self._select_val("char", "stash_gold"))),
"gold_trav_only": bool(int(self._select_val("char", "gold_trav_only"))),
"min_gold_to_pick": int(_default_iff(self._select_val("char", "min_gold_to_pick"), '', 0)),
"use_merc": bool(int(self._select_val("char", "use_merc"))),
"id_items": bool(int(self._select_val("char", "id_items"))),
"open_chests": bool(int(self._select_val("char", "open_chests"))),
@@ -268,56 +297,60 @@ class Config:
"sell_junk": bool(int(self._select_val("char", "sell_junk"))),
}
# Sorc base config
sorc_base_cfg = dict(self._config["sorceress"])
if "sorceress" in self._custom:
sorc_base_cfg.update(dict(self._custom["sorceress"]))
sorc_base_cfg = dict(self.configs["config"]["parser"]["sorceress"])
if "sorceress" in self.configs["custom"]["parser"]:
sorc_base_cfg.update(dict(self.configs["custom"]["parser"]["sorceress"]))
# blizz sorc
self.blizz_sorc = dict(self._config["blizz_sorc"])
if "blizz_sorc" in self._custom:
self.blizz_sorc.update(dict(self._custom["blizz_sorc"]))
self.blizz_sorc = dict(self.configs["config"]["parser"]["blizz_sorc"])
if "blizz_sorc" in self.configs["custom"]["parser"]:
self.blizz_sorc.update(dict(self.configs["custom"]["parser"]["blizz_sorc"]))
self.blizz_sorc.update(sorc_base_cfg)
# light sorc
self.light_sorc = dict(self._config["light_sorc"])
if "light_sorc" in self._custom:
self.light_sorc.update(dict(self._custom["light_sorc"]))
self.light_sorc = dict(self.configs["config"]["parser"]["light_sorc"])
if "light_sorc" in self.configs["custom"]["parser"]:
self.light_sorc.update(dict(self.configs["custom"]["parser"]["light_sorc"]))
self.light_sorc.update(sorc_base_cfg)
# nova sorc
self.nova_sorc = dict(self._config["nova_sorc"])
if "nova_sorc" in self._custom:
self.nova_sorc.update(dict(self._custom["nova_sorc"]))
self.nova_sorc = dict(self.configs["config"]["parser"]["nova_sorc"])
if "nova_sorc" in self.configs["custom"]["parser"]:
self.nova_sorc.update(dict(self.configs["custom"]["parser"]["nova_sorc"]))
self.nova_sorc.update(sorc_base_cfg)
# Palandin config
self.hammerdin = self._config["hammerdin"]
if "hammerdin" in self._custom:
self.hammerdin.update(self._custom["hammerdin"])
self.hammerdin = self.configs["config"]["parser"]["hammerdin"]
if "hammerdin" in self.configs["custom"]["parser"]:
self.hammerdin.update(self.configs["custom"]["parser"]["hammerdin"])
# Assasin config
self.trapsin = self._config["trapsin"]
if "trapsin" in self._custom:
self.trapsin.update(self._custom["trapsin"])
self.trapsin = self.configs["config"]["parser"]["trapsin"]
if "trapsin" in self.configs["custom"]["parser"]:
self.trapsin.update(self.configs["custom"]["parser"]["trapsin"])
# Barbarian config
self.barbarian = self._config["barbarian"]
if "barbarian" in self._custom:
self.barbarian.update(self._custom["barbarian"])
self.barbarian = self.configs["config"]["parser"]["barbarian"]
if "barbarian" in self.configs["custom"]["parser"]:
self.barbarian.update(self.configs["custom"]["parser"]["barbarian"])
self.barbarian = dict(self.barbarian)
self.barbarian["cry_frequency"] = float(self.barbarian["cry_frequency"])
# Basic config
self.basic = self._config["basic"]
if "basic" in self._custom:
self.basic.update(self._custom["basic"])
self.basic = self.configs["config"]["parser"]["basic"]
if "basic" in self.configs["custom"]["parser"]:
self.basic.update(self.configs["custom"]["parser"]["basic"])
# Basic Ranged config
self.basic_ranged = self._config["basic_ranged"]
if "basic_ranged" in self._custom:
self.basic_ranged.update(self._custom["basic_ranged"])
self.basic_ranged = self.configs["config"]["parser"]["basic_ranged"]
if "basic_ranged" in self.configs["custom"]["parser"]:
self.basic_ranged.update(self.configs["custom"]["parser"]["basic_ranged"])
# Necro config
self.necro = self._config["necro"]
if "necro" in self._custom:
self.necro.update(self._custom["necro"])
self.necro = self.configs["config"]["parser"]["necro"]
if "necro" in self.configs["custom"]["parser"]:
self.necro.update(self.configs["custom"]["parser"]["necro"])
self.poison_necro = self.configs["config"]["parser"]["poison_necro"]
if "poison_necro" in self.configs["custom"]["parser"]:
self.poison_necro.update(self.configs["custom"]["parser"]["poison_necro"])
self.advanced_options = {
"pathing_delay_factor": min(max(int(self._select_val("advanced_options", "pathing_delay_factor")), 1), 10),
@@ -335,32 +368,41 @@ class Config:
"hwnd_window_process": _default_iff(Config()._select_val("advanced_options", "hwnd_window_process"), ''),
"window_client_area_offset": tuple(map(int, Config()._select_val("advanced_options", "window_client_area_offset").split(","))),
"ocr_during_pickit": bool(int(self._select_val("advanced_options", "ocr_during_pickit"))),
"launch_options": self._select_val("advanced_options", "launch_options"),
"override_capabilities": _default_iff(Config()._select_optional("advanced_options", "override_capabilities"), ""),
}
self.items = {}
for key in self._pickit_config["items"]:
for key in self.configs["pickit"]["parser"]["items"]:
try:
self.items[key] = self.parse_item_config_string(key)
if self.items[key].pickit_type and not os.path.exists(f"./assets/items/{key}.png"):
print(f"Warning: You activated {key} in pickit, but there is no img available in assets/items")
Logger.warning(f"You activated {key} in pickit, but there is no img available in assets/items")
except ValueError as e:
print(f"Error with pickit config: {key} ({e})")
Logger.error(f"Error with pickit config: {key} ({e})")
self.colors = {}
for key in self._game_config["colors"]:
for key in self.configs["game"]["parser"]["colors"]:
self.colors[key] = np.split(np.array([int(x) for x in self._select_val("colors", key).split(",")]), 2)
self.ui_pos = {}
for key in self._game_config["ui_pos"]:
for key in self.configs["game"]["parser"]["ui_pos"]:
self.ui_pos[key] = int(self._select_val("ui_pos", key))
self.ui_roi = {}
for key in self._game_config["ui_roi"]:
for key in self.configs["game"]["parser"]["ui_roi"]:
self.ui_roi[key] = np.array([int(x) for x in self._select_val("ui_roi", key).split(",")])
open_width = int(self.ui_pos["slot_width"] * self.char["num_loot_columns"])
# calc roi for restricted inventory area
self.ui_roi["restricted_inventory_area"] = self.ui_roi["right_inventory"].copy()
self.ui_roi["restricted_inventory_area"][0] += open_width # left
self.ui_roi["restricted_inventory_area"][2] -= open_width # width
# calc roi for open inventory area
self.ui_roi["open_inventory_area"] = self.ui_roi["right_inventory"].copy()
self.ui_roi["open_inventory_area"][2] = open_width # width
self.path = {}
for key in self._game_config["path"]:
for key in self.configs["game"]["parser"]["path"]:
self.path[key] = np.reshape(np.array([int(x) for x in self._select_val("path", key).split(",")]), (-1, 2))
self.shop = {
@@ -375,19 +417,19 @@ class Config:
"apply_pather_adjustment": bool(int(self._select_val("scepters", "apply_pather_adjustment"))),
}
stash_destination_str = self._select_val("transmute","stash_destination")
self._transmute_config = {
self.configs["transmute"]["parser"] = {
"stash_destination": [int(x.strip()) for x in stash_destination_str.split(",")],
"transmute_every_x_game": self._select_val("transmute","transmute_every_x_game"),
}
if __name__ == "__main__":
from copy import deepcopy
config = self()
config = Config()
# Check if any added items miss templates
for k in config.items:
if not os.path.exists(f"./assets/items/{k}.png"):
print(f"Template not found: {k}")
Logger.warning(f"Template not found: {k}")
# Check if any item templates miss a config
for filename in os.listdir(f'assets/items'):
@@ -396,4 +438,4 @@ if __name__ == "__main__":
item_name = filename[:-4]
blacklist_item = item_name.startswith("bl__")
if item_name not in config.items and not blacklist_item:
print(f"Config not found for: " + filename)
Logger.warning(f"Config not found for: " + filename)
+5 -8
View File
@@ -1,14 +1,12 @@
from utils.misc import color_filter, cut_roi, wait
from template_finder import TemplateFinder
from screen import convert_screen_to_monitor, grab
from utils.misc import wait
from screen import grab
from config import Config
from utils.custom_mouse import mouse
import keyboard
import cv2
from logger import Logger
import time
from ui import view
from ui_manager import ScreenObjects, detect_screen_object
from ui_manager import ScreenObjects, is_visible
class DeathManager:
@@ -36,8 +34,7 @@ class DeathManager:
def handle_death_screen(self):
img = grab()
template_match = detect_screen_object(ScreenObjects.YouHaveDied, img)
if template_match.valid:
if is_visible(ScreenObjects.YouHaveDied, img):
Logger.warning("You have died!")
if Config().general["info_screenshots"]:
self._last_death_screenshot = "./info_screenshots/info_debug_death_" + time.strftime("%Y%m%d_%H%M%S") + ".png"
@@ -55,7 +52,7 @@ class DeathManager:
wait(0.1, 0.2)
mouse.release(button="left")
time.sleep(1)
if detect_screen_object(ScreenObjects.MainMenu).valid:
if is_visible(ScreenObjects.MainMenu):
# in this case chicken executed and left the game, but we were still dead.
return True
keyboard.send("esc")
-1
View File
@@ -42,7 +42,6 @@ class GameController:
do_restart = False
messenger = Messenger()
while 1:
self.health_manager.update_location(self.bot.get_curr_location())
max_game_length_reached = self.game_stats.get_current_game_length() > Config().general["max_game_length_s"]
max_consecutive_fails_reached = False if not Config().general["max_consecutive_fails"] else self.game_stats.get_consecutive_runs_failed() >= Config().general["max_consecutive_fails"]
if max_game_length_reached or max_consecutive_fails_reached or self.death_manager.died() or self.health_manager.did_chicken():
+5 -6
View File
@@ -1,9 +1,8 @@
from screen import grab
from config import Config
from death_manager import DeathManager
import time
import keyboard
from ui_manager import detect_screen_object, ScreenObjects
from ui_manager import ScreenObjects, is_visible
from ui import view, loading
from utils.misc import set_d2r_always_on_top
@@ -23,22 +22,22 @@ class GameRecovery:
# make sure we are not on loading screen
is_loading = loading.check_for_black_screen()
while is_loading:
is_loading = detect_screen_object(ScreenObjects.Loading).valid
is_loading = is_visible(ScreenObjects.Loading)
is_loading |= loading.check_for_black_screen()
time.sleep(0.5)
# lets just see if you might already be at hero selection
if detect_screen_object(ScreenObjects.MainMenu).valid:
if is_visible(ScreenObjects.MainMenu):
return True
# would have been too easy, maybe we have died?
if self._death_manager.handle_death_screen():
time.sleep(1)
continue
# check for save/exit button
if detect_screen_object(ScreenObjects.SaveAndExit).valid:
if is_visible(ScreenObjects.SaveAndExit):
view.save_and_exit()
continue
# maybe we are in-game in stash/inventory, press escape
elif detect_screen_object(ScreenObjects.InGame).valid:
elif is_visible(ScreenObjects.InGame):
keyboard.send("esc")
time.sleep(1)
return False
+57 -11
View File
@@ -1,3 +1,4 @@
import math
import numpy as np
import time
import threading
@@ -8,8 +9,11 @@ from logger import Logger
from config import Config
from messages import Messenger
from utils.misc import hms
from utils.levels import get_level
from version import __version__
from ui import player_bar
class GameStats:
def __init__(self):
@@ -31,6 +35,9 @@ class GameStats:
self._location_stats["totals"] = { "items": 0, "deaths": 0, "chickens": 0, "merc_deaths": 0, "failed_runs": 0 }
self._stats_filename = f'stats_{time.strftime("%Y%m%d_%H%M%S")}.log'
self._nopickup_active = False
self._starting_exp = 0
self._current_exp = 1
self._current_lvl = 0
def update_location(self, loc: str):
if self._location != loc:
@@ -43,7 +50,7 @@ class GameStats:
def log_item_keep(self, item_name: str, send_message: bool, img: np.ndarray, ocr_text: str = None):
Logger.debug(f"Stashed and logged: {item_name}")
filtered_items = ["_potion", "misc_gold"]
filtered_items = ["_potion", "misc_gold", "_amethyst", "_ruby", "misc_chipped_diamond", "misc_flawed_diamond", "misc_diamond", "misc_flawless_diamond", "_topaz", "_emerald", "_sapphire", "misc_chipped_skull", "misc_flawed_skull", "misc_skull", "misc_flawless_skull"]
if self._location is not None and not any(substring in item_name for substring in filtered_items):
self._location_stats[self._location]["items"].append(item_name)
self._location_stats["totals"]["items"] += 1
@@ -100,6 +107,20 @@ class GameStats:
self._consecutive_runs_failed = 0
Logger.info(f"End game. Elapsed time: {elapsed_time:.2f}s")
def log_exp(self):
exp = player_bar.get_experience()
if exp[1] > 0:
curr_lvl = get_level(exp[1])['lvl']
if curr_lvl > 0:
self._current_lvl = curr_lvl-1
if self._starting_exp == 0:
self._starting_exp = exp[0]
if exp[0] > 0:
self._current_exp = exp[0]
def pause_timer(self):
if self._timer is None or self._paused:
return
@@ -129,12 +150,40 @@ class GameStats:
elapsed_time_str = hms(elapsed_time)
avg_length_str = "n/a"
good_games_count = self._game_counter - self._runs_failed
if good_games_count > 0:
good_games_time = elapsed_time - self._failed_game_time
avg_length = good_games_time / float(good_games_count)
avg_length_str = hms(avg_length)
good_games_time = elapsed_time - self._failed_game_time
msg = f'\nSession length: {elapsed_time_str}\nGames: {self._game_counter}\nAvg Game Length: {avg_length_str}'
if good_games_count == 0:
good_games_count = 1
avg_length = good_games_time / float(good_games_count)
avg_length_str = hms(avg_length)
curr_lvl = get_level(self._current_lvl)
msg = f'\nSession length: {elapsed_time_str}'
msg += f'\nGames: {self._game_counter}'
msg += f'\nAvg Game Length: {avg_length_str}'
msg += f'\nCurrent Level: {curr_lvl["lvl"]}'
if curr_lvl["lvl"] < 99:
try:
exp_gained = self._current_exp - curr_lvl['exp']
per_to_lvl = exp_gained / curr_lvl["xp_to_next"]
gained_exp = self._current_exp - self._starting_exp
exp_per_second = gained_exp / good_games_time
exp_per_hour = round(exp_per_second * 3600, 1)
exp_per_game = round(gained_exp / float(good_games_count), 1)
exp_needed = curr_lvl['xp_to_next'] - exp_gained
time_to_lvl = exp_needed / exp_per_second
games_to_lvl = exp_needed / exp_per_game
msg += f'\nPercent to Level: {math.ceil(per_to_lvl*100)}%'
msg += f'\nXP Gained: {gained_exp:,}'
msg += f'\nXP Per Hour: {exp_per_hour:,}'
msg += f'\nXP Per Game: {exp_per_game:,}'
msg += f'\nTime Needed To Level: {hms(time_to_lvl)}'
msg += f'\nGames Needed To Level: {math.ceil(games_to_lvl):,}'
except:
Logger.warning("Failed to log exp")
table = BeautifulTable()
table.set_style(BeautifulTable.STYLE_BOX_ROUNDED)
@@ -145,7 +194,7 @@ class GameStats:
table.rows.append([location, len(stats["items"]), stats["chickens"], stats["deaths"], stats["merc_deaths"], stats["failed_runs"]])
table.rows.append([
"T" if Config().general['discord_status_condensed'] else "Total",
"T",
self._location_stats["totals"]["items"],
self._location_stats["totals"]["chickens"],
self._location_stats["totals"]["deaths"],
@@ -153,10 +202,7 @@ class GameStats:
self._location_stats["totals"]["failed_runs"]
])
if Config().general['discord_status_condensed']:
table.columns.header = ["Run", "I", "C", "D", "MD", "F"]
else:
table.columns.header = ["Run", "Items", "Chicken", "Death", "Merc Death", "Failed Runs"]
table.columns.header = ["Run", "I", "C", "D", "MD", "F"]
msg += f"\n{str(table)}\n"
return msg
+48 -29
View File
@@ -9,8 +9,24 @@ from logger import Logger
from screen import grab
import time
from config import Config
from inventory import common
from ui import view, meters
from ui_manager import detect_screen_object, ScreenObjects
from ui_manager import ScreenObjects, is_visible
from random import uniform
pause_state = True
def get_pause_state():
global pause_state
return pause_state
def set_pause_state(state: bool):
global pause_state
prev = get_pause_state()
if prev != state:
debug_str = "pausing" if state else "active"
Logger.info(f"Health Manager is now {debug_str}")
pause_state = state
class HealthManager:
def __init__(self):
@@ -21,8 +37,8 @@ class HealthManager:
self._last_mana = time.time()
self._last_merc_heal = time.time()
self._callback = None
self._pausing = True
self._last_chicken_screenshot = None
self._count_panel_detects = 0
def stop_monitor(self):
self._do_monitor = False
@@ -35,16 +51,7 @@ class HealthManager:
def reset_chicken_flag(self):
self._did_chicken = False
self._pausing = True
def update_location(self, loc: Location):
if loc is not None and type(loc) == str:
bosses = ["shenk", "eldritch", "pindle", "nihlathak", "trav", "arc", "diablo"]
prev_value = self._pausing
self._pausing = not any(substring in loc for substring in bosses)
if self._pausing != prev_value:
debug_str = "pausing" if self._pausing else "active"
Logger.info(f"Health Manager is now {debug_str}")
set_pause_state(True)
def _do_chicken(self, img):
if self._callback is not None:
@@ -64,71 +71,83 @@ class HealthManager:
time.sleep(0.01)
view.save_and_exit()
self._did_chicken = True
self._pausing = True
set_pause_state(True)
def start_monitor(self):
Logger.info("Start health monitoring")
self._do_monitor = True
self._did_chicken = False
start = time.time()
while self._do_monitor:
time.sleep(0.1)
# Wait until the flag is reset by main.py
if self._did_chicken or self._pausing: continue
if self._did_chicken or get_pause_state(): continue
img = grab()
if detect_screen_object(ScreenObjects.InGame, img).valid:
if is_visible(ScreenObjects.InGame, img):
health_percentage = meters.get_health(img)
mana_percentage = meters.get_mana(img)
lp_hp_potion_delay = 0 if health_percentage >= 0.99 else uniform(9, 10)
lp_mp_potion_delay = 0 if mana_percentage >= 0.99 else uniform(9, 10)
# check rejuv
success_drink_rejuv = False
last_drink = time.time() - self._last_rejuv
if (health_percentage < Config().char["take_rejuv_potion_health"] and last_drink > 1) or \
(mana_percentage < Config().char["take_rejuv_potion_mana"] and last_drink > 2):
if (health_percentage <= Config().char["take_rejuv_potion_health"] and last_drink > 1) or \
(mana_percentage <= Config().char["take_rejuv_potion_mana"] and last_drink > 2):
success_drink_rejuv = belt.drink_potion("rejuv", stats=[health_percentage, mana_percentage])
self._last_rejuv = time.time()
# in case no rejuv was used, check for chicken, health pot and mana pot usage
if not success_drink_rejuv:
# check health
last_drink = time.time() - self._last_health
if health_percentage < Config().char["take_health_potion"] and last_drink > 3.5:
if health_percentage <= Config().char["take_health_potion"] and last_drink > lp_hp_potion_delay:
belt.drink_potion("health", stats=[health_percentage, mana_percentage])
self._last_health = time.time()
# give the chicken a 6 sec delay to give time for a healing pot and avoid endless loop of chicken
elif health_percentage < Config().char["chicken"] and (time.time() - start) > 6:
elif health_percentage <= Config().char["chicken"] and (time.time() - start) > 6:
Logger.warning(f"Trying to chicken, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
# check mana
last_drink = time.time() - self._last_mana
if mana_percentage < Config().char["take_mana_potion"] and last_drink > 4:
if mana_percentage <= Config().char["take_mana_potion"] and last_drink > lp_mp_potion_delay:
belt.drink_potion("mana", stats=[health_percentage, mana_percentage])
self._last_mana = time.time()
# check merc
if detect_screen_object(ScreenObjects.MercIcon, img).valid:
if is_visible(ScreenObjects.MercIcon, img):
merc_health_percentage = meters.get_merc_health(img)
merc_hp_potion_delay = 0 if merc_health_percentage == 1 else uniform(9, 10)
last_drink = time.time() - self._last_merc_heal
if merc_health_percentage < Config().char["merc_chicken"]:
if merc_health_percentage <= Config().char["merc_chicken"]:
Logger.warning(f"Trying to chicken, merc HP {(merc_health_percentage*100):.1f}%!")
self._do_chicken(img)
if merc_health_percentage < Config().char["heal_rejuv_merc"] and last_drink > 4.0:
if merc_health_percentage <= Config().char["heal_rejuv_merc"] and last_drink > 4.0:
belt.drink_potion("rejuv", merc=True, stats=[merc_health_percentage])
self._last_merc_heal = time.time()
elif merc_health_percentage < Config().char["heal_merc"] and last_drink > 7.0:
elif merc_health_percentage <= Config().char["heal_merc"] and last_drink > merc_hp_potion_delay:
belt.drink_potion("health", merc=True, stats=[merc_health_percentage])
self._last_merc_heal = time.time()
if detect_screen_object(ScreenObjects.QuestSkillBtn, img).valid:
Logger.warning(f"Skill, stats, or quest button appeared--quit to avoid accidental allocations.")
self._do_chicken(img)
if is_visible(ScreenObjects.LeftPanel, img) or is_visible(ScreenObjects.RightPanel, img):
Logger.warning(f"Found an open inventory / quest / skill / stats page. Close it.")
self._count_panel_detects += 1
if self._count_panel_detects >= 2:
self._count_panel_detects = 0
Logger.warning(f"Found an open inventory / quest / skill / stats page again. Chicken to dismiss.")
self._do_chicken(img)
common.close()
Logger.debug("Stop health monitoring")
# Testing: Start dying or lossing mana and see if it works
# Testing: Start dying or losing mana and see if it works
if __name__ == "__main__":
import threading
import keyboard
import os
from health_manager import set_pause_state
keyboard.add_hotkey('f12', lambda: Logger.info('Exit Health Manager') or os._exit(1))
manager = HealthManager()
manager._pausing = False
set_pause_state(True)
Logger.info("Press f12 to exit health manager")
health_monitor_thread = threading.Thread(target=manager.start_monitor)
health_monitor_thread.start()
+35 -16
View File
@@ -3,14 +3,34 @@ from logger import Logger
from typing import List
import numpy as np
from template_finder import TemplateFinder
from inventory import common, consumables
from inventory.consumables import item_consumables_map
from inventory import common, consumables, personal
from ui import view
from ui_manager import is_visible, wait_until_visible, ScreenObjects, wait_until_hidden
from utils.custom_mouse import mouse
from utils.misc import cut_roi, wait, color_filter
from config import Config
from screen import convert_abs_to_monitor, convert_monitor_to_screen, convert_screen_to_monitor, grab
import keyboard
import os
def open(img: np.ndarray = None) -> np.ndarray:
img = grab() if img is None else img
if is_visible(ScreenObjects.BeltExpandable, img) and Config().char["belt_rows"] > 1:
keyboard.send(Config().char["show_belt"])
if not wait_until_hidden(ScreenObjects.BeltExpandable, 1):
return None
img = grab()
return img
def close(img: np.ndarray = None) -> np.ndarray:
img = grab() if img is None else img
if not is_visible(ScreenObjects.BeltExpandable, img):
keyboard.send("esc")
if not wait_until_visible(ScreenObjects.BeltExpandable, 2).valid:
success = view.return_to_play()
if not success:
return None
return img
def _potion_type(img: np.ndarray) -> str:
"""
@@ -80,13 +100,18 @@ def update_pot_needs():
if screen_mouse_pos[1] > Config().ui_pos["screen_height"] * 0.72:
center_m = convert_abs_to_monitor((-200, -120))
mouse.move(*center_m, randomize=100)
keyboard.send(Config().char["show_belt"])
wait(0.5)
img = open()
# first clean up columns that might be too much
img = grab()
for column in range(4):
potion_type = _potion_type(_cut_potion_img(img, column, 0))
if potion_type != "empty":
potion_type = None
try:
potion_type = _potion_type(_cut_potion_img(img, column, 0))
except TypeError:
Logger.error("Could not find potions in belt. Most likely due to \"show_belt\" in params.ini having the incorrect hotkey.")
Logger.error("Closing in 10 seconds..")
wait(10)
os._exit(1)
if potion_type and potion_type != "empty":
rows_left[potion_type] -= 1
if rows_left[potion_type] < 0:
rows_left[potion_type] += 1
@@ -112,21 +137,17 @@ def update_pot_needs():
break
elif current_column is not None and potion_type == "empty":
pot_needs[current_column] += 1
wait(0.2)
#Logger.debug(f"Will pickup: {pot_needs}")
keyboard.send(Config().char["show_belt"])
consumables.set_needs("health", pot_needs["health"])
consumables.set_needs("mana", pot_needs["mana"])
consumables.set_needs("rejuv", pot_needs["rejuv"])
close(img)
def fill_up_belt_from_inventory(num_loot_columns: int):
"""
Fill up your belt with pots from the inventory e.g. after death. It will open and close invetory by itself!
:param num_loot_columns: Number of columns used for loot from left
"""
keyboard.send(Config().char["inventory_screen"])
wait(0.7, 1.0)
img = grab()
img = personal.open()
pot_positions = []
for column, row in itertools.product(range(num_loot_columns), range(4)):
center_pos, slot_img = common.get_slot_pos_and_img(img, column, row)
@@ -141,6 +162,4 @@ def fill_up_belt_from_inventory(num_loot_columns: int):
mouse.click(button="left")
wait(0.3, 0.4)
keyboard.release("shift")
wait(0.2, 0.25)
keyboard.send(Config().char["inventory_screen"])
wait(0.5)
common.close(img)
+91 -100
View File
@@ -5,29 +5,14 @@ import keyboard
import time
from utils.custom_mouse import mouse
from template_finder import TemplateFinder
from ui_manager import detect_screen_object, ScreenObjects, center_mouse
from ui_manager import detect_screen_object, ScreenObjects, is_visible, wait_until_hidden
from utils.misc import wait, trim_black, color_filter, cut_roi
from inventory import consumables, personal
from inventory import consumables
from ui import view
from screen import grab
from dataclasses import dataclass
from screen import convert_screen_to_monitor, grab
from logger import Logger
from ocr import Ocr
@dataclass
class BoxInfo:
img: np.ndarray = None
pos: tuple = None
column: int = None
row: int = None
need_id: bool = False
sell: bool = False
keep: bool = False
def __getitem__(self, key):
return super().__getattribute__(key)
def __setitem__(self, key, value):
setattr(self, key, value)
from template_finder import TemplateMatch
def get_slot_pos_and_img(img: np.ndarray, column: int, row: int) -> tuple[tuple[int, int], np.ndarray]:
"""
@@ -65,10 +50,9 @@ def slot_has_item(slot_img: np.ndarray) -> bool:
def close(img: np.ndarray = None) -> np.ndarray:
img = grab() if img is None else img
if detect_screen_object(ScreenObjects.RightPanel, img).valid:
if is_visible(ScreenObjects.RightPanel, img) or is_visible(ScreenObjects.LeftPanel, img):
keyboard.send("esc")
wait(0.1, 0.2)
if detect_screen_object(ScreenObjects.RightPanel).valid:
if not wait_until_hidden(ScreenObjects.RightPanel, 1) and not wait_until_hidden(ScreenObjects.LeftPanel, 1):
success = view.return_to_play()
if not success:
return None
@@ -88,6 +72,8 @@ def calc_item_roi(img_pre, img_post):
final = np.bitwise_and.reduce([blue_red_mask, green_mask, diff_thresh])
_, roi = trim_black(final)
return roi
except ValueError:
Logger.debug(f"_calc_item_roi: Couldn't determine item dimensions--tooltip likely obscuring")
except BaseException as err:
Logger.error(f"_calc_item_roi: Unexpected {err=}, {type(err)=}")
return None
@@ -113,80 +99,7 @@ def id_item_with_tome(item_location: list, id_tome_location: list):
wait(0.1)
mouse.click(button="left")
consumables.increment_need("id", 1)
wait(0.2, 0.4)
def transfer_items(items: list, action: str = "drop") -> list:
#requires open inventory / stash / vendor
img = grab()
filtered = []
left_panel_open = detect_screen_object(ScreenObjects.LeftPanel, img).valid
if action == "drop":
filtered = [ item for item in items if item.keep == False and item.sell == False ]
elif action == "sell":
filtered = [ item for item in items if item.keep == False and item.sell == True ]
if not left_panel_open:
Logger.error(f"transfer_items: Can't perform, vendor is not open")
elif action == "stash":
if detect_screen_object(ScreenObjects.GoldBtnStash, img).valid:
filtered = [ item for item in items if item.keep == True ]
else:
Logger.error(f"transfer_items: Can't perform, stash is not open")
else:
Logger.error(f"transfer_items: incorrect action param={action}")
if filtered:
# if dropping, control+click to drop unless left panel is open, then drag to middle
# if stashing, control+click to stash
# if selling, control+click to sell
# TODO: if purchasing, right-click to buy
# TODO: if purchasing stack, shift+right-click to buy
if (action == "drop" and not left_panel_open) or action in ["sell", "stash"]:
keyboard.send('ctrl', do_release=False)
wait(0.2, 0.4)
for item in filtered:
attempts = 0
prev_gold_img = cut_roi(grab(), roi=Config().ui_roi["inventory_gold_digits"])
while attempts < 2:
# move to item position and left click
mouse.move(*item.pos, randomize=4, delay_factor=[0.2, 0.4])
wait(0.2, 0.4)
mouse.press(button="left")
wait(0.2, 0.4)
mouse.release(button="left")
wait(0.2, 0.4)
# if dropping, drag item to middle if vendor/stash is open
if action == "drop" and left_panel_open:
center_mouse()
wait(0.2, 0.3)
mouse.press(button="left")
wait(0.2, 0.3)
mouse.release(button="left")
wait(0.8, 1)
# check if item is still there
img=grab()
slot_img = get_slot_pos_and_img(img, item.column, item.row)[1]
if not slot_has_item(slot_img):
# item successfully transferred, delete from list
for cnt, o_item in enumerate(items):
if o_item.pos == item.pos:
items.pop(cnt)
break
# check and see if inventory gold count changed
new_gold_img = cut_roi(img, roi=Config().ui_roi["inventory_gold_digits"])
if prev_gold_img.shape == new_gold_img.shape and not(np.bitwise_xor(prev_gold_img, new_gold_img).any()):
Logger.info("Inventory gold is full, force stash")
personal.set_inventory_gold_full(True)
else:
personal.set_inventory_gold_full(False)
break
else:
# item is still there, try again
attempts += 1
if attempts > 1:
Logger.error(f"transfer_items: could not stash in position {item.pos}")
if (action == "drop" and not left_panel_open) or action in ["sell", "stash"]:
keyboard.send('ctrl', do_press=False)
wait(0.1)
return items
wait(0.1)
# use with caution--unreliable
def read_gold(img: np.ndarray = None, type: str = "inventory"):
@@ -215,15 +128,93 @@ def read_gold(img: np.ndarray = None, type: str = "inventory"):
Logger.debug(f"{type.upper()} gold: {number}")
return number
def wait_for_left_inventory(timeout: int = 5):
start=time.time()
while time.time() - start < timeout:
if left_inventory_ready(grab()):
Logger.debug("Vendor/stash inventory ready")
return True
wait(0.1)
Logger.error("wait_for_left_inventory: Vendor/stash inventory not detected")
return False
def left_inventory_ready(img = np.ndarray):
# on laggy PC's or online the vendor may take longer to have all of its inventory ready
if is_visible(ScreenObjects.LeftPanel, img):
# check for tab text
text, _ = color_filter(img, Config().colors["tab_text"])
text = cut_roi(text, Config().ui_roi["left_inventory_tabs"])
# check for red slots in inventory space
red, _ = color_filter(img, Config().colors["red_slot"])
red = cut_roi(red, Config().ui_roi["left_inventory"])
# check for blue slots in inventory space
blue, _ = color_filter(img, Config().colors["blue_slot"])
blue = cut_roi(blue, Config().ui_roi["left_inventory"])
# if none of the above are true, then inventory is empty and there are no tabs (not loaded yet)
return any(np.sum(i) > 0 for i in [text, red, blue])
return False
def tab_properties(idx: int = 0) -> dict[int, int, tuple]:
tab_width = round(int(Config().ui_roi["tab_indicator"][2]) / 4)
x_start = int(Config().ui_roi["tab_indicator"][0])
left = idx * tab_width + x_start
right = (idx + 1) * tab_width + x_start
x_center = (left + right) / 2
y_center = int(Config().ui_roi["tab_indicator"][1]) - 5
return {
"left": round(left),
"right": round(right),
"center": (x_center, y_center)
}
def indicator_location_to_tab_count(pos: tuple) -> int:
for i in range(4):
tab = tab_properties(i)
if tab["left"] <= pos[0] < tab["right"]:
return i
return -1
def get_active_tab(indicator: TemplateMatch = None) -> int:
indicator = detect_screen_object(ScreenObjects.TabIndicator) if indicator is None else indicator
if indicator.valid:
return indicator_location_to_tab_count(indicator.center)
else:
Logger.error("common/get_active_tab(): Error finding tab indicator")
return -1
def select_tab(idx: int):
# stash or vendor must be open
# indices start from 0
if not get_active_tab() == idx:
tab = tab_properties(idx)
pos = convert_screen_to_monitor(tab["center"])
mouse.move(*pos)
wait(0.2, 0.3)
mouse.click("left")
wait(0.2, 0.3)
if __name__ == "__main__":
import os
import keyboard
from config import Config
from screen import start_detecting_window, stop_detecting_window
from utils.misc import color_filter
start_detecting_window()
keyboard.add_hotkey('f12', lambda: os._exit(1))
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or stop_detecting_window() or os._exit(1))
print("Move to d2r window and press f11")
keyboard.wait("f11")
img = grab()
print(read_gold(img, "inventory"))
stop_detecting_window()
#select_tab(0)
color = Config().colors["tab_text"]
while 1:
# img = cv2.imread("")
img = grab()
a = cut_roi(img, Config().ui_roi["deposit_ok"])
b, _ = color_filter(img, Config().colors["tab_text"])
print(np.sum(b))
cv2.imshow('test', b)
key = cv2.waitKey(1)
+12 -11
View File
@@ -1,15 +1,13 @@
from config import Config
import cv2
import numpy as np
import keyboard
import time
import parse
from utils.custom_mouse import mouse
from template_finder import TemplateFinder
from ui_manager import detect_screen_object, ScreenObjects
from inventory import personal, common
from inventory import personal
from utils.misc import wait
from screen import grab, convert_screen_to_monitor
from screen import grab
from dataclasses import dataclass
from logger import Logger
from item import ItemCropper
@@ -111,7 +109,7 @@ def update_tome_key_needs(img: np.ndarray = None, item_type: str = "tp") -> bool
match = TemplateFinder().search(
[f"{item_type.upper()}_TOME", f"{item_type.upper()}_TOME_RED"],
img,
roi = personal.specific_inventory_roi("reserved"),
roi = Config().ui_roi["restricted_inventory_area"],
best_match = True,
normalize_monitor=True
)
@@ -124,7 +122,7 @@ def update_tome_key_needs(img: np.ndarray = None, item_type: str = "tp") -> bool
Logger.debug(f"update_tome_key_needs: could not find {item_type}")
return False
elif item_type.lower() in ["key"]:
match = TemplateFinder().search("INV_KEY", img, roi = personal.specific_inventory_roi("reserved"), normalize_monitor = True)
match = TemplateFinder().search("INV_KEY", img, roi = Config().ui_roi["restricted_inventory_area"], normalize_monitor = True)
if not match.valid:
return False
else:
@@ -136,11 +134,14 @@ def update_tome_key_needs(img: np.ndarray = None, item_type: str = "tp") -> bool
# get the item description box
item_box = ItemCropper().crop_item_descr(hovered_item, model="engd2r_inv_th_fast")
if item_box.valid:
result = parse.search("Quantity: {:d}", item_box.ocr_result.text).fixed[0]
if item_type.lower() in ["tp", "id"]:
set_needs(item_type, 20 - result)
if item_type.lower() == "key":
set_needs(item_type, 12 - result)
try:
result = parse.search("Quantity: {:d}", item_box.ocr_result.text).fixed[0]
if item_type.lower() in ["tp", "id"]:
set_needs(item_type, 20 - result)
if item_type.lower() == "key":
set_needs(item_type, 12 - result)
except Exception as e:
Logger.error(f"update_tome_key_needs: unable to parse quantity for {item_type}. Exception: {e}")
else:
Logger.error(f"update_tome_key_needs: Failed to capture item description box for {item_type}")
if Config().general["info_screenshots"]:
+145 -75
View File
@@ -3,11 +3,12 @@ from game_stats import GameStats
from item import ItemFinder, Item
from item.item_cropper import ItemText
from logger import Logger
from screen import convert_abs_to_monitor, grab, convert_screen_to_monitor
from screen import grab, convert_screen_to_monitor
import keyboard
import cv2
import time
import numpy as np
from dataclasses import dataclass
from template_finder import TemplateFinder
from config import Config
@@ -15,7 +16,7 @@ from utils.misc import wait, is_in_roi, mask_by_roi
from utils.custom_mouse import mouse
from inventory import stash, common, vendor
from ui import view
from ui_manager import detect_screen_object, wait_for_screen_object, ScreenObjects, center_mouse
from ui_manager import detect_screen_object, is_visible, select_screen_object_match, wait_until_visible, ScreenObjects, center_mouse, wait_for_update
from item import ItemCropper
from messages import Messenger
@@ -24,6 +25,20 @@ messenger = Messenger()
item_finder = ItemFinder()
nontradable_items = ["key of ", "essense of", "wirt's", "jade figurine"]
@dataclass
class BoxInfo:
img: np.ndarray = None
pos: tuple = None
column: int = None
row: int = None
need_id: bool = False
sell: bool = False
keep: bool = False
def __getitem__(self, key):
return super().__getattribute__(key)
def __setitem__(self, key, value):
setattr(self, key, value)
def get_inventory_gold_full():
global inv_gold_full
return inv_gold_full
@@ -58,80 +73,79 @@ def stash_all_items(items: list = None):
global messenger
if items is None:
Logger.debug("No items to stash, skip")
return False
return None
center_mouse()
# Wait till gold btn is found
Logger.debug("Searching for inventory gold btn...")
if not (gold_btn := wait_for_screen_object(ScreenObjects.GoldBtnInventory, time_out = 20)).valid:
Logger.error("Could not determine to be in stash menu. Continue...")
return
Logger.debug("Found inventory gold btn")
# Wait for stash to fully load
if not common.wait_for_left_inventory():
Logger.error("stash_all_items(): Could not determine to be in stash menu. Continue...")
return items
# stash gold
if Config().char["stash_gold"]:
img = grab()
if detect_screen_object(ScreenObjects.GoldNone, img).valid:
Logger.debug("No gold to stash")
else:
if not is_visible(ScreenObjects.GoldNone):
Logger.debug("Stashing gold")
stash.move_to_stash_tab(min(3, stash.curr_stash["gold"]))
try:
# Try to read gold count with OCR
stash_full_of_gold = common.read_gold(img, "stash") == 2500000
except:
stash_full_of_gold = False
common.select_tab(min(3, stash.get_curr_stash()["gold"]))
wait(0.7, 1)
stash_full_of_gold = False
# Try to read gold count with OCR
try: stash_full_of_gold = common.read_gold(grab(), "stash") == 2500000
except: pass
if not stash_full_of_gold:
# If gold read by OCR fails, fallback to old method
mouse.move(*gold_btn.center, randomize=4)
wait(0.1, 0.15)
mouse.press(button="left")
wait(0.25, 0.35)
mouse.release(button="left")
wait(0.4, 0.6)
keyboard.send("enter") #if stash already full of gold just nothing happens -> gold stays on char -> no popup window
wait(1.3, 1.6)
gold_btn = detect_screen_object(ScreenObjects.GoldBtnInventory)
select_screen_object_match(gold_btn)
if wait_until_visible(ScreenObjects.DepositBtn, 3).valid:
keyboard.send("enter") #if stash already full of gold just nothing happens -> gold stays on char -> no popup window
else:
Logger.error("stash_all_items(): deposit button not detected, failed to stash gold")
# move cursor away from button to interfere with screen grab
mouse.move(-120, 0, absolute=False, randomize=15)
stash_full_of_gold = not detect_screen_object(ScreenObjects.GoldNone).valid
mouse.move(-120, 0, absolute=False, randomize=15, delay_factor=[0.3, 0.5])
# if 0 gold becomes visible in personal inventory then the stash tab still has room for gold
stash_full_of_gold = not wait_until_visible(ScreenObjects.GoldNone, 1.5).valid
if stash_full_of_gold:
Logger.info("Stash tab is full of gold, selecting next stash tab.")
stash.curr_stash["gold"] += 1
Logger.debug("Stash tab is full of gold, selecting next stash tab.")
stash.set_curr_stash(gold = (stash.get_curr_stash()["gold"] + 1))
if Config().general["info_screenshots"]:
cv2.imwrite("./info_screenshots/info_gold_stash_full_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
if stash.curr_stash["gold"] > 3:
if stash.get_curr_stash()["gold"] > 3:
#decide if gold pickup should be disabled or gambling is active
vendor.set_gamble_status(True)
else:
# move to next stash
wait(0.5, 0.6)
# move to next stash tab
return stash_all_items(items=items)
else:
set_inventory_gold_full(False)
# check if stash tab is completely full (no empty slots)
stash.move_to_stash_tab(stash.curr_stash["items"])
while stash.curr_stash["items"] <= 3:
common.select_tab(stash.get_curr_stash()["items"])
while stash.get_curr_stash()["items"] <= 3:
img = grab()
if detect_screen_object(ScreenObjects.EmptyStashSlot, img).valid:
if is_visible(ScreenObjects.EmptyStashSlot, img):
break
else:
Logger.info(f"Stash tab completely full, advance to next")
Logger.debug(f"Stash tab completely full, advance to next")
if Config().general["info_screenshots"]:
cv2.imwrite("./info_screenshots/stash_tab_completely_full_" + time.strftime("%Y%m%d_%H%M%S") + ".png", img)
stash.curr_stash["items"] += -1 if Config().char["fill_shared_stash_first"] else 1
if (Config().char["fill_shared_stash_first"] and stash.curr_stash["items"] < 0) or stash.curr_stash["items"] > 3:
if Config().char["fill_shared_stash_first"]:
stash.set_curr_stash(items = (stash.get_curr_stash()["items"] - 1))
else:
stash.set_curr_stash(items = (stash.get_curr_stash()["items"] + 1))
if (Config().char["fill_shared_stash_first"] and stash.get_curr_stash()["items"] < 0) or stash.get_curr_stash()["items"] > 3:
stash.stash_full()
stash.move_to_stash_tab(stash.curr_stash["items"])
common.select_tab(stash.get_curr_stash()["items"])
# stash stuff
while True:
items = common.transfer_items(items, "stash")
items = transfer_items(items, "stash")
if items and any([item.keep for item in items]):
# could not stash all items, stash tab is likely full
Logger.debug("Wanted to stash item, but it's still in inventory. Assumes full stash. Move to next.")
if Config().general["info_screenshots"]:
cv2.imwrite("./info_screenshots/debug_info_inventory_not_empty_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
stash.curr_stash["items"] += -1 if Config().char["fill_shared_stash_first"] else 1
if (Config().char["fill_shared_stash_first"] and stash.curr_stash["items"] < 0) or stash.curr_stash["items"] > 3:
if Config().char["fill_shared_stash_first"]:
stash.set_curr_stash(items = (stash.get_curr_stash()["items"] - 1))
else:
stash.set_curr_stash(items = (stash.get_curr_stash()["items"] + 1))
if (Config().char["fill_shared_stash_first"] and stash.get_curr_stash()["items"] < 0) or stash.get_curr_stash()["items"] > 3:
stash.stash_full()
stash.move_to_stash_tab(stash.curr_stash["items"])
common.select_tab(stash.get_curr_stash()["items"])
else:
break
Logger.debug("Done stashing")
@@ -144,7 +158,6 @@ def keep_item(item_box: ItemText = None, found_item: Item = None, do_logging: bo
:param do_logging: Bool value to turn on/off logging for items that are found and should be kept
:return: Bool if item should be kept
"""
wait(0.2, 0.3)
ymax = 50 if item_box.data.shape[0] < 50 else item_box.data.shape[0]
img = item_box.data[0:ymax,:]
@@ -162,7 +175,7 @@ def keep_item(item_box: ItemText = None, found_item: Item = None, do_logging: bo
and any(item_type in found_item.name for item_type in ["uniq", "magic", "rare", "set"])
)
# items that are checked to be kept should all be identified unless an error occurred or user preferred unidentified
or detect_screen_object(ScreenObjects.Unidentified, item_box.data).valid
or is_visible(ScreenObjects.Unidentified, item_box.data)
):
include_props = False
exclude_props = False
@@ -243,41 +256,28 @@ def keep_item(item_box: ItemText = None, found_item: Item = None, do_logging: bo
return True
return False
def specific_inventory_roi(desired: str = "reserved"):
#roi spec: left, top, W, H
roi = Config().ui_roi["right_inventory"].copy()
open_width = Config().ui_pos["slot_width"] * Config().char["num_loot_columns"]
if desired == "reserved":
roi[0]=roi[0] + open_width
roi[2]=roi[2] - open_width
elif desired == "open":
roi[2]=open_width
else:
Logger.error(f"set_inventory_rois: unsupported desired={desired}")
return None
return roi
def open(img: np.ndarray = None) -> np.ndarray:
img = grab() if img is None else img
if not detect_screen_object(ScreenObjects.RightPanel, img).valid:
if not is_visible(ScreenObjects.RightPanel, img):
keyboard.send(Config().char["inventory_screen"])
if not wait_for_screen_object(ScreenObjects.RightPanel, 1).valid:
if not wait_until_visible(ScreenObjects.RightPanel, 1).valid:
if not view.return_to_play():
return None
keyboard.send(Config().char["inventory_screen"])
if not wait_for_screen_object(ScreenObjects.RightPanel, 1).valid:
if not wait_until_visible(ScreenObjects.RightPanel, 1).valid:
Logger.error(f"personal.open(): Failed to open inventory")
return None
img = grab()
return img
def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_stats: GameStats = None):
def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_stats: GameStats = None) -> list[BoxInfo]:
"""
Iterate over all picked items in inventory--ID items and decide which to stash
:param img: Image in which the item is searched (item details should be visible)
"""
center_mouse()
img = open(inp_img)
vendor_open = detect_screen_object(ScreenObjects.GoldBtnVendor, inp_img).valid
vendor_open = is_visible(ScreenObjects.GoldBtnVendor, inp_img)
slots = []
# check which slots have items
for column, row in itertools.product(range(Config().char["num_loot_columns"]), range(4)):
@@ -303,8 +303,8 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
# determine the item's ROI in inventory
cnt=0
while True:
pre = mask_by_roi(img, specific_inventory_roi("open"))
post = mask_by_roi(hovered_item, specific_inventory_roi("open"))
pre = mask_by_roi(img, Config().ui_roi["open_inventory_area"])
post = mask_by_roi(hovered_item, Config().ui_roi["open_inventory_area"])
# will sometimes have equivalent diff if mouse ends up in an inconvenient place.
if not np.array_equal(pre, post):
break
@@ -341,10 +341,10 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
implied_no_id = not (Config().items[found_item.name].include or Config().items[found_item.name].exclude)
implied_no_id |= not any(item_type in found_item.name for item_type in ["uniq", "magic", "rare", "set"])
if not implied_no_id:
if (is_unidentified := detect_screen_object(ScreenObjects.Unidentified, item_box.data).valid):
if (is_unidentified := is_visible(ScreenObjects.Unidentified, item_box.data)):
need_id = True
center_mouse()
tome_state, tome_pos = common.tome_state(grab(), tome_type = "id", roi = specific_inventory_roi("reserved"))
tome_state, tome_pos = common.tome_state(grab(), tome_type = "id", roi = Config().ui_roi["restricted_inventory_area"])
if is_unidentified and tome_state is not None and tome_state == "ok":
common.id_item_with_tome([x_m, y_m], tome_pos)
need_id = False
@@ -378,7 +378,7 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
if keep:
sell = need_id = False
box = common.BoxInfo(
box = BoxInfo(
img = item_box.data,
pos = (x_m, y_m),
column = slot[2],
@@ -391,10 +391,10 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
if not (keep or need_id) and vendor_open and item_can_be_traded:
Logger.debug(f"Selling {item_name}")
box.sell = True
common.transfer_items([box], action = "sell")
transfer_items([box], action = "sell")
continue
# if item is to be kept and is already ID'd or doesn't need ID, log and stash
if keep and not need_id:
if game_stats is not None and (keep and not need_id):
game_stats.log_item_keep(found_item.name, Config().items[found_item.name].pickit_type == 2, item_box.data, item_box.ocr_result.text)
# if item is to be kept or still needs to be sold or identified, append to list
if keep or sell or need_id:
@@ -403,7 +403,7 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
else:
# if item isn't going to be kept (or sold if vendor window not open), trash it
Logger.debug(f"Dropping {item_name}")
common.transfer_items([box], action = "drop")
transfer_items([box], action = "drop")
wait(0.3, 0.5)
else:
failed = True
@@ -414,5 +414,75 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
if Config().general["info_screenshots"]:
cv2.imwrite("./info_screenshots/failed_item_box_" + time.strftime("%Y%m%d_%H%M%S") + ".png", hovered_item)
if close_window:
if not is_visible(ScreenObjects.RightPanel, img):
center_mouse()
common.close()
return boxes
return boxes
def transfer_items(items: list, action: str = "drop", img: np.ndarray = None) -> list:
#requires open inventory / stash / vendor
img = img if img is not None else grab()
filtered = []
left_panel_open = is_visible(ScreenObjects.LeftPanel, img)
if action == "drop":
filtered = [ item for item in items if item.keep == False and item.sell == False ]
elif action == "sell":
filtered = [ item for item in items if item.keep == False and item.sell == True ]
if not left_panel_open:
Logger.error(f"transfer_items: Can't perform, vendor is not open")
elif action == "stash":
if is_visible(ScreenObjects.GoldBtnStash, img):
filtered = [ item for item in items if item.keep == True ]
else:
Logger.error(f"transfer_items: Can't perform, stash is not open")
else:
Logger.error(f"transfer_items: incorrect action param={action}")
if filtered:
# if dropping, control+click to drop unless left panel is open, then drag to middle
# if stashing, control+click to stash
# if selling, control+click to sell
if (action == "drop" and not left_panel_open) or action in ["sell", "stash"]:
keyboard.send('ctrl', do_release=False)
wait(0.1, 0.2)
for item in filtered:
pre_hover_img = grab()
_, slot_img = common.get_slot_pos_and_img(pre_hover_img, item.column, item.row)
if not common.slot_has_item(slot_img):
# item no longer exists in that position...
Logger.debug(f"Item at {item.pos} doesn't exist, skip and remove from list")
for cnt, o_item in enumerate(items):
if o_item.pos == item.pos:
items.pop(cnt)
break
continue
# move to item position and left click
mouse.move(*item.pos, randomize=4, delay_factor=[0.2, 0.4])
wait(0.2, 0.4)
pre_transfer_img = grab()
mouse.press(button="left")
# wait for inventory image to update indicating successful transfer / item select
success = wait_for_update(pre_transfer_img, Config().ui_roi["open_inventory_area"], 3)
mouse.release(button="left")
if not success:
Logger.warning(f"transfer_items: inventory unchanged after attempting to {action} item at position {item.pos}")
break
else:
# if dropping, drag item to middle if vendor/stash is open
if action == "drop" and left_panel_open:
center_mouse()
mouse.press(button="left")
wait(0.2, 0.3)
mouse.release(button="left")
# item successfully transferred, delete from list
Logger.debug(f"Confirmed {action} at position {item.pos}")
for cnt, o_item in enumerate(items):
if o_item.pos == item.pos:
items.pop(cnt)
break
if action == "sell":
# check and see if inventory gold count changed
if (gold_unchanged := not wait_for_update(pre_transfer_img, Config().ui_roi["inventory_gold_digits"], 3)):
Logger.info("Inventory gold is full, force stash")
set_inventory_gold_full(gold_unchanged)
keyboard.send('ctrl', do_press=False)
return items

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