✨ Support starrail player_card
11
.github/dependabot.yml
vendored
@ -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"
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
162
modules/apihelper/client/components/player_cards.py
Normal file
@ -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
|
@ -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:
|
||||
|
@ -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)) + "%")
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -1,411 +1,125 @@
|
||||
{
|
||||
"旅行者": [
|
||||
"丹恒": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率",
|
||||
"元素精通"
|
||||
"暴击率百分比",
|
||||
"暴击伤害百分比",
|
||||
"风属性伤害提高百分比"
|
||||
],
|
||||
"安柏": [
|
||||
"彦卿": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
"暴击率百分比",
|
||||
"暴击伤害百分比",
|
||||
"冰属性伤害提高百分比"
|
||||
],
|
||||
"凯亚": [
|
||||
"希儿": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
"暴击率百分比",
|
||||
"暴击伤害百分比",
|
||||
"量子属性伤害提高百分比",
|
||||
"速度"
|
||||
],
|
||||
"丽莎": [
|
||||
"姬子": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
"暴击率百分比",
|
||||
"暴击伤害百分比",
|
||||
"火属性伤害提高百分比"
|
||||
],
|
||||
"芭芭拉": [
|
||||
"生命值百分比",
|
||||
"元素充能效率",
|
||||
"元素精通"
|
||||
],
|
||||
"芭芭拉-核爆": [
|
||||
"阿兰": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
"暴击率百分比",
|
||||
"暴击伤害百分比",
|
||||
"雷属性伤害提高百分比"
|
||||
],
|
||||
"雷泽": [
|
||||
"黑塔": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成"
|
||||
"暴击率百分比",
|
||||
"暴击伤害百分比",
|
||||
"冰属性伤害提高百分比"
|
||||
],
|
||||
"香菱": [
|
||||
"希露瓦": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
"暴击率百分比",
|
||||
"暴击伤害百分比",
|
||||
"雷属性伤害提高百分比"
|
||||
],
|
||||
"北斗": [
|
||||
"克拉拉": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
"暴击率百分比",
|
||||
"暴击伤害百分比",
|
||||
"物理属性伤害提高百分比"
|
||||
],
|
||||
"行秋": [
|
||||
"虎克": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
"暴击率百分比",
|
||||
"暴击伤害百分比",
|
||||
"火属性伤害提高百分比"
|
||||
],
|
||||
"凝光": [
|
||||
"青雀": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
"暴击率百分比",
|
||||
"暴击伤害百分比",
|
||||
"量子属性伤害提高百分比"
|
||||
],
|
||||
"菲谢尔": [
|
||||
"素裳": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
"暴击率百分比",
|
||||
"暴击伤害百分比",
|
||||
"物理属性伤害提高百分比"
|
||||
],
|
||||
"班尼特": [
|
||||
"生命值百分比",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"诺艾尔": [
|
||||
"攻击力百分比",
|
||||
"杰帕德": [
|
||||
"防御力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
"防御力",
|
||||
"能量恢复效率百分比"
|
||||
],
|
||||
"重云": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"砂糖": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"琴": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"迪卢克": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
],
|
||||
"七七": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"莫娜": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"刻晴": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"物理伤害加成"
|
||||
],
|
||||
"温迪": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"可莉": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"迪奥娜": [
|
||||
"生命值百分比",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"达达利亚": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"辛焱": [
|
||||
"三月七": [
|
||||
"防御力百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成"
|
||||
"防御力",
|
||||
"能量恢复效率百分比"
|
||||
],
|
||||
"钟离": [
|
||||
"瓦尔特": [
|
||||
"虚数属性伤害提高百分比",
|
||||
"速度",
|
||||
"能量恢复效率百分比",
|
||||
"效果命中百分比"
|
||||
],
|
||||
"桑博": [
|
||||
"速度",
|
||||
"能量恢复效率百分比",
|
||||
"效果命中百分比",
|
||||
"风属性伤害提高百分比"
|
||||
],
|
||||
"佩拉": [
|
||||
"冰属性伤害提高百分比",
|
||||
"速度",
|
||||
"能量恢复效率百分比",
|
||||
"效果命中百分比"
|
||||
],
|
||||
"停云": [
|
||||
"攻击力百分比",
|
||||
"速度",
|
||||
"能量恢复效率百分比",
|
||||
"雷属性伤害提高百分比"
|
||||
],
|
||||
"艾丝妲": [
|
||||
"攻击力百分比",
|
||||
"火属性伤害提高百分比",
|
||||
"速度",
|
||||
"能量恢复效率百分比"
|
||||
],
|
||||
"白露": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成",
|
||||
"元素充能效率"
|
||||
"生命值",
|
||||
"治疗量加成百分比"
|
||||
],
|
||||
"钟离-安如磐石": [
|
||||
"娜塔莎": [
|
||||
"生命值百分比",
|
||||
"暴击率"
|
||||
"生命值",
|
||||
"治疗量加成百分比"
|
||||
],
|
||||
"阿贝多": [
|
||||
"防御力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害"
|
||||
],
|
||||
"甘雨": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
],
|
||||
"甘雨-永冻": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"魈": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"胡桃": [
|
||||
"生命值百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
],
|
||||
"罗莎莉亚": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成",
|
||||
"元素充能效率"
|
||||
],
|
||||
"烟绯": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"优菈": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成",
|
||||
"元素充能效率"
|
||||
],
|
||||
"枫原万叶": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"神里绫华": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"早柚": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"宵宫": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
],
|
||||
"埃洛伊": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害"
|
||||
],
|
||||
"九条裟罗": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"雷电将军": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率",
|
||||
"元素精通"
|
||||
],
|
||||
"珊瑚宫心海": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"元素充能效率",
|
||||
"治疗加成",
|
||||
"元素精通"
|
||||
],
|
||||
"托马": [
|
||||
"生命值百分比",
|
||||
"暴击率",
|
||||
"元素充能效率",
|
||||
"元素精通"
|
||||
],
|
||||
"五郎": [
|
||||
"防御力百分比",
|
||||
"暴击率",
|
||||
"元素充能效率"
|
||||
],
|
||||
"荒泷一斗": [
|
||||
"防御力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"云堇": [
|
||||
"防御力百分比",
|
||||
"暴击率",
|
||||
"元素充能效率"
|
||||
],
|
||||
"申鹤": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"八重神子": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"神里绫人": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"夜兰": [
|
||||
"生命值百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"久岐忍": [
|
||||
"生命值百分比",
|
||||
"元素精通",
|
||||
"治疗加成"
|
||||
],
|
||||
"鹿野院平藏": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"提纳里": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"柯莱": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"赛诺": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"妮露": [
|
||||
"生命值百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"纳西妲": [
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率",
|
||||
"攻击力百分比"
|
||||
],
|
||||
"流浪者": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"珐露珊": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"艾尔海森": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"瑶瑶": [
|
||||
"生命值百分比",
|
||||
"元素充能效率"
|
||||
],
|
||||
"迪希雅": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
"布洛妮娅": [
|
||||
"暴击伤害百分比",
|
||||
"速度",
|
||||
"能量恢复效率百分比",
|
||||
"风属性伤害提高百分比"
|
||||
]
|
||||
}
|
@ -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())
|
||||
|
@ -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())
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
@ -43,3 +43,7 @@ class LightCone(BaseModel):
|
||||
"""命途"""
|
||||
promote: list[LightConePromote]
|
||||
"""晋阶信息"""
|
||||
|
||||
@property
|
||||
def rarity(self) -> int:
|
||||
return 5 - list(Quality).index(self.quality)
|
||||
|
@ -23,4 +23,3 @@ class Monster(BaseModel):
|
||||
"""抗性"""
|
||||
find_area: str
|
||||
"""发现地点"""
|
||||
|
||||
|
@ -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]
|
||||
"""套装子图"""
|
||||
|
53
modules/wiki/models/relic_affix.py
Normal file
@ -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
|
@ -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())
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 = []
|
||||
|
@ -128,9 +128,7 @@ class ChallengePlugin(Plugin):
|
||||
return
|
||||
|
||||
async def reply_message_func(content: str) -> None:
|
||||
_reply_msg = await message.reply_text(
|
||||
f"开拓者 (<code>{uid}</code>) {content}", parse_mode=ParseMode.HTML
|
||||
)
|
||||
_reply_msg = await message.reply_text(f"开拓者 (<code>{uid}</code>) {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:
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
563
plugins/starrail/player_cards.py
Normal file
@ -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
|
||||
]
|
@ -53,13 +53,13 @@
|
||||
</div>
|
||||
<div class="command-description">玩家统计查询</div>
|
||||
</div>
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /player_card-->
|
||||
<!-- <i class="fa fa-user-circle-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">角色卡片</div>-->
|
||||
<!-- </div>-->
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/player_card
|
||||
<i class="fa fa-user-circle-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">角色卡片</div>
|
||||
</div>
|
||||
<!-- 最高查询类 -->
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
@ -75,13 +75,13 @@
|
||||
</div>
|
||||
<div class="command-description">查询当月开拓月历</div>
|
||||
</div>
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /abyss-->
|
||||
<!-- <i class="fa fa-id-card-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">查询当期深渊战绩</div>-->
|
||||
<!-- </div>-->
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/challenge
|
||||
<i class="fa fa-id-card-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">查询当期混沌回忆战绩</div>
|
||||
</div>
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /abyss_team-->
|
||||
|
BIN
resources/img/aaa.jpg
Normal file
After Width: | Height: | Size: 452 KiB |
43
resources/starrail/player_card/artifacts.html
Normal file
@ -0,0 +1,43 @@
|
||||
{% for item in artifacts %}
|
||||
<div class="bg-black bg-opacity-20 rounded-lg">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="relative">
|
||||
<img class="w-24 h-24" src="{{ item.equipment.icon }}" alt=""/>
|
||||
<div
|
||||
class="absolute bottom-3 right-3 px-1 text-base italic bg-black bg-opacity-50 rounded"
|
||||
>
|
||||
+{{ item.equipment.level }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="font-medium">{{ item.equipment.name }}</div>
|
||||
<div class="flex text-sm space-x-2">
|
||||
<div>{{ item.equipment.main_sub.name }}</div>
|
||||
<div class="italic">
|
||||
{{ item.equipment.main_sub.value }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-base {{ item.score_class }}">
|
||||
<span class="italic"> {{ item.score }} </span> 分 - {{ item.score_label
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{% for stat in item.equipment.sub %}
|
||||
<div class="px-4 py-1 odd:bg-white odd:bg-opacity-10 flex space-x-4 {% if item.substat_scores[loop.index0] == 0 %} text-neutral-400 {% endif %}">
|
||||
<div class="flex-1 truncate">
|
||||
{{ stat.name }}
|
||||
</div>
|
||||
<div class="min-w-30 italic text-right">
|
||||
+{{ stat.value }}
|
||||
</div>
|
||||
<div class="min-w-30 text-right">
|
||||
<span class="italic">{{ item.substat_scores[loop.index0] }}</span>
|
||||
<span class="text-sm">分</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
13
resources/starrail/player_card/constellations.html
Normal file
@ -0,0 +1,13 @@
|
||||
<div class="flex-1 flex items-end justify-center">
|
||||
<div class="flex pb-2">
|
||||
{% for item in images.constellations %}
|
||||
<div
|
||||
class="w-16 h-16 flex items-center justify-center bg-contain bg-no-repeat bg-center
|
||||
{%- if loop.index > character.Rank %} grayscale opacity-75 {% endif %}"
|
||||
style="background-image: url('img/talent-{{ character_detail.element.name | lower }}.png')"
|
||||
>
|
||||
<img src="{{ item }}" alt="" class="w-8 h-8" />
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
37
resources/starrail/player_card/holder.html
Normal file
@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-ch">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>holder</title>
|
||||
<link type="text/css" href="./style.css" rel="stylesheet"/>
|
||||
<link type="text/css" href="../../styles/public.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="overview">
|
||||
<div class="title">角色展柜</div>
|
||||
<div class="summarize">
|
||||
<div>
|
||||
<div>UID: {{ uid }}</div>
|
||||
<div>开拓等级: {{ level }} 级</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>签名: {{ signature }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="characters">
|
||||
{% for character in characters %}
|
||||
<div class="character">
|
||||
{% if character.constellation > 0 %}
|
||||
{% set bg = ['blue','blue', 'green','green', 'red', 'red'][character.constellation - 1] %}
|
||||
<div style="background-color: var(--{{ bg }})">{{ character.constellation }} 命</div>
|
||||
{% endif %}
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="{{ character.icon }}" alt=""/>
|
||||
</div>
|
||||
<div class="caption">Lv.{{ character.level }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
58
resources/starrail/player_card/holder_example.html
Normal file
@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-ch">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>holder_example</title>
|
||||
<link type="text/css" href="./style.css" rel="stylesheet"/>
|
||||
<link type="text/css" href="../../styles/public.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="overview">
|
||||
<div class="title">角色展柜</div>
|
||||
<div class="summarize">
|
||||
<div>
|
||||
<div>UID: 123456789</div>
|
||||
<div>冒险等阶: 55</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>签名: 貴方の運命は、すでに我が手中の糸が絡めとった!填充</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="characters">
|
||||
<div class="character">
|
||||
<div style="background-color: var(--green)">4命</div>
|
||||
<div class="element" style="background-image: url('../../img/element/Cryo.png')"></div>
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
|
||||
</div>
|
||||
<div class="caption">Lv.90</div>
|
||||
</div>
|
||||
<div class="character">
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character">
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character">
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character">
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character">
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
resources/starrail/player_card/img/bg-anemo.jpg
Normal file
After Width: | Height: | Size: 187 KiB |
BIN
resources/starrail/player_card/img/bg-cryo.jpg
Normal file
After Width: | Height: | Size: 173 KiB |
BIN
resources/starrail/player_card/img/bg-dendro.jpg
Normal file
After Width: | Height: | Size: 202 KiB |
BIN
resources/starrail/player_card/img/bg-electro.jpg
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
resources/starrail/player_card/img/bg-nombre.jpg
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
resources/starrail/player_card/img/bg-physical.jpg
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
resources/starrail/player_card/img/bg-pyro.jpg
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
resources/starrail/player_card/img/bg-quantum.jpg
Normal file
After Width: | Height: | Size: 182 KiB |
BIN
resources/starrail/player_card/img/holder_bg.png
Normal file
After Width: | Height: | Size: 301 KiB |
BIN
resources/starrail/player_card/img/star.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
resources/starrail/player_card/img/talent-anemo.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/starrail/player_card/img/talent-cryo.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/starrail/player_card/img/talent-dendro.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/starrail/player_card/img/talent-electro.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
resources/starrail/player_card/img/talent-nombre.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
resources/starrail/player_card/img/talent-physical.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
resources/starrail/player_card/img/talent-pyro.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
resources/starrail/player_card/img/talent-quantum.png
Normal file
After Width: | Height: | Size: 40 KiB |
108
resources/starrail/player_card/player_card.html
Normal file
@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Title</title>
|
||||
<script src="../../js/tailwindcss-3.1.8.js"></script>
|
||||
<link type="text/css" href="../../styles/public.css" rel="stylesheet" />
|
||||
<style>
|
||||
.text-shadow {
|
||||
text-shadow: 0 0.08em 0.1em #000, 0 0.1em 0.3em rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.star {
|
||||
background-image: url("./img/star.png");
|
||||
height: 1rem;
|
||||
width: 5rem;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.star-1 {
|
||||
background-position-y: 0;
|
||||
}
|
||||
.star-2 {
|
||||
background-position-y: -1rem;
|
||||
}
|
||||
.star-3 {
|
||||
background-position-y: -2rem;
|
||||
}
|
||||
.star-4 {
|
||||
background-position-y: -3rem;
|
||||
}
|
||||
.star-5 {
|
||||
background-position-y: -4rem;
|
||||
}
|
||||
@font-face {
|
||||
font-family: shicon;
|
||||
src: url(../../genshin/player_card/fonts/shicon.woff) format("woff");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
.enka-log {
|
||||
line-height: 0;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
.enka-log:before {
|
||||
content: "\e93a";
|
||||
font-family: shicon;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-xl text-neutral-200">
|
||||
<div
|
||||
class="data bg-no-repeat bg-cover pb-5 min-w-[845px] overflow-hidden"
|
||||
style="background-image: url('img/bg-{{ character_detail.element.name | lower }}.jpg')"
|
||||
>
|
||||
<div class="relative mb-4 overflow-hidden">
|
||||
<!-- Character Background -->
|
||||
<div
|
||||
class="absolute w-full h-full -left-1/4 opacity-80 bg-no-repeat bg-center"
|
||||
style="background-image: url('{{ images.banner_url }}'); background-size: auto 200%;"
|
||||
></div>
|
||||
<div class="relative w-full flex p-5 space-x-8">
|
||||
{% include 'starrail/player_card/constellations.html' %}
|
||||
|
||||
<div class="flex-1 space-y-4">
|
||||
<div class="text-right italic">
|
||||
<div class="characters-name text-5xl font-bold text-shadow mb-2">
|
||||
{{ character_detail.name }}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-row justify-end text-2xl text-shadow space-x-6"
|
||||
>
|
||||
<div>UID {{ uid }}</div>
|
||||
<div>Lv.{{ character.Level }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'starrail/player_card/skills.html' %}
|
||||
<div class="flex flex-col space-y-2">
|
||||
{% if weapon != none %}
|
||||
{% include 'starrail/player_card/weapon.html' %}
|
||||
{% endif %}
|
||||
{% include 'starrail/player_card/score.html' %}
|
||||
</div>
|
||||
{% include 'starrail/player_card/stats.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="px-5 relative">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
{% include 'starrail/player_card/artifacts.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logo -->
|
||||
<div class="mt-4 relative">
|
||||
<div class="text-gray-300 text-center opacity-70 text-lg">
|
||||
Inspired by Miao-Plugin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
12
resources/starrail/player_card/score.html
Normal file
@ -0,0 +1,12 @@
|
||||
<div class="flex-1 flex justify-evenly bg-black bg-opacity-20 rounded-lg">
|
||||
<div class="flex flex-col items-center justify-center space-y-2">
|
||||
<div class="text-5xl italic text-shadow {{ artifact_total_score_class }}">
|
||||
{{ artifact_total_score_label }}
|
||||
</div>
|
||||
<div class="text-base text-neutral-400">遗器评级</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center space-y-2">
|
||||
<div class="text-5xl italic text-shadow">{{ artifact_total_score }}</div>
|
||||
<div class="text-base text-neutral-400">遗器评分</div>
|
||||
</div>
|
||||
</div>
|
17
resources/starrail/player_card/skills.html
Normal file
@ -0,0 +1,17 @@
|
||||
<div class="flex flex-row">
|
||||
{% for item in images.skills %}
|
||||
<div class="mx-auto flex flex-col items-center justify-center">
|
||||
<div
|
||||
class="w-32 h-32 flex items-center justify-center bg-contain bg-no-repeat bg-center"
|
||||
style="background-image: url('img/talent-{{ character_detail.element.name | lower }}.png')"
|
||||
>
|
||||
<img src="{{ item }}" alt="" class="w-16 h-16" />
|
||||
</div>
|
||||
<div
|
||||
class="w-10 -mt-8 text-xl font-medium bg-white text-neutral-800 italic rounded-lg text-center bg-opacity-80"
|
||||
>
|
||||
{{ skills[loop.index0] }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
3
resources/starrail/player_card/stats.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="rounded-lg bg-black bg-opacity-20">
|
||||
|
||||
</div>
|
146
resources/starrail/player_card/style.css
Normal file
@ -0,0 +1,146 @@
|
||||
:root {
|
||||
--white: rgb(246 248 249);
|
||||
--bg-color: rgb(233 229 220);
|
||||
--h-color: rgb(203 189 162);
|
||||
--red: rgb(255 86 33/ 80%);
|
||||
--blue: rgb(98 168 233/ 80%);
|
||||
--green: rgb(67 185 124/ 80%);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: rgb(246 248 249 / 50%);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 750px;
|
||||
position: relative;
|
||||
filter: drop-shadow(2px 2px 5px rgb(0 0 0 /70%));
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 27px;
|
||||
font-weight: bold;
|
||||
color: var(--h-color);
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin: 10px 0;
|
||||
color: var(--h-color);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* 概览 */
|
||||
|
||||
.overview {
|
||||
height: 540px;
|
||||
padding: 20px 30px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-image: linear-gradient(to top, rgb(0 0 0 / 10%), rgb(0 0 0 / 10%)), url("./img/holder_bg.png");
|
||||
background-attachment: local;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.summarize {
|
||||
font-size: 20px;
|
||||
margin: 10px;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
border: 2px solid rgb(118 121 120 / 80%);
|
||||
outline: 4px solid rgb(70, 80, 100);
|
||||
background-color: rgb(70 80 100 / 60%);
|
||||
background-image: url("../abyss/background/banner 01.png"), url("../abyss/background/banner 02.png");
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
background-position: right, left;
|
||||
background-size: auto 100%, auto 100%;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.summarize > div {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
padding: 5px;
|
||||
color: var(--white);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.summarize > div > div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.characters {
|
||||
margin-left: 47px;
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.character {
|
||||
width: 120px;
|
||||
height: 150px;
|
||||
margin: 15px 12px;
|
||||
background-color: rgb(233 229 220);
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.characters > .character > .element {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
overflow: hidden;
|
||||
border-radius: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.character > .caption {
|
||||
font-size: 16px;
|
||||
margin: 4px 0 0;
|
||||
padding: 0;
|
||||
height: min-content;
|
||||
text-align: center;
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
.character > div:first-child:not(.icon, .element) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 3px;
|
||||
min-width: 27px;
|
||||
text-align: center;
|
||||
border-radius: 0 0 0 10px;
|
||||
filter: drop-shadow(1px 1px 5px rgb(0 0 0/50%));
|
||||
font-weight: 500;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.icon > img {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
}
|
17
resources/starrail/player_card/weapon.html
Normal file
@ -0,0 +1,17 @@
|
||||
<div class="flex-1 flex justify-evenly bg-black bg-opacity-20 rounded-lg bg-contain bg-no-repeat">
|
||||
<div class="flex flex-col items-center justify-center space-y-2">
|
||||
<img class="w-24 h-24" src="{{ images.equipment }}" alt=""/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center space-y-2">
|
||||
<div class="text-2xl">{{ weapon_detail.name }}</div>
|
||||
<div class="star star-{{ weapon_detail.rarity }}"></div>
|
||||
<div class="flex space-x-3 items-center">
|
||||
<div class="italic">Lv.{{ weapon.Level }}</div>
|
||||
<div
|
||||
class="rounded px-2 text-base {% if weapon.Rank == 5 %} bg-red-600 {% else %} bg-gray-600 {% endif %}"
|
||||
>
|
||||
精{{ weapon.Rank }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|