feat: Add Common Shop and Tactical Shop tasks with shared purchasing logic
This commit is contained in:
parent
4842d575a5
commit
1bb2ccdd6f
@ -639,12 +639,12 @@ When debugging on `nik-gpu`, copy relevant screenshots or debug images back into
|
|||||||
|
|
||||||
## Existing features
|
## Existing features
|
||||||
|
|
||||||
Current project state: mailbox, cafe, stamina, and story_sweep are all migrated to real Python. No Bash feature logic remains.
|
Current project state: mailbox, cafe, stamina, story_sweep, shop_common, and shop_tactical are all migrated to real Python. No Bash feature logic remains.
|
||||||
|
|
||||||
- `ba_dailies.sh` is a thin launcher that execs `ba_daily.py`
|
- `ba_dailies.sh` is a thin launcher that execs `ba_daily.py`
|
||||||
- `ba_daily.py` dispatches `mailbox`, `cafe`, `stamina`, `story_sweep`, and default flow to `ba_auto/tasks/`
|
- `ba_daily.py` dispatches `mailbox`, `cafe`, `stamina`, `story_sweep`, `shop_common`, `shop_tactical`, and default flow to `ba_auto/tasks/`
|
||||||
- default flow is `mailbox`, `cafe`, `stamina`
|
- default flow is `mailbox`, `cafe`, `stamina`
|
||||||
- `story_sweep` is opt-in only since it spends AP rather than reclaiming something free
|
- `story_sweep`, `shop_common`, and `shop_tactical` are opt-in only since they spend AP/credits/tactical coin rather than reclaiming something free
|
||||||
- `ba_auto/tasks/mailbox.py` and `ba_auto/tasks/cafe.py` click with `ba_auto/driver.py` primitives and verify state with `driver.color_at` and `ba_auto/navigation.py`
|
- `ba_auto/tasks/mailbox.py` and `ba_auto/tasks/cafe.py` click with `ba_auto/driver.py` primitives and verify state with `driver.color_at` and `ba_auto/navigation.py`
|
||||||
- mailbox and cafe were ported from the reference patterns around `module/mail.py` and `module/cafe_reward.py`
|
- mailbox and cafe were ported from the reference patterns around `module/mail.py` and `module/cafe_reward.py`
|
||||||
- no legacy bridge remains
|
- no legacy bridge remains
|
||||||
@ -653,6 +653,9 @@ Current project state: mailbox, cafe, stamina, and story_sweep are all migrated
|
|||||||
- `ba_auto/tasks/story_sweep.py` sweeps a config-driven list of exact `(region, stage, count)` targets from `config.STORY_SWEEP_TARGETS`
|
- `ba_auto/tasks/story_sweep.py` sweeps a config-driven list of exact `(region, stage, count)` targets from `config.STORY_SWEEP_TARGETS`
|
||||||
- story sweep navigates via OCR, using region-number readout and stage-label matching, rather than random selection
|
- story sweep navigates via OCR, using region-number readout and stage-label matching, rather than random selection
|
||||||
- see `plan.md` Phase 10 for the OCR-based story sweep port
|
- see `plan.md` Phase 10 for the OCR-based story sweep port
|
||||||
|
- `ba_auto/tasks/shop_common.py` and `ba_auto/tasks/shop_tactical.py` share a checkbox-grid-then-bulk-buy flow (`ba_auto/tasks/shop_utils.py`) against config-driven `(row, col, name, expected_price)` targets (`config.COMMON_SHOP_TARGETS` / `config.TACTICAL_SHOP_TARGETS`)
|
||||||
|
- item identification is by fixed grid position, not per-item OCR — the reference's own `get_item_position` indexes an external static price table this repo doesn't have, so a locally pixel-scanned position table is the faithful port, not an OCR-avoidance shortcut; price-digit OCR is layered on top as an extra catalog-drift safety check the reference doesn't even do per-item
|
||||||
|
- both shops were live-tested with real purchases (see `plan.md` Phase 11), which also surfaced a real, previously-unknown per-refresh-cycle purchase cap on these items (not shown as a visible counter) — the task correctly detected the now-unselectable items and safely declined rather than misfiring
|
||||||
- `ba_auto/detector.py` has `find_cafe_sparkle()`, the sparkle template-match ported in-process from the now-deleted `scripts/detect_and_click.py`
|
- `ba_auto/detector.py` has `find_cafe_sparkle()`, the sparkle template-match ported in-process from the now-deleted `scripts/detect_and_click.py`
|
||||||
- `scripts/ba_dailies_legacy.sh` and `scripts/detect_and_click.py` have been deleted
|
- `scripts/ba_dailies_legacy.sh` and `scripts/detect_and_click.py` have been deleted
|
||||||
- the `scripts/` directory itself no longer exists
|
- the `scripts/` directory itself no longer exists
|
||||||
|
|||||||
@ -31,6 +31,8 @@ Current task status:
|
|||||||
| `cafe` | Real Python (`ba_auto/tasks/cafe.py`). Verifies each room/dialog transition the same way as `mailbox` before acting; sparkle detection runs in-process via `ba_auto/detector.py` instead of shelling out per click. See "Fixed: the exit-game dialog bug" below for what this replaced, and `plan.md` Phase 6 follow-up for the multi-scale detection + persistent-polling changes made after a "farming affection doesn't happen" report. |
|
| `cafe` | Real Python (`ba_auto/tasks/cafe.py`). Verifies each room/dialog transition the same way as `mailbox` before acting; sparkle detection runs in-process via `ba_auto/detector.py` instead of shelling out per click. See "Fixed: the exit-game dialog bug" below for what this replaced, and `plan.md` Phase 6 follow-up for the multi-scale detection + persistent-polling changes made after a "farming affection doesn't happen" report. |
|
||||||
| `stamina` | Real Python (`ba_auto/tasks/stamina.py`). Opens the Mission panel and claims via its bulk "一括受取" button (Enter key) when enabled. Does **not** touch the Pyroxene Purchase (青輝石購入) menu's free-AP claim — that's a real-money purchase screen and was deliberately left unautomated; see `plan.md` Phase 8. |
|
| `stamina` | Real Python (`ba_auto/tasks/stamina.py`). Opens the Mission panel and claims via its bulk "一括受取" button (Enter key) when enabled. Does **not** touch the Pyroxene Purchase (青輝石購入) menu's free-AP claim — that's a real-money purchase screen and was deliberately left unautomated; see `plan.md` Phase 8. |
|
||||||
| `story_sweep` | Real Python (`ba_auto/tasks/story_sweep.py`). **Spends AP** — opt-in only, not part of the default flow. Sweeps a config-driven list of exact `(region, stage, count)` targets (`config.STORY_SWEEP_TARGETS` — edit this before running for real, it ships with a placeholder), navigating to each via OCR (region-number readout + stage-label matching) rather than a random pick. Verifies the MAX/`+` click actually raised the count before starting the sweep, clicks through the AP-usage-confirmation dialog, and closes the stage-info modal via its own X button afterward (Escape doesn't close it — confirmed live). See `plan.md` Phase 10. |
|
| `story_sweep` | Real Python (`ba_auto/tasks/story_sweep.py`). **Spends AP** — opt-in only, not part of the default flow. Sweeps a config-driven list of exact `(region, stage, count)` targets (`config.STORY_SWEEP_TARGETS` — edit this before running for real, it ships with a placeholder), navigating to each via OCR (region-number readout + stage-label matching) rather than a random pick. Verifies the MAX/`+` click actually raised the count before starting the sweep, clicks through the AP-usage-confirmation dialog, and closes the stage-info modal via its own X button afterward (Escape doesn't close it — confirmed live). See `plan.md` Phase 10. |
|
||||||
|
| `shop_common` | Real Python (`ba_auto/tasks/shop_common.py`). **Spends credits** — opt-in only, not part of the default flow. Buys a config-driven list of exact `(row, col, name, expected_price)` targets (`config.COMMON_SHOP_TARGETS`) from the 通常アイテム tab: OCR-verifies each item's price before checking its box, then clicks the bulk 購入 button and confirms through the purchase dialog. Live-tested with real purchases. See `plan.md` Phase 11. |
|
||||||
|
| `shop_tactical` | Real Python (`ba_auto/tasks/shop_tactical.py`). **Spends tactical coin** — opt-in only, not part of the default flow. Same flow as `shop_common` against the 戦術対抗戦 tab and `config.TACTICAL_SHOP_TARGETS`. Live-tested with real purchases. See `plan.md` Phase 11. |
|
||||||
|
|
||||||
## Prerequisites on nik-gpu
|
## Prerequisites on nik-gpu
|
||||||
|
|
||||||
@ -69,6 +71,8 @@ The game must already be running on `nik-gpu` (window titled `BlueArchive`). The
|
|||||||
~/ba_dailies.sh cafe # just cafe
|
~/ba_dailies.sh cafe # just cafe
|
||||||
~/ba_dailies.sh stamina # just the Mission-panel bulk claim
|
~/ba_dailies.sh stamina # just the Mission-panel bulk claim
|
||||||
~/ba_dailies.sh story_sweep # spends AP -- not in the default flow, run explicitly
|
~/ba_dailies.sh story_sweep # spends AP -- not in the default flow, run explicitly
|
||||||
|
~/ba_dailies.sh shop_common # spends credits -- not in the default flow, run explicitly
|
||||||
|
~/ba_dailies.sh shop_tactical # spends tactical coin -- not in the default flow, run explicitly
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also run these remotely without a separate `ssh` login step:
|
You can also run these remotely without a separate `ssh` login step:
|
||||||
|
|||||||
@ -186,3 +186,81 @@ SWEEP_RESULT_BUTTON_REGION = (700, 700, 1300, 1050)
|
|||||||
STORY_SWEEP_TARGETS = [
|
STORY_SWEEP_TARGETS = [
|
||||||
(1, 1, "max"),
|
(1, 1, "max"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Common Shop / Tactical Challenge Shop. Both tabs share the same underlying
|
||||||
|
# checkbox-grid-then-bulk-buy UI (the live equivalent of the reference's
|
||||||
|
# module/shop/shop_utils.py get_item_position/ensure_choose/buy pattern);
|
||||||
|
# see ba_auto/tasks/shop_utils.py for the shared control flow.
|
||||||
|
SHOP_ICON = (1155, 1085) # bottom nav "ショップ" icon on the home screen
|
||||||
|
SHOP_BACK_BUTTON = (85, 55)
|
||||||
|
SHOP_TAB_COMMON = (160, 208) # 通常アイテム tab (credit-point items)
|
||||||
|
# 戦術対抗戦 tab (tactical-coin items). The reference reaches this via
|
||||||
|
# goto_shop_by_name's OCR swipe-search over the shop-type tab list
|
||||||
|
# (module/shop/shop_utils.py) because that list can require scrolling on
|
||||||
|
# some accounts/versions. Confirmed live here: this account's tab list is
|
||||||
|
# only 7 entries and all fit on screen with no scroll needed, so a fixed
|
||||||
|
# click is faithful (there is nothing to search for), not a shortcut around
|
||||||
|
# the OCR the reference would otherwise need.
|
||||||
|
SHOP_TAB_TACTICAL = (160, 915)
|
||||||
|
|
||||||
|
# Item grid checkbox top-left-ish click point per (row, col), pixel-scanned
|
||||||
|
# live against the shop's real catalog (see plan.md's shop phase). Confirmed
|
||||||
|
# identical across both shop tabs -- it's the same shared UI component.
|
||||||
|
SHOP_ITEM_COL_X = [972, 1197, 1423, 1649]
|
||||||
|
SHOP_ITEM_ROW_Y = [297, 674]
|
||||||
|
# The checkbox glyph renders this vivid yellow-green only once checked
|
||||||
|
# (plain white/grey otherwise) -- pixel-sampled from a live checked vs.
|
||||||
|
# unchecked capture of the same card.
|
||||||
|
SHOP_CHECKED_RGB = ((60, 130, 80), (220, 245, 115))
|
||||||
|
# Each item's price-digit crop, as an (x1, y1, x2, y2) offset added to that
|
||||||
|
# item's own (col_x, row_y). Pixel-scanned and OCR-tested live against all 8
|
||||||
|
# configured targets' actual prices (12,500 up to 500,000) -- wide enough
|
||||||
|
# for the largest configured price without bleeding into the neighboring
|
||||||
|
# column's card.
|
||||||
|
SHOP_PRICE_OCR_OFFSET = (48, 166, 145, 195)
|
||||||
|
|
||||||
|
SHOP_BUY_BUTTON = (1751, 1112)
|
||||||
|
SHOP_CANCEL_BUTTON = (1525, 1112)
|
||||||
|
# A corner point that both the purchase-confirm dialog and the post-purchase
|
||||||
|
# "報酬獲得!" (reward acquired) banner dim away from pure white as they
|
||||||
|
# cover the screen -- confirmed live across both, and confirmed to stay pure
|
||||||
|
# white with no dialog open (across tab switches and scrolling). Cheaper and
|
||||||
|
# more robust than tracking each dialog's own layout individually.
|
||||||
|
SHOP_OVERLAY_PROBE = (100, 600)
|
||||||
|
SHOP_OVERLAY_IDLE_MIN_CHANNEL = 200
|
||||||
|
# Bounds shop_utils.confirm_purchase's "press Enter until idle" loop.
|
||||||
|
# Confirmed live: exactly 2 presses clears both the confirm dialog and the
|
||||||
|
# reward banner in one purchase; this leaves headroom for any additional
|
||||||
|
# one-time popup (e.g. a first-time notice) without spinning forever if the
|
||||||
|
# game is ever in a state this project doesn't recognize.
|
||||||
|
SHOP_PURCHASE_MAX_ENTER_PRESSES = 6
|
||||||
|
|
||||||
|
# Top status bar credit-point balance -- present on every screen, not
|
||||||
|
# shop-specific. Rect excludes the currency icon on the left, which OCR
|
||||||
|
# otherwise misreads as a spurious leading digit (confirmed live).
|
||||||
|
CREDIT_BALANCE_OCR_RECT = (1040, 15, 1290, 55)
|
||||||
|
# The Tactical Challenge Shop's own coin-balance readout, shown inline above
|
||||||
|
# the item grid on that tab specifically (not in the top status bar).
|
||||||
|
TACTICAL_COIN_OCR_RECT = (1090, 100, 1290, 150)
|
||||||
|
|
||||||
|
# (row, col, item name (log/debug only -- identification is by grid
|
||||||
|
# position, see shop_utils.py's module docstring for why), expected
|
||||||
|
# credit-point price). Buy-list confirmed with the user; all 8 are visible
|
||||||
|
# without scrolling.
|
||||||
|
COMMON_SHOP_TARGETS = [
|
||||||
|
(0, 0, "初級レポート", 12500),
|
||||||
|
(0, 1, "中級レポート", 125000),
|
||||||
|
(0, 2, "上級レポート", 300000),
|
||||||
|
(0, 3, "最上級レポート", 500000),
|
||||||
|
(1, 0, "初級強化珠", 10000),
|
||||||
|
(1, 1, "中級強化珠", 40000),
|
||||||
|
(1, 2, "上級強化珠", 96000),
|
||||||
|
(1, 3, "最上級強化珠", 128000),
|
||||||
|
]
|
||||||
|
|
||||||
|
# (row, col, item name, expected tactical-coin price). Buy-list confirmed
|
||||||
|
# with the user; both visible without scrolling.
|
||||||
|
TACTICAL_SHOP_TARGETS = [
|
||||||
|
(0, 0, "初級栄養ドリンク(AP30)", 15),
|
||||||
|
(0, 1, "中級栄養ドリンク(AP60)", 30),
|
||||||
|
]
|
||||||
|
|||||||
@ -12,8 +12,8 @@ Maps each local feature to the corresponding `~/repo/baas-reference/module/...`
|
|||||||
| Bounty | `module/rewarded_task.py` | Need to inspect | `ba_auto/tasks/bounty.py` | sweep/color/OCR adaptation | Not started |
|
| Bounty | `module/rewarded_task.py` | Need to inspect | `ba_auto/tasks/bounty.py` | sweep/color/OCR adaptation | Not started |
|
||||||
| Commissions | `module/clear_special_task_power.py` | Need to inspect | `ba_auto/tasks/commission.py` | sweep/color adaptation | Not started |
|
| Commissions | `module/clear_special_task_power.py` | Need to inspect | `ba_auto/tasks/commission.py` | sweep/color adaptation | Not started |
|
||||||
| Arena | `module/arena.py` | Need to inspect | `ba_auto/tasks/arena.py` | auto-fight + OCR + local driver | Not started |
|
| Arena | `module/arena.py` | Need to inspect | `ba_auto/tasks/arena.py` | auto-fight + OCR + local driver | Not started |
|
||||||
| Common Shop | `module/shop/common_shop.py`, `module/shop/shop_utils.py` | Need to inspect | `ba_auto/tasks/shop_common.py` | OCR + tab navigation + local clicks | Not started |
|
| Common Shop | `module/shop/common_shop.py`, `module/shop/shop_utils.py` | `implement`, `to_common_shop`, `get_item_position`/`ensure_choose`/`buy` (shared, see Tactical Shop row) | `ba_auto/tasks/shop_common.py`, `ba_auto/tasks/shop_utils.py` | `get_item_position`'s color+template item-state scan → fixed grid-position targets (`config.COMMON_SHOP_TARGETS`) + price-digit OCR verify, since the reference's own item-identification here indexes an external static price table (`self.static_config.common_shop_price_list`, fetched from a remote resource) this repo doesn't have — not per-item OCR, so this isn't an OCR-avoidance shortcut. Purchase-confirm dialog + reward-acquired banner handled via a single overlay-darkness probe (`config.SHOP_OVERLAY_PROBE`) instead of tracking each dialog's own layout | Done. Live-tested with real purchases (all 8 configured targets bought, cost matched exactly). Discovered live: these items have a per-refresh-cycle purchase cap not shown as a visible counter (unlike the 青輝石 tab's "あと1回購入可能" labels) — confirmed by re-running the task after purchase and observing it correctly detect the now-unselectable items (checkbox + individual 購入 button both unresponsive) and safely decline rather than guess. A fresh, everything-available run hasn't been re-verified since the account had already exhausted this cycle's purchases via that same test |
|
||||||
| Tactical Shop | `module/shop/tactical_challenge_shop.py`, `module/shop/shop_utils.py` | Need to inspect | `ba_auto/tasks/shop_tactical.py` | OCR + tab navigation + local clicks | Not started |
|
| Tactical Shop | `module/shop/tactical_challenge_shop.py`, `module/shop/shop_utils.py` | `implement`, `goto_shop_by_name`, shared `get_item_position`/`ensure_choose`/`buy` | `ba_auto/tasks/shop_tactical.py`, `ba_auto/tasks/shop_utils.py` | `goto_shop_by_name`'s OCR swipe-search over the shop-tab list → fixed click (`config.SHOP_TAB_TACTICAL`): this account's tab list is only 7 entries and fits on screen with no scroll needed, confirmed live, so there's nothing to search for — not an OCR-avoidance shortcut. Same grid-position + price-OCR-verify + overlay-probe design as Common Shop, sharing `shop_utils.run_shop_tab` | Done. Live-tested with real purchases (both configured AP-recovery drinks bought; AP and tactical-coin balance changes matched exactly) |
|
||||||
| Lesson/Schedule | `module/lesson.py` | Need to inspect | `ba_auto/tasks/lesson.py` | OCR + template/portrait search + local driver | Not started |
|
| Lesson/Schedule | `module/lesson.py` | Need to inspect | `ba_auto/tasks/lesson.py` | OCR + template/portrait search + local driver | Not started |
|
||||||
|
|
||||||
Do not implement a feature without filling at least the relevant row.
|
Do not implement a feature without filling at least the relevant row.
|
||||||
|
|||||||
27
ba_auto/tasks/shop_common.py
Normal file
27
ba_auto/tasks/shop_common.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"""Common Shop (通常アイテム) daily task. Ported from baas-reference
|
||||||
|
module/shop/common_shop.py's implement() -- see shop_utils.py for the
|
||||||
|
shared checkbox-grid-then-bulk-buy control flow both shop tabs use."""
|
||||||
|
from ba_auto import navigation
|
||||||
|
from ba_auto.tasks import shop_utils
|
||||||
|
|
||||||
|
|
||||||
|
def run(driver, config):
|
||||||
|
driver.focus_game()
|
||||||
|
|
||||||
|
driver.click(*config.SHOP_ICON)
|
||||||
|
driver.wait(2)
|
||||||
|
if not navigation.is_on_subscreen(driver):
|
||||||
|
print("[shop_common] could not confirm shop opened, aborting without pressing further keys")
|
||||||
|
return
|
||||||
|
|
||||||
|
shop_utils.run_shop_tab(
|
||||||
|
driver, config,
|
||||||
|
tab_button=config.SHOP_TAB_COMMON,
|
||||||
|
targets=config.COMMON_SHOP_TARGETS,
|
||||||
|
balance_rect=config.CREDIT_BALANCE_OCR_RECT,
|
||||||
|
currency_label="credits",
|
||||||
|
)
|
||||||
|
|
||||||
|
driver.click(*config.SHOP_BACK_BUTTON)
|
||||||
|
driver.wait(1.5)
|
||||||
|
print("[shop_common] Done.")
|
||||||
34
ba_auto/tasks/shop_tactical.py
Normal file
34
ba_auto/tasks/shop_tactical.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""Tactical Challenge Shop (戦術対抗戦) daily task. Ported from baas-reference
|
||||||
|
module/shop/tactical_challenge_shop.py's implement() -- see shop_utils.py for
|
||||||
|
the shared checkbox-grid-then-bulk-buy control flow both shop tabs use.
|
||||||
|
|
||||||
|
The reference reaches this tab via goto_shop_by_name's OCR swipe-search over
|
||||||
|
the shop-type tab list (module/shop/shop_utils.py), needed there because that
|
||||||
|
list can require scrolling. This account's tab list is only 7 entries and
|
||||||
|
fits on screen with no scrolling, confirmed live -- config.SHOP_TAB_TACTICAL
|
||||||
|
is a fixed click on the already-visible tab, not a shortcut around OCR (there
|
||||||
|
is nothing to search for)."""
|
||||||
|
from ba_auto import navigation
|
||||||
|
from ba_auto.tasks import shop_utils
|
||||||
|
|
||||||
|
|
||||||
|
def run(driver, config):
|
||||||
|
driver.focus_game()
|
||||||
|
|
||||||
|
driver.click(*config.SHOP_ICON)
|
||||||
|
driver.wait(2)
|
||||||
|
if not navigation.is_on_subscreen(driver):
|
||||||
|
print("[shop_tactical] could not confirm shop opened, aborting without pressing further keys")
|
||||||
|
return
|
||||||
|
|
||||||
|
shop_utils.run_shop_tab(
|
||||||
|
driver, config,
|
||||||
|
tab_button=config.SHOP_TAB_TACTICAL,
|
||||||
|
targets=config.TACTICAL_SHOP_TARGETS,
|
||||||
|
balance_rect=config.TACTICAL_COIN_OCR_RECT,
|
||||||
|
currency_label="tactical coin",
|
||||||
|
)
|
||||||
|
|
||||||
|
driver.click(*config.SHOP_BACK_BUTTON)
|
||||||
|
driver.wait(1.5)
|
||||||
|
print("[shop_tactical] Done.")
|
||||||
127
ba_auto/tasks/shop_utils.py
Normal file
127
ba_auto/tasks/shop_utils.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""Shared Common Shop / Tactical Shop grid logic.
|
||||||
|
|
||||||
|
Ported from baas-reference module/shop/shop_utils.py's get_item_position/
|
||||||
|
ensure_choose/buy pattern -- the checkbox-select-then-bulk-buy grid shared by
|
||||||
|
both shop tabs in the live client. Adapted from the reference's
|
||||||
|
color+template item-state scan to this project's fixed-grid-position +
|
||||||
|
price-OCR-verify design: unlike story_sweep, the reference's own
|
||||||
|
item-identification here isn't OCR at all -- it indexes into
|
||||||
|
self.static_config.common_shop_price_list, a table sourced from an external
|
||||||
|
resource this repo doesn't have. So a locally pixel-scanned, name-commented
|
||||||
|
position table (config.COMMON_SHOP_TARGETS / config.TACTICAL_SHOP_TARGETS) is
|
||||||
|
the faithful local equivalent, not a shortcut around OCR the reference uses.
|
||||||
|
Price-digit OCR -- something the reference doesn't even do per-item -- is
|
||||||
|
layered on top as an extra safety net against catalog drift, consistent with
|
||||||
|
this project's verify-before-spend pattern elsewhere (mailbox/cafe/story_sweep).
|
||||||
|
"""
|
||||||
|
from ba_auto import detector, navigation
|
||||||
|
|
||||||
|
|
||||||
|
def _checkbox_center(config, row, col):
|
||||||
|
return config.SHOP_ITEM_COL_X[col], config.SHOP_ITEM_ROW_Y[row]
|
||||||
|
|
||||||
|
|
||||||
|
def _checkbox_region(config, row, col):
|
||||||
|
x, y = _checkbox_center(config, row, col)
|
||||||
|
return (x - 15, y - 15, x + 15, y + 15)
|
||||||
|
|
||||||
|
|
||||||
|
def _price_rect(config, row, col):
|
||||||
|
x, y = _checkbox_center(config, row, col)
|
||||||
|
x1, y1, x2, y2 = config.SHOP_PRICE_OCR_OFFSET
|
||||||
|
return (x + x1, y + y1, x + x2, y + y2)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_checked(driver, config, row, col):
|
||||||
|
lo, hi = config.SHOP_CHECKED_RGB
|
||||||
|
return detector.region_contains_color(_checkbox_region(config, row, col), lo, hi)
|
||||||
|
|
||||||
|
|
||||||
|
def select_targets(driver, config, targets):
|
||||||
|
"""OCR-verify each target's price, then click its checkbox and confirm
|
||||||
|
it registers as checked. Returns (selected, skipped); a price mismatch
|
||||||
|
or an unconfirmed checkbox skips just that target rather than aborting
|
||||||
|
the whole run (mirrors story_sweep's per-target skip-not-abort design).
|
||||||
|
"""
|
||||||
|
selected = []
|
||||||
|
skipped = []
|
||||||
|
for row, col, name, expected_price in targets:
|
||||||
|
price = detector.read_int(_price_rect(config, row, col))
|
||||||
|
if price != expected_price:
|
||||||
|
print(f"[shop] '{name}' price read as {price}, expected {expected_price} -- skipping (catalog may have changed)")
|
||||||
|
skipped.append((name, "price_mismatch"))
|
||||||
|
continue
|
||||||
|
x, y = _checkbox_center(config, row, col)
|
||||||
|
driver.click(x, y)
|
||||||
|
driver.wait(0.3)
|
||||||
|
if not _is_checked(driver, config, row, col):
|
||||||
|
print(f"[shop] '{name}' checkbox did not register as checked -- skipping")
|
||||||
|
skipped.append((name, "checkbox_not_confirmed"))
|
||||||
|
continue
|
||||||
|
selected.append((name, expected_price))
|
||||||
|
return selected, skipped
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_purchase(driver, config):
|
||||||
|
"""Click the bulk Buy button, then click through the purchase-confirm
|
||||||
|
dialog and the reward-acquired banner. Both dim
|
||||||
|
config.SHOP_OVERLAY_PROBE away from pure white; press Enter until it
|
||||||
|
reads idle again, bounded by config.SHOP_PURCHASE_MAX_ENTER_PRESSES --
|
||||||
|
this project doesn't press keys past a bound against an unrecognized
|
||||||
|
state (see navigation.wait_for_state). Returns True once idle, False if
|
||||||
|
it never clears within the bound.
|
||||||
|
"""
|
||||||
|
driver.click(*config.SHOP_BUY_BUTTON)
|
||||||
|
driver.wait(1.5)
|
||||||
|
min_ch = config.SHOP_OVERLAY_IDLE_MIN_CHANNEL
|
||||||
|
for _ in range(config.SHOP_PURCHASE_MAX_ENTER_PRESSES):
|
||||||
|
r, g, b = driver.color_at(*config.SHOP_OVERLAY_PROBE)
|
||||||
|
if r > min_ch and g > min_ch and b > min_ch:
|
||||||
|
return True
|
||||||
|
driver.keypress("Return")
|
||||||
|
driver.wait(1.5)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def run_shop_tab(driver, config, *, tab_button, targets, balance_rect, currency_label):
|
||||||
|
"""Shared control flow for both shop tabs: open the tab, verify enough
|
||||||
|
currency for everything configured, select+buy, report the outcome.
|
||||||
|
Ported from the reference's common_shop.py/tactical_challenge_shop.py
|
||||||
|
implement() -- both are this same shape (read assets, calculate cost,
|
||||||
|
buy, verify), differing only in tab entry point and currency.
|
||||||
|
"""
|
||||||
|
if not targets:
|
||||||
|
print(f"[shop] no targets configured for {currency_label}, nothing to do")
|
||||||
|
return "no_targets"
|
||||||
|
|
||||||
|
driver.click(*tab_button)
|
||||||
|
driver.wait(1.5)
|
||||||
|
if not navigation.is_on_subscreen(driver):
|
||||||
|
print("[shop] could not confirm shop tab opened, aborting without pressing further keys")
|
||||||
|
return "navigation_failed"
|
||||||
|
|
||||||
|
balance = detector.read_int(balance_rect)
|
||||||
|
if balance is None:
|
||||||
|
print(f"[shop] could not read {currency_label} balance, aborting without spending")
|
||||||
|
return "balance_unreadable"
|
||||||
|
|
||||||
|
total_cost = sum(price for _, _, _, price in targets)
|
||||||
|
if balance < total_cost:
|
||||||
|
print(f"[shop] insufficient {currency_label}: have {balance}, need {total_cost} -- stopping, nothing bought")
|
||||||
|
return "inadequate_assets"
|
||||||
|
|
||||||
|
selected, skipped = select_targets(driver, config, targets)
|
||||||
|
if not selected:
|
||||||
|
print("[shop] nothing selected (all targets skipped), cancelling")
|
||||||
|
return "nothing_selected"
|
||||||
|
|
||||||
|
if not confirm_purchase(driver, config):
|
||||||
|
print("[shop] purchase confirmation did not resolve to an idle screen within the retry bound -- stopping, check the game manually")
|
||||||
|
return "unrecognized_state"
|
||||||
|
|
||||||
|
spent = sum(price for _, price in selected)
|
||||||
|
msg = f"[shop] bought {len(selected)} item(s) for {spent} {currency_label}"
|
||||||
|
if skipped:
|
||||||
|
msg += f", skipped {len(skipped)}"
|
||||||
|
print(msg)
|
||||||
|
return "purchased"
|
||||||
11
ba_daily.py
11
ba_daily.py
@ -3,17 +3,20 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from ba_auto import config, driver
|
from ba_auto import config, driver
|
||||||
from ba_auto.tasks import cafe, mailbox, stamina, story_sweep
|
from ba_auto.tasks import cafe, mailbox, shop_common, shop_tactical, stamina, story_sweep
|
||||||
|
|
||||||
TASKS = {
|
TASKS = {
|
||||||
"mailbox": mailbox.run,
|
"mailbox": mailbox.run,
|
||||||
"cafe": cafe.run,
|
"cafe": cafe.run,
|
||||||
"stamina": stamina.run,
|
"stamina": stamina.run,
|
||||||
"story_sweep": story_sweep.run,
|
"story_sweep": story_sweep.run,
|
||||||
|
"shop_common": shop_common.run,
|
||||||
|
"shop_tactical": shop_tactical.run,
|
||||||
}
|
}
|
||||||
# story_sweep is opt-in only (not in the default flow): it spends AP on a
|
# story_sweep and both shop tasks are opt-in only (not in the default flow):
|
||||||
# randomly-picked stage rather than reclaiming something free, which is a
|
# they spend AP/credits/tactical coin on a configured buy list rather than
|
||||||
# real resource decision the default unattended run shouldn't make blindly.
|
# reclaiming something free, which is a real resource decision the default
|
||||||
|
# unattended run shouldn't make blindly.
|
||||||
DEFAULT_ORDER = ["mailbox", "cafe", "stamina"]
|
DEFAULT_ORDER = ["mailbox", "cafe", "stamina"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
40
plan.md
40
plan.md
@ -156,8 +156,9 @@ Do not implement a feature without filling at least the relevant row.
|
|||||||
| Cafe pats + income | Migrated: `ba_auto/tasks/cafe.py` verifies each room/dialog opened via `driver.color_at` before acting; sparkle detection now runs in-process via `ba_auto/detector.py` instead of a per-click subprocess | Done |
|
| Cafe pats + income | Migrated: `ba_auto/tasks/cafe.py` verifies each room/dialog opened via `driver.color_at` before acting; sparkle detection now runs in-process via `ba_auto/detector.py` instead of a per-click subprocess | Done |
|
||||||
| Stamina/AP (mission claim) | Migrated (partial, Phase 8): `ba_auto/tasks/stamina.py` claims the Mission panel's bulk "一括受取" button. Daily Free Power (real-money purchase menu) intentionally not implemented | Daily Free Power still not started |
|
| Stamina/AP (mission claim) | Migrated (partial, Phase 8): `ba_auto/tasks/stamina.py` claims the Mission panel's bulk "一括受取" button. Daily Free Power (real-money purchase menu) intentionally not implemented | Daily Free Power still not started |
|
||||||
| Normal/Hard story AP sweep | Done (Phase 10, supersedes Phase 9's random-pick design): `ba_auto/tasks/story_sweep.py` sweeps a config-driven list of exact `(region, stage, count)` targets (`config.STORY_SWEEP_TARGETS`), navigating to each via OCR (region-number read + delta-click, stage-label OCR match) instead of "latest region, random stage." Opt-in only (`story_sweep` command), not part of the default daily flow | Done |
|
| Normal/Hard story AP sweep | Done (Phase 10, supersedes Phase 9's random-pick design): `ba_auto/tasks/story_sweep.py` sweeps a config-driven list of exact `(region, stage, count)` targets (`config.STORY_SWEEP_TARGETS`), navigating to each via OCR (region-number read + delta-click, stage-label OCR match) instead of "latest region, random stage." Opt-in only (`story_sweep` command), not part of the default daily flow | Done |
|
||||||
| Shared driver | `ba_auto/driver.py` built (`run_command`, `focus_game`, `click`, `move_mouse`, `scroll`, `keypress`, `screenshot`, `wait`, `color_at`); `click()` now splits `mousemove`/`click` into two xdotool calls (Phase 8 finding — fixes a real source of click flakiness); wired into `mailbox.py`, `cafe.py`, `stamina.py`, `story_sweep.py` | Extend with new primitives as future tasks need them |
|
| Common Shop / Tactical Shop | Done (Phase 11): `ba_auto/tasks/shop_common.py` / `shop_tactical.py` share a checkbox-grid-then-bulk-buy flow (`ba_auto/tasks/shop_utils.py`) against config-driven `(row, col, name, expected_price)` targets, price-OCR-verified before each click. Live-tested with real purchases in both shops. Opt-in only (`shop_common`/`shop_tactical` commands), not part of the default daily flow | Done |
|
||||||
| Python CLI | Built: `ba_daily.py` dispatches `mailbox`/`cafe`/`stamina`/`story_sweep`/default flow | Extend as new tasks are added |
|
| Shared driver | `ba_auto/driver.py` built (`run_command`, `focus_game`, `click`, `move_mouse`, `scroll`, `keypress`, `screenshot`, `wait`, `color_at`); `click()` now splits `mousemove`/`click` into two xdotool calls (Phase 8 finding — fixes a real source of click flakiness); wired into `mailbox.py`, `cafe.py`, `stamina.py`, `story_sweep.py`, `shop_common.py`, `shop_tactical.py` | Extend with new primitives as future tasks need them |
|
||||||
|
| Python CLI | Built: `ba_daily.py` dispatches `mailbox`/`cafe`/`stamina`/`story_sweep`/`shop_common`/`shop_tactical`/default flow | Extend as new tasks are added |
|
||||||
| Reference mapping | Built: `ba_auto/reference_notes/mapping.md` | Fill in reference file/function columns per feature |
|
| Reference mapping | Built: `ba_auto/reference_notes/mapping.md` | Fill in reference file/function columns per feature |
|
||||||
| Everything else | Not started | Implement reference-first in Python |
|
| Everything else | Not started | Implement reference-first in Python |
|
||||||
|
|
||||||
@ -367,6 +368,22 @@ Also fixed in passing, found only because live testing exercised the actual home
|
|||||||
|
|
||||||
Verified live end-to-end at least once, real AP spent: a genuine 5x sweep of 30-3 (AP 53→~5, confirmed via the "掃討完了" results screen's reward totals), including clicking through the usage-confirm dialog, the SKIP animation-skip screen, and the final reward-totals OK, landing back on the bare stage-info modal afterward. The genuine insufficient-AP path was also verified live (correctly cancelled the real "AP購入" purchase prompt without spending Pyroxene). Not yet re-verified end-to-end with the final rewritten code specifically (the color-based dynamic button-finding in `_watch_sweep_result`) at a nonzero AP balance — the manual walkthrough that discovered the dialogs used direct scripted clicks before the code was rewritten to match; the rewritten code's color-matching logic was separately verified offline against the exact screenshots captured live (all four dialog states correctly classified), but a fresh live run once AP regenerates would close that last gap. Not verified: sweeping a "-A" bonus stage (needs its own layout re-check, see above), an integer (non-"max") configured count actually being clicked via `SWEEP_PLUS_BUTTON`, and Hard-mode tab stages.
|
Verified live end-to-end at least once, real AP spent: a genuine 5x sweep of 30-3 (AP 53→~5, confirmed via the "掃討完了" results screen's reward totals), including clicking through the usage-confirm dialog, the SKIP animation-skip screen, and the final reward-totals OK, landing back on the bare stage-info modal afterward. The genuine insufficient-AP path was also verified live (correctly cancelled the real "AP購入" purchase prompt without spending Pyroxene). Not yet re-verified end-to-end with the final rewritten code specifically (the color-based dynamic button-finding in `_watch_sweep_result`) at a nonzero AP balance — the manual walkthrough that discovered the dialogs used direct scripted clicks before the code was rewritten to match; the rewritten code's color-matching logic was separately verified offline against the exact screenshots captured live (all four dialog states correctly classified), but a fresh live run once AP regenerates would close that last gap. Not verified: sweeping a "-A" bonus stage (needs its own layout re-check, see above), an integer (non-"max") configured count actually being clicked via `SWEEP_PLUS_BUTTON`, and Hard-mode tab stages.
|
||||||
|
|
||||||
|
### Phase 11: Common Shop + Tactical Shop
|
||||||
|
|
||||||
|
**Status: Done.** Ported `module/shop/common_shop.py` / `module/shop/tactical_challenge_shop.py`'s `implement()` and the shared `module/shop/shop_utils.py` (`to_common_shop`, `get_item_position`/`ensure_choose`/`buy`) to `ba_auto/tasks/shop_common.py` / `shop_tactical.py`, sharing control flow through a new `ba_auto/tasks/shop_utils.py`.
|
||||||
|
|
||||||
|
Key design call, made after live-capturing both shop tabs before writing any code: the reference's own item identification inside the grid is **not** OCR-based. `get_item_position` scans fixed pixel columns and matches a purchasable-state color plus a currency-icon template, then maps that grid position to an item identity via `self.static_config.common_shop_price_list` — a table sourced from an external resource this repo doesn't contain (the reference dataclass just declares the field; the actual values are fetched elsewhere, at BAAS's own runtime). So porting this feature couldn't mean "OCR the item names" (the reference doesn't do that either) — it meant building our own local equivalent of that static table by live-capturing the real catalog and letting the user pick their buy list from it, same shape as `STORY_SWEEP_TARGETS`. `config.COMMON_SHOP_TARGETS` / `config.TACTICAL_SHOP_TARGETS` are `(row, col, item name (comment only), expected price)` tuples; identification is by fixed grid position, with price-digit OCR (something the reference doesn't even do per-item) layered on as an extra live-catalog-drift safety net, consistent with this project's verify-before-spend pattern elsewhere.
|
||||||
|
|
||||||
|
What was found live, captured before writing any code (see the session's live-capture screenshots, not kept in the repo):
|
||||||
|
|
||||||
|
- **Both shop tabs share one UI**: a 4-column checkbox grid per item, then a single bulk "購入" (Buy) button that appears once ≥1 item is checked, rather than the reference's per-item purchase flow. Checked state renders a distinct vivid yellow-green on the checkbox glyph, easily told apart from the plain white/grey unchecked state by `detector.region_contains_color` — no template matching needed.
|
||||||
|
- **The tactical shop's tab list (7 entries) fits on screen with no scrolling**, so `config.SHOP_TAB_TACTICAL` is a fixed click rather than a port of the reference's `goto_shop_by_name` OCR swipe-search — there's nothing to search for on this account, so a fixed click is the faithful choice here, not a shortcut around OCR (see `CLAUDE.md`'s OCR policy: only skip OCR where the reference's own need for it doesn't apply).
|
||||||
|
- **Price-digit OCR needed real calibration**, same as Phase 10's stage labels: a rect wide enough for the widest configured price (500,000) without also catching the neighboring column's card, and narrow enough on the left to exclude the currency icon (which OCR otherwise misreads as a spurious leading digit — confirmed live, e.g. the top-bar credit balance read "454920755" instead of "154920755" until the icon was excluded).
|
||||||
|
- **One shared corner-pixel probe (`config.SHOP_OVERLAY_PROBE`, a bottom-left point) detects both the purchase-confirm dialog and the post-purchase "報酬獲得!" (reward acquired) banner** — both dim it away from pure white; it stays pure white with nothing open, confirmed stable across tab switches and scrolling. `shop_utils.confirm_purchase` presses Enter in a bounded loop until it reads idle again, rather than tracking each dialog's own layout individually.
|
||||||
|
- **Live-tested with real purchases, not just offline-verified.** With the user's explicit buy lists confirmed first (8 Common Shop items: 初級/中級/上級/最上級レポート + 初級/中級/上級/最上級強化珠; 2 Tactical Shop items: 初級/中級栄養ドリンク), both shops were run for real: Common Shop total cost matched the pre-calculated 1,211,500 credits exactly (confirmed against the game's own confirmation-dialog total); Tactical Shop's AP gain (+90) and coin spend (-45) matched exactly. Both purchases resolved cleanly back to an idle screen via the overlay-probe loop.
|
||||||
|
- **Discovered live, not anticipated going in: these shop items have a per-refresh-cycle purchase cap that isn't shown as a visible counter** (unlike the Pyroxene-shop tab's explicit "あと1回購入可能" labels — Common Shop items just look normal until you've already bought them, then go quietly unresponsive). Found by re-running the actual `shop_common` CLI task shortly after the manual purchase above: every configured item's checkbox failed to register as checked, and a direct test of the individual per-item "購入" button and the page's own "全て選択" (select-all) control confirmed those specific items are genuinely non-interactive right now (select-all successfully picked up *other*, not-yet-purchased items further down the list), while credits stayed unchanged throughout. The task correctly reported "nothing selected, cancelling" and spent nothing, rather than misfiring — a real edge case the price-verify + checkbox-confirm design caught safely, not a bug in it. Net effect: a fully "fresh, everything-available" unattended run hasn't been re-verified end-to-end today, since this account's cycle allowance for these specific items was already spent via the same session's manual calibration. Next run after the shop's own refresh timer (~5h, shown in-game as "更新まで") should exercise that path for real.
|
||||||
|
- **Not verified**: non-fully-visible target rows requiring a scroll (both current buy lists happen to be fully visible without scrolling, so `shop_utils` has no scroll/pagination logic yet — would need porting `buy()`'s `last_checked_idx`/swipe-diff tracking if a future buy list needs it); the pre-purchase "insufficient assets" abort path (both currencies were comfortably sufficient this run); the paid manual shop-refresh flow (`更新` button) — deliberately not automated for v1, same reasoning as Daily Free Power (real-currency spend needs explicit human intent, not a default unattended path).
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
### OCR
|
### OCR
|
||||||
@ -549,6 +566,8 @@ Arena is high-value but more risky than fixed claim tasks.
|
|||||||
|
|
||||||
### 8. Common Shop + Tactical Shop
|
### 8. Common Shop + Tactical Shop
|
||||||
|
|
||||||
|
**Status: Done — see Phase 11.**
|
||||||
|
|
||||||
Auto-buy configured items.
|
Auto-buy configured items.
|
||||||
|
|
||||||
Reference:
|
Reference:
|
||||||
@ -564,16 +583,16 @@ Local targets:
|
|||||||
```
|
```
|
||||||
ba_auto/tasks/shop_common.py
|
ba_auto/tasks/shop_common.py
|
||||||
ba_auto/tasks/shop_tactical.py
|
ba_auto/tasks/shop_tactical.py
|
||||||
|
ba_auto/tasks/shop_utils.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Needs:
|
Implemented version:
|
||||||
|
|
||||||
- OCR for currency balances
|
- OCR for currency balances (top-bar credits, in-panel tactical coin) — done
|
||||||
- shop tab detection
|
- shop tab detection — done via a fixed click (tactical tab list fits on screen with no scroll needed on this account, confirmed live); no scroll/pagination logic yet since both current buy lists are fully visible without scrolling
|
||||||
- configured buy list
|
- configured buy list — done, `config.COMMON_SHOP_TARGETS`/`config.TACTICAL_SHOP_TARGETS`, fixed `(row, col, name, expected_price)` per item (see Phase 11 for why identification is by grid position + price-OCR-verify rather than per-item OCR — the reference doesn't OCR item names here either)
|
||||||
- safe purchase confirmation logic
|
- safe purchase confirmation logic — done, a single overlay-darkness probe covers both the confirm dialog and the reward-acquired banner
|
||||||
|
- no-refresh (the paid manual `更新` refresh button is deliberately not automated, same reasoning as Daily Free Power)
|
||||||
Start with a no-refresh, fixed configured buy list.
|
|
||||||
|
|
||||||
### 9. Lesson / Schedule
|
### 9. Lesson / Schedule
|
||||||
|
|
||||||
@ -846,7 +865,8 @@ should run the default daily sequence.
|
|||||||
12. Implement Normal/Hard story AP sweep. — Done (Phase 9 built a random-pick heuristic; Phase 10 replaced it with the reference's actual OCR-based deterministic stage targeting, per Phase 9's own retrospective)
|
12. Implement Normal/Hard story AP sweep. — Done (Phase 9 built a random-pick heuristic; Phase 10 replaced it with the reference's actual OCR-based deterministic stage targeting, per Phase 9's own retrospective)
|
||||||
13. Implement group/club AP. — Not started
|
13. Implement group/club AP. — Not started
|
||||||
14. Set up OCR and port it for whichever remaining feature's reference implementation depends on it — not a blanket "only when needed" deferral; see `CLAUDE.md` → "OCR policy"
|
14. Set up OCR and port it for whichever remaining feature's reference implementation depends on it — not a blanket "only when needed" deferral; see `CLAUDE.md` → "OCR policy"
|
||||||
15. Attempt Arena/Shop/Lesson once OCR is in place, since their reference implementations depend on it.
|
15. Implement Common Shop + Tactical Shop. — Done (Phase 11)
|
||||||
|
16. Attempt Arena/Lesson once OCR is in place, since their reference implementations depend on it.
|
||||||
|
|
||||||
## Claude Code guidance summary
|
## Claude Code guidance summary
|
||||||
|
|
||||||
|
|||||||
2
setup.sh
2
setup.sh
@ -60,5 +60,5 @@ rm -rf "$HOME/ba_auto"
|
|||||||
cp -r ba_auto "$HOME/ba_auto"
|
cp -r ba_auto "$HOME/ba_auto"
|
||||||
|
|
||||||
echo "== Done =="
|
echo "== Done =="
|
||||||
echo "Run with: ~/ba_dailies.sh [mailbox|cafe|stamina|story_sweep]"
|
echo "Run with: ~/ba_dailies.sh [mailbox|cafe|stamina|story_sweep|shop_common|shop_tactical]"
|
||||||
echo "(requires the game already running, window titled 'BlueArchive')"
|
echo "(requires the game already running, window titled 'BlueArchive')"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user