211 lines
9.7 KiB
Python
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)
|