"""Normal story AP sweep. Reference: baas-reference/module/explore_tasks/sweep_task.py and task_utils.py (to_region/to_normal_event + an OCR-driven per-stage claim loop). That reference flow needs OCR (to read the current region number and match stage-name text via swipe_search_target_str) and per-locale template assets we don't have. This client exposes a much simpler path for the same goal (burn AP via already-3-starred stages): each stage's own info panel has a "掃討" (sweep) sub-panel with a count stepper (MIN/-/+/MAX) and a start button. So instead of porting the OCR-based region/stage lookup, this picks the latest unlocked region by spamming the "next region" arrow until it stops advancing (a plain state-change check, no OCR), then picks one of its stages essentially at random (see _pick_random_stage_row) and sweeps it with the in-game MAX count. """ import random from ba_auto import navigation OPEN_RETRIES = 3 POST_SWEEP_DISMISS_ROUNDS = 6 STAGE_MODAL_DIM_MAX_CHANNEL = 150 MAX_BUTTON_RETRIES = 3 MODAL_CLOSE_RETRIES = 3 def _is_stage_modal_open(driver, config): r, g, b = driver.color_at(*config.STAGE_MODAL_PROBE) return r < STAGE_MODAL_DIM_MAX_CHANNEL and g < STAGE_MODAL_DIM_MAX_CHANNEL and b < STAGE_MODAL_DIM_MAX_CHANNEL def _count_raised_above_one(driver, config): # The "-" stepper button is flat grey while count == 1 (its default, # disabled at the minimum) and turns vivid orange once raised -- cheap # way to confirm the MAX click actually registered without needing OCR # on the count itself. r, g, b = driver.color_at(*config.SWEEP_MINUS_BUTTON_PROBE) return r > 200 and g < 180 and b < 100 def _open_task_screen(driver, config): for attempt in range(1, OPEN_RETRIES + 1): driver.click(*config.WORK_ICON) driver.wait(2) if navigation.is_on_subscreen(driver): break print(f"[story_sweep] work hub not detected after click (attempt {attempt}/{OPEN_RETRIES})") else: return False for attempt in range(1, OPEN_RETRIES + 1): driver.click(*config.TASK_CARD) driver.wait(2) if navigation.is_on_subscreen(driver): return True print(f"[story_sweep] task screen not detected after click (attempt {attempt}/{OPEN_RETRIES})") return False def _go_to_latest_region(driver, config): print("[story_sweep] advancing to the latest unlocked region") x, y = config.REGION_RIGHT_ARROW for _ in range(config.REGION_RIGHT_ARROW_MAX_CLICKS): driver.click(x, y) driver.wait(1) def _pick_random_stage_row(driver, config): # Scrolling this list to either extreme always shows exactly 4 full # stage rows, since every region has at least 5 stages -- pick one # extreme at random, then a random one of its 4 rows. This isn't # perfectly uniform across a region's 5-6 stages (the middle ones are # reachable from both extremes and so are somewhat more likely), but it # avoids OCR or generic scroll-enumeration entirely. x, y = config.STAGE_LIST_SCROLL_POINT if random.random() < 0.5: driver.scroll(x, y, "up", config.STAGE_LIST_SCROLL_CLICKS) row_y = random.choice(config.STAGE_ROWS_AT_TOP_Y) else: driver.scroll(x, y, "down", config.STAGE_LIST_SCROLL_CLICKS) row_y = random.choice(config.STAGE_ROWS_AT_BOTTOM_Y) driver.wait(0.5) return row_y def _dismiss_sweep_result(driver, config): # Sweep completion shows a reward summary that needs dismissing; the # exact number of screens varies with what dropped. Once the underlying # 任務情報 modal reappears, its "Enter" hotkey is bound to the live # "任務開始" (start manual mission) button, not a no-op -- confirmed live # that pressing Enter there would burn AP on a real battle attempt. So # check for the modal's return before every press and stop immediately, # rather than trusting a fixed round count to line up exactly with the # number of reward screens. for _ in range(POST_SWEEP_DISMISS_ROUNDS): if _is_stage_modal_open(driver, config): return driver.keypress("Return") driver.wait(1.5) def _close_stage_modal(driver, config): # Escape does not close this modal (confirmed live: it stayed open with # focus on the live "任務開始" button after two Escape presses) -- the # only reliable close path is clicking its own X button, verified. for _ in range(MODAL_CLOSE_RETRIES): if not _is_stage_modal_open(driver, config): return True driver.click(*config.STAGE_MODAL_CLOSE_BUTTON) driver.wait(1) return not _is_stage_modal_open(driver, config) def run(driver, config): driver.focus_game() if not _open_task_screen(driver, config): print("[story_sweep] could not confirm task screen is open, aborting without pressing further keys") return _go_to_latest_region(driver, config) row_y = _pick_random_stage_row(driver, config) driver.click(config.STAGE_ENTER_X, row_y) driver.wait(2) if not _is_stage_modal_open(driver, config): print("[story_sweep] stage info panel not detected, aborting") if navigation.is_on_subscreen(driver): driver.keypress("Escape") driver.wait(1.5) return for attempt in range(1, MAX_BUTTON_RETRIES + 1): driver.click(*config.SWEEP_MAX_BUTTON) driver.wait(0.8) if _count_raised_above_one(driver, config): break print(f"[story_sweep] MAX click not detected (attempt {attempt}/{MAX_BUTTON_RETRIES})") else: print("[story_sweep] could not confirm sweep count was raised, aborting without spending AP") _close_stage_modal(driver, config) if navigation.is_on_subscreen(driver): driver.keypress("Escape") driver.wait(1.5) return driver.click(*config.SWEEP_START_BUTTON) driver.wait(2) print("[story_sweep] sweep started, waiting for results") _dismiss_sweep_result(driver, config) if not _close_stage_modal(driver, config): print("[story_sweep] warning: could not confirm stage info modal closed -- leaving it open rather than pressing further keys blindly") return if navigation.is_on_subscreen(driver): driver.keypress("Escape") driver.wait(1.5) print("[story_sweep] Done.")