Compare commits

..
40 Commits
Author SHA1 Message Date
aeon0 54af636d8f Doc formating 2021-11-07 21:50:19 +01:00
aeon0 41c05cc614 fix(path): Add another template to avoid getting stuck at shenk 2021-11-07 21:33:11 +01:00
aeon0 183bae8583 feat(release): Create empty custom.ini for each release 2021-11-07 21:25:25 +01:00
aeon0 6f190c8065 docs(md): Update docs 2021-11-07 21:22:18 +01:00
aeon0 6260a0ce30 fix(pickit): Workaround for pickup with flames, do shenk pickit twice 2021-11-07 21:07:53 +01:00
aeon0 35df3bae7b fix(move): Do not slow walk on teleport 2021-11-07 21:06:11 +01:00
aeon0 26c5ee9de5 add another template to avoid getting stuck 2021-11-07 21:04:53 +01:00
aeon0 1cfde7f560 fix(hammerdin): Recast in set intervall to come back from interrupt 2021-11-07 21:04:38 +01:00
aeon0 282f7a5606 fix(char): Remove useless import that might lead to issues in exe 2021-11-07 20:49:28 +01:00
aeon0 1a148afcfb Bump version to 0.1.3-rc0 2021-11-07 16:02:46 +01:00
aeon0 a91a91dc7c feat(path): Ref pathing shenk 2021-11-07 15:56:41 +01:00
aeon0 648d89ff28 feat(path): Refpathing to Eldritch 2021-11-07 13:52:18 +01:00
aeon0 489cc4ec36 feat(path): Refpoint pathing for pindle 2021-11-07 12:43:35 +01:00
aeon0 fddd72c3a5 Merge branch 'master' of https://github.com/aeon0/botty 2021-11-07 10:58:58 +01:00
aeon0 5abe2fb33e fix(death): Make sure keys are released when killing main bot thread 2021-11-07 10:57:20 +01:00
aeon0 df12dbd57c update docs 2021-11-07 01:39:37 +01:00
aeon0 9a3b054248 Merge pull request #3 from jagarop/master
add zstandard
2021-11-07 01:38:07 +01:00
aeon0 26246f141b Merge branch 'master' into master 2021-11-07 01:37:53 +01:00
aeon0 8d1f84062d doc(dev): Add note about user being a variable 2021-11-07 00:49:48 +01:00
aeon0 a7d31775f1 fix(start): Robustify game start with new disabled play button 2021-11-07 00:40:33 +01:00
jagarop 7c52b05d10 Update development.md 2021-11-06 20:09:59 -03:00
jagarop a7d7a96268 add zstandard 2021-11-06 20:06:29 -03:00
aeon0 8e48dfe3d0 Bump version to v0.1.3 2021-11-06 16:47:22 +01:00
aeon0 c89b35ff33 fix(ui): Game creation 2021-11-06 16:46:54 +01:00
aeon0 0d6ab59277 fix(bot): Reset of runs with specific shenk eldritch setup 2021-11-06 16:25:54 +01:00
aeon0 01bdc022ec fix(runtime): High cpu usage in idle cause of while loop 2021-11-06 15:11:23 +01:00
aeon0 06b0d4fb27 refactor(pixel): Remove last of hardcoded pixel values 2021-11-06 14:56:47 +01:00
aeon0 c751259319 feat(bot): More fail saves 2021-11-06 14:53:45 +01:00
aeon0 b1113b8f5a feat(shenk): Config for Eldritch only 2021-11-06 14:25:42 +01:00
aeon0 efced03d83 fix(ui): Handle grayed out play button 2021-11-06 14:05:32 +01:00
aeon0 e69a197135 Merge branch 'master' of https://github.com/aeon0/botty 2021-11-06 13:23:11 +01:00
aeon0 90869ebfbb refactor(botty): Add docs, clean up, put all hardcoded px values to .ini 2021-11-06 12:48:19 +01:00
aeon0 b9ccc733df doc(md): Add missing doc of slow_walk, custom.ini and note for conda path 2021-11-06 08:09:16 +01:00
aeon0 0635497a47 fix(stashing): Increase delays to avoid drag instead of stash 2021-11-06 07:56:58 +01:00
aeon0 8279332579 Merge pull request #2 from jagarop/master
Update development.md
2021-11-06 00:57:44 +01:00
aeon0 21dc086ebd feat(path): Add slow walk mode as backup 2021-11-06 00:55:29 +01:00
jagarop b80888aca6 Update development.md
missing command
2021-11-05 20:34:22 -03:00
aeon0 d59fbc0cd3 config(color): Increase range a bit and add note to readme about graphic card settings 2021-11-05 22:03:43 +01:00
aeon0 652823189c feat(config): Add custom config file 2021-11-05 21:59:17 +01:00
aeon0 1f1437ce1d doc(md): Add Download link a bit more obivous 2021-11-05 14:43:37 +01:00
74 changed files with 657 additions and 376 deletions
+1
View File
@@ -12,3 +12,4 @@ run.dist/
run.onefile-build/
generated/
botty_v*/
custom.ini
+22 -8
View File
@@ -1,19 +1,19 @@
# <img src="assets/docs/header_green.png" width="370">
Simple Pixelbot for Diablo 2 Resurrected written in python and opencv. Obviously only use it in offline mode as it is against the TOS of Blizzard to use it in online mode!
Simple Pixelbot for Diablo 2 Resurrected written in python and opencv. Obviously only use it in offline mode as it is against the TOS of Blizzard to use it in online mode! [**Download here**](https://github.com/aeon0/botty/releases)
Join the [**Discord Channel**](https://discord.gg/CnkfsjqN) for help and discussions.
[![Watch the video](assets/docs/video_thumbnail.png)](https://vimeo.com/641410429)
## Supported features
## What it does
- Run Pindle, Eldtritch, Shenk
- Pickit with per item config.
- Pickit with per item config
- Stash picked up items (using all 4 stashes)
- Prebuff
- Revive Merc if dead
- Heal at Malah if needed
- Take potions and chicken if in trouble during fights
- Take potions (health and mana) and chicken if in trouble during fights
- Check for death. In this case start another game and pick up corpse.
- Supported builds: Sorceress (Blizz, Light, Meteor), Hammerdin
- Debug color checker to easily verify your settings
@@ -28,6 +28,8 @@ All settings will automatically be set when you execute `run.exe` and press the
- Item Label Display: Hold
- Large Font Mode: on
**Note**: There have been issues reported with image sharpening being truned on via the graphic card settings itself outside of D2R. Try turning it off when running the bot.
### 2) Supported builds
#### Soceress
You can put any skills on left and right attack and see if it works out. E.g. Glacial Spike on left attack and Blizzard or right attack.
@@ -41,10 +43,25 @@ Open up D2R and wait till you are at the hero selection screen. Make sure the ch
### 5) Start Botty
Download the a prebuilt release [here](https://github.com/aeon0/botty/releases). Start `run.exe` in the botty folder, go to D2R window and press the "resume" key. Move to your D2R window and press "f11". You can always force stop botty with f12.
## Color Test Mode
To check if you graphic settings are good and if the bot would pick up items there is a **Color Test Mode** built in. Start botty and press F10 (Default key). This will open up a (mostly black) window. Start a game in D2R and throw some items of different type on the ground. If you now bring forward the debug window all items should show up with their names while the background is black. If you throw an item on the ground that should be picked up, it will have a red circle.</br>
<img src="assets/docs/color_checker.png" width="400">
## Development
Check out the [development.md](development.md) docu for infos on how to build from source and details of the project structure and code.
## param.ini
To ease the switch to new botty versions, you can also overwrite any of the param.ini fields in a __custom.ini__ file. When a new version of botty is released you just copy the file to the new version without having to port all your __param.ini__ changes to the new version. Example:
```ini
; custom.ini - overwrites 3 params in the param.ini
[general]
monitor=1
[routes]
run_pindle=1
run_shenk=0
```
[general] | Descriptions
--------------------------------|---------------------------------------------
monitor | Select on which monitor D2R is running in case multiple are available
@@ -62,6 +79,7 @@ run_shenk | Run shenk in each new game. Select "1" to run it "0" to leave it out
--------------------------------|---------------------------------------------
type | Build type. Currently only "sorceress" or "hammerdin" is supported
casting_frames | Depending on your char and fcr you will have a specific casting frame count. Check it here: https://diablo2.diablowiki.net/Breakpoints and fill in the right number. Determines how much delay there is after each teleport for example.
slow_walk | With this set to 1 the char will have a large delay for each running action in town. Set this to 1 if you keep getting stuck during traversing town.
num_loot_columns | Number of columns in inventory used for loot (from left!). Remaining space can be used for charms
take_health_potion | Health percentage when healing potion will be used
take_mana_potion | Mana percentage when mana potion will be used. Currently belt managment is not very clever and it is safest to only pick up health pots and make sure mana reg is enough for pindle to not need mana pots.
@@ -100,9 +118,5 @@ blessed_hammer | Hotkey for Blessed Hammer
--------------------------------|---------------------------------------------
item_type | Select "1" if item should be picked up, "0" if not.
## Some distant future plans
- Running chaos with AI algo
- Emulating Keyboard and Mouse with Arduino
## Support this project
This project is free. Support it by contributing in any technical way, giving feedback, PRs or by submitting issues. That being said, I am not above accepting some pixel currency :) So if you want to send some fg my way to keep my dopamine high, here is my jsp: [jojo22](https://forums.d2jsp.org/user.php?i=768967).
Binary file not shown.

After

Width:  |  Height:  |  Size: 930 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

+12 -3
View File
@@ -1,7 +1,7 @@
# Dev Docu
## Dependencies
- Install miniconda (https://docs.conda.io/en/latest/miniconda.html)
- Install miniconda (https://docs.conda.io/en/latest/miniconda.html). Note: You will have to check "Add conda to my PATH environment variable" in order to access the conda command in the cmd.
- Install git (https://gitforwindows.org/)
## Getting started
@@ -9,16 +9,22 @@
git clone https://github.com/aeon0/botty.git
cd botty
conda env create environment.yml
conda activate botty
python src/run.py
```
## Building from source
If you want to build a .exe from source you will have to first add the cv2 path to your PYTHONPATH:</br>
Edit System environment variables -> Enfironment Vriables... -> PYTHONPATH -> C:\Users\$USER\miniconda3\envs\botty\lib\site-packages\cv2
If you want to build a .exe from source you will have to first add the cv2 path to your PYTHONPATH in System variables or User variables for current user:</br>
Edit the system environment variables -> Environment Variables... -> New... </br>
Variable name: PYTHONPATH </br>
Variable value: C:\Users\YOUR_WINDOWS_USER_NAME\miniconda3\envs\botty\Lib\site-packages\cv2 </br>
If you installed your miniconda in another location you will of course have ot change it for that one.
```python
# building .exe and bundeling all needed resource into one folder
python release.py
```
For changelog run: `git log <PREVIOUS_TAG>..HEAD --oneline --decorate`
## Folder Structure
**/**</br>
@@ -42,6 +48,9 @@ Want to implement a new char or build. Check this folder out. You will have to i
**src/char**</br>
Utilities functions and scripts e.g. for easily creating templates to traverse nodes and automatically generate code for it</br>
## Adding Items
To add items you can check the **assets/items** folder. Screenshot whatever you want to pick up in the same way (all settings must be as if you ran the bot). Then add the filename to the param.ini [items] section (e.g. if boots_rare.png add boots_rare=1)
## State Diagram
The core logic of the bot is determined by a state machine with these states and transations. The bot.py which contains all of the transitions should have little implementation code which should be hidden as much as possible in the "manager" classes.
<img src="assets/docs/state_diagram.png" width="550"/>
+1
View File
@@ -14,3 +14,4 @@ dependencies:
- keyboard
- graphviz
- beautifultable
- zstandard
+2
View File
@@ -8,11 +8,13 @@ color_checker_key=f10
[routes]
run_pindle=1
run_eldritch=1
run_shenk=1
[char]
type=hammerdin
casting_frames=11
slow_walk=0
num_loot_columns=6
take_health_potion=0.8
take_mana_potion=0.6
+3
View File
@@ -34,6 +34,9 @@ installer_cmd = f"python -m nuitka --mingw --standalone --onefile --plugin-enabl
os.system(installer_cmd)
os.system(f"mkdir {botty_dir}")
with open(f"{botty_dir}/custom.ini", "w") as f:
f.write("; Add parameters you want to overwrite from param.ini here")
shutil.copy("run.exe", f"{botty_dir}/")
shutil.copy("ui.ini", f"{botty_dir}/")
shutil.copy("params.ini", f"{botty_dir}/")
+41 -31
View File
@@ -34,16 +34,19 @@ class Bot:
self._npc_manager = NpcManager(self._screen, self._template_finder)
self._pickit = PickIt(self._screen, self._item_finder, self._ui_manager)
if self._config.char["type"] == "sorceress":
self._char: IChar = Sorceress(self._config.sorceress, self._config.char, self._screen, self._template_finder, self._item_finder, self._ui_manager)
self._char: IChar = Sorceress(self._config.sorceress, self._config.char, self._screen, self._template_finder, self._ui_manager, self._pather)
elif self._config.char["type"] == "hammerdin":
self._char: IChar = Hammerdin(self._config.hammerdin, self._config.char, self._screen, self._template_finder, self._item_finder, self._ui_manager)
self._char: IChar = Hammerdin(self._config.hammerdin, self._config.char, self._screen, self._template_finder, self._ui_manager, self._pather)
else:
Logger.error(f'{self._config.char["type"]} is not supported! Closing down bot.')
os._exit(1)
self._route_config = self._config.routes
if self._route_config["run_shenk"] and not self._route_config["run_eldritch"]:
Logger.error("Running shenk without eldtritch is not supported. Either run none, both or eldritch only.")
os._exit(1)
self._do_runs = {
"run_pindle": self._route_config["run_pindle"],
"run_shenk": self._route_config["run_shenk"]
"run_shenk": self._route_config["run_shenk"] or self._route_config["run_eldritch"]
}
self._picked_up_items = False
self._tp_is_up = False
@@ -89,16 +92,18 @@ class Bot:
wait(delay, delay + 5.0)
Logger.info("Start new game")
self._timer = time.time()
found, _ = self._template_finder.search_and_wait("D2_LOGO_HS", time_out=100)
found, _ = self._template_finder.search_and_wait("D2_LOGO_HS", time_out=70)
if not found:
Logger.error("Something went wrong here, probably died. Exit game and closing down bot.")
Logger.error("Something went wrong here, bot is unsure about current location. Exit game and closing down bot.")
self._ui_manager.save_and_exit()
os._exit(1)
self._ui_manager.start_hell_game()
self._template_finder.search_and_wait("A5_TOWN_1")
self._tp_is_up = False
self._curr_location = Location.A5_TOWN_START
keyboard.send(self._config.char["stand_still"]) # just in case the key is still locked
# Make sure these keys are released
keyboard.release(self._config.char["stand_still"])
keyboard.release(self._config.char["show_items"])
self.trigger("maintenance")
def on_maintenance(self):
@@ -117,14 +122,12 @@ class Bot:
# TODO: If tp is up we always go back into the portal...
if not self._tp_is_up and (self._health_manager.get_health(img) < 0.6 or self._health_manager.get_mana(img) < 0.3):
Logger.info("Need some healing first. Go talk to Malah")
success = self._pather.traverse_nodes(self._curr_location, Location.MALAH, self._char)
if not success:
if not self._pather.traverse_nodes(self._curr_location, Location.MALAH, self._char):
self.trigger("end_game")
return
self._curr_location = Location.MALAH
self._npc_manager.open_npc_menu(Npc.MALAH)
success = self._pather.traverse_nodes(self._curr_location, Location.A5_TOWN_START, self._char)
if not success:
if not self._pather.traverse_nodes(self._curr_location, Location.A5_TOWN_START, self._char):
self.trigger("end_game")
return
self._curr_location = Location.A5_TOWN_START
@@ -132,15 +135,13 @@ class Bot:
# Stash stuff
if self._picked_up_items:
Logger.info("Stashing picked up items")
success = self._pather.traverse_nodes(self._curr_location, Location.A5_STASH, self._char)
if not success:
if not self._pather.traverse_nodes(self._curr_location, Location.A5_STASH, self._char):
self.trigger("end_game")
return
self._curr_location = Location.A5_STASH
time.sleep(1.5)
# sometimes waypoint is opened and stash not found because of that, check for that
found, _ = self._template_finder.search("WAYPOINT_MENU", self._screen.grab())
if found:
if self._template_finder.search("WAYPOINT_MENU", self._screen.grab())[0]:
keyboard.send("esc")
if self._char.select_by_template("A5_STASH"):
self._ui_manager.stash_all_items(self._config.char["num_loot_columns"])
@@ -153,13 +154,12 @@ class Bot:
merc_alive, _ = self._template_finder.search("MERC", self._screen.grab(), threshold=0.9, roi=[0, 0, 200, 200])
if not merc_alive:
Logger.info("Reviveing merc")
success = self._pather.traverse_nodes(self._curr_location, Location.QUAL_KEHK, self._char)
if not success:
if not self._pather.traverse_nodes(self._curr_location, Location.QUAL_KEHK, self._char):
self.trigger("end_game")
return
self._curr_location = Location.QUAL_KEHK
self._npc_manager.open_npc_menu(Npc.QUAL_KEHK)
self._npc_manager.press_npc_btn(Npc.QUAL_KEHK, "resurrect")
if self._npc_manager.open_npc_menu(Npc.QUAL_KEHK):
self._npc_manager.press_npc_btn(Npc.QUAL_KEHK, "resurrect")
time.sleep(2)
# Start a new run
@@ -205,13 +205,13 @@ class Bot:
bot._curr_location = Location.NIHLATHAK_PORTAL
wait(0.2, 0.4)
self.success &= bot._char.select_by_template("A5_RED_PORTAL")
self.success &= bot._template_finder.search_and_wait("PINDLE_STONE", time_out=15)[0]
self.success &= bot._template_finder.search_and_wait("PINDLE_0", time_out=20)[0]
if not self.success:
return
bot._char.pre_buff()
wait(0.2, 0.4)
bot._pather.traverse_nodes_fixed("PINDLE", bot._char)
bot._char.kill_pindle(bot._pather.get_fixed_path("PINDLE")[1])
bot._pather.traverse_nodes(Location.PINDLE_START, Location.PINDLE_SAVE_DIST, bot._char)
bot._char.kill_pindle()
wait(1.5, 1.8)
bot._picked_up_items = bot._pickit.pick_up_items(bot._char)
# in order to move away for items and such to have a clear tp, move to the end of the hall
@@ -235,19 +235,27 @@ class Bot:
bot._char.select_by_template("A5_WP")
wait(1.0)
bot._ui_manager.use_wp(4, 1)
bot._template_finder.search_and_wait("SHENK_FLAME")
self.success = bot._template_finder.search_and_wait("ELDRITCH_0", time_out=20)[0]
if not self.success:
return
bot._char.pre_buff()
# eldritch
bot._pather.traverse_nodes_fixed("ELDRITCH", bot._char)
bot._char.kill_eldritch(bot._pather.get_fixed_path("ELDRITCH")[1])
bot._pather.traverse_nodes(Location.ELDRITCH_START, Location.ELDRITCH_SAVE_DIST, bot._char)
bot._char.kill_eldritch()
wait(0.5)
bot._picked_up_items = bot._pickit.pick_up_items(bot._char)
# shenk
bot._pather.traverse_nodes_fixed("SHENK", bot._char)
wait(0.15, 0.2)
bot._char.kill_shenk(bot._pather.get_fixed_path("SHENK")[1])
wait(0.5)
bot._picked_up_items |= bot._pickit.pick_up_items(bot._char)
if bot._route_config["run_shenk"]:
self.success = bot._pather.traverse_nodes(Location.SHENK_START, Location.SHENK_SAVE_DIST, bot._char)
if not self.success:
return
wait(0.15, 0.2)
bot._char.kill_shenk()
wait(0.5)
# do two pickups on shenk because flames sometimes mess up item search
bot._picked_up_items |= bot._pickit.pick_up_items(bot._char)
wait(1.5, 1.8)
bot._picked_up_items |= bot._pickit.pick_up_items(bot._char)
self.success = True
return
run = RunShenk()
@@ -278,8 +286,10 @@ class Bot:
if self._timer is not None:
elapsed_time = time.time() - self._timer
Logger.info(f"End game. Elapsed time: {elapsed_time:.2f}s")
for key in self._do_runs:
self._do_runs[key] = self._route_config[key]
self._do_runs = {
"run_pindle": self._route_config["run_pindle"],
"run_shenk": self._route_config["run_shenk"] or self._route_config["run_eldritch"]
}
wait(0.2, 0.5)
self.trigger("create_game")
+24 -25
View File
@@ -1,22 +1,21 @@
import keyboard
import mouse
from utils import custom_mouse
from char.i_char import IChar
from template_finder import TemplateFinder
from item_finder import ItemFinder
from ui_manager import UiManager
from pather import Pather
from logger import Logger
from screen import Screen
from utils.misc import wait
import random
import time
from pather import Pather, Location
class Hammerdin(IChar):
def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, item_finder: ItemFinder, ui_manager: UiManager):
def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, ui_manager: UiManager, pather: Pather):
Logger.info("Setting up Hammerdin")
super().__init__(skill_hotkeys, char_config, screen, template_finder, item_finder, ui_manager)
super().__init__(skill_hotkeys, char_config, screen, template_finder, ui_manager)
self._pather = pather
def pre_buff(self):
keyboard.send(self._skill_hotkeys["holy_shield"])
@@ -37,7 +36,7 @@ class Hammerdin(IChar):
keyboard.send(self._char_config["weapon_switch"])
wait(0.25, 0.3)
def _cast_hammers(self, time_in_s):
def _cast_hammers(self, time_in_s: float):
keyboard.send(self._char_config["stand_still"], do_release=False)
wait(0.05)
keyboard.send(self._skill_hotkeys["blessed_hammer"])
@@ -46,8 +45,14 @@ class Hammerdin(IChar):
wait(0.05, 0.1)
mouse.press(button="left")
start = time.time()
i = 0
while (time.time() - start) < time_in_s:
time.sleep(0.05)
wait(0.04, 0.06)
i += 1
if i % 20 == 0:
mouse.release(button="left")
time.sleep(0.01)
mouse.press(button="left")
mouse.release(button="left")
keyboard.send(self._char_config["stand_still"], do_press=False)
@@ -55,33 +60,27 @@ class Hammerdin(IChar):
keyboard.send(self._skill_hotkeys["redemption"])
wait(1.5, 2.0)
def kill_pindle(self, pindle_pos_screen):
pos_monitor = self._screen.convert_screen_to_monitor(pindle_pos_screen)
keyboard.send(self._skill_hotkeys["teleport"])
custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15))
mouse.click(button="right")
def kill_pindle(self) -> bool:
wait(0.1, 0.15)
self._cast_hammers(7)
self._pather.traverse_nodes(Location.PINDLE_SAVE_DIST, Location.PINDLE_END, self, time_out=2.0)
self._cast_hammers(1)
# pindle sometimes knocks back, get back in
self._pather.traverse_nodes(Location.PINDLE_SAVE_DIST, Location.PINDLE_END, self, time_out=0.2)
self._cast_hammers(6)
wait(0.1, 0.15)
self._do_redemption()
return True
def kill_shenk(self, shenk_pos_screen):
pos_monitor = self._screen.convert_screen_to_monitor(shenk_pos_screen)
keyboard.send(self._skill_hotkeys["teleport"])
wait(0.05)
custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15))
wait(0.02)
mouse.click(button="right")
def kill_eldritch(self) -> bool:
self._pather.traverse_nodes(Location.ELDRITCH_SAVE_DIST, Location.ELDRITCH_END, self, time_out=0.2)
wait(0.1, 0.15)
self._cast_hammers(6)
wait(0.1, 0.15)
self._do_redemption()
return True
def kill_eldritch(self, eldritch_pos_screen):
pos_monitor = self._screen.convert_screen_to_monitor(eldritch_pos_screen)
keyboard.send(self._skill_hotkeys["teleport"])
custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15))
mouse.click(button="right")
def kill_shenk(self):
self._pather.traverse_nodes(Location.SHENK_SAVE_DIST, Location.SHENK_END, self, time_out=0.2)
wait(0.1, 0.15)
self._cast_hammers(6)
wait(0.1, 0.15)
+21 -24
View File
@@ -1,9 +1,7 @@
from typing import Tuple
from cv2 import threshold
import mouse
from utils import custom_mouse
from template_finder import TemplateFinder
from item_finder import ItemFinder
from ui_manager import UiManager
from screen import Screen
from utils.misc import wait
@@ -12,6 +10,8 @@ import math
import keyboard
from logger import Logger
import time
from typing import Dict, Tuple
from config import Config
def abstract(f):
@@ -20,56 +20,56 @@ def abstract(f):
return _decorator
class IChar:
def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, item_finder: ItemFinder, ui_manager: UiManager):
def __init__(self, skill_hotkeys: Dict, char_config: Dict, screen: Screen, template_finder: TemplateFinder, ui_manager: UiManager):
self._skill_hotkeys = skill_hotkeys
self._char_config = char_config
self._template_finder = template_finder
self._item_finder = item_finder
self._ui_manager = ui_manager
self._screen = screen
self._config = Config()
self._last_tp = time.time()
self._cast_duration = self._char_config["casting_frames"] * 0.04 + 0.02
def select_by_template(self, template_type: str) -> bool:
Logger.debug(f"Select {template_type}")
success, screen_loc = self._template_finder.search_and_wait(template_type, time_out=10)
if success:
x_m, y_m = self._screen.convert_screen_to_monitor(screen_loc)
# TODO: check if telekinse can be used
custom_mouse.move(x_m, y_m, duration=(random.random() * 0.1 + 0.2))
wait(0.1, 0.2)
custom_mouse.move(x_m, y_m, duration=0.3)
wait(0.3, 0.4)
mouse.click(button="left")
return True
return False
def move(self, pos_monitor):
def move(self, pos_monitor: Tuple[float, float]):
if not self._ui_manager.is_teleport_selected():
keyboard.send(self._skill_hotkeys["teleport"])
wait(0.1, 0.2)
if self._ui_manager.can_teleport():
custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.01 + 0.06))
mouse.click(button="right")
wait_min = self._char_config["casting_frames"] * 0.036
wait(wait_min, wait_min + 0.02)
time.sleep(self._cast_duration)
else:
# in case we want to walk we actually want to move a bit before the point cause d2r will always "overwalk"
pos_screen = self._screen.convert_monitor_to_screen(pos_monitor)
pos_abs = self._screen.convert_screen_to_abs(pos_screen)
dist = math.dist(pos_abs, (0, 0))
adjust_factor = (dist - 60) / dist
adjust_factor = (dist - 50) / dist
pos_abs = [int(pos_abs[0] * adjust_factor), int(pos_abs[1] * adjust_factor)]
x, y = self._screen.convert_abs_to_monitor(pos_abs)
custom_mouse.move(x, y, duration=(random.random() * 0.03 + 0.05))
mouse.click(button="left")
wait(0.02, 0.03)
if self._config.char["slow_walk"]:
wait(1.2, 1.4)
def tp_town(self):
# TODO: Make robust against no more tps
keyboard.send(self._char_config["tp"])
wait(0.05, 0.1)
mouse.click(button="right")
custom_mouse.move(1400 + random.randrange(0, 60), 300 + random.randrange(0, 60), duration=0.1)
time.sleep(1.4) # takes quite a while for tp to be visible
# TODO: 1920x1080 specific params
roi=[530, 180, 820, 600]
roi = self._config.ui_roi["tp_search"]
start = time.time()
while (time.time() - start) < 7:
img = self._screen.grab()
@@ -81,14 +81,11 @@ class IChar:
x, y = self._screen.convert_screen_to_monitor(pos)
# Note: Template is top of portal, thus move the y-position a bit to the bottom
# Also move a bit left and right to get rid of possibly highlight other things such as items
custom_mouse.move(x - 20 * random.random() * 3, y + random.random() * 5, duration=0.1)
custom_mouse.move(x + 20 * random.random() * 3, y + random.random() * 5, duration=0.02)
custom_mouse.move(x + random.random() * 3, y + random.random() * 5, duration=0.02)
wait(0.09, 0.11)
custom_mouse.move(x - 20, y, duration=0.13, randomize=5)
custom_mouse.move(x + 20, y, duration=0.05, randomize=5)
custom_mouse.move(x, y, duration=0.05, randomize=5)
wait(0.1, 0.14)
mouse.click(button="left")
# # we sometimes pick up items instead... just to be safe
# wait(0.09, 0.11)
# mouse.click(button="left")
return True
return False
@@ -97,13 +94,13 @@ class IChar:
pass
@abstract
def kill_pindle(self, pindle_pos_screen):
def kill_pindle(self) -> bool:
pass
@abstract
def kill_shenk(self, shenk_pos_screen):
def kill_shenk(self) -> bool:
pass
@abstract
def kill_eldritch(self, eldritch_pos_screen):
def kill_eldritch(self) -> bool:
pass
+43 -43
View File
@@ -10,12 +10,15 @@ from logger import Logger
from screen import Screen
from utils.misc import wait
import random
from typing import Tuple
from pather import Location, Pather
class Sorceress(IChar):
def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, item_finder: ItemFinder, ui_manager: UiManager):
def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, ui_manager: UiManager, pather: Pather):
Logger.info("Setting up Sorceress")
super().__init__(skill_hotkeys, char_config, screen, template_finder, item_finder, ui_manager)
super().__init__(skill_hotkeys, char_config, screen, template_finder, ui_manager)
self._pather = pather
def pre_buff(self):
keyboard.send(self._skill_hotkeys["frozen_armor"])
@@ -36,7 +39,7 @@ class Sorceress(IChar):
keyboard.send(self._char_config["weapon_switch"])
wait(0.25, 0.3)
def _left_attack(self, cast_pos, delay, spray = 10):
def _left_attack(self, cast_pos: Tuple[float, float], delay: float, spray: int = 10):
keyboard.send(self._char_config["stand_still"], do_release=False)
custom_mouse.move(cast_pos[0], cast_pos[1], duration=(random.random() * 0.05 + 0.15))
keyboard.send(self._skill_hotkeys["skill_left"])
@@ -48,7 +51,7 @@ class Sorceress(IChar):
wait(delay[0], delay[1])
keyboard.send(self._char_config["stand_still"], do_press=False)
def _main_attack(self, cast_pos, delay, spray = 10):
def _main_attack(self, cast_pos: Tuple[float, float], delay: float, spray: float = 10):
keyboard.send(self._skill_hotkeys["skill_right"])
x = cast_pos[0] + (random.random() * 2*spray - spray)
y = cast_pos[1] + (random.random() * 2*spray - spray)
@@ -56,31 +59,30 @@ class Sorceress(IChar):
mouse.click(button="right")
wait(delay[0], delay[1])
def kill_pindle(self, pindle_pos_screen):
def kill_pindle(self) -> bool:
delay = [0.2, 0.3]
pindle_pos_abs = self._screen.convert_screen_to_abs(pindle_pos_screen)
cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9]
cast_pos_monitor = self._screen.convert_abs_to_monitor(cast_pos_abs)
pindle_pos_monitor = self._screen.convert_screen_to_monitor(pindle_pos_screen)
self._main_attack(cast_pos_monitor, delay)
self._left_attack(cast_pos_monitor, delay)
self._main_attack(cast_pos_monitor, delay)
self._left_attack(cast_pos_monitor, delay)
wait(0.1, 0.15)
# Move to items
keyboard.send(self._skill_hotkeys["teleport"])
custom_mouse.move(pindle_pos_monitor[0], pindle_pos_monitor[1], duration=(random.random() * 0.05 + 0.15))
mouse.click(button="right")
wait(0.1, 0.15)
blizzard_cast_pos = self._screen.convert_abs_to_monitor([0, 0])
self._main_attack(blizzard_cast_pos, delay)
pindle_pos_abs = self._pather.find_abs_node_pos(104, self._screen.grab())
if pindle_pos_abs is not None:
cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9]
cast_pos_monitor = self._screen.convert_abs_to_monitor(cast_pos_abs)
self._main_attack(cast_pos_monitor, delay)
self._left_attack(cast_pos_monitor, delay)
self._main_attack(cast_pos_monitor, delay)
self._left_attack(cast_pos_monitor, delay)
wait(0.1, 0.15)
# Move to items
self._pather.traverse_nodes(Location.PINDLE_SAVE_DIST, Location.PINDLE_END, self)
wait(0.1, 0.15)
blizzard_cast_pos = self._screen.convert_abs_to_monitor([0, 0])
self._main_attack(blizzard_cast_pos, delay)
return True
return False
def kill_shenk(self, shenk_pos_screen):
def kill_shenk(self):
delay = [0.2, 0.3]
pos_abs = self._screen.convert_screen_to_abs(shenk_pos_screen)
pos_abs = self._pather.find_abs_node_pos(149, self._screen.grab())
cast_pos_abs = [pos_abs[0] * 0.9, pos_abs[1] * 0.9]
cast_pos_monitor = self._screen.convert_abs_to_monitor(cast_pos_abs)
pos_monitor = self._screen.convert_screen_to_monitor(shenk_pos_screen)
# TODO: Not sure if we need so much attacks... maybe add a "number_attack_sequenze" to the param.ini
self._main_attack(cast_pos_monitor, delay, 90)
self._left_attack(cast_pos_monitor, delay, 90)
@@ -91,26 +93,24 @@ class Sorceress(IChar):
self._left_attack(cast_pos_monitor, delay, 90)
wait(0.2, 0.3)
# Move to items
keyboard.send(self._skill_hotkeys["teleport"])
custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15))
mouse.click(button="right")
self._pather.traverse_nodes(Location.SHENK_SAVE_DIST, Location.SHENK_END, self, time_out=2.0)
def kill_eldritch(self, eldritch_pos_screen):
def kill_eldritch(self) -> bool:
delay = [0.2, 0.3]
pos_abs = self._screen.convert_screen_to_abs(eldritch_pos_screen)
cast_pos_abs = [pos_abs[0] * 0.9, pos_abs[1] * 0.9]
cast_pos_monitor = self._screen.convert_abs_to_monitor(cast_pos_abs)
pos_monitor = self._screen.convert_screen_to_monitor(eldritch_pos_screen)
self._main_attack(cast_pos_monitor, delay, 90)
self._left_attack(cast_pos_monitor, delay, 90)
self._main_attack(cast_pos_monitor, delay, 90)
self._main_attack(cast_pos_monitor, delay, 90)
self._left_attack(cast_pos_monitor, delay, 90)
wait(0.2, 0.3)
# Move to items
keyboard.send(self._skill_hotkeys["teleport"])
custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15))
mouse.click(button="right")
pos_abs = self._pather.find_abs_node_pos(123, self._screen.grab())
if pos_abs is not None:
cast_pos_abs = [pos_abs[0] * 0.9, pos_abs[1] * 0.9]
cast_pos_monitor = self._screen.convert_abs_to_monitor(cast_pos_abs)
self._main_attack(cast_pos_monitor, delay, 90)
self._left_attack(cast_pos_monitor, delay, 90)
self._main_attack(cast_pos_monitor, delay, 90)
self._main_attack(cast_pos_monitor, delay, 90)
self._left_attack(cast_pos_monitor, delay, 90)
wait(0.2, 0.3)
# Move to items
self._pather.traverse_nodes(Location.ELDRITCH_SAVE_DIST, Location.ELDRITCH_END, self, time_out=2.0)
return True
return False
if __name__ == "__main__":
@@ -126,6 +126,6 @@ if __name__ == "__main__":
t_finder = TemplateFinder(screen)
pather = Pather(screen, t_finder)
ui_manager = UiManager(screen, t_finder)
char = Sorceress(config.sorceress, config.char, screen, t_finder, None, ui_manager)
char = Sorceress(config.sorceress, config.char, screen, t_finder, ui_manager)
# char.pre_buff()
char.tp_town()
+65 -42
View File
@@ -1,64 +1,87 @@
import configparser
import numpy as np
import os
class Config:
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]
else:
return self._ui_config[section][key]
def __init__(self):
config = configparser.ConfigParser()
config.read('params.ini')
ui_config = configparser.ConfigParser()
ui_config.read('ui.ini')
self._config = configparser.ConfigParser()
self._config.read('params.ini')
self._ui_config = configparser.ConfigParser()
self._ui_config.read('ui.ini')
self._custom = configparser.ConfigParser()
if os.path.exists('custom.ini'):
self._custom.read('custom.ini')
self.general = {
"monitor": int(config["general"]["monitor"]),
"min_game_length_s": float(config["general"]["min_game_length_s"]),
"exit_key": config["general"]["exit_key"],
"resume_key": config["general"]["resume_key"],
"auto_settings_key": config["general"]["auto_settings_key"],
"color_checker_key": config["general"]["color_checker_key"],
}
self.routes = {
"run_pindle": bool(int(config["routes"]["run_pindle"])),
"run_shenk": bool(int(config["routes"]["run_shenk"])) if "run_shenk" in config["routes"] else False,
"monitor": int(self._select_val("general", "monitor")),
"min_game_length_s": float(self._select_val("general", "min_game_length_s")),
"exit_key": self._select_val("general", "exit_key"),
"resume_key": self._select_val("general", "resume_key"),
"auto_settings_key": self._select_val("general", "auto_settings_key"),
"color_checker_key": self._select_val("general", "color_checker_key"),
}
self.routes = {}
for key in self._config["routes"]:
self.routes[key] = bool(int(self._select_val("routes", key)))
self.char = {
"type": config["char"]["type"],
"show_items": config["char"]["show_items"],
"inventory_screen": config["char"]["inventory_screen"],
"stand_still": config["char"]["stand_still"],
"num_loot_columns": int(config["char"]["num_loot_columns"]),
"take_health_potion": float(config["char"]["take_health_potion"]),
"take_mana_potion": float(config["char"]["take_mana_potion"]),
"heal_merc": float(config["char"]["heal_merc"]),
"chicken": float(config["char"]["chicken"]),
"tp": config["char"]["tp"],
"show_belt": config["char"]["show_belt"],
"potion1": config["char"]["potion1"],
"potion2": config["char"]["potion2"],
"potion3": config["char"]["potion3"],
"potion4": config["char"]["potion4"],
"cta_available": bool(int(config["char"]["cta_available"])),
"weapon_switch": config["char"]["weapon_switch"],
"battle_orders": config["char"]["battle_orders"],
"battle_command": config["char"]["battle_command"],
"casting_frames": int(config["char"]["casting_frames"])
"type": self._select_val("char", "type"),
"show_items": self._select_val("char", "show_items"),
"inventory_screen": self._select_val("char", "inventory_screen"),
"stand_still": self._select_val("char", "stand_still"),
"num_loot_columns": int(self._select_val("char", "num_loot_columns")),
"take_health_potion": float(self._select_val("char", "take_health_potion")),
"take_mana_potion": float(self._select_val("char", "take_mana_potion")),
"heal_merc": float(self._select_val("char", "heal_merc")),
"chicken": float(self._select_val("char", "chicken")),
"tp": self._select_val("char", "tp"),
"show_belt": self._select_val("char", "show_belt"),
"potion1": self._select_val("char", "potion1"),
"potion2": self._select_val("char", "potion2"),
"potion3": self._select_val("char", "potion3"),
"potion4": self._select_val("char", "potion4"),
"cta_available": bool(int(self._select_val("char", "cta_available"))),
"weapon_switch": self._select_val("char", "weapon_switch"),
"battle_orders": self._select_val("char", "battle_orders"),
"battle_command": self._select_val("char", "battle_command"),
"casting_frames": int(self._select_val("char", "casting_frames")),
"slow_walk": bool(int(self._select_val("char", "slow_walk"))),
}
self.sorceress = config["sorceress"]
self.sorceress = dict(self._config["sorceress"])
if "sorceress" in self._custom:
self.sorceress.update(dict(self._custom["sorceress"]))
self.hammerdin = config["hammerdin"]
self.hammerdin = self._config["hammerdin"]
if "hammerdin" in self._custom:
self.hammerdin.update(self._custom["hammerdin"])
self.items = {}
for key in config["items"]:
self.items[key] = bool(int(config["items"][key]))
for key in self._config["items"]:
self.items[key] = bool(int(self._select_val("items", key)))
self.colors = {}
for key in ui_config["colors"]:
self.colors[key] = np.split(np.array([int(x) for x in ui_config["colors"][key].split(",")]), 2)
for key in self._ui_config["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 ui_config["ui_pos_1920_1080"]:
self.ui_pos[key] = int(ui_config["ui_pos_1920_1080"][key])
for key in self._ui_config["ui_pos_1920_1080"]:
self.ui_pos[key] = int(self._select_val("ui_pos_1920_1080", key))
self.ui_roi = {}
for key in self._ui_config["ui_roi_1920_1080"]:
self.ui_roi[key] = np.array([int(x) for x in self._select_val("ui_roi_1920_1080", key).split(",")])
if __name__ == "__main__":
config = Config()
+6 -3
View File
@@ -9,6 +9,7 @@ import cv2
from logger import Logger
import time
import random
from threading import Thread
class DeathManager:
@@ -17,7 +18,6 @@ class DeathManager:
self._screen = screen
self._template_finder = template_finder
_, self._you_have_died_filtered = color_filter(cv2.imread("assets/templates/you_have_died.png"), self._config.colors["red"])
self._search_roi = [self._config.ui_pos["death_roi_left"], self._config.ui_pos["death_roi_top"], self._config.ui_pos["death_roi_width"], self._config.ui_pos["death_roi_height"]]
self._died = False
self._do_monitor = False
@@ -34,11 +34,11 @@ class DeathManager:
mouse.click(button="left")
self._died = False
def start_monitor(self, run_thread):
def start_monitor(self, run_thread: Thread):
self._do_monitor = True
while self._do_monitor:
time.sleep(1.0) # no need to do this too frequent, when we died we are not in a hurry...
roi_img = cut_roi(self._screen.grab(), self._search_roi)
roi_img = cut_roi(self._screen.grab(), self._config.ui_roi["death"])
_, filtered_roi_img = color_filter(roi_img, self._config.colors["red"])
res = cv2.matchTemplate(filtered_roi_img, self._you_have_died_filtered, cv2.TM_CCOEFF_NORMED)
_, max_val, _, _ = cv2.minMaxLoc(res)
@@ -46,6 +46,9 @@ class DeathManager:
Logger.warning("You have died! Waiting 30 sec to make sure chicken does not do any funny during with this logic.")
# first wait a bit to make sure health manager is done with its chicken stuff which obviously failed
kill_thread(run_thread)
# clean up key presses that might be pressed in the run_thread
keyboard.release(self._config.char["stand_still"])
keyboard.release(self._config.char["show_items"])
time.sleep(30)
self._died = True
if self._template_finder.search("D2_LOGO_HS", self._screen.grab())[0]:
+13 -9
View File
@@ -9,6 +9,7 @@ from screen import Screen
import numpy as np
import time
from config import Config
from threading import Thread
class HealthManager:
@@ -48,7 +49,7 @@ class HealthManager:
keyboard.send(self._config.char[key])
break
def get_health(self, img):
def get_health(self, img: np.ndarray) -> float:
health_rec = [self._config.ui_pos["health_left"], self._config.ui_pos["health_top"], self._config.ui_pos["health_width"], self._config.ui_pos["health_height"]]
health_img = cut_roi(img, health_rec)
# red mask
@@ -61,14 +62,14 @@ class HealthManager:
health_percentage_green = (float(np.sum(mask)) / mask.size) * (1/255.0)
return max(health_percentage, health_percentage_green)
def get_mana(self, img):
def get_mana(self, img: np.ndarray) -> float:
mana_rec = [self._config.ui_pos["mana_left"], self._config.ui_pos["mana_top"], self._config.ui_pos["mana_width"], self._config.ui_pos["mana_height"]]
mana_img = cut_roi(img, mana_rec)
mask, _ = color_filter(mana_img, [np.array([117, 120, 20]), np.array([121, 255, 255])])
mana_percentage = (float(np.sum(mask)) / mask.size) * (1/255.0)
return mana_percentage
def get_merc_health(self, img):
def get_merc_health(self, img: np.ndarray) -> float:
health_rec = [self._config.ui_pos["merc_health_left"], self._config.ui_pos["merc_health_top"], self._config.ui_pos["merc_health_width"], self._config.ui_pos["merc_health_height"]]
merc_health_img = cut_roi(img, health_rec)
merc_health_img = cv2.cvtColor(merc_health_img, cv2.COLOR_BGR2GRAY)
@@ -76,15 +77,15 @@ class HealthManager:
merc_health_percentage = (float(np.sum(health_tresh)) / health_tresh.size) * (1/255.0)
return merc_health_percentage
def start_monitor(self, run_thread):
def start_monitor(self, run_thread: Thread):
Logger.debug("Start health monitoring")
self._do_monitor = True
self._did_chicken = False
start = time.time()
while self._do_monitor:
time.sleep(0.1)
img = self._screen.grab()
roi = [700, 650, 460, 250]
is_loading_black_roi = np.average(img[:, 0:500]) < 1.0
is_loading_black_roi = np.average(img[:, 0:self._config.ui_roi["loading_left_black"][2]]) < 1.0
if not is_loading_black_roi:
# check health
health_percentage = self.get_health(img)
@@ -92,12 +93,16 @@ class HealthManager:
if health_percentage < self._config.char["take_health_potion"] and last_drink > 2.5:
self._drink_poition(img, "health")
self._last_health = time.time()
elif health_percentage < self._config.char["chicken"]:
# give the chicken a 4 sec delay to give time for a healing pot and avoid endless loop of chicken
elif health_percentage < self._config.char["chicken"] and (time.time() - start) > 4:
Logger.warning("Trying to chicken!")
cv2.imwrite("info_debug_chicken.png", img)
self._ui_manager.save_and_exit()
self._did_chicken = True
kill_thread(run_thread)
# clean up key presses that might be pressed in the run_thread
keyboard.release(self._config.char["stand_still"])
keyboard.release(self._config.char["show_items"])
self._do_monitor = False
break
# check mana
@@ -107,8 +112,7 @@ class HealthManager:
self._drink_poition(img, "mana")
self._last_mana = time.time()
# check merc
roi = [0, 0, 150, 150]
merc_alive, _ = self._template_finder.search("MERC", img, roi=roi)
merc_alive, _ = self._template_finder.search("MERC", img, roi=self._config.ui_roi["merc_icon"])
if merc_alive:
merc_health_percentage = self.get_merc_health(img)
last_drink = time.time() - self._last_merc_healh
+5 -12
View File
@@ -39,22 +39,18 @@ class NpcManager:
}
def open_npc_menu(self, npc_key: Npc) -> bool:
# TODO: 1920x1080 specific. Cut off bottom skill bar
roi = [0, 0, 1920, 1000]
roi = self._config.ui_roi["cut_skill_bar"]
start = time.time()
while (time.time() - start) < 40:
while (time.time() - start) < 35:
img = self._screen.grab()
for key in self._npcs[npc_key]["template_group"]:
# TODO: 1920x1080 specific params
res, pos = self._template_finder.search(key, img, threshold=0.35, roi=[0, 0, 1920, 1000])
res, pos = self._template_finder.search(key, img, threshold=0.35, roi=roi)
if res:
x_m, y_m = self._screen.convert_screen_to_monitor(pos)
custom_mouse.move(x_m, y_m, duration=0.05)
time.sleep(0.2)
_, filtered_inp_w = color_filter(self._screen.grab(), self._config.colors["white"])
_, filtered_inp_g = color_filter(self._screen.grab(), self._config.colors["gold"])
# TODO: 1920x1080 specific params
# roi = [max(0, pos[0] - 200), max(0, pos[1] - 250), 400, 250]
res_w, _ = self._template_finder.search(self._npcs[npc_key]["name_tag_white"], filtered_inp_w, 0.92, roi=roi)
res_g, _ = self._template_finder.search(self._npcs[npc_key]["name_tag_gold"], filtered_inp_g, 0.92, roi=roi)
if res_w:
@@ -69,11 +65,8 @@ class NpcManager:
return False
def press_npc_btn(self, npc_key: Npc, action_btn_key: str):
# click resurrect btn
_, filtered_inp = color_filter(self._screen.grab(), self._config.colors["white"])
# TODO: 1920x1080 specific params
roi = [300, 0, 1400, 500]
res, pos = self._template_finder.search(self._npcs[npc_key]["action_btns"][action_btn_key], filtered_inp, 0.92, roi=roi)
res, pos = self._template_finder.search(self._npcs[npc_key]["action_btns"][action_btn_key], filtered_inp, 0.92, roi=self._config.ui_roi["cut_skill_bar"])
if res:
x_m, y_m = self._screen.convert_screen_to_monitor(pos)
custom_mouse.move(x_m, y_m, duration=0.1)
@@ -85,7 +78,7 @@ class NpcManager:
keyboard.send("esc")
# Testing: Stand close to Qual-Kehk and run
# Testing: Stand close to Qual-Kehk or Malah and run
if __name__ == "__main__":
from screen import Screen
from config import Config
+143 -71
View File
@@ -1,3 +1,4 @@
from char.i_char import IChar
from screen import Screen
from template_finder import TemplateFinder
from logger import Logger
@@ -7,31 +8,53 @@ import keyboard
import time
import os
import random
from typing import Tuple
from typing import Tuple, List
import cv2
from config import Config
from utils.misc import wait, is_in_roi
import numpy as np
class Location:
# A5 Town
A5_TOWN_START = "a5_town_start"
A5_STASH = "a5_stash"
A5_WP = "a5_wp"
QUAL_KEHK = "qual_kehk"
MALAH = "malah"
NIHLATHAK_PORTAL = "nihlathak_portal"
# Pindle
PINDLE_START = "pindle_start"
PINDLE_SAVE_DIST = "pindle_save_dist"
PINDLE_END = "pindle_end"
# Eldritch
ELDRITCH_START = "eldritch_start"
ELDRITCH_SAVE_DIST = "eldritch_save_dist"
ELDRITCH_END = "eldritch_end"
# Shenk
SHENK_START = "shenk_start"
SHENK_SAVE_DIST = "shenk_save_dist"
SHENK_END = "shenk_end"
class Pather:
"""
Traverses 'dynamic' pathes with reference templates and relative coordinates or statically recorded pathes.
Check utils/node_recorder.py to generate templates/refpoints and relative coordinates to nodes. Once you have refpoints and
nodes you can specify in which order this nodes should be traversed in self._paths.
"""
def __init__(self, screen: Screen, template_finder: TemplateFinder):
self._config = Config()
self._screen = screen
self._template_finder = template_finder
# TODO: params based on 1920x1080 (in rel coordinates to ref point)
self._range_x = [-950, 950]
self._range_y = [-530, 440]
self._range_x = [-self._config.ui_pos["center_x"] + 10, self._config.ui_pos["center_x"] - 10]
self._range_y = [-self._config.ui_pos["center_y"] + 10, self._config.ui_pos["center_y"] - self._config.ui_pos["skill_bar_height"] - 50]
self._nodes = {
# A5 town
0: {"A5_TOWN_0": (110-70, 373), "A5_TOWN_1": (-68-70, -205)},
1: {"A5_TOWN_0": (-466, 287), "A5_TOWN_1": (-644, -291), "A5_TOWN_0.5": (717, 349)},
2: {"A5_TOWN_0": (-552, 42), "A5_TOWN_0.5": (659-25, 90+20)},
2: {"A5_TOWN_0": (-552, 42-100), "A5_TOWN_0.5": (659-25, 90+20-100)},
3: {"A5_TOWN_1": (-414, 141), "A5_TOWN_2": (728, -90)},
4: {"A5_TOWN_1": (-701, 400), "A5_TOWN_2": (440, 169), "A5_TOWN_3": (-400, -208), "A5_TOWN_4": (243, -244)},
5: {"A5_TOWN_2": (555-100, 429-100), "A5_TOWN_3": (-285-100, 51-100), "A5_TOWN_4": (358-100, 15-100)},
@@ -43,8 +66,32 @@ class Pather:
12: {"A5_TOWN_8": (-209, -294), "A5_TOWN_9": (-248, 130)},
13: {"A5_TOWN_3": (262, 210)},
14: {"A5_TOWN_3": (694, 282)},
# Pindle
100: {"PINDLE_0": (-146, -60), "PINDLE_1": (-19, 335), "PINDLE_2": (-549, 127)},
101: {"PINDLE_1": (557, -68), "PINDLE_2": (27, -276), "PINDLE_3": (-185, 391)},
102: {"PINDLE_3": (334, 132), "PINDLE_4": (142, 323)},
103: {"PINDLE_3": (593, -113), "PINDLE_4": (401, 78)},
104: {"PINDLE_4": (1076, -176), "PINDLE_3": (1264, -366), "PINDLE_5": (-280, 356), "PINDLE_6": (-700, 133)},
# Eldritch
120: {"ELDRITCH_0": (439, 236), "ELDRITCH_1": (-461, 314)},
121: {"ELDRITCH_1": (-493, -155), "ELDRITCH_2": (616, 257), "ELDRITCH_3": (-137, 297)},
122: {"ELDRITCH_2": (530, -218), "ELDRITCH_3": (-223, -178)},
123: {"ELDRITCH_3": (-148, -498), "ELDRITCH_2": (604, -538), "ELDRITCH_4": (-163, -283)},
# Shenk
140: {"SHENK_0": (-224, -340), "SHENK_15": (120, 20), "SHENK_1": (667, -242)},
141: {"SHENK_0": (-194, 66), "SHENK_15": (116, 440), "SHENK_1": (696, 161), "SHENK_2": (-251, -51)},
142: {"SHENK_1": (876, 564), "SHENK_2": (-78, 352), "SHENK_3": (535, -194), "SHENK_4": (-665, -155)},
143: {"SHENK_2": (212, 758),"SHENK_3": (823, 209), "SHENK_4": (-377, 248), "SHENK_6": (-508, -103)},
144: {"SHENK_6": (-162, 185), "SHENK_7": (721, 226)},
145: {"SHENK_12": (146, -200), "SHENK_7": (1204, 558), "SHENK_6": (314, 520), "SHENK_8": (-367, 27)},
146: {"SHENK_12": (408, 166), "SHENK_9": (-497, -216), "SHENK_8": (-108, 387)},
147: {"SHENK_9": (-100, 208), "SHENK_10": (-646, 100)},
148: {"SHENK_9": (451, 395), "SHENK_10": (-97, 282), "SHENK_11": (-459, 208)},
149: {"SHENK_11": (532, 602), "SHENK_10": (882, 682), "SHENK_13": (730, 36), "SHENK_14": (510, 343)},
}
self._paths = {
# A5 Town
(Location.A5_TOWN_START, Location.NIHLATHAK_PORTAL): [3, 4, 5, 6, 8, 9],
(Location.A5_TOWN_START, Location.A5_STASH): [3, 4, 5],
(Location.A5_TOWN_START, Location.A5_WP): [3, 4],
@@ -56,63 +103,53 @@ class Pather:
(Location.A5_STASH, Location.A5_WP): [],
(Location.QUAL_KEHK, Location.NIHLATHAK_PORTAL): [12, 11, 10, 6, 8, 9],
(Location.QUAL_KEHK, Location.A5_WP): [12, 11, 10, 6],
# Pindle
(Location.PINDLE_START, Location.PINDLE_SAVE_DIST): [100, 101, 102, 103],
(Location.PINDLE_SAVE_DIST, Location.PINDLE_END): [104],
# Eldritch
(Location.ELDRITCH_START, Location.ELDRITCH_SAVE_DIST): [120, 121, 122],
(Location.ELDRITCH_SAVE_DIST, Location.ELDRITCH_END): [123],
# Shenk
(Location.SHENK_START, Location.SHENK_SAVE_DIST): [140, 141, 142, 143, 144, 145, 146, 147, 148],
(Location.SHENK_SAVE_DIST, Location.SHENK_END): [149],
}
self._fixed_tele_path = {
# 0: path to boss, 1: location of boss
"PINDLE": ([(1382, 53), (1685, 105), (1429, 122)], (1533, 327)),
"PINDLE_END": ([(600, 40)], None),
"ELDRITCH": ([(978, 95), (845, 109)], (1012, 42)),
"SHENK": ([(798, 869), (1112, 882), (1220, 860), (1330, 869), (1502, 836), (1247, 887), (1258, 901), (1463, 814), (1351, 778)], [1815, 772])
"PINDLE_END": ([(600, 40)], None), # to move away from items
}
def get_fixed_path(self, key: str):
return self._fixed_tele_path[key]
def _draw_debug(self, img, node_pos_abs_list, ref_pos_abs_list):
for node_pos_abs in node_pos_abs_list:
pos_screen = self._screen.convert_abs_to_screen(node_pos_abs)
cv2.circle(img, pos_screen, 10, (255, 0, 0), 10)
for ref_pos_screen in ref_pos_abs_list:
pos_screen = self._screen.convert_abs_to_screen(ref_pos_screen)
cv2.circle(img, pos_screen, 10, (0, 255, 0), 10)
img = cv2.resize(img, None, fx=0.5, fy=0.5)
cv2.namedWindow("x")
cv2.moveWindow("x", 2000, 30)
cv2.imshow("x", img)
cv2.waitKey(1)
def _display_all_nodes(self):
def _display_all_nodes_debug(self, filter: str = None):
while 1:
img = self._screen.grab()
for node_idx in self._nodes:
for template_type in self._nodes[node_idx]:
success, ref_pos_screen = self._template_finder.search(template_type, img)
if success:
# Get reference position of template in abs coordinates
ref_pos_abs = self._screen.convert_screen_to_abs(ref_pos_screen)
# Calc the abs node position with the relative coordinates (relative to ref)
node_pos_rel = self._nodes[node_idx][template_type]
node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
if self._check_abs_range_to_screen(node_pos_abs):
if filter is None or filter in template_type:
success, ref_pos_screen = self._template_finder.search(template_type, img)
if success:
# Get reference position of template in abs coordinates
ref_pos_abs = self._screen.convert_screen_to_abs(ref_pos_screen)
# Calc the abs node position with the relative coordinates (relative to ref)
node_pos_rel = self._nodes[node_idx][template_type]
node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
node_pos_abs = self._adjust_abs_range_to_screen(node_pos_abs)
x, y = self._screen.convert_abs_to_screen(node_pos_abs)
cv2.circle(img, (x, y), 5, (255, 0, 0), 3)
cv2.putText(img, str(node_idx), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
x, y = self._screen.convert_abs_to_monitor(ref_pos_abs)
cv2.circle(img, (x, y), 5, (0, 255, 0), 3)
cv2.putText(img, template_type, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
img = cv2.resize(img, None, fx=0.5, fy=0.5)
cv2.imshow("debug", img)
cv2.waitKey(1)
@staticmethod
def _convert_rel_to_abs(rel_loc, pos_abs):
@staticmethod
def _convert_rel_to_abs(rel_loc: Tuple[float, float], pos_abs: Tuple[float, float]) -> Tuple[float, float]:
return (rel_loc[0] + pos_abs[0], rel_loc[1] + pos_abs[1])
def _check_abs_range_to_screen(self, pos_abs: Tuple[float, float]) -> Tuple[float, float]:
is_in_x_range = self._range_x[0] < pos_abs[0] < self._range_x[1]
is_in_y_range = self._range_y[0] < pos_abs[1] < self._range_y[1]
return is_in_x_range and is_in_y_range
def traverse_nodes_fixed(self, key: str, char: IChar):
def traverse_nodes_fixed(self, key: str, char):
path = self._fixed_tele_path[key][0]
for pos in path:
x_m, y_m = self._screen.convert_screen_to_monitor(pos)
@@ -120,7 +157,58 @@ class Pather:
y_m += int(random.random() * 6 - 3)
char.move((x_m, y_m))
def traverse_nodes(self, start_location: Location, end_location: Location, char: IChar, debug: bool = False) -> bool:
def _adjust_abs_range_to_screen(self, abs_pos: Tuple[float, float]) -> Tuple[float, float]:
"""
Adjust an absolute coordinate so it will not go out of screen or click on any ui which will not move the char
:param abs_pos: Absolute position of the desired position to move to
:return: Absolute position of a valid position that can be clicked on
"""
f = 1.0
# Check for x-range
if abs_pos[0] > self._range_x[1]:
f = min(f, abs(self._range_x[1] / float(abs_pos[0])))
elif abs_pos[0] < self._range_x[0]:
f = min(f, abs(self._range_x[0] / float(abs_pos[0])))
# Check y-range
if abs_pos[1] > self._range_y[1]:
f = min(f, abs(self._range_y[1] / float(abs_pos[1])))
if abs_pos[1] < self._range_y[0]:
f = min(f, abs(self._range_y[0] / float(abs_pos[1])))
# Scale the position by the factor f
if f < 1.0:
abs_pos = (int(abs_pos[0] * f), int(abs_pos[1] * f))
# Check if adjusted position is "inside globe"
screen_pos = self._screen.convert_abs_to_screen(abs_pos)
if is_in_roi(self._config.ui_roi["mana_globe"], screen_pos) or is_in_roi(self._config.ui_roi["health_globe"], screen_pos):
# convert any of health or mana roi top coordinate to abs (x-coordinate is just a dummy 0 value)
new_range_y_bottom = self._screen.convert_screen_to_abs((0, self._config.ui_roi["mana_globe"][1]))[1]
f = abs(new_range_y_bottom / float(abs_pos[1]))
abs_pos = (int(abs_pos[0] * f), int(abs_pos[1] * f))
return abs_pos
def find_abs_node_pos(self, node_idx: int, img: np.ndarray) -> Tuple[float, float]:
node = self._nodes[node_idx]
for template_type in node:
success, ref_pos_screen = self._template_finder.search(template_type, img)
if success:
# Get reference position of template in abs coordinates
ref_pos_abs = self._screen.convert_screen_to_abs(ref_pos_screen)
# Calc the abs node position with the relative coordinates (relative to ref)
node_pos_rel = node[template_type]
node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
node_pos_abs = self._adjust_abs_range_to_screen(node_pos_abs)
return node_pos_abs
return None
def traverse_nodes(self, start_location: Location, end_location: Location, char: IChar, time_out: float = 7) -> bool:
"""
Traverse from one location to another
:param start_location: Location the char is starting at
:param end_location: Location the char is supposed to end up
:param char: Char that is traversing the nodes
:param time_out: Timeout in second. If no more move was found in that time it will cancel traverse
:return: Bool if traversed succesfull or False if it got stuck
"""
Logger.debug(f"Traverse from {start_location} to {end_location}")
path = self._paths[(start_location, end_location)]
for i, node_idx in enumerate(path):
@@ -128,7 +216,7 @@ class Pather:
last_move = time.time()
while not continue_to_next_node:
img = self._screen.grab()
if (time.time() - last_move) > 7:
if (time.time() - last_move) > time_out:
success, _ = self._template_finder.search("WAYPOINT_MENU", img)
if success:
# sometimes bot opens waypoint menu, close it to find templates again
@@ -137,34 +225,18 @@ class Pather:
last_move = time.time()
else:
cv2.imwrite("info_pather_got_stuck.png", img)
Logger.error("Got stuck exit")
Logger.error("Got stuck exit pather")
return False
_debug_node_pos_abs_list = []
_debug_ref_pos_abs_list = []
node = self._nodes[node_idx]
for template_type in node:
success, ref_pos_screen = self._template_finder.search(template_type, img)
if success:
# Get reference position of template in abs coordinates
ref_pos_abs = self._screen.convert_screen_to_abs(ref_pos_screen)
_debug_ref_pos_abs_list.append(ref_pos_abs)
# Calc the abs node position with the relative coordinates (relative to ref)
node_pos_rel = node[template_type]
node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
_debug_node_pos_abs_list.append(node_pos_abs)
if debug:
self._draw_debug(img, _debug_node_pos_abs_list, _debug_ref_pos_abs_list)
if self._check_abs_range_to_screen(node_pos_abs):
dist = math.dist(node_pos_abs, (0, 0))
# TODO: param based on 1920x1080
if dist < 150:
continue_to_next_node = True
else:
# Move the char
x_m, y_m = self._screen.convert_abs_to_monitor(node_pos_abs)
char.move((x_m, y_m))
last_move = time.time()
break
node_pos_abs = self.find_abs_node_pos(node_idx, img)
if node_pos_abs is not None:
dist = math.dist(node_pos_abs, (0, 0))
if dist < self._config.ui_pos["reached_node_dist"]:
continue_to_next_node = True
else:
# Move the char
x_m, y_m = self._screen.convert_abs_to_monitor(node_pos_abs)
char.move((x_m, y_m))
last_move = time.time()
return True
@@ -181,7 +253,7 @@ if __name__ == "__main__":
t_finder = TemplateFinder(screen)
pather = Pather(screen, t_finder)
ui_manager = UiManager(screen, t_finder)
char = Sorceress(config.sorceress, config.char, screen, t_finder, None, ui_manager)
char = Sorceress(config.sorceress, config.char, screen, t_finder, ui_manager, pather)
# pather.traverse_nodes_fixed("PINDLE", char)
# pather.traverse_nodes(Location.QUAL_KEHK, Location.NIHLATHAK_PORTAL, char, debug=True)
pather._display_all_nodes()
# pather.traverse_nodes(Location.PINDLE_START, Location.PINDLE_END, char)
pather._display_all_nodes_debug(filter="SHENK")
+8 -4
View File
@@ -19,6 +19,11 @@ class PickIt:
self._config = Config()
def pick_up_items(self, char: IChar) -> bool:
"""
Pick up all items with specified char
:param char: The character used to pick up the item
:return: Bool if any items were picked up or not. (Does not account for picking up scrolls and pots)
"""
found_items = False
keyboard.send(self._config.char["show_items"], do_press=True, do_release=False)
time.sleep(1.0) # sleep needed here to give d2r time to display items on screen on keypress
@@ -41,16 +46,15 @@ class PickIt:
if closest_item.dist > item.dist:
closest_item = item
x_m, y_m = self._screen.convert_screen_to_monitor(closest_item.center)
# TODO: 1920x1080 specific param
if closest_item.dist < 400:
if closest_item.dist < self._config.ui_pos["item_dist"]:
# no need to stash poitions and scrolls
if "potion" not in closest_item.name and "tp_scroll" != closest_item.name:
found_items = True
Logger.info(f"Picking up {closest_item.name}")
custom_mouse.move(x_m, y_m, duration=(random.random() * 0.01 + 0.02))
custom_mouse.move(x_m, y_m, duration=(random.random() * 0.03 + 0.08))
time.sleep(0.1)
mouse.click(button="left")
time.sleep(0.6)
time.sleep(0.5)
if self._ui_manager.is_overburdened():
Logger.warning("Inventory full, skipping pickit!")
# TODO: should go back to town and stash stuff then go back to picking up more stuff
+2
View File
@@ -10,6 +10,7 @@ from utils.remove_mouse_flags import remove_mouse_flag
from utils.auto_settings import adjust_settings
import threading
from beautifultable import BeautifulTable
import time
def start_bot():
@@ -54,6 +55,7 @@ if __name__ == "__main__":
elif keyboard.is_pressed(config.general['color_checker_key']):
run_color_checker()
break
time.sleep(0.02)
remove_flags_thread.join()
print("Press Enter to exit ...")
+8
View File
@@ -5,10 +5,13 @@ import cv2
import time
from logger import Logger
from typing import Tuple
from config import Config
import os
class Screen:
"""Grabs images from screen and converts differnt coordinate systems to each other"""
def __init__(self, monitor: int = 0):
self._sct = mss()
monitor_idx = monitor + 1 # sct saves the whole screen (including both monitors if available at index 0, then monitor 1 at 1 and 2 at 2)
@@ -18,7 +21,12 @@ class Screen:
if monitor_idx >= len(self._sct.monitors):
Logger.warning("Monitor index not available! Choose a smaller number for 'monitor' in the param.ini. Forcing value to 0 for now.")
monitor_idx = 1
config = Config()
self._monitor_roi = self._sct.monitors[monitor_idx]
# For windowed screens it is expected to always have them at the top left edge and adjust offset_top then
self._monitor_roi["top"] += config.ui_pos["offset_top"]
self._monitor_roi["width"] = config.ui_pos["screen_width"]
self._monitor_roi["height"] = config.ui_pos["screen_height"]
def convert_monitor_to_screen(self, screen_coord: Tuple[float, float]) -> Tuple[float, float]:
return (screen_coord[0] - self._monitor_roi["left"], screen_coord[1] - self._monitor_roi["top"])
+55 -9
View File
@@ -5,6 +5,7 @@ from typing import Tuple, Union, List
import numpy as np
from logger import Logger
import time
from config import Config
def load_template(path, scale_factor):
@@ -14,9 +15,14 @@ def load_template(path, scale_factor):
class TemplateFinder:
def __init__(self, screen: Screen, scale_factor: float = 0.5):
self.debug_last_score = -1.0
"""
:param screen: Screen object
:param scale_factor: Scale factor that is used for templates. Note: UI and NPC templates will always have scale of 1.0
"""
self.last_score = -1.0
self._screen = screen
self._scale_factor = scale_factor
self._config = Config()
self._templates = {
# Templates for node in A5 Town
"A5_TOWN_0": [load_template("assets/templates/a5_town/a5_town_0.png", self._scale_factor), self._scale_factor],
@@ -30,10 +36,36 @@ class TemplateFinder:
"A5_TOWN_7": [load_template("assets/templates/a5_town/a5_town_7.png", self._scale_factor), self._scale_factor],
"A5_TOWN_8": [load_template("assets/templates/a5_town/a5_town_8.png", self._scale_factor), self._scale_factor],
"A5_TOWN_9": [load_template("assets/templates/a5_town/a5_town_9.png", self._scale_factor), self._scale_factor],
# Template Pindle area (currently only used to check if we are at pindle, the walking is done statically)
"PINDLE_STONE": [load_template("assets/templates/pindle_stone.png", self._scale_factor), self._scale_factor],
# Template Shenk area (currently only used to check if we are at shenk at wp, the walking is done statically)
"SHENK_FLAME": [load_template("assets/templates/shenk_flame.png", self._scale_factor), self._scale_factor],
# Templates for nod at Pindle
"PINDLE_0": [load_template("assets/templates/pindle/pindle_0.png", self._scale_factor), self._scale_factor],
"PINDLE_1": [load_template("assets/templates/pindle/pindle_1.png", self._scale_factor), self._scale_factor],
"PINDLE_2": [load_template("assets/templates/pindle/pindle_2.png", self._scale_factor), self._scale_factor],
"PINDLE_3": [load_template("assets/templates/pindle/pindle_3.png", self._scale_factor), self._scale_factor],
"PINDLE_4": [load_template("assets/templates/pindle/pindle_4.png", self._scale_factor), self._scale_factor],
"PINDLE_5": [load_template("assets/templates/pindle/pindle_5.png", self._scale_factor), self._scale_factor],
"PINDLE_6": [load_template("assets/templates/pindle/pindle_6.png", self._scale_factor), self._scale_factor],
# Templates for nodes to Eldritch
"ELDRITCH_0": [load_template("assets/templates/eldritch/eldritch_0.png", self._scale_factor), self._scale_factor],
"ELDRITCH_1": [load_template("assets/templates/eldritch/eldritch_1.png", self._scale_factor), self._scale_factor],
"ELDRITCH_2": [load_template("assets/templates/eldritch/eldritch_2.png", self._scale_factor), self._scale_factor],
"ELDRITCH_3": [load_template("assets/templates/eldritch/eldritch_3.png", self._scale_factor), self._scale_factor],
"ELDRITCH_4": [load_template("assets/templates/eldritch/eldritch_4.png", self._scale_factor), self._scale_factor],
# Templates for nodes to Shenk (from Eldritch)
"SHENK_0": [load_template("assets/templates/shenk/shenk_0.png", self._scale_factor), self._scale_factor],
"SHENK_1": [load_template("assets/templates/shenk/shenk_1.png", self._scale_factor), self._scale_factor],
"SHENK_2": [load_template("assets/templates/shenk/shenk_2.png", self._scale_factor), self._scale_factor],
"SHENK_3": [load_template("assets/templates/shenk/shenk_3.png", self._scale_factor), self._scale_factor],
"SHENK_4": [load_template("assets/templates/shenk/shenk_4.png", self._scale_factor), self._scale_factor],
"SHENK_6": [load_template("assets/templates/shenk/shenk_6.png", self._scale_factor), self._scale_factor],
"SHENK_7": [load_template("assets/templates/shenk/shenk_7.png", self._scale_factor), self._scale_factor],
"SHENK_8": [load_template("assets/templates/shenk/shenk_8.png", self._scale_factor), self._scale_factor],
"SHENK_9": [load_template("assets/templates/shenk/shenk_9.png", self._scale_factor), self._scale_factor],
"SHENK_10": [load_template("assets/templates/shenk/shenk_10.png", self._scale_factor), self._scale_factor],
"SHENK_11": [load_template("assets/templates/shenk/shenk_11.png", self._scale_factor), self._scale_factor],
"SHENK_12": [load_template("assets/templates/shenk/shenk_12.png", self._scale_factor), self._scale_factor],
"SHENK_13": [load_template("assets/templates/shenk/shenk_13.png", self._scale_factor), self._scale_factor],
"SHENK_14": [load_template("assets/templates/shenk/shenk_14.png", self._scale_factor), self._scale_factor],
"SHENK_15": [load_template("assets/templates/shenk/shenk_15.png", self._scale_factor), self._scale_factor],
# Template Selectables
"A5_STASH": [load_template("assets/templates/a5_stash.png", self._scale_factor), self._scale_factor],
"A5_WP": [load_template("assets/templates/a5_wp.png", self._scale_factor), self._scale_factor],
@@ -45,6 +77,7 @@ class TemplateFinder:
"D2_LOGO_HS": [load_template("assets/templates/d2_logo_hs.png", 1.0), 1.0],
"LOADING": [load_template("assets/templates/loading.png", 1.0), 1.0],
"PLAY_BTN": [load_template("assets/templates/play_btn.png", 1.0), 1.0],
"PLAY_BTN_GRAY": [load_template("assets/templates/play_btn_gray.png", 1.0), 1.0],
"HELL_BTN": [load_template("assets/templates/hell_btn.png", 1.0), 1.0],
"SAVE_AND_EXIT": [load_template("assets/templates/save_and_exit.png", 1.0), 1.0],
"SERVER_ISSUES": [load_template("assets/templates/server_issues.png", 1.0), 1.0],
@@ -79,6 +112,14 @@ class TemplateFinder:
return self._templates[key][0]
def search(self, ref: Union[str, np.ndarray], inp_img: np.ndarray, threshold: float = 0.7, roi: List[float] = None) -> Tuple[bool, Tuple[float, float]]:
"""
Search for a template in an image
:param ref: Either key of a already loaded template or a image which is used as template
:param inp_img: Image in which the template will be searched
:param threshold: Threshold which determines if a template is found or not
:param roi: Region of Interest of the inp_img to restrict search area. Format [left, top, width, height]
:return: Returns found flag and the position as [bool, [x, y]]. If not found, position will be None. Position in image space.
"""
if roi is None:
# if no roi is provided roi = full inp_img
roi = [0, 0, inp_img.shape[1], inp_img.shape[0]]
@@ -101,7 +142,7 @@ class TemplateFinder:
if img.shape[0] > template.shape[0] and img.shape[1] > template.shape[1]:
res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
_, max_val, _, max_pos = cv2.minMaxLoc(res)
self.debug_last_score = max_val
self.last_score = max_val
if max_val > threshold:
ref_point = (max_pos[0] + int(template.shape[1] * 0.5) + rx, max_pos[1] + int(template.shape[0] * 0.5) + ry)
ref_point = (int(ref_point[0] * (1.0 / scale)), int(ref_point[1] * (1.0 / scale)))
@@ -109,12 +150,17 @@ class TemplateFinder:
return False, None
def search_and_wait(self, ref: str, roi: List[float] = None, time_out: float = None, threshold: float = 0.7) -> Tuple[bool, Tuple[float, float]]:
"""
Helper function that will loop and keep searching for a template
:param ref: Key of template which has been loaded beforehand
:param time_out: After this amount of time the search will stop and it will return [False, None]
Rest of params same as TemplateFinder.search()
"""
Logger.debug(f"Waiting for Template {ref}")
start = time.time()
while 1:
img = self._screen.grab()
# TODO: 1920x1080 specific params
is_loading_black_roi = np.average(img[:, 0:500]) < 1.0
is_loading_black_roi = np.average(img[:, 0:self._config.ui_roi["loading_left_black"][2]]) < 1.0
success, pos = self.search(ref, img, roi=roi, threshold=threshold)
if not is_loading_black_roi and success:
return True, pos
@@ -133,7 +179,7 @@ if __name__ == "__main__":
while 1:
img = screen.grab()
success, pos = template_finder.search("BLUE_PORTAL", img, threshold=0.67)
print(template_finder.debug_last_score)
print(template_finder.last_score)
if success:
cv2.circle(img, pos, 7, (255, 0, 0), thickness=5)
img = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_NEAREST)
+145 -83
View File
@@ -1,8 +1,9 @@
from screen import Screen
from template_finder import TemplateFinder
import mouse
from utils import custom_mouse, custom_keyboard
import keyboard # currently needed for a workaround
from typing import Tuple
from utils import custom_mouse
import keyboard
import random
import time
import cv2
@@ -16,36 +17,50 @@ from utils.misc import color_filter
class UiManager():
"""Everything that is clicking on some static 2D UI or is checking anything in regard to it should be placed here."""
def __init__(self, screen: Screen, template_finder: TemplateFinder):
self._config = Config()
self._template_finder = template_finder
self._screen = screen
self._curr_stash = 0 # 0: personal, 1: shared1, 2: shared2, 3: shared3
def use_wp(self, act, idx):
pos_act_btn = (self._config.ui_pos["wp_act_i_btn_x"] + self._config.ui_pos["wp_act_btn_width"] * act, self._config.ui_pos["wp_act_i_btn_y"])
x, y = self._screen.convert_screen_to_monitor(pos_act_btn)
custom_mouse.move(x, y, duration=0.4, randomize=5)
mouse.click(button="left")
wait(0.35, 0.4)
def use_wp(self, act: int, idx: int):
"""
Use Waypoint. The menu must be opened when calling the function.
:param act: Index of the act from left. Note that it start at 0. e.g. Act5 -> act=4
:param idx: Index of the waypoint from top. Note that it start at 0.
"""
# Note: We are currently only in act 5, thus no need to click here.
# pos_act_btn = (self._config.ui_pos["wp_act_i_btn_x"] + self._config.ui_pos["wp_act_btn_width"] * act, self._config.ui_pos["wp_act_i_btn_y"])
# x, y = self._screen.convert_screen_to_monitor(pos_act_btn)
# custom_mouse.move(x, y, duration=0.4, randomize=8)
# mouse.click(button="left")
# wait(0.3, 0.4)
pos_wp_btn = (self._config.ui_pos["wp_first_btn_x"], self._config.ui_pos["wp_first_btn_y"] + self._config.ui_pos["wp_btn_height"] * idx)
x, y = self._screen.convert_screen_to_monitor(pos_wp_btn)
custom_mouse.move(x, y, duration=0.4, randomize=5)
custom_mouse.move(x, y, duration=0.4, randomize=12)
wait(0.3, 0.4)
mouse.click(button="left")
def can_teleport(self):
img = self._screen.grab()
def can_teleport(self) -> bool:
"""
:return: Bool if teleport is red/available or not. Teleport skill must be selected on right skill slot when calling the function.
"""
roi = [
self._config.ui_pos["skill_right_x"] - (self._config.ui_pos["skill_width"] // 2),
self._config.ui_pos["skill_y"] - (self._config.ui_pos["skill_height"] // 2),
self._config.ui_pos["skill_width"],
self._config.ui_pos["skill_height"]
]
img = cut_roi(img, roi)
img = cut_roi(self._screen.grab(), roi)
avg = np.average(img)
return avg > 75.0
def is_teleport_selected(self):
def is_teleport_selected(self) -> bool:
"""
:return: Bool if teleport is currently the selected skill on the right skill slot.
"""
roi = [
self._config.ui_pos["skill_right_x"] - (self._config.ui_pos["skill_width"] // 2),
self._config.ui_pos["skill_y"] - (self._config.ui_pos["skill_height"] // 2),
@@ -58,11 +73,11 @@ class UiManager():
return True
return False
def is_overburdened(self):
#TODO: 1920x1080 specific roi
roi = [17, 765, 470, 90]
img = self._screen.grab()
img = cut_roi(img, roi)
def is_overburdened(self) -> bool:
"""
:return: Bool if the last pick up overburdened your char. Must be called right after picking up an item.
"""
img = cut_roi(self._screen.grab(), self._config.ui_roi["is_overburdened"])
_, filtered_img = color_filter(img, self._config.colors["gold"])
templates = [cv2.imread("assets/templates/inventory_full_msg_0.png"), cv2.imread("assets/templates/inventory_full_msg_1.png")]
for template in templates:
@@ -74,7 +89,12 @@ class UiManager():
return False
@staticmethod
def potion_type(img):
def potion_type(img: np.ndarray) -> str:
"""
Based on cut out image from belt, determines what type of potion it is. TODO: Add rejuv support
:param img: Cut out image of a belt slot
:return: Any of ["empty", "health", "mana"]
"""
avg_brightness = np.average(img)
if avg_brightness < 47:
return "empty"
@@ -89,7 +109,10 @@ class UiManager():
return "empty"
def check_free_belt_spots(self) -> bool:
# currently only checks if a whole row is free
"""
Check if any column in the belt is free (only checking the bottom row)
:return: Bool if any column in belt is free
"""
img = self._screen.grab()
for i in range(4):
roi = [
@@ -104,128 +127,163 @@ class UiManager():
return True
return False
def save_and_exit(self):
def save_and_exit(self) -> bool:
"""
Performes save and exit action from within game
:return: Bool if action was successful
"""
start = time.time()
while (time.time() - start) < 10:
while (time.time() - start) < 15:
keyboard.send("esc")
wait(0.05)
wait(0.1)
exit_btn_pos = (self._config.ui_pos["save_and_exit_x"], self._config.ui_pos["save_and_exit_y"])
found, _ = self._template_finder.search_and_wait("SAVE_AND_EXIT", roi=[750, 350, 410, 260], time_out=2)
found, _ = self._template_finder.search_and_wait("SAVE_AND_EXIT", roi=self._config.ui_roi["save_and_exit"], time_out=7)
if found:
x_m, y_m = self._screen.convert_screen_to_monitor(exit_btn_pos)
custom_mouse.move(x_m, y_m, duration=random.random()*0.05 + 0.15, randomize=10)
custom_mouse.move(x_m, y_m, duration=0.2, randomize=12)
wait(0.1)
mouse.click(button="left")
break
return True
return False
def start_hell_game(self):
# expects to be in hero selection screen
Logger.debug(f"Searching for Play Btn...")
# TODO: roi is with respect to 1920x1080
roi = [390, 760, 1150, 300]
def start_hell_game(self) -> bool:
"""
Starting a game in hell mode. Will wait and retry on server connection issue.
:return: Bool if action was successful
"""
while 1:
_, pos = self._template_finder.search_and_wait("PLAY_BTN", roi=roi)
x_range_offline = [self._config.ui_pos["play_x_offline"] - 50, self._config.ui_pos["play_x_offline"] + 50]
x_range_online = [self._config.ui_pos["play_x_online"] - 50, self._config.ui_pos["play_x_online"] + 50]
y_range = [self._config.ui_pos["play_y"] - 50, self._config.ui_pos["play_y"] + 50]
in_offline_range = x_range_offline[0] < pos[0] < x_range_offline[1]
in_online_range = x_range_online[0] < pos[0] < x_range_online[1]
mode_info = "online mode" if in_online_range else "offline mode"
if (in_offline_range or in_online_range) and y_range[0] < pos[1] < y_range[1]:
pos = [pos[0], self._config.ui_pos["play_y"]]
img = self._screen.grab()
# search offline btn
found_off, _ = self._template_finder.search("PLAY_BTN", img, roi=self._config.ui_roi["play_btn_offline"], threshold=0.8)
# search online btn with enabled and disabled version
found_on, _ = self._template_finder.search("PLAY_BTN", img, roi=self._config.ui_roi["play_btn_online"], threshold=0.8)
score_enabled = self._template_finder.last_score
self._template_finder.search("PLAY_BTN_GRAY", img, roi=self._config.ui_roi["play_btn_online"], threshold=0.8)
score_disabled = self._template_finder.last_score
found_on = found_on and score_enabled > score_disabled
if found_off or found_on:
x_s = self._config.ui_pos["play_x_offline"] if found_off else self._config.ui_pos["play_x_online"]
pos = [x_s, self._config.ui_pos["play_y"]]
x, y = self._screen.convert_screen_to_monitor(pos)
mode_info = "offline" if found_off else "online"
Logger.debug(f"Found Play Btn ({mode_info}) -> clicking it")
if mode_info == "online mode":
Logger.warning("You are currently creating a game in online mode!")
custom_mouse.move(x, y, duration=(random.random() * 0.2 + 0.5), randomize=5)
if mode_info == "online":
Logger.warning("You are creating a game in online mode!")
custom_mouse.move(x, y, duration=(random.random() * 0.2 + 0.4), randomize=5)
mouse.click(button="left")
break
time.sleep(3.0)
Logger.debug("Searching for Hell Btn...")
# TODO: roi is with respect to 1920x1080
roi = [550, 100, 1000, 800]
while 1:
_, pos = self._template_finder.search_and_wait("HELL_BTN", roi=roi)
# sanity x y check based on 1920x1080
# note: not checking y range as it often detects nightmare button as hell btn, not sure why
x_range = [self._config.ui_pos["hell_x"] - 50, self._config.ui_pos["hell_x"] + 50]
if x_range[0] < pos[0] < x_range[1]:
x, y = self._screen.convert_screen_to_monitor((self._config.ui_pos["hell_x"], self._config.ui_pos["hell_y"]))
Logger.debug("Found Hell Btn -> clicking it")
custom_mouse.move(x, y, duration=(random.random() * 0.2 + 0.5), randomize=5)
mouse.click(button="left")
break
found, pos = self._template_finder.search_and_wait("HELL_BTN", roi=self._config.ui_roi["hell_btn"], time_out=8)
if not found:
Logger.debug("Could not find hell btn, try from start again")
return self.start_hell_game()
# sanity x y check. Note: not checking y range as it often detects nightmare button as hell btn, not sure why
x, y = self._screen.convert_screen_to_monitor((self._config.ui_pos["hell_x"], self._config.ui_pos["hell_y"]))
Logger.debug("Found Hell Btn -> clicking it")
custom_mouse.move(x, y, duration=(random.random() * 0.1 + 0.4), randomize=5)
mouse.click(button="left")
break
# check for server issue
time.sleep(1.0)
wait(2.0)
server_issue, _ = self._template_finder.search("SERVER_ISSUES", self._screen.grab())
if server_issue:
Logger.warning("Server issue. waiting 20s")
Logger.warning("Server connection issue. waiting 20s")
x, y = self._screen.convert_screen_to_monitor((self._config.ui_pos["issue_occured_ok_x"], self._config.ui_pos["issue_occured_ok_y"]))
custom_mouse.move(x, y, duration=(random.random() * 0.4 + 0.5), randomize=5)
custom_mouse.move(x, y, duration=(random.random() * 0.1 + 0.4), randomize=5)
mouse.click(button="left")
wait(1, 2)
keyboard.send("esc")
wait(16, 18)
self.start_hell_game()
wait(18, 22)
return self.start_hell_game()
else:
return True
@staticmethod
def _slot_has_item(slot_img: np.ndarray) -> bool:
"""
Check if a specific slot in the inventory has an item or not based on color
:param slot_img: Image of the slot
:return: Bool if there is an item or not
"""
slot_img = cv2.cvtColor(slot_img, cv2.COLOR_BGR2HSV)
avg_brightness = np.average(slot_img[:, :, 2])
# TODO: magic param, move to param file (Could go as low as 11.0, but better be save, otherwise bot will stop cause "stash is full" if fails)
return avg_brightness > 16.0
def _get_slot_pos_and_img(self, img: np.ndarray, column: int, row: int):
def _get_slot_pos_and_img(self, img: np.ndarray, column: int, row: int) -> Tuple[Tuple[int, int], np.ndarray]:
"""
Get the pos and img of a specific slot position in Inventory. Inventory must be open in the image.
:param img: Image from screen.grab() not cut
:param column: Column in the Inventory
:param row: Row in the Inventory
:return: Returns position and image of the cut area as such: [[x, y], img]
"""
top_left_slot = (self._config.ui_pos["inventory_top_left_slot_x"], self._config.ui_pos["inventory_top_left_slot_y"])
slot_width = self._config.ui_pos["slot_width"]
slot_height= self._config.ui_pos["slot_height"]
slot = (top_left_slot[0] + slot_width * column, top_left_slot[1] + slot_height * row)
min_x = slot[0] + 7
max_x = slot[0] + slot_width - 7
min_y = slot[1] + 7
max_y = slot[1] + slot_height - 7
# decrease size to make sure not to have any borders of the slot in the image
offset_w = int(slot_width * 0.12)
offset_h = int(slot_height * 0.12)
min_x = slot[0] + offset_w
max_x = slot[0] + slot_width - offset_w
min_y = slot[1] + offset_h
max_y = slot[1] + slot_height - offset_h
slot_img = img[min_y:max_y, min_x:max_x]
center_pos = (int(slot[0] + (slot_width // 2)), int(slot[1] + (slot_height // 2)))
return center_pos, slot_img
def _inventory_has_items(self, img, num_loot_columns) -> bool:
def _inventory_has_items(self, img, num_loot_columns: int) -> bool:
"""
Check if Inventory has any items
:param img: Img from screen.grab() with inventory open
:param num_loot_columns: Number of columns to check from left
:return: Bool if inventory still has items or not
"""
for column, row in itertools.product(range(num_loot_columns), range(4)):
_, slot_img = self._get_slot_pos_and_img(img, column, row)
if self._slot_has_item(slot_img):
return True
return False
def stash_all_items(self, num_loot_columns):
def stash_all_items(self, num_loot_columns: int):
"""
Stashing all items in inventory. Stash UI must be open when calling the function.
:param num_loot_columns: Number of columns used for loot from left
"""
# TODO: Do not stash portal scrolls and potions but throw them out of inventory on the ground!
# then the pickit check for potions and belt free can also be removed
Logger.debug("Searching for inventory gold btn...")
#TODO: 1920x1080 specific params
gold_btn_pos = [self._config.ui_pos["inventory_gold_btn_x"], self._config.ui_pos["inventory_gold_btn_y"]]
inventory_roi = [gold_btn_pos[0] - 120, gold_btn_pos[1] - 60, 400, 150]
self._template_finder.search_and_wait("INVENTORY_GOLD_BTN", roi=inventory_roi)
if not self._template_finder.search_and_wait("INVENTORY_GOLD_BTN", roi=self._config.ui_roi["gold_btn"], time_out=20)[0]:
Logger.error("Could not determine to be in stash menu. Continue...")
return
Logger.debug("Found inventory gold btn")
# select the start stash
personal_stash_pos = (self._config.ui_pos["stash_personal_btn_x"], self._config.ui_pos["stash_personal_btn_y"])
stash_btn_width = self._config.ui_pos["stash_btn_width"]
next_stash_pos = (personal_stash_pos[0] + stash_btn_width * self._curr_stash, personal_stash_pos[1])
x_m, y_m = self._screen.convert_screen_to_monitor(next_stash_pos)
custom_mouse.move(x_m, y_m, duration=(random.random() * 0.2 + 0.6), randomize=5)
custom_mouse.move(x_m, y_m, duration=0.7, randomize=15)
mouse.click(button="left")
wait(0.3, 0.4)
# stash stuff
# TODO: Currenlty doesnt work with PostMessage (custom_keyboard) not sure why?!? investigate
keyboard.send('ctrl', do_release=False)
for column, row in itertools.product(range(num_loot_columns), range(4)):
img = self._screen.grab()
slot_pos, slot_img = self._get_slot_pos_and_img(img, column, row)
if self._slot_has_item(slot_img):
x_m, y_m = self._screen.convert_screen_to_monitor(slot_pos)
custom_mouse.move(x_m, y_m, duration=(random.random() * 0.1 + 0.2), randomize=3)
custom_mouse.move(x_m, y_m, duration=(random.random() * 0.2 + 0.3), randomize=5)
wait(0.1, 0.15)
mouse.click(button="left")
wait(0.4, 0.6)
keyboard.send('ctrl', do_press=False)
Logger.debug("Check if stash is full")
time.sleep(0.6)
# move mouse awai from inventory
# move mouse away from inventory, for some reason it was sometimes included in the grabed img
top_left_slot = (self._config.ui_pos["inventory_top_left_slot_x"], self._config.ui_pos["inventory_top_left_slot_y"])
move_to = (top_left_slot[0] - 250, top_left_slot[1] - 250)
x, y = self._screen.convert_screen_to_monitor(move_to)
@@ -244,11 +302,14 @@ class UiManager():
return self.stash_all_items(num_loot_columns)
Logger.debug("Done stashing")
wait(0.5, 0.6)
wait(0.4, 0.5)
keyboard.send("esc")
def fill_up_belt_from_inventory(self, num_loot_columns):
# Find all pots in the inventory
def fill_up_belt_from_inventory(self, 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(self._config.char["inventory_screen"])
wait(0.7, 1.0)
img = self._screen.grab()
@@ -262,8 +323,8 @@ class UiManager():
keyboard.press("shift")
for pos in pot_positions:
x, y = self._screen.convert_screen_to_monitor(pos)
custom_mouse.move(x, y, duration=0.15, randomize=3)
wait(0.1)
custom_mouse.move(x, y, duration=0.15, randomize=5)
wait(0.2, 0.3)
mouse.click(button="left")
wait(0.3, 0.4)
keyboard.release("shift")
@@ -281,6 +342,7 @@ if __name__ == "__main__":
screen = Screen(config.general["monitor"])
template_finder = TemplateFinder(screen)
ui_manager = UiManager(screen, template_finder)
ui_manager.start_hell_game()
# ui_manager.stash_all_items(6)
# ui_manager.use_wp(4, 1)
ui_manager.fill_up_belt_from_inventory(10)
# ui_manager.fill_up_belt_from_inventory(10)
+7 -6
View File
@@ -3,7 +3,7 @@ import random
import ctypes
from logger import Logger
import cv2
import uuid
from typing import List, Tuple
def wait(min_seconds, max_seconds = None):
@@ -12,11 +12,6 @@ def wait(min_seconds, max_seconds = None):
time.sleep(random.random() * (max_seconds - min_seconds) + min_seconds)
return
def get_mac():
mac_num = hex(uuid.getnode()).replace('0x', '').upper()
mac = ':'.join(mac_num[i: i + 2] for i in range(0, 11, 2))
return mac
def kill_thread(thread):
thread_id = thread.ident
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, ctypes.py_object(SystemExit))
@@ -28,6 +23,12 @@ def cut_roi(img, roi):
x, y, width, height = roi
return img[y:y+height, x:x+width]
def is_in_roi(roi: List[float], pos: Tuple[float, float]):
x, y, w, h = roi
is_in_x_range = x < pos[0] < x + w
is_in_y_range = y < pos[1] < y + h
return is_in_x_range and is_in_y_range
def color_filter(img, color_range):
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
color_mask = cv2.inRange(hsv_img, color_range[0], color_range[1])
+1 -1
View File
@@ -1 +1 @@
__version__ = '0.1.1'
__version__ = '0.1.3-rc0'
+29 -2
View File
@@ -3,14 +3,21 @@
; h_min, s_min, v_min, h_max, s_max, v_max
white=0,0,240,2,2,255
gray=0,0,119,2,2,126
blue=119,120,252,121,126,255
blue=119,113,252,121,127,255
green=59,250,251,61,255,255
yellow=29,128,251,31,137,255
gold=22,80,201,24,89,216
gold=22,80,201,24,89,225
orange=20,251,251,23,255,255
red=4,251,206,7,255,213
[ui_pos_1920_1080]
screen_width=1920
screen_height=1080
offset_top=0
center_x=960
center_y=540
; some distance
skill_bar_height=100
; game creation / ending buttons
play_x_online=800
play_x_offline=960
@@ -69,3 +76,23 @@ wp_act_btn_width=100
wp_first_btn_x=340
wp_first_btn_y=193
wp_btn_height=70
; pickit
item_dist=400
; pather
reached_node_dist=150
[ui_roi_1920_1080]
; all rois are in [left, top, width, height] format
is_overburdened=17,765,470,90
save_and_exit=750,350,410,260
play_btn_online=640,920,320,100
play_btn_offline=800,940,320,100
hell_btn=840,550,245,70
gold_btn=1390,736,400,150
health_globe=260,890,320,190
mana_globe=1350,890,320,190
cut_skill_bar=0,0,1920,980
merc_icon=0,0,150,150
loading_left_black=0,0,500,1080
death=770,290,380,75
tp_search=530,180,820,600