StarRailCopilot/tasks/daily/daily_quest.py

420 lines
17 KiB
Python
Raw Normal View History

from datetime import timedelta
import cv2
import numpy as np
from module.base.timer import Timer
from module.base.utils import crop
from module.config.utils import DEFAULT_TIME, get_server_next_update
from module.logger import logger
from module.ocr.ocr import Ocr, OcrResultButton
from module.ocr.utils import split_and_pair_buttons
from tasks.daily.assets.assets_daily_reward import *
2023-06-18 10:41:58 +00:00
from tasks.daily.camera import CameraUI
from tasks.daily.keywords import (
DailyQuest,
DailyQuestState,
KEYWORDS_DAILY_QUEST,
KEYWORDS_DAILY_QUEST_STATE,
)
from tasks.daily.synthesize import SynthesizeMaterialUI
from tasks.daily.use_technique import UseTechniqueUI
from tasks.dungeon.assets.assets_dungeon_ui import DAILY_TRAINING_CHECK
from tasks.dungeon.keywords import KEYWORDS_DUNGEON_TAB
from tasks.dungeon.ui import DungeonUI
from tasks.item.consumable_usage import ConsumableUsageUI
from tasks.item.relics import RelicsUI
from tasks.map.route.loader import RouteLoader
2023-09-23 22:52:52 +00:00
from tasks.map.route.route import ROUTE_DAILY
class DailyQuestOcr(Ocr):
merge_thres_y = 20
def pre_process(self, image):
image = super().pre_process(image)
image = crop(image, OCR_DAILY_QUEST.area)
mask = MASK_DAILY_QUEST.matched_button.image
# Remove "+200Activity"
cv2.bitwise_and(image, mask, dst=image)
return image
def after_process(self, result):
result = super().after_process(result)
2023-09-08 14:23:57 +00:00
if self.lang == 'cn':
2023-06-15 04:01:51 +00:00
result = result.replace("J", "")
result = result.replace(";", "")
2023-09-02 13:07:13 +00:00
result = result.replace("", "")
result = result.replace("宇审", "宇宙")
2023-09-12 10:39:23 +00:00
result = result.replace("凝带", "凝滞")
2023-06-18 10:41:58 +00:00
# 进行中」hbadarin
if "进行中" in result:
result = "进行中"
if "已领取" in result:
result = "已领取"
if self.lang == 'en':
result = result.replace('wor(d', 'world')
2023-09-19 07:05:23 +00:00
# Echo/ofWar
result = result.replace('cho/of', 'cho of')
2023-09-22 01:39:14 +00:00
# Catyx(Golden).1.times
result = result.replace('atyx', 'alyx')
if "progress" in result.lower():
result = "In Progress"
if "claimed" in result.lower():
result = "Claimed"
return result
class DailyQuestUI(DungeonUI, RouteLoader):
claimed_point_reward = False
synthesized_material = False
def _ensure_position(self, direction: str, skip_first_screenshot=True):
interval = Timer(5)
if direction == 'left':
template = DAILY_QUEST_LEFT_START
elif direction == 'right':
template = DAILY_QUEST_RIGHT_END
else:
logger.warning(f'Unknown drag direction: {direction}')
return
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.appear(template):
logger.info(f"Ensure position: {direction}")
break
# Might be a screenshot mixed with daily_training and get_reward
# Swipe at daily training page only
if interval.reached() and self.match_template_color(DAILY_TRAINING_CHECK):
self._daily_quests_swipe(direction)
interval.reset()
continue
def _daily_quests_swipe(self, direction: str):
2023-06-16 20:25:15 +00:00
vector = np.random.uniform(0.4, 0.5)
if direction == 'left':
swipe_vector = (vector * OCR_DAILY_QUEST.width, 0)
elif direction == 'right':
swipe_vector = (-vector * OCR_DAILY_QUEST.width, 0)
else:
logger.warning(f'Unknown drag direction: {direction}')
return
self.device.swipe_vector(swipe_vector, box=OCR_DAILY_QUEST.button,
random_range=(-10, -10, 10, 10), name='DAILY_QUEST_DRAG')
def _ocr_single_page(self) -> list[OcrResultButton]:
ocr = DailyQuestOcr(OCR_DAILY_QUEST)
results = ocr.matched_ocr(self.device.image, [DailyQuestState, DailyQuest], direct_ocr=True)
if len(results) < 8:
logger.warning(f"Recognition failed at {8 - len(results)} quests on one page")
def completed_state(state):
return state != KEYWORDS_DAILY_QUEST_STATE.Go and state != KEYWORDS_DAILY_QUEST_STATE.In_Progress
return [quest for quest, _ in
split_and_pair_buttons(results, split_func=completed_state, relative_area=(0, 0, 200, 720))]
2023-06-16 20:25:15 +00:00
def daily_quests_recognition(self) -> list[DailyQuest]:
"""
Returns incomplete quests only
"""
logger.info("Recognizing daily quests")
self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Daily_Training)
self._ensure_position('left')
results = self._ocr_single_page()
self._ensure_position('right')
results += [result for result in self._ocr_single_page() if result not in results]
2023-06-16 20:25:15 +00:00
results = [result.matched_keyword for result in results]
logger.info("Daily quests recognition complete")
2023-06-16 20:25:15 +00:00
logger.info(f"Daily quests: {results}")
self.config.stored.DailyQuest.write_quests(results)
return results
def _get_quest_reward(self, skip_first_screenshot=True):
self._ensure_position('left')
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.appear(DAILY_QUEST_FULL):
logger.info('No more quests to get, activity full')
break
if self.appear(DAILY_QUEST_GOTO):
logger.info('No more quests to get, have quests uncompleted')
break
if self.appear_then_click(DAILY_QUEST_REWARD, interval=1):
continue
def _no_reward_to_get(self):
return (
(self.appear(ACTIVE_POINTS_1_LOCKED) or self.appear(ACTIVE_POINTS_1_CHECKED))
and (self.appear(ACTIVE_POINTS_2_LOCKED) or self.appear(ACTIVE_POINTS_2_CHECKED))
and (self.appear(ACTIVE_POINTS_3_LOCKED) or self.appear(ACTIVE_POINTS_3_CHECKED))
and (self.appear(ACTIVE_POINTS_4_LOCKED) or self.appear(ACTIVE_POINTS_4_CHECKED))
and (self.appear(ACTIVE_POINTS_5_LOCKED) or self.appear(ACTIVE_POINTS_5_CHECKED))
)
2023-06-18 10:41:58 +00:00
def _all_reward_got(self):
return self.appear(ACTIVE_POINTS_5_CHECKED)
def _get_active_point_reward(self, skip_first_screenshot=True):
"""
self.claimed_point_reward will be set if claimed any point reward
"""
2023-07-20 07:03:32 +00:00
def get_active():
for b in [
2023-07-20 07:03:32 +00:00
ACTIVE_POINTS_1_UNLOCK,
ACTIVE_POINTS_2_UNLOCK,
ACTIVE_POINTS_3_UNLOCK,
ACTIVE_POINTS_4_UNLOCK,
ACTIVE_POINTS_5_UNLOCK
]:
# Black gift icon
if self.image_color_count(b, color=(61, 53, 53), threshold=221, count=100):
return b
2023-07-20 07:03:32 +00:00
return None
interval = Timer(2)
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self._no_reward_to_get():
logger.info('No more reward to get')
break
if self.handle_reward():
continue
2023-07-20 07:03:32 +00:00
if interval.reached():
if active := get_active():
self.device.click(active)
self.claimed_point_reward = True
2023-07-20 07:03:32 +00:00
interval.reset()
# Write stored
point = 0
for progress, button in zip(
[100, 200, 300, 400, 500],
[
ACTIVE_POINTS_1_CHECKED,
ACTIVE_POINTS_2_CHECKED,
ACTIVE_POINTS_3_CHECKED,
ACTIVE_POINTS_4_CHECKED,
ACTIVE_POINTS_5_CHECKED
]
):
if self.appear(button):
point = progress
logger.attr('Daily activity', point)
with self.config.multi_set():
self.config.stored.DailyActivity.set(point)
if point == 500:
self.config.stored.DailyQuest.write_quests([])
def get_daily_rewards(self):
2023-06-18 10:41:58 +00:00
"""
Returns:
int: If all reward got.
Pages:
in: Any
out: page_guide, Daily_Training
"""
logger.hr('Get daily rewards', level=1)
self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Daily_Training)
logger.info("Getting quest rewards")
self._get_quest_reward()
logger.info("Getting active point rewards")
self._get_active_point_reward()
2023-06-18 10:41:58 +00:00
if self._all_reward_got():
logger.info("All daily reward got")
return True
else:
logger.info('Daily reward got but not yet claimed')
return False
def do_daily_quests(self):
"""
Returns:
int: Number of quests done
"""
logger.hr('Recognize quests', level=1)
quests = self.config.stored.DailyQuest.load_quests()
2023-06-18 10:41:58 +00:00
done = 0
logger.hr('Do quests', level=1)
2023-12-27 16:56:15 +00:00
if KEYWORDS_DAILY_QUEST.Take_photos_1_times in quests:
2023-06-18 10:41:58 +00:00
CameraUI(self.config, self.device).take_picture()
done += 1
2023-12-27 16:56:15 +00:00
"""
2023-06-18 10:41:58 +00:00
if KEYWORDS_DAILY_QUEST.Synthesize_Consumable_1_time in quests:
2023-06-23 14:31:00 +00:00
if SynthesizeConsumablesUI(self.config, self.device).synthesize_consumables():
done += 1
2023-12-27 16:56:15 +00:00
"""
if KEYWORDS_DAILY_QUEST.Use_the_Omni_Synthesizer_1_times in quests:
2023-06-23 14:31:00 +00:00
if SynthesizeMaterialUI(self.config, self.device).synthesize_material():
self.synthesized_material = True
2023-06-23 14:31:00 +00:00
done += 1
2023-06-19 16:44:53 +00:00
if KEYWORDS_DAILY_QUEST.Use_Consumables_1_time in quests:
if ConsumableUsageUI(self.config, self.device).use_consumable():
done += 1
if KEYWORDS_DAILY_QUEST.Use_Technique_2_times in quests:
UseTechniqueUI(self.config, self.device).use_technique(2)
done += 1
if KEYWORDS_DAILY_QUEST.Salvage_any_Relic in quests:
if RelicsUI(self.config, self.device).salvage_relic():
done += 1
if KEYWORDS_DAILY_QUEST.Complete_Forgotten_Hall_1_time in quests:
2023-09-23 22:52:52 +00:00
self.route_run(ROUTE_DAILY.ForgottenHallStage1__route)
done += 1
2023-06-18 10:41:58 +00:00
"""
enemy x1 In_a_single_battle_inflict_3_Weakness_Break_of_different_Types
enemy x1 Inflict_Weakness_Break_5_times
enemy x2 Defeat_a_total_of_20_enemies
2023-12-27 16:56:15 +00:00
enemy x3 Enter_combat_by_attacking_enemie_Weakness_and_win_3_times
item x1 Destroy_3_destructible_objects
enemy x1 Use_an_Ultimate_to_deal_the_final_blow_1_time
"""
enemy = 0
item = 0
if KEYWORDS_DAILY_QUEST.In_a_single_battle_inflict_3_Weakness_Break_of_different_Types in quests:
enemy = max(enemy, 1)
if KEYWORDS_DAILY_QUEST.Inflict_Weakness_Break_5_times in quests:
enemy = max(enemy, 1)
if KEYWORDS_DAILY_QUEST.Defeat_a_total_of_20_enemies in quests:
enemy = max(enemy, 2)
2023-12-27 16:56:15 +00:00
if KEYWORDS_DAILY_QUEST.Enter_combat_by_attacking_enemie_Weakness_and_win_3_times in quests:
enemy = max(enemy, 3)
if KEYWORDS_DAILY_QUEST.Destroy_3_destructible_objects in quests:
item = max(item, 1)
if KEYWORDS_DAILY_QUEST.Use_an_Ultimate_to_deal_the_final_blow_1_time in quests:
enemy = max(enemy, 1)
logger.info(f'Himeko trial, enemy={enemy}, item={item}')
for run in [1, 2, 3]:
if enemy >= run and item >= run:
self.route_run(ROUTE_DAILY.HimekoTrial__route_item_enemy)
done += 1
elif enemy >= run:
self.route_run(ROUTE_DAILY.HimekoTrial__route_enemy)
done += 1
elif item >= run:
self.route_run(ROUTE_DAILY.HimekoTrial__route_item)
done += 1
else:
break
if max(enemy, item) > 0:
self.route_run(ROUTE_DAILY.HimekoTrial__exit)
2023-06-18 10:41:58 +00:00
return done
def check_future_achieve(self):
"""
Returns:
bool: True if daily activity will full, skip doing normal quests
False if daily activity will not full, do normal quests
"""
point = self.config.stored.DailyActivity.value
if point >= self.config.stored.DailyActivity.FIXED_TOTAL:
logger.warning('DailyActivity full, no need to check future')
return True
quests = self.config.stored.DailyQuest.load_quests()
if not len(quests):
logger.warning('DailyQuest empty, cannot check future')
return True
# Get task schedule
assignment = self.config.cross_get('Assignment.Scheduler.NextRun', default=DEFAULT_TIME)
dungeon = self.config.cross_get('Dungeon.Scheduler.NextRun', default=DEFAULT_TIME)
reset = get_server_next_update(self.config.Scheduler_ServerUpdate)
logger.info(f'Assignment next run: {assignment}')
logger.info(f'Dungeon next run: {dungeon}')
logger.info(f'Daily reset: {reset}')
# Calculate quests to be done in the future
future = 0
if KEYWORDS_DAILY_QUEST.Obtain_victory_in_combat_with_Support_Characters_1_times in quests:
# 10min in advance to do quests
if dungeon < reset - timedelta(minutes=10):
logger.info('Daily support can be achieved in the future')
future += 200
else:
logger.info('Daily support cannot achieved, dungeon task is scheduled tomorrow')
if KEYWORDS_DAILY_QUEST.Consume_120_Trailblaze_Power in quests:
# 12h10min in advance, waiting for stamina
if dungeon < reset - timedelta(hours=12, minutes=10):
logger.info('Stamina consume can be achieved in the future')
future += 200
else:
logger.info('Stamina consume cannot achieved, dungeon task is scheduled tomorrow')
if KEYWORDS_DAILY_QUEST.Dispatch_1_assignments in quests:
# 10min in advance to do quests
if assignment < reset - timedelta(minutes=10):
logger.info('Assignment can be achieved in the future')
future += 100
else:
logger.info('Assignment cannot achieved, assignment task is scheduled tomorrow')
# Check
logger.attr('Future daily activity', future)
if point + future >= self.config.stored.DailyActivity.FIXED_TOTAL:
logger.info('Daily activity will full, skip doing normal quests')
return True
else:
logger.info('Daily activity will not full, do normal quests')
return False
2023-06-18 10:41:58 +00:00
def run(self):
self.config.update_battle_pass_quests()
self.claimed_point_reward = False
self.synthesized_material = False
2023-06-18 10:41:58 +00:00
for _ in range(5):
got = self.get_daily_rewards()
if got:
break
self.daily_quests_recognition()
future = self.check_future_achieve()
if future:
break
2023-06-18 10:41:58 +00:00
done = self.do_daily_quests()
if not done:
logger.info('No more quests able to do')
break
# Scheduler
logger.attr('claimed_point_reward', self.claimed_point_reward)
logger.attr('synthesized_material', self.synthesized_material)
with self.config.multi_set():
# Check battle pass
# Cannot archive as daily is done by synthesizing material
# but battle pass quests need synthesizing consumables.
# if self.synthesized_material:
# quests = self.config.stored.BattlePassWeeklyQuest.load_quests()
2024-02-06 21:13:54 +00:00
# if KEYWORDS_BATTLE_PASS_QUEST.Synthesize_Consumables_1_times in quests:
# logger.info('Done weekly quest Synthesize_Consumables_1_times once')
# self.config.stored.BattlePassQuestSynthesizeConsumables.add()
# if self.config.stored.BattlePassQuestSynthesizeConsumables.is_full():
# logger.info('Achieve weekly quest BattlePassQuestSynthesizeConsumables')
# self.config.task_call('BattlePass')
# Update dashboard
if self.claimed_point_reward:
self.config.task_call('DataUpdate')
# Delay self
self.config.task_delay(server_update=True)
if __name__ == '__main__':
self = DailyQuestUI('src')
self.device.screenshot()
self.route_run(ROUTE_DAILY.HimekoTrial__route_enemy)