From 33eedc2c631763ee28bc78e7acc6f449a8e4ac5f Mon Sep 17 00:00:00 2001 From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com> Date: Wed, 12 Jun 2024 03:20:57 +0800 Subject: [PATCH] Add: Display overall progress of character planner --- config/template.json | 1 + module/config/argument/args.json | 12 +- module/config/argument/argument.yaml | 9 +- module/config/argument/gui.yaml | 2 + module/config/argument/stored.json | 27 ++- module/config/config_generated.py | 3 +- module/config/i18n/en-US.json | 7 +- module/config/i18n/es-ES.json | 7 +- module/config/i18n/ja-JP.json | 7 +- module/config/i18n/zh-CN.json | 7 +- module/config/i18n/zh-TW.json | 7 +- module/config/stored/classes.py | 5 + module/config/stored/stored_generated.py | 2 + module/webui/app.py | 5 + module/webui/widgets.py | 14 +- tasks/planner/model.py | 226 ++++++++++++++++++----- tasks/planner/scan.py | 1 + 17 files changed, 277 insertions(+), 65 deletions(-) diff --git a/config/template.json b/config/template.json index f15b5a042..8ef17f84a 100644 --- a/config/template.json +++ b/config/template.json @@ -47,6 +47,7 @@ "ServerUpdate": "04:00" }, "Planner": { + "PlannerOverall": {}, "Item_Credit": {}, "Item_Trailblaze_EXP": {}, "Item_Traveler_Guide": {}, diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 915b789f2..f849355a8 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -220,6 +220,14 @@ } }, "Planner": { + "PlannerOverall": { + "type": "stored", + "value": {}, + "display": true, + "stored": "StoredPlannerOverall", + "order": 4, + "color": "#85e7f2" + }, "Item_Credit": { "type": "planner", "value": {}, @@ -729,9 +737,7 @@ "type": "stored", "value": {}, "display": "hide", - "stored": "StoredEchoOfWar", - "order": 4, - "color": "#85e7f2" + "stored": "StoredEchoOfWar" }, "SimulatedUniverse": { "type": "stored", diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 39430f202..1a7052d33 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -129,15 +129,18 @@ DungeonStorage: stored: StoredDungeonDouble EchoOfWar: stored: StoredEchoOfWar - order: 4 - color: "#85e7f2" SimulatedUniverse: stored: StoredSimulatedUniverse order: 6 color: "#8fb5fe" SupportReward: Collect: true -Planner: {} +Planner: + PlannerOverall: + stored: StoredPlannerOverall + display: true + order: 4 + color: "#85e7f2" # Items will be injected in config updater Weekly: diff --git a/module/config/argument/gui.yaml b/module/config/argument/gui.yaml index 419167b0c..0a8837328 100644 --- a/module/config/argument/gui.yaml +++ b/module/config/argument/gui.yaml @@ -61,6 +61,8 @@ Dashboard: HoursAgo: DaysAgo: LongTimeAgo: + # Planner + EtaDays: AddAlas: PopupTitle: diff --git a/module/config/argument/stored.json b/module/config/argument/stored.json index ced444313..b15996aa5 100644 --- a/module/config/argument/stored.json +++ b/module/config/argument/stored.json @@ -38,15 +38,15 @@ "order": 3, "color": "#79dbc4" }, - "EchoOfWar": { - "name": "EchoOfWar", - "path": "Dungeon.DungeonStorage.EchoOfWar", - "i18n": "DungeonStorage.EchoOfWar.name", - "stored": "StoredEchoOfWar", + "PlannerOverall": { + "name": "PlannerOverall", + "path": "Dungeon.Planner.PlannerOverall", + "i18n": "Planner.PlannerOverall.name", + "stored": "StoredPlannerOverall", "attrs": { "time": "2020-01-01 00:00:00", - "total": 3, - "value": 0 + "comment": " Output: value = values.pop("value", "") total = values.pop("total", "") time_ = values.pop("time", "") + comment = values.pop("comment", "") if value != "" and total != "": # 0 / 100 @@ -353,6 +354,12 @@ def put_arg_stored(kwargs: T_Output_Kwargs) -> Output: put_text(value).style("--dashboard-value--"), 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 != "": # 100 rows = [put_scope(f"dashboard-value-{name}", [ @@ -393,6 +400,11 @@ def put_arg_planner(kwargs: T_Output_Kwargs) -> Output | None: except KeyError: # Hide items not needed by the planner 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) if isinstance(value, dict): @@ -402,7 +414,7 @@ def put_arg_planner(kwargs: T_Output_Kwargs) -> Output | None: total = tuple(total.values()) 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--"), ]) diff --git a/tasks/planner/model.py b/tasks/planner/model.py index 535ccb868..14e4f07cd 100644 --- a/tasks/planner/model.py +++ b/tasks/planner/model.py @@ -1,8 +1,9 @@ +import math import typing as t 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.config.stored.classes import now @@ -74,16 +75,37 @@ class MultiValue(BaseModelWithFallback): self.blue += other.blue 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): 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): item: ITEM_TYPES value: int | MultiValue = 0 total: int | MultiValue = 0 synthesize: int | MultiValue = 0 - progress: float = 0. + # progress: float = 0. + # eta: float = 0. time: datetime = DEFAULT_TIME @field_validator('item', mode='before') @@ -110,6 +132,12 @@ class StoredPlannerProxy(BaseModelWithFallback): self.synthesize = 0 return self + def clear(self): + if self.item.has_group_base: + self.value.clear() + else: + self.value = 0 + def update_synthesize(self): if self.item.has_group_base: green = self.value.green - self.total.green @@ -147,6 +175,82 @@ class StoredPlannerProxy(BaseModelWithFallback): else: 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): """ Args: @@ -157,46 +261,47 @@ class StoredPlannerProxy(BaseModelWithFallback): """ wave_done = max(wave_done, 0) # Items with a static drop rate will have `AVG * (wave_done + 1) - if self.item.dungeon.is_Calyx_Golden_Treasures: - return self.value + 24000 * (wave_done + 12) >= self.total - if self.item.dungeon.is_Calyx_Golden_Memories: - # purple, blue, green = 5, 1, 0 - value = self.value.equivalent_green() - 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) + remain = self.progress_remain + cost = self.combat_cost + drop = self.drop_equivalent_green + if cost == 10: + return remain <= drop * (wave_done + 12) else: - progress = self.value / self.total * 100 - self.progress = round(min(max(progress, 0), 100), 2) + return remain <= drop * (wave_done + 1) + + @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_progress() - self.time = now() + _ = self.progress + _ = self.eta + if time: + self.time = now() def load_value_total(self, item: ItemBase, value=None, total=None, synthesize=None): """ @@ -344,9 +449,11 @@ class PlannerProgressParser: def from_config(self, data): self.rows = {} - for row in data.values(): + for name, row in data.items(): if not row: continue + if name == 'PlannerOverall': + continue try: row = StoredPlannerProxy(**row) except (ScriptError, ValidationError) as e: @@ -355,8 +462,7 @@ class PlannerProgressParser: if not row.item.is_group_base: logger.error(f'from_config: item is not group base {row}') continue - row.update_synthesize() - row.update_progress() + row.update(time=False) self.rows[row.item.name] = row return self @@ -372,17 +478,37 @@ class PlannerProgressParser: self.rows[name] = row for row in self.rows.values(): - row.update() + row.update(time=True) def to_config(self) -> dict: data = {} for row in self.rows.values(): name = f'Item_{row.item.name}' - dic = row.model_dump() + dic = row.model_dump(exclude=SET_ROW_EXCLUDE) dic['item'] = row.item.name data[name] = dic 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]: """ Args: @@ -494,6 +620,7 @@ class PlannerMixin(UI): planner = self.planner data = planner.to_config() + progress, eta = planner.get_overall() with self.config.multi_set(): # Set value @@ -506,5 +633,14 @@ class PlannerMixin(UI): remove.append(key) for key in remove: 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') + + +if __name__ == '__main__': + self = PlannerMixin('src') + self.planner_write(self.planner) diff --git a/tasks/planner/scan.py b/tasks/planner/scan.py index ddda92178..bca9b6fd8 100644 --- a/tasks/planner/scan.py +++ b/tasks/planner/scan.py @@ -22,6 +22,7 @@ DETAIL_TITLE.load_search(RESULT_CHECK.search) class OcrItemName(Ocr): def after_process(self, result): result = result.replace('念火之心', '忿火之心') + result = re.sub('^火之心', '忿火之心', result) result = re.sub('工造机$', '工造机杼', result) result = re.sub('工造迥?轮', '工造迴轮', result) result = re.sub('月狂[療撩]?牙', '月狂獠牙', result)