From 3c47fabd8748c442a7d8f7ca096537ca6def37ac Mon Sep 17 00:00:00 2001 From: Nik Afiq Date: Thu, 16 Jul 2026 14:07:12 +0900 Subject: [PATCH] feat(stamina): Implement daily gem reward claiming logic and fix button detection --- ba_auto/config.py | 19 +++++++++++ ba_auto/reference_notes/mapping.md | 2 +- ba_auto/tasks/stamina.py | 52 +++++++++++++++++++++++++++--- plan.md | 10 ++++++ 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/ba_auto/config.py b/ba_auto/config.py index c7e9c27..53509dc 100644 --- a/ba_auto/config.py +++ b/ba_auto/config.py @@ -23,6 +23,25 @@ MISSION_ICON = (75, 350) # grey (r-b<20) when not. Calibrated live at 1920x1200. MISSION_CLAIM_PROBE = (1600, 1100) +# Separate "デイリーミッションを8回クリア" (clear 8 daily missions) summary +# bar's own 受取 (claim) button, ported from the reference's own +# `collect_daily_task_power.py::implement` -- it checks TWO distinct +# button regions, not one: the main 一括受取 area (MISSION_CLAIM_PROBE +# above) is drained in a loop first, then this second, separate button +# (reference's own "claim daily pyroxenes" region, positioned to the LEFT +# of the main claim-all button in both the reference's layout and this +# client's) is checked independently -- 一括受取 does NOT also claim this, +# confirmed live (2026-07-16): a real account state showed this button +# still claimable (a gem x20 reward) after 一括受取's own area had already +# gone grey. Also unlike 一括受取, this button has no Enter keybind shown +# on screen, so it needs an actual coordinate click rather than a keypress +# (matching the reference's own raw click here too, `self.click(976, 670, +# ...)`, rather than the img_reactions-driven Enter used elsewhere). +# Same bright-yellow-vs-grey signature as MISSION_CLAIM_PROBE, confirmed +# live at this exact point; pixel-scanned to sit clearly inside the button +# and off the "受取" text glyphs (which read near-black, ~(75,33,22)). +MISSION_DAILY_GEM_CLAIM_BUTTON = (1400, 1095) + CAFE_ICON = (165, 1100) CAFE_ROOM_SWITCH = (190, 160) CAFE_INCOME = (1780, 1105) diff --git a/ba_auto/reference_notes/mapping.md b/ba_auto/reference_notes/mapping.md index f6ff40f..be8274a 100644 --- a/ba_auto/reference_notes/mapping.md +++ b/ba_auto/reference_notes/mapping.md @@ -6,7 +6,7 @@ Maps each local feature to the corresponding `~/repo/baas-reference/module/...` |---|---|---|---|---|---| | 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. | -| 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 | +| 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. | | Gem shop daily free package | `module/collect_daily_free_power.py` | `implement`, `to_purchase_pyroxenes_menu`, `to_purchase_type`, `detect_free_power_availability`, `collect_daily_free_power`, `return_to_main_page` | `ba_auto/tasks/gem_shop.py` | Reference detects every step via `core.picture.co_detect` + fixed-region OpenCV template matching (no OCR anywhere in this reference flow) — ported here as plain color probes instead, matching this project's own established equivalent for a simple flat-color state-A/state-B difference (see `cafe.py`'s `CLAIM_DISABLED_RGB`, `stamina.py`'s `MISSION_CLAIM_PROBE`) rather than building new template assets. The free card's own status bar reads flat dark navy (~(41,65,90)) when available vs flat dark red (~(144,38,47)) once claimed today — a clean, high-contrast signal pixel-scanned from real captures, no OCR of the "0"/"1" count text needed. A dedicated 4-point "is this dialog (or a nested confirm/notice card) showing" check (`GEM_SHOP_DIALOG_PROBES`) was needed because `navigation.is_on_subscreen`/`is_modal_open`'s default probes both proved unreliable on this dialog (it overlays the home screen directly rather than being a full subscreen, and its own opaque white card sits right on top of `is_modal_open`'s probe point) — same class of default-probe mismatch `story_sweep.py`/`bounty.py` already documented for their own wide modals, fixed the same multi-point-beats-single-point way `navigation.is_header_bar_visible` was fixed for cafe's rank-up cutscene the same day. | Done. Live-calibrated against nik-gpu 2026-07-15 by manually driving the real dialog end-to-end via raw xdotool/scrot (see `scratchpad/gem_shop_*.png`) — this **genuinely claimed the account's real free package for the day** (0 yen, confirmed +10 AP / +10,000 credits via before/after counter values: AP 64→74, credits 161,391,144→161,401,144), which calibrated both the "available" and "already claimed" visual states from real data. The actual `gem_shop.py` module was then live-tested for real against the resulting "already claimed today" state. The "available → claim" code path itself uses the same coordinates/logic already confirmed live via the manual walkthrough, but has not yet been exercised by the module itself end-to-end — worth a follow-up live check the next time the package resets and hasn't been claimed yet. Added to `DEFAULT_ORDER` (unlike most recent additions): it only ever reclaims a genuinely free, once-per-day resource with no choice to make, matching mailbox/cafe/stamina's own "reclaim something free" category rather than the "spends a resource, needs opt-in" category. | | Normal/Hard story AP sweep | `module/explore_tasks/sweep_task.py`, `module/explore_tasks/task_utils.py` | `to_region` (ported: OCR region-number readout + delta-click), a scoped-down `swipe_search_target_str` (ported: OCR stage-row label matching), `start_sweep`'s named-outcome contract (ported via `navigation.wait_for_state`, this project's scoped `co_detect` port) | `ba_auto/tasks/story_sweep.py` | OCR region/stage-name matching, ported for real (Phase 10) — replaces Phase 9's "next-region arrow stops advancing, then random stage" heuristic; MAX click verified via `SWEEP_MINUS_BUTTON_PROBE` color check (reused, still correct), modal closed via its own X button (Escape doesn't close it; X-button position re-calibrated per stage-layout variant, see Phase 10) | Done (see `plan.md` Phase 10, supersedes Phase 9). Config-driven exact `(region, stage, count)` targets (`config.STORY_SWEEP_TARGETS`), not latest-region/random-stage. Opt-in only, not in default flow | | Event sweep | `module/sweep_activity.py`, `module/activities/activity_utils.py` | `activity_sweep` (main flow), `to_activity` (nav to the event's Story/Mission/Challenge tabs), `check_sweep_availability`/`color.check_sweep_availability` (SSS gate), `start_sweep`'s named-outcome contract (shared with story_sweep's, ported the same way via `navigation.wait_for_state`) | `ba_auto/tasks/event_sweep.py`, `ba_auto/navigation.py` (`return_to_home`) | `to_activity`'s bottom-nav-icon entry -> this client's home-screen event badge (`config.EVENT_BADGE_ICON`, confirmed live to be a rotating carousel -- see Status); the reference's config-string sweep-list parsing (arbitrary stage lists, per-stage float/fraction counts via `preprocess_activity_region`/`preprocess_activity_sweep_times`) -> a single date-ordinal-modulo rotation target over a fixed 9-12 sub-range, mirroring `story_sweep.py`'s own rotation, per explicit user direction; stage-number OCR (`detector.read_int`) replaces the reference's `swipe_search_target_str` template-button search, since this client's stage list only ever needs its bottom scroll extreme for the 9-12 target range | Done, with a live-reported bug fixed (see `plan.md`'s Phase 14 follow-up). Live-calibrated against nik-gpu 2026-07-10 against the currently-running "鉄道爆走事件" event (12 stages) -- zero real AP spent during calibration (every confirm dialog reached was cancelled via Escape, verified by the AP counter). The stage-info modal is structurally identical to story_sweep's (MIN/-/+/MAX stepper, same AP-confirm dialog reusing `SWEEP_CONFIRM_*`/`SWEEP_RESULT_BUTTON_REGION`), but simpler: no region navigation, one fixed modal layout confirmed across two different stages (09 and 12), and the modal closes on Escape (story_sweep's doesn't). The reference's SSS-availability gate for a never-cleared stage was never exercised (every stage 9-12 on this account was already 3-starred) -- `event_sweep.py` handles that gate the same defensive way story_sweep handles an unavailable target: if the MAX-button count-raise can't be verified, it aborts without spending AP rather than guessing. **Bug found on a real run**: the home-screen event badge turned out to be a rotating carousel (cycles between the current event's countdown and other notices, e.g. a finished event's leftover reward-claim reminder), so a click could land on a stale event's page instead. Fixed via a new shared `navigation.return_to_home` primitive (bounded press-back-until-home loop, built generically so other tasks can reuse it, per explicit user request) plus wrong-page detection in `_find_stage_row` (zero stage-row numbers OCR'd at all -> retry via `return_to_home`, up to 3 attempts). **Second bug found on the next real run** (after the above fix correctly recovered and correctly OCR'd the target row): the row's own 入場 (enter) button click had no retry, unlike every other click-then-confirm step in this module -- missed once, aborted the whole run. Fixed via `_open_stage_modal`, the same click-then-verify-then-retry pattern already used everywhere else in the file. **Third bug found on a third real run**: `_find_stage_row`'s wrong-page detection false-positived twice (a fresh navigation's list hadn't finished rendering on the first OCR pass) before self-correcting on the 3rd attempt, wasting expensive return-home retries on what was really just a timing race -- fixed with a cheap in-place rescan before concluding "wrong page." Also, once past that, the 掃討開始 (start sweep) click turned out to be the last bare, unretried click in the file -- fixed via `_click_sweep_start_and_verify`. **Fourth round, self-driven live iteration per explicit user direction (fix/deploy/run/screenshot/diagnose in a loop, no stopping to report)**: found two more root causes and reached the first confirmed real live sweep. (1) The badge carousel's auto-rotate timer is far slower than the retry window -- a run hit "wrong page" on all 3 attempts genuinely, confirmed by screenshot; fixed by discovering and clicking the carousel's own pagination dots directly (`config.EVENT_BADGE_DOT_X`) instead of hoping the ambiguous badge shows the right item. (2) A genuine cold-start settle delay (up to ~20s for the Quest tab's stage list to populate after a fresh navigation, most likely a one-time server round-trip), not a flaky race -- confirmed via standalone probe scripts polling the real OCR pipeline once/second for a minute; fixed by widening `STAGE_ROW_SCAN_ATTEMPTS`/`_RETRY_WAIT` to match. With both fixes, a real run completed end-to-end: 200 AP spent (10x MAX sweep), credits gained, clean return home, confirmed by screenshot -- see `plan.md`'s Phase 14 follow-up #4 for the full writeup. **Fifth round, reported by the user a day later**: the exact same wrong-page-looping symptom recurred. Per the user's explicit request, added a DIRECT check for the finished event's own "イベント期間が終了しました" text (Japanese OCR, newly installed on nik-gpu since this project previously only needed digit/English reads) instead of the indirect "zero valid rows" heuristic -- `config.EVENT_FINISHED_TEXT_RECT`/`_is_finished_event_page`, live-calibrated and confirmed both ways (exact phrase match on a real finished page, no false positive on the correct page). This immediately proved the badge carousel's dot-clicking (follow-up #4's fix) is NOT reliably controllable after all -- sometimes worked, sometimes didn't, on the identical badge state -- while simply waiting longer let the carousel's own auto-rotate timer land on the correct item independently. Fixed by widening `WRONG_PAGE_RETRIES`/`_WAIT` (3x3s -> 6x12s) to give the timer real room to cycle, keeping dot-clicking as a harmless supplementary nudge. Two more real sweeps confirmed this fully working (11x MAX sweep, 219 AP spent, credits gained, clean home return, confirmed by screenshot -- three real confirmed sweeps total across this whole investigation). Also found and fixed the real root cause of the lingering `"unrecognized_state"` cosmetic bug (not a budget issue after all): `_watch_sweep_result`'s "swept" condition required the stage modal to still be open, but this event's flow can auto-return all the way to the Quest list instead, a terminal state it never anticipated -- fixed with a second `ends` condition gated on having clicked at least one result button first. Not yet re-verified live (AP exhausted by the successful sweep). See `plan.md`'s Phase 14 follow-up #5 for the full writeup. **Sixth round (2026-07-11)**: the daily rotation picked stage 9 for the first time, hitting a previously-flagged-but-never-exercised gap -- rows 366/538 ("08"/"09") consistently misread by OCR ("2" and empty/None) on every psm mode, even though the crop looked completely clean by eye; confirmed NOT a navigation/timing bug since rows 710/883/1055 ("10"/"11"/"12") read fine in the same run. Root cause (`scratchpad/probe_ocr_fix_08_09.py`): tesseract's segmentation fails on this tight edge-to-edge crop (no whitespace margin) for a leading-zero digit pair specifically -- adding a plain white border around the upscaled crop before OCR fixed both digits exactly, at every psm mode, without affecting "10". Fixed via new `detector.read_int_bordered`, now used for all `EVENT_STAGE_ROW_Y` reads. **Confirmed live**: stage 9 found, sweep executed for real (AP 233->14, credits +5,892, screenshot-confirmed safe return home) -- the originally reported bug is fixed. The same run again logged `unrecognized_state`, proving follow-up #5's `clicked_any`-gate fix addressed a real but secondary issue, not the full story. Diagnosed at zero AP cost (`scratchpad/probe_result_button_fp.py`, checks `_find_result_button` against the plain Quest list with no sweep running): the shared `SWEEP_RESULT_BUTTON_REGION` (borrowed from `story_sweep.py`) reaches into this event's own character-art panel and false-positive-matches `SWEEP_CONFIRM_CYAN` there with no dialog showing at all, confirmed on both a wrong event page and the correct one's own plain list -- so `_watch_sweep_result` kept "finding" a result button after the real one was already dismissed, and its "modal closed, no result button" end condition could never match. Fixed with a new event_sweep-only `config.EVENT_SWEEP_RESULT_BUTTON_REGION` (x narrowed to exclude the character-art panel, still comfortably covering the real buttons), confirmed live against the actual false-positive condition (now returns `None` where it previously didn't) -- not yet re-confirmed via a fresh full sweep since AP was too low that day. See `plan.md`'s Phase 14 follow-up #6. | diff --git a/ba_auto/tasks/stamina.py b/ba_auto/tasks/stamina.py index 6f9f931..eb8c832 100644 --- a/ba_auto/tasks/stamina.py +++ b/ba_auto/tasks/stamina.py @@ -1,4 +1,15 @@ -"""Mission/task-menu AP+pyroxene claim. Ported from baas-reference module/collect_daily_task_power.py's claim-loop pattern (its rgb_in_range checks replaced with driver.color_at probes).""" +"""Mission/task-menu AP+pyroxene claim. Ported from baas-reference module/collect_daily_task_power.py's claim-loop pattern (its rgb_in_range checks replaced with driver.color_at probes). + +The reference's `implement` checks TWO separate button regions, not one: +the main 一括受取 (claim-all) area is drained in a loop first, then a +second, separate "claim daily pyroxenes" button is checked and clicked +independently -- 一括受取 does not also claim it. This was originally +missed here (only the first region was ported), reported live by the user +against a real account state where the second button ("デイリーミッショ +ンを8回クリア", a gem x20 reward) was still showing claimable after +一括受取 had already gone grey -- see config.py's +`MISSION_DAILY_GEM_CLAIM_BUTTON` for the live-calibration writeup. +""" from ba_auto import navigation @@ -6,20 +17,46 @@ OPEN_RETRIES = 3 # One claim-all round is normally enough (button goes grey right after), but # bound it in case multiple reward reveals need dismissing in sequence. CLAIM_MAX_ROUNDS = 5 +DAILY_GEM_CLAIM_MAX_ATTEMPTS = 3 # Yellow "one-click claim all" button vs. its own flat-grey disabled state -- # same background-pixel-probe idea as mailbox.CLAIM_ALL_PROBE/cafe.CLAIM_PROBE, # but distinguished by hue (yellow has a big r-b gap; grey doesn't) since the -# button's grey isn't a single fixed RGB the way mailbox/cafe's are. +# button's grey isn't a single fixed RGB the way mailbox/cafe's are. Also +# shared by the separate daily-gem claim button (config.MISSION_DAILY_GEM_CLAIM_BUTTON), +# which uses the same bright-yellow-vs-grey visual style. CLAIM_ENABLED_MIN_R = 230 CLAIM_ENABLED_MIN_RB_GAP = 100 -def _claim_enabled(driver, config): - r, g, b = driver.color_at(*config.MISSION_CLAIM_PROBE) +def _button_claimable(driver, probe): + r, g, b = driver.color_at(*probe) return r > CLAIM_ENABLED_MIN_R and (r - b) > CLAIM_ENABLED_MIN_RB_GAP +def _claim_enabled(driver, config): + return _button_claimable(driver, config.MISSION_CLAIM_PROBE) + + +def _daily_gem_claimable(driver, config): + return _button_claimable(driver, config.MISSION_DAILY_GEM_CLAIM_BUTTON) + + +def _claim_daily_gem(driver, config): + for attempt in range(1, DAILY_GEM_CLAIM_MAX_ATTEMPTS + 1): + driver.click(*config.MISSION_DAILY_GEM_CLAIM_BUTTON) + driver.wait(1.5) + # Dismiss the reward-reveal popup the same way the main claim-all + # loop does below -- this button has no Enter keybind of its own, + # but Enter still dismisses the reward card that appears after. + driver.keypress("Return") + driver.wait(1.5) + if not _daily_gem_claimable(driver, config): + return True + print(f"[stamina] daily gem reward still showing claimable after click (attempt {attempt}/{DAILY_GEM_CLAIM_MAX_ATTEMPTS})") + return not _daily_gem_claimable(driver, config) + + def run(driver, config): driver.focus_game() @@ -50,6 +87,13 @@ def run(driver, config): driver.wait(1.5) claimed_any = True + if _daily_gem_claimable(driver, config): + print("[stamina] claiming daily gem mission reward") + if _claim_daily_gem(driver, config): + claimed_any = True + else: + print("[stamina] warning: could not confirm the daily gem reward was claimed") + if not claimed_any: print("[stamina] nothing to claim") diff --git a/plan.md b/plan.md index 0b3d8ca..ec88205 100644 --- a/plan.md +++ b/plan.md @@ -388,6 +388,16 @@ Not yet done: Group/Club AP, and Daily Free Power (see above). The 青輝石購入 menu deliberately left unautomated above turned out to gate a real-money purchase UI in general, but the specific card this was always about (`module/collect_daily_free_power.py`'s 毎日無料パッケージ, 0 yen, AP+credits) is safe to automate on its own — it's a fixed, non-selectable, always-free claim with no purchase decision to make, same category as this phase's own Mission-panel claim. Built as a separate `ba_auto/tasks/gem_shop.py` rather than folded into `stamina.py`, since it's a different reference file/UI entry point. See Phase 16. +#### Phase 8 follow-up #2: daily gem reward wasn't being claimed (2026-07-16) + +Reported live by the user, with a saved reference screenshot (`screenshots/stamina/daily gem available.png`): stamina "should collect all reward in the mission, but it didn't collect the daily gem reward." + +Re-reading `module/collect_daily_task_power.py::implement` (already the reference for this task, but not fully ported the first time) confirmed the gap: it checks TWO separate button regions, not one. The main 一括受取 area is drained in a loop first (region A, the only one originally ported into `stamina.py`); then, independently, a second, separate button (region B, the reference's own "claim daily pyroxenes" check, positioned to the left of region A in both the reference's raw coordinates and this client's own layout) is checked and clicked -- 一括受取 does not also claim it. Live on nik-gpu, this second button corresponds to the "デイリーミッションを8回クリア" (clear 8 daily missions) summary bar's own 受取 button, a gem x20 reward, which was confirmed still showing claimable after 一括受取 had already gone flat grey. + +Ported region B as `MISSION_DAILY_GEM_CLAIM_BUTTON` (config.py), pixel-scanned live at native 1920x1200 (button spans roughly x=1385-1535, y=1080-1165; click point `(1400, 1095)` confirmed clean and off the "受取" text glyphs, which read near-black). Reuses the exact same bright-yellow-vs-grey color signature already established for `MISSION_CLAIM_PROBE` (`CLAIM_ENABLED_MIN_R`/`CLAIM_ENABLED_MIN_RB_GAP`, refactored into a shared `_button_claimable` helper rather than duplicated). Unlike 一括受取, this button shows no Enter keybind on screen, so it's claimed via an actual `driver.click` (matching the reference's own raw `self.click(976, 670, ...)` for this exact region) followed by an Enter press to dismiss the reward-reveal card, wrapped in a click-then-verify bounded retry (`_claim_daily_gem`, `DAILY_GEM_CLAIM_MAX_ATTEMPTS = 3`) matching this project's established discipline rather than trusting an unverified click. + +**Confirmed live** via the real `./ba_dailies.sh stamina` CLI path against a genuinely claimable reward (the account's real daily-mission-count tracker was at 8/8, gem reward pending): gem balance went from 12,384 → 12,404 -- exactly the reward's own stated +20 -- with the existing main claim-all also firing normally in the same run, and a clean, warning-free return to the true home screen afterward. + ### Phase 9: Normal/Hard story AP sweep **Status: Done.** Read `module/explore_tasks/sweep_task.py` and `module/explore_tasks/task_utils.py` — the reference flow reads the current region number and matches stage-name text via OCR (`swipe_search_target_str`) to navigate to a configured target stage, then runs a per-stage claim loop. This client exposes a much simpler path to the same goal (burn AP via already-3-starred stages) that avoids porting the OCR-based lookup entirely: each stage's own 任務情報 (task info) modal has a self-contained 掃討 (sweep) sub-panel with a MIN/-/+/MAX count stepper and a start button.