feat: implement Battle Pass reward claiming functionality and update configuration
This commit is contained in:
parent
8d3da6e6e1
commit
8a96ea4ffa
@ -1505,3 +1505,54 @@ LOGIN_LOADING_BUFFER_TOLERANCE = 12
|
||||
# specific, well-understood bad state (not "anything unrecognized") -- no
|
||||
# reason to wait the full 4 minutes once it's confidently identified.
|
||||
LOGIN_LOADING_BUFFER_STUCK_SECONDS = 60
|
||||
|
||||
# Battle Pass (バトルパス) -- ported from baas-reference's
|
||||
# module/collect_pass_reward.py. See ba_auto/tasks/battle_pass.py's module
|
||||
# docstring for the full reference-mapping writeup; constants below were
|
||||
# all calibrated live 2026-07-29 at this project's usual 1920x1200.
|
||||
|
||||
# Home-screen entry banner (bottom-left promo card, e.g. "シノンのバトル
|
||||
# パス SEASON 3") -- a home-screen entry point outside the bottom nav bar,
|
||||
# same idea as arena's WORK_ICON->TASK_CARD. Confirmed live: clicking here
|
||||
# opens the pass menu (after a brief loading splash).
|
||||
BATTLE_PASS_HOME_BANNER = (530, 935)
|
||||
|
||||
# "ミッション" (Mission) button inside the pass main menu, bottom-left.
|
||||
BATTLE_PASS_MISSION_BUTTON = (555, 1080)
|
||||
|
||||
# Neither navigation.is_on_subscreen nor is_modal_open 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.
|
||||
# This project-local probe instead checks a row of points along the
|
||||
# bottom edge (y=1190) that both battle-pass screens render as a flat,
|
||||
# fixed dark band (53,58,76) at EVERY sampled x, regardless of which
|
||||
# splash art or sub-screen is showing above it -- confirmed identical
|
||||
# across the pass main menu, the mission sub-screen, and both their
|
||||
# post-claim states. The true home screen has no such fixed band there at
|
||||
# all (it's character art all the way to the edge), so every sampled x
|
||||
# reads a different, brighter value instead. Used both to confirm the
|
||||
# pass menu opened at all, and, in the exit loop, to confirm we're still
|
||||
# inside SOME battle-pass screen (as opposed to home) without caring which.
|
||||
BATTLE_PASS_INSIDE_PROBES = ((10, 1190), (400, 1190), (960, 1190), (1500, 1190), (1900, 1190))
|
||||
BATTLE_PASS_INSIDE_RGB = (53, 58, 76)
|
||||
BATTLE_PASS_INSIDE_TOLERANCE = 12
|
||||
|
||||
# Distinguishes the mission sub-screen specifically from the pass main
|
||||
# menu (both satisfy BATTLE_PASS_INSIDE_PROBES above identically, since
|
||||
# they share the same footer chrome). The mission screen's own tab bar
|
||||
# ("全体/デイリー/ウィークリー/実績") renders a near-white active-tab
|
||||
# background spanning both these points; the pass main menu's own tab bar
|
||||
# ("一般CH/成長CH") sits further right and doesn't reach either x, leaving
|
||||
# whatever the character art renders there instead -- confirmed live,
|
||||
# clearly not white at either point on the pass main menu.
|
||||
BATTLE_PASS_MISSION_TAB_PROBES = ((1080, 60), (1450, 60))
|
||||
BATTLE_PASS_MISSION_TAB_MIN_CHANNEL = 245
|
||||
|
||||
# Same bright-yellow-vs-grey "一括受取" (claim-all) signature as
|
||||
# stamina.py's MISSION_CLAIM_PROBE, confirmed live at this exact point --
|
||||
# shared by BOTH battle-pass screens (the mission screen's own claim-all
|
||||
# button, and the pass main menu's own level-reward claim-all button,
|
||||
# render at the identical position). Has its own Enter keybind shown on
|
||||
# screen, same as MISSION_CLAIM_PROBE, so battle_pass.py uses a keypress
|
||||
# here rather than a coordinate click.
|
||||
BATTLE_PASS_CLAIM_PROBE = (1720, 1080)
|
||||
|
||||
File diff suppressed because one or more lines are too long
162
ba_auto/tasks/battle_pass.py
Normal file
162
ba_auto/tasks/battle_pass.py
Normal file
@ -0,0 +1,162 @@
|
||||
"""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.")
|
||||
15
ba_daily.py
15
ba_daily.py
@ -4,7 +4,7 @@ import os
|
||||
import sys
|
||||
|
||||
from ba_auto import config, driver, navigation
|
||||
from ba_auto.tasks import arena, bounty, cafe, circle, event_sweep, exit_game, gem_shop, lesson, login, mailbox, shop_common, shop_tactical, stamina, story_sweep, story_sweep_hard
|
||||
from ba_auto.tasks import arena, battle_pass, bounty, cafe, circle, event_sweep, exit_game, gem_shop, lesson, login, mailbox, shop_common, shop_tactical, stamina, story_sweep, story_sweep_hard
|
||||
|
||||
TASKS = {
|
||||
"login": login.run,
|
||||
@ -13,6 +13,7 @@ TASKS = {
|
||||
"stamina": stamina.run,
|
||||
"gem_shop": gem_shop.run,
|
||||
"circle": circle.run,
|
||||
"battle_pass": battle_pass.run,
|
||||
"story_sweep": story_sweep.run,
|
||||
# Bypasses story_sweep's campaign-active guard -- see that module's
|
||||
# docstring and config.TASK_CAMPAIGN_BADGE_RECT for why the guard exists
|
||||
@ -55,8 +56,10 @@ TASKS = {
|
||||
# way to get there from the title/loading/attendance-card states login.py
|
||||
# handles -- see that module's docstring. login.run() checks true-home
|
||||
# first on every internal poll, so this is a fast no-op on a session that's
|
||||
# already logged in, not a risky blind click every single day.
|
||||
DEFAULT_ORDER = ["login", "mailbox", "cafe", "stamina", "gem_shop", "circle"]
|
||||
# already logged in, not a risky blind click every single day. battle_pass
|
||||
# is the same free-reclaim category as gem_shop/circle -- see its own
|
||||
# module docstring.
|
||||
DEFAULT_ORDER = ["login", "mailbox", "cafe", "stamina", "gem_shop", "circle", "battle_pass"]
|
||||
|
||||
# Named multi-task sequences, run via `./ba_dailies.sh <preset-name>` --
|
||||
# distinct from DEFAULT_ORDER (the plain no-args flow, deliberately kept to
|
||||
@ -81,11 +84,11 @@ PRESETS = {
|
||||
"daily": [
|
||||
"login", "event_sweep", "cafe", "event_sweep", "circle", "lesson",
|
||||
"arena", "shop_common", "shop_tactical", "event_sweep", "bounty",
|
||||
"gem_shop", "mailbox", "stamina", "event_sweep", "exit_game",
|
||||
"gem_shop", "mailbox", "stamina", "battle_pass", "event_sweep", "exit_game",
|
||||
],
|
||||
"q4h": [
|
||||
"login", "cafe", "mailbox", "stamina", "story_sweep_hard",
|
||||
"event_sweep", "story_sweep", "exit_game"
|
||||
"login", "cafe", "mailbox", "stamina", "event_sweep",
|
||||
"story_sweep_hard", "event_sweep", "story_sweep", "exit_game"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
18
plan.md
18
plan.md
@ -1060,6 +1060,24 @@ A `None` read (OCR failure, or a full-screen ambient/cutscene state with no head
|
||||
|
||||
**Live-tested for real the same session** against the account's actual live AP (genuinely 10-11/240 at the time, well under the new floor -- no need to fabricate a low-AP scenario): confirmed `navigation.current_ap(driver)` reads the true value correctly against the live header (`10`, then `11` a few minutes later, matching the visible regen), and confirmed all three of `story_sweep.run(driver, config, force=True)`, `story_sweep_hard.run(driver, config, force=True)`, and `event_sweep.run(driver, config)` printed the new skip message and returned immediately with zero navigation -- verified via `navigation.is_on_subscreen(driver)` still reading `False` (still home) directly afterward. Also incidentally reconfirmed, while calibrating `HOME_AP_OCR_RECT`, that the game can spontaneously drop into a full-screen ambient/idle cutscene with no header chrome at all even while sitting on what `_ensure_home`/`is_on_subscreen` both still call "home" -- a plain Enter keypress cleared it back to the ordinary home view both times it was hit. Not a new bug (same class of state `cafe.py`'s `_dismiss_rank_up_if_shown` and `login.py`'s own state catalog already exist to handle), just newly relevant here since it's the specific state that makes `current_ap` correctly return `None`.
|
||||
|
||||
### Phase 25: Battle Pass (2026-07-29)
|
||||
|
||||
New feature, per explicit user request: claim mission-point rewards and pass-level rewards from the Battle Pass (バトルパス). Reference: `module/collect_pass_reward.py`'s `implement()` -- `main_page_to_pass_menu` -> `to_page_pass_mission` -> `collect_reward` (mission points) -> `to_page_pass_menu` (back) -> `collect_reward` (pass-level reward) -> `detect_statistics` (OCR level/point readout, not ported -- nothing in this project's flow consumes those numbers, so porting it would be unused OCR setup rather than skipped OCR-driven navigation). Full reference mapping in `ba_auto/reference_notes/mapping.md`'s new "Battle Pass" row.
|
||||
|
||||
Calibrated and live-tested end-to-end against the real game on nik-gpu (via direct ssh screenshot round-trips into `scratchpad/`, not a dry run):
|
||||
|
||||
- Home screen has its own entry points beyond the bottom nav bar (same pattern as arena's Work-hub card) -- a promo banner, currently "シノンのバトルパス SEASON 3", bottom-left. Clicking it opens the pass menu after a brief loading splash.
|
||||
- The pass main menu's "ミッション" button opens the mission sub-screen; its own back arrow / Escape returns directly to the pass main menu; a second Escape from there returns directly to true home -- no intermediate close-confirm screen at either step.
|
||||
- Both screens share an identical "一括受取" (claim-all) button, same bright-yellow-enabled/grey-disabled signature as `stamina.py`'s Mission-panel button, with the same Enter keybind. A real claim-all press on the mission screen claimed two ready rewards at once (200P + 1000P, confirmed via the level readout moving 3→4); a real claim-all press on the pass main menu claimed the newly-unlocked Lv.4 reward and showed a blocking "報酬獲得!" card that needed a second Enter to dismiss (the mission-screen claim didn't show this -- ported both tasks' claims with the extra dismiss press regardless, matching `stamina.py`'s own defensive convention, since it's a harmless no-op when nothing is actually showing).
|
||||
- **Real finding, not a hypothesis**: `navigation.is_on_subscreen`/`is_modal_open` both read false/false on both battle-pass screens -- exactly like true home (no bright subscreen header, no dark modal backdrop). If this task relied on `ba_daily.py`'s generic post-task `navigation.return_to_home()` cleanup like most other tasks do, that cleanup would think it was already home and do nothing, leaving the game stuck on the pass menu for whatever task runs next in the same preset. Confirmed via direct pixel comparison across several real captures of both screens. Fixed by giving `battle_pass.py` its own local probes (`config.BATTLE_PASS_INSIDE_PROBES`, a 5-point check against a flat dark footer band both battle-pass screens render identically regardless of splash art; `config.BATTLE_PASS_MISSION_TAB_PROBES`, a 2-point check distinguishing the mission sub-screen's own white active-tab background from the pass main menu's differently-positioned tab bar) and an explicit Escape-based exit loop at the end of `run()`, rather than trusting the generic cleanup.
|
||||
- Also hit live at the very start of this session (not new, see Phase 24's own note on the same state): the game's full-screen ambient/idle cutscene (no header/nav chrome at all) briefly made the very first calibration screenshot look like a blank art screen until a single click cleared it back to the ordinary home view.
|
||||
|
||||
Pure reclaim (no AP/credit/ticket spend, no choice to make) -- same category as gem_shop/circle, so added to `DEFAULT_ORDER` and the `daily` preset, not `q4h` (matching gem_shop/circle's own once-a-day-is-enough placement, since missions/levels don't refresh faster than that).
|
||||
|
||||
**First real `./ba_dailies.sh battle_pass` CLI run found a genuine bug**, distinct from the manual-click calibration above: Escape from the mission screen didn't always land on the pass main menu the way it did during calibration -- one real run overshot straight past it to home instead, which made the module skip the pass-level claim entirely (logged "not confirmed back on the pass menu, skipping pass-level claim") even though nothing was actually wrong with the game state. Root cause not fully pinned down (a manual step-by-step replay of the identical click sequence right afterward reproduced the calibrated 2-screen-deep Escape behavior correctly, so it's timing/flakiness-class, not a wrong coordinate or a wrong assumption about the screen depth) -- consistent with the same "occasional missed/extra click" flakiness this project already documents for mailbox/cafe icons elsewhere, not something worth chasing further. Fixed by replacing the single-retry-if-still-on-mission-screen check with `_ensure_pass_menu`, a proper self-healing loop: if 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 claim. Re-deployed and re-ran the real CLI command a second time: clean run, no retries needed, both claim checks resolved correctly, confirmed back at true home via screenshot.
|
||||
|
||||
**Status: Done.** Both claim paths (mission points, pass level) confirmed live with real reward claims during calibration; the actual `./ba_dailies.sh battle_pass` CLI dispatch itself confirmed live across two real runs (the second, post-fix, run clean).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### OCR
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user