Merge pull request #141 from Zebartin/rogue-dev

Add: Ocr and select rogue event options
This commit is contained in:
LmeSzinc 2023-10-10 00:13:35 +08:00 committed by GitHub
commit d028023523
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 5581 additions and 38 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,6 +1,8 @@
import itertools
import os
import re
import typing as t
from collections import defaultdict
from functools import cached_property
from module.base.code_generator import CodeGenerator
@ -13,9 +15,9 @@ UI_LANGUAGES = ['cn', 'cht', 'en', 'jp', 'es']
def text_to_variable(text):
text = re.sub("'s |s' ", '_', text)
text = re.sub('[ \-—:\'/•.]+', '_', text)
text = re.sub(r'[(),#"?!&]|</?\w+>', '', text)
text = re.sub(r'[(),#"?!&%*]|</?\w+>', '', text)
# text = re.sub(r'[#_]?\d+(_times?)?', '', text)
return text
return text.strip('_')
def dungeon_name(name: str) -> str:
@ -50,6 +52,11 @@ class TextMap:
def __init__(self, lang: str):
self.lang = lang
def __contains__(self, name: t.Union[int, str]) -> bool:
if isinstance(name, int) or (isinstance(name, str) and name.isdigit()):
return int(name) in self.data
return False
@cached_property
def data(self) -> dict[int, str]:
if not os.path.exists(TextMap.DATA_FOLDER):
@ -252,7 +259,7 @@ class KeywordExtract:
gen.write(output_file)
self.clear_keywords()
def generate_assignment_keywords(self):
def generate_assignments(self):
self.load_keywords(['空间站特派'])
self.write_keywords(
keyword_class='AssignmentEventGroup',
@ -347,6 +354,111 @@ class KeywordExtract:
self.write_keywords(keyword_class='RogueResonance', output_file='./tasks/rogue/keywords/resonance.py',
text_convert=blessing_name, extra_attrs=extra_attrs)
def generate_rogue_events(self):
# A talk contains several options
event_title_file = os.path.join(
TextMap.DATA_FOLDER, 'ExcelOutput',
'RogueTalkNameConfig.json'
)
event_title_ids = {
id_: deep_get(data, 'Name.Hash')
for id_, data in read_file(event_title_file).items()
}
event_title_texts = defaultdict(list)
for title_id, title_hash in event_title_ids.items():
if title_hash not in self.text_map['en']:
continue
_, title_text = self.find_keyword(title_hash, lang='en')
event_title_texts[text_to_variable(title_text)].append(title_id)
option_file = os.path.join(
TextMap.DATA_FOLDER, 'ExcelOutput',
'DialogueEventDisplay.json'
)
option_ids = {
id_: deep_get(data, 'EventTitle.Hash')
for id_, data in read_file(option_file).items()
}
# Key: event name id, value: list of option id/hash
options_grouped = dict()
# Drop invalid or duplicate options
def clean_options(options):
visited = set()
for i in options:
option_hash = option_ids[str(i)]
if option_hash not in self.text_map['en']:
continue
_, option_text = self.find_keyword(option_hash, lang='en')
if option_text in visited:
continue
visited.add(option_text)
yield option_hash
for group_title_ids in event_title_texts.values():
group_option_ids = []
for title_id in group_title_ids:
# Special case for Nildis (尼尔迪斯牌)
# Missing option: Give up
if title_id == '13501':
group_option_ids.append(13506)
option_id = title_id
# Name ids in Swarm Disaster (寰宇蝗灾) have a "1" prefix
if option_id not in option_ids:
option_id = title_id[1:]
# Some name may not has corresponding options
if option_id not in option_ids:
continue
group_option_ids += list(itertools.takewhile(
lambda x: str(x) in option_ids,
itertools.count(int(option_id))
))
if group_option_ids:
options_grouped[group_title_ids[0]] = group_option_ids
for title_id, options in options_grouped.items():
options_grouped[title_id] = list(clean_options(options))
for title_id in list(options_grouped.keys()):
if len(options_grouped[title_id]) == 0:
options_grouped.pop(title_id)
option_dup_count = defaultdict(int)
for option_hash_list in options_grouped.values():
for option_hash in option_hash_list:
if option_hash not in self.text_map['en']:
continue
_, option_text = self.find_keyword(option_hash, lang='en')
option_dup_count[text_to_variable(option_text)] += 1
def option_text_convert(title_index):
def wrapper(option_text):
option_var = text_to_variable(option_text)
if option_dup_count[option_var] > 1:
option_var = f'{option_var}_{title_index}'
return option_var
return wrapper
option_gen = None
last_id = 1
option_id_map = dict()
for i, (title_id, option_ids) in enumerate(options_grouped.items(), start=1):
self.load_keywords(option_ids)
option_gen = self.write_keywords(
keyword_class='RogueEventOption',
text_convert=option_text_convert(i),
generator=option_gen
)
cur_id = option_gen.last_id + 1
option_id_map[event_title_ids[title_id]] = list(
range(last_id, cur_id))
last_id = cur_id
output_file = './tasks/rogue/keywords/event_option.py'
print(f'Write {output_file}')
option_gen.write(output_file)
self.load_keywords([event_title_ids[x] for x in options_grouped])
self.write_keywords(
keyword_class='RogueEventTitle',
output_file='./tasks/rogue/keywords/event_title.py',
extra_attrs={'option_ids': option_id_map}
)
def iter_without_duplication(self, file: dict, keys):
visited = set()
for data in file.values():
@ -377,7 +489,7 @@ class KeywordExtract:
self.load_keywords(['本日任务', '本周任务', '本期任务'])
self.write_keywords(keyword_class='BattlePassMissionTab',
output_file='./tasks/battle_pass/keywords/mission_tab.py')
self.generate_assignment_keywords()
self.generate_assignments()
self.generate_forgotten_hall_stages()
self.generate_map_planes()
self.generate_character_keywords()
@ -396,6 +508,7 @@ class KeywordExtract:
self.load_keywords(list(self.iter_without_duplication(
read_file(os.path.join(TextMap.DATA_FOLDER, 'ExcelOutput', 'RogueBonus.json')), 'BonusTitle.Hash')))
self.write_keywords(keyword_class='RogueBonus', output_file='./tasks/rogue/keywords/bonus.py')
self.generate_rogue_events()
if __name__ == '__main__':

View File

@ -6,12 +6,13 @@ from typing import ClassVar
import module.config.server as server
from module.exception import ScriptError
REGEX_PUNCTUATION = re.compile(r'[ ,.\'"“”,。:;!??·•\-—/\\\n\t()\[\]()「」『』【】《》[]]')
# ord('') = 65294
REGEX_PUNCTUATION = re.compile(r'[ ,.\'"“”,。:;!??·•\-—/\\\n\t()\[\]()「」『』【】《》[]]')
def parse_name(n):
n = REGEX_PUNCTUATION.sub('', str(n)).lower()
return n
return n.strip()
@dataclass

View File

@ -33,13 +33,43 @@ CHOOSE_STORY = ButtonWrapper(
button=(848, 488, 1155, 525),
),
)
OCR_EVENT = ButtonWrapper(
name='OCR_EVENT',
OCR_OPTION = ButtonWrapper(
name='OCR_OPTION',
share=Button(
file='./assets/share/rogue/event/OCR_EVENT.png',
file='./assets/share/rogue/event/OCR_OPTION.png',
area=(789, 71, 1204, 647),
search=(769, 51, 1224, 667),
color=(25, 25, 25),
button=(789, 71, 1204, 647),
),
)
OCR_TITLE = ButtonWrapper(
name='OCR_TITLE',
share=Button(
file='./assets/share/rogue/event/OCR_TITLE.png',
area=(157, 639, 721, 689),
search=(137, 619, 741, 709),
color=(37, 35, 33),
button=(157, 639, 721, 689),
),
)
OPTION_SCROLL = ButtonWrapper(
name='OPTION_SCROLL',
share=Button(
file='./assets/share/rogue/event/OPTION_SCROLL.png',
area=(1214, 72, 1217, 648),
search=(1194, 52, 1237, 668),
color=(192, 171, 129),
button=(1214, 72, 1217, 648),
),
)
REST_AREA = ButtonWrapper(
name='REST_AREA',
share=Button(
file='./assets/share/rogue/event/REST_AREA.png',
area=(338, 222, 498, 382),
search=(318, 202, 518, 402),
color=(140, 127, 184),
button=(338, 222, 498, 382),
),
)

View File

@ -70,30 +70,28 @@ class RogueBuffOcr(Ocr):
"云[摘销锅]?逐步离": "云镝逐步离",
"制桑": "制穹桑",
"乌号基": "乌号綦",
"追摩物": "追孽物",
"流岚追摩?": "流岚追孽物",
"特月": "狩月",
"彤弓素.*": "彤弓素矰",
"白决射御": "白矢决射御",
"苦表": "苦衷",
"[沦沧]肌髓": "沦浃肌髓",
"[沦沧][決]?肌髓": "沦浃肌髓",
"进发": "迸发",
"永缩体": "永坍缩体",
"完美体验:绒默": "完美体验:缄默",
"灭回归不等式": "湮灭回归不等式",
"[涯]?灭回归不等式": "湮灭回归不等式",
r".*灾$": "禳灾",
"虚安供品": "虚妄供品",
"原初的苦$": "原初的苦衷",
"厌离邪": "厌离邪秽苦",
"厌离邪[移]?": "厌离邪秽苦",
r".*繁.*": "葳蕤繁祉,延彼遐龄",
}
for pat, replace in replace_pattern_dict.items():
result = re.sub(pat, replace, result)
elif self.lang == 'en':
replace_pattern_dict = {
"RestIin": "Restin",
}
for pat, replace in replace_pattern_dict.items():
result = re.sub(pat, replace, result)
for pat, replace in replace_pattern_dict.items():
result = re.sub(pat, replace, result)
return result

View File

@ -1,12 +1,158 @@
import random
import re
from dataclasses import dataclass
from functools import cached_property
from itertools import chain
from pponnxcr.predict_system import BoxedResult
from module.base.button import ClickButton
from module.base.utils import area_limit
from module.base.decorator import del_cached_property
from module.base.utils import area_limit, area_offset
from module.logger import logger
from tasks.rogue.assets.assets_rogue_event import CHOOSE_OPTION, CHOOSE_OPTION_CONFIRM, CHOOSE_STORY, OCR_EVENT
from module.ocr.ocr import Ocr, OcrResultButton
from module.ui.scroll import Scroll
from tasks.rogue.assets.assets_rogue_event import *
from tasks.rogue.assets.assets_rogue_ui import BLESSING_CONFIRM, PAGE_EVENT
from tasks.rogue.bleesing.ui import RogueUI
from tasks.rogue.event.preset import STRATEGIES, STRATEGY_COMMON
from tasks.rogue.keywords import (KEYWORDS_ROGUE_EVENT_OPTION,
KEYWORDS_ROGUE_EVENT_TITLE, RogueEventOption,
RogueEventTitle)
@dataclass
class OptionButton:
prefix_icon: ClickButton
button: OcrResultButton = None
is_valid: bool = True # Option with requirements might be disabled
is_bottom_page: bool = False
def __str__(self) -> str:
if self.button is not None:
return str(self.button.matched_keyword)
return super().__str__()
class OcrRogueEvent(Ocr):
merge_thres_y = 5
OCR_REPLACE = {
'cn': [],
'en': []
}
@cached_property
def ocr_regex(self) -> re.Pattern | None:
rules = self.OCR_REPLACE.get(self.lang)
if not rules:
return None
return re.compile('|'.join(
f'(?P<{kw.name}>{pat})'
for kw, pat in rules
))
def _after_process(self, result, keyword_class):
result = super().after_process(result)
if self.ocr_regex is None:
return result
matched = self.ocr_regex.fullmatch(result)
if matched is None:
return result
matched = keyword_class.find(matched.lastgroup)
matched = getattr(matched, self.lang)
return matched
class OcrRogueEventTitle(OcrRogueEvent):
OCR_REPLACE = {
'cn': [
(KEYWORDS_ROGUE_EVENT_TITLE.Rock_Paper_Scissors, '^猜拳.*'),
(KEYWORDS_ROGUE_EVENT_TITLE.Ka_ching_IPC_Banking_Part_1, '^咔.*其一.*'),
(KEYWORDS_ROGUE_EVENT_TITLE.Ka_ching_IPC_Banking_Part_2, '^咔.*其二.*'),
(KEYWORDS_ROGUE_EVENT_TITLE.Beast_Horde_Voracious_Catastrophe, '^兽群.*'),
],
'en': [
(KEYWORDS_ROGUE_EVENT_TITLE.Nomadic_Miners, '^Nomadic.*'),
(KEYWORDS_ROGUE_EVENT_TITLE.Nildis, '^Nildis.*'),
(KEYWORDS_ROGUE_EVENT_TITLE.Tavern, '^Tavern.*'),
(KEYWORDS_ROGUE_EVENT_TITLE.Insights_from_the_Universal_Dancer, '.*Dancer$'),
]
}
def after_process(self, result):
result = re.sub('卫[成戌]', '卫戍', result)
return self._after_process(result, RogueEventTitle)
class OcrRogueEventOption(OcrRogueEvent):
expected_options: list[OptionButton] = []
OCR_REPLACE = {
'cn': [
# Special cases with placeholder
(KEYWORDS_ROGUE_EVENT_OPTION.Deposit_2_Cosmic_Fragments_91, '存入\d+.*'),
(KEYWORDS_ROGUE_EVENT_OPTION.Withdraw_2_Cosmic_Fragments_91, '取出\d+.*'),
(KEYWORDS_ROGUE_EVENT_OPTION.Record_of_the_Aeon_of_1, '^关于.*'),
(KEYWORDS_ROGUE_EVENT_OPTION.I_ll_buy_it, '我买下?了'),
(KEYWORDS_ROGUE_EVENT_OPTION.Wait_for_them, '^等待.*'),
(KEYWORDS_ROGUE_EVENT_OPTION.Choose_number_two_It_snores_like_Andatur_Zazzalo, '.*二号.*安达.*'),
(KEYWORDS_ROGUE_EVENT_OPTION.Choose_number_three_Its_teeth_are_rusted, '.*三号.*牙齿.*'),
],
'en': [
(KEYWORDS_ROGUE_EVENT_OPTION.Deposit_2_Cosmic_Fragments_91, 'Deposit \d+.*'),
(KEYWORDS_ROGUE_EVENT_OPTION.Withdraw_2_Cosmic_Fragments_91, 'Withdraw \d+.*'),
(KEYWORDS_ROGUE_EVENT_OPTION.Record_of_the_Aeon_of_1,
'^Record of the Aeon.*'),
]
}
def filter_detected(self, result: BoxedResult) -> bool:
if not self.expected_options:
return True
right_bound = self.expected_options[0].prefix_icon.area[2]
if result.box[0] < right_bound:
return False
return True
def pre_process(self, image):
# Mask starlike icons to avoid them to be recognized as */#/+/米
offset = tuple(-x for x in self.button.area[:2])
for option in self.expected_options:
x1, y1, x2, y2 = area_offset(option.prefix_icon.area, offset)
image[y1:y2, x1:x2] = (0, 0, 0)
return image
def after_process(self, result):
return self._after_process(result, RogueEventOption)
class OptionScroll(Scroll):
def position_to_screen(self, position, random_range=(-0.05, 0.05)):
# This scroll itself can not be dragged, but OCR_OPTION.area can
area = super().position_to_screen(position, random_range)
confirm_width = self.area[0] - CHOOSE_OPTION_CONFIRM.button[0]
area_width = CHOOSE_OPTION_CONFIRM.button[0] - OCR_OPTION.area[0]
# A fixed offset is easy to fail for some reason
random_offset = random.uniform(0.2, 0.8) * area_width + confirm_width
area = area_offset(area, (-random_offset, 0))
# Flip drag direction upside down
return (
area[0], self.area[1] + self.area[3] - area[3],
area[2], self.area[1] + self.area[3] - area[1],
)
SCROLL_OPTION = OptionScroll(OPTION_SCROLL, color=(
219, 194, 145), name='SCROLL_OPTION')
class RogueEvent(RogueUI):
event_title: RogueEventTitle = None
options: list[OptionButton] = []
@cached_property
def valid_options(self) -> list[OptionButton]:
return [x for x in self.options if x.is_valid]
def handle_event_continue(self):
if self.appear(PAGE_EVENT, interval=0.6):
logger.info(f'{PAGE_EVENT} -> {BLESSING_CONFIRM}')
@ -25,34 +171,144 @@ class RogueEvent(RogueUI):
return False
def handle_event_option(self):
options = CHOOSE_OPTION.match_multi_template(self.device.image)
# Check color also, option with requirements might be disabled
options = [
option for option in options
if self.image_color_count(option.area, color=(181, 162, 126), threshold=221, count=25)
]
count = len(options)
"""
self.event_title SHOULD be set to None before calling this function
Pages:
in: page_rogue
"""
self.options = []
del_cached_property(self, 'valid_options')
self._event_option_match()
count = len(self.valid_options)
if count == 0:
return False
logger.attr('EventOption', count)
for button in options:
button.button = area_limit(button.button, OCR_EVENT.area)
logger.attr('EventOption', f'{count}/{len(self.options)}')
# Only one option, click directly
if count == 1:
if self.interval_is_reached(CHOOSE_OPTION, interval=2):
self.device.click(options[0])
self.device.click(self.valid_options[0].prefix_icon)
self.interval_reset(CHOOSE_OPTION)
return True
if self.interval_is_reached(CHOOSE_OPTION, interval=2):
option = self._event_option_filter(options)
self.device.click(option)
option = self._event_option_filter()
if SCROLL_OPTION.appear(main=self):
if option.is_bottom_page:
SCROLL_OPTION.set_bottom(main=self)
else:
SCROLL_OPTION.set_top(main=self)
self.device.click(option.prefix_icon)
self.interval_reset(CHOOSE_OPTION)
return True
return False
def _event_option_filter(self, options: list[ClickButton]) -> ClickButton:
# TODO: OCR options instead of choosing the last one
return options[-1]
def _event_option_match(self, is_bottom_page=False) -> int:
"""
Returns:
int: Number of option icons matched
"""
option_icons = CHOOSE_OPTION.match_multi_template(self.device.image)
for button in option_icons:
button.button = area_limit(button.button, OCR_OPTION.area)
self.options += [OptionButton(
prefix_icon=icon,
is_valid=self.image_color_count(icon.area, color=(
181, 162, 126), threshold=221, count=25),
is_bottom_page=is_bottom_page
) for icon in option_icons]
if option_icons:
del_cached_property(self, 'valid_options')
return len(option_icons)
def _event_option_ocr(self, expected_count: int) -> None:
"""
Args:
expected_count (int): Number of option icons matched
"""
expected_options = self.options[-expected_count:]
ocr = OcrRogueEventOption(OCR_OPTION)
ocr.expected_options = expected_options
ocr_results = ocr.matched_ocr(self.device.image, [RogueEventOption])
# Pair icons and ocr results
index = 0
all_matched = True
for option in expected_options:
_, y1, _, y2 = option.prefix_icon.area
for index in range(index, len(ocr_results)):
_, yy1, _, yy2 = ocr_results[index].area
if yy2 < y1:
continue
if yy1 > y2:
break
option.button = ocr_results[index]
break
if option.button is None:
option.is_valid = False
all_matched = False
if not all_matched:
logger.warning('Count of OCR_OPTION results is not as expected')
del_cached_property(self, 'valid_options')
def _event_option_filter(self) -> OptionButton:
if self.event_title is None:
# OCR area of rest area is different from other occurrences
if self.appear(REST_AREA):
self.event_title = KEYWORDS_ROGUE_EVENT_TITLE.Rest_Area
else:
# Title may contains multi lines
results = OcrRogueEventTitle(OCR_TITLE).matched_ocr(
self.device.image,
[RogueEventTitle]
)
if results:
self.event_title = results[0].matched_keyword
if self.event_title is None:
random_index = random.choice(range(len(self.valid_options)))
logger.warning('Failed to OCR title')
logger.info(f'Randomly select option {random_index+1}')
return self.valid_options[random_index]
strategy_name = self.config.RoguePath_DomainStrategy
logger.attr('DomainStrategy', strategy_name)
if strategy_name not in STRATEGIES:
logger.warning(
'Unknown domain strategy, fall back to STRATEGY_COMMON'
)
strategy = STRATEGIES.get(strategy_name, STRATEGY_COMMON)
if self.event_title not in strategy:
random_index = random.choice(range(len(self.valid_options)))
logger.info(f'No strategy preset for {self.event_title}')
logger.info(f'Randomly select option {random_index+1}')
return self.valid_options[random_index]
# Try ocr
if not self.options:
self._event_option_match()
self._event_option_ocr(len(self.options))
# Check next page if there is scroll
if SCROLL_OPTION.appear(main=self):
if SCROLL_OPTION.set_bottom(main=self):
expected = self._event_option_match(is_bottom_page=True)
self._event_option_ocr(expected)
priority = [
random.shuffle(x) or x
if isinstance(x, (list, tuple)) else [x]
for x in strategy[self.event_title]
]
priority = list(chain.from_iterable(priority))
# Reason why _keywords_to_find()[0] is used to compare:
# Text of options in different events can be the same,
# so it is possible that keywords returned by matched_ocr
# is not exactly the same as options in RogueEventTitle.option_ids.
for expect in priority:
for i, option in enumerate(self.valid_options):
ocr_text = option.button.matched_keyword._keywords_to_find()[0]
expect_text = expect._keywords_to_find()[0]
if ocr_text == expect_text:
logger.info(f'Select option {i+1}: {option}')
return option
logger.error('No option was selected, return the last instead')
logger.info(f'Select last option: {self.valid_options[-1]}')
return self.valid_options[-1]

187
tasks/rogue/event/preset.py Normal file
View File

@ -0,0 +1,187 @@
from tasks.rogue.keywords import KEYWORDS_ROGUE_EVENT_TITLE, KEYWORDS_ROGUE_EVENT_OPTION
# https://docs.qq.com/sheet/DY2Jua1VkWWhsdEpr
# TODO: events that only come up in Swarm Disaster (寰宇蝗灾)
STRATEGY_COMMON = {
KEYWORDS_ROGUE_EVENT_TITLE.Rest_Area: [
KEYWORDS_ROGUE_EVENT_OPTION.Enhance_2_random_Blessings,
KEYWORDS_ROGUE_EVENT_OPTION.Purchase_a_1_star_Blessing,
KEYWORDS_ROGUE_EVENT_OPTION.Purchase_1_Curio,
KEYWORDS_ROGUE_EVENT_OPTION.Leave_2
],
KEYWORDS_ROGUE_EVENT_TITLE.Ruan_Mei: [
KEYWORDS_ROGUE_EVENT_OPTION.Worship_Aeons_1,
KEYWORDS_ROGUE_EVENT_OPTION.Want_lots_of_money
],
KEYWORDS_ROGUE_EVENT_TITLE.Shopping_Channel: [
KEYWORDS_ROGUE_EVENT_OPTION.A_lotus_that_can_sing_the_Happy_Birthday_song,
KEYWORDS_ROGUE_EVENT_OPTION.A_mechanical_box,
KEYWORDS_ROGUE_EVENT_OPTION.A_box_of_expired_doughnuts,
KEYWORDS_ROGUE_EVENT_OPTION.Leave_this_place
],
KEYWORDS_ROGUE_EVENT_TITLE.Interactive_Arts: [
KEYWORDS_ROGUE_EVENT_OPTION.Action,
KEYWORDS_ROGUE_EVENT_OPTION.Musical,
KEYWORDS_ROGUE_EVENT_OPTION.Please_let_me_live
],
KEYWORDS_ROGUE_EVENT_TITLE.I_O_U_Dispenser: [
[
KEYWORDS_ROGUE_EVENT_OPTION.I_don_t_want_anything_This_is_very_nihilistic,
KEYWORDS_ROGUE_EVENT_OPTION.I_don_t_need_it
],
[
KEYWORDS_ROGUE_EVENT_OPTION.You_re_not_a_reliable_investment_manager,
KEYWORDS_ROGUE_EVENT_OPTION.I_hate_this_era
],
[
KEYWORDS_ROGUE_EVENT_OPTION.I_ll_buy_it,
KEYWORDS_ROGUE_EVENT_OPTION.I_want_money,
KEYWORDS_ROGUE_EVENT_OPTION.I_want_love
]
],
KEYWORDS_ROGUE_EVENT_TITLE.Statue: [
KEYWORDS_ROGUE_EVENT_OPTION.Believe_in_them_with_pure_devotion,
KEYWORDS_ROGUE_EVENT_OPTION.Discard_the_statue_Be_decisive
],
KEYWORDS_ROGUE_EVENT_TITLE.Unending_Darkness: [
KEYWORDS_ROGUE_EVENT_OPTION.Fight_the_pull,
KEYWORDS_ROGUE_EVENT_OPTION.Head_into_the_darkness,
],
KEYWORDS_ROGUE_EVENT_TITLE.The_Architects: [
KEYWORDS_ROGUE_EVENT_OPTION.Thank_the_Aeon_Qlipoth,
KEYWORDS_ROGUE_EVENT_OPTION.Leave_16
],
KEYWORDS_ROGUE_EVENT_TITLE.Cosmic_Merchant_Part_1: [
KEYWORDS_ROGUE_EVENT_OPTION.Purchase_a_metal_Wish_In_A_Bottle,
KEYWORDS_ROGUE_EVENT_OPTION.Leave_18,
KEYWORDS_ROGUE_EVENT_OPTION.Purchase_a_silver_ore_Wish_In_A_Bottle_18
],
KEYWORDS_ROGUE_EVENT_TITLE.Cosmic_Con_Job_Part_2: [
KEYWORDS_ROGUE_EVENT_OPTION.Purchase_a_supernium_Wish_In_A_Bottle_19,
KEYWORDS_ROGUE_EVENT_OPTION.Leave_19,
KEYWORDS_ROGUE_EVENT_OPTION.Purchase_an_amber_Wish_In_A_Bottle_19
],
KEYWORDS_ROGUE_EVENT_TITLE.Cosmic_Altruist_Part_3: [
KEYWORDS_ROGUE_EVENT_OPTION.Purchase_an_ore_box_20,
KEYWORDS_ROGUE_EVENT_OPTION.Purchase_a_diamond_box_20,
KEYWORDS_ROGUE_EVENT_OPTION.Leave_20
],
KEYWORDS_ROGUE_EVENT_TITLE.Bounty_Hunter: [
KEYWORDS_ROGUE_EVENT_OPTION.Walk_away_25,
KEYWORDS_ROGUE_EVENT_OPTION.Give_him_the_fur_you_re_wearing
],
KEYWORDS_ROGUE_EVENT_TITLE.Nomadic_Miners: [
KEYWORDS_ROGUE_EVENT_OPTION.Qlipoth_Favor,
KEYWORDS_ROGUE_EVENT_OPTION.Qlipoth_Blessing
],
KEYWORDS_ROGUE_EVENT_TITLE.Jim_Hulk_and_Jim_Hall: [
KEYWORDS_ROGUE_EVENT_OPTION.Jim_Hulk_collection,
KEYWORDS_ROGUE_EVENT_OPTION.Walk_away_5
],
KEYWORDS_ROGUE_EVENT_TITLE.The_Cremators: [
KEYWORDS_ROGUE_EVENT_OPTION.Bear_ten_carats_of_trash,
KEYWORDS_ROGUE_EVENT_OPTION.Give_everything_to_them,
],
KEYWORDS_ROGUE_EVENT_TITLE.Pixel_World: [
KEYWORDS_ROGUE_EVENT_OPTION.Jump_onto_the_bricks_to_the_right,
KEYWORDS_ROGUE_EVENT_OPTION.Climb_into_the_pipes_to_the_left
],
KEYWORDS_ROGUE_EVENT_TITLE.Societal_Dreamscape: [
KEYWORDS_ROGUE_EVENT_OPTION.Return_to_work,
KEYWORDS_ROGUE_EVENT_OPTION.Swallow_the_other_fish_eye_and_continue_to_enjoy_the_massage
],
KEYWORDS_ROGUE_EVENT_TITLE.Kindling_of_the_Self_Annihilator: [
KEYWORDS_ROGUE_EVENT_OPTION.Accept_the_flames_of_Self_destruction_and_destroy_the_black_box,
KEYWORDS_ROGUE_EVENT_OPTION.Refuse_17
],
KEYWORDS_ROGUE_EVENT_TITLE.Saleo_Part_1: [
KEYWORDS_ROGUE_EVENT_OPTION.Pick_Sal_22,
KEYWORDS_ROGUE_EVENT_OPTION.Pick_Leo_22
],
KEYWORDS_ROGUE_EVENT_TITLE.Sal_Part_2: [
KEYWORDS_ROGUE_EVENT_OPTION.Pick_Sal_23,
KEYWORDS_ROGUE_EVENT_OPTION.Let_Leo_out_23
],
KEYWORDS_ROGUE_EVENT_TITLE.Leo_Part_3: [
KEYWORDS_ROGUE_EVENT_OPTION.Let_Sal_out_24,
KEYWORDS_ROGUE_EVENT_OPTION.Pick_Leo_24
],
KEYWORDS_ROGUE_EVENT_TITLE.Implement_of_Error: [
KEYWORDS_ROGUE_EVENT_OPTION.Pick_an_Error_Code_Curio,
KEYWORDS_ROGUE_EVENT_OPTION.Leave_26
],
KEYWORDS_ROGUE_EVENT_TITLE.Make_A_Wish: [
KEYWORDS_ROGUE_EVENT_OPTION.Exchange_for_a_3_star_Blessing,
KEYWORDS_ROGUE_EVENT_OPTION.Exchange_for_a_2_star_Blessing,
KEYWORDS_ROGUE_EVENT_OPTION.Leave_33
],
KEYWORDS_ROGUE_EVENT_TITLE.Let_Exchange_Gifts: [
KEYWORDS_ROGUE_EVENT_OPTION.Blessing_Exchange,
KEYWORDS_ROGUE_EVENT_OPTION.Blessing_Reforge,
KEYWORDS_ROGUE_EVENT_OPTION.Leave_32
],
KEYWORDS_ROGUE_EVENT_TITLE.Robot_Sales_Terminal: [
KEYWORDS_ROGUE_EVENT_OPTION.Purchase_a_1_3_star_Blessing,
KEYWORDS_ROGUE_EVENT_OPTION.Purchase_a_1_2_star_Blessing,
KEYWORDS_ROGUE_EVENT_OPTION.Leave_34
],
KEYWORDS_ROGUE_EVENT_TITLE.History_Fictionologists: [
KEYWORDS_ROGUE_EVENT_OPTION.Record_of_the_Aeon_of_1,
KEYWORDS_ROGUE_EVENT_OPTION.Leave_4
],
# Swarm Disaster
KEYWORDS_ROGUE_EVENT_TITLE.Insights_from_the_Universal_Dancer: [
KEYWORDS_ROGUE_EVENT_OPTION.Tell_fortune,
KEYWORDS_ROGUE_EVENT_OPTION.Refuse_invitation
]
}
# Conservative, may take longer time
STRATEGY_COMBAT = {
KEYWORDS_ROGUE_EVENT_TITLE.Nildis: [
KEYWORDS_ROGUE_EVENT_OPTION.Flip_the_card,
KEYWORDS_ROGUE_EVENT_OPTION.Give_up
],
KEYWORDS_ROGUE_EVENT_TITLE.Insect_Nest: [
KEYWORDS_ROGUE_EVENT_OPTION.Go_deeper_into_the_insect_nest,
KEYWORDS_ROGUE_EVENT_OPTION.Wait_for_them,
KEYWORDS_ROGUE_EVENT_OPTION.Stop_at_the_entrance_of_the_nest,
KEYWORDS_ROGUE_EVENT_OPTION.Hug_it
],
KEYWORDS_ROGUE_EVENT_TITLE.We_Are_Cowboys: [
KEYWORDS_ROGUE_EVENT_OPTION.Protect_the_cowboy_final_honor,
KEYWORDS_ROGUE_EVENT_OPTION.Pay
],
KEYWORDS_ROGUE_EVENT_TITLE.Rock_Paper_Scissors: [
KEYWORDS_ROGUE_EVENT_OPTION.Fight_for_the_0_63_chance,
KEYWORDS_ROGUE_EVENT_OPTION.Pick_the_100_security
],
KEYWORDS_ROGUE_EVENT_TITLE.Tavern: [
KEYWORDS_ROGUE_EVENT_OPTION.Fight_both_together,
[
KEYWORDS_ROGUE_EVENT_OPTION.Challenge_Mr_France_security_team,
KEYWORDS_ROGUE_EVENT_OPTION.Challenge_the_burly_Avila_mercenary_company
]
],
KEYWORDS_ROGUE_EVENT_TITLE.Three_Little_Pigs: [
KEYWORDS_ROGUE_EVENT_OPTION.Play_a_bit_with_Sequence_Trotters,
KEYWORDS_ROGUE_EVENT_OPTION.Leave_14
]
}
# Aggressive
STRATEGY_OCCURRENCE = {
KEYWORDS_ROGUE_EVENT_TITLE.Insect_Nest: [
KEYWORDS_ROGUE_EVENT_OPTION.Go_deeper_into_the_insect_nest,
KEYWORDS_ROGUE_EVENT_OPTION.Hug_it,
KEYWORDS_ROGUE_EVENT_OPTION.Wait_for_them,
KEYWORDS_ROGUE_EVENT_OPTION.Stop_at_the_entrance_of_the_nest
]
}
for k, v in STRATEGY_COMBAT.items():
if k in STRATEGY_OCCURRENCE:
continue
STRATEGY_OCCURRENCE[k] = list(reversed(v))
STRATEGIES = {
'occurrence': {**STRATEGY_COMMON, **STRATEGY_OCCURRENCE},
'combat': {**STRATEGY_COMMON, **STRATEGY_COMBAT}
}

View File

@ -4,6 +4,9 @@ import tasks.rogue.keywords.curio as KEYWORDS_ROGUE_CURIO
import tasks.rogue.keywords.enhancement as KEYWORDS_ROGUE_ENHANCEMENT
import tasks.rogue.keywords.path as KEYWORDS_ROGUE_PATH
import tasks.rogue.keywords.resonance as KEYWORDS_ROGUE_RESONANCE
from tasks.rogue.keywords.classes import (RogueBlessing, RogueBonus, RogueEnhancement,
RoguePath, RogueResonance, RogueCurio)
import tasks.rogue.keywords.event_title as KEYWORDS_ROGUE_EVENT_TITLE
import tasks.rogue.keywords.event_option as KEYWORDS_ROGUE_EVENT_OPTION
from tasks.rogue.keywords.classes import (RogueBlessing, RogueBonus,
RogueCurio, RogueEnhancement,
RoguePath, RogueResonance,
RogueEventTitle, RogueEventOption)

View File

@ -68,3 +68,17 @@ class RogueEnhancement(Keyword):
def enhancement_keyword(self):
return [self.__getattribute__(f"{server}_parsed")
for server in UI_LANGUAGES if hasattr(self, f"{server}_parsed")]
@dataclass(repr=False)
class RogueEventTitle(Keyword):
instances: ClassVar = {}
option_ids: list[int]
def __hash__(self):
return super().__hash__()
@dataclass(repr=False)
class RogueEventOption(Keyword):
instances: ClassVar = {}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -90,6 +90,7 @@ class RouteBase(RouteBase_, RogueExit, RogueEvent):
out: is_in_main()
"""
logger.info('Clear occurrence')
self.event_title = None
while 1:
if skip_first_screenshot:
skip_first_screenshot = False