Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54af636d8f | ||
|
|
41c05cc614 | ||
|
|
183bae8583 | ||
|
|
6f190c8065 | ||
|
|
6260a0ce30 | ||
|
|
35df3bae7b | ||
|
|
26c5ee9de5 | ||
|
|
1cfde7f560 | ||
|
|
282f7a5606 | ||
|
|
1a148afcfb | ||
|
|
a91a91dc7c | ||
|
|
648d89ff28 | ||
|
|
489cc4ec36 | ||
|
|
fddd72c3a5 | ||
|
|
5abe2fb33e | ||
|
|
df12dbd57c | ||
|
|
9a3b054248 | ||
|
|
26246f141b | ||
|
|
8d1f84062d | ||
|
|
a7d31775f1 | ||
|
|
7c52b05d10 | ||
|
|
a7d7a96268 | ||
|
|
8e48dfe3d0 | ||
|
|
c89b35ff33 | ||
|
|
0d6ab59277 | ||
|
|
01bdc022ec | ||
|
|
06b0d4fb27 | ||
|
|
c751259319 | ||
|
|
b1113b8f5a | ||
|
|
efced03d83 | ||
|
|
e69a197135 | ||
|
|
90869ebfbb | ||
|
|
b9ccc733df | ||
|
|
0635497a47 | ||
|
|
8279332579 | ||
|
|
21dc086ebd | ||
|
|
b80888aca6 | ||
|
|
d59fbc0cd3 | ||
|
|
652823189c | ||
|
|
1f1437ce1d |
@@ -12,3 +12,4 @@ run.dist/
|
||||
run.onefile-build/
|
||||
generated/
|
||||
botty_v*/
|
||||
custom.ini
|
||||
|
||||
@@ -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.
|
||||
|
||||
[](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).
|
||||
|
||||
|
After Width: | Height: | Size: 930 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 910 KiB |
|
After Width: | Height: | Size: 919 KiB |
|
After Width: | Height: | Size: 920 KiB |
|
After Width: | Height: | Size: 865 KiB |
|
After Width: | Height: | Size: 884 KiB |
|
After Width: | Height: | Size: 899 KiB |
|
After Width: | Height: | Size: 890 KiB |
|
After Width: | Height: | Size: 859 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 935 KiB |
|
After Width: | Height: | Size: 933 KiB |
|
After Width: | Height: | Size: 921 KiB |
|
After Width: | Height: | Size: 909 KiB |
|
After Width: | Height: | Size: 940 KiB |
|
After Width: | Height: | Size: 954 KiB |
|
After Width: | Height: | Size: 949 KiB |
|
After Width: | Height: | Size: 907 KiB |
|
After Width: | Height: | Size: 885 KiB |
|
After Width: | Height: | Size: 921 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 129 KiB |
@@ -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"/>
|
||||
|
||||
@@ -14,3 +14,4 @@ dependencies:
|
||||
- keyboard
|
||||
- graphviz
|
||||
- beautifultable
|
||||
- zstandard
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}/")
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ...")
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 @@
|
||||
__version__ = '0.1.1'
|
||||
__version__ = '0.1.3-rc0'
|
||||
|
||||
@@ -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
|
||||
|
||||