ba-auto-daily/ba_auto/tasks/battle_pass.py

211 lines
9.7 KiB
Python

"""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.
**Real bug, found from a live `daily` preset run (2026-07-31, see
script_error.md)**: the exit loop below used to stop as soon as
_inside_battle_pass(driver, config) read False, with no further
verification and no failure print. That is a NEGATIVE condition only -- it
proves we're no longer showing battle pass's own dark footer band, not that
we've actually reached true home. A run landed on event_sweep straight
after battle_pass with three consecutive "event screen not detected after
click" retries and no warning logged in between, meaning both this task's
own exit loop AND ba_daily.py's generic post-task navigation.return_to_home
cleanup (which the module docstring above already documents can't tell the
pass menu apart from home either) silently agreed we were home when we
almost certainly weren't -- most likely a still-open reward-reveal card
(e.g. a level-up bundling more than one item, needing more than the single
extra dismiss Enter _claim_all already sends) darkening/covering that
footer probe just enough to misread as "left". Fixed by also requiring
navigation.is_home_nav_bar_visible(driver) -- the same positive bottom-nav-
bar signal login.py needed for the identical class of false-positive -- and
printing a clear warning if the loop still can't confirm true home after
EXIT_MAX_ROUNDS, so a stuck battle pass screen shows up in logs instead of
masquerading as "Done." Not yet re-confirmed live against a real repeat of
this exact stuck state (would need a real level-up to reproduce on demand).
"""
from ba_auto import navigation
OPEN_RETRIES = 3
MISSION_RETRIES = 3
CLAIM_MAX_ROUNDS = 3
DISMISS_EXTRA_PRESSES = 2
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 -- pressed
# DISMISS_EXTRA_PRESSES times rather than once, each a harmless
# no-op if nothing is actually showing (same assumption as
# stamina.py's own single extra press). Widened to more than one
# after a real stuck run (see this module's docstring) where the
# leading suspect is a level-up bundling more than one item into
# separate stacked cards, which a single dismiss press wouldn't
# fully clear -- leaving a card open that the next round's
# _claim_enabled check (covered by that same card) would then
# misread as "nothing left to claim" rather than "still blocked".
for _ in range(DISMISS_EXTRA_PRESSES):
driver.keypress("Return")
driver.wait(1.2)
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")
if not _exit_to_home(driver, config):
print("[battle_pass] warning: could not confirm return to home screen after exiting the pass menu")
print("[battle_pass] Done.")
def _confirmed_home(driver, config):
# Both conditions matter: _inside_battle_pass alone is a negative check
# (only proves the pass-menu footer band is gone, not that true home was
# actually reached -- see this module's docstring for the real stuck-run
# this fooled), and is_home_nav_bar_visible alone was calibrated against
# login.py's pre-login states, not battle pass's own screens.
return not _inside_battle_pass(driver, config) and navigation.is_home_nav_bar_visible(driver)
def _exit_to_home(driver, config):
for _ in range(EXIT_MAX_ROUNDS):
if _confirmed_home(driver, config):
return True
driver.keypress("Escape")
driver.wait(1.5)
return _confirmed_home(driver, config)