From ea8e4eae1f7020483fa0752df350b7a9d9d84500 Mon Sep 17 00:00:00 2001
From: omg-xtao <100690902+omg-xtao@users.noreply.github.com>
Date: Thu, 11 May 2023 23:09:39 +0800
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Support=20starrail=20player=5Fcard?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/dependabot.yml | 11 -
core/dependence/assets.py | 59 ++
metadata/shortname.py | 212 ++++---
.../apihelper/client/components/calendar.py | 2 -
.../client/components/player_cards.py | 162 +++++
modules/gacha_log/log.py | 1 -
modules/playercards/fight_prop.py | 147 +++--
modules/playercards/file.py | 27 +-
modules/playercards/helpers.py | 29 +-
.../playercards/metadata/FightPropRule.json | 532 ++++-------------
modules/wiki/character.py | 4 +-
modules/wiki/light_cone.py | 4 +-
modules/wiki/material.py | 4 +-
modules/wiki/models/enums.py | 93 ++-
modules/wiki/models/light_cone.py | 4 +
modules/wiki/models/monster.py | 1 -
modules/wiki/models/relic.py | 6 +
modules/wiki/models/relic_affix.py | 53 ++
modules/wiki/monster.py | 4 +-
modules/wiki/relic.py | 4 +-
plugins/account/account.py | 1 -
plugins/account/cookies.py | 2 -
plugins/app/inline.py | 2 +-
plugins/starrail/avatar_list.py | 10 +-
plugins/starrail/challenge.py | 8 +-
plugins/starrail/ledger.py | 2 +-
plugins/starrail/light_cone.py | 5 +-
plugins/starrail/material.py | 3 +-
plugins/starrail/player_cards.py | 563 ++++++++++++++++++
resources/bot/help/help.html | 28 +-
resources/img/aaa.jpg | Bin 0 -> 462813 bytes
resources/starrail/player_card/artifacts.html | 43 ++
.../starrail/player_card/constellations.html | 13 +
resources/starrail/player_card/holder.html | 37 ++
.../starrail/player_card/holder_example.html | 58 ++
.../starrail/player_card/img/bg-anemo.jpg | Bin 0 -> 191930 bytes
.../starrail/player_card/img/bg-cryo.jpg | Bin 0 -> 177538 bytes
.../starrail/player_card/img/bg-dendro.jpg | Bin 0 -> 206752 bytes
.../starrail/player_card/img/bg-electro.jpg | Bin 0 -> 171390 bytes
.../starrail/player_card/img/bg-nombre.jpg | Bin 0 -> 178887 bytes
.../starrail/player_card/img/bg-physical.jpg | Bin 0 -> 120862 bytes
.../starrail/player_card/img/bg-pyro.jpg | Bin 0 -> 178841 bytes
.../starrail/player_card/img/bg-quantum.jpg | Bin 0 -> 185989 bytes
.../starrail/player_card/img/holder_bg.png | Bin 0 -> 308095 bytes
resources/starrail/player_card/img/star.png | Bin 0 -> 4487 bytes
.../starrail/player_card/img/talent-anemo.png | Bin 0 -> 40733 bytes
.../starrail/player_card/img/talent-cryo.png | Bin 0 -> 40505 bytes
.../player_card/img/talent-dendro.png | Bin 0 -> 41393 bytes
.../player_card/img/talent-electro.png | Bin 0 -> 39749 bytes
.../player_card/img/talent-nombre.png | Bin 0 -> 40053 bytes
.../player_card/img/talent-physical.png | Bin 0 -> 30802 bytes
.../starrail/player_card/img/talent-pyro.png | Bin 0 -> 40268 bytes
.../player_card/img/talent-quantum.png | Bin 0 -> 41138 bytes
.../starrail/player_card/player_card.html | 108 ++++
resources/starrail/player_card/score.html | 12 +
resources/starrail/player_card/skills.html | 17 +
resources/starrail/player_card/stats.html | 3 +
resources/starrail/player_card/style.css | 146 +++++
resources/starrail/player_card/weapon.html | 17 +
59 files changed, 1770 insertions(+), 667 deletions(-)
delete mode 100644 .github/dependabot.yml
create mode 100644 modules/apihelper/client/components/player_cards.py
create mode 100644 modules/wiki/models/relic_affix.py
create mode 100644 plugins/starrail/player_cards.py
create mode 100644 resources/img/aaa.jpg
create mode 100644 resources/starrail/player_card/artifacts.html
create mode 100644 resources/starrail/player_card/constellations.html
create mode 100644 resources/starrail/player_card/holder.html
create mode 100644 resources/starrail/player_card/holder_example.html
create mode 100644 resources/starrail/player_card/img/bg-anemo.jpg
create mode 100644 resources/starrail/player_card/img/bg-cryo.jpg
create mode 100644 resources/starrail/player_card/img/bg-dendro.jpg
create mode 100644 resources/starrail/player_card/img/bg-electro.jpg
create mode 100644 resources/starrail/player_card/img/bg-nombre.jpg
create mode 100644 resources/starrail/player_card/img/bg-physical.jpg
create mode 100644 resources/starrail/player_card/img/bg-pyro.jpg
create mode 100644 resources/starrail/player_card/img/bg-quantum.jpg
create mode 100644 resources/starrail/player_card/img/holder_bg.png
create mode 100644 resources/starrail/player_card/img/star.png
create mode 100644 resources/starrail/player_card/img/talent-anemo.png
create mode 100644 resources/starrail/player_card/img/talent-cryo.png
create mode 100644 resources/starrail/player_card/img/talent-dendro.png
create mode 100644 resources/starrail/player_card/img/talent-electro.png
create mode 100644 resources/starrail/player_card/img/talent-nombre.png
create mode 100644 resources/starrail/player_card/img/talent-physical.png
create mode 100644 resources/starrail/player_card/img/talent-pyro.png
create mode 100644 resources/starrail/player_card/img/talent-quantum.png
create mode 100644 resources/starrail/player_card/player_card.html
create mode 100644 resources/starrail/player_card/score.html
create mode 100644 resources/starrail/player_card/skills.html
create mode 100644 resources/starrail/player_card/stats.html
create mode 100644 resources/starrail/player_card/style.css
create mode 100644 resources/starrail/player_card/weapon.html
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 4417c40..0000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-version: 2
-updates:
- - package-ecosystem: pip
- directory: "/"
- schedule:
- interval: daily
- time: "18:00"
- timezone: Asia/Shanghai
- open-pull-requests-limit: 10
- allow:
- - dependency-type: "direct"
diff --git a/core/dependence/assets.py b/core/dependence/assets.py
index ab8fcfe..90bc299 100644
--- a/core/dependence/assets.py
+++ b/core/dependence/assets.py
@@ -19,6 +19,8 @@ ASSETS_PATH.mkdir(exist_ok=True, parents=True)
DATA_MAP = {
"avatar": WikiModel.BASE_URL + "avatar_icons.json",
"light_cone": WikiModel.BASE_URL + "light_cone_icons.json",
+ "avatar_eidolon": WikiModel.BASE_URL + "avatar_eidolon_icons.json",
+ "avatar_skill": WikiModel.BASE_URL + "skill/info.json",
}
@@ -75,17 +77,28 @@ class _AvatarAssets(_AssetsService):
async def initialize(self):
logger.info("正在初始化角色素材图标")
html = await self.client.get(DATA_MAP["avatar"])
+ eidolons = await self.client.get(DATA_MAP["avatar_eidolon"])
+ eidolons_data = eidolons.json()
+ skills = await self.client.get(DATA_MAP["avatar_skill"])
+ skills_data = skills.json()
self.data = [AvatarIcon(**data) for data in html.json()]
self.name_map = {icon.name: icon for icon in self.data}
self.id_map = {icon.id: icon for icon in self.data}
tasks = []
for icon in self.data:
+ eidolons_s_data = eidolons_data.get(str(icon.id), [])
+ skills_s_data = [f"{i}.png" for i in skills_data if i.startswith(str(icon.id) + "_")]
base_path = self.path / f"{icon.id}"
base_path.mkdir(exist_ok=True, parents=True)
gacha_path = base_path / "gacha.webp"
icon_path = base_path / "icon.webp"
normal_path = base_path / "normal.webp"
square_path = base_path / "square.png"
+ eidolons_paths = [(base_path / f"eidolon_{eidolon_id}.webp") for eidolon_id in range(1, 7)]
+ skills_paths = []
+ for i in skills_s_data:
+ temp_end = "_".join(i.split("_")[1:])
+ skills_paths.append(base_path / f"skill_{temp_end}")
if not gacha_path.exists():
tasks.append(self._download(icon.gacha, gacha_path))
if not icon_path.exists():
@@ -94,6 +107,12 @@ class _AvatarAssets(_AssetsService):
tasks.append(self._download(icon.normal, normal_path))
if not square_path.exists() and icon.square:
tasks.append(self._download(icon.square, square_path))
+ for index, eidolon in enumerate(eidolons_paths):
+ if not eidolon.exists():
+ tasks.append(self._download(eidolons_s_data[index], eidolon))
+ for index, skill in enumerate(skills_paths):
+ if not skill.exists():
+ tasks.append(self._download(WikiModel.BASE_URL + "skill/" + skills_s_data[index], skill))
if len(tasks) >= 100:
await asyncio.gather(*tasks)
tasks = []
@@ -145,6 +164,46 @@ class _AvatarAssets(_AssetsService):
raise AssetsCouldNotFound("角色素材图标不存在", target)
return path
+ def eidolons(self, target: StrOrInt, second_target: StrOrInt = None) -> List[Path]:
+ """星魂"""
+ icon = self.get_target(target, second_target)
+ return [self.get_path(icon, f"eidolon_{i}") for i in range(1, 7)]
+
+ def skill_basic_atk(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
+ """普攻 001"""
+ icon = self.get_target(target, second_target)
+ return self.get_path(icon, "skill_basic_atk", "png")
+
+ def skill_skill(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
+ """战技 002"""
+ icon = self.get_target(target, second_target)
+ return self.get_path(icon, "skill_skill", "png")
+
+ def skill_ultimate(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
+ """终结技 003"""
+ icon = self.get_target(target, second_target)
+ return self.get_path(icon, "skill_ultimate", "png")
+
+ def skill_talent(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
+ """天赋 004"""
+ icon = self.get_target(target, second_target)
+ return self.get_path(icon, "skill_talent", "png")
+
+ def skill_technique(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
+ """秘技 007"""
+ icon = self.get_target(target, second_target)
+ return self.get_path(icon, "skill_technique", "png")
+
+ def skills(self, target: StrOrInt, second_target: StrOrInt = None) -> List[Path]:
+ icon = self.get_target(target, second_target)
+ return [
+ self.get_path(icon, "skill_basic_atk", "png"),
+ self.get_path(icon, "skill_skill", "png"),
+ self.get_path(icon, "skill_ultimate", "png"),
+ self.get_path(icon, "skill_talent", "png"),
+ self.get_path(icon, "skill_technique", "png"),
+ ]
+
class _LightConeAssets(_AssetsService):
path: Path
diff --git a/metadata/shortname.py b/metadata/shortname.py
index 26dc61f..7cdedfc 100644
--- a/metadata/shortname.py
+++ b/metadata/shortname.py
@@ -3,109 +3,120 @@ from __future__ import annotations
import functools
from typing import List
-from metadata.genshin import WEAPON_DATA
-
-__all__ = ["roles", "light_cones", "roleToId", "roleToName", "lightConeToName", "lightConeToId", "not_real_roles", "roleToTag"]
+__all__ = [
+ "roles",
+ "light_cones",
+ "roleToId",
+ "roleToName",
+ "idToRole",
+ "lightConeToName",
+ "lightConeToId",
+ "not_real_roles",
+ "roleToTag",
+]
# noinspection SpellCheckingInspection
roles = {
- 1001: ['三月七'],
- 1002: ['丹恒'],
- 1003: ['姬子'],
- 1004: ['瓦尔特'],
- 1005: ['卡芙卡'],
- 1006: ['银狼'],
- 1008: ['阿兰'],
- 1009: ['艾丝妲'],
- 1013: ['黑塔'],
- 1101: ['布洛妮娅'],
- 1102: ['希儿'],
- 1103: ['希露瓦'],
- 1104: ['杰帕德'],
- 1105: ['娜塔莎'],
- 1106: ['佩拉'],
- 1107: ['克拉拉'],
- 1108: ['桑博'],
- 1109: ['虎克'],
- 1201: ['青雀'],
- 1202: ['停云'],
- 1203: ['罗刹'],
- 1204: ['景元'],
- 1206: ['素裳'],
- 1209: ['彦卿'],
- 1211: ['白露'],
- 8004: ['开拓者'],
+ 8001: ["开拓者"],
+ 8002: ["开拓者"],
+ 8003: ["开拓者"],
+ 8004: ["开拓者"],
+ 1001: ["三月七"],
+ 1002: ["丹恒"],
+ 1003: ["姬子"],
+ 1004: ["瓦尔特"],
+ 1005: ["卡芙卡"],
+ 1006: ["银狼"],
+ 1008: ["阿兰"],
+ 1009: ["艾丝妲"],
+ 1013: ["黑塔"],
+ 1101: ["布洛妮娅"],
+ 1102: ["希儿"],
+ 1103: ["希露瓦"],
+ 1104: ["杰帕德"],
+ 1105: ["娜塔莎"],
+ 1106: ["佩拉"],
+ 1107: ["克拉拉"],
+ 1108: ["桑博"],
+ 1109: ["虎克"],
+ 1201: ["青雀"],
+ 1202: ["停云"],
+ 1203: ["罗刹"],
+ 1204: ["景元"],
+ 1206: ["素裳"],
+ 1209: ["彦卿"],
+ 1211: ["白露"],
}
not_real_roles = []
light_cones = {
- 20000: ['锋镝'],
- 20001: ['物穰'],
- 20002: ['天倾'],
- 20003: ['琥珀'],
- 20004: ['幽邃'],
- 20005: ['齐颂'],
- 20006: ['智库'],
- 20007: ['离弦'],
- 20008: ['嘉果'],
- 20009: ['乐圮'],
- 20010: ['戍御'],
- 20011: ['渊环'],
- 20012: ['轮契'],
- 20013: ['灵钥'],
- 20014: ['相抗'],
- 20015: ['蕃息'],
- 20016: ['俱殁'],
- 20017: ['开疆'],
- 20018: ['匿影'],
- 20019: ['调和'],
- 20020: ['睿见'],
- 21000: ['一场术后对话'],
- 21001: ['晚安与睡颜'],
- 21002: ['余生的第一天'],
- 21003: ['唯有沉默'],
- 21004: ['记忆中的模样'],
- 21005: ['鼹鼠党欢迎你'],
- 21006: ['「我」的诞生'],
- 21007: ['同一种心情'],
- 21008: ['猎物的视线'],
- 21009: ['朗道的选择'],
- 21010: ['论剑'],
- 21011: ['与行星相会'],
- 21012: ['秘密誓心'],
- 21013: ['别让世界静下来'],
- 21014: ['此时恰好'],
- 21015: ['决心如汗珠般闪耀'],
- 21016: ['宇宙市场趋势'],
- 21017: ['点个关注吧!'],
- 21018: ['舞!舞!舞!'],
- 21019: ['在蓝天下'],
- 21020: ['天才们的休憩'],
- 21021: ['等价交换'],
- 21022: ['延长记号'],
- 21023: ['我们是地火'],
- 21024: ['春水初生'],
- 21025: ['过往未来'],
- 21026: ['汪!散步时间!'],
- 21027: ['早餐的仪式感'],
- 21028: ['暖夜不会漫长'],
- 21029: ['后会有期'],
- 21030: ['这就是我啦!'],
- 21031: ['重返幽冥'],
- 21032: ['镂月裁云之意'],
- 21033: ['无处可逃'],
- 21034: ['今日亦是和平的一日'],
- 23000: ['银河铁道之夜'],
- 23001: ['于夜色中'],
- 23002: ['无可取代的东西'],
- 23003: ['但战斗还未结束'],
- 23004: ['以世界之名'],
- 23005: ['制胜的瞬间'],
- 23010: ['拂晓之前'],
- 23012: ['如泥酣眠'],
- 23013: ['时节不居'],
- 24000: ['记一位星神的陨落'],
- 24001: ['星海巡航'],
- 24002: ['记忆的质料']
+ 20000: ["锋镝"],
+ 20001: ["物穰"],
+ 20002: ["天倾"],
+ 20003: ["琥珀"],
+ 20004: ["幽邃"],
+ 20005: ["齐颂"],
+ 20006: ["智库"],
+ 20007: ["离弦"],
+ 20008: ["嘉果"],
+ 20009: ["乐圮"],
+ 20010: ["戍御"],
+ 20011: ["渊环"],
+ 20012: ["轮契"],
+ 20013: ["灵钥"],
+ 20014: ["相抗"],
+ 20015: ["蕃息"],
+ 20016: ["俱殁"],
+ 20017: ["开疆"],
+ 20018: ["匿影"],
+ 20019: ["调和"],
+ 20020: ["睿见"],
+ 21000: ["一场术后对话"],
+ 21001: ["晚安与睡颜"],
+ 21002: ["余生的第一天"],
+ 21003: ["唯有沉默"],
+ 21004: ["记忆中的模样"],
+ 21005: ["鼹鼠党欢迎你"],
+ 21006: ["「我」的诞生"],
+ 21007: ["同一种心情"],
+ 21008: ["猎物的视线"],
+ 21009: ["朗道的选择"],
+ 21010: ["论剑"],
+ 21011: ["与行星相会"],
+ 21012: ["秘密誓心"],
+ 21013: ["别让世界静下来"],
+ 21014: ["此时恰好"],
+ 21015: ["决心如汗珠般闪耀"],
+ 21016: ["宇宙市场趋势"],
+ 21017: ["点个关注吧!"],
+ 21018: ["舞!舞!舞!"],
+ 21019: ["在蓝天下"],
+ 21020: ["天才们的休憩"],
+ 21021: ["等价交换"],
+ 21022: ["延长记号"],
+ 21023: ["我们是地火"],
+ 21024: ["春水初生"],
+ 21025: ["过往未来"],
+ 21026: ["汪!散步时间!"],
+ 21027: ["早餐的仪式感"],
+ 21028: ["暖夜不会漫长"],
+ 21029: ["后会有期"],
+ 21030: ["这就是我啦!"],
+ 21031: ["重返幽冥"],
+ 21032: ["镂月裁云之意"],
+ 21033: ["无处可逃"],
+ 21034: ["今日亦是和平的一日"],
+ 23000: ["银河铁道之夜"],
+ 23001: ["于夜色中"],
+ 23002: ["无可取代的东西"],
+ 23003: ["但战斗还未结束"],
+ 23004: ["以世界之名"],
+ 23005: ["制胜的瞬间"],
+ 23010: ["拂晓之前"],
+ 23012: ["如泥酣眠"],
+ 23013: ["时节不居"],
+ 24000: ["记一位星神的陨落"],
+ 24001: ["星海巡航"],
+ 24002: ["记忆的质料"],
}
@@ -125,6 +136,13 @@ def roleToId(name: str) -> int | None:
return next((key for key, value in roles.items() for n in value if n == name), None)
+# noinspection PyPep8Naming
+@functools.lru_cache()
+def idToRole(aid: int) -> str | None:
+ """获取角色名"""
+ return roles.get(aid, [None])[0]
+
+
# noinspection PyPep8Naming
@functools.lru_cache()
def lightConeToName(shortname: str) -> str:
diff --git a/modules/apihelper/client/components/calendar.py b/modules/apihelper/client/components/calendar.py
index 00d527a..7f0521f 100644
--- a/modules/apihelper/client/components/calendar.py
+++ b/modules/apihelper/client/components/calendar.py
@@ -3,8 +3,6 @@ from datetime import datetime, timedelta
from typing import List, Tuple, Optional, Dict, Union, TYPE_CHECKING
from httpx import AsyncClient
-
-from metadata.genshin import AVATAR_DATA
from metadata.shortname import roleToId
from modules.apihelper.client.components.remote import Remote
from modules.apihelper.models.genshin.calendar import Date, FinalAct, ActEnum, ActDetail, ActTime, BirthChar
diff --git a/modules/apihelper/client/components/player_cards.py b/modules/apihelper/client/components/player_cards.py
new file mode 100644
index 0000000..5933bca
--- /dev/null
+++ b/modules/apihelper/client/components/player_cards.py
@@ -0,0 +1,162 @@
+from typing import List, Optional, Union, Dict
+
+import ujson
+from httpx import AsyncClient, TimeoutException
+from pydantic import BaseModel
+
+from modules.playercards.fight_prop import EquipmentsStats
+from modules.wiki.base import WikiModel
+from modules.wiki.models.relic_affix import RelicAffixAll
+from utils.enkanetwork import RedisCache
+from modules.playercards.file import PlayerCardsFile
+
+
+class Behavior(BaseModel):
+ BehaviorID: int
+ Level: int
+
+
+class Equipment(BaseModel):
+ ID: Optional[int] = 0
+ Level: Optional[int] = 0
+ Promotion: Optional[int] = 3
+ """星级"""
+ Rank: Optional[int] = 0
+ """叠影"""
+
+
+class SubAffix(BaseModel):
+ Cnt: Optional[int] = 1
+ Step: Optional[int] = 0
+ SubAffixID: int
+
+
+class Relic(BaseModel):
+ ID: int
+ Level: Optional[int] = 0
+ MainAffixID: int
+ RelicSubAffix: Optional[List[SubAffix]]
+ Type: int
+
+
+class Avatar(BaseModel):
+ AvatarID: int
+ BehaviorList: List[Behavior]
+ EquipmentID: Optional[Equipment]
+ Level: int
+ Promotion: Optional[int] = 4
+ Rank: Optional[int] = 0
+ RelicList: Optional[List[Relic]]
+
+
+class ChallengeData(BaseModel):
+ MazeGroupID: Optional[int]
+ MazeGroupIndex: Optional[int]
+ PreMazeGroupIndex: Optional[int]
+
+
+class PlayerSpaceInfo(BaseModel):
+ AchievementCount: Optional[int] = 0
+ AvatarCount: Optional[int] = 0
+ ChallengeData: ChallengeData
+ LightConeCount: Optional[int] = 0
+ PassAreaProgress: Optional[int] = 0
+
+
+class PlayerInfo(BaseModel):
+ Birthday: Optional[int]
+ CurFriendCount: Optional[int]
+ AvatarList: List[Avatar]
+ HeadIconID: Optional[int]
+ IsDisplayAvatarList: bool
+ Level: int
+ NickName: str
+ PlayerSpaceInfo: PlayerSpaceInfo
+ Signature: Optional[str]
+ UID: int
+ WorldLevel: Optional[int]
+
+
+class PlayerCardsError(Exception):
+ def __init__(self, msg):
+ self.msg = msg
+
+
+class PlayerCards:
+ url = "https://mhy.fuckmys.tk/sr_info/"
+ prop_url = f"{WikiModel.BASE_URL}relic_config.json"
+
+ def __init__(self, redis):
+ self.cache = RedisCache(redis.client, key="plugin:player_cards:fake_enka_network", ex=60)
+ self.client = AsyncClient()
+ self.player_cards_file = PlayerCardsFile()
+ self.init = False
+ self.relic_datas_map: Dict[int, RelicAffixAll] = {}
+
+ async def async_init(self):
+ if self.init:
+ return
+ self.relic_datas_map.clear()
+ req = await self.client.get(self.prop_url)
+ data = req.json()
+ for i in data:
+ self.relic_datas_map[i["id"]] = RelicAffixAll(**i)
+ self.init = True
+
+ async def update_data(self, uid: str) -> Union[PlayerInfo, str]:
+ try:
+ data = await self.cache.get(uid)
+ if data is not None:
+ return PlayerInfo.parse_obj(data)
+ user = await self.client.get(self.url + uid, timeout=15)
+ if user.status_code != 200:
+ raise PlayerCardsError(f"请求异常,错误代码 {user.status_code}")
+ data = ujson.loads(user.text)
+ error_code = data.get("ErrCode", 0)
+ if error_code:
+ raise PlayerCardsError(f"请求异常,错误代码 {error_code}")
+ data = data.get("PlayerDetailInfo", {})
+ data = await self.player_cards_file.merge_info(uid, data)
+ await self.cache.set(uid, data)
+ return PlayerInfo.parse_obj(data)
+ except TimeoutException:
+ error = "服务请求超时,请稍后重试"
+ except PlayerCardsError as e:
+ error = e.msg
+ return error
+
+ def get_affix_by_id(self, cid: int) -> RelicAffixAll:
+ return self.relic_datas_map.get(cid)
+
+ def get_set_by_id(self, cid: int) -> int:
+ if affix := self.get_affix_by_id(cid):
+ return affix.set_id
+ return 101
+
+ def get_affix(self, relic: Relic, main: bool = True, sub: bool = True) -> List[EquipmentsStats]:
+ affix = self.get_affix_by_id(relic.ID)
+ if not affix:
+ return []
+ main_affix = affix.main_affix[str(relic.MainAffixID)]
+ datas = (
+ [
+ EquipmentsStats(
+ prop_id=main_affix.property,
+ prop_value=main_affix.get_value(relic.Level),
+ )
+ ]
+ if main
+ else []
+ )
+ if not sub:
+ return datas
+ if relic.RelicSubAffix:
+ for sub_a in relic.RelicSubAffix:
+ sub_affix = affix.sub_affix[str(sub_a.SubAffixID)]
+ datas.append(
+ EquipmentsStats(
+ prop_id=sub_affix.property,
+ prop_value=sub_affix.get_value(sub_a.Step, sub_a.Cnt),
+ )
+ )
+ return datas
diff --git a/modules/gacha_log/log.py b/modules/gacha_log/log.py
index 409757c..9185ed2 100644
--- a/modules/gacha_log/log.py
+++ b/modules/gacha_log/log.py
@@ -205,7 +205,6 @@ class GachaLog:
except GachaLogMixedProvider as e:
raise GachaLogMixedProvider from e
except Exception as exc:
- breakpoint()
raise GachaLogException from exc
async def get_gacha_log_data(self, user_id: int, client: Client, authkey: str) -> int:
diff --git a/modules/playercards/fight_prop.py b/modules/playercards/fight_prop.py
index 7e971fa..dc6967d 100644
--- a/modules/playercards/fight_prop.py
+++ b/modules/playercards/fight_prop.py
@@ -1,68 +1,87 @@
-import enum
+import functools
+
+from pydantic import BaseModel
+
+from modules.wiki.models.enums import RelicAffix
+
+relic_affix_map = {
+ RelicAffix.AttackAddedRatio: "攻击力百分比",
+ RelicAffix.AttackDelta: "攻击力",
+ RelicAffix.BreakDamageAddedRatioBase: "击破特攻",
+ RelicAffix.CriticalChanceBase: "暴击率百分比",
+ RelicAffix.CriticalDamageBase: "暴击伤害百分比",
+ RelicAffix.DefenceAddedRatio: "防御力百分比",
+ RelicAffix.DefenceDelta: "防御力",
+ RelicAffix.FireAddedRatio: "火属性伤害提高百分比",
+ RelicAffix.HPAddedRatio: "生命值百分比",
+ RelicAffix.HPDelta: "生命值",
+ RelicAffix.HealRatioBase: "治疗量加成百分比",
+ RelicAffix.IceAddedRatio: "冰属性伤害提高百分比",
+ RelicAffix.ImaginaryAddedRatio: "虚数属性伤害提高百分比",
+ RelicAffix.PhysicalAddedRatio: "物理属性伤害提高百分比",
+ RelicAffix.QuantumAddedRatio: "量子属性伤害提高百分比",
+ RelicAffix.SpeedDelta: "速度",
+ RelicAffix.SPRatioBase: "能量恢复效率百分比",
+ RelicAffix.StatusProbabilityBase: "效果命中百分比",
+ RelicAffix.StatusResistanceBase: "效果抵抗百分比",
+ RelicAffix.ThunderAddedRatio: "雷属性伤害提高百分比",
+ RelicAffix.WindAddedRatio: "风属性伤害提高百分比",
+}
+relic_affix_name_map = {v: k for k, v in relic_affix_map.items()}
+relic_affix_score_map = {
+ RelicAffix.AttackAddedRatio: 1.0,
+ RelicAffix.AttackDelta: 0.5,
+ RelicAffix.BreakDamageAddedRatioBase: 1.0,
+ RelicAffix.CriticalChanceBase: 2.0,
+ RelicAffix.CriticalDamageBase: 2.0,
+ RelicAffix.DefenceAddedRatio: 1.0,
+ RelicAffix.DefenceDelta: 0.5,
+ RelicAffix.FireAddedRatio: 1.0,
+ RelicAffix.HPAddedRatio: 1.0,
+ RelicAffix.HPDelta: 0.5,
+ RelicAffix.HealRatioBase: 1.0,
+ RelicAffix.IceAddedRatio: 1.0,
+ RelicAffix.ImaginaryAddedRatio: 1.0,
+ RelicAffix.PhysicalAddedRatio: 1.0,
+ RelicAffix.QuantumAddedRatio: 1.0,
+ RelicAffix.SpeedDelta: 1.0,
+ RelicAffix.SPRatioBase: 1.0,
+ RelicAffix.StatusProbabilityBase: 1.0,
+ RelicAffix.StatusResistanceBase: 1.0,
+ RelicAffix.ThunderAddedRatio: 1.0,
+ RelicAffix.WindAddedRatio: 1.0,
+}
-class FightProp(enum.Enum):
- BASE_HP = "基础血量"
- FIGHT_PROP_BASE_ATTACK = "基础攻击力"
- FIGHT_PROP_BASE_DEFENSE = "基础防御力"
- FIGHT_PROP_BASE_HP = "基础血量"
- FIGHT_PROP_ATTACK = "攻击力"
- FIGHT_PROP_ATTACK_PERCENT = "攻击力百分比"
- FIGHT_PROP_HP = "生命值"
- FIGHT_PROP_HP_PERCENT = "生命值百分比"
- FIGHT_PROP_DEFENSE = "防御力"
- FIGHT_PROP_DEFENSE_PERCENT = "防御力百分比"
- FIGHT_PROP_ELEMENT_MASTERY = "元素精通"
- FIGHT_PROP_CRITICAL = "暴击率"
- FIGHT_PROP_CRITICAL_HURT = "暴击伤害"
- FIGHT_PROP_CHARGE_EFFICIENCY = "元素充能效率"
- FIGHT_PROP_FIRE_SUB_HURT = "火元素抗性"
- FIGHT_PROP_ELEC_SUB_HURT = "雷元素抗性"
- FIGHT_PROP_ICE_SUB_HURT = "冰元素抗性"
- FIGHT_PROP_WATER_SUB_HURT = "水元素抗性"
- FIGHT_PROP_WIND_SUB_HURT = "风元素抗性"
- FIGHT_PROP_ROCK_SUB_HURT = "岩元素抗性"
- FIGHT_PROP_GRASS_SUB_HURT = "草元素抗性"
- FIGHT_PROP_FIRE_ADD_HURT = "火元素伤害加成"
- FIGHT_PROP_ELEC_ADD_HURT = "雷元素伤害加成"
- FIGHT_PROP_ICE_ADD_HURT = "冰元素伤害加成"
- FIGHT_PROP_WATER_ADD_HURT = "水元素伤害加成"
- FIGHT_PROP_WIND_ADD_HURT = "风元素伤害加成"
- FIGHT_PROP_ROCK_ADD_HURT = "岩元素伤害加成"
- FIGHT_PROP_GRASS_ADD_HURT = "草元素伤害加成"
- FIGHT_PROP_PHYSICAL_ADD_HURT = "物理伤害加成"
- FIGHT_PROP_HEAL_ADD = "治疗加成"
+# noinspection PyPep8Naming
+@functools.lru_cache()
+def FightProp(prop: RelicAffix, percent: bool = True) -> str:
+ name = relic_affix_map.get(prop)
+ return name if percent else name.replace("百分比", "")
-class FightPropScore(enum.Enum):
- _value_: float
- value: float
- FIGHT_PROP_BASE_ATTACK = 1
- FIGHT_PROP_BASE_DEFENSE = 1
- FIGHT_PROP_BASE_HP = 1
- FIGHT_PROP_ATTACK = 662 / 3110 # 攻击力
- FIGHT_PROP_ATTACK_PERCENT = 4 / 3 # 攻击力百分比
- FIGHT_PROP_HP = 662 / 47800 # 生命
- FIGHT_PROP_HP_PERCENT = 4 / 3 # 生命百分比
- FIGHT_PROP_DEFENSE = 662 / 3890 # 防御力
- FIGHT_PROP_DEFENSE_PERCENT = 662 / 583 # 防御力百分比
- FIGHT_PROP_ELEMENT_MASTERY = 1 / 3 # 元素精通
- FIGHT_PROP_CRITICAL = 2 # 暴击率
- FIGHT_PROP_CRITICAL_HURT = 1 # 暴击伤害
- FIGHT_PROP_CHARGE_EFFICIENCY = 662 / 518 # 元素充能效率
- FIGHT_PROP_FIRE_SUB_HURT = 1
- FIGHT_PROP_ELEC_SUB_HURT = 1
- FIGHT_PROP_ICE_SUB_HURT = 1
- FIGHT_PROP_WATER_SUB_HURT = 1
- FIGHT_PROP_WIND_SUB_HURT = 1
- FIGHT_PROP_ROCK_SUB_HURT = 1
- FIGHT_PROP_GRASS_SUB_HURT = 1
- FIGHT_PROP_FIRE_ADD_HURT = 1
- FIGHT_PROP_ELEC_ADD_HURT = 1
- FIGHT_PROP_ICE_ADD_HURT = 1
- FIGHT_PROP_WATER_ADD_HURT = 1
- FIGHT_PROP_WIND_ADD_HURT = 1
- FIGHT_PROP_ROCK_ADD_HURT = 1
- FIGHT_PROP_GRASS_ADD_HURT = 1
- FIGHT_PROP_PHYSICAL_ADD_HURT = 1
- FIGHT_PROP_HEAL_ADD = 1
+# noinspection PyPep8Naming
+@functools.lru_cache()
+def nameToFightProp(name: str) -> RelicAffix:
+ return relic_affix_name_map.get(name)
+
+
+# noinspection PyPep8Naming
+@functools.lru_cache()
+def FightPropScore(prop) -> float:
+ return relic_affix_score_map.get(prop)
+
+
+class EquipmentsStats(BaseModel):
+ prop_id: RelicAffix
+ prop_value: float
+
+ @property
+ def name(self) -> str:
+ return FightProp(self.prop_id, False)
+
+ @property
+ def value(self) -> str:
+ return (
+ str(round(self.prop_value, 1)) if self.prop_value > 1 else str(str(round(self.prop_value * 100.0, 1)) + "%")
+ )
diff --git a/modules/playercards/file.py b/modules/playercards/file.py
index 51d8705..09c83c7 100644
--- a/modules/playercards/file.py
+++ b/modules/playercards/file.py
@@ -63,12 +63,25 @@ class PlayerCardsFile:
async with self._lock:
old_data = await self.load_history_info(uid)
if old_data is None:
- await self.save_json(self.get_file_path(uid), data)
- return data
- data["avatarInfoList"] = data.get("avatarInfoList", [])
- characters = [i.get("avatarId", 0) for i in data["avatarInfoList"]]
- for i in old_data.get("avatarInfoList", []):
- if i.get("avatarId", 0) not in characters:
- data["avatarInfoList"].append(i)
+ old_data = {}
+ avatars = []
+ avatar_ids = []
+ assist_avatar = data.get("AssistAvatar", None)
+ if assist_avatar:
+ avatars.append(assist_avatar)
+ avatar_ids.append(assist_avatar.get("AvatarID", 0))
+ for avatar in data.get("DisplayAvatarList", []):
+ if avatar.get("AvatarID", 0) in avatar_ids:
+ continue
+ avatars.append(avatar)
+ avatar_ids.append(avatar.get("AvatarID", 0))
+ data["AvatarList"] = avatars
+ if "AssistAvatar" in data:
+ del data["AssistAvatar"]
+ if "DisplayAvatarList" in data:
+ del data["DisplayAvatarList"]
+ for i in old_data.get("AvatarList", []):
+ if i.get("AvatarID", 0) not in avatar_ids:
+ data["AvatarList"].append(i)
await self.save_json(self.get_file_path(uid), data)
return data
diff --git a/modules/playercards/helpers.py b/modules/playercards/helpers.py
index c4c18c0..2324ba7 100644
--- a/modules/playercards/helpers.py
+++ b/modules/playercards/helpers.py
@@ -1,9 +1,9 @@
import os
import ujson as json
-from enkanetwork import EquipmentsStats
-from modules.playercards.fight_prop import FightProp, FightPropScore
+from modules.playercards.fight_prop import FightPropScore, EquipmentsStats, nameToFightProp
+from modules.wiki.models.enums import RelicAffix
_project_path = os.path.dirname(__file__)
_fight_prop_rule_file = os.path.join(_project_path, "metadata", "FightPropRule.json")
@@ -15,32 +15,23 @@ class ArtifactStatsTheory:
def __init__(self, character_name: str):
self.character_name = character_name
fight_prop_rule_list = fight_prop_rule_data.get(self.character_name, [])
- self.main_prop = [FightProp(fight_prop_rule) for fight_prop_rule in fight_prop_rule_list]
+ self.main_prop = [nameToFightProp(fight_prop_rule) for fight_prop_rule in fight_prop_rule_list]
if not self.main_prop:
self.main_prop = [
- FightProp.FIGHT_PROP_CRITICAL,
- FightProp.FIGHT_PROP_CRITICAL_HURT,
- FightProp.FIGHT_PROP_ATTACK_PERCENT,
+ RelicAffix.CriticalChanceBase,
+ RelicAffix.CriticalDamageBase,
+ RelicAffix.AttackAddedRatio,
]
- # 修正要评分的数值词条
- if FightProp.FIGHT_PROP_ATTACK_PERCENT in self.main_prop and FightProp.FIGHT_PROP_ATTACK not in self.main_prop:
- self.main_prop.append(FightProp.FIGHT_PROP_ATTACK)
- if FightProp.FIGHT_PROP_HP_PERCENT in self.main_prop and FightProp.FIGHT_PROP_HP not in self.main_prop:
- self.main_prop.append(FightProp.FIGHT_PROP_HP)
- if (
- FightProp.FIGHT_PROP_DEFENSE_PERCENT in self.main_prop
- and FightProp.FIGHT_PROP_DEFENSE not in self.main_prop
- ):
- self.main_prop.append(FightProp.FIGHT_PROP_DEFENSE)
def theory(self, sub_stats: EquipmentsStats) -> float:
- """圣遗物副词条评分
+ """圣遗物词条评分
Args:
sub_stats: 圣遗物对象
Returns:
返回得分
"""
score: float = 0
- if sub_stats.prop_id in map(lambda x: x.name, self.main_prop):
- score = float(FightPropScore[sub_stats.prop_id].value) * sub_stats.value
+ if sub_stats.prop_id in self.main_prop:
+ base_value = 100.0 if sub_stats.prop_value < 1 else 1.0
+ score = float(FightPropScore(sub_stats.prop_id) * sub_stats.prop_value * base_value)
return round(score, 1)
diff --git a/modules/playercards/metadata/FightPropRule.json b/modules/playercards/metadata/FightPropRule.json
index 6c56255..844f9f6 100644
--- a/modules/playercards/metadata/FightPropRule.json
+++ b/modules/playercards/metadata/FightPropRule.json
@@ -1,411 +1,125 @@
{
- "旅行者": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率",
- "元素精通"
- ],
- "安柏": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通"
- ],
- "凯亚": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "丽莎": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通"
- ],
- "芭芭拉": [
- "生命值百分比",
- "元素充能效率",
- "元素精通"
- ],
- "芭芭拉-核爆": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通"
- ],
- "雷泽": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "物理伤害加成"
- ],
- "香菱": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "北斗": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "行秋": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "凝光": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "菲谢尔": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通"
- ],
- "班尼特": [
- "生命值百分比",
- "元素充能效率",
- "治疗加成"
- ],
- "诺艾尔": [
- "攻击力百分比",
- "防御力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "重云": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "砂糖": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "琴": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "物理伤害加成",
- "元素充能效率",
- "治疗加成"
- ],
- "迪卢克": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通"
- ],
- "七七": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "物理伤害加成",
- "元素充能效率",
- "治疗加成"
- ],
- "莫娜": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "刻晴": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "物理伤害加成"
- ],
- "温迪": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "可莉": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "迪奥娜": [
- "生命值百分比",
- "元素充能效率",
- "治疗加成"
- ],
- "达达利亚": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "辛焱": [
- "防御力百分比",
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "物理伤害加成"
- ],
- "钟离": [
- "生命值百分比",
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "物理伤害加成",
- "元素充能效率"
- ],
- "钟离-安如磐石": [
- "生命值百分比",
- "暴击率"
- ],
- "阿贝多": [
- "防御力百分比",
- "暴击率",
- "暴击伤害"
- ],
- "甘雨": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通"
- ],
- "甘雨-永冻": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "魈": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "胡桃": [
- "生命值百分比",
- "暴击率",
- "暴击伤害",
- "元素精通"
- ],
- "罗莎莉亚": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "物理伤害加成",
- "元素充能效率"
- ],
- "烟绯": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "优菈": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "物理伤害加成",
- "元素充能效率"
- ],
- "枫原万叶": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "神里绫华": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "早柚": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率",
- "治疗加成"
- ],
- "宵宫": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通"
- ],
- "埃洛伊": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害"
- ],
- "九条裟罗": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "雷电将军": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率",
- "元素精通"
- ],
- "珊瑚宫心海": [
- "生命值百分比",
- "攻击力百分比",
- "元素充能效率",
- "治疗加成",
- "元素精通"
- ],
- "托马": [
- "生命值百分比",
- "暴击率",
- "元素充能效率",
- "元素精通"
- ],
- "五郎": [
- "防御力百分比",
- "暴击率",
- "元素充能效率"
- ],
- "荒泷一斗": [
- "防御力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "云堇": [
- "防御力百分比",
- "暴击率",
- "元素充能效率"
- ],
- "申鹤": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "八重神子": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "神里绫人": [
- "生命值百分比",
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "夜兰": [
- "生命值百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "久岐忍": [
- "生命值百分比",
- "元素精通",
- "治疗加成"
- ],
- "鹿野院平藏": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "提纳里": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "柯莱": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "赛诺": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "妮露": [
- "生命值百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "纳西妲": [
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率",
- "攻击力百分比"
- ],
- "流浪者": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "珐露珊": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素充能效率"
- ],
- "艾尔海森": [
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ],
- "瑶瑶": [
- "生命值百分比",
- "元素充能效率"
- ],
- "迪希雅": [
- "生命值百分比",
- "攻击力百分比",
- "暴击率",
- "暴击伤害",
- "元素精通",
- "元素充能效率"
- ]
+ "丹恒": [
+ "攻击力百分比",
+ "暴击率百分比",
+ "暴击伤害百分比",
+ "风属性伤害提高百分比"
+ ],
+ "彦卿": [
+ "攻击力百分比",
+ "暴击率百分比",
+ "暴击伤害百分比",
+ "冰属性伤害提高百分比"
+ ],
+ "希儿": [
+ "攻击力百分比",
+ "暴击率百分比",
+ "暴击伤害百分比",
+ "量子属性伤害提高百分比",
+ "速度"
+ ],
+ "姬子": [
+ "攻击力百分比",
+ "暴击率百分比",
+ "暴击伤害百分比",
+ "火属性伤害提高百分比"
+ ],
+ "阿兰": [
+ "攻击力百分比",
+ "暴击率百分比",
+ "暴击伤害百分比",
+ "雷属性伤害提高百分比"
+ ],
+ "黑塔": [
+ "攻击力百分比",
+ "暴击率百分比",
+ "暴击伤害百分比",
+ "冰属性伤害提高百分比"
+ ],
+ "希露瓦": [
+ "攻击力百分比",
+ "暴击率百分比",
+ "暴击伤害百分比",
+ "雷属性伤害提高百分比"
+ ],
+ "克拉拉": [
+ "攻击力百分比",
+ "暴击率百分比",
+ "暴击伤害百分比",
+ "物理属性伤害提高百分比"
+ ],
+ "虎克": [
+ "攻击力百分比",
+ "暴击率百分比",
+ "暴击伤害百分比",
+ "火属性伤害提高百分比"
+ ],
+ "青雀": [
+ "攻击力百分比",
+ "暴击率百分比",
+ "暴击伤害百分比",
+ "量子属性伤害提高百分比"
+ ],
+ "素裳": [
+ "攻击力百分比",
+ "暴击率百分比",
+ "暴击伤害百分比",
+ "物理属性伤害提高百分比"
+ ],
+ "杰帕德": [
+ "防御力百分比",
+ "防御力",
+ "能量恢复效率百分比"
+ ],
+ "三月七": [
+ "防御力百分比",
+ "防御力",
+ "能量恢复效率百分比"
+ ],
+ "瓦尔特": [
+ "虚数属性伤害提高百分比",
+ "速度",
+ "能量恢复效率百分比",
+ "效果命中百分比"
+ ],
+ "桑博": [
+ "速度",
+ "能量恢复效率百分比",
+ "效果命中百分比",
+ "风属性伤害提高百分比"
+ ],
+ "佩拉": [
+ "冰属性伤害提高百分比",
+ "速度",
+ "能量恢复效率百分比",
+ "效果命中百分比"
+ ],
+ "停云": [
+ "攻击力百分比",
+ "速度",
+ "能量恢复效率百分比",
+ "雷属性伤害提高百分比"
+ ],
+ "艾丝妲": [
+ "攻击力百分比",
+ "火属性伤害提高百分比",
+ "速度",
+ "能量恢复效率百分比"
+ ],
+ "白露": [
+ "生命值百分比",
+ "生命值",
+ "治疗量加成百分比"
+ ],
+ "娜塔莎": [
+ "生命值百分比",
+ "生命值",
+ "治疗量加成百分比"
+ ],
+ "布洛妮娅": [
+ "暴击伤害百分比",
+ "速度",
+ "能量恢复效率百分比",
+ "风属性伤害提高百分比"
+ ]
}
\ No newline at end of file
diff --git a/modules/wiki/character.py b/modules/wiki/character.py
index a5b2baf..53ccbc8 100644
--- a/modules/wiki/character.py
+++ b/modules/wiki/character.py
@@ -37,10 +37,10 @@ class Character(WikiModel):
self.all_avatars_name[m.name] = m
def get_by_id(self, cid: int) -> Optional[Avatar]:
- return self.all_avatars_map.get(cid, None)
+ return self.all_avatars_map.get(cid)
def get_by_name(self, name: str) -> Optional[Avatar]:
- return self.all_avatars_name.get(name, None)
+ return self.all_avatars_name.get(name)
def get_name_list(self) -> List[str]:
return list(self.all_avatars_name.keys())
diff --git a/modules/wiki/light_cone.py b/modules/wiki/light_cone.py
index fbf436f..09aac5f 100644
--- a/modules/wiki/light_cone.py
+++ b/modules/wiki/light_cone.py
@@ -37,10 +37,10 @@ class LightCone(WikiModel):
self.all_light_cones_name[m.name] = m
def get_by_id(self, cid: int) -> Optional[LightConeModel]:
- return self.all_light_cones_map.get(cid, None)
+ return self.all_light_cones_map.get(cid)
def get_by_name(self, name: str) -> Optional[LightConeModel]:
- return self.all_light_cones_name.get(name, None)
+ return self.all_light_cones_name.get(name)
def get_name_list(self) -> List[str]:
return list(self.all_light_cones_name.keys())
diff --git a/modules/wiki/material.py b/modules/wiki/material.py
index 35f4618..c137388 100644
--- a/modules/wiki/material.py
+++ b/modules/wiki/material.py
@@ -37,10 +37,10 @@ class Material(WikiModel):
self.all_materials_name[m.name] = m
def get_by_id(self, cid: int) -> Optional[MaterialModel]:
- return self.all_materials_map.get(cid, None)
+ return self.all_materials_map.get(cid)
def get_by_name(self, name: str) -> Optional[MaterialModel]:
- return self.all_materials_name.get(name, None)
+ return self.all_materials_name.get(name)
def get_name_list(self) -> List[str]:
return list(self.all_materials_name.keys())
diff --git a/modules/wiki/models/enums.py b/modules/wiki/models/enums.py
index 28ce262..46ba953 100644
--- a/modules/wiki/models/enums.py
+++ b/modules/wiki/models/enums.py
@@ -2,7 +2,8 @@ from enum import Enum
class Quality(str, Enum):
- """ 星级 """
+ """星级"""
+
Five = "五星"
Four = "四星"
Three = "三星"
@@ -11,7 +12,8 @@ class Quality(str, Enum):
class Destiny(str, Enum):
- """ 命途 """
+ """命途"""
+
HuiMie = "毁灭"
ZhiShi = "智识"
XunLie = "巡猎"
@@ -22,7 +24,8 @@ class Destiny(str, Enum):
class Element(str, Enum):
- """ 属性 """
+ """属性"""
+
Physical = "物理"
Pyro = "火"
Anemo = "风"
@@ -35,7 +38,8 @@ class Element(str, Enum):
class MonsterType(str, Enum):
- """ 怪物种类 """
+ """怪物种类"""
+
Normal = "普通"
Elite = "精英"
Leader = "首领"
@@ -43,7 +47,8 @@ class MonsterType(str, Enum):
class Area(str, Enum):
- """ 地区 """
+ """地区"""
+
Herta = "空间站「黑塔」"
YaLiLuo = "雅利洛-VI"
LuoFu = "仙舟「罗浮」"
@@ -51,7 +56,8 @@ class Area(str, Enum):
class MaterialType(str, Enum):
- """ 材料类型 """
+ """材料类型"""
+
AvatarUpdate = "角色晋阶材料"
XingJi = "行迹材料"
LightConeUpdate = "光锥晋阶材料"
@@ -66,7 +72,8 @@ class MaterialType(str, Enum):
class PropType(str, Enum):
- """ 遗器套装效果 """
+ """遗器套装效果"""
+
HP = "基础-生命值"
Defense = "基础-防御力"
Attack = "基础-攻击力"
@@ -82,3 +89,75 @@ class PropType(str, Enum):
Heal = "其他-治疗加成"
OtherCritical = "其他-效果命中"
Charge = "其他-能量充能效率"
+
+
+class RelicAffix(str, Enum):
+ AttackAddedRatio: str = "AttackAddedRatio"
+ """ 攻击力 百分比 """
+ AttackDelta: str = "AttackDelta"
+ """ 攻击力 """
+ BreakDamageAddedRatioBase: str = "BreakDamageAddedRatioBase"
+ """ 击破特攻 """
+ CriticalChanceBase: str = "CriticalChanceBase"
+ """ 暴击率 百分比 """
+ CriticalDamageBase: str = "CriticalDamageBase"
+ """ 暴击伤害 百分比 """
+ DefenceAddedRatio: str = "DefenceAddedRatio"
+ """ 防御力 百分比 """
+ DefenceDelta: str = "DefenceDelta"
+ """ 防御力 """
+ FireAddedRatio: str = "FireAddedRatio"
+ """ 火属性伤害提高 百分比 """
+ HPAddedRatio: str = "HPAddedRatio"
+ """ 生命值 百分比 """
+ HPDelta: str = "HPDelta"
+ """ 生命值 """
+ HealRatioBase: str = "HealRatioBase"
+ """ 治疗量加成 百分比"""
+ IceAddedRatio: str = "IceAddedRatio"
+ """ 冰属性伤害提高 百分比 """
+ ImaginaryAddedRatio: str = "ImaginaryAddedRatio"
+ """ 虚数属性伤害提高 百分比 """
+ PhysicalAddedRatio: str = "PhysicalAddedRatio"
+ """ 物理属性伤害提高 百分比 """
+ QuantumAddedRatio: str = "QuantumAddedRatio"
+ """ 量子属性伤害提高 百分比 """
+ SpeedDelta: str = "SpeedDelta"
+ """ 速度 """
+ SPRatioBase: str = "SPRatioBase"
+ """ 能量恢复效率 百分比 """
+ StatusProbabilityBase: str = "StatusProbabilityBase"
+ """ 效果命中 百分比 """
+ StatusResistanceBase: str = "StatusResistanceBase"
+ """ 效果抵抗 百分比 """
+ ThunderAddedRatio: str = "ThunderAddedRatio"
+ """ 雷属性伤害提高 百分比 """
+ WindAddedRatio: str = "WindAddedRatio"
+ """ 风属性伤害提高 百分比 """
+
+
+class RelicPosition(str, Enum):
+ HEAD: str = "HEAD"
+ """ 头 """
+ HAND: str = "HAND"
+ """ 手 """
+ BODY: str = "BODY"
+ """ 躯干 """
+ FOOT: str = "FOOT"
+ """ 脚 """
+ NECK: str = "NECK"
+ """ 位面球 """
+ OBJECT: str = "OBJECT"
+ """ 连结绳 """
+
+ @property
+ def num(self):
+ index_map = {
+ RelicPosition.HEAD: 0,
+ RelicPosition.HAND: 1,
+ RelicPosition.BODY: 2,
+ RelicPosition.FOOT: 3,
+ RelicPosition.NECK: 0,
+ RelicPosition.OBJECT: 1,
+ }
+ return index_map.get(self)
diff --git a/modules/wiki/models/light_cone.py b/modules/wiki/models/light_cone.py
index 0cb5a60..a9c8fee 100644
--- a/modules/wiki/models/light_cone.py
+++ b/modules/wiki/models/light_cone.py
@@ -43,3 +43,7 @@ class LightCone(BaseModel):
"""命途"""
promote: list[LightConePromote]
"""晋阶信息"""
+
+ @property
+ def rarity(self) -> int:
+ return 5 - list(Quality).index(self.quality)
diff --git a/modules/wiki/models/monster.py b/modules/wiki/models/monster.py
index ccc20d6..474f3c7 100644
--- a/modules/wiki/models/monster.py
+++ b/modules/wiki/models/monster.py
@@ -23,4 +23,3 @@ class Monster(BaseModel):
"""抗性"""
find_area: str
"""发现地点"""
-
diff --git a/modules/wiki/models/relic.py b/modules/wiki/models/relic.py
index 7272640..7de7f33 100644
--- a/modules/wiki/models/relic.py
+++ b/modules/wiki/models/relic.py
@@ -1,13 +1,19 @@
# 遗器套装
+from typing import List
+
from pydantic import BaseModel
class Relic(BaseModel):
id: int
"""遗器套装ID"""
+ bbs_id: int
+ """WIKI ID"""
name: str
"""套装名称"""
icon: str
"""套装图标"""
affect: str
"""套装效果"""
+ image_list: List[str]
+ """套装子图"""
diff --git a/modules/wiki/models/relic_affix.py b/modules/wiki/models/relic_affix.py
new file mode 100644
index 0000000..e2dbfe7
--- /dev/null
+++ b/modules/wiki/models/relic_affix.py
@@ -0,0 +1,53 @@
+from decimal import Decimal
+from typing import Optional, Dict
+
+from pydantic import BaseModel, root_validator
+
+from .enums import RelicAffix, RelicPosition
+
+
+class SingleRelicAffix(BaseModel):
+ id: int
+ property: RelicAffix
+ base_value: float
+ level_value: Optional[float] = None
+ step_value: Optional[float] = None
+ is_main: bool
+ max_step: Optional[int] = None
+
+ def get_value(self, level_or_step: int, cnt: int = 1) -> float:
+ base_value = Decimal(self.base_value) * Decimal(cnt)
+ add_value = Decimal(self.level_value if self.is_main else self.step_value)
+ return float(base_value + add_value * Decimal(level_or_step))
+
+
+class RelicAffixAll(BaseModel):
+ id: int
+ set_id: int
+ """ 套装ID """
+ type: RelicPosition
+ """ 遗器类型 """
+ rarity: int
+ """ 星级 """
+ main_affix_group: int
+ sub_affix_group: int
+ max_level: int
+ """ 最大等级 """
+ main_affix: Dict[str, SingleRelicAffix]
+ """ 主词条 """
+ sub_affix: Dict[str, SingleRelicAffix]
+ """ 副词条 """
+
+ @root_validator(pre=True)
+ def transform_dicts(cls, values):
+ for data in ["main_affix", "sub_affix"]:
+ affix = values.get(data)
+ if affix:
+ new_affix = {}
+ for key, value in affix.items():
+ if isinstance(value, dict):
+ new_affix[key] = SingleRelicAffix(**value)
+ else:
+ new_affix[key] = value
+ values[data] = new_affix
+ return values
diff --git a/modules/wiki/monster.py b/modules/wiki/monster.py
index 98741e3..7c24d6b 100644
--- a/modules/wiki/monster.py
+++ b/modules/wiki/monster.py
@@ -37,10 +37,10 @@ class Monster(WikiModel):
self.all_monsters_name[m.name] = m
def get_by_id(self, cid: int) -> Optional[MonsterModel]:
- return self.all_monsters_map.get(cid, None)
+ return self.all_monsters_map.get(cid)
def get_by_name(self, name: str) -> Optional[MonsterModel]:
- return self.all_monsters_name.get(name, None)
+ return self.all_monsters_name.get(name)
def get_name_list(self) -> List[str]:
return list(self.all_monsters_name.keys())
diff --git a/modules/wiki/relic.py b/modules/wiki/relic.py
index 674e263..8c4106a 100644
--- a/modules/wiki/relic.py
+++ b/modules/wiki/relic.py
@@ -37,10 +37,10 @@ class Relic(WikiModel):
self.all_relics_name[m.name] = m
def get_by_id(self, cid: int) -> Optional[RelicModel]:
- return self.all_relics_map.get(cid, None)
+ return self.all_relics_map.get(cid)
def get_by_name(self, name: str) -> Optional[RelicModel]:
- return self.all_relics_name.get(name, None)
+ return self.all_relics_name.get(name)
def get_name_list(self) -> List[str]:
return list(self.all_relics_name.keys())
diff --git a/plugins/account/account.py b/plugins/account/account.py
index 4d99dfa..e69a093 100644
--- a/plugins/account/account.py
+++ b/plugins/account/account.py
@@ -87,7 +87,6 @@ class BindAccountPlugin(Plugin.Conversation):
elif message.text == "HoYoLab":
await message.reply_text("很抱歉,暂不支持HoYoLab服务器", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
- bind_account_plugin_data.region = RegionEnum.HOYOLAB
else:
await message.reply_text("选择错误,请重新选择")
return CHECK_SERVER
diff --git a/plugins/account/cookies.py b/plugins/account/cookies.py
index 7d5b2ee..14b0613 100644
--- a/plugins/account/cookies.py
+++ b/plugins/account/cookies.py
@@ -131,8 +131,6 @@ class AccountCookiesPlugin(Plugin.Conversation):
elif message.text == "HoYoLab":
await message.reply_text("很抱歉,暂不支持HoYoLab服务器", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
- bbs_name = "HoYoLab"
- region = RegionEnum.HOYOLAB
else:
await message.reply_text("选择错误,请重新选择")
return CHECK_SERVER
diff --git a/plugins/app/inline.py b/plugins/app/inline.py
index 099d619..e9dff32 100644
--- a/plugins/app/inline.py
+++ b/plugins/app/inline.py
@@ -12,7 +12,7 @@ from telegram import (
)
from telegram.constants import ParseMode
from telegram.error import BadRequest
-from telegram.ext import CallbackContext, InlineQueryHandler
+from telegram.ext import CallbackContext
from core.plugin import Plugin, handler
from core.dependence.assets import AssetsService
diff --git a/plugins/starrail/avatar_list.py b/plugins/starrail/avatar_list.py
index e81830f..7b44114 100644
--- a/plugins/starrail/avatar_list.py
+++ b/plugins/starrail/avatar_list.py
@@ -14,7 +14,6 @@ from core.services.cookies import CookiesService
from core.services.template.models import FileType
from core.services.template.services import TemplateService
from core.services.wiki.services import WikiService
-from modules.wiki.models.enums import Quality
from plugins.tools.genshin import CookiesNotFoundError, GenshinHelper, PlayerNotFoundError
from utils.log import logger
@@ -106,14 +105,7 @@ class AvatarListPlugin(Plugin):
def get_light_cone_star(self, name: str) -> int:
light_cone = self.wiki_service.light_cone.get_by_name(name)
- star_int_map = {
- Quality.Five: 5,
- Quality.Four: 4,
- Quality.Three: 3,
- Quality.Two: 2,
- Quality.One: 1,
- }
- return star_int_map[light_cone.quality] if light_cone else 3
+ return light_cone.rarity if light_cone else 3
async def get_final_data(self, characters: List[StarRailDetailCharacter]) -> List[AvatarData]:
data = []
diff --git a/plugins/starrail/challenge.py b/plugins/starrail/challenge.py
index c32e15a..e82271b 100644
--- a/plugins/starrail/challenge.py
+++ b/plugins/starrail/challenge.py
@@ -128,9 +128,7 @@ class ChallengePlugin(Plugin):
return
async def reply_message_func(content: str) -> None:
- _reply_msg = await message.reply_text(
- f"开拓者 ({uid}
) {content}", parse_mode=ParseMode.HTML
- )
+ _reply_msg = await message.reply_text(f"开拓者 ({uid}
) {content}", parse_mode=ParseMode.HTML)
reply_text: Optional[Message] = None
@@ -180,7 +178,7 @@ class ChallengePlugin(Plugin):
"floor": floor_data,
"floor_time": floor_data.node_1.challenge_time.datetime.strftime("%Y-%m-%d %H:%M:%S"),
"floor_nodes": [floor_data.node_1, floor_data.node_2],
- "floor_num": floor
+ "floor_num": floor,
}
return render_data
@@ -244,7 +242,7 @@ class ChallengePlugin(Plugin):
8: "#1D2A5D",
9: "#292B58",
10: "#382024",
- }
+ },
}
if total:
diff --git a/plugins/starrail/ledger.py b/plugins/starrail/ledger.py
index a6c5923..5fde825 100644
--- a/plugins/starrail/ledger.py
+++ b/plugins/starrail/ledger.py
@@ -97,7 +97,7 @@ class LedgerPlugin(Plugin):
last_month = last_month.replace(day=1) - timedelta(days=1)
allow_month_year[last_month.month] = last_month.year
- if (month not in allow_month_year.keys()) or (not isinstance(month, int)):
+ if (month not in allow_month_year) or (not isinstance(month, int)):
raise IndexError
year = allow_month_year[month]
except IndexError:
diff --git a/plugins/starrail/light_cone.py b/plugins/starrail/light_cone.py
index 11bf389..8c4225a 100644
--- a/plugins/starrail/light_cone.py
+++ b/plugins/starrail/light_cone.py
@@ -35,10 +35,7 @@ class LightConePlugin(Plugin):
if len(args) >= 1:
light_cone_name = args[0]
else:
- reply_message = await message.reply_text(
- "请回复你要查询的光锥名称",
- reply_markup=InlineKeyboardMarkup(self.KEYBOARD)
- )
+ reply_message = await message.reply_text("请回复你要查询的光锥名称", reply_markup=InlineKeyboardMarkup(self.KEYBOARD))
if filters.ChatType.GROUPS.filter(reply_message):
self.add_delete_message_job(message)
self.add_delete_message_job(reply_message)
diff --git a/plugins/starrail/material.py b/plugins/starrail/material.py
index 5cdac50..a8b0dca 100644
--- a/plugins/starrail/material.py
+++ b/plugins/starrail/material.py
@@ -36,8 +36,7 @@ class MaterialPlugin(Plugin):
character_name = args[0]
else:
reply_message = await message.reply_text(
- "请回复你要查询的角色培养素材图鉴的角色名",
- reply_markup=InlineKeyboardMarkup(self.KEYBOARD)
+ "请回复你要查询的角色培养素材图鉴的角色名", reply_markup=InlineKeyboardMarkup(self.KEYBOARD)
)
if filters.ChatType.GROUPS.filter(reply_message):
self.add_delete_message_job(message)
diff --git a/plugins/starrail/player_cards.py b/plugins/starrail/player_cards.py
new file mode 100644
index 0000000..1598479
--- /dev/null
+++ b/plugins/starrail/player_cards.py
@@ -0,0 +1,563 @@
+import math
+from typing import List, Tuple, Union, Optional, TYPE_CHECKING, Dict
+
+from pydantic import BaseModel
+from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Message
+from telegram.constants import ChatAction
+from telegram.ext import filters
+from telegram.helpers import create_deep_linked_url
+
+from core.config import config
+from core.dependence.assets import AssetsService, AssetsCouldNotFound
+from core.dependence.redisdb import RedisDB
+from core.plugin import Plugin, handler
+from core.services.players import PlayersService
+from core.services.template.services import TemplateService
+from core.services.wiki.services import WikiService
+from metadata.shortname import roleToName, idToRole
+from modules.apihelper.client.components.player_cards import PlayerCards as PlayerCardsClient, PlayerInfo, Avatar, Relic
+from modules.playercards.fight_prop import EquipmentsStats
+from modules.playercards.helpers import ArtifactStatsTheory
+from utils.log import logger
+
+if TYPE_CHECKING:
+ from telegram.ext import ContextTypes
+ from telegram import Update
+
+try:
+ import ujson as jsonlib
+except ImportError:
+ import json as jsonlib
+
+
+class PlayerCards(Plugin):
+ def __init__(
+ self,
+ player_service: PlayersService,
+ template_service: TemplateService,
+ assets_service: AssetsService,
+ wiki_service: WikiService,
+ redis: RedisDB,
+ ):
+ self.player_service = player_service
+ self.client = PlayerCardsClient(redis)
+ self.cache = self.client.cache
+ self.assets_service = assets_service
+ self.template_service = template_service
+ self.wiki_service = wiki_service
+ self.kitsune: Optional[str] = None
+
+ async def initialize(self):
+ await self.client.async_init()
+
+ async def _load_history(self, uid) -> Optional[PlayerInfo]:
+ data = await self.client.player_cards_file.load_history_info(uid)
+ if data is None:
+ return None
+ return PlayerInfo.parse_obj(data)
+
+ async def get_uid_and_ch(
+ self, user_id: int, args: List[str], reply: Optional[Message]
+ ) -> Tuple[Optional[int], Optional[str]]:
+ """通过消息获取 uid,优先级:args > reply > self"""
+ uid, ch_name, user_id_ = None, None, user_id
+ if args:
+ for i in args:
+ if i is not None:
+ if i.isdigit() and len(i) == 9:
+ uid = int(i)
+ else:
+ ch_name = roleToName(i)
+ if reply:
+ try:
+ user_id_ = reply.from_user.id
+ except AttributeError:
+ pass
+ if not uid:
+ player_info = await self.player_service.get_player(user_id_)
+ if player_info is not None:
+ uid = player_info.player_id
+ if (not uid) and (user_id_ != user_id):
+ player_info = await self.player_service.get_player(user_id)
+ if player_info is not None:
+ uid = player_info.player_id
+ return uid, ch_name
+
+ @handler.command(command="player_card", block=False)
+ @handler.message(filters=filters.Regex("^角色卡片查询(.*)"), block=False)
+ async def player_cards(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None:
+ user = update.effective_user
+ message = update.effective_message
+ args = self.get_args(context)
+ await message.reply_chat_action(ChatAction.TYPING)
+ uid, ch_name = await self.get_uid_and_ch(user.id, args, message.reply_to_message)
+ if uid is None:
+ buttons = [
+ [
+ InlineKeyboardButton(
+ "点我绑定账号",
+ url=create_deep_linked_url(context.bot.username, "set_uid"),
+ )
+ ]
+ ]
+ if filters.ChatType.GROUPS.filter(message):
+ reply_message = await message.reply_text(
+ "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号",
+ reply_markup=InlineKeyboardMarkup(buttons),
+ )
+ self.add_delete_message_job(reply_message, delay=30)
+
+ self.add_delete_message_job(message, delay=30)
+ else:
+ await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons))
+ return
+ # 暂时只支持国服
+ if not (100000000 < uid < 200000000):
+ await message.reply_text("此功能暂时只支持国服")
+ return
+ data = await self._load_history(uid)
+ if data is None or len(data.AvatarList) == 0:
+ if isinstance(self.kitsune, str):
+ photo = self.kitsune
+ else:
+ photo = open("resources/img/aaa.jpg", "rb")
+ buttons = [
+ [
+ InlineKeyboardButton(
+ "更新面板",
+ callback_data=f"update_player_card|{user.id}|{uid}",
+ )
+ ]
+ ]
+ reply_message = await message.reply_photo(
+ photo=photo,
+ caption=f"角色列表未找到,请尝试点击下方按钮更新角色列表 - UID {uid}",
+ reply_markup=InlineKeyboardMarkup(buttons),
+ )
+ if reply_message.photo:
+ self.kitsune = reply_message.photo[-1].file_id
+ return
+ if ch_name is not None:
+ logger.info(
+ "用户 %s[%s] 角色卡片查询命令请求 || character_name[%s] uid[%s]",
+ user.full_name,
+ user.id,
+ ch_name,
+ uid,
+ )
+ else:
+ logger.info("用户 %s[%s] 角色卡片查询命令请求", user.full_name, user.id)
+ ttl = await self.cache.ttl(uid)
+
+ buttons = self.gen_button(data, user.id, uid, update_button=ttl < 0)
+ if isinstance(self.kitsune, str):
+ photo = self.kitsune
+ else:
+ photo = open("resources/img/aaa.jpg", "rb")
+ reply_message = await message.reply_photo(
+ photo=photo,
+ caption=f"请选择你要查询的角色 - UID {uid}",
+ reply_markup=InlineKeyboardMarkup(buttons),
+ )
+ if reply_message.photo:
+ self.kitsune = reply_message.photo[-1].file_id
+ return
+ for characters in data.AvatarList:
+ if idToRole(characters.AvatarID) == ch_name:
+ break
+ else:
+ await message.reply_text(f"角色展柜中未找到 {ch_name} ,请检查角色是否存在于角色展柜中,或者等待角色数据更新后重试")
+ return
+ if characters.AvatarID in {8001, 8002, 8003, 8004}:
+ await message.reply_text(f"暂不支持查询 {ch_name} 的角色卡片")
+ return
+ await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
+ render_result = await RenderTemplate(
+ uid,
+ characters,
+ self.template_service,
+ self.assets_service,
+ self.wiki_service,
+ self.client,
+ ).render() # pylint: disable=W0631
+ await render_result.reply_photo(
+ message,
+ filename=f"player_card_{uid}_{ch_name}.png",
+ )
+
+ @handler.callback_query(pattern=r"^update_player_card\|", block=False)
+ async def update_player_card(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
+ user = update.effective_user
+ message = update.effective_message
+ callback_query = update.callback_query
+
+ async def get_player_card_callback(callback_query_data: str) -> Tuple[int, int]:
+ _data = callback_query_data.split("|")
+ _user_id = int(_data[1])
+ _uid = int(_data[2])
+ logger.debug("callback_query_data函数返回 user_id[%s] uid[%s]", _user_id, _uid)
+ return _user_id, _uid
+
+ user_id, uid = await get_player_card_callback(callback_query.data)
+ if user.id != user_id:
+ await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True)
+ return
+
+ ttl = await self.cache.ttl(uid)
+
+ if ttl > 0:
+ await callback_query.answer(text=f"请等待 {ttl} 秒后再更新", show_alert=True)
+ return
+
+ await message.reply_chat_action(ChatAction.TYPING)
+ await callback_query.answer(text="正在获取角色列表 请不要重复点击按钮")
+ data = await self.client.update_data(str(uid))
+ if isinstance(data, str):
+ await callback_query.answer(text=data, show_alert=True)
+ return
+ if data.AvatarList is None:
+ await message.delete()
+ await callback_query.answer("请先将角色加入到角色展柜并允许查看角色详情后再使用此功能,如果已经添加了角色,请等待角色数据更新后重试", show_alert=True)
+ return
+ buttons = self.gen_button(data, user.id, uid, update_button=False)
+ render_data = await self.parse_holder_data(data)
+ holder = await self.template_service.render(
+ "starrail/player_card/holder.html",
+ render_data,
+ viewport={"width": 750, "height": 380},
+ ttl=60 * 10,
+ caption=f"更新角色列表成功,请选择你要查询的角色 - UID {uid}",
+ )
+ await holder.edit_media(message, reply_markup=InlineKeyboardMarkup(buttons))
+
+ @handler.callback_query(pattern=r"^get_player_card\|", block=False)
+ async def get_player_cards(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
+ callback_query = update.callback_query
+ user = callback_query.from_user
+ message = callback_query.message
+
+ async def get_player_card_callback(
+ callback_query_data: str,
+ ) -> Tuple[str, int, int]:
+ _data = callback_query_data.split("|")
+ _user_id = int(_data[1])
+ _uid = int(_data[2])
+ _result = _data[3]
+ logger.debug(
+ "callback_query_data函数返回 result[%s] user_id[%s] uid[%s]",
+ _result,
+ _user_id,
+ _uid,
+ )
+ return _result, _user_id, _uid
+
+ result, user_id, uid = await get_player_card_callback(callback_query.data)
+ if user.id != user_id:
+ await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True)
+ return
+ if result == "empty_data":
+ await callback_query.answer(text="此按钮不可用", show_alert=True)
+ return
+ page = 0
+ if result.isdigit():
+ page = int(result)
+ logger.info(
+ "用户 %s[%s] 角色卡片查询命令请求 || page[%s] uid[%s]",
+ user.full_name,
+ user.id,
+ page,
+ uid,
+ )
+ else:
+ logger.info(
+ "用户 %s[%s] 角色卡片查询命令请求 || character_name[%s] uid[%s]",
+ user.full_name,
+ user.id,
+ result,
+ uid,
+ )
+ data = await self._load_history(uid)
+ if isinstance(data, str):
+ await message.reply_text(data)
+ return
+ if data.AvatarList is None:
+ await message.delete()
+ await callback_query.answer("请先将角色加入到角色展柜并允许查看角色详情后再使用此功能,如果已经添加了角色,请等待角色数据更新后重试", show_alert=True)
+ return
+ if page:
+ buttons = self.gen_button(data, user.id, uid, page, await self.cache.ttl(uid) <= 0)
+ await message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(buttons))
+ await callback_query.answer(f"已切换到第 {page} 页", show_alert=False)
+ return
+ for characters in data.AvatarList:
+ if idToRole(characters.AvatarID) == result:
+ break
+ else:
+ await message.delete()
+ await callback_query.answer(f"角色展柜中未找到 {result} ,请检查角色是否存在于角色展柜中,或者等待角色数据更新后重试", show_alert=True)
+ return
+ if characters.AvatarID in {8001, 8002, 8003, 8004}:
+ await callback_query.answer(f"暂不支持查询 {result} 的角色卡片")
+ return
+ await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False)
+ await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
+ render_result = await RenderTemplate(
+ uid, characters, self.template_service, self.assets_service, self.wiki_service, self.client
+ ).render() # pylint: disable=W0631
+ render_result.filename = f"player_card_{uid}_{result}.png"
+ await render_result.edit_media(message)
+
+ @staticmethod
+ def gen_button(
+ data: PlayerInfo,
+ user_id: Union[str, int],
+ uid: int,
+ page: int = 1,
+ update_button: bool = True,
+ ) -> List[List[InlineKeyboardButton]]:
+ """生成按钮"""
+ buttons = []
+
+ if data.AvatarList:
+ buttons = [
+ InlineKeyboardButton(
+ idToRole(value.AvatarID),
+ callback_data=f"get_player_card|{user_id}|{uid}|{idToRole(value.AvatarID)}",
+ )
+ for value in data.AvatarList
+ if value.AvatarID
+ ]
+ all_buttons = [buttons[i : i + 4] for i in range(0, len(buttons), 4)]
+ send_buttons = all_buttons[(page - 1) * 3 : page * 3]
+ last_page = page - 1 if page > 1 else 0
+ all_page = math.ceil(len(all_buttons) / 3)
+ next_page = page + 1 if page < all_page and all_page > 1 else 0
+ last_button = []
+ if last_page:
+ last_button.append(
+ InlineKeyboardButton(
+ "<< 上一页",
+ callback_data=f"get_player_card|{user_id}|{uid}|{last_page}",
+ )
+ )
+ if last_page or next_page:
+ last_button.append(
+ InlineKeyboardButton(
+ f"{page}/{all_page}",
+ callback_data=f"get_player_card|{user_id}|{uid}|empty_data",
+ )
+ )
+ if update_button:
+ last_button.append(
+ InlineKeyboardButton(
+ "更新面板",
+ callback_data=f"update_player_card|{user_id}|{uid}",
+ )
+ )
+ if next_page:
+ last_button.append(
+ InlineKeyboardButton(
+ "下一页 >>",
+ callback_data=f"get_player_card|{user_id}|{uid}|{next_page}",
+ )
+ )
+ if last_button:
+ send_buttons.append(last_button)
+ return send_buttons
+
+ async def parse_holder_data(self, data: PlayerInfo) -> dict:
+ """
+ 生成渲染所需数据
+ """
+ characters_data = []
+ for idx, character in enumerate(data.AvatarList):
+ cid = 8004 if character.AvatarID in {8001, 8002, 8003, 8004} else character.AvatarID
+ try:
+ characters_data.append(
+ {
+ "level": character.Level,
+ "constellation": character.Rank,
+ "icon": self.assets_service.avatar.square(cid).as_uri(),
+ }
+ )
+ except AssetsCouldNotFound:
+ logger.warning("角色 %s 的头像资源获取失败", cid)
+ if idx > 6:
+ break
+ return {
+ "uid": data.UID,
+ "level": data.Level or 0,
+ "signature": data.Signature or "",
+ "characters": characters_data,
+ }
+
+
+class Artifact(BaseModel):
+ equipment: Dict = {}
+ # 圣遗物评分
+ score: float = 0
+ # 圣遗物评级
+ score_label: str = "E"
+ # 圣遗物评级颜色
+ score_class: str = ""
+ # 圣遗物单行属性评分
+ substat_scores: List[float]
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ for substat_scores in self.substat_scores:
+ self.score += substat_scores
+ self.score = round(self.score, 1)
+
+ for r in (
+ ("D", 10.0),
+ ("C", 16.5),
+ ("B", 23.1),
+ ("A", 29.7),
+ ("S", 36.3),
+ ("SS", 42.9),
+ ("SSS", 49.5),
+ ("ACE", 56.1),
+ ("ACE²", 66.0),
+ ):
+ if self.score >= r[1]:
+ self.score_label = r[0]
+ self.score_class = self.get_score_class(r[0])
+
+ @staticmethod
+ def get_score_class(label: str) -> str:
+ mapping = {
+ "D": "text-neutral-400",
+ "C": "text-neutral-200",
+ "B": "text-violet-400",
+ "A": "text-violet-400",
+ "S": "text-yellow-400",
+ "SS": "text-yellow-400",
+ "SSS": "text-yellow-400",
+ "ACE": "text-red-500",
+ "ACE²": "text-red-500",
+ }
+ return mapping.get(label, "text-neutral-400")
+
+
+class RenderTemplate:
+ def __init__(
+ self,
+ uid: Union[int, str],
+ character: Avatar,
+ template_service: TemplateService,
+ assets_service: AssetsService,
+ wiki_service: WikiService,
+ client: PlayerCardsClient,
+ ):
+ self.uid = uid
+ self.template_service = template_service
+ self.character = character
+ self.assets_service = assets_service
+ self.wiki_service = wiki_service
+ self.client = client
+
+ async def render(self):
+ images = await self.cache_images()
+
+ artifacts = self.find_artifacts()
+ artifact_total_score: float = sum(artifact.score for artifact in artifacts)
+ artifact_total_score = round(artifact_total_score, 1)
+
+ artifact_total_score_label: str = "E"
+ for r in (
+ ("D", 10.0),
+ ("C", 16.5),
+ ("B", 23.1),
+ ("A", 29.7),
+ ("S", 36.3),
+ ("SS", 42.9),
+ ("SSS", 49.5),
+ ("ACE", 56.1),
+ ("ACE²", 66.0),
+ ):
+ if artifact_total_score / 5 >= r[1]:
+ artifact_total_score_label = r[0]
+
+ weapon = None
+ weapon_detail = None
+ if self.character.EquipmentID and self.character.EquipmentID.ID:
+ weapon = self.character.EquipmentID
+ weapon_detail = self.wiki_service.light_cone.get_by_id(self.character.EquipmentID.ID)
+ skills = [0, 0, 0, 0, 0]
+ for index in range(5):
+ skills[index] = self.character.BehaviorList[index].Level
+ data = {
+ "uid": self.uid,
+ "character": self.character,
+ "character_detail": self.wiki_service.character.get_by_id(self.character.AvatarID),
+ "weapon": weapon,
+ "weapon_detail": weapon_detail,
+ # 圣遗物评分
+ "artifact_total_score": artifact_total_score,
+ # 圣遗物评级
+ "artifact_total_score_label": artifact_total_score_label,
+ # 圣遗物评级颜色
+ "artifact_total_score_class": Artifact.get_score_class(artifact_total_score_label),
+ "artifacts": artifacts,
+ "skills": skills,
+ "images": images,
+ }
+
+ return await self.template_service.render(
+ "starrail/player_card/player_card.html",
+ data,
+ {"width": 1000, "height": 1200},
+ full_page=True,
+ query_selector=".text-neutral-200",
+ ttl=7 * 24 * 60 * 60,
+ )
+
+ async def cache_images(self):
+ c = self.character
+ cid = c.AvatarID
+ data = {
+ "banner_url": self.assets_service.avatar.gacha(cid).as_uri(),
+ "skills": [i.as_uri() for i in self.assets_service.avatar.skills(cid)][:-1],
+ "constellations": [i.as_uri() for i in self.assets_service.avatar.eidolons(cid)],
+ "equipment": "",
+ }
+ if c.EquipmentID and c.EquipmentID.ID:
+ data["equipment"] = self.assets_service.light_cone.icon(c.EquipmentID.ID).as_uri()
+ return data
+
+ def find_artifacts(self) -> List[Artifact]:
+ """在 equipments 数组中找到圣遗物,并转换成带有分数的 model。equipments 数组包含圣遗物和武器"""
+
+ stats = ArtifactStatsTheory(idToRole(self.character.AvatarID))
+
+ def substat_score(s: EquipmentsStats) -> float:
+ return stats.theory(s)
+
+ def fix_equipment(e: Relic) -> Dict:
+ rid = e.ID
+ affix = self.client.get_affix_by_id(rid)
+ relic_set = self.wiki_service.relic.get_by_id(affix.set_id)
+ try:
+ icon = relic_set.image_list[affix.type.num]
+ except IndexError:
+ icon = relic_set.icon
+ return {
+ "id": rid,
+ "name": relic_set.name,
+ "icon": icon,
+ "level": e.Level,
+ "rank": affix.rarity,
+ "main_sub": self.client.get_affix(e, True, False)[0],
+ "sub": self.client.get_affix(e, False, True),
+ }
+
+ relic_list = self.character.RelicList or []
+ return [
+ Artifact(
+ equipment=fix_equipment(e),
+ # 圣遗物单行属性评分
+ substat_scores=[substat_score(s) for s in self.client.get_affix(e, False)],
+ )
+ for e in relic_list
+ ]
diff --git a/resources/bot/help/help.html b/resources/bot/help/help.html
index 543b8a7..ccb0ded 100644
--- a/resources/bot/help/help.html
+++ b/resources/bot/help/help.html
@@ -53,13 +53,13 @@
Pix+9KyS`37n!}*`H0D
zs}2lWV)v=`zc (8v}oNc-3m$uL0?Hvh*8Z;SraCwmAPLV#lwR6`_cymPr`G0%`tTNB#yIWI=Y;l
zn*RU}j$cC^w@}qn!&gl$JydL#30he8JX|;(!3yCmAbg~f>wO?_O#CNMYH2ggfynDI
z=T}n`vtRo?9qv9nis(%}(1%PwAc6r0l5~>;YX#!0djyS kc1;Rn;_R
zd{LJ%ziHO)k9n~7u9E?v7*+{GLI`oEg|WHA>o^Tv?>9d5r0PEsk;U=1301?xxU4)h
z(=8=LFU^JA00|qyPqX`)3+f9_{vn6@O5;2xk}5}IvNjk2hzhEB-7q5HhdJXha5`(Q
zi{5=N`qeiHRbo)qOC2Q~W%U%X&gyeg(_xfK&ht+pZSK7P0Dw5t(asuvmxqQ0Rhzw2
zG=8)%)Xl54Jje^Tr qXE(CbD
zotF`<4AxzwhYOBH6GoTi)Te6$vVcwZ*N{#e!F@^A!=;iKu5()6Dn{#YFJ%}Zp2g0*
z`XkQU=)r4j3PZ5latH%kT_W9qAYYn@I0N1{9tp+IJr0^BL$YQYf-W>R2(Myu4?R_2
za`#4a(5`zAIXJGU9SoH1eZBzw%b^>k)cbelm+Tq+tMg0t
z38SiXf>&JDVlg_PDEyOIF@SYZNwS!raJpy=Y(#p+bsZnoF72crvKF=OZnAKb>s7V2
z+pxQ_i2lgj{8ifew$akQQZqYdH|;p=7gVC{L4GrN#oETZe&kb+T(4~_e^U*gDe8GO
zR)zhZ7m2WYF+J@E&1m5irka=OP3Ms3koc7Z*i+p!vBxlN9PZC6Nbg^uJqi@#dT(Sk
zD5s|OUyQwmGcF%YKhd=Om6sbKZdlkF-2+8{QAs72GxeRE?{{F#?G-r=s!3$Jtmj;<
zB8Xr=LV$0s$
fqAx-WiFtiR8OKNA;N1Y`B@mYMnD6dm&?86Bt_iW_Pj$_iBTPxX*_tq>`QV-=-q5
zE@xsL6~|XUPk6ee!Wc#^CMyOlMGxpqDqtj%nbj_+bx0#ZL$qnNh4dgTCF@1GD*Auu
zlBdb&tIUc0D??H)=Nx|%F|@aH03oFR0I`UldhPhOx%h7lVl^<5Cu~KK+xbU#S^n`0
zhQ%bGhGfAdrly9b8cKIlLf6PzM8&NE)&Q4xL&l$JMg>W~&&~%XdV1n`;~m7AisAS-
z4NMLrHhCos$V*z$pgHsx)b-QFbF#aOIEVbEqDoq4eMs+4%XHH3Bh0N|M|cEO96^an
z!r28ZOsCEnVcI{w@HyN!v)A^K7)BQ-y$e