Files
my-botty/src/npc_manager.py
T
alexandClaude Fable 5 6301490cc4 fix: act-state verification, NPC dialogue confirm wait, stats integrity
Root causes from the 2026-06-09 session (51 games, 45 failed):

- Act desync: add TownManager.detect_current_act() and
  Bot._verify_town_location(); every maintenance/end-run retry and
  fallback now verifies the physical act instead of hardcoding town
  starts. open_wp/go_to_act self-heal act mismatches. Never run A1
  pathing when travel to A1 failed.
- NPC dialogue: poll action buttons up to 2.5s after click instead of
  a single-frame check (premature retry click was closing the dialog).
- Stats integrity: log_end_game skips duplicate calls (phantom 0s
  "successful" games were resetting the consecutive-fail breaker);
  clear stale failure reason at game start; set chicken flag before
  bot.stop() so chickens are no longer labeled "Bot stopped".
- Repair: prefer in-act Larzuk over cross-act Halbu trip (Halbu
  detection failed 100% last session and desynced the act state).

Documented as Bugs 9-12 in CLAUDE.md.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 08:23:10 +02:00

477 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import time
import os
import numpy as np
from input_layer import keyboard
import template_finder
from config import Config
from screen import grab
from ui_manager import ScreenObjects, center_mouse, is_visible, wait_until_hidden
from utils.misc import color_filter, wait
from utils.log_rotation import safe_imwrite
from logger import Logger
from input_layer import mouse
from math import sqrt
class Npc:
#A1
KASHYA = "kashya"
CHARSI = "charsi"
AKARA = "akara"
CAIN = "cain"
#A2
FARA = "fara"
DROGNAN = "droganan"
LYSANDER = "lysander"
#A3
ORMUS = "ormus"
#A4
TYRAEL = "tyrael"
JAMELLA = "jamella"
HALBU = "halbu"
#A5
QUAL_KEHK = "qual_kehk"
MALAH = "malah"
LARZUK = "larzuk"
ANYA = "anya"
def _build_npcs():
"""Build the npcs dict — deferred so the module can import on systems
missing template assets (e.g. headless Linux or incomplete clones)."""
return {
Npc.QUAL_KEHK: {
"name_tag_white": color_filter(template_finder.get_template("QUAL_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("QUAL_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"resurrect": {
"white": color_filter(template_finder.get_template("RESURRECT"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("RESURRECT_BLUE"), Config().colors["blue"])[1],
"red": color_filter(template_finder.get_template("RESURRECT"), Config().colors["red"])[1],
}
},
"template_group": ["QUAL_0", "QUAL_45", "QUAL_45_B", "QUAL_90", "QUAL_135", "QUAL_135_B", "QUAL_135_C", "QUAL_180", "QUAL_180_B", "QUAL_225", "QUAL_225_B", "QUAL_270", "QUAL_315"],
"roi": [225, 57, (850-225), (442-57)],
"poses": [[350, 140], [310, 268], [385, 341], [481, 196], [502, 212], [771, 254]]
},
Npc.MALAH: {
"name_tag_white": color_filter(template_finder.get_template("MALAH_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("MALAH_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["MALAH_FRONT", "MALAH_BACK", "MALAH_45", "MALAH_SIDE", "MALAH_SIDE_2"],
"roi": [383, 193, (762-383), (550-193)],
"poses": [[445, 485], [526, 473], [602, 381], [623, 368], [641, 323], [605, 300], [622, 272], [638, 284], [677, 308], [710, 288]]
},
Npc.LARZUK: {
"name_tag_white": color_filter(template_finder.get_template("LARZUK_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("LARZUK_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade_repair": {
"white": color_filter(template_finder.get_template("TRADE_REPAIR"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_REPAIR_BLUE"), Config().colors["blue"])[1],
}
},
"roi": [570, 70, (1038-570), (290-70)],
"template_group": ["LARZUK_FRONT", "LARZUK_BACK", "LARZUK_SIDE", "LARZUK_SIDE_2", "LARZUK_SIDE_3"],
"poses": [[733, 192], [911, 143]]
},
Npc.ANYA: {
"name_tag_white": color_filter(template_finder.get_template("ANYA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("ANYA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["ANYA_FRONT", "ANYA_BACK", "ANYA_SIDE"]
},
Npc.TYRAEL: {
"name_tag_white": color_filter(template_finder.get_template("TYRAEL_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("TYRAEL_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"resurrect": {
"white": color_filter(template_finder.get_template("RESURRECT"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("RESURRECT_BLUE"), Config().colors["blue"])[1],
"red": color_filter(template_finder.get_template("RESURRECT"), Config().colors["red"])[1],
}
},
"roi": [569, 86, (852-569), (357-86)],
"template_group": ["TYRAEL_1", "TYRAEL_2"]
},
Npc.ORMUS: {
"name_tag_white": color_filter(template_finder.get_template("ORMUS_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("ORMUS_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
}
},
"roi": [444, 13, (816-444), (331-13)],
"poses": [[526, 131], [602, 192], [698, 218], [756, 188]],
"template_group": ["ORMUS_0", "ORMUS_1", "ORMUS_2", "ORMUS_3", "ORMUS_4", "ORMUS_5"]
},
Npc.FARA: {
"name_tag_white": color_filter(template_finder.get_template("FARA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("FARA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade_repair": {
"white": color_filter(template_finder.get_template("TRADE_REPAIR"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_REPAIR_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["FARA_LIGHT_1", "FARA_LIGHT_2", "FARA_LIGHT_3", "FARA_LIGHT_4", "FARA_LIGHT_5", "FARA_LIGHT_6", "FARA_LIGHT_7", "FARA_LIGHT_8", "FARA_LIGHT_9", "FARA_MEDIUM_1", "FARA_MEDIUM_2", "FARA_MEDIUM_3", "FARA_MEDIUM_4", "FARA_MEDIUM_5", "FARA_MEDIUM_6", "FARA_MEDIUM_7", "FARA_DARK_1", "FARA_DARK_2", "FARA_DARK_3", "FARA_DARK_4", "FARA_DARK_5", "FARA_DARK_6", "FARA_DARK_7"]
},
Npc.DROGNAN: {
"name_tag_white": color_filter(template_finder.get_template("DROGNAN_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("DROGNAN_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["DROGNAN_FRONT", "DROGNAN_LEFT", "DROGNAN_RIGHT_SIDE"]
},
Npc.LYSANDER: {
"name_tag_white": color_filter(template_finder.get_template("LYSANDER_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("LYSANDER_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["LYSANDER_FRONT", "LYSANDER_BACK", "LYSANDER_SIDE", "LYSANDER_SIDE_2"]
},
Npc.CAIN: {
"name_tag_white": color_filter(template_finder.get_template("CAIN_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("CAIN_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"identify": {
"white": color_filter(template_finder.get_template("IDENTIFY"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("IDENTIFY_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["CAIN_0", "CAIN_1", "CAIN_2", "CAIN_3"]
},
Npc.JAMELLA: {
"name_tag_white": color_filter(template_finder.get_template("JAMELLA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("JAMELLA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
},
"gamble": {
"white": color_filter(template_finder.get_template("GAMBLE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("GAMBLE_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["JAMELLA_FRONT", "JAMELLA_BACK", "JAMELLA_SIDE", "JAMELLA_SIDE_2", "JAMELLA_SIDE_3", "JAMELLA_DRAWING"]
},
Npc.HALBU: {
"name_tag_white": color_filter(template_finder.get_template("HALBU_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("HALBU_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade_repair": {
"white": color_filter(template_finder.get_template("TRADE_REPAIR"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_REPAIR_BLUE"), Config().colors["blue"])[1],
}
},
"template_group": ["HALBU_FRONT", "HALBU_BACK", "HALBU_SIDE", "HALBU_SIDE_2"]
},
Npc.AKARA: {
"name_tag_white": color_filter(template_finder.get_template("AKARA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("AKARA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade": {
"white": color_filter(template_finder.get_template("TRADE"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_BLUE"), Config().colors["blue"])[1],
}
},
"roi": [605,176,(1004-605),(478-176)],
"poses": [[694,377], [836,378], [950,345], [869,290], [698,317]],
"template_group": ["AKARA_FRONT", "AKARA_BACK", "AKARA_SIDE", "AKARA_SIDE_2"]
},
Npc.CHARSI: {
"name_tag_white": color_filter(template_finder.get_template("CHARSI_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("CHARSI_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"trade_repair": {
"white": color_filter(template_finder.get_template("TRADE_REPAIR"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("TRADE_REPAIR_BLUE"), Config().colors["blue"])[1],
}
},
"roi": [249, 76, (543-249), (363-76)],
"poses": [[331, 227], [368, 284], [484, 174]],
"template_group": ["CHARSI_FRONT", "CHARSI_BACK", "CHARSI_SIDE", "CHARSI_SIDE_2", "CHARSI_SIDE_3"]
},
Npc.KASHYA: {
"name_tag_white": color_filter(template_finder.get_template("KASHYA_NAME_TAG_WHITE"), Config().colors["white"])[1],
"name_tag_gold": color_filter(template_finder.get_template("KASHYA_NAME_TAG_GOLD"), Config().colors["gold"])[1],
"action_btns": {
"resurrect": {
"white": color_filter(template_finder.get_template("RESURRECT"), Config().colors["white"])[1],
"blue": color_filter(template_finder.get_template("RESURRECT_BLUE"), Config().colors["blue"])[1],
"red": color_filter(template_finder.get_template("RESURRECT"), Config().colors["red"])[1],
}
},
"template_group": ["KASHYA_FRONT", "KASHYA_BACK", "KASHYA_SIDE", "KASHYA_SIDE_2"]
}
}
npcs = {}
try:
npcs = _build_npcs()
except Exception:
pass # npcs stays empty on headless/incomplete setups — populated lazily at runtime
def _action_btns_visible(npc_key: str, img) -> bool:
"""Return True if any action button for *npc_key* is detectable in *img*."""
if "action_btns" not in npcs.get(npc_key, {}):
return False
roi = Config().ui_roi["cut_skill_bar"]
for btn_key, btn_colors in npcs[npc_key]["action_btns"].items():
for color_name in ("white", "blue"):
if color_name not in btn_colors:
continue
try:
_, filtered = color_filter(img, Config().colors[color_name])
res = template_finder.search(btn_colors[color_name], filtered, 0.78, roi=roi)
if res.valid:
Logger.debug(f"NPC {npc_key} - action btn '{btn_key}' ({color_name}) detected (score {res.score:.3f})")
return True
except Exception:
pass
first_color = next(iter(btn_colors))
res = template_finder.search(btn_colors[first_color], img, 0.78, roi=roi, use_grayscale=True)
if res.valid:
Logger.debug(f"NPC {npc_key} - action btn '{btn_key}' (gray) detected (score {res.score:.3f})")
return True
return False
def _wait_action_btns(npc_key: str, timeout: float = 2.5) -> bool:
"""Poll for the NPC dialogue action buttons for up to *timeout* seconds.
A single-frame check misses slow dialogue fade-in animations."""
start = time.time()
while (time.time() - start) < timeout:
if _action_btns_visible(npc_key, grab()):
return True
wait(0.2, 0.3)
return False
def escape_dialogue(img) -> np.ndarray:
while is_visible(ScreenObjects.NPCDialogue, img):
keyboard.send("esc")
if wait_until_hidden(ScreenObjects.NPCDialogue, 0.5):
break
img = grab()
return img
def open_npc_menu(npc_key: Npc) -> bool:
global npcs
from screen import convert_monitor_to_screen
roi = Config().ui_roi["cut_skill_bar"]
roi_npc_search = Config().ui_roi["search_npcs"]
# Name tag match threshold — lower than the body template threshold because D2R
# name tag rendering can differ slightly from saved templates.
NAME_TAG_THRESHOLD = 0.26
# Timeout: 12 s per NPC search attempt (8 s was too short with body+pose fallback).
NPC_SEARCH_TIMEOUT = 12.0
# If inventory is open, close it before clicking. A4 NPCs (Cain, Halbu, Jamella,
# Tyrael) sit in the top-right zone that overlaps with equipped_inventory_area, so
# the click-safety guard blocks them when the inventory panel is still visible.
if template_finder.search(
"INVENTORY_GOLD_BTN", grab(), threshold=0.8,
roi=Config().ui_roi["gold_btn"], use_grayscale=True
).valid:
keyboard.send(Config().char["inventory_screen"])
wait(0.3, 0.4)
# Search for npc name tags by hovering to all template locations that are found
start = time.time()
attempts = 0
while (time.time() - start) < NPC_SEARCH_TIMEOUT:
img = grab()
# Fast path: if the target NPC's dialogue is already open, return immediately.
if _action_btns_visible(npc_key, img):
Logger.debug(f"NPC {npc_key} - dialogue already open (action buttons visible)")
return True
results = []
for key in npcs[npc_key]["template_group"]:
if attempts == 0 and "roi" in npcs[npc_key] and (time.time() - start) < 6:
roi_npc = npcs[npc_key]["roi"]
else:
roi_npc = roi_npc_search
res = template_finder.search(key, img, threshold=0.35, roi=roi_npc)
if res.valid:
is_unique = True
for r in results:
if (abs(r["pos"][0] - res.center_monitor[0]) + abs(r["pos"][1] - res.center_monitor[1])) < 22:
is_unique = False
break
if is_unique:
min_dist_val = 10000
if attempts == 0 and "poses" in npcs[npc_key]:
# find distance between template match and nearest pose (([x2] - x1)**2 + (y2 - y1)**2)
for pose in npcs[npc_key]["poses"]:
dist = sqrt((res.center_monitor[0] - pose[0])**2 + (res.center_monitor[1] - pose[1])**2)
min_dist_val = dist if dist < min_dist_val else min_dist_val
results.append({"pos": res.center_monitor, "score": res.score, "combo": min_dist_val / (res.score**2), "min_dist": min_dist_val})
# 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)
# Build a small ROI above the hover position where the NPC name tag appears.
# This prevents distant false-positive text from the wide screen ROI from
# triggering a click. NPC name tags appear 30130 px above the NPC sprite.
hover_screen = convert_monitor_to_screen(result["pos"])
name_tag_roi = [
max(0, hover_screen[0] - 120),
max(0, hover_screen[1] - 160),
240,
140,
]
_, 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, NAME_TAG_THRESHOLD, roi=name_tag_roi)
res_g = template_finder.search(npcs[npc_key]["name_tag_gold"], filtered_inp_g, NAME_TAG_THRESHOLD, roi=name_tag_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, NAME_TAG_THRESHOLD, roi=name_tag_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}, Body score: {result['score']:.3f}")
# Decision: when to click the NPC
# 1. Name tag confirmed (white or gold) AND position is within NPC's ROI.
# High white-text false positives (0.98+) occur at positions far outside the
# ROI (e.g. x=200 while Akara's ROI starts at x=605). Gating on in_npc_roi
# eliminates these without affecting true matches.
# 2. Body template matched well AND we're at a known pose location.
if "roi" in npcs[npc_key] and attempts == 0:
npc_roi = npcs[npc_key]["roi"]
in_npc_roi = (npc_roi[0] <= hover_screen[0] <= npc_roi[0] + npc_roi[2] and
npc_roi[1] <= hover_screen[1] <= npc_roi[1] + npc_roi[3])
else:
in_npc_roi = True
name_tag_confirmed = (res_w.valid or res_g.valid) and in_npc_roi
body_confident = result["score"] >= 0.40
pose_confirmed = "poses" in npcs[npc_key] and result["min_dist"] < 150
if name_tag_confirmed:
Logger.info(f"Clicking on {npc_key} at {mouse.get_position()} (name tag confirmed)")
elif body_confident and pose_confirmed:
Logger.info(f"Clicking on {npc_key} at {mouse.get_position()} (body+pose confirmed, name tag score {res_w.score:.3f})")
else:
Logger.debug(f"NPC {npc_key} - skipping pos {result['pos']}: name_tag={name_tag_confirmed} in_roi={in_npc_roi} body={body_confident} pose={pose_confirmed}")
continue
mouse.click(button="left")
attempts += 1
wait(0.5, 0.7)
# Confirm dialogue opened by checking for action buttons — more reliable than
# the NPCDialogue border template (stale) or gold name tag template (also stale).
# Poll for a couple of seconds: the dialogue fades in and a single-frame check
# can miss it, and a premature retry click would CLOSE the open dialogue.
if _wait_action_btns(npc_key, timeout=2.5):
Logger.debug(f"NPC {npc_key} - dialogue open (action buttons visible)")
return True
# Retry once: first click sometimes just turns the NPC toward you.
if attempts <= 2:
Logger.debug(f"NPC {npc_key} - action buttons not found, retrying click on body")
mouse.move(*result["pos"], randomize=3, delay_factor=[0.2, 0.3])
wait(0.2, 0.3)
mouse.click(button="left")
wait(0.5, 0.7)
if _wait_action_btns(npc_key, timeout=2.5):
Logger.debug(f"NPC {npc_key} - dialogue open after retry (action buttons visible)")
return True
Logger.warning(f"NPC {npc_key} - clicked but no action buttons found after retry")
else:
Logger.warning(f"NPC {npc_key} - clicked but no action buttons found")
wait(0.1) # Brief pause before next frame grab if no matches were valid
Logger.error(f"open_npc_menu: timed out finding {npc_key} — saving screenshot")
if Config().general["info_screenshots"]:
safe_imwrite("./log/screenshots/info/info_npc_menu_timeout_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab())
return False
def press_npc_btn(npc_key: Npc, action_btn_key: str) -> bool:
global npcs
Logger.info(f"press_npc_btn: looking for '{action_btn_key}' button for NPC '{npc_key}'")
for threshold in (0.85, 0.78):
img = grab()
_, filtered_inp_w = color_filter(img, Config().colors['white'])
res = template_finder.search(
npcs[npc_key]['action_btns'][action_btn_key]['white'],
filtered_inp_w, threshold, roi=Config().ui_roi['cut_skill_bar']
)
Logger.debug(f"press_npc_btn: white search at threshold {threshold} score={res.score:.3f}")
if not res.valid and 'blue' in npcs[npc_key]['action_btns'][action_btn_key]:
_, filtered_inp_b = color_filter(img, Config().colors['blue'])
res = template_finder.search(
npcs[npc_key]['action_btns'][action_btn_key]['blue'],
filtered_inp_b, threshold, roi=Config().ui_roi['cut_skill_bar']
)
Logger.debug(f"press_npc_btn: blue search at threshold {threshold} score={res.score:.3f}")
if not res.valid:
res = template_finder.search(
npcs[npc_key]['action_btns'][action_btn_key]['white'],
img, threshold, roi=Config().ui_roi['cut_skill_bar'],
use_grayscale=True,
)
Logger.debug(f"press_npc_btn: grayscale search at threshold {threshold} score={res.score:.3f}")
if res.valid:
Logger.info(f"press_npc_btn: found '{action_btn_key}' button, clicking")
mouse.move(*res.center_monitor, randomize=3, delay_factor=[1.0, 1.5])
wait(0.2, 0.4)
mouse.click(button='left')
wait(0.2, 0.3)
center_mouse()
return True
if 'red' in npcs[npc_key]['action_btns'][action_btn_key]:
img = grab()
_, filtered_inp_r = color_filter(img, Config().colors['red'])
res = template_finder.search(
npcs[npc_key]['action_btns'][action_btn_key]['red'],
filtered_inp_r, 0.78, roi=Config().ui_roi['cut_skill_bar']
)
if res.valid:
Logger.warning(f'Cannot afford {action_btn_key} (red button detected). Skipping...')
keyboard.send('esc')
wait(0.3)
return False
Logger.error(f'press_npc_btn: Could not find {action_btn_key} btn for NPC {npc_key}. Should not happen! Continue...')
keyboard.send('esc')
return False
# Testing: Stand close to Qual-Kehk or Malah and run
if __name__ == "__main__":
from screen import grab
from config import Config
import os
from input_layer import keyboard
keyboard.add_hotkey('f12', lambda: os._exit(1))
keyboard.wait("f11")
open_npc_menu(Npc.MALAH)