Nik Afiq e814b3d803 feat(cafe): implement student invitation and horizontal camera panning
- Added functionality to invite students into the cafe before farming, prioritizing the highest-affection candidates while skipping those that would cause costume swaps or move students from other rooms. This follows user direction from 2026-07-14.
- Introduced a new drag method for horizontal camera panning to reveal hidden content in the cafe, addressing user feedback regarding screen size limitations. The camera pans to both extremes before farming, ensuring all students are visible.
- Updated the cafe task logic to incorporate the new invitation and panning features, ensuring a seamless user experience with real-time confirmations and checks.
- Live-tested and confirmed the new features with real game-state changes, ensuring functionality aligns with user requirements and expectations.
2026-07-14 22:28:44 +09:00

110 lines
3.8 KiB
Python

"""Local PC/Steam/Proton control backend (xdotool/scrot wrappers)."""
import os
import subprocess
import time
import cv2
from ba_auto import config
PROBE_SHOT_PATH = os.path.join(config.SCRATCHPAD_DIR, "probe.png")
def run_command(args, **kwargs):
kwargs.setdefault("env", config.ENV)
kwargs.setdefault("check", True)
return subprocess.run(args, **kwargs)
def focus_game():
result = run_command(
["xdotool", "search", "--name", config.WINDOW_NAME],
check=False, capture_output=True, text=True,
)
window_ids = result.stdout.split()
if not window_ids:
raise RuntimeError("Blue Archive window not found. Is the game running?")
run_command(["xdotool", "windowactivate", window_ids[0]])
# windowactivate alone isn't enough -- confirmed live (twice now, see
# Handoff.md's original finding and plan.md's Bounty phase for a second
# occurrence) that a stray anti-cheat XIGNCODE window can render
# visibly on top of the game and intercept clicks at fixed positions
# (notably the shared BACK_BUTTON coordinate) even while xdotool still
# reports BlueArchive as the "active" window -- windowactivate changes
# input focus, not stacking order, so it doesn't fix this by itself.
# windowraise does. Cheap and harmless when no overlay is present.
run_command(["xdotool", "windowraise", window_ids[0]])
wait(0.5)
def click(x, y):
# A combined "mousemove X Y click 1" invocation is unreliable here --
# empirically (see plan.md's click-flakiness writeups) the game's input
# handler sometimes misses clicks fired before it's processed the move.
# Splitting into separate commands with a short pause between fixes it.
run_command(["xdotool", "mousemove", str(x), str(y)])
wait(0.2)
run_command(["xdotool", "click", "1"])
wait(0.5)
def move_mouse(x, y):
run_command(["xdotool", "mousemove", str(x), str(y)])
def drag(start_x, start_y, end_x, end_y, duration=0.6, steps=12):
"""Click-and-drag gesture: mousedown at (start_x, start_y), move to
(end_x, end_y) over `duration` seconds in `steps` increments, mouseup.
Distinct from scroll()'s wheel-based gesture -- scroll()'s own comment
already documents that a plain drag does NOT register as a list-scroll
gesture in this Proton client, but that finding was specific to
scrollable list widgets (mailbox/lesson/bounty/etc.); a room-view
camera (e.g. cafe.py's horizontal pan) is a different UI surface, not a
list, and needs an actual drag rather than a wheel click.
"""
run_command(["xdotool", "mousemove", str(start_x), str(start_y)])
wait(0.2)
run_command(["xdotool", "mousedown", "1"])
step_delay = duration / steps
for i in range(1, steps + 1):
x = start_x + (end_x - start_x) * i // steps
y = start_y + (end_y - start_y) * i // steps
run_command(["xdotool", "mousemove", str(x), str(y)])
wait(step_delay)
run_command(["xdotool", "mouseup", "1"])
wait(0.3)
def scroll(x, y, direction, clicks=1):
# xdotool button 4/5 = scroll wheel up/down. A drag (mousedown/move/mouseup)
# does not register as a list-scroll gesture in this Proton client; the
# wheel does.
button = "4" if direction == "up" else "5"
run_command(["xdotool", "mousemove", str(x), str(y)])
wait(0.2)
for _ in range(clicks):
run_command(["xdotool", "click", button])
wait(0.15)
wait(0.3)
def keypress(key):
run_command(["xdotool", "key", key])
wait(0.5)
def screenshot(path):
run_command(["scrot", "-a", "0,0,1920,1200", "-o", path])
def color_at(x, y):
screenshot(PROBE_SHOT_PATH)
image = cv2.imread(PROBE_SHOT_PATH)
b, g, r = image[y, x]
return int(r), int(g), int(b)
def wait(seconds):
time.sleep(seconds)