Merge remote-tracking branch 'upstream/dev' into add/rouge_buff_selector

# Conflicts:
#	module/config/argument/argument.yaml
This commit is contained in:
Hengyu 2023-08-27 17:38:33 +08:00
commit 9a17af2d12
47 changed files with 3363 additions and 227 deletions

View File

@ -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)。

View File

@ -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.

BIN
assets/character/Kafka.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/character/Luka.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -59,6 +59,26 @@ select {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAAAXNSR0IArs4c6QAADI1JREFUeF7tnVeTZlUVht9V5Y03lvdapVXeWMUfQL3BC2+8Qa0SkOyQMzPEGUKVoog5Y0TFnANmEQUVVBQjZhBMmAOCAdNrLd1Y37TTPeusc87X55z17tvZ65y9nrWfb+9ePd1t0BABEdiUgImNCIjA5gQkiHaHCGxBQIJoe4iABNEeEIEcAZ0gOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZAgOW6KKkJAghQptNLMEZisICQfD+AhAB4M4HYA15rZPbk0FTV3AiQfBOBRAA4EcAOAW9axHyYpCMlPAzhoQ1FvBrDHzK6be7G1/m4ESO4EsAvAQ1ci7wTwVDO7vtvTus2enCAkbwPwiC3SOMTM3tUtTc2eKwGS7wTw5C3Wf4CZfXus/CYlCMnLAewOJHuomTk4jQUTIPkOAIfsJ8W7ADxuLEmmJsitAA4I1vwwM3OAGgskQPLtAA4NprbbzK4Izu00bWqC/BnAAztkIEk6wJrL1I5yeFrXmNnBY+Q3NUHuAPDwjok+xcz800ZjAQRIvg3AYR1TudLMTusYE5o+NUHeC+CJoZXvPelwM3OwGjMmkJTDMx7tQ3JqgjwSwCfb9z+6llqSdCU2ofkk3+obPbGk683ssYm4UMikBPEVk/SWXrZDdYSZOWiNGREg+RYAhyeXfLCZXZOM3W/Y5ARpknhrL9uhOtLMHLjGDAj0kIPeAjazd4+Z5iQFkSRjlnw6zyb5ZgBHJFa0Fjl8XZMVpEniffBsh+ooM/MCaEyQAMk3ATgysbR/tZPjPYnYziGTFqRJ4i2/bIfqaDPzQmhMiEAPOf7Z5PBu51rG5AWRJGvZB2t7Cck3Ajgq8cK1yzH5K9YqRJLeAsx2qI4xMy+MxjYSIHk1gKMTS/hHOznel4jtFTKLE+T+DEl6KzDboZIkvbZKv+Aecvzd/0+Wma1djlmdIANJcqyZ+aeYxhoJkHwDgGMSr3Q5vJX7/kTsICGzOkFWJPHWYLZD5T9k4wXTWAMBkq8HcGziVX9rcnwgETtYyCwF8exJeosw26GSJINtoc0f1EOO+9q1alvlmOUVa7UcPSXZYWb+6aYxAgGSr/MfiU082uXwa9Vo/32ky5pme4KsXLe8ZZjtUB1nZl5IjQEJ9JDjr02ODw64nF6Pmr0g7bolSXptg+GCSV4FYEfiiX9p16rJyDH7K9aG65b317MdquPNzAur0YMAydcCOC7xCJfDr1UfSsSOGrKIE2TluuWtxGyH6gQz8wJrJAj0kMN/zNrl+HDitaOHLEqQdt2SJKNvm71fQPI1AI5PvPZP7Vo1STkWdcXacN3yvnu2Q3WimXnBNQIESL4awAmBqRunuBx+cnwkEbu2kMWdICvXLW8xZjtUJ5mZF15jCwI95Li3yfHRqQNerCDtuiVJRtqBJF8F4MTE42cjx2KvWBuuW95yzHaoTjYz3wgaKwRIvhLASQko/svH/Vr1sUTstoQs+gRZuW556zHboZIkw8jxx/YF+WzkKHGCDCTJKWbmn5qlB8lXADg5AcHl8JPj44nYbQ0pcYKsSOKtyGyH6lQz8w1ScvSQ4+4mxyfmCK6UIO0Ld29JZjtUJSUheSWAUxIb/A/tWjVLOUpdsTZ84d5HktPMzDdMiUHy5QBOTSTrcvi16tpE7GRCyp0gK9ctb1FmO1Snm5lvnEWPHnL8vsnhv0Z21qOsIO26JUk22b4kXwYg8xvTf9euVbOXo+wVa8N1y/v52Q7VGWbmG2lRg+RLAZyeSMrl8GvVYv6OZOkTZOW65a3LbIfqTDPzDbWI0UOO3zY5PrUIEC0JCdJAkCwvCcmXADgjscF/065Vi5JDV6wNO4GktzKzHaqzzMw32CwHyRcDODOxeJfDr1X+p7sXN3SC/L8k3tLMdqjONjPfaLMaPeT4dZNj1L9Vvp0wJcg+6JMsIwnJFwE4K7EJFy+Hrlhb7AqS3uLMdqh2mplvvEkPki8EcHZikb9qJ8cNidhZhegE2VoSb3VmO1STlqSHHL9sX5AvXg6dIIHPMpJ9JNllZv4pPalB8gUAdiYW5XL4F+SfScTOMkQnSKBsJL31me1QnWNmviEnMXrI8Ysmx2cnkciaFiFBgqBJegs026GahCQknw9gVzDl1Wk/b9eqUnLoitVxp/SU5Fwz8w26LYPk8wCck3i5y+HXqs8lYmcfohOkYwlJeks026E6z8x8o6519JDjribHjWtd8IReJkESxZiTJCSfC+DcRJo/a9eqsnLoipXYNfeHkPTvH2Q7VOebmW/cUQfJ5wA4L/ESl8OvVTclYhcVohOkRzlJeqs026G6wMx8A48yesjx0ybH50dZ2MweKkF6FmyKkpB8NoDzE6n9pF2rJEeDJ0ESu2hjCElvnWY7VBeamW/oQQbJKwBckHiYy+HXqi8kYhcbIkEGKi1Jb6FmO1SDSNJDjh+3k0NybNgPEmQgQfwxPSXZbWb+6Z8aJJ8F4MJE8I+aHF9MxC4+RIIMXGKS3lLNdqj2mJlv9E6D5OUAdncK+u9kl8OvVTcnYkuESJARykzSW6vZDlUnSXrIcWc7OSTHFntAgowgSLtu9ZHkIjPzU2HLQfKZAPbsb94+/t3l8JPjS4nYUiESZMRyk/RWa7ZDdbGZuQD7HCSfAeCixPLvaHJ8ORFbLkSCjFzyMSTpIccP27VKcgTrLkGCoPpMI+nfl8h2qC4xMz8t/jNIXgbg4sR6XA6/Vt2SiC0bIkHWVHqS3oLt3KFqy7vUzC7rIcftTY6vrCndxbxGgqyxlD0l8V+tc1Biube1a5XkSMCTIAlofUJI+vcr9tuh6vOOlViXw69VXx3oeeUeI0G2oeQkvTW7aYdqoCX9oMnxtYGeV/IxEmSbyj6yJN9v1yrJ0bO+EqQnwD7hJP37GP/rUPV51kqsy+HXqq8P9LzSj5Eg21x+kt6y9dbtEON77eSQHEPQBCBBBgLZ5zEDSfLdJsc3+qxFsXsTkCAT2REkLwHw9ORyXA6/Vn0zGa+wTQhIkAltDZKXAnhaxyV9p50ckqMjuMh0CRKhtMY5HSXxVu6TzOzWNS6x1KskyATLHfylC3cDeIyZfWuCKSxmSRJkoqUkeRWAHZss714AB0qO8YsnQcZnnH4DyUcDuBrAwwA8AMA9APyXuT3BzO5LP1iBYQISJIxKEysSkCAVq66cwwQkSBiVJlYkIEEqVl05hwlIkDAqTaxIQIJUrLpyDhOQIGFUmliRgASpWHXlHCYgQcKoNLEiAQlSserKOUxAgoRRaWJFAhKkYtWVc5iABAmj0sSKBCRIxaor5zABCRJGpYkVCUiQilVXzmECEiSMShMrEpAgFauunMMEJEgYlSZWJCBBKlZdOYcJSJAwKk2sSECCVKy6cg4TkCBhVJpYkYAEqVh15RwmIEHCqDSxIgEJUrHqyjlMQIKEUWliRQISpGLVlXOYgAQJo9LEigQkSMWqK+cwAQkSRqWJFQlIkIpVV85hAhIkjEoTKxKQIBWrrpzDBCRIGJUmViQgQSpWXTmHCUiQMCpNrEhAglSsunIOE5AgYVSaWJGABKlYdeUcJiBBwqg0sSIBCVKx6so5TECChFFpYkUCEqRi1ZVzmIAECaPSxIoEJEjFqivnMAEJEkaliRUJSJCKVVfOYQISJIxKEysSkCAVq66cwwQkSBiVJlYkIEEqVl05hwlIkDAqTaxIQIJUrLpyDhOQIGFUmliRgASpWHXlHCYgQcKoNLEiAQlSserKOUxAgoRRaWJFAhKkYtWVc5iABAmj0sSKBCRIxaor5zCBfwOoUQv2LE3rzAAAAABJRU5ErkJggg==");
}
.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;
}

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -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": {

View File

@ -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:

View File

@ -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": {

View File

@ -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 ====================

View File

@ -25,8 +25,13 @@ Daily:
Dungeon:
- Scheduler
- Dungeon
- DungeonDaily
- DungeonSupport
- DungeonStorage
DailyQuest:
- Scheduler
- AchievableQuest
- DailyStorage
BattlePass:
- Scheduler
Assignment:

View File

@ -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

View File

@ -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

View File

@ -8,7 +8,7 @@ class ManualConfig:
SCHEDULER_PRIORITY = """
Restart
> Dungeon > Assignment > DailyQuest > BattlePass
> DailyQuest > Dungeon > Assignment > BattlePass
"""
"""

View File

@ -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.

View File

@ -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",

View File

@ -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": "依頼設定",

View File

@ -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": "委托设置",

View File

@ -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": {

View File

@ -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

View File

@ -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")

View File

@ -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:

View File

@ -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 <name>`
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):
"""

View File

@ -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):

View File

@ -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()

View File

@ -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 <contact>\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 <contact> <x> <y> <pressure>\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 <contact> <x> <y> <pressure>\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()

View File

@ -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,
}

View File

@ -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):
"""

View File

@ -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
entry.group = group

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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:

View File

@ -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(

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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(

View File

@ -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)),
])

View File

@ -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

View File

@ -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'

View File

@ -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 '

View File

@ -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 * <max_scale>
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_diff> * ROTATION_SWIPE_MULTIPLY = <distance_to_swipe>
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)