Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7426e79504 | ||
|
|
5bceafec8b | ||
|
|
3d004062d7 | ||
|
|
0fcddd2aed | ||
|
|
237dfd6f34 | ||
|
|
70debc2b0d | ||
|
|
241fc15269 | ||
|
|
16a9832af0 | ||
|
|
94f6c82560 | ||
|
|
6eebe281ac | ||
|
|
bd092bd8d2 | ||
|
|
cdd16dbb92 | ||
|
|
0870384b8d | ||
|
|
bcea0129ba | ||
|
|
8989738c58 | ||
|
|
b457877623 | ||
|
|
d047886630 | ||
|
|
54888942f5 | ||
|
|
129c00ac44 | ||
|
|
f7ea236823 | ||
|
|
f94bd16b2b | ||
|
|
8db079f720 | ||
|
|
084bdb6573 | ||
|
|
ac3e79401b | ||
|
|
baa6bf0e9b | ||
|
|
f0c6d5a6b7 | ||
|
|
fd7294b895 | ||
|
|
7e700ad75e | ||
|
|
fcaffc5cf2 | ||
|
|
3e6f9a9bf1 | ||
|
|
700e7e23b2 | ||
|
|
d41771a6c4 | ||
|
|
58d2834949 | ||
|
|
215e589438 | ||
|
|
cf7eaa6d09 | ||
|
|
a30039bc9e | ||
|
|
27ebd1b4db | ||
|
|
203dce6775 | ||
|
|
63b761b5dc | ||
|
|
580d9c6253 | ||
|
|
fdc25d6c4d | ||
|
|
99778ea51f | ||
|
|
3e6521c2e6 | ||
|
|
648d42ed84 | ||
|
|
328ff67a4b | ||
|
|
f3d6677ef2 | ||
|
|
bbc183e82e | ||
|
|
e879514846 | ||
|
|
0e15b7ccdb | ||
|
|
5bdc6caa13 | ||
|
|
376fb05225 | ||
|
|
886fc88816 | ||
|
|
e156671b17 | ||
|
|
ea5071cf85 | ||
|
|
a15f7207fe | ||
|
|
e0e0ff01f1 | ||
|
|
20f59028e2 |
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 +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}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .paladin import Paladin
|
||||
from .fohdin import FoHdin
|
||||
from .hammerdin import Hammerdin
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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__":
|
||||
|
||||
@@ -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,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")
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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']:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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%
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||