Compare commits

..
57 Commits
Author SHA1 Message Date
mgleed 7426e79504 Params config readme cleanup (#870)
* Bump version to v0.7.4

* init

* fix missing |'s

* Update README.md
2022-06-14 17:01:50 -04:00
mgleed 5bceafec8b Bugfix: Hammerdin and FOHdin small fixes, inappropriate vigor casting, etc (#868)
* fixes

* increase timeouts to 3.2...pather is hacky
2022-06-14 14:55:55 -04:00
mgleed 3d004062d7 Bugfix: Character select screen, character not found (#869)
* init

* adjust

* should work on nip br this way
2022-06-14 14:55:43 -04:00
mgleed 0fcddd2aed Enhancement: Reposition teleport and rename tp to town_portal in params 2022-06-13 13:46:19 -04:00
mgleed 237dfd6f34 Bugfix: hammerdin config inheritance 2022-06-12 22:38:58 -04:00
jobithuandmgleed 70debc2b0d Major feature: FOHdin, Chaos Sanctuary (#765)
* Setting Up FOHdin for CS

* replaced hammers with FOH, but not targeted yet

* finish merging

* demo

* implemented all runs. avoid nihlatak.

* replaced FOH with Hammer for Nihl

* using hammers for dia now

* reverted dia hammers to FOH, so pure FOH runs CS

* added non mob-detection attacks to trav

* some tweaks

* tweaked hammer pindle for tele amu

* improved pindle for walker FOHdin

* Pindle, Eld, Shenk, CS tweaked. Trav needs work.

* fix NoneType error if nodes aren't found

* consolidate trav code

* small adjustment

* increase attack duration in loop condition rather than attack sequence

* pindle optimizations

* optimizations of pindle, eld, shenk

* more optimizations, trav still kinda ghetto

* update target detect to include closer targets

* implement concentration with holy bolt

* nihlathak and summoner

* revised CS trash atk pattern

* tweaked sealbosses!

* CS ready for pressure tests

* implement in-chaos pickit, match/case logic over if/elif, consolidate more functions, move some to i_char.py, patch health_manager.py to prevent it from closing inventory, update inspect_items() to allow items to be dropped and not kept to be sold

* revert change

* reserve hammerdin changes for a separate PR

* get rid of extra attack sequence

* disable target/attack logs for merge

* code cleanup

Co-authored-by: mgleed <[email protected]>
2022-06-12 15:44:54 -04:00
mgleed 241fc15269 Enhancement: Debug screenshots for character select failures (#860)
* init

* simplify

* timestamp constant
2022-06-12 15:09:15 -04:00
mgleed 16a9832af0 Bugfix: update active character template 2022-06-12 14:18:46 -04:00
jobithuandmgleed 94f6c82560 Hdin pindle tele charges (#856)
* added tele charges to pindle

* hdin pindle now uses tele charges

* separated native from charges

* True!

* delete unnecessary keyboard.send()

Co-authored-by: mgleed <[email protected]>
2022-06-12 12:05:45 -04:00
mgleed 6eebe281ac Bugfix: update node 300 on trav (#858) 2022-06-12 12:04:02 -04:00
mgleed bd092bd8d2 small error adjustments (#852) 2022-06-10 22:05:57 -04:00
NeoIsrafil cdd16dbb92 Enhancement: Sazabi set
* Add files via upload

Sazabis sword found to add since its decent for barb fury mercs.

* Update pickit.ini

Adds sazabi sword to pickit so users can turn it on if they want it.

* Update pickit.ini

* Add files via upload

sazabi helmet for addition to complete sazabi set

* Add files via upload

there, the pngs.  I don't know why I missed them the first time....sorry bout that

* Add files via upload

* Delete set_sazabi_sword.png

* Delete set_sazabi_helmet.JPG

* Delete set_sazabi_helmet.png

* Delete set_sazabi_sword.JPG

* Add files via upload

Fixed images
2022-06-10 22:01:00 -04:00
mgleed 0870384b8d Feature: Warn users if non-ascii chars in botty path, windows username, or D2R path (#843) 2022-06-08 15:31:57 -04:00
mgleed bcea0129ba Bugfix: NoneType messages if api or hook not set (#842) 2022-06-08 11:31:12 -04:00
mgleed 8989738c58 Bugfix: game recovery if unable to create game (#841)
* bugfix: game recovery if unable to create game

* fix on_init() error, add error msg
2022-06-08 11:30:57 -04:00
roozhouandmgleed b457877623 Bugfix: Select online tab before creating game when d2r switch to offline tab automatically (#834)
* BugFix: Select online tab when server disconnected

Issue #789

* Simplify online/offline tab selection logic

* more robust recovery, reusable restart_or_exit() fn in main, move attempts logic, remove param, etc.

Co-authored-by: mgleed <[email protected]>
2022-06-07 22:12:53 -04:00
Wang Xiangandmgleed d047886630 Bugfix: generic_api (#836)
* Fix generic_api

generic_api.py supports custom_loot_message_hook now.

* Simplified logic

Co-authored-by: mgleed <[email protected]>
2022-06-07 14:31:54 -04:00
Legitandmgleed 54888942f5 Bugfix: Better pathing for walkers in trav (#790)
* Better pathing for walkers in trav

* Add files via upload

* trim a few templates

* re-add column asset, utilize another v2 asset

* fix tab/space issue

Co-authored-by: mgleed <[email protected]>
2022-06-07 00:00:04 -04:00
badbrad2889andmgleed 129c00ac44 Feature: Support transmute of all gem quality (chipped, etc.) (#736)
* Added the ability to transmute all gem types and select in params.ini

* Added the assets

* Updated STD to standard for transmute and updated ini for list style

* Changed the assets to standard

* changed list in INI to run only flawless by default

* Optimized the transmute: will only need to go through search once

* update tab selection

* Update params.ini

Co-authored-by: mgleed <[email protected]>
2022-06-06 23:27:13 -04:00
mgleed f7ea236823 Bugfix: character select active asset ignored hardcore characters (#838) 2022-06-06 21:53:35 -04:00
RiddleandRiddle f94bd16b2b Enhancement: Catch missing config params (#835)
Co-authored-by: Riddle <[email protected]>
2022-06-06 20:57:29 -04:00
mgleed 8db079f720 Bugfix: Reduce identify time at cain (#837) 2022-06-06 20:55:46 -04:00
aeon0andmgleed 084bdb6573 feat/cached-img (#734)
* feat/cached-img

* thread safe

* update

* add a force_new param for grab() and enable it for pather, which is more dependent on exact current frame

* use existing img for pather screen object checks

Co-authored-by: mgleed <[email protected]>
2022-06-06 18:07:10 -04:00
Legitandmgleed ac3e79401b Enhancement: Make Nopickup command optional (#819)
* Making Nopickup command optional

* Update bot.py

* Update config.py

* set default on, rework logic

Co-authored-by: mgleed <[email protected]>
2022-06-06 13:39:31 -04:00
reqyl baa6bf0e9b Enhancement: Optimize trav (#829)
* Small optimization by know if current run is last

* Tele amu optimization

Prefer to not use tele changes when getting back inside to loot.
Only use tele charge if pathway was blocked.

This change makes bot use a less tele charges, and thereby improve run speed due to not having to repair as often.

* Pass whole list of runs instead of just last one

This would allow to e.g check first run / only run etc
2022-06-06 11:16:58 -04:00
Legitandmgleed f0c6d5a6b7 Bugfix: getting stuck at act 2 portal location
* Updates act 2 template

* Add files via upload

* Delete a2_town_27.png

* Delete a2_town_22.png

* Add files via upload

* Delete a2_town_20.png

* Add files via upload

* Add files via upload

* Delete a2_town_22.png

* Add files via upload

* Update a2.py

* Update params.ini

* remove guy with shadow. asset already existed and added

Co-authored-by: mgleed <[email protected]>
2022-06-06 11:16:03 -04:00
roozhou fd7294b895 Bugfix: Random guess should be random (#833) 2022-06-06 06:30:45 -04:00
roozhou 7e700ad75e Move _lerp function to i_char.py (#832) 2022-06-06 06:28:42 -04:00
mgleed fcaffc5cf2 Bugfix: Launch options and game recovery 2022-06-05 10:28:03 -04:00
mgleedandaeon0 3e6f9a9bf1 Migrate from Python 3.9 to 3.10 (#826)
* migrate from python 3.9 to 3.10, recompiled tesserocr

* cleanup old types, change all Unions to |

* add a few match/case statements

* Update ci.yml

Co-authored-by: aeon0 <[email protected]>
2022-06-05 08:01:28 -04:00
Riddle 700e7e23b2 Merge pull request #824 from aeon0/fix-pather-main-fn
Bugfix: Update pather _main_ function for recent template_finder update
2022-06-01 13:12:50 -04:00
mgleed d41771a6c4 fix 2022-05-31 21:24:29 -04:00
jagarop 58d2834949 Merge pull request #814 from Aimware0/master
Updated readme for contributions
2022-05-30 13:23:33 -03:00
Riddle 215e589438 Updated readme for contributions 2022-05-29 20:09:12 -04:00
Legitandmgleed cf7eaa6d09 Bugfix: Non-UTF-8 characters in corpse pickup text caused corpse to be missed
* Update to corpse pickup

* Add files via upload

* Update ui_manager.py

Co-authored-by: mgleed <[email protected]>
2022-05-20 22:51:01 -04:00
mgleed a30039bc9e Bugfix: Multiple item detect by shoppers 2022-05-18 01:59:49 -04:00
mgleed 27ebd1b4db Refactor: TemplateFinder() (#770)
* init, wip

* time to test

* remove unused dataclass

* tests passing, just need to run in-game

* fix wait logic

* add threading lock, delete excess funcs

* start putting best_match back in to search_and_wait()

* bugfixes, found a bad template

* scope

* fix npc detect
2022-05-18 01:35:27 -04:00
egut125 203dce6775 Include file in discord embed (#780) 2022-05-18 01:07:51 -04:00
egut125 63b761b5dc Feature: Separate generic and loot discord message webhooks (#729)
* New param custom_loot_message_hook

* Loot web hook fixes
2022-05-14 05:50:56 -04:00
mgleed 580d9c6253 Bugfix: Revert keyboard save/exit 2022-05-14 05:32:51 -04:00
Legit fdc25d6c4d Speed up nihlathak wait to reduce chickens/deaths (#769)
Speed up nihlathak wait timers to reduce chickens/deaths
2022-05-13 21:26:51 -04:00
mgleed 99778ea51f Refactor: Use class objects for mobs for mob checking (#767)
* init, wip

* refactor mob_check, wip

* update hammerdin

* fix hammerdin
2022-05-13 16:18:13 -04:00
mgleed 3e6521c2e6 Bugfix: Fix local pather pytest, update log to accommodate mob detect change (#766)
* init

* fix

* refactor
2022-05-13 16:15:43 -04:00
jobithuandmgleed 648d42ed84 Feature: Mob Detection for Chaos Sanctuary Hammerdin (#598)
* added mob detection to CS to speed up runs

* refactoring - redemption does not work

* remove unused filterimage param for template_finder.search_and_wait

* get rid of selfs, add live view within mob_detection.py

* mobcheck(self) -> mobcheck()

* cleanup, img as an arg to mobcheck

* prefix functions for encapsulation, cleanup redundant code

* simplify further

* trim

* simplify live-viewer, move

* corrected liveviewer -- opencv hue ranges to 180 not to 255

* rename funcs

* consolidate live-view into target_detect.py

* silly ide

* fix nonetype error

fox nonetype error - but no mobs are detected anymore :(

* script runs, but fails to deteect mobs

* add pytest for target detection, fix target sorting

* revert info_ss

* running now! added "mercwait" to debug runtime

* added testing back to pather.py

* update pytest, discrete blue/green filtering, cleanup code

* fix NoneType, add negative test

* consolidate tests

* fix oopsie

* add config param

* fix config param

* fixed atk sequences & logging spam

ready to merge after my final 2 tests

* undo bad import

Co-authored-by: mgleed <[email protected]>
2022-05-12 07:03:29 -04:00
egut125andmgleed 328ff67a4b Feature: Hydra Sorc (#730)
* New Hydra Build

Only runs pindle shenk eld so far

* Adjust hydra sorc to use any alt skill on right click

* Lowered spray

* Update config/params.ini

Co-authored-by: mgleed <[email protected]>
2022-05-11 04:18:51 -04:00
Legit f3d6677ef2 Bugfix: keyboard exit bug (#755)
* Fixing fast exit bug

* Update view.py

* Update view.py

* Update view.py
2022-05-09 19:26:52 -04:00
drallcom3 bbc183e82e Enhancement: Make log chicken an option (#743)
* Make log chicken an option

It can produce a lot of spam for low level bots.

* Game stats fix

Chicken wasn't counted in the report anymore.

* Bugfix
2022-05-09 05:11:35 -04:00
mgleed e879514846 Tests: Added template match tests (#753)
* init

* rename vars

* fix best match

* update per review
2022-05-09 05:00:15 -04:00
slapchop58 0e15b7ccdb Feature: Bone necro (#727)
* Added bone necro char

* Added damage_scaling option

* adjusted some targeting

* Removed colored logging statements, documented damaage_scaling option, and moved its usage to the base class i_char
2022-05-09 04:24:08 -04:00
Legit 5bdc6caa13 Bugfix: Lowered Eldritch End node (#726)
* Lowered Eldritch End node

Need verification doesnt impact hdin or light sorc.

* Fixed light sorc for new node change
2022-05-09 04:16:13 -04:00
roozhou 376fb05225 Enhancement: Faster save and exit using keyboard (#697)
* Enhancement: Faster save and exit using keyboard

* Enhancement: faster chicken

1) Remove delay among key/mouse release (human can do it simutaneously
2) Save screenshot after save_and_exit
2022-05-09 04:07:48 -04:00
aeon0 886fc88816 fix: Release shift in case of out of gold (#740) 2022-05-01 00:56:42 +02:00
SdxHex e156671b17 Bugfix: added CTA to basic and Range (#737)
Basic and range can now buff with CTA as well as use class buffs
2022-04-27 16:40:21 -04:00
roozhou ea5071cf85 BugFix: Do not write ANSI color codes to info.log (#733) 2022-04-23 15:33:25 -04:00
mgleed a15f7207fe Bugfix: Cleanup assets at bottom of shenk stairs, add two more templates 2022-04-23 15:21:10 -04:00
aeon0 e0e0ff01f1 Update docs for new run logic 2022-04-21 08:19:08 +02:00
mgleed 20f59028e2 bump (#721) 2022-04-20 20:19:34 -04:00
137 changed files with 4318 additions and 2690 deletions
+5 -5
View File
@@ -5,16 +5,16 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Setup Miniconda Python 3.9
- name: Setup Miniconda Python 3.10
uses: actions/setup-python@v2
with:
python-version: 3.9
python-version: '3.10'
# TODO: The below setp only chaches conda packages, need to cache pip seperatly
- name: Cache conda
uses: actions/cache@v2
env:
# Increase this value to reset cache if environment.yml has not changed
CACHE_NUMBER: 0
CACHE_NUMBER: 1
with:
path: ~/conda_pkgs_dir
key: conda-${{ env.CACHE_NUMBER }}-${{hashFiles('environment.yml') }}
@@ -22,14 +22,14 @@ jobs:
uses: actions/cache@v2
env:
# Increase this value to reset cache if environment.yml has not changed
CACHE_NUMBER: 0
CACHE_NUMBER: 1
with:
path: ~/pip
key: pip-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }}
- name: Install Miniconda
uses: conda-incubator/setup-miniconda@v2
with:
python-version: 3.9
python-version: '3.10'
activate-environment: botty
channel-priority: strict
environment-file: environment.yml
+88 -80
View File
@@ -25,92 +25,98 @@ Open up D2R and wait till you are at the hero selection screen. Make sure the ch
You can either run from python. Follow [development.md](development.md) for that. Or you download the a prebuilt release [here](https://github.com/aeon0/botty/releases) (the .rar file!). Start `main.exe` in the botty folder. Focus your D2R window and press the start key (default f11). You can always force stop botty with f12. Note: Botty will use the /nopickup command in the first game to avoid pickup up trash while traversing. This command will only allow item pickup when "show items" is active.
## Development
Check out the [development.md](development.md) docu for infos on how to build from source and details of the project structure and code.
## Support this project 💓
You can support this project by giving feedback, reporting bugs, or creating pull requests.
Contributions are welcome, and we encourage you to contribute to this project if you would like to help out. Botty is a open source project and almost excessively maintained by contributors (there has been 50+ contributors! <3). In our [**discord**](https://discord.gg/Jf3J8cuXWg) there is a contributor role, and you can ping one of the admins and ask for the role to talk to other contributors! Though you don't need to be in the [**discord**](https://discord.gg/Jf3J8cuXWg) to contribute, we do encourage you to do so.
## Graphic Debugger
To check if you graphic settings are good and if the bot would pick up items there is a **Graphic Debugger Mode**. Start botty and press F10 (Default key). This will open up a (mostly black) window. Start a game in D2R and go to A5. You should see some templates with blue circles detected and scores printed out to the console. To check item finding, throw some items of different types on the ground. The debug window should show the item names with black background. If you throw an item on the ground that should be picked up, it will have a red circle. The console will print out the scores for each item that would be picked up. Scores should be well above 0.9 for these items.</br>
<img src="assets/docs/graphic_debugger.png" width="900">
## 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.
## params.ini
All botty configuration files are located in the __config__ folder. To ease the switch to new botty versions, you can also overwrite any of the .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 **params.ini** changes to the new version. Example:
```ini
; custom.ini - overwrites 3 params in the params.ini
; custom.ini - overwrites 2 params in the params.ini
[general]
name=MyCustomName
[routes]
run_pindle=1
run_shenk=0
order=run_pindle, run_eldritch
```
| [general] | Descriptions |
| -------------------- | --------------------------|
| name | Name used in terminal and discord messages. |
| custom_message_hook | Add your own message hook here to get messages about drops and botty status updates, discord webhook is default. |
| logger_lvl | Can be any of [info, debug] and determines how much output you see on the command line. |
| max_game_length_s | Max game length in seconds. Botty will attempt to stop whatever it's doing and try to restart a new game at specified interval. If this fails, botty will attempt to shut down D2R and Bnet. |
| max_consecutive_fails | Botty will stop making games if the number of consecutive fails reaches this max value. |
| randomize_runs | Randomize the order of `[routes]` specified in `params.ini`. |
| difficulty | Set to `normal` `nightmare` or `hell` for game difficulty. |
| name | Name used in terminal and discord messages. |
| randomize_runs | Randomize the order of `[routes]` specified in `params.ini`. |
| saved_games_folder | [Optional] Defaults to `~\Saved Games\Diablo II Resurrected`. Used to store configuration settings for `f9` / auto settings. |
| custom_loot_message_hook | Add your message hook here (such as Discord channel) to get info about loot |
| custom_message_hook | Add your message hook here (such as Discord channel) to get info about botty status updates, discord webhook is default. |
| discord_log_chicken | Set to `1` to enable messages about bot chickens, `0` to disable. |
| discord_status_count | Number of games between discord status messages being sent. Leave empty for no status reports. |
| message_api_type | Which api to use to send botty messages. Supports "generic_api" (basic discord), or "discord" (discord embeds with images). |
| discord_status_count | Number of games between discord status messages being sent. Leave empty for no status reports.
| break_length_m | Break for `break_length_m` minutes every `max_runtime_before_break_m` minutes |
| max_runtime_before_break_m | ^ |
| d2r_path | [Optional] Path to `d2r.exe`. If not set, it will default to `C:\Program Files (x86)\Diablo II Resurrected\D2R.exe` when attempting to restart. |
| max_consecutive_fails | Botty will stop making games if the number of consecutive fails reaches this max value. |
| max_game_length_s | Max game length in seconds. Botty will attempt to stop whatever it's doing and try to restart a new game at specified interval. If this fails, botty will attempt to shut down D2R and Bnet. |
| restart_d2r_when_stuck | Set to `1` and botty will attempt to restart d2r in the case that botty is unable to recover its state (e.g: game crash). |
| info_screenshots | If `1`, the bot takes a screenshot with timestamp on every stuck / chicken / timeout / inventory full event. This is 1 by Default, so remember to clean up the folder every once in a while. |
| loot_screenshots | If `1`, the bot takes a screenshot with timestamp everytime he presses `show_items` button and saves it to `loot_screenshots` folder. Remember to clear them once in a while... |
| saved_games_folder | [Optional] Defaults to `~\Saved Games\Diablo II Resurrected`. Used to store configuration settings for `f9` / auto settings. |
| d2r_path | [Optional] Path to `d2r.exe`. If not set, it will default to `C:\Program Files (x86)\Diablo II Resurrected\D2R.exe` when attempting to restart. |
| restart_d2r_when_stuck | Set to `1` and botty will attempt to restart d2r in the case that botty is unable to recover its state (e.g: game crash). |
| [routes] | Descriptions |
| ------------ | ------------------------------------------------------------------------ |
| run_trav | Run Trav in each new game. Select "1" to run it "0" to leave it out. Specific trav gear is suggested |
| run_pindle | Run Pindle in each new game. Select "1" to run it "0" to leave it out. |
| run_eldritch | Run Eldritch in each new game. Select "1" to run it "0" to leave it out. |
| run_shenk | Run shenk in each new game. Select "1" to run it "0" to leave it out. |
| run_nihlathak | Run Nihlathak in each new game. Select "1" to run it "0" to leave it out. (Teleport required) |
| run_arcane | Run Arcane Sanctuary in each new game. Select "1" to run it "0" to leave it out. (Teleport required) |
| run_diablo | Run Diablo in each new game. Select "1" to run it "0" to leave it out. (Teleport required) |
| order | List of runs botty should do. These will be run in the the order listed unless `randomize_runs` is set to 1. Possible runs: </br> run_trav, run_pindle, run_eldritch, run_eldritch_shenk, run_nihlathak (requires teleport), run_arcane (requires teleport), run_diablo (requires teleport, only hammardin)
| [char] | Descriptions |
| ------------------ | -------------------------------------------------------------------------------------------------|
| 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. If your system has some delay e.g. on vms, you might have to increase this value above the suggest value in the table! |
| num_loot_columns | Number of columns in inventory used for loot (from left!). Remaining space can be used for charms |
| show_items | Hotkey for "show items" |
| inventory_screen | Hotkey to open inventory |
| force_move | Hotkey for "force move" |
| stand_still | Hotkey for "stand still". Note this can not be the default shift key as it would interfere with the merc healing routine |
| tp | Hotkey for using a town |
| belt_rows | Integer value of how many rows the char's belt has |
| show_belt | Hotkey for "show belt" |
| 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. If your system has some delay e.g. on vms, you might have to increase this value above the suggest value in the table! |
| cta_available | 0: no cta available, 1: cta is available and should be used during prebuff |
| num_loot_columns | Number of columns in inventory used for loot (from left!). Remaining space can be used for charms |
| force_move | Hotkey for "force move" |
| inventory_screen | Hotkey to open inventory |
| potion1 | Hotkey to take potion in slot 1 |
| potion2 | Hotkey to take potion in slot 2 |
| potion3 | Hotkey to take potion in slot 3 |
| potion4 | Hotkey to take potion in slot 4 |
| cta_available | 0: no cta available, 1: cta is available and should be used during prebuff |
| show_belt | Hotkey for "show belt" |
| show_items | Hotkey for "show items" |
| stand_still | Hotkey for "stand still". Note this can not be the default shift key as it would interfere with the merc healing routine |
| teleport | Hotkey for teleport (set blank if your character can't teleport) |
| town_portal | Hotkey for town portal |
| weapon_switch | Hotkey for "weapon switch" (only needed if cta_available=1) |
| battle_order | Hotkey for battle orders from cta (only needed if cta_available=1) |
| battle_command | Hotkey for battle command from cta (only needed if cta_available=1) |
| stash_gold | Bool value to stash gold each time when stashing items |
| min_gold_to_pick | Minimum quantity of gold to pickup (also must set misc_gold=1 in pickit config) |
| stash_gold | Bool value to stash gold each time when stashing items |
| use_merc | Set to 1 for using merc. Set to 0 for not using merc (will not revive merc when dead), default = 1 |
| atk_len_arc | Attack length for hdin/sorc fighting arcane |
| atk_len_trav | Attack length for hdin fighting trav (note this atk length will be applied in 4 different spots each) |
| atk_len_pindle | Attack length for hdin or number of attack sequences for sorc when fighting pindle |
| atk_len_eldritch | Attack length for hdin or number of attack sequences for sorc when fighting eldritch |
| atk_len_shenk | Attack length for hdin or number of attack sequences for sorc when fighting shenk |
| atk_len_nihlathak | Attack length for hdin or number of attack sequences for sorc when fighting nihlathak |
| atk_len_pindle | Attack length for hdin or number of attack sequences for sorc when fighting pindle |
| atk_len_shenk | Attack length for hdin or number of attack sequences for sorc when fighting shenk |
| atk_len_trav | Attack length for hdin fighting trav (note this atk length will be applied in 4 different spots each) |
| atk_len_cs_trashmobs | Attack length for hdin or number of attack sequences when fighting Trash Mobs in Chaos Sanctuary (Diablo) |
| atk_len_diablo_vizier | Attack length for hdin or number of attack sequences when fighting Sealboss A "Vizier of Chaos" in Chaos Sanctuary (Diablo) |
| atk_len_diablo_deseis | Attack length for hdin or number of attack sequences when fighting Sealboss B "Lord De Seis" in Chaos Sanctuary (Diablo) |
| atk_len_diablo_infector | Attack length for hdin or number of attack sequences when fighting Sealboss C "Infector of Souls" in Chaos Sanctuary (Diablo) |
| atk_len_diablo_infector | Attack length for hdin or number of attack sequences when fighting Diablo in Chaos Sanctuary |
| atk_len_diablo_vizier | Attack length for hdin or number of attack sequences when fighting Sealboss A "Vizier of Chaos" in Chaos Sanctuary (Diablo) |
| atk_len_diablo | Attack length for hdin or number of attack sequences when fighting Diablo in Chaos Sanctuary |
| cs_mob_detect | If 1, it will attempt to use holy freeze from merc / conviction aura / poison to detect nearby mobs to help speed-up CS run.
| cs_town_visits | CURRENTLY BROKEN, LEAVE AT 0 FOR NOW |
| kill_cs_trash | If 1, most Trash mob packs from Chaos Sancturay Entrance to Pentagram are cleared. If 0, the run starts at Pentagram and just kills Sealbosses & Diablo (default) |
| cs_town_visits | If 1, it will go to town, buy pots & stash items between clearing the seals, set 0 to deactivate (default) |
| belt_hp_columns | Number of belt columns for healing potions |
| belt_mp_columns | Number of belt columns for mana potions |
| belt_rejuv_columns | Number of belt columns for rejuv potions |
| take_health_potion | Health percentage when healing potion will be used. e.g. 0.6 = 60% helath |
| take_mana_potion | Mana percentage when mana potion will be used |
| take_rejuv_potion_health | Health percentag when rejuv potion will be used |
@@ -119,78 +125,91 @@ run_shenk=0
| heal_rejuv_merc | Merc health percentage when giving rejuv potion to merc |
| chicken | Will chicken (leave game) when player health percentage drops below set value, range 0 to 1. Set to 0 to not chicken. |
| merc_chicken | Will chicken (leave game) when merc health percentage drops below set value, range 0 to 1. Set to 0 to not chicken. |
| belt_rejuv_columns | Number of belt columns for rejuv potions |
| belt_hp_columns | Number of belt columns for healing potions |
| belt_mp_columns | Number of belt columns for mana potions |
| pre_buff_every_run | 0: Will only prebuff on first run, 1: Will prebuff after each run/boss |
| runs_per_stash | 0: Will only stash after intentional item pickup, 1+: Will force stash after # of runs set here (recommend at least 4 in case of accidental pickups) |
| runs_per_repair | 0: Will only repair when needed, 1+: Will force repair after # of runs set here |
| id_items | Will identify items at cain before stashing them. Cain must be rescued for this to work.|
| open_chests | Open up chests in some places. E.g. on dead ends of arcane. Note: currently bad runtime. |
| enable_no_pickup | When enabled, will type `/nopickup` into chat at game start, which can help reduce accidental pickups especially for walking characters. |
| fill_shared_stash_first | Fill stash tabs starting from right to left, filling personal stash last |
| gamble_items | List of items to gamble when stash fills with gold. Leave blank to disable. Supported items currently include circlet, ring, coronet, talon, amulet
| id_items | Will identify items with tome of ID or at Cain if enabled |
| open_chests | Open up chests in some places. E.g. on dead ends of arcane. |
| pre_buff_every_run | 0: Will only prebuff on first run, 1: Will prebuff after each run/boss |
| runs_per_repair | Force repair after `runs_per_repair` of runs. Set to 0 to repair only when needed. |
| runs_per_stash | 0: Will only stash after intentional item pickup, 1+: Will force stash after # of runs set here (recommend at least 4 in case of accidental pickups) |
| sell_junk | 0: Discard unwanted items by dropping them on ground. 1: Discard items by selling them at vendor. |
| [transmute] | Descriptions |
| ------------------ | -------------------------------------------------------------------------------------------------|
| transmute_every_x_game | How often to run transmute routine (currently transmutes flawless gems into perfect gems). Transmute routine depends on stashing routine it will only start after items stashing is done. E.g. so it could take more than X games to perform transmutes if there were no items to stash at the time. Default: 20 |
| stash_destination | Stash tabs by priority to place the results of the transmute. Default: 3,2,1,0. (It means the result will be first placed in stash 3 untils it's full, then to stash 2, etc. 0 - personal tab)
| transmute | Add any or all of `chipped, flawed, standard, flawless` to trasmute gems of these types |
| transmute_every_x_game | How often to run transmute routine (currently transmutes flawless gems into perfect gems). Transmute routine depends on stashing routine it will only start after items stashing is done. E.g. so it could take more than X games to perform transmutes if there were no items to stash at the time. Default: 20 |
### Builds
| [sorceress] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| teleport | Required Hotkey for teleport |
| frozen_armor | Optional Hotkey for frozen armor (or any of the other armors) |
| energy_shield | Optional Hotkey for energy shield |
| thunder_storm | Optional Hotkey for thunder storm |
| static_field | Optional Hotkey for static field |
| telekinesis | Optional Hotkey for telekinesis |
| [light_sorc] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| chain_lightning | Optional Hotkey for chain_lightning (must be bound to left skill) |
| lightning | Required Hotkey for lightning (must be bound to right skill) |
| frozen_orb | Optional Hotkey for frozen orb (must be bound to right skill) |
| [blizz_sorc] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| ice_blast | Optional Hotkey for ice_blast (must be bound to left skill) |
| blizzard | Required Hotkey for Blizzard (must be bound to right skill) |
| ice_blast | Optional Hotkey for ice_blast (must be bound to left skill) |
| [nova_sorc] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| nova | Required Hotkey for Nova (must be bound to right skill) |
| [hammerdin] | Descriptions |
| [hydra_sorc] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| alt_attack | Required Hotkey for any alternate attacking skill. Fireball,Lightning,Frozen Orb, etc. (must be bound to right skill) |
| hydra | Required Hotkey for Hydra (must be bound to right skill) |
| [paladin] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| teleport | Optional Hotkey for teleport. If left empty hammerdin will run instead of teleport. |
| concentration | Required Hotkey for Concentration |
| cleansing | Optional Hotkey for Cleansing |
| holy_shield | Required Hotkey for Holy Shield |
| blessed_hammer | Required Hotkey for Blessed Hammer. (must be bound to left skill!) |
| redemption | Optional Hotkey for Redemption |
| vigor | Optional Hotkey for Vigor |
| cleansing | Optional Hotkey for Cleansing |
| [fohdin] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| blessed_hammer | Hotkey for Blessed Hammer. (Optional. Bind to left skill) |
| concentration | Hotkey for Concentration |
| conviction | Hotkey for Conviction |
| foh | Hotkey for Fist of Heavens (Required) |
| holy_bolt | Hotkey for Holy Bolt (Required) |
| [hammerdin] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| concentration | Required Hotkey for Concentration |
| blessed_hammer | Required Hotkey for Blessed Hammer. (must be bound to left skill!) |
| [trapsin] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| teleport | Optional Hotkey for teleport. If left empty trapsin will run instead of teleport. |
| skill_left | Optional Hotkey for Left Skill |
| burst_of_speed | Optional Hotkey for Burst of Speed |
| fade | Optional Hotkey for Fade |
| shadow_warrior | Optional Hotkey for Shadow Warrior |
| lightning_sentry | Required Hotkey for Lightning Sentry |
| death_sentry | Required Hotkey for Death Sentry |
| cloak_of_shadows| Optional Hotkey for Cloak of Shadows |
| mind_blast | Optional Hotkey for Mind Blast |
| fade | Optional Hotkey for Fade |
| lightning_sentry | Required Hotkey for Lightning Sentry |
| shadow_warrior | Optional Hotkey for Shadow Warrior |
| skill_left | Optional Hotkey for Left Skill |
| [barbarian] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| teleport | Optional Hotkey for teleport. If left empty barb will run instead of teleport. |
| cry_frequency | Time in seconds between each cast of war_cry. Set to 0.0 if max fcr should be used |
| find_item | Optional Hotkey for Find Item |
| leap | Required Hotkey for Leap |
| shout | Required Hotkey for Shout |
| war_cry | Required Hotkey for War Cry |
| find_item | Optional Hotkey for Find Item |
| cry_frequency | Time in seconds between each cast of war_cry. Set to 0.0 if max fcr should be used |
| [Necro] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| teleport | leave this blank for now, teleport/static pathing is currently not supported |
| skill_left | Required Hotkey for attack (bonespear/teeth) |
| bone_armor | Required Hotkey for Bone Armor |
| clay_golem | Required Hotkey for Clay Golem |
@@ -198,19 +217,8 @@ run_shenk=0
| amp_dmg | Required Hotkey for Amplify Damage |
| corpse_explosion | Required Hotkey Corpse Explosion |
| raise_revive | Required Hotkey revive |
| clear_pindle_packs | clears mobs before pindle |
| [dclone] | Descriptions |
| -------------------- | --------------------------------------------------------------------- |
| region_ips | Start of the region ip you want to filter for. e.g. EU Server = 37.244.11, 37.244.48 |
| dclone_hotip | Hot ip you are looking for. Botty will stay in game and message you if a message_hook is set |
| damage_scaling | Adjusts time spent casting attack skills. Ex: 2 will cast twice as long |
| clear_pindle_packs | Clears mobs before pindle |
| [advanced_options] | Descriptions |
| -------------------- | --------------------------------------------------------------------- |
| pathing_delay_factor | A linear scaling factor, between 1 and 10, applied to pathing delays. |
| message_headers | Headers that are sent with each messages |
| message_body_template | Message body of the post message sent |
## Support this project
Support it by contributing in any technical way, giving feedback, bug reports or submitting PRs.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

+5 -4
View File
@@ -101,7 +101,7 @@ reduce_to_center=120,60,1040,540
search_npcs=120,0,1040,620
merc_icon=0,0,100,100
loading_left_black=0,0,350,720
death=513,193,253,50
death=444,198,397,71
tp_search=353,120,547,400
repair_btn=318,473,90,80
left_inventory=35,92,378,378
@@ -120,7 +120,7 @@ shrine_check=500,50,350,100
character_select=1033, 44, 226, 554
character_online_status=1033, 19, 226, 25
; character_sub_roi is with respect to matched active character template
character_name_sub_roi=8, 22, 192, 32
character_name_sub_roi=6, 22, 186, 18
cube_area_roi=167,209,113,152
cube_btn_roi=160,368,125,57
xp_bar_text=369,630,554,34
@@ -142,7 +142,7 @@ pindle_safe_dist=921,35, 1123,70, 960,88
eldritch_safe_dist=652,63, 563,73
trav_safe_dist=1156,438, 1120,282, 1059,525, 1146,312, 1132,346, 985,538, 1125,333
pindle_end=1037,174
eldritch_end=675,89
eldritch_end=675,210
shenk_end=1067,544
; Nihlathak WP to Level2 Stairs "a = bottom", " b = large room" and "c = small room"
ni1_a=871,472, 1205,600, 1162,600, 1169,584, 1169,584, 1232,213, 1221,237, 1164,228, 1145,572, 1146,547, 1223,185
@@ -209,6 +209,7 @@ dia_a2y_sealfake_sealboss=1194, 57
dia_a2y_hop_622=1020,147
dia_b1s_layout2_seal=1125,102, 1125,102, 1125,102, 1171,576, 651,584
dia_b1s_seal_deseis=302,128, 267,193, 395,502
dia_b1s_seal_deseis_foh=302,128, 267,193
dia_c1f_651_654=1250,328, 1250,328
dia_c1f_654_651=100,413, 100,413
dia_c1f_651_652=1261,402
@@ -241,4 +242,4 @@ dia_c2g_home_loop=175,175
; More aggressive Seal paths
dia_a_layout_bold=189,193, 189,193, 189,193, 189,193, 189,193, 189,193, 189,193, 189,193, 1100,337
dia_b_layout_bold=1129,148, 1129,148, 1129,148, 1129,148, 1129,148, 1129,148
dia_c_layout_bold=1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503
dia_c_layout_bold=1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503
+147 -111
View File
@@ -1,23 +1,31 @@
; There is detailed documentation for each parameter in the README.md
[general]
name=Botty
message_api_type=discord
custom_message_hook=
max_game_length_s=380
max_consecutive_fails=0
max_runtime_before_break_m=0
break_length_m=0
randomize_runs=0
difficulty=hell
info_screenshots=1
loot_screenshots=0
discord_status_count=20
name=Botty
randomize_runs=0
saved_games_folder=
; messaging
custom_loot_message_hook=
custom_message_hook=
discord_log_chicken=1
discord_status_count=20
message_api_type=discord
; breaks
break_length_m=0
max_runtime_before_break_m=0
; timers / fail handling
d2r_path=C:\Program Files (x86)\Diablo II Resurrected
; if you set this field to 1, botty will save the top-most visible character template just after
; the online/offline tab and upon restart will attempt to find the same template to select the char
max_consecutive_fails=5
max_game_length_s=380
; if you set this field to 1, botty will attempt to restart d2 after a crash or failure
restart_d2r_when_stuck=0
; screenshots
info_screenshots=1
loot_screenshots=0
[routes]
; Add these possible routes to "order" to run them:
@@ -36,53 +44,66 @@ order=run_pindle, run_eldritch_shenk
; ==========================
; These configs have to be alligned with your d2r settings and char build
type=light_sorc
casting_frames=10
show_items=alt
inventory_screen=i
tp=f9
;Note: loot columns are counted from left to right. Items on the left side of inventory will be considered lootable.
num_loot_columns=5
force_move=e
;Note: stand_still can not be the default "shift" as it would interfere with merc healing
stand_still=capslock
;Note: this is different from the default hotkey as "~" is for many keyboards not reachable without also pressing altgr
show_belt=k
belt_rows=4
casting_frames=10
cta_available=0
; num_loot_columns: Number of empty columns from left to right of inventory to be used for looting.
; Store charms, etc. to the right of the inventory.
num_loot_columns=5
; game hotkeys:
force_move=e
inventory_screen=i
potion1=1
potion2=2
potion3=3
potion4=4
cta_available=0
; show_belt is different from the default hotkey as "~" is for many keyboards not reachable without also pressing altgr
show_belt=k
show_items=alt
; stand_still cannot be the default "shift" as it would interfere with merc healing
stand_still=capslock
; teleport: leave empty if you can't use
teleport=
town_portal=f9
; call to arms settings:
weapon_switch=x
battle_orders=f7
battle_command=f8
; ==========================
; ==== Optional configs ====
; ==========================
stash_gold=1
min_gold_to_pick=1000
stash_gold=1
use_merc=1
; Attack length for barbarians should be as high as 8-10 and even 10-12 for trav/shenk
atk_len_arc=2.5
atk_len_trav=3.0
atk_len_pindle=3.0
atk_len_eldritch=3.0
atk_len_shenk=4.0
atk_len_nihlathak=4.0
atk_len_pindle=3.0
atk_len_shenk=4.0
atk_len_trav=3.0
;#########################
;Diablo & Chaos Sanctuary
; Chaos Sanctuary settings
atk_len_cs_trashmobs=1.5
atk_len_diablo_deseis=5.0
atk_len_diablo_infector=4.0
atk_len_diablo_vizier=2.0
atk_len_diablo=3.0
kill_cs_trash=0
cs_mob_detect=1
; cs_town_visits is currently broken, ignore for now
cs_town_visits=0
; DIABLO END
;#########################
kill_cs_trash=0
;Pots
; Belt settings
belt_hp_columns=1
belt_mp_columns=1
belt_rejuv_columns=2
; Potion/chicken settings
take_health_potion=0.8
take_mana_potion=0.5
take_rejuv_potion_health=0.4
@@ -91,24 +112,26 @@ heal_merc=0.7
heal_rejuv_merc=0.2
chicken=0.35
merc_chicken=0
belt_rejuv_columns=2
belt_hp_columns=1
belt_mp_columns=1
;Further Options
pre_buff_every_run=1
runs_per_repair=0
runs_per_stash=4
always_repair=0
id_items=1
sell_junk=0
open_chests=1
; Misc.
; helps reduce accidental pickups when enabled especially on walking characters
enable_no_pickup=1
fill_shared_stash_first=0
; to gamble, add any/all of the following: circlet, ring, coronet, talon, amulet
gamble_items=
id_items=1
open_chests=1
pre_buff_every_run=1
runs_per_repair=0
runs_per_stash=4
sell_junk=0
[transmute]
;stash tabs by priority where to put transmuted gems
stash_destination=3,2,1,0
; Add these possible gems to "transmute" to transmute them:
; chipped, flawed, standard, flawless
transmute=flawless
;how often we want to run transmute routine(e.g. every 100 games)
transmute_every_x_game=20
@@ -116,12 +139,11 @@ transmute_every_x_game=20
; ==== Builds: Sorceress ====
; ===========================
[sorceress]
teleport=
frozen_armor=
energy_shield=
thunder_storm=
telekinesis=
frozen_armor=
static_field=
telekinesis=
thunder_storm=
[light_sorc]
; chain_lightning must be bound to left skill (hotkey optional as it shouldnt change)
@@ -132,39 +154,55 @@ lightning=
frozen_orb=
[blizz_sorc]
; ice_blast must be left skill (hotkey optional as it shouldnt change)
ice_blast=
; blizzard must be right skill, hotkey required
blizzard=
; ice_blast must be left skill (hotkey optional as it shouldnt change)
ice_blast=
[nova_sorc]
; nova must be right skill, hotkey required
nova=f2
nova=
[hydra_sorc]
; only supports Pindle and Eldritch currently
; alt_attack is any alternate attacking right skill. Fireball,Lightning,Frozen Orb, hotkey required
alt_attack=
; hydra must be right skill, hotkey required
hydra=
; =========================
; ==== Builds: Paladin ====
; =========================
[hammerdin]
teleport=
concentration=
holy_shield=
blessed_hammer=
vigor=
redemption=
[paladin]
cleansing=
holy_shield=
redemption=
vigor=
[fohdin]
; foh must be left skill, hotkey required
blessed_hammer=
concentration=
conviction=
foh=
holy_bolt=
[hammerdin]
blessed_hammer=
concentration=
; ==========================
; ==== Builds: Assassin ====
; ==========================
; Currently no Trav implementaiton!
[trapsin]
teleport=
skill_left=
burst_of_speed=
fade=
shadow_warrior=
lightning_sentry=
death_sentry=
fade=
lightning_sentry=
shadow_warrior=
skill_left=
; ===========================
@@ -174,12 +212,11 @@ death_sentry=
; Make sure leap hotkey is set if you do not have Enigma
; Ensure Battle Order/Command hotkeys are set above in the [char] section
[barbarian]
teleport=
cry_frequency=0.7
find_item=
leap=
shout=
war_cry=
find_item=
cry_frequency=0.7
; ==========================
@@ -193,90 +230,89 @@ cry_frequency=0.7
;recomended to set skeleton mage to same as revive hot hey
;skeleton mages cause pathing issues not ideal to use yet
[necro]
teleport=
skill_left=
amp_dmg=
bone_armor=
clay_golem=
raise_skeleton=
clear_pindle_pack=
corpse_explosion=
heart_of_wolverine=
raise_mage=
raise_revive=
amp_dmg=
corpse_explosion=
clear_pindle_pack=
heart_of_wolverine=
raise_skeleton=
skill_left=
[poison_necro]
teleport=
poison_nova=
# Not Required
skill_left=
bone_armor=
# Can use any golem
clay_golem=
raise_skeleton=
raise_mage=
raise_revive=
clear_pindle_pack=
corpse_explosion=
heart_of_wolverine=
# Can Use any curse
lower_res=
corpse_explosion=
clear_pindle_pack=
heart_of_wolverine=
poison_nova=
raise_mage=
raise_revive=
raise_skeleton=
# Not Required
skill_left=
[bone_necro]
bone_armor=
bone_spear=
bone_spirit=
bone_wall=
clay_golem=
teeth=
; Damage scaling. One setting to adjust kill speed to account for better/worse gear.
; ex: 2 will cast all attack skills twice a long, .5 half as long. Currently only used by bone necro
damage_scaling=1
; ===========================
; ==== Builds: Basic ====
; ===========================
; Supported runs: UNTESTED (this is your job!)
; this will basically continuously left attack, unless right attack is available.
;if the buffs are not set they are not used (they are part of the prebuff setup)
; if the buffs are not set they are not used (they are part of the prebuff setup)
[basic]
teleport=
left_attack=
right_attack=
buff_1=
buff_2=
; ===========================
; ==== Builds: Basic Ranged====
; ===========================
; Supported runs: UNTESTED (this is your job!)
; this will basically continuously left attack, unless right attack is available.
;if the buffs are not set they are not used (they are part of the prebuff setup)
; if the buffs are not set they are not used (they are part of the prebuff setup)
[basic_ranged]
teleport=
left_attack=
right_attack=
buff_1=
buff_2=
[dclone]
; Normally have known region IP (that we didn't block) for ip hunting
; eg: EU Server expected IP would be 37.244.11.xxx or 37.244.48.xxx and US Server expected IP would be 158.115.222.xxx or 158.115.221.xxx
; region_ips=37.244.11, 37.244.48 (EU) or region_ips=158.115.222, 158.115.221 (US)
region_ips=
; This is the hot ip that we want to get
dclone_hotip=
[advanced_options]
;use "can_teleport_natively" or "can_teleport_with_charges" if you want to force certain behavior in case autodetection isn't working properly
override_capabilities=
pathing_delay_factor=4
message_headers=
message_body_template={{"content": "{msg}"}}
graphic_debugger_layer_creator=0
logg_lvl=debug
; startup hotkeys
restore_settings_from_backup_key=f7
settings_backup_key=f8
auto_settings_key=f9
graphic_debugger_key=f10
resume_key=f11
exit_key=f12
hwnd_window_title=
; etc.
graphic_debugger_layer_creator=0
hwnd_window_process=D2R\.exe
hwnd_window_title=
launch_options=
logg_lvl=debug
message_body_template={{"content": "{msg}"}}
message_headers=
ocr_during_pickit=0
;use "can_teleport_natively" or "can_teleport_with_charges" if you want to force certain behavior in case autodetection isn't working properly
override_capabilities=
pathing_delay_factor=4
;If you want to control Hyper-V window from host use 0,51 here
window_client_area_offset=0,0
ocr_during_pickit=0
launch_options=
+3 -1
View File
@@ -972,6 +972,8 @@ set_cow_king_helm=0
;------------------------------;
set_sazabi_armor=0
set_sazabi_sword=0
set_sazabi_helmet=0
;------------------------------;
;====== HSARUS' DEFENSE ======;
@@ -1470,4 +1472,4 @@ rare_thunder_maul=0, ethereal
rare_truncheon=0, ethereal
rare_tyrant_club=0, ethereal
rare_war_club=0, ethereal
rare_war_hammer=0, ethereal
rare_war_hammer=0, ethereal
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -2,7 +2,7 @@ name: botty
channels:
- conda-forge
dependencies:
- python=3.9
- python=3.10
- pywin32
- pip
- leptonica
@@ -29,7 +29,7 @@ dependencies:
- pillow
- discord.py
- parse
- dependencies/tesserocr-2.5.2-cp39-cp39-win_amd64.whl
- dependencies/tesserocr-2.5.2-cp310-cp310-win_amd64.whl
- typing_extensions
- rapidfuzz
- colorama
-1
View File
@@ -1 +0,0 @@
{"erode": 0, "dilate": 0, "blur": 0, "lh": 51, "ls": 153, "lv": 102, "uh": 51, "us": 107, "uv": 130, "bright": 155, "contrast": 33, "invert": 0, "thresh": 115}
+87 -70
View File
@@ -7,42 +7,41 @@ import random
import cv2
import math
from copy import copy
from typing import Union
from collections import OrderedDict
from health_manager import set_pause_state
from transmute import Transmute
from utils.misc import wait, hms
from utils.restart import safe_exit, restart_game
from game_stats import GameStats
from logger import Logger
from config import Config
from screen import grab
from template_finder import TemplateFinder
import template_finder
from char import IChar
from item import ItemFinder
from item.pickit import PickIt
from pather import Pather, Location
from char.sorceress import LightSorc, BlizzSorc, NovaSorc
from char.sorceress import LightSorc, BlizzSorc, NovaSorc,HydraSorc
from char.trapsin import Trapsin
from char.hammerdin import Hammerdin
from char.paladin.hammerdin import Hammerdin
from char.paladin import FoHdin
from char.barbarian import Barbarian
from char.necro import Necro
from char.poison_necro import Poison_Necro
from char.bone_necro import Bone_Necro
from char.basic import Basic
from char.basic_ranged import Basic_Ranged
from ui_manager import wait_until_hidden, wait_until_visible, ScreenObjects, is_visible
from ui_manager import wait_until_hidden, wait_until_visible, ScreenObjects, is_visible, detect_screen_object
from ui import meters, skills, view, character_select, main_menu
from inventory import personal, vendor, belt, common, consumables
from run import Pindle, ShenkEld, Trav, Nihlathak, Arcane, Diablo
from town import TownManager, A1, A2, A3, A4, A5, town_manager
# Added for dclone ip hunt
from messages import Messenger
from utils.dclone_ip import get_d2r_game_ip
class Bot:
_MAIN_MENU_MARKERS = ["MAIN_MENU_TOP_LEFT","MAIN_MENU_TOP_LEFT_DARK"]
def __init__(self, game_stats: GameStats):
self._game_stats = game_stats
@@ -52,29 +51,36 @@ class Bot:
self._pickit = PickIt(self._item_finder)
# Create Character
if Config().char["type"] in ["sorceress", "light_sorc"]:
self._char: IChar = LightSorc(Config().light_sorc, self._pather)
elif Config().char["type"] == "blizz_sorc":
self._char: IChar = BlizzSorc(Config().blizz_sorc, self._pather)
elif Config().char["type"] == "nova_sorc":
self._char: IChar = NovaSorc(Config().nova_sorc, self._pather)
elif Config().char["type"] == "hammerdin":
self._char: IChar = Hammerdin(Config().hammerdin, self._pather, self._pickit) #pickit added for diablo
elif Config().char["type"] == "trapsin":
self._char: IChar = Trapsin(Config().trapsin, self._pather)
elif Config().char["type"] == "barbarian":
self._char: IChar = Barbarian(Config().barbarian, self._pather)
elif Config().char["type"] == "poison_necro":
self._char: IChar = Poison_Necro(Config().poison_necro, self._pather)
elif Config().char["type"] == "necro":
self._char: IChar = Necro(Config().necro, self._pather)
elif Config().char["type"] == "basic":
self._char: IChar = Basic(Config().basic, self._pather)
elif Config().char["type"] == "basic_ranged":
self._char: IChar = Basic_Ranged(Config().basic_ranged, self._pather)
else:
Logger.error(f'{Config().char["type"]} is not supported! Closing down bot.')
os._exit(1)
match Config().char["type"]:
case "sorceress" | "light_sorc":
self._char: IChar = LightSorc(Config().light_sorc, self._pather)
case "blizz_sorc":
self._char: IChar = BlizzSorc(Config().blizz_sorc, self._pather)
case "nova_sorc":
self._char: IChar = NovaSorc(Config().nova_sorc, self._pather)
case "hydra_sorc":
self._char: IChar = HydraSorc(Config().hydra_sorc, self._pather)
case "hammerdin" | "paladin":
self._char: IChar = Hammerdin(Config().hammerdin, self._pather, self._pickit) #pickit added for diablo
case "fohdin":
self._char: IChar = FoHdin(Config().fohdin, self._pather, self._pickit) #pickit added for diablo
case "trapsin":
self._char: IChar = Trapsin(Config().trapsin, self._pather)
case "barbarian":
self._char: IChar = Barbarian(Config().barbarian, self._pather)
case "poison_necro":
self._char: IChar = Poison_Necro(Config().poison_necro, self._pather)
case "bone_necro":
self._char: IChar = Bone_Necro(Config().bone_necro, self._pather)
case "necro":
self._char: IChar = Necro(Config().necro, self._pather)
case "basic":
self._char: IChar = Basic(Config().basic, self._pather)
case "basic_ranged":
self._char: IChar = Basic_Ranged(Config().basic_ranged, self._pather)
case _:
Logger.error(f'{Config().char["type"]} is not supported! Closing down bot.')
os._exit(1)
# Create Town Manager
a5 = A5(self._pather, self._char)
@@ -95,20 +101,22 @@ class Bot:
}
# Adapt order to the config
self._do_runs = OrderedDict((k, self._do_runs[k]) for k in Config().routes_order if k in self._do_runs and self._do_runs[k])
runs = list(self._do_runs.keys())
self._do_runs_reset = copy(self._do_runs)
Logger.info(f"Doing runs: {self._do_runs_reset.keys()}")
if Config().general["randomize_runs"]:
self.shuffle_runs()
self._pindle = Pindle(self._pather, self._town_manager, self._char, self._pickit)
self._shenk = ShenkEld(self._pather, self._town_manager, self._char, self._pickit)
self._trav = Trav(self._pather, self._town_manager, self._char, self._pickit)
self._nihlathak = Nihlathak(self._pather, self._town_manager, self._char, self._pickit)
self._arcane = Arcane(self._pather, self._town_manager, self._char, self._pickit)
self._diablo = Diablo(self._pather, self._town_manager, self._char, self._pickit)
self._pindle = Pindle(self._pather, self._town_manager, self._char, self._pickit, runs)
self._shenk = ShenkEld(self._pather, self._town_manager, self._char, self._pickit, runs)
self._trav = Trav(self._pather, self._town_manager, self._char, self._pickit, runs)
self._nihlathak = Nihlathak(self._pather, self._town_manager, self._char, self._pickit, runs)
self._arcane = Arcane(self._pather, self._town_manager, self._char, self._pickit, runs)
self._diablo = Diablo(self._pather, self._town_manager, self._char, self._pickit, runs)
# Create member variables
self._picked_up_items = False
self._curr_loc: Union[bool, Location] = None
self._curr_loc: bool | Location = None
self._use_id_tome = True
self._use_keys = True
self._pre_buffed = False
@@ -175,6 +183,17 @@ class Bot:
if not self._stopping:
self.trigger(name, **kwargs)
def restart_or_exit(self, message: str =""):
if message:
Logger.error(message)
if Config().general["restart_d2r_when_stuck"]:
Logger.info("Restart botty")
restart_game(Config().general["d2r_path"], Config().advanced_options["launch_options"])
self.stop()
else:
Logger.info("Shut down botty")
safe_exit()
def current_game_length(self):
return self._game_stats.get_current_game_length()
@@ -202,20 +221,28 @@ class Bot:
self._game_stats.log_start_game()
keyboard.release(Config().char["stand_still"])
transition_to_screens = Bot._rebuild_as_asset_to_trigger({
"select_character": Bot._MAIN_MENU_MARKERS,
"select_character": main_menu.MAIN_MENU_MARKERS,
"start_from_town": town_manager.TOWN_MARKERS,
})
match = TemplateFinder().search_and_wait(list(transition_to_screens.keys()), best_match=True)
self.trigger_or_stop(transition_to_screens[match.name])
if (match := template_finder.search_and_wait(list(transition_to_screens.keys()), best_match=True)).valid:
self.trigger_or_stop(transition_to_screens[match.name])
else:
self.restart_or_exit(f"Failed to detect {list(transition_to_screens.keys())}.")
def on_select_character(self):
if Config().general['restart_d2r_when_stuck']:
# Make sure the correct char is selected
if character_select.has_char_template_saved():
character_select.select_char()
else:
character_select.save_char_online_status()
character_select.save_char_template()
# Make sure the correct char is selected
if not character_select.has_char_template_saved():
character_select.save_char_online_status()
character_select.save_char_template()
else:
if not character_select.select_char():
if Config().general["info_screenshots"]:
timestamp = time.strftime("%Y%m%d_%H%M%S")
cv2.imwrite("./info_screenshots/info_failed_character_select_" + timestamp + ".png", grab())
if character_select.has_char_template_saved():
saved_char_img = character_select.get_saved_char_template()
cv2.imwrite("./info_screenshots/info_failed_character_select_saved_template_" + timestamp + ".png", saved_char_img)
self.restart_or_exit(f"Character select failed.")
self.trigger_or_stop("create_game")
def on_create_game(self):
@@ -225,7 +252,8 @@ class Bot:
keyboard.send("esc")
main_menu.start_game()
view.move_to_corpse()
else: return
else:
self.restart_or_exit()
self.trigger_or_stop("start_from_town")
def on_start_from_town(self):
@@ -239,25 +267,12 @@ class Bot:
belt.fill_up_belt_from_inventory(Config().char["num_loot_columns"])
self._char.discover_capabilities()
if corpse_present and self._char.capabilities.can_teleport_with_charges and not self._char.select_tp():
keybind = self._char._skill_hotkeys["teleport"]
keybind = Config().char["teleport"]
Logger.info(f"Teleport keybind is lost upon death. Rebinding teleport to '{keybind}'")
self._char.remap_right_skill_hotkey("TELE_ACTIVE", self._char._skill_hotkeys["teleport"])
# Check for the current game ip and pause if we are able to obtain the hot ip
if Config().dclone["region_ips"] != "" and Config().dclone["dclone_hotip"] != "":
cur_game_ip = get_d2r_game_ip()
hot_ip = Config().dclone["dclone_hotip"]
Logger.debug(f"Current Game IP: {cur_game_ip} and HOTIP: {hot_ip}")
if hot_ip == cur_game_ip:
self._messenger.send_message(f"Dclone IP Found on IP: {cur_game_ip}")
print("Press Enter")
input()
os._exit(1)
else:
Logger.info(f"Please Enter the region ip and hot ip on config to use")
self._char.remap_right_skill_hotkey("TELE_ACTIVE", Config().char["teleport"])
# Run /nopickup command to avoid picking up stuff on accident
if not self._ran_no_pickup and not self._game_stats._nopickup_active:
if Config().char["enable_no_pickup"] and (not self._ran_no_pickup and not self._game_stats._nopickup_active):
self._ran_no_pickup = True
if view.enable_no_pickup():
self._game_stats._nopickup_active = True
@@ -414,7 +429,8 @@ class Bot:
if elapsed_time > (Config().general["max_runtime_before_break_m"]*60):
break_msg = f'Ran for {hms(elapsed_time)}, taking a break for {hms(Config().general["break_length_m"]*60)}.'
Logger.info(break_msg)
self._messenger.send_message(break_msg)
if self._messenger.enabled:
self._messenger.send_message(break_msg)
if not self._pausing:
self.toggle_pause()
@@ -422,7 +438,8 @@ class Bot:
break_msg = f'Break over, will now run for {hms(Config().general["max_runtime_before_break_m"]*60)}.'
Logger.info(break_msg)
self._messenger.send_message(break_msg)
if self._messenger.enabled:
self._messenger.send_message(break_msg)
if self._pausing:
self.toggle_pause()
@@ -450,7 +467,7 @@ class Bot:
# All the runs go here
# ==================================
def _ending_run_helper(self, res: Union[bool, tuple[Location, bool]]):
def _ending_run_helper(self, res: bool | tuple[Location, bool]):
self._game_stats._run_counter += 1
self._game_stats.log_exp()
# either fill member variables with result data or mark run as failed
@@ -523,4 +540,4 @@ class Bot:
if self._curr_loc:
set_pause_state(False)
res = self._diablo.battle(not self._pre_buffed)
self._ending_run_helper(res)
self._ending_run_helper(res)
+1 -1
View File
@@ -2,7 +2,7 @@ import keyboard
from ui import skills
from utils.custom_mouse import mouse
from char import IChar, CharacterCapabilities
from template_finder import TemplateFinder
import template_finder
from pather import Pather
from logger import Logger
from screen import convert_abs_to_monitor
+3 -1
View File
@@ -2,7 +2,7 @@ import keyboard
from ui import skills
from utils.custom_mouse import mouse
from char import IChar,CharacterCapabilities
from template_finder import TemplateFinder
import template_finder
from pather import Pather
from logger import Logger
from screen import convert_abs_to_monitor
@@ -43,6 +43,8 @@ class Basic(IChar):
keyboard.send(Config().char["stand_still"], do_press=False)
def pre_buff(self):
if Config().char["cta_available"]:
self._pre_buff_cta()
if self._skill_hotkeys["buff_1"]:
keyboard.send(self._skill_hotkeys["buff_1"])
wait(0.5, 0.15)
+4 -2
View File
@@ -2,7 +2,7 @@ import keyboard
from ui import skills
from utils.custom_mouse import mouse
from char import IChar
from template_finder import TemplateFinder
import template_finder
from pather import Pather
from logger import Logger
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab
@@ -43,6 +43,8 @@ class Basic_Ranged(IChar):
mouse.click(button="right")
def pre_buff(self):
if Config().char["cta_available"]:
self._pre_buff_cta()
if self._skill_hotkeys["buff_1"]:
keyboard.send(self._skill_hotkeys["buff_1"])
wait(0.5, 0.15)
@@ -190,7 +192,7 @@ class Basic_Ranged(IChar):
if __name__ == "__main__":
import os
import keyboard
from template_finder import TemplateFinder
import template_finder
from pather import Pather
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
+222
View File
@@ -0,0 +1,222 @@
import keyboard
from utils.custom_mouse import mouse
from char import IChar
import template_finder
from pather import Pather
from logger import Logger
from screen import grab, convert_abs_to_monitor, convert_screen_to_abs
from config import Config
from utils.misc import wait, rotate_vec, unit_vector
import random
from pather import Location, Pather
import screen as screen
import numpy as np
import time
import os
from ui_manager import ScreenObjects
class Bone_Necro(IChar):
def __init__(self, skill_hotkeys: dict, pather: Pather):
Logger.info("Setting up Bone Necro")
super().__init__(skill_hotkeys)
self._pather = pather
if "damage_scaling" in Config().bone_necro:
self.damage_scaling = float(Config().bone_necro["damage_scaling"])
def move_to(self, x, y):
pos_m = convert_abs_to_monitor((x, y))
self.pre_move()
self.move(pos_m, force_move=True)
def bone_wall(self, cast_pos_abs: tuple[float, float], spray: int):
if not self._skill_hotkeys["bone_wall"]:
raise ValueError("You did not set bone_wall hotkey!")
keyboard.send(Config().char["stand_still"], do_release=False)
keyboard.send(self._skill_hotkeys["bone_wall"])
wait(0.02, 0.08)
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
mouse.press(button="right")
wait(self._cast_duration+.04, self._cast_duration+.08)
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def pre_buff(self):
self.bone_armor()
#only CTA if pre trav
if Config().char["cta_available"]:
self._pre_buff_cta()
Logger.info("prebuff/cta")
def _clay_golem(self):
Logger.debug('Casting clay golem')
keyboard.send(self._skill_hotkeys["clay_golem"])
wait(0.05, 0.2)
mouse.click(button="right")
wait(self._cast_duration)
def bone_armor(self):
if self._skill_hotkeys["bone_armor"]:
keyboard.send(self._skill_hotkeys["bone_armor"])
wait(0.04, 0.1)
mouse.click(button="right")
wait(self._cast_duration)
if self._skill_hotkeys["clay_golem"]:
keyboard.send(self._skill_hotkeys["clay_golem"])
wait(0.04, 0.1)
mouse.click(button="right")
wait(self._cast_duration)
def _bone_armor(self):
if self._skill_hotkeys["bone_armor"]:
keyboard.send(self._skill_hotkeys["bone_armor"])
wait(0.04, 0.1)
mouse.click(button="right")
wait(self._cast_duration)
def _corpse_explosion(self, cast_pos_abs: tuple[float, float], spray: int = 10,cast_count: int = 8):
keyboard.send(Config().char["stand_still"], do_release=False)
Logger.debug(f'casting corpse explosion {cast_count} times with spray = {spray}')
for _ in range(cast_count):
if self._skill_hotkeys["corpse_explosion"]:
keyboard.send(self._skill_hotkeys["corpse_explosion"])
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
mouse.press(button="right")
wait(0.075, 0.1)
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def _cast_circle(self, cast_dir: tuple[float,float],cast_start_angle: float=0.0, cast_end_angle: float=90.0,cast_div: int = 10,cast_spell: str='raise_skeleton',delay: float=1.0, radius=120, hold_duration: float = 3, hold=True):
if hold:
Logger.info(f'Circle cast {cast_spell} from {cast_start_angle}º to {cast_end_angle}º over {hold_duration}s')
else:
Logger.info(f'Circle cast {cast_spell} from {cast_start_angle}º to {cast_end_angle}º over {cast_div} casts')
keyboard.send(Config().char["stand_still"], do_release=False)
keyboard.send(self._skill_hotkeys[cast_spell])
if hold:
mouse.press(button="right")
for i in range(cast_div):
angle = self._lerp(cast_start_angle,cast_end_angle,float(i)/cast_div)
target = unit_vector(rotate_vec(cast_dir, angle))
Logger.debug(f"Circle cast - current angle: {angle}º")
circle_pos_screen = self._pather._adjust_abs_range_to_screen(target*radius)
circle_pos_monitor = convert_abs_to_monitor(circle_pos_screen)
start = time.time()
mouse.move(*circle_pos_monitor,delay_factor=[0.95*delay, 1.05*delay])
duration = time.time() - start
if not hold:
mouse.press(button="right")
wait(.04, .08)
mouse.release(button="right")
wait(self._cast_duration)
else:
#adjust the speed so we finish in approximately the time requested
expected = (hold_duration/cast_div)
delay = delay*(expected/duration)
if hold:
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def kill_pindle(self) -> bool:
for pos in [[200,-100], [-150,100] ]:
self.bone_wall(pos, spray=10)
self.cast_in_arc(ability='bone_spear', cast_pos_abs=[110,-50], spread_deg=15, time_in_s=5)
self._corpse_explosion([165,-75], spray=100, cast_count=5)
self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[110,-50], spread_deg=15, time_in_s=2.5)
self._pather.traverse_nodes_fixed("pindle_end", self)
return True
def kill_eldritch(self) -> bool:
#build an arc of bone walls
for pos in [[50,-200], [-200,-175], [-350,50]]:
self.bone_wall(pos, spray=10)
self.cast_in_arc(ability='teeth', cast_pos_abs=[-20,-150], spread_deg=15, time_in_s=3)
self.cast_in_arc(ability='bone_spear', cast_pos_abs=[-20,-150], spread_deg=15, time_in_s=2)
self._corpse_explosion([-20,-240], spray=100, cast_count=5)
self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[0,-80], spread_deg=60, time_in_s=2.5)
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=0.6, force_tp=True)
self.bone_armor()
return True
def kill_shenk(self) -> bool:
self._cast_circle(cast_dir=[1,1],cast_start_angle=0,cast_end_angle=360,cast_div=5,cast_spell='bone_wall',delay=.8,radius=100, hold=False)
self.cast_in_arc(ability='teeth', cast_pos_abs=[160,75], spread_deg=360, time_in_s=6)
self.cast_in_arc(ability='teeth', cast_pos_abs=[160,75], spread_deg=30, time_in_s=2)
self._corpse_explosion([0,0], spray=200, cast_count=4)
self.cast_in_arc(ability='bone_spear', cast_pos_abs=[160,75], spread_deg=30, time_in_s=3)
self._corpse_explosion([240,112], spray=200, cast_count=8)
self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[80,37], spread_deg=60, time_in_s=3)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0)
return True
def kill_council(self) -> bool:
#move down adjacent to the right moat
self.move_to(-150,150)
self.move_to(-150,150)
#moat on right side, encircle with bone walls on the other 3 sides
for pos in [[100,-100], [-125,-25], [-50,100]]:
self.bone_wall(pos, spray=10)
self.cast_in_arc(ability='teeth', cast_pos_abs=[40,-100], spread_deg=180, time_in_s=5)
self.cast_in_arc(ability='bone_spear', cast_pos_abs=[40,-100], spread_deg=120, time_in_s=8)
self._corpse_explosion([40,-100], spray=200, cast_count=8)
self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[20,-50], spread_deg=180, time_in_s=5)
self._corpse_explosion([40,-100], spray=200, cast_count=8)
self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[20,-50], spread_deg=360, time_in_s=4)
return True
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
# Find nilhlatak position
nihlathak_pos_abs = self._pather.find_abs_node_pos(end_nodes[-1], grab())
if nihlathak_pos_abs is None:
return False
cast_pos_abs = np.array(nihlathak_pos_abs)*.2
self._cast_circle(cast_dir=[1,1],cast_start_angle=0,cast_end_angle=360,cast_div=5,cast_spell='bone_wall',delay=.8,radius=100, hold=False)
self._bone_armor()
self.cast_in_arc(ability='teeth', cast_pos_abs=cast_pos_abs, spread_deg=150, time_in_s=5)
self._bone_armor()
self._corpse_explosion(cast_pos_abs, spray=200, cast_count=8)
self.cast_in_arc(ability='bone_spear', cast_pos_abs=cast_pos_abs, spread_deg=10, time_in_s=5)
self._bone_armor()
self._corpse_explosion(np.array(nihlathak_pos_abs)*.75, spray=200, cast_count=10)
self.cast_in_arc(ability='bone_spirit', cast_pos_abs=cast_pos_abs, spread_deg=30, time_in_s=2.5)
# Move to items
wait(self._cast_duration, self._cast_duration + 0.2)
self._pather.traverse_nodes(end_nodes, self, timeout=0.8)
return True
def kill_summoner(self) -> bool:
# Attack
self.cast_in_arc(ability='teeth', cast_pos_abs=[30,30], spread_deg=360, time_in_s=3)
self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[30,30], spread_deg=360, time_in_s=2)
self._corpse_explosion([0,0], spray=200, cast_count=8)
self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[30,30], spread_deg=360, time_in_s=2)
return True
if __name__ == "__main__":
import os
import keyboard
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
from config import Config
from char import Necro
pather = Pather()
char = Necro(Config().necro, Config().char, pather)
File diff suppressed because it is too large Load Diff
+99 -27
View File
@@ -1,4 +1,4 @@
from typing import Dict, Tuple, Union, List, Callable
from typing import Callable
import random
import time
import cv2
@@ -10,29 +10,59 @@ from char.capabilities import CharacterCapabilities
from ui_manager import is_visible, wait_until_visible
from ui import skills
from utils.custom_mouse import mouse
from utils.misc import wait, cut_roi, is_in_roi, color_filter
from utils.misc import wait, cut_roi, is_in_roi, color_filter, arc_spread
from logger import Logger
from config import Config
from screen import grab, convert_monitor_to_screen, convert_screen_to_abs, convert_abs_to_monitor, convert_screen_to_monitor
from template_finder import TemplateFinder
import template_finder
from ocr import Ocr
from ui_manager import detect_screen_object, ScreenObjects
class IChar:
_CrossGameCapabilities: Union[None, CharacterCapabilities] = None
_CrossGameCapabilities: None | CharacterCapabilities = None
def __init__(self, skill_hotkeys: Dict):
def __init__(self, skill_hotkeys: dict):
self._skill_hotkeys = skill_hotkeys
self._last_tp = time.time()
self._ocr = Ocr()
# Add a bit to be on the save side
self._cast_duration = Config().char["casting_frames"] * 0.04 + 0.01
self.damage_scaling = float(Config().char.get("damage_scaling", 1.0))
self.capabilities = None
self._active_skill = {
"left": "",
"right": ""
}
def _set_active_skill(self, mouse_click_type: str = "left", skill: str =""):
self._active_skill[mouse_click_type] = skill
def _select_skill(self, skill: str, mouse_click_type: str = "left", delay: float | list | tuple = None):
if not (
skill in self._skill_hotkeys and (hotkey := self._skill_hotkeys[skill])
or (skill in Config().char and (hotkey := Config().char[skill]))
):
Logger.warning(f"No hotkey for skill: {skill}")
self._set_active_skill(mouse_click_type, "")
return False
if self._active_skill[mouse_click_type] != skill:
keyboard.send(hotkey)
self._set_active_skill(mouse_click_type, skill)
if delay:
try:
wait(*delay)
except:
try:
wait(delay)
except Exception as e:
Logger.warning(f"_select_skill: Failed to delay with delay: {delay}. Exception: {e}")
return True
def _discover_capabilities(self) -> CharacterCapabilities:
override = Config().advanced_options["override_capabilities"]
if override is None:
if self._skill_hotkeys["teleport"]:
if Config().char["teleport"]:
if self.select_tp():
if self.skill_is_charged():
return CharacterCapabilities(can_teleport_natively=False, can_teleport_with_charges=True)
@@ -58,7 +88,7 @@ class IChar:
def on_capabilities_discovered(self, capabilities: CharacterCapabilities):
pass
def pick_up_item(self, pos: Tuple[float, float], item_name: str = None, prev_cast_start: float = 0):
def pick_up_item(self, pos: tuple[float, float], item_name: str = None, prev_cast_start: float = 0):
mouse.move(pos[0], pos[1])
time.sleep(0.1)
mouse.click(button="left")
@@ -67,7 +97,7 @@ class IChar:
def select_by_template(
self,
template_type: Union[str, List[str]],
template_type: str | list[str],
success_func: Callable = None,
timeout: float = 8,
threshold: float = 0.68,
@@ -87,10 +117,10 @@ class IChar:
keyboard.send("esc")
start = time.time()
while timeout is None or (time.time() - start) < timeout:
template_match = TemplateFinder().search(template_type, grab(), threshold=threshold, normalize_monitor=True)
template_match = template_finder.search(template_type, grab(), threshold=threshold)
if template_match.valid:
Logger.debug(f"Select {template_match.name} ({template_match.score*100:.1f}% confidence)")
mouse.move(*template_match.center)
mouse.move(*template_match.center_monitor)
wait(0.2, 0.3)
mouse.click(button="left")
# check the successfunction for 2 sec, if not found, try again
@@ -128,10 +158,9 @@ class IChar:
mouse.move(x + w/2, y + h / 2)
mouse.click("left")
wait(0.3)
match = TemplateFinder().search(skill_asset, grab(), threshold=0.84, roi=expanded_skill_roi)
match = template_finder.search(skill_asset, grab(), threshold=0.84, roi=expanded_skill_roi)
if match.valid:
x, y = convert_screen_to_monitor(match.center)
mouse.move(x, y)
mouse.move(*match.center_monitor)
wait(0.3)
keyboard.send(hotkey)
wait(0.3)
@@ -142,18 +171,24 @@ class IChar:
return self._remap_skill_hotkey(skill_asset, hotkey, Config().ui_roi["skill_right"], Config().ui_roi["skill_right_expanded"])
def select_tp(self):
return skills.select_tp(self._skill_hotkeys["teleport"])
return skills.select_tp(Config().char["teleport"])
def pre_move(self):
# if teleport hotkey is set and if teleport is not already selected
if self.capabilities.can_teleport_natively:
self.select_tp()
self._set_active_skill("right", "teleport")
def move(self, pos_monitor: Tuple[float, float], force_tp: bool = False, force_move: bool = False):
def move(self, pos_monitor: tuple[float, float], force_tp: bool = False, force_move: bool = False):
factor = Config().advanced_options["pathing_delay_factor"]
if self._skill_hotkeys["teleport"] and \
(force_tp or (skills.is_right_skill_selected(["TELE_ACTIVE"]) and \
skills.is_right_skill_active())):
if "teleport" in Config().char and Config().char["teleport"] and (
force_tp
or (
skills.is_right_skill_selected(["TELE_ACTIVE"])
and skills.is_right_skill_active()
)
):
self._set_active_skill("right", "teleport")
mouse.move(pos_monitor[0], pos_monitor[1], randomize=3, delay_factor=[factor*0.1, factor*0.14])
wait(0.012, 0.02)
mouse.click(button="right")
@@ -174,8 +209,8 @@ class IChar:
keyboard.send(Config().char["force_move"])
else:
mouse.click(button="left")
def walk(self, pos_monitor: Tuple[float, float], force_tp: bool = False, force_move: bool = False):
def walk(self, pos_monitor: tuple[float, float], force_tp: bool = False, force_move: bool = False):
factor = Config().advanced_options["pathing_delay_factor"]
# in case we want to walk we actually want to move a bit before the point cause d2r will always "overwalk"
pos_screen = convert_monitor_to_screen(pos_monitor)
@@ -191,7 +226,7 @@ class IChar:
if force_move:
keyboard.send(Config().char["force_move"])
else:
mouse.click(button="left")
mouse.click(button="left")
def tp_town(self):
# will check if tp is available and select the skill
@@ -221,7 +256,7 @@ class IChar:
consumables.increment_need("tp", 1)
wait(0.8, 1.3) # takes quite a while for tp to be visible
if (template_match := detect_screen_object(ScreenObjects.TownPortal)).valid:
pos = template_match.center
pos = template_match.center_monitor
pos = (pos[0], pos[1] + 30)
# Note: Template is top of portal, thus move the y-position a bit to the bottom
mouse.move(*pos, randomize=6, delay_factor=[0.9, 1.1])
@@ -244,8 +279,7 @@ class IChar:
while time.time() - start < 4:
keyboard.send(Config().char["weapon_switch"])
wait(0.3, 0.35)
keyboard.send(Config().char["battle_command"])
wait(0.1, 0.19)
self._select_skill(skill = "battle_command", mouse_click_type="right", delay=(0.1, 0.2))
if skills.is_right_skill_selected(["BC", "BO"]):
switch_sucess = True
break
@@ -257,8 +291,7 @@ class IChar:
# We switched succesfully, let's pre buff
mouse.click(button="right")
wait(self._cast_duration + 0.16, self._cast_duration + 0.18)
keyboard.send(Config().char["battle_orders"])
wait(0.1, 0.19)
self._select_skill(skill = "battle_orders", mouse_click_type="right", delay=(0.1, 0.2))
mouse.click(button="right")
wait(self._cast_duration + 0.16, self._cast_duration + 0.18)
@@ -275,6 +308,45 @@ class IChar:
Logger.warning("Failed to switch weapon, try again")
wait(0.5)
def vec_to_monitor(self, target):
circle_pos_screen = self._pather._adjust_abs_range_to_screen(target)
return convert_abs_to_monitor(circle_pos_screen)
def _lerp(self,a: float,b: float, f:float):
return a + f * (b - a)
def cast_in_arc(self, ability: str, cast_pos_abs: tuple[float, float] = [0,-100], time_in_s: float = 3, spread_deg: float = 10, hold=True):
#scale cast time by damage_scaling
time_in_s *= self.damage_scaling
Logger.debug(f'Casting {ability} for {time_in_s:.02f}s at {cast_pos_abs} with {spread_deg}°')
if not self._skill_hotkeys[ability]:
raise ValueError(f"You did not set {ability} hotkey!")
keyboard.send(Config().char["stand_still"], do_release=False)
self._select_skill(skill = ability, mouse_click_type="right", delay=(0.02, 0.08))
target = self.vec_to_monitor(arc_spread(cast_pos_abs, spread_deg=spread_deg))
mouse.move(*target,delay_factor=[0.95, 1.05])
if hold:
mouse.press(button="right")
start = time.time()
while (time.time() - start) < time_in_s:
target = self.vec_to_monitor(arc_spread(cast_pos_abs, spread_deg=spread_deg))
if hold:
mouse.move(*target,delay_factor=[3, 8])
if not hold:
mouse.move(*target,delay_factor=[.2, .4])
wait(0.02, 0.04)
mouse.press(button="right")
wait(0.02, 0.06)
mouse.release(button="right")
wait(self._cast_duration, self._cast_duration)
if hold:
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def pre_buff(self):
pass
@@ -319,7 +391,7 @@ if __name__ == "__main__":
keyboard.wait("f11")
from utils.misc import cut_roi
from config import Config
from template_finder import TemplateFinder
import template_finder
from ocr import Ocr
from ui import skills
+31 -40
View File
@@ -1,14 +1,14 @@
import keyboard
from utils.custom_mouse import mouse
from char import IChar
from template_finder import TemplateFinder
import template_finder
from template_finder import TemplateMatch
from pather import Pather
from logger import Logger
from screen import grab, convert_abs_to_monitor, convert_screen_to_abs
from config import Config
from utils.misc import wait, rotate_vec, unit_vector
import random
from typing import Tuple
from pather import Location, Pather
import numpy as np
import time
@@ -53,7 +53,7 @@ class Necro(IChar):
roi = [640,0,640,720]
img = grab()
template_match = TemplateFinder().search(
template_match = template_finder.search(
['SHENK_DEATH_1','SHENK_DEATH_2','SHENK_DEATH_3','SHENK_DEATH_4'],
img,
threshold=0.6,
@@ -71,7 +71,7 @@ class Necro(IChar):
img = grab()
max_rev = 13
template_match = TemplateFinder().search(
template_match = template_finder.search(
['REV_BASE'],
img,
threshold=0.6,
@@ -85,7 +85,7 @@ class Necro(IChar):
for count in range(1,max_rev):
rev_num = "REV_"+str(count)
template_match = TemplateFinder().search(
template_match = template_finder.search(
[rev_num],
img,
threshold=0.66,
@@ -100,7 +100,7 @@ class Necro(IChar):
img = grab()
max_skeles = 13
template_match = TemplateFinder().search(
template_match = template_finder.search(
['SKELE_BASE'],
img,
threshold=0.6,
@@ -114,7 +114,7 @@ class Necro(IChar):
for count in range(1,max_skeles):
skele_num = "SKELE_"+str(count)
template_match = TemplateFinder().search(
template_match = template_finder.search(
[skele_num],
img,
threshold=0.66,
@@ -128,7 +128,7 @@ class Necro(IChar):
roi = [15,14,400,45]
img = grab()
template_match = TemplateFinder().search(
template_match = template_finder.search(
['CLAY'],
img,
threshold=0.6,
@@ -150,7 +150,7 @@ class Necro(IChar):
''' print counts for summons '''
Logger.info('\33[31m'+"Summon status | "+str(self._skeletons_count)+"skele | "+str(self._revive_count)+" rev | "+self._golem_count+" |"+'\033[0m')
def _revive(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=12):
def _revive(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=12):
Logger.info('\033[94m'+"raise revive"+'\033[0m')
keyboard.send(Config().char["stand_still"], do_release=False)
for _ in range(cast_count):
@@ -179,7 +179,7 @@ class Necro(IChar):
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def _raise_skeleton(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=16):
def _raise_skeleton(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=16):
Logger.info('\033[94m'+"raise skeleton"+'\033[0m')
keyboard.send(Config().char["stand_still"], do_release=False)
for _ in range(cast_count):
@@ -208,7 +208,7 @@ class Necro(IChar):
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def _raise_mage(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=16):
def _raise_mage(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=16):
Logger.info('\033[94m'+"raise mage"+'\033[0m')
keyboard.send(Config().char["stand_still"], do_release=False)
for _ in range(cast_count):
@@ -284,7 +284,7 @@ class Necro(IChar):
def _left_attack(self, cast_pos_abs: Tuple[float, float], spray: int = 10):
def _left_attack(self, cast_pos_abs: tuple[float, float], spray: int = 10):
keyboard.send(Config().char["stand_still"], do_release=False)
if self._skill_hotkeys["skill_left"]:
keyboard.send(self._skill_hotkeys["skill_left"])
@@ -299,7 +299,7 @@ class Necro(IChar):
keyboard.send(Config().char["stand_still"], do_press=False)
def _left_attack_single(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=6):
def _left_attack_single(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=6):
keyboard.send(Config().char["stand_still"], do_release=False)
if self._skill_hotkeys["skill_left"]:
keyboard.send(self._skill_hotkeys["skill_left"])
@@ -314,7 +314,7 @@ class Necro(IChar):
keyboard.send(Config().char["stand_still"], do_press=False)
def _amp_dmg(self, cast_pos_abs: Tuple[float, float], spray: float = 10):
def _amp_dmg(self, cast_pos_abs: tuple[float, float], spray: float = 10):
if self._skill_hotkeys["amp_dmg"]:
keyboard.send(self._skill_hotkeys["amp_dmg"])
@@ -326,7 +326,7 @@ class Necro(IChar):
wait(0.25, 0.35)
mouse.release(button="right")
def _corpse_explosion(self, cast_pos_abs: Tuple[float, float], spray: int = 10,cast_count: int = 8):
def _corpse_explosion(self, cast_pos_abs: tuple[float, float], spray: int = 10,cast_count: int = 8):
keyboard.send(Config().char["stand_still"], do_release=False)
Logger.info('\033[93m'+"corpse explosion~> random cast"+'\033[0m')
for _ in range(cast_count):
@@ -341,11 +341,7 @@ class Necro(IChar):
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def _lerp(self,a: float,b: float, f:float):
return a + f * (b - a)
def _cast_circle(self, cast_dir: Tuple[float,float],cast_start_angle: float=0.0, cast_end_angle: float=90.0,cast_div: int = 10,cast_v_div: int=4,cast_spell: str='raise_skeleton',delay: float=1.0,offset: float=1.0):
def _cast_circle(self, cast_dir: tuple[float,float],cast_start_angle: float=0.0, cast_end_angle: float=90.0,cast_div: int = 10,cast_v_div: int=4,cast_spell: str='raise_skeleton',delay: float=1.0,offset: float=1.0):
Logger.info('\033[93m'+"circle cast ~>"+cast_spell+'\033[0m')
keyboard.send(Config().char["stand_still"], do_release=False)
keyboard.send(self._skill_hotkeys[cast_spell])
@@ -583,15 +579,14 @@ class Necro(IChar):
def stairs_S(self):
roi = [0,0,1280,720]
img = grab()
template_match = TemplateFinder().search(
template_match = template_finder.search(
["TRAV_S","TRAV_S_1"],
img,
threshold=0.4,
roi=roi,
normalize_monitor=True
roi=roi
)
if template_match.valid:
pos = template_match.center
pos = template_match.center_monitor
pos = (pos[0], pos[1] )
Logger.debug("mid point >> "+str(pos))
@@ -607,15 +602,14 @@ class Necro(IChar):
def stairs_F(self):
roi = [0,0,1280,720]
img = grab()
template_match = TemplateFinder().search(
template_match = template_finder.search(
["TRAV_F"],
img,
threshold=0.4,
roi=roi,
normalize_monitor=True
roi=roi
)
if template_match.valid:
pos = template_match.center
pos = template_match.center_monitor
pos = (pos[0], pos[1] )
Logger.debug("mid point >> "+str(pos))
@@ -632,15 +626,14 @@ class Necro(IChar):
def stairs_W(self):
roi = [0,0,1280,720]
img = grab()
template_match = TemplateFinder().search(
template_match = template_finder.search(
["TRAV_W","TRAV_W_1"],
img,
threshold=0.4,
roi=roi,
normalize_monitor=True
roi=roi
)
if template_match.valid:
pos = template_match.center
pos = template_match.center_monitor
pos = (pos[0], pos[1] )
Logger.debug("mid point >> "+str(pos))
@@ -661,15 +654,14 @@ class Necro(IChar):
roi = [0,0,1280,720]
img = grab()
template_match = TemplateFinder().search(
template_match = template_finder.search(
["TRAV_18"],
img,
threshold=0.3,
roi=roi,
normalize_monitor=True
roi=roi
)
if template_match.valid:
pos = template_match.center
pos = template_match.center_monitor
pos = (pos[0], pos[1] )
Logger.debug("DURANCE ENTRANCE >> "+str(pos))
# Note: Template is top of portal, thus move the y-position a bit to the bottom
@@ -699,15 +691,14 @@ class Necro(IChar):
mouse.move(*target, randomize=6, delay_factor=[0.9, 1.1])
img = grab()
template_match = TemplateFinder().search(
template_match = template_finder.search(
["TO_TRAV_0"],
img,
threshold=0.95,
roi=roi,
normalize_monitor=True
roi=roi
)
if template_match.valid:
pos = template_match.center
pos = template_match.center_monitor
pos = (pos[0], pos[1] )
Logger.debug("DURANCE EXIT >> "+str(pos))
# Note: Template is top of portal, thus move the y-position a bit to the bottom
+3
View File
@@ -0,0 +1,3 @@
from .paladin import Paladin
from .fohdin import FoHdin
from .hammerdin import Hammerdin
+761
View File
@@ -0,0 +1,761 @@
import random
import keyboard
import time
import numpy as np
from health_manager import get_panel_check_paused, set_panel_check_paused
from inventory.personal import inspect_items
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab, convert_abs_to_screen
from utils.custom_mouse import mouse
from char.paladin import Paladin
from logger import Logger
from config import Config
from utils.misc import wait
from pather import Location
from target_detect import get_visible_targets, TargetInfo, log_targets
class FoHdin(Paladin):
def __init__(self, *args, **kwargs):
Logger.info("Setting up FoHdin")
super().__init__(*args, **kwargs)
self._pather.adapt_path((Location.A3_TRAV_START, Location.A3_TRAV_CENTER_STAIRS), [220, 221, 222, 903, 904, 905, 906])
def _cast_foh(self, cast_pos_abs: tuple[float, float], spray: int = 10, min_duration: float = 0, aura: str = "conviction"):
return self._cast_skill_with_aura(skill_name = "foh", cast_pos_abs = cast_pos_abs, spray = spray, min_duration = min_duration, aura = aura)
def _cast_holy_bolt(self, cast_pos_abs: tuple[float, float], spray: int = 10, min_duration: float = 0, aura: str = "concentration"):
#if skill is bound : concentration, use concentration, otherwise move on with conviction. alternatively use redemption whilst holybolting. conviction does not help holy bolt (its magic damage)
return self._cast_skill_with_aura(skill_name = "holy_bolt", cast_pos_abs = cast_pos_abs, spray = spray, min_duration = min_duration, aura = aura)
def _cast_hammers(self, min_duration: float = 0, aura: str = "concentration"): #for nihlathak
return self._cast_skill_with_aura(skill_name = "blessed_hammer", spray = 0, min_duration = min_duration, aura = aura)
def _move_and_attack(self, abs_move: tuple[int, int], atk_len: float, aura: str = "concentration"): #for nihalthak
pos_m = convert_abs_to_monitor(abs_move)
self.pre_move()
self.move(pos_m, force_move=True)
self._cast_hammers(atk_len, aura=aura)
def _generic_foh_attack_sequence(
self,
default_target_abs: tuple[int, int] = (0, 0),
min_duration: float = 0,
max_duration: float = 15,
foh_to_holy_bolt_ratio: int = 3,
target_detect: bool = True,
default_spray: int = 50,
aura: str = ""
) -> bool:
start = time.time()
target_check_count = 1
foh_aura = aura if aura else "conviction"
holy_bolt_aura = aura if aura else "concentration"
while (elapsed := (time.time() - start)) <= max_duration:
cast_pos_abs = default_target_abs
spray = default_spray
# if targets are detected, switch to targeting with reduced spread rather than present default cast position and default spread
if target_detect and (targets := get_visible_targets()):
# log_targets(targets)
spray = 5
cast_pos_abs = targets[0].center_abs
# TODO: add delay between FOH casts--doesn't properly cast each FOH in sequence
# cast foh to holy bolt with preset ratio (e.g. 3 foh followed by 1 holy bolt if foh_to_holy_bolt_ratio = 3)
if foh_to_holy_bolt_ratio > 0 and not target_check_count % (foh_to_holy_bolt_ratio + 1):
self._cast_holy_bolt(cast_pos_abs, spray=spray, aura=holy_bolt_aura)
else:
self._cast_foh(cast_pos_abs, spray=spray, aura=foh_aura)
# if time > minimum and either targets aren't set or targets don't exist, exit loop
if elapsed > min_duration and (not target_detect or not targets):
break
target_check_count += 1
return True
#FOHdin Attack Sequence Optimized for trash
def _cs_attack_sequence(self, min_duration: float = Config().char["atk_len_cs_trashmobs"], max_duration: float = Config().char["atk_len_cs_trashmobs"] * 3):
self._generic_foh_attack_sequence(default_target_abs=(20,20), min_duration = min_duration, max_duration = max_duration, default_spray=100, foh_to_holy_bolt_ratio=6)
self._activate_redemption_aura()
def _cs_trash_mobs_attack_sequence(self, min_duration: float = 1.2, max_duration: float = Config().char["atk_len_cs_trashmobs"]):
self._cs_attack_sequence(min_duration = min_duration, max_duration = max_duration)
def _cs_pickit(self, skip_inspect: bool = False):
new_items = self._pickit.pick_up_items(self)
self._picked_up_items |= new_items
if not skip_inspect and new_items:
set_panel_check_paused(True)
inspect_items(grab(), ignore_sell=True)
set_panel_check_paused(False)
def kill_pindle(self) -> bool:
atk_len_dur = float(Config().char["atk_len_pindle"])
pindle_pos_abs = convert_screen_to_abs(Config().path["pindle_end"][0])
if self.capabilities.can_teleport_natively or self.capabilities.can_teleport_with_charges:
# Slightly retreating, so the Merc gets charged
if not self._pather.traverse_nodes([102], self, timeout=1.0, do_pre_move=False, force_move=True,force_tp=False, use_tp_charge=False):
return False
# Doing one Teleport to safe_dist to grab our Merc
Logger.debug("Teleporting backwards to let Pindle charge the MERC. Looks strange, but is intended!") #I would leave this message in, so users dont complain that there is a strange movement pattern.
if not self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=False, force_tp=True, use_tp_charge=True):
return False
# Slightly retreating, so the Merc gets charged
if not self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=False, force_move=True, force_tp=False, use_tp_charge=False):
return False
else:
keyboard.send(self._skill_hotkeys["conviction"])
wait(0.15)
self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=False)
cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9]
self._generic_foh_attack_sequence(default_target_abs=cast_pos_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, default_spray=11)
if self.capabilities.can_teleport_natively:
self._pather.traverse_nodes_fixed("pindle_end", self)
else:
keyboard.send(self._skill_hotkeys["redemption"])
wait(0.15)
self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.0, do_pre_move=False)
# Use target-based attack sequence one more time before pickit
self._generic_foh_attack_sequence(default_target_abs=cast_pos_abs, max_duration=atk_len_dur, default_spray=11)
self._activate_cleanse_redemption()
return True
def kill_council(self) -> bool:
atk_len_dur = float(Config().char["atk_len_trav"])
keyboard.send(self._skill_hotkeys["conviction"])
wait(.15)
# traverse to nodes and attack
nodes = [225, 226, 300]
for i, node in enumerate(nodes):
self._pather.traverse_nodes([node], self, timeout=3.2, do_pre_move = False, force_tp=(self.capabilities.can_teleport_natively or i > 0), use_tp_charge=(self.capabilities.can_teleport_natively or i > 0))
default_target_abs = self._pather.find_abs_node_pos(node, img := grab()) or self._pather.find_abs_node_pos(906, img) or (-50, -50)
self._generic_foh_attack_sequence(default_target_abs=default_target_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, default_spray=80)
# return to 226 and prepare for pickit
self._pather.traverse_nodes([226], self, timeout=3.2, do_pre_move = False, force_tp=True, use_tp_charge=True)
default_target_abs = self._pather.find_abs_node_pos(226, img := grab()) or self._pather.find_abs_node_pos(906, img) or (-50, -50)
self._generic_foh_attack_sequence(default_target_abs=default_target_abs, max_duration=atk_len_dur*3, default_spray=80)
self._activate_cleanse_redemption()
return True
def kill_eldritch(self) -> bool:
eld_pos_abs = convert_screen_to_abs(Config().path["eldritch_end"][0])
atk_len_dur = float(Config().char["atk_len_eldritch"])
self._generic_foh_attack_sequence(default_target_abs=eld_pos_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, default_spray=70)
# move to end node
pos_m = convert_abs_to_monitor((70, -200))
self.pre_move()
self.move(pos_m, force_move=True)
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=0.1)
# check mobs one more time before pickit
self._generic_foh_attack_sequence(default_target_abs=eld_pos_abs, max_duration=atk_len_dur, default_spray=70)
self._activate_cleanse_redemption()
return True
def kill_shenk(self):
atk_len_dur = float(Config().char["atk_len_shenk"])
# traverse to shenk
keyboard.send(self._skill_hotkeys["conviction"])
wait(0.15)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0, do_pre_move=False, force_tp=True, use_tp_charge=True)
wait(0.05, 0.1)
# bypass mob detect first
self._cast_foh((0, 0), spray=11, min_duration = 2, aura = "conviction")
# then do generic mob detect sequence
diff = atk_len_dur if atk_len_dur <= 2 else (atk_len_dur - 2)
self._generic_foh_attack_sequence(min_duration=atk_len_dur - diff, max_duration=atk_len_dur*3 - diff, default_spray=10, target_detect=False)
self._activate_cleanse_redemption()
return True
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
atk_len_dur = Config().char["atk_len_nihlathak"]
# Move close to nihlathak
self._pather.traverse_nodes(end_nodes, self, timeout=0.8, do_pre_move=False)
if self._select_skill("blessed_hammer"):
self._cast_hammers(atk_len_dur/4)
self._cast_hammers(2*atk_len_dur/4, "redemption")
self._move_and_attack((30, 15), atk_len_dur/4, "redemption")
else:
Logger.warning("FOHDin without blessed hammer is not very strong vs. Nihlathak!")
self._generic_foh_attack_sequence(min_duration=atk_len_dur/2, max_duration=atk_len_dur, default_spray=70, aura="redemption")
self._generic_foh_attack_sequence(min_duration=atk_len_dur/2, max_duration=atk_len_dur, default_spray=70, aura="redemption")
self._generic_foh_attack_sequence(max_duration=atk_len_dur*2, default_spray=70)
self._activate_cleanse_redemption()
return True
def kill_summoner(self) -> bool:
# Attack
atk_len_dur = Config().char["atk_len_arc"]
self._generic_foh_attack_sequence(min_duration=atk_len_dur, max_duration=atk_len_dur*2, default_spray=80)
self._activate_cleanse_redemption()
return True
########################################################################################
# Chaos Sanctuary, Trash, Seal Bosses (a = Vizier, b = De Seis, c = Infector) & Diablo #
########################################################################################
def kill_cs_trash(self, location:str) -> bool:
###########
# SEALDANCE
###########
#these locations have no traverses and are basically identical.
#if location in ("sealdance", "rof_01", "rof_02", "entrance_hall_01", "entrance_hall_02", "entrance1_01", "entrance1_02", "entrance1_03", "entrance1_04", "entrance2_01", "entrance2_03"):
match location:
case "sealdance": #if seal opening fails & trash needs to be cleared -> used at ANY seal
### APPROACH
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
################
# CLEAR CS TRASH
################
case "rof_01": #node 603 - outside CS in ROF
### APPROACH ###
if not self._pather.traverse_nodes([603], self, timeout=3): return False #calibrate after static path
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
if not self._pather.traverse_nodes([603], self): return False #calibrate after looting
case "rof_02": #node 604 - inside ROF
### APPROACH ###
if not self._pather.traverse_nodes([604], self, timeout=3): return False #threshold=0.8 (ex 601)
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
case "entrance_hall_01": ##static_path "diablo_entrance_hall_1", node 677, CS Entrance Hall1
### APPROACH ###
self._pather.traverse_nodes_fixed("diablo_entrance_hall_1", self) # 604 -> 671 Hall1
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
case "entrance_hall_02": #node 670,671, CS Entrance Hall1, CS Entrance Hall1
### APPROACH ###
if not self._pather.traverse_nodes([670], self): return False # pull top mobs 672 to bottom 670
self._pather.traverse_nodes_fixed("diablo_entrance_1_670_672", self) # 604 -> 671 Hall1
if not self._pather.traverse_nodes([670], self): return False # pull top mobs 672 to bottom 670
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
if not self._pather.traverse_nodes([671], self): return False # calibrate before static path
self._pather.traverse_nodes_fixed("diablo_entrance_hall_2", self) # 671 -> LC Hall2
# TRASH LAYOUT A
case "entrance1_01": #static_path "diablo_entrance_hall_2", Hall1 (before layout check)
### APPROACH ###
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
if not self._pather.traverse_nodes([673], self): return False # , timeout=3): # Re-adjust itself and continues to attack
case "entrance1_02": #node 673
### APPROACH ###
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
self._pather.traverse_nodes_fixed("diablo_entrance_1_1", self) # Moves char to postion close to node 674 continues to attack
if not self._pather.traverse_nodes([674], self): return False#, timeout=3)
case "entrance1_03": #node 674
### APPROACH ###
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
if not self._pather.traverse_nodes([675], self): return False#, timeout=3) # Re-adjust itself
self._pather.traverse_nodes_fixed("diablo_entrance_1_1", self) #static path to get to be able to spot 676
if not self._pather.traverse_nodes([676], self): return False#, timeout=3)
case "entrance1_04": #node 676- Hall3
### APPROACH ###
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
# TRASH LAYOUT B
case "entrance2_01": #static_path "diablo_entrance_hall_2"
### APPROACH ###
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
case "entrance2_02": #node 682
### APPROACH ###
#if not self._pather.traverse_nodes([682], self): return False # , timeout=3):
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
case "entrance2_03": #node 683
### APPROACH ###
#if not self._pather.traverse_nodes([682], self): return False # , timeout=3):
#self._pather.traverse_nodes_fixed("diablo_entrance2_1", self)
#if not self._pather.traverse_nodes([683], self): return False # , timeout=3):
self._pather.traverse_nodes_fixed("diablo_trash_b_hall2_605_top1", self) #pull mobs from top
wait (0.2, 0.5)
self._pather.traverse_nodes_fixed("diablo_trash_b_hall2_605_top2", self) #pull mobs from top
if not self._pather.traverse_nodes([605], self): return False#, timeout=3)
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
case "entrance2_04": #node 686 - Hall3
### APPROACH ###
if not self._pather.traverse_nodes([605], self): return False#, timeout=3)
#if not self._pather.traverse_nodes([683,684], self): return False#, timeout=3)
#self._pather.traverse_nodes_fixed("diablo_entrance2_2", self)
#if not self._pather.traverse_nodes([685,686], self): return False#, timeout=3)
self._pather.traverse_nodes_fixed("diablo_trash_b_hall2_605_hall3", self)
if not self._pather.traverse_nodes([609], self): return False#, timeout=3)
self._pather.traverse_nodes_fixed("diablo_trash_b_hall3_pull_609", self)
if not self._pather.traverse_nodes([609], self): return False#, timeout=3)
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit(skip_inspect=True)
if not self._pather.traverse_nodes([609], self): return False#, timeout=3)
self._cs_pickit()
if not self._pather.traverse_nodes([609], self): return False#, timeout=3)
####################
# PENT TRASH TO SEAL
####################
case "dia_trash_a" | "dia_trash_b" | "dia_trash_c": #trash before between Pentagramm and Seal A Layoutcheck
### APPROACH ###
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
###############
# LAYOUT CHECKS
###############
case "layoutcheck_a" | "layoutcheck_b" | "layoutcheck_c": #layout check seal A, node 619 A1-L, node 620 A2-Y
### APPROACH ###
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
##################
# PENT BEFORE SEAL
##################
case "pent_before_a": #node 602, pentagram, before CTA buff & depature to layout check - not needed when trash is skipped & seals run in right order
### APPROACH ###
### ATTACK ###
### LOOT ###
Logger.debug("No attack choreography available in fohdin.py for this node " + location + " - skipping to shorten run.")
case "pent_before_b" | "pent_before_c": #node 602, pentagram, before CTA buff & depature to layout check
### APPROACH ###
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
###########
# SEAL A1-L
###########
case "A1-L_01": #node 611 seal layout A1-L: safe_dist
### APPROACH ###
if not self._pather.traverse_nodes([611], self): return False # , timeout=3):
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
# we loot at boss
case "A1-L_02": #node 612 seal layout A1-L: center
### APPROACH ###
if not self._pather.traverse_nodes([612], self): return False # , timeout=3):
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
# we loot at boss
case "A1-L_03": #node 613 seal layout A1-L: fake_seal
### APPROACH ###
if not self._pather.traverse_nodes([613], self): return False # , timeout=3):
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
case "A1-L_seal1": #node 613 seal layout A1-L: fake_seal
### APPROACH ###
self._cs_pickit()
if not self._pather.traverse_nodes([614], self): return False
### ATTACK ###
self._activate_redemption_aura()
### LOOT ###
# we loot at boss
case "A1-L_seal2": #node 614 seal layout A1-L: boss_seal
### APPROACH ###
if not self._pather.traverse_nodes([613, 615], self): return False # , timeout=3):
### ATTACK ###
self._activate_redemption_aura()
### LOOT ###
# we loot at boss
###########
# SEAL A2-Y
###########
case "A2-Y_01": #node 622 seal layout A2-Y: safe_dist
### APPROACH ###
if not self._pather.traverse_nodes_fixed("dia_a2y_hop_622", self): return False
Logger.debug("A2-Y: Hop!")
#if not self._pather.traverse_nodes([622], self): return False # , timeout=3):
if not self._pather.traverse_nodes([622], self): return False
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
# we loot at boss
case "A2-Y_02": #node 623 seal layout A2-Y: center
### APPROACH ###
# if not self._pather.traverse_nodes([623,624], self): return False #
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
# we loot at boss
case "A2-Y_03": #skipped
### APPROACH ###
### ATTACK ###
### LOOT ###
# we loot at boss
Logger.debug("No attack choreography available in fohdin.py for this node " + location + " - skipping to shorten run.")
case "A2-Y_seal1": #node 625 seal layout A2-Y: fake seal
### APPROACH ###
### ATTACK ###
### LOOT ###
# we loot at boss
if not self._pather.traverse_nodes([625], self): return False # , timeout=3):
self._activate_redemption_aura()
case "A2-Y_seal2":
### APPROACH ###
### ATTACK ###
### LOOT ###
# we loot at boss
self._pather.traverse_nodes_fixed("dia_a2y_sealfake_sealboss", self) #instead of traversing node 626 which causes issues
self._activate_redemption_aura()
###########
# SEAL B1-S
###########
case "B1-S_01" | "B1-S_02" | "B1-S_03":
### APPROACH ###
### ATTACK ###
### LOOT ###
# we loot at boss
Logger.debug("No attack choreography available in fohdin.py for this node " + location + " - skipping to shorten run.")
case "B1-S_seal2": #B only has 1 seal, which is the boss seal = seal2
### APPROACH ###
if not self._pather.traverse_nodes([634], self): return False # , timeout=3):
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
###########
# SEAL B2-U
###########
case "B2-U_01" | "B2-U_02" | "B2-U_03":
### APPROACH ###
### ATTACK ###
### LOOT ###
# we loot at boss
Logger.debug("No attack choreography available in fohdin.py for this node " + location + " - skipping to shorten run.")
case "B2-U_seal2": #B only has 1 seal, which is the boss seal = seal2
### APPROACH ###
self._pather.traverse_nodes_fixed("dia_b2u_bold_seal", self)
if not self._pather.traverse_nodes([644], self): return False # , timeout=3):
### ATTACK ###
self._activate_redemption_aura()
### LOOT ###
# we loot at boss
###########
# SEAL C1-F
###########
case "C1-F_01" | "C1-F_02" | "C1-F_03":
### APPROACH ###
### ATTACK ###
### LOOT ###
# we loot at boss
Logger.debug("No attack choreography available in fohdin.py for this node " + location + " - skipping to shorten run.")
case "C1-F_seal1":
### APPROACH ###
wait(0.1,0.3)
self._pather.traverse_nodes_fixed("dia_c1f_hop_fakeseal", self)
wait(0.1,0.3)
if not self._pather.traverse_nodes([655], self): return False # , timeout=3):
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
if not self._pather.traverse_nodes([655], self): return False # , timeout=3):
self._activate_redemption_aura()
case "C1-F_seal2":
### APPROACH ###
self._pather.traverse_nodes_fixed("dia_c1f_654_651", self)
if not self._pather.traverse_nodes([652], self): return False # , timeout=3):
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
if not self._pather.traverse_nodes([652], self): return False # , timeout=3):
self._activate_redemption_aura()
###########
# SEAL C2-G
###########
case "C2-G_01" | "C2-G_02" | "C2-G_03": #skipped
### APPROACH ###
### ATTACK ###
### LOOT ###
# we loot at boss
Logger.debug("No attack choreography available in fohdin.py for this node " + location + " - skipping to shorten run.")
case "C2-G_seal1":
### APPROACH ###
if not self._pather.traverse_nodes([663, 662], self) or not self._pather.traverse_nodes_fixed("dia_c2g_lc_661", self):
return False
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
if not self._pather.traverse_nodes([662], self): return False
self._activate_redemption_aura()
case "C2-G_seal2":
### APPROACH ###
# Killing infector here, because for C2G its the only seal where a bossfight occures BETWEEN opening seals
seal_layout="C2-G"
if not self._pather.traverse_nodes([662], self) or not self._pather.traverse_nodes_fixed("dia_c2g_663", self):
return False
### ATTACK ###
atk_dur_min = Config().char["atk_len_diablo_infector"]
atk_dur_max = atk_dur_min * 3
Logger.debug(seal_layout + ": Attacking Infector at position 1/2")
self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max)
if not self._pather.traverse_nodes([663], self): return False # , timeout=3):
Logger.debug(seal_layout + ": Attacking Infector at position 2/2")
self._cs_attack_sequence(min_duration=2, max_duration=atk_dur_max)
### LOOT ###
self._cs_pickit(skip_inspect=True) # inspect on other
if not self._pather.traverse_nodes([664, 665], self): return False # , timeout=3):
case _:
### APPROACH ###
Logger.error("No location argument given for kill_cs_trash(" + location + "), should not happen")
### ATTACK ###
self._cs_trash_mobs_attack_sequence()
### LOOT ###
self._cs_pickit()
return True
def kill_vizier(self, seal_layout:str) -> bool:
atk_dur_min = Config().char["atk_len_diablo_vizier"]
atk_dur_max = atk_dur_min * 3
match seal_layout:
case "A1-L":
### APPROACH ###
if not self._pather.traverse_nodes([612], self): return False # , timeout=3):
### ATTACK ###
Logger.debug(seal_layout + ": Attacking Vizier at position 1/2")
self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max)
Logger.debug(seal_layout + ": Attacking Vizier at position 2/2")
self._pather.traverse_nodes([611], self, timeout=3)
self._cs_attack_sequence(min_duration=2, max_duration=atk_dur_max)
### LOOT ###
self._cs_pickit(skip_inspect=True)
if not self._pather.traverse_nodes([612], self): return False # , timeout=3):
self._activate_cleanse_redemption()
self._cs_pickit()
if not self._pather.traverse_nodes([612], self): return False # , timeout=3): # recalibrate after loot
case "A2-Y":
### APPROACH ###
if not self._pather.traverse_nodes([627, 622], self): return False # , timeout=3):
### ATTACK ###
Logger.debug(seal_layout + ": Attacking Vizier at position 1/3")
self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max)
Logger.debug(seal_layout + ": Attacking Vizier at position 2/3")
self._pather.traverse_nodes([623], self, timeout=3)
self._cs_attack_sequence(min_duration=1.5, max_duration=atk_dur_max)
Logger.debug(seal_layout + ": Attacking Vizier at position 3/3")
if not self._pather.traverse_nodes([624], self): return False
self._cs_attack_sequence(min_duration=1.5, max_duration=atk_dur_max)
### LOOT ###
self._cs_pickit(skip_inspect=True)
if not self._pather.traverse_nodes([624], self): return False
if not self._pather.traverse_nodes_fixed("dia_a2y_hop_622", self): return False
Logger.debug(seal_layout + ": Hop!")
self._activate_cleanse_redemption()
if not self._pather.traverse_nodes([622], self): return False #, timeout=3):
self._activate_redemption_aura()
self._cs_pickit()
if not self._pather.traverse_nodes([622], self): return False # , timeout=3): #recalibrate after loot
case _:
Logger.warning(seal_layout + ": Invalid location for kill_vizier("+ seal_layout +"), should not happen.")
return False
return True
def kill_deseis(self, seal_layout:str) -> bool:
atk_dur_min = Config().char["atk_len_diablo_deseis"]
atk_dur_max = atk_dur_min * 3
match seal_layout:
case "B1-S":
### APPROACH ###
self._pather.traverse_nodes_fixed("dia_b1s_seal_deseis_foh", self)
nodes = [631]
### ATTACK ###
Logger.debug(f"{seal_layout}: Attacking De Seis at position 1/{len(nodes)+1}")
self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max)
for i, node in enumerate(nodes):
Logger.debug(f"{seal_layout}: Attacking De Seis at position {i+2}/{len(nodes)+1}")
self._pather.traverse_nodes([node], self, timeout=3)
self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max)
self._activate_redemption_aura(delay=[1, 2])
#if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_check_deseis_dead" + seal_layout + "_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
### LOOT ###
self._cs_pickit()
case "B2-U":
### APPROACH ###
self._pather.traverse_nodes_fixed("dia_b2u_644_646", self) # We try to breaking line of sight, sometimes makes De Seis walk into the hammercloud. A better attack sequence here could make sense.
#self._pather.traverse_nodes_fixed("dia_b2u_seal_deseis", self) # We try to breaking line of sight, sometimes makes De Seis walk into the hammercloud. A better attack sequence here could make sense.
nodes = [646, 641]
### ATTACK ###
Logger.debug(seal_layout + ": Attacking De Seis at position 1/{len(nodes)+1}")
self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max)
for i, node in enumerate(nodes):
Logger.debug(f"{seal_layout}: Attacking De Seis at position {i+2}/{len(nodes)+1}")
self._pather.traverse_nodes([node], self, timeout=3)
self._cs_attack_sequence(min_duration=2, max_duration=atk_dur_max)
#if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_check_deseis_dead" + seal_layout + "_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
### LOOT ###
self._cs_pickit(skip_inspect=True)
if not self._pather.traverse_nodes([641], self): return False # , timeout=3):
if not self._pather.traverse_nodes([646], self): return False # , timeout=3):
self._cs_pickit(skip_inspect=True)
if not self._pather.traverse_nodes([646], self): return False # , timeout=3):
if not self._pather.traverse_nodes([640], self): return False # , timeout=3):
self._cs_pickit()
case _:
Logger.warning(seal_layout + ": Invalid location for kill_deseis("+ seal_layout +"), should not happen.")
return False
return True
def kill_infector(self, seal_layout:str) -> bool:
atk_dur_min = Config().char["atk_len_diablo_infector"]
atk_dur_max = atk_dur_min * 3
match seal_layout:
case "C1-F":
### APPROACH ###
### ATTACK ###
Logger.debug(seal_layout + ": Attacking Infector at position 1/1")
self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max)
self._pather.traverse_nodes_fixed("dia_c1f_652", self)
Logger.debug(seal_layout + ": Attacking Infector at position 2/2")
self._cs_attack_sequence(min_duration=2, max_duration=atk_dur_max)
### LOOT ###
self._cs_pickit()
case "C2-G":
### APPROACH ###
if not self._pather.traverse_nodes([665], self): return False # , timeout=3):
### ATTACK ###
Logger.debug(seal_layout + ": Attacking Infector at position 1/2")
self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max)
if not self._pather.traverse_nodes([663], self): return False # , timeout=3):
Logger.debug(seal_layout + ": Attacking Infector at position 2/2")
self._cs_attack_sequence(min_duration=2, max_duration=atk_dur_max)
### LOOT ###
self._cs_pickit()
case _:
Logger.warning(seal_layout + ": Invalid location for kill_infector("+ seal_layout +"), should not happen.")
return False
return True
#no aura effect on dia. not good for mob detection
def kill_diablo(self) -> bool:
### APPROACH ###
### ATTACK ###
atk_len_dur = float(Config().char["atk_len_diablo"])
Logger.debug("Attacking Diablo at position 1/1")
diablo_abs = [100,-100] #hardcoded dia pos.
self._generic_foh_attack_sequence(default_target_abs=diablo_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, aura="concentration", foh_to_holy_bolt_ratio=2)
self._activate_cleanse_redemption()
### LOOT ###
#self._cs_pickit()
return True
File diff suppressed because it is too large Load Diff
+101
View File
@@ -0,0 +1,101 @@
import keyboard
from ui import skills
import time
import random
from utils.custom_mouse import mouse
from char import IChar, CharacterCapabilities
from pather import Pather
from logger import Logger
from config import Config
from utils.misc import wait
from screen import convert_abs_to_screen, convert_abs_to_monitor
from pather import Pather
#import cv2 #for Diablo
from item.pickit import PickIt #for Diablo
class Paladin(IChar):
def __init__(self, skill_hotkeys: dict, pather: Pather, pickit: PickIt):
Logger.info("Setting up Paladin")
super().__init__(skill_hotkeys)
self._pather = pather
self._pickit = pickit #for Diablo
self._picked_up_items = False #for Diablo
def pre_buff(self):
if Config().char["cta_available"]:
self._pre_buff_cta()
keyboard.send(self._skill_hotkeys["holy_shield"])
wait(0.04, 0.1)
mouse.click(button="right")
wait(self._cast_duration, self._cast_duration + 0.06)
def pre_move(self):
# select teleport if available
super().pre_move()
# in case teleport hotkey is not set or teleport can not be used, use vigor if set
should_cast_vigor = self._skill_hotkeys["vigor"] and not skills.is_right_skill_selected(["VIGOR"])
can_teleport = self.capabilities.can_teleport_natively and skills.is_right_skill_active()
if should_cast_vigor and not can_teleport:
keyboard.send(self._skill_hotkeys["vigor"])
wait(0.15, 0.25)
def _log_cast(self, skill_name: str, cast_pos_abs: tuple[float, float], spray: int, min_duration: float, aura: str):
msg = f"Casting skill {skill_name}"
if cast_pos_abs:
msg += f" at screen coordinate {convert_abs_to_screen(cast_pos_abs)}"
if spray:
msg += f" with spray of {spray}"
if min_duration:
msg += f" for {round(min_duration, 1)}s"
if aura:
msg += f" with {aura} active"
Logger.debug(msg)
def _click_cast(self, cast_pos_abs: tuple[float, float], spray: int, mouse_click_type: str = "left"):
if cast_pos_abs:
x = cast_pos_abs[0]
y = cast_pos_abs[1]
if spray:
x += (random.random() * 2 * spray - spray)
y += (random.random() * 2 * spray - spray)
pos_m = convert_abs_to_monitor((x, y))
mouse.move(*pos_m, delay_factor=[0.1, 0.2])
wait(0.06, 0.08)
mouse.press(button = mouse_click_type)
wait(0.06, 0.08)
mouse.release(button = mouse_click_type)
def _cast_skill_with_aura(self, skill_name: str, cast_pos_abs: tuple[float, float] = None, spray: int = 0, min_duration: float = 0, aura: str = ""):
#self._log_cast(skill_name, cast_pos_abs, spray, min_duration, aura)
# set aura if needed
if aura:
self._select_skill(aura, mouse_click_type = "right")
# ensure character stands still
keyboard.send(Config().char["stand_still"], do_release=False)
# set left hand skill
self._select_skill(skill_name, mouse_click_type = "left")
wait(0.05, 0.1)
# cast left hand skill
start = time.time()
if min_duration:
while (time.time() - start) <= min_duration:
self._click_cast(cast_pos_abs, spray)
else:
self._click_cast(cast_pos_abs, spray)
# release stand still key
keyboard.send(Config().char["stand_still"], do_press=False)
def _activate_redemption_aura(self, delay = [0.6, 0.8]):
self._select_skill("redemption", delay=delay)
def _activate_cleanse_aura(self, delay = [0.3, 0.4]):
self._select_skill("cleansing", delay=delay)
def _activate_cleanse_redemption(self):
self._activate_cleanse_aura()
self._activate_redemption_aura()
+24 -29
View File
@@ -1,20 +1,18 @@
import keyboard
from utils.custom_mouse import mouse
from char import IChar
from template_finder import TemplateFinder
import template_finder
from pather import Pather
from logger import Logger
from screen import grab, convert_abs_to_monitor, convert_screen_to_abs
from screen import grab
from config import Config
from utils.misc import wait, rotate_vec, unit_vector
import random
from typing import Tuple
from pather import Location, Pather
import screen as screen
import numpy as np
import time
import os
from ui_manager import ScreenObjects
class Poison_Necro(IChar):
def __init__(self, skill_hotkeys: dict, pather: Pather):
@@ -29,7 +27,7 @@ class Poison_Necro(IChar):
self._pather.offset_node(102, [15, 0])
self._pather.offset_node(103, [15, 0])
self._pather.offset_node(101, [100,-5])
#Diablo
self._pather.offset_node(644, [150, -70])
self._pather.offset_node(610620, [50, 50])
@@ -49,7 +47,7 @@ class Poison_Necro(IChar):
roi = [640,0,640,720]
img = grab()
template_match = TemplateFinder().search(
template_match = template_finder.search(
['SHENK_DEATH_1','SHENK_DEATH_2','SHENK_DEATH_3','SHENK_DEATH_4'],
img,
threshold=0.6,
@@ -67,7 +65,7 @@ class Poison_Necro(IChar):
img = grab()
max_rev = 13
template_match = TemplateFinder().search(
template_match = template_finder.search(
['REV_BASE'],
img,
threshold=0.6,
@@ -81,7 +79,7 @@ class Poison_Necro(IChar):
for count in range(1,max_rev):
rev_num = "REV_"+str(count)
template_match = TemplateFinder().search(
template_match = template_finder.search(
[rev_num],
img,
threshold=0.66,
@@ -102,14 +100,14 @@ class Poison_Necro(IChar):
wait(0.03, 0.04)
mouse.press(button="right")
wait(0.12, 0.2)
mouse.release(button="right")
mouse.release(button="right")
def _count_skeletons(self):
roi = [15,14,400,45]
img = grab()
max_skeles = 13
template_match = TemplateFinder().search(
template_match = template_finder.search(
['SKELE_BASE'],
img,
threshold=0.6,
@@ -123,7 +121,7 @@ class Poison_Necro(IChar):
for count in range(1,max_skeles):
skele_num = "SKELE_"+str(count)
template_match = TemplateFinder().search(
template_match = template_finder.search(
[skele_num],
img,
threshold=0.66,
@@ -137,7 +135,7 @@ class Poison_Necro(IChar):
roi = [15,14,400,45]
img = grab()
template_match = TemplateFinder().search(
template_match = template_finder.search(
['CLAY'],
img,
threshold=0.6,
@@ -159,7 +157,7 @@ class Poison_Necro(IChar):
''' print counts for summons '''
Logger.info('\33[31m'+"Summon status | "+str(self._skeletons_count)+"skele | "+str(self._revive_count)+" rev | "+self._golem_count+" |"+'\033[0m')
def _revive(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=12):
def _revive(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=12):
Logger.info('\033[94m'+"raise revive"+'\033[0m')
keyboard.send(Config().char["stand_still"], do_release=False)
for _ in range(cast_count):
@@ -188,7 +186,7 @@ class Poison_Necro(IChar):
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def _raise_skeleton(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=16):
def _raise_skeleton(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=16):
Logger.info('\033[94m'+"raise skeleton"+'\033[0m')
keyboard.send(Config().char["stand_still"], do_release=False)
for _ in range(cast_count):
@@ -217,7 +215,7 @@ class Poison_Necro(IChar):
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def _raise_mage(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=16):
def _raise_mage(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=16):
Logger.info('\033[94m'+"raise mage"+'\033[0m')
keyboard.send(Config().char["stand_still"], do_release=False)
for _ in range(cast_count):
@@ -293,7 +291,7 @@ class Poison_Necro(IChar):
def _left_attack(self, cast_pos_abs: Tuple[float, float], spray: int = 10):
def _left_attack(self, cast_pos_abs: tuple[float, float], spray: int = 10):
keyboard.send(Config().char["stand_still"], do_release=False)
if self._skill_hotkeys["skill_left"]:
keyboard.send(self._skill_hotkeys["skill_left"])
@@ -308,7 +306,7 @@ class Poison_Necro(IChar):
keyboard.send(Config().char["stand_still"], do_press=False)
def _left_attack_single(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=6):
def _left_attack_single(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=6):
keyboard.send(Config().char["stand_still"], do_release=False)
if self._skill_hotkeys["skill_left"]:
keyboard.send(self._skill_hotkeys["skill_left"])
@@ -323,7 +321,7 @@ class Poison_Necro(IChar):
keyboard.send(Config().char["stand_still"], do_press=False)
def _amp_dmg(self, cast_pos_abs: Tuple[float, float], spray: float = 10):
def _amp_dmg(self, cast_pos_abs: tuple[float, float], spray: float = 10):
if self._skill_hotkeys["amp_dmg"]:
keyboard.send(self._skill_hotkeys["amp_dmg"])
@@ -335,7 +333,7 @@ class Poison_Necro(IChar):
wait(0.25, 0.35)
mouse.release(button="right")
def _lower_res(self, cast_pos_abs: Tuple[float, float], spray: float = 10):
def _lower_res(self, cast_pos_abs: tuple[float, float], spray: float = 10):
if self._skill_hotkeys["lower_res"]:
keyboard.send(self._skill_hotkeys["lower_res"])
@@ -345,9 +343,9 @@ class Poison_Necro(IChar):
mouse.move(*cast_pos_monitor)
mouse.press(button="right")
wait(0.25, 0.35)
mouse.release(button="right")
mouse.release(button="right")
def _corpse_explosion(self, cast_pos_abs: Tuple[float, float], spray: int = 10,cast_count: int = 8):
def _corpse_explosion(self, cast_pos_abs: tuple[float, float], spray: int = 10,cast_count: int = 8):
keyboard.send(Config().char["stand_still"], do_release=False)
Logger.info('\033[93m'+"corpse explosion~> random cast"+'\033[0m')
for _ in range(cast_count):
@@ -363,10 +361,7 @@ class Poison_Necro(IChar):
keyboard.send(Config().char["stand_still"], do_press=False)
def _lerp(self,a: float,b: float, f:float):
return a + f * (b - a)
def _cast_circle(self, cast_dir: Tuple[float,float],cast_start_angle: float=0.0, cast_end_angle: float=90.0,cast_div: int = 10,cast_v_div: int=4,cast_spell: str='raise_skeleton',delay: float=1.0,offset: float=1.0):
def _cast_circle(self, cast_dir: tuple[float,float],cast_start_angle: float=0.0, cast_end_angle: float=90.0,cast_div: int = 10,cast_v_div: int=4,cast_spell: str='raise_skeleton',delay: float=1.0,offset: float=1.0):
Logger.info('\033[93m'+"circle cast ~>"+cast_spell+'\033[0m')
keyboard.send(Config().char["stand_still"], do_release=False)
keyboard.send(self._skill_hotkeys[cast_spell])
@@ -462,11 +457,11 @@ class Poison_Necro(IChar):
pos_m = screen.convert_abs_to_monitor((0, -200))
self.pre_move()
self.move(pos_m, force_move=True)
self._pather.traverse_nodes([229], self, timeout=2.5, force_tp=True, use_tp_charge=True)
self._pather.traverse_nodes([229], self, timeout=2.5, force_tp=True, use_tp_charge=True)
pos_m = screen.convert_abs_to_monitor((50, 0))
self.walk(pos_m, force_move=True)
#self._lower_res((-50, 0), spray=10)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
self.poison_nova(2.0)
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=9,cast_v_div=3,cast_spell='raise_skeleton',delay=1.2,offset=.8)
pos_m = screen.convert_abs_to_monitor((200, 50))
@@ -529,7 +524,7 @@ class Poison_Necro(IChar):
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=7200,cast_div=2,cast_v_div=2,cast_spell='corpse_explosion',delay=3.0,offset=1.8)
wait(self._cast_duration, self._cast_duration +.2)
self.poison_nova(3.0)
return True
return True
def kill_summoner(self) -> bool:
# Attack
@@ -543,7 +538,7 @@ class Poison_Necro(IChar):
self.move(pos_m, force_move=True)
wait(self._cast_duration, self._cast_duration + 0.2)
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_mage',delay=1.2,offset=.8)
return True
return True
if __name__ == "__main__":
+1
View File
@@ -2,3 +2,4 @@ from .sorceress import Sorceress
from .light_sorc import LightSorc
from .blizz_sorc import BlizzSorc
from .nova_sorc import NovaSorc
from .hydra_sorc import HydraSorc
+8 -9
View File
@@ -8,7 +8,7 @@ from pather import Location
import numpy as np
from screen import convert_abs_to_monitor, grab, convert_screen_to_abs
from config import Config
from template_finder import TemplateFinder
import template_finder
class BlizzSorc(Sorceress):
def __init__(self, *args, **kwargs):
@@ -73,7 +73,7 @@ class BlizzSorc(Sorceress):
self.move(pos_m, force_move=True)
self._blizzard((-50, -50), spray=10)
self._cast_static()
wait(0.75)
wait(0.75)
#move down
pos_m = convert_abs_to_monitor((0, 85))
self.pre_move()
@@ -225,21 +225,20 @@ class BlizzSorc(Sorceress):
nihlathak_pos_abs = self._pather.find_abs_node_pos(end_nodes[-1], grab())
if nihlathak_pos_abs is not None:
cast_pos_abs = np.array([nihlathak_pos_abs[0] * 1.0, nihlathak_pos_abs[1] * 1.0])
wait(0.8)
wait(0.5)
self._blizzard(cast_pos_abs, spray=0)
wait(0.3)
is_nihl = TemplateFinder().search(["NIHL_BAR"], grab(), threshold=0.8, roi=Config().ui_roi["enemy_info"]).valid
nihl_immune = TemplateFinder().search(["COLD_IMMUNE","COLD_IMMUNES"], grab(), threshold=0.8, roi=Config().ui_roi["enemy_info"]).valid
wait(0.2)
is_nihl = template_finder.search(["NIHL_BAR"], grab(), threshold=0.8, roi=Config().ui_roi["enemy_info"]).valid
nihl_immune = template_finder.search(["COLD_IMMUNE","COLD_IMMUNES"], grab(), threshold=0.8, roi=Config().ui_roi["enemy_info"]).valid
if is_nihl:
Logger.info("Found him!")
if nihl_immune:
Logger.info("Cold Immune! - Exiting")
return True
wait(0.8)
wait(0.5)
self._cast_static()
self._blizzard(cast_pos_abs, spray=15)
# Move to items
wait(1.3)
self._pather.traverse_nodes(end_nodes, self, timeout=0.8)
return True
@@ -258,7 +257,7 @@ class BlizzSorc(Sorceress):
if __name__ == "__main__":
import os
import keyboard
from template_finder import TemplateFinder
import template_finder
from pather import Pather
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
+100
View File
@@ -0,0 +1,100 @@
import time
import keyboard
from char.sorceress import Sorceress
from utils.custom_mouse import mouse
from logger import Logger
from utils.misc import wait
import random
from pather import Location
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab
from config import Config
class HydraSorc(Sorceress):
def __init__(self, *args, **kwargs):
Logger.info("Setting up HydraSorc Sorc")
super().__init__(*args, **kwargs)
self._hydra_time = None
def _alt_attack(self, cast_pos_abs: tuple[float, float], delay: tuple[float, float] = (0.16, 0.23), spray: float = 10):
keyboard.send(Config().char["stand_still"], do_release=False)
if self._skill_hotkeys["alt_attack"]:
keyboard.send(self._skill_hotkeys["alt_attack"])
for _ in range(5):
x = cast_pos_abs[0] + (random.random() * 2 * spray - spray)
y = cast_pos_abs[1] + (random.random() * 2 * spray - spray)
cast_pos_monitor = convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
mouse.press(button="right")
wait(delay[0], delay[1])
mouse.release(button="right")
keyboard.send(Config().char["stand_still"], do_press=False)
def _hydra(self, cast_pos_abs: tuple[float, float], spray: float = 10):
if self._hydra_time is None or time.time() - self._hydra_time > 10:
if not self._skill_hotkeys["hydra"]:
raise ValueError("You did not set a hotkey for hydra!")
keyboard.send(self._skill_hotkeys["hydra"])
self._hydra_time = time.time()
x = cast_pos_abs[0] + (random.random() * 2 * spray - spray)
y = cast_pos_abs[1] + (random.random() * 2 * spray - spray)
cast_pos_monitor = convert_abs_to_monitor((x, y))
mouse.move(*cast_pos_monitor)
mouse.press(button="right")
wait(2,3)
mouse.release(button="right")
def kill_pindle(self) -> bool:
pindle_pos_abs = convert_screen_to_abs(Config().path["pindle_end"][0])
cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9]
for _ in range(int(Config().char["atk_len_pindle"])):
self._hydra(cast_pos_abs, spray=0)
self._alt_attack(cast_pos_abs, spray=11)
# Move to items
wait(self._cast_duration, self._cast_duration + 0.2)
self._pather.traverse_nodes_fixed("pindle_end", self)
self._hydra_time = None
return True
def kill_eldritch(self) -> bool:
eld_pos_abs = convert_screen_to_abs(Config().path["eldritch_end"][0])
cast_pos_abs = [eld_pos_abs[0] * 0.9, eld_pos_abs[1] * 0.9]
for _ in range(int(Config().char["atk_len_eldritch"])):
self._hydra(cast_pos_abs, spray=0)
self._alt_attack(cast_pos_abs, spray=90)
# Move to items
wait(self._cast_duration, self._cast_duration + 0.2)
self._pather.traverse_nodes_fixed("eldritch_end", self)
self._hydra_time = None
return True
def kill_shenk(self) -> bool:
shenk_pos_abs = self._pather.find_abs_node_pos(149, grab())
if shenk_pos_abs is None:
shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0])
cast_pos_abs = [shenk_pos_abs[0] * 0.9, shenk_pos_abs[1] * 0.9]
for _ in range(int(Config().char["atk_len_shenk"] * 0.5)):
self._hydra(cast_pos_abs, spray=0)
self._alt_attack(cast_pos_abs, spray=90)
pos_m = convert_abs_to_monitor((150, 50))
self.pre_move()
self.move(pos_m, force_move=True)
shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0])
cast_pos_abs = [shenk_pos_abs[0] * 0.9, shenk_pos_abs[1] * 0.9]
for _ in range(int(Config().char["atk_len_shenk"] * 0.5)):
self._hydra(cast_pos_abs, spray=0)
self._alt_attack(cast_pos_abs, spray=90)
pos_m = convert_abs_to_monitor((150, 50))
self.pre_move()
self.move(pos_m, force_move=True)
shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0])
cast_pos_abs = [shenk_pos_abs[0] * 0.9, shenk_pos_abs[1] * 0.9]
for _ in range(int(Config().char["atk_len_shenk"])):
self._hydra(cast_pos_abs, spray=0)
self._alt_attack(cast_pos_abs, spray=90)
self.pre_move()
self.move(pos_m, force_move=True)
# Move to items
wait(self._cast_duration, self._cast_duration + 0.2)
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.4, force_tp=True)
self._hydra_time = None
return True
+5 -2
View File
@@ -73,7 +73,10 @@ class LightSorc(Sorceress):
self._chain_lightning(cast_pos_abs, spray=90)
# Move to items
wait(self._cast_duration, self._cast_duration + 0.2)
self._pather.traverse_nodes_fixed("eldritch_end", self)
pos_m = convert_abs_to_monitor((70, -200))
self.pre_move()
self.move(pos_m, force_move=True)
self._pather.traverse_nodes(Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END)
return True
def kill_shenk(self) -> bool:
@@ -245,4 +248,4 @@ if __name__ == "__main__":
from config import Config
pather = Pather()
char = LightSorc(Config().light_sorc, Config().char, pather)
char.kill_council()
char.kill_council()
+6 -7
View File
@@ -1,13 +1,12 @@
import keyboard
from typing import Tuple, Union, List, Callable
from typing import Callable
from utils.custom_mouse import mouse
from char import IChar
from template_finder import TemplateFinder
import template_finder
from pather import Pather
from screen import grab
from utils.misc import wait
import time
from typing import Tuple
from pather import Pather
from config import Config
from ui_manager import ScreenObjects, is_visible
@@ -17,7 +16,7 @@ class Sorceress(IChar):
super().__init__(skill_hotkeys)
self._pather = pather
def pick_up_item(self, pos: Tuple[float, float], item_name: str = None, prev_cast_start: float = 0):
def pick_up_item(self, pos: tuple[float, float], item_name: str = None, prev_cast_start: float = 0):
if self._skill_hotkeys["telekinesis"] and any(x in item_name for x in ['potion', 'misc_gold', 'tp_scroll']):
keyboard.send(self._skill_hotkeys["telekinesis"])
wait(0.1, 0.2)
@@ -36,7 +35,7 @@ class Sorceress(IChar):
def select_by_template(
self,
template_type: Union[str, List[str]],
template_type: str | list[str],
success_func: Callable = None,
timeout: float = 8,
threshold: float = 0.68,
@@ -51,11 +50,11 @@ class Sorceress(IChar):
keyboard.send("esc")
start = time.time()
while timeout is None or (time.time() - start) < timeout:
template_match = TemplateFinder().search(template_type, grab(), threshold=threshold, normalize_monitor=True)
template_match = template_finder.search(template_type, grab(), threshold=threshold)
if template_match.valid:
keyboard.send(self._skill_hotkeys["telekinesis"])
wait(0.1, 0.2)
mouse.move(*template_match.center)
mouse.move(*template_match.center_monitor)
wait(0.2, 0.3)
mouse.click(button="right")
# check the successfunction for 2 sec, if not found, try again
+2 -4
View File
@@ -1,14 +1,12 @@
import keyboard
from utils.custom_mouse import mouse
from char import IChar
from template_finder import TemplateFinder
from pather import Pather
from logger import Logger
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab
from config import Config
from utils.misc import wait, rotate_vec, unit_vector
import random
from typing import Tuple
from pather import Location, Pather
import numpy as np
@@ -38,7 +36,7 @@ class Trapsin(IChar):
mouse.click(button="right")
wait(self._cast_duration)
def _left_attack(self, cast_pos_abs: Tuple[float, float], spray: int = 10):
def _left_attack(self, cast_pos_abs: tuple[float, float], spray: int = 10):
keyboard.send(Config().char["stand_still"], do_release=False)
if self._skill_hotkeys["skill_left"]:
keyboard.send(self._skill_hotkeys["skill_left"])
@@ -53,7 +51,7 @@ class Trapsin(IChar):
keyboard.send(Config().char["stand_still"], do_press=False)
def _right_attack(self, cast_pos_abs: Tuple[float, float], spray: float = 10):
def _right_attack(self, cast_pos_abs: tuple[float, float], spray: float = 10):
keyboard.send(self._skill_hotkeys["lightning_sentry"])
x = cast_pos_abs[0] + (random.random() * 2 * spray - spray)
y = cast_pos_abs[1] + (random.random() * 2 * spray - spray)
+7 -7
View File
@@ -2,7 +2,7 @@ import time
import os
from logger import Logger
from template_finder import TemplateFinder
import template_finder
from screen import grab
from char import IChar
from config import Config
@@ -29,7 +29,7 @@ class Chest:
found_chest = True
start = time.time()
while time.time() - start < timeout:
template_match = TemplateFinder().search(templates, grab(), roi=Config().ui_roi["reduce_to_center"], threshold=threshold, use_grayscale=True, best_match=True, normalize_monitor=True)
template_match = template_finder.search(templates, grab(), roi=Config().ui_roi["reduce_to_center"], threshold=threshold, use_grayscale=True, best_match=True)
# search for at least 1.5 second, if no chest found, break
if not template_match.valid:
if time.time() - start > 1.5:
@@ -37,19 +37,19 @@ class Chest:
else:
found_chest = True
# move mouse and check for label
mouse.move(*template_match.center, delay_factor=[0.4, 0.6])
mouse.move(*template_match.center_monitor, delay_factor=[0.4, 0.6])
wait(0.13, 0.16)
chest_label_img = grab()
chest_label = TemplateFinder().search("CHEST_LABEL", chest_label_img, threshold=0.85)
is_locked = TemplateFinder().search("LOCKED", chest_label_img, threshold=0.85).valid
chest_label = template_finder.search("CHEST_LABEL", chest_label_img, threshold=0.85)
is_locked = template_finder.search("LOCKED", chest_label_img, threshold=0.85).valid
if chest_label.valid:
if is_locked:
consumables.increment_need("key", 1)
Logger.debug(f"Opening {template_match.name} ({template_match.score*100:.1f}% confidence)")
# TODO: Act as picking up a potion to support telekinesis. This workaround needs a proper solution.
self._char.pick_up_item(template_match.center, 'potion')
self._char.pick_up_item(template_match.center_monitor, 'potion')
wait(0.13, 0.16)
if TemplateFinder().search("LOCKED", grab(), threshold=0.85).valid:
if template_finder.search("LOCKED", grab(), threshold=0.85).valid:
templates.remove(template_match.name)
Logger.debug("No more keys, removing locked chest template")
continue
+59 -31
View File
@@ -6,7 +6,7 @@ import os
from dataclasses import dataclass
from logger import Logger
config_lock = threading.Lock()
from utils.misc import wait
def _default_iff(value, iff, default = None):
return default if value == iff else value
@@ -30,7 +30,6 @@ class Config:
gamble = {}
ui_roi = {}
ui_pos = {}
dclone = {}
routes = {}
routes_order = []
char = {}
@@ -41,10 +40,13 @@ class Config:
blizz_sorc = {}
light_sorc = {}
nova_sorc = {}
hydra_sorc = {}
hammerdin = {}
fohdin = {}
trapsin = {}
barbarian = {}
poison_necro = {}
bone_necro = {}
necro = {}
basic = {}
basic_ranged = {}
@@ -68,27 +70,33 @@ class Config:
def _select_val(self, section: str, key: str = None):
found_in = ""
if section in self.configs["custom"]["parser"] and key in self.configs["custom"]["parser"][section]:
found_val = self.configs["custom"]["parser"][section][key]
found_in = "custom"
elif section in self.configs["config"]["parser"]:
found_val = self.configs["config"]["parser"][section][key]
found_in = "config"
elif section in self.configs["pickit"]["parser"]:
found_val = self.configs["pickit"]["parser"][section][key]
found_in = "pickit"
elif section in self.configs["shop"]["parser"]:
found_val = self.configs["shop"]["parser"][section][key]
found_in = "shop"
else:
found_val = self.configs["game"]["parser"][section][key]
found_in = "game"
try:
if section in self.configs["custom"]["parser"] and key in self.configs["custom"]["parser"][section]:
found_val = self.configs["custom"]["parser"][section][key]
found_in = "custom"
elif section in self.configs["config"]["parser"]:
found_val = self.configs["config"]["parser"][section][key]
found_in = "config"
elif section in self.configs["pickit"]["parser"]:
found_val = self.configs["pickit"]["parser"][section][key]
found_in = "pickit"
elif section in self.configs["shop"]["parser"]:
found_val = self.configs["shop"]["parser"][section][key]
found_in = "shop"
else:
found_val = self.configs["game"]["parser"][section][key]
found_in = "game"
for var_name in self.configs[found_in]["vars"]: # set variable.
if var_name in found_val:
var_val = self.configs[found_in]["vars"][var_name]
found_val = found_val.replace(var_name, var_val)
return found_val
for var_name in self.configs[found_in]["vars"]: # set variable.
if var_name in found_val:
var_val = self.configs[found_in]["vars"][var_name]
found_val = found_val.replace(var_name, var_val)
return found_val
except KeyError:
Logger.error(f"Key '{key}' not found in section '{section}'")
Logger.error("Closing in 10 seconds..")
wait(10)
os._exit(1)
def parse_item_config_string(self, key: str = None) -> ItemProps:
string = self._select_val("items", key).upper()
@@ -219,19 +227,15 @@ class Config:
"difficulty": self._select_val("general", "difficulty"),
"message_api_type": self._select_val("general", "message_api_type"),
"custom_message_hook": self._select_val("general", "custom_message_hook"),
"custom_loot_message_hook": self._select_val("general", "custom_loot_message_hook"),
"discord_status_count": False if not self._select_val("general", "discord_status_count") else int(self._select_val("general", "discord_status_count")),
"discord_log_chicken": bool(int(self._select_val("general", "discord_log_chicken"))),
"info_screenshots": bool(int(self._select_val("general", "info_screenshots"))),
"loot_screenshots": bool(int(self._select_val("general", "loot_screenshots"))),
"d2r_path": _default_iff(self._select_val("general", "d2r_path"), "", "C:\Program Files (x86)\Diablo II Resurrected"),
"restart_d2r_when_stuck": bool(int(self._select_val("general", "restart_d2r_when_stuck"))),
}
# Added for dclone ip hunting
self.dclone = {
"region_ips": self._select_val("dclone", "region_ips"),
"dclone_hotip": self._select_val("dclone", "dclone_hotip"),
}
self.routes = {}
order_str = self._select_val("routes", "order")
self.routes_order = [x.strip() for x in order_str.split(",")]
@@ -245,6 +249,7 @@ class Config:
"type": self._select_val("char", "type"),
"show_items": self._select_val("char", "show_items"),
"inventory_screen": self._select_val("char", "inventory_screen"),
"teleport": self._select_val("char", "teleport"),
"stand_still": self._select_val("char", "stand_still"),
"force_move": self._select_val("char", "force_move"),
"num_loot_columns": int(self._select_val("char", "num_loot_columns")),
@@ -256,7 +261,7 @@ class Config:
"heal_rejuv_merc": float(self._select_val("char", "heal_rejuv_merc")),
"chicken": float(self._select_val("char", "chicken")),
"merc_chicken": float(self._select_val("char", "merc_chicken")),
"tp": self._select_val("char", "tp"),
"town_portal": self._select_val("char", "town_portal"),
"belt_rows": int(self._select_val("char", "belt_rows")),
"show_belt": self._select_val("char", "show_belt"),
"potion1": self._select_val("char", "potion1"),
@@ -291,10 +296,12 @@ class Config:
"atk_len_cs_trashmobs": float(self._select_val("char", "atk_len_cs_trashmobs")),
"kill_cs_trash": bool(int(self._select_val("char", "kill_cs_trash"))),
"cs_town_visits": bool(int(self._select_val("char", "cs_town_visits"))),
"cs_mob_detect": bool(int(self._select_val("char", "cs_mob_detect"))),
"runs_per_stash": False if not self._select_val("char", "runs_per_stash") else int(self._select_val("char", "runs_per_stash")),
"runs_per_repair": False if not self._select_val("char", "runs_per_repair") else int(self._select_val("char", "runs_per_repair")),
"gamble_items": False if not self._select_val("char", "gamble_items") else self._select_val("char", "gamble_items").replace(" ","").split(","),
"sell_junk": bool(int(self._select_val("char", "sell_junk"))),
"enable_no_pickup": bool(int(self._select_val("char", "enable_no_pickup"))),
}
# Sorc base config
sorc_base_cfg = dict(self.configs["config"]["parser"]["sorceress"])
@@ -315,13 +322,28 @@ class Config:
if "nova_sorc" in self.configs["custom"]["parser"]:
self.nova_sorc.update(dict(self.configs["custom"]["parser"]["nova_sorc"]))
self.nova_sorc.update(sorc_base_cfg)
# hydra sorc
self.hydra_sorc = dict(self.configs["config"]["parser"]["hydra_sorc"])
if "hydra_sorc" in self.configs["custom"]["parser"]:
self.hydra_sorc.update(dict(self.configs["custom"]["parser"]["hydra_sorc"]))
self.hydra_sorc.update(sorc_base_cfg)
# Palandin config
# Paladin base config
paladin_base_cfg = dict(self.configs["config"]["parser"]["paladin"])
if "paladin" in self.configs["custom"]["parser"]:
paladin_base_cfg.update(dict(self.configs["custom"]["parser"]["paladin"]))
# Hammerdin config
self.hammerdin = self.configs["config"]["parser"]["hammerdin"]
if "hammerdin" in self.configs["custom"]["parser"]:
self.hammerdin.update(self.configs["custom"]["parser"]["hammerdin"])
self.hammerdin.update(paladin_base_cfg)
# FoHdin config
self.fohdin = dict(self.configs["config"]["parser"]["fohdin"])
if "fohdin" in self.configs["custom"]["parser"]:
self.fohdin.update(self.configs["custom"]["parser"]["fohdin"])
self.fohdin.update(paladin_base_cfg)
# Assasin config
# Assassin config
self.trapsin = self.configs["config"]["parser"]["trapsin"]
if "trapsin" in self.configs["custom"]["parser"]:
self.trapsin.update(self.configs["custom"]["parser"]["trapsin"])
@@ -352,6 +374,10 @@ class Config:
if "poison_necro" in self.configs["custom"]["parser"]:
self.poison_necro.update(self.configs["custom"]["parser"]["poison_necro"])
self.bone_necro = self.configs["config"]["parser"]["bone_necro"]
if "bone_necro" in self.configs["custom"]["parser"]:
self.bone_necro.update(self.configs["custom"]["parser"]["bone_necro"])
self.advanced_options = {
"pathing_delay_factor": min(max(int(self._select_val("advanced_options", "pathing_delay_factor")), 1), 10),
"message_headers": self._select_val("advanced_options", "message_headers"),
@@ -417,9 +443,11 @@ class Config:
"apply_pather_adjustment": bool(int(self._select_val("scepters", "apply_pather_adjustment"))),
}
stash_destination_str = self._select_val("transmute","stash_destination")
transmute_str = self._select_val("transmute","transmute")
self.configs["transmute"]["parser"] = {
"stash_destination": [int(x.strip()) for x in stash_destination_str.split(",")],
"transmute_every_x_game": self._select_val("transmute","transmute_every_x_game"),
"transmute": [x.strip() for x in transmute_str.split(",")],
}
if __name__ == "__main__":
+12 -15
View File
@@ -1,4 +1,3 @@
import os
import threading
import time
import cv2
@@ -13,7 +12,7 @@ from health_manager import HealthManager
from logger import Logger
from messages import Messenger
from screen import grab, get_offset_state
from utils.restart import restart_game, kill_game
from utils.restart import restart_game, safe_exit
from utils.misc import kill_thread, set_d2r_always_on_top, restore_d2r_window_visibility
@@ -41,10 +40,11 @@ class GameController:
self.health_manager.set_callback(lambda: self.bot.stop() or kill_thread(self.bot_thread))
do_restart = False
messenger = Messenger()
force_stopped = False
while 1:
max_game_length_reached = self.game_stats.get_current_game_length() > Config().general["max_game_length_s"]
max_consecutive_fails_reached = False if not Config().general["max_consecutive_fails"] else self.game_stats.get_consecutive_runs_failed() >= Config().general["max_consecutive_fails"]
if max_game_length_reached or max_consecutive_fails_reached or self.death_manager.died() or self.health_manager.did_chicken():
if max_game_length_reached or max_consecutive_fails_reached or self.death_manager.died() or self.health_manager.did_chicken() or (force_stopped := self.bot._stopping):
# Some debug and logging
if max_game_length_reached:
Logger.info(f"Max game length reached. Attempting to restart {Config().general['name']}!")
@@ -60,9 +60,9 @@ class GameController:
if max_consecutive_fails_reached:
msg = f"Consecutive fails {self.game_stats.get_consecutive_runs_failed()} >= Max {Config().general['max_consecutive_fails']}. Quitting botty."
Logger.error(msg)
if Config().general["custom_message_hook"]:
if messenger.enabled:
messenger.send_message(msg)
self.safe_exit(1)
safe_exit(1)
else:
do_restart = self.game_recovery.go_to_hero_selection()
break
@@ -72,16 +72,16 @@ class GameController:
# Reset flags before running a new bot
self.death_manager.reset_death_flag()
self.health_manager.reset_chicken_flag()
self.game_stats.log_end_game(failed=max_game_length_reached)
self.game_stats.log_end_game(failed = (max_game_length_reached or force_stopped))
return self.run_bot()
else:
if Config().general["info_screenshots"]:
cv2.imwrite("./info_screenshots/info_could_not_recover_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
if Config().general['restart_d2r_when_stuck']:
Logger.error("Could not recover from a max game length violation. Restarting the Game.")
if Config().general["custom_message_hook"]:
if messenger.enabled:
messenger.send_message("Got stuck and will now restart D2R")
if restart_game(Config().general["d2r_path"]):
if restart_game(Config().general["d2r_path"], Config().advanced_options["launch_options"]):
self.game_stats.log_end_game(failed=max_game_length_reached)
if self.setup_screen():
self.start_health_manager_thread()
@@ -89,12 +89,13 @@ class GameController:
self.game_recovery = GameRecovery(self.death_manager)
return self.run_bot()
Logger.error("Could not restart the game. Quitting.")
messenger.send_message("Got stuck and could not restart the game. Quitting.")
if messenger.enabled:
messenger.send_message("Got stuck and could not restart the game. Quitting.")
else:
Logger.error("Could not recover from a max game length violation. Quitting botty.")
if Config().general["custom_message_hook"]:
if messenger.enabled:
messenger.send_message("Got stuck and will now quit botty")
self.safe_exit(1)
safe_exit(1)
def start(self):
# Check if we user should update the d2r settings
@@ -147,7 +148,3 @@ class GameController:
def toggle_pause_bot(self):
if self.bot:
self.bot.toggle_pause()
def safe_exit(self, error_code=0):
kill_game()
os._exit(error_code)
+7 -4
View File
@@ -55,7 +55,7 @@ class GameStats:
self._location_stats[self._location]["items"].append(item_name)
self._location_stats["totals"]["items"] += 1
if send_message:
if send_message and self._messenger.enabled:
self._messenger.send_item(item_name, img, self._location, ocr_text)
def log_death(self, img: str):
@@ -64,7 +64,8 @@ class GameStats:
self._location_stats[self._location]["deaths"] += 1
self._location_stats["totals"]["deaths"] += 1
self._messenger.send_death(self._location, img)
if self._messenger.enabled:
self._messenger.send_death(self._location, img)
def log_chicken(self, img: str):
self._chicken_counter += 1
@@ -72,7 +73,8 @@ class GameStats:
self._location_stats[self._location]["chickens"] += 1
self._location_stats["totals"]["chickens"] += 1
self._messenger.send_chicken(self._location, img)
if Config().general["discord_log_chicken"] and self._messenger.enabled:
self._messenger.send_chicken(self._location, img)
def log_merc_death(self):
self._merc_death_counter += 1
@@ -209,7 +211,8 @@ class GameStats:
def _send_status_update(self):
msg = f"Status Report\n{self._create_msg()}\nVersion: {__version__}"
self._messenger.send_message(msg)
if self._messenger.enabled:
self._messenger.send_message(msg)
def _save_stats_to_file(self):
msg = self._create_msg()
+20 -13
View File
@@ -15,19 +15,30 @@ from ui_manager import ScreenObjects, is_visible
from random import uniform
pause_state = True
panel_check_paused = False
def get_pause_state():
global pause_state
return pause_state
def set_pause_state(state: bool):
global pause_state
prev = get_pause_state()
if prev != state:
debug_str = "pausing" if state else "active"
debug_str = "paused" if state else "active"
Logger.info(f"Health Manager is now {debug_str}")
pause_state = state
def get_panel_check_paused():
return panel_check_paused
def set_panel_check_paused(state: bool):
global panel_check_paused
prev = get_panel_check_paused()
if prev != state:
debug_str = "pausing" if state else "activating"
Logger.info(f"Health Manager is now {debug_str} inventory panel check")
panel_check_paused = state
class HealthManager:
def __init__(self):
self._do_monitor = False
@@ -57,19 +68,15 @@ class HealthManager:
if self._callback is not None:
self._callback()
self._callback = None
# clean up key presses that might be pressed in the run_thread
keyboard.release(Config().char["stand_still"])
keyboard.release(Config().char["show_items"])
mouse.release(button="left")
mouse.release(button="right")
view.save_and_exit()
if Config().general["info_screenshots"]:
self._last_chicken_screenshot = "./info_screenshots/info_debug_chicken_" + time.strftime("%Y%m%d_%H%M%S") + ".png"
cv2.imwrite(self._last_chicken_screenshot, img)
# clean up key presses that might be pressed in the run_thread
keyboard.release(Config().char["stand_still"])
wait(0.02, 0.05)
keyboard.release(Config().char["show_items"])
wait(0.02, 0.05)
mouse.release(button="left")
wait(0.02, 0.05)
mouse.release(button="right")
time.sleep(0.01)
view.save_and_exit()
self._did_chicken = True
set_pause_state(True)
@@ -128,7 +135,7 @@ class HealthManager:
elif merc_health_percentage <= Config().char["heal_merc"] and last_drink > merc_hp_potion_delay:
belt.drink_potion("health", merc=True, stats=[merc_health_percentage])
self._last_merc_heal = time.time()
if is_visible(ScreenObjects.LeftPanel, img) or is_visible(ScreenObjects.RightPanel, img):
if not get_panel_check_paused() and (is_visible(ScreenObjects.LeftPanel, img) or is_visible(ScreenObjects.RightPanel, img)):
Logger.warning(f"Found an open inventory / quest / skill / stats page. Close it.")
self._count_panel_detects += 1
if self._count_panel_detects >= 2:
+3 -4
View File
@@ -1,8 +1,7 @@
import itertools
from logger import Logger
from typing import List
import numpy as np
from template_finder import TemplateFinder
import template_finder
from inventory import common, consumables, personal
from ui import view
from ui_manager import is_visible, wait_until_visible, ScreenObjects, wait_until_hidden
@@ -72,7 +71,7 @@ def _cut_potion_img(img: np.ndarray, column: int, row: int) -> np.ndarray:
]
return cut_roi(img, roi)
def drink_potion(potion_type: str, merc: bool = False, stats: List = []) -> bool:
def drink_potion(potion_type: str, merc: bool = False, stats: list = []) -> bool:
img = grab()
for i in range(4):
potion_img = _cut_potion_img(img, i, 0)
@@ -151,7 +150,7 @@ def fill_up_belt_from_inventory(num_loot_columns: int):
pot_positions = []
for column, row in itertools.product(range(num_loot_columns), range(4)):
center_pos, slot_img = common.get_slot_pos_and_img(img, column, row)
found = TemplateFinder().search(["GREATER_HEALING_POTION", "GREATER_MANA_POTION", "SUPER_HEALING_POTION", "SUPER_MANA_POTION", "FULL_REJUV_POTION", "REJUV_POTION"], slot_img, threshold=0.9).valid
found = template_finder.search(["GREATER_HEALING_POTION", "GREATER_MANA_POTION", "SUPER_HEALING_POTION", "SUPER_MANA_POTION", "FULL_REJUV_POTION", "REJUV_POTION"], slot_img, threshold=0.9).valid
if found:
pot_positions.append(center_pos)
keyboard.press("shift")
+9 -5
View File
@@ -4,8 +4,8 @@ import numpy as np
import keyboard
import time
from utils.custom_mouse import mouse
from template_finder import TemplateFinder
from ui_manager import detect_screen_object, ScreenObjects, is_visible, wait_until_hidden
from ui_manager import detect_screen_object, ScreenObjects, is_visible, wait_until_hidden, center_mouse
import template_finder
from utils.misc import wait, trim_black, color_filter, cut_roi
from inventory import consumables
from ui import view
@@ -80,12 +80,12 @@ def calc_item_roi(img_pre, img_post):
def tome_state(img: np.ndarray = None, tome_type: str = "tp", roi: list = None):
img = img if img is not None else grab()
if (tome_found := TemplateFinder().search([f"{tome_type.upper()}_TOME", f"{tome_type.upper()}_TOME_RED"], img, roi = roi, threshold = 0.8, best_match = True, normalize_monitor = True)).valid:
if (tome_found := template_finder.search([f"{tome_type.upper()}_TOME", f"{tome_type.upper()}_TOME_RED"], img, roi = roi, threshold = 0.8, best_match = True)).valid:
if tome_found.name == f"{tome_type.upper()}_TOME":
state = "ok"
else:
state = "empty"
position = tome_found.center
position = tome_found.center_monitor
else:
state = position = None
return state, position
@@ -179,7 +179,11 @@ def get_active_tab(indicator: TemplateMatch = None) -> int:
if indicator.valid:
return indicator_location_to_tab_count(indicator.center)
else:
Logger.error("common/get_active_tab(): Error finding tab indicator")
center_mouse()
if (indicator := detect_screen_object(ScreenObjects.TabIndicator)).valid:
return indicator_location_to_tab_count(indicator.center)
else:
Logger.error("common/get_active_tab(): Error finding tab indicator")
return -1
def select_tab(idx: int):
+4 -5
View File
@@ -4,7 +4,7 @@ import numpy as np
import time
import parse
from utils.custom_mouse import mouse
from template_finder import TemplateFinder
import template_finder
from inventory import personal
from utils.misc import wait
from screen import grab
@@ -106,12 +106,11 @@ def should_buy(item_name: str = None, min_remaining: int = None, min_needed: int
def update_tome_key_needs(img: np.ndarray = None, item_type: str = "tp") -> bool:
img = personal.open(img)
if item_type.lower() in ["tp", "id"]:
match = TemplateFinder().search(
match = template_finder.search(
[f"{item_type.upper()}_TOME", f"{item_type.upper()}_TOME_RED"],
img,
roi = Config().ui_roi["restricted_inventory_area"],
best_match = True,
normalize_monitor=True
)
if match.valid:
if match.name == f"{item_type.upper()}_TOME_RED":
@@ -122,13 +121,13 @@ def update_tome_key_needs(img: np.ndarray = None, item_type: str = "tp") -> bool
Logger.debug(f"update_tome_key_needs: could not find {item_type}")
return False
elif item_type.lower() in ["key"]:
match = TemplateFinder().search("INV_KEY", img, roi = Config().ui_roi["restricted_inventory_area"], normalize_monitor = True)
match = template_finder.search("INV_KEY", img, roi = Config().ui_roi["restricted_inventory_area"])
if not match.valid:
return False
else:
Logger.error(f"update_tome_key_needs failed, item_type: {item_type} not supported")
return False
mouse.move(*match.center, randomize=4, delay_factor=[0.5, 0.7])
mouse.move(*match.center_monitor, randomize=4, delay_factor=[0.5, 0.7])
wait(0.2, 0.2)
hovered_item = grab()
# get the item description box
+22 -19
View File
@@ -10,7 +10,7 @@ import time
import numpy as np
from dataclasses import dataclass
from template_finder import TemplateFinder
import template_finder
from config import Config
from utils.misc import wait, is_in_roi, mask_by_roi
from utils.custom_mouse import mouse
@@ -77,7 +77,7 @@ def stash_all_items(items: list = None):
center_mouse()
# Wait for stash to fully load
if not common.wait_for_left_inventory():
Logger.error("stash_all_items(): Could not determine to be in stash menu. Continue...")
Logger.error("stash_all_items(): Failed to find stash menu. Continue...")
return items
# stash gold
if Config().char["stash_gold"]:
@@ -194,7 +194,7 @@ def keep_item(item_box: ItemText = None, found_item: Item = None, do_logging: bo
found_subprops=[]
for subprop in prop:
try:
template_match = TemplateFinder().search(subprop, img, threshold=0.95)
template_match = template_finder.search(subprop, img, threshold=0.95)
except:
Logger.error(f"{found_item.name}: can't find template file for required {prop}, ignore just in case")
template_match = lambda: None; template_match.valid = True
@@ -212,7 +212,7 @@ def keep_item(item_box: ItemText = None, found_item: Item = None, do_logging: bo
break
else:
try:
template_match = TemplateFinder().search(prop, img, threshold=0.95)
template_match = template_finder.search(prop, img, threshold=0.95)
except:
Logger.error(f"{found_item.name}: can't find template file for required {prop}, ignore just in case")
template_match = lambda: None; template_match.valid = True
@@ -236,7 +236,7 @@ def keep_item(item_box: ItemText = None, found_item: Item = None, do_logging: bo
found_props = []
for prop in exclude_props:
try:
template_match = TemplateFinder().search(prop, img, threshold=0.97)
template_match = template_finder.search(prop, img, threshold=0.97)
except:
Logger.error(f"{found_item.name}: can't find template file for exclusion {prop}, ignore just in case")
template_match = lambda: None; template_match.valid = False
@@ -270,7 +270,7 @@ def open(img: np.ndarray = None) -> np.ndarray:
img = grab()
return img
def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_stats: GameStats = None) -> list[BoxInfo]:
def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_stats: GameStats = None, ignore_sell: bool = False) -> list[BoxInfo]:
"""
Iterate over all picked items in inventory--ID items and decide which to stash
:param img: Image in which the item is searched (item details should be visible)
@@ -377,6 +377,8 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
keep = keep_item(item_box, found_item) if found_item else False
if keep:
sell = need_id = False
if ignore_sell:
sell = False
box = BoxInfo(
img = item_box.data,
@@ -424,19 +426,20 @@ def transfer_items(items: list, action: str = "drop", img: np.ndarray = None) ->
img = img if img is not None else grab()
filtered = []
left_panel_open = is_visible(ScreenObjects.LeftPanel, img)
if action == "drop":
filtered = [ item for item in items if item.keep == False and item.sell == False ]
elif action == "sell":
filtered = [ item for item in items if item.keep == False and item.sell == True ]
if not left_panel_open:
Logger.error(f"transfer_items: Can't perform, vendor is not open")
elif action == "stash":
if is_visible(ScreenObjects.GoldBtnStash, img):
filtered = [ item for item in items if item.keep == True ]
else:
Logger.error(f"transfer_items: Can't perform, stash is not open")
else:
Logger.error(f"transfer_items: incorrect action param={action}")
match action:
case "drop":
filtered = [ item for item in items if item.keep == False and item.sell == False ]
case "sell":
filtered = [ item for item in items if item.keep == False and item.sell == True ]
if not left_panel_open:
Logger.error(f"transfer_items: Can't perform, vendor is not open")
case "stash":
if is_visible(ScreenObjects.GoldBtnStash, img):
filtered = [ item for item in items if item.keep == True ]
else:
Logger.error(f"transfer_items: Can't perform, stash is not open")
case _:
Logger.error(f"transfer_items: incorrect action param={action}")
if filtered:
# if dropping, control+click to drop unless left panel is open, then drag to middle
# if stashing, control+click to stash
+1 -1
View File
@@ -1,4 +1,4 @@
from template_finder import TemplateFinder
import template_finder
from screen import grab, convert_screen_to_monitor
from config import Config
import os
+9 -8
View File
@@ -1,6 +1,6 @@
from math import floor
import keyboard
from template_finder import TemplateFinder
import template_finder
from config import Config
import numpy as np
from utils.misc import wait
@@ -49,20 +49,20 @@ def repair() -> bool:
return True
def gamble():
if (refresh_btn := TemplateFinder().search_and_wait("REFRESH", threshold=0.79, timeout=4, normalize_monitor=True)).valid:
if (refresh_btn := template_finder.search_and_wait("REFRESH", threshold=0.79, timeout=4)).valid:
#Gambling window is open. Starting to spent some coins
max_gamble_count = floor(2000000/188000) # leave about 500k gold and assume buying coronets at ~188k
while get_gamble_status() and get_gamble_count() < max_gamble_count:
img=grab()
for item in Config().char["gamble_items"]:
# while desired gamble item is not on screen, refresh
while not (desired_item := TemplateFinder().search (item.upper(), grab(), roi=Config().ui_roi["left_inventory"], normalize_monitor=True)).valid:
mouse.move(*refresh_btn.center, randomize=12, delay_factor=[1.0, 1.5])
while not (desired_item := template_finder.search (item.upper(), grab(), roi=Config().ui_roi["left_inventory"])).valid:
mouse.move(*refresh_btn.center_monitor, randomize=12, delay_factor=[1.0, 1.5])
wait(0.1, 0.15)
mouse.click(button="left")
wait(0.1, 0.15)
# desired item found, purchase it
mouse.move(*desired_item.center, randomize=12, delay_factor=[1.0, 1.5])
mouse.move(*desired_item.center_monitor, randomize=12, delay_factor=[1.0, 1.5])
wait(0.1, 0.15)
mouse.click(button="right")
wait(0.4, 0.6)
@@ -109,8 +109,8 @@ def buy_item(template_name: str, quantity: int = 1, img: np.ndarray = None, shif
"""
if img is None:
img = grab()
if (desired_item := TemplateFinder().search(template_name, inp_img=img, roi=Config().ui_roi["left_inventory"], normalize_monitor=True)).valid:
mouse.move(*desired_item.center, randomize=8, delay_factor=[1.0, 1.5])
if (desired_item := template_finder.search(template_name, inp_img=img, roi=Config().ui_roi["left_inventory"])).valid:
mouse.move(*desired_item.center_monitor, randomize=8, delay_factor=[1.0, 1.5])
if shift_click:
keyboard.send('shift', do_release=False)
wait(0.5, 0.8)
@@ -118,6 +118,7 @@ def buy_item(template_name: str, quantity: int = 1, img: np.ndarray = None, shif
wait(0.4, 0.6)
if is_visible(ScreenObjects.NotEnoughGold):
Logger.warning(f"Out of gold, could not purchase {template_name}")
keyboard.send('shift', do_release=True)
keyboard.send("esc")
return False
keyboard.send('shift', do_release=True)
@@ -138,4 +139,4 @@ def buy_item(template_name: str, quantity: int = 1, img: np.ndarray = None, shif
return False
Logger.error(f"buy_item: Desired item {template_name} not found")
return False
return False
+2 -2
View File
@@ -4,7 +4,7 @@ from dataclasses import dataclass
import time
from utils.misc import color_filter, erode_to_black
from template_finder import TemplateFinder
import template_finder
from ocr import Ocr, OcrResult
from config import Config
from logger import Logger
@@ -126,7 +126,7 @@ class ItemCropper:
overlaps_inventory = False if (x+w<box2[0] or box2[0]+box2[2]<x or y+h+50+10<box2[1] or box2[1]+box2[3]<y) else True # padded height because footer isn't included in contour
if contains_black and (contains_white or contains_orange) and mostly_dark and expected_height and expected_width and overlaps_inventory:
footer_height_max = (720 - (y + h)) if (y + h + 35) > 720 else 35
found_footer = TemplateFinder().search(["TO_TOOLTIP"], inp_img, threshold=0.8, roi=[x, y+h, w, footer_height_max]).valid
found_footer = template_finder.search(["TO_TOOLTIP"], inp_img, threshold=0.8, roi=[x, y+h, w, footer_height_max]).valid
if found_footer:
ocr_result = self._ocr.image_to_text(cropped_item, psm=6, model=model)[0]
result.color = "black"
+1 -3
View File
@@ -1,5 +1,4 @@
import cv2
from typing import Tuple
import numpy as np
import time
import os
@@ -9,7 +8,6 @@ import math
from config import Config
from utils.misc import color_filter, cut_roi
from item import ItemCropper
from template_finder import TemplateFinder
from ocr import OcrResult, Ocr
@@ -21,7 +19,7 @@ class Template:
@dataclass
class Item:
center: Tuple[float, float] = None # (x, y) in screen coordinates
center: tuple[float, float] = None # (x, y) in screen coordinates
name: str = None
score: float = -1.0
dist: float = -1.0
+1 -2
View File
@@ -178,8 +178,7 @@ if __name__ == "__main__":
import os
from config import Config
from char.sorceress import LightSorc
from char.hammerdin import Hammerdin
from template_finder import TemplateFinder
from char.paladin import Hammerdin
from pather import Pather
import keyboard
+7 -7
View File
@@ -9,14 +9,14 @@ from colorama import Fore, Back, Style, init
init()
class CustomFormatter(logging.Formatter):
format = f'[{__version__} %(asctime)s] %(levelname)-10s %(message)s'
_format = f'[{__version__} %(asctime)s] %(levelname)-10s %(message)s'
FORMATS = {
logging.DEBUG: Fore.WHITE + format + Fore.WHITE,
logging.INFO: Fore.LIGHTBLUE_EX + format + Fore.WHITE,
logging.WARNING: Fore.LIGHTYELLOW_EX + format + Fore.WHITE,
logging.ERROR: Fore.LIGHTRED_EX + format + Fore.WHITE,
logging.CRITICAL: Fore.RED + format + Fore.WHITE
logging.DEBUG: Fore.WHITE + _format + Fore.WHITE,
logging.INFO: Fore.LIGHTBLUE_EX + _format + Fore.WHITE,
logging.WARNING: Fore.LIGHTYELLOW_EX + _format + Fore.WHITE,
logging.ERROR: Fore.LIGHTRED_EX + _format + Fore.WHITE,
logging.CRITICAL: Fore.RED + _format + Fore.WHITE
}
@@ -96,7 +96,7 @@ class Logger:
_format = CustomFormatter()
Logger.string_handler.setFormatter(_format)
Logger.console_handler.setFormatter(_format)
Logger.file_handler.setFormatter(_format)
Logger.file_handler.setFormatter(logging.Formatter(_format._format))
# Add the handler to the logger
Logger.logger.addHandler(Logger.string_handler)
+15
View File
@@ -5,6 +5,7 @@ from beautifultable import BeautifulTable
import logging
import traceback
import screen
import string
from version import __version__
from config import Config
from logger import Logger
@@ -45,6 +46,19 @@ def on_exit():
restore_d2r_window_visibility()
os._exit(1)
def startup_checks():
# check if paths contain non-ascii characters
check_for_non_ascii = {
"D2R path": Config().general["d2r_path"],
"Windows username": os.getlogin(),
"Botty path": os.getcwd()
}
for key, value in check_for_non_ascii.items():
strip_punctuation = value.translate(str.maketrans('', '', string.punctuation))
if not len(strip_punctuation) == len(strip_punctuation.encode()):
print(f"\n!! WARNING: {key} ({value}) contains incompatible characters. This could result in Botty encoding errors.\n")
def main():
controllers = Controllers(
GameController(),
@@ -56,6 +70,7 @@ def main():
Logger.init(logging.DEBUG)
else:
print(f"ERROR: Unkown logg_lvl {Config().advanced_options['logg_lvl']}. Must be one of [info, debug]")
startup_checks()
# Create folder for debug screenshots if they dont exist yet
if not os.path.exists("stats"):
+21 -13
View File
@@ -13,12 +13,20 @@ class DiscordEmbeds(GenericApi):
def __init__(self):
self._file = None
self._psnURL = "https://i.psnprofiles.com/games/3bffee/trophies/"
if Config().general["custom_message_hook"]:
self._webhook = self._get_webhook(Config().general["custom_message_hook"])
self._loot_webhook = self._get_webhook(Config().general["custom_loot_message_hook"])
def _get_webhook(self, hook_url: str):
hook = None
if not hook_url:
hook_url = Config().general['custom_message_hook']
if hook_url:
try:
self._webhook = Webhook.from_url(Config().general['custom_message_hook'], adapter=RequestsWebhookAdapter(), )
except InvalidArgument:
self._webhook = None
Logger.warning(f"Your custom_message_hook URL {Config().general['custom_message_hook']} is invalid, Discord updates will not be sent")
hook = Webhook.from_url(hook_url, adapter=RequestsWebhookAdapter(), )
except BaseException as e:
Logger.warning(f"Your custom_message_hook URL {hook_url} is invalid, Discord updates will not be sent")
Logger.error(f"Error initializing webhook {hook_url}: {e}")
return hook
def send_item(self, item: str, image: np.ndarray, location: str, ocr_text: str = None):
imgName = item.replace('_', '-')
@@ -34,7 +42,7 @@ class DiscordEmbeds(GenericApi):
e.set_thumbnail(url=f"{self._psnURL}41L6bd712.png")
e.set_image(url=f"attachment://{imgName}.png")
e.add_field(name="OCR Text", value=f"{ocr_text}", inline=False)
self._send_embed(e, file)
self._send_embed(e, self._loot_webhook, file)
def send_death(self, location, image_path):
file = self._add_file(image_path, "death.png")
@@ -43,7 +51,7 @@ class DiscordEmbeds(GenericApi):
e.description=(f"Died at {location}")
e.set_thumbnail(url=f"{self._psnURL}33L5e3600.png")
e.set_image(url="attachment://death.png")
self._send_embed(e, file)
self._send_embed(e, self._webhook, file)
def send_chicken(self, location, image_path):
file = self._add_file(image_path, "chicken.png")
@@ -52,32 +60,32 @@ class DiscordEmbeds(GenericApi):
e.description=(f"chickened at {location}")
e.set_thumbnail(url=f"{self._psnURL}39Ldf113b.png")
e.set_image(url="attachment://chicken.png")
self._send_embed(e, file)
self._send_embed(e, self._webhook, file)
def send_stash(self):
e = discord.Embed(title=f"{Config().general['name']} has a full stash!", color=Color.dark_grey())
e.title=(f"{Config().general['name']} has a full stash!")
e.description=(f"{Config().general['name']} has to quit. \n They cannot store anymore items!")
e.set_thumbnail(url=f"{self._psnURL}35L63a9df.png")
self._send_embed(e)
self._send_embed(e, self._webhook)
def send_gold(self):
e = discord.Embed(title=f"{Config().general['name']} is rich!", color=Color.dark_grey())
e.title=(f"{Config().general['name']} is Rich!")
e.description=(f"{Config().general['name']} can't store any more money!\n turning off gold pickup.")
e.set_thumbnail(url=f"{self._psnURL}6L341955.png")
self._send_embed(e)
self._send_embed(e, self._webhook)
def send_message(self, msg: str):
msg = f"{Config().general['name']}: {msg}"
e = discord.Embed(title=f"Update:", description=f"```{msg}```", color=Color.dark_teal())
self._send_embed(e)
self._send_embed(e, self._webhook)
def _send_embed(self, e, file = None):
def _send_embed(self, e, webhook, file = None):
e.set_footer(text=f'Botty v.{__version__} by Aeon')
e.timestamp=datetime.datetime.now(datetime.timezone.utc)
try:
if self._webhook: self._webhook.send(embed=e, file=file, username=Config().general['name'])
webhook.send(embed=e, file=file, username=Config().general['name'])
except BaseException as err:
Logger.error(f"Error sending Discord embed: {err}")
+5 -4
View File
@@ -8,7 +8,7 @@ class GenericApi:
def send_item(self, item: str, image: np.ndarray, location: str, ocr_text: str = None):
msg = f"Found {item} at {location}"
self._send(msg)
self._send(msg, loot = True)
def send_death(self, location: str, image_path: str = None):
msg = f"You have died at {location}"
@@ -29,12 +29,13 @@ class GenericApi:
def send_message(self, msg: str):
self._send(msg)
def _send(self, msg: str):
def _send(self, msg: str, loot = False):
msg = f"{Config().general['name']}: {msg}"
url = Config().general['custom_message_hook']
if not url:
if not (url := Config().general['custom_message_hook']):
return
if loot and (loot_url := Config().general['custom_loot_message_hook']):
url = loot_url
headers = {}
if Config().advanced_options['message_headers']:
+2
View File
@@ -14,6 +14,8 @@ class Messenger:
self._message_api = DiscordEmbeds()
else:
self._message_api = None
self.enabled = Config().general["message_api_type"] and Config().general["custom_message_hook"]
def send_item(self, item: str, image: np.ndarray, location: str, ocr_text: str = None):
self._message_api.send_item(item, image, location, ocr_text)
+75 -77
View File
@@ -2,7 +2,7 @@ import time
import os
import numpy as np
import keyboard
from template_finder import TemplateFinder
import template_finder
from config import Config
from screen import grab
from ui_manager import ScreenObjects, center_mouse, is_visible, wait_until_hidden
@@ -35,12 +35,12 @@ class Npc:
npcs = {
Npc.QUAL_KEHK: {
"name_tag_white": color_filter(TemplateFinder().get_template("QUAL_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("QUAL_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("QUAL_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("QUAL_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"resurrect": {
"white": color_filter(TemplateFinder().get_template("RESURRECT"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("RESURRECT_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("RESURRECT"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("RESURRECT_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["QUAL_0", "QUAL_45", "QUAL_45_B", "QUAL_90", "QUAL_135", "QUAL_135_B", "QUAL_135_C", "QUAL_180", "QUAL_180_B", "QUAL_225", "QUAL_225_B", "QUAL_270", "QUAL_315"],
@@ -48,12 +48,12 @@ npcs = {
"poses": [[350, 140], [310, 268], [385, 341], [481, 196], [502, 212], [771, 254]]
},
Npc.MALAH: {
"name_tag_white": color_filter(TemplateFinder().get_template("MALAH_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("MALAH_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("MALAH_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("MALAH_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(TemplateFinder().get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("TRADE_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["MALAH_FRONT", "MALAH_BACK", "MALAH_45", "MALAH_SIDE", "MALAH_SIDE_2"],
@@ -61,12 +61,12 @@ npcs = {
"poses": [[445, 485], [526, 473], [602, 381], [623, 368], [641, 323], [605, 300], [622, 272], [638, 284], [677, 308], [710, 288]]
},
Npc.LARZUK: {
"name_tag_white": color_filter(TemplateFinder().get_template("LARZUK_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("LARZUK_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("LARZUK_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("LARZUK_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade_repair": {
"white": color_filter(TemplateFinder().get_template("TRADE_REPAIR"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("TRADE_REPAIR_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("TRADE_REPAIR"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_REPAIR_BLUE"), Config().colors["blue"])[1],
}
},
"roi": [570, 70, (1038-570), (290-70)],
@@ -74,35 +74,35 @@ npcs = {
"poses": [[733, 192], [911, 143]]
},
Npc.ANYA: {
"name_tag_white": color_filter(TemplateFinder().get_template("ANYA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("ANYA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("ANYA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("ANYA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(TemplateFinder().get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("TRADE_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["ANYA_FRONT", "ANYA_BACK", "ANYA_SIDE"]
},
Npc.TYRAEL: {
"name_tag_white": color_filter(TemplateFinder().get_template("TYRAEL_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("TYRAEL_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("TYRAEL_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("TYRAEL_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"resurrect": {
"white": color_filter(TemplateFinder().get_template("RESURRECT"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("RESURRECT_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("RESURRECT"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("RESURRECT_BLUE"), Config().colors["blue"])[1],
}
},
"roi": [569, 86, (852-569), (357-86)],
"template_group": ["TYRAEL_1", "TYRAEL_2"]
},
Npc.ORMUS: {
"name_tag_white": color_filter(TemplateFinder().get_template("ORMUS_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("ORMUS_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("ORMUS_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("ORMUS_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(TemplateFinder().get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("TRADE_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
}
},
"roi": [444, 13, (816-444), (331-13)],
@@ -110,82 +110,82 @@ npcs = {
"template_group": ["ORMUS_0", "ORMUS_1", "ORMUS_2", "ORMUS_3", "ORMUS_4", "ORMUS_5"]
},
Npc.FARA: {
"name_tag_white": color_filter(TemplateFinder().get_template("FARA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("FARA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("FARA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("FARA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade_repair": {
"white": color_filter(TemplateFinder().get_template("TRADE_REPAIR"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("TRADE_REPAIR_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("TRADE_REPAIR"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_REPAIR_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["FARA_LIGHT_1", "FARA_LIGHT_2", "FARA_LIGHT_3", "FARA_LIGHT_4", "FARA_LIGHT_5", "FARA_LIGHT_6", "FARA_LIGHT_7", "FARA_LIGHT_8", "FARA_LIGHT_9", "FARA_MEDIUM_1", "FARA_MEDIUM_2", "FARA_MEDIUM_3", "FARA_MEDIUM_4", "FARA_MEDIUM_5", "FARA_MEDIUM_6", "FARA_MEDIUM_7", "FARA_DARK_1", "FARA_DARK_2", "FARA_DARK_3", "FARA_DARK_4", "FARA_DARK_5", "FARA_DARK_6", "FARA_DARK_7"]
},
Npc.DROGNAN: {
"name_tag_white": color_filter(TemplateFinder().get_template("DROGNAN_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("DROGNAN_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("DROGNAN_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("DROGNAN_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(TemplateFinder().get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("TRADE_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["DROGNAN_FRONT", "DROGNAN_LEFT", "DROGNAN_RIGHT_SIDE"]
},
Npc.LYSANDER: {
"name_tag_white": color_filter(TemplateFinder().get_template("LYSANDER_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("LYSANDER_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("LYSANDER_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("LYSANDER_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(TemplateFinder().get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("TRADE_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["LYSANDER_FRONT", "LYSANDER_BACK", "LYSANDER_SIDE", "LYSANDER_SIDE_2"]
},
Npc.CAIN: {
"name_tag_white": color_filter(TemplateFinder().get_template("CAIN_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("CAIN_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("CAIN_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("CAIN_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"identify": {
"white": color_filter(TemplateFinder().get_template("IDENTIFY"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("IDENTIFY_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("IDENTIFY"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("IDENTIFY_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["CAIN_0", "CAIN_1", "CAIN_2", "CAIN_3"]
},
Npc.JAMELLA: {
"name_tag_white": color_filter(TemplateFinder().get_template("JAMELLA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("JAMELLA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("JAMELLA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("JAMELLA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(TemplateFinder().get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("TRADE_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
},
"gamble": {
"white": color_filter(TemplateFinder().get_template("GAMBLE"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("GAMBLE_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("GAMBLE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("GAMBLE_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["JAMELLA_FRONT", "JAMELLA_BACK", "JAMELLA_SIDE", "JAMELLA_SIDE_2", "JAMELLA_SIDE_3", "JAMELLA_DRAWING"]
},
Npc.HALBU: {
"name_tag_white": color_filter(TemplateFinder().get_template("HALBU_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("HALBU_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("HALBU_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("HALBU_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade_repair": {
"white": color_filter(TemplateFinder().get_template("TRADE_REPAIR"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("TRADE_REPAIR_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("TRADE_REPAIR"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_REPAIR_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["HALBU_FRONT", "HALBU_BACK", "HALBU_SIDE", "HALBU_SIDE_2"]
},
Npc.AKARA: {
"name_tag_white": color_filter(TemplateFinder().get_template("AKARA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("AKARA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("AKARA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("AKARA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(TemplateFinder().get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("TRADE_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
}
},
"roi": [603, 176, (1002-603), (478-176)],
@@ -193,12 +193,12 @@ npcs = {
"template_group": ["AKARA_FRONT", "AKARA_BACK", "AKARA_SIDE", "AKARA_SIDE_2"]
},
Npc.CHARSI: {
"name_tag_white": color_filter(TemplateFinder().get_template("CHARSI_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("CHARSI_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("CHARSI_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("CHARSI_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade_repair": {
"white": color_filter(TemplateFinder().get_template("TRADE_REPAIR"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("TRADE_REPAIR_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("TRADE_REPAIR"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_REPAIR_BLUE"), Config().colors["blue"])[1],
}
},
"roi": [249, 76, (543-249), (363-76)],
@@ -206,12 +206,12 @@ npcs = {
"template_group": ["CHARSI_FRONT", "CHARSI_BACK", "CHARSI_SIDE", "CHARSI_SIDE_2", "CHARSI_SIDE_3"]
},
Npc.KASHYA: {
"name_tag_white": color_filter(TemplateFinder().get_template("KASHYA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(TemplateFinder().get_template("KASHYA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"name_tag_white": color_filter(template_finder.get_template("KASHYA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("KASHYA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"resurrect": {
"white": color_filter(TemplateFinder().get_template("RESURRECT"), Config().colors["white"])[1],
"blue": color_filter(TemplateFinder().get_template("RESURRECT_BLUE"), Config().colors["blue"])[1],
"white": color_filter(template_finder.get_template("RESURRECT"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("RESURRECT_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["KASHYA_FRONT", "KASHYA_BACK", "KASHYA_SIDE", "KASHYA_SIDE_2"]
@@ -242,11 +242,11 @@ def open_npc_menu(npc_key: Npc) -> bool:
roi_npc = npcs[npc_key]["roi"]
else:
roi_npc = roi_npc_search
res = TemplateFinder().search(key, img, threshold=0.35, roi=roi_npc, normalize_monitor=True)
res = template_finder.search(key, img, threshold=0.35, roi=roi_npc)
if res.valid:
is_unique = True
for r in results:
if (abs(r["pos"][0] - res.center[0]) + abs(r["pos"][1] - res.center[1])) < 22:
if (abs(r["pos"][0] - res.center_monitor[0]) + abs(r["pos"][1] - res.center_monitor[1])) < 22:
is_unique = False
break
if is_unique:
@@ -254,9 +254,9 @@ def open_npc_menu(npc_key: Npc) -> bool:
if attempts == 0 and "poses" in npcs[npc_key]:
# find distance between template match and nearest pose (([x2] - x1)**2 + (y2 - y1)**2)
for pose in npcs[npc_key]["poses"]:
dist = sqrt((res.center[0] - pose[0])**2 + (res.center[1] - pose[1])**2)
dist = sqrt((res.center_monitor[0] - pose[0])**2 + (res.center_monitor[1] - pose[1])**2)
min_dist = dist if dist < min_dist else min_dist
results.append({"pos": res.center, "score": res.score, "combo": min_dist / (res.score**2)})
results.append({"pos": res.center_monitor, "score": res.score, "combo": min_dist / (res.score**2)})
# sort by composite of template match score and distance to NPC pose
results = sorted(results, key=lambda r: r["combo"])
for result in results:
@@ -266,14 +266,14 @@ def open_npc_menu(npc_key: Npc) -> bool:
img = escape_dialogue(img)
_, filtered_inp_w = color_filter(img, Config().colors["white"])
_, filtered_inp_g = color_filter(img, Config().colors["gold"])
res_w = TemplateFinder().search(npcs[npc_key]["name_tag_white"], filtered_inp_w, 0.9, roi=roi).valid
res_g = TemplateFinder().search(npcs[npc_key]["name_tag_gold"], filtered_inp_g, 0.9, roi=roi).valid
res_w = template_finder.search(npcs[npc_key]["name_tag_white"], filtered_inp_w, 0.9, roi=roi).valid
res_g = template_finder.search(npcs[npc_key]["name_tag_gold"], filtered_inp_g, 0.9, roi=roi).valid
if res_w:
mouse.click(button="left")
attempts += 1
wait(0.7, 1.0)
_, filtered_inp = color_filter(grab(), Config().colors["gold"])
res = TemplateFinder().search(npcs[npc_key]["name_tag_gold"], filtered_inp, 0.9, roi=roi).valid
res = template_finder.search(npcs[npc_key]["name_tag_gold"], filtered_inp, 0.9, roi=roi).valid
if res:
return True
elif res_g:
@@ -285,21 +285,19 @@ def press_npc_btn(npc_key: Npc, action_btn_key: str):
img = grab()
img = escape_dialogue(img)
_, filtered_inp_w = color_filter(img, Config().colors["white"])
res = TemplateFinder().search(
res = template_finder.search(
npcs[npc_key]["action_btns"][action_btn_key]["white"],
filtered_inp_w, 0.85, roi=Config().ui_roi["cut_skill_bar"],
normalize_monitor=True
filtered_inp_w, 0.85, roi=Config().ui_roi["cut_skill_bar"]
)
if not res.valid and "blue" in npcs[npc_key]["action_btns"][action_btn_key]:
# search for highlighted / blue action btn
_, filtered_inp_b = color_filter(img, Config().colors["blue"])
res = TemplateFinder().search(
res = template_finder.search(
npcs[npc_key]["action_btns"][action_btn_key]["blue"],
filtered_inp_b, 0.85, roi=Config().ui_roi["cut_skill_bar"],
normalize_monitor=True
filtered_inp_b, 0.85, roi=Config().ui_roi["cut_skill_bar"]
)
if res.valid:
mouse.move(*res.center, randomize=3, delay_factor=[1.0, 1.5])
mouse.move(*res.center_monitor, randomize=3, delay_factor=[1.0, 1.5])
wait(0.2, 0.4)
mouse.click(button="left")
center_mouse()
+2 -3
View File
@@ -7,7 +7,6 @@ from rapidfuzz.string_metric import levenshtein
import csv
from utils.misc import erode_to_black
from logger import Logger
from typing import List, Union
from dataclasses import dataclass
@dataclass
@@ -65,7 +64,7 @@ class Ocr:
return image.tobytes(), width, height, bytes_per_pixel, bytes_per_line
def image_to_text(self,
images: Union[np.ndarray, List[np.ndarray]],
images: np.ndarray | list[np.ndarray],
model: str = "engd2r_inv_th",
psm: int = 3,
word_list: str = "all_strings.txt",
@@ -266,7 +265,7 @@ class Ocr:
while "II" in text:
cnt += 1
if cnt >30:
Logger.error(f"Error 4 on {ocr_output}")
Logger.error(f"Error ' II ' -> ' 11 ' on {ocr_output}")
break
text = text.replace("II", "11")
repeat=True
+35 -25
View File
@@ -3,7 +3,6 @@ import keyboard
import time
import os
import random
from typing import Tuple, Union, List
import cv2
import numpy as np
from utils.custom_mouse import mouse
@@ -12,7 +11,7 @@ from utils.misc import is_in_roi
from config import Config
from logger import Logger
from screen import convert_screen_to_monitor, convert_abs_to_screen, convert_abs_to_monitor, convert_screen_to_abs, grab, stop_detecting_window
from template_finder import TemplateFinder
import template_finder
from char import IChar
from ui_manager import detect_screen_object, ScreenObjects, is_visible, select_screen_object_match
@@ -129,11 +128,11 @@ class Pather:
143: {'SHENK_4': (-251, 165), 'SHENK_2': (141, 505), 'SHENK_3': (549, 139), 'SHENK_6': (-339, -69)},
144: {'SHENK_6': (-108, 123), 'SHENK_7': (481, 151)},
145: {'SHENK_7': (803, 372), 'SHENK_12': (97, -133), 'SHENK_6': (209, 347), 'SHENK_8': (-245, 18)},
146: {'SHENK_12': (272, 111), 'SHENK_9': (-331, -144), 'SHENK_8': (-72, 258)},
147: {'SHENK_16': (317, -18), 'SHENK_9': (-67, 139), 'SHENK_10': (-431, 67)},
148: {'SHENK_16': (682, 103), 'SHENK_9': (301, 263), 'SHENK_10': (-65, 188), 'SHENK_11': (-306, 139)},
146: {'SHENK_12': (272, 111), 'SHENK_9': (-331, -144), 'SHENK_8': (-72, 258), 'SHENK_19': (-120, -221)},
147: {'SHENK_16': (317, -18), 'SHENK_9': (-67, 139), 'SHENK_10': (-431, 67), 'SHENK_19': (468, 144), 'SHENK_20': (-61, -278)},
148: {'SHENK_16': (682, 103), 'SHENK_9': (301, 263), 'SHENK_10': (-65, 188), 'SHENK_11': (-306, 139), 'SHENK_19': (108, 25), 'SHENK_20': (308, -154)},
149: {'SHENK_11': (261, 395), 'SHENK_10': (495, 421), 'SHENK_13': (393, -9)},
150: {"SHENK_V2_6": (-80, -6), "SHENK_V2_3": (-89, 5), "SHENK_V2_4": (-16, -138), "SHENK_V2_7": (-15, -144), "SHENK_V2_8": (-193, -160)},
150: {"SHENK_V2_6": (-80, -6), "SHENK_V2_3": (-89, 5), "SHENK_V2_4": (-16, -138), "SHENK_V2_7": (-15, -144), "SHENK_V2_8": (-193, -160), 'SHENK_20': (461, 9)},
151: {"SHENK_V2_8": (109, 88), "SHENK_V2_7": (287, 104), "SHENK_V2_4": (286, 110), "SHENK_V2_6": (222, 242), "SHENK_V2_3": (213, 253)},
# A4 town
160: {"A4_TOWN_4": (-100, -83), "A4_TOWN_3": (-117, 288), "A4_TOWN_0": (-364, 201), "A4_TOWN_6": (24, -375), "A4_TOWN_5": (-347, -227)},
@@ -164,13 +163,15 @@ class Pather:
222: {"TRAV_5": (-218, 106), "TRAV_4": (120, 230), "TRAV_2": (426, 7), "TRAV_7": (-719, -166), "TRAV_7_V2": (-719, -166), "TRAV_1": (862, -153)},
223: {"TRAV_5": (344, 123), 'TRAV_4': (682, 247), "TRAV_8": (-353, -31), "TRAV_7": (-157, -149), "TRAV_7_V2": (-157, -149), "TRAV_22": (-368, -222), "TRAV_22_V2": (-368, -222), "TRAV_23": (-579, 116)},
224: {'TRAV_7': (411, -129), 'TRAV_7_V2': (411, -129), 'TRAV_27': (-363, 163), "TRAV_8": (214, -11), "TRAV_23": (-11, 136), "TRAV_10": (-130, -187), "TRAV_10_V2": (-130, -187), "TRAV_24": (-274, 15), "TRAV_22": (200, -202), "TRAV_22_V2": (200, -202)},
225: {'TRAV_27': (96, 359), 'TRAV_8': (670, 187), 'TRAV_7': (867, 69), 'TRAV_7_V2': (867, 69), "TRAV_11": (10, 214), "TRAV_19": (-298, 539), "TRAV_24": (181, 213), "TRAV_12": (-408, -73), "TRAV_25": (-538, 132)},
225: {'TRAV_27': (96, 359), 'TRAV_8': (670, 187), 'TRAV_7': (867, 69), 'TRAV_7_V2': (867, 69), "TRAV_11": (10, 214), "TRAV_19": (-298, 539), "TRAV_24": (181, 213), "TRAV_12": (-408, -73), "TRAV_25": (-538, 132), 'TRAV_225_V3_0': (549, -147), 'TRAV_225_V3_1': (555, 125), 'TRAV_225_V3_3': (-149, -229), 'TRAV_225_V3_7': (-242, 13), 'TRAV_V2_2': (-27, 103), 'TRAV_225_V3_2': (31, 135)},
226: {"TRAV_12": (-75, -172), "TRAV_25": (-205, 33), "TRAV_13": (-252, 195), "TRAV_11": (343, 115), "TRAV_18": (-514, 373), "TRAV_19": (35, 440), "TRAV_17": (-231, 242), "TRAV_27": (428, 263), "TRAV_29": (-929, 334), "TRAV_28": (-614, 338)},
227: {"TRAV_11": (65, -42), "TRAV_24": (236, -43), "TRAV_19": (-243, 283), 'TRAV_18': (-792, 216), "TRAV_12": (-356, -330), "TRAV_25": (-483, -124), 'TRAV_27': (154, 104)},
228: {"TRAV_13": (8, 9), "TRAV_17": (29, 56), "TRAV_25": (58, -152), "TRAV_16": (-198, -110), "TRAV_18": (-251, 188)},
229: {"TRAV_18": (-250, 58), "TRAV_25": (59, -282), "TRAV_17": (30, -74), "TRAV_13": (9, -121), "TRAV_16": (-138, -241)},
230: {"TRAV_19": (157, 39), "TRAV_18": (-392, -28), "TRAV_17": (-112, -160), "TRAV_13": (-133, -207), "TRAV_25": (-83, -368)},
300: {"TRAV_V3_4": (-101, 134), "TRAV_V3_5": (72, 220), "TRAV_V3_1": (237, -24), "TRAV_V3_3": (-318, 224), "TRAV_V3_11": (472, 39)},
300: {"TRAV_V3_4": (-101, 134), "TRAV_V3_5": (72, 220), "TRAV_V3_1": (237, -24), "TRAV_V3_3": (-318, 224), "TRAV_V3_11": (472, 39),
"TRAV_16": (129, -86), "TRAV_V2_0": (472, 65), "TRAV_17": (357, 84), "TRAV_12": (512, -333),
},
301: {"TRAV_V3_7": (178, -33), "TRAV_V3_6": (170, 157), "TRAV_V3_0": (88, -235), "TRAV_V3_5": (-335, -95), "TRAV_V3_8": (444, -108)},
302: {"TRAV_V3_0": (-18, 6), "TRAV_V3_7": (73, 208), "TRAV_V3_8": (339, 133), "TRAV_V3_6": (65, 398), "TRAV_V3_5": (-440, 146)},
304: {"TRAV_V2_4": (125, -148), "TRAV_V2_3": (-187, 55), "TRAV_V2_1": (-207, 59), "TRAV_V2_2": (267, 183), "TRAV_V2_0": (-159, 403)},
@@ -492,10 +493,10 @@ class Pather:
)
@staticmethod
def _convert_rel_to_abs(rel_loc: Tuple[float, float], pos_abs: Tuple[float, float]) -> Tuple[float, float]:
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 traverse_nodes_fixed(self, key: Union[str, List[Tuple[float, float]]], char: IChar) -> bool:
def traverse_nodes_fixed(self, key: str | list[tuple[float, float]], char: IChar) -> bool:
if not char.capabilities.can_teleport_natively:
error_msg = "Teleport is required for static pathing"
Logger.error(error_msg)
@@ -511,9 +512,9 @@ class Pather:
x_m, y_m = convert_screen_to_monitor(path[i])
x_m += int(random.random() * 6 - 3)
y_m += int(random.random() * 6 - 3)
t0 = grab()
t0 = grab(force_new=True)
char.move((x_m, y_m))
t1 = grab()
t1 = grab(force_new=True)
# check difference between the two frames to determine if tele was good or not
diff = cv2.absdiff(t0, t1)
diff = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
@@ -530,7 +531,7 @@ class Pather:
# cv2.imwrite(f"./info_screenshots/nil_path_{key}_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
return True
def _adjust_abs_range_to_screen(self, abs_pos: Tuple[float, float]) -> Tuple[float, float]:
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
@@ -569,9 +570,9 @@ class Pather:
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, threshold: float = 0.68) -> Tuple[float, float]:
def find_abs_node_pos(self, node_idx: int, img: np.ndarray, threshold: float = 0.68) -> tuple[float, float]:
node = self._nodes[node_idx]
template_match = TemplateFinder().search(
template_match = template_finder.search(
[*node],
img,
best_match=False,
@@ -591,7 +592,7 @@ class Pather:
def traverse_nodes(
self,
path: Union[tuple[Location, Location], list[int]],
path: tuple[Location, Location] | list[int],
char: IChar,
timeout: float = 5,
force_tp: bool = False,
@@ -638,10 +639,10 @@ class Pather:
did_force_move = False
teleport_count = 0
while not continue_to_next_node:
img = grab()
img = grab(force_new=True)
# Handle timeout
if (time.time() - last_move) > timeout:
if is_visible(ScreenObjects.WaypointLabel):
if is_visible(ScreenObjects.WaypointLabel, img):
# sometimes bot opens waypoint menu, close it to find templates again
Logger.debug("Opened wp, closing it again")
keyboard.send("esc")
@@ -658,9 +659,11 @@ class Pather:
# Sometimes we get stuck at rocks and stuff, after a few seconds force a move into the last known direction
if not did_force_move and time.time() - last_move > 3.1:
pos_abs = (0, 150)
if last_direction is not None:
pos_abs = last_direction
else:
angle = random.random() * math.pi * 2
pos_abs = (math.cos(angle) * 150, math.sin(angle) * 150)
pos_abs = self._adjust_abs_range_to_screen(pos_abs)
Logger.debug(f"Pather: taking a random guess towards " + str(pos_abs))
x_m, y_m = convert_abs_to_monitor(pos_abs)
@@ -670,7 +673,7 @@ class Pather:
# Sometimes we get stuck at a Shrine or Stash, after a few seconds check if the screen was different, if force a left click.
if (teleport_count + 1) % 30 == 0:
if (match := detect_screen_object(ScreenObjects.ShrineArea)).valid:
if (match := detect_screen_object(ScreenObjects.ShrineArea, img)).valid:
if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_shrine_check_before" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
Logger.debug(f"Shrine found, activating it")
select_screen_object_match(match)
@@ -706,9 +709,9 @@ if __name__ == "__main__":
display_img = img.copy()
template_map = {}
template_scores = {}
for template_type in TemplateFinder()._templates:
for template_type in template_finder.stored_templates().keys():
if filter is None or filter in template_type:
template_match = TemplateFinder().search(template_type, img, use_grayscale=True, threshold=0.78)
template_match = template_finder.search(template_type, img, use_grayscale=True, threshold=0.78)
if template_match.valid:
template_map[template_type] = template_match.center
template_scores[template_type] = template_match.score
@@ -746,9 +749,16 @@ if __name__ == "__main__":
start_detecting_window()
from config import Config
from char.sorceress import LightSorc
from char.hammerdin import Hammerdin
from char.paladin.hammerdin import Hammerdin
from item.pickit import PickIt
pather = Pather()
display_all_nodes(pather, "ELDRITCH")
#char = Hammerdin(Config().hammerdin, pather, PickIt) #Config().char,
#char.discover_capabilities()
stop_detecting_window()
display_all_nodes(pather, "TRAV_")
#pather.traverse_nodes([120, 121, 122, 123, 122, 121, 120], char) #works!
#pather.traverse_nodes_fixed("dia_trash_c", char)
#display_all_nodes(pather, "SHENK")
#pather.traverse_nodes([141,142,143], char)
#stop_detecting_window()
+11 -7
View File
@@ -2,9 +2,8 @@ from char.i_char import IChar
from config import Config
from logger import Logger
from pather import Location, Pather
from typing import Union
from item.pickit import PickIt
from template_finder import TemplateFinder
import template_finder
from town.town_manager import TownManager
from utils.misc import wait
from dataclasses import dataclass
@@ -13,20 +12,25 @@ from ui import waypoint
from health_manager import set_pause_state
class Arcane:
name = "run_arcane"
def __init__(
self,
pather: Pather,
town_manager: TownManager,
char: IChar,
pickit: PickIt,
runs: list[str]
):
self._pather = pather
self._town_manager = town_manager
self._char = char
self._pickit = pickit
self._chest = Chest(self._char, 'arcane')
self._runs = runs
def approach(self, start_loc: Location) -> Union[bool, Location]:
def approach(self, start_loc: Location) -> bool | Location:
Logger.info("Run Arcane")
set_pause_state(True)
if not self._char.capabilities.can_teleport_natively:
@@ -42,13 +46,13 @@ class Arcane:
# Check if we arrived at platform
templates_platform = ["ARC_PLATFORM_1", "ARC_PLATFORM_2", "ARC_PLATFORM_3", "ARC_CENTER"]
tempaltes_summoner = ["ARC_ALTAR", "ARC_ALTAR3", "ARC_END_STAIRS", "ARC_END_STAIRS_2"]
match_platform = TemplateFinder().search_and_wait(templates_platform, threshold=0.55, timeout=0.5, use_grayscale=True, take_ss=False)
match_summoner = TemplateFinder().search_and_wait(tempaltes_summoner, threshold=0.79, timeout=0.5, use_grayscale=True, take_ss=False)
match_platform = template_finder.search_and_wait(templates_platform, threshold=0.55, timeout=0.5, use_grayscale=True)
match_summoner = template_finder.search_and_wait(tempaltes_summoner, threshold=0.79, timeout=0.5, use_grayscale=True)
if not match_platform.valid and not match_summoner.valid:
# We might have arrived at summoner, move up stairs with static traverse
self._pather.traverse_nodes_fixed(traverse_to_summoner, self._char)
# try to match summoner again
match_summoner = TemplateFinder().search_and_wait(tempaltes_summoner, threshold=0.79, timeout=1.0, use_grayscale=True, take_ss=False)
match_summoner = template_finder.search_and_wait(tempaltes_summoner, threshold=0.79, timeout=1.0, use_grayscale=True)
if match_summoner.valid:
if self._pather.traverse_nodes([461], self._char, timeout=2.2, force_tp=True):
return True
@@ -57,7 +61,7 @@ class Arcane:
self._pather.traverse_nodes([462], self._char, timeout=1.3, force_tp=True)
return False
def battle(self, do_pre_buff: bool) -> Union[bool, tuple[Location, bool]]:
def battle(self, do_pre_buff: bool) -> bool | tuple[Location, bool]:
picked_up_items = False
@dataclass
class PathData:
+90 -97
View File
@@ -4,9 +4,8 @@ from char.i_char import IChar
from config import Config
from logger import Logger
from pather import Location, Pather
from typing import Union
from item.pickit import PickIt
from template_finder import TemplateFinder
import template_finder
from town.town_manager import TownManager, A4
from utils.misc import wait
from utils.custom_mouse import mouse
@@ -16,12 +15,16 @@ from ui import skills, loading, waypoint
from inventory import belt, personal
class Diablo:
name = "run_diablo"
def __init__(
self,
pather: Pather,
town_manager: TownManager,
char: IChar,
pickit: PickIt
pickit: PickIt,
runs: list[str]
):
self._pather = pather
self._town_manager = town_manager
@@ -29,12 +32,14 @@ class Diablo:
self._pickit = pickit
self._picked_up_items = False
self.used_tps = 0
self._curr_loc: Union[bool, Location] = Location.A4_TOWN_START
self._curr_loc: bool | Location = Location.A4_TOWN_START
self._runs = runs
def approach(self, start_loc: Location) -> Union[bool, Location, bool]:
def approach(self, start_loc: Location) -> bool | Location:
Logger.info("Run Diablo")
Logger.debug("settings for trash =" + str(Config().char["kill_cs_trash"]))
Logger.debug("settings for mob_detection =" + str(Config().char["cs_mob_detect"]))
if not self._char.capabilities.can_teleport_natively:
raise ValueError("Diablo requires teleport")
if not self._town_manager.open_wp(start_loc):
@@ -52,7 +57,7 @@ class Diablo:
if not buy_pots:
Logger.debug(location + ": Got enough pots, no need to go to town right now.")
else:
Logger.debug(location + ": Going back to town to visit our friend Jamella (heal/clear debuffs/restock potions)")
Logger.info(location + ": Going back to town to visit our friend Jamella (heal/clear debuffs/restock potions)")
success = self._char.tp_town()
if success:
self._curr_loc = self._town_manager.wait_for_tp(self._curr_loc)
@@ -81,7 +86,7 @@ class Diablo:
if not self._pather.traverse_nodes([164, 163], self._char, timeout=2): return False
wait(0.22, 0.28)
if (template_match := detect_screen_object(ScreenObjects.TownPortalReduced)).valid:
pos = template_match.center
pos = template_match.center_monitor
pos = (pos[0], pos[1] + 30)
Logger.debug(location + ": Going through portal...")
# Note: Template is top of portal, thus move the y-position a bit to the bottom
@@ -97,11 +102,11 @@ class Diablo:
Logger.debug("CS after town: Re-open TP")
if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/TP_after_town" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
if not skills.has_tps():
Logger.warning("CS after Town: failed to open TP, higher chance of failing runs from now on, you should buy new TPs! (hint: always_repair=1)")
Logger.warning("CS after Town: failed to open TP, higher chance of failing runs from now on, you should buy new TPs!")
self.used_tps += 20
mouse.click(button="right")
self.used_tps += 1
Logger.debug("CS after town: FYI, total TPs used: " + str(self.used_tps))
Logger.info("Back in CS after visiting town: FYI, total TPs used: " + str(self.used_tps))
return True
@@ -112,9 +117,9 @@ class Diablo:
Logger.debug(seal_layout + ": trying to open (try #" + str(i+1)+")")
self._char.select_by_template(seal_closedtemplates, threshold=0.5, timeout=0.1, telekinesis=True)
wait(i*0.5)
found = TemplateFinder().search_and_wait(seal_opentemplates, threshold=0.75, timeout=0.1, best_match=True, take_ss=False).valid
found = template_finder.search_and_wait(seal_opentemplates, threshold=0.75, timeout=0.1).valid
if found:
Logger.debug(seal_layout +": is open - "+'\033[92m'+" open"+'\033[0m')
Logger.info(seal_layout +": is open - "+'\033[92m'+" open"+'\033[0m')
break
else:
Logger.debug(seal_layout +": is closed - "+'\033[91m'+" closed"+'\033[0m')
@@ -142,7 +147,7 @@ class Diablo:
templates = ["DIA_NEW_PENT_TP", "DIA_NEW_PENT_0", "DIA_NEW_PENT_1", "DIA_NEW_PENT_2"]
start_time = time.time()
while not found and time.time() - start_time < 15:
found = TemplateFinder().search_and_wait(templates, threshold=0.83, timeout=0.1, best_match=True, take_ss=False).valid
found = template_finder.search_and_wait(templates, threshold=0.83, timeout=0.1, suppress_debug=True).valid
if not found: self._pather.traverse_nodes_fixed(path, self._char)
if not found:
if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_failed_loop_pentagram_" + path + "_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
@@ -152,7 +157,7 @@ class Diablo:
#CLEAR CS TRASH
def _entrance_hall(self) -> bool:
Logger.debug("CS Trash: Starting to clear Trash")
Logger.info("CS Trash: Starting to clear Trash")
Logger.debug("CS Trash: clearing first hall 1/2 - location: entrance_hall_01")
self._char.kill_cs_trash("entrance_hall_01")
Logger.debug("CS Trash: clearing first hall 1/2 - location: entrance_hall_02")
@@ -160,10 +165,10 @@ class Diablo:
if not self._pather.traverse_nodes([605], self._char): return False
templates = ["DIABLO_ENTRANCE_53", "DIABLO_ENTRANCE_51","DIABLO_ENTRANCE_50", "DIABLO_ENTRANCE_52", "DIABLO_ENTRANCE_54", "DIABLO_ENTRANCE_55"]
if TemplateFinder().search_and_wait(templates, threshold=0.8, timeout=0.1, best_match=False, take_ss=False).valid:
if template_finder.search_and_wait(templates, threshold=0.8, timeout=0.1).valid:
Logger.debug("CS Trash (A): Layout_check step 1/2: Layout A templates found")
templates = ["DIABLO_ENTRANCE2_55", "DIABLO_ENTRANCE2_50", "DIABLO_ENTRANCE2_51", "DIABLO_ENTRANCE2_52","DIABLO_ENTRANCE2_53","DIABLO_ENTRANCE2_54","DIABLO_ENTRANCE2_15","DIABLO_ENTRANCE2_56"]
if not TemplateFinder().search_and_wait(templates, threshold=0.8, timeout=0.5, best_match=True, take_ss=False).valid:
if not template_finder.search_and_wait(templates, threshold=0.8, timeout=0.5).valid:
Logger.debug("CS Trash (A): Layout_check step 2/2: Layout B templates NOT found - "+'\033[95m'+"all fine, proceeding with Layout A"+'\033[0m')
entrance1_layout = "CS Trash (A):"
#if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_" + entrance1_layout + "_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
@@ -184,7 +189,7 @@ class Diablo:
else:
Logger.debug("CS Trash (B): Layout_check step 1/2: Layout A templates NOT found")
templates = ["DIABLO_ENTRANCE2_55", "DIABLO_ENTRANCE2_50", "DIABLO_ENTRANCE2_51", "DIABLO_ENTRANCE2_52","DIABLO_ENTRANCE2_53","DIABLO_ENTRANCE2_54","DIABLO_ENTRANCE2_15","DIABLO_ENTRANCE2_56"]
if TemplateFinder().search_and_wait(templates, threshold=0.8, timeout=0.1, best_match=False, take_ss=False).valid:
if template_finder.search_and_wait(templates, threshold=0.8, timeout=0.1).valid:
Logger.debug("CS Trash (B): Layout_check step 2/2: Layout B templates found - "+'\033[96m'+"all fine, proceeding with Layout B"+'\033[0m')
entrance2_layout = "CS Trash (B):"
#if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_" + entrance2_layout + "_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
@@ -213,7 +218,7 @@ class Diablo:
templates = ["DIA_NEW_PENT_0", "DIA_NEW_PENT_1", "DIA_NEW_PENT_2"]
start_time = time.time()
while not found and time.time() - start_time < 10:
found = TemplateFinder().search_and_wait(templates, threshold=0.8, timeout=0.1, best_match=True, take_ss=False).valid
found = template_finder.search_and_wait(templates, threshold=0.8, timeout=0.1, suppress_debug=True).valid
if not found:
self._pather.traverse_nodes_fixed("diablo_wp_pentagram_loop", self._char)
if not found:
@@ -234,7 +239,7 @@ class Diablo:
templates = ["DIABLO_CS_ENTRANCE_0", "DIABLO_CS_ENTRANCE_2", "DIABLO_CS_ENTRANCE_3"]
start_time = time.time()
while not found and time.time() - start_time < 10:
found = TemplateFinder().search_and_wait(templates, threshold=0.8, timeout=0.1, best_match=True, take_ss=False).valid
found = template_finder.search_and_wait(templates, threshold=0.8, timeout=0.1, suppress_debug=True).valid
if not found:
self._pather.traverse_nodes_fixed("diablo_wp_entrance_loop", self._char)
if not found:
@@ -250,7 +255,7 @@ class Diablo:
templates = ["DIA_NEW_PENT_TP", "DIA_NEW_PENT_0", "DIA_NEW_PENT_1", "DIA_NEW_PENT_2"]
start_time = time.time()
while not found and time.time() - start_time < 15:
found = TemplateFinder().search_and_wait(templates, threshold=0.83, timeout=0.1, best_match=True, take_ss=False).valid
found = template_finder.search_and_wait(templates, threshold=0.83, timeout=0.1, suppress_debug=True).valid
if not found: self._pather.traverse_nodes_fixed("diablo_wp_pentagram_loop", self._char)
if not found:
if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_failed_loop_pentagram_diablo_wp_pentagram_loop_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
@@ -270,7 +275,7 @@ class Diablo:
self.used_tps += 1
Logger.debug("CS: FYI, total TPs used: " + str(self.used_tps))
if not self._pather.traverse_nodes([602], self._char, threshold=0.80): return False
Logger.debug("CS: Calibrated at PENTAGRAM")
Logger.info("CS: Calibrated at PENTAGRAM")
return True
"""
@@ -287,7 +292,7 @@ class Diablo:
templates = ["DIA_NEW_PENT_TP", "DIA_NEW_PENT_0", "DIA_NEW_PENT_1", "DIA_NEW_PENT_2"]
start_time = time.time()
while not found and time.time() - start_time < 15:
found = TemplateFinder().search_and_wait(templates, threshold=0.83, timeout=0.1, best_match=True, take_ss=False).valid
found = template_finder.search_and_wait(templates, threshold=0.83, timeout=0.1).valid
if not found: self._pather.traverse_nodes_fixed(loop_path, self._char)
if not found:
if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_failed_loop_pentagram_" + path + "_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
@@ -302,65 +307,65 @@ class Diablo:
self._pather.traverse_nodes([602], self._char)
self._pather.traverse_nodes_fixed("dia_trash_a", self._char)
Logger.debug("CS TRASH: A Pent to LC")
self._char.kill_cs_trash("trash_a")
self._char.kill_cs_trash("dia_trash_a")
#if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_Trash_A_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
Logger.info("CS TRASH: A looping to PENTAGRAM")
Logger.debug("CS TRASH: A looping to PENTAGRAM")
if not self._loop_pentagram("dia_a1l_home_loop"): return False
if not self._pather.traverse_nodes([602], self._char): return False
Logger.info("CS TRASH: A calibrated at PENTAGRAM")
Logger.debug("CS TRASH: A calibrated at PENTAGRAM")
self._pather.traverse_nodes_fixed("dia_trash_b", self._char)
Logger.debug("CS TRASH: B Pent to LC")
self._char.kill_cs_trash("trash_b")
self._char.kill_cs_trash("dia_trash_b")
#if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_Trash_B_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
Logger.info("CS TRASH: B looping to PENTAGRAM")
Logger.debug("CS TRASH: B looping to PENTAGRAM")
if not self._loop_pentagram("dia_b1s_home_loop"): return False
if not self._pather.traverse_nodes([602], self._char): return False
Logger.info("CS TRASH: B calibrated at PENTAGRAM")
Logger.debug("CS TRASH: B calibrated at PENTAGRAM")
self._pather.traverse_nodes_fixed("dia_trash_c", self._char)
Logger.debug("CS TRASH: C Pent to LC")
self._char.kill_cs_trash("trash_c")
self._char.kill_cs_trash("dia_trash_c")
#if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_Trash_C_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
Logger.info("CS TRASH: C looping to PENTAGRAM")
Logger.debug("CS TRASH: C looping to PENTAGRAM")
if not self._loop_pentagram("dia_c1f_home_loop"): return False
if not self._pather.traverse_nodes([602], self._char): return False
Logger.info("CS TRASH: C calibrated at PENTAGRAM")
Logger.debug("CS TRASH: C calibrated at PENTAGRAM")
#CHECK SEAL LAYOUT
def _layoutcheck(self, sealname:str, boss:str, static_layoutcheck:str, trash_location:str , calibration_node:str, calibration_threshold:str, confirmation_node:str, templates_primary:list[str], templates_confirmation:list[str]):
if sealname == "A":
seal_layout1:str = "A1-L"
seal_layout2:str = "A2-Y"
params_seal1 = seal_layout1, [614], [615], [611], "dia_a1l_home", "dia_a1l_home_loop", [602], ["DIA_A1L2_14_OPEN"], ["DIA_A1L2_14_CLOSED", "DIA_A1L2_14_CLOSED_DARK", "DIA_A1L2_14_MOUSEOVER"], ["DIA_A1L2_5_OPEN"], ["DIA_A1L2_5_CLOSED","DIA_A1L2_5_MOUSEOVER"]
params_seal2 = seal_layout2, [625], [626], [622], "dia_a2y_home", "dia_a2y_home_loop", [602], ["DIA_A2Y4_29_OPEN"], ["DIA_A2Y4_29_CLOSED", "DIA_A2Y4_29_MOUSEOVER"], ["DIA_A2Y4_36_OPEN"], ["DIA_A2Y4_36_CLOSED", "DIA_A2Y4_36_MOUSEOVER"]
threshold_primary=0.8
threshold_confirmation=0.85
threshold_confirmation2=0.8
confirmation_node2=None
elif sealname == "B":
seal_layout2:str = "B1-S"
seal_layout1:str = "B2-U"
params_seal2 = seal_layout2, None, [634], [632], "dia_b1s_home", "dia_b1s_home_loop", [602], None, None, ["DIA_B1S2_23_OPEN"], ["DIA_B1S2_23_CLOSED","DIA_B1S2_23_MOUSEOVER"]
params_seal1 = seal_layout1, None, [644], [640], "dia_b2u_home", "dia_b2u_home_loop", [602], None, None, ["DIA_B2U2_16_OPEN"], ["DIA_B2U2_16_CLOSED", "DIA_B2U2_16_MOUSEOVER"]
confirmation_node2=[634]
threshold_primary=0.8
threshold_confirmation2=0.8
threshold_confirmation=0.75
elif sealname == "C":
seal_layout1:str = "C1-F"
seal_layout2:str = "C2-G"
params_seal1 = seal_layout1, [655], [652], [654], "dia_c1f_home", "dia_c1f_home_loop", [602], ["DIA_C1F_OPEN_NEAR"], ["DIA_C1F_CLOSED_NEAR","DIA_C1F_MOUSEOVER_NEAR"], ["DIA_B2U2_16_OPEN", "DIA_C1F_BOSS_OPEN_RIGHT", "DIA_C1F_BOSS_OPEN_LEFT"], ["DIA_C1F_BOSS_MOUSEOVER_LEFT", "DIA_C1F_BOSS_CLOSED_NEAR_LEFT", "DIA_C1F_BOSS_CLOSED_NEAR_RIGHT"]
params_seal2 = seal_layout2, [661], [665], [665], "dia_c2g_home", "dia_c2g_home_loop", [602], ["DIA_C2G2_7_OPEN"], ["DIA_C2G2_7_CLOSED", "DIA_C2G2_7_MOUSEOVER"], ["DIA_C2G2_21_OPEN"], ["DIA_C2G2_21_CLOSED", "DIA_C2G2_21_MOUSEOVER"]
threshold_primary=0.8
confirmation_node2=None
threshold_confirmation=0.8
threshold_confirmation2=0.8
else:
Logger.warning(sealname + ": something is wrong - cannot check layouts: Aborting run.")
return False
match sealname:
case "A":
seal_layout1:str = "A1-L"
seal_layout2:str = "A2-Y"
params_seal1 = seal_layout1, [614], [615], [611], "dia_a1l_home", "dia_a1l_home_loop", [602], ["DIA_A1L2_14_OPEN"], ["DIA_A1L2_14_CLOSED", "DIA_A1L2_14_CLOSED_DARK", "DIA_A1L2_14_MOUSEOVER"], ["DIA_A1L2_5_OPEN"], ["DIA_A1L2_5_CLOSED","DIA_A1L2_5_MOUSEOVER"]
params_seal2 = seal_layout2, [625], [626], [622], "dia_a2y_home", "dia_a2y_home_loop", [602], ["DIA_A2Y4_29_OPEN"], ["DIA_A2Y4_29_CLOSED", "DIA_A2Y4_29_MOUSEOVER"], ["DIA_A2Y4_36_OPEN"], ["DIA_A2Y4_36_CLOSED", "DIA_A2Y4_36_MOUSEOVER"]
threshold_primary=0.8
threshold_confirmation=0.85
threshold_confirmation2=0.8
confirmation_node2=None
case "B":
seal_layout2:str = "B1-S"
seal_layout1:str = "B2-U"
params_seal2 = seal_layout2, None, [634], [632], "dia_b1s_home", "dia_b1s_home_loop", [602], None, None, ["DIA_B1S2_23_OPEN"], ["DIA_B1S2_23_CLOSED","DIA_B1S2_23_MOUSEOVER"]
params_seal1 = seal_layout1, None, [644], [640], "dia_b2u_home", "dia_b2u_home_loop", [602], None, None, ["DIA_B2U2_16_OPEN"], ["DIA_B2U2_16_CLOSED", "DIA_B2U2_16_MOUSEOVER"]
confirmation_node2=[634]
threshold_primary=0.8
threshold_confirmation2=0.8
threshold_confirmation=0.75
case "C":
seal_layout1:str = "C1-F"
seal_layout2:str = "C2-G"
params_seal1 = seal_layout1, [655], [652], [654], "dia_c1f_home", "dia_c1f_home_loop", [602], ["DIA_C1F_OPEN_NEAR"], ["DIA_C1F_CLOSED_NEAR","DIA_C1F_MOUSEOVER_NEAR"], ["DIA_B2U2_16_OPEN", "DIA_C1F_BOSS_OPEN_RIGHT", "DIA_C1F_BOSS_OPEN_LEFT"], ["DIA_C1F_BOSS_MOUSEOVER_LEFT", "DIA_C1F_BOSS_CLOSED_NEAR_LEFT", "DIA_C1F_BOSS_CLOSED_NEAR_RIGHT"]
params_seal2 = seal_layout2, [661], [665], [665], "dia_c2g_home", "dia_c2g_home_loop", [602], ["DIA_C2G2_7_OPEN"], ["DIA_C2G2_7_CLOSED", "DIA_C2G2_7_MOUSEOVER"], ["DIA_C2G2_21_OPEN"], ["DIA_C2G2_21_CLOSED", "DIA_C2G2_21_MOUSEOVER"]
threshold_primary=0.8
confirmation_node2=None
threshold_confirmation=0.8
threshold_confirmation2=0.8
case _:
Logger.warning(sealname + ": something is wrong - cannot check layouts: Aborting run.")
return False
self._pather.traverse_nodes_fixed(static_layoutcheck, self._char)
self._char.kill_cs_trash(trash_location)
@@ -369,25 +374,25 @@ class Diablo:
if not self._pather.traverse_nodes(calibration_node, self._char, threshold=calibration_threshold,): return False
#if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_LC_" + sealname + "_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
#check1 using primary templates
if not TemplateFinder().search_and_wait(templates_primary, threshold =threshold_primary, timeout=0.1, best_match=True, take_ss=False).valid:
if not template_finder.search_and_wait(templates_primary, threshold =threshold_primary, timeout=0.1).valid:
Logger.debug(f"{seal_layout1}: Layout_check step 1/2 - templates NOT found for "f"{seal_layout2}")
#cross-check for confirmation
if not confirmation_node == None:
if not self._pather.traverse_nodes(confirmation_node, self._char, threshold=calibration_threshold,): return False
if not TemplateFinder().search_and_wait(templates_confirmation, threshold=threshold_confirmation, timeout=0.1, best_match=True, take_ss=False).valid:
if not template_finder.search_and_wait(templates_confirmation, threshold=threshold_confirmation, timeout=0.1).valid:
Logger.warning(f"{seal_layout2}: Layout_check failure - could not determine the seal Layout at" f"{sealname} ("f"{boss}) - "+'\033[91m'+"aborting run"+'\033[0m')
if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_" + seal_layout1 + "_LC_fail" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
return False
else:
Logger.debug(f"{seal_layout1}: Layout_check step 2/2 - templates found for "f"{seal_layout1} - "+'\033[93m'+"all fine, proceeding with "f"{seal_layout1}"+'\033[0m')
Logger.info(f"{seal_layout1}: Layout_check step 2/2 - templates found for "f"{seal_layout1} - "+'\033[93m'+"all fine, proceeding with "f"{seal_layout1}"+'\033[0m')
if not self._seal(*params_seal1): return False
else:
Logger.debug(f"{seal_layout2}: Layout_check step 1/2 - templates found for {seal_layout1}")
#cross-check for confirmation
if not confirmation_node2 == None:
if not self._pather.traverse_nodes(confirmation_node2, self._char, threshold=calibration_threshold,): return False
if not TemplateFinder().search_and_wait(templates_confirmation, threshold=threshold_confirmation2, timeout=0.1, best_match=True, take_ss=False).valid:
Logger.debug(f"{seal_layout2}: Layout_check step 2/2 - templates NOT found for "f"{seal_layout1} - "+'\033[94m'+"all fine, proceeding with "f"{seal_layout2}"+'\033[0m')
if not template_finder.search_and_wait(templates_confirmation, threshold=threshold_confirmation2, timeout=0.1).valid:
Logger.info(f"{seal_layout2}: Layout_check step 2/2 - templates NOT found for "f"{seal_layout1} - "+'\033[96m'+"all fine, proceeding with "f"{seal_layout2}"+'\033[0m')
if not self._seal(*params_seal2): return False
else:
Logger.warning(f"{seal_layout2}: Layout_check failure - could not determine the seal Layout at" f"{sealname} ("f"{boss}) - "+'\033[91m'+"aborting run"+'\033[0m')
@@ -399,7 +404,7 @@ class Diablo:
#CLEAR SEAL
def _seal(self, seal_layout:str, node_seal1:str, node_seal2:str, node_calibrate_to_pent:str, static_pent:str, static_loop_pent:str, node_calibrate_at_pent:str, seal1_opentemplates:list[str], seal1_closedtemplates:list[str], seal2_opentemplates:list[str], seal2_closedtemplates:list[str], ) -> bool:
#if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_" + seal_layout + "_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
Logger.debug(seal_layout +": Starting to clear Seal")
Logger.info(seal_layout +": Starting to clear Seal")
### CLEAR TRASH ###
Logger.debug(seal_layout + "_01: Kill trash")
self._char.kill_cs_trash(seal_layout + "_01")
@@ -420,18 +425,19 @@ class Diablo:
if not self._pather.traverse_nodes(node_seal2, self._char): return False
if not self._sealdance(seal2_opentemplates, seal2_closedtemplates, seal_layout + ": Seal2", node_seal2): return False
### KILL BOSS ###
if seal_layout == ("A1-L") or seal_layout == ("A2-Y"):
Logger.debug(seal_layout + ": Kill Boss A (Vizier)")
self._char.kill_vizier(seal_layout)
elif seal_layout == ("B1-S") or seal_layout == ("B2-U"):
Logger.debug(seal_layout + ": Kill Boss B (De Seis)")
self._char.kill_deseis(seal_layout)
elif seal_layout == ("C1-F") or seal_layout == ("C2-G"):
Logger.debug(seal_layout + ": Kill Boss C (Infector)")
self._char.kill_infector(seal_layout)
else:
Logger.warning(seal_layout + ": Error - no Boss known here - aborting run")
return False
match seal_layout:
case "A1-L" | "A2-Y":
Logger.debug(seal_layout + ": Kill Boss A (Vizier)")
self._char.kill_vizier(seal_layout)
case "B1-S" | "B2-U":
Logger.debug(seal_layout + ": Kill Boss B (De Seis)")
self._char.kill_deseis(seal_layout)
case "C1-F" | "C2-G":
Logger.debug(seal_layout + ": Kill Boss C (Infector)")
self._char.kill_infector(seal_layout)
case _:
Logger.warning(seal_layout + ": Error - no Boss known here - aborting run")
return False
### GO HOME ###
if not self._pather.traverse_nodes(node_calibrate_to_pent, self._char): return False
Logger.debug(seal_layout + ": Static Pathing to Pentagram")
@@ -439,11 +445,11 @@ class Diablo:
Logger.debug(seal_layout + ": Looping to Pentagram")
if not self._loop_pentagram(static_loop_pent): return False
if not self._pather.traverse_nodes(node_calibrate_at_pent, self._char): return False
Logger.debug(seal_layout + ": finished seal & calibrated at PENTAGRAM")
Logger.info(seal_layout + ": finished seal & calibrated at PENTAGRAM")
return True
def battle(self, do_pre_buff: bool) -> Union[bool, tuple[Location, bool]]:
def battle(self, do_pre_buff: bool) -> bool | tuple[Location, bool]:
self._picked_up_items = False
self.used_tps = 0
if do_pre_buff: self._char.pre_buff()
@@ -458,14 +464,10 @@ class Diablo:
#Arrive at and clear Pentagram
if not self._cs_pentagram(): return False
""" NEW APPROACH ONLY HAS 50% SUCCESS RATE
#if Config().char["kill_cs_trash"]: if not self._trash_seals("A", "dia_trash_a", [606], "dia_trash_a_loop", 0.78): return False
#if Config().char["kill_cs_trash"]: if not self._trash_seals("B", "dia_trash_b", [607], "dia_trash_b_loop", 0.85): return False #high threshold needed to avoid chasing 607 ghosts
#if Config().char["kill_cs_trash"]: if not self._trash_seals("C", "dia_trash_c", [608], "dia_trash_c_loop", 0.78): return False
"""
#OLD APPROACH HAS 80% SUCCESS RATE
if Config().char["kill_cs_trash"]: self._trash_seals()
# Maintenance at Pentagram after Trash & clear Seal A: Vizier (to the left)
if Config().char["kill_cs_trash"]: self._char.kill_cs_trash("pent_before_a")
if not self._pather.traverse_nodes([602], self._char): return False
@@ -486,13 +488,11 @@ class Diablo:
if Config().char["cs_town_visits"]: self._cs_town_visit("C")
if do_pre_buff: self._char.pre_buff()
if not self._layoutcheck("C", "Infector", "dia_c_layout_bold", "layoutcheck_c", [650660], 0.83, None, ["DIA_C2G_BOSS_CLOSED_LAYOUTCHECK1", "DIA_C2G_BOSS_CLOSED_LAYOUTCHECK4", "DIA_C2G_BOSS_CLOSED_LAYOUTCHECK5", "DIA_C2G_BOSS_CLOSED_LAYOUTCHECK2", "DIA_C2G_BOSS_CLOSED_LAYOUTCHECK3",], ["DIA_C1F_LAYOUTCHECK1", "DIA_C1F_LAYOUTCHECK2", "DIA_C1F_LAYOUTCHECK3"]): return False
# Kill Diablo
Logger.debug("Waiting for Diablo to spawn")
Logger.info("Waiting for Diablo to spawn")
if not self._pather.traverse_nodes([602], self._char): return False
self._char.kill_diablo()
if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_dia_kill_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
self._picked_up_items = self._pickit.pick_up_items(char=self._char)
self._picked_up_items |= self._pickit.pick_up_items(char=self._char)
wait(0.5, 0.7)
return (Location.A4_DIABLO_END, self._picked_up_items)
@@ -507,14 +507,7 @@ if __name__ == "__main__":
game_stats = GameStats()
bot = Bot(game_stats)
# Idea list:
# Ghetto Mob detection: clear CS, take a screenshot w/o mobs at each attack point. template check on arrival at each node the regions where mobs can spawn. if no match: attack. if match: skip.
# use minimap to confirm map layouts & pentragram loop?
# Efficiency overview.
# Clear Trash Entrance Layout A: 91 % efficiency
# Clear Trash Entrance Layout B: 80 % efficiency
# Clear Trash between Pentagram & Seals: XX% efficiency <- measure gain | new issue, if merc kills mobs at edge of screen &
# Clear Seals: 98% efficiency. Root Cause: templates covered by corpses or getting lost when tele back to pentagram. Fix: Optimize templates used at nodes.
# LC A failing -> add template of seal to the calibration node. in case she overshoots to the seal, it will rubber-band her back to LC node.
# Kill Dia with Trash: >70%
+13 -10
View File
@@ -1,10 +1,8 @@
from char.i_char import IChar
from config import Config
from logger import Logger
from pather import Location, Pather
from typing import Union
from item.pickit import PickIt
from template_finder import TemplateFinder
import template_finder
from town.town_manager import TownManager
from utils.misc import wait
from dataclasses import dataclass
@@ -14,19 +12,24 @@ import random
from ui import loading, waypoint
class Nihlathak:
name = "run_nihlathak"
def __init__(
self,
pather: Pather,
town_manager: TownManager,
char: IChar,
pickit: PickIt
pickit: PickIt,
runs: list[str]
):
self._pather = pather
self._town_manager = town_manager
self._char = char
self._pickit = pickit
self._runs = runs
def approach(self, start_loc: Location) -> Union[bool, Location, bool]:
def approach(self, start_loc: Location) -> bool | Location:
Logger.info("Run Nihlathak")
if not self._char.capabilities.can_teleport_natively:
raise ValueError("Nihlathak requires teleport")
@@ -37,10 +40,10 @@ class Nihlathak:
return Location.A5_NIHLATHAK_START
return False
def battle(self, do_pre_buff: bool) -> Union[bool, tuple[Location, bool]]:
def battle(self, do_pre_buff: bool) -> bool | tuple[Location, bool]:
# TODO: We might need a second template for each option as merc might run into the template and we dont find it then
# Let's check which layout ("NI1_A = bottom exit" , "NI1_B = large room", "NI1_C = small room")
template_match = TemplateFinder().search_and_wait(["NI1_A", "NI1_B", "NI1_C"], threshold=0.65, timeout=20)
template_match = template_finder.search_and_wait(["NI1_A", "NI1_B", "NI1_C"], threshold=0.65, timeout=20)
if not template_match.valid:
return False
if do_pre_buff:
@@ -50,7 +53,7 @@ class Nihlathak:
# Its xpects that the static routes defined in game.ini are named: "ni1_a", "ni1_b", "ni1_c"
self._pather.traverse_nodes_fixed(template_match.name.lower(), self._char)
found_loading_screen_func = lambda: loading.wait_for_loading_screen(2.0) or \
TemplateFinder().search_and_wait(["NI2_SEARCH_0", "NI2_SEARCH_1"], threshold=0.8, timeout=0.5).valid
template_finder.search_and_wait(["NI2_SEARCH_0", "NI2_SEARCH_1"], threshold=0.8, timeout=0.5).valid
# look for stairs
if not self._char.select_by_template(["NI1_STAIRS", "NI1_STAIRS_2", "NI1_STAIRS_3", "NI1_STAIRS_4"], found_loading_screen_func, threshold=0.63, timeout=4):
# do a random tele jump and try again
@@ -59,7 +62,7 @@ class Nihlathak:
if not self._char.select_by_template(["NI1_STAIRS", "NI1_STAIRS_2", "NI1_STAIRS_3", "NI1_STAIRS_4"], found_loading_screen_func, threshold=0.63, timeout=4):
return False
# Wait until templates in lvl 2 entrance are found
if not TemplateFinder().search_and_wait(["NI2_SEARCH_0", "NI2_SEARCH_1", "NI2_SEARCH_2"], threshold=0.8, timeout=20).valid:
if not template_finder.search_and_wait(["NI2_SEARCH_0", "NI2_SEARCH_1", "NI2_SEARCH_2"], threshold=0.8, timeout=20).valid:
return False
wait(1.0) # wait to make sure the red writing is gone once we check for the eye
@dataclass
@@ -82,7 +85,7 @@ class Nihlathak:
# Move to spot where eye would be visible
self._pather.traverse_nodes_fixed(data.circle_static_path_key, self._char)
# Search for eye
template_match = TemplateFinder().search_and_wait(data.template_name, threshold=0.7, best_match=True, timeout=3)
template_match = template_finder.search_and_wait(data.template_name, threshold=0.7, best_match=True, timeout=3)
# If it is found, move down that hallway
if template_match.valid and template_match.name.endswith("_SAFE_DIST"):
self._pather.traverse_nodes_fixed(data.destination_static_path_key, self._char)
+10 -7
View File
@@ -1,28 +1,31 @@
from char import IChar
from config import Config
from logger import Logger
from pather import Location, Pather
from typing import Union
from item.pickit import PickIt
from template_finder import TemplateFinder
import template_finder
from town.town_manager import TownManager
from utils.misc import wait
from ui import loading
class Pindle:
name = "run_pindle"
def __init__(
self,
pather: Pather,
town_manager: TownManager,
char: IChar,
pickit: PickIt
pickit: PickIt,
runs: list[str]
):
self._pather = pather
self._town_manager = town_manager
self._char = char
self._pickit = pickit
self.runs = runs
def approach(self, start_loc: Location) -> Union[bool, Location]:
def approach(self, start_loc: Location) -> bool | Location:
# Go through Red Portal in A5
Logger.info("Run Pindle")
loc = self._town_manager.go_to_act(5, start_loc)
@@ -36,9 +39,9 @@ class Pindle:
return False
return Location.A5_PINDLE_START
def battle(self, do_pre_buff: bool) -> Union[bool, tuple[Location, bool]]:
def battle(self, do_pre_buff: bool) -> bool | tuple[Location, bool]:
# Kill Pindle
if not TemplateFinder().search_and_wait(["PINDLE_0", "PINDLE_1"], threshold=0.65, timeout=20).valid:
if not template_finder.search_and_wait(["PINDLE_0", "PINDLE_1"], threshold=0.65, timeout=20).valid:
return False
if do_pre_buff:
self._char.pre_buff()
+10 -7
View File
@@ -1,28 +1,31 @@
from char import IChar
from config import Config
from logger import Logger
from pather import Location, Pather
from typing import Union
from item.pickit import PickIt
from template_finder import TemplateFinder
import template_finder
from town.town_manager import TownManager
from utils.misc import wait
from ui import waypoint
class ShenkEld:
name = "run_shenk"
def __init__(
self,
pather: Pather,
town_manager: TownManager,
char: IChar,
pickit: PickIt
pickit: PickIt,
runs: list[str]
):
self._pather = pather
self._town_manager = town_manager
self._char = char
self._pickit = pickit
self._runs = runs
def approach(self, start_loc: Location) -> Union[bool, Location, bool]:
def approach(self, start_loc: Location) -> bool | Location:
Logger.info("Run Eldritch")
# Go to Frigid Highlands
if not self._town_manager.open_wp(start_loc):
@@ -32,10 +35,10 @@ class ShenkEld:
return Location.A5_ELDRITCH_START
return False
def battle(self, do_shenk: bool, do_pre_buff: bool, game_stats) -> Union[bool, tuple[Location, bool]]:
def battle(self, do_shenk: bool, do_pre_buff: bool, game_stats) -> bool | tuple[Location, bool]:
# Eldritch
game_stats.update_location("Eld")
if not TemplateFinder().search_and_wait(["ELDRITCH_0", "ELDRITCH_0_V2", "ELDRITCH_0_V3", "ELDRITCH_START", "ELDRITCH_START_V2"], threshold=0.65, timeout=20).valid:
if not template_finder.search_and_wait(["ELDRITCH_0", "ELDRITCH_0_V2", "ELDRITCH_0_V3", "ELDRITCH_START", "ELDRITCH_START_V2"], threshold=0.65, timeout=20).valid:
return False
if do_pre_buff:
self._char.pre_buff()
+15 -10
View File
@@ -1,29 +1,32 @@
from char import IChar
from config import Config
from logger import Logger
from pather import Location, Pather
from typing import Union
from item.pickit import PickIt
from template_finder import TemplateFinder
import template_finder
from town.town_manager import TownManager
from utils.misc import wait
from ui import waypoint
class Trav:
name = "run_trav"
def __init__(
self,
pather: Pather,
town_manager: TownManager,
char: IChar,
pickit: PickIt
pickit: PickIt,
runs: list[str]
):
self._pather = pather
self._town_manager = town_manager
self._char = char
self._pickit = pickit
self._runs = runs
def approach(self, start_loc: Location) -> Union[bool, Location]:
def approach(self, start_loc: Location) -> bool | Location:
# Go to Travincal via waypoint
Logger.info("Run Trav")
if not self._town_manager.open_wp(start_loc):
@@ -33,9 +36,9 @@ class Trav:
return Location.A3_TRAV_START
return False
def battle(self, do_pre_buff: bool) -> Union[bool, tuple[Location, bool]]:
def battle(self, do_pre_buff: bool) -> bool | tuple[Location, bool]:
# Kill Council
if not TemplateFinder().search_and_wait(["TRAV_0", "TRAV_1", "TRAV_20"], threshold=0.65, timeout=20).valid:
if not template_finder.search_and_wait(["TRAV_0", "TRAV_1", "TRAV_20"], threshold=0.65, timeout=20).valid:
return False
if do_pre_buff:
self._char.pre_buff()
@@ -49,9 +52,11 @@ class Trav:
wait(0.2, 0.3)
# If we can teleport we want to move back inside and also check loot there
if self._char.capabilities.can_teleport_natively or self._char.capabilities.can_teleport_with_charges:
if not self._pather.traverse_nodes([229], self._char, timeout=2.5, use_tp_charge=True):
if not self._pather.traverse_nodes([229], self._char, timeout=2.5, use_tp_charge=self._char.capabilities.can_teleport_natively):
self._pather.traverse_nodes([228, 229], self._char, timeout=2.5, use_tp_charge=True)
picked_up_items |= self._pickit.pick_up_items(self._char)
# Make sure we go back to the center to not hide the tp
self._pather.traverse_nodes([230], self._char, timeout=2.5)
# If travincal run is not the last run
if self.name != self._runs[-1]:
# Make sure we go back to the center to not hide the tp
self._pather.traverse_nodes([230], self._char, timeout=2.5)
return (Location.A3_TRAV_CENTER_STAIRS, picked_up_items)
+22 -9
View File
@@ -1,10 +1,10 @@
import numpy as np
from mss import mss
from logger import Logger
from typing import Tuple
from utils.misc import WindowSpec, find_d2r_window, wait
from config import Config
import threading
import time
sct = mss()
monitor_roi = sct.monitors[0]
@@ -13,6 +13,9 @@ monitor_x_range = None
monitor_y_range = None
detect_window = True
detect_window_thread = None
last_grab = None
cached_img = None
cached_img_lock = threading.Lock()
FIND_WINDOW = WindowSpec(
title_regex=Config().advanced_options["hwnd_window_title"],
@@ -65,21 +68,31 @@ def stop_detecting_window():
if detect_window_thread:
detect_window_thread.join()
def grab() -> np.ndarray:
def grab(force_new: bool = False) -> np.ndarray:
global monitor_roi
img = np.array(sct.grab(monitor_roi))
return img[:, :, :3]
global cached_img
global last_grab
# with 25fps we have 40ms per frame. If we check for 20ms range to make sure we can still get each frame if we want.
if not force_new and cached_img is not None and last_grab is not None and time.perf_counter() - last_grab < 0.04:
return cached_img
else:
with cached_img_lock:
last_grab = time.perf_counter()
img = np.array(sct.grab(monitor_roi))
with cached_img_lock:
cached_img = img[:, :, :3]
return cached_img
# TODO: Move the below funcs to utils(?)
def convert_monitor_to_screen(screen_coord: Tuple[float, float]) -> Tuple[float, float]:
def convert_monitor_to_screen(screen_coord: tuple[float, float]) -> tuple[float, float]:
global monitor_roi
if screen_coord is None:
Logger.error("convert_monitor_to_screen: empty coordinates passed")
return None
return (screen_coord[0] - monitor_roi["left"], screen_coord[1] - monitor_roi["top"])
def convert_screen_to_monitor(screen_coord: Tuple[float, float]) -> Tuple[float, float]:
def convert_screen_to_monitor(screen_coord: tuple[float, float]) -> tuple[float, float]:
global monitor_roi
if screen_coord is None:
Logger.error("convert_screen_to_monitor: empty coordinates passed")
@@ -88,7 +101,7 @@ def convert_screen_to_monitor(screen_coord: Tuple[float, float]) -> Tuple[float,
y = screen_coord[1] + monitor_roi["top"]
return (np.clip(x, *monitor_x_range), np.clip(y, *monitor_y_range))
def convert_abs_to_screen(abs_coord: Tuple[float, float]) -> Tuple[float, float]:
def convert_abs_to_screen(abs_coord: tuple[float, float]) -> tuple[float, float]:
global monitor_roi
if abs_coord is None:
Logger.error("convert_screen_to_monitor: empty coordinates passed")
@@ -96,14 +109,14 @@ def convert_abs_to_screen(abs_coord: Tuple[float, float]) -> Tuple[float, float]
# abs has it's center on char which is the center of the screen
return ((monitor_roi["width"] // 2) + abs_coord[0], (monitor_roi["height"] // 2) + abs_coord[1])
def convert_screen_to_abs(screen_coord: Tuple[float, float]) -> Tuple[float, float]:
def convert_screen_to_abs(screen_coord: tuple[float, float]) -> tuple[float, float]:
global monitor_roi
if screen_coord is None:
Logger.error("convert_screen_to_abs: empty coordinates passed")
return None
return (screen_coord[0] - (monitor_roi["width"] // 2), screen_coord[1] - (monitor_roi["height"] // 2))
def convert_abs_to_monitor(abs_coord: Tuple[float, float]) -> Tuple[float, float]:
def convert_abs_to_monitor(abs_coord: tuple[float, float]) -> tuple[float, float]:
if abs_coord is None:
Logger.error("convert_abs_to_monitor: empty coordinates passed")
return None
+30 -29
View File
@@ -10,7 +10,7 @@ from screen import grab, convert_screen_to_monitor
from config import Config
from logger import Logger
from npc_manager import Npc, open_npc_menu, press_npc_btn
from template_finder import TemplateFinder
import template_finder
from utils.custom_mouse import mouse
from utils.misc import wait, load_template
@@ -93,30 +93,30 @@ class AnyaShopper:
img = grab()
# 20 IAS gloves have a unique color so we can skip all others
ias_glove = TemplateFinder(True).search(
ref=load_template(asset_folder + "ias_gloves.png", 1.0),
ias_glove = template_finder.search(
ref=load_template(asset_folder + "ias_gloves.png"),
inp_img=img,
threshold=0.96,
roi=Config().ui_roi["left_inventory"],
normalize_monitor=True,
roi=Config().ui_roi["left_inventory"]
)
if ias_glove.valid:
self.ias_gloves_seen += 1
mouse.move(*ias_glove.center)
mouse.move(*ias_glove.center_monitor)
time.sleep(0.1)
img = grab()
if self.look_for_plus_3_gloves is True:
gg_gloves = TemplateFinder(True).search(
gg_gloves = template_finder.search(
ref=load_template(
asset_folder + "gg_gloves.png", 1.0 # assets for javazon gloves are mixed up, this one need +3 as in the 1080p version
asset_folder + "gg_gloves.png" # assets for javazon gloves are mixed up, this one need +3 as in the 1080p version
),
inp_img=img,
threshold=0.80
)
if gg_gloves.valid:
mouse.click(button="right")
self._messenger.send_message("Bought awesome IAS/+3 gloves!")
if self._messenger.enabled:
self._messenger.send_message("Bought awesome IAS/+3 gloves!")
Logger.info("IAS/+3 gloves bought!")
self.gloves_bought += 1
@@ -124,16 +124,17 @@ class AnyaShopper:
else:
if self.look_for_plus_2_gloves is True:
g_gloves = TemplateFinder(True).search(
g_gloves = template_finder.search(
ref=load_template(
asset_folder + "g_gloves.png", 1.0
asset_folder + "g_gloves.png"
),
inp_img=img,
threshold=0.80
)
if g_gloves.valid:
mouse.click(button="right")
self._messenger.send_message("Bought some decent IAS/+2 gloves")
if self._messenger.enabled:
self._messenger.send_message("Bought some decent IAS/+2 gloves")
Logger.info("IAS/+2 gloves bought!")
self.gloves_bought += 1
time.sleep(1)
@@ -148,35 +149,33 @@ class AnyaShopper:
claw_pos = []
img = grab().copy()
claw_keys = ["CLAW1", "CLAW2", "CLAW3"]
for ck in claw_keys:
template_match = TemplateFinder(True).search(ck, img, roi=self.roi_vendor)
if template_match.valid:
claw_pos.append(template_match.center)
template_matches = template_finder.search_all(claw_keys, img, roi=self.roi_vendor)
for template_match in template_matches:
claw_pos.append(template_match.center_monitor)
# check out each claw
for pos in claw_pos:
# cv2.circle(img, pos, 3, (0, 255, 0), 2)
x_m, y_m = convert_screen_to_monitor(pos)
mouse.move(x_m, y_m, randomize=3, delay_factor=[0.5, 0.6])
mouse.move(*pos, randomize=3, delay_factor=[0.5, 0.6])
wait(0.5, 0.6)
img_stats = grab()
trap_score = 0
melee_score = 0
if TemplateFinder(True).search("3_TO_TRAPS", img_stats, roi=self.roi_claw_stats, threshold=0.94).valid:
if template_finder.search("3_TO_TRAPS", img_stats, roi=self.roi_claw_stats, threshold=0.94).valid:
trap_score += 12
elif TemplateFinder(True).search("TO_TRAPS", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid:
elif template_finder.search("TO_TRAPS", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid:
trap_score += 8
if TemplateFinder(True).search("2_TO_ASSA", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid:
if template_finder.search("2_TO_ASSA", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid:
trap_score += 10
melee_score += 10
if TemplateFinder(True).search("TO_VENOM", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid:
if template_finder.search("TO_VENOM", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid:
melee_score += 6
if TemplateFinder(True).search("TO_LIGHT", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid:
if template_finder.search("TO_LIGHT", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid:
trap_score += 6
if TemplateFinder(True).search("TO_WB", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid:
if template_finder.search("TO_WB", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid:
melee_score += 2
trap_score += 1
if TemplateFinder(True).search("TO_DS", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid:
if template_finder.search("TO_DS", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid:
trap_score += 4
self.claws_evaluated += 1
@@ -184,7 +183,8 @@ class AnyaShopper:
if trap_score > self.trap_claw_min_score and self.look_for_trap_claws is True:
# pick it up
mouse.click(button="right")
self._messenger.send_message(f"Bought some terrific trap Claws (score: {trap_score})")
if self._messenger.enabled:
self._messenger.send_message(f"Bought some terrific trap Claws (score: {trap_score})")
Logger.info(f"Trap Claws (score: {trap_score}) bought!")
self.claws_bought += 1
@@ -193,7 +193,8 @@ class AnyaShopper:
if melee_score > self.melee_claw_min_score and self.look_for_melee_claws is True:
# pick it up
mouse.click(button="right")
self._messenger.send_message(f"Bought some mad melee Claws (score: {melee_score})")
if self._messenger.enabled:
self._messenger.send_message(f"Bought some mad melee Claws (score: {melee_score})")
Logger.info(f"Melee Claws (score: {melee_score}) bought!")
self.claws_bought += 1
time.sleep(1)
@@ -221,9 +222,9 @@ class AnyaShopper:
def select_by_template(self, template_type: str) -> bool:
Logger.debug(f"Select {template_type}")
template_match = TemplateFinder(True).search_and_wait(template_type, timeout=10, normalize_monitor=True)
template_match = template_finder.search_and_wait(template_type, timeout=10)
if template_match.valid:
mouse.move(*template_match.center)
mouse.move(*template_match.center_monitor)
wait(0.1, 0.2)
mouse.click(button="left")
return True
+10 -12
View File
@@ -3,7 +3,7 @@ import os
import time
import math
import random
from typing import Dict, Tuple, Union, List, Callable
from typing import Callable
import keyboard
import numpy as np
@@ -12,7 +12,7 @@ from screen import convert_screen_to_monitor, grab, convert_abs_to_monitor, conv
from config import Config
from logger import Logger
from npc_manager import Npc, open_npc_menu, press_npc_btn
from template_finder import TemplateFinder
import template_finder
from utils.custom_mouse import mouse
from utils.misc import wait
@@ -90,24 +90,22 @@ class DrognanShopper:
item_pos = []
img = grab().copy()
item_keys = ["SCEPTER1", "SCEPTER2", "SCEPTER3", "SCEPTER4", "SCEPTER5"]
for ck in item_keys:
template_match = TemplateFinder(True).search(ck, img, roi=self.roi_vendor)
if template_match.valid:
item_pos.append(template_match.center)
template_matches = template_finder.search_all(item_keys, img, roi=self.roi_vendor)
for template_match in template_matches:
item_pos.append(template_match.center_monitor)
# check out each item
for pos in item_pos:
x_m, y_m = convert_screen_to_monitor(pos)
mouse.move(x_m, y_m, randomize=3, delay_factor=[0.5, 0.6])
mouse.move(*pos, randomize=3, delay_factor=[0.5, 0.6])
wait(0.5, 0.6)
img_stats = grab()
# First check for +2 Paladin Skills. This weeds out most scepters right away.
if TemplateFinder(True).search("2_TO_PALADIN_SKILLS", img_stats, roi=self.roi_shop_item_stats, threshold=0.94).valid:
if template_finder.search("2_TO_PALADIN_SKILLS", img_stats, roi=self.roi_shop_item_stats, threshold=0.94).valid:
# Has 2 Pally skills, check blessed hammers next
if TemplateFinder(True).search("TO_BLESSED_HAMMERS", img_stats, roi=self.roi_shop_item_stats, threshold=0.9).valid:
if template_finder.search("TO_BLESSED_HAMMERS", img_stats, roi=self.roi_shop_item_stats, threshold=0.9).valid:
# Has 2 Pally skills AND Blessed Hammers, check Concentration next
if TemplateFinder(True).search("TO_CONCENTRATION", img_stats, roi=self.roi_shop_item_stats, threshold=0.9).valid:
if template_finder.search("TO_CONCENTRATION", img_stats, roi=self.roi_shop_item_stats, threshold=0.9).valid:
# Has 2 Pally skills AND Blessed Hammers AND Concentration. We're good! Buy it!
mouse.click(button="right")
Logger.info(f"Item bought!")
@@ -137,7 +135,7 @@ class DrognanShopper:
self.hold_move(pos_m, time_held=(2.0 / self.speed_factor))
# A variation of the move() function from pather.py
def hold_move(self, pos_monitor: Tuple[float, float], time_held: float = 2.0):
def hold_move(self, pos_monitor: tuple[float, float], time_held: float = 2.0):
factor = Config().advanced_options["pathing_delay_factor"]
# in case we want to walk we actually want to move a bit before the point cause d2r will always "overwalk"
pos_screen = convert_monitor_to_screen(pos_monitor)
+271
View File
@@ -0,0 +1,271 @@
import cv2
import numpy as np
import time
from screen import convert_screen_to_monitor, grab, convert_screen_to_abs, convert_abs_to_monitor
from logger import Logger
from math import dist
from utils.misc import color_filter, is_in_roi
import json
from dataclasses import dataclass
FILTER_RANGES=[
{"erode": 1, "blur": 3, "lh": 38, "ls": 169, "lv": 50, "uh": 70, "us": 255, "uv": 255}, # poison
{"erode": 1, "blur": 3, "lh": 110, "ls": 169, "lv": 50, "uh": 120, "us": 255, "uv": 255} # frozen
]
@dataclass
class TargetInfo:
roi: tuple = None
center: tuple = None
center_abs: tuple = None
center_monitor: tuple = None
distance: int = 0
def log_targets(targets: list[TargetInfo]):
if (num_targets := len(targets)) > 0:
if num_targets == 1:
msg = f"{num_targets} target detected"
elif num_targets > 1:
msg = f"{num_targets} targets detected, closest"
Logger.debug(f"{msg} at {targets[0].center}, distance: {round(targets[0].distance)}")
def _dist_to_center(pos):
return dist(pos, (1280/2, 720/2))
def get_visible_targets(
img: np.ndarray = None,
radius_min: int = 150,
radius_max: int = 1280,
ignore_roi: list[int] = [600, 300, (1280/2 - 600)*2, (720/2 - 300)*2],
use_radius: bool = False
) -> list[TargetInfo]:
"""
:param img: The image to find targets in
:param radius_min: The minimum radius of the target [Default: 150, Integer 0 - 1280]
:param radius_max: The maximum radius of the target [Default: 1280, Integer 0 - 1280]
:param ignore_roi: The region of interest to ignore [Default: [600, 300, (1280/2 - 600)*2, (720/2 - 300)*2]]
:param use_radius: Whether to use the radius of the target (True) or the ignore_roi parameter (False)
Returns a list of TargetInfo objects
"""
img = grab() if img is None else img
targets = []
for filter in FILTER_RANGES:
filterimage, threshz = _process_image(img, mask_char=True, mask_hud=True, **filter) # HSV Filter for BLUE and GREEN (Posison Nova & Holy Freeze)
filterimage, rectangles, positions = _add_markers(filterimage, threshz, rect_min_size=100, rect_max_size=200, marker=True) # rather large rectangles
if positions:
for cnt, position in enumerate(positions):
distance = _dist_to_center(position)
condition = (radius_min <= distance <= radius_max) if use_radius else (not is_in_roi(ignore_roi, position))
if condition:
targets.append(TargetInfo(
roi = rectangles[cnt],
center = position,
center_monitor = convert_screen_to_monitor(position),
center_abs = convert_screen_to_abs(position),
distance = distance
))
if targets:
targets = sorted(targets, key=lambda obj: obj.distance)
return targets
def _bright_contrast(img: np.ndarray, brightness: int = 255, contrast: int = 127):
"""
:param img: The image to which filters should be applied
:param brightness: adjust Brightness of the picture [Default: 255, Integer 0 - 255]
:param contrast: adjust Contrast of the picture [Default: 127, Integer 0 - 254]
Returns variables cal
"""
brightness = int((brightness - 0) * (255 - (-255)) / (510 - 0) + (-255))
contrast = int((contrast - 0) * (127 - (-127)) / (254 - 0) + (-127))
if brightness != 0:
if brightness > 0:
shadow = brightness
max = 255
else:
shadow = 0
max = 255 + brightness
al_pha = (max - shadow) / 255
ga_mma = shadow
cal = cv2.addWeighted(img, al_pha, img, 0, ga_mma)
else:
cal = img
if contrast != 0:
alpha = float(131 * (contrast + 127)) / (127 * (131 - contrast))
gamma = 127 * (1 - alpha)
cal = cv2.addWeighted(cal, alpha, cal, 0, gamma)
return cal
def _process_image(img, mask_char:bool=False, mask_hud:bool=True, erode:int=None, dilate:int=None, blur:int=None, lh:int=None, ls:int=None, lv:int=None, uh:int=None, us:int=None, uv:int=None, bright:int=None, contrast:int=None, thresh:int=None, invert:int=None):
"""
Helper function that will apply HSV filters
:param img: The image to which filters should be applied
:param mask_char: apply a black rectangle to mask your char (e.g. filter out Set-Item glow effect or auras) [Default: False, Bool]
:param hud_mask: removes the HUD from the returned image [Default: True, Bool]
:param erode: erode (thin lines) in the picture [Default: None, Integer, no filter: 0, 0 - 36]
:param dilate: dilate (thicken lines) in the picture [Default: None, Integer, no filter: 0, 0 - 36]
:param blur: blur the picture [Default: None, no filter: 0, Integer, 0 - 30]
:param lh: cut-off Hue BELOW this value [Default: None, no filter: 0, Integer 0 - 180]
:param ls: cut-off Saturation BELOW this value [Default: None, no filter: 0, Integer 0 - 255]
:param lv: cut-off Value BELOW this value [Default: None, no filter: 0, Integer 0 - 255]
:param uh: cut-off Hue ABOVE this value [Default: None, no filter: 180, Integer 0 - 180]
:param us: cut-off Saturation ABOVE this value [Default: None, no filter: 255, Integer 0 - 255]
:param uv: cut-off Value ABOVE this value [Default: None, no filter: 255, Integer 0 - 255]
:param bright: adjust Brightness of the picture [Default: None, no filter: 255, Integer 0 - 255]
:param contrast: adjust Contrast of the picture [Default: None, no filter: 127, Integer 0 - 254]
:param thresh: adjust Threshold of the picture [Default: None, no filter: 0, Integer 0 - 255]
:param invert: Invert the picture [Default: None, no filter: 0, Integer 0 - 1]
Returns variables filterimage and threshz
"""
img = img
if mask_hud:
hud_mask = cv2.imread(f"./assets/hud_mask.png", cv2.IMREAD_GRAYSCALE)
img = cv2.bitwise_and(img, img, mask=hud_mask)
if mask_char: img = cv2.rectangle(img, (600,250), (700,400), (0,0,0), -1) # black out character by drawing a black box above him (e.g. ignore set glow)
if erode:
kernel = np.ones((erode, erode), 'uint8')
img = cv2.erode(img, kernel, None, iterations=1)
if dilate:
kernel = np.ones((dilate, dilate), 'uint8')
img = cv2.dilate(img, kernel, iterations=1)
if blur: img = cv2.blur(img, (blur, blur))
if bright or contrast: img = _bright_contrast(img, bright, contrast)
if lh or ls or lv or uh or us or uv:
_, img = color_filter(img, (np.array([lh, ls, lv]), np.array([uh, us, uv])))
if thresh: img = cv2.threshold(img, thresh, 255, cv2.THRESH_BINARY)[1]
threshz = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
if thresh: _, threshz = cv2.threshold(threshz, thresh, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
if invert: img = 255 - img
filterimage = img
return filterimage, threshz
# add rectangles and crosses - adapted from Nathan's Live-View
def _add_markers(img:str, threshz:str, rect_min_size:int=20, rect_max_size:int=50, line_color:str=(0, 255, 0), line_type:str=cv2.LINE_4, marker:bool=False, marker_color:str=(0, 255, 255), marker_type:str=cv2.MARKER_CROSS):
"""
Helper function that will add rectangles and crosshairs to allow object detection
:param img: The image to which filters should be applied
:param threshz: The image that had the threshold adjusted (obtained from _process_image())
:param rect_min_size: Minimum size for the rectangle to be drawn [Default: 20, Integer]
:param rect_max_size: Minimum size for the rectangle to be drawn [Default: 50, Integer]
:param line_color: Color of the rectangle line [Default: (0, 255, 0)]
:param line_type: Type of Line of the rectangle [Default: cv2.LINE_4]
:param marker: Add Marker to center of rectangles [Default: False, Bool]
:param marker_color: Color for the Marker [Default: (0, 255, 255)]
:param marker_type: Type for the Marker [Default: cv2.MARKER_CROSS]
Returns variables img (containing markers) & rectangles (x,y,w,h for each) & marker (x,y for eah)
"""
#add rectangles
n_labels, _, stats, _ = cv2.connectedComponentsWithStats(threshz)
rectangles = []
markers = []
for i in range(1, n_labels):
if stats[i, cv2.CC_STAT_AREA] >= rect_min_size <= rect_max_size:
x = stats[i, cv2.CC_STAT_LEFT]
y = stats[i, cv2.CC_STAT_TOP]
w = stats[i, cv2.CC_STAT_WIDTH]
h = stats[i, cv2.CC_STAT_HEIGHT]
cv2.rectangle(img, (x, y), (x + w, y + h), color=line_color, thickness=1)
rect = (int(x), int(y), int(w), int(h))
rectangles.append(rect)
if marker:
center_x = x + int(w/2)
center_y = y + int(h/2)
cv2.drawMarker(img, (center_x, center_y), color=marker_color, markerType=marker_type, markerSize=15, thickness=2)
mark = (int(center_x), int(center_y))
markers.append(mark)
return img, rectangles, markers
class LiveViewer:
def __init__(self):
with open("./src/utils/live-view-last-settings.json") as f:
self.settings = json.loads(f.read())
self.hud_mask = cv2.imread(f"./assets/hud_mask.png", cv2.IMREAD_GRAYSCALE)
self.hud_mask = cv2.threshold(self.hud_mask, 1, 255, cv2.THRESH_BINARY)[1]
self.use_existing_image = False
self.existing_image_path = "test/assets/mobs.png"
self.setup()
self.live_view()
def setup(self):
cv2.namedWindow("Settings", cv2.WINDOW_NORMAL)
cv2.namedWindow("Results", cv2.WINDOW_NORMAL)
cv2.createTrackbar('erode', 'Settings', self.settings['erode'], 36, self.value_update)
cv2.createTrackbar('dilate', 'Settings', self.settings['dilate'], 36, self.value_update)
cv2.createTrackbar('blur', 'Settings', self.settings['blur'], 30, self.value_update)
cv2.createTrackbar('lh', 'Settings', self.settings['lh'], 180, self.value_update)
cv2.createTrackbar('ls', 'Settings', self.settings['ls'], 255, self.value_update)
cv2.createTrackbar('lv', 'Settings', self.settings['lv'], 255, self.value_update)
cv2.createTrackbar('uh', 'Settings', self.settings['uh'], 180, self.value_update)
cv2.createTrackbar('us', 'Settings', self.settings['us'], 255, self.value_update)
cv2.createTrackbar('uv', 'Settings', self.settings['uv'], 255, self.value_update)
cv2.createTrackbar('bright', 'Settings', self.settings['bright'], 255, self.value_update)
cv2.createTrackbar('contrast', 'Settings', self.settings['contrast'], 254, self.value_update)
cv2.createTrackbar('thresh', 'Settings', self.settings['thresh'], 255, self.value_update)
cv2.createTrackbar('invert', 'Settings', self.settings['invert'], 1, self.value_update)
def value_update(self, ignore: bool = False):
if not self.use_existing_image:
self.image = grab()
else:
self.image = cv2.imread(self.existing_image_path)
self.image = cv2.bitwise_and(self.image, self.image, mask=self.hud_mask)
# black out character
self.image = cv2.rectangle(self.image, (550,250), (700,400), (0,0,0), -1)
try:
self.settings['erode'] = cv2.getTrackbarPos('erode', 'Settings')
self.settings['dilate'] = cv2.getTrackbarPos('dilate', 'Settings')
self.settings['blur'] = cv2.getTrackbarPos('blur', 'Settings')
self.settings['lh'] = cv2.getTrackbarPos('lh', 'Settings')
self.settings['ls'] = cv2.getTrackbarPos('ls', 'Settings')
self.settings['lv'] = cv2.getTrackbarPos('lv', 'Settings')
self.settings['uh'] = cv2.getTrackbarPos('uh', 'Settings')
self.settings['us'] = cv2.getTrackbarPos('us', 'Settings')
self.settings['uv'] = cv2.getTrackbarPos('uv', 'Settings')
self.settings['bright'] = cv2.getTrackbarPos('bright', 'Settings')
self.settings['contrast'] = cv2.getTrackbarPos('contrast', 'Settings')
self.settings['thresh'] = cv2.getTrackbarPos('thresh', 'Settings')
self.settings['invert'] = cv2.getTrackbarPos('invert', 'Settings')
with open("./src/utils/live-view-last-settings.json", 'w') as f:
f.write(json.dumps(self.settings))
except cv2.error:
return
self.image, threshz = _process_image(self.image, **self.settings)
filterimage, _, _ = _add_markers(self.image, threshz, rect_min_size=100, rect_max_size=200, marker=True)
cv2.imshow("Results", filterimage)
cv2.waitKey(1)
def live_view(self):
while True:
self.value_update()
# Testing: Have whatever you want to detect on the screen
if __name__ == "__main__":
import keyboard
import os
from logger import Logger
from screen import start_detecting_window, stop_detecting_window
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or stop_detecting_window() or os._exit(1))
start_detecting_window()
print("Move to d2r window and press f11")
keyboard.wait("f11")
# l = LiveViewer()
masked_image = False
def _toggle_masked_image():
global masked_image
masked_image = not masked_image
while 1:
img = grab()
targets = get_visible_targets()
display_img = img.copy()
for target in targets:
x, y = target.center
cv2.rectangle(display_img, target.roi[:2], (target.roi[0] + target.roi[2], target.roi[1] + target.roi[3]), (0, 0, 255), 1)
cv2.imshow('test', display_img)
key = cv2.waitKey(1)
+204 -220
View File
@@ -1,243 +1,226 @@
import cv2
import threading
from copy import deepcopy
from screen import convert_screen_to_monitor, grab
from typing import Union
from dataclasses import dataclass
import numpy as np
from logger import Logger
import time
import os
from config import Config
from utils.misc import cut_roi, load_template, list_files_in_folder, alpha_to_mask, roi_center, color_filter
from utils.misc import cut_roi, load_template, list_files_in_folder, alpha_to_mask, roi_center, color_filter, mask_by_roi
from functools import cache
template_finder_lock = threading.Lock()
templates_lock = threading.Lock()
@dataclass
class Template:
name: str = None
img_bgra: np.ndarray = None
img_bgr: np.ndarray = None
img_gray: np.ndarray = None
alpha_mask: np.ndarray = None
@dataclass
class TemplateMatch:
name: str = None
score: float = -1.0
center: tuple[float, float] = None
center: tuple[int, int] = None
center_monitor: tuple[int, int] = None
region: list[float] = None
region_monitor: list[float] = None
valid: bool = False
class TemplateFinder:
TEMPLATE_PATHS = [
"assets\\templates",
"assets\\npc",
"assets\\shop",
"assets\\item_properties",
"assets\\chests",
"assets\\gamble",
]
@cache
def stored_templates() -> dict[Template]:
paths = []
templates = {}
for path in TEMPLATE_PATHS:
paths += list_files_in_folder(path)
for file_path in paths:
file_name: str = os.path.basename(file_path)
if file_name.lower().endswith('.png'):
key = file_name[:-4].upper()
template_img = load_template(file_path)
templates[key] = Template(
name = key,
img_bgra = template_img,
img_bgr = cv2.cvtColor(template_img, cv2.COLOR_BGRA2BGR),
img_gray = cv2.cvtColor(template_img, cv2.COLOR_BGRA2GRAY),
alpha_mask = alpha_to_mask(template_img)
)
return templates
def get_template(key):
with templates_lock:
return stored_templates()[key].img_bgr
def _process_template_refs(ref: str | np.ndarray | list[str]) -> list[Template]:
templates = []
if type(ref) != list:
ref = [ref]
for i in ref:
# if the reference is a string, then it's a reference to a named template asset
if type(i) == str:
templates.append(stored_templates()[i.upper()])
# if the reference is an image, append new Template class object
elif type(i) == np.ndarray:
templates.append(Template(
img_bgr = i,
img_gray = cv2.cvtColor(i, cv2.COLOR_BGR2GRAY),
alpha_mask = alpha_to_mask(i)
))
return templates
def _single_template_match(template: Template, inp_img: np.ndarray = None, roi: list = None, color_match: list = None, use_grayscale: bool = False) -> TemplateMatch:
inp_img = inp_img if inp_img is not None else grab()
template_match = TemplateMatch()
# crop image to roi
if roi is None:
# if no roi is provided roi = full inp_img
roi = [0, 0, inp_img.shape[1], inp_img.shape[0]]
rx, ry, rw, rh = roi
img = inp_img[ry:ry + rh, rx:rx + rw]
# filter for desired color or make grayscale
if color_match:
template_img, = color_filter(template.img_bgr, color_match)[1],
img = color_filter(img, color_match)[1]
elif use_grayscale:
template_img = template.img_gray
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
else:
template_img = template.img_bgr
if not (img.shape[0] > template_img.shape[0] and img.shape[1] > template_img.shape[1]):
Logger.error(f"Image shape and template shape are incompatible: {template.name}. Image: {img.shape}, Template: {template_img.shape}")
else:
res = cv2.matchTemplate(img, template_img, cv2.TM_CCOEFF_NORMED, mask = template.alpha_mask)
np.nan_to_num(res, copy=False, nan=0.0, posinf=0.0, neginf=0.0)
_, max_val, _, max_pos = cv2.minMaxLoc(res)
# save rectangle corresponding to matched region
rec_x = int((max_pos[0] + rx))
rec_y = int((max_pos[1] + ry))
rec_w = int(template_img.shape[1])
rec_h = int(template_img.shape[0])
template_match.region = [rec_x, rec_y, rec_w, rec_h]
template_match.region_monitor = [*convert_screen_to_monitor((rec_x, rec_y)), rec_w, rec_h]
template_match.center = roi_center(template_match.region)
template_match.center_monitor = convert_screen_to_monitor(template_match.center)
template_match.name = template.name
template_match.score = max_val
template_match.valid = True
return template_match
def search(
ref: str | np.ndarray | list[str],
inp_img: np.ndarray,
threshold: float = 0.68,
roi: list[float] = None,
use_grayscale: bool = False,
color_match: list = False,
best_match: bool = False
) -> TemplateMatch:
"""
Loads images from assets/templates and assets/npc and provides search functions
to find these assets within another image
IMPORTANT: This method must be thread safe!
Search for a template in an image
:param ref: Either key of a already loaded template, list of such keys, 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]
:param use_grayscale: Use grayscale template matching for speed up
:param color_match: Pass a color to be used by misc.color_filter to filter both image of interest and template image (format Config().colors["color"])
:param best_match: If list input, will search for list of templates by best match. Default behavior is first match.
:return: Returns a TemplateMatch object with a valid flag
"""
TEMPLATE_PATHS = [
"assets\\templates",
"assets\\npc",
"assets\\shop",
"assets\\item_properties",
"assets\\chests",
"assets\\gamble",
]
_instance = None
def __new__(cls, save_last_res=False):
if cls._instance is None:
cls._instance = super(TemplateFinder, cls).__new__(cls)
cls._instance._save_last_res = save_last_res
if cls._instance._save_last_res:
# do not use this when running botty as it is used accross multiple threads! Just used in shopper as a workaround for now
cls._instance.last_res = None
# load templates with their filename as key in the dict
pathes = []
for path in TemplateFinder.TEMPLATE_PATHS:
pathes += list_files_in_folder(path)
cls._instance._templates = {}
for file_path in pathes:
file_name: str = os.path.basename(file_path)
if file_name.lower().endswith('.png'):
key = file_name[:-4].upper()
template_img = load_template(file_path, 1.0, True)
mask = alpha_to_mask(template_img)
cls._instance._templates[key] = [
cv2.cvtColor(template_img, cv2.COLOR_BGRA2BGR),
cv2.cvtColor(template_img, cv2.COLOR_BGRA2GRAY),
1.0,
mask
]
return cls._instance
def get_template(self, key):
return cv2.cvtColor(self._templates[key][0], cv2.COLOR_BGRA2BGR)
def search(
self,
ref: Union[str, np.ndarray, list[str]],
inp_img: np.ndarray,
threshold: float = 0.68,
roi: list[float] = None,
normalize_monitor: bool = False,
best_match: bool = False,
use_grayscale: bool = False,
color_match: list = False,
) -> TemplateMatch:
"""
Search for a template in an image
:param ref: Either key of a already loaded template, list of such keys, 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]
:param normalize_monitor: If True will return positions in monitor coordinates. Otherwise in coordinates of the input image.
:param best_match: If list input, will search for list of templates by best match. Default behavior is first match.
:param use_grayscale: Use grayscale template matching for speed up
:param color_match: Pass a color to be used by misc.color_filter to filter both image of interest and template image (format Config().colors["color"])
:return: Returns a TemplateMatch object with a valid flag
"""
if roi is None:
# if no roi is provided roi = full inp_img
roi = [0, 0, inp_img.shape[1], inp_img.shape[0]]
rx, ry, rw, rh = roi
inp_img = inp_img[ry:ry + rh, rx:rx + rw]
if type(ref) == str:
if not color_match:
templates = [self._templates[ref][use_grayscale]]
templates = _process_template_refs(ref)
matches = []
for template in templates:
match = _single_template_match(template, inp_img, roi, color_match, use_grayscale)
if match.score >= threshold:
if not best_match:
return match
else:
templates = [color_filter(self._templates[ref][0], color_match)[1]]
if use_grayscale:
templates = [cv2.cvtColor(templates[0], cv2.COLOR_BGR2GRAY)]
scales = [self._templates[ref][2]]
masks = [self._templates[ref][3]]
names = [ref]
best_match = False
elif type(ref) == list:
if type(ref[0]) == str:
if not color_match:
templates = [self._templates[i][use_grayscale] for i in ref]
else:
templates = [color_filter(self._templates[i][0], color_match)[1] for i in ref]
if use_grayscale:
templates = [cv2.cvtColor(i, cv2.COLOR_BGR2GRAY) for i in templates]
scales = [self._templates[i][2] for i in ref]
masks = [self._templates[i][3] for i in ref]
names = ref
else:
if not color_match:
templates = ref
else:
templates = [color_filter(i, color_match)[1] for i in ref]
if use_grayscale:
templates = [cv2.cvtColor(i, cv2.COLOR_BGR2GRAY) for i in templates]
scales = [1.0] * len(ref)
masks = [None] * len(ref)
else:
if not color_match:
templates = [ref]
else:
templates = [color_filter(ref, color_match)[1]]
if use_grayscale:
templates = [cv2.cvtColor(i, cv2.COLOR_BGR2GRAY) for i in templates]
scales = [1.0]
masks = [None]
best_match = False
matches.append(match)
if matches:
matches = sorted(matches, key=lambda obj: obj.score, reverse=True)
return matches[0]
return TemplateMatch()
scores = [0] * len(templates)
ref_points = [(0, 0)] * len(templates)
recs = [[0, 0, 0, 0]] * len(templates)
if color_match:
inp_img = color_filter(inp_img, color_match)[1]
def search_and_wait(
ref: str | list[str],
roi: list[float] = None,
timeout: float = 30,
threshold: float = 0.68,
use_grayscale: bool = False,
color_match: list = False,
best_match: bool = False,
suppress_debug: bool = False,
) -> TemplateMatch:
"""
Helper function that will loop and keep searching for a template
:param timeout: After this amount of time the search will stop and it will return [False, None]
:Other params are the same as for TemplateFinder.search()
:returns a TemplateMatch object
"""
if not suppress_debug:
Logger.debug(f"Waiting for templates: {ref}")
start = time.time()
template_match = TemplateMatch()
while (time_remains := time.time() - start < timeout):
img = grab()
is_loading_black_roi = np.average(img[:, 0:Config().ui_roi["loading_left_black"][2]]) < 1.0
if not is_loading_black_roi or "LOADING" in ref:
template_match = search(ref, img, roi=roi, threshold=threshold, use_grayscale=use_grayscale, color_match=color_match, best_match=best_match)
if template_match.valid:
break
if not time_remains:
Logger.debug(f"Could not find desired templates")
else:
Logger.debug(f"Found match: {template_match.name} ({template_match.score*100:.1f}% confidence)")
return template_match
for count, template in enumerate(templates):
template_match = TemplateMatch()
scale = scales[count]
mask = masks[count]
if scale != 1:
img: np.ndarray = cv2.resize(inp_img, None, fx=scale, fy=scale, interpolation=cv2.INTER_NEAREST)
rx *= scale
ry *= scale
rw *= scale
rh *= scale
else:
img: np.ndarray = inp_img
if img.shape[0] > template.shape[0] and img.shape[1] > template.shape[1]:
if use_grayscale:
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED, mask=mask)
np.nan_to_num(res, copy=False, nan=0.0, posinf=0.0, neginf=0.0)
_, max_val, _, max_pos = cv2.minMaxLoc(res)
if self._save_last_res:
with template_finder_lock:
self.last_res = deepcopy(res)
if max_val > threshold:
rec = [int((max_pos[0] + rx) // scale), int((max_pos[1] +ry) // scale), int(template.shape[1] // scale), int(template.shape[0] // scale)]
ref_point = roi_center(rec)
if normalize_monitor:
ref_point = convert_screen_to_monitor(ref_point)
rec[0], rec[1] = convert_screen_to_monitor((rec[0], rec[1]))
if best_match:
scores[count] = max_val
ref_points[count] = ref_point
recs[count] = rec
else:
try: template_match.name = names[count]
except: pass
template_match.center = ref_point
template_match.score = max_val
template_match.region = rec
template_match.valid = True
return template_match
if len(scores) > 0 and max(scores) > 0:
idx=scores.index(max(scores))
try: template_match.name = names[idx]
except: pass
template_match.center = ref_points[idx]
template_match.score = scores[idx]
template_match.region = recs[idx]
template_match.valid = True
else:
template_match = TemplateMatch()
return template_match
def search_and_wait(
self,
ref: Union[str, list[str]],
roi: list[float] = None,
timeout: float = None,
threshold: float = 0.68,
normalize_monitor: bool = False,
best_match: bool = False,
take_ss: bool = True,
use_grayscale: bool = False,
suppress_debug: bool = False,
color_match: list = False,
) -> TemplateMatch:
"""
Helper function that will loop and keep searching for a template
:param timeout: After this amount of time the search will stop and it will return [False, None]
:param take_ss: Bool value to take screenshot on timeout or not (flag must still be set in params!)
Other params are the same as for TemplateFinder.search()
"""
if type(ref) is str:
ref = [ref]
if not suppress_debug:
Logger.debug(f"Waiting for templates: {ref}")
start = time.time()
while 1:
img = grab()
template_match = self.search(ref, img, roi=roi, threshold=threshold, best_match=best_match, use_grayscale=use_grayscale, normalize_monitor=normalize_monitor, color_match=color_match)
is_loading_black_roi = np.average(img[:, 0:Config().ui_roi["loading_left_black"][2]]) < 1.0
if not is_loading_black_roi or "LOADING" in ref:
if template_match.valid:
Logger.debug(f"Found Match: {template_match.name} ({template_match.score*100:.1f}% confidence)")
return template_match
if timeout is not None and (time.time() - start) > timeout:
if Config().general["info_screenshots"] and take_ss:
cv2.imwrite(f"./info_screenshots/info_wait_for_{ref}_timeout_" + time.strftime("%Y%m%d_%H%M%S") + ".png", img)
if take_ss:
Logger.debug(f"Could not find any of the above templates")
return template_match
def search_all(
ref: str | np.ndarray | list[str],
inp_img: np.ndarray,
threshold: float = 0.68,
roi: list[float] = None,
use_grayscale: bool = False,
color_match: list = False,
) -> list[TemplateMatch]:
"""
Returns a list of all templates scoring above set threshold on the screen
:Other params are the same as for TemplateFinder.search()
:return: Returns a list of TemplateMatch objects
"""
templates = _process_template_refs(ref)
matches = []
img = inp_img
while True:
any_found = False
for template in templates:
match = _single_template_match(template, img, roi, color_match, use_grayscale)
if (ind_found := match.score >= threshold):
matches.append(match)
img = mask_by_roi(img, match.region, "inverse")
any_found |= ind_found
if not any_found:
break
return matches
# Testing: Have whatever you want to find on the screen
@@ -246,7 +229,7 @@ if __name__ == "__main__":
import os
from screen import start_detecting_window, stop_detecting_window
from utils.misc import wait
from template_finder import TemplateFinder
import template_finder
start_detecting_window()
wait(0.1)
@@ -264,7 +247,8 @@ if __name__ == "__main__":
keyboard.wait("f11")
# enter the template names you are trying to detect here
_template_list = ["TRAV_0","TRAV_1","TRAV_3","TRAV_4","TRAV_5","TRAV_7","TRAV_8","TRAV_10","TRAV_11","TRAV_12","TRAV_13","TRAV_16","TRAV_17","TRAV_18","TRAV_19","TRAV_2","TRAV_20","TRAV_21","TRAV_22","TRAV_23","TRAV_24","TRAV_25","TRAV_27","TRAV_28","TRAV_29","TRAV_V2_0","TRAV_V2_1","TRAV_V2_2","TRAV_V2_3","TRAV_V2_4","TRAV_V3_0","TRAV_V3_1","TRAV_V3_11","TRAV_V3_3","TRAV_V3_4","TRAV_V3_5","TRAV_V3_6","TRAV_V3_7","TRAV_V3_8"]
_template_list = ["SHENK_0","SHENK_1","SHENK_10","SHENK_11","SHENK_12","SHENK_13","SHENK_15","SHENK_16","SHENK_17","SHENK_18","SHENK_19","SHENK_2","SHENK_20","SHENK_3","SHENK_4","SHENK_6","SHENK_7","SHENK_8","SHENK_9","SHENK_DEATH_0","SHENK_DEATH_1","SHENK_DEATH_2","SHENK_DEATH_3","SHENK_DEATH_4","SHENK_V2_3","SHENK_V2_4","SHENK_V2_6","SHENK_V2_7","SHENK_V2_8"]
_current_template_idx = -1
_last_stored_idx = 0
@@ -278,7 +262,7 @@ if __name__ == "__main__":
cv2.imwrite(match['filename'], match['img'])
Logger.info(f"{match['filename']} saved")
def _toggle_all_templates():
def _toggle_templates():
global _current_template_idx
_current_template_idx = -1 if _current_template_idx != -1 else _last_stored_idx
if _current_template_idx == -1:
@@ -309,7 +293,7 @@ if __name__ == "__main__":
keyboard.add_hotkey('up', lambda: _incr_threshold(0.05))
keyboard.add_hotkey('left', lambda: _incr_template_idx(-1))
keyboard.add_hotkey('right', lambda: _incr_template_idx(1))
keyboard.add_hotkey('f9', lambda: _toggle_all_templates())
keyboard.add_hotkey('f9', lambda: _toggle_templates())
keyboard.add_hotkey('f10', lambda: _save_visible_templates())
while 1:
@@ -321,7 +305,7 @@ if __name__ == "__main__":
else:
templates = [_template_list[_current_template_idx]]
for key in templates:
template_match = TemplateFinder().search(key, img, threshold=_current_threshold)
template_match = template_finder.search(key, img, threshold=_current_threshold)
if template_match.valid:
x, y = template_match.center
cv2.putText(display_img, str(template_match.name), template_match.center, cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA)
+10 -12
View File
@@ -1,11 +1,9 @@
from char import IChar
from town.i_act import IAct
from screen import grab
from config import Config
from npc_manager import Npc, open_npc_menu, press_npc_btn
from pather import Pather, Location
from typing import Union
from template_finder import TemplateFinder
import template_finder
from ui_manager import ScreenObjects, is_visible
from utils.misc import wait
@@ -23,7 +21,7 @@ class A1(IAct):
def can_stash(self) -> bool: return True
def can_trade_and_repair(self) -> bool: return True
def resurrect(self, curr_loc: Location) -> Union[Location, bool]:
def resurrect(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A1_KASHYA_CAIN), self._char, force_move=True):
return False
if open_npc_menu(Npc.KASHYA):
@@ -34,7 +32,7 @@ class A1(IAct):
def open_wp(self, curr_loc: Location) -> bool:
if not self._pather.traverse_nodes((curr_loc, Location.A1_WP_SOUTH), self._char, force_move=True): return False
wait(0.5, 0.7)
if not TemplateFinder().search("A1_WP", grab()).valid:
if not template_finder.search("A1_WP", grab()).valid:
curr_loc = Location.A1_WP_SOUTH
if not self._pather.traverse_nodes((curr_loc, Location.A1_WP_NORTH), self._char, force_move=True): return False
wait(0.5, 0.7)
@@ -42,27 +40,27 @@ class A1(IAct):
# decreased threshold because we sometimes walk "over" it during pathing
return self._char.select_by_template(["A1_WP"], found_wp_func, threshold=0.62)
def wait_for_tp(self) -> Union[Location, bool]:
success = TemplateFinder().search_and_wait(["A1_TOWN_7", "A1_TOWN_9"], timeout=20).valid
def wait_for_tp(self) -> Location | bool:
success = template_finder.search_and_wait(["A1_TOWN_7", "A1_TOWN_9"], timeout=20).valid
if not self._pather.traverse_nodes([Location.A1_TOWN_TP, Location.A1_KASHYA_CAIN], self._char, force_move=True): return False
if success:
return Location.A1_KASHYA_CAIN
return False
def identify(self, curr_loc: Location) -> Union[Location, bool]:
def identify(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A1_KASHYA_CAIN), self._char, force_move=True): return False
if open_npc_menu(Npc.CAIN):
press_npc_btn(Npc.CAIN, "identify")
return Location.A1_KASHYA_CAIN
return False
def open_trade_menu(self, curr_loc: Location) -> Union[Location, bool]:
def open_trade_menu(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A1_AKARA), self._char, force_move=True): return False
open_npc_menu(Npc.AKARA)
press_npc_btn(Npc.AKARA, "trade")
return Location.A1_AKARA
def open_stash(self, curr_loc: Location) -> Union[Location, bool]:
def open_stash(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A1_STASH), self._char, force_move=True):
return False
wait(0.5, 0.6)
@@ -75,12 +73,12 @@ class A1(IAct):
return False
return Location.A1_STASH
def heal(self, curr_loc: Location) -> Union[Location, bool]:
def heal(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A1_AKARA), self._char, force_move=True): return False
open_npc_menu(Npc.AKARA)
return Location.A1_AKARA
def open_trade_and_repair_menu(self, curr_loc: Location) -> Union[Location, bool]:
def open_trade_and_repair_menu(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A1_CHARSI), self._char, force_move=True): return False
open_npc_menu(Npc.CHARSI)
press_npc_btn(Npc.CHARSI, "trade_repair")
+8 -9
View File
@@ -3,8 +3,7 @@ from town.i_act import IAct
from screen import grab
from npc_manager import Npc, open_npc_menu, press_npc_btn
from pather import Pather, Location
from typing import Union
from template_finder import TemplateFinder
import template_finder
from utils.misc import wait
from ui_manager import ScreenObjects, is_visible
@@ -20,13 +19,13 @@ class A2(IAct):
def can_heal(self) -> bool: return True
def can_trade_and_repair(self) -> bool: return True
def heal(self, curr_loc: Location) -> Union[Location, bool]:
def heal(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A2_FARA_STASH), self._char, force_move=True): return False
if open_npc_menu(Npc.FARA):
return Location.A2_FARA_STASH
return False
def open_stash(self, curr_loc: Location) -> Union[Location, bool]:
def open_stash(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A2_FARA_STASH), self._char, force_move=True):
return False
wait(0.3)
@@ -39,21 +38,21 @@ class A2(IAct):
return False
return Location.A2_FARA_STASH
def identify(self, curr_loc: Location) -> Union[Location, bool]:
def identify(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A2_FARA_STASH), self._char, force_move=True): return False
if open_npc_menu(Npc.CAIN):
press_npc_btn(Npc.CAIN, "identify")
return Location.A2_FARA_STASH
return False
def open_trade_menu(self, curr_loc: Location) -> Union[Location, bool]:
def open_trade_menu(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A2_DROGNAN), self._char, force_move=True): return False
if open_npc_menu(Npc.DROGNAN):
press_npc_btn(Npc.DROGNAN, "trade")
return Location.A2_DROGNAN
return False
def open_trade_and_repair_menu(self, curr_loc: Location) -> Union[Location, bool]:
def open_trade_and_repair_menu(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A2_FARA_STASH), self._char, force_move=True): return
if open_npc_menu(Npc.FARA):
press_npc_btn(Npc.FARA, "trade_repair")
@@ -66,8 +65,8 @@ class A2(IAct):
found_wp_func = lambda: is_visible(ScreenObjects.WaypointLabel)
return self._char.select_by_template(["A2_WP_LIGHT", "A2_WP_DARK"], found_wp_func, telekinesis=True)
def wait_for_tp(self) -> Union[Location, bool]:
template_match = TemplateFinder().search_and_wait(["A2_TOWN_21", "A2_TOWN_22", "A2_TOWN_20", "A2_TOWN_19"], timeout=20)
def wait_for_tp(self) -> Location | bool:
template_match = template_finder.search_and_wait(["A2_TOWN_21", "A2_TOWN_22", "A2_TOWN_20", "A2_TOWN_19", "A2_TOWN_26"], timeout=20)
if template_match.valid:
self._pather.traverse_nodes((Location.A2_TP, Location.A2_FARA_STASH), self._char, force_move=True)
return Location.A2_FARA_STASH
+7 -8
View File
@@ -3,8 +3,7 @@ from town.i_act import IAct
from screen import grab
from npc_manager import Npc, open_npc_menu, press_npc_btn
from pather import Pather, Location
from typing import Union
from template_finder import TemplateFinder
import template_finder
from utils.misc import wait
from ui_manager import ScreenObjects, is_visible
@@ -21,19 +20,19 @@ class A3(IAct):
def can_identify(self) -> bool: return True
def can_stash(self) -> bool: return True
def heal(self, curr_loc: Location) -> Union[Location, bool]:
def heal(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A3_ORMUS), self._char, force_move=True): return False
open_npc_menu(Npc.ORMUS)
return Location.A3_ORMUS
def open_trade_menu(self, curr_loc: Location) -> Union[Location, bool]:
def open_trade_menu(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A3_ORMUS), self._char, force_move=True): return False
if open_npc_menu(Npc.ORMUS):
press_npc_btn(Npc.ORMUS, "trade")
return Location.A3_ORMUS
return False
def open_stash(self, curr_loc: Location) -> Union[Location, bool]:
def open_stash(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A3_STASH_WP), self._char, force_move=True):
return False
wait(0.3)
@@ -52,14 +51,14 @@ class A3(IAct):
found_wp_func = lambda: is_visible(ScreenObjects.WaypointLabel)
return self._char.select_by_template("A3_WP", found_wp_func, telekinesis=True)
def wait_for_tp(self) -> Union[Location, bool]:
template_match = TemplateFinder().search_and_wait("A3_TOWN_10", timeout=20)
def wait_for_tp(self) -> Location | bool:
template_match = template_finder.search_and_wait("A3_TOWN_10", timeout=20)
if template_match.valid:
self._pather.traverse_nodes((Location.A3_STASH_WP, Location.A3_STASH_WP), self._char, force_move=True)
return Location.A3_STASH_WP
return False
def identify(self, curr_loc: Location) -> Union[Location, bool]:
def identify(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A3_STASH_WP), self._char, force_move=True): return False
if open_npc_menu(Npc.CAIN):
press_npc_btn(Npc.CAIN, "identify")

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