StarRailCopilot/tasks/dungeon/dungeon.py

462 lines
21 KiB
Python

from datetime import timedelta
from module.base.decorator import set_cached_property
from module.base.utils import area_offset
from module.config.stored.classes import now
from module.logger import logger
from tasks.battle_pass.keywords import KEYWORDS_BATTLE_PASS_QUEST
from tasks.combat.combat import Combat
from tasks.daily.keywords import KEYWORDS_DAILY_QUEST
from tasks.dungeon.event import DungeonEvent
from tasks.dungeon.keywords import DungeonList, KEYWORDS_DUNGEON_LIST, KEYWORDS_DUNGEON_NAV, KEYWORDS_DUNGEON_TAB
from tasks.dungeon.stamina import DungeonStamina
from tasks.item.synthesize import Synthesize
class Dungeon(DungeonStamina, DungeonEvent, Combat):
called_daily_support = False
achieved_daily_quest = False
achieved_weekly_quest = False
running_double = False
support_once = True
daily_quests = []
weekly_quests = []
def _dungeon_run(self, dungeon: DungeonList, team: int = None, wave_limit: int = 0, support_character: str = None,
skip_ui_switch: bool = False):
"""
Args:
dungeon:
team: 1 to 6.
wave_limit: Limit combat runs, 0 means no limit.
support_character: Support character name
skip_ui_switch: True if already at dungeon aside
Returns:
int: Run count
Pages:
in: page_main, DUNGEON_COMBAT_INTERACT
or COMBAT_PREPARE
out: page_main
"""
if team is None:
team = self.config.Dungeon_Team
if support_character is None and self.config.DungeonSupport_Use == 'always_use':
support_character = self.config.DungeonSupport_Character
logger.hr('Dungeon run', level=1)
logger.info(f'Dungeon: {dungeon}, team={team}, wave_limit={wave_limit}, support_character={support_character}')
if not skip_ui_switch:
interact = self.get_dungeon_interact()
if interact is not None and interact == dungeon:
logger.info('Already nearby dungeon')
else:
self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index)
self.dungeon_goto(dungeon)
if dungeon == KEYWORDS_DUNGEON_LIST.Stagnant_Shadow_Blaze:
if self.handle_destructible_around_blaze():
self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index)
self.dungeon_goto(dungeon)
self.combat_enter_from_map()
# Check double event remain before combat
# Conservatively prefer the smaller result
if (dungeon.is_Calyx_Golden or dungeon.is_Calyx_Crimson) and \
self.running_double and self.config.stored.DungeonDouble.calyx > 0:
calyx = self.get_double_event_remain_at_combat()
if calyx is not None and calyx < self.config.stored.DungeonDouble.calyx:
self.config.stored.DungeonDouble.calyx = calyx
wave_limit = calyx
if calyx == 0:
return 0
if dungeon.is_Cavern_of_Corrosion and self.running_double and \
self.config.stored.DungeonDouble.relic > 0:
relic = self.get_double_event_remain_at_combat()
if relic is not None and relic < self.config.stored.DungeonDouble.relic:
self.config.stored.DungeonDouble.relic = relic
wave_limit = relic
if relic == 0:
return 0
# No need, already checked in Survival_Index
# if dungeon.is_Ornament_Extraction and self.running_double and \
# self.config.stored.DungeonDouble.rogue > 0:
# rogue = self.get_double_event_remain_at_combat()
# if rogue is not None and rogue < self.config.stored.DungeonDouble.rogue:
# self.config.stored.DungeonDouble.rogue = rogue
# wave_limit = rogue
# if rogue == 0:
# return 0
# Combat
self.dungeon = dungeon
count = self.combat(team=team, wave_limit=wave_limit, support_character=support_character)
self.dungeon = None
# Update quest states
with self.config.multi_set():
# Calyx_Golden
if dungeon.is_Calyx_Golden:
if KEYWORDS_DAILY_QUEST.Clear_Calyx_Golden_1_times in self.daily_quests:
logger.info('Achieved daily quest Clear_Calyx_Golden_1_times')
self.achieved_daily_quest = True
if KEYWORDS_BATTLE_PASS_QUEST.Clear_Calyx_1_times in self.weekly_quests:
logger.info('Done weekly quest Clear_Calyx_1_times once')
self.config.stored.BattlePassQuestCalyx.add()
if self.config.stored.BattlePassQuestCalyx.is_full():
logger.info('Achieved weekly quest BattlePassQuestCalyx')
self.achieved_weekly_quest = True
# Calyx_Crimson
if dungeon.is_Calyx_Crimson:
if KEYWORDS_DAILY_QUEST.Clear_Calyx_Crimson_1_times in self.daily_quests:
logger.info('Achieve daily quest Clear_Calyx_Crimson_1_times')
self.achieved_daily_quest = True
if KEYWORDS_BATTLE_PASS_QUEST.Clear_Calyx_1_times in self.weekly_quests:
logger.info('Done weekly quest Clear_Calyx_1_times once')
self.config.stored.BattlePassQuestCalyx.add()
if self.config.stored.BattlePassQuestCalyx.is_full():
logger.info('Achieved weekly quest BattlePassQuestCalyx')
self.achieved_weekly_quest = True
# Stagnant_Shadow
if dungeon.is_Stagnant_Shadow:
if KEYWORDS_DAILY_QUEST.Clear_Stagnant_Shadow_1_times in self.daily_quests:
logger.info('Achieve daily quest Clear_Stagnant_Shadow_1_times')
self.achieved_daily_quest = True
if KEYWORDS_BATTLE_PASS_QUEST.Clear_Stagnant_Shadow_1_times in self.weekly_quests:
logger.info('Done weekly quest Clear_Stagnant_Shadow_1_times once')
self.config.stored.BattlePassQuestStagnantShadow.add()
if self.config.stored.BattlePassQuestStagnantShadow.is_full():
logger.info('Achieved weekly quest Clear_Stagnant_Shadow_1_times')
self.achieved_weekly_quest = True
# Cavern_of_Corrosion
if dungeon.is_Cavern_of_Corrosion:
if KEYWORDS_DAILY_QUEST.Clear_Cavern_of_Corrosion_1_times in self.daily_quests:
logger.info('Achieve daily quest Clear_Cavern_of_Corrosion_1_times')
self.achieved_daily_quest = True
if KEYWORDS_BATTLE_PASS_QUEST.Clear_Cavern_of_Corrosion_1_times in self.weekly_quests:
logger.info('Done weekly quest Clear_Cavern_of_Corrosion_1_times once')
self.config.stored.BattlePassQuestCavernOfCorrosion.add()
if self.config.stored.BattlePassQuestCavernOfCorrosion.is_full():
logger.info('Achieved weekly quest Clear_Cavern_of_Corrosion_1_times')
self.achieved_weekly_quest = True
# Echo_of_War
if dungeon.is_Echo_of_War:
if KEYWORDS_DAILY_QUEST.Complete_Echo_of_War_1_times in self.daily_quests:
logger.info('Achieve daily quest Complete_Echo_of_War_1_times')
self.achieved_daily_quest = True
if KEYWORDS_BATTLE_PASS_QUEST.Complete_Echo_of_War_1_times in self.weekly_quests:
logger.info('Done weekly quest Complete_Echo_of_War_1_times once')
self.config.stored.BattlePassQuestEchoOfWar.add()
if self.config.stored.BattlePassQuestEchoOfWar.is_full():
logger.info('Achieved weekly quest Complete_Echo_of_War_1_times')
self.achieved_weekly_quest = True
# Ornament_Extraction
if dungeon.is_Ornament_Extraction:
if KEYWORDS_BATTLE_PASS_QUEST.Complete_Divergent_Universe_or_Simulated_Universe_1_times in self.weekly_quests:
logger.info('Achieved weekly quest Complete_Divergent_Universe_or_Simulated_Universe_1_times')
# No need to add since it's 0/1
self.achieved_weekly_quest = True
# Support quest
if support_character is not None:
self.called_daily_support = True
if KEYWORDS_DAILY_QUEST.Obtain_victory_in_combat_with_Support_Characters_1_times in self.daily_quests:
logger.info('Achieve daily quest Obtain_victory_in_combat_with_Support_Characters_1_times')
self.achieved_daily_quest = True
# Stamina quest
self.check_stamina_quest(self.combat_wave_cost * count)
# Check trailblaze power, this may stop current task
if self.is_trailblaze_power_exhausted():
# Scheduler
self.delay_dungeon_task(dungeon)
self.check_synthesize()
self.config.task_stop()
return count
def dungeon_run(
self, dungeon: DungeonList, team: int = None, wave_limit: int = 0, support_character: str = None):
"""
Run dungeon, and handle daily support
Args:
dungeon:
team: 1 to 6.
wave_limit: Limit combat runs, 0 means no limit.
support_character: Support character name
Returns:
int: Run count
Pages:
in: Any
out: page_main
"""
require = self.require_compulsory_support()
if require and self.support_once:
logger.info('Run once with support')
count = self._dungeon_run(dungeon=dungeon, team=team, wave_limit=1,
support_character=self.config.DungeonSupport_Character)
logger.info('Run the rest waves without compulsory support')
if wave_limit >= 2 or wave_limit == 0:
# Already at page_name with DUNGEON_COMBAT_INTERACT
if wave_limit >= 2:
wave_limit -= 1
count += self._dungeon_run(dungeon=dungeon, team=team, wave_limit=wave_limit,
support_character=support_character, skip_ui_switch=True)
return count
elif require and not self.support_once:
# Run with support all the way
return self._dungeon_run(dungeon=dungeon, team=team, wave_limit=0,
support_character=self.config.DungeonSupport_Character)
else:
# Normal run
return self._dungeon_run(dungeon=dungeon, team=team, wave_limit=wave_limit,
support_character=support_character)
def update_double_event_record(self):
"""
Pages:
in: Any
out: page_guide, Survival_Index
"""
# Update double event records
if (self.config.stored.DungeonDouble.is_expired()
or self.config.stored.DungeonDouble.calyx > 0
or self.config.stored.DungeonDouble.relic > 0
or self.config.stored.DungeonDouble.rogue > 0):
update = self.config.stored.DungeonDouble.time
if update <= now() <= update + timedelta(seconds=5):
logger.info('Dungeon double just updated, skip')
return
logger.info('Get dungeon double remains')
# UI switches
switched = self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index)
if not switched and not self._dungeon_survival_index_top_appear():
logger.info('Reset nav states')
# Nav must at top, reset nav states
self.ui_goto_main()
self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index)
# Check remains
calyx = 0
relic = 0
rogue = 0
if self.has_double_rogue_event():
rogue = self.get_double_rogue_remain()
if self.has_double_calyx_event():
self._dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Calyx_Golden)
calyx = self.get_double_event_remain()
if self.has_double_relic_event():
self._dungeon_nav_goto(KEYWORDS_DUNGEON_NAV.Cavern_of_Corrosion)
relic = self.get_double_rogue_remain()
with self.config.multi_set():
self.config.stored.DungeonDouble.calyx = calyx
self.config.stored.DungeonDouble.relic = relic
self.config.stored.DungeonDouble.rogue = rogue
def run(self):
self.sync_config_traiblaze_power('Ornament')
self.config.update_battle_pass_quests()
self.config.update_daily_quests()
self.check_synthesize()
self.called_daily_support = False
self.achieved_daily_quest = False
self.achieved_weekly_quest = False
self.running_double = False
self.daily_quests = self.config.stored.DailyQuest.load_quests()
self.weekly_quests = self.config.stored.BattlePassWeeklyQuest.load_quests()
self.update_double_event_record()
# Run double events
planner = self.planner.get_dungeon(double_calyx=True)
# Double calyx
if self.config.stored.DungeonDouble.calyx > 0:
logger.info('Run double calyx')
dungeon = DungeonList.find(self.config.Dungeon_NameAtDoubleCalyx)
if planner is not None:
dungeon = planner
self.is_doing_planner = True
self.running_double = True
self.dungeon_run(dungeon=dungeon, wave_limit=self.config.stored.DungeonDouble.calyx)
self.is_doing_planner = False
# Double relic
if self.config.stored.DungeonDouble.relic > 0:
logger.info('Run double relic')
dungeon = DungeonList.find(self.config.Dungeon_NameAtDoubleRelic)
self.running_double = True
self.dungeon_run(dungeon=dungeon, wave_limit=self.config.stored.DungeonDouble.relic)
self.running_double = False
# Dungeon to clear all trailblaze power
do_rogue = False
if self.config.is_task_enabled('Rogue') and not self.config.is_task_enabled('Ornament'):
if self.config.cross_get('Rogue.RogueWorld.UseStamina'):
logger.info('Going to use stamina in rogue')
do_rogue = True
elif self.config.cross_get('Rogue.RogueWorld.DoubleEvent') \
and self.config.stored.DungeonDouble.rogue > 0:
logger.info('Going to use stamina in double rogue event')
do_rogue = True
final = DungeonList.find(self.config.Dungeon_Name)
# Planner
planner = self.planner.get_dungeon()
if planner is not None:
final = planner
self.is_doing_planner = True
# Use all stamina
if do_rogue:
# Use support if prioritize rogue
if self.require_compulsory_support():
logger.info('Run dungeon with support once as stamina is rogue prioritized')
self.dungeon_run(dungeon=final, wave_limit=1)
self.is_doing_planner = False
# Store immersifiers
logger.info('Prioritize stamina for simulated universe, skip dungeon')
amount = 0
if not self.config.cross_get('Rogue.RogueWorld.UseStamina') \
and self.config.cross_get('Rogue.RogueWorld.DoubleEvent') \
and self.config.stored.DungeonDouble.rogue > 0:
amount = self.config.stored.DungeonDouble.rogue
stored = self.immersifier_store(max_store=amount)
self.check_stamina_quest(stored * 40)
# call rogue task if accumulated to 4
with self.config.multi_set():
if self.config.stored.Immersifier.value >= 4:
# Schedule behind rogue
self.config.task_delay(minute=5)
self.config.task_call('Rogue')
# Scheduler
self.delay_dungeon_task(KEYWORDS_DUNGEON_LIST.Simulated_Universe_World_1)
self.config.task_stop()
else:
# Combat
self.dungeon_run(final)
self.is_doing_planner = False
# Scheduler
self.delay_dungeon_task(final)
self.check_synthesize()
self.config.task_stop()
def check_synthesize(self):
logger.info('Check synthesize')
synthesize = Synthesize(config=self.config, device=self.device, task=self.config.task.command)
set_cached_property(synthesize, 'planner', self.planner)
if synthesize.synthesize_needed():
synthesize.synthesize_planner()
def delay_dungeon_task(self, dungeon: DungeonList):
logger.attr('achieved_daily_quest', self.achieved_daily_quest)
logger.attr('achieved_weekly_quest', self.achieved_weekly_quest)
with self.config.multi_set():
# Check battle pass
if self.achieved_weekly_quest:
self.config.task_call('BattlePass')
# Check daily
if self.achieved_daily_quest:
self.config.task_call('DailyQuest')
else:
# Check future daily
if KEYWORDS_DAILY_QUEST.Obtain_victory_in_combat_with_Support_Characters_1_times in self.daily_quests:
logger.error("Dungeon ran but support daily haven't been finished yet")
self.config.task_call('DailyQuest')
# Delay tasks
self.dungeon_stamina_delay(dungeon)
def handle_destructible_around_blaze(self):
"""
Stagnant_Shadow_Blaze has a destructible object nearby, attacks are aimed at it first
so destroy it first
Returns:
bool: If handled.
Pages:
in: COMBAT_PREPARE
out: page_main, map position changed if handled
"""
logger.hr('Handle destructible around blaze')
self.combat_exit()
# Check if there's a front sight at bottom-left corner
area = area_offset((-50, -150, 0, 0), offset=self.config.ASSETS_RESOLUTION)
skip_first_screenshot = True
self.map_A_timer.reset()
handled = False
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
if self.image_color_count(area, color=(48, 170, 204), threshold=221, count=50):
logger.info(f'Found destructible object')
if self.handle_map_A():
handled = True
continue
else:
logger.info(f'No destructible object')
if not handled:
break
if self.map_A_timer.reached():
break
return handled
def require_compulsory_support(self) -> bool:
require = False
if not self.config.stored.DailyActivity.is_full():
if KEYWORDS_DAILY_QUEST.Obtain_victory_in_combat_with_Support_Characters_1_times \
in self.daily_quests:
require = True
logger.attr('called_daily_support', self.called_daily_support)
if self.called_daily_support:
require = False
# Not required, cause any dungeon run will achieve the quest
logger.attr('DungeonSupport_Use', self.config.DungeonSupport_Use)
if self.config.DungeonSupport_Use == 'always_use':
require = False
logger.attr('Require compulsory support', require)
return require
def check_stamina_quest(self, stamina_used: int):
logger.info(f'Used {stamina_used} stamina')
if KEYWORDS_BATTLE_PASS_QUEST.Consume_a_total_of_1_Trailblaze_Power_1400_Trailblazer_Power_max in self.weekly_quests:
logger.info(f'Done Consume_a_total_of_1_Trailblaze_Power_1400_Trailblazer_Power_max stamina {stamina_used}')
self.config.stored.BattlePassQuestTrailblazePower.add(stamina_used)
if self.config.stored.BattlePassQuestTrailblazePower.is_full():
logger.info('Achieved weekly quest Consume_a_total_of_1_Trailblaze_Power_1400_Trailblazer_Power_max')
self.achieved_weekly_quest = True
if KEYWORDS_DAILY_QUEST.Consume_120_Trailblaze_Power in self.daily_quests:
logger.info(f'Done Consume_120_Trailblaze_Power stamina {stamina_used}')
self.achieved_daily_quest = True
def sync_config_traiblaze_power(self, set_task):
# Sync Dungeon.TrailblazePower and Ornament.TrailblazePower
with self.config.multi_set():
value = self.config.TrailblazePower_ExtractReservedTrailblazePower
keys = [set_task, 'TrailblazePower', 'ExtractReservedTrailblazePower']
if self.config.cross_get(keys) != value:
self.config.cross_set(keys, value)
value = self.config.TrailblazePower_UseFuel
keys = [set_task, 'TrailblazePower', 'UseFuel']
if self.config.cross_get(keys) != value:
self.config.cross_set(keys, value)
value = self.config.TrailblazePower_FuelReserve
keys = [set_task, 'TrailblazePower', 'FuelReserve']
if self.config.cross_get(keys) != value:
self.config.cross_set(keys, value)