mirror of
https://github.com/LmeSzinc/StarRailCopilot.git
synced 2024-12-12 07:29:03 +00:00
453 lines
12 KiB
Python
453 lines
12 KiB
Python
from datetime import datetime
|
|
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
|
|
|
|
|
|
def now():
|
|
return datetime.now().replace(microsecond=0)
|
|
|
|
|
|
def iter_attribute(cls):
|
|
"""
|
|
Args:
|
|
cls: Class or object
|
|
|
|
Yields:
|
|
str, obj: Attribute name, attribute value
|
|
"""
|
|
for attr in dir(cls):
|
|
if attr.startswith('_'):
|
|
continue
|
|
value = getattr(cls, attr)
|
|
if type(value).__name__ in ['function', 'property']:
|
|
continue
|
|
yield attr, value
|
|
|
|
|
|
class StoredBase:
|
|
time = DEFAULT_TIME
|
|
|
|
def __init__(self, key):
|
|
self._key = key
|
|
self._config = None
|
|
|
|
@cached_property
|
|
def _name(self):
|
|
return self._key.split('.')[-1]
|
|
|
|
def _bind(self, config):
|
|
"""
|
|
Args:
|
|
config (AzurLaneConfig):
|
|
"""
|
|
self._config = config
|
|
|
|
@functools_cached_property
|
|
def _stored(self):
|
|
assert self._config is not None, 'StoredBase._bind() must be called before getting stored data'
|
|
from module.logger import logger
|
|
|
|
out = {}
|
|
stored = deep_get(self._config.data, keys=self._key, default={})
|
|
for attr, default in self._attrs.items():
|
|
value = stored.get(attr, default)
|
|
if attr == 'time':
|
|
if not isinstance(value, datetime):
|
|
try:
|
|
value = datetime.fromisoformat(value)
|
|
except ValueError:
|
|
logger.warning(f'{self._name} has invalid attr: {attr}={value}, use default={default}')
|
|
value = default
|
|
else:
|
|
if not isinstance(value, type(default)):
|
|
logger.warning(f'{self._name} has invalid attr: {attr}={value}, use default={default}')
|
|
value = default
|
|
|
|
out[attr] = value
|
|
return out
|
|
|
|
@cached_property
|
|
def _attrs(self) -> dict:
|
|
"""
|
|
All attributes defined
|
|
"""
|
|
attrs = {
|
|
# time is the first one
|
|
'time': DEFAULT_TIME
|
|
}
|
|
for attr, value in iter_attribute(self.__class__):
|
|
if attr.islower():
|
|
attrs[attr] = value
|
|
return attrs
|
|
|
|
def __setattr__(self, key, value):
|
|
if key in self._attrs:
|
|
stored = self._stored
|
|
stored['time'] = now()
|
|
stored[key] = value
|
|
self._config.modified[self._key] = stored
|
|
if self._config.auto_update:
|
|
self._config.update()
|
|
else:
|
|
super().__setattr__(key, value)
|
|
|
|
def __getattribute__(self, item):
|
|
if not item.startswith('_') and item in self._attrs:
|
|
return self._stored[item]
|
|
else:
|
|
return super().__getattribute__(item)
|
|
|
|
def is_expired(self) -> bool:
|
|
return False
|
|
|
|
def show(self):
|
|
"""
|
|
Log self
|
|
"""
|
|
from module.logger import logger
|
|
logger.attr(self._name, self._stored)
|
|
|
|
|
|
class StoredExpiredAt0400(StoredBase):
|
|
def is_expired(self):
|
|
from module.logger import logger
|
|
self.show()
|
|
expired = self.time < get_server_last_update('04:00')
|
|
logger.attr(f'{self._name} expired', expired)
|
|
return expired
|
|
|
|
|
|
class StoredExpiredAtMonday0400(StoredBase):
|
|
def is_expired(self):
|
|
from module.logger import logger
|
|
self.show()
|
|
expired = self.time < get_server_last_monday_update('04:00')
|
|
logger.attr(f'{self._name} expired', expired)
|
|
return expired
|
|
|
|
|
|
class StoredInt(StoredBase):
|
|
value = 0
|
|
|
|
def clear(self):
|
|
self.value = 0
|
|
|
|
|
|
class StoredCounter(StoredBase):
|
|
value = 0
|
|
total = 0
|
|
|
|
FIXED_TOTAL = 0
|
|
|
|
def set(self, value, total=0):
|
|
if self.FIXED_TOTAL:
|
|
total = self.FIXED_TOTAL
|
|
with self._config.multi_set():
|
|
self.value = value
|
|
self.total = total
|
|
|
|
def clear(self):
|
|
self.value = 0
|
|
|
|
def to_counter(self) -> str:
|
|
return f'{self.value}/{self.total}'
|
|
|
|
def is_full(self) -> bool:
|
|
return self.value >= self.total
|
|
|
|
def get_remain(self) -> int:
|
|
return self.total - self.value
|
|
|
|
def add(self, value=1):
|
|
self.value += value
|
|
|
|
@cached_property
|
|
def _attrs(self) -> dict:
|
|
attrs = super()._attrs
|
|
if self.FIXED_TOTAL:
|
|
attrs['total'] = self.FIXED_TOTAL
|
|
return attrs
|
|
|
|
@functools_cached_property
|
|
def _stored(self):
|
|
stored = super()._stored
|
|
if self.FIXED_TOTAL:
|
|
stored['total'] = self.FIXED_TOTAL
|
|
return stored
|
|
|
|
|
|
class StoredDailyActivity(StoredCounter, StoredExpiredAt0400):
|
|
FIXED_TOTAL = 500
|
|
|
|
|
|
class StoredTrailblazePower(StoredCounter):
|
|
FIXED_TOTAL = 240
|
|
|
|
def predict_current(self) -> int:
|
|
"""
|
|
Predict current stamina from records
|
|
"""
|
|
# Overflowed
|
|
value = self.value
|
|
if value >= self.FIXED_TOTAL:
|
|
return value
|
|
# Invalid time, record in the future
|
|
record = self.time
|
|
now = datetime.now()
|
|
if record >= now:
|
|
return value
|
|
# Calculate
|
|
# Recover 1 trailbaze power each 6 minutes
|
|
diff = (now - record).total_seconds()
|
|
value += int(diff // 360)
|
|
return value
|
|
|
|
|
|
class StoredResersed(StoredCounter):
|
|
FIXED_TOTAL = 2400
|
|
|
|
|
|
class StoredImmersifier(StoredCounter):
|
|
FIXED_TOTAL = 8
|
|
|
|
|
|
class StoredSimulatedUniverse(StoredCounter, StoredExpiredAtMonday0400):
|
|
pass
|
|
|
|
|
|
class StoredSimulatedUniverseElite(StoredCounter, StoredExpiredAtMonday0400):
|
|
# These variables are used in Rogue Farming feature.
|
|
|
|
# FIXED_TOTAL --- Times of boss drop chance per week. In current version of StarRail, this value is 100.
|
|
FIXED_TOTAL = 100
|
|
|
|
# value --- Times left to farm. Resets to 100 every Monday 04:00, and decreases each time the elite boss is cleared.
|
|
|
|
|
|
class StoredAssignment(StoredCounter):
|
|
pass
|
|
|
|
|
|
class StoredDaily(StoredCounter, StoredExpiredAt0400):
|
|
quest1 = ''
|
|
quest2 = ''
|
|
quest3 = ''
|
|
quest4 = ''
|
|
quest5 = ''
|
|
quest6 = ''
|
|
quest7 = ''
|
|
quest8 = ''
|
|
|
|
FIXED_TOTAL = 8
|
|
|
|
def load_quests(self):
|
|
"""
|
|
Returns:
|
|
list[DailyQuest]: Note that must check if quests are expired
|
|
"""
|
|
# DailyQuest should be lazy loaded
|
|
from tasks.daily.keywords import DailyQuest
|
|
quests = []
|
|
for name in [self.quest1, self.quest2, self.quest3, self.quest4,
|
|
self.quest5, self.quest6, self.quest7, self.quest8]:
|
|
if not name:
|
|
continue
|
|
try:
|
|
quest = DailyQuest.find(name)
|
|
quests.append(quest)
|
|
except ScriptError:
|
|
pass
|
|
return quests
|
|
|
|
def write_quests(self, quests):
|
|
"""
|
|
Args:
|
|
quests (list[DailyQuest, str]):
|
|
"""
|
|
from tasks.daily.keywords import DailyQuest
|
|
quests = [q.name if isinstance(q, DailyQuest) else q for q in quests]
|
|
with self._config.multi_set():
|
|
self.set(value=max(self.FIXED_TOTAL - len(quests), 0))
|
|
try:
|
|
self.quest1 = quests[0]
|
|
except IndexError:
|
|
self.quest1 = ''
|
|
try:
|
|
self.quest2 = quests[1]
|
|
except IndexError:
|
|
self.quest2 = ''
|
|
try:
|
|
self.quest3 = quests[2]
|
|
except IndexError:
|
|
self.quest3 = ''
|
|
try:
|
|
self.quest4 = quests[3]
|
|
except IndexError:
|
|
self.quest4 = ''
|
|
try:
|
|
self.quest5 = quests[4]
|
|
except IndexError:
|
|
self.quest5 = ''
|
|
try:
|
|
self.quest6 = quests[5]
|
|
except IndexError:
|
|
self.quest6 = ''
|
|
try:
|
|
self.quest7 = quests[6]
|
|
except IndexError:
|
|
self.quest7 = ''
|
|
try:
|
|
self.quest8 = quests[7]
|
|
except IndexError:
|
|
self.quest8 = ''
|
|
|
|
def clear(self):
|
|
with self._config.multi_set():
|
|
self.quest1 = ''
|
|
self.quest2 = ''
|
|
self.quest3 = ''
|
|
self.quest4 = ''
|
|
self.quest5 = ''
|
|
self.quest6 = ''
|
|
self.quest7 = ''
|
|
self.quest8 = ''
|
|
|
|
|
|
class StoredDungeonDouble(StoredExpiredAt0400):
|
|
calyx = 0
|
|
relic = 0
|
|
rogue = 0
|
|
|
|
|
|
class StoredEchoOfWar(StoredCounter, StoredExpiredAtMonday0400):
|
|
FIXED_TOTAL = 3
|
|
|
|
|
|
class StoredBattlePassLevel(StoredCounter):
|
|
FIXED_TOTAL = 70
|
|
|
|
|
|
class StoredBattlePassWeeklyQuest(StoredCounter, StoredExpiredAtMonday0400):
|
|
quest1 = ''
|
|
quest2 = ''
|
|
quest3 = ''
|
|
quest4 = ''
|
|
quest5 = ''
|
|
quest6 = ''
|
|
quest7 = ''
|
|
|
|
FIXED_TOTAL = 7
|
|
|
|
def load_quests(self):
|
|
"""
|
|
Returns:
|
|
list[DailyQuest]: Note that must check if quests are expired
|
|
"""
|
|
# BattlePassQuest should be lazy loaded
|
|
from tasks.battle_pass.keywords import BattlePassQuest
|
|
quests = []
|
|
for name in [self.quest1, self.quest2, self.quest3, self.quest4, self.quest5, self.quest6, self.quest7]:
|
|
if not name:
|
|
continue
|
|
try:
|
|
quest = BattlePassQuest.find(name)
|
|
quests.append(quest)
|
|
except ScriptError:
|
|
pass
|
|
return quests
|
|
|
|
def write_quests(self, quests):
|
|
"""
|
|
Args:
|
|
quests (list[DailyQuest, str]):
|
|
"""
|
|
from tasks.battle_pass.keywords import BattlePassQuest
|
|
quests = [q.name if isinstance(q, BattlePassQuest) else q for q in quests]
|
|
with self._config.multi_set():
|
|
self.set(value=max(self.FIXED_TOTAL - len(quests), 0))
|
|
try:
|
|
self.quest1 = quests[0]
|
|
except IndexError:
|
|
self.quest1 = ''
|
|
try:
|
|
self.quest2 = quests[1]
|
|
except IndexError:
|
|
self.quest2 = ''
|
|
try:
|
|
self.quest3 = quests[2]
|
|
except IndexError:
|
|
self.quest3 = ''
|
|
try:
|
|
self.quest4 = quests[3]
|
|
except IndexError:
|
|
self.quest4 = ''
|
|
try:
|
|
self.quest5 = quests[4]
|
|
except IndexError:
|
|
self.quest5 = ''
|
|
try:
|
|
self.quest6 = quests[5]
|
|
except IndexError:
|
|
self.quest6 = ''
|
|
try:
|
|
self.quest7 = quests[6]
|
|
except IndexError:
|
|
self.quest7 = ''
|
|
|
|
def clear(self):
|
|
with self._config.multi_set():
|
|
self.quest1 = ''
|
|
self.quest2 = ''
|
|
self.quest3 = ''
|
|
self.quest4 = ''
|
|
self.quest5 = ''
|
|
self.quest6 = ''
|
|
self.quest7 = ''
|
|
|
|
|
|
class StoredBattlePassSimulatedUniverse(StoredCounter):
|
|
FIXED_TOTAL = 1
|
|
|
|
|
|
class StoredBattlePassQuestCalyx(StoredCounter):
|
|
FIXED_TOTAL = 20
|
|
|
|
|
|
class StoredBattlePassQuestEchoOfWar(StoredCounter):
|
|
FIXED_TOTAL = 2
|
|
|
|
|
|
class StoredBattlePassQuestCredits(StoredCounter):
|
|
FIXED_TOTAL = 300000
|
|
|
|
|
|
class StoredBattlePassQuestSynthesizeConsumables(StoredCounter):
|
|
FIXED_TOTAL = 10
|
|
|
|
|
|
class StoredBattlePassQuestStagnantShadow(StoredCounter):
|
|
FIXED_TOTAL = 3
|
|
|
|
|
|
class StoredBattlePassQuestCavernOfCorrosion(StoredCounter):
|
|
FIXED_TOTAL = 8
|
|
|
|
|
|
class StoredBattlePassQuestTrailblazePower(StoredCounter):
|
|
# Dynamic total from 100 to 1400
|
|
LIST_TOTAL = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400]
|
|
|
|
|
|
class StoredPlanner(StoredBase):
|
|
value: int
|
|
total: int
|
|
synthesize: int
|
|
|
|
|
|
class StoredPlannerOverall(StoredBase):
|
|
value: str = '??%'
|
|
comment: str = '<??d'
|