diff --git a/ba_auto/config.py b/ba_auto/config.py index 92164f5..2591104 100644 --- a/ba_auto/config.py +++ b/ba_auto/config.py @@ -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) diff --git a/ba_auto/reference_notes/mapping.md b/ba_auto/reference_notes/mapping.md index a7d2745..7ca0b6c 100644 --- a/ba_auto/reference_notes/mapping.md +++ b/ba_auto/reference_notes/mapping.md @@ -4,6 +4,7 @@ Maps each local feature to the corresponding `~/repo/baas-reference/module/...` | Local feature | Reference file | Reference functions/classes | Local file | Backend replacements | Status | |---|---|---|---|---|---| +| Battle Pass | `module/collect_pass_reward.py` | `implement` (main flow), `main_page_to_pass_menu`, `to_page_pass_mission`, `to_page_pass_menu`, `collect_reward` -- `detect_statistics`/`detect_pass_level`/`detect_pass_next_level_point`/`detect_pass_weekly_point` (OCR level/point readout) deliberately not ported, nothing in this project's flow consumes those numbers | `ba_auto/tasks/battle_pass.py` | `core/picture.py::co_detect`'s img_possibles/img_ends (template-image matching against ~20 assets this project doesn't have) → local color probes (`config.BATTLE_PASS_INSIDE_PROBES`/`BATTLE_PASS_MISSION_TAB_PROBES`/`BATTLE_PASS_CLAIM_PROBE`); claim-all button reuses stamina.py's exact bright-yellow-vs-grey enabled/disabled signature and its Enter-then-Enter-to-dismiss-popup convention | Done, live-tested 2026-07-29: real mission-point claim (Lv.3→4) and real pass-level reward claim, both confirmed via screenshots, plus two real `./ba_dailies.sh battle_pass` CLI runs (the first surfaced a real Escape-overshoots-to-home navigation bug, fixed via a self-healing `_ensure_pass_menu` loop rather than a single fixed retry; the second, post-fix, run was clean). `navigation.is_on_subscreen`/`is_modal_open` both read false/false on both battle-pass screens (same false-negative pattern login.py's own pre-home states hit) -- this module carries its own local probes and explicitly Escapes back to home at the end rather than trusting `ba_daily.py`'s generic post-task `return_to_home` cleanup, which would otherwise no-op and leave the game stuck on the pass menu. Also hit live at the very start of this session (not new -- the same full-screen ambient/idle cutscene state Phase 24 already documents, no header/nav chrome at all, cleared by a single click/Enter anywhere): `_ensure_home()`'s pre-task check would misread it as home (same false/false pattern as every other state in that family), so a run starting while it's showing would need that first click/keypress to land correctly. Not specific to this feature, no action taken here. Pure reclaim, no AP/credit/ticket spend -- added to `DEFAULT_ORDER` and the `daily` preset (not `q4h`, matching gem_shop/circle's once-a-day-is-enough placement). | | Mailbox | `module/mail.py` | `to_mail`, `implement` | `ba_auto/tasks/mailbox.py` | tap/click via xdotool, screenshot via scrot, `color.rgb_in_range` → `driver.color_at` pixel-probe check | Migrated: real Python, state-verified via color probe (no legacy bridge) | | Cafe | `module/cafe_reward.py` | `to_cafe` (its `relationship_rank_up` popup-handling now also ported, see below), `interaction_for_cafe_solve_method3`, `collect`, `invite_girl`/`invite_by_affection`/`checkConfirmInvite` (student invitation, added 2026-07-14) | `ba_auto/tasks/cafe.py` | `picture.co_detect`/`color.rgb_in_range` → `driver.color_at` pixel-probe checks; sparkle template match ported in-process into `ba_auto/detector.py` (`find_cafe_sparkle`, now multi-scale) | Migrated: real Python, state-verified via color probes (no legacy bridge). Pat loop now polls for the full attempt budget instead of stopping on the first miss (see `plan.md` Phase 6 follow-up) — not yet confirmed against a live sparkle since none was available during testing. `_dismiss_rank_up_if_shown` reuses `navigation.is_on_subscreen` to detect and clear the full-screen bond-rank-up cutscene after a pat (see `plan.md` Phase 6 follow-up: rank-up popups) — not yet live-confirmed against a real trigger. **Student invitation (2026-07-14)**: per explicit user direction, invite a student into each room before farming it, preferring highest affection, always skipping (never confirming) a candidate that would swap an already-seated student's costume or move one in from the other room — directly ports `invite_by_affection`/`checkConfirmInvite`'s own logic with the reference's default `cafe_reward_allow_exchange_student`/`cafe_reward_allow_duplicate_invite` both `False` (no local config exists to make either configurable). Live-calibrated against nik-gpu with **zero real tickets spent** — all 3 real dialog variants (normal confirm, same-room costume-swap warning, neighboring-room move warning) found and confirmed live, every one cancelled rather than confirmed during calibration. The heart-shaped affection badge OCR reuses `detector.read_int_on_heart_badge` directly (pixel-confirmed the same widget lesson.py's own badges use — digit pixels sampled RG, matching that function's exact masking assumption) rather than building a second OCR path. Dialog-type detection OCRs the title bar and checks for either warning's own distinctive substring (`衣装`/`隣`) rather than an exact match, mirroring `event_sweep.py`'s own `"終了"` substring-match reasoning. All 5 heart-badge reads and all 3 dialog classifications verified offline against the real saved calibration screenshots before deploying (exact match, zero mismatches). **Confirmed live with real tickets spent, both rooms**: room 1 correctly skipped one 衣装替え (costume-swap) candidate then invited row 1 cleanly; room 2 correctly skipped three consecutive 隣のカフェの生徒を招待 (neighboring-room-move) candidates (expected — room 1's own invite had just taken the account's highest-affection students) then invited row 3 cleanly. Both newly-invited students were immediately patted successfully in the same run (the "newly invited student can be farmed" requirement), income was claimed, and the task returned cleanly to the true home screen with no warnings anywhere in the log. **Horizontal camera panning (2026-07-14)**: per explicit user direction ("due to my screen size... move screen most right and most left then farm"), `_pat_room` now pans the room camera to its rightmost extreme via a new `driver.drag()` primitive (this project's first — distinct from `driver.scroll()`'s wheel-based gesture, which is for list widgets, not a room camera), farms there, pans to the leftmost extreme, farms there too — no vertical panning, per the user's own instruction. Ports the reference's `zoom_out`'s underlying intent (see the whole room regardless of width) via the user's own specified mechanism (panning) rather than the reference's (zoom). Live-confirmed drag-direction-to-reveal-side mapping, that HUD elements (status bar, ticket buttons) stay fixed regardless of pan (no camera reset needed afterward), and that one drag already reaches the true extreme (extra repeats are a confirmed no-op, kept as a safety margin). **Confirmed live with a real game-state change**: a full run patted a real sparkle in room 2 (score 0.997) specifically after panning to an extreme, while room 1 found nothing that run (expected — per-student cooldown). Clean end-to-end completion, no warnings. **Invite ticket cooldown bugfix (2026-07-14)**: real-usage report (with screenshot) that the invite step kept failing once the account's ticket went on cooldown — `_open_invite_list`'s original check (`not is_on_subscreen`) couldn't distinguish the real MomoTalk list opening from a "通知" cooldown notice ("待機時間が経過した後に、再度招待することができます。") opening directly instead, since both dim the header the same way; misread as "list opened", it sent `_ensure_invite_sort`/`_try_invite_row`'s fixed coordinates into a dialog that has none of them. Fixed by checking `navigation.is_modal_open` (the darker real-dialog reading) first — a dialog appearing before any row is clicked can only mean the ticket click raised one directly — and dismissing it via the shared `SWEEP_CONFIRM_BUTTON`, returning `False` so the existing "skip this room's invite" fallback handles it. **Confirmed live**: both rooms correctly detected and dismissed the cooldown notice with no cascading errors, task completed cleanly (exit 0, true home screen confirmed via screenshot). See `plan.md`'s Phase 6 follow-up #4. **Two further real-usage fixes (2026-07-15, `next_fix.md`)**: (1) the cooldown-notice dismiss above now presses Escape and verifies via `navigation.is_modal_open` with bounded retry, instead of a fixed-coordinate click that could miss and leave the notice open under the following pan drags; (2) `_dismiss_rank_up_if_shown` switched from `navigation.is_on_subscreen`'s single-pixel header probe (found live to misread some characters' rank-up cutscene art as bright) to a new `navigation.is_header_bar_visible`, requiring 8 spread-out header-row points to all read bright via one atomic multi-point capture (`driver.colors_at`, added alongside it). Both best-effort — not yet re-confirmed live post-fix. **A real rank-up happened naturally via cron (2026-07-16)**, reported live with a screenshot of the game left stuck on the cutscene — exposed a third bug, a timing gap rather than a threshold problem: `_dismiss_rank_up_if_shown` was only ever called once, immediately after a pat, with no wait beforehand, but the cutscene renders with its own client-side animation delay, so that single check could catch the tail end of the still-normal room view and conclude "clear" a beat before the actual cutscene appeared. Confirmed via a live pixel-check that the resulting stuck cutscene reads under `is_on_subscreen`/`is_modal_open`'s thresholds too (`SUBSCREEN_HEADER_PROBE` read `(180,227,244)`, r=180 < the 200 both checks need), so `navigation.return_to_home`'s generic cleanup also couldn't recover it — every "are we home" check downstream falsely agreed nothing was wrong, and the whole `q4h` cron run finished "successfully" with the game actually left stuck. Fixed by checking on every `_pat_current_view` poll iteration instead of only right after a pat, so a delayed cutscene appearance is caught (and dismissed) roughly a second later instead of never. **Confirmed live**: manually reproduced the exact stuck state left by the incident and confirmed a single Enter (the same action `_dismiss_rank_up_if_shown` already takes) cleared it back to the real cafe room view; the per-iteration check itself has not yet been re-exercised against a fresh live rank-up trigger end-to-end (same "could not force one on demand" caveat as the original fix). **`SPARKLE_CLICK_OFFSET` recalibrated (2026-07-17)**: reported live with a screenshot (`screenshots/cafe/bug_student_close_together.png`) — when two students stand close together, the original `(75, 47)` offset (ported byte-for-byte from `scripts/detect_and_click.py`, never independently verified) overshoots past the intended target's head and lands on a different, closer student instead; the click doesn't register, so `find_cafe_sparkle()` matches the same still-showing sparkle again next iteration, repeatedly clicking nearly the same point for the rest of the room's budget (confirmed live: 13 of 16 "pats" in one run clustered within a ~10px box). First recalibration attempt, `(0, 0)` (raw template-match center), was also wrong — reported live ("clicking the actual sparkle instead of the student head"): a coincidental ambient thought-bubble animation at the test position had been misread as a hover confirmation. Properly recalibrated via numbered candidate-point overlays (screenshots with several labeled offset options drawn on a real live sparkle; the user picked the correct one directly on-screen) against two independent real students in different poses (sitting at an arcade cabinet, lying on a couch) — both converged on the same small offset, `(51, 15)`. **Confirmed live** via the real `./ba_dailies.sh cafe` CLI path: 6 sparkles patted across both rooms at genuinely distinct coordinates with zero repeated-click clustering, clean `[cafe] Done.` and true-home return. | | Stamina/AP | `module/collect_daily_task_power.py` | `to_tasks`/`implement` | `ba_auto/tasks/stamina.py` | `color.rgb_in_range` → `driver.color_at`; reference's per-tab claim loop → live UI's single "一括受取" bulk-claim button + Enter | Migrated: Mission-panel claim done (see `plan.md` Phase 8). The gem shop's own Daily Free Power flow (`module/collect_daily_free_power.py`) is a separate task now — see the "Gem shop daily free package" row below. **Daily gem reward bugfix (2026-07-16)**: reported live by the user against a real account state — the reference's own `implement` checks TWO separate button regions (the main 一括受取 area, then a second, separate "claim daily pyroxenes" button checked independently after), but only the first was ported originally. Fixed by porting the second region too: a new `MISSION_DAILY_GEM_CLAIM_BUTTON` (config.py), reusing the same bright-yellow-vs-grey color-probe style as the existing `MISSION_CLAIM_PROBE`, clicked via `driver.click` (not Enter — unlike 一括受取, this button has no keybind shown on screen) with a click-then-verify retry loop matching this project's established discipline. **Confirmed live**: gem balance went from 12,384 → 12,404 (exactly the reward's stated +20), clean return to the true home screen, no warnings. | diff --git a/ba_auto/tasks/battle_pass.py b/ba_auto/tasks/battle_pass.py new file mode 100644 index 0000000..c077b45 --- /dev/null +++ b/ba_auto/tasks/battle_pass.py @@ -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.") diff --git a/ba_daily.py b/ba_daily.py index 632e0f8..e582c8e 100644 --- a/ba_daily.py +++ b/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 ` -- # 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" ] } diff --git a/plan.md b/plan.md index 6514ec8..40bfca5 100644 --- a/plan.md +++ b/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