diff --git a/README.md b/README.md index fdb3827b7..457ffd5ad 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,8 @@ python gui.py Discord https://discord.gg/aJkt3mKDEr QQ群 752620927 +- [小地图识别原理](https://github.com/LmeSzinc/StarRailCopilot/wiki/MinimapTracking) - 开发文档(目录在侧边栏):[Alas wiki](https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/1.-Start),但很多内容是新写的,建议阅读源码和历史提交。 - - 开发路线图:[#10](https://github.com/LmeSzinc/StarRailCopilot/issues/10) ,欢迎提交 PR,挑选你感兴趣的部分进行开发即可。 > **如何添加多语言/多服务器支持?** 需要适配 assets,参考 [开发文档 “添加一个 Button” 一节](https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/4.1.-Detection-objects#%E6%B7%BB%E5%8A%A0%E4%B8%80%E4%B8%AA-button)。 diff --git a/README_en.md b/README_en.md index 73551d069..21b73138e 100644 --- a/README_en.md +++ b/README_en.md @@ -71,6 +71,8 @@ Keep the bot running, SRC will auto login and empty trailblaze power when it's r Discord https://discord.gg/aJkt3mKDEr QQ Group 752620927 +- [Minimap Tracking](https://github.com/LmeSzinc/StarRailCopilot/wiki/MinimapTracking) + - Development Docs (menu is on sidebar): [Alas wiki](https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/1.-Start) (in Chinese). However, there is ton of code newly written, it is recommended to read the source code and historical commits. - Development Road Map: [#10](https://github.com/LmeSzinc/StarRailCopilot/issues/10). Pull Requests are welcomed, just pick the part you interested to work on. diff --git a/assets/character/Kafka.png b/assets/character/Kafka.png new file mode 100644 index 000000000..6cb35cb26 Binary files /dev/null and b/assets/character/Kafka.png differ diff --git a/assets/character/Luka.png b/assets/character/Luka.png new file mode 100644 index 000000000..9b91a168f Binary files /dev/null and b/assets/character/Luka.png differ diff --git a/assets/gui/css/dark-alas.css b/assets/gui/css/dark-alas.css index 79c1835ab..601fa354c 100644 --- a/assets/gui/css/dark-alas.css +++ b/assets/gui/css/dark-alas.css @@ -59,6 +59,26 @@ select { background-image: url("data:image/png;base64,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"); } +.state > select { + border-bottom: 0; + background-image: none; + pointer-events: none; +} + +.state-bold > select { + font-weight: bold; + color: #7a77bb; +} + +.state-light > select { + color: #777777; +} + +[id^="pywebio-scope-arg_stored-stored-value-"] > div > input { + border-bottom: none; + background-color: transparent !important; +} + textarea { border: 1px solid #21262d; } diff --git a/assets/gui/css/light-alas.css b/assets/gui/css/light-alas.css index 8db33eac7..b995776ef 100644 --- a/assets/gui/css/light-alas.css +++ b/assets/gui/css/light-alas.css @@ -60,6 +60,26 @@ select { background-image: url("data:image/png;base64,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"); } +.state > select { + border-bottom: 0; + background-image: none; + pointer-events: none; +} + +.state-bold > select { + font-weight: bold; + color: #7a77bb; +} + +.state-light > select { + color: #777777; +} + +[id^="pywebio-scope-arg_stored-stored-value-"] > div > input { + border-bottom: none; + background-color: transparent !important; +} + textarea { border: 1px solid lightgrey; } diff --git a/assets/share/dungeon/event/DOUBLE_RELIC_EVENT_TAG.png b/assets/share/dungeon/event/DOUBLE_RELIC_EVENT_TAG.png new file mode 100644 index 000000000..574b194a1 Binary files /dev/null and b/assets/share/dungeon/event/DOUBLE_RELIC_EVENT_TAG.png differ diff --git a/assets/share/map/control/ROTATION_SWIPE_AREA.png b/assets/share/map/control/ROTATION_SWIPE_AREA.png new file mode 100644 index 000000000..904e593de Binary files /dev/null and b/assets/share/map/control/ROTATION_SWIPE_AREA.png differ diff --git a/config/template.json b/config/template.json index 39cd0e0c6..61d0d1fe3 100644 --- a/config/template.json +++ b/config/template.json @@ -42,9 +42,21 @@ "Dungeon": { "Name": "Calyx_Golden_Treasures", "NameAtDoubleCalyx": "Calyx_Golden_Treasures", - "Team": 1, - "Support": "when_daily", - "SupportCharacter": "FirstCharacter" + "NameAtDoubleRelic": "Cavern_of_Corrosion_Path_of_Providence", + "Team": 1 + }, + "DungeonDaily": { + "CalyxGolden": "Calyx_Golden_Treasures", + "CalyxCrimson": "Calyx_Crimson_Erudition", + "StagnantShadow": "do_not_archive", + "CavernOfCorrosion": "Cavern_of_Corrosion_Path_of_Providence" + }, + "DungeonSupport": { + "Use": "when_daily", + "Character": "FirstCharacter" + }, + "DungeonStorage": { + "DungeonDouble": {} } }, "DailyQuest": { @@ -53,6 +65,37 @@ "NextRun": "2020-01-01 00:00:00", "Command": "DailyQuest", "ServerUpdate": "04:00" + }, + "AchievableQuest": { + "Complete_1_Daily_Mission": "not_supported", + "Clear_Calyx_Golden_1_times": "not_set", + "Complete_Calyx_Crimson_1_time": "not_set", + "Clear_Stagnant_Shadow_1_times": "not_set", + "Clear_Cavern_of_Corrosion_1_times": "not_set", + "In_a_single_battle_inflict_3_Weakness_Break_of_different_Types": "not_supported", + "Inflict_Weakness_Break_5_times": "not_supported", + "Defeat_a_total_of_20_enemies": "not_supported", + "Enter_combat_by_attacking_enemy_Weakness_and_win_3_times": "not_supported", + "Use_Technique_2_times": "achievable", + "Go_on_assignment_1_time": "not_set", + "Take_1_photo": "achievable", + "Destroy_3_destructible_objects": "not_supported", + "Complete_Forgotten_Hall_1_time": "not_supported", + "Complete_Echo_of_War_1_times": "not_supported", + "Complete_1_stage_in_Simulated_Universe_Any_world": "not_supported", + "Obtain_victory_in_combat_with_support_characters_1_time": "not_set", + "Use_an_Ultimate_to_deal_the_final_blow_1_time": "not_supported", + "Level_up_any_character_1_time": "not_supported", + "Level_up_any_Light_Cone_1_time": "not_supported", + "Level_up_any_Relic_1_time": "not_supported", + "Salvage_any_Relic": "achievable", + "Synthesize_Consumable_1_time": "achievable", + "Synthesize_material_1_time": "achievable", + "Use_Consumables_1_time": "achievable" + }, + "DailyStorage": { + "DailyActivity": {}, + "DailyQuest": {} } }, "BattlePass": { diff --git a/module/base/timer.py b/module/base/timer.py index a36182de0..b849d1fc0 100644 --- a/module/base/timer.py +++ b/module/base/timer.py @@ -112,6 +112,10 @@ class Timer: else: return 0. + def set_current(self, current, count=0): + self._current = time.time() - current + self._reach_count = count + def reached(self): """ Returns: diff --git a/module/config/argument/args.json b/module/config/argument/args.json index d1b967a22..4190c027c 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -218,6 +218,20 @@ "Calyx_Crimson_Nihility" ] }, + "NameAtDoubleRelic": { + "type": "select", + "value": "Cavern_of_Corrosion_Path_of_Providence", + "option": [ + "do_not_participate", + "Cavern_of_Corrosion_Path_of_Gelid_Wind", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch", + "Cavern_of_Corrosion_Path_of_Drifting", + "Cavern_of_Corrosion_Path_of_Providence", + "Cavern_of_Corrosion_Path_of_Holy_Hymn", + "Cavern_of_Corrosion_Path_of_Conflagration", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers" + ] + }, "Team": { "type": "select", "value": 1, @@ -229,17 +243,76 @@ 5, 6 ] + } + }, + "DungeonDaily": { + "CalyxGolden": { + "type": "select", + "value": "Calyx_Golden_Treasures", + "option": [ + "do_not_achieve", + "Calyx_Golden_Memories", + "Calyx_Golden_Aether", + "Calyx_Golden_Treasures" + ] }, - "Support": { + "CalyxCrimson": { + "type": "select", + "value": "Calyx_Crimson_Erudition", + "option": [ + "do_not_achieve", + "Calyx_Crimson_Destruction", + "Calyx_Crimson_Preservation", + "Calyx_Crimson_Hunt", + "Calyx_Crimson_Abundance", + "Calyx_Crimson_Erudition", + "Calyx_Crimson_Harmony", + "Calyx_Crimson_Nihility" + ] + }, + "StagnantShadow": { + "type": "select", + "value": "do_not_archive", + "option": [ + "do_not_achieve", + "Stagnant_Shadow_Quanta", + "Stagnant_Shadow_Gust", + "Stagnant_Shadow_Fulmination", + "Stagnant_Shadow_Blaze", + "Stagnant_Shadow_Spike", + "Stagnant_Shadow_Rime", + "Stagnant_Shadow_Mirage", + "Stagnant_Shadow_Icicle", + "Stagnant_Shadow_Doom", + "Stagnant_Shadow_Celestial" + ] + }, + "CavernOfCorrosion": { + "type": "select", + "value": "Cavern_of_Corrosion_Path_of_Providence", + "option": [ + "do_not_achieve", + "Cavern_of_Corrosion_Path_of_Gelid_Wind", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch", + "Cavern_of_Corrosion_Path_of_Drifting", + "Cavern_of_Corrosion_Path_of_Providence", + "Cavern_of_Corrosion_Path_of_Holy_Hymn", + "Cavern_of_Corrosion_Path_of_Conflagration", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers" + ] + } + }, + "DungeonSupport": { + "Use": { "type": "select", "value": "when_daily", "option": [ - "do_not_use", "always_use", - "when_daily" + "when_daily", + "do_not_use" ] }, - "SupportCharacter": { + "Character": { "type": "select", "value": "FirstCharacter", "option": [ @@ -256,6 +329,8 @@ "Himeko", "Hook", "JingYuan", + "Kafka", + "Luka", "Luocha", "March7th", "Natasha", @@ -274,6 +349,14 @@ "Yukong" ] } + }, + "DungeonStorage": { + "DungeonDouble": { + "type": "stored", + "value": {}, + "display": "hide", + "stored": "StoredDungeonDouble" + } } }, "DailyQuest": { @@ -297,6 +380,397 @@ "value": "04:00", "display": "hide" } + }, + "AchievableQuest": { + "Complete_1_Daily_Mission": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Clear_Calyx_Golden_1_times": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Complete_Calyx_Crimson_1_time": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Clear_Stagnant_Shadow_1_times": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Clear_Cavern_of_Corrosion_1_times": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "In_a_single_battle_inflict_3_Weakness_Break_of_different_Types": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Inflict_Weakness_Break_5_times": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Defeat_a_total_of_20_enemies": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Enter_combat_by_attacking_enemy_Weakness_and_win_3_times": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Use_Technique_2_times": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Go_on_assignment_1_time": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Take_1_photo": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Destroy_3_destructible_objects": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Complete_Forgotten_Hall_1_time": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Complete_Echo_of_War_1_times": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Complete_1_stage_in_Simulated_Universe_Any_world": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Obtain_victory_in_combat_with_support_characters_1_time": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Use_an_Ultimate_to_deal_the_final_blow_1_time": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Level_up_any_character_1_time": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Level_up_any_Light_Cone_1_time": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Level_up_any_Relic_1_time": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Salvage_any_Relic": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Synthesize_Consumable_1_time": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Synthesize_material_1_time": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + }, + "Use_Consumables_1_time": { + "type": "state", + "value": "achievable", + "option": [ + "achievable", + "not_set", + "not_supported" + ], + "option_bold": [ + "achievable" + ], + "option_light": [ + "not_supported" + ] + } + }, + "DailyStorage": { + "DailyActivity": { + "type": "stored", + "value": {}, + "display": "hide", + "stored": "StoredDailyActivity" + }, + "DailyQuest": { + "type": "stored", + "value": {}, + "display": "hide", + "stored": "StoredDaily" + } } }, "BattlePass": { diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 29eedfe0b..ad9d1e1cd 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -71,41 +71,74 @@ Optimization: Dungeon: Name: - # Options will be injected in config updater + # Dungeon names will be injected in config updater value: Calyx_Golden_Treasures - option: [ Calyx_Golden_Treasures, ] + option: [ ] NameAtDoubleCalyx: - # Options will be injected in config updater value: Calyx_Golden_Treasures option: [ do_not_participate, ] + NameAtDoubleRelic: + value: Cavern_of_Corrosion_Path_of_Providence + option: [ do_not_participate, ] Team: value: 1 option: [ 1, 2, 3, 4, 5, 6 ] - Support: +DungeonDaily: + # Dungeon names will be injected in config updater + CalyxGolden: + value: Calyx_Golden_Treasures + option: [ do_not_achieve, ] + CalyxCrimson: + value: Calyx_Crimson_Erudition + option: [ do_not_achieve, ] + StagnantShadow: + value: do_not_archive + option: [ do_not_achieve, ] + CavernOfCorrosion: + value: Cavern_of_Corrosion_Path_of_Providence + option: [ do_not_achieve, ] +DungeonSupport: + Use: value: when_daily - option: [ do_not_use, always_use, when_daily ] - SupportCharacter: + option: [ always_use, when_daily, do_not_use ] + Character: # Options will be injected in config updater value: FirstCharacter option: [ FirstCharacter, ] +DungeonStorage: + DungeonDouble: + stored: StoredDungeonDouble + +AchievableQuest: + # Quests will be injected in config updater +# Complete_1_Daily_Mission: +# type: state +# value: achievable +# option: [ achievable, not_set, not_supported ] +# option_bold: [ achievable, ] +DailyStorage: + DailyActivity: + stored: StoredDailyActivity + DailyQuest: + stored: StoredDaily Assignment: Duration: value: 20 - option: [ 4, 8, 12, 20 ] + option: [4, 8, 12, 20] # Options in Name_x will be injected in config updater Name_1: value: Nameless_Land_Nameless_People - option: [ Nameless_Land_Nameless_People, ] + option: [Nameless_Land_Nameless_People, ] Name_2: value: Akashic_Records - option: [ Nameless_Land_Nameless_People, ] + option: [Nameless_Land_Nameless_People, ] Name_3: value: The_Invisible_Hand - option: [ Nameless_Land_Nameless_People, ] + option: [Nameless_Land_Nameless_People, ] Name_4: value: Nine_Billion_Names - option: [ Nameless_Land_Nameless_People, ] + option: [Nameless_Land_Nameless_People, ] # ==================== Rogue ==================== diff --git a/module/config/argument/task.yaml b/module/config/argument/task.yaml index 2563e53d5..6f2f868ba 100644 --- a/module/config/argument/task.yaml +++ b/module/config/argument/task.yaml @@ -25,8 +25,13 @@ Daily: Dungeon: - Scheduler - Dungeon + - DungeonDaily + - DungeonSupport + - DungeonStorage DailyQuest: - Scheduler + - AchievableQuest + - DailyStorage BattlePass: - Scheduler Assignment: diff --git a/module/config/config.py b/module/config/config.py index aaeea7101..79d16abf8 100644 --- a/module/config/config.py +++ b/module/config/config.py @@ -3,11 +3,14 @@ import datetime import operator import threading +from module.base.decorator import cached_property, del_cached_property from module.base.filter import Filter from module.base.utils import SelectedGrids from module.config.config_generated import GeneratedConfig from module.config.config_manual import ManualConfig from module.config.config_updater import ConfigUpdater +from module.config.stored.stored_generated import StoredGenerated +from module.config.stored.classes import iter_attribute from module.config.utils import * from module.config.watcher import ConfigWatcher from module.exception import RequestHumanTakeover, ScriptError @@ -168,6 +171,15 @@ class AzurLaneConfig(ConfigUpdater, ManualConfig, GeneratedConfig, ConfigWatcher self.data, keys="Alas.Optimization.CloseGameDuringWait", default=False ) + @cached_property + def stored(self) -> StoredGenerated: + stored = StoredGenerated() + # Bind config + for _, value in iter_attribute(stored): + value._bind(self) + del_cached_property(value, '_stored') + return stored + def get_next_task(self): """ Calculate tasks, set pending_task and waiting_task @@ -241,6 +253,7 @@ class AzurLaneConfig(ConfigUpdater, ManualConfig, GeneratedConfig, ConfigWatcher ) # Don't use self.modified = {}, that will create a new object. self.modified.clear() + del_cached_property(self, 'stored') self.write_file(self.config_name, data=self.data) def update(self): @@ -471,6 +484,20 @@ class AzurLaneConfig(ConfigUpdater, ManualConfig, GeneratedConfig, ConfigWatcher def is_task_enabled(self, task): return bool(self.cross_get(keys=[task, 'Scheduler', 'Enable'], default=False)) + def update_daily_quests(self): + """ + Raises: + TaskEnd: Call task `DailyQuest` and stop current task + """ + if self.stored.DailyActivity.is_expired(): + logger.info('Daily activity expired, call task to update') + self.task_call('DailyQuest') + self.task_stop() + if self.stored.DailyQuest.is_expired(): + logger.info('Daily quests expired, call task to update') + self.task_call('DailyQuest') + self.task_stop() + @property def DEVICE_SCREENSHOT_METHOD(self): return self.Emulator_ScreenshotMethod diff --git a/module/config/config_generated.py b/module/config/config_generated.py index 7af5928c2..ae9aa4ef0 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -41,9 +41,52 @@ class GeneratedConfig: # Group `Dungeon` Dungeon_Name = 'Calyx_Golden_Treasures' # Calyx_Golden_Memories, Calyx_Golden_Aether, Calyx_Golden_Treasures, Calyx_Crimson_Destruction, Calyx_Crimson_Preservation, Calyx_Crimson_Hunt, Calyx_Crimson_Abundance, Calyx_Crimson_Erudition, Calyx_Crimson_Harmony, Calyx_Crimson_Nihility, Stagnant_Shadow_Quanta, Stagnant_Shadow_Gust, Stagnant_Shadow_Fulmination, Stagnant_Shadow_Blaze, Stagnant_Shadow_Spike, Stagnant_Shadow_Rime, Stagnant_Shadow_Mirage, Stagnant_Shadow_Icicle, Stagnant_Shadow_Doom, Stagnant_Shadow_Celestial, Cavern_of_Corrosion_Path_of_Gelid_Wind, Cavern_of_Corrosion_Path_of_Jabbing_Punch, Cavern_of_Corrosion_Path_of_Drifting, Cavern_of_Corrosion_Path_of_Providence, Cavern_of_Corrosion_Path_of_Holy_Hymn, Cavern_of_Corrosion_Path_of_Conflagration, Cavern_of_Corrosion_Path_of_Elixir_Seekers Dungeon_NameAtDoubleCalyx = 'Calyx_Golden_Treasures' # do_not_participate, Calyx_Golden_Memories, Calyx_Golden_Aether, Calyx_Golden_Treasures, Calyx_Crimson_Destruction, Calyx_Crimson_Preservation, Calyx_Crimson_Hunt, Calyx_Crimson_Abundance, Calyx_Crimson_Erudition, Calyx_Crimson_Harmony, Calyx_Crimson_Nihility + Dungeon_NameAtDoubleRelic = 'Cavern_of_Corrosion_Path_of_Providence' # do_not_participate, Cavern_of_Corrosion_Path_of_Gelid_Wind, Cavern_of_Corrosion_Path_of_Jabbing_Punch, Cavern_of_Corrosion_Path_of_Drifting, Cavern_of_Corrosion_Path_of_Providence, Cavern_of_Corrosion_Path_of_Holy_Hymn, Cavern_of_Corrosion_Path_of_Conflagration, Cavern_of_Corrosion_Path_of_Elixir_Seekers Dungeon_Team = 1 # 1, 2, 3, 4, 5, 6 - Dungeon_Support = 'when_daily' # do_not_use, always_use, when_daily - Dungeon_SupportCharacter = 'FirstCharacter' # FirstCharacter, Arlan, Asta, Bailu, Blade, Bronya, Clara, DanHeng, Gepard, Herta, Himeko, Hook, JingYuan, Luocha, March7th, Natasha, Pela, Qingque, Sampo, Seele, Serval, SilverWolf, Sushang, Tingyun, TrailblazerDestruction, TrailblazerPreservation, Welt, Yanqing, Yukong + + # Group `DungeonDaily` + DungeonDaily_CalyxGolden = 'Calyx_Golden_Treasures' # do_not_achieve, Calyx_Golden_Memories, Calyx_Golden_Aether, Calyx_Golden_Treasures + DungeonDaily_CalyxCrimson = 'Calyx_Crimson_Erudition' # do_not_achieve, Calyx_Crimson_Destruction, Calyx_Crimson_Preservation, Calyx_Crimson_Hunt, Calyx_Crimson_Abundance, Calyx_Crimson_Erudition, Calyx_Crimson_Harmony, Calyx_Crimson_Nihility + DungeonDaily_StagnantShadow = 'do_not_archive' # do_not_achieve, Stagnant_Shadow_Quanta, Stagnant_Shadow_Gust, Stagnant_Shadow_Fulmination, Stagnant_Shadow_Blaze, Stagnant_Shadow_Spike, Stagnant_Shadow_Rime, Stagnant_Shadow_Mirage, Stagnant_Shadow_Icicle, Stagnant_Shadow_Doom, Stagnant_Shadow_Celestial + DungeonDaily_CavernOfCorrosion = 'Cavern_of_Corrosion_Path_of_Providence' # do_not_achieve, Cavern_of_Corrosion_Path_of_Gelid_Wind, Cavern_of_Corrosion_Path_of_Jabbing_Punch, Cavern_of_Corrosion_Path_of_Drifting, Cavern_of_Corrosion_Path_of_Providence, Cavern_of_Corrosion_Path_of_Holy_Hymn, Cavern_of_Corrosion_Path_of_Conflagration, Cavern_of_Corrosion_Path_of_Elixir_Seekers + + # Group `DungeonSupport` + DungeonSupport_Use = 'when_daily' # always_use, when_daily, do_not_use + DungeonSupport_Character = 'FirstCharacter' # FirstCharacter, Arlan, Asta, Bailu, Blade, Bronya, Clara, DanHeng, Gepard, Herta, Himeko, Hook, JingYuan, Kafka, Luka, Luocha, March7th, Natasha, Pela, Qingque, Sampo, Seele, Serval, SilverWolf, Sushang, Tingyun, TrailblazerDestruction, TrailblazerPreservation, Welt, Yanqing, Yukong + + # Group `DungeonStorage` + DungeonStorage_DungeonDouble = {} + + # Group `AchievableQuest` + AchievableQuest_Complete_1_Daily_Mission = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Clear_Calyx_Golden_1_times = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Complete_Calyx_Crimson_1_time = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Clear_Stagnant_Shadow_1_times = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Clear_Cavern_of_Corrosion_1_times = 'achievable' # achievable, not_set, not_supported + AchievableQuest_In_a_single_battle_inflict_3_Weakness_Break_of_different_Types = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Inflict_Weakness_Break_5_times = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Defeat_a_total_of_20_enemies = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Enter_combat_by_attacking_enemy_Weakness_and_win_3_times = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Use_Technique_2_times = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Go_on_assignment_1_time = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Take_1_photo = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Destroy_3_destructible_objects = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Complete_Forgotten_Hall_1_time = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Complete_Echo_of_War_1_times = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Complete_1_stage_in_Simulated_Universe_Any_world = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Obtain_victory_in_combat_with_support_characters_1_time = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Use_an_Ultimate_to_deal_the_final_blow_1_time = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Level_up_any_character_1_time = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Level_up_any_Light_Cone_1_time = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Level_up_any_Relic_1_time = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Salvage_any_Relic = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Synthesize_Consumable_1_time = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Synthesize_material_1_time = 'achievable' # achievable, not_set, not_supported + AchievableQuest_Use_Consumables_1_time = 'achievable' # achievable, not_set, not_supported + + # Group `DailyStorage` + DailyStorage_DailyActivity = {} + DailyStorage_DailyQuest = {} # Group `Assignment` Assignment_Duration = 20 # 4, 8, 12, 20 diff --git a/module/config/config_manual.py b/module/config/config_manual.py index 659b42392..1f4893bbc 100644 --- a/module/config/config_manual.py +++ b/module/config/config_manual.py @@ -8,7 +8,7 @@ class ManualConfig: SCHEDULER_PRIORITY = """ Restart - > Dungeon > Assignment > DailyQuest > BattlePass + > DailyQuest > Dungeon > Assignment > BattlePass """ """ diff --git a/module/config/config_updater.py b/module/config/config_updater.py index c50c11980..3150c9b4d 100644 --- a/module/config/config_updater.py +++ b/module/config/config_updater.py @@ -4,7 +4,7 @@ from cached_property import cached_property from deploy.Windows.utils import DEPLOY_TEMPLATE, poor_yaml_read, poor_yaml_write from module.base.timer import timer -from module.config.server import to_package, VALID_PACKAGE, VALID_CHANNEL_PACKAGE +from module.config.server import VALID_CHANNEL_PACKAGE, VALID_PACKAGE, to_package from module.config.utils import * CONFIG_IMPORT = ''' @@ -32,6 +32,11 @@ def gui_lang_to_ingame_lang(lang: str) -> str: return DICT_GUI_TO_INGAME.get(lang, 'en') +def get_generator(): + from module.base.code_generator import CodeGenerator + return CodeGenerator() + + class ConfigGenerator: @cached_property def argument(self): @@ -47,6 +52,55 @@ class ConfigGenerator: """ data = {} raw = read_file(filepath_argument('argument')) + + def option_add(keys, options): + options = deep_get(raw, keys=keys, default=[]) + options + deep_set(raw, keys=keys, value=options) + + # Insert dungeons + from tasks.dungeon.keywords import DungeonList + option_add( + keys='Dungeon.Name.option', + options=[dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_daily_dungeon]) + # Double events + option_add( + keys='Dungeon.NameAtDoubleCalyx.option', + options=[dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_Calyx]) + option_add( + keys='Dungeon.NameAtDoubleRelic.option', + options=[dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_Cavern_of_Corrosion]) + # Dungeon daily + option_add( + keys='DungeonDaily.CalyxGolden.option', + options=[dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_Calyx_Golden]) + option_add( + keys='DungeonDaily.CalyxCrimson.option', + options=[dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_Calyx_Crimson]) + option_add( + keys='DungeonDaily.StagnantShadow.option', + options=[dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_Stagnant_Shadow]) + option_add( + keys='DungeonDaily.CavernOfCorrosion.option', + options=[dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_Cavern_of_Corrosion]) + # Insert characters + from tasks.character.keywords import CharacterList + unsupported_characters = ["DanHengImbibitorLunae"] + characters = [character.name for character in CharacterList.instances.values() + if character.name not in unsupported_characters] + option_add(keys='DungeonSupport.Character.option', options=characters) + # Insert daily quests + from tasks.daily.keywords import DailyQuest + for quest in DailyQuest.instances.values(): + quest: DailyQuest + deep_set(raw, keys=['AchievableQuest', quest.name], value={ + 'type': 'state', + 'value': 'achievable', + 'option': ['achievable', 'not_set', 'not_supported'], + 'option_bold': ['achievable'], + 'option_light': ['not_supported'], + }) + + # Load for path, value in deep_iter(raw, depth=2): arg = { 'type': 'input', @@ -56,6 +110,9 @@ class ConfigGenerator: if not isinstance(value, dict): value = {'value': value} arg['type'] = data_to_type(value, arg=path[1]) + if arg['type'] == 'stored': + value['value'] = {} + arg['display'] = 'hide' # Hide `stored` by default if isinstance(value['value'], datetime): arg['type'] = 'datetime' arg['validate'] = 'datetime' @@ -63,14 +120,6 @@ class ConfigGenerator: arg.update(value) deep_set(data, keys=path, value=arg) - # Define storage group - # arg = { - # 'type': 'storage', - # 'value': {}, - # 'valuetype': 'ignore', - # 'display': 'disabled', - # } - # deep_set(data, keys=['Storage', 'Storage'], value=arg) return data @cached_property @@ -216,6 +265,28 @@ class ConfigGenerator: for text in lines: f.write(text + '\n') + @timer + def generate_stored(self): + import module.config.stored.classes as classes + gen = get_generator() + gen.add('from module.config.stored.classes import (') + with gen.tab(): + for cls in sorted([name for name in dir(classes) if name.startswith('Stored')]): + gen.add(cls + ',') + gen.add(')') + gen.Empty() + gen.Empty() + gen.Empty() + gen.CommentAutoGenerage('module/config/config_updater.py') + + with gen.Class('StoredGenerated'): + for path, data in deep_iter(self.args, depth=3): + cls = data.get('stored') + if cls: + gen.add(f'{path[-1]} = {cls}("{".".join(path)}")') + + gen.write('module/config/stored/stored_generated.py') + @timer def generate_i18n(self, lang): """ @@ -282,28 +353,65 @@ class ConfigGenerator: if dungeon.name in dailies: value = dungeon.__getattribute__(ingame_lang) deep_set(new, keys=['Dungeon', 'Name', dungeon.name], value=value) - # Copy dungeon i18n to double events - for dungeon in deep_get(new, keys='Dungeon.NameAtDoubleCalyx').values(): - if '_' in dungeon: - value = deep_get(new, keys=['Dungeon', 'Name', dungeon]) - if value: - deep_set(new, keys=['Dungeon', 'NameAtDoubleCalyx', dungeon], value=value) + # Copy dungeon i18n to double events + def update_dungeon_names(keys): + for dungeon in deep_get(new, keys=keys).values(): + if '_' in dungeon: + value = deep_get(new, keys=['Dungeon', 'Name', dungeon]) + if value: + deep_set(new, keys=f'{keys}.{dungeon}', value=value) + + update_dungeon_names('Dungeon.NameAtDoubleCalyx') + update_dungeon_names('Dungeon.NameAtDoubleRelic') + update_dungeon_names('DungeonDaily.CalyxGolden') + update_dungeon_names('DungeonDaily.CalyxCrimson') + update_dungeon_names('DungeonDaily.StagnantShadow') + update_dungeon_names('DungeonDaily.CavernOfCorrosion') + + # Character names from tasks.character.keywords import CharacterList ingame_lang = gui_lang_to_ingame_lang(lang) - characters = deep_get(self.argument, keys='Dungeon.SupportCharacter.option') + characters = deep_get(self.argument, keys='DungeonSupport.Character.option') for character in CharacterList.instances.values(): if character.name in characters: value = character.__getattribute__(ingame_lang) if "Trailblazer" in value: continue - deep_set(new, keys=['Dungeon', 'SupportCharacter', character.name], value=value) + deep_set(new, keys=['DungeonSupport', 'Character', character.name], value=value) + + # Daily quests + from tasks.daily.keywords import DailyQuest + for quest in DailyQuest.instances.values(): + value = quest.__getattribute__(ingame_lang) + deep_set(new, keys=['AchievableQuest', quest.name, 'name'], value=value) + # deep_set(new, keys=['DailyQuest', quest.name, 'help'], value='') + copy_from = 'Complete_1_Daily_Mission' + if quest.name != copy_from: + for option in deep_get(self.args, keys=['DailyQuest', 'AchievableQuest', copy_from, 'option']): + value = deep_get(new, keys=['AchievableQuest', copy_from, option]) + deep_set(new, keys=['AchievableQuest', quest.name, option], value=value) # GUI i18n for path, _ in deep_iter(self.gui, depth=2): group, key = path deep_load(keys=['Gui', group], words=(key,)) + # zh-TW + dic_repl = { + '設置': '設定', + '支持': '支援', + '啓': '啟', + '异': '異', + '服務器': '伺服器', + '文件': '檔案', + } + if lang == 'zh-TW': + for path, value in deep_iter(new, depth=3): + for before, after in dic_repl.items(): + value = value.replace(before, after) + deep_set(new, keys=path, value=value) + write_file(filepath_i18n(lang), new) @cached_property @@ -366,25 +474,6 @@ class ConfigGenerator: # update('template-docker', docker) # update('template-docker-cn', docker, cn) - def insert_dungeon(self): - from tasks.dungeon.keywords import DungeonList - dungeons = [dungeon.name for dungeon in DungeonList.instances.values() if dungeon.is_daily_dungeon] - deep_set(self.argument, keys='Dungeon.Name.option', value=dungeons) - deep_set(self.args, keys='Dungeon.Dungeon.Name.option', value=dungeons) - - from tasks.character.keywords import CharacterList - unsupported_characters = ["DanHengImbibitorLunae", 'Kafka', 'Luka'] - characters = ['FirstCharacter'] + [character.name for character in CharacterList.instances.values() if - character.name not in unsupported_characters] - deep_set(self.argument, keys='Dungeon.SupportCharacter.option', value=characters) - deep_set(self.args, keys='Dungeon.Dungeon.SupportCharacter.option', value=characters) - - dungeons = deep_get(self.argument, keys='Dungeon.NameAtDoubleCalyx.option') - dungeons += [dungeon.name for dungeon in DungeonList.instances.values() - if dungeon.is_Calyx_Golden or dungeon.is_Calyx_Crimson] - deep_set(self.argument, keys='Dungeon.NameAtDoubleCalyx.option', value=dungeons) - deep_set(self.args, keys='Dungeon.Dungeon.NameAtDoubleCalyx.option', value=dungeons) - def insert_assignment(self): from tasks.assignment.keywords import AssignmentEntry assignments = [entry.name for entry in AssignmentEntry.instances.values()] @@ -404,13 +493,13 @@ class ConfigGenerator: _ = self.args _ = self.menu # _ = self.event - self.insert_dungeon() self.insert_assignment() self.insert_package() # self.insert_server() write_file(filepath_args(), self.args) write_file(filepath_args('menu'), self.menu) self.generate_code() + self.generate_stored() for lang in LANGUAGES: self.generate_i18n(lang) self.generate_deploy_template() @@ -419,6 +508,8 @@ class ConfigGenerator: class ConfigUpdater: # source, target, (optional)convert_func redirection = [ + ('Dungeon.Dungeon.Support', 'Dungeon.DungeonSupport.Use'), + ('Dungeon.Dungeon.SupportCharacter', 'Dungeon.DungeonSupport.Character'), ] @cached_property @@ -439,7 +530,9 @@ class ConfigUpdater: def deep_load(keys): data = deep_get(self.args, keys=keys, default={}) value = deep_get(old, keys=keys, default=data['value']) - if is_template or value is None or value == '' or data['type'] == 'lock' or data.get('display') == 'hide': + typ = data['type'] + display = data.get('display') + if is_template or value is None or value == '' or typ == 'lock' or (display == 'hide' and typ != 'stored'): value = data['value'] value = parse_value(value, data=data) deep_set(new, keys=keys, value=value) @@ -449,6 +542,7 @@ class ConfigUpdater: if not is_template: new = self.config_redirect(old, new) + new = self.update_state(new) return new @@ -501,6 +595,53 @@ class ConfigUpdater: return new + @staticmethod + def update_state(data): + def set_daily(quest, value): + if value is True: + value = 'achievable' + if value is False: + value = 'not_set' + deep_set(data, keys=['DailyQuest', 'AchievableQuest', quest], value=value) + + set_daily('Complete_1_Daily_Mission', 'not_supported') + # Dungeon + dungeon = deep_get(data, keys='Dungeon.Scheduler.Enable') + set_daily('Clear_Calyx_Golden_1_times', + dungeon and deep_get(data, 'Dungeon.DungeonDaily.CalyxGolden') != 'do_not_achieve') + set_daily('Complete_Calyx_Crimson_1_time', + dungeon and deep_get(data, 'Dungeon.DungeonDaily.CalyxCrimson') != 'do_not_achieve') + set_daily('Clear_Stagnant_Shadow_1_times', + dungeon and deep_get(data, 'Dungeon.DungeonDaily.StagnantShadow') != 'do_not_achieve') + set_daily('Clear_Cavern_of_Corrosion_1_times', + dungeon and deep_get(data, 'Dungeon.DungeonDaily.CavernOfCorrosion') != 'do_not_achieve') + # Combat requirements + set_daily('In_a_single_battle_inflict_3_Weakness_Break_of_different_Types', 'not_supported') + set_daily('Inflict_Weakness_Break_5_times', 'not_supported') + set_daily('Defeat_a_total_of_20_enemies', 'not_supported') + set_daily('Enter_combat_by_attacking_enemy_Weakness_and_win_3_times', 'not_supported') + set_daily('Use_Technique_2_times', 'achievable') + # Other game systems + set_daily('Go_on_assignment_1_time', deep_get(data, 'Assignment.Scheduler.Enable')) + set_daily('Take_1_photo', 'achievable') + set_daily('Destroy_3_destructible_objects', 'not_supported') + set_daily('Complete_Forgotten_Hall_1_time', 'not_supported') + set_daily('Complete_Echo_of_War_1_times', 'not_supported') + set_daily('Complete_1_stage_in_Simulated_Universe_Any_world', 'not_supported') + set_daily('Obtain_victory_in_combat_with_support_characters_1_time', + dungeon and deep_get(data, 'Dungeon.DungeonSupport.Use') in ['when_daily', 'always_use']) + set_daily('Use_an_Ultimate_to_deal_the_final_blow_1_time', 'not_supported') + # Build + set_daily('Level_up_any_character_1_time', 'not_supported') + set_daily('Level_up_any_Light_Cone_1_time', 'not_supported') + set_daily('Level_up_any_Relic_1_time', 'not_supported') + # Items + set_daily('Salvage_any_Relic', 'achievable') + set_daily('Synthesize_Consumable_1_time', 'achievable') + set_daily('Synthesize_material_1_time', 'achievable') + set_daily('Use_Consumables_1_time', 'achievable') + return data + def read_file(self, config_name, is_template=False): """ Read and update config file. diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index d95753206..0190eaf82 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -234,6 +234,18 @@ "Calyx_Crimson_Harmony": "Trace: Harmony (Bud of Harmony)", "Calyx_Crimson_Nihility": "Trace: Nihility (Bud of Nihility)" }, + "NameAtDoubleRelic": { + "name": "At Double Relic Event, choose dungeon", + "help": "Return to the default dungeon settings after double times exhausted", + "do_not_participate": "Dont participate in event", + "Cavern_of_Corrosion_Path_of_Gelid_Wind": "Relics: Ice Set & Wind Set (Path of Gelid Wind)", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch": "Relics: Physical Set & Break Effect Set (Path of Jabbing Punch)", + "Cavern_of_Corrosion_Path_of_Drifting": "Relics: Healing Set & Musketeer Set (Path of Drifting)", + "Cavern_of_Corrosion_Path_of_Providence": "Relics: Guard Set & Quantum Set (Path of Providence)", + "Cavern_of_Corrosion_Path_of_Holy_Hymn": "Relics: DEF Set & Lighting Set (Path of Holy Hymn)", + "Cavern_of_Corrosion_Path_of_Conflagration": "Relics: Fire Set & Imaginary Set (Path of Conflagration)", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers": "Relics: HP Set & SPD Set (Path of Elixir Seekers)" + }, "Team": { "name": "Dungeon Team", "help": "", @@ -243,18 +255,77 @@ "4": "4", "5": "5", "6": "6" + } + }, + "DungeonDaily": { + "_info": { + "name": "Daily Quest Settings", + "help": "Clear required dungeon once to achieve daily quests" }, - "Support": { - "name": "Enable buddy support", - "help": "Whether to enable buddy support", - "do_not_use": "do_not_use", - "always_use": "always_use", - "when_daily": "when_daily" + "CalyxGolden": { + "name": "Clear Calyx Golden 1 times", + "help": "", + "do_not_achieve": "Don't Do This Quest", + "Calyx_Golden_Memories": "Material: Character EXP (Bud of Memories)", + "Calyx_Golden_Aether": "Material: Light Cone EXP (Bud of Aether)", + "Calyx_Golden_Treasures": "Material: Credit (Bud of Treasures)" }, - "SupportCharacter": { - "name": "Dungeon.SupportCharacter.name", - "help": "Dungeon.SupportCharacter.help", - "FirstCharacter": "FirstCharacter", + "CalyxCrimson": { + "name": "Clear Calyx Crimson 1 times", + "help": "", + "do_not_achieve": "Don't Do This Quest", + "Calyx_Crimson_Destruction": "Trace: Destruction (Bud of Destruction)", + "Calyx_Crimson_Preservation": "Trace: Preservation (Bud of Preservation)", + "Calyx_Crimson_Hunt": "Trace: Hunt (Bud of Hunt)", + "Calyx_Crimson_Abundance": "Trace: Abundance (Bud of Abundance)", + "Calyx_Crimson_Erudition": "Trace: Erudition (Bud of Erudition)", + "Calyx_Crimson_Harmony": "Trace: Harmony (Bud of Harmony)", + "Calyx_Crimson_Nihility": "Trace: Nihility (Bud of Nihility)" + }, + "StagnantShadow": { + "name": "Clear Stagnant Shadow 1 times", + "help": "", + "do_not_achieve": "Don't Do This Quest", + "Stagnant_Shadow_Quanta": "Ascension: Quantum (Shape of Quanta)", + "Stagnant_Shadow_Gust": "Ascension: Wind (Shape of Gust)", + "Stagnant_Shadow_Fulmination": "Ascension: Lighting (Shape of Fulmination)", + "Stagnant_Shadow_Blaze": "Ascension: Fire (Shape of Blaze)", + "Stagnant_Shadow_Spike": "Ascension: Physical (Shape of Spike)", + "Stagnant_Shadow_Rime": "Ascension: Ice (Shape of Rime)", + "Stagnant_Shadow_Mirage": "Ascension: Imaginary (Shape of Mirage)", + "Stagnant_Shadow_Icicle": "Ascension: Ice (Shape of Icicle)", + "Stagnant_Shadow_Doom": "Ascension: Lighting (Shape of Doom)", + "Stagnant_Shadow_Celestial": "Ascension: Wind (Shape of Celestial)" + }, + "CavernOfCorrosion": { + "name": "Clear Cavern of Corrosion 1 times", + "help": "", + "do_not_achieve": "Don't Do This Quest", + "Cavern_of_Corrosion_Path_of_Gelid_Wind": "Relics: Ice Set & Wind Set (Path of Gelid Wind)", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch": "Relics: Physical Set & Break Effect Set (Path of Jabbing Punch)", + "Cavern_of_Corrosion_Path_of_Drifting": "Relics: Healing Set & Musketeer Set (Path of Drifting)", + "Cavern_of_Corrosion_Path_of_Providence": "Relics: Guard Set & Quantum Set (Path of Providence)", + "Cavern_of_Corrosion_Path_of_Holy_Hymn": "Relics: DEF Set & Lighting Set (Path of Holy Hymn)", + "Cavern_of_Corrosion_Path_of_Conflagration": "Relics: Fire Set & Imaginary Set (Path of Conflagration)", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers": "Relics: HP Set & SPD Set (Path of Elixir Seekers)" + } + }, + "DungeonSupport": { + "_info": { + "name": "Support Settings", + "help": "" + }, + "Use": { + "name": "Use Friend Support", + "help": "", + "always_use": "Always Use", + "when_daily": "Use Only When Required by Dailies", + "do_not_use": "Don't Use" + }, + "Character": { + "name": "Support Character", + "help": "Select a friend support character, if not found, select the default (first) role", + "FirstCharacter": "First Character", "Arlan": "Arlan", "Asta": "Asta", "Bailu": "Bailu", @@ -267,6 +338,8 @@ "Himeko": "Himeko", "Hook": "Hook", "JingYuan": "Jing Yuan", + "Kafka": "Kafka", + "Luka": "Luka", "Luocha": "Luocha", "March7th": "March 7th", "Natasha": "Natasha", @@ -278,13 +351,218 @@ "SilverWolf": "Silver Wolf", "Sushang": "Sushang", "Tingyun": "Tingyun", - "TrailblazerDestruction": "TrailblazerDestruction", - "TrailblazerPreservation": "TrailblazerPreservation", + "TrailblazerDestruction": "Trailblazer Destruction", + "TrailblazerPreservation": "Trailblazer Preservation", "Welt": "Welt", "Yanqing": "Yanqing", "Yukong": "Yukong" } }, + "DungeonStorage": { + "_info": { + "name": "DungeonStorage._info.name", + "help": "DungeonStorage._info.help" + }, + "DungeonDouble": { + "name": "DungeonStorage.DungeonDouble.name", + "help": "DungeonStorage.DungeonDouble.help" + } + }, + "AchievableQuest": { + "_info": { + "name": "Achievable Quests", + "help": "When the task status is \"Not Set\", you need to configure the SRC as required to achieve the quest\nNote: Please keep more tasks in \"Achievable\" status, otherwise SRC may not be able to grind 500 activity" + }, + "Complete_1_Daily_Mission": { + "name": "Complete 1 Daily Mission", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Clear_Calyx_Golden_1_times": { + "name": "Clear Calyx (Golden) 1 time(s)", + "help": "Need to configure and enable the \"Dungeon\" task", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Complete_Calyx_Crimson_1_time": { + "name": "Complete Calyx (Crimson) 1 time", + "help": "Need to configure and enable the \"Dungeon\" task", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Clear_Stagnant_Shadow_1_times": { + "name": "Clear Stagnant Shadow 1 time(s)", + "help": "Need to configure and enable the \"Dungeon\" task", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Clear_Cavern_of_Corrosion_1_times": { + "name": "Clear Cavern of Corrosion 1 time(s)", + "help": "Need to configure and enable the \"Dungeon\" task", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "In_a_single_battle_inflict_3_Weakness_Break_of_different_Types": { + "name": "In a single battle, inflict 3 Weakness Break of different Types", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Inflict_Weakness_Break_5_times": { + "name": "Inflict Weakness Break 5 times", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Defeat_a_total_of_20_enemies": { + "name": "Defeat a total of 20 enemies", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Enter_combat_by_attacking_enemy_Weakness_and_win_3_times": { + "name": "Enter combat by attacking enemy's Weakness and win 3 times", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Use_Technique_2_times": { + "name": "Use Technique 2 times", + "help": "Achievable by default, will go to the abyssal 1 and use technique twice", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Go_on_assignment_1_time": { + "name": "Go on assignment 1 time", + "help": "Need to configure and enable the \"Assignment\" task", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Take_1_photo": { + "name": "Take 1 photo", + "help": "Achievable by default", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Destroy_3_destructible_objects": { + "name": "Destroy 3 destructible objects", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Complete_Forgotten_Hall_1_time": { + "name": "Complete Forgotten Hall 1 time", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Complete_Echo_of_War_1_times": { + "name": "Complete Echo of War 1 time(s)", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Complete_1_stage_in_Simulated_Universe_Any_world": { + "name": "Complete 1 stage in Simulated Universe (Any world)", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Obtain_victory_in_combat_with_support_characters_1_time": { + "name": "Obtain victory in combat with support characters 1 time", + "help": "Need to configure and enable the \"Dungeon\" task, configure support settings also", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Use_an_Ultimate_to_deal_the_final_blow_1_time": { + "name": "Use an Ultimate to deal the final blow 1 time", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Level_up_any_character_1_time": { + "name": "Level up any character 1 time", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Level_up_any_Light_Cone_1_time": { + "name": "Level up any Light Cone 1 time", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Level_up_any_Relic_1_time": { + "name": "Level up any Relic 1 time", + "help": "", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Salvage_any_Relic": { + "name": "Salvage any Relic", + "help": "Achievable by default, will salvage the first one in reverse order of rarity", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Synthesize_Consumable_1_time": { + "name": "Synthesize Consumable 1 time", + "help": "Achievable by default, will synthesize low-rarity snacks", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Synthesize_material_1_time": { + "name": "Synthesize material 1 time", + "help": "Achievable by default, will synthesize low-rarity material", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + }, + "Use_Consumables_1_time": { + "name": "Use Consumables 1 time", + "help": "Achievable by default, will use gear. If there is no material, synthesized before use", + "achievable": "Achievable", + "not_set": "Not Set", + "not_supported": "Not Supported Yet" + } + }, + "DailyStorage": { + "_info": { + "name": "DailyStorage._info.name", + "help": "DailyStorage._info.help" + }, + "DailyActivity": { + "name": "DailyStorage.DailyActivity.name", + "help": "DailyStorage.DailyActivity.help" + }, + "DailyQuest": { + "name": "DailyStorage.DailyQuest.name", + "help": "DailyStorage.DailyQuest.help" + } + }, "Assignment": { "_info": { "name": "Assignment Settings", diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index ce4740795..3fea7ce77 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -234,6 +234,18 @@ "Calyx_Crimson_Harmony": "疑似花萼(赤)・調和の蕾", "Calyx_Crimson_Nihility": "疑似花萼(赤)・虚無の蕾" }, + "NameAtDoubleRelic": { + "name": "Dungeon.NameAtDoubleRelic.name", + "help": "Dungeon.NameAtDoubleRelic.help", + "do_not_participate": "do_not_participate", + "Cavern_of_Corrosion_Path_of_Gelid_Wind": "侵蝕トンネル・霜風の路", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch": "侵蝕トンネル・迅拳の路", + "Cavern_of_Corrosion_Path_of_Drifting": "侵蝕トンネル・漂泊の路", + "Cavern_of_Corrosion_Path_of_Providence": "侵蝕トンネル・睿治の路", + "Cavern_of_Corrosion_Path_of_Holy_Hymn": "侵蝕トンネル・聖頌の路", + "Cavern_of_Corrosion_Path_of_Conflagration": "侵蝕トンネル・野焔の路", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers": "侵蝕トンネル・薬使の路" + }, "Team": { "name": "Dungeon.Team.name", "help": "Dungeon.Team.help", @@ -243,17 +255,76 @@ "4": "4", "5": "5", "6": "6" + } + }, + "DungeonDaily": { + "_info": { + "name": "DungeonDaily._info.name", + "help": "DungeonDaily._info.help" }, - "Support": { - "name": "Dungeon.Support.name", - "help": "Dungeon.Support.help", - "do_not_use": "do_not_use", + "CalyxGolden": { + "name": "DungeonDaily.CalyxGolden.name", + "help": "DungeonDaily.CalyxGolden.help", + "do_not_achieve": "do_not_achieve", + "Calyx_Golden_Memories": "疑似花萼(金)・回憶の蕾", + "Calyx_Golden_Aether": "疑似花萼(金)・エーテルの蕾", + "Calyx_Golden_Treasures": "疑似花萼(金)・秘蔵の蕾" + }, + "CalyxCrimson": { + "name": "DungeonDaily.CalyxCrimson.name", + "help": "DungeonDaily.CalyxCrimson.help", + "do_not_achieve": "do_not_achieve", + "Calyx_Crimson_Destruction": "疑似花萼(赤)・壊滅の蕾", + "Calyx_Crimson_Preservation": "疑似花萼(赤)・存護の蕾", + "Calyx_Crimson_Hunt": "疑似花萼(赤)・巡狩の蕾", + "Calyx_Crimson_Abundance": "疑似花萼(赤)・豊穣の蕾", + "Calyx_Crimson_Erudition": "疑似花萼(赤)・知恵の蕾", + "Calyx_Crimson_Harmony": "疑似花萼(赤)・調和の蕾", + "Calyx_Crimson_Nihility": "疑似花萼(赤)・虚無の蕾" + }, + "StagnantShadow": { + "name": "DungeonDaily.StagnantShadow.name", + "help": "DungeonDaily.StagnantShadow.help", + "do_not_achieve": "do_not_achieve", + "Stagnant_Shadow_Quanta": "凝結虚影・虚海の形", + "Stagnant_Shadow_Gust": "凝結虚影・薫風の形", + "Stagnant_Shadow_Fulmination": "凝結虚影・鳴雷の形", + "Stagnant_Shadow_Blaze": "凝結虚影・炎華の形", + "Stagnant_Shadow_Spike": "凝結虚影・切先の形", + "Stagnant_Shadow_Rime": "凝結虚影・霜晶の形", + "Stagnant_Shadow_Mirage": "凝結虚影・幻光の形", + "Stagnant_Shadow_Icicle": "凝結虚影・氷柱の形", + "Stagnant_Shadow_Doom": "凝結虚影・震厄の形", + "Stagnant_Shadow_Celestial": "凝結虚影・天人の形" + }, + "CavernOfCorrosion": { + "name": "DungeonDaily.CavernOfCorrosion.name", + "help": "DungeonDaily.CavernOfCorrosion.help", + "do_not_achieve": "do_not_achieve", + "Cavern_of_Corrosion_Path_of_Gelid_Wind": "侵蝕トンネル・霜風の路", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch": "侵蝕トンネル・迅拳の路", + "Cavern_of_Corrosion_Path_of_Drifting": "侵蝕トンネル・漂泊の路", + "Cavern_of_Corrosion_Path_of_Providence": "侵蝕トンネル・睿治の路", + "Cavern_of_Corrosion_Path_of_Holy_Hymn": "侵蝕トンネル・聖頌の路", + "Cavern_of_Corrosion_Path_of_Conflagration": "侵蝕トンネル・野焔の路", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers": "侵蝕トンネル・薬使の路" + } + }, + "DungeonSupport": { + "_info": { + "name": "DungeonSupport._info.name", + "help": "DungeonSupport._info.help" + }, + "Use": { + "name": "DungeonSupport.Use.name", + "help": "DungeonSupport.Use.help", "always_use": "always_use", - "when_daily": "when_daily" + "when_daily": "when_daily", + "do_not_use": "do_not_use" }, - "SupportCharacter": { - "name": "Dungeon.SupportCharacter.name", - "help": "Dungeon.SupportCharacter.help", + "Character": { + "name": "DungeonSupport.Character.name", + "help": "DungeonSupport.Character.help", "FirstCharacter": "FirstCharacter", "Arlan": "アーラン", "Asta": "アスター", @@ -267,6 +338,8 @@ "Himeko": "姫子", "Hook": "フック", "JingYuan": "景元", + "Kafka": "カフカ", + "Luka": "ルカ", "Luocha": "羅刹", "March7th": "三月なのか", "Natasha": "ナターシャ", @@ -285,6 +358,211 @@ "Yukong": "御空" } }, + "DungeonStorage": { + "_info": { + "name": "DungeonStorage._info.name", + "help": "DungeonStorage._info.help" + }, + "DungeonDouble": { + "name": "DungeonStorage.DungeonDouble.name", + "help": "DungeonStorage.DungeonDouble.help" + } + }, + "AchievableQuest": { + "_info": { + "name": "AchievableQuest._info.name", + "help": "AchievableQuest._info.help" + }, + "Complete_1_Daily_Mission": { + "name": "デイリークエストを1回クリアする", + "help": "AchievableQuest.Complete_1_Daily_Mission.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Clear_Calyx_Golden_1_times": { + "name": "「疑似花萼(金)」を1回クリアする", + "help": "AchievableQuest.Clear_Calyx_Golden_1_times.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Complete_Calyx_Crimson_1_time": { + "name": "「疑似花萼(赤)」を1回クリアする", + "help": "AchievableQuest.Complete_Calyx_Crimson_1_time.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Clear_Stagnant_Shadow_1_times": { + "name": "「凝結虚影」を1回クリアする", + "help": "AchievableQuest.Clear_Stagnant_Shadow_1_times.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Clear_Cavern_of_Corrosion_1_times": { + "name": "「侵蝕トンネル」を1回クリアする", + "help": "AchievableQuest.Clear_Cavern_of_Corrosion_1_times.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "In_a_single_battle_inflict_3_Weakness_Break_of_different_Types": { + "name": "一度の戦闘で、異なる3種の属性の弱点撃破を発動する", + "help": "AchievableQuest.In_a_single_battle_inflict_3_Weakness_Break_of_different_Types.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Inflict_Weakness_Break_5_times": { + "name": "累計で弱点撃破効果を5回発動する", + "help": "AchievableQuest.Inflict_Weakness_Break_5_times.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Defeat_a_total_of_20_enemies": { + "name": "敵を累計で20体倒す", + "help": "AchievableQuest.Defeat_a_total_of_20_enemies.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Enter_combat_by_attacking_enemy_Weakness_and_win_3_times": { + "name": "弱点を攻撃して戦闘に入り、3回勝利する", + "help": "AchievableQuest.Enter_combat_by_attacking_enemy_Weakness_and_win_3_times.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Use_Technique_2_times": { + "name": "秘技を累計2回発動する", + "help": "AchievableQuest.Use_Technique_2_times.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Go_on_assignment_1_time": { + "name": "依頼に1回派遣する", + "help": "AchievableQuest.Go_on_assignment_1_time.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Take_1_photo": { + "name": "1回撮影する", + "help": "AchievableQuest.Take_1_photo.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Destroy_3_destructible_objects": { + "name": "破壊できるオブジェクトを累計で3つ破壊する", + "help": "AchievableQuest.Destroy_3_destructible_objects.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Complete_Forgotten_Hall_1_time": { + "name": "「忘却の庭」を1回クリアする", + "help": "AchievableQuest.Complete_Forgotten_Hall_1_time.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Complete_Echo_of_War_1_times": { + "name": "「歴戦余韻」を1回クリアする", + "help": "AchievableQuest.Complete_Echo_of_War_1_times.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Complete_1_stage_in_Simulated_Universe_Any_world": { + "name": "「模擬宇宙」のエリアを1つクリアする(任意の世界)", + "help": "AchievableQuest.Complete_1_stage_in_Simulated_Universe_Any_world.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Obtain_victory_in_combat_with_support_characters_1_time": { + "name": "サポートキャラを使い、戦闘に1回勝利する", + "help": "AchievableQuest.Obtain_victory_in_combat_with_support_characters_1_time.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Use_an_Ultimate_to_deal_the_final_blow_1_time": { + "name": "必殺技で最後の一撃を1回与える", + "help": "AchievableQuest.Use_an_Ultimate_to_deal_the_final_blow_1_time.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Level_up_any_character_1_time": { + "name": "任意のキャラを1回レベルアップする", + "help": "AchievableQuest.Level_up_any_character_1_time.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Level_up_any_Light_Cone_1_time": { + "name": "任意の光円錐を1回レベルアップする", + "help": "AchievableQuest.Level_up_any_Light_Cone_1_time.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Level_up_any_Relic_1_time": { + "name": "任意の遺物を1回レベルアップする", + "help": "AchievableQuest.Level_up_any_Relic_1_time.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Salvage_any_Relic": { + "name": "任意の遺物1つを分解する", + "help": "AchievableQuest.Salvage_any_Relic.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Synthesize_Consumable_1_time": { + "name": "消耗品を1回合成する", + "help": "AchievableQuest.Synthesize_Consumable_1_time.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Synthesize_material_1_time": { + "name": "素材を1回合成する", + "help": "AchievableQuest.Synthesize_material_1_time.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + }, + "Use_Consumables_1_time": { + "name": "消耗品を1個使う", + "help": "AchievableQuest.Use_Consumables_1_time.help", + "achievable": "achievable", + "not_set": "not_set", + "not_supported": "not_supported" + } + }, + "DailyStorage": { + "_info": { + "name": "DailyStorage._info.name", + "help": "DailyStorage._info.help" + }, + "DailyActivity": { + "name": "DailyStorage.DailyActivity.name", + "help": "DailyStorage.DailyActivity.help" + }, + "DailyQuest": { + "name": "DailyStorage.DailyQuest.name", + "help": "DailyStorage.DailyQuest.help" + } + }, "Assignment": { "_info": { "name": "依頼設定", diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 9ffe63e2b..eecc3cd69 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -35,7 +35,7 @@ "help": "" }, "Assignment": { - "name": "委托设置", + "name": "委托", "help": "" }, "Rogue": { @@ -217,7 +217,7 @@ "Cavern_of_Corrosion_Path_of_Providence": "遗器:铁卫套+量子套(睿治之径•侵蚀隧洞)", "Cavern_of_Corrosion_Path_of_Holy_Hymn": "遗器:防御套+雷套(圣颂之径•侵蚀隧洞)", "Cavern_of_Corrosion_Path_of_Conflagration": "遗器:火套+虚数套(野焰之径•侵蚀隧洞)", - "Cavern_of_Corrosion_Path_of_Elixir_Seekers": "遗器:生命套+速度套 (药使之径•侵蚀隧洞)" + "Cavern_of_Corrosion_Path_of_Elixir_Seekers": "遗器:生命套+速度套(药使之径•侵蚀隧洞)" }, "NameAtDoubleCalyx": { "name": "有双倍花活动时,选择副本", @@ -234,6 +234,18 @@ "Calyx_Crimson_Harmony": "行迹材料:同谐(同谐之蕾•拟造花萼赤)", "Calyx_Crimson_Nihility": "行迹材料:虚无(虚无之蕾•拟造花萼赤)" }, + "NameAtDoubleRelic": { + "name": "有遗器活动时,选择副本", + "help": "次数耗尽后回退到默认打本设置", + "do_not_participate": "不参与活动", + "Cavern_of_Corrosion_Path_of_Gelid_Wind": "遗器:冰套+风套(霜风之径•侵蚀隧洞)", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch": "遗器:物理套+击破套(迅拳之径•侵蚀隧洞)", + "Cavern_of_Corrosion_Path_of_Drifting": "遗器:治疗套+快枪手(漂泊之径•侵蚀隧洞)", + "Cavern_of_Corrosion_Path_of_Providence": "遗器:铁卫套+量子套(睿治之径•侵蚀隧洞)", + "Cavern_of_Corrosion_Path_of_Holy_Hymn": "遗器:防御套+雷套(圣颂之径•侵蚀隧洞)", + "Cavern_of_Corrosion_Path_of_Conflagration": "遗器:火套+虚数套(野焰之径•侵蚀隧洞)", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers": "遗器:生命套+速度套(药使之径•侵蚀隧洞)" + }, "Team": { "name": "打本队伍", "help": "", @@ -243,15 +255,74 @@ "4": "4", "5": "5", "6": "6" + } + }, + "DungeonDaily": { + "_info": { + "name": "每日任务设置", + "help": "打一次特定的本,以满足每日任务的要求" }, - "Support": { - "name": "启用好友支援", - "help": "是否启用好友支援", - "do_not_use": "否", - "always_use": "是", - "when_daily": "仅当每日任务需要时" + "CalyxGolden": { + "name": "完成1次拟造花萼(金)", + "help": "", + "do_not_achieve": "不完成这个任务", + "Calyx_Golden_Memories": "材料:角色经验(回忆之蕾•拟造花萼金)", + "Calyx_Golden_Aether": "材料:武器经验(以太之蕾•拟造花萼金)", + "Calyx_Golden_Treasures": "材料:信用点(藏珍之蕾•拟造花萼金)" }, - "SupportCharacter": { + "CalyxCrimson": { + "name": "完成1次拟造花萼(赤)", + "help": "", + "do_not_achieve": "不完成这个任务", + "Calyx_Crimson_Destruction": "行迹材料:毁灭(毁灭之蕾•拟造花萼赤)", + "Calyx_Crimson_Preservation": "行迹材料:存护(存护之蕾•拟造花萼赤)", + "Calyx_Crimson_Hunt": "行迹材料:巡猎(存护之蕾•拟造花萼赤)", + "Calyx_Crimson_Abundance": "行迹材料:丰饶(丰饶之蕾•拟造花萼赤)", + "Calyx_Crimson_Erudition": "行迹材料:智识(智识之蕾•拟造花萼赤)", + "Calyx_Crimson_Harmony": "行迹材料:同谐(同谐之蕾•拟造花萼赤)", + "Calyx_Crimson_Nihility": "行迹材料:虚无(虚无之蕾•拟造花萼赤)" + }, + "StagnantShadow": { + "name": "完成1次凝滞虚影", + "help": "", + "do_not_achieve": "不完成这个任务", + "Stagnant_Shadow_Quanta": "角色晋阶材料:量子(空海之形•凝滞虚影)", + "Stagnant_Shadow_Gust": "角色晋阶材料:风(巽风之形•凝滞虚影)", + "Stagnant_Shadow_Fulmination": "角色晋阶材料:雷(鸣雷之形•凝滞虚影)", + "Stagnant_Shadow_Blaze": "角色晋阶材料:火(炎华之形•凝滞虚影)", + "Stagnant_Shadow_Spike": "角色晋阶材料:物理(锋芒之形•凝滞虚影)", + "Stagnant_Shadow_Rime": "角色晋阶材料:冰(霜晶之形•凝滞虚影)", + "Stagnant_Shadow_Mirage": "角色晋阶材料:虚数(幻光之形•凝滞虚影)", + "Stagnant_Shadow_Icicle": "角色晋阶材料:冰(冰棱之形•凝滞虚影)", + "Stagnant_Shadow_Doom": "角色晋阶材料:雷(震厄之形•凝滞虚影)", + "Stagnant_Shadow_Celestial": "角色晋阶材料:风(天人之形•凝滞虚影)" + }, + "CavernOfCorrosion": { + "name": "完成1次侵蚀隧洞", + "help": "", + "do_not_achieve": "不完成这个任务", + "Cavern_of_Corrosion_Path_of_Gelid_Wind": "遗器:冰套+风套(霜风之径•侵蚀隧洞)", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch": "遗器:物理套+击破套(迅拳之径•侵蚀隧洞)", + "Cavern_of_Corrosion_Path_of_Drifting": "遗器:治疗套+快枪手(漂泊之径•侵蚀隧洞)", + "Cavern_of_Corrosion_Path_of_Providence": "遗器:铁卫套+量子套(睿治之径•侵蚀隧洞)", + "Cavern_of_Corrosion_Path_of_Holy_Hymn": "遗器:防御套+雷套(圣颂之径•侵蚀隧洞)", + "Cavern_of_Corrosion_Path_of_Conflagration": "遗器:火套+虚数套(野焰之径•侵蚀隧洞)", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers": "遗器:生命套+速度套(药使之径•侵蚀隧洞)" + } + }, + "DungeonSupport": { + "_info": { + "name": "支援设置", + "help": "" + }, + "Use": { + "name": "使用好友支援", + "help": "", + "always_use": "总是使用", + "when_daily": "仅当每日任务需要时使用", + "do_not_use": "不使用" + }, + "Character": { "name": "好友支援角色", "help": "选择好友支援角色,未找到则选择默认(第一个)角色", "FirstCharacter": "支援列表第一个角色", @@ -267,6 +338,8 @@ "Himeko": "姬子", "Hook": "虎克", "JingYuan": "景元", + "Kafka": "卡芙卡", + "Luka": "卢卡", "Luocha": "罗刹", "March7th": "三月七", "Natasha": "娜塔莎", @@ -285,6 +358,211 @@ "Yukong": "驭空" } }, + "DungeonStorage": { + "_info": { + "name": "DungeonStorage._info.name", + "help": "DungeonStorage._info.help" + }, + "DungeonDouble": { + "name": "DungeonStorage.DungeonDouble.name", + "help": "DungeonStorage.DungeonDouble.help" + } + }, + "AchievableQuest": { + "_info": { + "name": "可完成的任务", + "help": "任务状态为 \"未设置\" 时需要按照要求设置SRC,才能启用任务\n注意:请让更多的任务处于 \"可完成\" 状态,否则SRC可能无法完成500点的活跃度要求" + }, + "Complete_1_Daily_Mission": { + "name": "完成1个日常任务", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Clear_Calyx_Golden_1_times": { + "name": "完成1次「拟造花萼(金)」", + "help": "需要设置并启用\"每日副本\"任务", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Complete_Calyx_Crimson_1_time": { + "name": "完成1次「拟造花萼(赤)」", + "help": "需要设置并启用\"每日副本\"任务", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Clear_Stagnant_Shadow_1_times": { + "name": "完成1次「凝滞虚影」", + "help": "需要设置并启用\"每日副本\"任务", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Clear_Cavern_of_Corrosion_1_times": { + "name": "完成1次「侵蚀隧洞」", + "help": "需要设置并启用\"每日副本\"任务", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "In_a_single_battle_inflict_3_Weakness_Break_of_different_Types": { + "name": "单场战斗中,触发3种不同属性的弱点击破", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Inflict_Weakness_Break_5_times": { + "name": "累计触发弱点击破效果5次", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Defeat_a_total_of_20_enemies": { + "name": "累计消灭20个敌人", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Enter_combat_by_attacking_enemy_Weakness_and_win_3_times": { + "name": "利用弱点进入战斗并获胜3次", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Use_Technique_2_times": { + "name": "累计施放2次秘技", + "help": "默认可完成,将前往深渊一施放2次秘技", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Go_on_assignment_1_time": { + "name": "派遣1次委托", + "help": "需要设置并启用\"委托\"任务", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Take_1_photo": { + "name": "拍照1次", + "help": "默认可完成", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Destroy_3_destructible_objects": { + "name": "累计击碎3个可破坏物", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Complete_Forgotten_Hall_1_time": { + "name": "完成1次「忘却之庭」", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Complete_Echo_of_War_1_times": { + "name": "完成1次「历战余响」", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Complete_1_stage_in_Simulated_Universe_Any_world": { + "name": "通关「模拟宇宙」(任意世界)的1个区域", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Obtain_victory_in_combat_with_support_characters_1_time": { + "name": "使用支援角色并获得战斗胜利1次", + "help": "需要设置并启用\"每日副本\",且设置好友支援", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Use_an_Ultimate_to_deal_the_final_blow_1_time": { + "name": "施放终结技造成制胜一击1次", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Level_up_any_character_1_time": { + "name": "将任意角色等级提升1次", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Level_up_any_Light_Cone_1_time": { + "name": "将任意光锥等级提升1次", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Level_up_any_Relic_1_time": { + "name": "将任意遗器等级提升1次", + "help": "", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Salvage_any_Relic": { + "name": "分解任意1件遗器", + "help": "默认可完成,将分解遗器稀有度倒序的第一个", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Synthesize_Consumable_1_time": { + "name": "合成1次消耗品", + "help": "默认可完成,将合成最低级零食", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Synthesize_material_1_time": { + "name": "合成1次材料", + "help": "默认可完成,将合成最低级材料", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + }, + "Use_Consumables_1_time": { + "name": "使用1件消耗品", + "help": "默认可完成,将使用护具,无材料时先合成再使用", + "achievable": "可完成", + "not_set": "未设置", + "not_supported": "暂未支持" + } + }, + "DailyStorage": { + "_info": { + "name": "DailyStorage._info.name", + "help": "DailyStorage._info.help" + }, + "DailyActivity": { + "name": "DailyStorage.DailyActivity.name", + "help": "DailyStorage.DailyActivity.help" + }, + "DailyQuest": { + "name": "DailyStorage.DailyQuest.name", + "help": "DailyStorage.DailyQuest.help" + } + }, "Assignment": { "_info": { "name": "委托设置", diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 00e90cc18..09c2a8dc1 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -35,7 +35,7 @@ "help": "" }, "Assignment": { - "name": "委託設置", + "name": "委託", "help": "" }, "Rogue": { @@ -72,7 +72,7 @@ }, "Serial": { "name": "模擬器 Serial", - "help": "常見的模擬器 Serial 可以查詢下方列表\n填 \"auto\" 自動檢測模擬器,多個模擬器正在運行或使用不支持自動檢測的模擬器時無法使用 \"auto\",必須手動填寫\n模擬器預設 Serial:\n- 藍疊模擬器 127.0.0.1:5555\n- 藍疊模擬器4 Hyper-v版,填\"bluestacks4-hyperv\"自動連接,多開填\"bluestacks4-hyperv-2\"以此類推\n- 藍疊模擬器5 Hyper-v版,填\"bluestacks5-hyperv\"自動連接,多開填\"bluestacks5-hyperv-1\"以此類推\n- 夜神模擬器 127.0.0.1:62001\n- 夜神模擬器64位元 127.0.0.1:59865\n- MuMu模擬器/MuMu模擬器X 127.0.0.1:7555\n- MuMu模擬器12 127.0.0.1:16384\n- 逍遙模擬器 127.0.0.1:21503\n- 雷電模擬器 emulator-5554 或 127.0.0.1:5555\n- WSA,填\"wsa-0\"使遊戲在後臺運行,需要使用第三方軟件操控或關閉\n如果你使用了模擬器的多開功能,他們的 Serial 將不是預設的,可以在 console.bat 中執行 `adb devices` 查詢,或根據模擬器官方的教程填寫" + "help": "常見的模擬器 Serial 可以查詢下方列表\n填 \"auto\" 自動檢測模擬器,多個模擬器正在運行或使用不支援自動檢測的模擬器時無法使用 \"auto\",必須手動填寫\n模擬器預設 Serial:\n- 藍疊模擬器 127.0.0.1:5555\n- 藍疊模擬器4 Hyper-v版,填\"bluestacks4-hyperv\"自動連接,多開填\"bluestacks4-hyperv-2\"以此類推\n- 藍疊模擬器5 Hyper-v版,填\"bluestacks5-hyperv\"自動連接,多開填\"bluestacks5-hyperv-1\"以此類推\n- 夜神模擬器 127.0.0.1:62001\n- 夜神模擬器64位元 127.0.0.1:59865\n- MuMu模擬器/MuMu模擬器X 127.0.0.1:7555\n- MuMu模擬器12 127.0.0.1:16384\n- 逍遙模擬器 127.0.0.1:21503\n- 雷電模擬器 emulator-5554 或 127.0.0.1:5555\n- WSA,填\"wsa-0\"使遊戲在後臺運行,需要使用第三方軟件操控或關閉\n如果你使用了模擬器的多開功能,他們的 Serial 將不是預設的,可以在 console.bat 中執行 `adb devices` 查詢,或根據模擬器官方的教程填寫" }, "PackageName": { "name": "遊戲伺服器", @@ -108,7 +108,7 @@ }, "EmulatorInfo": { "_info": { - "name": "模擬器設置", + "name": "模擬器設定", "help": "下列數值是根據Serial自動填充的,如果不懂請不要隨意修改" }, "Emulator": { @@ -159,7 +159,7 @@ }, "OnePushConfig": { "name": "錯誤推送設定", - "help": "發生無法處理的异常後,使用 Onepush 推送错误消息。設定參考文檔:https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/Onepush-configuration-%5BCN%5D" + "help": "發生無法處理的異常後,使用 Onepush 推送错误消息。設定參考文檔:https://github.com/LmeSzinc/AzurLaneAutoScript/wiki/Onepush-configuration-%5BCN%5D" } }, "Optimization": { @@ -190,7 +190,7 @@ }, "Name": { "name": "副本名稱", - "help": "默認打本設置", + "help": "默認打本設定", "Calyx_Golden_Memories": "材料:角色經驗(回憶之蕾•擬造花萼金)", "Calyx_Golden_Aether": "材料:武器經驗(乙太之蕾•擬造花萼金)", "Calyx_Golden_Treasures": "材料:信用點(藏珍之蕾•擬造花萼金)", @@ -221,7 +221,7 @@ }, "NameAtDoubleCalyx": { "name": "有雙倍花活動時,選擇副本", - "help": "次數耗儘後回退到默認打本設置", + "help": "次數耗儘後回退到默認打本設定", "do_not_participate": "不參與活動", "Calyx_Golden_Memories": "材料:角色經驗(回憶之蕾•擬造花萼金)", "Calyx_Golden_Aether": "材料:武器經驗(乙太之蕾•擬造花萼金)", @@ -234,6 +234,18 @@ "Calyx_Crimson_Harmony": "行跡材料:同諧(同諧之蕾•擬造花萼赤)", "Calyx_Crimson_Nihility": "行跡材料:虛無(虛無之蕾•擬造花萼赤)" }, + "NameAtDoubleRelic": { + "name": "有遺器活動時,選擇副本", + "help": "次數耗儘後回退到默認打本設定", + "do_not_participate": "不參與活動", + "Cavern_of_Corrosion_Path_of_Gelid_Wind": "遺器:冰套+風套(霜風之徑•侵蝕隧洞)", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch": "遺器:物理套+擊破套(迅拳之徑•侵蝕隧洞)", + "Cavern_of_Corrosion_Path_of_Drifting": "遺器:治療套+快槍手(漂泊之徑•侵蝕隧洞)", + "Cavern_of_Corrosion_Path_of_Providence": "遺器:鐵衛套+量子套(睿治之徑•侵蝕隧洞)", + "Cavern_of_Corrosion_Path_of_Holy_Hymn": "遺器:防禦套+雷套(聖頌之徑•侵蝕隧洞)", + "Cavern_of_Corrosion_Path_of_Conflagration": "遺器:火套+虛數套(野焰之徑•侵蝕隧洞)", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers": "遺器:生命套+速度套(藥使之徑•侵蝕隧洞)" + }, "Team": { "name": "打本隊伍", "help": "", @@ -243,18 +255,77 @@ "4": "4", "5": "5", "6": "6" + } + }, + "DungeonDaily": { + "_info": { + "name": "每日任務設定", + "help": "打一次特定的本,以滿足每日任務的要求" }, - "Support": { - "name": "Dungeon.Support.name", - "help": "Dungeon.Support.help", - "do_not_use": "do_not_use", - "always_use": "always_use", - "when_daily": "when_daily" + "CalyxGolden": { + "name": "完成1次擬造花萼(金)", + "help": "", + "do_not_achieve": "不完成這個任務", + "Calyx_Golden_Memories": "材料:角色經驗(回憶之蕾•擬造花萼金)", + "Calyx_Golden_Aether": "材料:武器經驗(乙太之蕾•擬造花萼金)", + "Calyx_Golden_Treasures": "材料:信用點(藏珍之蕾•擬造花萼金)" }, - "SupportCharacter": { - "name": "Dungeon.SupportCharacter.name", - "help": "Dungeon.SupportCharacter.help", - "FirstCharacter": "FirstCharacter", + "CalyxCrimson": { + "name": "完成1次擬造花萼(赤)", + "help": "", + "do_not_achieve": "不完成這個任務", + "Calyx_Crimson_Destruction": "行跡材料:毀滅(毀滅之蕾•擬造花萼赤)", + "Calyx_Crimson_Preservation": "行跡材料:存護(存護之蕾•擬造花萼赤)", + "Calyx_Crimson_Hunt": "行跡材料:巡獵(存護之蕾•擬造花萼赤)", + "Calyx_Crimson_Abundance": "行跡材料:豐饒(豐饒之蕾•擬造花萼赤)", + "Calyx_Crimson_Erudition": "行跡材料:智識(智識之蕾•擬造花萼赤)", + "Calyx_Crimson_Harmony": "行跡材料:同諧(同諧之蕾•擬造花萼赤)", + "Calyx_Crimson_Nihility": "行跡材料:虛無(虛無之蕾•擬造花萼赤)" + }, + "StagnantShadow": { + "name": "完成1次凝滯虛影", + "help": "", + "do_not_achieve": "不完成這個任務", + "Stagnant_Shadow_Quanta": "角色晉階材料:量子(空海之形•凝滯虛影)", + "Stagnant_Shadow_Gust": "角色晉階材料:風(巽風之形•凝滯虛影)", + "Stagnant_Shadow_Fulmination": "角色晉階材料:雷(鳴雷之形•凝滯虛影)", + "Stagnant_Shadow_Blaze": "角色晉階材料:火(炎華之形•凝滯虛影)", + "Stagnant_Shadow_Spike": "角色晉階材料:物理(鋒芒之形•凝滯虛影)", + "Stagnant_Shadow_Rime": "角色晉階材料:冰(霜晶之形•凝滯虛影)", + "Stagnant_Shadow_Mirage": "角色晉階材料:虛數(幻光之形•凝滯虛影)", + "Stagnant_Shadow_Icicle": "角色晉階材料:冰(冰稜之形•凝滯虛影)", + "Stagnant_Shadow_Doom": "角色晉階材料:雷(震厄之形•凝滯虛影)", + "Stagnant_Shadow_Celestial": "角色晉階材料:風(天人之形•凝滯虛影)" + }, + "CavernOfCorrosion": { + "name": "完成1次侵蝕隧洞", + "help": "", + "do_not_achieve": "不完成這個任務", + "Cavern_of_Corrosion_Path_of_Gelid_Wind": "遺器:冰套+風套(霜風之徑•侵蝕隧洞)", + "Cavern_of_Corrosion_Path_of_Jabbing_Punch": "遺器:物理套+擊破套(迅拳之徑•侵蝕隧洞)", + "Cavern_of_Corrosion_Path_of_Drifting": "遺器:治療套+快槍手(漂泊之徑•侵蝕隧洞)", + "Cavern_of_Corrosion_Path_of_Providence": "遺器:鐵衛套+量子套(睿治之徑•侵蝕隧洞)", + "Cavern_of_Corrosion_Path_of_Holy_Hymn": "遺器:防禦套+雷套(聖頌之徑•侵蝕隧洞)", + "Cavern_of_Corrosion_Path_of_Conflagration": "遺器:火套+虛數套(野焰之徑•侵蝕隧洞)", + "Cavern_of_Corrosion_Path_of_Elixir_Seekers": "遺器:生命套+速度套(藥使之徑•侵蝕隧洞)" + } + }, + "DungeonSupport": { + "_info": { + "name": "支援設定", + "help": "" + }, + "Use": { + "name": "使用好友支援", + "help": "", + "always_use": "總是使用", + "when_daily": "僅當每日任務需要時使用", + "do_not_use": "不使用" + }, + "Character": { + "name": "好友支援角色", + "help": "選擇好友支援角色,未找到則選擇默認(第一個)角色", + "FirstCharacter": "支援列表第一個角色", "Arlan": "阿蘭", "Asta": "艾絲妲", "Bailu": "白露", @@ -267,6 +338,8 @@ "Himeko": "姬子", "Hook": "虎克", "JingYuan": "景元", + "Kafka": "卡芙卡", + "Luka": "盧卡", "Luocha": "羅剎", "March7th": "三月七", "Natasha": "娜塔莎", @@ -278,16 +351,221 @@ "SilverWolf": "銀狼", "Sushang": "素裳", "Tingyun": "停雲", - "TrailblazerDestruction": "TrailblazerDestruction", - "TrailblazerPreservation": "TrailblazerPreservation", + "TrailblazerDestruction": "開拓者•毀滅", + "TrailblazerPreservation": "開拓者•存護", "Welt": "瓦爾特", "Yanqing": "彥卿", "Yukong": "馭空" } }, + "DungeonStorage": { + "_info": { + "name": "DungeonStorage._info.name", + "help": "DungeonStorage._info.help" + }, + "DungeonDouble": { + "name": "DungeonStorage.DungeonDouble.name", + "help": "DungeonStorage.DungeonDouble.help" + } + }, + "AchievableQuest": { + "_info": { + "name": "可完成的任務", + "help": "任務狀態為 \"未設定\" 時需要按照要求設定SRC,才能啟用任務\n注意:請讓更多的任務處於 \"可完成\" 狀態,否則SRC可能無法完成500點的活躍度要求" + }, + "Complete_1_Daily_Mission": { + "name": "完成1個每日任務", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Clear_Calyx_Golden_1_times": { + "name": "完成1次「擬造花萼(金)」", + "help": "需要設定並啟用\"每日副本\"任務", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Complete_Calyx_Crimson_1_time": { + "name": "完成1次「擬造花萼(赤)」", + "help": "需要設定並啟用\"每日副本\"任務", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Clear_Stagnant_Shadow_1_times": { + "name": "完成1次「凝滯虛影」", + "help": "需要設定並啟用\"每日副本\"任務", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Clear_Cavern_of_Corrosion_1_times": { + "name": "完成1次「侵蝕隧洞」", + "help": "需要設定並啟用\"每日副本\"任務", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "In_a_single_battle_inflict_3_Weakness_Break_of_different_Types": { + "name": "單場戰鬥中,觸發3種不同屬性的弱點擊破", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Inflict_Weakness_Break_5_times": { + "name": "累積觸發弱點擊破效果5次", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Defeat_a_total_of_20_enemies": { + "name": "累積消滅20個敵人", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Enter_combat_by_attacking_enemy_Weakness_and_win_3_times": { + "name": "利用弱點進入戰鬥並獲勝3次", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Use_Technique_2_times": { + "name": "累積施放2次秘技", + "help": "默認可完成,將前往深淵一施放2次秘技", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Go_on_assignment_1_time": { + "name": "派遣1次委託", + "help": "需要設定並啟用\"委託\"任務", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Take_1_photo": { + "name": "拍照1次", + "help": "默认可完成", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Destroy_3_destructible_objects": { + "name": "累積擊碎3個可破壞物", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Complete_Forgotten_Hall_1_time": { + "name": "完成1次「忘卻之庭」", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Complete_Echo_of_War_1_times": { + "name": "完成1次「歷戰餘響」", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Complete_1_stage_in_Simulated_Universe_Any_world": { + "name": "完成「模擬宇宙」任意世界的1個區域", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Obtain_victory_in_combat_with_support_characters_1_time": { + "name": "使用支援角色並獲得戰鬥勝利1次", + "help": "需要設定並啟用\"每日副本\",且設並好友支援", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Use_an_Ultimate_to_deal_the_final_blow_1_time": { + "name": "施放終結技造成制勝一擊1次", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Level_up_any_character_1_time": { + "name": "將任意角色等級提升1次", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Level_up_any_Light_Cone_1_time": { + "name": "將任意光錐等級提升1次", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Level_up_any_Relic_1_time": { + "name": "將任意遺器等級提升1次", + "help": "", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Salvage_any_Relic": { + "name": "分解任意1件遺器", + "help": "默認可完成,將分解遺器稀有度倒序的第一個", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Synthesize_Consumable_1_time": { + "name": "合成1次消耗品", + "help": "默認可完成,將合成最低級零食", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Synthesize_material_1_time": { + "name": "合成1次素材", + "help": "默認可完成,將合成最低級素材", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + }, + "Use_Consumables_1_time": { + "name": "使用1件消耗品", + "help": "默認可完成,將使用護具,無材料時先合成再使用", + "achievable": "可完成", + "not_set": "未設定", + "not_supported": "暫未支援" + } + }, + "DailyStorage": { + "_info": { + "name": "DailyStorage._info.name", + "help": "DailyStorage._info.help" + }, + "DailyActivity": { + "name": "DailyStorage.DailyActivity.name", + "help": "DailyStorage.DailyActivity.help" + }, + "DailyQuest": { + "name": "DailyStorage.DailyQuest.name", + "help": "DailyStorage.DailyQuest.help" + } + }, "Assignment": { "_info": { - "name": "委託設置", + "name": "委託設定", "help": "領取獎勵並派遣,優先處理指定委託\n若處理指定委託之後未達到上限,則按經驗材料 → 角色專屬素材 → 合成材料的順序來派遣委託" }, "Duration": { @@ -509,17 +787,17 @@ "UpdateStart": "開始更新", "UpdateWait": "等待所有 Alas 完成當前任務", "UpdateRun": "更新中", - "UpdateSuccess": "更新成功,正在重啓", + "UpdateSuccess": "更新成功,正在重啟", "UpdateFailed": "更新失敗,可在./log/*_gui.txt中找到錯誤日誌", "UpdateChecking": "檢查更新中", - "UpdateCancel": "取消更新,重啓 Alas 中", - "UpdateFinish": "更新成功,請手動重啓", + "UpdateCancel": "取消更新,重啟 Alas 中", + "UpdateFinish": "更新成功,請手動重啟", "Local": "本地", "Upstream": "上游倉庫", "Author": "作者", "Time": "提交時間", "Message": "提交資訊", - "DisabledWarn": "更新模塊未啟用,你需要手動重啓 Alas 進行更新", + "DisabledWarn": "更新模塊未啟用,你需要手動重啟 Alas 進行更新", "DetailedHistory": "詳細提交歷史" }, "Remote": { diff --git a/module/config/stored/classes.py b/module/config/stored/classes.py new file mode 100644 index 000000000..2127baa22 --- /dev/null +++ b/module/config/stored/classes.py @@ -0,0 +1,242 @@ +import time +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_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__): + 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) + + def dashboard(self) -> str: + """ + Return a string to show on GUI + """ + return 'None' + + def readable_time(self): + diff = self.time.timestamp() - time.time() + if diff < -1: + return '', 'TimeError' + elif diff < 60: + # < 1 min + return '', 'JustNow' + elif diff < 3600: + return str(int(diff // 60)), 'MinutesAgo' + elif diff < 86400: + return str(int(diff // 86400)), 'HoursAgo' + elif diff < 129600: + return str(int(diff // 129600)), 'DaysAgo' + else: + # > 15 days + return '', 'LongTimeAgo' + + +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 StoredInt(StoredBase): + value = 0 + + +class StoredCounter(StoredBase): + current = 0 + total = 0 + + def set(self, current, total): + with self._config.multi_set(): + self.current = current + self.total = total + + def to_counter(self) -> str: + return f'{self.current}/{self.total}' + + def is_full(self) -> bool: + return self.current >= self.total + + def get_remain(self) -> int: + return self.total - self.current + + +class StoredDailyActivity(StoredCounter, StoredExpiredAt0400): + def set(self, current): + return super().set(current=current, total=500) + + @property + def _stored(self): + stored = super()._stored + stored['total'] = 500 + return stored + + +class StoredDaily(StoredExpiredAt0400): + quest1 = '' + quest2 = '' + quest3 = '' + quest4 = '' + quest5 = '' + quest6 = '' + + 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]: + 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(): + 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 = '' + + +class StoredDungeonDouble(StoredExpiredAt0400): + calyx = 0 + relic = 0 diff --git a/module/config/stored/stored_generated.py b/module/config/stored/stored_generated.py new file mode 100644 index 000000000..cea0b78e8 --- /dev/null +++ b/module/config/stored/stored_generated.py @@ -0,0 +1,18 @@ +from module.config.stored.classes import ( + StoredBase, + StoredCounter, + StoredDaily, + StoredDailyActivity, + StoredDungeonDouble, + StoredExpiredAt0400, + StoredInt, +) + + +# This file was auto-generated, do not modify it manually. To generate: +# ``` python -m module/config/config_updater.py ``` + +class StoredGenerated: + DungeonDouble = StoredDungeonDouble("Dungeon.DungeonStorage.DungeonDouble") + DailyActivity = StoredDailyActivity("DailyQuest.DailyStorage.DailyActivity") + DailyQuest = StoredDaily("DailyQuest.DailyStorage.DailyQuest") diff --git a/module/config/utils.py b/module/config/utils.py index 6fbca7479..4efb890d7 100644 --- a/module/config/utils.py +++ b/module/config/utils.py @@ -332,8 +332,9 @@ def data_to_type(data, **kwargs): """ | Condition | Type | | ------------------------------------ | -------- | - | Value is bool | checkbox | - | Arg has options | select | + | `value` is bool | checkbox | + | Arg has `options` | select | + | Arg has `stored` | select | | `Filter` is in name (in data['arg']) | textarea | | Rest of the args | input | @@ -345,10 +346,12 @@ def data_to_type(data, **kwargs): str: """ kwargs.update(data) - if isinstance(kwargs['value'], bool): + if isinstance(kwargs.get('value'), bool): return 'checkbox' elif 'option' in kwargs and kwargs['option']: return 'select' + elif 'stored' in kwargs and kwargs['stored']: + return 'stored' elif 'Filter' in kwargs['arg']: return 'textarea' else: diff --git a/module/device/connection.py b/module/device/connection.py index f21c88572..dbd5f3416 100644 --- a/module/device/connection.py +++ b/module/device/connection.py @@ -111,6 +111,8 @@ class Connection(ConnectionAttr): logger.attr('PackageName', self.package) logger.attr('Server', self.config.SERVER) + self.check_mumu_app_keep_alive() + @Config.when(DEVICE_OVER_HTTP=False) def adb_command(self, cmd, timeout=10): """ @@ -216,13 +218,25 @@ class Connection(ConnectionAttr): # str return result + def adb_getprop(self, name): + """ + Get system property in Android, same as `getprop ` + + Args: + name (str): Property name + + Returns: + str: + """ + return self.adb_shell(['getprop', name]).strip() + @cached_property def cpu_abi(self) -> str: """ Returns: str: arm64-v8a, armeabi-v7a, x86, x86_64 """ - abi = self.adb_shell(['getprop', 'ro.product.cpu.abi']).strip() + abi = self.adb_getprop('ro.product.cpu.abi') if not len(abi): logger.error(f'CPU ABI invalid: "{abi}"') return abi @@ -232,7 +246,7 @@ class Connection(ConnectionAttr): """ Android SDK/API levels, see https://apilevels.com/ """ - sdk = self.adb_shell(['getprop', 'ro.build.version.sdk']).strip() + sdk = self.adb_getprop('ro.build.version.sdk') try: return int(sdk) except ValueError: @@ -244,12 +258,32 @@ class Connection(ConnectionAttr): def is_avd(self): if get_serial_pair(self.serial)[0] is None: return False - if 'ranchu' in self.adb_shell(['getprop', 'ro.hardware']): + if 'ranchu' in self.adb_getprop('ro.hardware'): return True - if 'goldfish' in self.adb_shell(['getprop', 'ro.hardware.audio.primary']): + if 'goldfish' in self.adb_getprop('ro.hardware.audio.primary'): return True return False + def check_mumu_app_keep_alive(self): + if not self.is_mumu_family: + return False + + res = self.adb_getprop('nemud.app_keep_alive') + logger.attr('nemud.app_keep_alive', res) + if res == '': + # Empry property, might not be a mumu emulator or might be an old mumu + return True + elif res == 'false': + # Disabled + return True + elif res == 'true': + # https://mumu.163.com/help/20230802/35047_1102450.html + logger.critical('请在MuMu模拟器设置内关闭 "后台挂机时保活运行"') + raise RequestHumanTakeover + else: + logger.warning(f'Invalid nemud.app_keep_alive value: {res}') + return False + @cached_property def _nc_server_host_port(self): """ diff --git a/module/device/connection_attr.py b/module/device/connection_attr.py index 987175764..dcc23d46c 100644 --- a/module/device/connection_attr.py +++ b/module/device/connection_attr.py @@ -101,7 +101,9 @@ class ConnectionAttr: @cached_property def is_mumu_family(self): - return self.serial == '127.0.0.1:7555' + # 127.0.0.1:7555 + # 127.0.0.1:16384 + 32*n + return self.serial == '127.0.0.1:7555' or self.serial.startswith('127.0.0.1:16') @cached_property def is_emulator(self): diff --git a/module/device/method/maatouch.py b/module/device/method/maatouch.py index 24056a58f..8122505bc 100644 --- a/module/device/method/maatouch.py +++ b/module/device/method/maatouch.py @@ -77,6 +77,19 @@ def retry(func): return retry_wrapper +class MaatouchBuilder(CommandBuilder): + def __init__(self, device, contact=0, handle_orientation=False): + """ + Args: + device (MaaTouch): + """ + + super().__init__(device, contact, handle_orientation) + + def send(self): + return self.device.maatouch_send(builder=self) + + class MaaTouchNotInstalledError(Exception): pass @@ -94,7 +107,7 @@ class MaaTouch(Connection): @cached_property def maatouch_builder(self): self.maatouch_init() - return CommandBuilder(self, handle_orientation=False) + return MaatouchBuilder(self) def maatouch_init(self): logger.hr('MaaTouch init') @@ -165,14 +178,14 @@ class MaaTouch(Connection): ) ) - def maatouch_send(self): - content = self.maatouch_builder.to_minitouch() + def maatouch_send(self, builder: MaatouchBuilder): + content = builder.to_minitouch() # logger.info("send operation: {}".format(content.replace("\n", "\\n"))) byte_content = content.encode('utf-8') self._maatouch_stream.sendall(byte_content) self._maatouch_stream.recv(0) - self.sleep(self.maatouch_builder.delay / 1000 + self.maatouch_builder.DEFAULT_DELAY) - self.maatouch_builder.clear() + self.sleep(self.maatouch_builder.delay / 1000 + builder.DEFAULT_DELAY) + builder.clear() def maatouch_install(self): logger.hr('MaaTouch install') @@ -187,7 +200,7 @@ class MaaTouch(Connection): builder = self.maatouch_builder builder.down(x, y).commit() builder.up().commit() - self.maatouch_send() + builder.send() @retry def long_click_maatouch(self, x, y, duration=1.0): @@ -195,7 +208,7 @@ class MaaTouch(Connection): builder = self.maatouch_builder builder.down(x, y).commit().wait(duration) builder.up().commit() - self.maatouch_send() + builder.send() @retry def swipe_maatouch(self, p1, p2): @@ -203,14 +216,14 @@ class MaaTouch(Connection): builder = self.maatouch_builder builder.down(*points[0]).commit() - self.maatouch_send() + builder.send() for point in points[1:]: builder.move(*point).commit().wait(10) - self.maatouch_send() + builder.send() builder.up().commit() - self.maatouch_send() + builder.send() @retry def drag_maatouch(self, p1, p2, point_random=(-10, -10, 10, 10)): @@ -220,15 +233,15 @@ class MaaTouch(Connection): builder = self.maatouch_builder builder.down(*points[0]).commit() - self.maatouch_send() + builder.send() for point in points[1:]: builder.move(*point).commit().wait(10) - self.maatouch_send() + builder.send() builder.move(*p2).commit().wait(140) builder.move(*p2).commit().wait(140) - self.maatouch_send() + builder.send() builder.up().commit() - self.maatouch_send() + builder.send() diff --git a/module/device/method/minitouch.py b/module/device/method/minitouch.py index 70abd1a1c..405bc0299 100644 --- a/module/device/method/minitouch.py +++ b/module/device/method/minitouch.py @@ -184,7 +184,7 @@ class CommandBuilder: max_x = 1280 max_y = 720 - def __init__(self, device, handle_orientation=True): + def __init__(self, device, contact=0, handle_orientation=True): """ Args: device: @@ -192,6 +192,7 @@ class CommandBuilder: self.device = device self.commands = [] self.delay = 0 + self.contact = contact self.handle_orientation = handle_orientation @property @@ -243,21 +244,21 @@ class CommandBuilder: self.delay += ms return self - def up(self, contact=0): + def up(self): """ add minitouch command: 'u \n' """ - self.commands.append(Command('u', contact=contact)) + self.commands.append(Command('u', contact=self.contact)) return self - def down(self, x, y, contact=0, pressure=100): + def down(self, x, y, pressure=100): """ add minitouch command: 'd \n' """ x, y = self.convert(x, y) - self.commands.append(Command('d', x=x, y=y, contact=contact, pressure=pressure)) + self.commands.append(Command('d', x=x, y=y, contact=self.contact, pressure=pressure)) return self - def move(self, x, y, contact=0, pressure=100): + def move(self, x, y, pressure=100): """ add minitouch command: 'm \n' """ x, y = self.convert(x, y) - self.commands.append(Command('m', x=x, y=y, contact=contact, pressure=pressure)) + self.commands.append(Command('m', x=x, y=y, contact=self.contact, pressure=pressure)) return self def clear(self): @@ -271,6 +272,9 @@ class CommandBuilder: def to_atx_agent(self) -> List[str]: return [command.to_atx_agent(self.max_x, self.max_y) for command in self.commands] + def send(self): + return self.device.minitouch_send(builder=self) + class MinitouchNotInstalledError(Exception): pass @@ -446,14 +450,14 @@ class Minitouch(Connection): ) @Config.when(DEVICE_OVER_HTTP=False) - def minitouch_send(self): - content = self.minitouch_builder.to_minitouch() + def minitouch_send(self, builder: CommandBuilder): + content = builder.to_minitouch() # logger.info("send operation: {}".format(content.replace("\n", "\\n"))) byte_content = content.encode('utf-8') self._minitouch_client.sendall(byte_content) self._minitouch_client.recv(0) - time.sleep(self.minitouch_builder.delay / 1000 + self.minitouch_builder.DEFAULT_DELAY) - self.minitouch_builder.clear() + time.sleep(self.minitouch_builder.delay / 1000 + builder.DEFAULT_DELAY) + builder.clear() @cached_property def _minitouch_loop(self): @@ -514,8 +518,8 @@ class Minitouch(Connection): self._minitouch_ws = self._minitouch_loop_run(connect()) @Config.when(DEVICE_OVER_HTTP=True) - def minitouch_send(self): - content = self.minitouch_builder.to_atx_agent() + def minitouch_send(self, builder: CommandBuilder): + content = builder.to_atx_agent() async def send(): for row in content: @@ -523,15 +527,15 @@ class Minitouch(Connection): await self._minitouch_ws.send(row) self._minitouch_loop_run(send()) - time.sleep(self.minitouch_builder.delay / 1000 + self.minitouch_builder.DEFAULT_DELAY) - self.minitouch_builder.clear() + time.sleep(builder.delay / 1000 + builder.DEFAULT_DELAY) + builder.clear() @retry def click_minitouch(self, x, y): builder = self.minitouch_builder builder.down(x, y).commit() builder.up().commit() - self.minitouch_send() + builder.send() @retry def long_click_minitouch(self, x, y, duration=1.0): @@ -539,7 +543,7 @@ class Minitouch(Connection): builder = self.minitouch_builder builder.down(x, y).commit().wait(duration) builder.up().commit() - self.minitouch_send() + builder.send() @retry def swipe_minitouch(self, p1, p2): @@ -547,14 +551,14 @@ class Minitouch(Connection): builder = self.minitouch_builder builder.down(*points[0]).commit() - self.minitouch_send() + builder.send() for point in points[1:]: builder.move(*point).commit().wait(10) - self.minitouch_send() + builder.send() builder.up().commit() - self.minitouch_send() + builder.send() @retry def drag_minitouch(self, p1, p2, point_random=(-10, -10, 10, 10)): @@ -564,15 +568,15 @@ class Minitouch(Connection): builder = self.minitouch_builder builder.down(*points[0]).commit() - self.minitouch_send() + builder.send() for point in points[1:]: builder.move(*point).commit().wait(10) - self.minitouch_send() + builder.send() builder.move(*p2).commit().wait(140) builder.move(*p2).commit().wait(140) - self.minitouch_send() + builder.send() builder.up().commit() - self.minitouch_send() + builder.send() diff --git a/module/webui/widgets.py b/module/webui/widgets.py index 31b4381ff..c14ebc635 100644 --- a/module/webui/widgets.py +++ b/module/webui/widgets.py @@ -1,3 +1,4 @@ +import copy import json import random import string @@ -324,6 +325,35 @@ def put_arg_input(kwargs: T_Output_Kwargs) -> Output: ) +def product_stored_row(kwargs: T_Output_Kwargs, key, value): + kwargs = copy.copy(kwargs) + kwargs["name"] += f'_{key}' + kwargs["value"] = value + return put_input(**kwargs).style("--input--") + + +def put_arg_stored(kwargs: T_Output_Kwargs) -> Output: + name: str = kwargs["name"] + kwargs["disabled"] = True + + values = kwargs.pop("value", {}) + time_ = values.pop("time", "") + + rows = [product_stored_row(kwargs, key, value) for key, value in values.items() if value] + if time_: + rows += [product_stored_row(kwargs, "time", time_)] + return put_scope( + f"arg_container-stored-{name}", + [ + get_title_help(kwargs), + put_scope( + f"arg_stored-stored-value-{name}", + rows, + ) + ] + ) + + def put_arg_select(kwargs: T_Output_Kwargs) -> Output: name: str = kwargs["name"] value: str = kwargs["value"] @@ -355,6 +385,37 @@ def put_arg_select(kwargs: T_Output_Kwargs) -> Output: ) +def put_arg_state(kwargs: T_Output_Kwargs) -> Output: + name: str = kwargs["name"] + value: str = kwargs["value"] + options: List[str] = kwargs["options"] + options_label: List[str] = kwargs.pop("options_label", []) + _: str = kwargs.pop("invalid_feedback", None) + bold: bool = value in kwargs.pop("option_bold", []) + light: bool = value in kwargs.pop("option_light", []) + + option = [{ + "label": next((opt_label for opt, opt_label in zip(options, options_label) if opt == value), value), + "value": value, + "selected": True, + }] + if bold: + kwargs["class"] = "form-control state state-bold" + elif light: + kwargs["class"] = "form-control state state-light" + else: + kwargs["class"] = "form-control state" + kwargs["options"] = option + + return put_scope( + f"arg_container-select-{name}", + [ + get_title_help(kwargs), + put_select(**kwargs).style("--input--"), + ], + ) + + def put_arg_textarea(kwargs: T_Output_Kwargs) -> Output: name: str = kwargs["name"] mode: str = kwargs.pop("mode", None) @@ -437,6 +498,8 @@ _widget_type_to_func: Dict[str, Callable] = { "textarea": put_arg_textarea, "checkbox": put_arg_checkbox, "storage": put_arg_storage, + "state": put_arg_state, + "stored": put_arg_stored, } diff --git a/tasks/assignment/assignment.py b/tasks/assignment/assignment.py index 24191c754..e15877253 100644 --- a/tasks/assignment/assignment.py +++ b/tasks/assignment/assignment.py @@ -3,19 +3,27 @@ from datetime import datetime from module.logger import logger from module.ocr.ocr import Duration from tasks.assignment.assets.assets_assignment_claim import CLAIM -from tasks.assignment.assets.assets_assignment_ui import (DISPATCHED, - OCR_ASSIGNMENT_TIME) +from tasks.assignment.assets.assets_assignment_ui import ( + DISPATCHED, + OCR_ASSIGNMENT_TIME, +) from tasks.assignment.claim import AssignmentClaim -from tasks.assignment.keywords import * +from tasks.assignment.keywords import ( + AssignmentEntry, + KEYWORDS_ASSIGNMENT_GROUP, +) from tasks.base.page import page_assignment, page_menu +from tasks.daily.keywords import KEYWORDS_DAILY_QUEST from tasks.daily.synthesize import SynthesizeUI class Assignment(AssignmentClaim, SynthesizeUI): def run(self, assignments: list[AssignmentEntry] = None, duration: int = None): + self.config.update_daily_quests() + if assignments is None: assignments = ( - getattr(self.config, f'Assignment_Name_{i+1}', None) for i in range(4)) + getattr(self.config, f'Assignment_Name_{i + 1}', None) for i in range(4)) # remove duplicate while keeping order assignments = list(dict.fromkeys( x for x in assignments if x is not None)) @@ -46,7 +54,12 @@ class Assignment(AssignmentClaim, SynthesizeUI): # Scheduler delay = min(self.dispatched.values()) logger.info(f'Delay assignment check to {str(delay)}') - self.config.task_delay(target=delay) + with self.config.multi_set(): + quests = self.config.stored.DailyQuest.load_quests() + if KEYWORDS_DAILY_QUEST.Go_on_assignment_1_time in quests: + logger.info('Achieved daily quest Go_on_assignment_1_time') + self.config.task_call('DailyQuest') + self.config.task_delay(target=delay) def _check_inlist(self, assignments: list[AssignmentEntry], duration: int): """ diff --git a/tasks/assignment/keywords/__init__.py b/tasks/assignment/keywords/__init__.py index 3ecbdeebf..d307fb7ec 100644 --- a/tasks/assignment/keywords/__init__.py +++ b/tasks/assignment/keywords/__init__.py @@ -1,6 +1,6 @@ import tasks.assignment.keywords.entry as KEYWORDS_ASSIGNMENT_ENTRY import tasks.assignment.keywords.group as KEYWORDS_ASSIGNMENT_GROUP -from tasks.assignment.keywords.classes import AssignmentGroup, AssignmentEntry +from tasks.assignment.keywords.classes import AssignmentEntry, AssignmentGroup KEYWORDS_ASSIGNMENT_GROUP.Character_Materials.entries = ( KEYWORDS_ASSIGNMENT_ENTRY.Nine_Billion_Names, @@ -22,9 +22,9 @@ KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials.entries = ( KEYWORDS_ASSIGNMENT_ENTRY.The_Blossom_in_the_Storm, ) for group in ( - KEYWORDS_ASSIGNMENT_GROUP.Character_Materials, - KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits, - KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials, + KEYWORDS_ASSIGNMENT_GROUP.Character_Materials, + KEYWORDS_ASSIGNMENT_GROUP.EXP_Materials_Credits, + KEYWORDS_ASSIGNMENT_GROUP.Synthesis_Materials, ): for entry in group.entries: - entry.group = group \ No newline at end of file + entry.group = group diff --git a/tasks/base/ui.py b/tasks/base/ui.py index 3dc5241c0..1159f00ec 100644 --- a/tasks/base/ui.py +++ b/tasks/base/ui.py @@ -8,6 +8,7 @@ from tasks.base.assets.assets_base_page import CLOSE from tasks.base.page import Page, page_main from tasks.base.popup import PopupHandler from tasks.base.state import StateMixin +from tasks.combat.assets.assets_combat_finish import COMBAT_EXIT from tasks.combat.assets.assets_combat_prepare import COMBAT_PREPARE @@ -282,6 +283,8 @@ class UI(PopupHandler, StateMixin): if self.appear(COMBAT_PREPARE, interval=5): logger.info(f'UI additional: {COMBAT_PREPARE} -> {CLOSE}') self.device.click(CLOSE) + if self.appear_then_click(COMBAT_EXIT, interval=5): + return True return False diff --git a/tasks/combat/combat.py b/tasks/combat/combat.py index 44115f075..dd5511abe 100644 --- a/tasks/combat/combat.py +++ b/tasks/combat/combat.py @@ -3,13 +3,12 @@ from module.logger import logger from tasks.base.assets.assets_base_page import CLOSE 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, COMBAT_TEAM_DISMISSSUPPORT -from tasks.combat.assets.assets_combat_support import COMBAT_SUPPORT_ADD, COMBAT_SUPPORT_LIST +from tasks.combat.assets.assets_combat_team import COMBAT_TEAM_PREPARE, COMBAT_TEAM_SUPPORT from tasks.combat.interact import CombatInteract from tasks.combat.prepare import CombatPrepare from tasks.combat.state import CombatState -from tasks.combat.team import CombatTeam from tasks.combat.support import CombatSupport +from tasks.combat.team import CombatTeam from tasks.map.control.joystick import MapControlJoystick @@ -69,9 +68,8 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo """ Args: team: 1 to 6. - skip_first_screenshot: support_character: Support character name - + Returns: bool: True if success to enter combat False if trialblaze power is not enough @@ -272,21 +270,23 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo self.device.click(COMBAT_EXIT) continue - def combat(self, team: int = 1, wave_limit: int = 0, skip_first_screenshot=True, support_character: str = None): + def is_stamina_exhausted(self) -> bool: + flag = self.state.TrailblazePower < self.combat_wave_cost + logger.attr('StaminaExhausted', flag) + return flag + + def combat(self, team: int = 1, wave_limit: int = 0, support_character: str = None, skip_first_screenshot=True): """ Combat until trailblaze power runs out. Args: team: 1 to 6. wave_limit: Limit combat runs, 0 means no limit. - skip_first_screenshot: - use_support: "do_not_use", "always_use", "when_daily" - is_daily: True if is a daily task support_character: Support character name + skip_first_screenshot: Returns: - bool: True if trailblaze power exhausted - False if reached wave_limit but still have trailblaze power + int: Run count Pages: in: COMBAT_PREPARE @@ -298,6 +298,7 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo self.combat_wave_limit = wave_limit self.combat_wave_done = 0 + run_count = 0 while 1: logger.hr('Combat', level=2) logger.info(f'Combat, team={team}, wave={self.combat_wave_done}/{self.combat_wave_limit}') @@ -312,7 +313,9 @@ class Combat(CombatInteract, CombatPrepare, CombatState, CombatTeam, CombatSuppo finish = self.combat_finish() if self._combat_should_reenter(): continue + run_count += 1 if finish: break - return self.state.TrailblazePower < self.combat_wave_cost + logger.attr('CombatRunCount', run_count) + return run_count diff --git a/tasks/combat/state.py b/tasks/combat/state.py index 25cb85d9d..b396f8c74 100644 --- a/tasks/combat/state.py +++ b/tasks/combat/state.py @@ -94,6 +94,7 @@ class CombatState(UI): self._combat_auto_checked = True else: if self._combat_click_interval.reached(): + self.device.image_save() self.device.click(COMBAT_AUTO) self._combat_click_interval.reset() return True diff --git a/tasks/combat/support.py b/tasks/combat/support.py index cc7489238..a97caf287 100644 --- a/tasks/combat/support.py +++ b/tasks/combat/support.py @@ -25,6 +25,11 @@ class SupportCharacter: # __bool__ is called when use an object of the class in a boolean context return self.button is not None + def __str__(self): + return f'SupportCharacter({self.name})' + + __repr__ = __str__ + def _scale_character(self): """ Returns: @@ -154,7 +159,10 @@ class CombatSupport(UI): name=COMBAT_SUPPORT_LIST_SCROLL.name) if scroll.appear(main=self): if not scroll.at_bottom(main=self): + # Dropdown to load the entire support list, so large threshold is acceptable + scroll.drag_threshold, backup = 0.2, scroll.drag_threshold scroll.set_bottom(main=self) + scroll.drag_threshold = backup scroll.set_top(main=self) logger.info("Searching support") diff --git a/tasks/daily/daily_quest.py b/tasks/daily/daily_quest.py index 4146ee9ba..ef1309690 100644 --- a/tasks/daily/daily_quest.py +++ b/tasks/daily/daily_quest.py @@ -99,6 +99,7 @@ class DailyQuestUI(DungeonUI): results = [result.matched_keyword for result in results] logger.info("Daily quests recognition complete") logger.info(f"Daily quests: {results}") + self.config.stored.DailyQuest.write_quests(results) return results def _get_quest_reward(self, skip_first_screenshot=True): @@ -128,7 +129,7 @@ class DailyQuestUI(DungeonUI): def _get_active_point_reward(self, skip_first_screenshot=True): def get_active(): - for button in [ + for b in [ ACTIVE_POINTS_1_UNLOCK, ACTIVE_POINTS_2_UNLOCK, ACTIVE_POINTS_3_UNLOCK, @@ -136,8 +137,8 @@ class DailyQuestUI(DungeonUI): ACTIVE_POINTS_5_UNLOCK ]: # Black gift icon - if self.image_color_count(button, color=(61, 53, 53), threshold=221, count=100): - return button + if self.image_color_count(b, color=(61, 53, 53), threshold=221, count=100): + return b return None interval = Timer(2) @@ -156,6 +157,26 @@ class DailyQuestUI(DungeonUI): self.device.click(active) interval.reset() + # Write stored + point = 0 + for progress, button in zip( + [100, 200, 300, 400, 500], + [ + ACTIVE_POINTS_1_CHECKED, + ACTIVE_POINTS_2_CHECKED, + ACTIVE_POINTS_3_CHECKED, + ACTIVE_POINTS_4_CHECKED, + ACTIVE_POINTS_5_CHECKED + ] + ): + if self.appear(button): + point = progress + logger.attr('Daily activity', point) + with self.config.multi_set(): + self.config.stored.DailyActivity.set(point) + if point == 500: + self.config.stored.DailyQuest.write_quests([]) + def get_daily_rewards(self): """ Returns: diff --git a/tasks/dungeon/assets/assets_dungeon_event.py b/tasks/dungeon/assets/assets_dungeon_event.py index ea2f6fa44..f09003247 100644 --- a/tasks/dungeon/assets/assets_dungeon_event.py +++ b/tasks/dungeon/assets/assets_dungeon_event.py @@ -13,6 +13,16 @@ DOUBLE_CALYX_EVENT_TAG = ButtonWrapper( button=(329, 224, 425, 392), ), ) +DOUBLE_RELIC_EVENT_TAG = ButtonWrapper( + name='DOUBLE_RELIC_EVENT_TAG', + share=Button( + file='./assets/share/dungeon/event/DOUBLE_RELIC_EVENT_TAG.png', + area=(329, 505, 425, 589), + search=(309, 485, 445, 609), + color=(211, 206, 198), + button=(329, 505, 425, 589), + ), +) OCR_DOUBLE_EVENT_REMAIN = ButtonWrapper( name='OCR_DOUBLE_EVENT_REMAIN', share=Button( diff --git a/tasks/dungeon/dungeon.py b/tasks/dungeon/dungeon.py index 0e703b685..f0e2c54da 100644 --- a/tasks/dungeon/dungeon.py +++ b/tasks/dungeon/dungeon.py @@ -1,50 +1,210 @@ from module.base.utils import area_offset from module.logger import logger 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_TAB from tasks.dungeon.ui import DungeonUI class Dungeon(DungeonUI, DungeonEvent, Combat): - def run(self, dungeon: DungeonList = None, team: int = None, use_support: str = None, is_daily: bool = False, - support_character: str = None): - if dungeon is None: - dungeon = DungeonList.find(self.config.Dungeon_Name) + called_daily_support = False + achieved_daily_quest = False + daily_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: Any + out: page_main + """ if team is None: team = self.config.Dungeon_Team - if use_support is None: - use_support = self.config.Dungeon_Support - if support_character is None: - support_character = self.config.Dungeon_SupportCharacter if use_support == "always_use" or use_support == "when_daily" and is_daily else None + if support_character is None and self.config.DungeonSupport_Use == 'always_use': + support_character = self.config.DungeonSupport_Character - # UI switches - switched = self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) - if not switched: - # Nav must at top, reset nav states - self.ui_goto_main() + 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: self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) + self.dungeon_goto(dungeon) - # Check double events - if self.config.Dungeon_NameAtDoubleCalyx != 'do_not_participate' and self.has_double_calyx_event(): - calyx = DungeonList.find(self.config.Dungeon_NameAtDoubleCalyx) - self._dungeon_nav_goto(calyx) - if remain := self.get_double_event_remain(): - self.dungeon_goto(calyx) - if self.combat(team, wave_limit=remain, support_character=support_character): - self.delay_dungeon_task(calyx) - self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) + 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) # Combat - self.dungeon_goto(dungeon) + count = self.combat(team=team, wave_limit=wave_limit, support_character=support_character) - if dungeon == KEYWORDS_DUNGEON_LIST.Stagnant_Shadow_Blaze: - if self.handle_destructible_around_blaze(): + # Update quest states + if dungeon.is_Calyx_Golden \ + and 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 dungeon.is_Calyx_Crimson \ + and KEYWORDS_DAILY_QUEST.Complete_Calyx_Crimson_1_time in self.daily_quests: + logger.info('Achieve daily quest Complete_Calyx_Crimson_1_time') + self.achieved_daily_quest = True + if dungeon.is_Stagnant_Shadow \ + and 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 dungeon.is_Cavern_of_Corrosion \ + and 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 support_character is not None: + self.called_daily_support = True + if KEYWORDS_DAILY_QUEST.Obtain_victory_in_combat_with_support_characters_1_time: + logger.info('Achieve daily quest Obtain_victory_in_combat_with_support_characters_1_time') + self.achieved_daily_quest = True + + # Check stamina, this may stop current task + if self.is_stamina_exhausted(): + self.delay_dungeon_task(dungeon) + 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: + 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 + + else: + # Normal run + return self._dungeon_run(dungeon=dungeon, team=team, wave_limit=wave_limit, + support_character=support_character) + + def run(self): + self.config.update_daily_quests() + self.called_daily_support = False + self.achieved_daily_quest = False + self.daily_quests = self.config.stored.DailyQuest.load_quests() + + # Update double event records + if self.config.stored.DungeonDouble.is_expired(): + logger.info('Get dungeon double remains') + # UI switches + switched = self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) + if not switched: + # Nav must at top, reset nav states + self.ui_goto_main() self.dungeon_tab_goto(KEYWORDS_DUNGEON_TAB.Survival_Index) - self.dungeon_goto(dungeon) + # Check remains + calyx = 0 + relic = 0 + if self.has_double_calyx_event(): + self._dungeon_nav_goto(KEYWORDS_DUNGEON_LIST.Calyx_Golden_Treasures) + calyx = self.get_double_event_remain() + if self.has_double_relic_event(): + self._dungeon_nav_goto(KEYWORDS_DUNGEON_LIST.Cavern_of_Corrosion_Path_of_Gelid_Wind) + relic = self.get_double_event_remain() + with self.config.multi_set(): + self.config.stored.DungeonDouble.calyx = calyx + self.config.stored.DungeonDouble.relic = relic - self.combat(team=team, support_character=support_character) - self.delay_dungeon_task(dungeon) + # Run double events + ran_calyx_golden = False + ran_calyx_crimson = False + ran_cavern_of_corrosion = False + # Double calyx + if self.config.Dungeon_NameAtDoubleCalyx != 'do_not_participate' \ + and self.config.stored.DungeonDouble.calyx > 0: + logger.info('Run double calyx') + dungeon = DungeonList.find(self.config.Dungeon_NameAtDoubleCalyx) + if self.dungeon_run(dungeon=dungeon, wave_limit=self.config.stored.DungeonDouble.calyx): + if dungeon.is_Calyx_Golden: + ran_calyx_golden = True + if dungeon.is_Calyx_Crimson: + ran_calyx_crimson = True + # Double relic + if self.config.Dungeon_NameAtDoubleRelic != 'do_not_participate' \ + and self.config.stored.DungeonDouble.relic > 0: + logger.info('Run double relic') + dungeon = DungeonList.find(self.config.Dungeon_NameAtDoubleRelic) + if self.dungeon_run(dungeon=dungeon, wave_limit=self.config.stored.DungeonDouble.relic): + ran_cavern_of_corrosion = True + + # Dungeon to clear all trailblaze power + final = DungeonList.find(self.config.Dungeon_Name) + + # Run dungeon that required by daily quests + # Calyx_Golden + if KEYWORDS_DAILY_QUEST.Clear_Calyx_Golden_1_times in self.daily_quests \ + and self.config.DungeonDaily_CalyxGolden != 'do_not_achieve' \ + and not final.is_Calyx_Golden \ + and not ran_calyx_golden: + logger.info('Run Calyx_Golden once') + dungeon = DungeonList.find(self.config.DungeonDaily_CalyxGolden) + self.dungeon_run(dungeon=dungeon, wave_limit=1) + # Calyx_Crimson + if KEYWORDS_DAILY_QUEST.Complete_Calyx_Crimson_1_time in self.daily_quests \ + and self.config.DungeonDaily_CalyxCrimson != 'do_not_achieve' \ + and not final.is_Calyx_Crimson \ + and not ran_calyx_crimson: + logger.info('Run Calyx_Crimson once') + dungeon = DungeonList.find(self.config.DungeonDaily_CalyxCrimson) + self.dungeon_run(dungeon=dungeon, wave_limit=1) + # Stagnant_Shadow + if KEYWORDS_DAILY_QUEST.Clear_Stagnant_Shadow_1_times in self.daily_quests \ + and self.config.DungeonDaily_StagnantShadow != 'do_not_achieve' \ + and not final.is_Stagnant_Shadow: + logger.info('Run Stagnant_Shadow once') + dungeon = DungeonList.find(self.config.DungeonDaily_StagnantShadow) + self.dungeon_run(dungeon=dungeon, wave_limit=1) + # Cavern_of_Corrosion + if KEYWORDS_DAILY_QUEST.Clear_Cavern_of_Corrosion_1_times in self.daily_quests \ + and self.config.DungeonDaily_CavernOfCorrosion != 'do_not_achieve' \ + and not final.is_Cavern_of_Corrosion \ + and not ran_cavern_of_corrosion: + logger.info('Run Cavern_of_Corrosion once') + dungeon = DungeonList.find(self.config.DungeonDaily_CavernOfCorrosion) + self.dungeon_run(dungeon=dungeon, wave_limit=1) + + # Combat + self.dungeon_run(final) + self.delay_dungeon_task(final) def delay_dungeon_task(self, dungeon): if dungeon.is_Cavern_of_Corrosion: @@ -54,7 +214,12 @@ class Dungeon(DungeonUI, DungeonEvent, Combat): # Recover 1 trailbaze power each 6 minutes cover = max(limit - self.state.TrailblazePower, 0) * 6 logger.info(f'Currently has {self.state.TrailblazePower} need {cover} minutes to reach {limit}') - self.config.task_delay(minute=cover) + logger.attr('achieved_daily_quest', self.achieved_daily_quest) + with self.config.multi_set(): + if self.achieved_daily_quest: + self.config.task_call('DailyQuest') + self.config.task_delay(minute=cover) + self.config.task_stop() def handle_destructible_around_blaze(self): """ @@ -95,3 +260,23 @@ class Dungeon(DungeonUI, DungeonEvent, Combat): 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_time \ + 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 diff --git a/tasks/dungeon/event.py b/tasks/dungeon/event.py index 896a7d239..6c9f0c464 100644 --- a/tasks/dungeon/event.py +++ b/tasks/dungeon/event.py @@ -1,7 +1,11 @@ from module.logger import logger from module.ocr.ocr import DigitCounter from tasks.base.ui import UI -from tasks.dungeon.assets.assets_dungeon_event import DOUBLE_CALYX_EVENT_TAG, OCR_DOUBLE_EVENT_REMAIN +from tasks.dungeon.assets.assets_dungeon_event import ( + DOUBLE_CALYX_EVENT_TAG, + DOUBLE_RELIC_EVENT_TAG, + OCR_DOUBLE_EVENT_REMAIN +) class DungeonEvent(UI): @@ -11,9 +15,20 @@ class DungeonEvent(UI): in: page_guide, Survival_Index, nav at top """ has = self.image_color_count(DOUBLE_CALYX_EVENT_TAG, color=(252, 209, 123), threshold=221, count=50) + has |= self.image_color_count(DOUBLE_CALYX_EVENT_TAG, color=(252, 251, 140), threshold=221, count=50) logger.attr('Double calyx', has) return has + def has_double_relic_event(self) -> bool: + """ + Pages: + in: page_guide, Survival_Index, nav at top + """ + has = self.image_color_count(DOUBLE_RELIC_EVENT_TAG, color=(252, 209, 123), threshold=221, count=50) + has |= self.image_color_count(DOUBLE_RELIC_EVENT_TAG, color=(252, 251, 140), threshold=221, count=50) + logger.attr('Double relic', has) + return has + def get_double_event_remain(self) -> int: """ Pages: @@ -21,7 +36,7 @@ class DungeonEvent(UI): """ ocr = DigitCounter(OCR_DOUBLE_EVENT_REMAIN) remain, _, total = ocr.ocr_single_line(self.device.image) - if total != 12: + if total not in [3, 12]: logger.warning(f'Invalid double event remain') remain = 0 logger.attr('Double event remain', remain) diff --git a/tasks/dungeon/keywords/classes.py b/tasks/dungeon/keywords/classes.py index 5c49a79d9..39e482dbb 100644 --- a/tasks/dungeon/keywords/classes.py +++ b/tasks/dungeon/keywords/classes.py @@ -28,6 +28,10 @@ class DungeonList(Keyword): def is_Calyx_Crimson(self): return 'Calyx_Crimson' in self.name + @cached_property + def is_Calyx(self): + return self.is_Calyx_Golden or self.is_Calyx_Crimson + @cached_property def is_Stagnant_Shadow(self): return 'Stagnant_Shadow' in self.name diff --git a/tasks/forgotten_hall/ui.py b/tasks/forgotten_hall/ui.py index ddd4b518c..9e3d2ee3b 100644 --- a/tasks/forgotten_hall/ui.py +++ b/tasks/forgotten_hall/ui.py @@ -200,4 +200,4 @@ class ForgottenHallUI(DungeonUI): if self.match_template_color(DUNGEON_ENTER_CHECKED): logger.info("Forgotten hall dungeon entered") break - joystick.handle_map_run() + joystick.handle_map_2x_run() diff --git a/tasks/map/assets/assets_map_control.py b/tasks/map/assets/assets_map_control.py index a0782a019..142d1b8d6 100644 --- a/tasks/map/assets/assets_map_control.py +++ b/tasks/map/assets/assets_map_control.py @@ -33,6 +33,16 @@ JOYSTICK = ButtonWrapper( button=(234, 546, 262, 574), ), ) +ROTATION_SWIPE_AREA = ButtonWrapper( + name='ROTATION_SWIPE_AREA', + share=Button( + file='./assets/share/map/control/ROTATION_SWIPE_AREA.png', + area=(264, 87, 990, 219), + search=(244, 67, 1010, 239), + color=(255, 255, 255), + button=(264, 87, 990, 219), + ), +) RUN_BUTTON = ButtonWrapper( name='RUN_BUTTON', share=Button( diff --git a/tasks/map/control/control.py b/tasks/map/control/control.py new file mode 100644 index 000000000..c2a2d9799 --- /dev/null +++ b/tasks/map/control/control.py @@ -0,0 +1,265 @@ +from functools import cached_property + +from module.base.timer import Timer +from module.logger import logger +from tasks.map.assets.assets_map_control import ROTATION_SWIPE_AREA +from tasks.map.control.joystick import JoystickContact, MapControlJoystick +from tasks.map.control.waypoint import Waypoint, WaypointRun, WaypointStraightRun, ensure_waypoint +from tasks.map.minimap.minimap import Minimap +from tasks.map.resource.const import diff_to_180_180 + + +class MapControl(MapControlJoystick): + @cached_property + def minimap(self) -> Minimap: + return Minimap() + + def handle_rotation_set(self, target, threshold=15): + """ + Set rotation while running. + self.minimap.update_rotation() must be called first. + + Args: + target: Target degree (0~360) + threshold: + + Returns: + bool: If swiped rotation + """ + if self.minimap.is_rotation_near(target, threshold=threshold): + return False + + # if abs(self.minimap.rotation_diff(target)) > 60: + # self.device.image_save() + # exit(1) + + logger.info(f'Rotation set: {target}') + diff = self.minimap.rotation_diff(target) * self.minimap.ROTATION_SWIPE_MULTIPLY + diff = min(diff, self.minimap.ROTATION_SWIPE_MAX_DISTANCE) + diff = max(diff, -self.minimap.ROTATION_SWIPE_MAX_DISTANCE) + + self.device.swipe_vector((-diff, 0), box=ROTATION_SWIPE_AREA.area, duration=(0.2, 0.5)) + return True + + def rotation_set(self, target, threshold=15, skip_first_screenshot=False): + """ + Set rotation while standing. + + Args: + target: Target degree (0~360) + threshold: + skip_first_screenshot: + + Returns: + bool: If swiped rotation + """ + interval = Timer(1, count=2) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + self.minimap.update_rotation(self.device.image) + self.minimap.log_minimap() + + # End + if self.minimap.is_rotation_near(target, threshold=threshold): + logger.info(f'Rotation is now at: {target}') + break + + if interval.reached(): + if self.handle_rotation_set(target, threshold=threshold): + interval.reset() + continue + + def _goto( + self, + contact: JoystickContact, + waypoint: Waypoint, + end_point_opt=True, + skip_first_screenshot=False + ): + """ + Point to point walk. + + Args: + contact: + JoystickContact, must be wrapped with: + `with JoystickContact(self) as contact:` + waypoint: + Position to goto, (x, y) + end_point_opt: + True to enable endpoint optimizations, + character will smoothly approach target position + skip_first_screenshot: + """ + logger.hr('Goto', level=2) + logger.info(f'Goto {waypoint}') + self.device.stuck_record_clear() + self.device.click_record_clear() + + end_point_opt = end_point_opt and waypoint.end_point_opt + allow_2x_run = waypoint.speed in ['2x_run'] + allow_straight_run = waypoint.speed in ['2x_run', 'straight_run'] + allow_run = waypoint.speed in ['2x_run', 'straight_run', 'run'] + allow_rotation_set = True + last_rotation = 0 + + direction_interval = Timer(0.5, count=1) + rotation_interval = Timer(0.3, count=1) + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + self.device.screenshot() + + # Update + self.minimap.update(self.device.image) + + # Arrive + if self.minimap.is_position_near(waypoint.position, threshold=waypoint.get_threshold(end_point_opt)): + logger.info(f'Arrive {waypoint}') + break + + # Switch run case + diff = self.minimap.position_diff(waypoint.position) + if end_point_opt: + if allow_2x_run and diff < 20: + logger.info(f'Approaching target, diff={round(diff, 1)}, disallow 2x_run') + allow_2x_run = False + if allow_straight_run and diff < 15: + logger.info(f'Approaching target, diff={round(diff, 1)}, disallow straight_run') + direction_interval = Timer(0.2) + self._map_2x_run_timer.reset() + allow_straight_run = False + if allow_run and diff < 7: + logger.info(f'Approaching target, diff={round(diff, 1)}, disallow run') + direction_interval = Timer(0.2) + allow_run = False + + # Control + direction = self.minimap.position2direction(waypoint.position) + if allow_2x_run: + # Run with 2x_run button + # - Set rotation once + # - Continuous fine-tuning direction + # - Enable 2x_run + if allow_rotation_set: + # Cache rotation cause rotation detection has a higher error rate + last_rotation = self.minimap.rotation + if self.minimap.is_rotation_near(direction, threshold=10): + logger.info(f'Already at target rotation, ' + f'current={last_rotation}, target={direction}, disallow rotation_set') + allow_rotation_set = False + if allow_rotation_set and rotation_interval.reached(): + if self.handle_rotation_set(direction, threshold=10): + rotation_interval.reset() + direction_interval.reset() + if direction_interval.reached(): + contact.set(direction=diff_to_180_180(direction - last_rotation), run=True) + direction_interval.reset() + self.handle_map_2x_run(run=True) + elif allow_straight_run: + # Run with 2x_run button + # - Set rotation once + # - Continuous fine-tuning direction + # - Disable 2x_run + if allow_rotation_set: + # Cache rotation cause rotation detection has a higher error rate + last_rotation = self.minimap.rotation + if self.minimap.is_rotation_near(direction, threshold=10): + logger.info(f'Already at target rotation, ' + f'current={last_rotation}, target={direction}, disallow rotation_set') + allow_rotation_set = False + if allow_rotation_set and rotation_interval.reached(): + if self.handle_rotation_set(direction, threshold=10): + rotation_interval.reset() + direction_interval.reset() + if direction_interval.reached(): + contact.set(direction=diff_to_180_180(direction - last_rotation), run=True) + direction_interval.reset() + self.handle_map_2x_run(run=False) + elif allow_run: + # Run + # - No rotation set + # - Continuous fine-tuning direction + # - Disable 2x_run + if allow_rotation_set: + last_rotation = self.minimap.rotation + allow_rotation_set = False + if direction_interval.reached(): + contact.set(direction=diff_to_180_180(direction - last_rotation), run=True) + self.handle_map_2x_run(run=False) + else: + # Walk + # - Continuous fine-tuning direction + # - Disable 2x_run + if allow_rotation_set: + last_rotation = self.minimap.rotation + allow_rotation_set = False + if direction_interval.reached(): + contact.set(direction=diff_to_180_180(direction - last_rotation), run=False) + direction_interval.reset() + self.handle_map_2x_run(run=False) + + def goto( + self, + waypoints, + skip_first_screenshot=True + ): + """ + Go along a list of position, or goto target position + + Args: + waypoints: + position (x, y) to goto, or a list of position to go along. + Waypoint object to goto, or a list of Waypoint objects to go along. + + skip_first_screenshot: + """ + logger.hr('Goto', level=1) + if not isinstance(waypoints, list): + waypoints = [waypoints] + waypoints = [ensure_waypoint(point) for point in waypoints] + logger.info(f'Go along {len(waypoints)} waypoints') + end_list = [False for _ in waypoints] + end_list[-1] = True + + with JoystickContact(self) as contact: + for point, end in zip(waypoints, end_list): + point: Waypoint + self._goto( + contact=contact, + waypoint=point, + end_point_opt=end, + skip_first_screenshot=skip_first_screenshot + ) + skip_first_screenshot = True + + end_point = waypoints[-1] + if end_point.end_point_rotation is not None: + self.rotation_set(end_point.end_point_rotation, threshold=end_point.end_point_rotation_threshold) + + +if __name__ == '__main__': + # Control test in Himeko trail + # Must manually enter Himeko trail first and dismiss popup + self = MapControl('alas') + self.minimap.set_plane('Jarilo_BackwaterPass', floor='F1') + self.device.screenshot() + self.minimap.init_position((519, 359)) + # Visit 3 items + self.goto([ + WaypointRun((577.6, 363.4)), + ]) + self.goto([ + WaypointStraightRun((577.5, 369.4), end_point_rotation=200), + ]) + self.goto([ + WaypointRun((581.5, 387.3)), + WaypointRun((577.4, 411.5)), + ]) + # Goto boss + self.goto([ + WaypointStraightRun((607.6, 425.3)), + ]) diff --git a/tasks/map/control/joystick.py b/tasks/map/control/joystick.py index 77ced0dbd..41dd23f6e 100644 --- a/tasks/map/control/joystick.py +++ b/tasks/map/control/joystick.py @@ -1,15 +1,135 @@ +import math from functools import cached_property from module.base.timer import Timer +from module.device.method.maatouch import MaatouchBuilder +from module.device.method.minitouch import CommandBuilder, insert_swipe, random_normal_distribution +from module.exception import ScriptError from module.logger import logger from tasks.base.ui import UI from tasks.map.assets.assets_map_control import * +class JoystickContact: + CENTER = (JOYSTICK.area[0] + JOYSTICK.area[2]) / 2, (JOYSTICK.area[1] + JOYSTICK.area[3]) / 2 + # Minimum radius 49px + RADIUS_WALK = (55, 65) + # Minimum radius 103px + RADIUS_RUN = (105, 115) + + def __init__(self, main): + """ + Args: + main (MapControlJoystick): + """ + self.main = main + self.prev_point = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Lift finger when: + - Walk event ends, JoystickContact ends + - Any error is raised + Can not lift finger when: + - Process is force terminated + """ + builder = self.builder + if self.is_downed: + builder.up().commit() + builder.send() + logger.info('JoystickContact ends') + else: + logger.info('JoystickContact ends but it was never downed') + + @property + def is_downed(self): + return self.prev_point is not None + + @cached_property + def builder(self): + """ + Initialize a command builder + """ + method = self.main.config.Emulator_ControlMethod + if method == 'MaaTouch': + # Get the very first builder to initialize MaaTouch + _ = self.main.device.maatouch_builder + builder = MaatouchBuilder(self.main.device, contact=1) + elif method == 'minitouch': + # Get the very first builder to initialize minitouch + _ = self.main.device.minitouch_builder + builder = CommandBuilder(self.main.device, contact=1) + else: + raise ScriptError(f'Control method {method} does not support multi-finger, ' + f'please use MaaTouch or minitouch instead') + + # def empty_func(): + # pass + # + # # No clear() + # builder.clear = empty_func + # No delay + builder.DEFAULT_DELAY = 0. + + return builder + + @classmethod + def direction2screen(cls, direction, run=True): + """ + Args: + direction (int, float): Direction to goto (0~360) + run: True for character running, False for walking + + Returns: + tuple[int, int]: Position on screen to control joystick + """ + direction += random_normal_distribution(-5, 5, n=5) + radius = cls.RADIUS_RUN if run else cls.RADIUS_WALK + radius = random_normal_distribution(*radius, n=5) + + direction = math.radians(direction) + point = ( + cls.CENTER[0] + radius * math.sin(direction), + cls.CENTER[1] - radius * math.cos(direction), + ) + point = (int(round(point[0])), int(round(point[1]))) + return point + + def set(self, direction, run=True): + """ + Set joystick to given position + + Args: + direction (int, float): Direction to goto (0~360) + run: True for character running, False for walking + """ + logger.info(f'JoystickContact set to {direction}') + point = JoystickContact.direction2screen(direction, run=run) + builder = self.builder + + if self.is_downed: + points = insert_swipe(p0=self.prev_point, p3=point, speed=20) + for point in points[1:]: + builder.move(*point).commit().wait(10) + builder.send() + else: + builder.down(*point).commit() + builder.send() + # Character starts moving, RUN button is still unavailable in a short time. + # Assume available in 0.3s + # We still have reties if 0.3s is incorrect. + self.main._map_2x_run_timer.set_current(0.7) + + self.prev_point = point + + class MapControlJoystick(UI): _map_A_timer = Timer(1) _map_E_timer = Timer(1) - _map_run_timer = Timer(1) + _map_2x_run_timer = Timer(1) @cached_property def joystick_center(self) -> tuple[float, float]: @@ -64,7 +184,7 @@ class MapControlJoystick(UI): return False - def handle_map_run(self): + def handle_map_2x_run(self, run=True): """ Keep character running. Note that RUN button can only be clicked when character is moving. @@ -74,9 +194,13 @@ class MapControlJoystick(UI): """ is_running = self.image_color_count(RUN_BUTTON, color=(208, 183, 138), threshold=221, count=100) - if not is_running and self._map_run_timer.reached(): + if run and not is_running and self._map_2x_run_timer.reached(): self.device.click(RUN_BUTTON) - self._map_run_timer.reset() + self._map_2x_run_timer.reset() + return True + if not run and is_running and self._map_2x_run_timer.reached(): + self.device.click(RUN_BUTTON) + self._map_2x_run_timer.reset() return True return False diff --git a/tasks/map/control/waypoint.py b/tasks/map/control/waypoint.py new file mode 100644 index 000000000..046101fd5 --- /dev/null +++ b/tasks/map/control/waypoint.py @@ -0,0 +1,79 @@ +from dataclasses import dataclass + + +@dataclass +class Waypoint: + # Position to goto, (x, y) + position: tuple + # Position diff < threshold is considered as arrived + # `threshold` is used first if it is set + threshold: int = None + # If `threshold` is not set, `waypoint_threshold` and `endpoint_threshold` are used + waypoint_threshold: int = 10 + endpoint_threshold: int = 3 + # Max move speed, '2x_run', 'straight_run', 'run', 'walk' + # See MapControl._goto() for details of each speed level + speed: str = '2x_run' + + """ + The following attributes are only be used if this waypoint is the end point of goto() + """ + # True to enable endpoint optimizations, character will smoothly approach target position + # False to stop all controls at arrive + end_point_opt: bool = True + # Set rotation after arrive, 0~360 + end_point_rotation: int = None + end_point_rotation_threshold: int = 15 + + def __str__(self): + return f'Waypoint({self.position})' + + __repr__ = __str__ + + def get_threshold(self, end): + """ + Args: + end: True if this is an end point + + Returns: + int + """ + if self.threshold is not None: + return self.threshold + if end: + return self.endpoint_threshold + else: + return self.waypoint_threshold + + +def ensure_waypoint(point) -> Waypoint: + """ + Args: + point: Position (x, y) or Waypoint object + + Returns: + Waypoint: + """ + if isinstance(point, Waypoint): + return point + return Waypoint(point) + + +@dataclass(repr=False) +class Waypoint2xRun(Waypoint): + speed: str = '2x_run' + + +@dataclass(repr=False) +class WaypointStraightRun(Waypoint): + speed: str = 'straight_run' + + +@dataclass(repr=False) +class WaypointRun(Waypoint): + speed: str = 'run' + + +@dataclass(repr=False) +class WaypointWalk(Waypoint): + speed: str = 'walk' diff --git a/tasks/map/minimap/minimap.py b/tasks/map/minimap/minimap.py index 2263fa80a..933c89f09 100644 --- a/tasks/map/minimap/minimap.py +++ b/tasks/map/minimap/minimap.py @@ -67,7 +67,7 @@ class Minimap(MapResource): search_position = np.array(self.position, dtype=np.int64) search_position += self.POSITION_FEATURE_PAD search_size = np.array(image_size(local)) * self.POSITION_SEARCH_RADIUS - search_half = (search_size // 2 * 2).astype(np.int64) + search_half = (search_size // 2).astype(np.int64) search_area = area_offset((0, 0, *(search_half * 2)), offset=-search_half) search_area = area_offset(search_area, offset=np.multiply(search_position, self.POSITION_SEARCH_SCALE)) search_area = np.array(search_area).astype(np.int64) @@ -269,11 +269,12 @@ class Minimap(MapResource): # Extract minimap = self.get_minimap(image, radius=self.MINIMAP_RADIUS) _, _, v = cv2.split(rgb2yuv(minimap)) - image = cv2.subtract(255, v) - # image = cv2.GaussianBlur(image, (3, 3), 0) + image = cv2.subtract(128, v) + + image = cv2.GaussianBlur(image, (3, 3), 0) # Expand circle into rectangle - remap = cv2.remap(image, *self.RotationRemapData, cv2.INTER_LINEAR)[d * 2 // 10:d * 6 // 10].astype(np.float32) + remap = cv2.remap(image, *self.RotationRemapData, cv2.INTER_LINEAR)[d * 1 // 10:d * 6 // 10].astype(np.float32) remap = cv2.resize(remap, None, fx=scale, fy=scale, interpolation=cv2.INTER_LINEAR) # Find derivative gradx = cv2.Scharr(remap, cv2.CV_32F, 1, 0) @@ -284,7 +285,7 @@ class Minimap(MapResource): # Magic parameters for scipy.find_peaks para = { # 'height': (50, 800), - 'height': 50, + 'height': 35, # 'prominence': (0, 400), # 'width': (0, d * scale / 20), # 'distance': d * scale / 18, @@ -356,14 +357,17 @@ class Minimap(MapResource): self.rotation_confidence = rotation_confidence self.rotation = rotation - def update(self, image): + def update(self, image, show_log=True): """ Update minimap, costs about 7.88ms. """ self.update_position(image) self.update_direction(image) self.update_rotation(image) + if show_log: + self.log_minimap() + def log_minimap(self): # MiniMap P:(567.5, 862.8) (1.00x|0.439|0.157), D:303.8 (0.253), R:304 (0.846) logger.info( f'MiniMap ' diff --git a/tasks/map/resource/const.py b/tasks/map/resource/const.py index 34048872c..59f851eca 100644 --- a/tasks/map/resource/const.py +++ b/tasks/map/resource/const.py @@ -1,5 +1,6 @@ import os +import numpy as np from PIL import Image from module.base.utils import load_image @@ -16,13 +17,14 @@ class ResourceConst: # Downscale GIMAP and minimap for faster run POSITION_SEARCH_SCALE = 0.5 # Search the area that is 1.666x minimap, about 100px in wild on GIMAP - POSITION_SEARCH_RADIUS = 1.333 + POSITION_SEARCH_RADIUS = 1.666 # Can't figure out why but the result_of_0.5_lookup_scale + 0.5 ~= result_of_1.0_lookup_scale POSITION_MOVE_PATCH = (0.5, 0.5) # Position starting from the upper-left corner of the template image # but search an area larger than map # MINIMAP_RADIUS * POSITION_SEARCH_RADIUS * - POSITION_FEATURE_PAD = int(MINIMAP_RADIUS * POSITION_SEARCH_RADIUS * 1.5) + # POSITION_FEATURE_PAD = int(MINIMAP_RADIUS * POSITION_SEARCH_RADIUS * 1.5) + POSITION_FEATURE_PAD = 155 # Must be odd, equals int(9 * POSITION_SEARCH_SCALE) + 1 POSITION_AREA_DILATE = 5 @@ -43,6 +45,12 @@ class ResourceConst: # Pad 600px, cause camera sight in game is larger than GIMAP BIGMAP_BORDER_PAD = int(600 * BIGMAP_SEARCH_SCALE) + # Swipe 400px is about 85~90 degree + # * ROTATION_SWIPE_MULTIPLY = + ROTATION_SWIPE_MULTIPLY = 400 / 85 + # Max distance in one swipe, limited in -600px~600px + ROTATION_SWIPE_MAX_DISTANCE = 600 + def __init__(self): # Usually to be 0.4~0.5 self.position_similarity = 0. @@ -50,6 +58,8 @@ class ResourceConst: self.position_similarity_local = 0. # Current position on GIMAP with an error of about 0.1 pixel self.position: tuple[float, float] = (0, 0) + # Minimap scale factor, 1.0~1.25 + self.position_scale = 1.0 # Usually > 0.3 # Warnings will be logged if similarity <= 0.8 @@ -82,3 +92,81 @@ class ResourceConst: file = self.filepath(file) print(f'Save image: {file}') Image.fromarray(image).save(file) + + def position_diff(self, target): + """ + Args: + target: Target position (x, y) + + Returns: + float: Distance to current position + """ + return np.linalg.norm(np.subtract(target, self.position)) + + def is_position_near(self, target, threshold=5): + return self.position_diff(target) <= threshold + + def position2direction(self, target): + """ + Args: + target: Target position (x, y) + + Returns: + float: Direction from current position to target position (0~360) + """ + diff = np.subtract(target, self.position) + theta = np.rad2deg(np.arccos(-diff[1] / np.linalg.norm(diff))) + if diff[0] < 0: + theta = 360 - theta + return theta + + def direction_diff(self, target): + """ + Args: + target: Target degree (0~360) + + Returns: + float: Diff to current direction (-180~180) + """ + return diff_to_180_180(self.direction - target) + + def is_direction_near(self, target, threshold=15): + return abs(self.direction_diff(target)) <= threshold + + def rotation_diff(self, target): + """ + Args: + target: Target degree (0~360) + + Returns: + float: Diff to current rotation (-180~180) + """ + return diff_to_180_180(self.rotation - target) + + def is_rotation_near(self, target, threshold=10): + return abs(self.rotation_diff(target)) <= threshold + + +def diff_to_180_180(diff): + """ + Args: + diff: Degree diff + + Returns: + float: Degree diff (-180~180) + """ + diff = diff % 360 + if diff > 180: + diff -= 360 + return round(diff, 3) + + +def diff_to_0_360(diff): + """ + Args: + diff: Degree diff + + Returns: + float: Degree diff (0~360) + """ + return round(diff % 360, 3)