✨ 添加角色卡片插件
Co-authored-by: Li Chuangbo <im@chuangbo.li> Co-authored-by: xtaodada <xtao@xtaolink.cn> Co-authored-by: luoshuijs <luoshuijs@outlook.com>
@ -66,3 +66,5 @@ python ./run.py
|
||||
| [西风驿站 猫冬](https://bbs.mihoyo.com/ys/accountCenter/postList?id=74019947) | 本项目攻略图图源 |
|
||||
| [Yunzai-Bot](https://github.com/Le-niao/Yunzai-Bot) | 本项使用的抽卡图片和前端资源来源 |
|
||||
| [Crawler-ghhw](https://github.com/DGP-Studio/Crawler-ghhw) | 本项目参考的爬虫代码 |
|
||||
| [Enka.Network](https://enka.network) | 角色卡片的数据来源 |
|
||||
| [miao-plugin](https://github.com/yoimiya-kokomi/miao-plugin) | 角色卡片的参考项目 |
|
||||
|
68
modules/playercards/fight_prop.py
Normal file
@ -0,0 +1,68 @@
|
||||
import enum
|
||||
|
||||
|
||||
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 = "治疗加成"
|
||||
|
||||
|
||||
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
|
42
modules/playercards/helpers.py
Normal file
@ -0,0 +1,42 @@
|
||||
import os
|
||||
|
||||
import ujson as json
|
||||
from enkanetwork import EquipmentsStats
|
||||
|
||||
from modules.playercards.fight_prop import FightProp, FightPropScore
|
||||
|
||||
_project_path = os.path.dirname(__file__)
|
||||
_fight_prop_rule_file = os.path.join(_project_path, "metadata", "FightPropRule.json")
|
||||
with open(_fight_prop_rule_file, "r", encoding="utf-8") as f:
|
||||
fight_prop_rule_data: dict = json.load(f)
|
||||
|
||||
|
||||
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]
|
||||
if not self.main_prop:
|
||||
self.main_prop = [FightProp.FIGHT_PROP_CRITICAL, FightProp.FIGHT_PROP_CRITICAL_HURT,
|
||||
FightProp.FIGHT_PROP_ATTACK_PERCENT]
|
||||
# 修正要评分的数值词条
|
||||
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
|
||||
return round(score, 1)
|
380
modules/playercards/metadata/FightPropRule.json
Normal file
@ -0,0 +1,380 @@
|
||||
{
|
||||
"旅行者": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"安柏": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"物理伤害加成"
|
||||
],
|
||||
"凯亚": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成",
|
||||
"元素充能效率"
|
||||
],
|
||||
"丽莎": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
],
|
||||
"芭芭拉": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"芭芭拉-核爆": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"雷泽": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成"
|
||||
],
|
||||
"香菱": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"北斗": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"行秋": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"凝光": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"菲谢尔": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"物理伤害加成"
|
||||
],
|
||||
"班尼特": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"诺艾尔": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"重云": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"砂糖": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"琴": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"迪卢克": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
],
|
||||
"七七": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"莫娜": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"刻晴": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"物理伤害加成"
|
||||
],
|
||||
"温迪": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"可莉": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"迪奥娜": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"达达利亚": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"辛焱": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成"
|
||||
],
|
||||
"钟离": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成",
|
||||
"元素充能效率"
|
||||
],
|
||||
"钟离-安如磐石": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"阿贝多": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害"
|
||||
],
|
||||
"甘雨": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
],
|
||||
"甘雨-永冻": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"魈": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"胡桃": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
],
|
||||
"罗莎莉亚": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成",
|
||||
"元素充能效率"
|
||||
],
|
||||
"烟绯": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"优菈": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"物理伤害加成",
|
||||
"元素充能效率"
|
||||
],
|
||||
"枫原万叶": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"神里绫华": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"早柚": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"宵宫": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通"
|
||||
],
|
||||
"埃洛伊": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害"
|
||||
],
|
||||
"九条裟罗": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"雷电将军": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"珊瑚宫心海": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"托马": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"五郎": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"荒泷一斗": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"云堇": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"申鹤": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"八重神子": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"神里绫人": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"夜兰": [
|
||||
"生命值百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素充能效率"
|
||||
],
|
||||
"久岐忍": [
|
||||
"生命值百分比",
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率",
|
||||
"治疗加成"
|
||||
],
|
||||
"鹿野院平藏": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"提纳里": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
],
|
||||
"柯莱": [
|
||||
"攻击力百分比",
|
||||
"暴击率",
|
||||
"暴击伤害",
|
||||
"元素精通",
|
||||
"元素充能效率"
|
||||
]
|
||||
}
|
308
plugins/genshin/player_cards.py
Normal file
@ -0,0 +1,308 @@
|
||||
from typing import Union, List, Any, Tuple
|
||||
|
||||
from enkanetwork import (
|
||||
EnkaNetworkAPI,
|
||||
Equipments,
|
||||
EquipmentsType,
|
||||
EquipmentsStats,
|
||||
Stats,
|
||||
CharacterInfo,
|
||||
Assets,
|
||||
DigitType, EnkaServerError, Forbidden, UIDNotFounded, VaildateUIDError, HTTPException, StatsPercentage, )
|
||||
from pydantic import BaseModel
|
||||
from telegram import Update
|
||||
from telegram.constants import ChatAction
|
||||
from telegram.ext import CommandHandler, filters, CallbackContext, MessageHandler
|
||||
|
||||
from core.baseplugin import BasePlugin
|
||||
from core.plugin import Plugin, handler
|
||||
from core.template import TemplateService
|
||||
from core.user import UserService
|
||||
from core.user.error import UserNotFoundError
|
||||
from modules.playercards.helpers import ArtifactStatsTheory
|
||||
from utils.bot import get_all_args
|
||||
from utils.decorators.error import error_callable
|
||||
from utils.decorators.restricts import restricts
|
||||
from utils.helpers import url_to_file
|
||||
from utils.log import logger
|
||||
from utils.models.base import RegionEnum
|
||||
|
||||
assets = Assets(lang="chs")
|
||||
|
||||
|
||||
class PlayerCards(Plugin, BasePlugin):
|
||||
def __init__(self, user_service: UserService = None, template_service: TemplateService = None):
|
||||
self.user_service = user_service
|
||||
self.client = EnkaNetworkAPI(lang="chs")
|
||||
self.template_service = template_service
|
||||
|
||||
@handler(CommandHandler, command="player_card", block=False)
|
||||
@handler(MessageHandler, filters=filters.Regex("^角色卡片查询(.*)"), block=False)
|
||||
@restricts()
|
||||
@error_callable
|
||||
async def player_cards(self, update: Update, context: CallbackContext) -> None:
|
||||
user = update.effective_user
|
||||
message = update.effective_message
|
||||
args = get_all_args(context)
|
||||
await message.reply_chat_action(ChatAction.TYPING)
|
||||
try:
|
||||
user_info = await self.user_service.get_user_by_id(user.id)
|
||||
if user_info.region == RegionEnum.HYPERION:
|
||||
uid = user_info.yuanshen_uid
|
||||
else:
|
||||
uid = user_info.genshin_uid
|
||||
except UserNotFoundError:
|
||||
reply_message = await message.reply_text("未查询到账号信息,请先私聊派蒙绑定账号")
|
||||
if filters.ChatType.GROUPS.filter(message):
|
||||
self._add_delete_message_job(
|
||||
context, reply_message.chat_id, reply_message.message_id, 30
|
||||
)
|
||||
self._add_delete_message_job(
|
||||
context, message.chat_id, message.message_id, 30
|
||||
)
|
||||
return
|
||||
if len(args) == 1:
|
||||
character_name = args[0]
|
||||
else:
|
||||
reply_message = await message.reply_text("请回复角色名参数")
|
||||
if filters.ChatType.GROUPS.filter(reply_message):
|
||||
self._add_delete_message_job(context, message.chat_id, message.message_id)
|
||||
self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id)
|
||||
return
|
||||
logger.info(f"用户 {user.full_name}[{user.id}] 角色卡片查询命令请求 || character_name[{character_name}] uid[{uid}]")
|
||||
try:
|
||||
data = await self.client.fetch_user(uid)
|
||||
except EnkaServerError:
|
||||
await message.reply_text("Enka.Network 服务请求错误,请稍后重试")
|
||||
return
|
||||
except Forbidden:
|
||||
await message.reply_text("Enka.Network 服务请求被拒绝,请稍后重试")
|
||||
return
|
||||
except HTTPException:
|
||||
await message.reply_text("Enka.Network HTTP 服务请求错误,请稍后重试")
|
||||
return
|
||||
except UIDNotFounded:
|
||||
await message.reply_text("UID 未找到")
|
||||
return
|
||||
except VaildateUIDError:
|
||||
await message.reply_text("UID 错误或者非法")
|
||||
return
|
||||
if len(data.characters) == 0:
|
||||
await message.reply_text("请先将角色加入到角色展柜并允许查看角色详情")
|
||||
return
|
||||
for characters in data.characters:
|
||||
if characters.name == character_name:
|
||||
break
|
||||
else:
|
||||
await message.reply_text(f"角色展柜中未找到 {character_name}")
|
||||
return
|
||||
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
||||
pnd_data = await RenderTemplate(uid, characters, self.template_service).render()
|
||||
await message.reply_photo(pnd_data, filename=f"player_card_{uid}_{character_name}.png")
|
||||
|
||||
|
||||
class Artifact(BaseModel):
|
||||
"""在 enka Equipments model 基础上扩展了圣遗物评分数据"""
|
||||
|
||||
equipment: Equipments
|
||||
# 圣遗物评分
|
||||
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),
|
||||
("C", 16.5),
|
||||
("B", 23.1),
|
||||
("A", 29.7),
|
||||
("S", 36.3),
|
||||
("SS", 42.9),
|
||||
("SSS", 49.5),
|
||||
("ACE", 56.1),
|
||||
("ACE²", 66)):
|
||||
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-neutral-200",
|
||||
"A": "text-violet-400",
|
||||
"S": "text-violet-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: CharacterInfo, template_service: TemplateService = None):
|
||||
self.uid = uid
|
||||
self.template_service = template_service
|
||||
# 因为需要替换线上 enka 图片地址为本地地址,先克隆数据,避免修改原数据
|
||||
self.character = character.copy(deep=True)
|
||||
|
||||
async def render(self):
|
||||
# 缓存所有图片到本地
|
||||
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),
|
||||
("C", 16.5),
|
||||
("B", 23.1),
|
||||
("A", 29.7),
|
||||
("S", 36.3),
|
||||
("SS", 42.9),
|
||||
("SSS", 49.5),
|
||||
("ACE", 56.1),
|
||||
("ACE²", 66)):
|
||||
if artifact_total_score / 5 >= r[1]:
|
||||
artifact_total_score_label = r[0]
|
||||
|
||||
data = {
|
||||
"uid": self.uid,
|
||||
"character": self.character,
|
||||
"stats": await self.de_stats(),
|
||||
"weapon": self.find_weapon(),
|
||||
# 圣遗物评分
|
||||
"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,
|
||||
|
||||
# 需要在模板中使用的 enum 类型
|
||||
"DigitType": DigitType,
|
||||
}
|
||||
|
||||
# html = await self.template_service.render_async(
|
||||
# "genshin/player_card", "player_card.html", data
|
||||
# )
|
||||
# logger.debug(html)
|
||||
|
||||
return await self.template_service.render(
|
||||
"genshin/player_card",
|
||||
"player_card.html",
|
||||
data,
|
||||
{"width": 950, "height": 1080},
|
||||
full_page=True,
|
||||
)
|
||||
|
||||
async def de_stats(self) -> List[Tuple[str, Any]]:
|
||||
stats = self.character.stats
|
||||
items: List[Tuple[str, Any]] = []
|
||||
logger.debug(self.character.stats)
|
||||
|
||||
# items.append(("基础生命值", stats.BASE_HP.to_rounded()))
|
||||
items.append(("生命值", stats.FIGHT_PROP_MAX_HP.to_rounded()))
|
||||
# items.append(("基础攻击力", stats.FIGHT_PROP_BASE_ATTACK.to_rounded()))
|
||||
items.append(("攻击力", stats.FIGHT_PROP_CUR_ATTACK.to_rounded()))
|
||||
# items.append(("基础防御力", stats.FIGHT_PROP_BASE_DEFENSE.to_rounded()))
|
||||
items.append(("防御力", stats.FIGHT_PROP_CUR_DEFENSE.to_rounded()))
|
||||
items.append(("暴击率", stats.FIGHT_PROP_CRITICAL.to_percentage_symbol()))
|
||||
items.append(
|
||||
(
|
||||
"暴击伤害",
|
||||
stats.FIGHT_PROP_CRITICAL_HURT.to_percentage_symbol(),
|
||||
)
|
||||
)
|
||||
items.append(
|
||||
(
|
||||
"元素充能效率",
|
||||
stats.FIGHT_PROP_CHARGE_EFFICIENCY.to_percentage_symbol(),
|
||||
)
|
||||
)
|
||||
items.append(("元素精通", stats.FIGHT_PROP_ELEMENT_MASTERY.to_rounded()))
|
||||
|
||||
# 查找元素伤害加成和治疗加成
|
||||
max_stat = StatsPercentage() # 用于记录最高元素伤害加成 避免武器特效影响
|
||||
for stat in stats:
|
||||
if 40 <= stat[1].id <= 46: # 元素伤害加成
|
||||
if max_stat.value <= stat[1].value:
|
||||
max_stat = stat[1]
|
||||
elif stat[1].id == 29: # 物理伤害加成
|
||||
pass
|
||||
elif stat[1].id != 26: # 治疗加成
|
||||
continue
|
||||
value = (
|
||||
stat[1].to_rounded()
|
||||
if isinstance(stat[1], Stats)
|
||||
else stat[1].to_percentage_symbol()
|
||||
)
|
||||
if value in ("0%", 0):
|
||||
continue
|
||||
name = assets.get_hash_map(stat[0])
|
||||
if name is None:
|
||||
continue
|
||||
items.append((name, value))
|
||||
|
||||
if max_stat.id != 0:
|
||||
for item in items:
|
||||
if "元素伤害加成" in item[0]:
|
||||
if max_stat.to_percentage_symbol() != item[1]:
|
||||
items.remove(item)
|
||||
|
||||
return items
|
||||
|
||||
async def cache_images(self) -> None:
|
||||
"""缓存所有图片到本地"""
|
||||
# TODO: 并发下载所有资源
|
||||
c = self.character
|
||||
# 角色
|
||||
c.image.banner.url = await url_to_file(c.image.banner.url)
|
||||
|
||||
# 技能
|
||||
for item in c.skills:
|
||||
item.icon.url = await url_to_file(item.icon.url)
|
||||
|
||||
# 命座
|
||||
for item in c.constellations:
|
||||
item.icon.url = await url_to_file(item.icon.url)
|
||||
|
||||
# 装备,包括圣遗物和武器
|
||||
for item in c.equipments:
|
||||
item.detail.icon.url = await url_to_file(item.detail.icon.url)
|
||||
|
||||
def find_weapon(self) -> Union[Equipments, None]:
|
||||
"""在 equipments 数组中找到武器,equipments 数组包含圣遗物和武器"""
|
||||
for item in self.character.equipments:
|
||||
if item.type == EquipmentsType.WEAPON:
|
||||
return item
|
||||
|
||||
def find_artifacts(self) -> List[Artifact]:
|
||||
"""在 equipments 数组中找到圣遗物,并转换成带有分数的 model。equipments 数组包含圣遗物和武器"""
|
||||
|
||||
stats = ArtifactStatsTheory(self.character.name)
|
||||
|
||||
def substat_score(s: EquipmentsStats) -> float:
|
||||
return stats.theory(s)
|
||||
|
||||
return [
|
||||
Artifact(
|
||||
equipment=e,
|
||||
# 圣遗物单行属性评分
|
||||
substat_scores=[substat_score(s) for s in e.detail.substats],
|
||||
)
|
||||
for e in self.character.equipments
|
||||
if e.type == EquipmentsType.ARTIFACT
|
||||
]
|
62
resources/genshin/player_card/artifacts.html
Normal file
@ -0,0 +1,62 @@
|
||||
{% 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.detail.icon.url }}"
|
||||
alt="{{ item.equipment.detail.name }}"
|
||||
/>
|
||||
<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.detail.artifact_name_set }}</div>
|
||||
<div class="flex text-sm space-x-2">
|
||||
<div>{{ item.equipment.detail.mainstats.name }}</div>
|
||||
<div class="italic">
|
||||
{{ item.equipment.detail.mainstats.value }}
|
||||
{%- if item.equipment.detail.mainstats.type == DigitType.PERCENT -%} % {%- endif %}
|
||||
</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.detail.substats %}
|
||||
<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">
|
||||
{%- if stat.name == '元素充能效率' -%}
|
||||
充能效率
|
||||
{%- else -%}
|
||||
{{ stat.name }}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<div class="min-w-30 italic text-right">
|
||||
+{{ stat.value }}
|
||||
{%- if stat.type == DigitType.PERCENT -%} % {%- endif %}
|
||||
</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>
|
||||
<!-- 计算圣遗物分数就行了 原神又不是PVP 也卷不动隔壁 -->
|
||||
<!--
|
||||
<div
|
||||
class="px-4 py-1 flex justify-between border-t border-neutral-200 border-dashed"
|
||||
>
|
||||
<div>备用</div>
|
||||
<div class="italic">x 100.0%</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
{% endfor %}
|
13
resources/genshin/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 character.constellations %}
|
||||
<div
|
||||
class="w-16 h-16 flex items-center justify-center bg-contain bg-no-repeat bg-center
|
||||
{%- if not item.unlocked %} grayscale opacity-75 {% endif %}"
|
||||
style="background-image: url('img/talent-{{ character.element.name | lower }}.png')"
|
||||
>
|
||||
<img src="{{ item.icon.url }}" alt="" class="w-8 h-8" />
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
BIN
resources/genshin/player_card/img/bg-anemo.jpg
Normal file
After Width: | Height: | Size: 187 KiB |
BIN
resources/genshin/player_card/img/bg-cryo.jpg
Normal file
After Width: | Height: | Size: 173 KiB |
BIN
resources/genshin/player_card/img/bg-dendro.jpg
Normal file
After Width: | Height: | Size: 202 KiB |
BIN
resources/genshin/player_card/img/bg-electro.jpg
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
resources/genshin/player_card/img/bg-geo.jpg
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
resources/genshin/player_card/img/bg-hydro.jpg
Normal file
After Width: | Height: | Size: 182 KiB |
BIN
resources/genshin/player_card/img/bg-pyro.jpg
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
resources/genshin/player_card/img/star.png
Executable file
After Width: | Height: | Size: 4.4 KiB |
BIN
resources/genshin/player_card/img/talent-anemo.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/genshin/player_card/img/talent-cryo.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/genshin/player_card/img/talent-dendro.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/genshin/player_card/img/talent-electro.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
resources/genshin/player_card/img/talent-geo.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
resources/genshin/player_card/img/talent-hydro.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/genshin/player_card/img/talent-pyro.png
Normal file
After Width: | Height: | Size: 39 KiB |
@ -1,112 +0,0 @@
|
||||
body {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #f24e4c;
|
||||
width: 1600px;
|
||||
height: 700px;;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.left {
|
||||
left: 0;
|
||||
flex: 0 0 33.4%;
|
||||
max-width: 33.4%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
/* background-color: rgb(255,100,126); */
|
||||
}
|
||||
|
||||
.middle {
|
||||
left: 30%;
|
||||
flex: 0 0 33.3%;
|
||||
max-width: 34%;
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
background-color: rgb(246, 52, 64);
|
||||
}
|
||||
|
||||
.right {
|
||||
right: 0;
|
||||
flex: 0 0 33.3%;
|
||||
max-width: 33.3%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
background-color: rgb(208, 52, 58);
|
||||
}
|
||||
|
||||
.characters {
|
||||
|
||||
}
|
||||
|
||||
|
||||
.gacha-splash {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.characters-info {
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.characters-name {
|
||||
|
||||
}
|
||||
|
||||
.passive-talents {
|
||||
left: 0;
|
||||
width: 50%;
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
|
||||
.passive-talents-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.passive-talents-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.attack-talents {
|
||||
right: 0;
|
||||
width: 50%;
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
|
||||
.attack-talents-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.attack-talents-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.text {
|
||||
text-shadow: 0 .08em .1em #000, 0 .1em .3em rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
.hp-name {
|
||||
|
||||
}
|
||||
|
||||
.hp-value {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.numerical-panel {
|
||||
width: 100%;
|
||||
}
|
@ -1,15 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Title</title>
|
||||
<link href="./player_card.html.css" rel="stylesheet">
|
||||
<link href="../styles/tailwind.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<div class="container">
|
||||
<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);
|
||||
}
|
||||
|
||||
</div>
|
||||
<body>
|
||||
.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;
|
||||
}
|
||||
</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.element.name | lower }}.jpg')"
|
||||
>
|
||||
<div class="relative mb-4 overflow-hidden">
|
||||
<!-- Character Background -->
|
||||
<div
|
||||
class="absolute w-full h-full -left-1/4 top-8 opacity-80 bg-cover bg-no-repeat bg-center"
|
||||
style="background-image: url('{{ character.image.banner.url }}');"
|
||||
></div>
|
||||
<div class="relative w-full flex p-5 space-x-8">
|
||||
{% include 'constellations.html' %}
|
||||
|
||||
</body>
|
||||
<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.name }}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-row justify-end text-2xl text-shadow space-x-2"
|
||||
>
|
||||
<div class="characters-level pr-4">UID {{ uid }}</div>
|
||||
<div class="characters-level">Lv.{{ character.level }}</div>
|
||||
<div class="characters-level bg-red-600 rounded-lg px-2">
|
||||
❤ {{ character.friendship_level }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'skills.html' %} {% include 'stats.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="px-5 relative">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
|
||||
<div class="flex flex-col space-y-2">
|
||||
{% include 'weapon.html' %}
|
||||
{% include 'score.html' %}
|
||||
</div>
|
||||
|
||||
{% include 'artifacts.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
12
resources/genshin/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 {{ 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">{{ artifact_total_score }}</div>
|
||||
<div class="text-base text-neutral-400">圣遗物评分</div>
|
||||
</div>
|
||||
</div>
|
17
resources/genshin/player_card/skills.html
Normal file
@ -0,0 +1,17 @@
|
||||
<div class="flex flex-row">
|
||||
{% for item in character.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.element.name | lower }}.png')"
|
||||
>
|
||||
<img src="{{ item.icon.url }}" 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"
|
||||
>
|
||||
{{ item.level }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
10
resources/genshin/player_card/stats.html
Normal file
@ -0,0 +1,10 @@
|
||||
<div class="rounded-lg overflow-hidden bg-black bg-opacity-20">
|
||||
{% for (name, value) in stats %}
|
||||
<div
|
||||
class="flex justify-between px-10 py-1.5 even:bg-black even:bg-opacity-30"
|
||||
>
|
||||
<div>{{ name }}</div>
|
||||
<div class="italic">{{ value }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
17
resources/genshin/player_card/weapon.html
Normal file
@ -0,0 +1,17 @@
|
||||
<div
|
||||
class="flex-1 flex items-center bg-black bg-opacity-20 rounded-lg bg-contain bg-no-repeat"
|
||||
style="background-image: url('{{ weapon.detail.icon.url }}')"
|
||||
>
|
||||
<div
|
||||
class="flex-1 p-4 flex flex-col items-end justify-center h-full space-y-1"
|
||||
>
|
||||
<div class="text-2xl text-shadow">{{ weapon.detail.name }}</div>
|
||||
<div class="star star-{{ weapon.detail.rarity }}"></div>
|
||||
<div class="flex space-x-3 items-center">
|
||||
<div class="italic text-shadow">Lv.{{ weapon.level }}</div>
|
||||
<div class="bg-gray-600 rounded px-2 text-base">
|
||||
精{{ weapon.refinement }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|