"""Anime Vanguards - run a stage, place your units, take the reward, again.""" from __future__ import annotations import time from .. import config, routine, screen, winput from ..runner import Runner NAME = "animevanguards" DEFAULTS = { "hotkeys": {"pause": "f7", "stop": "f8", "capture": "f6"}, "routines": { # Recorded with `record`. "build": "build", "loop": None, "loop_every": 6.0, "speed": 1.0, }, "match": { "start_delay": 4.0, # after pressing start, before the build order "max_seconds": 900.0, # give up on a match that never ends "check_every": 0.75, # how often to look at the results probe "load_seconds": 12.0, # after replay, before the next match begins "between": 1.0, }, "points": { # Each is {"at": [x, y], "color": [r, g, b]} or null. "close": None, # X on a popup, clicked before each match "start": None, # the button that starts / replays a match "end_probe": None, # a pixel that is only this colour on results "replay": None, # results-screen replay button "confirm": None, # a second click, if replay asks to confirm }, "tolerance": 26, # per-channel colour slack on the probe "antiafk": {"enabled": True, "period": 240.0, "key": "space"}, } POINTS = [ ("close", "Popup close button (the X). Skip if nothing pops up."), ("start", "The button that STARTS a match from the lobby or results screen."), ("end_probe", "A pixel that is only this colour when the RESULTS screen is up " "— a coloured header or button on the panel, not grey background."), ("replay", "The REPLAY / PLAY AGAIN button on the results screen."), ("confirm", "A confirmation click after replay, if there is one. Skip if not."), ] # ------------------------------------------------------------- calibrate def calibrate(cfg: dict) -> dict: keys = cfg["hotkeys"] print("\ncalibrating Anime Vanguards") print("Put Roblox where you can see each thing before you capture it —") print("the results-screen points are easiest to capture at the end of a run.") for key, prompt in POINTS: current = cfg["points"].get(key) if current: print(f"\n{key}: currently {current['at']} colour {current['color']}") captured = routine.capture_point(f"{key} — {prompt}", keys) if captured: cfg["points"][key] = captured return cfg # ------------------------------------------------------------------ bits def _click_point(point: dict | None, runner: Runner, label: str) -> bool: if not point or not point.get("at"): return False x, y = point["at"] runner.log(f"click {label} at {x},{y}") winput.click(x, y) return True def _probe_matches(point: dict | None, tolerance: int) -> bool: if not point or not point.get("at") or not point.get("color"): return False x, y = point["at"] return screen.close_enough(screen.pixel(x, y), point["color"], tolerance) class AnimeVanguards: def __init__(self, cfg: dict, runner: Runner): self.cfg = cfg self.runner = runner self.matches = 0 self.started = time.monotonic() names = cfg["routines"] self.build = self._routine(names.get("build"), runner) self.loop = self._routine(names.get("loop"), runner) self.speed = float(names.get("speed", 1.0) or 1.0) self._afk_at = time.monotonic() runner.stats.update(matches=0, started=self.started) @staticmethod def _routine(name, runner: Runner) -> list[dict]: if not name: return [] try: return config.load_routine(name) except FileNotFoundError: runner.log(f"no routine called {name!r} — carrying on without it " f"(record one with: python macro.py record {name})") return [] # ------------------------------------------------------------ pieces def antiafk(self) -> None: settings = self.cfg["antiafk"] if not settings.get("enabled"): return if time.monotonic() - self._afk_at < float(settings.get("period", 240)): return self._afk_at = time.monotonic() # A jump, not a mouse jiggle: Roblox's idle timer watches for input the game itself. winput.tap(settings.get("key", "space")) def wait_for_end(self) -> str: """Wait out the match, upgrading if there is a loop routine.""" match = self.cfg["match"] probe = self.cfg["points"].get("end_probe") tolerance = int(self.cfg.get("tolerance", 26)) deadline = time.monotonic() + float(match.get("max_seconds", 900)) next_loop = time.monotonic() if not probe: self.runner.log("no end_probe calibrated — waiting on the timer instead") while time.monotonic() < deadline: self.runner.wait(float(match.get("check_every", 0.75))) self.antiafk() if _probe_matches(probe, tolerance): # Twice, a beat apart: a matching pixel can be a coincidence of an effect passing over. self.runner.wait(0.5) if _probe_matches(probe, tolerance): return "results" if self.loop and time.monotonic() >= next_loop: next_loop = time.monotonic() + float(self.cfg["routines"].get("loop_every", 6)) routine.play(self.runner, self.loop, self.speed) return "timeout" # -------------------------------------------------------------- match def one_match(self) -> None: cfg = self.cfg points = cfg["points"] match = cfg["match"] if _click_point(points.get("close"), self.runner, "close"): self.runner.wait(0.6) if _click_point(points.get("start"), self.runner, "start"): self.runner.wait(float(match.get("start_delay", 4))) else: self.runner.log("no start point calibrated — assuming the match is already running") if self.build: self.runner.log(f"placing — {len(self.build)} recorded steps") routine.play(self.runner, self.build, self.speed) else: self.runner.log("no build routine recorded — nothing to place") how = self.wait_for_end() self.matches += 1 self.runner.stats["matches"] = self.matches minutes = (time.monotonic() - self.started) / 60 self.runner.log(f"match {self.matches} ended ({how}) — {minutes:.1f} min elapsed") if _click_point(points.get("replay"), self.runner, "replay"): self.runner.wait(1.0) if _click_point(points.get("confirm"), self.runner, "confirm"): self.runner.wait(1.0) self.runner.wait(float(match.get("load_seconds", 12))) self.runner.wait(float(match.get("between", 1))) def run(once: bool = False, runner: Runner | None = None) -> None: """Farm until stopped.""" cfg = config.load(NAME, DEFAULTS) # Written back on every run, so the file always exists to edit and gains any setting. config.save(NAME, cfg) runner = runner or Runner("Anime Vanguards", cfg["hotkeys"]) macro = AnimeVanguards(cfg, runner) if once: runner.log("single match — press pause to begin") def tick(): macro.one_match() runner.stopped = True else: def tick(): macro.one_match() runner.loop(tick) runner.log(f"{macro.matches} matches this session.")