diff --git a/.gitignore b/.gitignore index cadf1bb8..08c02e1c 100644 --- a/.gitignore +++ b/.gitignore @@ -125,6 +125,8 @@ LICENSE picture_list.json version.json *relics*.json +char_panel.json +char_weight.json screencast.png screencast1.png utils/A_.py diff --git a/README.md b/README.md index 461f6cbb..740cb6d9 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,20 @@ This software is open source, free of charge and for learning and exchange purpo - [ ] 模拟宇宙正在开发 - [x] GUI开发 - [ ] 后续将会新增找宝箱、锄大地顺带捡垃圾等功能 +- [x] 遗器模块 (不支持GUI) + - [x] 遗器的识别、匹配、搜索 + - [x] 角色配装的保存、编辑、读取并装备 + - [x] 队伍配装的保存、读取并装备 + - [x] 支持创建、编辑、载入角色裸装面板与属性权重 + - [x] 支持对相关文本的风格化打印 + - [ ] 对遗器与角色配装的评估、推荐 [项目进度请点击查看](https://github.com/users/Night-stars-1/projects/2) +## 遗器模块展示 + + + ## 贡献 [问题反馈](https://github.com/Starry-Wind/StarRailAssistant/issues/new/choose) | [PR 提交](https://github.com/Starry-Wind/StarRailAssistant/compare) diff --git a/data/fixed_data/char_weight_default.json b/data/fixed_data/char_weight_default.json new file mode 100644 index 00000000..1c7a3d05 --- /dev/null +++ b/data/fixed_data/char_weight_default.json @@ -0,0 +1,3 @@ +{ + "说明": "此文件为预留文件,待后续开发 (内容物为角色的默认属性权重)" +} \ No newline at end of file diff --git a/utils/calculated.py b/utils/calculated.py index 399b5625..7a962968 100644 --- a/utils/calculated.py +++ b/utils/calculated.py @@ -6,6 +6,7 @@ import re import sys import time +import copy from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Literal, Optional, Tuple, Union @@ -22,6 +23,8 @@ from pynput.keyboard import Controller as KeyboardController from pynput.keyboard import Key from pynput.mouse import Controller as MouseController +from questionary import Validator, ValidationError, Style +from collections.abc import Iterable from .config import CONFIG_FILE_NAME, _, get_file, sra_config_obj from .cv_tools import CV_Tools, show_img @@ -699,6 +702,7 @@ def part_ocr(self, points = (0,0,0,0), debug=False, left=False, number=False, im if debug: log.info(data) # show_img(img_fp) + os.makedirs("logs/image", exist_ok=True) timestamp_str = str(int(datetime.timestamp(datetime.now()))) cv.imwrite(f"logs/image/relic_{str(points)}_{timestamp_str}.png", img_fp) else: @@ -993,16 +997,19 @@ def change_team(self): class Array2dict: - def __init__(self, arr: np.ndarray, key_index: int = -1, value_index: Optional[int]=None): + def __init__(self, arr: Union[np.ndarray, List[str]], key_index: int = -1, value_index: Optional[int]=None): """ 说明: - 将np数组转化为字典暂住内存,用于对数组短时间内的频繁查找 + 将np数组或序列转化为字典暂住内存,用于短时间内的频繁查找 参数: - :param arr: 二维数组 + :param arr: 二维数组或一维序列 :param key_index: 待查找的关键字所在的数组列标 :param value_index: 待查找的数值所在的数组列标 (为空时表示查找关键字的行标) """ - if arr.ndim != 2: + if isinstance(arr, list): + self.data_dict = {element: idx for idx, element in enumerate(arr)} + return + if isinstance(arr, np.ndarray) and arr.ndim != 2: raise ValueError("输入的数组必须为二维数组") # 将np数组转化为字典 if value_index is None: # 默认将key的行标作为value,以取代np.where @@ -1012,8 +1019,137 @@ def __init__(self, arr: np.ndarray, key_index: int = -1, value_index: Optional[i # log.debug(self.data_dict) def __getitem__(self, key: Any) -> Any: - return self.data_dict[key] + return self.data_dict.get(key, None) + +class FloatValidator(Validator): + """ + 说明: + 为questionary校验所输入的文本是否为规定范围内的小数 + """ + def __init__(self, st: Optional[float]=None, ed: Optional[float]=None) -> None: + super().__init__() + self.st = st + self.ed = ed + + def validate(self, document): + try: + number = float(document.text) + except ValueError: + raise ValidationError(message=_("请输入整数或小数")) + if self.st is not None and number < self.st: + raise ValidationError(message=_("数字小于下界{}").format(self.st)) + if self.ed is not None and number > self.ed: + raise ValidationError(message=_("数字大于下界{}").format(self.ed)) + + +class ConflictValidator(Validator): + """ + 说明: + 为questionary校验所输入的文本是否存在命名冲突 + """ + def __init__(self, names: Iterable[str]) -> None: + super().__init__() + self.names = names + + def validate(self, document): + if document.text in self.names: + raise ValidationError(message=_("存在命名冲突"), cursor_position=len(document.text)) + + +class StyledText(List[Tuple[str, str]]): + """ + 说明: + 风格化文本序列,继承 List[Tuple[str, str]],(0-style_class, 1-text), + 重载了`append()`和`extend()`方法,支持链式操作。 + 可直接作为`questionary.Choice.description`的初始化参数 + """ + def __getitem__(self, key: slice) -> List["StyledText"]: + return StyledText(super().__getitem__(key)) + + def append(self, text: Union[str, Tuple[str, str]], style_class: str="") -> "StyledText": + if isinstance(text, str): + if style_class and "class:" not in style_class: + style_class = "class:" + style_class + super().append((style_class, text)) + else: + super().append(text) + return self + + def extend(self, *texts: Iterable[Tuple[str, str]], sep: Optional[Union[str, Tuple[str, str]]]=None, indent: int=0) -> "StyledText": + """ + 说明: + 继承`list.extend()` + 参数: + :param texts: 任意个`StyledText`类型的文本序列 + :param sep: 插入在文本序列间的内容,默认为空 + :param indent: 起始位置的缩进长度,默认为零 + """ + if indent: + self.append(" "*indent) + for i, __iterable in enumerate(texts): + if i and sep: + self.append(sep) + super().extend(__iterable) + return self + + def splitlines(self, keepends=False) -> List["StyledText"]: + """ + 说明: + 按行分割为`StyledText`序列 + """ + lines_list = [StyledText()] + def end_with_n(s :str) -> bool: + return s[-1] == "\n" + for style, text in self: + if text == "": + continue + lines = str(text).splitlines(keepends=True) + lines_list[-1].append( + (style, lines[0][:-1] if end_with_n(lines[0]) and not keepends else lines[0]) + ) + for line in lines[1:]: + lines_list.append( + StyledText([ + (style, line[:-1] if end_with_n(line) and not keepends else line) + ])) + if end_with_n(lines[-1]): # 开启空的一行 + lines_list.append(StyledText()) + if not lines_list[-1]: # 删除无效行 + lines_list.pop() + return lines_list + +def combine_styled_text(*texts: StyledText, prefix: Optional[Union[str, Tuple[str, str]]]=None, **kwargs) -> StyledText: + """ + 说明: + 将多个风格化文本序列,按行进行横向并联 + 参数: + :param sep: 插入在文本序列间的内容,默认为空 + :param prefix: 文本前缀 + :param indent: 起始位置的缩进长度,默认为零 + """ + result = StyledText() + lines_list_list: List[List[StyledText]] = [] + if prefix: + result.append(prefix) + for text in texts: + lines_list_list.append(text.splitlines()) + for line in zip(*lines_list_list): + result.extend(*line, **kwargs) + result.append("\n") + return result + +def print_styled_text(text: StyledText, style: Style, **kwargs: Any) -> None: + """ + 说明: + 打印风格化文本 + """ + from prompt_toolkit import print_formatted_text + from prompt_toolkit.formatted_text import FormattedText + + print_formatted_text(FormattedText(text), style=style, **kwargs) + + def get_data_hash(data: Any, key_filter: Optional[List[str]]=None, speed_modified=False) -> str: """ 说明: @@ -1027,7 +1163,7 @@ def get_data_hash(data: Any, key_filter: Optional[List[str]]=None, speed_modifie if not key_filter: tmp_data = data elif isinstance(data, dict): - tmp_data = {key: value for key, value in data.items() if key not in key_filter} + tmp_data = copy.deepcopy({key: value for key, value in data.items() if key not in key_filter}) # 深拷贝 if speed_modified and _("速度") in tmp_data["subs_stats"]: tmp_data["subs_stats"][_("速度")] = float(int(tmp_data["subs_stats"][_("速度")])) # 去除小数部分 else: diff --git a/utils/config.py b/utils/config.py index f1df29f9..86a40197 100644 --- a/utils/config.py +++ b/utils/config.py @@ -13,26 +13,43 @@ from .log import log CONFIG_FILE_NAME = "config.json" -RELIC_FILE_NAME = "relics_set.json" -LOADOUT_FILE_NAME = "relics_loadout.json" -TEAM_FILE_NAME = "relics_team.json" + +USER_DATA_PREFIX = "data/user_data/" +FIXED_DATA_PREFIX = "data/fixed_data/" +os.makedirs(USER_DATA_PREFIX, exist_ok=True) + +RELIC_FILE_NAME = USER_DATA_PREFIX + "relics_set.json" +LOADOUT_FILE_NAME = USER_DATA_PREFIX + "relics_loadout.json" +TEAM_FILE_NAME = USER_DATA_PREFIX + "relics_team.json" +CHAR_PANEL_FILE_NAME = USER_DATA_PREFIX + "char_panel.json" +CHAR_WEIGHT_FILE_NAME = USER_DATA_PREFIX + "char_weight.json" def normalize_file_path(filename): # 尝试在当前目录下读取文件 current_dir = os.getcwd() - file_path = os.path.join(current_dir, filename) - if os.path.exists(file_path): - return file_path + pre_file_path = os.path.join(current_dir, filename) + if os.path.exists(pre_file_path): + return pre_file_path else: # 如果当前目录下没有该文件,则尝试在上一级目录中查找 parent_dir = os.path.dirname(current_dir) file_path = os.path.join(parent_dir, filename) if os.path.exists(file_path): return file_path - else: - # 如果上一级目录中也没有该文件,则返回None - return None + # 如果仍然没有,则尝试在当前目录仅查找文件名 + pre_filename = str(filename).rsplit('/', 1)[-1] + file_path = os.path.join(current_dir, pre_filename) + if os.path.exists(file_path): + if str(filename).rsplit('/', 1)[0] == USER_DATA_PREFIX[:-1]: + # 判断为旧版本 (<=1.8.7) 数据文件位置 + import shutil + shutil.move(file_path, pre_file_path) + log.info(_("文件位置更改,由'{}'迁移至'{}'").format(pre_filename, filename)) + return pre_file_path + return file_path + # 如果仍然没有,则返回None + return None def read_json_file(filename: str, path=False, schema:dict=None) -> dict: @@ -328,6 +345,8 @@ class SRAData(metaclass=SRADataMeta): """是否在打印遗器信息时显示拓展信息""" ndigits_for_relic: int = 2 """在打印遗器信息时的小数精度""" + stats_weight_for_relic: int = 0 + """遗器副词条档位权重:0-空,1-主流赋值,2-真实比例赋值,3-主流赋值比例矫正""" auto_shutdown: bool = False """是否自动关机""" diff --git a/utils/questionary/questionary/prompts/common.py b/utils/questionary/questionary/prompts/common.py index 634136d0..3896fbc9 100644 --- a/utils/questionary/questionary/prompts/common.py +++ b/utils/questionary/questionary/prompts/common.py @@ -72,7 +72,7 @@ class Choice: shortcut_key: Optional[str] """A shortcut key for the choice""" - description: Optional[str] + description: Optional[FormattedText] """Choice description""" def __init__( @@ -82,7 +82,7 @@ def __init__( disabled: Optional[str] = None, checked: Optional[bool] = False, shortcut_key: Optional[Union[str, bool]] = True, - description: Optional[str] = None, + description: Optional[FormattedText] = None, ) -> None: self.disabled = disabled self.title = title @@ -445,7 +445,11 @@ def append(index: int, choice: Choice): description = current.description if description is not None: - tokens.append(("class:text", " Description: {}".format(description))) + tokens.append(("class:text", " Description: ")) + if isinstance(description, list): + tokens.extend(description) + else: + tokens.append(("class:text", description)) else: tokens.pop() # Remove last newline. return tokens diff --git a/utils/questionary/questionary/prompts/select.py b/utils/questionary/questionary/prompts/select.py index 6f3ef8a6..45ac5dcc 100644 --- a/utils/questionary/questionary/prompts/select.py +++ b/utils/questionary/questionary/prompts/select.py @@ -32,7 +32,7 @@ def select( use_shortcuts: bool = False, use_arrow_keys: bool = True, use_indicator: bool = False, - use_jk_keys: bool = True, + use_jk_keys: bool = False, use_emacs_keys: bool = True, show_selected: bool = False, show_description: bool = True, @@ -135,14 +135,18 @@ def select( if choices is None or len(choices) == 0: raise ValueError("A list of choices needs to be provided.") - if use_shortcuts and len(choices) > len(InquirerControl.SHORTCUT_KEYS): - raise ValueError( - "A list with shortcuts supports a maximum of {} " - "choices as this is the maximum number " - "of keyboard shortcuts that are available. You" - "provided {} choices!" - "".format(len(InquirerControl.SHORTCUT_KEYS), len(choices)) + if use_shortcuts: + real_len_of_choices = sum( + 1 for c in choices if not isinstance(c, Separator) ) + if real_len_of_choices > len(InquirerControl.SHORTCUT_KEYS): + raise ValueError( + "A list with shortcuts supports a maximum of {} " + "choices as this is the maximum number " + "of keyboard shortcuts that are available. You " + "provided {} choices!" + "".format(len(InquirerControl.SHORTCUT_KEYS), real_len_of_choices) + ) merged_style = merge_styles_default([style]) @@ -204,7 +208,7 @@ def _(event): "for movement are disabled. " "This choice is not reachable.".format(c.title) ) - if isinstance(c, Separator) or c.shortcut_key is None: + if isinstance(c, Separator) or c.shortcut_key is None or c.disabled: continue # noinspection PyShadowingNames diff --git a/utils/relic.py b/utils/relic.py index 82dd8c5d..4d0e865b 100644 --- a/utils/relic.py +++ b/utils/relic.py @@ -3,74 +3,73 @@ import math import pprint import numpy as np +from itertools import chain from collections import Counter from typing import Any, Dict, List, Literal, Optional, Tuple, Union -from .questionary.questionary import select, Choice +import utils.questionary.questionary as questionary +from utils.questionary.questionary import Choice, Separator, Style # 改用本地的questionary模块,使之具备show_description功能,基于'tmbo/questionary/pull/330' -# from questionary import select, Choice # questionary原项目更新并具备当前功能后,可进行替换 +# import questionary # questionary原项目更新并具备当前功能后,可进行替换 from .relic_constants import * -from .calculated import calculated, Array2dict, get_data_hash, str_just -from .config import (read_json_file, modify_json_file, rewrite_json_file, - RELIC_FILE_NAME, LOADOUT_FILE_NAME, TEAM_FILE_NAME, _, sra_config_obj) +from .calculated import (calculated, Array2dict, StyledText, FloatValidator, ConflictValidator, + get_data_hash, str_just, print_styled_text, combine_styled_text) +from .config import (RELIC_FILE_NAME, LOADOUT_FILE_NAME, TEAM_FILE_NAME, CHAR_PANEL_FILE_NAME, CHAR_WEIGHT_FILE_NAME, USER_DATA_PREFIX, + read_json_file, modify_json_file, rewrite_json_file, _, sra_config_obj) from .exceptions import Exception, RelicOCRException from .log import log pp = pprint.PrettyPrinter(indent=1, width=40, sort_dicts=False) IS_PC = True # paltform flag (同时保存了模拟器与PC的识别位点) +INDENT = "\n" + " " * 5 # description的一级缩进 -class Relic: - """ - <<<遗器模块>>> - 已完成功能: - 1.识别遗器数据 (单次用时约0.5s) - a.录入的遗器数据保存在'relics_set.json'文件 - b.可识别遗器的[部位、套装、稀有度、等级、主词条、副词条]属性 - c.支持所有稀有度遗器 (识别指定点位色相[黄、紫、蓝、绿]) - 2.遗器数据匹配 - a.精确匹配:通过计算与匹配遗器哈希值 - b.模糊匹配:判断新旧遗器是否存在升级关系,若匹配成功,则新遗器将自动替换配装中的旧遗器, - 并在遗器数据中建立后继关系,此功能可通过设置开关 - 3.遗器数据增强 - a.支持计算[四星、五星]遗器的副词条的[强化次数、档位总积分、修正数值(提高原数值的小数精度)] - 1).对于'速度'属性只能做保守估计,其他属性可做准确计算 - 2).【新增】可借助其他工具获得'速度'属性的精确值,并手动修改json文件中'速度'属性的小数位, - 修改后的数据可永久保留,将不影响遗器哈希值计算与模糊匹配,并用于后续的数值计算 - b.【新增】支持计算[四星、五星]遗器的主词条的[修正数值] - c.【新增】遗器数据打印时的小数精度可通过设置选择,范围为[0,1,2,3] - d.基于遗器数据增强的遗器数据校验功能 (可检测出大部分的遗器识别错误),可通过设置开关 - e.遗器数据增强可通过设置开关 - 4.保存角色配装 - a.录入的配装数据保存在'relics_loadout.json'文件 - b.【新增】可检查配装是否已经存在,存在的配装不重复录入 - 5.读取角色配装并装备 - a.基于遗器匹配,遗器将强制'替换',包含[替换己方已装备的遗器、替换对方已装备的遗器] - b.自动对遗器的[套装、稀有度]属性进行筛选,加快遗器搜索 - c.【新增】配装选择时,将会打印配装信息,包含[内外圈套装、遗器主词条名称、属性数值统计] - 6.【新增】保存队伍配装 - a.录入的队伍配装数据保存在'relics_team.json'文件 - b.录入方式包含[全识别、参考已有的配装数据] - c.可检查队伍是否存在冲突遗器 - 7.【新增】读取队伍配装并装备 - a.队伍选择时,将会打印队伍信息,包含[角色构成、各角色内外圈套装、各角色遗器主词条名称] - b.对当前队伍的角色顺序不做要求 - b.只支持对已有队伍进行配装,不支持选择相应角色构建队伍 - - 待解决问题: - 1.【已解决】OCR准确率低: - 对于中文识别更换为项目早期的OCR模型;对于数字识别更换为仅包含英文数字的轻量模型 - 待开发功能: - 1.配装管理 [删、改] (需考虑队伍配装) - 2.对忘却之庭双队配装的保存做额外处理,并检查队伍间的遗器冲突 - ... - - 开发者说明: - 1.本模块的所有识别位点均采用百分比相对坐标,以兼容不同平台支持不同分辨率 - 2.本模块首先会基于安卓模拟器进行测试,再基于PC端测试 - 3.【新增】本模块的主体功能已全部完成,现转入日常维护与不定时支线功能开发 - 4.【新增】本模块暂不支持简体中文之外的语言 - 4.【新增】本模块暂未有开发GUI的计划 +class StatsWeight: + """ + 说明: + 属性权重 """ + def __init__(self, weight: Dict[str, float]={}): + self.weight = {} + for key, value in weight.items(): + if key in [_("生命值"), _("攻击力"), _("防御力")]: + self.weight[key+"%"] = value # 大词条 + self.weight[key] = value / 2 # 小词条,权重减半 + continue + elif key == _("速度"): + self.weight["速度%"] = value # 大小词条,权值等同 + self.weight[key] = value + + def get_color(self, key: str, modify=False) -> str: + if self.weight == {}: # 未载入权重数据的情形 + return "" + # 白值打印修饰 + if key[-2:] == _("白值"): + value = self.get_weight(key[:-2]+"%") # 白值视为大词条 + # if value == 0: + # value = 0.1 # 标记值,意为所有白值至少为"weight_1" + else: + value = self.get_weight(key, modify) + # 赋色 + if value == 0: + return "weight_0" # 灰色 (无效词条) + elif value <= 0.5: + return "weight_1" # 白色 + else: + return "weight_2" # 黄色 + + def get_weight(self, key: str, modify=False) -> float: + # 权重打印修饰 + if modify and key in [_("生命值"), _("攻击力"), _("防御力")]: + key += "%" + return self.weight.get(key, 0) # 缺损值默认为无效词条 + + def __repr__(self) -> str: + return "\n" + pp.pformat(self.weight) + + def __bool__(self) -> bool: + return self.weight != {} + +class Relic: def __init__(self, title=_("崩坏:星穹铁道")): """ @@ -89,52 +88,116 @@ def __init__(self, title=_("崩坏:星穹铁道")): """是否在打印遗器信息时显示详细信息 (如各副词条的强化次数、档位积分,以及提高原数据的小数精度)""" self.ndigits: Literal[0, 1, 2, 3] = sra_config_obj.ndigits_for_relic """在打印遗器信息时的小数精度""" + self.subs_stats_iter_weight: Literal[0, 1, 2, 3] = sra_config_obj.stats_weight_for_relic + """副词条档位权重选择:0-空,1-主流赋值,2-真实比例赋值,3-主流赋值的比例矫正""" + self.activate_conditional = False + """在打印面板信息时开启条件效果""" + self.loadout_detail_type: Literal[0, 1] = 0 + """配装详情的类别:0-面板详情,1-遗器详情""" + self.msg_style = Style([ + ("highlighted", "fg:#FF9D00 bold"), # 在select中对选中的项进行高亮 + ("bold", "bold"), + ("green", "fg:#45bd45"), + ("grey", "fg:#5f819d"), + ("orange", "fg:#FF9D00"), + ("red", "red"), + ("reverse", "reverse"), + ("grey_reverse", "fg:#5f819d reverse"), + ("rarity_5", "orange"), + ("rarity_4", "magenta"), + ("rarity_3", "blue"), + ("rarity_2", "green"), + ("weight_0", "fg:#5f819d"), + ("weight_1", "bold"), + ("weight_2", "fg:#FF9D00 bold") + ]) # 读取json文件,仅初始化时检查格式规范 - self.relics_data: Dict[str, Dict[str, Any]] = read_json_file(RELIC_FILE_NAME, schema = RELIC_SCHEMA) - self.loadout_data: Dict[str, Dict[str, List[str]]] = read_json_file(LOADOUT_FILE_NAME, schema = LOADOUT_SCHEMA) - self.team_data: Dict[str, Dict[str, Any]] = read_json_file(TEAM_FILE_NAME, schema = TEAM_SCHEMA) + self.relics_data: Dict[str, Dict[str, Any]] = read_json_file(RELIC_FILE_NAME, schema=RELIC_SCHEMA) + self.char_panel_data: Dict[str, Dict[str, Dict[str, Any]]] = read_json_file(CHAR_PANEL_FILE_NAME, schema=CHAR_STATS_PANEL_SCHEMA) + self.char_weight_data: Dict[str, Dict[str, Dict[str, Any]]] = read_json_file(CHAR_WEIGHT_FILE_NAME, schema=CHAR_STATS_WEIGHT_SCHEMA) + try: + self.loadout_data: Dict[str, Dict[str, Dict[str, Any]]] = read_json_file(LOADOUT_FILE_NAME, schema=LOADOUT_SCHEMA) + except: + # 尝试使用旧版本 (<=1.8.7) 格式读取数据 + log.error(_(f"'{LOADOUT_FILE_NAME}'读取失败,尝试使用旧版本格式读取")) + self.loadout_data: Dict[str, Dict[str, List[str]]] = read_json_file(LOADOUT_FILE_NAME, schema=LOADOUT_SCHEMA_OLD) + for loadouts in self.loadout_data.values(): + for loadout_name, relic_hash in loadouts.items(): + loadouts[loadout_name] = {"relic_hash": relic_hash} + # pp.pprint(self.loadout_data) + rewrite_json_file(LOADOUT_FILE_NAME, self.loadout_data) + log.info(_(f"'{LOADOUT_FILE_NAME}'再读成功,已将其更新至当前版本格式")) + self.team_data: Dict[str, Dict[str, Dict[str, Any]]] = read_json_file(TEAM_FILE_NAME, schema=TEAM_SCHEMA) - log.info(_("遗器模块初始化完成")) - log.info(_(f"共载入 {len(list(self.relics_data.keys()))} 件遗器数据")) - log.info(_(f"共载入 {sum(len(char_loadouts) for char_name, char_loadouts in self.loadout_data.items())} 套配装数据")) - log.info(_(f"共载入 {sum(len(group_data) for group_name, group_data in self.team_data.items())} 组队伍数据")) + log.info(_("共载入 {} 件遗器数据").format(len(self.relics_data))) + log.info(_("共载入 {} 套配装数据").format(sum(len(char_loadouts) for char_loadouts in self.loadout_data.values()))) + log.info(_("共载入 {} 组队伍数据").format(sum(len(group_data) for group_data in self.team_data.values()))) + log.info(_("共载入 {} 位角色的裸装面板").format(len(self.char_panel_data))) + log.info(_("共载入 {} 位角色的属性权重").format(len(self.char_weight_data))) # 校验遗器哈希值 if not self.check_relic_data_hash(): - option = select(_("是否依据当前遗器数据更新哈希值:"), [_("是"), _("否")]).ask() - if option == _("是"): + if questionary.confirm(_("是否依据当前遗器数据更新哈希值"), default=False).ask(): self.check_relic_data_hash(updata=True) # 校验队伍配装规范 if not self.check_team_data(): log.error(_("怀疑为手动错误修改json文件导致")) + # 首次启动权值进行选择 + if self.subs_stats_iter_weight == 0: + msg = INDENT+_("仅首次启动进行选择")+INDENT+_("后续可在'config.json'中的'stats_weight_for_relic'选择设置[1,2,3]") + self.subs_stats_iter_weight = questionary.select( + _("请选择副词条三个档位的权值"), + choices = [ + Choice(SUBS_STATS_TIER_WEIGHT[1][-1], value = 1, + description = INDENT+_("主流赋值")+msg), + Choice(SUBS_STATS_TIER_WEIGHT[2][-1], value = 2, + description = INDENT+_("真实比例,除了五星遗器'速度'属性的真实比例为 [0.7692, 0.8846, 1.0]")+msg), + Choice(SUBS_STATS_TIER_WEIGHT[3][-1], value = 3, + description = INDENT+_("主流赋值比例矫正后的结果")+msg), + Separator(" ") + ], + instruction = _("(将影响词条计算与遗器评分)"), + use_shortcuts = True, + ).ask() + sra_config_obj.stats_weight_for_relic = self.subs_stats_iter_weight # 修改配置文件 + # 用户提示 + questionary.print(_("推荐使用 Windows Termianl 并将窗口调整至合适宽度,以达到更佳的显示效果"), "green") def relic_entrance(self): """ 说明: 遗器模块入口 """ - title = _("遗器模块:") - tab = "\n" + " " * 5 - options = [ - Choice(_("保存当前角色的配装"), value = 0, - description = tab + _("请使游戏保持在[角色]界面")), - Choice(_("保存当前队伍的配装"), value = 1, - description = tab + _("请使游戏保持在[角色]界面") + tab + _("并确保[角色头像列表]移动至开头")), - Choice(_("读取当前角色的配装记录"), value = 2, - description = tab + _("请使游戏保持在[角色]界面")), - Choice(_("读取队伍的配装记录"), value = 3, - description = tab + _("请使游戏保持在[角色]界面") + tab + _("并确保[角色头像列表]移动至开头")), - Choice(_("识别当前遗器数据"), value = 4, - description = tab + _("请使游戏保持在[角色]-[遗器]-[遗器替换]界面") + tab + _("推荐手动点击[对比]提高识别度")), - _("<返回主菜单>") - ] # 注:[角色]界面的前继可为[队伍]-[角色选择]-[详情]界面 + title = _("遗器模块:") option = None # 保存上一次的选择 + msg = "\n"+INDENT+_("注:[角色]界面的前继可为[队伍]-[角色选择]-[详情]等界面") while True: + options = [ + Choice(_("识别遗器数据"), value = 4, + description = INDENT+_("支持批量识别、载入[属性权重]进行评估、导出数据\n")+INDENT+_("注:对于[速度]副属性只能做保守评估,其他属性可做准确计算") + +INDENT+_(" 可以借助第三方工具获得[速度]副属性的精确值,")+INDENT+_(" 并手动修改'relics_set.json'文件中相应的小数位,")+INDENT+_(" 修改后的数据会永久保存,不影响遗器哈希值,可用于后续评估")), + Choice(_("保存当前角色的配装"), value = 0, + description = INDENT+_("请使游戏保持在[角色]界面")+msg), + Choice(_("保存当前队伍的配装"), value = 1, + description = INDENT+_("请使游戏保持在[角色]界面")+INDENT+_("并确保目标队伍[1号位]-[角色头像]移动至列表[起始位置]")+INDENT+_("对于[混沌回忆]的队伍可以分上下半分别保存")+msg), + Choice(_("读取当前角色的配装并装备"), value = 2, + description = INDENT+_("请使游戏保持在[角色]界面")+msg), + Choice(_("读取队伍的配装并装备"), value = 3, + description = INDENT+_("请使游戏保持在[角色]界面")+INDENT+_("并确保目标队伍[1号位]-[角色头像]移动至列表[起始位置]")+INDENT+_("对于[混沌回忆]的队伍可以分上下半分别读取")+msg), + Choice(_("编辑角色配装"), value = 5, + description = INDENT+_("支持查看配装、配装重命名、替换遗器等功能")), + Choice(_("编辑角色裸装面板"), value = 6, + description = INDENT+_("此处的[角色裸装面板]是指角色卸下[遗器]、佩戴[光锥]时的角色面板")+INDENT+_("若不满足于交互界面,可在明晰构造方法的前提下直接编辑'char_panel.json'文件")), + Choice(_("编辑角色属性权重"), value = 7, + description = INDENT+_("权重范围为0~1,缺损值为0")+INDENT+_("评分系统开发中...当前权重只会影响[属性着色]与[有效词条]计算")), + Choice(_("<清空控制台>"), shortcut_key='c'), + Choice(_("<返回主菜单>"), shortcut_key='z'), + Separator(" "), + ] self.calculated.switch_cmd() - option = select(title, options, default=option, show_description=True).ask() + option = questionary.select(title, options, default=option, use_shortcuts=True, style=self.msg_style).ask() if option == 0: - self.calculated.switch_window() self.save_loadout_for_char() elif option == 1: self.save_loadout_for_team() @@ -143,12 +206,443 @@ def relic_entrance(self): elif option == 3: self.equip_loadout_for_team() elif option == 4: - self.calculated.switch_window() - data = self.try_ocr_relic() - self.print_relic(data) + self.batch_ocr_relics() + elif option == 5: + self.edit_loadout_for_char() + elif option == 6: + self.edit_character_panel() + elif option == 7: + self.edit_character_weight() + elif option == _("<清空控制台>"): + import os + os.system("cls") elif option == _("<返回主菜单>"): break - + + def batch_ocr_relics(self, stats_weight: StatsWeight=StatsWeight(), weight_name: Optional[str]=None): + """ + 说明: + 批量识别遗器 + """ + # [1]选择识别范围与权重 + options_0 = [ + Choice(_("仅当前遗器"), value=0, + description = INDENT+_("请使游戏保持在[角色]-[遗器]-[遗器替换]界面")+INDENT+_("建议识别前手动点击[对比]提高识别度")), + Choice(_("当前筛选条件下的当前所选部位的所有遗器"), value=1, + description = INDENT+_("识别途中不可中断")+INDENT+_("请使游戏保持在[角色]-[遗器]-[遗器替换]界面")+INDENT+_("建议识别前手动点击[对比]提高识别度")), + Choice(_("<<载入属性权重>>"), shortcut_key='x', + description=combine_styled_text(self.print_stats_weight(stats_weight), prefix="\n 启用权重:\n", indent=5) if stats_weight else None), + Choice(_("<返回上一级>"), shortcut_key='z'), + Separator(" "), + ] + option_0 = questionary.select(_("请选择识别的范围"), options_0, use_shortcuts=True, style=self.msg_style).ask() + if option_0 == _("<返回上一级>"): + return + if option_0 == _("<<载入属性权重>>"): + charcacter_names = sorted(self.char_weight_data.keys()) # 对权重数据中角色名称排序 + options_1 = [] + for char_name in charcacter_names: + has_ = char_name in self.char_weight_data + __, weight = self.find_char_weight(char_name) + weight_text = combine_styled_text(self.print_stats_weight(weight), prefix="\n", indent=5) + choice_text = str_just(char_name, 15) + _("■ {}").format("1" if has_ else "0") + options_1.append( + Choice(choice_text, value=char_name, description=weight_text) + ) + options_1.extend([ + Choice(_("<<创建临时权重>>"), shortcut_key='x'), + Choice(_("<取消>"), shortcut_key='z'), + Separator(" ") + ]) + option_1 = questionary.select(_("请选择属性权重:"), options_1, use_shortcuts=True, style=self.msg_style).ask() + if option_1 == _("<取消>"): + return self.batch_ocr_relics() + elif option_1 == _("<<创建临时权重>>"): + stats_weight = self.edit_character_weight(True) + if stats_weight is None: # 取消创建 + return self.batch_ocr_relics() + weight_name = _("临时权重") + else: + weight_name, stats_weight = self.find_char_weight(option_1) + return self.batch_ocr_relics(stats_weight, weight_name) + # [2]进行识别 + relics_data = {} + self.calculated.switch_window() + if option_0 == 0: + tmp_data = self.try_ocr_relic() + tmp_hash = get_data_hash(tmp_data) + self.print_relic(tmp_data, tmp_hash, stats_weight) + relics_data[tmp_hash] = tmp_data + elif option_0 == 1: + relics_data = self.search_relic(stats_weight=stats_weight, overtime=None) + log.info(_("共识别到 {} 件遗器").format(len(relics_data))) + # [3]选择数据保存方式 + self.calculated.switch_cmd() + options_3 = [ + Choice(_("不保存"), value=0), + Choice(_("录入遗器数据库"), value=1), + Choice(_("保存至单独文件"), value=2), + ] + option_3 = questionary.select(_("请选择保存方式:"), options_3).ask() + if option_3 == 0: + return self.batch_ocr_relics(stats_weight, weight_name) + elif option_3 == 1: + cnt = 0 + for tmp_hash, tmp_data in relics_data.items(): + if tmp_hash not in self.relics_data: + self.add_relic_data(tmp_data, tmp_hash) + cnt += 1 + rewrite_json_file(RELIC_FILE_NAME, self.relics_data) + log.info(_("共写入 {} 件新遗器至'{}'文件").format(cnt, RELIC_FILE_NAME)) + elif option_3 == 2: + from datetime import datetime + file_name = "{}relics_set_{}.json".format(USER_DATA_PREFIX, str(int(datetime.timestamp(datetime.now())))) + rewrite_json_file(file_name, relics_data) + log.info(_("共写入 {} 件遗器至'{}'文件").format(len(relics_data), file_name)) + return self.batch_ocr_relics(stats_weight, weight_name) + + def edit_loadout_for_char(self): + """ + 说明: + 编辑角色配装 (查看配装、更改名称、替换遗器(更改/新建)) + """ + charcacter_names = sorted(self.loadout_data.keys()) # 对配装数据中角色名称排序 + option_0 = None + def interface_3(key_hash: str, equip_index: int) -> Optional[str]: + """ + 说明: + 第[3]层级,替换遗器 + """ + option_3 = None + options_3 = [ + Choice(_("识别当前遗器"), value = 0, + description = INDENT+_("请使游戏保持在[角色]-[遗器]-[遗器替换]界面")+INDENT+_("建议识别前手动点击[对比]提高识别度")), + # 【待扩展】查询遗器数据库、推荐系统 + Choice(_("<返回上一级>"), shortcut_key='z'), + Separator(" "), + ] + # [0]进行选择 + option_3 = questionary.select(_("请选择替换方式:"), options_3, use_shortcuts=True, style=self.msg_style).ask() + if option_3 == _("<返回上一级>"): + return + elif option_3 != 0: + ... # 【待扩展】 + # [1]识别当前遗器 + self.calculated.switch_window() + tmp_data = self.try_ocr_relic() + tmp_hash = get_data_hash(tmp_data) + log.debug("\n"+pp.pformat(tmp_data)) + if tmp_hash in self.relics_data: + tmp_data = self.relics_data[tmp_hash] # 载入可能的速度修正 + self.calculated.switch_cmd() + # [2]有效性检测 + key_data = self.relics_data[key_hash] + if tmp_data["equip_set"] != key_data["equip_set"]: + log.error(_("遗器替换失败:识别到错误部位")) + return None + if tmp_hash == key_hash: + log.error(_("遗器替换失败:识别到相同遗器")) + return None + # [3]打印对比信息 + tmp_text = self.print_relic(tmp_data, tmp_hash, char_weight, False) + key_text = self.print_relic(key_data, key_hash, char_weight, False) + print("\n {:>28} {:<28}".format("<<<<<<< NEW", "OLD >>>>>>>")) + print_styled_text(combine_styled_text(tmp_text, key_text, sep=" "*4, indent=2), style=self.msg_style) + # [4]模糊匹配 + if self.is_fuzzy_match and self.compare_relics(key_data, tmp_data): + log.info(_("模糊匹配成功!识别到新遗器为旧遗器升级后,自动更新数据库")) + # 更新数据库 (录入新遗器数据,并将配装数据中的旧有哈希值替换) + tmp_data["pre_ver_hash"] = key_hash # 建立后继关系 + self.updata_relic_data(key_hash, tmp_hash, equip_index, tmp_data) + return tmp_hash + # [5]是否进行替换 + if questionary.confirm(_("是否进行替换:")).ask(): + # 录入遗器数据 + if tmp_hash in self.relics_data: + log.info(_("遗器数据已存在")) + else: + log.info(_("录入遗器数据")) + self.add_relic_data(tmp_data, tmp_hash) + return tmp_hash + else: + return None + def interface_2(old_relics_hash: List[str]): + """ + 说明: + 第[2]层级,配装内部选项 + """ + option_2 = None + new_relics_hash = old_relics_hash.copy() + while True: + options_2 = [] + # 生成选项 + for equip_index, (equip_set_name, new_hash, old_hash) in enumerate(zip(EQUIP_SET_NAME, new_relics_hash, old_relics_hash)): + msg = StyledText() + msg.append("\n\n") + relic_text = self.print_relic(self.relics_data[new_hash], new_hash, char_weight, False) + relic_text = combine_styled_text(relic_text, indent=2) + msg.extend(relic_text) + tag = _("[已更改]") if new_hash != old_hash else " " + # 使用本配装的队伍 + teams_in_loadout = self.find_teams_in_loadout(character_name, loadout_name) + teams_msg = INDENT.join( + " {}) {} ■ {}".format(i+1, str_just(team_name, 17), ", ".join(list(self.team_data[group_name][team_name]["team_members"].keys()))) + for i, (group_name, team_name) in enumerate(teams_in_loadout) + ) if teams_in_loadout else _(" --空--") + options_2.append(Choice(_("替换{} {}").format(equip_set_name, tag), value=(equip_index, new_hash), description=msg)) + options_2.extend([ + # 【待扩展】删除配装、更改权重、更改面板 + Choice(_("<完成并更新配装 (可进行重命名)>"), shortcut_key='y', description=INDENT+_("使用本配装的队伍:")+INDENT+teams_msg), + Choice(_("<完成并新建配装>"), shortcut_key='x'), + Choice(_("<取消>"), shortcut_key='z'), + Separator(" "), + ]) + # 进行选择 + option_2 = questionary.select(_("请选择要编辑的内容:"), options_2, use_shortcuts=True, style=self.msg_style).ask() + character_data = self.loadout_data[character_name] + # 处理特殊选择 + if option_2 == _("<取消>"): + return + elif option_2 == _("<完成并新建配装>"): + new_loadout_name = questionary.text(_("命名配装名称:"), validate=ConflictValidator(character_data.keys())).ask() + self.loadout_data[character_name][new_loadout_name] = {"relic_hash": new_relics_hash} + rewrite_json_file(LOADOUT_FILE_NAME, self.loadout_data) + return + elif option_2 == _("<完成并更新配装 (可进行重命名)>"): + new_loadout_name = loadout_name + if questionary.confirm(_("是否更改配装名称"), default=False).ask(): + new_loadout_name = questionary.text(_("命名配装名称:"), validate=ConflictValidator(character_data.keys())).ask() + # 判断是否是否修改了遗器数据 + new_loadout_data = {"relic_hash": new_relics_hash} if new_relics_hash != old_relics_hash else None + # 尝试进行配装修改 + ret = self.updata_loadout_data(character_name, loadout_name, new_loadout_name, new_loadout_data) + if ret: + return # 配装修改成功 + else: + continue # 配装修改失败,给予机会重新修改 + # 替换遗器 + equip_index, key_hash = option_2 + new_hash = interface_3(key_hash, equip_index) + if new_hash: + new_relics_hash[equip_index] = new_hash + # 第[0]层级,选择角色 + while True: + options_0 = [ + Choice(str_just(char_name, 15) + _("■ {}").format(len(self.loadout_data[char_name])), value = char_name) + for char_name in charcacter_names + ] + if not options_0: + options_0.append(Choice(_(" --空--"), disabled=_("请先保存角色配装"))) + options_0.append(Choice(_("<返回上一级>"), shortcut_key='z')) + option_0 = questionary.select(_("请选择角色:"), options_0, default=option_0, use_shortcuts=True, style=self.msg_style).ask() + if option_0 == "<返回上一级>": + return + character_name = option_0 + # 查询角色裸装面板 + char_weight_name, char_weight = self.find_char_weight(character_name) + # 第[1]层级,选择配装 + option_1 = None + while True: + option_1 = self.ask_loadout_options(character_name, title=_("请选择要编辑的配装:")) + if option_1 == _("<返回上一级>"): + break + loadout_name, relics_hash = option_1 + interface_2(relics_hash) + + def edit_character_weight(self, tmp=False) -> Optional[StatsWeight]: + """ + 说明: + 编辑角色属性权重 + 参数: + :parma tmp: 是否为创建临时权重 + """ + option_0 = None + def interface_1(char_weight: Dict[str, Union[Dict[str, float], Any]]) -> Optional[Dict[str, Union[Dict[str, float], Any]]]: + """ + 说明: + 第[1]层级,编辑权重 + """ + option_1 = None + weight: Dict[str, float] = char_weight["weight"].copy() + def get_choices(st: Optional[int]=None, ed: Optional[int]=None) -> List[Choice]: + choices = [] + for name in WEIGHT_STATS_NAME[st:ed]: # 按需切片 + # 按需显示已有数值 + value_str = "{value:.2f}".format(value=weight[name]) if name in weight else " " + choices.append(Choice(str_just(name, 15) + f"{value_str:>7}", value=name)) + return choices + while True: + options_1 = get_choices(0,-7) + [Separator()] + get_choices(-7) + [Separator()] + options_1.extend([ + Choice(_("<取消>"), shortcut_key='q'), + Choice(_("<完成>"), shortcut_key='z'), + Separator(" "), + ]) + # 进行选择 + option_1 = questionary.select(_("编辑权重"), options_1, default=option_1, use_shortcuts=True, style=self.msg_style).ask() + if option_1 == _("<取消>"): + return None + elif option_1 == _("<完成>"): + log.debug("\n"+pp.pformat(weight)) + return {"weight": weight} + # 进行编辑 + name = option_1 + value = questionary.text("请输入数值:", validate=FloatValidator(0, 1)).ask() # input float + weight[name] = float(value[:4]) # 更新数据 (截取数据到百分位) + # 第[0]层级 + if tmp: # 创建临时权重 + tmp_weight = interface_1({"weight": {}}) + return StatsWeight(tmp_weight["weight"]) if tmp_weight else None + while True: + # 选择人物 + charcacter_names = sorted(self.loadout_data.keys()) # 对配装数据中角色名称排序 + options_0 = [] + for char_name in charcacter_names: + has_ = char_name in self.char_weight_data + if has_: + __, weight = self.find_char_weight(char_name) + weight_text = combine_styled_text(self.print_stats_weight(weight), prefix="\n", indent=5) + else: + weight_text = None + choice_text = str_just(char_name, 15) + _("■ {}").format("1" if has_ else "0") + options_0.append( + Choice(choice_text, value=char_name, description=weight_text) + ) + if not options_0: + options_0.append(Choice(_(" --空--"), disabled=_("请先保存角色配装"))) + options_0.extend([ + Choice(_("<返回上一级>"), shortcut_key='z'), + Separator(" ") + ]) + option_0 = questionary.select(_("请选择角色:"), options_0, default=option_0, use_shortcuts=True, style=self.msg_style).ask() + if option_0 == _("<返回上一级>"): + return + # 获取记录 + charcacter_name = option_0 + weight_name, charcacter_weight = list(self.char_weight_data.get(charcacter_name, {"None":{"weight": {}}}).items())[0] + ... # 【待扩展】同一角色支持保存多组权重 + # 交互编辑 + charcacter_weight = interface_1(charcacter_weight) + if charcacter_weight is None: # 取消编辑 + continue + # 保存记录 + if weight_name == "None": weight_name = "" + if not weight_name or weight_name and questionary.confirm(_("是否对权重重命名"), default=False).ask(): + weight_name = questionary.text(_("命名权重名称:"), default=weight_name).ask() + self.char_weight_data[charcacter_name] = {weight_name: charcacter_weight} + rewrite_json_file(CHAR_WEIGHT_FILE_NAME, self.char_weight_data) + log.info(_("角色权重编辑成功")) + + def edit_character_panel(self): + """ + 说明: + 编辑角色裸装面板 + """ + option_0 = None + def interface_2(title: str, stats: Dict[str, float], stats_names: List[str]): + """ + 说明: + 第[2]层级,选择编辑的属性,结果通过stats字典引用返回 + """ + option_2 = None + def get_choices(st: Optional[int]=None, ed: Optional[int]=None) -> List[Choice]: + choices = [] + for name in stats_names[st:ed]: # 按需切片,并显示已有数值 + value_str = "{value:.2f}{pre}".format(value=stats[name], pre=" " if name in NOT_PRE_STATS else "%") if name in stats else " " + choices.append(Choice(str_just(name, 15) + f"{value_str:>7}", value=name)) + return choices + while True: + # 生成选择 + if stats_names[0] == BASE_VALUE_NAME[0]: # 白值面板 + options_2 = get_choices() + else: # 属性面板,使属性按组别呈现 + options_2 = get_choices(0,8) + [Separator()] + get_choices(8,15) + [Separator()] + get_choices(15,22) + [Separator()] + get_choices(22,28) + [Separator()] + get_choices(28) + options_2.append(Choice(_("<返回上一级>"), shortcut_key='z')) + # 进行选择 + option_2 = questionary.select(title, options_2, default=option_2, use_shortcuts=True, style=self.msg_style).ask() + if option_2 == _("<返回上一级>"): + return + # 处理选择 + name = option_2 + value = questionary.text("请输入数值:", validate=FloatValidator(0)).ask() # input float + stats[name] = float(value) # 更新数据 + def interface_1(char_panel: Dict[str, Union[Dict[str, float], List[str]]]) -> Optional[Dict[str, Union[Dict[str, float], List[str]]]]: + """ + 说明: + 第[1]层级,选择编辑的类别 + """ + option_1 = None + base_values = char_panel.get("base", {}).copy() # 白值属性 + additonal_stats = char_panel.get("additonal", {"暴击率":5, "暴击伤害":50, "能量恢复效率":100}).copy() # 附加属性 (设置角色默认值) + conditional_stats = char_panel.get("conditional", {}).copy() # 条件属性 + extra_effect_list:list = char_panel.get("extra_effect", []).copy() # 额外效果 + while True: + extra_effect_msg = "".join(INDENT+f"{i+1}.{text}" for i, text in enumerate(extra_effect_list)) + options_1 = [ + Choice(_("白值属性"), value = 0, + description = INDENT+_("游戏内的[白值]只显示到整数位,推荐通过第三方获取精确数值")), + Choice(_("附加属性 (可选)"), value=1, + description = INDENT+_("涵盖[基础面板]、[形迹]、[光锥]、[命座]等提供的固定属性加成")+INDENT+_("注意区分大小词条,不推荐直接无脑录入角色面板内的[绿值]作为小词条")), + Choice(_("条件属性 (可选)"), value=2, + description = INDENT+_("涵盖[形迹]、[光锥]、[命座]等提供的条件属性加成")+INDENT+_("在[角色配装]中打印[面板信息]时可通过开关控制条件效果的开启")+INDENT+_("推荐每一个条件效果对应一条[额外效果说明]记录在下方")), + Choice(_("额外效果说明 (可选)"), value=3, + description = extra_effect_msg if extra_effect_msg else INDENT+_("涵盖条件属性的文本说明、其他额外效果说明,例如:")+INDENT+_(" 光锥5叠:当装备者生命百分比小于50%造成伤害提高50%")+INDENT+_(" 光锥5叠:当装备者消灭敌方目标后,恢复50%能量")), + Choice(_("<取消>"), shortcut_key='q'), + Choice(_("<完成>"), shortcut_key='z', disabled = None if len(base_values)==len(BASE_VALUE_NAME) else _("白值属性缺失")), + Separator(" "), + ] # 注:必须白值属性非空才可选择完成 + option_1 = questionary.select(_("编辑人物裸装面板:"), choices=options_1, default=option_1, use_shortcuts=True, style=self.msg_style).ask() + if option_1 == 0: + interface_2(_("编辑白值属性"), base_values, BASE_VALUE_NAME) + elif option_1 == 1: + interface_2(_("编辑附加属性"), additonal_stats, ALL_STATS_NAME) + elif option_1 == 2: + interface_2(_("编辑条件属性"), conditional_stats, ALL_STATS_NAME) + elif option_1 == 3: + text_lines = questionary.text("添加额外效果 (一行为一条)", multiline=True).ask() + text_lines = filter(lambda x: not re.match(r"\s*$", x), text_lines.split("\n")) # 过滤无效行 + extra_effect_list.extend(text_lines) + elif option_1 == _("<取消>"): + return None + elif option_1 == _("<完成>"): + char_panel = { + "base": base_values, + "additonal": additonal_stats, + "conditional": conditional_stats, + "extra_effect": extra_effect_list, + } + log.debug("\n"+pp.pformat(char_panel)) + return char_panel + # 第[0]层级 + while True: + # 选择人物 + charcacter_names = sorted(self.loadout_data.keys()) # 对配装数据中角色名称排序 + options_0 = [ + Choice(str_just(char_name, 15) + _("■ {}").format("1" if char_name in self.char_panel_data else "0"), value = char_name) + for char_name in charcacter_names + ] + if not options_0: + options_0.append(Choice(_(" --空--"), disabled=_("请先保存角色配装"))) + options_0.append(Choice(_("<返回上一级>"), shortcut_key='z')) + option_0 = questionary.select(_("请选择角色:"), options_0, default=option_0, use_shortcuts=True, style=self.msg_style).ask() + if option_0 == _("<返回上一级>"): + return + # 获取记录 + charcacter_name = option_0 + panel_name, charcacter_panel = list(self.char_panel_data.get(charcacter_name, {"None":{}}).items())[0] + ... # 【待扩展】同一角色支持保存多个面板 + # 交互编辑 + charcacter_panel = interface_1(charcacter_panel) + if charcacter_panel is None: # 取消编辑 + continue + # 保存记录 + if panel_name == "None": panel_name = "" + if not panel_name or panel_name and questionary.confirm(_("是否对面板重命名"), default=False).ask(): + panel_name = questionary.text(_("命名面板名称:"), default=panel_name).ask() + self.char_panel_data[charcacter_name] = {panel_name: charcacter_panel} + rewrite_json_file(CHAR_PANEL_FILE_NAME, self.char_panel_data) + log.info(_("角色面板编辑成功")) + def equip_loadout_for_team(self): """ 说明: @@ -156,18 +650,18 @@ def equip_loadout_for_team(self): """ char_pos_list = [(26,6),(31,6),(37,6),(42,6),...,(75,6)] if IS_PC else [(5,16),(5,27),(5,38),(5,49),...,(5,81)] # 选择队伍 - option = select( + option = questionary.select( _("请选择对当前队伍进行遗器装备的编队:"), - choices = self.get_team_choice_options() + [(_("<返回上一级>"))], - show_description = True, # 需questionary具备对show_description的相关支持 + choices = self.get_team_options() + [Choice(_("<返回上一级>"), shortcut_key='z'), Separator(" ")], + use_shortcuts=True, style=self.msg_style, ).ask() if option == _("<返回上一级>"): return team_members = option # 得到 (char_name: loadout_name) 的键值对 - # 检查人物列表是否移动至开头 + # 检查人物列表是否移动至开头 (取消自动,改为用户手动) self.calculated.switch_window() - self.calculated.relative_swipe(char_pos_list[0], char_pos_list[-1]) # 滑动人物列表 - time.sleep(1) + # self.calculated.relative_swipe(char_pos_list[0], char_pos_list[-1]) # 滑动人物列表 + # time.sleep(1) self.calculated.relative_click((12,40) if IS_PC else (16,48)) # 点击导航栏的遗器,进入[角色]-[遗器]界面 time.sleep(1) # 依次点击人物,进行配装 (编队人物无序) @@ -179,7 +673,7 @@ def equip_loadout_for_team(self): if character_name not in team_members: log.error(_(f"编队错误:角色'{character_name}'不应在当前队伍中")) return - relic_hash = self.loadout_data[character_name][team_members[character_name]] + relic_hash = self.loadout_data[character_name][team_members[character_name]]["relic_hash"] self.equip_loadout(relic_hash) log.info(_("队伍配装完毕")) @@ -189,17 +683,15 @@ def equip_loadout_for_char(self, character_name :Optional[str]=None): 装备当前[角色]界面本人物的遗器配装 """ # 识别当前人物名称 + self.calculated.switch_window() character_name = self.ocr_character_name() if character_name is None else character_name character_data = self.loadout_data[character_name] # 选择配装 if not character_data: # 字典为空 log.info(_("当前人物配装记录为空")) return - option = select( - _("请选择将要进行装备的配装:"), - choices = self.get_loadout_choice_options(character_name) + [(_("<返回上一级>"))], - show_description = True, # 需questionary具备对show_description的相关支持 - ).ask() + self.calculated.switch_cmd() + option = self.ask_loadout_options(character_name, title=_("请选择要装备的配装:")) if option == _("<返回上一级>"): return loadout_name, relic_hash = option @@ -235,15 +727,18 @@ def equip_loadout(self, relics_hash:List[str]): log.debug(tmp_hash) relic_set_index = relic_set_name_dict[tmp_data["relic_set"]] rarity = tmp_data["rarity"] - # 筛选遗器 (加快遗器搜索) - relic_filter.do(relic_set_index, rarity) - # 搜索遗器 - pos = self.search_relic(equip_indx, key_hash=tmp_hash, key_data=tmp_data) + # 在筛选遗器前尝试匹配当前遗器表格中的首个遗器 (加快配装) + pos = self.search_relic(equip_indx, key_hash=tmp_hash, key_data=tmp_data, max_num=1) + if pos is None: + # 筛选遗器 (加快遗器搜索) + relic_filter.do(relic_set_index, rarity) + # 搜索遗器 + pos = self.search_relic(equip_indx, key_hash=tmp_hash, key_data=tmp_data) if pos is None: log.error(_(f"遗器搜索失败: {tmp_hash}")) continue # 点击装备 - self.calculated.relative_click(pos) + # self.calculated.relative_click(pos) # 重复性点击 button = self.calculated.ocr_pos_for_single_line([_("装备"), _("替换"), _("卸下")], points=(80,90,85,94) if IS_PC else (75,90,82,95)) # 需识别[装备,替换,卸下] if button in [0,1]: log.info(_("点击装备")) @@ -270,7 +765,7 @@ def save_loadout_for_team(self): relics_hash_list = [] loadout_name_list = [] # [1]选择队伍的人数 (能否通过页面识别?) - char_num = int(select(_("请选择队伍人数:"), ['4','3','2','1']).ask()) + char_num = int(questionary.select(_("请选择队伍人数:"), ['4','3','2','1']).ask()) # [2]选择是否为互斥队伍组别 group_name = "compatible" # 默认为非互斥队组别 ... # 互斥队组别【待扩展】 @@ -279,21 +774,22 @@ def save_loadout_for_team(self): group_data = self.team_data[group_name] # [3]选择组建方式 options = { - _("全识别"): 0, - _("参考已有的配装数据"): 1 + _("参考已有的配装记录"): 0, + _("全识别"): 1, } - option_ = select(_("请选择组建方式:"), options).ask() + option_ = questionary.select(_("请选择组建方式:"), options).ask() state = options[option_] - # [4]检查人物列表是否移动至开头 + # [4]检查人物列表是否移动至开头 (取消自动,改为用户手动) self.calculated.switch_window() - self.calculated.relative_swipe(char_pos_list[0], char_pos_list[-1]) # 滑动人物列表 - time.sleep(1) + # self.calculated.relative_swipe(char_pos_list[0], char_pos_list[-1]) # 滑动人物列表 + # time.sleep(1) self.calculated.relative_click((12,40) if IS_PC else (16,48)) # 点击导航栏的遗器,进入[角色]-[遗器]界面 time.sleep(1) # [5]依次点击人物,识别配装 char_index = 0 is_retrying = False character_name = None + char_weight = StatsWeight() loadout_dict = self.HashList2dict() while char_index < char_num: char_pos = char_pos_list[char_index] @@ -303,23 +799,30 @@ def save_loadout_for_team(self): self.calculated.relative_click(char_pos) # 点击人物 time.sleep(2) character_name = self.ocr_character_name() # 识别当前人物名称 + __, char_weight = self.find_char_weight(character_name) # [5.2]选择识别当前,还是录入已有 option = None - if state == 1: + if state == 0: self.calculated.switch_cmd() - option = select( - _("请选择配装:"), - choices = self.get_loadout_choice_options(character_name) + [_("<识别当前配装>"), _("<退出>")], - show_description = True, # 需questionary具备对show_description的相关支持 - ).ask() + option = self.ask_loadout_options( + character_name, + add_options=[ + Choice(_("<识别当前配装>"), shortcut_key='y', description=INDENT+_("请使游戏保持在当前[角色]界面")), + Choice(_("<退出>"), shortcut_key='z')] + ) if option == _("<退出>"): # 退出本次编队 return elif option != _("<识别当前配装>"): loadout_name, relics_hash = option # 获取已录入的配装数据 - if state == 0 or option == _("<识别当前配装>"): + else: + self.calculated.switch_window() + self.calculated.relative_click((12,40) if IS_PC else (16,48)) # 再次点击导航栏的遗器,防止用户离开此界面 + time.sleep(1) + if state == 1 or option == _("<识别当前配装>"): self.calculated.switch_window() - relics_hash = self.save_loadout() - print(_("配装信息:\n {}\n{}").format(self.get_loadout_brief(relics_hash), self.get_loadout_detail(relics_hash, 2))) + relics_hash = self.save_loadout(char_weight) + print(_("'{}'配装信息:").format(character_name)) + print_styled_text(self.get_loadout_detail_0(relics_hash, character_name, 2), style=self.msg_style) loadout_name = self.find_loadout_name(character_name, relics_hash) if loadout_name: log.info(_(f"配装记录已存在,配装名称:{loadout_name}")) @@ -339,19 +842,29 @@ def save_loadout_for_team(self): loadout_name_list.append(loadout_name) is_retrying = False char_index += 1 - print(_("队伍配装信息:{}").format("".join("\n " + str_just(char_name, 10) + " " + self.get_loadout_brief(relics_hash) + print(_("队伍配装信息:{}\n").format("".join("\n " + str_just(char_name, 10) + " " + self.get_loadout_brief(relics_hash) for char_name, relics_hash in zip(char_name_list, relics_hash_list)))) # [6]自定义名称 self.calculated.switch_cmd() - team_name = input(_(">>>>命名编队名称 (将同时作为各人物新建配装的名称): ")) - while team_name in group_data or \ - any([team_name in self.loadout_data[character_name] for character_name, loadout_name in zip(char_name_list, loadout_name_list) if loadout_name is None]): - team_name = input(_(">>>>命名冲突,请重命名: ")) + # 统计互斥名称 + names = chain(group_data.keys()) # 队伍互斥名称 + for character_name, loadout_name in zip(char_name_list, loadout_name_list): + if loadout_name is None: # 配装互斥名称 + names = chain(names, self.loadout_data[character_name].keys()) + team_name = questionary.text( + _("命名编队名称:"), + instruction = _("(将同时作为角色的新建配装名称)"), + validate = ConflictValidator(names), + ).ask() + # team_name = input(_(">>>>命名编队名称 (将同时作为各人物新建配装的名称): ")) + # while team_name in group_data or \ + # any([team_name in self.loadout_data[character_name] for character_name, loadout_name in zip(char_name_list, loadout_name_list) if loadout_name is None]): + # team_name = input(_(">>>>命名冲突,请重命名: ")) # [7]录入数据 for i, (char_name, relics_hash, loadout_name) in enumerate(zip(char_name_list, relics_hash_list, loadout_name_list)): if loadout_name is None: loadout_name_list[i] = team_name - self.loadout_data[char_name][team_name] = relics_hash + self.loadout_data[char_name][team_name] = {"relic_hash": relics_hash} rewrite_json_file(LOADOUT_FILE_NAME, self.loadout_data) group_data[team_name] = {"team_members": {key: value for key, value in zip(char_name_list, loadout_name_list)}} rewrite_json_file(TEAM_FILE_NAME, self.team_data) @@ -362,25 +875,27 @@ def save_loadout_for_char(self): 说明: 保存当前[角色]界面本人物的遗器配装 """ + self.calculated.switch_window() character_name = self.ocr_character_name() # 识别当前人物名称 character_data = self.loadout_data[character_name] + __, char_weight = self.find_char_weight(character_name) self.calculated.relative_click((12,40) if IS_PC else (16,48)) # 点击导航栏的遗器,进入[角色]-[遗器]界面 time.sleep(1) - relics_hash = self.save_loadout() + relics_hash = self.save_loadout(char_weight) self.calculated.switch_cmd() - print(_("配装信息:\n {}\n{}").format(self.get_loadout_brief(relics_hash), self.get_loadout_detail(relics_hash, 2))) + print(_("'{}'配装信息:").format(character_name)) + print_styled_text(self.get_loadout_detail_0(relics_hash, character_name, 2), style=self.msg_style) loadout_name = self.find_loadout_name(character_name, relics_hash) if loadout_name: log.info(_(f"配装记录已存在,配装名称:{loadout_name}")) return - loadout_name = input(_(">>>>命名配装名称: ")) # 需作为字典key值,确保唯一性 (但不同的人物可以有同一配装名称) - while loadout_name in character_data: - loadout_name = input(_(">>>>命名冲突,请重命名: ")) - character_data[loadout_name] = relics_hash + # 需作为字典key值,确保唯一性 (但不同的人物可以有同一配装名称) + loadout_name = questionary.text(_("命名配装名称:"), validate=ConflictValidator(character_data.keys())).ask() + character_data[loadout_name] = {"relic_hash": relics_hash} rewrite_json_file(LOADOUT_FILE_NAME, self.loadout_data) log.info(_("配装录入成功")) - def save_loadout(self, max_retries=3) -> list[str]: + def save_loadout(self, char_weight: StatsWeight=StatsWeight(), max_retries=3) -> list[str]: """ 说明: 保存当前[角色]-[遗器]界面内的遗器配装 @@ -399,13 +914,14 @@ def save_loadout(self, max_retries=3) -> list[str]: time.sleep(1) tmp_data = self.try_ocr_relic(equip_indx, max_retries) tmp_hash = get_data_hash(tmp_data, RELIC_DATA_FILTER) - log.debug("\n"+pp.pformat(tmp_data)) - self.print_relic(tmp_data) if tmp_hash in self.relics_data: log.info(_("遗器数据已存在")) + tmp_data = self.relics_data[tmp_hash] # 载入可能的速度修正 else: log.info(_("录入遗器数据")) self.add_relic_data(tmp_data, tmp_hash) + log.debug("\n"+pp.pformat(tmp_data)) + self.print_relic(tmp_data, tmp_hash, char_weight) relics_hash.append(tmp_hash) self.calculated.relative_click((97,6) if IS_PC else (96,5)) # 退出[遗器替换]界面,返回[角色]-[遗器]界面 time.sleep(0.5) @@ -470,27 +986,42 @@ def search_relic_set_for_filter(self, relic_set_index: int): """ 说明: 在当前滑动[角色]-[遗器]-[遗器替换]-[遗器筛选]-[遗器套装筛选]界面内,搜索遗器套装名,并点击。 - 综合OCR识别与方位计算 + 综合OCR识别与方位计算 (游戏1.5版本共28套遗器) 参数: :param equip_set_index: 遗器套装索引 """ is_left = relic_set_index % 2 == 0 # 计算左右栏 - page_num = 0 if relic_set_index < 8 else (1 if relic_set_index < 16 else 2) # 计算页数 (将第2页的末尾两件放至第3页来处理) - last_page = 1 + # 计算页数 (页间有重叠,每页滑动4行) + if relic_set_index < 8: + page_num = 0 + elif relic_set_index < 16: + page_num = 1 + elif relic_set_index < 24: + page_num = 2 + else: + page_num = 3 + last_page = 3 # 滑动翻页 for i in range(page_num): time.sleep(0.2) - self.calculated.relative_swipe((30,60) if IS_PC else (30,62), (30,31) if IS_PC else (30,27)) # 整页翻动 (此界面的动态延迟较大) - if i != last_page: # 非末页,将翻页的动态延迟暂停 (末页会有个短暂反弹动画后自动停止) + self.calculated.relative_swipe((30,60) if IS_PC else (30,62), (30,36) if IS_PC else (30,32)) # 页面翻动4行 (此动作的动态延迟较大) + if page_num != last_page: # 非末页,将翻页的动态延迟暂停 self.calculated.relative_click((35,35) if IS_PC else (35,32), 0.5) # 长按选中 time.sleep(0.5) self.calculated.relative_click((35,35) if IS_PC else (35,32)) # 取消选中 + elif i == last_page-1: + time.sleep(0.5) # 等待末页的短暂反弹动画停止 points = ((28,33,42,63) if is_left else (53,33,67,63)) if IS_PC else ((22,29,41,65) if is_left else (53,29,72,65)) self.calculated.ocr_click(RELIC_SET_NAME[relic_set_index, 1], points=points) - def search_relic(self, equip_indx: int, key_hash: Optional[str]=None, key_data: Optional[Dict[str, Any]]=None, overtime=180, max_retries=3 - ) -> Optional[tuple[int, int]]: + def search_relic( + self, equip_indx: Optional[int]=None, + key_hash: Optional[str]=None, + key_data: Optional[Dict[str, Any]]=None, + stats_weight: StatsWeight=StatsWeight(), + max_num :Optional[int]=None, overtime :Optional[int]=180, max_retries=3 + ) -> Union[Optional[tuple[int, int]], Dict[str, Dict[str, Any]]]: """ 说明: 在当前滑动[角色]-[遗器]-[遗器替换]界面内,搜索匹配的遗器。 @@ -498,59 +1029,96 @@ def search_relic(self, equip_indx: int, key_hash: Optional[str]=None, key_data: key_data非空: 激活模糊匹配 (假设数据保存期间遗器再次升级,匹配成功后自动更新遗器数据); key_hash & key_data均空: 遍历当前页面内的遗器 参数: - :param equip_indx: 遗器部位索引 + :param equip_indx: 遗器部位索引 (激活匹配状态时需非空) :param key_hash: 所记录的遗器哈希值 :param key_data: 所记录的遗器数据 + :param stats_weight: 属性权重 (用于修饰遗器打印) + :param max_num: 搜索的遗器数量上限 :param overtime: 超时 + :param max_retries: 单个遗器OCR重试次数 返回: - :return pos: 坐标 + :return: 匹配状态返回遗器的坐标,非匹配状态返回遗器记录 """ pos_start = (5,24) if IS_PC else (7,28) d_x, d_y, k_x, k_y = (7, 14, 4, 5) if IS_PC else (8, 17, 4, 4) r_x = range(pos_start[0], pos_start[0]+d_x*k_x, d_x) r_y = range(pos_start[1], pos_start[1]+d_y*k_y, d_y) - pre_pos = [""] + relics_data = {"": None} # 记录识别的遗器,初始化为标记值 + matching = not (key_hash is None and key_data is None) + skiped_line = False # 是否执行过跳行功能 + def format_return() -> Optional[Dict[str, Dict[str, Any]]]: + """ + 说明: 格式化返回值 + """ + if matching: # 激活匹配状态 + return None + # 非匹配状态 + relics_data.pop("") # 删除标记值 + return relics_data start_time = time.time() while True: - for index in range(0, k_y*k_x): + index = 0 + while index < k_y*k_x: i = index // k_x # 行 j = index % k_x # 列 x, y = r_x[j], r_y[i] # 坐标查表 self.calculated.relative_click((x, y)) # 点击遗器,同时将翻页的动态延迟暂停 time.sleep(0.2) - log.info(f"({i+1},{j+1},{len(pre_pos)})") # 显示当前所识别遗器的方位与序列号 + log.info(f"({i+1},{j+1},{len(relics_data)})") # 显示当前所识别遗器的方位与序列号 tmp_data = self.try_ocr_relic(equip_indx, max_retries) + if tmp_data is None: # 识别到 "未装备",即此时遗器表格为空 + return format_return() # log.info("\n"+pp.pformat(tmp_data)) tmp_hash = get_data_hash(tmp_data) - if key_hash and key_hash == tmp_hash: # 精确匹配 + # 精确匹配 + if key_hash and key_hash == tmp_hash: return (x, y) - if key_data and self.is_fuzzy_match and self.compare_relics(key_data, tmp_data): # 模糊匹配 - print(_("<<<<旧遗器>>>>")) - self.print_relic(key_data) - print(_("<<<<新遗器>>>>")) - self.print_relic(tmp_data) - log.info(_("模糊匹配成功!自动更新遗器数据")) + # 模糊匹配 + if key_data and self.is_fuzzy_match and self.compare_relics(key_data, tmp_data): + # 打印对比信息 + tmp_text = self.print_relic(tmp_data, tmp_hash, stats_weight, False) + key_text = self.print_relic(key_data, key_hash, stats_weight, False) + print("\n {:>28} {:<28}".format("<<<<<<< NEW", "OLD >>>>>>>")) + print_styled_text(combine_styled_text(tmp_text, key_text, sep=" "*4, indent=2), style=self.msg_style) + log.info(_("模糊匹配成功!识别到新遗器为旧遗器升级后,自动更新数据库")) # 更新数据库 (录入新遗器数据,并将配装数据中的旧有哈希值替换) tmp_data["pre_ver_hash"] = key_hash # 建立后继关系 self.updata_relic_data(key_hash, tmp_hash, equip_indx, tmp_data) return (x, y) # 判断是否遍历完毕 - if pre_pos[-1] == tmp_hash: - log.info(_("遗器数据未发生变化,怀疑点击到空白区域搜索至最后")) - return None # 判断点击到空白,遗器数据未发生变化,结束搜索 - if j == 0 and tmp_hash in pre_pos: # 判断当前行的首列遗器是否已被搜索 + if tmp_hash in relics_data and list(relics_data.keys())[-1] == tmp_hash: + log.info(_("点击到空白区域,判定为搜索至最后")) + return format_return() # 判断点击到空白,遗器数据未发生变化,结束搜索 + if j == 0 and tmp_hash in relics_data: # 判断当前行的首列遗器是否已被搜索 if i == k_y-1: log.info(_("已搜索至最后")) - return None # 判断已滑动至末页,结束搜索 - break # 本行已搜索过,跳出本行 - pre_pos.append(tmp_hash) # 记录 + return format_return() # 判断已滑动至末页,结束搜索 + log.info(_("本行已搜索过,跳过本行")) + index += k_x # 本行已搜索过,跳过本行 + skiped_line = True + continue + if j == k_x-1 and i == k_y-1 and skiped_line: + log.info(_("出现过跳行,判定为搜索至最后")) + return format_return() # 出现过跳行操作,且搜索至表尾,判断为搜索结束 + # 判断是否达到搜索上限 + if max_num and len(relics_data) >= max_num: + if not matching: + self.print_relic(tmp_data, tmp_hash, stats_weight) + relics_data[tmp_hash] = tmp_data # 记录 + return format_return() + # 判断是否超时 + if overtime and time.time() - start_time > overtime: + log.info(_("识别超时")) + return format_return() + # 非匹配状态,打印每一次的识别结果 + if not matching: + self.print_relic(tmp_data, tmp_hash, stats_weight) + # 本次循环结束 + relics_data[tmp_hash] = tmp_data # 记录 + index += 1 # 滑动翻页 (从末尾位置滑动至顶部,即刚好滑动一整页) log.info(_("滑动翻页")) self.calculated.relative_swipe((pos_start[0], pos_start[1]+(k_y-1)*d_y), (pos_start[0], pos_start[1]-d_y)) - if time.time() - start_time > overtime: - log.info(_("识别超时")) - break - return None def compare_relics(self, old_data: Dict[str, Any], new_data: Dict[str, Any]) -> bool: """ @@ -571,33 +1139,127 @@ def compare_relics(self, old_data: Dict[str, Any], new_data: Dict[str, Any]) -> return False # 考虑手动提高速度数据精度的情况 return True + def find_char_weight(self, char_name:str) -> Tuple[Optional[str], StatsWeight]: + """ + 通过角色名查询属性权重 + """ + char_weight = StatsWeight() + char_weight_name = None + if char_name in self.char_weight_data: + char_weights = self.char_weight_data[char_name] + if len(char_weights) > 0: + # 默认载入首个 + char_weight = StatsWeight(list(char_weights.values())[0]["weight"]) + char_weight_name = "{}_{}".format(char_name, list(char_weights.keys())[0]) + ... # 【待扩展】处理多组权重 + return char_weight_name, char_weight + + def find_char_panel(self, char_name:str) -> Tuple[Optional[str], Optional[Dict[str, Any]]]: + """ + 通过角色名查询裸装面板 + """ + char_panel, char_panel_name = None, None + if char_name in self.char_panel_data: + char_panels = self.char_panel_data[char_name] + if len(char_panels) > 0: + # 默认载入首个 + char_panel = list(char_panels.values())[0] + char_panel_name = "{}_{}".format(char_name, list(char_panels.keys())[0]) + ... # 【待扩展】处理多个面板 + return char_panel_name, char_panel + def find_loadout_name(self, char_name: str, relics_hash: List[str]) -> Optional[str]: """ 说明: - 通过配装数据在记录中寻找配装名称 + 通过配装数据查询配装名称 """ - for loadout_name, hash_list in self.loadout_data[char_name].items(): - if hash_list == relics_hash: + for loadout_name, loadout_data in self.loadout_data[char_name].items(): + if loadout_data["relic_hash"] == relics_hash: return loadout_name return None + def find_teams_in_loadout(self, char_name: str, loadout_name: str) -> List[Tuple[str, str]]: + """ + 说明: + 通过角色名与配装名查询所在的队伍 + """ + ret = [] + for group_name, team_group in self.team_data.items(): + for team_name, team_data in team_group.items(): + if (char_name, loadout_name) in team_data["team_members"].items(): + ret.append((group_name, team_name)) + return ret + + def updata_loadout_data(self, char_name: str, old_name: str, new_name: str, new_data: Optional[Dict[str, Any]]=None) -> bool: + """ + 说明: + 更改配装数据,先后修改配装与队伍文件, + 若修改了遗器配装,需检查队伍配装规范性 + """ + # 尝试修改配装文件 + if new_data is None: + self.loadout_data[char_name][new_name] = self.loadout_data[char_name].pop(old_name) + else: + self.loadout_data[char_name].pop(old_name) + self.loadout_data[char_name][new_name] = new_data + # 尝试修改队伍文件 + check = True + teams_in_loadout = self.find_teams_in_loadout(char_name, old_name) + for group_name, team_name in teams_in_loadout: + team_data = self.team_data[group_name][team_name] + team_data["team_members"][char_name] = new_name + if new_data is not None: # 若修改了遗器配装,需检查队伍配装规范性 + check = self.check_team_loadout(team_name, team_data) + if check: # 校验成功,保存修改 + rewrite_json_file(LOADOUT_FILE_NAME, self.loadout_data) + rewrite_json_file(TEAM_FILE_NAME, self.team_data) + log.info(_("配装修改成功")) + return True + else: # 校验失败,任务回滚 + self.loadout_data = read_json_file(LOADOUT_FILE_NAME) + self.team_data = read_json_file(TEAM_FILE_NAME) + log.error(_("配装修改失败")) + return False + def check_team_data(self) -> bool: """ 说明: - 检查队伍配装数据是否满足规范 + 检查队伍配装数据的数据完整性与配装规范性 """ ret = True for group_name, team_group in self.team_data.items(): for team_name, team_data in team_group.items(): - loadout_dict = self.HashList2dict() - [loadout_dict.add(self.loadout_data[char_name][loadout_name], char_name) for char_name, loadout_name in team_data["team_members"].items()] - for equip_index, char_names, element in loadout_dict.find_duplicate_hash(): - log.error(_("队伍遗器冲突:'{}'队伍的{}间的'{}'遗器冲突,遗器哈希值:{}").format(team_name, char_names, EQUIP_SET_NAME[equip_index], element)) - ret = False + ret = self.check_team_loadout(team_name, team_data) if group_name != "compatible": ... # 互斥队伍组别【待扩展】 if ret: log.info(_("队伍配装校验成功")) return ret + + def check_team_loadout(self, team_name: str, team_data: Dict[str, Any]) -> bool: + """ + 说明: + 检查当前队伍配装的数据完整性与配装规范性 + """ + ret = True + loadout_dict = self.HashList2dict() + # 数据完整性 + for char_name, loadout_name in team_data["team_members"].items(): + char_data = self.loadout_data.get(char_name) + if char_data is None: + log.error(_("角色记录缺失:'{}'队伍的'{}'角色记录缺失").format(team_name, char_name)) + ret = False + continue + loadout_data = char_data.get(loadout_name) + if loadout_data is None: + log.error(_("配装记录缺失:'{}'队伍的'{}'角色的'{}'配装记录缺失").format(team_name, char_name, loadout_name)) + ret = False + continue + loadout_dict.add(loadout_data["relic_hash"], char_name) + # 配装规范性 + for equip_index, char_names, element in loadout_dict.find_duplicate_hash(): + log.error(_("队伍遗器冲突:'{}'队伍的{}间的'{}'遗器冲突,遗器哈希值:{}").format(team_name, char_names, EQUIP_SET_NAME[equip_index], element)) + ret = False + return ret class HashList2dict: """ @@ -645,7 +1307,7 @@ def check_relic_data_hash(self, updata=False) -> bool: 检查遗器数据是否发生手动修改 (应对json数据格式变动或手动矫正仪器数值), 若发生修改,可选择更新仪器哈希值,并替换配装数据中相应的数值 """ - equip_set_dict = {key: value for value, key in enumerate(EQUIP_SET_NAME)} + equip_set_dict = Array2dict(EQUIP_SET_NAME) relics_data_copy = self.relics_data.copy() # 字典迭代过程中不允许修改key cnt = 0 for old_hash, data in relics_data_copy.items(): @@ -688,8 +1350,8 @@ def updata_relic_data(self, old_hash: str, new_hash: str, equip_indx: int, new_d # 修改配装文件 for char_name, loadouts in self.loadout_data.items(): for loadout_name, hash_list in loadouts.items(): - if hash_list[equip_indx] == old_hash: - self.loadout_data[char_name][loadout_name][equip_indx] = new_hash + if hash_list["relic_hash"][equip_indx] == old_hash: + self.loadout_data[char_name][loadout_name]["relic_hash"][equip_indx] = new_hash rewrite_json_file(LOADOUT_FILE_NAME, self.loadout_data) # 队伍配装文件无需修改 @@ -715,14 +1377,14 @@ def ocr_character_name(self) -> str: :return character_name: 人物名称 """ str = self.calculated.ocr_pos_for_single_line(points=(10.4,6,18,9) if IS_PC else (13,4,22,9)) # 识别人物名称 (主角名称为玩家自定义,无法适用预选列表) - character_name = re.sub(r"[.’,,。、·'-_——「」/|\[\]\"\\]", '', str) # 删除由于背景光点造成的误判 + character_name = re.sub(r"[.’,,。、·'-_——「」/|\[\]\"\\\s]", '', str) # 删除由于背景光点造成的误判 log.info(_(f"识别人物: {character_name}")) if character_name not in self.loadout_data: self.loadout_data = modify_json_file(LOADOUT_FILE_NAME, character_name, {}) log.info(_("创建新人物")) return character_name - def try_ocr_relic(self, equip_set_index:Optional[int]=None, max_retries=3) -> Dict[str, Any]: + def try_ocr_relic(self, equip_set_index:Optional[int]=None, max_retries=3) -> Optional[Dict[str, Any]]: """ 说明: 在规定次数内尝试OCR遗器数据 @@ -733,17 +1395,21 @@ def try_ocr_relic(self, equip_set_index:Optional[int]=None, max_retries=3) -> Di :return result_data: 遗器数据包 """ retry = 0 + debug = False while True: # 视作偶发错误进行重试 try: - data = self.ocr_relic(equip_set_index) + data = self.ocr_relic(equip_set_index, debug) return data except: if retry >= max_retries: + self.calculated.switch_cmd() + questionary.print(_("请检查是否按要求与建议进行操作。\n若仍然发生此错误,请同时上传相应的日志文件与'logs/image'目录下的相应截图至交流群或gihub"), "orange") raise Exception(_("重试次数达到上限")) retry += 1 + debug = True # 开启对识别区域的截图保存 log.info(_(f"第 {retry} 次尝试重新OCR")) - - def ocr_relic(self, equip_set_index: Optional[int]=None) -> Dict[str, Any]: + + def ocr_relic(self, equip_set_index: Optional[int]=None, debug=False) -> Optional[Dict[str, Any]]: """ 说明: OCR当前静态[角色]-[遗器]-[遗器替换]界面内的遗器数据,单次用时约0.5s。 @@ -758,16 +1424,19 @@ def ocr_relic(self, equip_set_index: Optional[int]=None) -> Dict[str, Any]: img_pc = self.calculated.take_screenshot() # 仅截取一次图片 # [1]部位识别 if equip_set_index is None: - equip_set_index = self.calculated.ocr_pos_for_single_line(EQUIP_SET_NAME, points=(77,19,83,23) if IS_PC else (71,22,78,26), img_pk=img_pc) + equip_set_index = self.calculated.ocr_pos_for_single_line(EQUIP_SET_NAME, points=(77,19,83,23) if IS_PC else (71,22,78,26), img_pk=img_pc, debug=debug) if equip_set_index < 0: raise RelicOCRException(_("遗器套装OCR错误")) equip_set_name = EQUIP_SET_NAME[equip_set_index] # [2]套装识别 name_list = RELIC_SET_NAME[:, 0].tolist() name_list = name_list[:RELIC_INNER_SET_INDEX] if equip_set_index < 4 else name_list[RELIC_INNER_SET_INDEX:] # 取外圈/内圈的切片 - relic_set_index = self.calculated.ocr_pos_for_single_line(name_list, points=(77,15,92,19) if IS_PC else (71,17,88,21), img_pk=img_pc) + name_list += [_("未装备")] + relic_set_index = self.calculated.ocr_pos_for_single_line(name_list, points=(77,15,92,19) if IS_PC else (71,17,88,21), img_pk=img_pc, debug=debug) if relic_set_index < 0: raise RelicOCRException(_("遗器部位OCR错误")) + if relic_set_index == len(name_list)-1: # 识别到"未装备",即遗器为空 + return None if equip_set_index in [4, 5]: relic_set_index += RELIC_INNER_SET_INDEX # 还原内圈遗器的真实索引 relic_set_name = RELIC_SET_NAME[relic_set_index, -1] @@ -785,14 +1454,14 @@ def ocr_relic(self, equip_set_index: Optional[int]=None) -> Dict[str, Any]: else: raise RelicOCRException(_("遗器稀有度识别错误")) # [4]等级识别 - level = self.calculated.ocr_pos_for_single_line(points=(95,19,98,23) if IS_PC else (94,22,98,26), number=True, img_pk=img_pc) + level = self.calculated.ocr_pos_for_single_line(points=(95,19,98,23) if IS_PC else (94,22,98,26), number=True, img_pk=img_pc, debug=debug) level = int(level.split('+')[-1]) # 消除开头可能的'+'号 if level > 15: raise RelicOCRException(_("遗器等级OCR错误")) # [5]主属性识别 name_list = BASE_STATS_NAME_FOR_EQUIP[equip_set_index][:, 0].tolist() - base_stats_index = self.calculated.ocr_pos_for_single_line(name_list, points=(79.5,25,92,29) if IS_PC else (74,29,89,34), img_pk=img_pc) - base_stats_value = self.calculated.ocr_pos_for_single_line(points=(93,25,98,29) if IS_PC else (91,29,98,34), number=True, img_pk=img_pc) + base_stats_index = self.calculated.ocr_pos_for_single_line(name_list, points=(79.5,25,92,29) if IS_PC else (74,29,89,34), img_pk=img_pc, debug=debug) + base_stats_value = self.calculated.ocr_pos_for_single_line(points=(93,25,98,29) if IS_PC else (91,29,98,34), number=True, img_pk=img_pc, debug=debug) if base_stats_index < 0: raise RelicOCRException(_("遗器主词条OCR错误")) if base_stats_value is None: @@ -810,14 +1479,16 @@ def ocr_relic(self, equip_set_index: Optional[int]=None) -> Dict[str, Any]: subs_stats_dict = {} total_level = 0 for name_point, value_point in zip(subs_stats_name_points, subs_stats_value_points): - tmp_index = self.calculated.ocr_pos_for_single_line(name_list, points=name_point, img_pk=img_pc) - if tmp_index is None: break # 所识别data为空,即词条为空,正常退出循环 - if tmp_index < 0: + # 必须先识别属性数值,后识别属性名称 + # (当副词条不足4个时,需判定识别结果为空,而此时属性名称的识别区域会可能与下方的"套装效果"文字重合使结果非空) + tmp_value = self.calculated.ocr_pos_for_single_line(points=value_point, number=True, img_pk=img_pc, debug=debug) + if tmp_value: + tmp_value = str(tmp_value).replace('.', '') # 删除所有真假小数点 + if tmp_value is None or tmp_value == '': + break # 所识别data为空,判断词条为空,正常退出循环 + tmp_index = self.calculated.ocr_pos_for_single_line(name_list, points=name_point, img_pk=img_pc, debug=debug) + if tmp_index is None or tmp_index < 0: raise RelicOCRException(_("遗器副词条OCR错误")) - tmp_value = self.calculated.ocr_pos_for_single_line(points=value_point, number=True, img_pk=img_pc) - if tmp_value is None: - raise RelicOCRException(_("遗器副词条数值OCR错误")) - tmp_value = str(tmp_value).replace('.', '') # 删除所有真假小数点 if '%' in tmp_value: s = tmp_value.split('%')[0] # 修正'48%1'如此的错误识别 tmp_value = s[:-1] + '.' + s[-1:] # 添加未识别的小数点 @@ -849,46 +1520,163 @@ def ocr_relic(self, equip_set_index: Optional[int]=None) -> Dict[str, Any]: log.info(f"用时\033[1;92m『{seconds:.1f}秒』\033[0m") return result_data - def print_relic(self, data: Dict[str, Any]): + def print_relic( + self, data: Dict[str, Any], relic_hash: Optional[str]=None, + char_weight: StatsWeight=StatsWeight(), flag=True + ) -> Optional[StyledText]: """ 说明: - 打印遗器信息, - 可通过is_detail设置打印普通信息与拓展信息 - """ - token = [] - token.append(_("部位: {equip_set}").format(equip_set=data["equip_set"])) - token.append(_("套装: {relic_set}").format(relic_set=data["relic_set"])) - token.append(_("星级: {star}").format(star='★'*data["rarity"])) - token.append(_("等级: +{level}").format(level=data["level"])) - token.append(_("主词条:")) - name, value = list(data["base_stats"].items())[0] - pre = " " if name in NOT_PRE_STATS else "%" - result = self.get_base_stats_detail((name, value), data["rarity"], data["level"]) - if result: - token.append(_(" {name:<4}\t{value:>7.{ndigits}f}{pre}").format(name=name, value=result, pre=pre, ndigits=self.ndigits)) - else: - token.append(_(" {name:<4}\t{value:>5}{pre} [ERROR]").format(name=name, value=value, pre=pre)) - token.append(_("副词条:")) + 打印遗器信息 (占用窗口列数28), + 可通过`detail_for_relic`设置打印普通信息与详细信息, + 参数: + :param data: 遗器数据 + :param relic_hash: 遗器哈希值 (填入即打印) + :param char_weight: 角色属性权重 + :param flag: 是-打印,否-返回文本 + """ + token = StyledText() + rarity = data["rarity"] + token.append("{:<3}".format("+"+str(data["level"])), "bold") + token.append(" {}".format(str_just(data["equip_set"], 7))) + token.append(" {star:>5}".format(star='★'*rarity), f"rarity_{rarity}") + # 副词条 + sub_token = StyledText() subs_stats_dict = Array2dict(SUBS_STATS_NAME) + total_num = 0 # 总词条数或有效词条数 + bad_num = 0 # 强化歪了的次数 + good_num = 0 # 强化中了的次数 for name, value in data["subs_stats"].items(): pre = " " if name in NOT_PRE_STATS else "%" - if not self.is_detail or data["rarity"] not in [4,5]: # 不满足校验条件 - token.append(_(" {name:<4}\t{value:>5}{pre}").format(name=name, value=value, pre=pre)) + if not self.is_detail or rarity not in [4,5]: # 不满足校验条件 + sub_token.append(_(" ■ {name} {value:>6.{nd}f}{pre}\n").format(name=str_just(name, 10), value=value, pre=pre, nd=self.ndigits)) continue stats_index = subs_stats_dict[name] # 增强信息并校验数据 - ret = self.get_subs_stats_detail((name, value), data["rarity"], stats_index) + ret = self.get_subs_stats_detail((name, value), rarity, stats_index) if ret: # 数据校验成功 level, score, result = ret + num = self.get_num_of_stats(ret, rarity) # 计算词条数 + if char_weight and char_weight.get_weight(name) > 0 or not char_weight: + total_num += num + good_num += level-1 + if char_weight and char_weight.get_weight(name) == 0: + bad_num += level-1 tag = '>'*(level-1) # 强化次数的标识 - token.append(_(" {name:<4}\t{tag:<7}{value:>7.{ndigits}f}{pre} [{score}]"). - format(name=name, tag=tag, value=result, score=score, pre=pre, ndigits=self.ndigits)) + sub_token.append( + _(" {num:.1f} {name}{tag:<6}{value:>6.{nd}f}{pre}\n"). \ + format(name=str_just(name, 10), tag=tag, value=result, num=num, pre=pre, nd=self.ndigits), + char_weight.get_color(name)) else: # 数据校验失败 - token.append(_(" {name:<4}\t{value:>5}{pre} [ERROR]").format(name=name, value=value, pre=pre)) - token.append('-'*50) - print("\n".join(token)) + sub_token.append(" ERR", "red") + sub_token.append(_(" {name} {value:>6.{nd}f}{pre}\n").format(name=str_just(name, 10), value=value, pre=pre, nd=self.ndigits)) + # 填充空缺的副词条 (防止遗器联合打印时出现错位) + for __ in range(4-len(data["subs_stats"])): + sub_token.append(_("{:>5}{}\n").format(" ", str_just(_("---空---"), 23)), "grey") + # 遗器得分 + ... # 计算方法【待扩展】 + if rarity in [6]: # 此处为示例 + token.append(_(" 36.9分"), "highlighted") + token.append(_(" SSS\n"), "orange") + else: + token.append(" "*11+"\n") + # 套装 + token.append(str_just("="+data["relic_set"]+"=", 18)) + # 副词条统计 + if not self.is_detail or rarity not in [4,5]: + token.append(" "*10+"\n") + elif char_weight: + if good_num == 0: # 未有副词条的强化次数 + token.append(_(" 有效 "), "green") + elif bad_num == 0: + token.append(_(" 全中 "), "green") + else: + token.append(_(" 歪{}次 ").format(bad_num), "green") + token.append("{:>3.1f}\n".format(total_num), "green") + else: + token.append(" 总计 ", "green") + token.append("{:>3.1f}\n".format(total_num), "green") + # 主词条 + name, value = list(data["base_stats"].items())[0] + pre = " " if name in NOT_PRE_STATS else "%" + result = self.get_base_stats_detail((name, value), rarity, data["level"]) + if result: + token.append(_("■ {name}{value:>6.{nd}f}{pre}\n").format(name=str_just(name, 19), value=result, pre=pre, nd=self.ndigits)) + else: + token.append(_("■ {name} ").format(name=str_just(name, 13))) + token.append("ERR", "red") + token.append(_(" {value:>6.{nd}f}{pre}\n").format(value=value, pre=pre, nd=self.ndigits)) + token.extend(sub_token) + if relic_hash: + token.append("{:>28}\n".format("hash:"+relic_hash[:10]), "grey") + if flag: + print_styled_text(token, style=self.msg_style) + else: + return token - def get_team_choice_options(self) -> List[Choice]: + def print_stats_weight(self, stats_weight: Union[StatsWeight, Dict[str, float]]) -> Optional[StyledText]: + """ + 说明: + 打印属性权重信息 + """ + if isinstance(stats_weight, dict): + stats_weight = StatsWeight(stats_weight) + token = StyledText() + has_ = False # 标记有无属性伤害 + for index, name in enumerate(WEIGHT_STATS_NAME): + value = stats_weight.get_weight(name, True) + value_str = "{:.2f}".format(value) + if index >= 11: # 过滤属性伤害 + if index == 17 and not has_ and value == 0 : # 无属性伤害的情形 + token.append(str_just(_("属性伤害"), 15) + f"{value_str:>7}\n", "weight_0") + continue + elif value == 0: + continue + else: has_ = True + token.append(str_just(name, 15) + f"{value_str:>7}\n", stats_weight.get_color(name, True)) + return token + + def ask_loadout_options( + self, character_name: str, + add_options: Optional[List[Choice]] = [Choice(_("<返回上一级>"), shortcut_key='z')], + title: str = _("请选择配装:"), + ) -> Union[Tuple[str, List[str]], str]: + """ + 说明: + 询问并获得该角色配装的选择 + 参数: + :param character_name: 人物名称 + :param add_options: 附加选项 (注意已占用快捷键'x','v',需要至少有一个退出选项) + :param title: 标题 + 返回: + :return option: (配装名称,遗器哈希值列表)元组 或 附加选项名称 + """ + options = self.get_loadout_options(character_name) + if options: + if self.loadout_detail_type == 0: + options.append(Choice(_("<<切换为遗器详情>>"), shortcut_key='v')) + options.append( + Choice( + _("<<关闭条件效果>>") if self.activate_conditional else _("<<开启条件效果>>"), shortcut_key='x', + description = INDENT+_("涵盖[遗器套装效果]与自定义的[角色裸装面板]中的条件效果")+INDENT+_("开启时,默认激活全部条件效果的最大效果") + ) + ) # 【待扩展】自动激活达到可计算触发条件的条件效果 + else: + options.append(Choice(_("<<切换为面板详情>>"), shortcut_key='v')) + else: + options.append(Separator(_("--配装记录为空--"))) + if add_options: + options.extend(add_options) + options.append(Separator(" ")) + option = questionary.select(title, choices=options, use_shortcuts=True, style=self.msg_style).ask() + if option in [_("<<切换为遗器详情>>"), _("<<切换为面板详情>>")]: + self.loadout_detail_type = (self.loadout_detail_type + 1) & 1 + return self.ask_loadout_options(character_name, add_options) + if option in [_("<<关闭条件效果>>"), _("<<开启条件效果>>")]: + self.activate_conditional = not self.activate_conditional + return self.ask_loadout_options(character_name, add_options) + return option + + def get_team_options(self) -> List[Choice]: """ 说明: 获取所有队伍配装记录的选项表 @@ -899,18 +1687,19 @@ def get_team_choice_options(self) -> List[Choice]: :return description: 队员配装的简要信息 """ group_data = self.team_data["compatible"] # 获取非互斥队组别信息 + group_data = sorted(group_data.items()) # 按键名即队伍名称排序 ... # 获取互斥队伍组别信息【待扩展】 prefix = "\n" + " " * 5 choice_options = [Choice( title = str_just(team_name, 12), value = team_data["team_members"], description = "".join( - prefix + str_just(char_name, 10) + " " + self.get_loadout_brief(self.loadout_data[char_name][loadout_name]) + prefix + str_just(char_name, 10) + " " + self.get_loadout_brief(self.loadout_data[char_name][loadout_name]["relic_hash"]) for char_name, loadout_name in team_data["team_members"].items()) - ) for team_name, team_data in group_data.items()] + ) for team_name, team_data in group_data] return choice_options - def get_loadout_choice_options(self, character_name:str) -> List[Choice]: + def get_loadout_options(self, character_name: str) -> List[Choice]: """ 说明: 获取该人物配装记录的选项表 @@ -923,68 +1712,273 @@ def get_loadout_choice_options(self, character_name:str) -> List[Choice]: :return description: 配装各属性数值统计 """ character_data = self.loadout_data[character_name] + character_data = sorted(character_data.items()) # 按键名即配装名排序 choice_options = [Choice( - title = str_just(loadout_name, 14) + " " + self.get_loadout_brief(relics_hash), - value = (loadout_name, relics_hash), - description = '\n' + self.get_loadout_detail(relics_hash, 5, True) - ) for loadout_name, relics_hash in character_data.items()] + title = str_just(loadout_name, 16) + " " + self.get_loadout_brief(loadout_data["relic_hash"]), + value = (loadout_name, loadout_data["relic_hash"]), + description = self.get_loadout_detail(loadout_data["relic_hash"], character_name, 5) + ) for loadout_name, loadout_data in character_data] return choice_options - - def get_loadout_detail(self, relics_hash: List[str], tab_num: int=0, flag=False) -> str: + + def get_loadout_detail(self, relics_hash: List[str], character_name: Optional[str]=None, indent_num: int=0) -> StyledText: + """ + 说明: + 获取配装的详细信息 (0-面板详情,1-遗器详情) + 参数: + :param relics_hash: 遗器哈希值列表 + :param indent_num: 缩进长度 + :param character_name: 人物名称 (非空时激活人物裸装面板统计) + """ + if self.loadout_detail_type == 0: + return self.get_loadout_detail_0(relics_hash, character_name, indent_num) + if self.loadout_detail_type == 1: + return self.get_loadout_detail_1(relics_hash, character_name, indent_num-3) + + def get_loadout_detail_1(self, relics_hash: List[str], character_name: Optional[str]=None, indent_num: int=0) -> StyledText: + """ + 说明: + 获取配装的遗器详情 + 参数: + :param relics_hash: 遗器哈希值列表 + :param character_name: 人物名称 (非空时激活人物属性权重) + :param indent_num: 缩进长度 + """ + msg = StyledText() + text_list = [] + # 查询角色裸装面板 + char_weight_name, char_weight = self.find_char_weight(character_name) + # 处理遗器数据 + # for equip_index in [0,2,4,1,3,5]: # 优化部位显示顺序 + for equip_index in range(len(relics_hash)): # 主流显示顺序 + tmp_hash = relics_hash[equip_index] + tmp_data = self.relics_data[tmp_hash] + text_list.append(self.print_relic(tmp_data, tmp_hash, char_weight, False)) + msg.append(_("词条挡位权值{}\n").format(SUBS_STATS_TIER_WEIGHT[self.subs_stats_iter_weight][-1])) + msg.append("\n") + msg.extend(combine_styled_text(*text_list[:3], sep=" ", indent=indent_num)) + msg.append("\n") + msg.extend(combine_styled_text(*text_list[3:], sep=" ", indent=indent_num)) + indent = "\n" + " "*indent_num + msg.append(indent) + msg.append(_("属性权重'{}'").format(char_weight_name) if char_weight else _("未启用权重")) + msg.append(_(" 评分系统开发中...\n"), "grey") + return msg + + def get_loadout_detail_0(self, relics_hash: List[str], character_name: Optional[str]=None, indent_num: int=0) -> StyledText: """ 说明: - 获取配装的详细信息 (各属性数值统计) + 获取配装的面板详情 + 参数: + :param relics_hash: 遗器哈希值列表 + :param character_name: 人物名称 (非空时激活人物裸装面板与属性权重) + :param indent_num: 缩进长度 """ - stats_total_value = [0 for _ in range(len(STATS_NAME))] - stats_name_dict = Array2dict(STATS_NAME) + stats_panel_S = np.zeros(len(ALL_STATS_NAME)) # 固定属性面板 + stats_panel_C = np.zeros(len(ALL_STATS_NAME)) # 条件属性面板 + extra_effect_list = [] # 额外效果说明 + stats_name_dict = Array2dict(ALL_STATS_NAME) base_stats_dict = Array2dict(BASE_STATS_NAME) subs_stats_dict = Array2dict(SUBS_STATS_NAME) - for equip_indx in range(len((relics_hash))): - tmp_data = self.relics_data[relics_hash[equip_indx]] + # [0.1]查询角色裸装面板 + char_panel_name, char_panel = self.find_char_panel(character_name) + # [0.2]查询角色属性权重 + char_weight_name, char_weight = self.find_char_weight(character_name) + # [1]统计遗器主副属性 + relic_subs_nums = [.0]*6 # 各部位遗器的副词条词条总数 + for equip_index, relic_hash in enumerate(relics_hash): + tmp_data: Dict[str, Dict[str, float]] = self.relics_data[relic_hash] rarity = tmp_data["rarity"] level = tmp_data["level"] stats_list = [(key, self.get_base_stats_detail((key, value), rarity, level, base_stats_dict[key])) for key, value in tmp_data["base_stats"].items()] # 获取数值精度提高后的主词条 - stats_list.extend([(key, self.get_subs_stats_detail((key, value), rarity, subs_stats_dict[key])[-1]) - for key, value in tmp_data["subs_stats"].items()]) # 获取数值精度提高后的副词条 + for key, value in tmp_data["subs_stats"].items(): + ret = self.get_subs_stats_detail((key, value), rarity, subs_stats_dict[key]) + stats_list.append((key, ret[-1])) # 获取数值精度提高后的副词条 + num = self.get_num_of_stats(ret, rarity) # 计算词条数 + if char_weight and char_weight.get_weight(key) > 0 or not char_weight: + relic_subs_nums[equip_index] += num # 当有权重时统计有效词条,无权重时统计总词条 for key, value in stats_list: - stats_total_value[stats_name_dict[key]] += value # 数值统计 - token_list = [] + stats_panel_S[stats_name_dict[key]] += value # 数值统计 + # [2]统计遗器套装效果 + set_cnt: Counter = self.get_loadout_brief(relics_hash, False) + def parse_set_effect(set_effect_list: List[StatsEffect]): + """ + 说明: 解析遗器套装效果,进行数值统计 + """ + for effect in set_effect_list: + if isinstance(effect, str): + extra_effect_list.append(effect) + elif isinstance(effect, tuple): + key, value, unconditional = effect + if unconditional: # 非条件效果 + stats_panel_S[stats_name_dict[key]] += value + elif not unconditional and self.activate_conditional: # 已开启的条件效果 + stats_panel_C[stats_name_dict[key]] += value + for set_idx, cnt in set_cnt.items(): + if cnt >= 2: # 激活二件套效果 + parse_set_effect(SET_EFFECT_OF_TWO_PC[set_idx]) + if cnt >= 4: # 激活四件套效果 + parse_set_effect(SET_EFFECT_OF_FOUR_PC[set_idx]) + # [3]统计人物裸装面板 + def parse_char_stats_panel() -> List[Tuple[float, float]]: + """ + 说明:解析人物裸装面板,引用传递返回参数stats_panel + """ + # 统计附加属性 + additonal_stats = char_panel.get("additonal", {}) + for key, value in additonal_stats.items(): + stats_panel_S[stats_name_dict[key]] += value + # 统计条件属性 + conditional_stats = char_panel.get("conditional", {}) + if self.activate_conditional: + for key, value in conditional_stats.items(): + stats_panel_C[stats_name_dict[key]] += value + # 统计额外效果 + extra_effect_list.extend(char_panel.get("extra_effect", [])) + # 通过白值计算绿值 + b_value: Dict[str, float] = char_panel["base"] + bs_value = [b_value[_("生命值白值")], b_value[_("攻击力白值")], b_value[_("防御力白值")], b_value[_("速度白值")]] + en_value = [] + for i, j, base in zip([3,4,5,7], [0,1,2,6], bs_value): # 大小词条的索引 + large = stats_panel_S[i] + stats_panel_C[i] + small = stats_panel_S[j] + stats_panel_C[j] + en_value.append(base * large / 100 + small) + return list(zip(bs_value, en_value)) + if char_panel: + bs_and_en_value = parse_char_stats_panel() + else: + bs_and_en_value = [(0, 0)] * len(BASE_VALUE_NAME) + # [4]合成属性分词 + token_list: List[StyledText] = [] + total_stats_num = .0 has_ = False # 标记有无属性伤害 - for index, value in enumerate(stats_total_value): - name = STATS_NAME[index, -1] - if index in range(11, 18): - if index == 17 and not has_ and value == 0 : # 无属性伤害的情形 + normal_stats_len = len(STATS_NAME) # 额外属性的起始索引 (预防被激活多个属性伤害的情形) + for index, (std, cnd) in enumerate(zip(stats_panel_S, stats_panel_C)): + name = ALL_STATS_NAME[index] + value = std + cnd + color = char_weight.get_color(name) if index < len(STATS_NAME) else "" + if index in range(15, 22): # 过滤属性伤害 + if index == 21 and not has_ and value == 0 : # 无属性伤害的情形 name = _("属性伤害") - elif value == 0: continue + elif value == 0: + normal_stats_len -= 1 + continue else: has_ = True + elif index >= len(STATS_NAME) and value == 0: + continue # 跳过无效的额外属性 + token = StyledText() pre = " " if name in NOT_PRE_STATS else "%" - token_list.append(_("{name}{value:>7.{ndigits}f}{pre}").format(name=str_just(name, 15), value=value, pre=pre, ndigits=self.ndigits)) - msg = "" - column = 2 # 栏数 (可调节) - tab = " " * tab_num - for index in range(len(token_list)): # 分栏输出 (纵向顺序,横向逆序,保证余数项在左栏) - i = index // column - j = index % column - n = (column-j-1) * len(token_list) // column + i - msg += token_list[n] if j != 0 else tab+token_list[n] - msg += "\n" if j == column-1 else " " - if msg[-1] != "\n": msg += "\n" - if flag: msg += "\n" + tab + _("(未计算遗器套装的属性加成)") # 【待扩展】 + # 词条数量 + subs_idx = subs_stats_dict[name] + if subs_idx is not None or name == _("速度%") and value != 0 and char_panel: + tmp_name, tmp_value = name, value # 修饰 + if name == _("速度%"): # 将速度大词条转化为小词条来计算 + tmp_name = _("速度") + tmp_value = bs_and_en_value[3][0] * value / 100 + elif char_panel and name == _("暴击率"): # 减去面板默认提供的 + tmp_value = value - 5 + elif char_panel and name == _("暴击伤害"): # 减去面板默认提供的 + tmp_value = value - 50 + num = self.get_num_of_stats(self.get_subs_stats_detail((tmp_name, tmp_value), 5, subs_idx, check=False)) + if char_weight and char_weight.get_weight(name) > 0 or not char_weight: + total_stats_num += num + token.append("{:>4.1f} ".format(num), color) + else: + token.append(" " * 5) + # 名称数值 + token.append( + "{name}{value:>7.{ndigits}f}{pre}".format(name=str_just(name, 13), value=value, pre=pre, ndigits=self.ndigits), color + ) + # 条件效果 + if self.activate_conditional and cnd != 0: + # token.append("{std:>4.{ndigits}f}".format(std=std, ndigits=self.ndigits), "") + token.append("{cnd:>7.{ndigits}f}".format(cnd=cnd, ndigits=self.ndigits), "green") + elif self.activate_conditional: + token.append(" " * 7) + token_list.append(token) + # # 更改属性的打印顺序 + # token_list = token_list[0:1]+token_list[3:4]+token_list[1:2]+token_list[4:5]+token_list[2:3]+token_list[5:6]+token_list[6:] + # [5]打印信息 + msg = StyledText() + indent = " " * indent_num + indent_ = "\n" + indent + msg.append(_("词条挡位权值{}\n").format(SUBS_STATS_TIER_WEIGHT[self.subs_stats_iter_weight][-1])) + def format_table(sequence: List[StyledText], column=2, reverse=True) -> StyledText: + """ + 说明: 打印单个表单 + """ + msg = StyledText() + sep = " " + for index in range(len(sequence)): # 分栏输出 (纵向顺序,横向逆序,保证余数项在左栏) + i = index // column + j = index % column + n = ((column-j-1) * len(sequence) // column + i) if reverse else (j * len(sequence) // column + i) + msg.append(sep if j != 0 else indent_) + msg.extend(sequence[n]) + if msg: msg.append("\n") + return msg + # [5.1]打印遗器简要信息 + relic_list: List[StyledText] = [] + # for equip_index in range(len(relics_hash)): # 优化部位显示顺序 + for equip_index in [0,3,1,4,2,5]: # 主流显示顺序 + token = StyledText() + relic_hash = relics_hash[equip_index] + num = relic_subs_nums[equip_index] # 词条数 + tmp_data = self.relics_data[relic_hash] + rarity = tmp_data["rarity"] + token.append("{:.1f}".format(num), "green") + token.append("{}".format(EQUIP_SET_ADDR[equip_index])) + token.append(":{}".format(relic_hash[:10])) + relic_list.append(token) + msg.extend(format_table(relic_list, 3, False)) + # [5.2]打印白值绿值 + msg.append("\n") + for idx, (bs, en) in enumerate(bs_and_en_value): + name = BASE_VALUE_NAME[idx] + msg.append(indent) + msg.append("{name}".format(name=str_just(name[:int(_("-2"))], 8)), char_weight.get_color(name)) # 截除某尾的‘白值’字样 + msg.append("{total:>9.{ndigits}f}".format(total=bs+en, ndigits=self.ndigits), char_weight.get_color(name)) + msg.append("{bs:>9.{ndigits}f}".format(bs=bs, ndigits=self.ndigits), "") + msg.append("{en:>9.{ndigits}f}".format(en=en, ndigits=self.ndigits), "green") + msg.append(" | ") # 分隔符 + # [5.3]利用右侧空间打印其他信息 + if idx == 0: + msg.append(_("遗器副词条")) + msg.append("{:.1f}".format(sum(relic_subs_nums)), "green") + msg.append(_(" 总计词条")) + msg.append("{:.1f}".format(total_stats_num), "green") + elif idx == 1: + msg.append(_("裸装面板'{}'").format(char_panel_name) if char_panel else _("未启用裸装面板")) + elif idx == 2: + msg.append(_("属性权重'{}'").format(char_weight_name) if char_weight else _("未启用权重")) + elif idx == 3: + msg.append(_("评分系统开发中..."), "grey") + msg.append("\n") + # [5.4]打印属性数值统计 + msg.extend(format_table(token_list[:normal_stats_len])) # 遗器主副属性 + msg.extend(format_table(token_list[normal_stats_len:])) # 额外属性 + # [5.5]打印额外效果 + if extra_effect_list: + msg.append("\n" + " " * (indent_num-3) + _("额外效果:")) + msg.append("".join(indent_ + f"{i+1}) {text}" for i, text in enumerate(extra_effect_list))) + msg.append("\n") return msg - def get_loadout_brief(self, relics_hash: List[str]) -> str: + def get_loadout_brief(self, relics_hash: List[str], flag = True) -> Union[str, Counter]: """ 说明: 获取配装的简要信息 (包含内外圈套装信息与主词条信息) + 参数: + :param relics_hash: 遗器哈希值列表 + :param flag: 如果`True`返回消息,如果`False`返回遗器套装计数器 """ - set_abbr_dict = Array2dict(RELIC_SET_NAME, -1, 2) + set_name_dict = Array2dict(RELIC_SET_NAME) stats_abbr_dict = Array2dict(BASE_STATS_NAME, -1, 1) outer_set_list, inner_set_list, base_stats_list = [], [], [] # 获取遗器数据 for equip_indx in range(len((relics_hash))): tmp_data = self.relics_data[relics_hash[equip_indx]] - tmp_set = set_abbr_dict[tmp_data["relic_set"]] + tmp_set = set_name_dict[tmp_data["relic_set"]] tmp_base_stats = stats_abbr_dict[list(tmp_data["base_stats"].keys())[0]] base_stats_list.append(tmp_base_stats) if equip_indx < 4: @@ -993,12 +1987,13 @@ def get_loadout_brief(self, relics_hash: List[str]) -> str: inner_set_list.append(tmp_set) # 内圈 outer_set_cnt = Counter(outer_set_list) inner_set_cnt = Counter(inner_set_list) + if not flag: + return outer_set_cnt + inner_set_cnt # 生成信息 - outer = _("外:") + '+'.join([str(cnt) + name for name, cnt in outer_set_cnt.items()]) + " " - inner = _("内:") + '+'.join([str(cnt) + name for name, cnt in inner_set_cnt.items()]) + " " - # stats = " ".join([EQUIP_SET_ADDR[idx]+":"+name for idx, name in enumerate(base_stats_list) if idx > 1]) + outer = _("外:") + '+'.join([str(cnt) + RELIC_SET_NAME[set_idx, 2] for set_idx, cnt in outer_set_cnt.items()]) + inner = _("内:") + '+'.join([str(cnt) + RELIC_SET_NAME[set_idx, 2] for set_idx, cnt in inner_set_cnt.items()]) stats = ".".join([name for idx, name in enumerate(base_stats_list) if idx > 1]) # 排除头部与手部 - msg = str_just(stats, 17) + " " + str_just(inner, 10) + " " + outer # 将长度最不定的外圈信息放至最后 + msg = str_just(stats, 17) + " " + str_just(inner, 10) + " " + outer # 将长度最不定的外圈信息放至最后 return msg def get_base_stats_detail(self, data: Tuple[str, float], rarity: int, level: int, stats_index: Optional[int]=None) -> Optional[float]: @@ -1032,7 +2027,7 @@ def get_base_stats_detail(self, data: Tuple[str, float], rarity: int, level: int return None return result - def get_subs_stats_detail(self, data: Tuple[str, float], rarity: int, stats_index: Optional[int]=None) -> Optional[Tuple[int, int, float]]: + def get_subs_stats_detail(self, data: Tuple[str, float], rarity: int, stats_index: Optional[int]=None, check=True) -> Optional[Tuple[int, int, float]]: """ 说明: 计算副词条的详细信息 (如强化次数、档位积分,以及提高原数据的小数精度) @@ -1043,6 +2038,7 @@ def get_subs_stats_detail(self, data: Tuple[str, float], rarity: int, stats_inde :param data: 遗器副词条键值对 :param stats_index: 遗器副词条索引 :param rarity: 遗器稀有度 + :param check: 开启校验 返回: :return level: 强化次数: 0次强化记为1,最高5次强化为6 :return score: 档位总积分: 1档记0分, 2档记1分, 3档记2分 @@ -1065,6 +2061,10 @@ def get_subs_stats_detail(self, data: Tuple[str, float], rarity: int, stats_inde level -= 1 score = math.ceil((value - a_*level) / d - 1.e-6) result = round(a*level + d*score, 4) # 四舍五入 (考虑浮点数运算的数值损失) + # 不启用校验,用于统计词条使用 + if not check: + log.debug(f"({name}, {value}): [{a}, {d}], l={level}, s={score}, r={result}") + return (level, score, result) # 校验数据 check = result - value if check < 0 or \ @@ -1075,4 +2075,18 @@ def get_subs_stats_detail(self, data: Tuple[str, float], rarity: int, stats_inde log.error(_(f"校验失败,原数据或计算方法有误: {data}")) log.debug(f"[{a}, {d}], l={level}, s={score}, r={result}") return None - return (level, score, result) \ No newline at end of file + return (level, score, result) + + def get_num_of_stats(self, stats_detail: Tuple[int, int, Any], rarity: int=5) -> Optional[float]: + """ + 说明: + 计算词条数量 + """ + if rarity not in [4,5]: + return None + level, score, __ = stats_detail + level_w, score_w, __ = SUBS_STATS_TIER_WEIGHT[self.subs_stats_iter_weight] + num = level*level_w + score*score_w + if rarity == 4: # 四星与五星遗器比值为 0.8 + num *= 0.8 + return num diff --git a/utils/relic_constants.py b/utils/relic_constants.py index 6dab2993..2dcff2cb 100644 --- a/utils/relic_constants.py +++ b/utils/relic_constants.py @@ -2,8 +2,11 @@ 遗器模块相关静态数据 """ import numpy as np +from typing import Any, Dict, List, Literal, Optional, Tuple, Union from .config import _ +StatsEffect = Union[str, Tuple[str, Optional[int], bool]] + EQUIP_SET_NAME = [_("头部"), _("手部"), _("躯干"), _("脚部"), _("位面球"), _("连结绳")] """遗器部位名称,已经按游戏界面顺序排序""" @@ -11,52 +14,59 @@ EQUIP_SET_ADDR = [_("头"), _("手"), _("衣"), _("鞋"), _("球"), _("绳")] """遗器部位简称""" -# 注:因为数据有时要行取有时要列取,故采用数组存储 -RELIC_SET_NAME = np.array([ +RELIC_SET_NAME = np.array([ # 注:因为数据有时要行取有时要列取,故采用数组存储 # 外圈 [_("过客"), _("过客"), _("治疗"), _("云无留迹的过客")], [_("枪手"), _("枪手"), _("快枪手"), _("野穗伴行的快枪手")], [_("圣骑"), _("圣骑"), _("圣骑"), _("净庭教宗的圣骑士")], [_("雪猎"), _("猎人"), _("冰套"), _("密林卧雪的猎人")], - [_("拳王"), _("拳王"), _("物理"), _("街头出身的拳王")], + [_("拳王"), _("拳王"), _("物理套"), _("街头出身的拳王")], [_("铁卫"), _("铁卫"), _("铁卫"), _("戍卫风雪的铁卫")], [_("火匠"), _("火匠"), _("火套"), _("熔岩锻铸的火匠")], - [_("天才"), _("天才"), _("量子"), _("繁星璀璨的天才")], + [_("天才"), _("天才"), _("量子套"), _("繁星璀璨的天才")], [_("乐队"), _("雷电"), _("雷套"), _("激奏雷电的乐队")], [_("翔"), _("翔"), _("风套"), _("晨昏交界的翔鹰")], [_("怪盗"), _("怪盗"), _("怪盗"), _("流星追迹的怪盗")], - [_("废"), _("废"), _("虚数"), _("盗匪荒漠的废土客")], + [_("废"), _("废"), _("虚数套"), _("盗匪荒漠的废土客")], [_("者"), _("长存"), _("莳者"), _("宝命长存的莳者")], [_("信使"), _("信使"), _("信使"), _("骇域漫游的信使")], + [_("大公"), _("大公"), _("追击套"), _("毁烬焚骨的大公")], + [_("系囚"), _("系囚"), _("DOT套"), _("幽锁深牢的系囚")], # 内圈 [_("黑塔"), _("太空"), _("空间站"), _("太空封印站")], [_("仙"), _("仙"), _("仙舟"), _("不老者的仙舟")], [_("公司"), _("公司"), _("命中"), _("泛银河商业公司")], [_("贝洛"), _("贝洛"), _("防御"), _("筑城者的贝洛伯格")], # 注:有散件名为'贝洛伯格的铁卫防线' - [_("螺丝"), _("差分"), _("差分"), _("星体差分机")], + [_("丝星"), _("差分"), _("差分"), _("星体差分机")], [_("萨尔"), _("停转"), _("停转"), _("停转的萨尔索图")], [_("利亚"), _("盗贼"), _("击破"), _("盗贼公国塔利亚")], [_("瓦克"), _("瓦克"), _("翁瓦克"), _("生命的翁瓦克")], [_("泰科"), _("繁星"), _("繁星"), _("繁星竞技场")], - [_("伊须"), _("龙骨"), _("龙骨"), _("折断的龙骨")] + [_("伊须"), _("龙骨"), _("龙骨"), _("折断的龙骨")], + [_("格拉"), _("格拉"), _("苍穹"), _("苍穹战线格拉默")], + [_("匹诺"), _("匹诺"), _("匹诺"), _("梦想之地匹诺康尼")], ], dtype=np.str_) """遗器套装名称:0-套装散件名的共有词(ocr-必须),1-套装名的特异词(ocr-可选,为了增强鲁棒性),2-玩家惯用简称(print),3-套装全称(json),已按[1.4游戏]遗器筛选界面排序 (且前段为外圈,后段为内圈)""" -RELIC_INNER_SET_INDEX = 14 +RELIC_INNER_SET_INDEX = 16 """RELIC_SET_NAME参数的遗器内圈的起始点索引""" STATS_NAME = np.array([ [_("命值"), _("生"), _("生命值")], [_("击力"), _("攻"), _("攻击力")], - [_("防御"), _("防"), _("防御力")], + [_("防"), _("防"), _("防御力")], [_("命值"), _("生"), _("生命值%")], [_("击力"), _("攻"), _("攻击力%")], - [_("防御"), _("防"), _("防御力%")], + [_("防"), _("防"), _("防御力%")], [_("度"), _("速"), _("速度")], + [_("度"), _("速"), _("速度%")], # 注:非遗器主副属性 (不用于OCR,因为属性相关性放置在此) [_("击率"), _("暴击"), _("暴击率")], [_("击伤"), _("爆伤"), _("暴击伤害")], - [_("命中"), _("命中"), _("效果命中")], [_("治疗"), _("治疗"), _("治疗量加成")], + [_("命中"), _("命中"), _("效果命中")], + [_("抵抗"), _("效果抵抗"), _("效果抵抗")], + [_("破"), _("击破"), _("击破特攻")], + [_("恢复"), _("能"), _("能量恢复效率")], [_("理"), _("伤害"), _("物理属性伤害")], [_("火"), _("火伤"), _("火属性伤害")], [_("冰"), _("冰伤"), _("冰属性伤害")], @@ -64,16 +74,16 @@ [_("风"), _("风伤"), _("风属性伤害")], [_("量"), _("量子"), _("量子属性伤害")], [_("数"), _("虚数"), _("虚数属性伤害")], - [_("抵抗"), _("效果抵抗"), _("效果抵抗")], - [_("破"), _("击破"), _("击破特攻")], - [_("恢复"), _("能"), _("能量恢复效率")] ], dtype=np.str_) """遗器属性名称:0-属性名的特异词(ocr-不区分大小词条),1-玩家惯用简称(print),2-属性全称(json-区分大小词条)""" -NOT_PRE_STATS = [_("生命值"), _("攻击力"), _("防御力"), _("速度")] +BASE_VALUE_NAME = [_("生命值白值"), _("攻击力白值"), _("防御力白值"), _("速度白值")] +"""角色白值属性名称""" + +NOT_PRE_STATS = [_("生命值"), _("攻击力"), _("防御力"), _("速度")] + BASE_VALUE_NAME """遗器的整数属性名称""" -BASE_STATS_NAME = np.concatenate((STATS_NAME[:2],STATS_NAME[3:-3],STATS_NAME[-2:]), axis=0) +BASE_STATS_NAME = np.concatenate((STATS_NAME[:2],STATS_NAME[3:7],STATS_NAME[8:12],STATS_NAME[13:]), axis=0) """遗器主属性名称""" BASE_STATS_NAME_FOR_EQUIP = [ @@ -81,14 +91,33 @@ BASE_STATS_NAME[1:2], np.vstack((BASE_STATS_NAME[2:5],BASE_STATS_NAME[6:10])), BASE_STATS_NAME[2:6], - np.vstack((BASE_STATS_NAME[2:5],BASE_STATS_NAME[10:17])), - np.vstack((BASE_STATS_NAME[2:5],BASE_STATS_NAME[-2:])) + np.vstack((BASE_STATS_NAME[2:5],BASE_STATS_NAME[-7:])), + np.vstack((BASE_STATS_NAME[2:5],BASE_STATS_NAME[10:12])) ] """遗器各部位主属性名称""" -SUBS_STATS_NAME = np.vstack((STATS_NAME[:10],STATS_NAME[-3:-1])) +SUBS_STATS_NAME = np.vstack((STATS_NAME[:7],STATS_NAME[8:10],STATS_NAME[11:14])) """遗器副属性名称,已按副词条顺序排序""" +EXTRA_STATS_NAME = [ + _("属性抗性"), _("受到伤害降低"), _("护盾量"), _("抵抗控制"), _("无视防御力"), _("属性抗性穿透"), # 均为己方的buff + _("造成伤害"), _("普攻伤害"), _("战技伤害"), _("终结技伤害"), _("追加攻击伤害"), _("持续伤害")] +"""额外属性名称 (遗器套装效果涉及的部分属性)""" + +ALL_STATS_NAME: List[str] = STATS_NAME[:, -1].tolist() + EXTRA_STATS_NAME +"""全属性名称 (包含遗器属性、遗器套装效果涉及的属性)""" + +WEIGHT_STATS_NAME: List[str] = np.vstack((STATS_NAME[:3],STATS_NAME[6:7],STATS_NAME[8:]))[:, -1].tolist() +"""供设定权重的属性名称""" + +SUBS_STATS_TIER_WEIGHT = [ + None, + (0.9, 0.1, "[0.9, 1.0, 1.1]"), # 主流赋值 + (0.8, 0.1, "[0.8, 0.9, 1.0]"), # 真实比例赋值,除五星遗器'速度'属性的真实比例为 [0.7692, 0.8846, 1.0] + (1-1/9, 1/9, "[0.888~, 1.0, 1.111~]"), # 主流赋值比例矫正 +] +"""副词条档位权重:0-空,1-主流赋值,2-真实比例赋值,3-主流赋值比例矫正 (五星遗器*1,四星遗器*0.8)""" + SUBS_STATS_TIER = [ [(27.096, 3.3870 ), (13.548 , 1.6935 ), (13.548 , 1.6935 ), (2.7648, 0.3456), (2.7648, 0.3456), (3.456, 0.4320), # 四星遗器数值 (1.60, 0.20), (2.0736, 0.2592), (4.1472, 0.5184), (2.7648, 0.3456), (2.7648, 0.3456), (4.1472, 0.5184)], @@ -98,13 +127,77 @@ BASE_STATS_TIER = [ [( 90.3168, 31.61088), (45.1584, 15.80544), (5.5296, 1.9354), (5.5296, 1.9354), (6.9120, 2.4192), (3.2256, 1.1), # 四星遗器数值 - (4.1472, 1.4515), ( 8.2944, 2.9030), (5.5296, 1.9354), (4.4237, 1.5483), (4.9766, 1.7418), ( 8.2944, 2.9030), (2.4883, 0.8709)], + (4.1472, 1.4515), ( 8.2944, 2.9030), (4.4237, 1.5483), (5.5296, 1.9354), ( 8.2944, 2.9030), (2.4883, 0.8709), (4.9766, 1.7418)], [(112.896, 39.5136 ), (56.448, 19.7568 ), (6.9120, 2.4192), (6.9120, 2.4192), (8.6400, 3.0240), (4.032, 1.4), # 五星遗器数值 - (5.1840, 1.8144), (10.3680, 3.6288), (6.9120, 2.4192), (5.5296, 1.9354), (6.2208, 2.1773), (10.3680, 3.6288), (3.1104, 1.0886)]] + (5.1840, 1.8144), (10.3680, 3.6288), (5.5296, 1.9354), (6.9120, 2.4192), (10.3680, 3.6288), (3.1104, 1.0886), (6.2208, 2.1773)]] """主属性词条级别:t0-基础值,t1-每提升一级的数值;l1-四星遗器数值,l2-五星遗器数值 <<数据来源:米游社@666bj>>""" for i in range(len(BASE_STATS_TIER)): - BASE_STATS_TIER[i][10:10] = [BASE_STATS_TIER[i][10]] * 6 # 复制属性伤害 + BASE_STATS_TIER[i].extend([BASE_STATS_TIER[i][-1]] * 6) # 复制属性伤害 + +SET_EFFECT_OF_TWO_PC: List[List[StatsEffect]] = [ +# 外圈 + [(_("治疗量加成"), 10, True)], + [(_("攻击力%"), 12, True)], + [(_("防御力%"), 15, True)], + [(_("冰属性伤害"), 10, True)], + [(_("物理伤害"), 10, True)], + [(_("受到伤害降低"), 8, True)], + [(_("火属性伤害"), 10, True)], + [(_("量子属性伤害"), 10, True)], + [(_("雷属性伤害"), 10, True)], + [(_("风属性伤害"), 10, True)], + [(_("击破特攻"), 16, True)], + [(_("虚数属性伤害"), 10, True)], + [(_("生命值%"), 12, True)], + [(_("速度%"), 6, True)], + [(_("追加攻击伤害"), 20, True)], + [(_("攻击力%"), 12, True)], +# 内圈 + [(_("攻击力%"), 12, True), (_("攻击力%"), 12, False), _("当装备者的速度大于等于120时,攻击力额外提高12%")], + [(_("生命值%"), 12, True), (_("攻击力%"), 8, False), _("当装备者的速度大于等于120时,我方全体攻击力提高8%")], + [(_("效果命中"), 10, True), (_("攻击力"), None, False), _("同时提高装备者等同于当前效果命中25%的攻击力,最多提高25%")], + [(_("防御力%"), 15, True), (_("防御力%"), 15, False), _("当装备者的效果命中大于等于50%时,防御力额外提高15%")], + [(_("暴击伤害"), 16, True), (_("暴击率"), 60, False), _("当装备者的暴击伤害大于等于120%时,进入战斗后装备者的暴击率提高60%,持续到施放首次攻击后结束")], + [(_("暴击率"), 8, True), (_("终结技伤害"), 15, False), (_("追加攻击伤害"), 15, False), _("当装备者当前暴击率大于等于50%时,终结技和追加攻击造成的伤害提高15%")], + [(_("击破特攻"), 16, True), (_("击破特攻"), 20, False), _("当装备者的速度大于等于145时,击破特攻额外提高20%")], + [(_("能量恢复效率"), 5, True), _("当装备者的速度大于等于120时,进入战斗时立刻使行动提前40%")], + [(_("暴击率"), 8, True), (_("普攻伤害"), 20, False), (_("战技伤害"), 20, False), _("当装备者的当前暴击率大于等于70%时,普攻和战技造成的伤害提高20%")], + [(_("效果抵抗"), 10, True), (_("暴击伤害"), 10, False), _("当装备者的效果抵抗大于等于30%时,我方全体暴击伤害提高10%")], + [(_("攻击力%"), 12, True), (_("造成伤害"), 12, False), (_("造成伤害"), 6, False), _("当装备者的速度大于等于135/160时,使装备者造成的伤害提高12%/18%")], + [(_("能量恢复效率"), 5, True), (_("造成伤害"), 10, False), _("使队伍中与装备者属性相同的我方其他角色造成的伤害提高10%")], +] +"""遗器二件套效果,True-非条件效果,False-条件效果""" + +for set_index, set_effect in enumerate(SET_EFFECT_OF_TWO_PC): + if isinstance(set_effect[-1], str): + set_effect[-1] = RELIC_SET_NAME[set_index, 2] + _("2件套:") + set_effect[-1] + +SET_EFFECT_OF_FOUR_PC: List[List[StatsEffect]] = [ +# 外圈 + [_("在战斗开始时,立即为我方恢复1个战技点")], + [(_("速度%"), 6, True), (_("普攻伤害"), 10, True)], + [(_("护盾量加成"), 20, True)], + [(_("暴击伤害"), 25, False), _("当装备者施放终结技时,暴击伤害提高25%,持续2回合")], + [(_("攻击力%"), 25, False), _("当装备者施放攻击或受到攻击后,其在本场战斗中攻击力提高5%,最多叠加5层")], + [_("回合开始时,如果装备者当前生命值百分比小于等于50%,则回复等同于自身生命上限8%的生命值,并恢复5点能量")], + [(_("战技伤害"), 12, True), (_("火属性伤害"), 12, False), _("使施放终结技后的下一次攻击造成的火属性伤害提高12%")], + [(_("无视防御力"), 10, True), (_("无视防御力"), 10, False), _("若目标拥有量子属性弱点,额外无视其10%的防御力")], + [(_("攻击力%"), 20, False), _("当装备者施放战技时,使装备者的攻击力提高20%,持续1回合")], + [_("当装备者施放终结技后,使其行动提前25%")], + [(_("击破特攻"), 16, True), _("当装备者击破敌方目标弱点后,恢复3点能量")], + [(_("暴击率"), 10, False), (_("暴击伤害"), 20, False), _("装备者对陷入负面效果的敌方目标造成伤害时暴击率提高10%"), _("装备者对陷入禁锢状态的敌方目标造成伤害时暴击伤害提高20%")], + [(_("暴击率"), 12, False), _("当装备者受到攻击或被我方目标消耗生命值后,暴击率提高8%,持续2回合,该效果最多叠加2层")], + [(_("速度%"), 12, False), _("当装备者对我方目标施放终结技时,我方全体速度提高12%,持续1回合,该效果无法叠加")], + [(_("攻击力%"), 48, False), _("装备者施放追加攻击时,根据追加攻击造成伤害的次数,每次造成伤害时使装备者的攻击力提高6%,最多叠加8次,持续3回合。该效果在装备者下一次施放追加攻击时移除")], + [(_("无视防御力"), 18, False), _("敌方目标每承受1个持续伤害效果,装备者对其造成伤害时就无视其6%的防御力,最多计入3个持续伤害效果")], +] +"""遗器四件套效果,True-非条件效果,False-条件效果""" + +for set_index, set_effect in enumerate(SET_EFFECT_OF_FOUR_PC): + for i, effect in enumerate(set_effect): + if isinstance(effect, str): + set_effect[i] = RELIC_SET_NAME[set_index, 2] + _("4件套:") + effect RELIC_SCHEMA = { @@ -156,6 +249,26 @@ """遗器数据json格式规范""" LOADOUT_SCHEMA = { + "type": "object", + "additionalProperties": { # [主键]人物名称 (以OCR结果为准) + "type": "object", + "additionalProperties": { # [次主键]配装名称 (自定义) + "type": "object", + "properties": { + "relic_hash": { # 配装组成 (6件遗器,按部位排序) + "type": "array", + "minItems": 6, + "maxItems": 6, + "items": {"type": "string"} # [外键]遗器哈希值 + }, + # 【待扩展】如裸装面板、遗器属性权重 + }, + "required": ["relic_hash"], + "additionalProperties": False +}}} +"""人物配装数据json格式规范""" + +LOADOUT_SCHEMA_OLD = { "type": "object", "additionalProperties": { # [主键]人物名称 (以OCR结果为准) "type": "object", @@ -165,7 +278,7 @@ "maxItems": 6, "items": {"type": "string"} # [外键]遗器哈希值 }}} -"""人物配装数据json格式规范""" +"""人物配装数据json格式规范 (兼容旧版)""" TEAM_SCHEMA_PART = { "additionalProperties": { # [主键]队伍名称 (自定义) @@ -195,5 +308,71 @@ } """队伍配装数据json格式规范""" +CHAR_STATS_PANEL_SCHEMA = { + "type": "object", + "additionalProperties": { # [主键]人物名称 (以OCR结果为准) + "type": "object", + "additionalProperties": { # [次主键]面板名称 (自定义) + "type": "object", + "properties": { + "base": { # 白值属性面板 + "properties": { + key: {"type": "number"} for key in BASE_VALUE_NAME + }, + "required": BASE_VALUE_NAME, # 白值属性为必需 + "additionalProperties": False + }, + "additonal": { # 附加属性面板 + "properties": { + key: {"type": "number"} for key in ALL_STATS_NAME + }, + "additionalProperties": False + }, + "conditional": { # 条件属性面板 + "properties": { + key: {"type": "number"} for key in ALL_STATS_NAME + }, + "additionalProperties": False + }, + "extra_effect": { # 额外效果说明 + "type": "array", + "items": {"type": "string"} + }, + # 【待扩展】 如"global" + }, + "required": ["base"], # 白值属性面板为必需 + "additionalProperties": False +}}} +"""角色属性面板""" + +WEIGHT_SCHEMA_PART = { + "type": "object", + "properties": { + "weight": { + "type": "object", + "properties": { + key: {"type": "number"} for key in WEIGHT_STATS_NAME + }, + "additionalProperties": False + }, + # 【待扩展】 如"global" + }, + "required": ["weight"], + "additionalProperties": False +} +CHAR_STATS_WEIGHT_SCHEMA = { + "type": "object", + "additionalProperties": { # [主键]人物名称 (以OCR结果为准) + "type": "object", + "properties": { + "default": # [次主键]默认权重 (默认,名称不可更改) + WEIGHT_SCHEMA_PART, + }, + "additionalProperties": # [次主键]自定义权重 (自定义)【待扩展】 + WEIGHT_SCHEMA_PART, + } +} +"""角色属性权重""" + RELIC_DATA_FILTER = ["pre_ver_hash"] """遗器数据过滤器""" \ No newline at end of file