- Updated arena.py to allow multiple battles per invocation, looping until tickets are exhausted or a rank-1 condition is met. Introduced _fight_one function for single battle logic and added cooldown handling between fights. - Enhanced lesson.py to implement a tiered priority system for scheduling lessons based on student slots available, replacing the previous highest affection value selection. Introduced functions for scanning all regions and building a priority queue for lesson scheduling. - Centralized return-to-home logic in ba_daily.py to ensure the game returns to the home screen before and after each task, improving robustness against navigation issues. - Added retry mechanism for returning to home, allowing for transient navigation issues to be handled gracefully without aborting tasks.
888 lines
51 KiB
Python
888 lines
51 KiB
Python
"""Central configuration for ba-auto-daily, ported from the old ba_dailies.sh."""
|
|
import os
|
|
|
|
DISPLAY = ":0"
|
|
XAUTHORITY = "/run/user/1000/gdm/Xauthority"
|
|
ENV = {**os.environ, "DISPLAY": DISPLAY, "XAUTHORITY": XAUTHORITY}
|
|
|
|
WINDOW_NAME = "BlueArchive"
|
|
ASSET_DIR = os.path.expanduser("~/ba_assets")
|
|
|
|
# Runtime working files (probe/detection screenshots); never /tmp, per CLAUDE.md.
|
|
SCRATCHPAD_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scratchpad")
|
|
os.makedirs(SCRATCHPAD_DIR, exist_ok=True)
|
|
|
|
# Refined from (1726, 60): that coordinate sat on the edge of the icon's
|
|
# hitbox and intermittently missed during live testing.
|
|
MAILBOX_ICON = (1732, 50)
|
|
CLAIM_ALL = (1691, 1128)
|
|
|
|
MISSION_ICON = (75, 350)
|
|
# Background pixel inside the "一括受取" (claim all) button on the Mission
|
|
# panel: bright yellow (r~250, r-b~180+) when something is claimable, flat
|
|
# grey (r-b<20) when not. Calibrated live at 1920x1200.
|
|
MISSION_CLAIM_PROBE = (1600, 1100)
|
|
|
|
CAFE_ICON = (165, 1100)
|
|
CAFE_ROOM_SWITCH = (190, 160)
|
|
CAFE_INCOME = (1780, 1105)
|
|
# Max sparkle-detection attempts per room (hits and misses both count --
|
|
# sparkles are on a per-student cooldown, so most checks legitimately find
|
|
# nothing and the loop keeps polling rather than giving up after one miss).
|
|
CAFE_MAX_CLICKS_PER_ROOM = 15
|
|
CAFE_SPARKLE_TEMPLATE = os.path.join(ASSET_DIR, "cafe_sparkle.png")
|
|
# A pat that crosses an affection-rank threshold shows a full-screen "絆ラン
|
|
# クアップ!" (Bond Rank Up!) cutscene with no cafe header visible at all --
|
|
# confirmed against screenshots/cafe/student/01-02: navigation.is_on_subscreen's
|
|
# header probe reads (183,220,240) there (r<200, fails) vs (248,249,250) on
|
|
# the normal cafe screen (r>200, passes), so the existing header-brightness
|
|
# check already tells the two apart. Bounds how many Enter presses
|
|
# _dismiss_rank_up_if_shown will try before giving up.
|
|
CAFE_RANK_UP_DISMISS_RETRIES = 5
|
|
|
|
# Home -> お仕事 (Work hub) -> 任務 (Task) card -> Normal/Hard story region browser.
|
|
WORK_ICON = (1793, 1138)
|
|
# Moved up from the original (1370, 450): that point sat close enough to the
|
|
# 任務 card's bottom edge that a live Phase 10 run missed and landed on the
|
|
# "総力戦" (Total War) card in the row below instead -- confirmed live via
|
|
# screenshot, not just a hunch. (1250, 380) sits solidly mid-card, on the
|
|
# "任務" title text itself, well clear of every edge.
|
|
TASK_CARD = (1250, 380)
|
|
REGION_RIGHT_ARROW = (1862, 598)
|
|
# Pixel-scanline-scanned (not visually estimated -- see plan.md Phase 8's
|
|
# lesson) from scratchpad/stage_info.png: the "<" chevron's navy-blue pixel
|
|
# centroid was (66, 597), mirroring REGION_RIGHT_ARROW. Used for the
|
|
# OCR-driven to_region port (ba_auto/tasks/story_sweep.py) to step backward
|
|
# when the current region is past the target.
|
|
REGION_LEFT_ARROW = (66, 598)
|
|
# Bounds the "read region, click delta, re-check" loop in
|
|
# ba_auto/tasks/story_sweep.py's _go_to_region. A correct read normally
|
|
# converges in one round; this just guards against a stuck OCR misread.
|
|
REGION_NAV_MAX_ATTEMPTS = 8
|
|
|
|
# Region-number readout on the region browser's left panel (the "Area 30"
|
|
# card's big digits, below the smaller "Area" label). Rect pixel-scanned from
|
|
# scratchpad/stage_info.png: the "Area" label occupies roughly y 295-325, the
|
|
# number itself y 330-372 -- this rect isolates just the digits. Replaces
|
|
# _go_to_latest_region's "spam the arrow and hope" (see plan.md Phase 9's
|
|
# retrospective) with task_utils.py::to_region's actual OCR-read-and-click-
|
|
# the-exact-delta approach.
|
|
REGION_NUMBER_OCR_RECT = (175, 325, 250, 380)
|
|
|
|
# Stage list panel (right side of the region browser). Scrolling to either
|
|
# extreme always shows exactly 4 full stage rows, since every region has at
|
|
# least 5 stages -- scrolling past either end is a harmless no-op (verified
|
|
# live), so a generous bounded click count is safe. Each stage row's "入場"
|
|
# (enter) button sits at STAGE_ENTER_X across both scroll extremes; the two
|
|
# row-position sets below were measured at each extreme.
|
|
STAGE_LIST_SCROLL_POINT = (1400, 700)
|
|
# 10 was the original (Phase 9) calibration, but live re-testing during
|
|
# Phase 10 found it insufficient to reach the opposite extreme when the list
|
|
# was already scrolled near the other end (it undershot, landing between the
|
|
# two calibrated row-position sets and producing garbled OCR reads) -- 20
|
|
# reliably reached either extreme regardless of starting position. Scrolling
|
|
# past either end remains a harmless no-op (verified live both phases).
|
|
STAGE_LIST_SCROLL_CLICKS = 20
|
|
STAGE_ENTER_X = 1683
|
|
STAGE_ROWS_AT_TOP_Y = (424, 570, 718, 866)
|
|
STAGE_ROWS_AT_BOTTOM_Y = (483, 630, 778, 926)
|
|
|
|
# Stage-label OCR rect (e.g. "30-1", "30-A"), offset from a row's known
|
|
# center y above. Pixel-scanned across all eight row positions (both
|
|
# STAGE_ROWS_AT_TOP_Y and _BOTTOM_Y) in live captures -- x[1030,1150],
|
|
# y[row_y-40, row_y-4] consistently isolates just the label text line above
|
|
# the row's star-rating icons. The symmetric row_y+/-40 crop tried first
|
|
# during calibration included the stars below and reliably broke OCR (empty
|
|
# or garbled reads) even with a character whitelist -- see plan.md Phase 10.
|
|
# Replaces _pick_random_stage_row's "scroll to an extreme, grab a random one
|
|
# of the 4 rows" with a scoped-down port of the reference's
|
|
# swipe_search_target_str: since this client's stage list only ever has 2
|
|
# relevant scroll positions (top/bottom extremes, both already known-good),
|
|
# full swipe-and-retry generality isn't needed -- just OCR each of the 4
|
|
# visible rows at each extreme and match by label text.
|
|
STAGE_LABEL_OCR_X = (1030, 1150)
|
|
STAGE_LABEL_OCR_Y_PAD = (40, 4)
|
|
|
|
# 任務情報 (stage info) modal's sweep sub-panel. This modal is wide enough
|
|
# that navigation.MODAL_DIM_PROBE (960, 200) lands on the modal's own white
|
|
# card instead of the dimmed backdrop -- use a corner point that's outside
|
|
# the card in either scroll/region state instead.
|
|
STAGE_MODAL_PROBE = (1870, 600)
|
|
# Regular numbered stages (30-1..30-5) render an extra "集中指揮"/"簡易攻略"
|
|
# tab row and a manual "任務開始" panel below the sweep panel that the
|
|
# bonus "-A" stage Phase 9 originally calibrated against does not have --
|
|
# discovered live during Phase 10 when Phase 9's coordinates (calibrated
|
|
# only against 30-A) missed the MAX button on 30-3 by ~43px vertically.
|
|
# These values are pixel-scanned against 30-3's tabbed layout and are what
|
|
# config.STORY_SWEEP_TARGETS will hit in the common case (sweeping a
|
|
# regular numbered stage, not the "-A" bonus stage). If a target's stage is
|
|
# "A", these may be off by the same ~43px the old (untabbed) calibration
|
|
# used -- not yet re-confirmed against an actual "-A" stage since this fix;
|
|
# see plan.md Phase 10.
|
|
SWEEP_MAX_BUTTON = (1620, 550)
|
|
SWEEP_START_BUTTON = (1400, 710)
|
|
# The "-" stepper button next to the sweep count: flat grey (240,240,239)
|
|
# while count is still at its default of 1, vivid orange (255,111,0) once
|
|
# MAX (or any +) has raised it. Used to verify the MAX click actually landed
|
|
# instead of trusting a single click blindly, since this gates real AP spend.
|
|
SWEEP_MINUS_BUTTON_PROBE = (1285, 555)
|
|
# The modal's own "X" close icon (top-right corner of the white card).
|
|
# Escape does NOT close this modal (confirmed live: two Escape presses left
|
|
# it open with focus on the live "任務開始"/start-mission button) -- must
|
|
# click this explicitly. Pinned via pixel-scanline scan of the glyph's
|
|
# crossing point, not visual estimation.
|
|
#
|
|
# The whole card (not just the sweep sub-panel) is vertically centered on
|
|
# its own content height rather than anchored at a fixed absolute position
|
|
# -- confirmed live during Phase 10: the tabbed regular-stage layout (see
|
|
# SWEEP_MAX_BUTTON above) is taller than the "-A" bonus-stage layout this
|
|
# was originally calibrated against, and its X button sits ~46px higher
|
|
# on screen (225 vs the old 271) as a result. This value is re-measured
|
|
# against the taller, tabbed layout.
|
|
STAGE_MODAL_CLOSE_BUTTON = (1691, 225)
|
|
# "+" stepper button, for configured exact (non-"max") sweep counts. Pinned
|
|
# via color-scan (bright cyan glyph centroid) against 30-3's tabbed layout
|
|
# (see SWEEP_MAX_BUTTON above) -- unlike SWEEP_MAX_BUTTON/
|
|
# SWEEP_MINUS_BUTTON_PROBE this specific button has NOT been live-clicked
|
|
# yet; confirm it before relying on a non-"max" configured count (see
|
|
# plan.md Phase 10).
|
|
SWEEP_PLUS_BUTTON = (1520, 550)
|
|
|
|
# Clicking 掃討開始 (start sweep) always raises an "AP<N>使用して、掃討を
|
|
# <M>回行いますか?" usage-confirmation dialog before the sweep actually
|
|
# runs -- discovered live during Phase 10; the previous design had no
|
|
# handling for this dialog at all, which is what a crude early placeholder
|
|
# probe was misreading as "inadequate_ap" on every sweep, successful or not.
|
|
#
|
|
# If AP is too low for even one sweep (confirmed live by deliberately
|
|
# emptying the count via MAX/"+" at low AP), a dialog that looks the same
|
|
# and sits at the *same* OK-button position appears instead, but titled
|
|
# "AP購入" (real-currency AP purchase) with a gold/yellow OK button instead
|
|
# of cyan. The two are told apart by that color, not by position.
|
|
SWEEP_CONFIRM_BUTTON = (1150, 810)
|
|
SWEEP_CONFIRM_CANCEL_BUTTON = (770, 810)
|
|
SWEEP_CONFIRM_CYAN = ((90, 190, 230), (200, 240, 256))
|
|
SWEEP_CONFIRM_GOLD = ((200, 200, 50), (256, 256, 150))
|
|
|
|
# Region spanning every button this task clicks through after 掃討開始: the
|
|
# AP-usage-confirm OK above, and the "掃討完了" (sweep complete) results
|
|
# screen's "SKIP" (first, skips the reward-reveal animation) and final "OK"
|
|
# (after full reward totals appear) buttons. SKIP and the final OK share
|
|
# SWEEP_CONFIRM_CYAN's color but sit ~120px apart vertically, so this task
|
|
# finds whichever one is showing by color within this region instead of
|
|
# hardcoding each dialog's exact Y position.
|
|
SWEEP_RESULT_BUTTON_REGION = (700, 700, 1300, 1050)
|
|
|
|
# (region, stage, count) targets to sweep, mirroring the reference's
|
|
# unfinished_normal_tasks shape (module/explore_tasks/sweep_task.py). `stage`
|
|
# is 1-5, or the string "A" for the bonus stage that only exists when
|
|
# region % 3 == 0. `count` is a positive int (uses the "+" stepper) or the
|
|
# literal string "max" (uses the in-game MAX button).
|
|
#
|
|
# Empty by default -- the earlier placeholder here was (1, 1, "max"), which
|
|
# a real run then dutifully swept region 1 stage 1 instead of the account's
|
|
# actual last region, since story_sweep has no "find the latest region"
|
|
# heuristic anymore (see plan.md Phase 9's retrospective for why that
|
|
# heuristic was removed). Add entries here for any additional fixed targets
|
|
# you want swept every run, on top of the daily rotation target below.
|
|
STORY_SWEEP_TARGETS = []
|
|
|
|
# Daily-rotating target: sweeps a single region, cycling through its stages
|
|
# one per day rather than grinding the same stage every run, per explicit
|
|
# user direction. `ROTATION_STAGE_COUNT` is how many stages that region has
|
|
# (1..N); which one runs today is `today's date -> N` via a plain date
|
|
# ordinal modulo, not the calendar day-of-year, so the cycle doesn't skip or
|
|
# repeat around a year boundary. Set STORY_SWEEP_ROTATION_REGION to None to
|
|
# disable this and only sweep STORY_SWEEP_TARGETS.
|
|
STORY_SWEEP_ROTATION_REGION = 30
|
|
STORY_SWEEP_ROTATION_STAGE_COUNT = 6
|
|
STORY_SWEEP_ROTATION_COUNT = "max"
|
|
|
|
# Event sweep (module/sweep_activity.py -> module/activities/<current_event>.py
|
|
# -> module/activities/activity_utils.py's activity_sweep). Ported per
|
|
# explicit user direction 2026-07-10: rather than the reference's config-
|
|
# string sweep-list parsing (preprocess_activity_region/
|
|
# preprocess_activity_sweep_times, e.g. "9,10,11" stages x "0.5,3,1/3"
|
|
# counts), this sweeps exactly one stage per run, chosen from a fixed
|
|
# sub-range of the current event's stage list via the same date-ordinal-
|
|
# modulo rotation as STORY_SWEEP_ROTATION_* (see story_sweep.py's
|
|
# _rotation_target docstring for why that's a plain date ordinal modulo, not
|
|
# day-of-year).
|
|
#
|
|
# Live-calibrated against nik-gpu on 2026-07-10 against the currently-running
|
|
# "鉄道爆走事件" event (12 stages, all already 3-starred on this account) --
|
|
# see scratchpad/event_probe_* for the captured screenshots this was
|
|
# pixel-scanned/OCR-tested against. Zero real AP was spent during calibration
|
|
# (every confirm dialog reached was cancelled via Escape, verified by the AP
|
|
# counter being unchanged before/after).
|
|
|
|
# Home screen's top-right event badge/thumbnail slot. Chosen over the
|
|
# bottom-left banner slot, which cycles between several unrelated banners
|
|
# (gacha pickups, other campaigns) and was confirmed live to not reliably
|
|
# reach the current story event either. This slot is ALSO a rotating
|
|
# carousel, though -- confirmed live post-launch: it cycles between the
|
|
# current event's own countdown (e.g. "終了まであと11日") and OTHER notices,
|
|
# including an already-finished event's remaining reward-claim-period
|
|
# reminder, so a single click can land on a stale event's page instead of
|
|
# the current one. event_sweep.py's own wrong-page detection + retry (via
|
|
# navigation.return_to_home) exists specifically to recover from this, since
|
|
# no fixed click here is guaranteed to hit the right content on the first
|
|
# try. Back-button navigation now goes through navigation.return_to_home's
|
|
# shared BACK_BUTTON constant instead of a dedicated config entry here.
|
|
EVENT_BADGE_ICON = (1787, 300)
|
|
|
|
# The badge carousel's own small pagination dots, directly below the
|
|
# thumbnail -- confirmed LIVE CLICKABLE and immediately switch which item
|
|
# the badge shows, rather than waiting for the carousel's own auto-rotate
|
|
# timer (observed to be slow enough that a fixed page could still be
|
|
# showing 20-30+ seconds later, well past this task's retry window). Pixel-
|
|
# scanned live: with 2 known carousel items, the dots sit ~14px apart
|
|
# starting at x=1780; clicking the leftmost one deterministically selected
|
|
# the current running event over an old event's leftover reward-claim
|
|
# reminder. event_sweep.py clicks a specific dot BEFORE each badge-open
|
|
# attempt (cycling through positions across its outer retry loop) instead
|
|
# of repeatedly clicking the ambiguous badge and hoping the timer has
|
|
# moved on. Clicking a dot position that doesn't exist (beyond however many
|
|
# items are actually queued) is a harmless no-op click on plain background.
|
|
EVENT_BADGE_DOT_Y = 400
|
|
EVENT_BADGE_DOT_X = (1780, 1794, 1808)
|
|
|
|
# A finished event's Quest tab shows plain "イベント期間が終了しました。"
|
|
# (event period has ended) text instead of any stage-row cards. This rect
|
|
# and the lang="jpn" OCR read against it are LIVE-CONFIRMED both ways
|
|
# (2026-07-10, against "嵐過天晴", a genuinely finished event reachable via
|
|
# the badge carousel's 2nd dot position at that time): the OCR returned the
|
|
# exact phrase verbatim on the finished-event page (`detector.read_text`
|
|
# with psm=6), and on the correct/current event's page the same rect reads
|
|
# unrelated stage-list text with no false-positive "終了" substring match.
|
|
# Used as an early-exit, authoritative-when-positive check inside
|
|
# _find_stage_row's scan loop -- a positive match ends the wait immediately
|
|
# as a confirmed wrong page; a negative match does NOT prove the page is
|
|
# right (a different finished event's layout might differ), so patient
|
|
# rescanning still continues regardless either way up to the existing
|
|
# budget. If that budget is ever fully exhausted with no rows and no
|
|
# confirmed finished-page text, _find_stage_row saves a debug screenshot to
|
|
# scratchpad/ (event_sweep_no_rows_debug.png) for further diagnosis.
|
|
EVENT_FINISHED_TEXT_RECT = (1030, 600, 1810, 750)
|
|
|
|
# Top-right tab bar inside the event screen (reference's activity_menu
|
|
# story/mission/challenge tabs -- rendered as English labels "Story / Quest /
|
|
# Challenge" even on this JP client). "Quest" is this client's rendering of
|
|
# the reference's "mission" tab, the one that hosts the sweep-able stage
|
|
# list. Clicked explicitly every run rather than trusting the screen's
|
|
# default landing tab (it happened to default here during calibration, but
|
|
# nothing guarantees that for a future event).
|
|
EVENT_QUEST_TAB = (1416, 165)
|
|
|
|
# Stage list, scrolled to its bottom extreme. Confirmed live this always
|
|
# reveals the event's LAST 5 stages at these exact row positions regardless
|
|
# of starting scroll state -- unlike story_sweep's region stage list, row
|
|
# height here stayed constant across both 1-line and 2-line wrapped titles
|
|
# during calibration, so no separate top/bottom row-position sets were
|
|
# needed for this task's stages-9-12 target range.
|
|
EVENT_STAGE_LIST_SCROLL_POINT = (1400, 700)
|
|
EVENT_STAGE_LIST_SCROLL_CLICKS = 25
|
|
EVENT_STAGE_ROW_Y = (366, 538, 710, 883, 1055)
|
|
EVENT_STAGE_ENTER_X = 1698
|
|
|
|
# Stage-number OCR crop (e.g. "09", "12"), offset from a row's known center
|
|
# y -- rect = (x1, row_y - top_pad, x2, row_y + bottom_pad). Pixel-scanned
|
|
# live across all 5 bottom-extreme row positions; tight enough to exclude
|
|
# the 3-star rating row rendered just below each number.
|
|
EVENT_STAGE_NUMBER_OCR_X = (1055, 1145)
|
|
EVENT_STAGE_NUMBER_OCR_Y_PAD = (30, 5)
|
|
|
|
# 任務情報 (stage info) modal. Corner probe is pixel-identical to
|
|
# story_sweep's STAGE_MODAL_PROBE (same dark-backdrop dialog component) but
|
|
# kept as its own constant to avoid coupling the two tasks' config together.
|
|
EVENT_STAGE_MODAL_PROBE = (1870, 600)
|
|
EVENT_STAGE_MODAL_DIM_MAX_CHANNEL = 150
|
|
|
|
# Sweep sub-panel. Unlike story_sweep, this modal has ONE fixed layout --
|
|
# confirmed identical across both stages tested live (09 and 12), no
|
|
# tabbed-vs-plain variant to account for.
|
|
EVENT_SWEEP_MAX_BUTTON = (1631, 511)
|
|
EVENT_SWEEP_PLUS_BUTTON = (1517, 511)
|
|
# The "-" stepper button's fill: flat grey while count is still at its
|
|
# default of 1, a distinctly saturated coral/orange once MAX (or any "+")
|
|
# has raised it -- confirmed live (171,172,171) vs (251,173,152). Told apart
|
|
# by color *spread* (max-min channel) rather than story_sweep's simpler
|
|
# r>200-and-g<180 rule, since this button's raised color isn't as vividly
|
|
# orange.
|
|
EVENT_SWEEP_MINUS_BUTTON_PROBE = (1281, 511)
|
|
EVENT_SWEEP_START_BUTTON = (1400, 668)
|
|
|
|
# Modal close. Confirmed live this modal DOES close on Escape (unlike
|
|
# story_sweep's, which doesn't -- see STAGE_MODAL_CLOSE_BUTTON above); the
|
|
# X button position is kept as a fallback if Escape ever doesn't clear it.
|
|
EVENT_STAGE_MODAL_CLOSE_BUTTON = (1704, 271)
|
|
|
|
# The AP-usage confirmation dialog raised by 掃討開始, and the post-sweep
|
|
# "掃討完了" SKIP/OK result screen, reuse SWEEP_CONFIRM_BUTTON/
|
|
# SWEEP_CONFIRM_CANCEL_BUTTON/SWEEP_CONFIRM_CYAN/SWEEP_CONFIRM_GOLD directly
|
|
# (defined above under story_sweep) -- confirmed live pixel-identical
|
|
# position and color, since it's the same shared dialog component both
|
|
# tasks reach after their own 掃討開始 click.
|
|
#
|
|
# NOT shared: SWEEP_RESULT_BUTTON_REGION. Confirmed live (2026-07-11,
|
|
# scratchpad/probe_result_button_fp.py) that region's x-range (700-1300)
|
|
# reaches into this event's own Quest-list character-art panel on the
|
|
# left, which false-positive-matched SWEEP_CONFIRM_CYAN on the plain list
|
|
# with NO sweep-result dialog showing at all -- this is why
|
|
# _watch_sweep_result kept "finding" a result button and never reached its
|
|
# "modal closed, no result button" end condition after a real sweep
|
|
# finished, even though the sweep itself succeeded (AP spent, credits
|
|
# gained, confirmed by screenshot). Narrower x-range here (1000-1300)
|
|
# still comfortably covers the real buttons (SWEEP_CONFIRM_BUTTON's own
|
|
# x=1150 sits well inside it) while excluding the character-art panel,
|
|
# confirmed against the same saved false-positive screenshot returning
|
|
# no match afterward.
|
|
EVENT_SWEEP_RESULT_BUTTON_REGION = (1000, 700, 1300, 1050)
|
|
|
|
# Rotation target: which of stages 9-12 to sweep today, per explicit user
|
|
# request (2026-07-10) -- "the current event has up to 12 stages, randomly
|
|
# choose stage 9-12, same mod%4 date method as story sweep." Same
|
|
# date-ordinal-modulo scheme as STORY_SWEEP_ROTATION_*, just over this
|
|
# sub-range instead of 1..N. Set EVENT_SWEEP_ROTATION_STAGE_MIN to None to
|
|
# disable.
|
|
#
|
|
# NOT yet live-confirmed: what the stage-info modal looks like for a stage
|
|
# that has never been cleared to SSS -- the reference's own activity_sweep
|
|
# gates the sweep panel on check_sweep_availability()=="sss" and falls back
|
|
# to a manual fight otherwise. Every stage 9-12 on this account was already
|
|
# 3-starred during calibration, so that gate was never actually exercised.
|
|
# event_sweep.py handles this the same defensive way story_sweep handles an
|
|
# unavailable region/stage: if the MAX-button click can't be verified to
|
|
# have raised the sweep count, it aborts that target without spending AP
|
|
# rather than guessing.
|
|
EVENT_SWEEP_ROTATION_STAGE_MIN = 9
|
|
EVENT_SWEEP_ROTATION_STAGE_MAX = 12
|
|
EVENT_SWEEP_ROTATION_COUNT = "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),
|
|
]
|
|
|
|
# Lesson/Schedule (module/lesson.py). This client renders the reference's
|
|
# paged single-region view as a scrollable "Location Select" list of 12
|
|
# named regions instead -- these names are the reference's own
|
|
# lesson_region_name.JP list (core/config/default_config.py), embedded there
|
|
# directly rather than fetched externally, so hardcoding it locally isn't an
|
|
# external-data problem the way the shop price table was. They're used only
|
|
# for logging here, not for OCR identification -- unlike the reference's
|
|
# paged arrows, this list's scroll position is deterministic (see
|
|
# REGION_ROW_Y below), so there's nothing to OCR-locate.
|
|
LESSON_ICON = (314, 1100) # bottom nav "スケジュール" icon on the home screen
|
|
LESSON_BACK_BUTTON = (85, 55) # shared by both the region-list and per-region map screens
|
|
|
|
# Clicking LESSON_ICON does not always land on the Location Select list --
|
|
# confirmed live: the game remembers the last-viewed region and reopens
|
|
# directly to its per-region isometric map instead (e.g. after a previous run
|
|
# was interrupted mid-region). lesson.py's _ensure_location_select_list
|
|
# detects and recovers from this via LESSON_ALL_SCHEDULES_BUTTON below
|
|
# already being visible before any row has been clicked.
|
|
|
|
LESSON_REGION_NAMES = [
|
|
"シャーレオフィス", "シャーレ居住区", "ゲヘナ学園・中央区", "アビドス高等学校",
|
|
"ミレニアム・スタディーエリア", "トリニティ・スクエア", "レッドウインター連邦学園",
|
|
"百鬼夜行中心部", "D.U.シラトリ区", "山海経中央特区", "春葉原", "ワイルドハント総合芸術地区",
|
|
]
|
|
# The region list only ever settles at two scroll positions -- scrolled fully
|
|
# to top (regions 0-5 visible) or fully to bottom (regions 6-11) -- confirmed
|
|
# live: 15 scroll-down clicks always lands on the same bottom state, it
|
|
# doesn't keep scrolling past it. Each row's card is clickable at this
|
|
# center-ish Y regardless of which of the two scroll states is showing.
|
|
LESSON_REGION_ROW_Y = [265, 420, 585, 745, 900, 1060]
|
|
LESSON_REGION_LIST_SCROLL_POINT = (1400, 700)
|
|
LESSON_REGION_LIST_SCROLL_CLICKS = 15
|
|
LESSON_REGION_ROW_X = 1400
|
|
|
|
# Per-region isometric map screen -- opens the "全てのスケジュール" grid
|
|
# modal (this client's rendering of the reference's per-region 3x3
|
|
# get_lesson_each_region_status/get_lesson_relationship_counts grid).
|
|
LESSON_ALL_SCHEDULES_BUTTON = (1770, 1118)
|
|
LESSON_GRID_MODAL_CLOSE_BUTTON = (1710, 211)
|
|
|
|
# Grid modal cell layout: up to 3 columns x 3 rows of location cards, each
|
|
# showing up to 3 student portraits with a heart-shaped affection-count
|
|
# badge at bottom-right. Pixel-scanned live against two different regions'
|
|
# modals (Gehenna Central: 8 cells, Schale Office: 7 cells) -- consistent
|
|
# across both. A region with more than 9 currently-unlocked locations would
|
|
# need scrolling inside this modal, which isn't implemented (not yet seen
|
|
# live on this account; see plan.md).
|
|
LESSON_GRID_COL_X = [270, 786, 1302] # portrait-slot-0 center per column
|
|
LESSON_GRID_ROW_HEADER_Y = [380, 608, 836] # click target to open a cell's info panel
|
|
LESSON_GRID_ROW_PORTRAIT_Y = [486, 713, 940]
|
|
LESSON_GRID_PORTRAIT_STEP_X = 109 # slot 1/2 center = slot 0 center + N * this
|
|
# Badge center sits below-right of each portrait-slot's own center point.
|
|
LESSON_GRID_BADGE_OFFSET = (38, 24)
|
|
LESSON_GRID_BADGE_OCR_HALF_SIZE = (26, 20) # crop half-width/half-height around badge center
|
|
# A green checkmark can appear at top-right of a portrait ALONGSIDE its
|
|
# unchanged heart badge number, not instead of it (confirmed live -- an
|
|
# earlier assumption that "done" always blanks the number was wrong) --
|
|
# this is the only reliable "already done today" signal, so it's checked
|
|
# separately rather than inferred from the badge OCR.
|
|
LESSON_GRID_CHECKMARK_OFFSET = (41, -31)
|
|
LESSON_GRID_CHECKMARK_RGB = ((130, 190, 60), (220, 255, 160))
|
|
LESSON_GRID_CHECKMARK_HALF_SIZE = (14, 13)
|
|
# A masked, digit-only OCR read of the heart badge can still occasionally
|
|
# fuse a stray leading digit from portrait art bleeding into the crop's left
|
|
# edge (confirmed live: "13" -> "413", "18" -> "418", reproducible across
|
|
# every psm mode -- see detector.read_int_on_heart_badge). Real affection
|
|
# values never reach this range in practice, so treat anything this large
|
|
# as contamination and discard it rather than trust it.
|
|
LESSON_GRID_BADGE_MAX_PLAUSIBLE = 99
|
|
|
|
# "保有チケット N/M" readout, top-left of the region-list/map screens.
|
|
# The original rect (295, 133, 385, 172) clipped in a stray fragment of the
|
|
# "チケット" label's own trailing katakana glyph a few pixels left of the
|
|
# first digit -- confirmed live: it read back as "/7" (whole leading digit
|
|
# dropped) instead of "7/7", reproducible across a fresh screenshot every
|
|
# time, not a one-off render glitch. Tightening the left edge past that
|
|
# fragment (and the right edge in to match) fixed it; re-confirmed stable
|
|
# across 6 consecutive fresh reads. See Handoff.md for the live debugging
|
|
# session that found this.
|
|
LESSON_TICKET_OCR_RECT = (315, 135, 375, 170)
|
|
|
|
# Per-cell info panel ("スケジュール情報"), opened by clicking a grid cell.
|
|
# Its Start button and the post-schedule report's OK button (below) are the
|
|
# same bright-cyan gradient button at nearly the same position -- confirmed
|
|
# by direct pixel sample, sharing one color range.
|
|
LESSON_ACTION_BUTTON_RGB = ((90, 195, 235), (150, 240, 255))
|
|
LESSON_INFO_START_BUTTON = (960, 890)
|
|
LESSON_INFO_CLOSE_BUTTON = (1435, 240)
|
|
|
|
# After clicking Start, a variable sequence of intermediate screens can
|
|
# appear before settling back on the grid modal -- a bond-rank-up full-
|
|
# screen cutscene (confirmed live, see screenshots/lesson/ -- its art is
|
|
# character-dependent, so no fixed color/position reliably identifies it)
|
|
# and/or a "スケジュールレポート" results modal. A transient "run the
|
|
# schedule twice" campaign multiplier was also observed live, doubling how
|
|
# many of these screens appear in a row -- rather than special-case any of
|
|
# this, `lesson.py` presses Enter in a bounded loop and re-checks two fixed
|
|
# markers each round:
|
|
#
|
|
# - the grid modal's own title underline (this exact yellow-gold, confirmed
|
|
# live) is visible ONLY when the grid modal is frontmost and idle -- both
|
|
# the report modal and the cutscene cover it, confirmed live against all
|
|
# three states.
|
|
LESSON_GRID_IDLE_PROBE_RECT = (790, 244, 1130, 252)
|
|
LESSON_GRID_IDLE_RGB = ((235, 220, 70), (255, 250, 130))
|
|
# - the report modal's OK button -- same color as LESSON_ACTION_BUTTON_RGB,
|
|
# its own confirmed fixed position -- clicked directly rather than folded
|
|
# into the blind Enter-press fallback, since we can verify it precisely.
|
|
LESSON_REPORT_OK_BUTTON = (960, 895)
|
|
LESSON_POST_SCHEDULE_MAX_ENTER_PRESSES = 8
|
|
|
|
# Arena / Tactical Challenge (module/arena.py). Fights a real ranked PvP
|
|
# battle each run -- opt-in only, never in DEFAULT_ORDER (see
|
|
# ba_auto/reference_notes/mapping.md's Arena row). Per explicit user decision
|
|
# (2026-07-09): v1 fights exactly one battle per invocation, matching the
|
|
# reference's own per-call pacing -- it relies on a persistent background
|
|
# thread rescheduling itself 55 minutes later for the next ticket, which this
|
|
# project's one-shot-per-invocation CLI has no equivalent for.
|
|
|
|
# Policy knobs, carried over unchanged from the reference's own
|
|
# core/config/default_config.py defaults. These have no live-UI dependency,
|
|
# so they're safe to set now rather than waiting for calibration.
|
|
ARENA_COMPONENT_NUMBER = 1 # which of the 3 visible opponent slots to challenge (1-3)
|
|
ARENA_LEVEL_DIFF = 0 # accept an opponent up to this many levels above self (negative = only below)
|
|
ARENA_MAX_REFRESH_TIMES = 10 # give up rerolling for an acceptable opponent after this many refreshes
|
|
ARENA_STOP_FIGHT_WHEN_RANK1 = False # if True and current rank OCRs as 1, skip fighting and just collect rewards
|
|
|
|
# Per explicit user direction (2026-07-12): arena.py now loops fighting
|
|
# battles until the OCR'd ticket count reaches 0, rather than exactly one
|
|
# battle per invocation (see arena.py's module docstring for the full
|
|
# history of that earlier design and why it changed). ARENA_MAX_FIGHTS_
|
|
# PER_RUN is a defensive upper bound only -- not a hardcoded assumption of
|
|
# the account's real daily ticket count (5, per the user, but read live via
|
|
# OCR every run like everything else in this project) -- guarding against a
|
|
# runaway loop if ticket-count OCR ever misreads persistently, matching the
|
|
# project's established bounded-retry convention (OPEN_RETRIES,
|
|
# RETURN_HOME_MAX_ROUNDS, etc.). ARENA_POST_BATTLE_COOLDOWN is the real
|
|
# in-game lockout after a battle finishes before the next one can be
|
|
# queued, per the user's explicit info -- not yet independently
|
|
# live-confirmed against the exact UI symptom (a disabled button vs. a
|
|
# genuinely unresponsive click) since the user described it directly rather
|
|
# than this being discovered through live probing.
|
|
ARENA_MAX_FIGHTS_PER_RUN = 10
|
|
ARENA_POST_BATTLE_COOLDOWN = 30
|
|
|
|
# Live-calibrated against nik-gpu on 2026-07-09 (see scratchpad/arena_calib_*
|
|
# for the captured screenshots this was pixel-scanned/OCR-tested against).
|
|
# This client does NOT expose Tactical Challenge as a bottom-nav icon like
|
|
# the reference's main-page nav -- it's a card inside the お仕事 (Work) hub,
|
|
# reached the same way story_sweep's 任務 card is (config.WORK_ICON).
|
|
ARENA_WORK_HUB_CARD = (1310, 985) # 戦術対抗戦 card inside the WORK_ICON hub
|
|
|
|
# "保有チケット N/M" readout, left info panel. OCR-confirmed live (read as
|
|
# "5/5" against a real 5/5 balance) -- reference's own get_tickets splits on
|
|
# "/" and keeps the first number, ported the same way in arena.py.
|
|
ARENA_TICKET_OCR_RECT = (295, 780, 395, 812)
|
|
# "N位" rank readout, left info panel, tight-cropped to exclude both the
|
|
# rank-icon graphic (its own art was misread as spurious digits when
|
|
# included, e.g. "29" -> "207") and the trailing "位" glyph. OCR-confirmed
|
|
# live (read 29 against a real rank-29 display). Only needed if
|
|
# ARENA_STOP_FIGHT_WHEN_RANK1 is True.
|
|
ARENA_RANK_OCR_RECT = (208, 500, 285, 555)
|
|
|
|
# Time reward (時間報酬, continuous income meter -- reclaimable repeatedly
|
|
# as it re-accrues, confirmed live: button went claimable again within
|
|
# seconds of a claim once 90/1,000K had re-accumulated) and daily reward
|
|
# (デイリー報酬, genuinely once/day -- shows a countdown timer once claimed).
|
|
# Click points sit on the button's own label text; probes are offset left
|
|
# onto plain background fill, confirmed clean (no text) across every row of
|
|
# a live pixel grid-scan in both claimed and claimable states.
|
|
ARENA_TIME_REWARD_BUTTON = (525, 641)
|
|
ARENA_TIME_REWARD_PROBE = (455, 640)
|
|
ARENA_DAILY_REWARD_BUTTON = (525, 758)
|
|
ARENA_DAILY_REWARD_PROBE = (455, 757)
|
|
# Claimable = vivid gold fill; claimed/not-yet = flat neutral grey -- same
|
|
# two-state pattern as the reference's own JP.json rgb_in_range check for
|
|
# this exact feature, just recalibrated to this client's resolution/colors.
|
|
# Pixel-sampled live across both reward slots and both states.
|
|
ARENA_REWARD_CLAIMABLE_RGB = ((235, 205, 45), (255, 240, 95))
|
|
ARENA_REWARD_CLAIMED_RGB = ((200, 200, 200), (225, 225, 225))
|
|
|
|
# Season opponent list (3 rows). Row 1's click point is live-confirmed (it
|
|
# opened the opponent-info modal below); rows 2/3 are extrapolated from the
|
|
# same ~255px row spacing visible in scratchpad/arena_calib_05_after_close.png
|
|
# but not yet individually click-confirmed.
|
|
ARENA_OPPONENT_ROW_X = 900
|
|
ARENA_OPPONENT_ROW_Y = [400, 655, 910]
|
|
# "リスト更新" -- refreshes all 3 shown opponents at once (matches the
|
|
# reference's single refresh click for its 3 fixed slots, not a per-slot
|
|
# reroll). Not yet click-confirmed.
|
|
ARENA_REFRESH_LIST_BUTTON = (1750, 280)
|
|
|
|
# Opponent-info modal ("対戦相手"). This client merges the reference's two
|
|
# separate screens (opponent-info, then a distinct formation-edit/
|
|
# "攻撃編成" screen) into ONE modal that shows both the matchup and the
|
|
# attack-formation button together.
|
|
ARENA_MODAL_CLOSE_BUTTON = (1518, 207) # live-confirmed: closes with no ticket cost
|
|
# navigation.is_modal_open's shared (960, 200) darkness probe does NOT work
|
|
# here -- confirmed live it reads INVERTED for this specific screen: the
|
|
# arena list's own background art at that point is already dark
|
|
# (91, 113, 165), while the opponent-info modal's white card is bright
|
|
# (247, 250, 252) there. Same probe point, opposite rule -- use r > 200 to
|
|
# mean "modal open", not r < 150.
|
|
ARENA_MODAL_PROBE = (960, 200)
|
|
ARENA_MODAL_OPEN_MIN_CHANNEL = 200
|
|
# "攻撃編成" (Attack Formation) button -- this is the actual fight-commit
|
|
# click; the modal shows a live ticket-count preview next to it (e.g.
|
|
# "5→4") confirming it's what spends the ticket. Same gold fill family as
|
|
# the reward buttons. Click-confirmed live 2026-07-09 (ticket went 5 -> 4,
|
|
# landed on the attack-formation/squad screen below).
|
|
ARENA_ATTACK_FORMATION_BUTTON = (958, 918)
|
|
|
|
# Live-tested for real on 2026-07-09 (one real ticket spent, one real ranked
|
|
# fight, WIN, rank 29 -> 21): see scratchpad/arena_fight_* for the captured
|
|
# screenshots. Confirmed:
|
|
#
|
|
# - ARENA_ATTACK_FORMATION_BUTTON's click DOES work as the fight-commit step
|
|
# (ticket went 5 -> 4) and lands on a "攻撃編成" squad-formation screen,
|
|
# matching the reference's edit-force screen.
|
|
# - "戦闘スキップ" (Battle Skip) was ALREADY ON by default on this account
|
|
# (cyan checkmark confirmed via pixel-grid-scan) -- check_skip_button's
|
|
# reroll-if-off logic still needs porting for accounts/states where it
|
|
# isn't, but no live-confirmed "off" sample exists yet to calibrate that
|
|
# state's color against.
|
|
# - The actual fight button is "出撃" (Sortie), bottom-right of the
|
|
# attack-formation screen, also bound to Enter (confirmed via keypress).
|
|
ARENA_SORTIE_CONFIRM_KEY = "Return" # attack-formation screen's 出撃 button responds to Enter
|
|
|
|
# IMPORTANT, live-discovered timing gotcha: after Sortie, the battle
|
|
# auto-resolves (skip is on) and the game queues a "対戦結果" (Battle
|
|
# Result) WIN/LOSE modal with a reward -- but it can take longer than a
|
|
# couple seconds to actually render. A screenshot taken ~2s after the Sortie
|
|
# keypress showed the arena list ALREADY updated (rank, ticket count,
|
|
# wait-time cooldown) with NO result modal visible at all -- looking like
|
|
# the reference's separate battle-win/battle-lost detection was unnecessary
|
|
# here. That was wrong: the WIN modal was still pending and only rendered
|
|
# on the NEXT click, confirmed by clicking an opponent row afterward and
|
|
# getting the queued WIN screen instead. Do not assume "list looks normal
|
|
# again" means the result was already handled.
|
|
#
|
|
# Two more real bugs were found live trying to precisely detect this
|
|
# modal's own confirm button, both on 2026-07-09, using two of the
|
|
# session's three real arena tickets:
|
|
#
|
|
# 1. WIN and LOSE modals are NOT the same height -- WIN shows a reward
|
|
# showcase (credit points) above its confirm button, LOSE doesn't, so
|
|
# LOSE's confirm button sits noticeably higher on screen (~y 710-810 vs
|
|
# WIN's ~y 815-870). A fixed probe point calibrated only against WIN
|
|
# missed LOSE entirely, which also made reward collection silently read
|
|
# "not claimable" right after -- it was actually checking the reward
|
|
# buttons while the still-undetected LOSE modal was covering them, not a
|
|
# real reward-state problem.
|
|
# 2. Widening the search into a region spanning both button positions (the
|
|
# same fix story_sweep.py's own two-position SKIP/OK button used) fixed
|
|
# that, but then a live rerun found the region also caught stray
|
|
# cyan-ish pixels from the opponent list's own portrait art -- a false
|
|
# centroid match there was clicked and opened a completely unrelated
|
|
# opponent's "対戦相手" info modal instead of confirming anything. A
|
|
# narrower region (offline-validated against every saved WIN/LOSE/
|
|
# opponent-info/plain-list screenshot before risking the session's last
|
|
# ticket on it) still detected the *same* stuck state on the very next
|
|
# live run -- the false-positive source is fundamentally unpredictable
|
|
# per-refresh portrait art, not something a fixed region can rule out.
|
|
#
|
|
# Given the opponent-info modal's OWN attack-formation button responds to
|
|
# Enter and spends a real ticket, the close call here is real: this project
|
|
# already has a documented precedent (see CLAUDE.md's story_sweep writeup)
|
|
# for exactly "a mistimed keypress could have started a real battle."
|
|
#
|
|
# Fixed properly by abandoning per-button color detection entirely.
|
|
# ba_auto/tasks/arena.py's _wait_for_result instead presses Enter --
|
|
# confirmed to be the universal safe dismiss for both modals and for an
|
|
# unrelated "list refresh expired" notice that can also appear -- in a
|
|
# bounded blind-retry loop, the same pattern lesson.py's _run_one_schedule
|
|
# already uses for its own "variable sequence of post-action screens"
|
|
# problem. The one genuine hazard (the opponent-info modal's Enter-bound
|
|
# attack-formation button) is guarded by a HARD gate checked before every
|
|
# single press: if that specific modal is ever detected (via its own gold
|
|
# button at ARENA_ATTACK_FORMATION_BUTTON, a fixed, reliable, non-color-
|
|
# region check), the function stops immediately without pressing Enter,
|
|
# rather than risk spending a second ticket.
|
|
ARENA_RESULT_CONFIRM_KEY = "Return" # universal safe dismiss for both WIN/LOSE and the list-refresh-expired notice
|
|
|
|
# Self/opponent level OCR, for choose_enemy's reroll logic -- read directly
|
|
# from the list screen (matching the reference's own self_level_region/
|
|
# opponent_level_region, both read there before any modal opens), not from
|
|
# inside the opponent-info modal. Live-calibrated 2026-07-09: the naive
|
|
# tight crop (just the "90" glyphs) OCR'd as None even with a correctly
|
|
# time-matched screenshot -- turned out to be a real crop-size problem, not
|
|
# a stale-screenshot one this time. Debugged by dumping the exact
|
|
# thresholded image tesseract sees (scratchpad/probe_arena_level_ocr*.py):
|
|
# the tight crop was legible to the eye but too marginal for tesseract at
|
|
# 3x upscale, succeeding on some rows/psm modes and not others. A few extra
|
|
# pixels of padding on each side fixed it outright across every row, still
|
|
# using the existing 3x OCR_UPSCALE pipeline -- no pipeline change needed,
|
|
# just a less tight crop.
|
|
ARENA_SELF_LEVEL_OCR_RECT = (288, 342, 335, 383) # white-on-dark -- use read_int_white_on_dark
|
|
ARENA_OPPONENT_LEVEL_OCR_RECTS = [ # dark-on-light -- use plain read_int
|
|
(735, 498, 782, 533),
|
|
(735, 736, 782, 771),
|
|
(735, 973, 782, 1008),
|
|
]
|
|
|
|
# Battle-skip toggle on the attack-formation screen (see
|
|
# ARENA_ATTACK_FORMATION_BUTTON above -- this is the screen it lands on).
|
|
# Pixel-grid-scanned live against a real ON state (cyan checkmark); no OFF
|
|
# sample has been seen yet since it was already on by default this session,
|
|
# so ARENA_SKIP_ON_RGB is only confirmed to correctly detect "on", not yet
|
|
# confirmed to correctly reject a real "off" state.
|
|
ARENA_SKIP_TOGGLE_PROBE = (1670, 1020)
|
|
ARENA_SKIP_TOGGLE_CLICK = (1670, 1020)
|
|
ARENA_SKIP_ON_RGB = ((60, 205, 235), (140, 255, 255))
|
|
|
|
# Still entirely unknown / unexercised:
|
|
# - the LOSE variant of the result modal
|
|
# - ARENA_REFRESH_LIST_BUTTON has not actually been click-confirmed -- this
|
|
# session's one live fight never needed a reroll (the chosen opponent was
|
|
# already an acceptable level), so choose_enemy's refresh path is
|
|
# implemented per the reference but not yet exercised live
|
|
# - template images (see detector.find_template) for best-record/
|
|
# season-record or any other screen this client might show between
|
|
# confirming attack formation and the fight resolving, if one ever
|
|
# appears (none did across this one live fight)
|
|
|
|
# Bounty (指名手配, module/rewarded_task.py). Live-calibrated against
|
|
# nik-gpu 2026-07-11, zero real tickets spent -- every confirm dialog reached
|
|
# was cancelled via Escape, ticket count (6/6) confirmed unchanged
|
|
# before/after across all 3 areas. See ba_auto/tasks/bounty.py's module
|
|
# docstring for the full design writeup (per explicit user direction: choose
|
|
# one of 3 areas via the same date-ordinal-modulo rotation story_sweep.py/
|
|
# event_sweep.py already use, always sweep that area's latest/highest stage).
|
|
|
|
# Work-hub card entry point (WORK_ICON, defined above -> this card). Lands
|
|
# directly on the Location Select screen -- no separate bus-icon/
|
|
# sub-navigation step needed on this client, unlike the reference's
|
|
# bottom-nav "main_page_bus" icon.
|
|
BOUNTY_CARD = (1113, 645)
|
|
|
|
# Location Select's 3 fixed area rows -- all 3 fit on screen with no
|
|
# scrolling needed. Index 0/1/2 = ハイウェイ/砂漠の線路/校舎, this client's
|
|
# rendering of the reference's OVERPASS/DESSERT RAILWAY/CLASSROOM (matched by
|
|
# each area's own recommended-school grouping, e.g. ハイウェイ's
|
|
# ゲヘナ/山海経/ヴァルキューレ/ハイランダー lining up with the reference's
|
|
# bounty_name[0] = "OVERPASS" grouping).
|
|
BOUNTY_AREA_ROW_Y = (300, 475, 650)
|
|
BOUNTY_AREA_ROW_X = 1400
|
|
BOUNTY_AREA_NAMES = ("ハイウェイ", "砂漠の線路", "校舎")
|
|
|
|
# Stage list, forced to its bottom scroll extreme every run (not just
|
|
# trusted to already be there -- see module docstring). Confirmed live
|
|
# across all 3 areas: exactly 10 stages per area (numbered 01-10, lettered
|
|
# A-J), row 10 is the true list end (scrolling further is a no-op), and
|
|
# every stage was already 3-starred/SSS-cleared. This makes the bottom-most
|
|
# row always literally "the latest stage available" by construction -- no
|
|
# OCR search needed to locate a specific target the way story_sweep/
|
|
# event_sweep need (their target lists have more entries than fit on screen
|
|
# at once; this one doesn't).
|
|
BOUNTY_STAGE_LIST_SCROLL_POINT = (1400, 700)
|
|
BOUNTY_STAGE_LIST_SCROLL_CLICKS = 25
|
|
BOUNTY_LATEST_STAGE_ROW_Y = 1086
|
|
BOUNTY_STAGE_ENTER_X = 1685
|
|
|
|
# Stage-number OCR crop for the bottom-most row (e.g. "10"), diagnostic/log
|
|
# only -- selection is always "bottom-most row," not a text match, so this
|
|
# isn't on the decision path. Offset from BOUNTY_LATEST_STAGE_ROW_Y.
|
|
BOUNTY_STAGE_NUMBER_OCR_X = (1040, 1120)
|
|
BOUNTY_STAGE_NUMBER_OCR_Y_PAD = (35, 35)
|
|
|
|
# 任務情報 (task info) modal. MAX button / minus-stepper-probe / start-sweep
|
|
# button positions and colors confirmed live pixel-identical to
|
|
# EVENT_SWEEP_MAX_BUTTON/EVENT_SWEEP_MINUS_BUTTON_PROBE/
|
|
# EVENT_SWEEP_START_BUTTON (including the exact same raised-vs-default
|
|
# minus-button colors, (171,172,171) vs (251,173,152)) -- kept as separate
|
|
# BOUNTY_-prefixed constants anyway to avoid coupling the two tasks'
|
|
# config together, matching this file's own established pattern (see
|
|
# EVENT_STAGE_MODAL_PROBE's comment for the earlier instance of the same
|
|
# reasoning).
|
|
BOUNTY_SWEEP_MIN_BUTTON = (1178, 511)
|
|
BOUNTY_SWEEP_MAX_BUTTON = (1631, 511)
|
|
BOUNTY_SWEEP_PLUS_BUTTON = (1517, 511)
|
|
BOUNTY_SWEEP_MINUS_BUTTON_PROBE = (1281, 511)
|
|
BOUNTY_SWEEP_START_BUTTON = (1400, 668)
|
|
|
|
# Modal-open probe. NOT reused from EVENT_STAGE_MODAL_PROBE: that corner
|
|
# point (1870, 600) sits on this screen's own dark-blue background
|
|
# REGARDLESS of whether the modal is open (confirmed live: both states read
|
|
# dark there), since this screen's underlying art differs from event_sweep's
|
|
# -- it can't tell the two states apart here. (1850, 1150) does: confirmed
|
|
# live dark (<150 all channels) with the modal open, bright
|
|
# (160,203,225-ish) with it closed, both directions checked.
|
|
BOUNTY_STAGE_MODAL_PROBE = (1850, 1150)
|
|
BOUNTY_STAGE_MODAL_DIM_MAX_CHANNEL = 150
|
|
# Modal close. Confirmed live this modal DOES close on Escape (like
|
|
# event_sweep's, unlike story_sweep's) -- the X button position is kept as a
|
|
# fallback, matching event_sweep.py's own close pattern.
|
|
BOUNTY_STAGE_MODAL_CLOSE_BUTTON = (1691, 271)
|
|
|
|
# The ticket-usage confirm dialog ("指名手配チケットをN使用して、掃討をN回
|
|
# 行いますか?") is the exact same shared "通知" dialog component as the
|
|
# AP-usage-confirm dialog -- confirmed live pixel-identical position and
|
|
# color to SWEEP_CONFIRM_BUTTON/SWEEP_CONFIRM_CANCEL_BUTTON/
|
|
# SWEEP_CONFIRM_CYAN (defined above under story_sweep), reused directly.
|
|
# SWEEP_CONFIRM_GOLD's use as this same dialog's "can't proceed" variant is
|
|
# now live-confirmed too, just via a different trigger than expected: not
|
|
# insufficient tickets at the 掃討開始 click (never observed -- this account
|
|
# always had enough tickets for the count requested), but a real Pyroxene
|
|
# "指名手配チケット購入" (buy more tickets) prompt that appeared automatically
|
|
# after a sweep dropped the ticket count to exactly 0 (2026-07-11 live test).
|
|
# Same button position/color, confirmed live -- no Pyroxene was actually
|
|
# spent (gem balance unchanged before/after), and _watch_sweep_result now
|
|
# has its own explicit guard against this state -- see its comment.
|
|
|
|
# Post-sweep result screen ("掃討完了"). Live-confirmed 2026-07-11 (count=1,
|
|
# the only value possible with the 1 ticket available for this test): a
|
|
# SINGLE "OK" button dialog (no separate SKIP step -- unlike story_sweep/
|
|
# event_sweep's SKIP-then-OK sequence, though that may be specific to a
|
|
# single-round sweep; a bulk/MAX sweep's result flow is NOT yet confirmed to
|
|
# match). Pixel-scanned real OK-button bbox: x 783-1139, y 879-991.
|
|
#
|
|
# The FIRST live test of this (also 2026-07-11) used a naive copy of
|
|
# EVENT_SWEEP_RESULT_BUTTON_REGION (1000-1300 x, 700-1050 y) and hit a real,
|
|
# confirmed-live false positive: that region's y-range overlapped
|
|
# BOUNTY_SWEEP_START_BUTTON's own real cyan pixels (掃討開始 spans y
|
|
# 622-715), so once the real result dialog was dismissed and the plain
|
|
# stage-info modal reappeared (with 0 tickets left), _find_result_button
|
|
# re-matched 掃討開始's corner and clicked it -- which is what triggered the
|
|
# real ticket-purchase prompt described above. Narrowed here (matching
|
|
# EVENT_SWEEP_RESULT_BUTTON_REGION's own history of being fixed the same
|
|
# way after its own live false-positive) to a tight box around the
|
|
# confirmed real OK-button bbox, comfortably clear of 掃討開始's y-range.
|
|
BOUNTY_SWEEP_RESULT_BUTTON_REGION = (750, 850, 1160, 1010)
|
|
|
|
# A SECOND, smaller contamination source was also found live within the
|
|
# corrected region above: the modal's own "獲得期待報酬" reward-icon artwork
|
|
# (visible whenever the plain stage-info modal is showing, sitting at the
|
|
# same y-position as the result dialog's OK button) has a handful of pixels
|
|
# that incidentally fall inside SWEEP_CONFIRM_CYAN's broad range too --
|
|
# confirmed live at ~878 stray pixels vs the real OK button's ~34,000.
|
|
# find_color_centroid's min_pixels parameter (see detector.py) filters this
|
|
# out; set well below the real button's count and well above the confirmed
|
|
# noise floor, with wide margin on both sides for icon-art variance across
|
|
# different stages/areas this project hasn't captured yet.
|
|
BOUNTY_RESULT_BUTTON_MIN_PIXELS = 3000
|
|
|
|
# Which area to sweep today, per explicit user request (2026-07-11): "3
|
|
# areas, choose random (mod%3), run the latest stage available." Same
|
|
# date-ordinal-modulo scheme as STORY_SWEEP_ROTATION_*/EVENT_SWEEP_ROTATION_*.
|
|
BOUNTY_SWEEP_COUNT = "max"
|