Support starrail player_card

This commit is contained in:
omg-xtao 2023-05-11 23:09:39 +08:00 committed by GitHub
parent 5616f0637c
commit ea8e4eae1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1770 additions and 667 deletions

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

@ -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)) + "%")
)

View File

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

View File

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

View File

@ -1,411 +1,125 @@
{
"旅行者": [
"丹恒": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率",
"元素精通"
"暴击率百分比",
"暴击伤害百分比",
"风属性伤害提高百分比"
],
"安柏": [
"彦卿": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通"
"暴击率百分比",
"暴击伤害百分比",
"冰属性伤害提高百分比"
],
"凯亚": [
"希儿": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
"暴击率百分比",
"暴击伤害百分比",
"量子属性伤害提高百分比",
"速度"
],
"丽莎": [
"姬子": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通"
"暴击率百分比",
"暴击伤害百分比",
"火属性伤害提高百分比"
],
"芭芭拉": [
"生命值百分比",
"元素充能效率",
"元素精通"
],
"芭芭拉-核爆": [
"阿兰": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通"
"暴击率百分比",
"暴击伤害百分比",
"雷属性伤害提高百分比"
],
"雷泽": [
"黑塔": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"物理伤害加成"
"暴击率百分比",
"暴击伤害百分比",
"冰属性伤害提高百分比"
],
"香菱": [
"希露瓦": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
"暴击率百分比",
"暴击伤害百分比",
"雷属性伤害提高百分比"
],
"北斗": [
"克拉拉": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
"暴击率百分比",
"暴击伤害百分比",
"物理属性伤害提高百分比"
],
"行秋": [
"虎克": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
"暴击率百分比",
"暴击伤害百分比",
"火属性伤害提高百分比"
],
"凝光": [
"青雀": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
"暴击率百分比",
"暴击伤害百分比",
"量子属性伤害提高百分比"
],
"菲谢尔": [
"素裳": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通"
"暴击率百分比",
"暴击伤害百分比",
"物理属性伤害提高百分比"
],
"班尼特": [
"生命值百分比",
"元素充能效率",
"治疗加成"
],
"诺艾尔": [
"攻击力百分比",
"杰帕德": [
"防御力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
"防御力",
"能量恢复效率百分比"
],
"重云": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"砂糖": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"琴": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"物理伤害加成",
"元素充能效率",
"治疗加成"
],
"迪卢克": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通"
],
"七七": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"物理伤害加成",
"元素充能效率",
"治疗加成"
],
"莫娜": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"刻晴": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"物理伤害加成"
],
"温迪": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"可莉": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"迪奥娜": [
"生命值百分比",
"元素充能效率",
"治疗加成"
],
"达达利亚": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"辛焱": [
"三月七": [
"防御力百分比",
"攻击力百分比",
"暴击率",
"暴击伤害",
"物理伤害加成"
"防御力",
"能量恢复效率百分比"
],
"钟离": [
"瓦尔特": [
"虚数属性伤害提高百分比",
"速度",
"能量恢复效率百分比",
"效果命中百分比"
],
"桑博": [
"速度",
"能量恢复效率百分比",
"效果命中百分比",
"风属性伤害提高百分比"
],
"佩拉": [
"冰属性伤害提高百分比",
"速度",
"能量恢复效率百分比",
"效果命中百分比"
],
"停云": [
"攻击力百分比",
"速度",
"能量恢复效率百分比",
"雷属性伤害提高百分比"
],
"艾丝妲": [
"攻击力百分比",
"火属性伤害提高百分比",
"速度",
"能量恢复效率百分比"
],
"白露": [
"生命值百分比",
"攻击力百分比",
"暴击率",
"暴击伤害",
"物理伤害加成",
"元素充能效率"
"生命值",
"治疗量加成百分比"
],
"钟离-安如磐石": [
"娜塔莎": [
"生命值百分比",
"暴击率"
"生命值",
"治疗量加成百分比"
],
"阿贝多": [
"防御力百分比",
"暴击率",
"暴击伤害"
],
"甘雨": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通"
],
"甘雨-永冻": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
],
"魈": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
],
"胡桃": [
"生命值百分比",
"暴击率",
"暴击伤害",
"元素精通"
],
"罗莎莉亚": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"物理伤害加成",
"元素充能效率"
],
"烟绯": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"优菈": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"物理伤害加成",
"元素充能效率"
],
"枫原万叶": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"神里绫华": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
],
"早柚": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率",
"治疗加成"
],
"宵宫": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通"
],
"埃洛伊": [
"攻击力百分比",
"暴击率",
"暴击伤害"
],
"九条裟罗": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
],
"雷电将军": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率",
"元素精通"
],
"珊瑚宫心海": [
"生命值百分比",
"攻击力百分比",
"元素充能效率",
"治疗加成",
"元素精通"
],
"托马": [
"生命值百分比",
"暴击率",
"元素充能效率",
"元素精通"
],
"五郎": [
"防御力百分比",
"暴击率",
"元素充能效率"
],
"荒泷一斗": [
"防御力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
],
"云堇": [
"防御力百分比",
"暴击率",
"元素充能效率"
],
"申鹤": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
],
"八重神子": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"神里绫人": [
"生命值百分比",
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"夜兰": [
"生命值百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"久岐忍": [
"生命值百分比",
"元素精通",
"治疗加成"
],
"鹿野院平藏": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
],
"提纳里": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"柯莱": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"赛诺": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"妮露": [
"生命值百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"纳西妲": [
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率",
"攻击力百分比"
],
"流浪者": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
],
"珐露珊": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素充能效率"
],
"艾尔海森": [
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
],
"瑶瑶": [
"生命值百分比",
"元素充能效率"
],
"迪希雅": [
"生命值百分比",
"攻击力百分比",
"暴击率",
"暴击伤害",
"元素精通",
"元素充能效率"
"布洛妮娅": [
"暴击伤害百分比",
"速度",
"能量恢复效率百分比",
"风属性伤害提高百分比"
]
}

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ from enum import Enum
class Quality(str, Enum):
"""星级"""
Five = "五星"
Four = "四星"
Three = "三星"
@ -12,6 +13,7 @@ class Quality(str, Enum):
class Destiny(str, Enum):
"""命途"""
HuiMie = "毁灭"
ZhiShi = "智识"
XunLie = "巡猎"
@ -23,6 +25,7 @@ class Destiny(str, Enum):
class Element(str, Enum):
"""属性"""
Physical = "物理"
Pyro = ""
Anemo = ""
@ -36,6 +39,7 @@ class Element(str, Enum):
class MonsterType(str, Enum):
"""怪物种类"""
Normal = "普通"
Elite = "精英"
Leader = "首领"
@ -44,6 +48,7 @@ class MonsterType(str, Enum):
class Area(str, Enum):
"""地区"""
Herta = "空间站「黑塔」"
YaLiLuo = "雅利洛-VI"
LuoFu = "仙舟「罗浮」"
@ -52,6 +57,7 @@ class Area(str, Enum):
class MaterialType(str, Enum):
"""材料类型"""
AvatarUpdate = "角色晋阶材料"
XingJi = "行迹材料"
LightConeUpdate = "光锥晋阶材料"
@ -67,6 +73,7 @@ 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)

View File

@ -43,3 +43,7 @@ class LightCone(BaseModel):
"""命途"""
promote: list[LightConePromote]
"""晋阶信息"""
@property
def rarity(self) -> int:
return 5 - list(Quality).index(self.quality)

View File

@ -23,4 +23,3 @@ class Monster(BaseModel):
"""抗性"""
find_area: str
"""发现地点"""

View File

@ -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]
"""套装子图"""

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = []

View File

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

View File

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

View File

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

View File

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

View 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
]

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

View 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 %}

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View 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>

View 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>

View 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>

View File

@ -0,0 +1,3 @@
<div class="rounded-lg bg-black bg-opacity-20">
</div>

View 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;
}

View 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>