Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bc34a7b2f | ||
|
|
2dc1356cf7 | ||
|
|
ae0ab54efe | ||
|
|
bfb367ee62 | ||
|
|
8d925509fa | ||
|
|
811d656022 | ||
|
|
c7c0e9e1a3 | ||
|
|
9bc085af3a | ||
|
|
a5d7fdaffa | ||
|
|
16bf599d88 | ||
|
|
59ba4409b5 | ||
|
|
f510965d05 | ||
|
|
d43441358d | ||
|
|
1ece05f7a7 | ||
|
|
ca93f3aa5d | ||
|
|
5c6e2211d6 | ||
|
|
f469d98f74 | ||
|
|
699fcac7af | ||
|
|
c22275c1bb | ||
|
|
470f42b1b9 | ||
|
|
4b5b8385f8 | ||
|
|
42655d5209 | ||
|
|
09a33ccf3d | ||
|
|
fd93910941 | ||
|
|
51de6f9f00 | ||
|
|
d615f11608 | ||
|
|
c6d2f3f068 | ||
|
|
0606adb5da | ||
|
|
82576282ab | ||
|
|
e6fe7ef609 | ||
|
|
45e7a5ca9a | ||
|
|
7d3710d1e8 | ||
|
|
e354ed8074 | ||
|
|
c4a5c6fe34 | ||
|
|
2079b81df4 | ||
|
|
e64b312562 | ||
|
|
cceb7905c7 | ||
|
|
4007087ceb | ||
|
|
e64f1151f3 | ||
|
|
46202e5a69 | ||
|
|
782001d338 | ||
|
|
4eeadb934f |
@@ -59,7 +59,6 @@ run_shenk=0
|
||||
| difficulty | Set to `normal` `nightmare` or `hell` for game difficulty. |
|
||||
| message_api_type | Which api to use to send botty messages. Supports "generic_api" (basic discord), or "discord" (discord embeds with images). |
|
||||
| discord_status_count | Number of games between discord status messages being sent. Leave empty for no status reports.
|
||||
| discord_status_condensed | Toggles condensed view of Discord status messages. 0 Full text, 1 Condensed text.
|
||||
| info_screenshots | If `1`, the bot takes a screenshot with timestamp on every stuck / chicken / timeout / inventory full event. This is 1 by Default, so remember to clean up the folder every once in a while. |
|
||||
| loot_screenshots | If `1`, the bot takes a screenshot with timestamp everytime he presses `show_items` button and saves it to `loot_screenshots` folder. Remember to clear them once in a while... |
|
||||
| saved_games_folder | [Optional] Defaults to `~\Saved Games\Diablo II Resurrected`. Used to store configuration settings for `f9` / auto settings. |
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
|
||||
"name": "botty",
|
||||
|
||||
"savepath": "../"
|
||||
|
||||
}
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -86,7 +86,7 @@ chat_line_1=12,537,391,25
|
||||
save_and_exit=500,233,273,173
|
||||
play_btn=426,616,320,71
|
||||
difficulty_select=536,236,210,320
|
||||
gold_btn=927,491,267,100
|
||||
gold_btn=997,521,20,18
|
||||
inventory_gold=980,510,150,40
|
||||
gold_btn_stash=150,480,40,65
|
||||
vendor_gold_digits=186,509,97,16
|
||||
@@ -134,6 +134,7 @@ quest_skill_btn=284,455,710,121
|
||||
left_inventory_tabs=31,62,385,28
|
||||
tab_indicator=31,82,385,14
|
||||
deposit_btn=454,365,186,43
|
||||
equipped_inventory_area=861,59,387,287
|
||||
|
||||
[path]
|
||||
; static pathes in format: x0,y0, x1,y1, x2,y2, ...
|
||||
|
||||
@@ -5,28 +5,30 @@ message_api_type=discord
|
||||
custom_message_hook=
|
||||
max_game_length_s=380
|
||||
max_consecutive_fails=0
|
||||
max_runtime_before_break_m=0
|
||||
break_length_m=0
|
||||
randomize_runs=0
|
||||
difficulty=hell
|
||||
info_screenshots=1
|
||||
loot_screenshots=0
|
||||
discord_status_count=20
|
||||
discord_status_condensed=1
|
||||
saved_games_folder=
|
||||
d2r_path=C:\Program Files (x86)\Diablo II Resurrected\D2R.exe
|
||||
d2r_path=C:\Program Files (x86)\Diablo II Resurrected
|
||||
; if you set this field to 1, botty will save the top-most visible character template just after
|
||||
; the online/offline tab and upon restart will attempt to find the same template to select the char
|
||||
restart_d2r_when_stuck=0
|
||||
|
||||
|
||||
[routes]
|
||||
run_trav=0
|
||||
run_pindle=1
|
||||
run_eldritch=1
|
||||
run_shenk=0
|
||||
run_nihlathak=0
|
||||
run_arcane=0
|
||||
run_diablo=0
|
||||
; Note: You can not split up or change order of eldritch and shenk as botty treats it as a single run
|
||||
order=run_pindle, run_eldritch, run_shenk, run_nihlathak, run_trav, run_arcane, run_diablo
|
||||
; Add these possible routes to "order" to run them:
|
||||
; run_trav
|
||||
; run_pindle
|
||||
; run_eldritch
|
||||
; run_eldritch_shenk
|
||||
; run_nihlathak
|
||||
; run_arcane
|
||||
; run_diablo
|
||||
order=run_pindle, run_eldritch_shenk
|
||||
|
||||
[char]
|
||||
; ==========================
|
||||
@@ -203,6 +205,23 @@ corpse_explosion=
|
||||
clear_pindle_pack=
|
||||
heart_of_wolverine=
|
||||
|
||||
[poison_necro]
|
||||
teleport=
|
||||
poison_nova=
|
||||
# Not Required
|
||||
skill_left=
|
||||
bone_armor=
|
||||
# Can use any golem
|
||||
clay_golem=
|
||||
raise_skeleton=
|
||||
raise_mage=
|
||||
raise_revive=
|
||||
# Can Use any curse
|
||||
lower_res=
|
||||
corpse_explosion=
|
||||
clear_pindle_pack=
|
||||
heart_of_wolverine=
|
||||
|
||||
; ===========================
|
||||
; ==== Builds: Basic ====
|
||||
; ===========================
|
||||
@@ -260,3 +279,4 @@ hwnd_window_process=D2R\.exe
|
||||
;If you want to control Hyper-V window from host use 0,51 here
|
||||
window_client_area_offset=0,0
|
||||
ocr_during_pickit=0
|
||||
launch_options=
|
||||
|
||||
@@ -37,15 +37,31 @@
|
||||
|
||||
;examples
|
||||
; keeps small charms with resi, 7_mf or 3 max_AD and AR
|
||||
; magic_small_charm=1, (and(resi, 7_mf), and(3_maximum_damage, attack_rating)
|
||||
; magic_small_charm=1, (all_resist, 7_magicfind, and(3_maximum_damage, attack_rating))
|
||||
|
||||
; keeps all Skiller or (Life, max_AD, Life) GC
|
||||
; magic_grand_charm=1, (Amazonskiller, Assasinskiller, Barbarianskiller, Druidskiller, Necromancerskiller, Paladinskiller, Sorceressskiller, (Life, maximum_damage, attack_rating))
|
||||
|
||||
; keeps greater talons with (2 Assa Skills + 3 lightning sentry) or (+Traps skills and + lightning sentry)
|
||||
; magic_greater_talons=1, ((2_assasin_skills, 3_lightning_sentry), (traps, lightning_sentry))
|
||||
|
||||
|
||||
; you can also create and use variables for filters you use often
|
||||
; make sure it's under the [variables] section
|
||||
|
||||
;examples
|
||||
; creates a variable called has_skiller
|
||||
; has_skiller=Amazonskiller, Assasinskiller, Barbarianskiller, Druidskiller, Necromancerskiller, Paladinskiller, Sorceressskiller
|
||||
; use the has_skiller variable
|
||||
; magic_grand_charm=1, (has_skiller, (Life, maximum_damage, attack_rating))
|
||||
|
||||
[variables]
|
||||
;==================================================================;
|
||||
;<<<<<<<<<<<<<<<<<<<<<<<<<<<< Variables >>>>>>>>>>>>>>>>>>>>>>>>>>>>;
|
||||
;==================================================================;
|
||||
|
||||
has_skiller=Amazonskiller, Assasinskiller, Barbarianskiller, Druidskiller, Necromancerskiller, Paladinskiller, Sorceressskiller
|
||||
lightning_fire_cold_res=and(lightning_resist, fire_resist, cold_resist)
|
||||
|
||||
[items]
|
||||
;==================================================================;
|
||||
;<<<<<<<<<<<<<<<<<<<<<<<<<<<< MISC >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>;
|
||||
@@ -152,7 +168,7 @@ rune_33_zod=2
|
||||
;==================================================================;
|
||||
|
||||
;magic charms
|
||||
magic_grand_charm=1, (Amazonskiller, Assasinskiller, Barbarianskiller, Druidskiller, Necromancerskiller, Paladinskiller, Sorceressskiller, (Life, maximum_damage, attack_rating))
|
||||
magic_grand_charm=1, (has_skiller, (Life, maximum_damage, attack_rating))
|
||||
magic_large_charm=0
|
||||
magic_small_charm=1, (all_resist, 7_magicfind, 20_life, and(3_maximum_damage, attack_rating))
|
||||
|
||||
@@ -345,25 +361,25 @@ rare_colossus_girdle=0,, ethereal
|
||||
;---------------------------------------;
|
||||
|
||||
;normal rare boots
|
||||
rare_greaves=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_chain_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_light_plated_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_heavy_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_greaves=1, lightning_fire_cold_res, ethereal
|
||||
rare_boots=1, lightning_fire_cold_res, ethereal
|
||||
rare_chain_boots=1, lightning_fire_cold_res, ethereal
|
||||
rare_light_plated_boots=1, lightning_fire_cold_res, ethereal
|
||||
rare_heavy_boots=1, lightning_fire_cold_res, ethereal
|
||||
|
||||
;exceptional rare boots
|
||||
rare_war_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_battle_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_mesh_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_sharkskin_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_demonhide_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_war_boots=1, lightning_fire_cold_res, ethereal
|
||||
rare_battle_boots=1, lightning_fire_cold_res, ethereal
|
||||
rare_mesh_boots=1, lightning_fire_cold_res, ethereal
|
||||
rare_sharkskin_boots=1, lightning_fire_cold_res, ethereal
|
||||
rare_demonhide_boots=1, lightning_fire_cold_res, ethereal
|
||||
|
||||
;elite rare boots
|
||||
rare_myrmidon_greaves=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_mirrored_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_boneweave_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_scarabshell_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_wyrmhide_boots=1, and(lightning_resist, fire_resist, cold_resist), ethereal
|
||||
rare_myrmidon_greaves=1, lightning_fire_cold_res, ethereal
|
||||
rare_mirrored_boots=1, lightning_fire_cold_res, ethereal
|
||||
rare_boneweave_boots=1, lightning_fire_cold_res, ethereal
|
||||
rare_scarabshell_boots=1, lightning_fire_cold_res, ethereal
|
||||
rare_wyrmhide_boots=1, lightning_fire_cold_res, ethereal
|
||||
|
||||
;---------------------------------------;
|
||||
;============= RARE GLOVES =============;
|
||||
@@ -709,14 +725,14 @@ white_superior_phase_blade=0
|
||||
;-----------------------------------------;
|
||||
|
||||
;eth/non eth/socketed weapons
|
||||
gray_weapon_berserker_axe=0, (socketed_1, socketed_2, socketed_3, socketed_4)
|
||||
gray_weapon_berserker_axe=0,, (socketed_1, socketed_2, socketed_3, socketed_4)
|
||||
gray_weapon_crystal_sword=0,, (ethereal, socketed_1, socketed_2, socketed_3)
|
||||
gray_weapon_flail=0,, (ethereal, socketed_1, socketed_2, socketed_3)
|
||||
gray_weapon_phase_blade=0,, (ethereal, socketed_1, socketed_2, socketed_3, socketed_4, socketed_6)
|
||||
gray_weapon_war_scepter=1,, (ethereal, socketed_1, socketed_2, socketed_3, socketed_4)
|
||||
gray_divine_scepter=1,, (ethereal, socketed_1, socketed_2, socketed_3, socketed_4)
|
||||
gray_caduceus=1,, (ethereal, socketed_1, socketed_2, socketed_3, socketed_4)
|
||||
gray_weapon_war_pike=0, (socketed_1, socketed_2, socketed_3, socketed_4, socketed_5)
|
||||
gray_weapon_war_pike=0,, (socketed_1, socketed_2, socketed_3, socketed_4, socketed_5)
|
||||
|
||||
;superior ethereal
|
||||
gray_superior_berserker_axe=1, enhanced_damage, (socketed_1, socketed_2, socketed_3, socketed_4)
|
||||
@@ -1454,4 +1470,4 @@ rare_thunder_maul=0, ethereal
|
||||
rare_truncheon=0, ethereal
|
||||
rare_tyrant_club=0, ethereal
|
||||
rare_war_club=0, ethereal
|
||||
rare_war_hammer=0, ethereal
|
||||
rare_war_hammer=0, ethereal
|
||||
@@ -31,3 +31,5 @@ dependencies:
|
||||
- parse
|
||||
- dependencies/tesserocr-2.5.2-cp39-cp39-win_amd64.whl
|
||||
- typing_extensions
|
||||
- rapidfuzz
|
||||
- colorama
|
||||
@@ -5,11 +5,13 @@ import time
|
||||
import os
|
||||
import random
|
||||
import cv2
|
||||
import math
|
||||
from copy import copy
|
||||
from typing import Union
|
||||
from collections import OrderedDict
|
||||
from health_manager import set_pause_state
|
||||
from transmute import Transmute
|
||||
from utils.misc import wait, hms
|
||||
|
||||
from game_stats import GameStats
|
||||
from logger import Logger
|
||||
@@ -25,6 +27,7 @@ from char.trapsin import Trapsin
|
||||
from char.hammerdin import Hammerdin
|
||||
from char.barbarian import Barbarian
|
||||
from char.necro import Necro
|
||||
from char.poison_necro import Poison_Necro
|
||||
from char.basic import Basic
|
||||
from char.basic_ranged import Basic_Ranged
|
||||
from ui_manager import wait_until_hidden, wait_until_visible, ScreenObjects, is_visible
|
||||
@@ -61,6 +64,8 @@ class Bot:
|
||||
self._char: IChar = Trapsin(Config().trapsin, self._pather)
|
||||
elif Config().char["type"] == "barbarian":
|
||||
self._char: IChar = Barbarian(Config().barbarian, self._pather)
|
||||
elif Config().char["type"] == "poison_necro":
|
||||
self._char: IChar = Poison_Necro(Config().poison_necro, self._pather)
|
||||
elif Config().char["type"] == "necro":
|
||||
self._char: IChar = Necro(Config().necro, self._pather)
|
||||
elif Config().char["type"] == "basic":
|
||||
@@ -80,16 +85,13 @@ class Bot:
|
||||
self._town_manager = TownManager(a1, a2, a3, a4, a5)
|
||||
|
||||
# Create runs
|
||||
if Config().routes["run_shenk"] and not Config().routes["run_eldritch"]:
|
||||
Logger.error("Running shenk without eldtritch is not supported. Either run none or both")
|
||||
os._exit(1)
|
||||
self._do_runs = {
|
||||
"run_trav": Config().routes["run_trav"],
|
||||
"run_pindle": Config().routes["run_pindle"],
|
||||
"run_shenk": Config().routes["run_shenk"] or Config().routes["run_eldritch"],
|
||||
"run_nihlathak": Config().routes["run_nihlathak"],
|
||||
"run_arcane": Config().routes["run_arcane"],
|
||||
"run_diablo": Config().routes["run_diablo"],
|
||||
"run_trav": Config().routes.get("run_trav"),
|
||||
"run_pindle": Config().routes.get("run_pindle"),
|
||||
"run_shenk": Config().routes.get("run_eldritch") or Config().routes.get("run_eldritch_shenk"),
|
||||
"run_nihlathak": Config().routes.get("run_nihlathak"),
|
||||
"run_arcane": Config().routes.get("run_arcane"),
|
||||
"run_diablo": Config().routes.get("run_diablo"),
|
||||
}
|
||||
# Adapt order to the config
|
||||
self._do_runs = OrderedDict((k, self._do_runs[k]) for k in Config().routes_order if k in self._do_runs and self._do_runs[k])
|
||||
@@ -115,6 +117,7 @@ class Bot:
|
||||
self._current_threads = []
|
||||
self._ran_no_pickup = False
|
||||
self._previous_run_failed = False
|
||||
self._timer = time.time()
|
||||
|
||||
# Create State Machine
|
||||
self._states=['initialization','hero_selection', 'town', 'pindle', 'shenk', 'trav', 'nihlathak', 'arcane', 'diablo']
|
||||
@@ -227,6 +230,19 @@ class Bot:
|
||||
|
||||
def on_start_from_town(self):
|
||||
self._curr_loc = self._town_manager.wait_for_town_spawn()
|
||||
|
||||
# Handle picking up corpse in case of death
|
||||
if (corpse_present := is_visible(ScreenObjects.Corpse)):
|
||||
self._previous_run_failed = True
|
||||
view.pickup_corpse()
|
||||
wait_until_hidden(ScreenObjects.Corpse)
|
||||
belt.fill_up_belt_from_inventory(Config().char["num_loot_columns"])
|
||||
self._char.discover_capabilities()
|
||||
if corpse_present and self._char.capabilities.can_teleport_with_charges and not self._char.select_tp():
|
||||
keybind = self._char._skill_hotkeys["teleport"]
|
||||
Logger.info(f"Teleport keybind is lost upon death. Rebinding teleport to '{keybind}'")
|
||||
self._char.remap_right_skill_hotkey("TELE_ACTIVE", self._char._skill_hotkeys["teleport"])
|
||||
|
||||
# Check for the current game ip and pause if we are able to obtain the hot ip
|
||||
if Config().dclone["region_ips"] != "" and Config().dclone["dclone_hotip"] != "":
|
||||
cur_game_ip = get_d2r_game_ip()
|
||||
@@ -248,6 +264,9 @@ class Bot:
|
||||
Logger.info("Activated /nopickup")
|
||||
else:
|
||||
Logger.error("Failed to detect if /nopickup command was applied or not")
|
||||
|
||||
self._game_stats.log_exp()
|
||||
|
||||
self.trigger_or_stop("maintenance")
|
||||
|
||||
def on_maintenance(self):
|
||||
@@ -258,18 +277,6 @@ class Bot:
|
||||
if not view.dismiss_skills_icon():
|
||||
view.return_to_play()
|
||||
|
||||
# Handle picking up corpse in case of death
|
||||
if (corpse_present := is_visible(ScreenObjects.Corpse)):
|
||||
self._previous_run_failed = True
|
||||
view.pickup_corpse()
|
||||
wait_until_hidden(ScreenObjects.Corpse)
|
||||
belt.fill_up_belt_from_inventory(Config().char["num_loot_columns"])
|
||||
self._char.discover_capabilities()
|
||||
if corpse_present and self._char.capabilities.can_teleport_with_charges and not self._char.select_tp():
|
||||
keybind = self._char._skill_hotkeys["teleport"]
|
||||
Logger.info(f"Teleport keybind is lost upon death. Rebinding teleport to '{keybind}'")
|
||||
self._char.remap_right_skill_hotkey("TELE_ACTIVE", self._char._skill_hotkeys["teleport"])
|
||||
|
||||
# Look at belt to figure out how many pots need to be picked up
|
||||
belt.update_pot_needs()
|
||||
|
||||
@@ -399,6 +406,29 @@ class Bot:
|
||||
view.save_and_exit()
|
||||
set_pause_state(True)
|
||||
self._game_stats.log_end_game(failed=failed)
|
||||
|
||||
if Config().general["max_runtime_before_break_m"] and Config().general["break_length_m"]:
|
||||
elapsed_time = time.time() - self._timer
|
||||
Logger.debug(f'Session length = {math.ceil(elapsed_time/60)} minutes, max_runtime_before_break_m {Config().general["max_runtime_before_break_m"]}.')
|
||||
|
||||
if elapsed_time > (Config().general["max_runtime_before_break_m"]*60):
|
||||
break_msg = f'Ran for {hms(elapsed_time)}, taking a break for {hms(Config().general["break_length_m"]*60)}.'
|
||||
Logger.info(break_msg)
|
||||
self._messenger.send_message(break_msg)
|
||||
if not self._pausing:
|
||||
self.toggle_pause()
|
||||
|
||||
wait(Config().general["break_length_m"]*60)
|
||||
|
||||
break_msg = f'Break over, will now run for {hms(Config().general["max_runtime_before_break_m"]*60)}.'
|
||||
Logger.info(break_msg)
|
||||
self._messenger.send_message(break_msg)
|
||||
if self._pausing:
|
||||
self.toggle_pause()
|
||||
|
||||
self._timer = time.time()
|
||||
|
||||
|
||||
self._do_runs = copy(self._do_runs_reset)
|
||||
if Config().general["randomize_runs"]:
|
||||
self.shuffle_runs()
|
||||
@@ -422,6 +452,7 @@ class Bot:
|
||||
# ==================================
|
||||
def _ending_run_helper(self, res: Union[bool, tuple[Location, bool]]):
|
||||
self._game_stats._run_counter += 1
|
||||
self._game_stats.log_exp()
|
||||
# either fill member variables with result data or mark run as failed
|
||||
failed_run = True
|
||||
if res:
|
||||
@@ -438,7 +469,7 @@ class Bot:
|
||||
def on_run_pindle(self):
|
||||
res = False
|
||||
self._do_runs["run_pindle"] = False
|
||||
self._game_stats.update_location("Pin" if Config().general['discord_status_condensed'] else "Pindle")
|
||||
self._game_stats.update_location("Pin")
|
||||
self._curr_loc = self._pindle.approach(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
@@ -451,13 +482,13 @@ class Bot:
|
||||
self._curr_loc = self._shenk.approach(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
res = self._shenk.battle(Config().routes["run_shenk"], not self._pre_buffed, self._game_stats)
|
||||
res = self._shenk.battle(Config().routes.get("run_eldritch_shenk"), not self._pre_buffed, self._game_stats)
|
||||
self._ending_run_helper(res)
|
||||
|
||||
def on_run_trav(self):
|
||||
res = False
|
||||
self._do_runs["run_trav"] = False
|
||||
self._game_stats.update_location("Trav" if Config().general['discord_status_condensed'] else "Travincal")
|
||||
self._game_stats.update_location("Trav")
|
||||
self._curr_loc = self._trav.approach(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
@@ -467,7 +498,7 @@ class Bot:
|
||||
def on_run_nihlathak(self):
|
||||
res = False
|
||||
self._do_runs["run_nihlathak"] = False
|
||||
self._game_stats.update_location("Nihl" if Config().general['discord_status_condensed'] else "Nihlathak")
|
||||
self._game_stats.update_location("Nihl")
|
||||
self._curr_loc = self._nihlathak.approach(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
@@ -477,7 +508,7 @@ class Bot:
|
||||
def on_run_arcane(self):
|
||||
res = False
|
||||
self._do_runs["run_arcane"] = False
|
||||
self._game_stats.update_location("Arc" if Config().general['discord_status_condensed'] else "Arcane")
|
||||
self._game_stats.update_location("Arc")
|
||||
self._curr_loc = self._arcane.approach(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
@@ -487,7 +518,7 @@ class Bot:
|
||||
def on_run_diablo(self):
|
||||
res = False
|
||||
self._do_runs["run_diablo"] = False
|
||||
self._game_stats.update_location("Dia" if Config().general['discord_status_condensed'] else "Diablo")
|
||||
self._game_stats.update_location("Dia")
|
||||
self._curr_loc = self._diablo.approach(self._curr_loc)
|
||||
if self._curr_loc:
|
||||
set_pause_state(False)
|
||||
|
||||
@@ -30,7 +30,7 @@ class IChar:
|
||||
self.capabilities = None
|
||||
|
||||
def _discover_capabilities(self) -> CharacterCapabilities:
|
||||
override = Config().advanced_options["override_capabilities"];
|
||||
override = Config().advanced_options["override_capabilities"]
|
||||
if override is None:
|
||||
if self._skill_hotkeys["teleport"]:
|
||||
if self.select_tp():
|
||||
@@ -174,6 +174,24 @@ class IChar:
|
||||
keyboard.send(Config().char["force_move"])
|
||||
else:
|
||||
mouse.click(button="left")
|
||||
|
||||
def walk(self, pos_monitor: Tuple[float, float], force_tp: bool = False, force_move: bool = False):
|
||||
factor = Config().advanced_options["pathing_delay_factor"]
|
||||
# in case we want to walk we actually want to move a bit before the point cause d2r will always "overwalk"
|
||||
pos_screen = convert_monitor_to_screen(pos_monitor)
|
||||
pos_abs = convert_screen_to_abs(pos_screen)
|
||||
dist = math.dist(pos_abs, (0, 0))
|
||||
min_wd = max(10, Config().ui_pos["min_walk_dist"])
|
||||
max_wd = random.randint(int(Config().ui_pos["max_walk_dist"] * 0.65), Config().ui_pos["max_walk_dist"])
|
||||
adjust_factor = max(max_wd, min(min_wd, dist - 50)) / max(min_wd, dist)
|
||||
pos_abs = [int(pos_abs[0] * adjust_factor), int(pos_abs[1] * adjust_factor)]
|
||||
x, y = convert_abs_to_monitor(pos_abs)
|
||||
mouse.move(x, y, randomize=5, delay_factor=[factor*0.1, factor*0.14])
|
||||
wait(0.012, 0.02)
|
||||
if force_move:
|
||||
keyboard.send(Config().char["force_move"])
|
||||
else:
|
||||
mouse.click(button="left")
|
||||
|
||||
def tp_town(self):
|
||||
# will check if tp is available and select the skill
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
import keyboard
|
||||
from utils.custom_mouse import mouse
|
||||
from char import IChar
|
||||
from template_finder import TemplateFinder
|
||||
from pather import Pather
|
||||
from logger import Logger
|
||||
from screen import grab, convert_abs_to_monitor, convert_screen_to_abs
|
||||
from config import Config
|
||||
from utils.misc import wait, rotate_vec, unit_vector
|
||||
import random
|
||||
from typing import Tuple
|
||||
from pather import Location, Pather
|
||||
import screen as screen
|
||||
import numpy as np
|
||||
import time
|
||||
import os
|
||||
from ui_manager import ScreenObjects
|
||||
|
||||
class Poison_Necro(IChar):
|
||||
def __init__(self, skill_hotkeys: dict, pather: Pather):
|
||||
os.system('color')
|
||||
Logger.info("\033[94m<<Setting up Necro>>\033[0m")
|
||||
super().__init__(skill_hotkeys)
|
||||
self._pather = pather
|
||||
#custom necro pathing for pindle
|
||||
self._pather.adapt_path((Location.A5_PINDLE_START, Location.A5_PINDLE_SAFE_DIST), [100,101])
|
||||
self._pather.adapt_path((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), [104])
|
||||
#minor offsets to pindle fight locations
|
||||
self._pather.offset_node(102, [15, 0])
|
||||
self._pather.offset_node(103, [15, 0])
|
||||
self._pather.offset_node(101, [100,-5])
|
||||
|
||||
#Diablo
|
||||
self._pather.offset_node(644, [150, -70])
|
||||
self._pather.offset_node(610620, [50, 50])
|
||||
self._pather.offset_node(631, [-50, 50])
|
||||
self._pather.offset_node(656, [-70, -50])
|
||||
|
||||
self._shenk_dead = 0
|
||||
self._skeletons_count=0
|
||||
self._mages_count=0
|
||||
self._golem_count="none"
|
||||
self._revive_count=0
|
||||
|
||||
|
||||
def _check_shenk_death(self):
|
||||
''' make sure shenk is dead checking for fireballs so we can exit combat sooner '''
|
||||
|
||||
roi = [640,0,640,720]
|
||||
img = grab()
|
||||
|
||||
template_match = TemplateFinder().search(
|
||||
['SHENK_DEATH_1','SHENK_DEATH_2','SHENK_DEATH_3','SHENK_DEATH_4'],
|
||||
img,
|
||||
threshold=0.6,
|
||||
roi=roi,
|
||||
use_grayscale = False
|
||||
)
|
||||
if template_match.valid:
|
||||
self._shenk_dead=1
|
||||
Logger.info('\33[31m'+"Shenks Dead, looting..."+'\033[0m')
|
||||
else:
|
||||
return True
|
||||
|
||||
def _count_revives(self):
|
||||
roi = [15,14,400,45]
|
||||
img = grab()
|
||||
max_rev = 13
|
||||
|
||||
template_match = TemplateFinder().search(
|
||||
['REV_BASE'],
|
||||
img,
|
||||
threshold=0.6,
|
||||
roi=roi
|
||||
)
|
||||
if template_match.valid:
|
||||
self._revive_count=max_rev
|
||||
else:
|
||||
self._revive_count=0
|
||||
return True
|
||||
|
||||
for count in range(1,max_rev):
|
||||
rev_num = "REV_"+str(count)
|
||||
template_match = TemplateFinder().search(
|
||||
[rev_num],
|
||||
img,
|
||||
threshold=0.66,
|
||||
roi=roi,
|
||||
use_grayscale = False
|
||||
)
|
||||
if template_match.valid:
|
||||
self._revive_count=count
|
||||
|
||||
|
||||
def poison_nova(self, time_in_s: float):
|
||||
if not self._skill_hotkeys["poison_nova"]:
|
||||
raise ValueError("You did not set poison nova hotkey!")
|
||||
keyboard.send(self._skill_hotkeys["poison_nova"])
|
||||
wait(0.05, 0.1)
|
||||
start = time.time()
|
||||
while (time.time() - start) < time_in_s:
|
||||
wait(0.03, 0.04)
|
||||
mouse.press(button="right")
|
||||
wait(0.12, 0.2)
|
||||
mouse.release(button="right")
|
||||
|
||||
def _count_skeletons(self):
|
||||
roi = [15,14,400,45]
|
||||
img = grab()
|
||||
max_skeles = 13
|
||||
|
||||
template_match = TemplateFinder().search(
|
||||
['SKELE_BASE'],
|
||||
img,
|
||||
threshold=0.6,
|
||||
roi=roi
|
||||
)
|
||||
if template_match.valid:
|
||||
self._skeletons_count=max_skeles
|
||||
else:
|
||||
self._skeletons_count=0
|
||||
return True
|
||||
|
||||
for count in range(1,max_skeles):
|
||||
skele_num = "SKELE_"+str(count)
|
||||
template_match = TemplateFinder().search(
|
||||
[skele_num],
|
||||
img,
|
||||
threshold=0.66,
|
||||
roi=roi,
|
||||
use_grayscale = False
|
||||
)
|
||||
if template_match.valid:
|
||||
self._skeletons_count=count
|
||||
|
||||
def _count_gol(self):
|
||||
roi = [15,14,400,45]
|
||||
img = grab()
|
||||
|
||||
template_match = TemplateFinder().search(
|
||||
['CLAY'],
|
||||
img,
|
||||
threshold=0.6,
|
||||
roi=roi
|
||||
)
|
||||
if template_match.valid:
|
||||
self._golem_count="clay gol"
|
||||
else:
|
||||
self._golem_count="none"
|
||||
return True
|
||||
|
||||
def _summon_count(self):
|
||||
''' see how many summons and which golem are out '''
|
||||
|
||||
self._count_skeletons()
|
||||
self._count_revives()
|
||||
self._count_gol()
|
||||
def _summon_stat(self):
|
||||
''' print counts for summons '''
|
||||
Logger.info('\33[31m'+"Summon status | "+str(self._skeletons_count)+"skele | "+str(self._revive_count)+" rev | "+self._golem_count+" |"+'\033[0m')
|
||||
|
||||
def _revive(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=12):
|
||||
Logger.info('\033[94m'+"raise revive"+'\033[0m')
|
||||
keyboard.send(Config().char["stand_still"], do_release=False)
|
||||
for _ in range(cast_count):
|
||||
if self._skill_hotkeys["raise_revive"]:
|
||||
keyboard.send(self._skill_hotkeys["raise_revive"])
|
||||
#Logger.info("revive -> cast")
|
||||
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
|
||||
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
|
||||
|
||||
nx = cast_pos_monitor[0]
|
||||
ny = cast_pos_monitor[1]
|
||||
if(nx>1280):
|
||||
nx=1275
|
||||
if(ny>720):
|
||||
ny=715
|
||||
if(nx<0):
|
||||
nx=0
|
||||
if(ny<0):
|
||||
ny=0
|
||||
clamp = [nx,ny]
|
||||
|
||||
mouse.move(*clamp)
|
||||
mouse.press(button="right")
|
||||
wait(0.075, 0.1)
|
||||
mouse.release(button="right")
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
|
||||
def _raise_skeleton(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=16):
|
||||
Logger.info('\033[94m'+"raise skeleton"+'\033[0m')
|
||||
keyboard.send(Config().char["stand_still"], do_release=False)
|
||||
for _ in range(cast_count):
|
||||
if self._skill_hotkeys["raise_skeleton"]:
|
||||
keyboard.send(self._skill_hotkeys["raise_skeleton"])
|
||||
#Logger.info("raise skeleton -> cast")
|
||||
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
|
||||
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
|
||||
|
||||
nx = cast_pos_monitor[0]
|
||||
ny = cast_pos_monitor[1]
|
||||
if(nx>1280):
|
||||
nx=1279
|
||||
if(ny>720):
|
||||
ny=719
|
||||
if(nx<0):
|
||||
nx=0
|
||||
if(ny<0):
|
||||
ny=0
|
||||
clamp = [nx,ny]
|
||||
|
||||
mouse.move(*clamp)
|
||||
mouse.press(button="right")
|
||||
wait(0.02, 0.05)
|
||||
mouse.release(button="right")
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
|
||||
def _raise_mage(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=16):
|
||||
Logger.info('\033[94m'+"raise mage"+'\033[0m')
|
||||
keyboard.send(Config().char["stand_still"], do_release=False)
|
||||
for _ in range(cast_count):
|
||||
if self._skill_hotkeys["raise_mage"]:
|
||||
keyboard.send(self._skill_hotkeys["raise_mage"])
|
||||
#Logger.info("raise skeleton -> cast")
|
||||
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
|
||||
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
|
||||
|
||||
nx = cast_pos_monitor[0]
|
||||
ny = cast_pos_monitor[1]
|
||||
if(nx>1280):
|
||||
nx=1279
|
||||
if(ny>720):
|
||||
ny=719
|
||||
if(nx<0):
|
||||
nx=0
|
||||
if(ny<0):
|
||||
ny=0
|
||||
clamp = [nx,ny]
|
||||
|
||||
mouse.move(*clamp)
|
||||
mouse.press(button="right")
|
||||
wait(0.02, 0.05)
|
||||
mouse.release(button="right")
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
|
||||
|
||||
def pre_buff(self):
|
||||
#only CTA if pre trav
|
||||
if Config().char["cta_available"]:
|
||||
self._pre_buff_cta()
|
||||
if self._shenk_dead==1:
|
||||
Logger.info("trav buff?")
|
||||
#self._heart_of_wolverine()
|
||||
Logger.info("prebuff/cta")
|
||||
|
||||
|
||||
def _heart_of_wolverine(self):
|
||||
Logger.info('\033[94m'+"buff ~> heart_of_wolverine"+'\033[0m')
|
||||
keyboard.send(self._skill_hotkeys["heart_of_wolverine"])
|
||||
wait(0.05, 0.2)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration)
|
||||
|
||||
def _clay_golem(self):
|
||||
Logger.info('\033[94m'+"cast ~> clay golem"+'\033[0m')
|
||||
keyboard.send(self._skill_hotkeys["clay_golem"])
|
||||
wait(0.05, 0.2)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration)
|
||||
|
||||
|
||||
def bone_armor(self):
|
||||
if self._skill_hotkeys["bone_armor"]:
|
||||
keyboard.send(self._skill_hotkeys["bone_armor"])
|
||||
wait(0.04, 0.1)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration)
|
||||
if self._skill_hotkeys["clay_golem"]:
|
||||
keyboard.send(self._skill_hotkeys["clay_golem"])
|
||||
wait(0.04, 0.1)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration)
|
||||
|
||||
def _bone_armor(self):
|
||||
if self._skill_hotkeys["bone_armor"]:
|
||||
keyboard.send(self._skill_hotkeys["bone_armor"])
|
||||
wait(0.04, 0.1)
|
||||
mouse.click(button="right")
|
||||
wait(self._cast_duration)
|
||||
|
||||
|
||||
|
||||
def _left_attack(self, cast_pos_abs: Tuple[float, float], spray: int = 10):
|
||||
keyboard.send(Config().char["stand_still"], do_release=False)
|
||||
if self._skill_hotkeys["skill_left"]:
|
||||
keyboard.send(self._skill_hotkeys["skill_left"])
|
||||
for _ in range(10):
|
||||
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
|
||||
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
mouse.press(button="left")
|
||||
wait(0.25, 0.3)
|
||||
mouse.release(button="left")
|
||||
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
|
||||
def _left_attack_single(self, cast_pos_abs: Tuple[float, float], spray: int = 10, cast_count: int=6):
|
||||
keyboard.send(Config().char["stand_still"], do_release=False)
|
||||
if self._skill_hotkeys["skill_left"]:
|
||||
keyboard.send(self._skill_hotkeys["skill_left"])
|
||||
for _ in range(cast_count):
|
||||
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
|
||||
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
mouse.press(button="left")
|
||||
wait(0.25, 0.3)
|
||||
mouse.release(button="left")
|
||||
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
|
||||
def _amp_dmg(self, cast_pos_abs: Tuple[float, float], spray: float = 10):
|
||||
if self._skill_hotkeys["amp_dmg"]:
|
||||
keyboard.send(self._skill_hotkeys["amp_dmg"])
|
||||
|
||||
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
|
||||
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
mouse.press(button="right")
|
||||
wait(0.25, 0.35)
|
||||
mouse.release(button="right")
|
||||
|
||||
def _lower_res(self, cast_pos_abs: Tuple[float, float], spray: float = 10):
|
||||
if self._skill_hotkeys["lower_res"]:
|
||||
keyboard.send(self._skill_hotkeys["lower_res"])
|
||||
|
||||
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
|
||||
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
mouse.press(button="right")
|
||||
wait(0.25, 0.35)
|
||||
mouse.release(button="right")
|
||||
|
||||
def _corpse_explosion(self, cast_pos_abs: Tuple[float, float], spray: int = 10,cast_count: int = 8):
|
||||
keyboard.send(Config().char["stand_still"], do_release=False)
|
||||
Logger.info('\033[93m'+"corpse explosion~> random cast"+'\033[0m')
|
||||
for _ in range(cast_count):
|
||||
if self._skill_hotkeys["corpse_explosion"]:
|
||||
keyboard.send(self._skill_hotkeys["corpse_explosion"])
|
||||
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
|
||||
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
|
||||
cast_pos_monitor = screen.convert_abs_to_monitor((x, y))
|
||||
mouse.move(*cast_pos_monitor)
|
||||
mouse.press(button="right")
|
||||
wait(0.075, 0.1)
|
||||
mouse.release(button="right")
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
|
||||
|
||||
def _lerp(self,a: float,b: float, f:float):
|
||||
return a + f * (b - a)
|
||||
|
||||
def _cast_circle(self, cast_dir: Tuple[float,float],cast_start_angle: float=0.0, cast_end_angle: float=90.0,cast_div: int = 10,cast_v_div: int=4,cast_spell: str='raise_skeleton',delay: float=1.0,offset: float=1.0):
|
||||
Logger.info('\033[93m'+"circle cast ~>"+cast_spell+'\033[0m')
|
||||
keyboard.send(Config().char["stand_still"], do_release=False)
|
||||
keyboard.send(self._skill_hotkeys[cast_spell])
|
||||
mouse.press(button="right")
|
||||
|
||||
for i in range(cast_div):
|
||||
angle = self._lerp(cast_start_angle,cast_end_angle,float(i)/cast_div)
|
||||
target = unit_vector(rotate_vec(cast_dir, angle))
|
||||
#Logger.info("current angle ~> "+str(angle))
|
||||
for j in range(cast_v_div):
|
||||
circle_pos_screen = self._pather._adjust_abs_range_to_screen((target*120.0*float(j+1.0))*offset)
|
||||
circle_pos_monitor = screen.convert_abs_to_monitor(circle_pos_screen)
|
||||
mouse.move(*circle_pos_monitor,delay_factor=[0.3*delay, .6*delay])
|
||||
|
||||
|
||||
#Logger.info("circle move")
|
||||
mouse.release(button="right")
|
||||
keyboard.send(Config().char["stand_still"], do_press=False)
|
||||
|
||||
|
||||
def kill_pindle(self) -> bool:
|
||||
pos_m = screen.convert_abs_to_monitor((0, 30))
|
||||
self.walk(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
|
||||
self.poison_nova(3.0)
|
||||
pos_m = screen.convert_abs_to_monitor((0, -50))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
pos_m = screen.convert_abs_to_monitor((50, 0))
|
||||
self.walk(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=5,cast_v_div=2,cast_spell='corpse_explosion',delay=1.1,offset=1.8)
|
||||
self.poison_nova(3.0)
|
||||
return True
|
||||
|
||||
def kill_eldritch(self) -> bool:
|
||||
pos_m = screen.convert_abs_to_monitor((0, -100))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self.bone_armor()
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
|
||||
self.poison_nova(2.0)
|
||||
self._summon_stat()
|
||||
# move a bit back
|
||||
pos_m = screen.convert_abs_to_monitor((0, 50))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self.poison_nova(2.0)
|
||||
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=0.6, force_tp=True)
|
||||
pos_m = screen.convert_abs_to_monitor((0, 170))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=8,cast_v_div=4,cast_spell='raise_revive',delay=1.2,offset=.8)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=8,cast_v_div=4,cast_spell='raise_skeleton',delay=1.1,offset=.8)
|
||||
pos_m = screen.convert_abs_to_monitor((0, -50))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=8,cast_v_div=4,cast_spell='raise_mage',delay=1.1,offset=1.0)
|
||||
pos_m = screen.convert_abs_to_monitor((-75, 0))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=8,cast_v_div=4,cast_spell='raise_skeleton',delay=1.1,offset=.5)
|
||||
self._summon_count()
|
||||
self._summon_stat()
|
||||
|
||||
self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=0.6, force_tp=True)
|
||||
return True
|
||||
|
||||
|
||||
def kill_shenk(self) -> bool:
|
||||
self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0)
|
||||
#pos_m = self._screen.convert_abs_to_monitor((50, 0))
|
||||
#self.walk(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
|
||||
self.poison_nova(3.0)
|
||||
pos_m = screen.convert_abs_to_monitor((0, -50))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=10,cast_v_div=4,cast_spell='raise_mage',delay=1.1,offset=.8)
|
||||
pos_m = screen.convert_abs_to_monitor((50, 0))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=10,cast_v_div=4,cast_spell='raise_revive',delay=1.1,offset=.8)
|
||||
pos_m = screen.convert_abs_to_monitor((-20, -20))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_skeleton',delay=1.1,offset=.8)
|
||||
self._summon_count()
|
||||
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=2,cast_v_div=1,cast_spell='corpse_explosion',delay=3.0,offset=1.8)
|
||||
return True
|
||||
|
||||
|
||||
def kill_council(self) -> bool:
|
||||
pos_m = screen.convert_abs_to_monitor((0, -200))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self._pather.traverse_nodes([229], self, timeout=2.5, force_tp=True, use_tp_charge=True)
|
||||
pos_m = screen.convert_abs_to_monitor((50, 0))
|
||||
self.walk(pos_m, force_move=True)
|
||||
#self._lower_res((-50, 0), spray=10)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
|
||||
self.poison_nova(2.0)
|
||||
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=9,cast_v_div=3,cast_spell='raise_skeleton',delay=1.2,offset=.8)
|
||||
pos_m = screen.convert_abs_to_monitor((200, 50))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
pos_m = screen.convert_abs_to_monitor((30, -50))
|
||||
self.walk(pos_m, force_move=True)
|
||||
self.poison_nova(2.0)
|
||||
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=2,cast_v_div=1,cast_spell='corpse_explosion',delay=3.0,offset=1.8)
|
||||
#wait(self._cast_duration, self._cast_duration +.2)
|
||||
pos_m = screen.convert_abs_to_monitor((-200, 200))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
pos_m = screen.convert_abs_to_monitor((-100, 200))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self._pather.traverse_nodes([226], self, timeout=2.5, force_tp=True, use_tp_charge=True)
|
||||
pos_m = screen.convert_abs_to_monitor((0, 30))
|
||||
self.walk(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
|
||||
wait(0.5)
|
||||
self.poison_nova(4.0)
|
||||
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=2,cast_v_div=1,cast_spell='corpse_explosion',delay=3.0,offset=1.8)
|
||||
#wait(self._cast_duration, self._cast_duration +.2)
|
||||
#self.poison_nova(2.0)
|
||||
pos_m = screen.convert_abs_to_monitor((50, 0))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=5,cast_v_div=2,cast_spell='corpse_explosion',delay=0.5,offset=1.8)
|
||||
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=9,cast_v_div=3,cast_spell='raise_skeleton',delay=1.2,offset=.8)
|
||||
#self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=9,cast_v_div=3,cast_spell='raise_mage',delay=1.2,offset=.8)
|
||||
pos_m = screen.convert_abs_to_monitor((-200, -200))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self._pather.traverse_nodes([229], self, timeout=2.5, force_tp=True, use_tp_charge=True)
|
||||
pos_m = screen.convert_abs_to_monitor((20, -50))
|
||||
self.walk(pos_m, force_move=True)
|
||||
self.poison_nova(2.0)
|
||||
pos_m = screen.convert_abs_to_monitor((50, 0))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=5,cast_v_div=2,cast_spell='corpse_explosion',delay=3.0,offset=1.8)
|
||||
pos_m = screen.convert_abs_to_monitor((-30, -20))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_skeleton',delay=1.2,offset=.8)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_mage',delay=1.2,offset=.8)
|
||||
return True
|
||||
|
||||
def kill_nihlathak(self, end_nodes: list[int]) -> bool:
|
||||
# Move close to nihlathak
|
||||
self._pather.traverse_nodes(end_nodes, self, timeout=0.8, do_pre_move=True)
|
||||
pos_m = screen.convert_abs_to_monitor((20, 20))
|
||||
self.walk(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
|
||||
self.poison_nova(3.0)
|
||||
pos_m = screen.convert_abs_to_monitor((50, 0))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=7200,cast_div=2,cast_v_div=2,cast_spell='corpse_explosion',delay=3.0,offset=1.8)
|
||||
wait(self._cast_duration, self._cast_duration +.2)
|
||||
self.poison_nova(3.0)
|
||||
return True
|
||||
|
||||
def kill_summoner(self) -> bool:
|
||||
# Attack
|
||||
pos_m = screen.convert_abs_to_monitor((0, 30))
|
||||
self.walk(pos_m, force_move=True)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0)
|
||||
wait(0.5)
|
||||
self.poison_nova(3.0)
|
||||
pos_m = screen.convert_abs_to_monitor((50, 0))
|
||||
self.pre_move()
|
||||
self.move(pos_m, force_move=True)
|
||||
wait(self._cast_duration, self._cast_duration + 0.2)
|
||||
self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_mage',delay=1.2,offset=.8)
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
from char import Necro
|
||||
pather = Pather()
|
||||
char = Necro(Config().necro, Config().char, pather)
|
||||
@@ -3,10 +3,8 @@ import string
|
||||
import threading
|
||||
import numpy as np
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from logger import Logger
|
||||
|
||||
config_lock = threading.Lock()
|
||||
|
||||
|
||||
@@ -23,13 +21,9 @@ class ItemProps:
|
||||
|
||||
class Config:
|
||||
data_loaded = False
|
||||
# all configparser objects
|
||||
_config = None
|
||||
_game_config = None
|
||||
_pickit_config = None
|
||||
_shop_config = None
|
||||
_transmute_config = None
|
||||
_custom = None
|
||||
|
||||
configs = {}
|
||||
|
||||
# config data
|
||||
general = {}
|
||||
advanced_options = {}
|
||||
@@ -50,6 +44,7 @@ class Config:
|
||||
hammerdin = {}
|
||||
trapsin = {}
|
||||
barbarian = {}
|
||||
poison_necro = {}
|
||||
necro = {}
|
||||
basic = {}
|
||||
basic_ranged = {}
|
||||
@@ -72,16 +67,28 @@ class Config:
|
||||
return default
|
||||
|
||||
def _select_val(self, section: str, key: str = None):
|
||||
if section in self._custom and key in self._custom[section]:
|
||||
return self._custom[section][key]
|
||||
elif section in self._config:
|
||||
return self._config[section][key]
|
||||
elif section in self._pickit_config:
|
||||
return self._pickit_config[section][key]
|
||||
elif section in self._shop_config:
|
||||
return self._shop_config[section][key]
|
||||
found_in = ""
|
||||
if section in self.configs["custom"]["parser"] and key in self.configs["custom"]["parser"][section]:
|
||||
found_val = self.configs["custom"]["parser"][section][key]
|
||||
found_in = "custom"
|
||||
elif section in self.configs["config"]["parser"]:
|
||||
found_val = self.configs["config"]["parser"][section][key]
|
||||
found_in = "config"
|
||||
elif section in self.configs["pickit"]["parser"]:
|
||||
found_val = self.configs["pickit"]["parser"][section][key]
|
||||
found_in = "pickit"
|
||||
elif section in self.configs["shop"]["parser"]:
|
||||
found_val = self.configs["shop"]["parser"][section][key]
|
||||
found_in = "shop"
|
||||
else:
|
||||
return self._game_config[section][key]
|
||||
found_val = self.configs["game"]["parser"][section][key]
|
||||
found_in = "game"
|
||||
|
||||
for var_name in self.configs[found_in]["vars"]: # set variable.
|
||||
if var_name in found_val:
|
||||
var_val = self.configs[found_in]["vars"][var_name]
|
||||
found_val = found_val.replace(var_name, var_val)
|
||||
return found_val
|
||||
|
||||
def parse_item_config_string(self, key: str = None) -> ItemProps:
|
||||
string = self._select_val("items", key).upper()
|
||||
@@ -170,35 +177,52 @@ class Config:
|
||||
self.items["misc_gold"].pickit_type = 1
|
||||
|
||||
def load_data(self):
|
||||
self._config = configparser.ConfigParser()
|
||||
self._config.read('config/params.ini')
|
||||
self._game_config = configparser.ConfigParser()
|
||||
self._game_config.read('config/game.ini')
|
||||
self._pickit_config = configparser.ConfigParser()
|
||||
self._pickit_config.read('config/pickit.ini')
|
||||
self._shop_config = configparser.ConfigParser()
|
||||
self._shop_config.read('config/shop.ini')
|
||||
self._custom = configparser.ConfigParser()
|
||||
Logger.debug("Loading config")
|
||||
self.configs = {
|
||||
"config": {"parser": configparser.ConfigParser(), "vars": {}},
|
||||
"game": {"parser": configparser.ConfigParser(), "vars": {}},
|
||||
"pickit": {"parser": configparser.ConfigParser(), "vars": {}},
|
||||
"shop": {"parser": configparser.ConfigParser(), "vars": {}},
|
||||
"transmute": {"parser": configparser.ConfigParser(), "vars": {}},
|
||||
"custom": {"parser": configparser.ConfigParser(), "vars": {}},
|
||||
}
|
||||
self.configs["config"]["parser"].read('config/params.ini')
|
||||
self.configs["game"]["parser"].read('config/game.ini')
|
||||
self.configs["pickit"]["parser"].read('config/pickit.ini')
|
||||
self.configs["shop"]["parser"].read('config/shop.ini')
|
||||
self.configs["transmute"]["parser"].read('config/transmute.ini')
|
||||
|
||||
if os.environ.get('RUN_ENV') != "test" and os.path.exists('config/custom.ini'):
|
||||
try:
|
||||
self._custom.read('config/custom.ini')
|
||||
self.configs["custom"]["parser"].read('config/custom.ini')
|
||||
except configparser.MissingSectionHeaderError:
|
||||
Logger.error("custom.ini missing section header, defaulting to params.ini")
|
||||
|
||||
for config_name in self.configs:
|
||||
config = self.configs[config_name]
|
||||
try:
|
||||
for var in config["parser"]["variables"]:
|
||||
config["vars"][var] = config["parser"]["variables"][var] # set var name to var value
|
||||
except KeyError: # "" header section was not found for this config file.
|
||||
pass
|
||||
|
||||
|
||||
|
||||
self.general = {
|
||||
"saved_games_folder": self._select_val("general", "saved_games_folder"),
|
||||
"name": self._select_val("general", "name"),
|
||||
"max_game_length_s": float(self._select_val("general", "max_game_length_s")),
|
||||
"max_consecutive_fails": int(self._select_val("general", "max_consecutive_fails")),
|
||||
"max_runtime_before_break_m": float(_default_iff(self._select_val("general", "max_runtime_before_break_m"), '', 0)),
|
||||
"break_length_m": float(_default_iff(self._select_val("general", "break_length_m"), '', 0)),
|
||||
"randomize_runs": bool(int(self._select_val("general", "randomize_runs"))),
|
||||
"difficulty": self._select_val("general", "difficulty"),
|
||||
"message_api_type": self._select_val("general", "message_api_type"),
|
||||
"custom_message_hook": self._select_val("general", "custom_message_hook"),
|
||||
"discord_status_count": False if not self._select_val("general", "discord_status_count") else int(self._select_val("general", "discord_status_count")),
|
||||
"discord_status_condensed": bool(int(self._select_val("general", "discord_status_condensed"))),
|
||||
"info_screenshots": bool(int(self._select_val("general", "info_screenshots"))),
|
||||
"loot_screenshots": bool(int(self._select_val("general", "loot_screenshots"))),
|
||||
"d2r_path": self._select_val("general", "d2r_path"),
|
||||
"d2r_path": _default_iff(self._select_val("general", "d2r_path"), "", "C:\Program Files (x86)\Diablo II Resurrected"),
|
||||
"restart_d2r_when_stuck": bool(int(self._select_val("general", "restart_d2r_when_stuck"))),
|
||||
}
|
||||
|
||||
@@ -209,11 +233,13 @@ class Config:
|
||||
}
|
||||
|
||||
self.routes = {}
|
||||
order_str = self._select_val("routes", "order").replace("run_eldritch", "run_shenk")
|
||||
order_str = self._select_val("routes", "order")
|
||||
self.routes_order = [x.strip() for x in order_str.split(",")]
|
||||
del self._config["routes"]["order"]
|
||||
for key in self._config["routes"]:
|
||||
self.routes[key] = bool(int(self._select_val("routes", key)))
|
||||
del self.configs["config"]["parser"]["routes"]["order"]
|
||||
for key in self.routes_order:
|
||||
self.routes[key] = True
|
||||
# Botty only knows "run_shenk" but in orders we split run_eldritch and run_eldritch_shenk
|
||||
self.routes_order = ["run_shenk" if x in ["run_eldritch", "run_eldritch_shenk"] else x for x in self.routes_order]
|
||||
|
||||
self.char = {
|
||||
"type": self._select_val("char", "type"),
|
||||
@@ -271,56 +297,60 @@ class Config:
|
||||
"sell_junk": bool(int(self._select_val("char", "sell_junk"))),
|
||||
}
|
||||
# Sorc base config
|
||||
sorc_base_cfg = dict(self._config["sorceress"])
|
||||
if "sorceress" in self._custom:
|
||||
sorc_base_cfg.update(dict(self._custom["sorceress"]))
|
||||
sorc_base_cfg = dict(self.configs["config"]["parser"]["sorceress"])
|
||||
if "sorceress" in self.configs["custom"]["parser"]:
|
||||
sorc_base_cfg.update(dict(self.configs["custom"]["parser"]["sorceress"]))
|
||||
# blizz sorc
|
||||
self.blizz_sorc = dict(self._config["blizz_sorc"])
|
||||
if "blizz_sorc" in self._custom:
|
||||
self.blizz_sorc.update(dict(self._custom["blizz_sorc"]))
|
||||
self.blizz_sorc = dict(self.configs["config"]["parser"]["blizz_sorc"])
|
||||
if "blizz_sorc" in self.configs["custom"]["parser"]:
|
||||
self.blizz_sorc.update(dict(self.configs["custom"]["parser"]["blizz_sorc"]))
|
||||
self.blizz_sorc.update(sorc_base_cfg)
|
||||
# light sorc
|
||||
self.light_sorc = dict(self._config["light_sorc"])
|
||||
if "light_sorc" in self._custom:
|
||||
self.light_sorc.update(dict(self._custom["light_sorc"]))
|
||||
self.light_sorc = dict(self.configs["config"]["parser"]["light_sorc"])
|
||||
if "light_sorc" in self.configs["custom"]["parser"]:
|
||||
self.light_sorc.update(dict(self.configs["custom"]["parser"]["light_sorc"]))
|
||||
self.light_sorc.update(sorc_base_cfg)
|
||||
# nova sorc
|
||||
self.nova_sorc = dict(self._config["nova_sorc"])
|
||||
if "nova_sorc" in self._custom:
|
||||
self.nova_sorc.update(dict(self._custom["nova_sorc"]))
|
||||
self.nova_sorc = dict(self.configs["config"]["parser"]["nova_sorc"])
|
||||
if "nova_sorc" in self.configs["custom"]["parser"]:
|
||||
self.nova_sorc.update(dict(self.configs["custom"]["parser"]["nova_sorc"]))
|
||||
self.nova_sorc.update(sorc_base_cfg)
|
||||
|
||||
# Palandin config
|
||||
self.hammerdin = self._config["hammerdin"]
|
||||
if "hammerdin" in self._custom:
|
||||
self.hammerdin.update(self._custom["hammerdin"])
|
||||
self.hammerdin = self.configs["config"]["parser"]["hammerdin"]
|
||||
if "hammerdin" in self.configs["custom"]["parser"]:
|
||||
self.hammerdin.update(self.configs["custom"]["parser"]["hammerdin"])
|
||||
|
||||
# Assasin config
|
||||
self.trapsin = self._config["trapsin"]
|
||||
if "trapsin" in self._custom:
|
||||
self.trapsin.update(self._custom["trapsin"])
|
||||
self.trapsin = self.configs["config"]["parser"]["trapsin"]
|
||||
if "trapsin" in self.configs["custom"]["parser"]:
|
||||
self.trapsin.update(self.configs["custom"]["parser"]["trapsin"])
|
||||
|
||||
# Barbarian config
|
||||
self.barbarian = self._config["barbarian"]
|
||||
if "barbarian" in self._custom:
|
||||
self.barbarian.update(self._custom["barbarian"])
|
||||
self.barbarian = self.configs["config"]["parser"]["barbarian"]
|
||||
if "barbarian" in self.configs["custom"]["parser"]:
|
||||
self.barbarian.update(self.configs["custom"]["parser"]["barbarian"])
|
||||
self.barbarian = dict(self.barbarian)
|
||||
self.barbarian["cry_frequency"] = float(self.barbarian["cry_frequency"])
|
||||
|
||||
# Basic config
|
||||
self.basic = self._config["basic"]
|
||||
if "basic" in self._custom:
|
||||
self.basic.update(self._custom["basic"])
|
||||
self.basic = self.configs["config"]["parser"]["basic"]
|
||||
if "basic" in self.configs["custom"]["parser"]:
|
||||
self.basic.update(self.configs["custom"]["parser"]["basic"])
|
||||
|
||||
# Basic Ranged config
|
||||
self.basic_ranged = self._config["basic_ranged"]
|
||||
if "basic_ranged" in self._custom:
|
||||
self.basic_ranged.update(self._custom["basic_ranged"])
|
||||
self.basic_ranged = self.configs["config"]["parser"]["basic_ranged"]
|
||||
if "basic_ranged" in self.configs["custom"]["parser"]:
|
||||
self.basic_ranged.update(self.configs["custom"]["parser"]["basic_ranged"])
|
||||
|
||||
# Necro config
|
||||
self.necro = self._config["necro"]
|
||||
if "necro" in self._custom:
|
||||
self.necro.update(self._custom["necro"])
|
||||
self.necro = self.configs["config"]["parser"]["necro"]
|
||||
if "necro" in self.configs["custom"]["parser"]:
|
||||
self.necro.update(self.configs["custom"]["parser"]["necro"])
|
||||
|
||||
self.poison_necro = self.configs["config"]["parser"]["poison_necro"]
|
||||
if "poison_necro" in self.configs["custom"]["parser"]:
|
||||
self.poison_necro.update(self.configs["custom"]["parser"]["poison_necro"])
|
||||
|
||||
self.advanced_options = {
|
||||
"pathing_delay_factor": min(max(int(self._select_val("advanced_options", "pathing_delay_factor")), 1), 10),
|
||||
@@ -338,32 +368,41 @@ class Config:
|
||||
"hwnd_window_process": _default_iff(Config()._select_val("advanced_options", "hwnd_window_process"), ''),
|
||||
"window_client_area_offset": tuple(map(int, Config()._select_val("advanced_options", "window_client_area_offset").split(","))),
|
||||
"ocr_during_pickit": bool(int(self._select_val("advanced_options", "ocr_during_pickit"))),
|
||||
"launch_options": self._select_val("advanced_options", "launch_options"),
|
||||
"override_capabilities": _default_iff(Config()._select_optional("advanced_options", "override_capabilities"), ""),
|
||||
}
|
||||
|
||||
self.items = {}
|
||||
for key in self._pickit_config["items"]:
|
||||
for key in self.configs["pickit"]["parser"]["items"]:
|
||||
try:
|
||||
self.items[key] = self.parse_item_config_string(key)
|
||||
if self.items[key].pickit_type and not os.path.exists(f"./assets/items/{key}.png"):
|
||||
print(f"Warning: You activated {key} in pickit, but there is no img available in assets/items")
|
||||
Logger.warning(f"You activated {key} in pickit, but there is no img available in assets/items")
|
||||
except ValueError as e:
|
||||
print(f"Error with pickit config: {key} ({e})")
|
||||
Logger.error(f"Error with pickit config: {key} ({e})")
|
||||
|
||||
self.colors = {}
|
||||
for key in self._game_config["colors"]:
|
||||
for key in self.configs["game"]["parser"]["colors"]:
|
||||
self.colors[key] = np.split(np.array([int(x) for x in self._select_val("colors", key).split(",")]), 2)
|
||||
|
||||
self.ui_pos = {}
|
||||
for key in self._game_config["ui_pos"]:
|
||||
for key in self.configs["game"]["parser"]["ui_pos"]:
|
||||
self.ui_pos[key] = int(self._select_val("ui_pos", key))
|
||||
|
||||
self.ui_roi = {}
|
||||
for key in self._game_config["ui_roi"]:
|
||||
for key in self.configs["game"]["parser"]["ui_roi"]:
|
||||
self.ui_roi[key] = np.array([int(x) for x in self._select_val("ui_roi", key).split(",")])
|
||||
open_width = int(self.ui_pos["slot_width"] * self.char["num_loot_columns"])
|
||||
# calc roi for restricted inventory area
|
||||
self.ui_roi["restricted_inventory_area"] = self.ui_roi["right_inventory"].copy()
|
||||
self.ui_roi["restricted_inventory_area"][0] += open_width # left
|
||||
self.ui_roi["restricted_inventory_area"][2] -= open_width # width
|
||||
# calc roi for open inventory area
|
||||
self.ui_roi["open_inventory_area"] = self.ui_roi["right_inventory"].copy()
|
||||
self.ui_roi["open_inventory_area"][2] = open_width # width
|
||||
|
||||
self.path = {}
|
||||
for key in self._game_config["path"]:
|
||||
for key in self.configs["game"]["parser"]["path"]:
|
||||
self.path[key] = np.reshape(np.array([int(x) for x in self._select_val("path", key).split(",")]), (-1, 2))
|
||||
|
||||
self.shop = {
|
||||
@@ -378,19 +417,19 @@ class Config:
|
||||
"apply_pather_adjustment": bool(int(self._select_val("scepters", "apply_pather_adjustment"))),
|
||||
}
|
||||
stash_destination_str = self._select_val("transmute","stash_destination")
|
||||
self._transmute_config = {
|
||||
self.configs["transmute"]["parser"] = {
|
||||
"stash_destination": [int(x.strip()) for x in stash_destination_str.split(",")],
|
||||
"transmute_every_x_game": self._select_val("transmute","transmute_every_x_game"),
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
from copy import deepcopy
|
||||
config = self()
|
||||
config = Config()
|
||||
|
||||
# Check if any added items miss templates
|
||||
for k in config.items:
|
||||
if not os.path.exists(f"./assets/items/{k}.png"):
|
||||
print(f"Template not found: {k}")
|
||||
Logger.warning(f"Template not found: {k}")
|
||||
|
||||
# Check if any item templates miss a config
|
||||
for filename in os.listdir(f'assets/items'):
|
||||
@@ -399,4 +438,4 @@ if __name__ == "__main__":
|
||||
item_name = filename[:-4]
|
||||
blacklist_item = item_name.startswith("bl__")
|
||||
if item_name not in config.items and not blacklist_item:
|
||||
print(f"Config not found for: " + filename)
|
||||
Logger.warning(f"Config not found for: " + filename)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import math
|
||||
import numpy as np
|
||||
import time
|
||||
import threading
|
||||
@@ -8,8 +9,11 @@ from logger import Logger
|
||||
from config import Config
|
||||
from messages import Messenger
|
||||
from utils.misc import hms
|
||||
from utils.levels import get_level
|
||||
from version import __version__
|
||||
|
||||
from ui import player_bar
|
||||
|
||||
|
||||
class GameStats:
|
||||
def __init__(self):
|
||||
@@ -31,6 +35,9 @@ class GameStats:
|
||||
self._location_stats["totals"] = { "items": 0, "deaths": 0, "chickens": 0, "merc_deaths": 0, "failed_runs": 0 }
|
||||
self._stats_filename = f'stats_{time.strftime("%Y%m%d_%H%M%S")}.log'
|
||||
self._nopickup_active = False
|
||||
self._starting_exp = 0
|
||||
self._current_exp = 1
|
||||
self._current_lvl = 0
|
||||
|
||||
def update_location(self, loc: str):
|
||||
if self._location != loc:
|
||||
@@ -43,7 +50,7 @@ class GameStats:
|
||||
|
||||
def log_item_keep(self, item_name: str, send_message: bool, img: np.ndarray, ocr_text: str = None):
|
||||
Logger.debug(f"Stashed and logged: {item_name}")
|
||||
filtered_items = ["_potion", "misc_gold"]
|
||||
filtered_items = ["_potion", "misc_gold", "_amethyst", "_ruby", "misc_chipped_diamond", "misc_flawed_diamond", "misc_diamond", "misc_flawless_diamond", "_topaz", "_emerald", "_sapphire", "misc_chipped_skull", "misc_flawed_skull", "misc_skull", "misc_flawless_skull"]
|
||||
if self._location is not None and not any(substring in item_name for substring in filtered_items):
|
||||
self._location_stats[self._location]["items"].append(item_name)
|
||||
self._location_stats["totals"]["items"] += 1
|
||||
@@ -100,6 +107,20 @@ class GameStats:
|
||||
self._consecutive_runs_failed = 0
|
||||
Logger.info(f"End game. Elapsed time: {elapsed_time:.2f}s")
|
||||
|
||||
def log_exp(self):
|
||||
exp = player_bar.get_experience()
|
||||
|
||||
if exp[1] > 0:
|
||||
curr_lvl = get_level(exp[1])['lvl']
|
||||
if curr_lvl > 0:
|
||||
self._current_lvl = curr_lvl-1
|
||||
|
||||
if self._starting_exp == 0:
|
||||
self._starting_exp = exp[0]
|
||||
|
||||
if exp[0] > 0:
|
||||
self._current_exp = exp[0]
|
||||
|
||||
def pause_timer(self):
|
||||
if self._timer is None or self._paused:
|
||||
return
|
||||
@@ -129,12 +150,40 @@ class GameStats:
|
||||
elapsed_time_str = hms(elapsed_time)
|
||||
avg_length_str = "n/a"
|
||||
good_games_count = self._game_counter - self._runs_failed
|
||||
if good_games_count > 0:
|
||||
good_games_time = elapsed_time - self._failed_game_time
|
||||
avg_length = good_games_time / float(good_games_count)
|
||||
avg_length_str = hms(avg_length)
|
||||
good_games_time = elapsed_time - self._failed_game_time
|
||||
|
||||
msg = f'\nSession length: {elapsed_time_str}\nGames: {self._game_counter}\nAvg Game Length: {avg_length_str}'
|
||||
if good_games_count == 0:
|
||||
good_games_count = 1
|
||||
|
||||
avg_length = good_games_time / float(good_games_count)
|
||||
avg_length_str = hms(avg_length)
|
||||
|
||||
curr_lvl = get_level(self._current_lvl)
|
||||
|
||||
msg = f'\nSession length: {elapsed_time_str}'
|
||||
msg += f'\nGames: {self._game_counter}'
|
||||
msg += f'\nAvg Game Length: {avg_length_str}'
|
||||
msg += f'\nCurrent Level: {curr_lvl["lvl"]}'
|
||||
|
||||
if curr_lvl["lvl"] < 99:
|
||||
try:
|
||||
exp_gained = self._current_exp - curr_lvl['exp']
|
||||
per_to_lvl = exp_gained / curr_lvl["xp_to_next"]
|
||||
gained_exp = self._current_exp - self._starting_exp
|
||||
exp_per_second = gained_exp / good_games_time
|
||||
exp_per_hour = round(exp_per_second * 3600, 1)
|
||||
exp_per_game = round(gained_exp / float(good_games_count), 1)
|
||||
exp_needed = curr_lvl['xp_to_next'] - exp_gained
|
||||
time_to_lvl = exp_needed / exp_per_second
|
||||
games_to_lvl = exp_needed / exp_per_game
|
||||
msg += f'\nPercent to Level: {math.ceil(per_to_lvl*100)}%'
|
||||
msg += f'\nXP Gained: {gained_exp:,}'
|
||||
msg += f'\nXP Per Hour: {exp_per_hour:,}'
|
||||
msg += f'\nXP Per Game: {exp_per_game:,}'
|
||||
msg += f'\nTime Needed To Level: {hms(time_to_lvl)}'
|
||||
msg += f'\nGames Needed To Level: {math.ceil(games_to_lvl):,}'
|
||||
except:
|
||||
Logger.warning("Failed to log exp")
|
||||
|
||||
table = BeautifulTable()
|
||||
table.set_style(BeautifulTable.STYLE_BOX_ROUNDED)
|
||||
@@ -145,7 +194,7 @@ class GameStats:
|
||||
table.rows.append([location, len(stats["items"]), stats["chickens"], stats["deaths"], stats["merc_deaths"], stats["failed_runs"]])
|
||||
|
||||
table.rows.append([
|
||||
"T" if Config().general['discord_status_condensed'] else "Total",
|
||||
"T",
|
||||
self._location_stats["totals"]["items"],
|
||||
self._location_stats["totals"]["chickens"],
|
||||
self._location_stats["totals"]["deaths"],
|
||||
@@ -153,10 +202,7 @@ class GameStats:
|
||||
self._location_stats["totals"]["failed_runs"]
|
||||
])
|
||||
|
||||
if Config().general['discord_status_condensed']:
|
||||
table.columns.header = ["Run", "I", "C", "D", "MD", "F"]
|
||||
else:
|
||||
table.columns.header = ["Run", "Items", "Chicken", "Death", "Merc Death", "Failed Runs"]
|
||||
table.columns.header = ["Run", "I", "C", "D", "MD", "F"]
|
||||
|
||||
msg += f"\n{str(table)}\n"
|
||||
return msg
|
||||
|
||||
@@ -12,6 +12,7 @@ from config import Config
|
||||
from inventory import common
|
||||
from ui import view, meters
|
||||
from ui_manager import ScreenObjects, is_visible
|
||||
from random import uniform
|
||||
|
||||
pause_state = True
|
||||
|
||||
@@ -77,6 +78,7 @@ class HealthManager:
|
||||
self._do_monitor = True
|
||||
self._did_chicken = False
|
||||
start = time.time()
|
||||
|
||||
while self._do_monitor:
|
||||
time.sleep(0.1)
|
||||
# Wait until the flag is reset by main.py
|
||||
@@ -85,40 +87,45 @@ class HealthManager:
|
||||
if is_visible(ScreenObjects.InGame, img):
|
||||
health_percentage = meters.get_health(img)
|
||||
mana_percentage = meters.get_mana(img)
|
||||
|
||||
lp_hp_potion_delay = 0 if health_percentage >= 0.99 else uniform(9, 10)
|
||||
lp_mp_potion_delay = 0 if mana_percentage >= 0.99 else uniform(9, 10)
|
||||
|
||||
# check rejuv
|
||||
success_drink_rejuv = False
|
||||
last_drink = time.time() - self._last_rejuv
|
||||
if (health_percentage < Config().char["take_rejuv_potion_health"] and last_drink > 1) or \
|
||||
(mana_percentage < Config().char["take_rejuv_potion_mana"] and last_drink > 2):
|
||||
if (health_percentage <= Config().char["take_rejuv_potion_health"] and last_drink > 1) or \
|
||||
(mana_percentage <= Config().char["take_rejuv_potion_mana"] and last_drink > 2):
|
||||
success_drink_rejuv = belt.drink_potion("rejuv", stats=[health_percentage, mana_percentage])
|
||||
self._last_rejuv = time.time()
|
||||
# in case no rejuv was used, check for chicken, health pot and mana pot usage
|
||||
if not success_drink_rejuv:
|
||||
# check health
|
||||
last_drink = time.time() - self._last_health
|
||||
if health_percentage < Config().char["take_health_potion"] and last_drink > 3.5:
|
||||
if health_percentage <= Config().char["take_health_potion"] and last_drink > lp_hp_potion_delay:
|
||||
belt.drink_potion("health", stats=[health_percentage, mana_percentage])
|
||||
self._last_health = time.time()
|
||||
# give the chicken a 6 sec delay to give time for a healing pot and avoid endless loop of chicken
|
||||
elif health_percentage < Config().char["chicken"] and (time.time() - start) > 6:
|
||||
elif health_percentage <= Config().char["chicken"] and (time.time() - start) > 6:
|
||||
Logger.warning(f"Trying to chicken, player HP {(health_percentage*100):.1f}%!")
|
||||
self._do_chicken(img)
|
||||
# check mana
|
||||
last_drink = time.time() - self._last_mana
|
||||
if mana_percentage < Config().char["take_mana_potion"] and last_drink > 4:
|
||||
if mana_percentage <= Config().char["take_mana_potion"] and last_drink > lp_mp_potion_delay:
|
||||
belt.drink_potion("mana", stats=[health_percentage, mana_percentage])
|
||||
self._last_mana = time.time()
|
||||
# check merc
|
||||
if is_visible(ScreenObjects.MercIcon, img):
|
||||
merc_health_percentage = meters.get_merc_health(img)
|
||||
merc_hp_potion_delay = 0 if merc_health_percentage == 1 else uniform(9, 10)
|
||||
last_drink = time.time() - self._last_merc_heal
|
||||
if merc_health_percentage < Config().char["merc_chicken"]:
|
||||
if merc_health_percentage <= Config().char["merc_chicken"]:
|
||||
Logger.warning(f"Trying to chicken, merc HP {(merc_health_percentage*100):.1f}%!")
|
||||
self._do_chicken(img)
|
||||
if merc_health_percentage < Config().char["heal_rejuv_merc"] and last_drink > 4.0:
|
||||
if merc_health_percentage <= Config().char["heal_rejuv_merc"] and last_drink > 4.0:
|
||||
belt.drink_potion("rejuv", merc=True, stats=[merc_health_percentage])
|
||||
self._last_merc_heal = time.time()
|
||||
elif merc_health_percentage < Config().char["heal_merc"] and last_drink > 7.0:
|
||||
elif merc_health_percentage <= Config().char["heal_merc"] and last_drink > merc_hp_potion_delay:
|
||||
belt.drink_potion("health", merc=True, stats=[merc_health_percentage])
|
||||
self._last_merc_heal = time.time()
|
||||
if is_visible(ScreenObjects.LeftPanel, img) or is_visible(ScreenObjects.RightPanel, img):
|
||||
|
||||
@@ -11,6 +11,7 @@ from utils.misc import cut_roi, wait, color_filter
|
||||
from config import Config
|
||||
from screen import convert_abs_to_monitor, convert_monitor_to_screen, convert_screen_to_monitor, grab
|
||||
import keyboard
|
||||
import os
|
||||
|
||||
def open(img: np.ndarray = None) -> np.ndarray:
|
||||
img = grab() if img is None else img
|
||||
@@ -102,8 +103,15 @@ def update_pot_needs():
|
||||
img = open()
|
||||
# first clean up columns that might be too much
|
||||
for column in range(4):
|
||||
potion_type = _potion_type(_cut_potion_img(img, column, 0))
|
||||
if potion_type != "empty":
|
||||
potion_type = None
|
||||
try:
|
||||
potion_type = _potion_type(_cut_potion_img(img, column, 0))
|
||||
except TypeError:
|
||||
Logger.error("Could not find potions in belt. Most likely due to \"show_belt\" in params.ini having the incorrect hotkey.")
|
||||
Logger.error("Closing in 10 seconds..")
|
||||
wait(10)
|
||||
os._exit(1)
|
||||
if potion_type and potion_type != "empty":
|
||||
rows_left[potion_type] -= 1
|
||||
if rows_left[potion_type] < 0:
|
||||
rows_left[potion_type] += 1
|
||||
|
||||
@@ -5,30 +5,15 @@ import keyboard
|
||||
import time
|
||||
from utils.custom_mouse import mouse
|
||||
from template_finder import TemplateFinder
|
||||
from ui_manager import detect_screen_object, ScreenObjects, center_mouse, is_visible, wait_until_hidden
|
||||
from ui_manager import detect_screen_object, ScreenObjects, is_visible, wait_until_hidden
|
||||
from utils.misc import wait, trim_black, color_filter, cut_roi
|
||||
from inventory import consumables, personal
|
||||
from inventory import consumables
|
||||
from ui import view
|
||||
from screen import convert_screen_to_monitor, grab
|
||||
from dataclasses import dataclass
|
||||
from logger import Logger
|
||||
from ocr import Ocr
|
||||
from template_finder import TemplateMatch
|
||||
|
||||
@dataclass
|
||||
class BoxInfo:
|
||||
img: np.ndarray = None
|
||||
pos: tuple = None
|
||||
column: int = None
|
||||
row: int = None
|
||||
need_id: bool = False
|
||||
sell: bool = False
|
||||
keep: bool = False
|
||||
def __getitem__(self, key):
|
||||
return super().__getattribute__(key)
|
||||
def __setitem__(self, key, value):
|
||||
setattr(self, key, value)
|
||||
|
||||
def get_slot_pos_and_img(img: np.ndarray, column: int, row: int) -> tuple[tuple[int, int], np.ndarray]:
|
||||
"""
|
||||
Get the pos and img of a specific slot position in Inventory. Inventory must be open in the image.
|
||||
@@ -87,6 +72,8 @@ def calc_item_roi(img_pre, img_post):
|
||||
final = np.bitwise_and.reduce([blue_red_mask, green_mask, diff_thresh])
|
||||
_, roi = trim_black(final)
|
||||
return roi
|
||||
except ValueError:
|
||||
Logger.debug(f"_calc_item_roi: Couldn't determine item dimensions--tooltip likely obscuring")
|
||||
except BaseException as err:
|
||||
Logger.error(f"_calc_item_roi: Unexpected {err=}, {type(err)=}")
|
||||
return None
|
||||
@@ -114,83 +101,6 @@ def id_item_with_tome(item_location: list, id_tome_location: list):
|
||||
consumables.increment_need("id", 1)
|
||||
wait(0.1)
|
||||
|
||||
def transfer_items(items: list, action: str = "drop") -> list:
|
||||
#requires open inventory / stash / vendor
|
||||
img = grab()
|
||||
filtered = []
|
||||
left_panel_open = is_visible(ScreenObjects.LeftPanel, img)
|
||||
if action == "drop":
|
||||
filtered = [ item for item in items if item.keep == False and item.sell == False ]
|
||||
elif action == "sell":
|
||||
filtered = [ item for item in items if item.keep == False and item.sell == True ]
|
||||
if not left_panel_open:
|
||||
Logger.error(f"transfer_items: Can't perform, vendor is not open")
|
||||
elif action == "stash":
|
||||
if is_visible(ScreenObjects.GoldBtnStash, img):
|
||||
filtered = [ item for item in items if item.keep == True ]
|
||||
else:
|
||||
Logger.error(f"transfer_items: Can't perform, stash is not open")
|
||||
else:
|
||||
Logger.error(f"transfer_items: incorrect action param={action}")
|
||||
if filtered:
|
||||
# if dropping, control+click to drop unless left panel is open, then drag to middle
|
||||
# if stashing, control+click to stash
|
||||
# if selling, control+click to sell
|
||||
# TODO: if purchasing, right-click to buy
|
||||
# TODO: if purchasing stack, shift+right-click to buy
|
||||
if (action == "drop" and not left_panel_open) or action in ["sell", "stash"]:
|
||||
keyboard.send('ctrl', do_release=False)
|
||||
wait(0.2, 0.4)
|
||||
for item in filtered:
|
||||
attempts = 1
|
||||
prev_gold_img = cut_roi(grab(), roi=Config().ui_roi["inventory_gold_digits"])
|
||||
while attempts <= 2:
|
||||
# move to item position and left click
|
||||
mouse.move(*item.pos, randomize=4, delay_factor=[0.2, 0.4])
|
||||
wait(0.2, 0.4)
|
||||
mouse.press(button="left")
|
||||
wait(0.2, 0.4)
|
||||
mouse.release(button="left")
|
||||
# if dropping, drag item to middle if vendor/stash is open
|
||||
if action == "drop" and left_panel_open:
|
||||
center_mouse()
|
||||
wait(0.2, 0.3)
|
||||
mouse.press(button="left")
|
||||
wait(0.2, 0.3)
|
||||
mouse.release(button="left")
|
||||
# check if item is still there
|
||||
item_remains = True
|
||||
start = time.time()
|
||||
while item_remains and (time.time() - start) < 3:
|
||||
slot_img = get_slot_pos_and_img(grab(), item.column, item.row)[1]
|
||||
item_remains = slot_has_item(slot_img)
|
||||
if not item_remains:
|
||||
# item successfully transferred, delete from list
|
||||
for cnt, o_item in enumerate(items):
|
||||
if o_item.pos == item.pos:
|
||||
items.pop(cnt)
|
||||
break
|
||||
if action == "sell":
|
||||
# check and see if inventory gold count changed
|
||||
start = time.time()
|
||||
gold_unchanged = True
|
||||
while gold_unchanged and (time.time() - start) < 2:
|
||||
new_gold_img = cut_roi(grab(), roi=Config().ui_roi["inventory_gold_digits"])
|
||||
gold_unchanged = prev_gold_img.shape == new_gold_img.shape and not(np.bitwise_xor(prev_gold_img, new_gold_img).any())
|
||||
if gold_unchanged:
|
||||
Logger.info("Inventory gold is full, force stash")
|
||||
personal.set_inventory_gold_full(gold_unchanged)
|
||||
break
|
||||
else:
|
||||
# item is still there, try again
|
||||
attempts += 1
|
||||
if attempts > 2:
|
||||
Logger.error(f"transfer_items: could not stash in position {item.pos}")
|
||||
if (action == "drop" and not left_panel_open) or action in ["sell", "stash"]:
|
||||
keyboard.send('ctrl', do_press=False)
|
||||
wait(0.1)
|
||||
return items
|
||||
|
||||
# use with caution--unreliable
|
||||
def read_gold(img: np.ndarray = None, type: str = "inventory"):
|
||||
if type not in ["vendor", "inventory", "stash"]:
|
||||
@@ -258,10 +168,11 @@ def tab_properties(idx: int = 0) -> dict[int, int, tuple]:
|
||||
}
|
||||
|
||||
def indicator_location_to_tab_count(pos: tuple) -> int:
|
||||
for i in range(3):
|
||||
for i in range(4):
|
||||
tab = tab_properties(i)
|
||||
if tab["left"] <= pos[0] < tab["right"]:
|
||||
return i
|
||||
return -1
|
||||
|
||||
def get_active_tab(indicator: TemplateMatch = None) -> int:
|
||||
indicator = detect_screen_object(ScreenObjects.TabIndicator) if indicator is None else indicator
|
||||
@@ -269,7 +180,7 @@ def get_active_tab(indicator: TemplateMatch = None) -> int:
|
||||
return indicator_location_to_tab_count(indicator.center)
|
||||
else:
|
||||
Logger.error("common/get_active_tab(): Error finding tab indicator")
|
||||
return False
|
||||
return -1
|
||||
|
||||
def select_tab(idx: int):
|
||||
# stash or vendor must be open
|
||||
@@ -306,4 +217,4 @@ if __name__ == "__main__":
|
||||
print(np.sum(b))
|
||||
|
||||
cv2.imshow('test', b)
|
||||
key = cv2.waitKey(1)
|
||||
key = cv2.waitKey(1)
|
||||
|
||||
@@ -109,7 +109,7 @@ def update_tome_key_needs(img: np.ndarray = None, item_type: str = "tp") -> bool
|
||||
match = TemplateFinder().search(
|
||||
[f"{item_type.upper()}_TOME", f"{item_type.upper()}_TOME_RED"],
|
||||
img,
|
||||
roi = personal.specific_inventory_roi("reserved"),
|
||||
roi = Config().ui_roi["restricted_inventory_area"],
|
||||
best_match = True,
|
||||
normalize_monitor=True
|
||||
)
|
||||
@@ -122,7 +122,7 @@ def update_tome_key_needs(img: np.ndarray = None, item_type: str = "tp") -> bool
|
||||
Logger.debug(f"update_tome_key_needs: could not find {item_type}")
|
||||
return False
|
||||
elif item_type.lower() in ["key"]:
|
||||
match = TemplateFinder().search("INV_KEY", img, roi = personal.specific_inventory_roi("reserved"), normalize_monitor = True)
|
||||
match = TemplateFinder().search("INV_KEY", img, roi = Config().ui_roi["restricted_inventory_area"], normalize_monitor = True)
|
||||
if not match.valid:
|
||||
return False
|
||||
else:
|
||||
|
||||
@@ -8,6 +8,7 @@ import keyboard
|
||||
import cv2
|
||||
import time
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
|
||||
from template_finder import TemplateFinder
|
||||
from config import Config
|
||||
@@ -15,7 +16,7 @@ from utils.misc import wait, is_in_roi, mask_by_roi
|
||||
from utils.custom_mouse import mouse
|
||||
from inventory import stash, common, vendor
|
||||
from ui import view
|
||||
from ui_manager import detect_screen_object, is_visible, select_screen_object_match, wait_until_visible, ScreenObjects, center_mouse
|
||||
from ui_manager import detect_screen_object, is_visible, select_screen_object_match, wait_until_visible, ScreenObjects, center_mouse, wait_for_update
|
||||
from item import ItemCropper
|
||||
from messages import Messenger
|
||||
|
||||
@@ -24,6 +25,20 @@ messenger = Messenger()
|
||||
item_finder = ItemFinder()
|
||||
nontradable_items = ["key of ", "essense of", "wirt's", "jade figurine"]
|
||||
|
||||
@dataclass
|
||||
class BoxInfo:
|
||||
img: np.ndarray = None
|
||||
pos: tuple = None
|
||||
column: int = None
|
||||
row: int = None
|
||||
need_id: bool = False
|
||||
sell: bool = False
|
||||
keep: bool = False
|
||||
def __getitem__(self, key):
|
||||
return super().__getattribute__(key)
|
||||
def __setitem__(self, key, value):
|
||||
setattr(self, key, value)
|
||||
|
||||
def get_inventory_gold_full():
|
||||
global inv_gold_full
|
||||
return inv_gold_full
|
||||
@@ -118,7 +133,7 @@ def stash_all_items(items: list = None):
|
||||
common.select_tab(stash.get_curr_stash()["items"])
|
||||
# stash stuff
|
||||
while True:
|
||||
items = common.transfer_items(items, "stash")
|
||||
items = transfer_items(items, "stash")
|
||||
if items and any([item.keep for item in items]):
|
||||
# could not stash all items, stash tab is likely full
|
||||
Logger.debug("Wanted to stash item, but it's still in inventory. Assumes full stash. Move to next.")
|
||||
@@ -241,20 +256,6 @@ def keep_item(item_box: ItemText = None, found_item: Item = None, do_logging: bo
|
||||
return True
|
||||
return False
|
||||
|
||||
def specific_inventory_roi(desired: str = "reserved"):
|
||||
#roi spec: left, top, W, H
|
||||
roi = Config().ui_roi["right_inventory"].copy()
|
||||
open_width = Config().ui_pos["slot_width"] * Config().char["num_loot_columns"]
|
||||
if desired == "reserved":
|
||||
roi[0]=roi[0] + open_width
|
||||
roi[2]=roi[2] - open_width
|
||||
elif desired == "open":
|
||||
roi[2]=open_width
|
||||
else:
|
||||
Logger.error(f"set_inventory_rois: unsupported desired={desired}")
|
||||
return None
|
||||
return roi
|
||||
|
||||
def open(img: np.ndarray = None) -> np.ndarray:
|
||||
img = grab() if img is None else img
|
||||
if not is_visible(ScreenObjects.RightPanel, img):
|
||||
@@ -269,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):
|
||||
def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_stats: GameStats = None) -> list[BoxInfo]:
|
||||
"""
|
||||
Iterate over all picked items in inventory--ID items and decide which to stash
|
||||
:param img: Image in which the item is searched (item details should be visible)
|
||||
@@ -302,8 +303,8 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
|
||||
# determine the item's ROI in inventory
|
||||
cnt=0
|
||||
while True:
|
||||
pre = mask_by_roi(img, specific_inventory_roi("open"))
|
||||
post = mask_by_roi(hovered_item, specific_inventory_roi("open"))
|
||||
pre = mask_by_roi(img, Config().ui_roi["open_inventory_area"])
|
||||
post = mask_by_roi(hovered_item, Config().ui_roi["open_inventory_area"])
|
||||
# will sometimes have equivalent diff if mouse ends up in an inconvenient place.
|
||||
if not np.array_equal(pre, post):
|
||||
break
|
||||
@@ -343,7 +344,7 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
|
||||
if (is_unidentified := is_visible(ScreenObjects.Unidentified, item_box.data)):
|
||||
need_id = True
|
||||
center_mouse()
|
||||
tome_state, tome_pos = common.tome_state(grab(), tome_type = "id", roi = specific_inventory_roi("reserved"))
|
||||
tome_state, tome_pos = common.tome_state(grab(), tome_type = "id", roi = Config().ui_roi["restricted_inventory_area"])
|
||||
if is_unidentified and tome_state is not None and tome_state == "ok":
|
||||
common.id_item_with_tome([x_m, y_m], tome_pos)
|
||||
need_id = False
|
||||
@@ -377,7 +378,7 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
|
||||
if keep:
|
||||
sell = need_id = False
|
||||
|
||||
box = common.BoxInfo(
|
||||
box = BoxInfo(
|
||||
img = item_box.data,
|
||||
pos = (x_m, y_m),
|
||||
column = slot[2],
|
||||
@@ -390,7 +391,7 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
|
||||
if not (keep or need_id) and vendor_open and item_can_be_traded:
|
||||
Logger.debug(f"Selling {item_name}")
|
||||
box.sell = True
|
||||
common.transfer_items([box], action = "sell")
|
||||
transfer_items([box], action = "sell")
|
||||
continue
|
||||
# if item is to be kept and is already ID'd or doesn't need ID, log and stash
|
||||
if game_stats is not None and (keep and not need_id):
|
||||
@@ -402,7 +403,7 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
|
||||
else:
|
||||
# if item isn't going to be kept (or sold if vendor window not open), trash it
|
||||
Logger.debug(f"Dropping {item_name}")
|
||||
common.transfer_items([box], action = "drop")
|
||||
transfer_items([box], action = "drop")
|
||||
wait(0.3, 0.5)
|
||||
else:
|
||||
failed = True
|
||||
@@ -413,5 +414,75 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
|
||||
if Config().general["info_screenshots"]:
|
||||
cv2.imwrite("./info_screenshots/failed_item_box_" + time.strftime("%Y%m%d_%H%M%S") + ".png", hovered_item)
|
||||
if close_window:
|
||||
if not is_visible(ScreenObjects.RightPanel, img):
|
||||
center_mouse()
|
||||
common.close()
|
||||
return boxes
|
||||
return boxes
|
||||
|
||||
def transfer_items(items: list, action: str = "drop", img: np.ndarray = None) -> list:
|
||||
#requires open inventory / stash / vendor
|
||||
img = img if img is not None else grab()
|
||||
filtered = []
|
||||
left_panel_open = is_visible(ScreenObjects.LeftPanel, img)
|
||||
if action == "drop":
|
||||
filtered = [ item for item in items if item.keep == False and item.sell == False ]
|
||||
elif action == "sell":
|
||||
filtered = [ item for item in items if item.keep == False and item.sell == True ]
|
||||
if not left_panel_open:
|
||||
Logger.error(f"transfer_items: Can't perform, vendor is not open")
|
||||
elif action == "stash":
|
||||
if is_visible(ScreenObjects.GoldBtnStash, img):
|
||||
filtered = [ item for item in items if item.keep == True ]
|
||||
else:
|
||||
Logger.error(f"transfer_items: Can't perform, stash is not open")
|
||||
else:
|
||||
Logger.error(f"transfer_items: incorrect action param={action}")
|
||||
if filtered:
|
||||
# if dropping, control+click to drop unless left panel is open, then drag to middle
|
||||
# if stashing, control+click to stash
|
||||
# if selling, control+click to sell
|
||||
if (action == "drop" and not left_panel_open) or action in ["sell", "stash"]:
|
||||
keyboard.send('ctrl', do_release=False)
|
||||
wait(0.1, 0.2)
|
||||
for item in filtered:
|
||||
pre_hover_img = grab()
|
||||
_, slot_img = common.get_slot_pos_and_img(pre_hover_img, item.column, item.row)
|
||||
if not common.slot_has_item(slot_img):
|
||||
# item no longer exists in that position...
|
||||
Logger.debug(f"Item at {item.pos} doesn't exist, skip and remove from list")
|
||||
for cnt, o_item in enumerate(items):
|
||||
if o_item.pos == item.pos:
|
||||
items.pop(cnt)
|
||||
break
|
||||
continue
|
||||
# move to item position and left click
|
||||
mouse.move(*item.pos, randomize=4, delay_factor=[0.2, 0.4])
|
||||
wait(0.2, 0.4)
|
||||
pre_transfer_img = grab()
|
||||
mouse.press(button="left")
|
||||
# wait for inventory image to update indicating successful transfer / item select
|
||||
success = wait_for_update(pre_transfer_img, Config().ui_roi["open_inventory_area"], 3)
|
||||
mouse.release(button="left")
|
||||
if not success:
|
||||
Logger.warning(f"transfer_items: inventory unchanged after attempting to {action} item at position {item.pos}")
|
||||
break
|
||||
else:
|
||||
# if dropping, drag item to middle if vendor/stash is open
|
||||
if action == "drop" and left_panel_open:
|
||||
center_mouse()
|
||||
mouse.press(button="left")
|
||||
wait(0.2, 0.3)
|
||||
mouse.release(button="left")
|
||||
# item successfully transferred, delete from list
|
||||
Logger.debug(f"Confirmed {action} at position {item.pos}")
|
||||
for cnt, o_item in enumerate(items):
|
||||
if o_item.pos == item.pos:
|
||||
items.pop(cnt)
|
||||
break
|
||||
if action == "sell":
|
||||
# check and see if inventory gold count changed
|
||||
if (gold_unchanged := not wait_for_update(pre_transfer_img, Config().ui_roi["inventory_gold_digits"], 3)):
|
||||
Logger.info("Inventory gold is full, force stash")
|
||||
set_inventory_gold_full(gold_unchanged)
|
||||
keyboard.send('ctrl', do_press=False)
|
||||
return items
|
||||
@@ -123,7 +123,7 @@ class ItemCropper:
|
||||
expected_height = True if (self._box_expected_height_range[0] < h < self._box_expected_height_range[1]) else False
|
||||
expected_width = True if (self._box_expected_width_range[0] < w < self._box_expected_width_range[1]) else False
|
||||
box2 = Config().ui_roi[f"{inventory_side}_inventory"]
|
||||
overlaps_inventory = False if (x+w<box2[0] or box2[0]+box2[2]<x or y+h+28+10<box2[1] or box2[1]+box2[3]<y) else True # padded height because footer isn't included in contour
|
||||
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
|
||||
@@ -140,10 +140,11 @@ class ItemCropper:
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
import os
|
||||
from screen import grab, start_detecting_window
|
||||
from template_finder import TemplateFinder
|
||||
|
||||
from screen import start_detecting_window, grab
|
||||
start_detecting_window()
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
print("Move to d2r window and press f11")
|
||||
keyboard.wait("f11")
|
||||
|
||||
keyboard.add_hotkey('f12', lambda: os._exit(1))
|
||||
cropper = ItemCropper()
|
||||
@@ -151,9 +152,23 @@ if __name__ == "__main__":
|
||||
while 1:
|
||||
img = grab().copy()
|
||||
res = cropper.crop_item_descr(img, model="engd2r_inv_th_fast")
|
||||
if res["color"]:
|
||||
if res.valid:
|
||||
x, y, w, h = res.roi
|
||||
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 1)
|
||||
Logger.debug(f"{res.ocr_result['text']}")
|
||||
#Logger.debug(f"{res.ocr_result['text']}")
|
||||
|
||||
Logger.debug(f"OCR ITEM DESCR: Mean conf: {res.ocr_result.mean_confidence}")
|
||||
for i, line in enumerate(list(filter(None, res.ocr_result.text.splitlines()))):
|
||||
Logger.debug(f"OCR LINE{i}: {line}")
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
found_low_confidence = False
|
||||
for cnt, x in enumerate(res.ocr_result['word_confidences']):
|
||||
if x <= 88:
|
||||
try:
|
||||
Logger.debug(f"Low confidence word #{cnt}: {res.ocr_result['original_text'].split()[cnt]} -> {res.ocr_result['text'].split()[cnt]}, Conf: {x}")
|
||||
found_low_confidence = True
|
||||
except: pass
|
||||
|
||||
|
||||
cv2.imshow("res", img)
|
||||
cv2.waitKey(1)
|
||||
cv2.waitKey(5000)
|
||||
|
||||
@@ -4,11 +4,30 @@ import os
|
||||
import sys
|
||||
import warnings
|
||||
from version import __version__
|
||||
from colorama import Fore, Back, Style, init
|
||||
|
||||
init()
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno)
|
||||
formatter = logging.Formatter(log_fmt)
|
||||
return formatter.format(record)
|
||||
|
||||
class Logger:
|
||||
"""Manage logging"""
|
||||
_logger_level = None
|
||||
_formatter = logging.Formatter(f'[{__version__} %(asctime)s] %(levelname)-10s %(message)s')
|
||||
_log_contents = io.StringIO()
|
||||
_current_log_file_path = "info.log"
|
||||
_output = "" # intercepted output from stdout and stderr
|
||||
@@ -74,9 +93,10 @@ class Logger:
|
||||
Logger.file_handler.setLevel(Logger._logger_level)
|
||||
|
||||
# Optionally add a formatter
|
||||
Logger.string_handler.setFormatter(Logger._formatter)
|
||||
Logger.console_handler.setFormatter(Logger._formatter)
|
||||
Logger.file_handler.setFormatter(Logger._formatter)
|
||||
_format = CustomFormatter()
|
||||
Logger.string_handler.setFormatter(_format)
|
||||
Logger.console_handler.setFormatter(_format)
|
||||
Logger.file_handler.setFormatter(_format)
|
||||
|
||||
# Add the handler to the logger
|
||||
Logger.logger.addHandler(Logger.string_handler)
|
||||
|
||||
@@ -71,8 +71,6 @@ class DiscordEmbeds(GenericApi):
|
||||
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())
|
||||
if not Config().general['discord_status_condensed']:
|
||||
e.set_thumbnail(url=f"{self._psnURL}36L4a4994.png")
|
||||
self._send_embed(e)
|
||||
|
||||
def _send_embed(self, e, file = None):
|
||||
|
||||
@@ -2,8 +2,9 @@ from tesserocr import PyTessBaseAPI, PSM, OEM
|
||||
import numpy as np
|
||||
import cv2
|
||||
import re
|
||||
from rapidfuzz.process import extractOne
|
||||
from rapidfuzz.string_metric import levenshtein
|
||||
import csv
|
||||
import difflib
|
||||
from utils.misc import erode_to_black
|
||||
from logger import Logger
|
||||
from typing import List, Union
|
||||
@@ -77,7 +78,7 @@ class Ocr:
|
||||
fix_regexps: bool = True,
|
||||
check_known_errors: bool = True,
|
||||
check_wordlist: bool = True,
|
||||
word_match_threshold: float = 0.9
|
||||
word_match_threshold: float = 0.5
|
||||
) -> list[str]:
|
||||
"""
|
||||
Uses Tesseract to read image(s)
|
||||
@@ -156,13 +157,14 @@ class Ocr:
|
||||
"""
|
||||
OCR output processing functions:
|
||||
"""
|
||||
|
||||
def _check_known_errors(self, text):
|
||||
for key, value in self._ocr_errors.items():
|
||||
if key in text:
|
||||
text = text.replace(key, value)
|
||||
return text
|
||||
|
||||
def _check_wordlist(self, text: str = None, word_list: str = None, confidences: list = [], match_threshold: float = 0.9) -> str:
|
||||
def _check_wordlist(self, text: str = None, word_list: str = None, confidences: list = [], match_threshold: float = 0.5) -> str:
|
||||
with open(f'assets/tessdata/word_lists/{word_list}') as file:
|
||||
word_list = [line.rstrip() for line in file]
|
||||
|
||||
@@ -173,12 +175,14 @@ class Ocr:
|
||||
word = word.strip()
|
||||
if word and word != "NEWLINEHERE":
|
||||
try:
|
||||
if confidences[word_count] <= 88:
|
||||
if (word not in word_list) and (re.sub(r"[^a-zA-Z0-9]", "", word) not in word_list):
|
||||
closest_match = difflib.get_close_matches(word, word_list, cutoff=match_threshold)
|
||||
if closest_match and closest_match != word:
|
||||
new_string += f"{closest_match[0]} "
|
||||
Logger.debug(f"check_wordlist: Replacing {word} ({confidences[word_count]}%) with {closest_match[0]}, score=")
|
||||
if confidences[word_count] <= 90:
|
||||
alphanumeric = re.sub(r"[^a-zA-Z0-9]", "", word)
|
||||
if not alphanumeric.isnumeric() and (word not in word_list) and alphanumeric not in word_list:
|
||||
closest_match, similarity, _ = extractOne(word, word_list, scorer=levenshtein)
|
||||
normalized_similarity = 1 - similarity / len(word)
|
||||
if (normalized_similarity) >= (match_threshold):
|
||||
new_string += f"{closest_match} "
|
||||
Logger.debug(f"check_wordlist: Replacing {word} ({confidences[word_count]}%) with {closest_match}, similarity={normalized_similarity*100:.1f}%")
|
||||
else:
|
||||
new_string += f"{word} "
|
||||
else:
|
||||
@@ -190,8 +194,8 @@ class Ocr:
|
||||
# bizarre word_count index exceeded sometimes... can't reproduce and words otherwise seem to match up
|
||||
Logger.error(f"check_wordlist: IndexError for word: {word}, index: {word_count}, text: {text}")
|
||||
return text
|
||||
except:
|
||||
Logger.error(f"check_wordlist: Unknown error for word: {word}, index: {word_count}, text: {text}")
|
||||
except Exception as e:
|
||||
Logger.error(f"check_wordlist: Unknown error for word: {word}, index: {word_count}, text: {text}, exception: {e}")
|
||||
return text
|
||||
elif word == "NEWLINEHERE":
|
||||
new_string += "\n"
|
||||
@@ -300,6 +304,6 @@ if __name__ == "__main__":
|
||||
fix_regexps = False,
|
||||
check_known_errors = False,
|
||||
check_wordlist = False,
|
||||
word_match_threshold = 0.9
|
||||
word_match_threshold = 0.5
|
||||
)[0]
|
||||
Logger.debug(ocr_result.text)
|
||||
@@ -119,10 +119,10 @@ class Pather:
|
||||
103: {'PINDLE_3': (395, -75), 'PINDLE_4': (267, 52)},
|
||||
104: {'PINDLE_4': (717, -117), 'PINDLE_3': (843, -244), 'PINDLE_5': (-187, 237), 'PINDLE_6': (-467, 89)},
|
||||
# Eldritch
|
||||
120: {'ELDRITCH_0': (293, 24), 'ELDRITCH_1': (-307, 76), 'ELDRITCH_5': (27, -164), 'ELDRITCH_6': (400, -50)},
|
||||
121: {'ELDRITCH_6': (360, -244), 'ELDRITCH_1': (-329, -103), 'ELDRITCH_2': (411, 171), 'ELDRITCH_3': (-91, 198), 'ELDRITCH_7': (409, 180), 'ELDRITCH_8': (465, 345)},
|
||||
122: {'ELDRITCH_2': (353, -145), 'ELDRITCH_3': (-149, -119), 'ELDRITCH_9': (-253, -118), 'ELDRITCH_7': (352, -134), 'ELDRITCH_8': (404, 29)},
|
||||
123: {'ELDRITCH_3': (-99, -252), 'ELDRITCH_2': (403, -279), 'ELDRITCH_4': (-62, -109), 'ELDRITCH_9': (-204, -254), 'ELDRITCH_8': (400, -267), 'ELDRITCH_8': (454, -104)},
|
||||
120: {'ELDRITCH_0': (293, 24), 'ELDRITCH_0_V2': (293, 24), 'ELDRITCH_0_V3': (293, 24), 'ELDRITCH_1': (-307, 76), 'ELDRITCH_1_V2': (-307, 76), 'ELDRITCH_5': (27, -164), 'ELDRITCH_6': (400, -50)},
|
||||
121: {'ELDRITCH_6': (360, -244), 'ELDRITCH_1': (-329, -103), 'ELDRITCH_1_V2': (-329, -103), 'ELDRITCH_2': (411, 171), 'ELDRITCH_2_V2': (411, 171), 'ELDRITCH_3': (-91, 198), 'ELDRITCH_7': (409, 180), 'ELDRITCH_7_V2': (409, 180), 'ELDRITCH_8': (465, 345), 'ELDRITCH_8_V2': (465, 345)},
|
||||
122: {'ELDRITCH_2': (353, -145), 'ELDRITCH_2_V2': (353, -145), 'ELDRITCH_3': (-149, -119), 'ELDRITCH_9': (-253, -118), 'ELDRITCH_7': (352, -134), 'ELDRITCH_7_V2': (352, -134), 'ELDRITCH_8': (404, 29), 'ELDRITCH_8_V2': (404, 29)},
|
||||
123: {'ELDRITCH_3': (-99, -252), 'ELDRITCH_2': (403, -279), 'ELDRITCH_2_V2': (403, -279), 'ELDRITCH_4': (-62, -109), 'ELDRITCH_9': (-204, -254), 'ELDRITCH_8': (454, -104), 'ELDRITCH_8_V2': (454, -104)},
|
||||
# Shenk
|
||||
141: {'SHENK_0': (-129, 44), 'SHENK_1': (464, 107), 'SHENK_2': (-167, -34), 'SHENK_17': (-520, 528), 'SHENK_15': (77, 293), 'SHENK_18': (518, 512)},
|
||||
142: {'SHENK_1': (584, 376), 'SHENK_4': (-443, -103), 'SHENK_2': (-52, 235), 'SHENK_3': (357, -129)},
|
||||
@@ -161,10 +161,10 @@ class Pather:
|
||||
# Trav
|
||||
220: {"TRAV_0": (445, 384), "TRAV_20": (-259, 267), "TRAV_1": (-248, -139), "TRAV_2": (-682, 21), "TRAV_21": (25, 180)},
|
||||
221: {"TRAV_2": (-153, -101), "TRAV_3": (-125, 201), "TRAV_20": (270, 145), "TRAV_1": (281, -261), "TRAV_4": (-459, 122)},
|
||||
222: {"TRAV_5": (-218, 106), "TRAV_4": (120, 230), "TRAV_2": (426, 7), "TRAV_7": (-719, -166), "TRAV_1": (862, -153)},
|
||||
223: {"TRAV_5": (344, 123), 'TRAV_4': (682, 247), "TRAV_8": (-353, -31), "TRAV_7": (-157, -149), "TRAV_22": (-368, -222), "TRAV_23": (-579, 116)},
|
||||
224: {'TRAV_7': (411, -129), 'TRAV_27': (-363, 163), "TRAV_8": (214, -11), "TRAV_23": (-11, 136), "TRAV_10": (-130, -187), "TRAV_24": (-274, 15), "TRAV_22": (200, -202)},
|
||||
225: {'TRAV_27': (96, 359), 'TRAV_8': (670, 187), 'TRAV_7': (867, 69), "TRAV_11": (10, 214), "TRAV_19": (-298, 539), "TRAV_24": (181, 213), "TRAV_12": (-408, -73), "TRAV_25": (-538, 132)},
|
||||
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)},
|
||||
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)},
|
||||
@@ -182,7 +182,7 @@ class Pather:
|
||||
404: {"A2_TOWN_14": (79, 190), "A2_TOWN_15": (244, -12), "A2_TOWN_13": (-270, 123), "A2_TOWN_11": (-258, -287), "A2_TOWN_12": (-599, -143)},
|
||||
405: {"A2_TOWN_10": (65, -175), "A2_TOWN_17": (-108, 164), "A2_TOWN_16": (-304, -11), "A2_TOWN_9": (319, -68), "A2_TOWN_18": (-415, -284)},
|
||||
406: {"A2_TOWN_18": (108, -143), "A2_TOWN_16": (219, 129), "A2_TOWN_19": (-293, 21), "A2_TOWN_17": (415, 304), "A2_TOWN_10": (588, -34)},
|
||||
408: {"A2_TOWN_20": (-26, -109), "A2_TOWN_25": (-82, 278), "A2_TOWN_19": (344, 38), "A2_TOWN_18": (745, -125)},
|
||||
408: {"A2_TOWN_20": (-26, -109), "A2_TOWN_25": (-82, 278), "A2_TOWN_19": (344, 38), "A2_TOWN_18": (745, -125), "A2_TOWN_26": (-831, -73)},
|
||||
409: {"A2_TOWN_14": (477, 294), "A2_TOWN_13": (128, 226), "A2_TOWN_11": (140, -182), "A2_TOWN_12": (-201, -40)},
|
||||
410: {"A2_TOWN_13": (416, 82), "A2_TOWN_12": (87, -184), "A2_TOWN_21": (-211, 10), "A2_TOWN_22": (-178, 269)},
|
||||
411: {"A2_TOWN_22": (298, 0), "A2_TOWN_23": (0, 190), "A2_TOWN_21": (265, -260), "A2_TOWN_24": (-150, -185)},
|
||||
@@ -732,7 +732,7 @@ if __name__ == "__main__":
|
||||
cv2.circle(display_img, (x, y), 5, (0, 255, 0), 3)
|
||||
cv2.putText(display_img, template_type, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA)
|
||||
wrt_origin = (-ref_pos_abs[0], -ref_pos_abs[1])
|
||||
# print(f'"{template_type}": {wrt_origin}')
|
||||
print(f'"{template_type}": {wrt_origin}')
|
||||
# display_img = cv2.resize(display_img, None, fx=0.5, fy=0.5)
|
||||
# if round(time.time() - start) % 3 == 0:
|
||||
# cv2.imwrite("./info_screenshots/pather_" + time.strftime("%Y%m%d_%H%M%S") + ".png", display_img)
|
||||
@@ -749,6 +749,6 @@ if __name__ == "__main__":
|
||||
from char.hammerdin import Hammerdin
|
||||
pather = Pather()
|
||||
|
||||
display_all_nodes(pather, "A2_TOWN")
|
||||
display_all_nodes(pather, "ELDRITCH")
|
||||
|
||||
stop_detecting_window()
|
||||
@@ -34,8 +34,8 @@ class ShenkEld:
|
||||
|
||||
def battle(self, do_shenk: bool, do_pre_buff: bool, game_stats) -> Union[bool, tuple[Location, bool]]:
|
||||
# Eldritch
|
||||
game_stats.update_location("Eld" if Config().general['discord_status_condensed'] else "Eldritch")
|
||||
if not TemplateFinder().search_and_wait(["ELDRITCH_0", "ELDRITCH_START"], threshold=0.65, timeout=20).valid:
|
||||
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:
|
||||
return False
|
||||
if do_pre_buff:
|
||||
self._char.pre_buff()
|
||||
@@ -52,7 +52,7 @@ class ShenkEld:
|
||||
# Shenk
|
||||
if do_shenk:
|
||||
Logger.info("Run Shenk")
|
||||
game_stats.update_location("Shk" if Config().general['discord_status_condensed'] else "Shenk")
|
||||
game_stats.update_location("Shk")
|
||||
self._curr_loc = Location.A5_SHENK_START
|
||||
# No force move, otherwise we might get stuck at stairs!
|
||||
if not self._pather.traverse_nodes((Location.A5_SHENK_START, Location.A5_SHENK_SAFE_DIST), self._char):
|
||||
|
||||
@@ -9,7 +9,7 @@ from logger import Logger
|
||||
import time
|
||||
import os
|
||||
from config import Config
|
||||
from utils.misc import 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
|
||||
|
||||
template_finder_lock = threading.Lock()
|
||||
|
||||
@@ -244,29 +244,94 @@ class TemplateFinder:
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
import os
|
||||
from screen import start_detecting_window
|
||||
start_detecting_window()
|
||||
from screen import start_detecting_window, stop_detecting_window
|
||||
from utils.misc import wait
|
||||
from template_finder import TemplateFinder
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
print("Move to d2r window and press f11")
|
||||
start_detecting_window()
|
||||
wait(0.1)
|
||||
|
||||
print("\n== Hotkeys ==")
|
||||
print("F11: Start")
|
||||
print("F12: Exit")
|
||||
print("Down arrow: decrease template matching threshold")
|
||||
print("Up arrow: increase template matching threshold")
|
||||
print("Left arrow: decrease template index")
|
||||
print("Right arrow: increase template index")
|
||||
print("F9: toggle all vs. individual template(s)")
|
||||
print("F10: save visible template(s)")
|
||||
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or stop_detecting_window() or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
|
||||
search_templates = ["CORPSE", "CORPSE_BARB", "CORPSE_DRU", "CORPSE_NEC", "CORPSE_PAL", "CORPSE_SIN", "CORPSE_SORC", "CORPSE_ZON"]
|
||||
# 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"]
|
||||
|
||||
_current_template_idx = -1
|
||||
_last_stored_idx = 0
|
||||
_current_threshold = 0.5
|
||||
_visible_templates = []
|
||||
|
||||
def _save_visible_templates():
|
||||
if not os.path.exists("info_screenshots"):
|
||||
os.system("mkdir info_screenshots")
|
||||
for match in _visible_templates:
|
||||
cv2.imwrite(match['filename'], match['img'])
|
||||
Logger.info(f"{match['filename']} saved")
|
||||
|
||||
def _toggle_all_templates():
|
||||
global _current_template_idx
|
||||
_current_template_idx = -1 if _current_template_idx != -1 else _last_stored_idx
|
||||
if _current_template_idx == -1:
|
||||
Logger.info(f"Searching for templates: {_template_list}")
|
||||
else:
|
||||
Logger.info(f"Searching for template: {_template_list[_current_template_idx]}")
|
||||
|
||||
def _incr_template_idx(direction: int = 1):
|
||||
global _current_template_idx, _last_stored_idx
|
||||
if \
|
||||
(-1 < _current_template_idx < len(_template_list) - 1) or \
|
||||
(_current_template_idx == -1 and direction > 0) or \
|
||||
(_current_template_idx == len(_template_list) - 1 and direction < 0):
|
||||
_current_template_idx += direction
|
||||
_last_stored_idx = _current_template_idx
|
||||
if _current_template_idx == -1:
|
||||
Logger.info(f"Searching for templates: {_template_list}")
|
||||
else:
|
||||
Logger.info(f"Searching for template: {_template_list[_current_template_idx]}")
|
||||
|
||||
def _incr_threshold(direction: int = 1):
|
||||
global _current_threshold
|
||||
if (_current_threshold < 1 and direction > 0) or (_current_threshold > 0 and direction < 0):
|
||||
_current_threshold = round(_current_threshold + direction, 2)
|
||||
Logger.info(f"_current_threshold = {_current_threshold}")
|
||||
|
||||
keyboard.add_hotkey('down', lambda: _incr_threshold(-0.05))
|
||||
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('f10', lambda: _save_visible_templates())
|
||||
|
||||
while 1:
|
||||
# img = cv2.imread("")
|
||||
_visible_templates = []
|
||||
img = grab()
|
||||
display_img = img.copy()
|
||||
start = time.time()
|
||||
for key in search_templates:
|
||||
template_match = TemplateFinder().search(key, img, best_match=True, threshold=0.5, use_grayscale=True)
|
||||
if _current_template_idx < 0:
|
||||
templates = _template_list
|
||||
else:
|
||||
templates = [_template_list[_current_template_idx]]
|
||||
for key in templates:
|
||||
template_match = TemplateFinder().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)
|
||||
cv2.circle(display_img, template_match.center, 7, (255, 0, 0), thickness=5)
|
||||
cv2.rectangle(display_img, template_match.region[:2], (template_match.region[0] + template_match.region[2], template_match.region[1] + template_match.region[3]), (0, 0, 255), 1)
|
||||
print(f"Name: {template_match.name} Pos: {template_match.center}, Dist: {625-x, 360-y}, Score: {template_match.score}")
|
||||
|
||||
# print(time.time() - start)
|
||||
# display_img = cv2.resize(display_img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_NEAREST)
|
||||
match = {
|
||||
'filename': f"./info_screenshots/{key.lower()}.png",
|
||||
'img': cut_roi(img, template_match.region)
|
||||
}
|
||||
_visible_templates.append(match)
|
||||
cv2.imshow('test', display_img)
|
||||
key = cv2.waitKey(1)
|
||||
key = cv2.waitKey(3000)
|
||||
|
||||
@@ -140,7 +140,7 @@ class TownManager:
|
||||
Logger.error("buy_consumables: Error purchasing keys")
|
||||
# Sell items, if any
|
||||
if items:
|
||||
items = common.transfer_items(items, action = "sell")
|
||||
items = personal.transfer_items(items, action = "sell")
|
||||
common.close()
|
||||
return new_loc, items
|
||||
Logger.warning(f"Could not buy consumables in {curr_act}. Continue.")
|
||||
@@ -219,7 +219,7 @@ class TownManager:
|
||||
new_loc = self._acts[curr_act].open_trade_and_repair_menu(curr_loc)
|
||||
if not new_loc: return False, False
|
||||
if items:
|
||||
items = common.transfer_items(items, "sell")
|
||||
items = personal.transfer_items(items, "sell")
|
||||
vendor.repair()
|
||||
wait(0.1, 0.2)
|
||||
common.close()
|
||||
@@ -229,7 +229,7 @@ class TownManager:
|
||||
new_loc = self._acts[Location.A5_TOWN_START].open_trade_and_repair_menu(new_loc)
|
||||
if not new_loc: return False, False
|
||||
if items:
|
||||
items = common.transfer_items(items, "sell")
|
||||
items = personal.transfer_items(items, "sell")
|
||||
vendor.repair()
|
||||
wait(0.1, 0.2)
|
||||
common.close()
|
||||
|
||||
@@ -144,7 +144,8 @@ class Transmute:
|
||||
self.pick_from_inventory_at(x, y)
|
||||
|
||||
def select_tab_with_enough_space(self, s: Stash) -> None:
|
||||
tabs_priority = Config()._transmute_config["stash_destination"]
|
||||
tabs_priority = Config().configs["transmute"]["parser"]["stash_destination"]
|
||||
Config().configs["transmute"]["parser"]["stash_destination"]
|
||||
for tab in tabs_priority:
|
||||
if s.get_empty_on_tab(tab) > 0:
|
||||
common.select_tab(tab)
|
||||
@@ -152,7 +153,7 @@ class Transmute:
|
||||
|
||||
def put_back_all_gems(self, s: Stash) -> None:
|
||||
Logger.info(
|
||||
f'Putting back gems in the following stash tabs (by priority): {Config()._transmute_config["stash_destination"]}')
|
||||
f'Putting back gems in the following stash tabs (by priority): {Config().configs["transmute"]["parser"]["stash_destination"]}')
|
||||
perfect_gems = self.inspect_inventory_area(
|
||||
PERFECT_GEMS + FLAWLESS_GEMS)
|
||||
|
||||
@@ -162,7 +163,8 @@ class Transmute:
|
||||
self.pick_from_inventory_at(*perfect_gems.pop(gem))
|
||||
|
||||
def should_transmute(self) -> bool:
|
||||
every_x_game = Config()._transmute_config["transmute_every_x_game"]
|
||||
every_x_game = Config().configs["transmute"]["parser"]["transmute_every_x_game"]
|
||||
|
||||
if every_x_game is None or every_x_game == "" or int(every_x_game) <= 0:
|
||||
return False
|
||||
return self._game_stats._game_counter - self._last_game >= int(every_x_game)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import time
|
||||
import keyboard
|
||||
from config import Config
|
||||
from utils.misc import wait
|
||||
from logger import Logger
|
||||
from ui import error_screens
|
||||
from ui_manager import detect_screen_object, is_visible, select_screen_object_match, ScreenObjects
|
||||
|
||||
|
||||
def _play_active(match) -> bool:
|
||||
return match.name == "PLAY_BTN"
|
||||
|
||||
def start_game() -> bool:
|
||||
"""
|
||||
Starting a game. Will wait and retry on server connection issue.
|
||||
@@ -12,12 +17,15 @@ def start_game() -> bool:
|
||||
"""
|
||||
Logger.debug("Wait for Play button")
|
||||
start = time.time()
|
||||
difficulty=Config().general["difficulty"].lower()
|
||||
difficulty_key="r" if difficulty == "normal" else "n" if difficulty == "nightmare" else "h"
|
||||
while True:
|
||||
if (m := detect_screen_object(ScreenObjects.PlayBtn)).valid:
|
||||
if play_active(m):
|
||||
if _play_active(m):
|
||||
# found active play button
|
||||
Logger.debug(f"Found Play Btn")
|
||||
Logger.debug(f"Found Play Btn, select and press key: {difficulty_key}")
|
||||
select_screen_object_match(m)
|
||||
keyboard.press(difficulty_key)
|
||||
break
|
||||
# else found inactive play button, continue loop
|
||||
else:
|
||||
@@ -28,33 +36,23 @@ def start_game() -> bool:
|
||||
if time.time() - start > 90:
|
||||
Logger.error("start_game: Active play button never appeared")
|
||||
return False
|
||||
difficulty=Config().general["difficulty"].upper()
|
||||
# TODO: need to revise logic here
|
||||
if difficulty == "NORMAL": Difficulty = ScreenObjects.Normal
|
||||
elif difficulty == "NIGHTMARE": Difficulty = ScreenObjects.Nightmare
|
||||
elif difficulty == "HELL": Difficulty = ScreenObjects.Hell
|
||||
else: Logger.error(f"Invalid difficulty: {Config().general['difficulty']}")
|
||||
start = time.time()
|
||||
while True:
|
||||
#look for difficulty select
|
||||
if (m := detect_screen_object(Difficulty)).valid:
|
||||
select_screen_object_match(m)
|
||||
break
|
||||
#check for loading screen
|
||||
if is_visible(ScreenObjects.Loading):
|
||||
Logger.debug("Found loading screen / creating game screen rather than difficulty select, normal difficulty")
|
||||
Logger.debug("Found loading screen / creating game")
|
||||
keyboard.release(difficulty_key)
|
||||
break
|
||||
else:
|
||||
wait(1,2)
|
||||
# check for server issue
|
||||
if is_visible(ScreenObjects.ServerError):
|
||||
error_screens.handle_error()
|
||||
keyboard.release(difficulty_key)
|
||||
return start_game()
|
||||
|
||||
if time.time() - start > 15:
|
||||
Logger.error(f"Could not find {difficulty}_BTN or LOADING, start over")
|
||||
keyboard.release(difficulty_key)
|
||||
return start_game()
|
||||
return True
|
||||
|
||||
def play_active(match) -> bool:
|
||||
return match.name == "PLAY_BTN"
|
||||
@@ -14,6 +14,8 @@ def get_experience():
|
||||
wait(0.05)
|
||||
# crop roi
|
||||
img = grab()
|
||||
|
||||
mouse.move(x_m, y_m-50, randomize = (8,1))
|
||||
crop = cut_roi(img, Config().ui_roi["xp_bar_text"])
|
||||
ocr_result = Ocr().image_to_text(
|
||||
images = crop,
|
||||
@@ -27,12 +29,34 @@ def get_experience():
|
||||
fix_regexps = False,
|
||||
check_known_errors = False,
|
||||
check_wordlist = False,
|
||||
word_match_threshold = 0.9
|
||||
word_match_threshold = 0.5
|
||||
)[0]
|
||||
|
||||
split_text = ocr_result.text.split(' ')
|
||||
current_exp = int(split_text[1].replace(',', ''))
|
||||
required_exp = int(split_text[3].replace(',', ''))
|
||||
return current_exp, required_exp, round((current_exp / required_exp) * 100, 2)
|
||||
|
||||
try:
|
||||
split_text = split_text[split_text.index("EXPERIENCE:"):]
|
||||
current_exp = int(split_text[1].replace(',', '').replace('.', ''))
|
||||
required_exp = int(split_text[3].replace(',', '').replace('.', ''))
|
||||
return current_exp, required_exp
|
||||
except Exception as e:
|
||||
Logger.error(f"EXP OCR Error: {split_text}")
|
||||
Logger.error(f"EXP OCR Error: {str(e)}")
|
||||
return 0,0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
import os
|
||||
|
||||
from screen import start_detecting_window
|
||||
start_detecting_window()
|
||||
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
print("Go to D2R window and press f11 to start game")
|
||||
keyboard.wait("f11")
|
||||
|
||||
exp = get_experience()
|
||||
Logger.debug(f"EXP curr: {exp[0]}")
|
||||
Logger.debug(f"EXP req: {exp[1]}")
|
||||
Logger.debug(f"EXP per: {exp[2]}")
|
||||
|
||||
@@ -87,7 +87,7 @@ def get_skill_charges(ocr, img: np.ndarray = None):
|
||||
fix_regexps = False,
|
||||
check_known_errors = False,
|
||||
check_wordlist = False,
|
||||
word_match_threshold = 0.9
|
||||
word_match_threshold = 0.5
|
||||
)[0]
|
||||
try:
|
||||
return int(ocr_result.text)
|
||||
|
||||
@@ -81,7 +81,7 @@ def return_to_play() -> bool:
|
||||
substrings = ["NPC", "Panel", "SaveAndExit"]
|
||||
img=grab()
|
||||
start=time.time()
|
||||
while (elapsed := (time.time() - start) < 5):
|
||||
while (elapsed := (time.time() - start) < 8):
|
||||
need_escape = False
|
||||
match = detect_screen_object(ScreenObjects.InGame, img)
|
||||
if match.valid and "DARK" in match.name:
|
||||
@@ -92,7 +92,7 @@ def return_to_play() -> bool:
|
||||
break
|
||||
if need_escape:
|
||||
keyboard.send("esc")
|
||||
wait(1)
|
||||
wait(1.7)
|
||||
img=grab()
|
||||
else:
|
||||
break
|
||||
@@ -113,9 +113,4 @@ if __name__ == "__main__":
|
||||
from config import Config
|
||||
from template_finder import TemplateFinder
|
||||
|
||||
while True:
|
||||
img = grab()
|
||||
if (result := TemplateFinder().search(["YOU_HAVE_DIED", "NOT_ENOUGH_GOLD"], img, color_match=Config().colors["red"], use_grayscale=True, best_match=True)).valid:
|
||||
print(f"match: {result.score}")
|
||||
else:
|
||||
print("no match")
|
||||
return_to_play()
|
||||
@@ -2,9 +2,10 @@ import keyboard
|
||||
import os
|
||||
import numpy as np
|
||||
import time
|
||||
import cv2
|
||||
from typing import Union, TypeVar, Callable
|
||||
from utils.custom_mouse import mouse
|
||||
from utils.misc import wait
|
||||
from utils.misc import wait, cut_roi, image_is_equal
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
from screen import grab, convert_screen_to_monitor, convert_abs_to_monitor
|
||||
@@ -80,24 +81,6 @@ class ScreenObjects:
|
||||
threshold=0.9,
|
||||
use_grayscale=True
|
||||
)
|
||||
Normal=ScreenObject(
|
||||
ref=["NORMAL_BTN"],
|
||||
roi="difficulty_select",
|
||||
threshold=0.9,
|
||||
use_grayscale=True
|
||||
)
|
||||
Nightmare=ScreenObject(
|
||||
ref=["NIGHTMARE_BTN"],
|
||||
roi="difficulty_select",
|
||||
threshold=0.9,
|
||||
use_grayscale=True
|
||||
)
|
||||
Hell=ScreenObject(
|
||||
ref=["HELL_BTN"],
|
||||
roi="difficulty_select",
|
||||
threshold=0.9,
|
||||
use_grayscale=True
|
||||
)
|
||||
CubeInventory=ScreenObject(
|
||||
ref=["HORADRIC_CUBE"],
|
||||
roi="left_inventory",
|
||||
@@ -156,7 +139,8 @@ class ScreenObjects:
|
||||
)
|
||||
GoldBtnInventory=ScreenObject(
|
||||
ref="INVENTORY_GOLD_BTN",
|
||||
roi="gold_btn"
|
||||
roi="gold_btn",
|
||||
use_grayscale=True
|
||||
)
|
||||
GoldBtnStash=ScreenObject(
|
||||
ref="INVENTORY_GOLD_BTN",
|
||||
@@ -309,6 +293,12 @@ def wait_until_hidden(screen_object: ScreenObject, timeout: float = 3) -> bool:
|
||||
Logger.debug(f"{screen_object.ref} still found after {timeout} seconds")
|
||||
return hidden
|
||||
|
||||
def wait_for_update(img: np.ndarray, roi: list[int] = None, timeout: float = 3) -> bool:
|
||||
roi = roi if roi is not None else [0, 0, img.shape[0]-1, img.shape[1] -1]
|
||||
if not (change := wait_until(lambda: cut_roi(grab(), roi), lambda res: not image_is_equal(cut_roi(img, roi), res), timeout)[1]):
|
||||
Logger.debug(f"ROI: '{roi}' unchanged after {timeout} seconds")
|
||||
return change
|
||||
|
||||
def wait_until(func: Callable[[], T], is_success: Callable[[T], bool], timeout = None) -> Union[T, None]:
|
||||
start = time.time()
|
||||
while (time.time() - start) < timeout:
|
||||
@@ -340,12 +330,14 @@ def center_mouse(delay_factor: list = None):
|
||||
# Testing: Move to whatever ui to test and run
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from screen import start_detecting_window
|
||||
from screen import start_detecting_window, grab, stop_detecting_window
|
||||
start_detecting_window()
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
print("Go to D2R window and press f11 to start game")
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or stop_detecting_window() or os._exit(1))
|
||||
print("Go to D2R window and press f11 to start")
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
while 1:
|
||||
print(list_visible_objects())
|
||||
time.sleep(1)
|
||||
|
||||
print(wait_for_update(grab(), Config().ui_roi["right_inventory"], timeout=5))
|
||||
# while 1:
|
||||
# print(list_visible_objects())
|
||||
# time.sleep(1)
|
||||
|
||||
@@ -4,7 +4,7 @@ import shutil
|
||||
|
||||
from config import Config
|
||||
from mss import mss
|
||||
from utils.misc import close_down_d2
|
||||
from utils.misc import close_down_bnet_launcher, close_down_d2
|
||||
|
||||
|
||||
def get_d2r_folder() -> str:
|
||||
@@ -25,27 +25,68 @@ def get_d2r_folder() -> str:
|
||||
assert(f"Could not find D2R Saved Games at {d2_saved_games}")
|
||||
return d2_saved_games
|
||||
|
||||
|
||||
def backup_settings():
|
||||
d2_saved_games = get_d2r_folder()
|
||||
if os.path.exists(d2_saved_games + "\\Settings_backup.json"):
|
||||
r = input("Backup file already exist, are you sure you want to overwrite it? [y] to confirm")
|
||||
if r == 'y':
|
||||
shutil.copyfile(d2_saved_games + "\\Settings.json", d2_saved_games + "\\Settings_backup.json")
|
||||
print("Settings backed up successfully.")
|
||||
else:
|
||||
shutil.copyfile(d2_saved_games + "\\Settings.json", d2_saved_games + "\\Settings_backup.json")
|
||||
print("Settings backed up successfully.")
|
||||
backup_file = f"{d2_saved_games}/Settings_backup.json"
|
||||
current_file = f"{d2_saved_games}/Settings.json"
|
||||
if os.path.exists(backup_file):
|
||||
r = input("D2R settings backup already exists, are you sure you want to overwrite it? [y] to confirm")
|
||||
if not r == 'y':
|
||||
return
|
||||
shutil.copyfile(current_file, backup_file)
|
||||
print("D2R settings backed up successfully.")
|
||||
|
||||
backup_file = f"{d2_saved_games}/launch_options_backup.txt"
|
||||
current_file = f"{os.getenv('APPDATA')}/Battle.net/Battle.net.config"
|
||||
if os.path.exists(backup_file):
|
||||
r = input("D2R launch options backup already exists, are you sure you want to overwrite it? [y] to confirm")
|
||||
if not r == 'y':
|
||||
return
|
||||
f = open(current_file)
|
||||
curr_settings = json.load(f)
|
||||
launch_options = curr_settings["Games"]["osi"]["AdditionalLaunchArguments"]
|
||||
with open(backup_file, 'w') as f:
|
||||
f.write(launch_options)
|
||||
print("D2R launch options backed up successfully.")
|
||||
|
||||
def restore_settings_from_backup():
|
||||
d2_saved_games = get_d2r_folder()
|
||||
if os.path.exists(d2_saved_games + "\\Settings_backup.json"):
|
||||
close_down_d2()
|
||||
shutil.copyfile(d2_saved_games + "\\Settings_backup.json", d2_saved_games + "\\Settings.json")
|
||||
print("Settings restored successfully.")
|
||||
else:
|
||||
print("No backup was found, couldn't restore settings.")
|
||||
backup_file = f"{d2_saved_games}/Settings_backup.json"
|
||||
current_file = f"{d2_saved_games}/Settings.json"
|
||||
if not os.path.exists(backup_file):
|
||||
print("No D2R settings backup file was found, couldn't restore.")
|
||||
return
|
||||
close_down_d2()
|
||||
shutil.copyfile(backup_file, current_file)
|
||||
print("D2R settings restored successfully.")
|
||||
|
||||
backup_file = f"{d2_saved_games}/launch_options_backup.txt"
|
||||
current_file = f"{os.getenv('APPDATA')}/Battle.net/Battle.net.config"
|
||||
if not os.path.exists(backup_file):
|
||||
print("No D2R launch options backup file was found, couldn't restore.")
|
||||
return
|
||||
with open(backup_file, 'r') as f:
|
||||
launch_options = f.read().strip()
|
||||
set_launch_settings(launch_options)
|
||||
print("D2R launch options restored successfully.")
|
||||
|
||||
def set_launch_settings(launch_options):
|
||||
close_down_bnet_launcher()
|
||||
f = open(f"{os.getenv('APPDATA')}/Battle.net/Battle.net.config")
|
||||
curr_settings = json.load(f)
|
||||
curr_settings["Games"]["osi"]["AdditionalLaunchArguments"] = launch_options
|
||||
with open(f"{os.getenv('APPDATA')}/Battle.net/Battle.net.config", 'w') as outfile:
|
||||
json.dump(curr_settings, outfile, indent=4)
|
||||
|
||||
def copy_mod_files():
|
||||
new_path = os.path.join(Config().general['d2r_path'], "mods\\botty")
|
||||
if not os.path.exists(new_path):
|
||||
os.makedirs(new_path)
|
||||
try:
|
||||
shutil.rmtree(new_path)
|
||||
shutil.copytree("assets/mods/botty", new_path)
|
||||
except OSError as error:
|
||||
print(error)
|
||||
|
||||
def adjust_settings():
|
||||
close_down_d2()
|
||||
@@ -57,6 +98,9 @@ def adjust_settings():
|
||||
os._exit(1)
|
||||
d2_saved_games = get_d2r_folder()
|
||||
# adjust settings
|
||||
if Config().advanced_options["launch_options"]:
|
||||
set_launch_settings(Config().advanced_options["launch_options"])
|
||||
copy_mod_files()
|
||||
f = open(d2_saved_games + "\\Settings.json")
|
||||
curr_settings = json.load(f)
|
||||
f = open("assets/d2r_settings.json")
|
||||
@@ -86,4 +130,4 @@ def check_settings() -> dict:
|
||||
return diff_settings
|
||||
|
||||
if __name__ == "__main__":
|
||||
adjust_settings()
|
||||
copy_mod_files()
|
||||
|
||||
@@ -7,7 +7,11 @@ import random
|
||||
import math
|
||||
import time
|
||||
from typing import Union, Tuple
|
||||
|
||||
import screen
|
||||
from config import Config
|
||||
from utils.misc import is_in_roi
|
||||
from logger import Logger
|
||||
from template_finder import TemplateFinder
|
||||
|
||||
def isNumeric(val):
|
||||
return isinstance(val, (float, int, np.int32, np.int64, np.float32, np.float64))
|
||||
@@ -254,13 +258,34 @@ class mouse:
|
||||
for point in human_curve.points:
|
||||
_mouse.move(point[0], point[1], duration=delta)
|
||||
|
||||
@staticmethod
|
||||
def _is_clicking_safe():
|
||||
# Because of reports that botty lost equiped items, let's check if the inventory is open, and if it is, restrict the mouse move
|
||||
mouse_pos = screen.convert_monitor_to_screen(_mouse.get_position())
|
||||
is_inventory_open = TemplateFinder().search(
|
||||
"INVENTORY_GOLD_BTN",
|
||||
screen.grab(),
|
||||
threshold=0.8,
|
||||
roi=Config().ui_roi["gold_btn"],
|
||||
use_grayscale=True
|
||||
).valid
|
||||
if is_inventory_open:
|
||||
is_in_equipped_area = is_in_roi(Config().ui_roi["equipped_inventory_area"], mouse_pos)
|
||||
is_in_restricted_inventory_area = is_in_roi(Config().ui_roi["restricted_inventory_area"], mouse_pos)
|
||||
if is_in_restricted_inventory_area or is_in_equipped_area:
|
||||
Logger.error("Mouse wants to click in equipped area. Cancel action.")
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def click(button):
|
||||
_mouse.click(button)
|
||||
if button != "left" or mouse._is_clicking_safe():
|
||||
_mouse.click(button)
|
||||
|
||||
@staticmethod
|
||||
def press(button):
|
||||
_mouse.press(button)
|
||||
if button != "left" or mouse._is_clicking_safe():
|
||||
_mouse.press(button)
|
||||
|
||||
@staticmethod
|
||||
def release(button):
|
||||
@@ -274,9 +299,21 @@ class mouse:
|
||||
def wheel(delta):
|
||||
_mouse.wheel(delta)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mouse.move(100, 100)
|
||||
time.sleep(2)
|
||||
mouse.move(200, 200)
|
||||
time.sleep(2)
|
||||
mouse.move(1800, 800)
|
||||
import os
|
||||
import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
screen.find_and_set_window_position()
|
||||
move_to_ok = screen.convert_screen_to_monitor((400, 420))
|
||||
move_to_bad_equiped = screen.convert_screen_to_monitor((900, 170))
|
||||
move_to_bad_inventory = screen.convert_screen_to_monitor((1200, 400))
|
||||
mouse.move(*move_to_ok)
|
||||
mouse.click("left")
|
||||
time.sleep(1)
|
||||
mouse.move(*move_to_bad_equiped)
|
||||
mouse.click("left")
|
||||
time.sleep(1)
|
||||
mouse.move(*move_to_bad_inventory)
|
||||
mouse.click("left")
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
levels = {}
|
||||
levels[1] = levels[0] = { "lvl":1, "exp":0, "xp_to_next":500 }
|
||||
levels[2] = levels[500] = { "lvl":2, "exp":500, "xp_to_next":1000 }
|
||||
levels[3] = levels[1500] = { "lvl":3, "exp":1500, "xp_to_next":2250 }
|
||||
levels[4] = levels[3750] = { "lvl":4, "exp":3750, "xp_to_next":4125 }
|
||||
levels[5] = levels[7875] = { "lvl":5, "exp":7875, "xp_to_next":6300 }
|
||||
levels[6] = levels[14175] = { "lvl":6, "exp":14175, "xp_to_next":8505 }
|
||||
levels[7] = levels[22680] = { "lvl":7, "exp":22680, "xp_to_next":10206 }
|
||||
levels[8] = levels[32886] = { "lvl":8, "exp":32886, "xp_to_next":11510 }
|
||||
levels[9] = levels[44396] = { "lvl":9, "exp":44396, "xp_to_next":13319 }
|
||||
levels[10] = levels[57715] = { "lvl":10, "exp":57715, "xp_to_next":14429 }
|
||||
levels[11] = levels[72144] = { "lvl":11, "exp":72144, "xp_to_next":18036 }
|
||||
levels[12] = levels[90180] = { "lvl":12, "exp":90180, "xp_to_next":22545 }
|
||||
levels[13] = levels[112725] = { "lvl":13, "exp":112725, "xp_to_next":28181 }
|
||||
levels[14] = levels[140906] = { "lvl":14, "exp":140906, "xp_to_next":35226 }
|
||||
levels[15] = levels[176132] = { "lvl":15, "exp":176132, "xp_to_next":44033 }
|
||||
levels[16] = levels[220165] = { "lvl":16, "exp":220165, "xp_to_next":55042 }
|
||||
levels[17] = levels[275207] = { "lvl":17, "exp":275207, "xp_to_next":68801 }
|
||||
levels[18] = levels[344008] = { "lvl":18, "exp":344008, "xp_to_next":86002 }
|
||||
levels[19] = levels[430010] = { "lvl":19, "exp":430010, "xp_to_next":107503 }
|
||||
levels[20] = levels[537513] = { "lvl":20, "exp":537513, "xp_to_next":134378 }
|
||||
levels[21] = levels[671891] = { "lvl":21, "exp":671891, "xp_to_next":167973 }
|
||||
levels[22] = levels[839864] = { "lvl":22, "exp":839864, "xp_to_next":209966 }
|
||||
levels[23] = levels[1049830] = { "lvl":23, "exp":1049830, "xp_to_next":262457 }
|
||||
levels[24] = levels[1312287] = { "lvl":24, "exp":1312287, "xp_to_next":328072 }
|
||||
levels[25] = levels[1640359] = { "lvl":25, "exp":1640359, "xp_to_next":410090 }
|
||||
levels[26] = levels[2050449] = { "lvl":26, "exp":2050449, "xp_to_next":512612 }
|
||||
levels[27] = levels[2563061] = { "lvl":27, "exp":2563061, "xp_to_next":640765 }
|
||||
levels[28] = levels[3203826] = { "lvl":28, "exp":3203826, "xp_to_next":698434 }
|
||||
levels[29] = levels[3902260] = { "lvl":29, "exp":3902260, "xp_to_next":761293 }
|
||||
levels[30] = levels[4663553] = { "lvl":30, "exp":4663553, "xp_to_next":829810 }
|
||||
levels[31] = levels[5493363] = { "lvl":31, "exp":5493363, "xp_to_next":904492 }
|
||||
levels[32] = levels[6397855] = { "lvl":32, "exp":6397855, "xp_to_next":985897 }
|
||||
levels[33] = levels[7383752] = { "lvl":33, "exp":7383752, "xp_to_next":1074627 }
|
||||
levels[34] = levels[8458379] = { "lvl":34, "exp":8458379, "xp_to_next":1171344 }
|
||||
levels[35] = levels[9629723] = { "lvl":35, "exp":9629723, "xp_to_next":1276765 }
|
||||
levels[36] = levels[10906488] = { "lvl":36, "exp":10906488, "xp_to_next":1391674 }
|
||||
levels[37] = levels[12298162] = { "lvl":37, "exp":12298162, "xp_to_next":1516924 }
|
||||
levels[38] = levels[13815086] = { "lvl":38, "exp":13815086, "xp_to_next":1653448 }
|
||||
levels[39] = levels[15468534] = { "lvl":39, "exp":15468534, "xp_to_next":1802257 }
|
||||
levels[40] = levels[17270791] = { "lvl":40, "exp":17270791, "xp_to_next":1964461 }
|
||||
levels[41] = levels[19235252] = { "lvl":41, "exp":19235252, "xp_to_next":2141263 }
|
||||
levels[42] = levels[21376515] = { "lvl":42, "exp":21376515, "xp_to_next":2333976 }
|
||||
levels[43] = levels[23710491] = { "lvl":43, "exp":23710491, "xp_to_next":2544034 }
|
||||
levels[44] = levels[26254525] = { "lvl":44, "exp":26254525, "xp_to_next":2772997 }
|
||||
levels[45] = levels[29027522] = { "lvl":45, "exp":29027522, "xp_to_next":3022566 }
|
||||
levels[46] = levels[32050088] = { "lvl":46, "exp":32050088, "xp_to_next":3294598 }
|
||||
levels[47] = levels[35344686] = { "lvl":47, "exp":35344686, "xp_to_next":3591112 }
|
||||
levels[48] = levels[38935798] = { "lvl":48, "exp":38935798, "xp_to_next":3914311 }
|
||||
levels[49] = levels[42850109] = { "lvl":49, "exp":42850109, "xp_to_next":4266600 }
|
||||
levels[50] = levels[47116709] = { "lvl":50, "exp":47116709, "xp_to_next":4650593 }
|
||||
levels[51] = levels[51767302] = { "lvl":51, "exp":51767302, "xp_to_next":5069147 }
|
||||
levels[52] = levels[56836449] = { "lvl":52, "exp":56836449, "xp_to_next":5525370 }
|
||||
levels[53] = levels[62361819] = { "lvl":53, "exp":62361819, "xp_to_next":6022654 }
|
||||
levels[54] = levels[68384473] = { "lvl":54, "exp":68384473, "xp_to_next":6564692 }
|
||||
levels[55] = levels[74949165] = { "lvl":55, "exp":74949165, "xp_to_next":7155515 }
|
||||
levels[56] = levels[82104680] = { "lvl":56, "exp":82104680, "xp_to_next":7799511 }
|
||||
levels[57] = levels[89904191] = { "lvl":57, "exp":89904191, "xp_to_next":8501467 }
|
||||
levels[58] = levels[98405658] = { "lvl":58, "exp":98405658, "xp_to_next":9266598 }
|
||||
levels[59] = levels[107672256] = { "lvl":59, "exp":107672256, "xp_to_next":10100593 }
|
||||
levels[60] = levels[117772849] = { "lvl":60, "exp":117772849, "xp_to_next":11009646 }
|
||||
levels[61] = levels[128782495] = { "lvl":61, "exp":128782495, "xp_to_next":12000515 }
|
||||
levels[62] = levels[140783010] = { "lvl":62, "exp":140783010, "xp_to_next":13080560 }
|
||||
levels[63] = levels[153863570] = { "lvl":63, "exp":153863570, "xp_to_next":14257811 }
|
||||
levels[64] = levels[168121381] = { "lvl":64, "exp":168121381, "xp_to_next":15541015 }
|
||||
levels[65] = levels[183662396] = { "lvl":65, "exp":183662396, "xp_to_next":16939705 }
|
||||
levels[66] = levels[200602101] = { "lvl":66, "exp":200602101, "xp_to_next":18464279 }
|
||||
levels[67] = levels[219066380] = { "lvl":67, "exp":219066380, "xp_to_next":20126064 }
|
||||
levels[68] = levels[239192444] = { "lvl":68, "exp":239192444, "xp_to_next":21937409 }
|
||||
levels[69] = levels[261129853] = { "lvl":69, "exp":261129853, "xp_to_next":23911777 }
|
||||
levels[70] = levels[285041630] = { "lvl":70, "exp":285041630, "xp_to_next":26063836 }
|
||||
levels[71] = levels[311105466] = { "lvl":71, "exp":311105466, "xp_to_next":28409582 }
|
||||
levels[72] = levels[339515048] = { "lvl":72, "exp":339515048, "xp_to_next":30966444 }
|
||||
levels[73] = levels[370481492] = { "lvl":73, "exp":370481492, "xp_to_next":33753424 }
|
||||
levels[74] = levels[404234916] = { "lvl":74, "exp":404234916, "xp_to_next":36791232 }
|
||||
levels[75] = levels[441026148] = { "lvl":75, "exp":441026148, "xp_to_next":40102443 }
|
||||
levels[76] = levels[481128591] = { "lvl":76, "exp":481128591, "xp_to_next":43711663 }
|
||||
levels[77] = levels[524840254] = { "lvl":77, "exp":524840254, "xp_to_next":47645713 }
|
||||
levels[78] = levels[572485967] = { "lvl":78, "exp":572485967, "xp_to_next":51933826 }
|
||||
levels[79] = levels[624419793] = { "lvl":79, "exp":624419793, "xp_to_next":56607872 }
|
||||
levels[80] = levels[681027665] = { "lvl":80, "exp":681027665, "xp_to_next":61702579 }
|
||||
levels[81] = levels[742730244] = { "lvl":81, "exp":742730244, "xp_to_next":67255812 }
|
||||
levels[82] = levels[809986056] = { "lvl":82, "exp":809986056, "xp_to_next":73308835 }
|
||||
levels[83] = levels[883294891] = { "lvl":83, "exp":883294891, "xp_to_next":79906630 }
|
||||
levels[84] = levels[963201521] = { "lvl":84, "exp":963201521, "xp_to_next":87098226 }
|
||||
levels[85] = levels[1050299747] = { "lvl":85, "exp":1050299747, "xp_to_next":94937067 }
|
||||
levels[86] = levels[1145236814] = { "lvl":86, "exp":1145236814, "xp_to_next":103481403 }
|
||||
levels[87] = levels[1248718217] = { "lvl":87, "exp":1248718217, "xp_to_next":112794729 }
|
||||
levels[88] = levels[1361512946] = { "lvl":88, "exp":1361512946, "xp_to_next":122946255 }
|
||||
levels[89] = levels[1484459201] = { "lvl":89, "exp":1484459201, "xp_to_next":134011418 }
|
||||
levels[90] = levels[1618470619] = { "lvl":90, "exp":1618470619, "xp_to_next":146072446 }
|
||||
levels[91] = levels[1764543065] = { "lvl":91, "exp":1764543065, "xp_to_next":159218965 }
|
||||
levels[92] = levels[1923762030] = { "lvl":92, "exp":1923762030, "xp_to_next":173548673 }
|
||||
levels[93] = levels[2097310703] = { "lvl":93, "exp":2097310703, "xp_to_next":189168053 }
|
||||
levels[94] = levels[2286478756] = { "lvl":94, "exp":2286478756, "xp_to_next":206193177 }
|
||||
levels[95] = levels[2492671933] = { "lvl":95, "exp":2492671933, "xp_to_next":224750564 }
|
||||
levels[96] = levels[2717422497] = { "lvl":96, "exp":2717422497, "xp_to_next":244978115 }
|
||||
levels[97] = levels[2962400612] = { "lvl":97, "exp":2962400612, "xp_to_next":267026144 }
|
||||
levels[98] = levels[3229426756] = { "lvl":98, "exp":3229426756, "xp_to_next":291058498 }
|
||||
levels[99] = levels[3520485254] = { "lvl":99, "exp":3520485254, "xp_to_next":None }
|
||||
|
||||
|
||||
def get_level(index: int):
|
||||
try:
|
||||
return levels[index]
|
||||
except:
|
||||
return { "lvl":0, "exp":0, "xp_to_next":0 }
|
||||
@@ -25,6 +25,9 @@ import psutil
|
||||
def close_down_d2():
|
||||
subprocess.call(["taskkill","/F","/IM","D2R.exe"], stderr=subprocess.DEVNULL)
|
||||
|
||||
def close_down_bnet_launcher():
|
||||
subprocess.call(["taskkill","/F","/IM","Battle.net.exe"], stderr=subprocess.DEVNULL)
|
||||
|
||||
@dataclass
|
||||
class WindowSpec:
|
||||
title_regex: 'Union[str, None]' = None
|
||||
@@ -211,6 +214,12 @@ def rotate_vec(vec: np.ndarray, deg: float) -> np.ndarray:
|
||||
def unit_vector(vec: np.ndarray) -> np.ndarray:
|
||||
return vec / dist(vec, (0, 0))
|
||||
|
||||
def image_is_equal(img1: np.ndarray, img2: np.ndarray) -> bool:
|
||||
shape_equal = img1.shape == img2.shape
|
||||
if not shape_equal:
|
||||
Logger.debug("image_is_equal: Image shape is not equal")
|
||||
return False
|
||||
return not(np.bitwise_xor(img1, img2).any())
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# print(find_d2r_window())
|
||||
|
||||
@@ -19,18 +19,15 @@ def process_exists(process_name):
|
||||
|
||||
def kill_game():
|
||||
while process_exists("D2R.exe"):
|
||||
os.system("taskkill /f /im BlizzardError.exe")
|
||||
os.system("taskkill /f /im D2R.exe")
|
||||
wait(1.0, 1.5)
|
||||
|
||||
def restart_game(d2_path = None):
|
||||
if not d2_path:
|
||||
path = "C:\Program Files (x86)\Diablo II Resurrected\D2R.exe"
|
||||
else:
|
||||
path = d2_path
|
||||
def restart_game(d2_path):
|
||||
kill_game()
|
||||
wait(1.0, 1.5)
|
||||
# This method should function similar to opening the exe via double-click
|
||||
os.startfile(path)
|
||||
os.startfile(f"{d2_path}/D2R.exe")
|
||||
wait(4.4, 5.5)
|
||||
for i in range(20):
|
||||
keyboard.send("space")
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = '0.7.2'
|
||||
__version__ = '0.7.3'
|
||||
|
||||