fix(town): A5 waypoint HUD false-positive, safe merc-panel dismiss, Tyrael for resurrect
Three linked failures that together took the nightmare run from ~8% to ~37% failures.
1. A5_WP matched the HUD. main's recaptured a5_wp.png (14f5876, pulled in by my merge)
scores 0.966 on the belt/mana-orb area at (980, 656) — above threshold and higher than
the real stone — so every search locked onto the HUD, clicked it, and open_wp burned
~83s escalating through anchors before giving up. buy_consumables hit 222-253s and
tripped the 240s maintenance timeout.
Fix: search the waypoint within ui_roi[cut_skill_bar] so the HUD can never compete
(select_by_template gained an optional roi for this), and search BOTH captures with
best_match — main's new one and the pre-merge a5_wp_2.png, which reaches 0.73-0.98 here
against the new one's 0.50-0.58. Neither setup has to lose. 222s -> 33s.
2. My own regression, introduced an hour earlier. The merc check pressed 'o' and closed
the panel only when MercPanelText matched, leaving the CHRONICLE panel (what 'o' opens
on this client) up all game. I "fixed" that with an unconditional esc — which is worse:
with nothing open, esc opens the GAME MENU, which LeftPanel/RightPanel do not match, so
nothing closed that either and every later search saw a menu instead of the town.
Fix: always toggle 'o' back. 'o' opened it, so 'o' closes it, and it cannot open
something new. Symmetry, not detection.
3. resurrect_npc=a4_tyrael (new config, mirrors repair_npc). Measured today:
A4 Tyrael ok 8.2s, ok 24.9s - 0 errors, ever
A5 Qual-Kehk fail 113.6s / 52.0s / 163.1s / 72.6s - 43 errors
Tyrael stands on a fixed spot by the A4 waypoint; Qual-Kehk is the least reliable NPC
in the route. Falls back to the current act if the trip to A4 fails.
Verified live: 4 games, 0 failures, town 17-23s (was 178-230s), approach 11-15s.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6ac1105fc7
commit
11186e2c7d
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -382,6 +382,15 @@ runs_per_repair=5
|
||||
; 2026-06-10: switched to a5_larzuk — session logs showed Halbu detection failing
|
||||
; 100% (body score ~0.39) and each failed A4 trip desynced the bot's act state.
|
||||
repair_npc=a5_larzuk
|
||||
; resurrect_npc: which NPC revives the mercenary. Blank = whichever act the character is
|
||||
; already in. Set to a4_tyrael to always use Act 4 Tyrael.
|
||||
; Measured 2026-08-27 on this setup:
|
||||
; A4 Tyrael ok 8.2s, ok 24.9s - 0 errors, ever
|
||||
; A5 Qual-Kehk fail 113.6s / 52.0s / 163.1s / 72.6s - 43 errors
|
||||
; Tyrael stands on a fixed spot by the A4 waypoint; Qual-Kehk is the least reliable NPC in
|
||||
; the route. A failed hunt costs more than the whole A5->A4 trip, and one 163s hunt pushed
|
||||
; the town visit past max_maintenance_time_s and killed the game.
|
||||
resurrect_npc=a4_tyrael
|
||||
; runs_per_stash: stash/sell every X runs (blank/0 disables)
|
||||
runs_per_stash=4
|
||||
; sell_junk: 1 = vendor non-keep items automatically
|
||||
|
||||
+10
-15
@@ -969,23 +969,18 @@ class Bot:
|
||||
keyboard.send("o")
|
||||
wait(0.3, 0.4)
|
||||
merc_panel_open = is_visible(ScreenObjects.MercPanelText)
|
||||
# ALWAYS toggle 'o' back, whatever it opened. Symmetry is the only safe
|
||||
# dismiss here:
|
||||
# - closing only when MercPanelText matched left the CHRONICLE panel
|
||||
# (what 'o' actually opens on this client) up for the whole game
|
||||
# - "just send esc" is WORSE: with nothing open, esc opens the GAME MENU,
|
||||
# which LeftPanel/RightPanel do not match, so nothing closes that either
|
||||
# and every later template search sees a menu instead of the town
|
||||
# 'o' opened it, so 'o' closes it, and it cannot open something new.
|
||||
keyboard.send("o")
|
||||
wait(0.25, 0.35)
|
||||
if merc_panel_open:
|
||||
# Merc is alive - panel opened, close it
|
||||
keyboard.send("o")
|
||||
wait(0.2, 0.3)
|
||||
merc_visible = True
|
||||
else:
|
||||
# 'o' opened SOMETHING that is not the merc panel and nothing closed it.
|
||||
# On this client it opens the CHRONICLE collection panel, which covers
|
||||
# the screen and blanks every later template match — the run then dies
|
||||
# on click_red_portal after ~66s of flailing at a covered screen
|
||||
# (g90 2026-08-27). Always dismiss whatever appeared, then confirm.
|
||||
keyboard.send("esc")
|
||||
wait(0.25, 0.35)
|
||||
if is_visible(ScreenObjects.LeftPanel) or is_visible(ScreenObjects.RightPanel):
|
||||
Logger.warning("Merc check: a panel is still open after esc — sending esc again")
|
||||
keyboard.send("esc")
|
||||
wait(0.25, 0.35)
|
||||
gs = self._game_stats
|
||||
skip_until = getattr(gs, "_merc_resurrect_skip_until", 0)
|
||||
if not merc_visible and gs._game_counter < skip_until:
|
||||
|
||||
+7
-2
@@ -130,7 +130,8 @@ class IChar:
|
||||
success_func: Callable = None,
|
||||
timeout: float = 8,
|
||||
threshold: float = 0.68,
|
||||
telekinesis: bool = False
|
||||
telekinesis: bool = False,
|
||||
roi: list = None
|
||||
) -> bool:
|
||||
"""
|
||||
Finds any template from the template finder and interacts with it
|
||||
@@ -138,6 +139,10 @@ class IChar:
|
||||
:param success_func: Function that will return True if the interaction is successful e.g. return True when loading screen is reached, defaults to None
|
||||
:param timeout: Timeout for the whole template selection, defaults to None
|
||||
:param threshold: Threshold which determines if a template is found or not. None will use default form .ini files
|
||||
:param roi: Optional [left, top, width, height] to restrict the search. Pass
|
||||
ui_roi["cut_skill_bar"] for WORLD objects: the HUD is static, so a template
|
||||
that happens to resemble part of it wins every frame and the real object is
|
||||
never clicked. Cost a whole evening of A5_WP failures on 2026-08-27.
|
||||
:return: True if success. False otherwise
|
||||
"""
|
||||
templates = template_type if isinstance(template_type, list) else [template_type]
|
||||
@@ -148,7 +153,7 @@ class IChar:
|
||||
wait(0.2, 0.3)
|
||||
start = time.time()
|
||||
while timeout is None or (time.time() - start) < timeout:
|
||||
template_match = template_finder.search(template_type, grab(), threshold=threshold)
|
||||
template_match = template_finder.search(template_type, grab(), threshold=threshold, roi=roi)
|
||||
if template_match.valid:
|
||||
Logger.debug(f"Select {template_match.name} ({template_match.score*100:.1f}% confidence)")
|
||||
mouse.move(*template_match.center_monitor)
|
||||
|
||||
@@ -376,6 +376,7 @@ class Config:
|
||||
"runs_per_stash": False if not self._select_val("char", "runs_per_stash") else int(self._select_val("char", "runs_per_stash")),
|
||||
"runs_per_repair": False if not self._select_val("char", "runs_per_repair") else int(self._select_val("char", "runs_per_repair")),
|
||||
"repair_npc": self._select_optional("char", "repair_npc", "a4_halbu").strip().lower(),
|
||||
"resurrect_npc": self._select_optional("char", "resurrect_npc", "").strip().lower(),
|
||||
"gamble_items": False if not self._select_val("char", "gamble_items") else self._select_val("char", "gamble_items").replace(" ","").split(","),
|
||||
"sell_junk": bool(int(self._select_val("char", "sell_junk"))),
|
||||
"protect_shields_from_sell": bool(int(self._select_optional("char", "protect_shields_from_sell", "1"))),
|
||||
|
||||
+15
-2
@@ -7,6 +7,7 @@ import template_finder
|
||||
from utils.misc import wait
|
||||
from ui_manager import ScreenObjects, is_visible
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
from input_layer import mouse
|
||||
|
||||
|
||||
@@ -146,11 +147,23 @@ class A5(IAct):
|
||||
deadline = time.time() + self._OPEN_WP_BUDGET_S
|
||||
found_wp_func = lambda: is_visible(ScreenObjects.WaypointLabel)
|
||||
|
||||
# The waypoint is a WORLD object, so never let the HUD compete. main's recaptured
|
||||
# a5_wp.png matches the belt/mana-orb area at 0.966 — above threshold and higher
|
||||
# than the real stone — so every search locked onto (980, 656), clicked the HUD,
|
||||
# and open_wp burned ~83s escalating through anchors before giving up. That took
|
||||
# buy_consumables to 222-253s and tripped the 240s maintenance timeout.
|
||||
_wp_roi = Config().ui_roi["cut_skill_bar"]
|
||||
# Search BOTH waypoint captures and take the best. main recaptured a5_wp.png
|
||||
# (125x112); on this client it only reaches ~0.50-0.58 on the real stone while the
|
||||
# older capture (a5_wp_2.png, 201x120) historically hit 0.73-0.98. Rather than pick
|
||||
# a winner and break the other setup, let best_match decide per frame.
|
||||
_wp_tpl = ["A5_WP", "A5_WP_2"]
|
||||
|
||||
def _try_click_wp(threshold: float = 0.55, require_visible: bool = False) -> bool:
|
||||
wait(0.4, 0.6)
|
||||
# Direct full-screen scan first — if the WP stone is on screen, walk onto
|
||||
# it and click, regardless of how we got here.
|
||||
wp_match = template_finder.search("A5_WP", grab(), threshold=threshold)
|
||||
wp_match = template_finder.search(_wp_tpl, grab(), threshold=threshold, roi=_wp_roi, best_match=True)
|
||||
if not wp_match.valid:
|
||||
# Nothing on screen. For the speculative pre-scan below, stop here:
|
||||
# calling select_by_template anyway just spins out its 4s timeout and
|
||||
@@ -163,7 +176,7 @@ class A5(IAct):
|
||||
else:
|
||||
self._char.move(wp_match.center_monitor, force_move=True)
|
||||
wait(0.4, 0.6)
|
||||
return self._char.select_by_template("A5_WP", found_wp_func, threshold=threshold, timeout=4.0, telekinesis=True)
|
||||
return self._char.select_by_template(_wp_tpl, found_wp_func, threshold=threshold, timeout=4.0, telekinesis=True, roi=_wp_roi)
|
||||
|
||||
# 0) Immediate direct WP scan — after TP-back from Pindle the char often spawns
|
||||
# near the WP and a quick scan avoids all pathing issues from stale curr_loc.
|
||||
|
||||
@@ -297,6 +297,30 @@ class TownManager:
|
||||
if curr_act is None:
|
||||
Logger.error(f"resurrect: could not determine act from location {curr_loc}")
|
||||
return False
|
||||
# Prefer A4 Tyrael over A5 Qual-Kehk when configured. Tyrael stands on a fixed spot
|
||||
# next to the A4 waypoint and detects reliably; Qual-Kehk is the worst NPC in the
|
||||
# route. Measured over 2026-08-27:
|
||||
# A4 Tyrael ok 8.2s, ok 24.9s - 0 errors, ever
|
||||
# A5 Qual-Kehk fail 113.6s, 52.0s, 163.1s, 72.6s - 43 errors
|
||||
# A failed Qual-Kehk hunt costs more than the whole A5->A4 waypoint trip, and one
|
||||
# 163s hunt took the town visit past the 240s maintenance timeout and killed the
|
||||
# game outright.
|
||||
resurrect_pref = (Config().char.get("resurrect_npc") or "").lower()
|
||||
if curr_act == Location.A5_TOWN_START and resurrect_pref in ("a4_tyrael", "tyrael", "act4"):
|
||||
Logger.info("Resurrect preference is Act 4 Tyrael — traveling from A5")
|
||||
a4_loc = self.go_to_act(4, curr_loc)
|
||||
if a4_loc:
|
||||
set_panel_check_paused(True)
|
||||
res = self._acts[Location.A4_TOWN_START].resurrect(a4_loc)
|
||||
if res:
|
||||
return res
|
||||
set_panel_check_paused(False)
|
||||
# Travelled but failed — leave the breadcrumb so the caller re-anchors to
|
||||
# A4 rather than assuming A5 (Bug 9).
|
||||
self.last_known_loc = a4_loc
|
||||
Logger.warning("Resurrect: Tyrael failed after travelling to A4")
|
||||
return False
|
||||
Logger.warning("Resurrect: could not travel to A4 — falling back to the current act")
|
||||
# check if we can resurrect in current act
|
||||
if self._acts[curr_act].can_resurrect():
|
||||
set_panel_check_paused(True)
|
||||
|
||||
Reference in New Issue
Block a user