Resolve merge conflicts, keeping local Win10 specific changes

This commit is contained in:
FiskenPoul
2026-06-05 21:47:11 +02:00
parent 7dcbb5284b
commit 95f520e968
22 changed files with 800 additions and 738 deletions

View File

@@ -15,7 +15,7 @@
"VSync": 1,
"Framerate Cap": 60,
"Framerate Target": 0,
"Window Mode": 0,
"Window Mode": 1,
"Graphic Presets": 4,
"Texture Quality": 4,
"Texture Anisotropy": 0,

File diff suppressed because it is too large Load Diff

View File

@@ -100,8 +100,7 @@ mana_slice=961,610,7,101
cut_skill_bar=0,0,1280,653
reduce_to_center=120,60,1040,540
search_npcs=120,0,1040,620
merc_icon=5,5,60,60
merc_panel=0,0,450,100
merc_icon=10,9,56,56
loading_left_black=0,0,350,720
death=444,198,397,71
tp_search=353,120,547,400

View File

@@ -8,19 +8,9 @@
; bnet_pass=${BOTTY_BNET_PASS}
; char_name=${BOTTY_CHAR_NAME}
; difficulty: game difficulty to create ("normal", "nightmare", "hell")
;
; Hammerdin difficulty guide (CTA build with Conviction aura):
; NORMAL - Very easy. 1-2h Hammer kills most packs. No CTA needed.
; NIGHTMARE - Recommended starting point. Bosses hit ~100-200 dmg.
; CTA + Conviction drops resists. Need decent armor (ED/HR).
; Gear targets: ~200 AR, ~15 ED, ~30% HR, ~50% FCR on hammer.
; HELL - Bosses hit 400-800+ dmg per swing. Conviction is mandatory.
; Gear targets: ~350+ AR, ~20+ ED, ~50%+ HR, ~60%+ FCR,
; ~150% IAS on weapon. Full Rejuv belt recommended.
; If you chicken/die repeatedly, drop to Nightmare first.
difficulty=nightmare
difficulty=hell
; name: bot profile name used in logs/messages and mod launch option replacement
name=fistman
name=fiskenersej
; randomize_runs: 0 = run in listed order, 1 = shuffle run order
randomize_runs=0
; target_tz: target Terror Zone id (leave as default unless you know the mapping)
@@ -32,7 +22,7 @@ level_max_steps=20
; Set to 1 to enable auto-login and auto-launch of D2R on startup.
; Credentials below are ONLY used when auto_login=1.
auto_login=1
auto_login=0
bnet_name=${BOTTY_BNET_NAME}
bnet_pass=${BOTTY_BNET_PASS}
@@ -42,13 +32,13 @@ char_name=${BOTTY_CHAR_NAME}
; messaging
; custom_loot_message_hook: optional separate webhook for loot notifications
custom_loot_message_hook=${BOTTY_CUSTOM_LOOT_MESSAGE_HOOK}
custom_loot_message_hook=https://discord.com/api/webhooks/1510992232939913297/20rbnGmpTWMKBBL6JN0JaE897jLGOmHl6zclJ1AO82o2dtD67YHfAKMD-e4G8DWvUOXV
; custom_message_hook: main webhook for status/death/chicken messages
custom_message_hook=${BOTTY_CUSTOM_MESSAGE_HOOK}
custom_message_hook=https://discord.com/api/webhooks/1510992232939913297/20rbnGmpTWMKBBL6JN0JaE897jLGOmHl6zclJ1AO82o2dtD67YHfAKMD-e4G8DWvUOXV
; discord_log_chicken: 1 = send chicken/death style notifications
discord_log_chicken=1
; discord_status_runs: send periodic status every X completed runs (blank/0 disables)
discord_status_runs=10
discord_status_runs=100
; discord_status_count: legacy fallback, send periodic status every X games (blank/0 disables)
discord_status_count=20
; message_api_type: "" disables messaging, "discord" or "generic_api"
@@ -66,14 +56,10 @@ d2r_path=C:\Program Files (x86)\Diablo II Resurrected
; max_consecutive_fails: stop bot after this many failed runs in a row
max_consecutive_fails=5
; max_game_length_s: emergency timeout per run/game
max_game_length_s=600
; auto_downgrade_threshold: if combined chickens+deaths exceed this number within 1 hour,
; bot automatically lowers difficulty by one tier (hell->nightmare->normal) and restarts.
; Set to 0 to disable.
auto_downgrade_threshold=40
max_game_length_s=160
; if you set this field to 1, botty will attempt to restart d2 after a crash or failure
restart_d2r_when_stuck=1
restart_d2r_when_stuck=0
; hardcore: 1 enables hardcore-safe assumptions in some routines
hardcore=0
@@ -181,7 +167,7 @@ skill_mistake_chance = 0.015
; run_countess (Act 1 Forgotten Tower)
; run_mephisto (Act 3 Durance of Hate)
; run_baal (Act 5 Throne of Destruction)
order=run_pindle, run_eldritch_shenk, run_nihlathak, run_arcane, run_diablo, run_vizier, run_trav
order=run_trav
[char]
; ==========================
@@ -194,9 +180,9 @@ type=hammerdin
; belt_rows: in-game belt size (2/3/4)
belt_rows=4
; casting_frames: your char cast breakpoint (affects action timing)
casting_frames=8
casting_frames=11
; cta_casting_frames: cast breakpoint on CTA swap (if used)
cta_casting_frames=8
cta_casting_frames=11
; attack_frames: base attack animation timing for non-cast attacks
attack_frames=15
; cta_available: 1 if Call to Arms swap exists, else 0
@@ -225,13 +211,13 @@ show_items=alt
; stand_still cannot be the default "shift" as it would interfere with merc healing
stand_still=capslock
; teleport: leave empty if you can't use
teleport=b
town_portal=6
teleport=f4
town_portal=f8
; call to arms settings:
; weapon_switch/battle_orders/battle_command only used when cta_available=1
weapon_switch=w
battle_orders=7
battle_command=8
battle_orders=f6
battle_command=f5
; ==========================
; ==== Optional configs ====
@@ -240,43 +226,13 @@ stash_gold=1
use_merc=1
; Attack length for barbarians should be as high as 8-10 and even 10-12 for trav/shenk
;
; Hammerdin attack lengths (seconds of hammer spam per boss):
; atk_len_trav = 3.0 (Council of 3 - 3 council members, easy)
; atk_len_pindle = 8.0 (Pindle + Eldritch + Shenk - longest fight)
; Pindle stats by difficulty:
; Normal: 1064-1588 HP, 75% fire, 70% poison
; Nightmare: 4773-5859 HP, 100% poison
; Hell: 13094-16070 HP, 75% fire, 50% cold, 33% light, 100% poison
; With Conviction (-33% res), Hell Pindle = 50% fire, 17% cold, 0% light, 67% poison
; CTA adds +100% dmg to hammer. Need ~60%+ FCR and good IAS to kill in 8s.
; atk_len_nihlathak = 4.0 (Nihlathak - single boss)
; atk_len_eldritch = 3.0 (Eldritch only - single boss)
; atk_len_shenk = 4.0 (Shenk only - single boss)
; atk_len_arc = 2.5 (Summoner in Arcane - single boss)
; atk_len_diablo = 10.0 (Diablo in CS - longest single boss)
; atk_len_countess = 3.0 (Countess - single boss, easy)
; atk_len_andariel = 4.0 (Andariel - single boss)
; atk_len_mephisto = 12.0 (Mephisto - single boss, high HP)
; atk_len_baal = 10.0 (Baal - single boss)
; atk_len_baal_waves = 30.0 (Baal's spawn waves before boss)
;
; Chaos Sanctuary (Diablo run) individual fights:
; atk_len_cs_trashmobs = 2.0 (Trash packs in CS)
; atk_len_diablo_vizier = 2.0 (Vizier of Chaos - seal boss A)
; atk_len_diablo_infector = 4.0 (Infector of Souls - seal boss C)
; atk_len_diablo_deseis = 5.0 (Lord De Seis - seal boss B, hardest seal)
;
; Increase these if your hammer kills slower (low IAS/FCR).
; Decrease if your hammer kills faster (high IAS/FCR, good gear).
; If you die during a fight, the attack length is too long for your defense.
atk_len_arc=2.5
atk_len_eldritch=3.0
atk_len_nihlathak=4.0
atk_len_pindle=8.0
atk_len_pindle=3.0
atk_len_shenk=4.0
atk_len_diablo=10.0
atk_len_trav=3.0
atk_len_diablo=3.0
atk_len_trav=5.0
; Boss run attack lengths (per-character defaults in kill_* methods)
; Adjust these if your build is faster/slower against these bosses
@@ -302,14 +258,14 @@ belt_mp_columns=1
belt_rejuv_columns=2
; Potion/chicken settings
take_health_potion=0.95
take_mana_potion=0.90
take_rejuv_potion_health=0.80
take_rejuv_potion_mana=0.55
heal_merc=0.85
heal_rejuv_merc=0.55
chicken=0.50
merc_chicken=0.20
take_health_potion=0.60
take_mana_potion=0.40
take_rejuv_potion_health=0.40
take_rejuv_potion_mana=0.10
heal_merc=0.60
heal_rejuv_merc=0.25
chicken=0.45
merc_chicken=0.25
; Misc.
; helps reduce accidental pickups when enabled especially on walking characters
@@ -319,24 +275,24 @@ fill_shared_stash_first=0
; to gamble, add any/all of the following: circlet, ring, coronet, talon, amulet
gamble_items=
; open_chests: 1 = open clickable chests along path when possible
open_chests=1
open_chests=0
; pre_buff_every_run: 1 = always recast buffs at run start
pre_buff_every_run=0
; runs_per_repair: visit repair vendor every X runs (blank/0 disables)
runs_per_repair=5
runs_per_repair=2000
; repair_npc: preferred repair vendor strategy.
; - a4_halbu (recommended, most stable)
; - a5_larzuk (faster when templates are reliable)
repair_npc=a4_halbu
; runs_per_stash: stash/sell every X runs (blank/0 disables)
runs_per_stash=4
runs_per_stash=1
; sell_junk: 1 = vendor non-keep items automatically
sell_junk=1
sell_junk=0
; protect_shields_from_sell: 1 = never vendor items with "shield" in detected name (recommended safety)
protect_shields_from_sell=1
; pick_rares_for_gold: 1 = pick all yellow (rare) ground items; non-keep rares will be sold
; Requires sell_junk=1 to convert extra pickups into gold.
pick_rares_for_gold=0
pick_rares_for_gold=1
; pick_gold: 1 = pick up ground gold piles, 0 = ignore all ground gold
pick_gold=1
@@ -347,12 +303,12 @@ stash_destination=3,2,1,0
; chipped, flawed, standard, flawless
transmute=flawless
;how often we want to run transmute routine(e.g. every 100 games)
transmute_every_x_game=2000
transmute_every_x_game=500
; number of stash tabs (ROTW has more than 6, adjust as needed)
stash_tabs=6
; potion transmute settings
; convert_rejuv: 1 = convert regular Rejuv Potions to Full Rejuv via cube (3 -> 1)
convert_rejuv=1
convert_rejuv=0
; min_rejuv_to_convert: minimum regular rejuv potions in inventory before starting conversion
min_rejuv_to_convert=6
@@ -402,23 +358,22 @@ hydra=f1
; ==== Builds: Paladin ====
; =========================
[paladin]
cleansing=f9
holy_shield=f2
redemption=f3
vigor=f4
cleansing=f2
holy_shield=f7
redemption=
vigor=f3
[fohdin]
; foh must be left skill, hotkey required
blessed_hammer=f1
concentration=f8
conviction=f5
blessed_hammer=f9
concentration=f1
conviction=
foh=f6
holy_bolt=f7
[hammerdin]
blessed_hammer=f1
concentration=f8
conviction=f5
blessed_hammer=f9
concentration=f1
; =========================
; ==== Builds: Warlock ====
@@ -576,13 +531,13 @@ buff_2=
; select_runs_key: open run selector UI
select_runs_key=pagedown
; restore_settings_from_backup_key: restore backed-up D2R settings
restore_settings_from_backup_key=insert
restore_settings_from_backup_key=f10
; settings_backup_key: create backup of current D2R settings
settings_backup_key=pause
; auto_settings_key: auto-apply required D2R settings
auto_settings_key=pageup
; graphic_debugger_key: toggle on-screen debug layers
graphic_debugger_key=delete
graphic_debugger_key=insert
; resume_key: start/pause bot loop
resume_key=f11
; exit_key: hard stop bot
@@ -606,7 +561,7 @@ message_headers=
; ocr_during_pickit: 1 = run OCR while looting (slower, more diagnostics)
ocr_during_pickit=0
;use "can_teleport_natively" or "can_teleport_with_charges" if you want to force certain behavior in case autodetection isn't working properly
override_capabilities=can_teleport_natively
override_capabilities=
; pathing_delay_factor: movement/click delay multiplier (1 fast .. 10 slow)
pathing_delay_factor=2
; If you want to control Hyper-V window from host use 0,51 here

BIN
debug_full_screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

BIN
debug_merc_area.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
debug_portal_search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
debug_screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
debug_screen_full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -100,7 +100,7 @@ class IChar:
return False
else:
mouse.move(pos[0], pos[1])
wait(0.1, 0.12)
wait(0.04, 0.06)
mouse.click(button="left")
#Current logic only sets force_run if we previously teled.
@@ -157,8 +157,7 @@ class IChar:
while time.time() - check_success_start < 2:
if success_func is None or success_func():
return True
elapsed = time.time() - start
Logger.error(f"select_by_template: could not find {template_type} after {elapsed:.1f}s (timeout={timeout}s, threshold={threshold})")
Logger.error(f"Wanted to select {template_type}, but could not find it")
return False
def skill_is_charged(self, img: np.ndarray = None) -> bool:
@@ -304,83 +303,56 @@ class IChar:
return False
def _pre_buff_cta(self):
"""Switch to CTA weapon, cast BC + BO, switch back to main.
Uses the BC skill-bar template to verify each weapon switch rather than
blindly toggling — prevents the character from fighting with the wrong
weapon after a chicken/death interrupted a previous buff cycle and saved
the game with the CTA slot active.
"""
bc_hotkey = Config().char.get("battle_command")
if not bc_hotkey:
# No hotkey configured → can't verify; fall back to unverified toggle
Logger.warning("_pre_buff_cta: battle_command hotkey not set — using unverified weapon switch")
# Check if we are already on CTA side
if skills.is_right_skill_selected(["BC", "BO"]):
keyboard.send(Config().char["weapon_switch"])
wait(0.3, 0.35)
self._select_skill(skill="battle_command", mouse_click_type="right", delay=(0.1, 0.2))
wait(0.4, 0.45)
# Save current skill img
skill_before = cut_roi(grab(), Config().ui_roi["skill_right"])
# Try to switch weapons and select bo until we find the skill on the right skill slot
start = time.time()
switch_sucess = False
while time.time() - start < 4:
keyboard.send(Config().char["weapon_switch"])
wait(0.4, 0.45)
keyboard.send(Config().char["battle_command"])
wait(0.2, 0.3)
if skills.is_right_skill_selected(["BC", "BO"]):
switch_sucess = True
break
else:
Logger.warning("Failed to find Battle Command, swapping weapons again.")
if not switch_sucess:
Logger.error("You dont have Battle Command bound, or you do not have CTA. Ending CTA buff")
return False
else:
# We switched succesfully, let's pre buff
mouse.click(button="right")
wait(self._cta_cast_duration, self._cta_cast_duration)
self._select_skill(skill="battle_orders", mouse_click_type="right", delay=(0.1, 0.2))
self._select_skill(skill = "battle_orders", mouse_click_type="right", delay=(0.1, 0.2))
mouse.click(button="right")
wait(self._cta_cast_duration, self._cta_cast_duration)
if Config().char["buff_with_cta"]:
self.cast_buffs(self._cta_cast_duration)
wait(0.08, 0.08)
keyboard.send(Config().char["weapon_switch"])
wait(0.3, 0.35)
return True
wait(0.08,0.08) #Give extra time after last cast to allow weapon switch to be available
# --- Step 1: ensure we start on the main weapon slot ---
# If a previous run was interrupted mid-buff (chicken / death), D2R may have
# saved with the CTA slot active. Detect that by pressing the BC hotkey and
# checking if BC appears on the right skill bar.
keyboard.send(bc_hotkey)
wait(0.12, 0.18)
if skills.is_right_skill_selected(["BC"]):
Logger.warning("_pre_buff_cta: game started on CTA slot (leftover from interrupted buff) — correcting to main weapon")
# Make sure the switch back to the original weapon is good
start = time.time()
switch_sucess = False
while time.time() - start < 4:
keyboard.send(Config().char["weapon_switch"])
wait(0.45, 0.55)
# --- Step 2: switch TO CTA slot and verify ---
keyboard.send(Config().char["weapon_switch"])
wait(0.3, 0.35)
keyboard.send(bc_hotkey)
wait(0.12, 0.18)
on_cta = skills.is_right_skill_selected(["BC"])
if not on_cta:
Logger.warning("_pre_buff_cta: switch to CTA slot failed — retrying")
keyboard.send(Config().char["weapon_switch"])
wait(0.45, 0.55)
keyboard.send(bc_hotkey)
wait(0.12, 0.18)
on_cta = skills.is_right_skill_selected(["BC"])
if not on_cta:
Logger.error("_pre_buff_cta: could not confirm CTA slot after retry — casting Holy Shield from main and skipping CTA buffs")
self.cast_buffs(self._cast_duration)
return True # buffs cast from main; weapon slot is correct
# --- Step 3: cast BC (already selected above) then BO ---
mouse.click(button="right")
wait(self._cta_cast_duration, self._cta_cast_duration)
self._select_skill(skill="battle_orders", mouse_click_type="right", delay=(0.1, 0.2))
mouse.click(button="right")
wait(self._cta_cast_duration, self._cta_cast_duration)
if Config().char["buff_with_cta"]:
self.cast_buffs(self._cta_cast_duration)
wait(0.08, 0.08)
# --- Step 4: switch back to main weapon slot and verify ---
keyboard.send(Config().char["weapon_switch"])
wait(0.3, 0.35)
if skills.is_right_skill_selected(["BC"]):
Logger.warning("_pre_buff_cta: switch back to main failed — retrying")
keyboard.send(Config().char["weapon_switch"])
wait(0.45, 0.55)
if skills.is_right_skill_selected(["BC"]):
Logger.error("_pre_buff_cta: still on CTA slot after retry — CHARACTER WILL FIGHT WITH WRONG WEAPON")
# Buffs are cast; return True so pre_buff() doesn't skip Holy Shield entirely.
return True
wait(0.4, 0.45)
skill_after = cut_roi(grab(), Config().ui_roi["skill_right"])
_, max_val, _, _ = cv2.minMaxLoc(cv2.matchTemplate(skill_after, skill_before, cv2.TM_CCOEFF_NORMED))
if max_val > 0.8:
switch_sucess = True
break
else:
Logger.warning("Failed to switch weapon, try again")
wait(0.5)
return switch_sucess
def vec_to_monitor(self, target):

View File

@@ -14,7 +14,7 @@ class DeathManager:
def __init__(self):
self._died = False
self._do_monitor = False
self._loop_delay = 1.0
self._loop_delay = 0.5
self._callback = None
self._last_death_screenshot = None

View File

@@ -1,20 +1,19 @@
import cv2
import time
import random
from threading import Lock
from inventory import belt
from pather import Location
import cv2
from threading import Lock
import time
from input_layer import keyboard
from input_layer import mouse
from utils.misc import wait
from logger import Logger
from utils.log_rotation import safe_imwrite
from screen import grab
import time
from config import Config
from inventory import common
from ui import view, meters
from ui_manager import ScreenObjects, is_visible
from random import uniform
class HealthManager:
@@ -92,104 +91,99 @@ class HealthManager:
Logger.info("Start health monitoring")
self._do_monitor = True
self._did_chicken = False
start = time.time()
lp_hp_potion_delay = 10.24
lp_mp_potion_delay = 5.12
lp_mp_potion_delay = 1.0
merc_hp_potion_delay = 10.24
while self._do_monitor:
if self._did_chicken or self.get_pause_state():
wait(1)
wait(0.5, 0.7)
continue
fn_start = time.perf_counter()
img = grab()
if is_visible(ScreenObjects.InGame, img):
health_percentage = meters.get_health(img)
mana_percentage = meters.get_mana(img)
try:
fn_start = time.perf_counter()
img = grab()
if is_visible(ScreenObjects.InGame, img):
health_percentage = meters.get_health(img)
mana_percentage = meters.get_mana(img)
# check rejuv first
success_drink_rejuv = False
last_drink = time.time() - self._last_rejuv
#It seems that hit recovery can delay the use of juvs and 15 frames is max recovery time.
#To delay two juvs being used back to back, we'll need to wait the recovery time between uses.
#15 frames is 0.60 seconds.
if last_drink > 0.60:
if (health_percentage <= Config().char["take_rejuv_potion_health"]) or \
(mana_percentage <= Config().char["take_rejuv_potion_mana"]):
# Simulate human reaction time (100-300ms)
wait(0.1, 0.3)
success_drink_rejuv = belt.drink_potion("rejuv", stats=[health_percentage, mana_percentage])
#failure to drink juv (likely out of juvs). Perform chicken
if not success_drink_rejuv:
Logger.warning(f"Failed to drink rejuv. Trying to chicken, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
continue
self._last_rejuv = time.time()
# check rejuv first
success_drink_rejuv = False
last_drink = time.time() - self._last_rejuv
if last_drink > 0.60:
if (health_percentage <= Config().char["take_rejuv_potion_health"]) or \
(mana_percentage <= Config().char["take_rejuv_potion_mana"]):
# Critical: Reduce delay to minimum
wait(0.02, 0.05)
success_drink_rejuv = belt.drink_potion("rejuv", stats=[health_percentage, mana_percentage])
if not success_drink_rejuv:
Logger.warning(f"Failed to drink rejuv. Trying to chicken, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
continue
self._last_rejuv = time.time()
#if we drink two juvs within 8 seconds of one another AND HP is still low, may be in bad state. Perform chicken
# Only chicken on rapid double-rejuv if HP was the actual trigger (not just mana).
# Hammerdins spend mana fast — mana-triggered rejuvs can legitimately fire back-to-back.
if last_drink < 8 and health_percentage <= Config().char["take_rejuv_potion_health"]:
Logger.warning(f"Two juvs drank within {last_drink} seconds. Trying to chicken, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
continue
if last_drink < 8:
Logger.warning(f"Two juvs drank within {last_drink:.1f} seconds. Trying to chicken, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
continue
# Default chicken if health is below threshold.
if health_percentage <= Config().char["chicken"]:
Logger.warning(f"Trying to chicken, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
continue
# 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 > lp_hp_potion_delay:
wait(0.1, 0.3) # human reaction time
belt.drink_potion("health", stats=[health_percentage, mana_percentage])
self._last_health = time.time()
# check mana
last_drink = time.time() - self._last_mana
if mana_percentage <= Config().char["take_mana_potion"] and last_drink > lp_mp_potion_delay:
wait(0.1, 0.3) # human reaction time
belt.drink_potion("mana", stats=[health_percentage, mana_percentage])
self._last_mana = time.time()
# check merc health and heal/chicken as needed
if any([Config().char[x] for x in ["heal_rejuv_merc", "merc_chicken", "heal_merc"]]):
merc_health = meters.get_merc_health(img)
last_drink = time.time() - self._last_merc_heal
# chicken if merc is too low
if Config().char["merc_chicken"] and (merc_health < Config().char["merc_chicken"]):
Logger.warning(f"Trying to chicken, merc HP {(merc_health*100):.1f}%!")
if health_percentage <= Config().char["chicken"]:
Logger.warning(f"Trying to chicken, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
continue
# rejuv if merc is critically low
if Config().char["heal_rejuv_merc"] and (merc_health < Config().char["heal_rejuv_merc"]) and (last_drink > 4.0):
wait(0.1, 0.3) # human reaction time
belt.drink_potion("rejuv", merc=True, stats=[merc_health])
self._last_merc_heal = time.time()
elif Config().char["heal_merc"] and (merc_health < Config().char["heal_merc"]) and (last_drink > merc_hp_potion_delay):
wait(0.1, 0.3) # human reaction time
belt.drink_potion("health", merc=True, stats=[merc_health])
self._last_merc_heal = time.time()
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 > lp_hp_potion_delay:
wait(0.05, 0.1)
if belt.drink_potion("health", stats=[health_percentage, mana_percentage]):
self._last_health = time.time()
# check mana
last_drink = time.time() - self._last_mana
if mana_percentage <= Config().char["take_mana_potion"] and last_drink > lp_mp_potion_delay:
wait(0.05, 0.1)
if belt.drink_potion("mana", stats=[health_percentage, mana_percentage]):
self._last_mana = time.time()
# check merc health
if any([Config().char[x] for x in ["heal_rejuv_merc", "merc_chicken", "heal_merc"]]):
merc_health = meters.get_merc_health(img)
last_drink = time.time() - self._last_merc_heal
if Config().char["merc_chicken"] and (merc_health < Config().char["merc_chicken"]):
Logger.warning(f"Trying to chicken, merc HP {(merc_health*100):.1f}%!")
self._do_chicken(img)
continue
if Config().char["heal_rejuv_merc"] and (merc_health < Config().char["heal_rejuv_merc"]) and (last_drink > 4.0):
wait(0.05, 0.1)
if belt.drink_potion("rejuv", merc=True, stats=[merc_health]):
self._last_merc_heal = time.time()
elif Config().char["heal_merc"] and (merc_health < Config().char["heal_merc"]) and (last_drink > merc_hp_potion_delay):
wait(0.05, 0.1)
if belt.drink_potion("health", merc=True, stats=[merc_health]):
self._last_merc_heal = time.time()
# Close any open panels that might block detection
if not self.get_panel_check_paused() and (is_visible(ScreenObjects.LeftPanel, img) or is_visible(ScreenObjects.RightPanel, img)):
Logger.warning(f"Found an open inventory / quest / skill / stats page. Close it.")
self._count_panel_detects += 1
if self._count_panel_detects >= 2:
self._count_panel_detects = 0
Logger.warning(f"Found an open inventory / quest / skill / stats page again. Chicken to dismiss.")
Logger.warning(f"Found open panels (inv/quest/stats) twice. Chickening to be safe.")
self._do_chicken(img)
continue
Logger.debug("Found an open panel. Closing it.")
common.close()
fn_end = time.perf_counter()
# Add timing jitter to avoid perfectly regular polling pattern (anti-cheat)
import random
base_wait = 3/25 - (fn_end - fn_start)
jitter = random.uniform(0.8, 1.2) # ±20% jitter
wait_time = max(0.0, base_wait * jitter)
if wait_time > 0:
wait(wait_time) # wait ~3 frames before rechecking
fn_end = time.perf_counter()
# Target ~15 FPS polling with anti-cheat jitter
base_wait = 1/15 - (fn_end - fn_start)
wait_time = max(0.01, base_wait * random.uniform(0.8, 1.2))
wait(wait_time)
except Exception as e:
Logger.error(f"HealthManager error: {e}")
wait(0.5)
Logger.debug("Stop health monitoring")

View File

@@ -260,15 +260,21 @@ class mouse:
@staticmethod
def click(button):
if button != "left" or mouse._is_clicking_safe():
try:
from utils.stealth import apply_click_delay
apply_click_delay()
except Exception:
from logger import Logger
Logger.warning("[Stealth] apply_click_delay failed, falling back to manual delay")
from utils.misc import wait as _wait
_wait(0.05, 0.3)
# DISABLED STEALTH FOR DEBUGGING
# try:
# from utils.stealth import apply_click_delay
# apply_click_delay()
# except Exception:
# from logger import Logger
# Logger.warning("[Stealth] apply_click_delay failed, falling back to manual delay")
# from utils.misc import wait as _wait
# _wait(0.05, 0.3)
from logger import Logger
Logger.debug(f"Executing native click: {button}")
_native_click(button)
else:
from logger import Logger
Logger.debug(f"Click blocked by safety check (button: {button})")
@staticmethod
def press(button):

View File

@@ -177,7 +177,7 @@ def _make_mouse_input(flags: int, dx: int = 0, dy: int = 0, data: int = 0):
i.union.mi.dx = dx
i.union.mi.dy = dy
i.union.mi.mouseData = data
i.union.mi.dwFlags = flags | MOUSEEVENTF_ABSOLUTE
i.union.mi.dwFlags = flags
i.union.mi.dwExtraInfo = None
return i
@@ -285,8 +285,9 @@ def mouse_up(button: str = "left"):
raise ValueError(f"Unknown button: {button}")
def mouse_click(button: str = "left"):
"""Click (press + release) a mouse button."""
"""Click (press + release) a mouse button with a reliable delay."""
mouse_down(button)
time.sleep(0.1) # 100ms hold time is very reliable for games
mouse_up(button)
def mouse_wheel(delta: int):

View File

@@ -78,7 +78,7 @@ def _potion_type(img: np.ndarray) -> str:
roi = [int(w * 0.4), int(h * 0.3), int(w * 0.4), int(h * 0.7)]
img = cut_roi(img, roi)
avg_brightness = np.average(img)
if avg_brightness < 47:
if avg_brightness < 30:
return "empty"
score_list = []
# rejuv
@@ -92,7 +92,7 @@ def _potion_type(img: np.ndarray) -> str:
score_list.append((float(np.sum(mask)) / mask.size) * (1/255.0))
# find max score
max_val = np.max(score_list)
if max_val > 0.28:
if max_val > 0.20:
idx = np.argmax(score_list)
types = ["rejuv", "health", "mana"]
return types[idx]

View File

@@ -120,9 +120,8 @@ class PickIt:
tele_success = True
Logger.info(f"Teled to pickup item {ground_item.Name} and now at distance {ground_item.Distance}")
char.pick_up_item((ground_item.CenterMonitor['x'], ground_item.CenterMonitor['y']), item_name=ground_item.Name, distance=ground_item.Distance, force_run=True)
# Wait for the pickup animation to complete before moving on
# Issue #939: bot was teleporting away before item was actually grabbed
wait(0.2, 0.3)
# Wait briefly for the pickup animation to start/complete
wait(0.08, 0.12)
break
if not tele_success:
Logger.warning(f"Failed to scan item '{item.Name}' after tele. Items visible: {len(items)}")

View File

@@ -272,25 +272,46 @@ def open_npc_menu(npc_key: Npc) -> bool:
results.append({"pos": res.center_monitor, "score": res.score, "combo": min_dist / (res.score**2)})
# sort by composite of template match score and distance to NPC pose
results = sorted(results, key=lambda r: r["combo"])
if not results:
Logger.debug(f"NPC {npc_key} search - No templates matched in this frame")
for result in results:
Logger.debug(f"Hovering over NPC at monitor pos: {result['pos']}")
mouse.move(*result["pos"], randomize=3, delay_factor=[0.3, 0.5])
wait(0.2, 0.3)
img = grab()
img = escape_dialogue(img)
_, filtered_inp_w = color_filter(img, Config().colors["white"])
_, filtered_inp_g = color_filter(img, Config().colors["gold"])
res_w = template_finder.search(npcs[npc_key]["name_tag_white"], filtered_inp_w, 0.9, roi=roi).valid
res_g = template_finder.search(npcs[npc_key]["name_tag_gold"], filtered_inp_g, 0.9, roi=roi).valid
if res_w:
res_w = template_finder.search(npcs[npc_key]["name_tag_white"], filtered_inp_w, 0.5, roi=roi)
res_g = template_finder.search(npcs[npc_key]["name_tag_gold"], filtered_inp_g, 0.5, roi=roi)
# fallback to grayscale if color filter is too aggressive
if not res_w.valid:
res_w_gray = template_finder.search(npcs[npc_key]["name_tag_white"], img, 0.5, roi=roi, use_grayscale=True)
if res_w_gray.score > res_w.score:
res_w = res_w_gray
Logger.debug(f"NPC {npc_key} hover - using grayscale fallback (score: {res_w.score:.3f})")
Logger.debug(f"NPC {npc_key} hover - White score: {res_w.score:.3f}, Gold score: {res_g.score:.3f}")
if res_w.valid:
Logger.info(f"Clicking on {npc_key} at {mouse.get_position()}")
mouse.click(button="left")
attempts += 1
wait(0.7, 1.0)
_, filtered_inp = color_filter(grab(), Config().colors["gold"])
res = template_finder.search(npcs[npc_key]["name_tag_gold"], filtered_inp, 0.9, roi=roi).valid
if res:
# Wait for gold name tag to appear (indicates NPC menu opened or mouse stayed over NPC)
wait(0.8, 1.2)
img_after = grab()
_, filtered_inp_g2 = color_filter(img_after, Config().colors["gold"])
res_g2 = template_finder.search(npcs[npc_key]["name_tag_gold"], filtered_inp_g2, 0.5, roi=roi)
if res_g2.valid:
Logger.debug(f"NPC {npc_key} - gold name tag confirmed after click")
return True
elif res_g:
else:
Logger.warning(f"NPC {npc_key} - clicked but gold name tag not found (score: {res_g2.score:.3f})")
elif res_g.valid:
Logger.debug(f"NPC {npc_key} - already hovered/selected (gold tag score: {res_g.score:.3f})")
return True
wait(0.1) # Brief pause before next frame grab if no matches were valid
return False
def press_npc_btn(npc_key: Npc, action_btn_key: str):

View File

@@ -96,7 +96,7 @@ def _single_template_match(template: Template, inp_img: np.ndarray = None, roi:
# filter for desired color or make grayscale
if color_match:
template_img, = color_filter(template.img_bgr, color_match)[1],
template_img = color_filter(template.img_bgr, color_match)[1]
img = color_filter(img, color_match)[1]
elif use_grayscale:
template_img = template.img_gray
@@ -104,25 +104,32 @@ def _single_template_match(template: Template, inp_img: np.ndarray = None, roi:
else:
template_img = template.img_bgr
if not (img.shape[0] > template_img.shape[0] and img.shape[1] > template_img.shape[1]):
Logger.error(f"Image shape and template shape are incompatible: {template.name}. Image: {img.shape}, Template: {template_img.shape}, roi: {roi}")
if not (img.shape[0] >= template_img.shape[0] and img.shape[1] >= template_img.shape[1]):
from logger import Logger
Logger.debug(f"Incompatible shapes for {template.name}: Image {img.shape}, Template {template_img.shape}, ROI {roi}")
else:
res = cv2.matchTemplate(img, template_img, cv2.TM_CCOEFF_NORMED, mask = template.alpha_mask)
np.nan_to_num(res, copy=False, nan=0.0, posinf=0.0, neginf=0.0)
_, max_val, _, max_pos = cv2.minMaxLoc(res)
try:
res = cv2.matchTemplate(img, template_img, cv2.TM_CCOEFF_NORMED, mask = template.alpha_mask)
np.nan_to_num(res, copy=False, nan=0.0, posinf=0.0, neginf=0.0)
_, max_val, _, max_pos = cv2.minMaxLoc(res)
# save rectangle corresponding to matched region
rec_x = int((max_pos[0] + rx))
rec_y = int((max_pos[1] + ry))
rec_w = int(template_img.shape[1])
rec_h = int(template_img.shape[0])
template_match.region = [rec_x, rec_y, rec_w, rec_h]
template_match.region_monitor = [*convert_screen_to_monitor((rec_x, rec_y)), rec_w, rec_h]
template_match.center = roi_center(template_match.region)
template_match.center_monitor = convert_screen_to_monitor(template_match.center)
template_match.name = template.name
template_match.score = max_val
template_match.valid = True
# save rectangle corresponding to matched region
rec_x = int((max_pos[0] + rx))
rec_y = int((max_pos[1] + ry))
rec_w = int(template_img.shape[1])
rec_h = int(template_img.shape[0])
template_match.region = [rec_x, rec_y, rec_w, rec_h]
template_match.region_monitor = [*convert_screen_to_monitor((rec_x, rec_y)), rec_w, rec_h]
template_match.center = roi_center(template_match.region)
template_match.center_monitor = convert_screen_to_monitor(template_match.center)
template_match.name = template.name
template_match.score = max_val
template_match.valid = True
except Exception as e:
from logger import Logger
Logger.error(f"Template match failed for {template.name}: {e}")
import traceback
Logger.debug(traceback.format_exc())
return template_match
@@ -160,7 +167,8 @@ def search(
# Single template — no benefit to parallelize
if len(templates) == 1:
match = _single_template_match(templates[0], inp_img, roi, color_match, use_grayscale)
return match if match.score >= threshold else TemplateMatch()
match.valid = match.score >= threshold
return match
# Multiple templates — parallel search
matches = []
@@ -171,10 +179,8 @@ def search(
]
for future in concurrent.futures.as_completed(futures):
match = future.result()
if match.score >= threshold:
if not best_match:
return match
matches.append(match)
match.valid = match.score >= threshold
matches.append(match)
if matches:
matches = sorted(matches, key=lambda obj: obj.score, reverse=True)

View File

@@ -20,7 +20,7 @@ def get_mana(img: np.ndarray) -> float:
return mana_percentage
def get_merc_health(img: np.ndarray) -> float:
health_rec = [Config().ui_pos["merc_health_left"], Config().ui_pos["merc_health_top"], Config().ui_pos["merc_health_width"], 1]
health_rec = [Config().ui_pos["merc_health_left"], Config().ui_pos["merc_health_top"], Config().ui_pos["merc_health_width"], 3]
merc_health_img = cut_roi(img, health_rec)
merc_health_img = cv2.cvtColor(merc_health_img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(merc_health_img, 5, 255, cv2.THRESH_BINARY)

View File

@@ -17,7 +17,7 @@ def is_left_skill_selected(template_list: list[str]) -> bool:
"""
skill_left_ui_roi = Config().ui_roi["skill_left"]
for template in template_list:
if template_finder.search(template, grab(), threshold=0.84, roi=skill_left_ui_roi).valid:
if template_finder.search(template, grab(), threshold=0.8, roi=skill_left_ui_roi).valid:
return True
return False
@@ -73,7 +73,7 @@ def is_right_skill_selected(template_list: list[str]) -> bool:
"""
skill_right_ui_roi = Config().ui_roi["skill_right"]
for template in template_list:
if template_finder.search(template, grab(), threshold=0.84, roi=skill_right_ui_roi).valid:
if template_finder.search(template, grab(), threshold=0.8, roi=skill_right_ui_roi).valid:
return True
return False
@@ -82,7 +82,7 @@ def wait_until_right_skill_selected(template: str, timeout: float = 1) -> bool:
:return: Bool if skill gets selected on the right skill slot within the timeout.
"""
skill_right_ui_roi = Config().ui_roi["skill_right"]
if template_finder.search_and_wait(template, roi=skill_right_ui_roi, timeout = timeout, threshold=0.84).valid:
if template_finder.search_and_wait(template, roi=skill_right_ui_roi, timeout = timeout, threshold=0.8).valid:
return True
return False

View File

@@ -61,13 +61,7 @@ class ScreenObjects:
MercIcon=ScreenObject(
ref=["MERC_A2", "MERC_A2_2", "MERC_A1", "MERC_A5", "MERC_A3"],
roi="merc_icon",
threshold=0.85,
use_grayscale=True
)
MercPanelText=ScreenObject(
ref=["MERCENARY_SCREEN"],
roi="merc_panel",
threshold=0.85,
threshold=0.75,
use_grayscale=True
)
PlayBtn=ScreenObject(
@@ -182,7 +176,7 @@ class ScreenObjects:
YouHaveDied=ScreenObject(
ref="YOU_HAVE_DIED",
roi="death",
threshold=0.9,
threshold=0.8,
color_match=Config().colors["red"],
use_grayscale=True
)
@@ -257,8 +251,10 @@ class ScreenObjects:
threshold=0.8,
)
NotEnoughGold=ScreenObject(
ref="MISSING_GOLD",
threshold=0.8,
ref="NOT_ENOUGH_GOLD",
threshold=0.9,
color_match=Config().colors["red"],
use_grayscale=True
)
QuestSkillBtn=ScreenObject(
ref="QUEST_SKILL_BTN",

BIN
test_grab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB