"""Battle Pass (バトルパス) mission-point and level-reward claim. Ported from baas-reference's module/collect_pass_reward.py::implement(): main_page_to_pass_menu(self) # home -> pass main menu to_page_pass_mission(self) # pass main menu -> mission sub-screen collect_reward(self) # claim-all mission points to_page_pass_menu(self) # mission sub-screen -> pass main menu collect_reward(self) # claim-all pass-level reward detect_statistics(self) # OCR level/point/weekly-point readout detect_statistics is not ported: it only feeds the reference's own local stat-tracking file, and nothing in this project's flow consumes those numbers -- there is no OCR-driven decision anywhere in this task, so porting it would be unused OCR setup, not skipped OCR-driven navigation (the thing CLAUDE.md's OCR policy actually guards against). Reference navigation is template-image-driven via core/picture.py:: co_detect (img_possibles/img_ends against ~20 image assets this project doesn't have); the local equivalent signals are ported as color probes instead (config.BATTLE_PASS_*), the same substitution this project already uses for navigation.is_on_subscreen/is_modal_open. Those two generic probes do NOT fire correctly on either battle-pass screen -- both read exactly like true home (no bright subscreen header, no dark modal backdrop), confirmed live 2026-07-29 -- so this module carries its own local probes (BATTLE_PASS_INSIDE_PROBES/BATTLE_PASS_MISSION_TAB_PROBES) rather than reusing those, and explicitly presses Escape back to home at the end instead of relying on ba_daily.py's generic post-task navigation.return_to_home() cleanup, which would otherwise read the pass menu as "already home" and do nothing -- leaving the game stuck there for whatever task runs next in the same preset. The claim-all ("一括受取") button reuses stamina.py's exact bright- yellow-vs-grey enabled/disabled signature (same button style, confirmed live at this project's own coordinates) and its reward-reveal-popup dismiss convention (an extra Enter press after the claim press, harmless no-op if nothing is actually showing) -- both button instances on this task's two screens behaved the same way live, including a real level-up "報酬獲得!" card that needed that second Enter to clear. This is a pure reclaim like mailbox/cafe/stamina/gem_shop/circle: nothing here spends AP/credits/tickets, so it belongs in ba_daily.py's DEFAULT_ORDER rather than staying opt-in. """ OPEN_RETRIES = 3 MISSION_RETRIES = 3 CLAIM_MAX_ROUNDS = 3 EXIT_MAX_ROUNDS = 4 def _claim_enabled(driver, config): r, g, b = driver.color_at(*config.BATTLE_PASS_CLAIM_PROBE) return r > 230 and (r - b) > 100 def _inside_battle_pass(driver, config): for r, g, b in driver.colors_at(config.BATTLE_PASS_INSIDE_PROBES): target_r, target_g, target_b = config.BATTLE_PASS_INSIDE_RGB tol = config.BATTLE_PASS_INSIDE_TOLERANCE if abs(r - target_r) > tol or abs(g - target_g) > tol or abs(b - target_b) > tol: return False return True def _on_mission_screen(driver, config): for r, g, b in driver.colors_at(config.BATTLE_PASS_MISSION_TAB_PROBES): if not (r > config.BATTLE_PASS_MISSION_TAB_MIN_CHANNEL and g > config.BATTLE_PASS_MISSION_TAB_MIN_CHANNEL and b > config.BATTLE_PASS_MISSION_TAB_MIN_CHANNEL): return False return True def _open_pass_menu(driver, config): for attempt in range(1, OPEN_RETRIES + 1): driver.click(*config.BATTLE_PASS_HOME_BANNER) driver.wait(2.5) # loading splash between home and the pass menu if _inside_battle_pass(driver, config): return True print(f"[battle_pass] pass menu not detected after click (attempt {attempt}/{OPEN_RETRIES})") return False def _open_mission_screen(driver, config): for attempt in range(1, MISSION_RETRIES + 1): driver.click(*config.BATTLE_PASS_MISSION_BUTTON) driver.wait(1.5) if _on_mission_screen(driver, config): return True print(f"[battle_pass] mission screen not detected after click (attempt {attempt}/{MISSION_RETRIES})") return False def _ensure_pass_menu(driver, config, max_attempts=3): """Confirm we're on the pass main menu specifically (not the mission sub-screen, not home), self-healing rather than just retrying Escape -- live-tested 2026-07-29 found Escape from the mission screen doesn't always land on the pass menu the way it did during calibration; one real run overshot straight past it to home instead (still an Escape-per-subscreen game, just not the fixed 2-level distance calibration assumed). If we're still on the mission screen, Escape again; if we've landed all the way back at home, re-open the pass menu from its home banner rather than giving up on the pass-level claim. """ for _ in range(max_attempts): if _inside_battle_pass(driver, config) and not _on_mission_screen(driver, config): return True if _on_mission_screen(driver, config): driver.keypress("Escape") driver.wait(1.5) continue if not _open_pass_menu(driver, config): return False return _inside_battle_pass(driver, config) and not _on_mission_screen(driver, config) def _claim_all(driver, config): claimed_any = False for _ in range(CLAIM_MAX_ROUNDS): if not _claim_enabled(driver, config): break print("[battle_pass] claiming reward") driver.keypress("Return") driver.wait(1.5) # dismiss the reward-reveal ("報酬獲得!"/TOUCH) popup; harmless # no-op if nothing is actually showing, same assumption as # stamina.py's identical second press driver.keypress("Return") driver.wait(1.5) claimed_any = True return claimed_any def run(driver, config): driver.focus_game() if not _open_pass_menu(driver, config): print("[battle_pass] could not confirm the pass menu opened, aborting without pressing further keys") return if _open_mission_screen(driver, config): if not _claim_all(driver, config): print("[battle_pass] no mission rewards to claim") driver.keypress("Escape") driver.wait(1.5) else: print("[battle_pass] could not confirm the mission screen opened, skipping mission claim") if _ensure_pass_menu(driver, config): if not _claim_all(driver, config): print("[battle_pass] no pass-level reward to claim") else: print("[battle_pass] could not confirm the pass menu for the level-reward claim, skipping") for _ in range(EXIT_MAX_ROUNDS): if not _inside_battle_pass(driver, config): break driver.keypress("Escape") driver.wait(1.5) print("[battle_pass] Done.")