Add: Display overall progress of character planner

This commit is contained in:
LmeSzinc 2024-06-12 03:20:57 +08:00
parent d6d61bb6fc
commit 33eedc2c63
17 changed files with 277 additions and 65 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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": "<??d",
"value": "??%"
},
"order": 4,
"color": "#85e7f2"
@ -714,6 +714,19 @@
"order": 0,
"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": {
"name": "DailyQuest",
"path": "DailyQuest.DailyStorage.DailyQuest",

View File

@ -53,7 +53,7 @@ class GeneratedConfig:
# Group `DungeonSupport`
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`
DungeonStorage_TrailblazePower = {}
@ -66,6 +66,7 @@ class GeneratedConfig:
SupportReward_Collect = True
# Group `Planner`
Planner_PlannerOverall = {}
Planner_Item_Credit = {}
Planner_Item_Trailblaze_EXP = {}
Planner_Item_Traveler_Guide = {}

View File

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

View File

@ -465,6 +465,10 @@
"name": "Progreso del planificador de personajes",
"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": {
"name": "Crédito",
"help": ""
@ -1269,7 +1273,8 @@
"MinutesAgo": "hace {time}m",
"HoursAgo": "hace {time}h",
"DaysAgo": "hace {time}d",
"LongTimeAgo": "hace mucho tiempo"
"LongTimeAgo": "hace mucho tiempo",
"EtaDays": "ETA {time}d"
},
"AddAlas": {
"PopupTitle": "Añadir nueva configuración",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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