1
0
mirror of https://github.com/TheFunny/ArisuAutoSweeper synced 2025-12-16 19:55:12 +00:00

Compare commits

...

6 Commits

22 changed files with 636 additions and 22 deletions

View File

@ -15,6 +15,7 @@ The script is still under active development. The following features have been i
- [x] **Cafe** Claim rewards / Interact / Second floor
- [x] **Club** Claim AP
- [x] **Mailbox** Claim rewards
- [x] **Bounty** Auto sweep
- [x] **Tactical Challenge** Claim rewards / Auto battle
Supported servers:

View File

@ -15,6 +15,7 @@
- [x] **咖啡厅** 领取奖励 / 互动 / 第二咖啡厅
- [x] **公会** 领取体力
- [x] **邮箱** 领取奖励
- [x] **悬赏通缉** 自动扫荡
- [x] **战术对抗赛** 领取奖励 / 自动战斗
目前支持的服务器:
@ -22,6 +23,14 @@
- [x] 日服
- [x] 国际服 - 全球
## 已知问题
若愿意提供其他服务器支持,请开 PR 或 Issue。
- **大小月卡**:未实现自动领取~~(因为没买过)~~,可能不影响使用。愿意提供图片的请开 Issue
- **月卡的额外悬赏券和学院交流券**:不太清楚月卡领取额外券的机制~~(因为没买过)~~,可能影响相关任务使用券和体力的计算。愿意提供相关信息的请开
Issue
## 相关项目
- [AzurLaneAutoScript](https://github.com/LmeSzinc/AzurLaneAutoScript): 碧蓝航线自动化脚本

4
aas.py
View File

@ -34,6 +34,10 @@ class ArisuAutoSweeper(AzurLaneAutoScript):
from tasks.mail.mail import Mail
Mail(config=self.config, device=self.device).run()
def bounty(self):
from tasks.bounty.bounty import Bounty
Bounty(config=self.config, device=self.device).run()
def tactical_challenge(self):
from tasks.tactical_challenge.tactical_challenge import TacticalChallenge
TacticalChallenge(config=self.config, device=self.device).run()

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -63,6 +63,26 @@
"ServerUpdate": "04:00"
}
},
"Bounty": {
"Scheduler": {
"Enable": true,
"NextRun": "2020-01-01 00:00:00",
"Command": "Bounty",
"ServerUpdate": "04:00"
},
"Highway": {
"Stage": 1,
"Count": 2
},
"DesertRailroad": {
"Stage": 1,
"Count": 2
},
"Schoolhouse": {
"Stage": 1,
"Count": 2
}
},
"TacticalChallenge": {
"Scheduler": {
"Enable": true,
@ -84,7 +104,10 @@
"ItemStorage": {
"AP": {},
"Credit": {},
"Pyroxene": {}
"Pyroxene": {},
"BountyTicket": {},
"ScrimmageTicket": {},
"TacticalChallengeTicket": {}
}
}
}

View File

@ -257,6 +257,96 @@
}
}
},
"Bounty": {
"Scheduler": {
"Enable": {
"type": "checkbox",
"value": true,
"option": [
true,
false
]
},
"NextRun": {
"type": "datetime",
"value": "2020-01-01 00:00:00",
"validate": "datetime"
},
"Command": {
"type": "input",
"value": "Bounty",
"display": "hide"
},
"ServerUpdate": {
"type": "input",
"value": "04:00",
"display": "hide"
}
},
"Highway": {
"Stage": {
"type": "select",
"value": 1,
"option": [
1,
2,
3,
4,
5,
6,
7,
8,
9
]
},
"Count": {
"type": "input",
"value": 2
}
},
"DesertRailroad": {
"Stage": {
"type": "select",
"value": 1,
"option": [
1,
2,
3,
4,
5,
6,
7,
8,
9
]
},
"Count": {
"type": "input",
"value": 2
}
},
"Schoolhouse": {
"Stage": {
"type": "select",
"value": 1,
"option": [
1,
2,
3,
4,
5,
6,
7,
8,
9
]
},
"Count": {
"type": "input",
"value": 2
}
}
},
"TacticalChallenge": {
"Scheduler": {
"Enable": {
@ -348,6 +438,30 @@
"stored": "StoredInt",
"order": 3,
"color": "#21befc"
},
"BountyTicket": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredBountyTicket",
"order": 4,
"color": "#94cb44"
},
"ScrimmageTicket": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredScrimmageTicket",
"order": 5,
"color": "#f86c6a"
},
"TacticalChallengeTicket": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredTacticalChallengeTicket",
"order": 6,
"color": "#7ac8e5"
}
}
}

View File

@ -81,6 +81,22 @@ Cafe:
AutoAdjust: true
SecondCafe: true
Highway:
Stage:
value: 1
option: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Count: 2
DesertRailroad:
Stage:
value: 1
option: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Count: 2
Schoolhouse:
Stage:
value: 1
option: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Count: 2
TacticalChallenge:
PlayerSelect:
value: 0
@ -99,3 +115,15 @@ ItemStorage:
stored: StoredInt
order: 3
color: "#21befc"
BountyTicket:
stored: StoredBountyTicket
order: 4
color: "#94cb44"
ScrimmageTicket:
stored: StoredScrimmageTicket
order: 5
color: "#f86c6a"
TacticalChallengeTicket:
stored: StoredTacticalChallengeTicket
order: 6
color: "#7ac8e5"

View File

@ -13,6 +13,9 @@ Mail:
Circle:
Scheduler:
Enable: true
Bounty:
Scheduler:
Enable: true
TacticalChallenge:
Scheduler:
Enable: true

View File

@ -14,6 +14,7 @@
"Cafe",
"Mail",
"Circle",
"Bounty",
"TacticalChallenge",
"DataUpdate"
]

View File

@ -35,5 +35,44 @@
},
"order": 3,
"color": "#21befc"
},
"BountyTicket": {
"name": "BountyTicket",
"path": "DataUpdate.ItemStorage.BountyTicket",
"i18n": "ItemStorage.BountyTicket.name",
"stored": "StoredBountyTicket",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 6,
"value": 0
},
"order": 4,
"color": "#94cb44"
},
"ScrimmageTicket": {
"name": "ScrimmageTicket",
"path": "DataUpdate.ItemStorage.ScrimmageTicket",
"i18n": "ItemStorage.ScrimmageTicket.name",
"stored": "StoredScrimmageTicket",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 6,
"value": 0
},
"order": 5,
"color": "#f86c6a"
},
"TacticalChallengeTicket": {
"name": "TacticalChallengeTicket",
"path": "DataUpdate.ItemStorage.TacticalChallengeTicket",
"i18n": "ItemStorage.TacticalChallengeTicket.name",
"stored": "StoredTacticalChallengeTicket",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 5,
"value": 0
},
"order": 6,
"color": "#7ac8e5"
}
}

View File

@ -29,6 +29,11 @@ Daily:
- Scheduler
Circle:
- Scheduler
Bounty:
- Scheduler
- Highway
- DesertRailroad
- Schoolhouse
TacticalChallenge:
- Scheduler
- TacticalChallenge

View File

@ -45,6 +45,18 @@ class GeneratedConfig:
Cafe_AutoAdjust = True
Cafe_SecondCafe = True
# Group `Highway`
Highway_Stage = 1 # 1, 2, 3, 4, 5, 6, 7, 8, 9
Highway_Count = 2
# Group `DesertRailroad`
DesertRailroad_Stage = 1 # 1, 2, 3, 4, 5, 6, 7, 8, 9
DesertRailroad_Count = 2
# Group `Schoolhouse`
Schoolhouse_Stage = 1 # 1, 2, 3, 4, 5, 6, 7, 8, 9
Schoolhouse_Count = 2
# Group `TacticalChallenge`
TacticalChallenge_PlayerSelect = 0 # 0, 1, 2, 3
@ -52,3 +64,6 @@ class GeneratedConfig:
ItemStorage_AP = {}
ItemStorage_Credit = {}
ItemStorage_Pyroxene = {}
ItemStorage_BountyTicket = {}
ItemStorage_ScrimmageTicket = {}
ItemStorage_TacticalChallengeTicket = {}

View File

@ -8,7 +8,7 @@ class ManualConfig:
SCHEDULER_PRIORITY = """
Restart
> Cafe > Circle > Mail > DataUpdate > TacticalChallenge
> Cafe > Circle > Mail > DataUpdate > Bounty > TacticalChallenge
"""
"""

View File

@ -30,6 +30,10 @@
"name": "Club",
"help": ""
},
"Bounty": {
"name": "Bounty",
"help": ""
},
"TacticalChallenge": {
"name": "Tactical Challenge",
"help": ""
@ -209,6 +213,75 @@
"help": "JP server only\nEnable auto switch to second floor and perform interaction"
}
},
"Highway": {
"_info": {
"name": "Overpass",
"help": ""
},
"Stage": {
"name": "Select Stage",
"help": "",
"1": "01 - Overpass A",
"2": "02 - Overpass B",
"3": "03 - Overpass C",
"4": "04 - Overpass D",
"5": "05 - Overpass E",
"6": "06 - Overpass F",
"7": "07 - Overpass G",
"8": "08 - Overpass H",
"9": "09 - Overpass I"
},
"Count": {
"name": "Sweep X times",
"help": "Sweep the selected stage for X times"
}
},
"DesertRailroad": {
"_info": {
"name": "Desert Railroad",
"help": ""
},
"Stage": {
"name": "Select Stage",
"help": "",
"1": "01 - Abandoned Train A",
"2": "02 - Abandoned Train B",
"3": "03 - Abandoned Train C",
"4": "04 - Abandoned Train D",
"5": "05 - Abandoned Train E",
"6": "06 - Abandoned Train F",
"7": "07 - Abandoned Train G",
"8": "08 - Abandoned Train H",
"9": "09 - Abandoned Train I"
},
"Count": {
"name": "Sweep X times",
"help": "Sweep the selected stage for X times"
}
},
"Schoolhouse": {
"_info": {
"name": "Classroom",
"help": ""
},
"Stage": {
"name": "Select Stage",
"help": "",
"1": "01 - Besieged Classroom A",
"2": "02 - Besieged Classroom B",
"3": "03 - Besieged Classroom C",
"4": "04 - Besieged Classroom D",
"5": "05 - Besieged Classroom E",
"6": "06 - Besieged Classroom F",
"7": "07 - Besieged Classroom G",
"8": "08 - Besieged Classroom H",
"9": "09 - Besieged Classroom I"
},
"Count": {
"name": "Sweep X times",
"help": "Sweep the selected stage for X times"
}
},
"TacticalChallenge": {
"_info": {
"name": "Tactical Challenge Settings",
@ -239,6 +312,18 @@
"Pyroxene": {
"name": "Pyroxene",
"help": ""
},
"BountyTicket": {
"name": "Bounty Ticket",
"help": ""
},
"ScrimmageTicket": {
"name": "Scrimmage Ticket",
"help": ""
},
"TacticalChallengeTicket": {
"name": "Tactical Challenge Ticket",
"help": ""
}
},
"Gui": {

View File

@ -30,6 +30,10 @@
"name": "公会",
"help": "社团 / 小组"
},
"Bounty": {
"name": "通缉悬赏",
"help": ""
},
"TacticalChallenge": {
"name": "战术对抗赛",
"help": "战术大赛 / 竞技场"
@ -209,6 +213,75 @@
"help": "仅支持日服\n自动切换第二咖啡厅进行互动点击"
}
},
"Highway": {
"_info": {
"name": "高架公路",
"help": ""
},
"Stage": {
"name": "选择关卡",
"help": "",
"1": "01 - 高架公路 A",
"2": "02 - 高架公路 B",
"3": "03 - 高架公路 C",
"4": "04 - 高架公路 D",
"5": "05 - 高架公路 E",
"6": "06 - 高架公路 F",
"7": "07 - 高架公路 G",
"8": "08 - 高架公路 H",
"9": "09 - 高架公路 I"
},
"Count": {
"name": "扫荡次数",
"help": "扫荡所选择关卡 X 次"
}
},
"DesertRailroad": {
"_info": {
"name": "沙漠铁路",
"help": ""
},
"Stage": {
"name": "选择关卡",
"help": "",
"1": "01 - 被遗弃的列车 A",
"2": "02 - 被遗弃的列车 B",
"3": "03 - 被遗弃的列车 C",
"4": "04 - 被遗弃的列车 D",
"5": "05 - 被遗弃的列车 E",
"6": "06 - 被遗弃的列车 F",
"7": "07 - 被遗弃的列车 G",
"8": "08 - 被遗弃的列车 H",
"9": "09 - 被遗弃的列车 I"
},
"Count": {
"name": "扫荡次数",
"help": "扫荡所选择关卡 X 次"
}
},
"Schoolhouse": {
"_info": {
"name": "教室",
"help": ""
},
"Stage": {
"name": "选择关卡",
"help": "",
"1": "01 - 被袭击的教室 A",
"2": "02 - 被袭击的教室 B",
"3": "03 - 被袭击的教室 C",
"4": "04 - 被袭击的教室 D",
"5": "05 - 被袭击的教室 E",
"6": "06 - 被袭击的教室 F",
"7": "07 - 被袭击的教室 G",
"8": "08 - 被袭击的教室 H",
"9": "09 - 被袭击的教室 I"
},
"Count": {
"name": "扫荡次数",
"help": "扫荡所选择关卡 X 次"
}
},
"TacticalChallenge": {
"_info": {
"name": "战术对抗赛设置",
@ -239,6 +312,18 @@
"Pyroxene": {
"name": "青辉石",
"help": ""
},
"BountyTicket": {
"name": "悬赏通缉票券",
"help": ""
},
"ScrimmageTicket": {
"name": "学院交流会票券",
"help": ""
},
"TacticalChallengeTicket": {
"name": "战术对抗赛票券",
"help": ""
}
},
"Gui": {

View File

@ -3,6 +3,8 @@ from functools import cached_property as functools_cached_property
from module.base.decorator import cached_property
from module.config.utils import DEFAULT_TIME, deep_get, get_server_last_monday_update, get_server_last_update
# from module.exception import ScriptError
@ -173,6 +175,20 @@ class StoredCounter(StoredBase):
class StoredAP(StoredCounter):
pass
class StoredBountyTicket(StoredCounter, StoredExpiredAt0400):
FIXED_TOTAL = 6
pass
class StoredScrimmageTicket(StoredCounter, StoredExpiredAt0400):
FIXED_TOTAL = 6
class StoredTacticalChallengeTicket(StoredCounter, StoredExpiredAt0400):
FIXED_TOTAL = 5
# class StoredDailyActivity(StoredCounter, StoredExpiredAt0400):
# FIXED_TOTAL = 500
#

View File

@ -1,10 +1,9 @@
from module.config.stored.classes import (
StoredAP,
StoredBase,
StoredCounter,
StoredExpiredAt0400,
StoredExpiredAtMonday0400,
StoredBountyTicket,
StoredInt,
StoredScrimmageTicket,
StoredTacticalChallengeTicket,
)
@ -15,3 +14,6 @@ class StoredGenerated:
AP = StoredAP("DataUpdate.ItemStorage.AP")
Credit = StoredInt("DataUpdate.ItemStorage.Credit")
Pyroxene = StoredInt("DataUpdate.ItemStorage.Pyroxene")
BountyTicket = StoredBountyTicket("DataUpdate.ItemStorage.BountyTicket")
ScrimmageTicket = StoredScrimmageTicket("DataUpdate.ItemStorage.ScrimmageTicket")
TacticalChallengeTicket = StoredTacticalChallengeTicket("DataUpdate.ItemStorage.TacticalChallengeTicket")

View File

@ -3,6 +3,17 @@ 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_BOUNTY = ButtonWrapper(
name='CHECK_BOUNTY',
jp=Button(
file='./assets/jp/bounty/CHECK_BOUNTY.png',
area=(654, 87, 889, 129),
search=(634, 67, 909, 149),
color=(88, 113, 139),
button=(654, 87, 889, 129),
),
en=None,
)
CHECK_DESERT_RAILROAD = ButtonWrapper(
name='CHECK_DESERT_RAILROAD',
jp=Button(
@ -36,6 +47,17 @@ CHECK_SCHOOLHOUSE = ButtonWrapper(
),
en=None,
)
OCR_TICKET = ButtonWrapper(
name='OCR_TICKET',
jp=Button(
file='./assets/jp/bounty/OCR_TICKET.png',
area=(195, 85, 237, 113),
search=(175, 65, 257, 133),
color=(197, 206, 213),
button=(195, 85, 237, 113),
),
en=None,
)
SELECT_DESERT_RAILROAD = ButtonWrapper(
name='SELECT_DESERT_RAILROAD',
jp=Button(

106
tasks/bounty/bounty.py Normal file
View File

@ -0,0 +1,106 @@
from enum import Flag
from module.base.timer import Timer
from module.exception import RequestHumanTakeover
from module.logger import logger
from tasks.base.assets.assets_base_page import BACK
from tasks.base.page import page_bounty
from tasks.bounty.assets.assets_bounty import *
from tasks.bounty.ui import BountyUI
class BountyStatus(Flag):
OCR = 0
SELECT = 1
ENTER = 2
SWEEP = 3
END = 4
FINISH = 5
class Bounty(BountyUI):
@property
def bounty_info(self):
bounty = (SELECT_HIGHWAY, SELECT_DESERT_RAILROAD, SELECT_SCHOOLHOUSE)
check = (CHECK_HIGHWAY, CHECK_DESERT_RAILROAD, CHECK_SCHOOLHOUSE)
stage = (self.config.Highway_Stage, self.config.DesertRailroad_Stage, self.config.Schoolhouse_Stage)
count = (self.config.Highway_Count, self.config.DesertRailroad_Count, self.config.Schoolhouse_Count)
info = zip(bounty, check, stage, count)
return filter(lambda x: x[3] > 0, info)
@property
def valid_task(self) -> list:
task = list(self.bounty_info)
if not task:
logger.warning('Bounty enabled but no task set')
self.config.task_delay(server_update=True)
self.config.task_stop()
return task
@property
def is_ticket_enough(self) -> bool:
return self.config.stored.BountyTicket.value >= self.current_count
@property
def current_bounty(self):
return self.task[0][:2]
@property
def current_stage(self):
return self.task[0][2]
@property
def current_count(self):
return self.task[0][3]
def handle_bounty(self, status):
match status:
case BountyStatus.OCR:
if self.get_ticket():
return BountyStatus.SELECT
case BountyStatus.SELECT:
if not self.is_ticket_enough:
logger.warning('Bounty ticket not enough')
raise RequestHumanTakeover
if self.select_bounty(*self.current_bounty):
return BountyStatus.ENTER
case BountyStatus.ENTER:
if self.enter_stage(self.current_stage):
return BountyStatus.SWEEP
case BountyStatus.SWEEP:
if self.do_sweep(self.current_count):
self.task.pop(0)
if not self.task:
return BountyStatus.FINISH
return BountyStatus.END
return BountyStatus.ENTER
case BountyStatus.END:
if self.appear(CHECK_BOUNTY):
return BountyStatus.OCR
self.click_with_interval(BACK, interval=2)
case BountyStatus.FINISH:
return status
case _:
logger.warning(f'Invalid status: {status}')
return status
def run(self):
self.ui_ensure(page_bounty)
self.task = self.valid_task
action_timer = Timer(0.5, 1)
status = BountyStatus.OCR
while 1:
self.device.screenshot()
if self.ui_additional():
continue
if action_timer.reached_and_reset():
logger.attr('Status', status)
status = self.handle_bounty(status)
if status == BountyStatus.FINISH:
break
self.config.task_delay(server_update=True)

48
tasks/bounty/ui.py Normal file
View File

@ -0,0 +1,48 @@
from module.base.timer import Timer
from module.logger import logger
from module.ocr.ocr import DigitCounter
from tasks.base.ui import UI
from tasks.bounty.assets.assets_bounty import *
from tasks.stage.list import StageList
from tasks.stage.sweep import StageSweep
BOUNTY_LIST = StageList('BountyList')
BOUNTY_SWEEP = StageSweep('BountySweep', 6)
class BountyUI(UI):
def select_bounty(self, dest_enter: ButtonWrapper, dest_check: ButtonWrapper):
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
def enter_stage(self, index: int) -> bool:
if BOUNTY_LIST.select_index_enter(index, self):
return True
return False
def do_sweep(self, num: int) -> bool:
if BOUNTY_SWEEP.do_sweep(self, num=num):
return True
return False
def get_ticket(self):
"""
Page:
in: page_bounty
"""
if not self.appear(CHECK_BOUNTY):
logger.warning('OCR failed due to invalid page')
return False
ticket, _, total = DigitCounter(OCR_TICKET).ocr_single_line(self.device.image)
if total == 0:
logger.warning('Invalid ticket')
return False
logger.attr('BountyTicket', ticket)
self.config.stored.BountyTicket.set(ticket)
return True

View File

@ -19,11 +19,10 @@ class StageSweep:
def __init__(
self,
name: str,
sweep_num: int,
max_sweep: int,
):
self.name = name
self.sweep_num = sweep_num
self.sweep_num = None
self.check: ButtonWrapper = None
self.num: Digit = None
@ -45,7 +44,6 @@ class StageSweep:
self.current_sweep = 0
self.sweep_method = None
self.set_mode()
def __str__(self):
return f'StageSweep({self.name})'
@ -88,9 +86,10 @@ class StageSweep:
self.skip_ok_upper = button_skip_ok_upper if button_skip_ok_upper else SKIP_OK_UPPER
self.skip_ok_lower = button_skip_ok_lower if button_skip_ok_lower else SKIP_OK_LOWER
def set_mode(self, mode: str = None):
if mode is None:
match self.sweep_num:
def set_mode(self, mode: str = None, num: int = None) -> bool:
if num is not None:
self.sweep_num = num
match num:
case 0:
self.sweep_method = self.set_sweep_min
case -1:
@ -98,15 +97,19 @@ class StageSweep:
case x if x > 0:
self.sweep_method = self.set_sweep_num
case _:
logger.warning(f'Invalid sweep num: {self.sweep_num}')
return
match mode:
case 'max':
self.sweep_method = self.set_sweep_max
case 'min':
self.sweep_method = self.set_sweep_min
case _:
logger.warning(f'Invalid sweep mode: {mode}')
logger.warning(f'Invalid sweep num: {num}')
return True
if mode is not None:
match mode:
case 'max':
self.sweep_method = self.set_sweep_max
case 'min':
self.sweep_method = self.set_sweep_min
case _:
logger.warning(f'Invalid sweep mode: {mode}')
return True
logger.warning(f'Invalid sweep setting')
return False
def check_sweep(self, main: ModuleBase):
return main.appear(self.check)
@ -115,8 +118,11 @@ class StageSweep:
return main.appear(self.skip_skip) or main.appear(self.skip_ok_upper) or main.appear(self.skip_ok_lower)
def load_sweep_num(self, main: ModuleBase):
timer = Timer(0.5, 2).start()
while 1:
main.device.screenshot()
if not timer.reached_and_reset():
continue
ocr_result = self.num.detect_and_ocr(main.device.image)
if not ocr_result:
logger.warning(f'No valid num in {self.num.name}')
@ -204,7 +210,9 @@ class StageSweep:
if retry.reached_and_reset():
main.click_with_interval(self.min, interval=0)
def do_sweep(self, main: ModuleBase, skip_first_screenshot=True) -> bool:
def do_sweep(self, main: ModuleBase, mode: str = None, num: int = None, skip_first_screenshot=True) -> bool:
if not self.set_mode(mode, num):
return False
timer = Timer(0.5, 1)
timer_stable = Timer(0.5, 1).start()
status = SweepStatus.SELECT