mirror of
https://github.com/TheFunny/ArisuAutoSweeper
synced 2026-06-09 20:04:52 +00:00
feat: mission/commissions/event
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
from module.base.button import Button, ButtonWrapper
|
||||
|
||||
# This file was auto-generated, do not modify it manually. To generate:
|
||||
# ``` python -m dev_tools.button_extract ```
|
||||
|
||||
CHECK_BD = ButtonWrapper(
|
||||
name='CHECK_BD',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/CHECK_BD.png',
|
||||
area=(94, 135, 325, 194),
|
||||
search=(74, 115, 345, 214),
|
||||
color=(208, 215, 220),
|
||||
button=(94, 135, 325, 194),
|
||||
),
|
||||
)
|
||||
CHECK_COMMISSIONS = ButtonWrapper(
|
||||
name='CHECK_COMMISSIONS',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/CHECK_COMMISSIONS.png',
|
||||
area=(646, 78, 909, 135),
|
||||
search=(626, 58, 929, 155),
|
||||
color=(70, 96, 124),
|
||||
button=(646, 78, 909, 135),
|
||||
),
|
||||
)
|
||||
CHECK_IR = ButtonWrapper(
|
||||
name='CHECK_IR',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/CHECK_IR.png',
|
||||
area=(97, 137, 340, 191),
|
||||
search=(77, 117, 360, 211),
|
||||
color=(213, 220, 223),
|
||||
button=(97, 137, 340, 191),
|
||||
),
|
||||
)
|
||||
CHECK_MISSION_SWEEP = ButtonWrapper(
|
||||
name='CHECK_MISSION_SWEEP',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/CHECK_MISSION_SWEEP.png',
|
||||
area=(654, 184, 703, 209),
|
||||
search=(634, 164, 723, 229),
|
||||
color=(208, 213, 219),
|
||||
button=(654, 184, 703, 209),
|
||||
),
|
||||
)
|
||||
HARD_OFF = ButtonWrapper(
|
||||
name='HARD_OFF',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/HARD_OFF.png',
|
||||
area=(947, 132, 1193, 182),
|
||||
search=(927, 112, 1213, 202),
|
||||
color=(242, 246, 248),
|
||||
button=(947, 132, 1193, 182),
|
||||
),
|
||||
)
|
||||
HARD_ON = ButtonWrapper(
|
||||
name='HARD_ON',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/HARD_ON.png',
|
||||
area=(940, 133, 1189, 186),
|
||||
search=(920, 113, 1209, 206),
|
||||
color=(200, 71, 63),
|
||||
button=(940, 133, 1189, 186),
|
||||
),
|
||||
)
|
||||
LEFT = ButtonWrapper(
|
||||
name='LEFT',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/LEFT.png',
|
||||
area=(0, 301, 89, 408),
|
||||
search=(0, 281, 109, 428),
|
||||
color=(193, 224, 241),
|
||||
button=(0, 301, 89, 408),
|
||||
),
|
||||
)
|
||||
NORMAL_OFF = ButtonWrapper(
|
||||
name='NORMAL_OFF',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/NORMAL_OFF.png',
|
||||
area=(682, 135, 927, 182),
|
||||
search=(662, 115, 947, 202),
|
||||
color=(238, 243, 246),
|
||||
button=(682, 135, 927, 182),
|
||||
),
|
||||
)
|
||||
NORMAL_ON = ButtonWrapper(
|
||||
name='NORMAL_ON',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/NORMAL_ON.png',
|
||||
area=(682, 137, 924, 185),
|
||||
search=(662, 117, 944, 205),
|
||||
color=(62, 81, 89),
|
||||
button=(682, 137, 924, 185),
|
||||
),
|
||||
)
|
||||
OCR_AREA = ButtonWrapper(
|
||||
name='OCR_AREA',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/OCR_AREA.png',
|
||||
area=(108, 176, 176, 219),
|
||||
search=(88, 156, 196, 239),
|
||||
color=(237, 238, 240),
|
||||
button=(108, 176, 176, 219),
|
||||
),
|
||||
)
|
||||
RIGHT = ButtonWrapper(
|
||||
name='RIGHT',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/RIGHT.png',
|
||||
area=(1202, 311, 1280, 412),
|
||||
search=(1182, 291, 1280, 432),
|
||||
color=(193, 223, 241),
|
||||
button=(1202, 311, 1280, 412),
|
||||
),
|
||||
)
|
||||
SELECT_BD = ButtonWrapper(
|
||||
name='SELECT_BD',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/SELECT_BD.png',
|
||||
area=(1016, 165, 1227, 211),
|
||||
search=(996, 145, 1247, 231),
|
||||
color=(205, 212, 220),
|
||||
button=(1016, 165, 1227, 211),
|
||||
),
|
||||
)
|
||||
SELECT_IR = ButtonWrapper(
|
||||
name='SELECT_IR',
|
||||
jp=None,
|
||||
en=Button(
|
||||
file='./assets/en/mission/SELECT_IR.png',
|
||||
area=(1004, 267, 1237, 321),
|
||||
search=(984, 247, 1257, 341),
|
||||
color=(214, 220, 227),
|
||||
button=(1004, 267, 1237, 321),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,260 @@
|
||||
from enum import Enum
|
||||
|
||||
from module.base.timer import Timer
|
||||
from module.exception import RequestHumanTakeover
|
||||
from module.logger import logger
|
||||
from tasks.mission.ui import MissionUI, CommissionsUI
|
||||
from tasks.stage.ap import AP
|
||||
from tasks.cafe.cafe import Cafe
|
||||
from tasks.circle.circle import Circle
|
||||
from tasks.task.task import Task
|
||||
from tasks.mail.mail import Mail
|
||||
from tasks.item.data_update import DataUpdate
|
||||
import json
|
||||
import math
|
||||
from filelock import FileLock
|
||||
from datetime import datetime
|
||||
|
||||
class MissionStatus(Enum):
|
||||
AP = 0 # Calculate AP and decide to terminate Mission module or not
|
||||
NAVIGATE = 1 # Navigate to the stage page for example the commissions page or mission page
|
||||
SELECT = 2 # Select the stage mode for example hard or normal in mission
|
||||
ENTER = 3 # Search and for the stage in the stage list and enter
|
||||
SWEEP = 4 # Sweep the stage
|
||||
RECHARGE = 5 # Recharge AP from other taks if they are enabled
|
||||
FINISH = -1 # Inidicate termination of Mission module
|
||||
|
||||
|
||||
class Mission(MissionUI, CommissionsUI):
|
||||
_stage_ap = [10, 15, 15, 15]
|
||||
|
||||
@property
|
||||
def stage_ap(self):
|
||||
return self._stage_ap
|
||||
|
||||
@property
|
||||
def mission_info(self) -> list:
|
||||
"""
|
||||
Read the config from MCE/config.json and extract the queue, a list of list.
|
||||
If queue is empty repopulate from preferred template.
|
||||
|
||||
Format of each element in queue: [mode, stage, sweep_num]
|
||||
e.g. ["N", "1-1", 3]
|
||||
|
||||
Mode Acronyms:
|
||||
"N" : Normal Mission
|
||||
"H" : Hard Mission
|
||||
"E" : Event Quest
|
||||
"IR" : Item Retrieval / Commission where you get credit
|
||||
"BD" : Base Defense / Commission where you get exp
|
||||
|
||||
Returns:
|
||||
list of list
|
||||
"""
|
||||
queue = []
|
||||
try:
|
||||
with open("MCE/config.json") as json_file:
|
||||
config_data = json.load(json_file)
|
||||
queue = config_data["Queue"]
|
||||
self.recharge_AP = config_data["RechargeAP"]
|
||||
self.reset_daily = config_data["ResetDaily"]
|
||||
self.reset_time = config_data["ResetTime"]
|
||||
self.last_run = config_data["LastRun"]
|
||||
self.event = config_data["Event"]
|
||||
if self.check_reset_daily() or not queue:
|
||||
preferred_template = config_data["PreferredTemplate"]
|
||||
queue = config_data["Templates"][preferred_template]
|
||||
if not self.event:
|
||||
queue = [x for x in queue if x[0] != "E"]
|
||||
except:
|
||||
logger.error("Failed to read configuration file")
|
||||
finally:
|
||||
return queue
|
||||
|
||||
def check_reset_daily(self):
|
||||
# Check if it's time to reset the queue
|
||||
if self.reset_daily:
|
||||
current_datetime = datetime.now().replace(microsecond=0) # Round to the nearest second
|
||||
current_date = current_datetime.date()
|
||||
current_time = current_datetime.time()
|
||||
last_run_datetime = datetime.strptime(self.last_run, "%Y-%m-%d %H:%M:%S")
|
||||
reset_time = datetime.strptime(self.reset_time, "%H:%M:%S").time()
|
||||
|
||||
if current_date != last_run_datetime.date() and current_time >= reset_time:
|
||||
self.last_run = str(datetime.now().replace(microsecond=0))
|
||||
logger.info("Reset Daily activated.")
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def valid_task(self) -> list:
|
||||
task = self.mission_info
|
||||
if not task:
|
||||
logger.warning('Mission enabled but no task set')
|
||||
#self.error_handler()
|
||||
return task
|
||||
|
||||
@property
|
||||
def current_mode(self):
|
||||
return self.task[0][0]
|
||||
|
||||
@property
|
||||
def current_stage(self):
|
||||
return self.task[0][1]
|
||||
|
||||
@property
|
||||
def current_count(self):
|
||||
if self.current_mode == "H" and self.task[0][2] > 3:
|
||||
return 3
|
||||
return self.task[0][2]
|
||||
|
||||
@current_count.setter
|
||||
def current_count(self, value):
|
||||
self.task[0][2] = value
|
||||
|
||||
def select(self) -> bool:
|
||||
"""
|
||||
A wrapper method to select the current_mode
|
||||
by calling the specific method based on its type.
|
||||
|
||||
Return
|
||||
True if selection happens without any problem, False otherwise.
|
||||
"""
|
||||
if self.current_mode in ["N", "H"]:
|
||||
return self.select_mission(self.current_mode, self.current_stage)
|
||||
elif self.current_mode in ["BD", "IR"]:
|
||||
return self.select_commission(self.current_mode)
|
||||
elif self.current_mode == "E":
|
||||
#return self.select_mode(SWITCH_QUEST)
|
||||
logger.error("Event not yet implemented")
|
||||
return False
|
||||
else:
|
||||
logger.error("Uknown mode")
|
||||
return False
|
||||
|
||||
def get_realistic_count(self) -> int:
|
||||
"""
|
||||
Calculate the possible number of sweeps based on the current AP
|
||||
"""
|
||||
ap_cost = 20 if self.current_mode == "H" else 10
|
||||
required_ap = ap_cost * self.current_count
|
||||
return math.floor(min(required_ap, self.current_ap) / ap_cost)
|
||||
|
||||
def update_task(self, failure=False):
|
||||
"""
|
||||
Update self.task and save the current state of the queue in
|
||||
MCE/config.json.
|
||||
"""
|
||||
try:
|
||||
if failure or self.current_count == self.realistic_count:
|
||||
self.previous_mode = self.current_mode
|
||||
self.task.pop(0)
|
||||
else:
|
||||
self.previous_mode = None
|
||||
self.current_count -= self.realistic_count
|
||||
with open("MCE/config.json", "r") as json_file:
|
||||
config_data = json.load(json_file)
|
||||
with open("MCE/config.json", "w") as json_file:
|
||||
config_data["Queue"] = self.task
|
||||
config_data["LastRun"] = self.last_run
|
||||
json.dump(config_data, json_file, indent=2)
|
||||
except:
|
||||
logger.error("Failed to save configuration")
|
||||
self.task = []
|
||||
|
||||
def update_ap(self):
|
||||
ap_cost = 20 if self.current_mode == "H" else 10
|
||||
ap = self.config.stored.AP
|
||||
ap_old = ap.value
|
||||
ap_new = ap_old - ap_cost * self.realistic_count
|
||||
ap.set(ap_new, ap.total)
|
||||
logger.info(f'Set AP: {ap_old} -> {ap_new}')
|
||||
|
||||
def recharge(self) -> bool:
|
||||
"""
|
||||
Check if AP related modules such as cafe, circle, task, mail are enabled and run them if they are.
|
||||
task_call only works after the current task is finished so is not suitable.
|
||||
"""
|
||||
cafe_reward = self.config.cross_get(["Cafe", "Scheduler", "Enable"]) and self.config.cross_get(["Cafe", "Cafe", "Reward"])
|
||||
circle = self.config.cross_get(["Circle", "Scheduler", "Enable"])
|
||||
task = self.config.cross_get(["Task", "Scheduler", "Enable"])
|
||||
mail = self.config.cross_get(["Mail", "Scheduler", "Enable"])
|
||||
ap_tasks = [(cafe_reward,Cafe), (circle, Circle), (task, Task), (mail, Mail)]
|
||||
modules = [x[1] for x in ap_tasks if x[0]]
|
||||
if not modules:
|
||||
logger.info("Recharge AP was enabled but no AP related modules were enabled")
|
||||
return False
|
||||
for module in modules:
|
||||
module(config=self.config, device=self.device).run()
|
||||
return True
|
||||
|
||||
def handle_mission(self, status):
|
||||
match status:
|
||||
case MissionStatus.AP:
|
||||
if not self.task:
|
||||
return MissionStatus.FINISH
|
||||
self.realistic_count = self.get_realistic_count()
|
||||
if self.realistic_count == 0 and self.recharge_AP:
|
||||
self.recharge_AP = False
|
||||
return MissionStatus.RECHARGE
|
||||
elif self.realistic_count == 0 and not self.recharge_AP:
|
||||
return MissionStatus.FINISH
|
||||
else:
|
||||
return MissionStatus.NAVIGATE
|
||||
case MissionStatus.NAVIGATE:
|
||||
self.navigate(self.previous_mode, self.current_mode)
|
||||
return MissionStatus.SELECT
|
||||
case MissionStatus.SELECT:
|
||||
if self.select():
|
||||
return MissionStatus.ENTER
|
||||
self.update_task(failure=True)
|
||||
return MissionStatus.AP
|
||||
case MissionStatus.ENTER:
|
||||
if self.enter_stage(self.current_stage):
|
||||
return MissionStatus.SWEEP
|
||||
self.update_task(failure=True)
|
||||
return MissionStatus.AP
|
||||
case MissionStatus.SWEEP:
|
||||
if self.do_sweep(self.current_mode, self.realistic_count):
|
||||
self.update_ap()
|
||||
self.update_task()
|
||||
else:
|
||||
self.update_task(failure=True)
|
||||
return MissionStatus.AP
|
||||
case MissionStatus.RECHARGE:
|
||||
if self.recharge():
|
||||
DataUpdate(config=self.config, device=self.device).run()
|
||||
return MissionStatus.AP
|
||||
return MissionStatus.FINISH
|
||||
case MissionStatus.FINISH:
|
||||
return status
|
||||
case _:
|
||||
logger.warning(f'Invalid status: {status}')
|
||||
return status
|
||||
|
||||
def run(self):
|
||||
self.lock = FileLock("MCE/config.json.lock")
|
||||
with self.lock.acquire():
|
||||
self.previous_mode = None
|
||||
self.task = self.valid_task
|
||||
action_timer = Timer(0.5, 1)
|
||||
status = MissionStatus.AP
|
||||
|
||||
"""Update the dashboard to accurately calculate AP"""
|
||||
DataUpdate(config=self.config, device=self.device).run()
|
||||
|
||||
while 1:
|
||||
self.device.screenshot()
|
||||
|
||||
if self.ui_additional():
|
||||
continue
|
||||
|
||||
if action_timer.reached_and_reset():
|
||||
logger.attr('Status', status)
|
||||
status = self.handle_mission(status)
|
||||
|
||||
if status == MissionStatus.FINISH:
|
||||
break
|
||||
|
||||
self.config.task_delay(server_update=True)
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
from module.base.timer import Timer
|
||||
from module.logger import logger
|
||||
from module.ui.switch import Switch
|
||||
from module.ocr.ocr import Digit
|
||||
from tasks.base.assets.assets_base_page import BACK, MISSION_CHECK
|
||||
from tasks.base.page import page_mission, page_commissions
|
||||
from tasks.base.ui import UI
|
||||
from tasks.mission.assets.assets_mission import *
|
||||
from tasks.stage.ap import AP
|
||||
from tasks.stage.mission_list import StageList
|
||||
from tasks.stage.sweep import StageSweep
|
||||
|
||||
|
||||
SHARED_LIST = StageList('SharedList')
|
||||
MISSION_SWEEP = StageSweep('MissionSweep', 60)
|
||||
MISSION_SWEEP.set_button(button_check=CHECK_MISSION_SWEEP) # Check sweep is different for mission
|
||||
SHARED_SWEEP = StageSweep('SharedSweep', 60)
|
||||
|
||||
SWITCH_NORMAL = Switch("Normal_switch")
|
||||
SWITCH_NORMAL.add_state("on", NORMAL_ON)
|
||||
SWITCH_NORMAL.add_state("off", NORMAL_OFF)
|
||||
|
||||
SWITCH_HARD = Switch("HARD_switch")
|
||||
SWITCH_HARD.add_state("on", HARD_ON)
|
||||
SWITCH_HARD.add_state("off", HARD_OFF)
|
||||
|
||||
SWITCH_QUEST = None
|
||||
|
||||
"""
|
||||
A dictionary that maps the mode to a tuple where
|
||||
the first element is an argument to go_back and second is for ui_ensure
|
||||
Missing for "E" because there are no event in Global and no page_event
|
||||
"""
|
||||
MODE_TO_PAGE = {
|
||||
"N": (MISSION_CHECK, page_mission),
|
||||
"H": (MISSION_CHECK, page_mission),
|
||||
"BD": (CHECK_BD, page_commissions),
|
||||
"IR": (CHECK_IR, page_commissions),
|
||||
"E" : ()
|
||||
}
|
||||
|
||||
|
||||
class MissionUI(UI, AP):
|
||||
def select_mission(self, mode, stage):
|
||||
area = int(stage.split("-")[0])
|
||||
if not self.select_area(area):
|
||||
logger.warning("Area not found")
|
||||
return False
|
||||
|
||||
to_switch = {
|
||||
"N": SWITCH_NORMAL,
|
||||
"H": SWITCH_HARD
|
||||
}
|
||||
switch = to_switch[mode]
|
||||
if not self.select_mode(switch) and not self.select_area(area):
|
||||
return False
|
||||
return True
|
||||
|
||||
def select_area(self, num):
|
||||
""""
|
||||
May require further error handling for these cases.
|
||||
1. Fails to ocr area number
|
||||
2. May trigger too many click exception when clicking left or right too many times
|
||||
3. Area not unlocked. Simplest way if left or right button are still present
|
||||
but problem is it is expensive to check every time and they always keep moving.
|
||||
"""
|
||||
tries = 0
|
||||
ocr_area = Digit(OCR_AREA)
|
||||
while 1:
|
||||
try:
|
||||
self.device.screenshot()
|
||||
current_area = int(ocr_area.ocr_single_line(self.device.image))
|
||||
if current_area == num:
|
||||
return True
|
||||
elif current_area > num:
|
||||
[self.click_with_interval(LEFT, interval=1) for x in range(abs(current_area-num))]
|
||||
elif current_area < num:
|
||||
[self.click_with_interval(RIGHT, interval=1) for x in range(abs(current_area-num))]
|
||||
except:
|
||||
tries += 1
|
||||
if tries > 5:
|
||||
return False
|
||||
|
||||
def select_mode(self, switch):
|
||||
"""
|
||||
Set switch to on.
|
||||
Returns:
|
||||
True if switch is set, False if switch not found
|
||||
"""
|
||||
if not switch.appear(main=self):
|
||||
logger.info(f'{switch.name} not found')
|
||||
return False
|
||||
switch.set('on', main=self)
|
||||
return True
|
||||
|
||||
def enter_stage(self, index: int) -> bool:
|
||||
if not index:
|
||||
index = SHARED_LIST.insight_max_sweepable_index(self)
|
||||
if SHARED_LIST.select_index_enter(self, index):
|
||||
return True
|
||||
return False
|
||||
|
||||
def do_sweep(self, mode, num: int) -> bool:
|
||||
if mode in ["N", "H"]:
|
||||
return MISSION_SWEEP.do_sweep(self, num=num)
|
||||
else:
|
||||
return SHARED_SWEEP.do_sweep(self, num=num)
|
||||
|
||||
def navigate(self, prev, next):
|
||||
"""
|
||||
go_back is called when the previous stage and next stage are in
|
||||
the same game mode.
|
||||
For example, "N" and "H" are in Mission so we call go_back.
|
||||
If different, ui_ensure is called for example, "N" and "IR".
|
||||
"""
|
||||
if prev==next or (prev in ["N", "H"] and next in ["N", "H"]):
|
||||
self.go_back(MODE_TO_PAGE[next][0])
|
||||
elif prev in ["BD", "IR"] and next in ["BD", "IR"]:
|
||||
self.go_back(CHECK_COMMISSIONS)
|
||||
else:
|
||||
self.ui_ensure(MODE_TO_PAGE[next][1])
|
||||
|
||||
def go_back(self, check):
|
||||
while 1:
|
||||
self.device.screenshot()
|
||||
if self.match_color(check) and self.appear(check):
|
||||
return True
|
||||
self.click_with_interval(BACK, interval=2)
|
||||
|
||||
class CommissionsUI(UI, AP):
|
||||
"""Works the same way as select_bounty"""
|
||||
def select_commission(self, mode):
|
||||
to_button = {
|
||||
"IR": (SELECT_IR, CHECK_IR),
|
||||
"BD": (SELECT_BD, CHECK_BD)
|
||||
}
|
||||
dest_enter, dest_check = to_button[mode]
|
||||
timer = Timer(5, 10).start()
|
||||
while 1:
|
||||
self.device.screenshot()
|
||||
self.appear_then_click(dest_enter, interval=1)
|
||||
if self.appear(dest_check):
|
||||
return True
|
||||
if timer.reached():
|
||||
return False
|
||||
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
|
||||
from module.base.base import ModuleBase
|
||||
from module.base.timer import Timer
|
||||
from module.base.utils import area_pad, area_size, area_offset
|
||||
from module.logger import logger
|
||||
from module.ocr.ocr import Ocr
|
||||
from tasks.stage.assets.assets_stage_list import *
|
||||
|
||||
|
||||
class StageList:
|
||||
swipe_vector_range = (0.65, 0.70)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
button_list: ButtonWrapper = None,
|
||||
button_index: ButtonWrapper = None,
|
||||
button_item: ButtonWrapper = None,
|
||||
button_enter: ButtonWrapper = None,
|
||||
button_stars: ButtonWrapper = None,
|
||||
swipe_direction: str = "down"
|
||||
):
|
||||
self.name = name
|
||||
self.stage = button_list if button_list else STAGE_LIST
|
||||
self.index_ocr = Ocr(button_index if button_index else OCR_INDEX, lang='en')
|
||||
self.stage_item = (button_item if button_item else STAGE_ITEM).button
|
||||
self.enter = button_enter if button_enter else STAGE_ENTER
|
||||
self.sweepable = button_stars if button_stars else STAGE_STARS
|
||||
self.swipe_direction = swipe_direction
|
||||
|
||||
self.current_index_min = 1
|
||||
self.current_index_max = 1
|
||||
self.current_indexes: list[tuple[str, tuple]] = []
|
||||
|
||||
def __str__(self):
|
||||
return f'StageList({self.name})'
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __eq__(self, other):
|
||||
return str(self) == str(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
@property
|
||||
def _indexes(self) -> list[int]:
|
||||
return [x[0] for x in self.current_indexes]
|
||||
|
||||
def load_stage_indexes(self, main: ModuleBase):
|
||||
self.current_indexes = list(
|
||||
filter(
|
||||
lambda x: re.match(r'^\d{1,2}-?\d?$', x[0]) and x[0] != '00',
|
||||
map(lambda x: (x.ocr_text, x.box), self.index_ocr.detect_and_ocr(main.device.image))
|
||||
)
|
||||
)
|
||||
if not self.current_indexes:
|
||||
logger.warning(f'No valid index in {self.index_ocr.name}')
|
||||
return
|
||||
indexes = self._indexes
|
||||
|
||||
self.current_index_min = min(indexes)
|
||||
self.current_index_max = max(indexes)
|
||||
logger.attr(self.index_ocr.name, f'Index range: {self.current_index_min} - {self.current_index_max}')
|
||||
|
||||
def swipe_page(self, direction: str, main: ModuleBase, vector_range=None, reverse=False):
|
||||
"""
|
||||
Args:
|
||||
direction: up, down
|
||||
main:
|
||||
vector_range (tuple[float, float]):
|
||||
reverse (bool):
|
||||
"""
|
||||
if vector_range is None:
|
||||
vector_range = self.swipe_vector_range
|
||||
vector = np.random.uniform(*vector_range)
|
||||
width, height = area_size(self.stage.button)
|
||||
if direction == 'up':
|
||||
vector = (0, vector * height)
|
||||
elif direction == 'down':
|
||||
vector = (0, -vector * height)
|
||||
else:
|
||||
logger.warning(f'Unknown swipe direction: {direction}')
|
||||
return
|
||||
|
||||
if reverse:
|
||||
vector = (-vector[0], -vector[1])
|
||||
main.device.swipe_vector(vector, self.stage.button, name=f'{self.name}_SWIPE')
|
||||
|
||||
def insight_index(self, index: int, main: ModuleBase, skip_first_screenshot=True) -> bool:
|
||||
"""
|
||||
Args:
|
||||
index:
|
||||
main:
|
||||
skip_first_screenshot:
|
||||
|
||||
Returns:
|
||||
If success
|
||||
"""
|
||||
logger.info(f'Insight index: {index}')
|
||||
last_indexes: set[int] = set()
|
||||
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
main.device.screenshot()
|
||||
|
||||
self.load_stage_indexes(main=main)
|
||||
|
||||
if self.current_index_min <= index <= self.current_index_max:
|
||||
return True
|
||||
|
||||
indexes = self._indexes
|
||||
if indexes and last_indexes == set(indexes):
|
||||
logger.warning(f'No more index {index}')
|
||||
return False
|
||||
last_indexes = set(indexes)
|
||||
|
||||
if index < self.current_index_min:
|
||||
self.swipe_page(self.swipe_direction, main, reverse=True)
|
||||
elif index > self.current_index_max:
|
||||
self.swipe_page(self.swipe_direction, main)
|
||||
|
||||
main.wait_until_stable(
|
||||
self.stage.button,
|
||||
timer=Timer(0, 0),
|
||||
timeout=Timer(1.5, 5)
|
||||
)
|
||||
|
||||
def insight_max_sweepable_index(self, main: ModuleBase, skip_first_screenshot=True) -> int:
|
||||
"""
|
||||
Args:
|
||||
main:
|
||||
skip_first_screenshot:
|
||||
|
||||
Returns:
|
||||
Index of max sweepable stage
|
||||
"""
|
||||
logger.info('Insight sweepable index')
|
||||
max_sweepable_index = 0
|
||||
last_max_sweepable_index = 0
|
||||
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
main.device.screenshot()
|
||||
|
||||
self.load_stage_indexes(main=main)
|
||||
|
||||
sweepable_index = next(
|
||||
filter(
|
||||
lambda x: not self.is_sweepable(main, self.search_box(x[-1][:2])),
|
||||
self.current_indexes
|
||||
), None
|
||||
)
|
||||
|
||||
# all sweepable
|
||||
if sweepable_index is None:
|
||||
logger.info('All sweepable')
|
||||
max_sweepable_index = self.current_index_max
|
||||
self.swipe_page(self.swipe_direction, main)
|
||||
if max_sweepable_index == last_max_sweepable_index:
|
||||
logger.info(f'Max sweepable index: {max_sweepable_index}')
|
||||
return max_sweepable_index
|
||||
last_max_sweepable_index = max_sweepable_index
|
||||
# all not sweepable
|
||||
elif int(sweepable_index[0]) == self.current_index_min:
|
||||
logger.info('All not sweepable')
|
||||
if int(sweepable_index[0]) == 1:
|
||||
logger.warning('No sweepable index')
|
||||
return 0
|
||||
self.swipe_page(self.swipe_direction, main, reverse=True)
|
||||
else:
|
||||
logger.info(f'Sweepable index: {int(sweepable_index[0]) - 1}')
|
||||
return int(sweepable_index[0]) - 1
|
||||
|
||||
main.wait_until_stable(
|
||||
self.stage.button,
|
||||
timer=Timer(0, 0),
|
||||
timeout=Timer(1.5, 5)
|
||||
)
|
||||
|
||||
def is_sweepable(self, main: ModuleBase, search_box) -> bool:
|
||||
self.sweepable.load_search(search_box)
|
||||
return main.appear(self.sweepable, similarity=0.8)
|
||||
|
||||
def search_box(
|
||||
self,
|
||||
index_cord: tuple[int, int],
|
||||
padding: tuple[int, int] = (-20, -15)
|
||||
) -> tuple[int, int, int, int]:
|
||||
stage_item_box = area_pad((*padding, *area_size(self.stage_item)))
|
||||
return area_offset(stage_item_box, index_cord)
|
||||
|
||||
def select_index_enter(
|
||||
self,
|
||||
main: ModuleBase,
|
||||
index: int,
|
||||
insight: bool = True,
|
||||
sweepable: bool = True,
|
||||
padding: tuple[int, int] = (-20, -15),
|
||||
skip_first_screenshot: bool = True,
|
||||
interval: int = 1.5
|
||||
) -> bool:
|
||||
# insight index, if failed, return False
|
||||
if insight and not self.insight_index(index, main, skip_first_screenshot):
|
||||
return False
|
||||
logger.info(f'Select index: {index}')
|
||||
click_interval = Timer(interval)
|
||||
load_index_interval = Timer(1)
|
||||
timeout = Timer(15, 10).start()
|
||||
while 1:
|
||||
if skip_first_screenshot:
|
||||
skip_first_screenshot = False
|
||||
else:
|
||||
main.device.screenshot()
|
||||
|
||||
# load index if not insight
|
||||
if load_index_interval.reached_and_reset():
|
||||
self.load_stage_indexes(main=main)
|
||||
|
||||
# find box of index
|
||||
index_box = next(filter(lambda x: x[0] == index, self.current_indexes), None)
|
||||
|
||||
if index_box is None:
|
||||
logger.warning(f'No index {index} in {self.index_ocr.name}')
|
||||
continue
|
||||
|
||||
search_box = self.search_box(index_box[-1][:2], padding)
|
||||
if sweepable and not self.is_sweepable(main, search_box):
|
||||
logger.warning(f'Index {index} is not sweepable')
|
||||
return False
|
||||
|
||||
self.enter.load_search(search_box)
|
||||
click_button = self.enter.match_multi_template(main.device.image)
|
||||
|
||||
if not click_button:
|
||||
logger.warning(f'No clickable {self.enter.name}')
|
||||
continue
|
||||
|
||||
if click_interval.reached_and_reset():
|
||||
main.device.click(click_button[0])
|
||||
return True
|
||||
|
||||
if timeout.reached():
|
||||
logger.warning(f'{self.enter.name} failed')
|
||||
return False
|
||||
@@ -5,7 +5,13 @@ from module.base.button import Button, ButtonWrapper
|
||||
|
||||
CLAIM = ButtonWrapper(
|
||||
name='CLAIM',
|
||||
jp=None,
|
||||
jp=Button(
|
||||
file='./assets/jp/task/CLAIM.png',
|
||||
area=(936, 641, 1010, 696),
|
||||
search=(916, 621, 1030, 716),
|
||||
color=(230, 210, 63),
|
||||
button=(936, 641, 1010, 696),
|
||||
),
|
||||
en=Button(
|
||||
file='./assets/en/task/CLAIM.png',
|
||||
area=(935, 639, 1015, 698),
|
||||
@@ -16,7 +22,13 @@ CLAIM = ButtonWrapper(
|
||||
)
|
||||
CLAIMED = ButtonWrapper(
|
||||
name='CLAIMED',
|
||||
jp=None,
|
||||
jp=Button(
|
||||
file='./assets/jp/task/CLAIMED.png',
|
||||
area=(935, 641, 1010, 696),
|
||||
search=(915, 621, 1030, 716),
|
||||
color=(211, 211, 210),
|
||||
button=(935, 641, 1010, 696),
|
||||
),
|
||||
en=Button(
|
||||
file='./assets/en/task/CLAIMED.png',
|
||||
area=(937, 641, 1010, 696),
|
||||
@@ -27,7 +39,13 @@ CLAIMED = ButtonWrapper(
|
||||
)
|
||||
CLAIMED_ALL = ButtonWrapper(
|
||||
name='CLAIMED_ALL',
|
||||
jp=None,
|
||||
jp=Button(
|
||||
file='./assets/jp/task/CLAIMED_ALL.png',
|
||||
area=(1057, 640, 1242, 701),
|
||||
search=(1037, 620, 1262, 720),
|
||||
color=(192, 193, 196),
|
||||
button=(1057, 640, 1242, 701),
|
||||
),
|
||||
en=Button(
|
||||
file='./assets/en/task/CLAIMED_ALL.png',
|
||||
area=(1058, 641, 1240, 701),
|
||||
@@ -38,7 +56,13 @@ CLAIMED_ALL = ButtonWrapper(
|
||||
)
|
||||
CLAIM_ALL = ButtonWrapper(
|
||||
name='CLAIM_ALL',
|
||||
jp=None,
|
||||
jp=Button(
|
||||
file='./assets/jp/task/CLAIM_ALL.png',
|
||||
area=(1058, 641, 1242, 701),
|
||||
search=(1038, 621, 1262, 720),
|
||||
color=(235, 218, 67),
|
||||
button=(1058, 641, 1242, 701),
|
||||
),
|
||||
en=Button(
|
||||
file='./assets/en/task/CLAIM_ALL.png',
|
||||
area=(1054, 642, 1243, 700),
|
||||
|
||||
Reference in New Issue
Block a user