diff --git a/src/char/i_char.py b/src/char/i_char.py index 67b447f..366e590 100644 --- a/src/char/i_char.py +++ b/src/char/i_char.py @@ -232,6 +232,18 @@ class IChar: Logger.error("timeout waiting for tele skill to activate") def move(self, pos_monitor: tuple[float, float], force_tp: bool = False, force_move: bool = False): + # Never click into the HUD. Neither branch below did this, so a move + # target low on the screen landed on the interface — most visibly the + # loot-filter toggles at the bottom-left (screen x 395-560, y 692-712), + # which a right-click flips. Reported after Larzuk trips: he stands on + # the left of Harrogath, so moves to and from him aim at that corner. + # + # Latent for a long time and surfaced by Enigma: the walk branch shrinks + # its target toward centre via adjust_factor, which mostly kept clicks + # off the HUD by accident, while the teleport branch clicks the raw + # target. The pather's anti-stuck path already guarded this; move() did + # not. + pos_monitor = get_closest_non_hud_pixel(pos_monitor, "monitor") factor = Config().advanced_options["pathing_delay_factor"] if "teleport" in Config().char and Config().char["teleport"] and ( force_tp diff --git a/test/test_hud_clicks.py b/test/test_hud_clicks.py new file mode 100644 index 0000000..05dfcb0 --- /dev/null +++ b/test/test_hud_clicks.py @@ -0,0 +1,50 @@ +"""move() must never click into the HUD. + +Reported 2026-08-28: after a Larzuk trip the bot pressed escape and then +toggled the LOOT FILTER. Larzuk stands on the left of Harrogath, so moves to +and from him aim at the bottom-left corner — where D2R puts the seven +loot-filter category buttons (screen x 395-560, y 692-712). A right-click there +flips a filter, which changes what renders and therefore what every later +template match can see. + +IChar.move() applied no HUD avoidance in either branch. The pather's anti-stuck +path already called get_closest_non_hud_pixel; move() did not. + +Latent for a long time and surfaced by Enigma: the walk branch shrinks its +target toward centre via adjust_factor, which mostly kept clicks off the HUD by +accident, while the teleport branch clicks the raw target. +""" +import inspect + + +FILTER_ROW = [(400, 700), (470, 700), (550, 700), (470, 710)] + + +def test_hud_mask_covers_the_loot_filter_buttons(): + import cv2 + mask = cv2.imread("assets/hud_mask.png", cv2.IMREAD_GRAYSCALE) + mask = cv2.threshold(mask, 1, 255, cv2.THRESH_BINARY)[1] + for x, y in FILTER_ROW: + assert mask[y, x] == 0, f"loot-filter button ({x},{y}) is not masked as HUD" + + +def test_targets_on_the_filter_row_are_moved_off_it(): + from ui_manager import get_closest_non_hud_pixel + for x, y in FILTER_ROW: + ox, oy = get_closest_non_hud_pixel((x + 5, y + 98), "monitor") # monitor offset + assert (oy - 98) < 690, f"({x},{y}) still lands on the HUD at y={oy - 98}" + + +def test_a_centre_target_is_left_alone(): + """The guard must not perturb ordinary targets.""" + from ui_manager import get_closest_non_hud_pixel + assert get_closest_non_hud_pixel((645, 458), "monitor") == (645, 458) + + +def test_move_applies_the_guard(): + from char.i_char import IChar + src = inspect.getsource(IChar.move) + assert "get_closest_non_hud_pixel" in src, "move() can still click into the HUD" + guard_at = src.index("get_closest_non_hud_pixel") + first_click = min([i for i in (src.find("mouse.move"), src.find("mouse.click")) if i != -1]) + assert guard_at < first_click, "the guard must run before any click"