Add: Record progress of battle pass quests

This commit is contained in:
LmeSzinc 2024-01-02 00:43:34 +08:00
parent 0028681cb4
commit 4cd2e06849
17 changed files with 455 additions and 20 deletions

View File

@ -113,7 +113,14 @@
},
"BattlePassStorage": {
"BattlePassLevel": {},
"BattlePassWeeklyQuest": {}
"BattlePassWeeklyQuest": {},
"BattlePassSimulatedUniverse": {},
"BattlePassQuestCalyx": {},
"BattlePassQuestEchoOfWar": {},
"BattlePassQuestCredits": {},
"BattlePassQuestSynthesizeConsumables": {},
"BattlePassQuestCavernOfCorrosion": {},
"BattlePassQuestTrailblazePower": {}
}
},
"Assignment": {

View File

@ -910,6 +910,48 @@
"value": {},
"display": "hide",
"stored": "StoredBattlePassWeeklyQuest"
},
"BattlePassSimulatedUniverse": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredBattlePassSimulatedUniverse"
},
"BattlePassQuestCalyx": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredBattlePassQuestCalyx"
},
"BattlePassQuestEchoOfWar": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredBattlePassQuestEchoOfWar"
},
"BattlePassQuestCredits": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredBattlePassQuestCredits"
},
"BattlePassQuestSynthesizeConsumables": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredBattlePassQuestSynthesizeConsumables"
},
"BattlePassQuestCavernOfCorrosion": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredBattlePassQuestCavernOfCorrosion"
},
"BattlePassQuestTrailblazePower": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredBattlePassQuestTrailblazePower"
}
}
},

View File

@ -162,6 +162,21 @@ BattlePassStorage:
color: "#cbe45b"
BattlePassWeeklyQuest:
stored: StoredBattlePassWeeklyQuest
# Quests progress
BattlePassSimulatedUniverse:
stored: StoredBattlePassSimulatedUniverse
BattlePassQuestCalyx:
stored: StoredBattlePassQuestCalyx
BattlePassQuestEchoOfWar:
stored: StoredBattlePassQuestEchoOfWar
BattlePassQuestCredits:
stored: StoredBattlePassQuestCredits
BattlePassQuestSynthesizeConsumables:
stored: StoredBattlePassQuestSynthesizeConsumables
BattlePassQuestCavernOfCorrosion:
stored: StoredBattlePassQuestCavernOfCorrosion
BattlePassQuestTrailblazePower:
stored: StoredBattlePassQuestTrailblazePower
Assignment:
# Options in Name_x will be injected in config updater

View File

@ -165,5 +165,96 @@
},
"order": 0,
"color": "#777777"
},
"BattlePassSimulatedUniverse": {
"name": "BattlePassSimulatedUniverse",
"path": "BattlePass.BattlePassStorage.BattlePassSimulatedUniverse",
"i18n": "BattlePassStorage.BattlePassSimulatedUniverse.name",
"stored": "StoredBattlePassSimulatedUniverse",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 1,
"value": 0
},
"order": 0,
"color": "#777777"
},
"BattlePassQuestCalyx": {
"name": "BattlePassQuestCalyx",
"path": "BattlePass.BattlePassStorage.BattlePassQuestCalyx",
"i18n": "BattlePassStorage.BattlePassQuestCalyx.name",
"stored": "StoredBattlePassQuestCalyx",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 20,
"value": 0
},
"order": 0,
"color": "#777777"
},
"BattlePassQuestEchoOfWar": {
"name": "BattlePassQuestEchoOfWar",
"path": "BattlePass.BattlePassStorage.BattlePassQuestEchoOfWar",
"i18n": "BattlePassStorage.BattlePassQuestEchoOfWar.name",
"stored": "StoredBattlePassQuestEchoOfWar",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 2,
"value": 0
},
"order": 0,
"color": "#777777"
},
"BattlePassQuestCredits": {
"name": "BattlePassQuestCredits",
"path": "BattlePass.BattlePassStorage.BattlePassQuestCredits",
"i18n": "BattlePassStorage.BattlePassQuestCredits.name",
"stored": "StoredBattlePassQuestCredits",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 300000,
"value": 0
},
"order": 0,
"color": "#777777"
},
"BattlePassQuestSynthesizeConsumables": {
"name": "BattlePassQuestSynthesizeConsumables",
"path": "BattlePass.BattlePassStorage.BattlePassQuestSynthesizeConsumables",
"i18n": "BattlePassStorage.BattlePassQuestSynthesizeConsumables.name",
"stored": "StoredBattlePassQuestSynthesizeConsumables",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 10,
"value": 0
},
"order": 0,
"color": "#777777"
},
"BattlePassQuestCavernOfCorrosion": {
"name": "BattlePassQuestCavernOfCorrosion",
"path": "BattlePass.BattlePassStorage.BattlePassQuestCavernOfCorrosion",
"i18n": "BattlePassStorage.BattlePassQuestCavernOfCorrosion.name",
"stored": "StoredBattlePassQuestCavernOfCorrosion",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 8,
"value": 0
},
"order": 0,
"color": "#777777"
},
"BattlePassQuestTrailblazePower": {
"name": "BattlePassQuestTrailblazePower",
"path": "BattlePass.BattlePassStorage.BattlePassQuestTrailblazePower",
"i18n": "BattlePassStorage.BattlePassQuestTrailblazePower.name",
"stored": "StoredBattlePassQuestTrailblazePower",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 0,
"value": 0
},
"order": 0,
"color": "#777777"
}
}

View File

@ -104,6 +104,13 @@ class GeneratedConfig:
# Group `BattlePassStorage`
BattlePassStorage_BattlePassLevel = {}
BattlePassStorage_BattlePassWeeklyQuest = {}
BattlePassStorage_BattlePassSimulatedUniverse = {}
BattlePassStorage_BattlePassQuestCalyx = {}
BattlePassStorage_BattlePassQuestEchoOfWar = {}
BattlePassStorage_BattlePassQuestCredits = {}
BattlePassStorage_BattlePassQuestSynthesizeConsumables = {}
BattlePassStorage_BattlePassQuestCavernOfCorrosion = {}
BattlePassStorage_BattlePassQuestTrailblazePower = {}
# Group `Assignment`
Assignment_Name_1 = 'Nameless_Land_Nameless_People' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm, Legend_of_the_Puppet_Master, The_Wages_of_Humanity

View File

@ -692,6 +692,34 @@
"BattlePassWeeklyQuest": {
"name": "Honor Mission",
"help": ""
},
"BattlePassSimulatedUniverse": {
"name": "Complete Simulated Universe 1 time(s)",
"help": ""
},
"BattlePassQuestCalyx": {
"name": "Clear Calyx 20 time(s)",
"help": ""
},
"BattlePassQuestEchoOfWar": {
"name": "Complete Echo of War 2 time(s)",
"help": ""
},
"BattlePassQuestCredits": {
"name": "Use 300,000 credits",
"help": ""
},
"BattlePassQuestSynthesizeConsumables": {
"name": "Synthesize Consumables 10 time(s)",
"help": ""
},
"BattlePassQuestCavernOfCorrosion": {
"name": "Clear Cavern of Corrosion 8 time(s)",
"help": ""
},
"BattlePassQuestTrailblazePower": {
"name": "Consume a total of 100 Trailblaze Power (1400 Trailblazer Power max)",
"help": ""
}
},
"Assignment": {

View File

@ -692,6 +692,34 @@
"BattlePassWeeklyQuest": {
"name": "Misión de Honor",
"help": ""
},
"BattlePassSimulatedUniverse": {
"name": "Completa el Universo Simulado 1 vez",
"help": ""
},
"BattlePassQuestCalyx": {
"name": "Completa Cáliz 20 vez",
"help": ""
},
"BattlePassQuestEchoOfWar": {
"name": "Completa Ecos de la guerra 2 vez",
"help": ""
},
"BattlePassQuestCredits": {
"name": "Consume 300 000 créditos",
"help": ""
},
"BattlePassQuestSynthesizeConsumables": {
"name": "Sintetiza consumibles 10 veces",
"help": ""
},
"BattlePassQuestCavernOfCorrosion": {
"name": "Completa Caverna de la corrosión 8 veces",
"help": ""
},
"BattlePassQuestTrailblazePower": {
"name": "Consume un total de 100 pts. de Poder trazacaminos (máx. 1400 pts.)",
"help": ""
}
},
"Assignment": {

View File

@ -692,6 +692,34 @@
"BattlePassWeeklyQuest": {
"name": "BattlePassStorage.BattlePassWeeklyQuest.name",
"help": "BattlePassStorage.BattlePassWeeklyQuest.help"
},
"BattlePassSimulatedUniverse": {
"name": "BattlePassStorage.BattlePassSimulatedUniverse.name",
"help": "BattlePassStorage.BattlePassSimulatedUniverse.help"
},
"BattlePassQuestCalyx": {
"name": "BattlePassStorage.BattlePassQuestCalyx.name",
"help": "BattlePassStorage.BattlePassQuestCalyx.help"
},
"BattlePassQuestEchoOfWar": {
"name": "BattlePassStorage.BattlePassQuestEchoOfWar.name",
"help": "BattlePassStorage.BattlePassQuestEchoOfWar.help"
},
"BattlePassQuestCredits": {
"name": "BattlePassStorage.BattlePassQuestCredits.name",
"help": "BattlePassStorage.BattlePassQuestCredits.help"
},
"BattlePassQuestSynthesizeConsumables": {
"name": "BattlePassStorage.BattlePassQuestSynthesizeConsumables.name",
"help": "BattlePassStorage.BattlePassQuestSynthesizeConsumables.help"
},
"BattlePassQuestCavernOfCorrosion": {
"name": "BattlePassStorage.BattlePassQuestCavernOfCorrosion.name",
"help": "BattlePassStorage.BattlePassQuestCavernOfCorrosion.help"
},
"BattlePassQuestTrailblazePower": {
"name": "BattlePassStorage.BattlePassQuestTrailblazePower.name",
"help": "BattlePassStorage.BattlePassQuestTrailblazePower.help"
}
},
"Assignment": {

View File

@ -692,6 +692,34 @@
"BattlePassWeeklyQuest": {
"name": "无名勋礼任务",
"help": ""
},
"BattlePassSimulatedUniverse": {
"name": "完成1次「模拟宇宙」",
"help": ""
},
"BattlePassQuestCalyx": {
"name": "完成20次「拟造花萼」",
"help": ""
},
"BattlePassQuestEchoOfWar": {
"name": "完成2次「历战余响」",
"help": ""
},
"BattlePassQuestCredits": {
"name": "累计消耗30万信用点",
"help": ""
},
"BattlePassQuestSynthesizeConsumables": {
"name": "累计合成消耗品10次",
"help": ""
},
"BattlePassQuestCavernOfCorrosion": {
"name": "完成8次「侵蚀隧洞」",
"help": ""
},
"BattlePassQuestTrailblazePower": {
"name": "累计消耗100点开拓力上限1400点开拓力",
"help": ""
}
},
"Assignment": {

View File

@ -692,6 +692,34 @@
"BattlePassWeeklyQuest": {
"name": "無名勳禮任務",
"help": ""
},
"BattlePassSimulatedUniverse": {
"name": "完成1次「模擬宇宙」",
"help": ""
},
"BattlePassQuestCalyx": {
"name": "完成20次「擬造花萼」",
"help": ""
},
"BattlePassQuestEchoOfWar": {
"name": "完成2次「歷戰餘響」",
"help": ""
},
"BattlePassQuestCredits": {
"name": "累计消耗30万信用点",
"help": ""
},
"BattlePassQuestSynthesizeConsumables": {
"name": "累计合成消耗品10次",
"help": ""
},
"BattlePassQuestCavernOfCorrosion": {
"name": "完成8次「侵蚀隧洞」",
"help": ""
},
"BattlePassQuestTrailblazePower": {
"name": "累積消耗100點開拓力上限1400點開拓力",
"help": ""
}
},
"Assignment": {

View File

@ -351,3 +351,37 @@ class StoredBattlePassWeeklyQuest(StoredCounter, StoredExpiredAt0400):
self.quest7 = quests[6]
except IndexError:
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
# Not exists on client side
# class StoredBattlePassQuestStagnantShadow(StoredCounter):
# FIXED_TOTAL = 8
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]

View File

@ -2,6 +2,13 @@ from module.config.stored.classes import (
StoredAssignment,
StoredBase,
StoredBattlePassLevel,
StoredBattlePassQuestCalyx,
StoredBattlePassQuestCavernOfCorrosion,
StoredBattlePassQuestCredits,
StoredBattlePassQuestEchoOfWar,
StoredBattlePassQuestSynthesizeConsumables,
StoredBattlePassQuestTrailblazePower,
StoredBattlePassSimulatedUniverse,
StoredBattlePassWeeklyQuest,
StoredCounter,
StoredDaily,
@ -30,6 +37,13 @@ class StoredGenerated:
DailyQuest = StoredDaily("DailyQuest.DailyStorage.DailyQuest")
BattlePassLevel = StoredBattlePassLevel("BattlePass.BattlePassStorage.BattlePassLevel")
BattlePassWeeklyQuest = StoredBattlePassWeeklyQuest("BattlePass.BattlePassStorage.BattlePassWeeklyQuest")
BattlePassSimulatedUniverse = StoredBattlePassSimulatedUniverse("BattlePass.BattlePassStorage.BattlePassSimulatedUniverse")
BattlePassQuestCalyx = StoredBattlePassQuestCalyx("BattlePass.BattlePassStorage.BattlePassQuestCalyx")
BattlePassQuestEchoOfWar = StoredBattlePassQuestEchoOfWar("BattlePass.BattlePassStorage.BattlePassQuestEchoOfWar")
BattlePassQuestCredits = StoredBattlePassQuestCredits("BattlePass.BattlePassStorage.BattlePassQuestCredits")
BattlePassQuestSynthesizeConsumables = StoredBattlePassQuestSynthesizeConsumables("BattlePass.BattlePassStorage.BattlePassQuestSynthesizeConsumables")
BattlePassQuestCavernOfCorrosion = StoredBattlePassQuestCavernOfCorrosion("BattlePass.BattlePassStorage.BattlePassQuestCavernOfCorrosion")
BattlePassQuestTrailblazePower = StoredBattlePassQuestTrailblazePower("BattlePass.BattlePassStorage.BattlePassQuestTrailblazePower")
Assignment = StoredAssignment("Assignment.Assignment.Assignment")
Credit = StoredInt("DataUpdate.ItemStorage.Credit")
StallerJade = StoredInt("DataUpdate.ItemStorage.StallerJade")

View File

@ -201,3 +201,17 @@ class Keyword:
# Not found
raise ScriptError(f'Cannot find a {cls.__name__} instance that matches "{name}"')
class KeywordDigitCounter(Keyword):
"""
A fake Keyword class to filter digit counters in ocr results
OcrResultButton.match_keyword will be a str
"""
@classmethod
def find(cls, name, lang: str = None, ignore_punctuation=True):
from module.ocr.ocr import DigitCounter
if DigitCounter.is_format_matched(name):
return name
else:
raise ScriptError

View File

@ -1,7 +1,6 @@
import re
import time
from datetime import timedelta
from typing import Optional
import cv2
import numpy as np
@ -13,13 +12,12 @@ from module.base.decorator import cached_property
from module.base.utils import area_pad, corner2area, crop, extract_white_letters, float2str
from module.exception import ScriptError
from module.logger import logger
from module.ocr.keyword import Keyword
from module.ocr.models import OCR_MODEL, TextSystem
from module.ocr.utils import merge_buttons
class OcrResultButton:
def __init__(self, boxed_result: BoxedResult, matched_keyword: Optional[Keyword]):
def __init__(self, boxed_result: BoxedResult, matched_keyword):
"""
Args:
boxed_result: BoxedResult from ppocr-onnx

View File

@ -74,8 +74,8 @@ def pair_buttons(group1, group2, relative_area):
Pair buttons in group1 with those in group2 in the relative_area.
Args:
group1 (list[OcrResultButton]):
group2 (list[OcrResultButton]):
group1 (list[OcrResultButton], Iterable[OcrResultButton]):
group2 (list[OcrResultButton], Iterable[OcrResultButton]):
relative_area (tuple[int, int, int, int]):
Yields:

View File

@ -1,14 +1,17 @@
import datetime
import re
from dataclasses import dataclass
import numpy as np
from module.base.timer import Timer
from module.base.utils import get_color
from module.config.stored.classes import StoredCounter
from module.config.utils import get_server_next_update
from module.logger.logger import logger
from module.ocr.ocr import Digit, Duration, Ocr
from module.ocr.utils import split_and_pair_buttons
from module.ocr.keyword import KeywordDigitCounter
from module.ocr.ocr import Digit, DigitCounter, Duration, Ocr, OcrResultButton
from module.ocr.utils import pair_buttons
from module.ui.scroll import Scroll
from module.ui.switch import Switch
from tasks.base.assets.assets_base_page import BATTLE_PASS_CHECK, MAIN_GOTO_BATTLE_PASS
@ -67,6 +70,20 @@ class BattlePassQuestOcr(Ocr):
return result
@dataclass
class DataBattlePassQuest:
quest: BattlePassQuest
state: BattlePassQuestState = None
digit: KeywordDigitCounter = ''
def __eq__(self, other):
return self.quest == other.quest
@property
def is_incomplete(self) -> bool:
return self.state == KEYWORD_BATTLE_PASS_QUEST_STATE.Navigate
class BattlePassUI(UI):
MAX_LEVEL = 70
@ -239,8 +256,7 @@ class BattlePassUI(UI):
# Update quests
self.battle_pass_mission_tab_goto(
KEYWORD_BATTLE_PASS_MISSION_TAB.This_Week_Missions)
quests = self.battle_pass_quests_recognition()
self.config.stored.BattlePassWeeklyQuest.write_quests(quests)
self.battle_pass_quests_recognition()
if previous_level == self.MAX_LEVEL:
return previous_level
@ -253,20 +269,32 @@ class BattlePassUI(UI):
self._claim_rewards()
return current_level
def ocr_single_page(self):
def ocr_single_page(self) -> list[DataBattlePassQuest]:
"""
Returns incomplete quests only
"""
logger.hr('Battle pass ocr single page')
ocr = BattlePassQuestOcr(OCR_BATTLE_PASS_QUEST)
results = ocr.matched_ocr(self.device.image, [BattlePassQuest, BattlePassQuestState])
results = ocr.matched_ocr(
self.device.image, keyword_classes=[BattlePassQuest, BattlePassQuestState, KeywordDigitCounter])
def completed_state(state):
return state != KEYWORD_BATTLE_PASS_QUEST_STATE.Navigate
# Product DataBattlePassQuest objects
data_quest: dict[OcrResultButton, DataBattlePassQuest] = {
result: DataBattlePassQuest(result.matched_keyword)
for result in results if isinstance(result.matched_keyword, BattlePassQuest)
}
# Update quest state
list_attr = [result for result in results if isinstance(result.matched_keyword, BattlePassQuestState)]
for quest, state in pair_buttons(data_quest, list_attr, relative_area=(0, 0, 800, 100)):
data_quest[quest].state = state.matched_keyword
# Update quest progress
list_attr = [result for result in results if isinstance(result.matched_keyword, str)]
for quest, digit in pair_buttons(data_quest, list_attr, relative_area=(-50, 0, 200, 70)):
data_quest[quest].digit = digit.matched_keyword
return [incomplete_quest for incomplete_quest, _ in
split_and_pair_buttons(results, split_func=completed_state, relative_area=(0, 0, 800, 100))]
return list(data_quest.values())
def battle_pass_quests_recognition(self) -> list[BattlePassQuest]:
def battle_pass_quests_recognition(self):
"""
Pages:
in: page_battle_pass, KEYWORD_BATTLE_PASS_TAB.Missions, weekly or period
@ -275,7 +303,7 @@ class BattlePassUI(UI):
scroll = Scroll(MISSION_PAGE_SCROLL, color=(198, 198, 198))
scroll.set_top(main=self)
results = []
results: list[DataBattlePassQuest] = []
while 1:
results += [result for result in self.ocr_single_page() if result not in results]
if scroll.at_bottom(main=self):
@ -284,8 +312,50 @@ class BattlePassUI(UI):
else:
scroll.next_page(main=self)
results = [result.matched_keyword for result in results]
return results
# Convert quest keyword to stored object
dic_quest_to_stored = {
KEYWORD_BATTLE_PASS_QUEST.Complete_Simulated_Universe_1_times:
self.config.stored.BattlePassSimulatedUniverse,
KEYWORD_BATTLE_PASS_QUEST.Clear_Calyx_1_times:
self.config.stored.BattlePassQuestCalyx,
KEYWORD_BATTLE_PASS_QUEST.Complete_Echo_of_War_1_times:
self.config.stored.BattlePassQuestEchoOfWar,
KEYWORD_BATTLE_PASS_QUEST.Use_300000_credits:
self.config.stored.BattlePassQuestCredits,
KEYWORD_BATTLE_PASS_QUEST.Synthesize_Consumables_1_times:
self.config.stored.BattlePassQuestSynthesizeConsumables,
KEYWORD_BATTLE_PASS_QUEST.Clear_Cavern_of_Corrosion_1_times:
self.config.stored.BattlePassQuestCavernOfCorrosion,
KEYWORD_BATTLE_PASS_QUEST.Consume_a_total_of_1_Trailblaze_Power_1400_Trailblazer_Power_max:
self.config.stored.BattlePassQuestTrailblazePower,
}
with self.config.multi_set():
# Write incomplete quests
quests = [result.quest for result in results if result.is_incomplete]
self.config.stored.BattlePassWeeklyQuest.write_quests(quests)
# Create an OCR object just for calling format_result()
ocr = DigitCounter(OCR_BATTLE_PASS_QUEST)
# Write quest progresses
for result in results:
ocr.name = result.quest.name
current, _, total = ocr.format_result(result.digit)
if total == 0:
logger.error(f'Battle pass quests {result.quest} progress invalid: {result.digit}')
continue
stored: StoredCounter = dic_quest_to_stored.get(result.quest, None)
# Check if exist
if stored is None:
logger.error(f'Battle pass quest {result.quest} has not corresponding stored object')
continue
# Check total
if stored.FIXED_TOTAL and total != stored.FIXED_TOTAL:
logger.error(f'Battle pass quest progress {current}/{total} does not match its stored object')
continue
if hasattr(stored, 'LIST_TOTAL') and total not in stored.LIST_TOTAL:
logger.error(f'Battle pass quest progress {current}/{total} is not in LIST_TOTAL')
continue
# Set
stored.set(current, total=total)
def has_battle_pass_entrance(self, skip_first_screenshot=True):
"""

View File

@ -26,6 +26,9 @@ class BattlePassQuest(Keyword):
return remove_digit(name) == remove_digit(keyword)
def __hash__(self) -> int:
return super().__hash__()
@dataclass(repr=False)
class BattlePassQuestState(Keyword):