StarRailCopilot/tasks/combat/combat.py

351 lines
12 KiB
Python
Raw Normal View History

2023-07-10 14:23:11 +00:00
from module.base.decorator import run_once
from module.exception import RequestHumanTakeover
2023-06-16 19:15:26 +00:00
from module.logger import logger
from tasks.combat.assets.assets_combat_finish import COMBAT_AGAIN, COMBAT_EXIT
from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE
from tasks.combat.assets.assets_combat_team import COMBAT_TEAM_PREPARE, COMBAT_TEAM_SUPPORT
2023-06-16 19:15:26 +00:00
from tasks.combat.interact import CombatInteract
from tasks.combat.prepare import CombatPrepare
2023-09-23 18:08:36 +00:00
from tasks.combat.skill import CombatSkill
2023-06-16 19:15:26 +00:00
from tasks.combat.state import CombatState
from tasks.combat.support import CombatSupport
from tasks.combat.team import CombatTeam
2023-06-27 18:26:32 +00:00
from tasks.map.control.joystick import MapControlJoystick
2023-06-16 19:15:26 +00:00
2023-09-23 18:08:36 +00:00
class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSupport, CombatSkill, MapControlJoystick):
2023-06-16 19:15:26 +00:00
def handle_combat_prepare(self):
"""
2023-07-10 14:23:11 +00:00
Returns:
bool: If able to run a combat
2023-06-16 19:15:26 +00:00
Pages:
in: COMBAT_PREPARE
"""
2023-07-10 14:23:11 +00:00
self.combat_waves = 1
2023-06-16 19:15:26 +00:00
current = self.combat_get_trailblaze_power()
2023-06-27 15:52:41 +00:00
cost = self.combat_get_wave_cost()
if cost == 10:
2023-07-10 14:23:11 +00:00
# Calyx
self.combat_waves = min(current // self.combat_wave_cost, 6)
if self.combat_wave_limit:
self.combat_waves = min(self.combat_waves, self.combat_wave_limit - self.combat_wave_done)
logger.info(
f'Current has {current}, combat costs {self.combat_wave_cost}, '
f'wave={self.combat_wave_done}/{self.combat_wave_limit}, '
f'able to do {self.combat_waves} waves')
else:
logger.info(f'Current has {current}, combat costs {self.combat_wave_cost}, '
f'able to do {self.combat_waves} waves')
if self.combat_waves > 0:
self.combat_set_wave(self.combat_waves)
2023-06-16 19:15:26 +00:00
else:
2023-07-10 14:23:11 +00:00
# Others
logger.info(f'Current has {current}, combat costs {self.combat_wave_cost}, '
f'do {self.combat_waves} wave')
# Check limits
2023-08-27 16:41:49 +00:00
if self.config.stored.TrailblazePower.value < self.combat_wave_cost:
2023-07-10 14:23:11 +00:00
logger.info('Trailblaze power exhausted, cannot continue combat')
return False
if self.combat_waves <= 0:
logger.info('Combat wave limited, cannot continue combat')
return False
return True
2023-06-16 19:15:26 +00:00
2023-06-27 18:26:32 +00:00
def handle_ascension_dungeon_prepare(self):
"""
Returns:
bool: If clicked.
"""
if self.combat_wave_cost == 30 and self.is_in_main():
2023-06-27 18:26:32 +00:00
if self.handle_map_A():
return True
return False
def combat_prepare(self, team=1, support_character: str = None):
2023-06-16 19:15:26 +00:00
"""
Args:
team: 1 to 6.
support_character: Support character name
Returns:
bool: True if success to enter combat
False if trialblaze power is not enough
2023-06-16 19:15:26 +00:00
Pages:
in: COMBAT_PREPARE
out: is_combat_executing
"""
logger.hr('Combat prepare')
skip_first_screenshot = True
pre_set_team = bool(support_character)
trial = 0
2023-06-16 19:15:26 +00:00
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
# End
if self.is_combat_executing():
return True
# Relics full
# Clicking between COMBAT_PREPARE and COMBAT_TEAM_PREPARE
if trial > 3:
logger.critical('Failed to enter dungeon after 3 trial, probably because relics are full')
raise RequestHumanTakeover
2023-06-16 19:15:26 +00:00
# Click
if self.appear(COMBAT_TEAM_SUPPORT) and support_character:
if pre_set_team:
self.team_set(team)
pre_set_team = False
continue
self.support_set(support_character)
continue
if self.appear(COMBAT_TEAM_PREPARE, interval=2):
self.team_set(team)
self.device.click(COMBAT_TEAM_PREPARE)
self.interval_reset(COMBAT_TEAM_PREPARE)
continue
if self.appear(COMBAT_TEAM_PREPARE):
self.interval_reset(COMBAT_PREPARE)
2023-09-19 17:20:52 +00:00
self.map_A_timer.reset()
2023-06-16 19:15:26 +00:00
if self.appear(COMBAT_PREPARE, interval=2):
2023-07-10 14:23:11 +00:00
if not self.handle_combat_prepare():
return False
2023-06-16 19:15:26 +00:00
self.device.click(COMBAT_PREPARE)
self.interval_reset(COMBAT_PREPARE)
trial += 1
2023-06-16 19:15:26 +00:00
continue
if self.handle_combat_interact():
continue
2023-06-27 18:26:32 +00:00
if self.handle_ascension_dungeon_prepare():
continue
2023-09-26 19:14:12 +00:00
if self.handle_popup_confirm():
continue
2023-06-16 19:15:26 +00:00
def combat_execute(self, expected_end=None):
2023-06-16 19:15:26 +00:00
"""
Args:
expected_end: A function returns bool, True represents end.
2023-06-16 19:15:26 +00:00
Pages:
in: is_combat_executing
out: COMBAT_AGAIN
"""
logger.hr('Combat execute')
skip_first_screenshot = True
is_executing = True
self.combat_state_reset()
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
# End
if callable(expected_end) and expected_end():
logger.info(f'Combat execute ended at {expected_end.__name__}')
break
2023-06-16 19:15:26 +00:00
if self.appear(COMBAT_AGAIN):
2023-09-21 16:23:00 +00:00
logger.info(f'Combat execute ended at {COMBAT_AGAIN}')
break
if self.is_in_main():
logger.info(f'Combat execute ended at page_main')
2023-06-16 19:15:26 +00:00
break
# Daemon
if self.is_combat_executing():
if not is_executing:
logger.info('Combat continues')
self.device.stuck_record_clear()
is_executing = True
else:
is_executing = False
if self.handle_combat_state():
continue
def _combat_can_again(self) -> bool:
"""
Pages:
in: COMBAT_AGAIN
"""
2023-09-26 19:14:12 +00:00
current = self.combat_get_trailblaze_power(expect_reduce=self.combat_wave_cost > 0)
2023-07-10 14:23:11 +00:00
# Wave limit
if self.combat_wave_limit:
if self.combat_wave_done + self.combat_waves > self.combat_wave_limit:
logger.info(f'Combat wave limit: {self.combat_wave_done}/{self.combat_wave_limit}, '
f'can not run again')
return False
# Cost limit
2023-06-27 15:52:41 +00:00
if self.combat_wave_cost == 10:
2023-07-10 14:23:11 +00:00
if current >= self.combat_wave_cost * self.combat_waves:
2023-06-27 15:52:41 +00:00
logger.info(f'Current has {current}, combat costs {self.combat_wave_cost}, can run again')
2023-06-16 19:15:26 +00:00
return True
else:
2023-06-27 15:52:41 +00:00
logger.info(f'Current has {current}, combat costs {self.combat_wave_cost}, can not run again')
2023-06-16 19:15:26 +00:00
return False
2023-09-26 19:14:12 +00:00
elif self.combat_wave_cost <= 0:
logger.info(f'Free combat, combat costs {self.combat_wave_cost}, can not run again')
return False
2023-06-16 19:15:26 +00:00
else:
2023-06-27 15:52:41 +00:00
if current >= self.combat_wave_cost:
logger.info(f'Current has {current}, combat costs {self.combat_wave_cost}, can run again')
return True
else:
logger.info(f'Current has {current}, combat costs {self.combat_wave_cost}, can not run again')
return False
2023-06-16 19:15:26 +00:00
2023-07-10 14:23:11 +00:00
def _combat_should_reenter(self):
"""
Returns:
bool: True to re-enter combat and run with another wave settings
"""
# Wave limit
if self.combat_wave_limit:
if self.combat_wave_done < self.combat_wave_limit:
logger.info(f'Combat wave limit: {self.combat_wave_done}/{self.combat_wave_limit}, '
f'run again with less waves')
return True
else:
return False
# Cost limit
2023-08-27 16:41:49 +00:00
if self.config.stored.TrailblazePower.value >= self.combat_wave_cost:
2023-07-10 14:23:11 +00:00
logger.info('Still having some trailblaze power run with less waves to empty it')
return True
2023-06-16 19:15:26 +00:00
def combat_finish(self) -> bool:
"""
Returns:
bool: True if exit, False if again
Pages:
in: COMBAT_AGAIN
out: page_main if exit
is_combat_executing if again
"""
logger.hr('Combat finish')
2023-07-10 14:23:11 +00:00
@run_once
def add_wave_done():
self.combat_wave_done += self.combat_waves
logger.info(f'Done {self.combat_waves} waves at total')
2023-06-16 19:15:26 +00:00
skip_first_screenshot = True
combat_can_again = None
2023-06-16 19:15:26 +00:00
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
# End
2023-06-27 18:26:32 +00:00
if self.is_in_main():
2023-06-16 19:15:26 +00:00
logger.info('Combat finishes at page_main')
return True
if self.is_combat_executing():
logger.info('Combat finishes at another combat')
return False
# Click
# Game client might slow to response COMBAT_AGAIN clicks
if self.appear(COMBAT_AGAIN, interval=5):
2023-07-10 14:23:11 +00:00
add_wave_done()
# Cache the result of _combat_can_again() as no expected stamina reduce during retry
if combat_can_again is None:
combat_can_again = self._combat_can_again()
if combat_can_again:
2023-06-16 19:15:26 +00:00
self.device.click(COMBAT_AGAIN)
else:
self.device.click(COMBAT_EXIT)
self.interval_reset(COMBAT_AGAIN)
2023-12-25 12:34:44 +00:00
continue
if self.handle_get_light_cone():
continue
2023-06-16 19:15:26 +00:00
def combat_exit(self, skip_first_screenshot=True):
"""
Pages:
in: Any page during combat
out: page_main
"""
logger.info('Combat exit')
self.interval_clear([COMBAT_PREPARE, COMBAT_TEAM_PREPARE, COMBAT_AGAIN])
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
else:
self.device.screenshot()
# End
2023-06-27 18:26:32 +00:00
if self.is_in_main():
break
# Click
if self.handle_ui_close(COMBAT_PREPARE, interval=2):
continue
if self.handle_ui_close(COMBAT_TEAM_PREPARE, interval=2):
continue
if self.appear(COMBAT_AGAIN, interval=2):
logger.info(f'{COMBAT_AGAIN} -> {COMBAT_EXIT}')
self.device.click(COMBAT_EXIT)
continue
if self.handle_get_light_cone():
continue
2023-08-27 16:41:49 +00:00
def is_trailblaze_power_exhausted(self) -> bool:
flag = self.config.stored.TrailblazePower.value < self.combat_wave_cost
logger.attr('TrailblazePowerExhausted', flag)
return flag
def combat(self, team: int = 1, wave_limit: int = 0, support_character: str = None, skip_first_screenshot=True):
2023-06-16 19:15:26 +00:00
"""
Combat until trailblaze power runs out.
Args:
team: 1 to 6.
2023-07-10 14:23:11 +00:00
wave_limit: Limit combat runs, 0 means no limit.
support_character: Support character name
skip_first_screenshot:
2023-06-16 19:15:26 +00:00
2023-07-10 15:36:35 +00:00
Returns:
int: Run count
2023-07-10 15:36:35 +00:00
Raises:
RequestHumanTakeover: If relics are full
2023-06-16 19:15:26 +00:00
Pages:
in: COMBAT_PREPARE
or page_main with DUNGEON_COMBAT_INTERACT
out: page_main
"""
if not skip_first_screenshot:
self.device.screenshot()
2023-07-10 14:23:11 +00:00
self.combat_wave_limit = wave_limit
self.combat_wave_done = 0
2023-06-16 19:15:26 +00:00
while 1:
logger.hr('Combat', level=2)
2023-07-10 14:23:11 +00:00
logger.info(f'Combat, team={team}, wave={self.combat_wave_done}/{self.combat_wave_limit}')
# Prepare
prepare = self.combat_prepare(team, support_character)
if not prepare:
self.combat_exit()
break
# Execute
2023-06-16 19:15:26 +00:00
self.combat_execute()
# Finish
2023-06-16 19:15:26 +00:00
finish = self.combat_finish()
2023-07-10 14:23:11 +00:00
if self._combat_should_reenter():
2023-06-16 19:15:26 +00:00
continue
if finish:
break
2023-07-10 15:36:35 +00:00
2024-01-01 19:54:54 +00:00
logger.attr('combat_wave_done', self.combat_wave_done)
return self.combat_wave_done