Merge pull request #508 from LmeSzinc/dev

Dev
This commit is contained in:
LmeSzinc 2024-06-12 12:30:44 +08:00 committed by GitHub
commit fc8ce808ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 315 additions and 82 deletions

View File

@ -47,6 +47,7 @@
"ServerUpdate": "04:00" "ServerUpdate": "04:00"
}, },
"Planner": { "Planner": {
"PlannerOverall": {},
"Item_Credit": {}, "Item_Credit": {},
"Item_Trailblaze_EXP": {}, "Item_Trailblaze_EXP": {},
"Item_Traveler_Guide": {}, "Item_Traveler_Guide": {},

View File

@ -66,6 +66,8 @@ class GenerateItemBase(GenerateKeyword):
continue continue
dic.setdefault(item_id, dungeon_id) dic.setdefault(item_id, dungeon_id)
# Credict
dic.setdefault(2, 1003)
return dic return dic
@ -79,6 +81,7 @@ class GenerateItemCurrency(GenerateItemBase):
if data['subtype'] == 'Virtual' and data['item_id'] < 100: if data['subtype'] == 'Virtual' and data['item_id'] < 100:
if data['item_id'] not in self.whitelist: if data['item_id'] not in self.whitelist:
continue continue
data['dungeon_id'] = self.dict_itemid_to_dungeonid.get(data['item_id'], -1)
yield data yield data

View File

@ -101,7 +101,7 @@ class AzurLaneAutoScript:
self.device.sleep(10) self.device.sleep(10)
return False return False
except GamePageUnknownError: except GamePageUnknownError:
logger.info('Game server may be under maintenance or network may be broken, check server status now') # logger.info('Game server may be under maintenance or network may be broken, check server status now')
self.checker.check_now() self.checker.check_now()
if self.checker.is_available(): if self.checker.is_available():
logger.critical('Game page unknown') logger.critical('Game page unknown')

View File

@ -57,12 +57,12 @@ class Button(Resource):
return load_image(self.file, self.area) return load_image(self.file, self.area)
@cached_property @cached_property
def image_binary(self): def image_luma(self):
return rgb2gray(self.image) return rgb2luma(self.image)
def resource_release(self): def resource_release(self):
del_cached_property(self, 'image') del_cached_property(self, 'image')
del_cached_property(self, 'image_binary') del_cached_property(self, 'image_luma')
self.clear_offset() self.clear_offset()
def __str__(self): def __str__(self):
@ -119,7 +119,7 @@ class Button(Resource):
self._button_offset = np.array(point) + self.search[:2] - self.area[:2] self._button_offset = np.array(point) + self.search[:2] - self.area[:2]
return sim > similarity return sim > similarity
def match_template_binary(self, image, similarity=0.85, direct_match=False) -> bool: def match_template_luma(self, image, similarity=0.85, direct_match=False) -> bool:
""" """
Detects assets by template matching. Detects assets by template matching.
@ -135,8 +135,8 @@ class Button(Resource):
""" """
if not direct_match: if not direct_match:
image = crop(image, self.search, copy=False) image = crop(image, self.search, copy=False)
image = rgb2gray(image) image = rgb2luma(image)
res = cv2.matchTemplate(self.image_binary, image, cv2.TM_CCOEFF_NORMED) res = cv2.matchTemplate(self.image_luma, image, cv2.TM_CCOEFF_NORMED)
_, sim, _, point = cv2.minMaxLoc(res) _, sim, _, point = cv2.minMaxLoc(res)
self._button_offset = np.array(point) + self.search[:2] - self.area[:2] self._button_offset = np.array(point) + self.search[:2] - self.area[:2]
@ -254,9 +254,9 @@ class ButtonWrapper(Resource):
return True return True
return False return False
def match_template_binary(self, image, similarity=0.85, direct_match=False) -> bool: def match_template_luma(self, image, similarity=0.85, direct_match=False) -> bool:
for assets in self.buttons: for assets in self.buttons:
if assets.match_template_binary(image, similarity=similarity, direct_match=direct_match): if assets.match_template_luma(image, similarity=similarity, direct_match=direct_match):
self._matched_button = assets self._matched_button = assets
return True return True
return False return False

View File

@ -220,6 +220,14 @@
} }
}, },
"Planner": { "Planner": {
"PlannerOverall": {
"type": "stored",
"value": {},
"display": true,
"stored": "StoredPlannerOverall",
"order": 4,
"color": "#85e7f2"
},
"Item_Credit": { "Item_Credit": {
"type": "planner", "type": "planner",
"value": {}, "value": {},
@ -729,9 +737,7 @@
"type": "stored", "type": "stored",
"value": {}, "value": {},
"display": "hide", "display": "hide",
"stored": "StoredEchoOfWar", "stored": "StoredEchoOfWar"
"order": 4,
"color": "#85e7f2"
}, },
"SimulatedUniverse": { "SimulatedUniverse": {
"type": "stored", "type": "stored",

View File

@ -129,15 +129,18 @@ DungeonStorage:
stored: StoredDungeonDouble stored: StoredDungeonDouble
EchoOfWar: EchoOfWar:
stored: StoredEchoOfWar stored: StoredEchoOfWar
order: 4
color: "#85e7f2"
SimulatedUniverse: SimulatedUniverse:
stored: StoredSimulatedUniverse stored: StoredSimulatedUniverse
order: 6 order: 6
color: "#8fb5fe" color: "#8fb5fe"
SupportReward: SupportReward:
Collect: true Collect: true
Planner: {} Planner:
PlannerOverall:
stored: StoredPlannerOverall
display: true
order: 4
color: "#85e7f2"
# Items will be injected in config updater # Items will be injected in config updater
Weekly: Weekly:

View File

@ -61,6 +61,8 @@ Dashboard:
HoursAgo: HoursAgo:
DaysAgo: DaysAgo:
LongTimeAgo: LongTimeAgo:
# Planner
EtaDays:
AddAlas: AddAlas:
PopupTitle: PopupTitle:

View File

@ -38,15 +38,15 @@
"order": 3, "order": 3,
"color": "#79dbc4" "color": "#79dbc4"
}, },
"EchoOfWar": { "PlannerOverall": {
"name": "EchoOfWar", "name": "PlannerOverall",
"path": "Dungeon.DungeonStorage.EchoOfWar", "path": "Dungeon.Planner.PlannerOverall",
"i18n": "DungeonStorage.EchoOfWar.name", "i18n": "Planner.PlannerOverall.name",
"stored": "StoredEchoOfWar", "stored": "StoredPlannerOverall",
"attrs": { "attrs": {
"time": "2020-01-01 00:00:00", "time": "2020-01-01 00:00:00",
"total": 3, "comment": "<??d",
"value": 0 "value": "??%"
}, },
"order": 4, "order": 4,
"color": "#85e7f2" "color": "#85e7f2"
@ -714,6 +714,19 @@
"order": 0, "order": 0,
"color": "#777777" "color": "#777777"
}, },
"EchoOfWar": {
"name": "EchoOfWar",
"path": "Dungeon.DungeonStorage.EchoOfWar",
"i18n": "DungeonStorage.EchoOfWar.name",
"stored": "StoredEchoOfWar",
"attrs": {
"time": "2020-01-01 00:00:00",
"total": 3,
"value": 0
},
"order": 0,
"color": "#777777"
},
"DailyQuest": { "DailyQuest": {
"name": "DailyQuest", "name": "DailyQuest",
"path": "DailyQuest.DailyStorage.DailyQuest", "path": "DailyQuest.DailyStorage.DailyQuest",

View File

@ -53,7 +53,7 @@ class GeneratedConfig:
# Group `DungeonSupport` # Group `DungeonSupport`
DungeonSupport_Use = 'when_daily' # always_use, when_daily, do_not_use DungeonSupport_Use = 'when_daily' # always_use, when_daily, do_not_use
DungeonSupport_Character = 'FirstCharacter' # FirstCharacter, Acheron, Argenti, Arlan, Asta, Aventurine, Bailu, BlackSwan, Blade, Boothill, Bronya, Clara, DanHeng, DanHengImbibitorLunae, DrRatio, FuXuan, Gallagher, Gepard, Guinaifen, Hanya, Herta, Himeko, Hook, Huohuo, JingYuan, Jingliu, Kafka, Luka, Luocha, Lynx, March7th, Misha, Natasha, Pela, Qingque, Robin, RuanMei, Sampo, Seele, Serval, SilverWolf, Sparkle, Sushang, Tingyun, TopazNumby, TrailblazerDestruction, TrailblazerPreservation, Welt, Xueyi, Yanqing, Yukong DungeonSupport_Character = 'FirstCharacter' # FirstCharacter, Acheron, Argenti, Arlan, Asta, Aventurine, Bailu, BlackSwan, Blade, Boothill, Bronya, Clara, DanHeng, DanHengImbibitorLunae, DrRatio, FuXuan, Gallagher, Gepard, Guinaifen, Hanya, Herta, Himeko, Hook, Huohuo, JingYuan, Jingliu, Kafka, Luka, Luocha, Lynx, March7th, Misha, Natasha, Pela, Qingque, Robin, RuanMei, Sampo, Seele, Serval, SilverWolf, Sparkle, Sushang, Tingyun, TopazNumby, TrailblazerDestruction, TrailblazerHarmony, TrailblazerPreservation, Welt, Xueyi, Yanqing, Yukong
# Group `DungeonStorage` # Group `DungeonStorage`
DungeonStorage_TrailblazePower = {} DungeonStorage_TrailblazePower = {}
@ -66,6 +66,7 @@ class GeneratedConfig:
SupportReward_Collect = True SupportReward_Collect = True
# Group `Planner` # Group `Planner`
Planner_PlannerOverall = {}
Planner_Item_Credit = {} Planner_Item_Credit = {}
Planner_Item_Trailblaze_EXP = {} Planner_Item_Trailblaze_EXP = {}
Planner_Item_Traveler_Guide = {} Planner_Item_Traveler_Guide = {}

View File

@ -465,6 +465,10 @@
"name": "Character Planner Progress", "name": "Character Planner Progress",
"help": "Character planner is prioritized. After completed, \"Dungeon Settings\" will be executed." "help": "Character planner is prioritized. After completed, \"Dungeon Settings\" will be executed."
}, },
"PlannerOverall": {
"name": "Planner",
"help": "Overall progress of character planner"
},
"Item_Credit": { "Item_Credit": {
"name": "Credit", "name": "Credit",
"help": "" "help": ""
@ -1269,7 +1273,8 @@
"MinutesAgo": "{time}min ago", "MinutesAgo": "{time}min ago",
"HoursAgo": "{time}h ago", "HoursAgo": "{time}h ago",
"DaysAgo": "{time}d ago", "DaysAgo": "{time}d ago",
"LongTimeAgo": "long time ago" "LongTimeAgo": "long time ago",
"EtaDays": "ETA {time}d"
}, },
"AddAlas": { "AddAlas": {
"PopupTitle": "Add new config", "PopupTitle": "Add new config",

View File

@ -465,6 +465,10 @@
"name": "Progreso del planificador de personajes", "name": "Progreso del planificador de personajes",
"help": "Se prioriza el planificador de personajes. Una vez completado, se ejecutará la \"Ajustes de Mazmorra\"." "help": "Se prioriza el planificador de personajes. Una vez completado, se ejecutará la \"Ajustes de Mazmorra\"."
}, },
"PlannerOverall": {
"name": "Plan.",
"help": "Progreso general del planificador de personajes"
},
"Item_Credit": { "Item_Credit": {
"name": "Crédito", "name": "Crédito",
"help": "" "help": ""
@ -1269,7 +1273,8 @@
"MinutesAgo": "hace {time}m", "MinutesAgo": "hace {time}m",
"HoursAgo": "hace {time}h", "HoursAgo": "hace {time}h",
"DaysAgo": "hace {time}d", "DaysAgo": "hace {time}d",
"LongTimeAgo": "hace mucho tiempo" "LongTimeAgo": "hace mucho tiempo",
"EtaDays": "ETA {time}d"
}, },
"AddAlas": { "AddAlas": {
"PopupTitle": "Añadir nueva configuración", "PopupTitle": "Añadir nueva configuración",

View File

@ -465,6 +465,10 @@
"name": "Planner._info.name", "name": "Planner._info.name",
"help": "Planner._info.help" "help": "Planner._info.help"
}, },
"PlannerOverall": {
"name": "Planner.PlannerOverall.name",
"help": "Planner.PlannerOverall.help"
},
"Item_Credit": { "Item_Credit": {
"name": "信用ポイント", "name": "信用ポイント",
"help": "" "help": ""
@ -1269,7 +1273,8 @@
"MinutesAgo": "Gui.Dashboard.MinutesAgo", "MinutesAgo": "Gui.Dashboard.MinutesAgo",
"HoursAgo": "Gui.Dashboard.HoursAgo", "HoursAgo": "Gui.Dashboard.HoursAgo",
"DaysAgo": "Gui.Dashboard.DaysAgo", "DaysAgo": "Gui.Dashboard.DaysAgo",
"LongTimeAgo": "Gui.Dashboard.LongTimeAgo" "LongTimeAgo": "Gui.Dashboard.LongTimeAgo",
"EtaDays": "Gui.Dashboard.EtaDays"
}, },
"AddAlas": { "AddAlas": {
"PopupTitle": "新しいコンフィグを追加", "PopupTitle": "新しいコンフィグを追加",

View File

@ -465,6 +465,10 @@
"name": "养成规划进度", "name": "养成规划进度",
"help": "优先执行养成规划,养成规划完成后,执行 \"每日副本设置\"" "help": "优先执行养成规划,养成规划完成后,执行 \"每日副本设置\""
}, },
"PlannerOverall": {
"name": "养成规划",
"help": "角色养成规划总体进度"
},
"Item_Credit": { "Item_Credit": {
"name": "信用点", "name": "信用点",
"help": "" "help": ""
@ -1269,7 +1273,8 @@
"MinutesAgo": "{time}分钟前", "MinutesAgo": "{time}分钟前",
"HoursAgo": "{time}小时前", "HoursAgo": "{time}小时前",
"DaysAgo": "{time}天前", "DaysAgo": "{time}天前",
"LongTimeAgo": "很久以前" "LongTimeAgo": "很久以前",
"EtaDays": "剩余{time}天"
}, },
"AddAlas": { "AddAlas": {
"PopupTitle": "添加新配置", "PopupTitle": "添加新配置",

View File

@ -465,6 +465,10 @@
"name": "養成規劃進度", "name": "養成規劃進度",
"help": "優先執行養成規劃,養成規劃完成後,執行 \"每日副本設定\"" "help": "優先執行養成規劃,養成規劃完成後,執行 \"每日副本設定\""
}, },
"PlannerOverall": {
"name": "養成規劃",
"help": "角色養成規劃總體進度"
},
"Item_Credit": { "Item_Credit": {
"name": "信用點", "name": "信用點",
"help": "" "help": ""
@ -1269,7 +1273,8 @@
"MinutesAgo": "{time}分鐘前", "MinutesAgo": "{time}分鐘前",
"HoursAgo": "{time}小時前", "HoursAgo": "{time}小時前",
"DaysAgo": "{time}天前", "DaysAgo": "{time}天前",
"LongTimeAgo": "很久以前" "LongTimeAgo": "很久以前",
"EtaDays": "剩餘{time}日"
}, },
"AddAlas": { "AddAlas": {
"PopupTitle": "添加新的設定", "PopupTitle": "添加新的設定",

View File

@ -404,3 +404,8 @@ class StoredPlanner(StoredBase):
value: int value: int
total: int total: int
synthesize: int synthesize: int
class StoredPlannerOverall(StoredBase):
value: str = '??%'
comment: str = '<??d'

View File

@ -20,6 +20,7 @@ from module.config.stored.classes import (
StoredImmersifier, StoredImmersifier,
StoredInt, StoredInt,
StoredPlanner, StoredPlanner,
StoredPlannerOverall,
StoredSimulatedUniverse, StoredSimulatedUniverse,
StoredSimulatedUniverseElite, StoredSimulatedUniverseElite,
StoredTrailblazePower, StoredTrailblazePower,
@ -33,6 +34,7 @@ class StoredGenerated:
CloudRemainSeasonPass = StoredInt("Alas.CloudStorage.CloudRemainSeasonPass") CloudRemainSeasonPass = StoredInt("Alas.CloudStorage.CloudRemainSeasonPass")
CloudRemainPaid = StoredInt("Alas.CloudStorage.CloudRemainPaid") CloudRemainPaid = StoredInt("Alas.CloudStorage.CloudRemainPaid")
CloudRemainFree = StoredInt("Alas.CloudStorage.CloudRemainFree") CloudRemainFree = StoredInt("Alas.CloudStorage.CloudRemainFree")
PlannerOverall = StoredPlannerOverall("Dungeon.Planner.PlannerOverall")
Item_Credit = StoredPlanner("Dungeon.Planner.Item_Credit") Item_Credit = StoredPlanner("Dungeon.Planner.Item_Credit")
Item_Trailblaze_EXP = StoredPlanner("Dungeon.Planner.Item_Trailblaze_EXP") Item_Trailblaze_EXP = StoredPlanner("Dungeon.Planner.Item_Trailblaze_EXP")
Item_Traveler_Guide = StoredPlanner("Dungeon.Planner.Item_Traveler_Guide") Item_Traveler_Guide = StoredPlanner("Dungeon.Planner.Item_Traveler_Guide")

View File

@ -341,6 +341,11 @@ class AlasGUI(Frame):
put_text(config.get("value", nodata)).style("--dashboard-value--"), put_text(config.get("value", nodata)).style("--dashboard-value--"),
put_text(f' / {config.get("total", "")}').style("--dashboard-time--"), put_text(f' / {config.get("total", "")}').style("--dashboard-time--"),
] ]
elif "comment" in dic.get("attrs", []) and config.get("comment") is not None:
return [
put_text(config.get("value", nodata)).style("--dashboard-value--"),
put_text(f' {config.get("comment", "")}').style("--dashboard-time--"),
]
else: else:
return [ return [
put_text(config.get("value", nodata)).style("--dashboard-value--"), put_text(config.get("value", nodata)).style("--dashboard-value--"),

View File

@ -346,6 +346,7 @@ def put_arg_stored(kwargs: T_Output_Kwargs) -> Output:
value = values.pop("value", "") value = values.pop("value", "")
total = values.pop("total", "") total = values.pop("total", "")
time_ = values.pop("time", "") time_ = values.pop("time", "")
comment = values.pop("comment", "")
if value != "" and total != "": if value != "" and total != "":
# 0 / 100 # 0 / 100
@ -353,6 +354,12 @@ def put_arg_stored(kwargs: T_Output_Kwargs) -> Output:
put_text(value).style("--dashboard-value--"), put_text(value).style("--dashboard-value--"),
put_text(f" / {total}").style("--dashboard-time--"), put_text(f" / {total}").style("--dashboard-time--"),
])] ])]
elif value != "" and comment != "":
# 88% <1.2d
rows = [put_scope(f"dashboard-value-{name}", [
put_text(value).style("--dashboard-value--"),
put_text(f" {comment}").style("--dashboard-time--"),
])]
elif value != "": elif value != "":
# 100 # 100
rows = [put_scope(f"dashboard-value-{name}", [ rows = [put_scope(f"dashboard-value-{name}", [
@ -393,6 +400,11 @@ def put_arg_planner(kwargs: T_Output_Kwargs) -> Output | None:
except KeyError: except KeyError:
# Hide items not needed by the planner # Hide items not needed by the planner
return None return None
eta = values.get("eta", 0)
if eta > 0:
eta = f" - {t('Gui.Dashboard.EtaDays', time=eta)}"
else:
eta = ""
value = values.pop('value', 0) value = values.pop('value', 0)
if isinstance(value, dict): if isinstance(value, dict):
@ -402,7 +414,7 @@ def put_arg_planner(kwargs: T_Output_Kwargs) -> Output | None:
total = tuple(total.values()) total = tuple(total.values())
row = put_scope(f"arg_stored-stored-value-{name}", [ row = put_scope(f"arg_stored-stored-value-{name}", [
put_text(f"{progress:.2f}%").style("--dashboard-bold--"), put_text(f"{progress:.2f}%{eta}").style("--dashboard-bold--"),
put_text(f"{value} / {total}").style("--dashboard-time--"), put_text(f"{value} / {total}").style("--dashboard-time--"),
]) ])

View File

@ -96,7 +96,8 @@ class UI(MainPage):
if self.handle_popup_confirm(): if self.handle_popup_confirm():
timeout.reset() timeout.reset()
continue continue
if self.appear_then_click(LOGIN_CONFIRM, interval=5): if self.is_in_login_confirm(interval=5):
self.device.click(LOGIN_CONFIRM)
timeout.reset() timeout.reset()
continue continue
if self.appear(MAP_LOADING, interval=5): if self.appear(MAP_LOADING, interval=5):
@ -169,7 +170,8 @@ class UI(MainPage):
continue continue
if self.handle_popup_confirm(): if self.handle_popup_confirm():
continue continue
if self.appear_then_click(LOGIN_CONFIRM, interval=5): if self.is_in_login_confirm(interval=5):
self.device.click(LOGIN_CONFIRM)
continue continue
# Reset connection # Reset connection
@ -314,11 +316,11 @@ class UI(MainPage):
return False return False
appear = False appear = False
if MAIN_GOTO_CHARACTER.match_template_binary(self.device.image): if MAIN_GOTO_CHARACTER.match_template_luma(self.device.image):
if self.image_color_count(MAIN_GOTO_CHARACTER, color=(235, 235, 235), threshold=234, count=400): if self.image_color_count(MAIN_GOTO_CHARACTER, color=(235, 235, 235), threshold=234, count=400):
appear = True appear = True
if not appear: if not appear:
if MAP_EXIT.match_template_binary(self.device.image): if MAP_EXIT.match_template_luma(self.device.image):
if self.image_color_count(MAP_EXIT, color=(235, 235, 235), threshold=221, count=50): if self.image_color_count(MAP_EXIT, color=(235, 235, 235), threshold=221, count=50):
appear = True appear = True
@ -327,6 +329,20 @@ class UI(MainPage):
return appear return appear
def is_in_login_confirm(self, interval=0):
self.device.stuck_record_add(LOGIN_CONFIRM)
if interval and not self.interval_is_reached(LOGIN_CONFIRM, interval=interval):
return False
appear = LOGIN_CONFIRM.match_template_luma(self.device.image)
if appear and interval:
self.interval_reset(LOGIN_CONFIRM, interval=interval)
return appear
def is_in_map_exit(self, interval=0): def is_in_map_exit(self, interval=0):
self.device.stuck_record_add(MAP_EXIT) self.device.stuck_record_add(MAP_EXIT)
@ -334,7 +350,7 @@ class UI(MainPage):
return False return False
appear = False appear = False
if MAP_EXIT.match_template_binary(self.device.image): if MAP_EXIT.match_template_luma(self.device.image):
if self.image_color_count(MAP_EXIT, color=(235, 235, 235), threshold=221, count=50): if self.image_color_count(MAP_EXIT, color=(235, 235, 235), threshold=221, count=50):
appear = True appear = True

View File

@ -56,7 +56,8 @@ class Login(UI, LoginAndroidCloud):
orientation_timer.reset() orientation_timer.reset()
# Login # Login
if self.appear_then_click(LOGIN_CONFIRM): if self.is_in_login_confirm(interval=5):
self.device.click(LOGIN_CONFIRM)
login_success = True login_success = True
continue continue
if self.appear_then_click(USER_AGREEMENT_ACCEPT): if self.appear_then_click(USER_AGREEMENT_ACCEPT):

View File

@ -14,7 +14,7 @@ Credit = ItemCurrency(
rarity='Rare', rarity='Rare',
item_id=2, item_id=2,
item_group=0, item_group=0,
dungeon_id=-1, dungeon_id=1003,
) )
Trailblaze_EXP = ItemCurrency( Trailblaze_EXP = ItemCurrency(
id=2, id=2,

View File

@ -1,8 +1,9 @@
import math
import typing as t import typing as t
from datetime import datetime from datetime import datetime
from functools import partial from functools import cached_property as functools_cached_property, partial
from pydantic import BaseModel, ValidationError, WrapValidator, field_validator, model_validator from pydantic import BaseModel, ValidationError, WrapValidator, computed_field, field_validator, model_validator
from module.base.decorator import cached_property, del_cached_property from module.base.decorator import cached_property, del_cached_property
from module.config.stored.classes import now from module.config.stored.classes import now
@ -74,16 +75,37 @@ class MultiValue(BaseModelWithFallback):
self.blue += other.blue self.blue += other.blue
self.purple += other.purple self.purple += other.purple
def __sub__(self, other):
green = max(self.green - other.green, 0)
blue = max(self.blue - other.blue, 0)
purple = max(self.purple - other.purple, 0)
return MultiValue(green=green, blue=blue, purple=purple)
def equivalent_green(self): def equivalent_green(self):
return self.green + self.blue * 3 + self.purple * 9 return self.green + self.blue * 3 + self.purple * 9
def clear(self):
self.green = 0
self.blue = 0
self.purple = 0
SET_ROW_EXCLUDE = {
'drop_equivalent_green',
'combat_cost',
'progress_remain',
'progress_total',
'progress_current',
}
class StoredPlannerProxy(BaseModelWithFallback): class StoredPlannerProxy(BaseModelWithFallback):
item: ITEM_TYPES item: ITEM_TYPES
value: int | MultiValue = 0 value: int | MultiValue = 0
total: int | MultiValue = 0 total: int | MultiValue = 0
synthesize: int | MultiValue = 0 synthesize: int | MultiValue = 0
progress: float = 0. # progress: float = 0.
# eta: float = 0.
time: datetime = DEFAULT_TIME time: datetime = DEFAULT_TIME
@field_validator('item', mode='before') @field_validator('item', mode='before')
@ -110,6 +132,12 @@ class StoredPlannerProxy(BaseModelWithFallback):
self.synthesize = 0 self.synthesize = 0
return self return self
def clear(self):
if self.item.has_group_base:
self.value.clear()
else:
self.value = 0
def update_synthesize(self): def update_synthesize(self):
if self.item.has_group_base: if self.item.has_group_base:
green = self.value.green - self.total.green green = self.value.green - self.total.green
@ -147,6 +175,82 @@ class StoredPlannerProxy(BaseModelWithFallback):
else: else:
self.value.blue += self.synthesize.purple * 3 self.value.blue += self.synthesize.purple * 3
@computed_field(repr=False)
@functools_cached_property
def drop_equivalent_green(self) -> float:
# Tracks_of_Destiny
if self.item.dungeon is None:
return 1
if self.item.dungeon.is_Calyx_Golden_Treasures:
return 24000
if self.item.dungeon.is_Calyx_Golden_Memories:
# purple, blue, green = 5, 1, 0
return 48
if self.item.dungeon.is_Calyx_Golden_Aether:
# purple, blue, green = 1, 2, 2.5
return 17.5
if self.item.is_ItemAscension:
return 3
if self.item.is_ItemTrace:
# purple, blue, green = 0.155, 1, 1.25
return 5.645
if self.item.is_ItemWeekly:
return 3
raise ScriptError(f'{self} has no drop_equivalent_green defined')
@computed_field(repr=False)
@functools_cached_property
def combat_cost(self) -> int:
# Tracks_of_Destiny
if self.item.dungeon is None:
return 30
if self.item.dungeon.is_Calyx_Golden:
return 10
if self.item.is_ItemAscension:
return 30
if self.item.is_ItemTrace:
return 10
if self.item.is_ItemWeekly:
return 30
raise ScriptError(f'{self} has no stamina_pre_combat defined')
@computed_field(repr=False)
@functools_cached_property
def progress_remain(self) -> float:
if self.item.has_group_base:
remain = self.total - self.value - self.synthesize
return remain.equivalent_green()
else:
remain = max(self.total - self.value, 0)
return remain
@computed_field(repr=False)
@functools_cached_property
def progress_total(self) -> float:
if self.item.has_group_base:
return self.total.equivalent_green()
else:
return self.total
@computed_field(repr=False)
@functools_cached_property
def progress_current(self) -> float:
if self.item.has_group_base:
current = self.progress_total - self.progress_remain
current = min(max(current, 0), self.progress_total)
return current
else:
current = self.value
current = min(max(current, 0), self.total)
return current
@computed_field
@functools_cached_property
def progress(self) -> float:
# 0 to 100
progress = self.progress_current / self.progress_total * 100
return round(min(max(progress, 0), 100), 2)
def is_approaching_total(self, wave_done: int = 0): def is_approaching_total(self, wave_done: int = 0):
""" """
Args: Args:
@ -157,46 +261,47 @@ class StoredPlannerProxy(BaseModelWithFallback):
""" """
wave_done = max(wave_done, 0) wave_done = max(wave_done, 0)
# Items with a static drop rate will have `AVG * (wave_done + 1) # Items with a static drop rate will have `AVG * (wave_done + 1)
if self.item.dungeon.is_Calyx_Golden_Treasures: remain = self.progress_remain
return self.value + 24000 * (wave_done + 12) >= self.total cost = self.combat_cost
if self.item.dungeon.is_Calyx_Golden_Memories: drop = self.drop_equivalent_green
# purple, blue, green = 5, 1, 0 if cost == 10:
value = self.value.equivalent_green() return remain <= drop * (wave_done + 12)
total = self.total.equivalent_green()
return value + 48 * (wave_done + 12) >= total
if self.item.dungeon.is_Calyx_Golden_Aether:
# purple, blue, green = 1, 2, 2.5
value = self.value.equivalent_green()
total = self.total.equivalent_green()
return value + 17.5 * (wave_done + 12) >= total
if self.item.is_ItemAscension:
return self.value + 3 * (wave_done + 1) >= self.total
if self.item.is_ItemTrace:
# purple, blue, green = 0.155, 1, 1.25
value = self.value.equivalent_green()
total = self.total.equivalent_green()
return value + 5.645 * (wave_done + 12) >= total
if self.item.is_ItemWeekly:
return self.value + 3 * (wave_done + 1) >= self.total
return False
def update_progress(self):
if self.item.has_group_base:
total = self.total.equivalent_green()
green = min(self.value.green, self.total.green)
blue = min(self.value.blue + self.synthesize.blue, self.total.blue)
purple = min(self.value.purple + self.synthesize.purple, self.total.purple)
value = green + blue * 3 + purple * 9
progress = value / total * 100
self.progress = round(min(max(progress, 0), 100), 2)
else: else:
progress = self.value / self.total * 100 return remain <= drop * (wave_done + 1)
self.progress = round(min(max(progress, 0), 100), 2)
@computed_field
@functools_cached_property
def eta(self) -> float:
"""
Estimate remaining days to farm
"""
if not self.need_farm():
return 0.
if self.item.dungeon is None:
return 0.
remain = self.progress_remain
cost = self.combat_cost
drop = self.drop_equivalent_green
if self.item.is_ItemWeekly:
weeks = math.ceil(remain / drop / 3)
return weeks * 7
else:
stamina = math.ceil(remain / drop) * cost
return round(stamina / 240, 1)
def update(self, time=False):
for attr in SET_ROW_EXCLUDE:
del_cached_property(self, attr)
del_cached_property(self, 'progress')
del_cached_property(self, 'eta')
def update(self):
self.update_synthesize() self.update_synthesize()
self.update_progress() _ = self.progress
self.time = now() _ = self.eta
if time:
self.time = now()
def load_value_total(self, item: ItemBase, value=None, total=None, synthesize=None): def load_value_total(self, item: ItemBase, value=None, total=None, synthesize=None):
""" """
@ -344,9 +449,11 @@ class PlannerProgressParser:
def from_config(self, data): def from_config(self, data):
self.rows = {} self.rows = {}
for row in data.values(): for name, row in data.items():
if not row: if not row:
continue continue
if name == 'PlannerOverall':
continue
try: try:
row = StoredPlannerProxy(**row) row = StoredPlannerProxy(**row)
except (ScriptError, ValidationError) as e: except (ScriptError, ValidationError) as e:
@ -355,8 +462,7 @@ class PlannerProgressParser:
if not row.item.is_group_base: if not row.item.is_group_base:
logger.error(f'from_config: item is not group base {row}') logger.error(f'from_config: item is not group base {row}')
continue continue
row.update_synthesize() row.update(time=False)
row.update_progress()
self.rows[row.item.name] = row self.rows[row.item.name] = row
return self return self
@ -372,17 +478,37 @@ class PlannerProgressParser:
self.rows[name] = row self.rows[name] = row
for row in self.rows.values(): for row in self.rows.values():
row.update() row.update(time=True)
def to_config(self) -> dict: def to_config(self) -> dict:
data = {} data = {}
for row in self.rows.values(): for row in self.rows.values():
name = f'Item_{row.item.name}' name = f'Item_{row.item.name}'
dic = row.model_dump() dic = row.model_dump(exclude=SET_ROW_EXCLUDE)
dic['item'] = row.item.name dic['item'] = row.item.name
data[name] = dic data[name] = dic
return data return data
def get_overall(self):
"""
Calculate overall progress
Note that this method will clear all values
Returns:
float: Progress percentage
float: ETA in days
"""
eta = 0.
progress_current = 0.
progress_total = 0.
for row in self.rows.values():
eta += row.eta
progress_current += row.progress_current
progress_total += row.progress_total
progress = round(progress_current / progress_total * 100, 2)
return progress, eta
def iter_row_to_farm(self, need_farm=True) -> t.Iterable[StoredPlannerProxy]: def iter_row_to_farm(self, need_farm=True) -> t.Iterable[StoredPlannerProxy]:
""" """
Args: Args:
@ -494,6 +620,7 @@ class PlannerMixin(UI):
planner = self.planner planner = self.planner
data = planner.to_config() data = planner.to_config()
progress, eta = planner.get_overall()
with self.config.multi_set(): with self.config.multi_set():
# Set value # Set value
@ -506,5 +633,14 @@ class PlannerMixin(UI):
remove.append(key) remove.append(key)
for key in remove: for key in remove:
self.config.cross_set(f'Dungeon.Planner.{key}', {}) self.config.cross_set(f'Dungeon.Planner.{key}', {})
print(progress, eta)
# Set overall
self.config.stored.PlannerOverall.value = f'{progress:.2f}%'
self.config.stored.PlannerOverall.comment = f'<{eta:.1f}d'
del_cached_property(self, 'planner') del_cached_property(self, 'planner')
if __name__ == '__main__':
self = PlannerMixin('src')
self.planner_write(self.planner)

View File

@ -4,6 +4,7 @@ import cv2
from pponnxcr.predict_system import BoxedResult from pponnxcr.predict_system import BoxedResult
from module.base.utils import area_center, area_in_area from module.base.utils import area_center, area_in_area
from module.exception import GamePageUnknownError
from module.logger import logger from module.logger import logger
from module.ocr.ocr import Ocr, OcrWhiteLetterOnComplexBackground from module.ocr.ocr import Ocr, OcrWhiteLetterOnComplexBackground
from module.ui.scroll import AdaptiveScroll from module.ui.scroll import AdaptiveScroll
@ -22,6 +23,7 @@ DETAIL_TITLE.load_search(RESULT_CHECK.search)
class OcrItemName(Ocr): class OcrItemName(Ocr):
def after_process(self, result): def after_process(self, result):
result = result.replace('念火之心', '忿火之心') result = result.replace('念火之心', '忿火之心')
result = re.sub('^火之心', '忿火之心', result)
result = re.sub('工造机$', '工造机杼', result) result = re.sub('工造机$', '工造机杼', result)
result = re.sub('工造迥?轮', '工造迴轮', result) result = re.sub('工造迥?轮', '工造迴轮', result)
result = re.sub('月狂[療撩]?牙', '月狂獠牙', result) result = re.sub('月狂[療撩]?牙', '月狂獠牙', result)
@ -172,7 +174,7 @@ class PlannerScan(SynthesizeUI, PlannerMixin):
logger.hr('Parse planner result', level=2) logger.hr('Parse planner result', level=2)
if not self.ui_page_appear(page_planner): if not self.ui_page_appear(page_planner):
logger.error('Not in page_planner, game must in the planner result page before scanning') logger.error('Not in page_planner, game must in the planner result page before scanning')
return [] raise GamePageUnknownError
scroll = AdaptiveScroll(RESULT_SCROLL.button, name=RESULT_SCROLL.name) scroll = AdaptiveScroll(RESULT_SCROLL.button, name=RESULT_SCROLL.name)
scroll.drag_threshold = 0.1 scroll.drag_threshold = 0.1