update
5
ci.py
@ -1,8 +1,11 @@
|
||||
import asyncio
|
||||
from configparser import RawConfigParser
|
||||
from logging import getLogger
|
||||
from os import mkdir, sep
|
||||
from os.path import exists
|
||||
|
||||
from pyrogram import Client
|
||||
|
||||
from httpx import AsyncClient, get
|
||||
from sqlitedict import SqliteDict
|
||||
|
||||
@ -39,3 +42,5 @@ client = AsyncClient(timeout=10.0, headers=headers)
|
||||
me = get(f"https://api.telegram.org/bot{bot_token}/getme").json()
|
||||
# 初始化客户端
|
||||
app = Client("bot", bot_token=bot_token, api_id=api_id, api_hash=api_hash, plugins={"root": "plugins"})
|
||||
logger = getLogger("enka")
|
||||
lock = asyncio.Lock()
|
||||
|
BIN
data/font/HYWH-65W.ttf
Normal file
BIN
data/font/tttgbnumber.ttf
Normal file
16
data/g2plot.min.js
vendored
Normal file
BIN
data/imgs/bg-anemo.jpg
Normal file
After Width: | Height: | Size: 187 KiB |
BIN
data/imgs/bg-cryo.jpg
Normal file
After Width: | Height: | Size: 173 KiB |
BIN
data/imgs/bg-dendro.jpg
Normal file
After Width: | Height: | Size: 202 KiB |
BIN
data/imgs/bg-electro.jpg
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
data/imgs/bg-geo.jpg
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
data/imgs/bg-hydro.jpg
Normal file
After Width: | Height: | Size: 182 KiB |
BIN
data/imgs/bg-pyro.jpg
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
data/imgs/talent-anemo.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
data/imgs/talent-cryo.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
data/imgs/talent-dendro.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
data/imgs/talent-electro.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
data/imgs/talent-geo.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
data/imgs/talent-hydro.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
data/imgs/talent-pyro.png
Normal file
After Width: | Height: | Size: 39 KiB |
1114
data/panel-0.2.7.css
Normal file
137
data/panel-0.2.7.html
Normal file
@ -0,0 +1,137 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.2">
|
||||
<link rel="stylesheet" href="./panel-{{ css }}.css">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container" class="{{ data['element'] }}">
|
||||
<img class="UIGachaAvatarImg" src="./{{ data['name'] }}/{{ data['gachaAvatarImg'] }}.png">
|
||||
<div class="UID">{{ uid }}</div>
|
||||
<div class="AvatarLevel">
|
||||
<div class="exp r{{ data['rarity'] }}"><div>等级 {{ data["level"] }}</div></div>
|
||||
<div class="fetter"><div>好感 {{ data["fetter"] }}</div></div>
|
||||
</div>
|
||||
<div class="AvatarCons">
|
||||
{% for con in data["consts"] %}
|
||||
<div class="cons {{ con['style'] }}"><img class="UITalentIcon" src="./{{ data['name'] }}/{{ con['icon'] }}.png" /></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="TopRightBlur">
|
||||
<h1 class="AvatarTitle">{{ data["slogan"] }}·{{ data["name"] }}</h1>
|
||||
<div class="AvatarSkills">
|
||||
{% for sKey, sValue in data["skills"].items() %}
|
||||
<div class="skill">
|
||||
<div class="element"></div>
|
||||
<img class="UISkillIcon" src="./{{ data['name'] }}/{{ sValue['icon'] }}.png" />
|
||||
<div class="level {{ sValue['style'] }}"><div>{{ sValue['level'] }}</div></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<ul class="PropPanel">
|
||||
{% for pKey, prop in data["fightProp"].items() %}
|
||||
<li>
|
||||
{% if prop["weight"] %}
|
||||
<span class="weight {{ 'error' if prop.get('error', '') else '' }}">{{ prop["weight"] }}</span>
|
||||
{% endif %}
|
||||
<div>{{ pKey }}</div>
|
||||
<span class="value">{{ prop["value"] }}
|
||||
{% if prop.get('detail', '') %}
|
||||
<br /><div>{{ prop["detail"][0] }}</div><div>{{ prop["detail"][1] }}</div>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="AvatarEquips">
|
||||
<div class="item">
|
||||
<div class="mark">
|
||||
<div class="tip {{ data['relicCalc']['rank'] }}"></div>
|
||||
<div class="detail">
|
||||
<div class="level {{ data['relicCalc']['rank'] }}">{{ data["relicCalc"]["rank"] }}</div>
|
||||
<div class="goal">{{ data["relicCalc"]["total"] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="weapon">
|
||||
<img class="UIWeaponIcon" src="./weapon/{{ data['weapon']['icon'] }}.png">
|
||||
<div class="level r{{ data['weapon']['rarity'] }}">{{ data["weapon"]["level"] }}</div>
|
||||
<div class="affix a{{ data['weapon']['affix'] }}">{{ data["weapon"]["affix"] }}</div>
|
||||
<div class="name">{{ data["weapon"]["name"] }}</div>
|
||||
<div class="prop main">基础攻击力 {{ data["weapon"]["main"] }}</div>
|
||||
<div class="prop sub">{{ data["weapon"]["sub"]["prop"] }} {{ data["weapon"]["sub"]["value"] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% for arti in data["relics"] %}
|
||||
<div class="item arti">
|
||||
<img class="UIRelicIcon r{{ arti['rarity'] }}" src="./artifacts/{{ arti['icon'] }}.png">
|
||||
<div class="level">{{ arti["level"] }}</div>
|
||||
<div class="title">
|
||||
<div class="name">{{ arti["name"] }}</div>
|
||||
<div class="mark {{ arti['calc']['rank'] }}">{{ arti["calc"]["total"] }} - {{ arti["calc"]['rank'] }}</div>
|
||||
</div>
|
||||
<ul class="affix">
|
||||
<li class="main">
|
||||
<div class="key">{{ arti["main"]["prop"] }}</div>
|
||||
<div class="value">{{ arti["main"]["value"] }}</div>
|
||||
<div class="goal">{{ arti["calc"]["main"] }}</div>
|
||||
</li>
|
||||
|
||||
{% for sub in arti["sub"] %}
|
||||
<li class="sub {{ arti['calc']['sub'][loop.index0]['style'] }}">
|
||||
<div class="key">{{ sub["prop"] }}</div>
|
||||
<div class="value">{{ sub["value"] }}</div>
|
||||
<div class="goal">{{ arti["calc"]["sub"][loop.index0]["goal"] }}</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="note">
|
||||
{% if arti["calc"]["nohit"] %}
|
||||
<div class="nohit">{{ arti["calc"]["nohit"] }}</div>
|
||||
{% endif %}
|
||||
<div class="calc">
|
||||
{% if arti["pos"] >= 3 %}
|
||||
<div class="main">{{ arti["calc"]["main_pct"] }}</div>
|
||||
{% endif %}
|
||||
<div class="total">{{ arti["calc"]["total_pct"] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if data["damage"] %}
|
||||
<ul class="AvatarDamage">
|
||||
<li class="title">{{ data["damage"]["level"] }}</li>
|
||||
<li class="head">
|
||||
<!-- use css to fill this -->
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</li>
|
||||
{% for dmg in data["damage"]["data"] %}
|
||||
<li class="dmg">
|
||||
<div>{{ dmg[0] }}</div>
|
||||
<div>{{ dmg[1] }}</div>
|
||||
<div>{{ dmg[2] }}</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if data["damage"]["buff"] %}
|
||||
<ul class="AvatarBuffs">
|
||||
<li class="title"></li>
|
||||
{% for buff in data["damage"]["buff"] %}
|
||||
<li class="buf">
|
||||
<div>{{ buff[0] }}</div>
|
||||
<div>{{ buff[1] }}</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="copyright"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
166
defs/browser.py
Normal file
@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Author : yanyongyu
|
||||
@Date : 2021-03-12 13:42:43
|
||||
@LastEditors : yanyongyu
|
||||
@LastEditTime : 2021-11-01 14:05:41
|
||||
@Description : None
|
||||
@GitHub : https://github.com/yanyongyu
|
||||
"""
|
||||
__author__ = "yanyongyu"
|
||||
|
||||
import asyncio
|
||||
import platform
|
||||
import jinja2
|
||||
from contextlib import asynccontextmanager
|
||||
from os import getcwd
|
||||
from typing import Optional, AsyncIterator, Literal, Union
|
||||
from playwright.async_api import Page, Browser, async_playwright, Error
|
||||
from ci import logger
|
||||
from uvicorn.loops import asyncio as _asyncio
|
||||
from uvicorn import config
|
||||
|
||||
|
||||
def asyncio_setup():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
|
||||
@property
|
||||
def should_reload(self):
|
||||
return False
|
||||
|
||||
|
||||
if platform.system() == "Windows":
|
||||
_asyncio.asyncio_setup = asyncio_setup
|
||||
config.Config.should_reload = should_reload
|
||||
logger.warning("检测到当前为 Windows 系统,已自动注入猴子补丁")
|
||||
|
||||
_browser: Optional[Browser] = None
|
||||
_playwright = None
|
||||
|
||||
|
||||
async def init(**kwargs) -> Browser:
|
||||
global _browser
|
||||
global _playwright
|
||||
_playwright = await async_playwright().start()
|
||||
try:
|
||||
_browser = await launch_browser(**kwargs)
|
||||
except Error:
|
||||
await install_browser()
|
||||
_browser = await launch_browser(**kwargs)
|
||||
return _browser
|
||||
|
||||
|
||||
async def launch_browser(**kwargs) -> Browser:
|
||||
return await _playwright.chromium.launch(**kwargs)
|
||||
|
||||
|
||||
async def get_browser(**kwargs) -> Browser:
|
||||
return _browser or await init(**kwargs)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_new_page(**kwargs) -> AsyncIterator[Page]:
|
||||
browser = await get_browser()
|
||||
page = await browser.new_page(**kwargs)
|
||||
try:
|
||||
yield page
|
||||
finally:
|
||||
await page.close()
|
||||
|
||||
|
||||
async def shutdown_browser():
|
||||
await _browser.close()
|
||||
await _playwright.stop()
|
||||
|
||||
|
||||
async def install_browser():
|
||||
logger.info("正在安装 chromium")
|
||||
import sys
|
||||
from playwright.__main__ import main
|
||||
|
||||
sys.argv = ["", "install", "chromium"]
|
||||
try:
|
||||
main()
|
||||
except SystemExit:
|
||||
pass
|
||||
|
||||
|
||||
async def html_to_pic(
|
||||
html: str,
|
||||
wait: int = 0,
|
||||
template_path: str = f"file://{getcwd()}",
|
||||
type: Literal["jpeg", "png"] = "png",
|
||||
quality: Union[int, None] = None,
|
||||
**kwargs,
|
||||
) -> bytes:
|
||||
"""html转图片
|
||||
Args:
|
||||
html (str): html文本
|
||||
wait (int, optional): 等待时间. Defaults to 0.
|
||||
template_path (str, optional): 模板路径 如 "file:///path/to/template/"
|
||||
type (Literal["jpeg", "png"]): 图片类型, 默认 png
|
||||
quality (int, optional): 图片质量 0-100 当为`png`时无效
|
||||
**kwargs: 传入 page 的参数
|
||||
Returns:
|
||||
bytes: 图片, 可直接发送
|
||||
"""
|
||||
# logger.debug(f"html:\n{html}")
|
||||
if "file:" not in template_path:
|
||||
raise Exception("template_path 应该为 file:///path/to/template")
|
||||
async with get_new_page(**kwargs) as page:
|
||||
await page.goto(template_path)
|
||||
await page.set_content(html, wait_until="networkidle")
|
||||
await page.wait_for_timeout(wait)
|
||||
img_raw = await page.screenshot(
|
||||
full_page=True,
|
||||
type=type,
|
||||
quality=quality,
|
||||
)
|
||||
return img_raw
|
||||
|
||||
|
||||
async def template_to_pic(
|
||||
template_path: str,
|
||||
template_name: str,
|
||||
templates: dict,
|
||||
pages=None,
|
||||
wait: int = 0,
|
||||
type: Literal["jpeg", "png"] = "png",
|
||||
quality: Union[int, None] = None,
|
||||
) -> bytes:
|
||||
"""使用jinja2模板引擎通过html生成图片
|
||||
Args:
|
||||
template_path (str): 模板路径
|
||||
template_name (str): 模板名
|
||||
templates (dict): 模板内参数 如: {"name": "abc"}
|
||||
pages (dict): 网页参数 Defaults to
|
||||
{"base_url": f"file://{getcwd()}", "viewport": {"width": 500, "height": 10}}
|
||||
wait (int, optional): 网页载入等待时间. Defaults to 0.
|
||||
type (Literal["jpeg", "png"]): 图片类型, 默认 png
|
||||
quality (int, optional): 图片质量 0-100 当为`png`时无效
|
||||
Returns:
|
||||
bytes: 图片 可直接发送
|
||||
"""
|
||||
|
||||
if pages is None:
|
||||
pages = {
|
||||
"viewport": {"width": 500, "height": 10},
|
||||
"base_url": f"file://{getcwd()}",
|
||||
}
|
||||
template_env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(template_path),
|
||||
enable_async=True,
|
||||
)
|
||||
template = template_env.get_template(template_name)
|
||||
|
||||
return await html_to_pic(
|
||||
template_path=f"file://{template_path}",
|
||||
html=await template.render_async(**templates),
|
||||
wait=wait,
|
||||
type=type,
|
||||
quality=quality,
|
||||
**pages,
|
||||
)
|
15
defs/models.py
Normal file
@ -0,0 +1,15 @@
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Char(BaseModel):
|
||||
name: str
|
||||
file_id: str
|
||||
time: int
|
||||
|
||||
|
||||
class Player(BaseModel):
|
||||
name: str = ""
|
||||
time: int = 0
|
||||
all_char: List[Char] = []
|
124
defs/player.py
@ -2,102 +2,106 @@ import json
|
||||
import time
|
||||
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
from os.path import exists
|
||||
from typing import List
|
||||
|
||||
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
||||
from ci import channel_id, app, sqlite
|
||||
from ci import channel_id, app, sqlite, lock
|
||||
from defs.models import Char as CharModel, Player as PlayerModel
|
||||
from gspanel.__utils__ import LOCAL_DIR
|
||||
from gspanel.data_source import getRawData, getPanelMsg
|
||||
from gspanel.data_source import getPanel, getAvatarData
|
||||
|
||||
|
||||
def gen_char_dict(name: str, file_id: str) -> dict:
|
||||
return {"name": name, "file_id": file_id, "time": int(time.time())}
|
||||
def gen_char_dict(name: str, file_id: str) -> CharModel:
|
||||
return CharModel(name=name, file_id=file_id, time=int(time.time()))
|
||||
|
||||
|
||||
class Player:
|
||||
name: str = ""
|
||||
uid: str = ""
|
||||
all_char: List[dict] = []
|
||||
time: int = 0
|
||||
uid: str = "0"
|
||||
_player: PlayerModel
|
||||
|
||||
def __init__(self, uid: str):
|
||||
self.uid = uid
|
||||
self.time = 0
|
||||
self.all_char = []
|
||||
if not exists(LOCAL_DIR / "cache" / f"{uid}__data.json"):
|
||||
return
|
||||
with open(LOCAL_DIR / "cache" / f"{uid}__data.json", "r", encoding="utf-8") as fp:
|
||||
self.cache_path = LOCAL_DIR / "cache" / f"{uid}.json"
|
||||
sources = sqlite.get(self.uid, None)
|
||||
if sources:
|
||||
self._player = PlayerModel.parse_raw(sources)
|
||||
else:
|
||||
self._player = PlayerModel(name=self.get_name_from_cache(), time=0, all_char=[])
|
||||
|
||||
def get_name_from_cache(self) -> str:
|
||||
if not exists(self.cache_path):
|
||||
return "unknown"
|
||||
with open(self.cache_path, "r", encoding="utf-8") as fp:
|
||||
data = json.load(fp)
|
||||
self.name = data.get("playerInfo", {}).get("nickname", "")
|
||||
return data.get("playerInfo", {}).get("nickname", "")
|
||||
|
||||
def update_name(self):
|
||||
with open(LOCAL_DIR / "cache" / f"{self.uid}__data.json", "r", encoding="utf-8") as fp:
|
||||
data = json.load(fp)
|
||||
self.name = data.get("playerInfo", {}).get("nickname", "")
|
||||
def update_name(self) -> None:
|
||||
self._player.name = self.get_name_from_cache()
|
||||
|
||||
@staticmethod
|
||||
def can_refresh():
|
||||
return sqlite.get("can_refresh", True)
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._player.name
|
||||
|
||||
@staticmethod
|
||||
def lock():
|
||||
sqlite["can_refresh"] = False
|
||||
@property
|
||||
def time(self) -> int:
|
||||
return self._player.time
|
||||
|
||||
@staticmethod
|
||||
def unlock():
|
||||
sqlite["can_refresh"] = True
|
||||
@property
|
||||
def all_char(self) -> List[CharModel]:
|
||||
return self._player.all_char
|
||||
|
||||
def update_or_create(self, char: CharModel) -> None:
|
||||
for i in self._player.all_char:
|
||||
if i.name == char.name:
|
||||
i.file_id = char.file_id
|
||||
i.time = char.time
|
||||
return
|
||||
self._player.all_char.append(char)
|
||||
|
||||
async def update_char(self):
|
||||
if not self.can_refresh():
|
||||
if lock.locked():
|
||||
return "有刷新任务正在运行,请稍等一会儿再试"
|
||||
self.lock()
|
||||
await lock.acquire()
|
||||
try:
|
||||
all_char_ = await getRawData(self.uid)
|
||||
all_char_ = all_char_.get("list", [])
|
||||
all_char_ = await getAvatarData(self.uid)
|
||||
all_char_ = [i["name"] for i in all_char_.get("avatars", [])]
|
||||
success = []
|
||||
failed = []
|
||||
for i in all_char_:
|
||||
for f in self.all_char:
|
||||
if f["name"] == i:
|
||||
self.all_char.remove(f)
|
||||
break
|
||||
try:
|
||||
all_char = await getPanelMsg(self.uid, i)
|
||||
img = all_char.get("pic", None)
|
||||
error = all_char.get("error", None)
|
||||
if img:
|
||||
msg = await app.send_photo(channel_id, img)
|
||||
self.all_char.append(gen_char_dict(i, msg.photo.file_id))
|
||||
elif error:
|
||||
return error
|
||||
else:
|
||||
print(all_char)
|
||||
rt = await getPanel(self.uid, i)
|
||||
if isinstance(rt, bytes):
|
||||
bytes_stream = BytesIO(rt)
|
||||
bytes_stream.name = "avatar.png"
|
||||
msg = await app.send_photo(channel_id, bytes_stream)
|
||||
self.update_or_create(gen_char_dict(i, msg.photo.file_id))
|
||||
success.append(i)
|
||||
elif isinstance(rt, str):
|
||||
failed.append(i)
|
||||
except Exception as e:
|
||||
failed.append(i)
|
||||
print(e)
|
||||
continue
|
||||
return f"成功缓存了 UID{self.uid} 的{'、'.join(all_char_)}等 {len(all_char_)} 位角色数据!"
|
||||
return f"缓存成功了 UID{self.uid} 的 {'、'.join(success)} {len(success)} 位角色数据!\n" \
|
||||
f"缓存失败了 {'、'.join(failed)} {len(failed)} 位角色数据!"
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return "数据刷新失败,请重试"
|
||||
finally:
|
||||
self.unlock()
|
||||
lock.release()
|
||||
|
||||
def export(self):
|
||||
return {"name": self.name, "uid": self.uid, "time": int(time.time()), "all_char": self.all_char}
|
||||
|
||||
def restore(self):
|
||||
sources = sqlite.get(self.uid, None)
|
||||
if sources:
|
||||
self.name = sources.get("name", "")
|
||||
self.time = sources.get("time", 0)
|
||||
self.all_char = sources.get("all_char", [])
|
||||
return self._player.json()
|
||||
|
||||
def gen_keyboard(self) -> InlineKeyboardMarkup:
|
||||
data = []
|
||||
temp_ = []
|
||||
num = 0
|
||||
for i in self.all_char:
|
||||
name = i.get("name", "")
|
||||
for i in self._player.all_char:
|
||||
name = i.name
|
||||
temp_.append(InlineKeyboardButton(name, callback_data=f"{self.uid}|{name}"))
|
||||
num += 1
|
||||
if num == 3:
|
||||
@ -116,9 +120,9 @@ class Player:
|
||||
return datetime.strftime(datetime.fromtimestamp(time_stamp), '%Y-%m-%d %H:%M:%S')
|
||||
|
||||
def gen_all_char(self) -> str:
|
||||
if not self.all_char:
|
||||
if not self._player.all_char:
|
||||
return ""
|
||||
text = "缓存角色有:\n"
|
||||
for i in self.all_char:
|
||||
text += "🔸 " + i.get("name", "") + f" `{self.parse_time(i.get('time', time.time()))}`\n"
|
||||
for i in self._player.all_char:
|
||||
text += f"🔸 {i.name} `{self.parse_time(i.time)}`\n"
|
||||
return text
|
||||
|
@ -5,15 +5,11 @@ import time
|
||||
|
||||
async def refresh_player(uid: str, force=False) -> str:
|
||||
data = Player(uid)
|
||||
data.restore()
|
||||
if not force and data.time + 60 * 5 > int(time.time()):
|
||||
return "刷新过快,请稍等一会儿再试"
|
||||
text = await data.update_char()
|
||||
if not text:
|
||||
return "数据刷新失败,请重试"
|
||||
try:
|
||||
data.update_name()
|
||||
except FileNotFoundError:
|
||||
return "数据刷新失败,请重试"
|
||||
data.update_name()
|
||||
sqlite[uid] = data.export()
|
||||
return text
|
||||
|
@ -1,12 +1,18 @@
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
from re import IGNORECASE, findall, sub
|
||||
from typing import List, Set, Tuple, Union
|
||||
|
||||
from httpx import AsyncClient
|
||||
from playwright.async_api import Browser, async_playwright
|
||||
from httpx import AsyncClient, Client
|
||||
|
||||
GROW_VALUE = { # 词条成长值
|
||||
from ci import logger
|
||||
|
||||
# from nonebot import get_driver
|
||||
# from nonebot.drivers import Driver
|
||||
# from nonebot.log import logger
|
||||
|
||||
GROW_VALUE = { # 理论最高档(4档)词条成长值
|
||||
"暴击率": 3.89,
|
||||
"暴击伤害": 7.77,
|
||||
"元素精通": 23.31,
|
||||
@ -18,13 +24,32 @@ GROW_VALUE = { # 词条成长值
|
||||
"物理伤害加成": 7.288,
|
||||
"治疗加成": 4.487,
|
||||
}
|
||||
SINGLE_VALUE = { # 用于计算词条数
|
||||
"暴击率": 3.3,
|
||||
"暴击伤害": 6.6,
|
||||
"元素精通": 19.75,
|
||||
"生命值百分比": 4.975,
|
||||
"攻击力百分比": 4.975,
|
||||
"防御力百分比": 6.2,
|
||||
"元素充能效率": 5.5,
|
||||
}
|
||||
MAIN_AFFIXS = { # 可能的主词条
|
||||
"3": "攻击力百分比,防御力百分比,生命值百分比,元素精通,元素充能效率".split(","), # EQUIP_SHOES
|
||||
"4": "攻击力百分比,防御力百分比,生命值百分比,元素精通,元素伤害加成,物理伤害加成".split(","), # EQUIP_RING
|
||||
"5": "攻击力百分比,防御力百分比,生命值百分比,元素精通,治疗加成,暴击率,暴击伤害".split(","), # EQUIP_DRESS
|
||||
}
|
||||
SUB_AFFIXS = "攻击力,攻击力百分比,防御力,防御力百分比,生命值,生命值百分比,元素精通,元素充能效率,暴击率,暴击伤害".split(",")
|
||||
# STAR = {"QUALITY_ORANGE": 5, "QUALITY_PURPLE": 4}
|
||||
RANK_MAP = [
|
||||
["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],
|
||||
]
|
||||
ELEM = {
|
||||
"Fire": "火",
|
||||
"Water": "水",
|
||||
@ -34,7 +59,13 @@ ELEM = {
|
||||
"Ice": "冰",
|
||||
"Rock": "岩",
|
||||
}
|
||||
POS = ["EQUIP_BRACER", "EQUIP_NECKLACE", "EQUIP_SHOES", "EQUIP_RING", "EQUIP_DRESS"]
|
||||
POS = {
|
||||
"EQUIP_BRACER": "生之花",
|
||||
"EQUIP_NECKLACE": "死之羽",
|
||||
"EQUIP_SHOES": "时之沙",
|
||||
"EQUIP_RING": "空之杯",
|
||||
"EQUIP_DRESS": "理之冠",
|
||||
}
|
||||
SKILL = {"1": "a", "2": "e", "9": "q"}
|
||||
DMG = {
|
||||
"40": "火",
|
||||
@ -62,21 +93,66 @@ PROP = {
|
||||
"FIGHT_PROP_FIRE_ADD_HURT": "火元素伤害加成",
|
||||
"FIGHT_PROP_ELEC_ADD_HURT": "雷元素伤害加成",
|
||||
"FIGHT_PROP_WATER_ADD_HURT": "水元素伤害加成",
|
||||
"FIGHT_PROP_GRASS_ADD_HURT": "草元素伤害加成",
|
||||
"FIGHT_PROP_WIND_ADD_HURT": "风元素伤害加成",
|
||||
"FIGHT_PROP_ICE_ADD_HURT": "冰元素伤害加成",
|
||||
"FIGHT_PROP_ROCK_ADD_HURT": "岩元素伤害加成",
|
||||
}
|
||||
|
||||
_browser: Optional[Browser] = None
|
||||
# driver: Driver = get_driver()
|
||||
|
||||
EXPIRE_SEC = 60 * 5
|
||||
LOCAL_DIR = Path("resources")
|
||||
# GSPANEL_ALIAS: Set[Union[str, Tuple[str, ...]]] = (
|
||||
# set(driver.config.gspanel_alias)
|
||||
# if hasattr(driver.config, "gspanel_alias")
|
||||
# else {"面板"}
|
||||
# )
|
||||
LOCAL_DIR = (
|
||||
Path() / "data"
|
||||
)
|
||||
SCALE_FACTOR = (
|
||||
1.0
|
||||
)
|
||||
DOWNLOAD_MIRROR = (
|
||||
"https://enka.network/ui/"
|
||||
)
|
||||
if not LOCAL_DIR.exists():
|
||||
LOCAL_DIR.mkdir(parents=True, exist_ok=True)
|
||||
if not (LOCAL_DIR / "cache").exists():
|
||||
(LOCAL_DIR / "cache").mkdir(parents=True, exist_ok=True)
|
||||
if not (LOCAL_DIR / "qq-uid.json").exists():
|
||||
(LOCAL_DIR / "qq-uid.json").write_text("{}", encoding="UTF-8")
|
||||
_client = Client(verify=False)
|
||||
CALC_RULES = _client.get("https://cdn.monsterx.cn/bot/gspanel/calc-rule.json").json()
|
||||
(LOCAL_DIR / "calc-rule.json").write_text(
|
||||
json.dumps(CALC_RULES, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
CHAR_DATA = _client.get("https://cdn.monsterx.cn/bot/gspanel/char-data.json").json()
|
||||
(LOCAL_DIR / "char-data.json").write_text(
|
||||
json.dumps(CHAR_DATA, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
CHAR_ALIAS = _client.get("https://cdn.monsterx.cn/bot/gspanel/char-alias.json").json()
|
||||
(LOCAL_DIR / "char-alias.json").write_text(
|
||||
json.dumps(CHAR_ALIAS, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
TEAM_ALIAS = _client.get("https://cdn.monsterx.cn/bot/gspanel/team-alias.json").json()
|
||||
(LOCAL_DIR / "team-alias.json").write_text(
|
||||
json.dumps(TEAM_ALIAS, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
HASH_TRANS = _client.get("https://cdn.monsterx.cn/bot/gspanel/hash-trans.json").json()
|
||||
(LOCAL_DIR / "hash-trans.json").write_text(
|
||||
json.dumps(HASH_TRANS, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
RELIC_APPEND = _client.get("https://cdn.monsterx.cn/bot/gspanel/relic-append.json").json()
|
||||
(LOCAL_DIR / "relic-append.json").write_text(
|
||||
json.dumps(RELIC_APPEND, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
TPL_VERSION = "0.2.7"
|
||||
|
||||
|
||||
def kStr(prop: str) -> str:
|
||||
def kStr(prop: str, reverse: bool = False) -> str:
|
||||
"""转换词条名称为简短形式"""
|
||||
if reverse:
|
||||
return prop.replace("充能", "元素充能").replace("伤加成", "元素伤害加成").replace("物理元素", "物理")
|
||||
return (
|
||||
prop.replace("百分比", "")
|
||||
.replace("元素充能", "充能")
|
||||
@ -87,42 +163,92 @@ def kStr(prop: str) -> str:
|
||||
|
||||
def vStr(prop: str, value: Union[int, float]) -> str:
|
||||
"""转换词条数值为字符串形式"""
|
||||
if prop in {"生命值", "攻击力", "防御力", "元素精通"}:
|
||||
if prop in ["生命值", "攻击力", "防御力", "元素精通"]:
|
||||
return str(value)
|
||||
else:
|
||||
return f"{str(round(value, 1))}%"
|
||||
return str(round(value, 1)) + "%"
|
||||
|
||||
|
||||
async def initBrowser(**kwargs) -> Optional[Browser]:
|
||||
global _browser
|
||||
browser = await async_playwright().start()
|
||||
try:
|
||||
_browser = await browser.chromium.launch(**kwargs)
|
||||
return _browser
|
||||
except Exception as e:
|
||||
print(f"启动 Chromium 发生错误 {type(e)}:{e}")
|
||||
return None
|
||||
def getServer(uid: str) -> str:
|
||||
"""获取指定 UID 所属服务器,返回如 ``cn_gf01``"""
|
||||
if uid[0] == "5":
|
||||
return "cn_qd01"
|
||||
elif uid[0] == "6":
|
||||
return "os_usa"
|
||||
elif uid[0] == "7":
|
||||
return "os_euro"
|
||||
elif uid[0] == "8":
|
||||
return "os_asia"
|
||||
elif uid[0] == "9":
|
||||
return "os_cht"
|
||||
return "cn_gf01"
|
||||
|
||||
|
||||
async def getBrowser(**kwargs) -> Optional[Browser]:
|
||||
return _browser or await initBrowser(**kwargs)
|
||||
async def formatInput(msg: str, qq: str, atqq: str = "") -> Tuple[str, str]:
|
||||
"""
|
||||
输入消息中的 UID 与角色名格式化,应具备处理 ``msg`` 为空、包含中文或数字的能力。
|
||||
- 首个中文字符串捕获为角色名,若不包含则返回 ``all`` 请求角色面板列表数据
|
||||
- 首个数字字符串捕获为 UID,若不包含则返回 ``uidHelper()`` 根据绑定配置查找的 UID
|
||||
|
||||
* ``param msg: str`` 输入消息,由 ``state["_prefix"]["command_arg"]`` 或 ``event.get_plaintext()`` 生成,可能包含 CQ 码
|
||||
* ``param qq: str`` 输入消息触发 QQ
|
||||
* ``param atqq: str = ""`` 输入消息中首个 at 的 QQ
|
||||
- ``return: Tuple[str, str]`` UID、角色名
|
||||
"""
|
||||
uid, char, tmp = "", "", ""
|
||||
group = findall(
|
||||
r"[0-9]+|[\u4e00-\u9fa5]+|[a-z]+", sub(r"\[CQ:.*\]", "", msg), flags=IGNORECASE
|
||||
)
|
||||
for s in group:
|
||||
if s.isdigit():
|
||||
if len(s) == 9:
|
||||
if not uid:
|
||||
uid = s
|
||||
else:
|
||||
# 0人,1斗,97忍
|
||||
tmp = s
|
||||
elif s.encode().isalpha():
|
||||
# dio娜,abd
|
||||
tmp = s.lower()
|
||||
elif not s.isdigit() and not char:
|
||||
char = tmp + s
|
||||
uid = uid or await uidHelper(atqq or qq)
|
||||
char = await aliasWho(char or tmp or "全部")
|
||||
return uid, char
|
||||
|
||||
|
||||
async def formatTeam(msg: str, qq: str, atqq: str = "") -> Tuple[str, List]:
|
||||
"""
|
||||
输入消息中的 UID 与队伍角色名格式化
|
||||
|
||||
* ``param msg: str`` 输入消息,由 ``MessageSegment.data["text"]`` 拼接组成
|
||||
* ``param qq: str`` 输入消息触发 QQ
|
||||
* ``param atqq: str = ""`` 输入消息中首个 at 的 QQ
|
||||
- ``return: Tuple[str, List]`` UID、队伍角色名
|
||||
"""
|
||||
uid, chars = "", []
|
||||
for seg in msg.split():
|
||||
_uid, char = await formatInput(seg, qq, atqq)
|
||||
uid = uid or _uid
|
||||
if char != "全部" and char not in chars:
|
||||
logger.info(f"从 QQ{qq} 的输入「{seg}」中识别到 UID[{uid}] CHAR[{char}]")
|
||||
chars.append(char)
|
||||
if not msg:
|
||||
uid, _ = await formatInput("", qq, atqq)
|
||||
if len(chars) == 1:
|
||||
searchTeam = await aliasTeam(chars[0])
|
||||
chars = searchTeam if isinstance(searchTeam, List) else chars
|
||||
return uid, chars
|
||||
|
||||
|
||||
async def fetchInitRes() -> None:
|
||||
"""
|
||||
插件初始化资源下载,通过阿里云 CDN 获取 Enka.Network API 提供的 JSON 文件、HTML 模板资源文件、角色词条权重配置等
|
||||
- https://raw.githubusercontent.com/EnkaNetwork/API-docs/master/store/loc.json
|
||||
- https://raw.githubusercontent.com/Dimbreath/GenshinData/master/TextMap/TextMapCHS.json
|
||||
- https://raw.githubusercontent.com/EnkaNetwork/API-docs/master/store/characters.json
|
||||
- https://raw.githubusercontent.com/monsterxcn/nonebot_plugin_gspanel/main/data/gspanel/template.json
|
||||
- https://raw.githubusercontent.com/monsterxcn/nonebot_plugin_gspanel/main/data/gspanel/calc-rule.json
|
||||
插件初始化资源下载,通过阿里云 CDN 获取 HTML 模板资源文件、角色词条权重配置、角色数据、TextMap 中文翻译数据等
|
||||
"""
|
||||
print("正在检查面板插件所需资源...")
|
||||
logger.info("正在检查面板插件所需资源...")
|
||||
# 仅首次启用插件下载的文件
|
||||
initRes = [
|
||||
"https://cdn.monsterx.cn/bot/gspanel/font/华文中宋.TTF",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/font/HYWH-65W.ttf",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/font/NZBZ.ttf",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/font/tttgbnumber.ttf",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/bg-anemo.jpg",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/bg-cryo.jpg",
|
||||
@ -131,8 +257,6 @@ async def fetchInitRes() -> None:
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/bg-geo.jpg",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/bg-hydro.jpg",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/bg-pyro.jpg",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/card-bg.png",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/star.png",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/talent-anemo.png",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/talent-cryo.png",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/talent-dendro.png",
|
||||
@ -140,8 +264,11 @@ async def fetchInitRes() -> None:
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/talent-geo.png",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/talent-hydro.png",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/talent-pyro.png",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/style.css",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/tpl.html",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/g2plot.min.js",
|
||||
f"https://cdn.monsterx.cn/bot/gspanel/team-{TPL_VERSION}.css",
|
||||
f"https://cdn.monsterx.cn/bot/gspanel/team-{TPL_VERSION}.html",
|
||||
f"https://cdn.monsterx.cn/bot/gspanel/panel-{TPL_VERSION}.css",
|
||||
f"https://cdn.monsterx.cn/bot/gspanel/panel-{TPL_VERSION}.html",
|
||||
]
|
||||
tasks = []
|
||||
for r in initRes:
|
||||
@ -149,48 +276,22 @@ async def fetchInitRes() -> None:
|
||||
tasks.append(download(r, local=("" if "." in d else d)))
|
||||
await asyncio.gather(*tasks)
|
||||
tasks.clear()
|
||||
print("首次启用所需资源检查完毕..")
|
||||
# 总是尝试更新的文件
|
||||
urls = [
|
||||
# "https://cdn.monsterx.cn/bot/gapanel/loc.json", # 仅包含 zh-CN 语言
|
||||
"https://cdn.monsterx.cn/bot/gspanel/TextMapCHS.json",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/characters.json",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/calc-rule.json",
|
||||
]
|
||||
tmp = {"0": {}, "1": {}, "2": {}}
|
||||
async with AsyncClient(verify=False) as client:
|
||||
for idx, url in enumerate(urls):
|
||||
tmp[str(idx)] = (await client.get(url)).json()
|
||||
(LOCAL_DIR / url.split("/")[-1]).write_text(
|
||||
json.dumps(tmp[str(idx)], ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
# 额外生成一份 {"中文名": "8 位角色 ID", ...} 配置
|
||||
name2id, unknownCnt = {}, 1
|
||||
for charId in tmp["1"]: # characters.json
|
||||
if not tmp["1"][charId].get("NameTextMapHash"):
|
||||
# 10000005-502 10000005-503 10000005-505
|
||||
# 10000007-702 10000007-703 10000007-705
|
||||
continue
|
||||
nameTextMapHash = tmp["1"][charId]["NameTextMapHash"] # type: int
|
||||
nameCn = tmp["0"].get(str(nameTextMapHash), f"未知角色{unknownCnt}")
|
||||
name2id[nameCn] = charId
|
||||
if nameCn == f"未知角色{unknownCnt}":
|
||||
unknownCnt += 1
|
||||
(LOCAL_DIR / "name2id.json").write_text(
|
||||
json.dumps(name2id, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
print("面板插件所需资源检查完毕!")
|
||||
logger.info("面板插件所需资源检查完毕!")
|
||||
|
||||
|
||||
async def download(url: str, local: Union[Path, str] = "") -> Union[Path, None]:
|
||||
async def download(
|
||||
url: str, local: Union[Path, str] = "", retry: int = 3
|
||||
) -> Union[Path, None]:
|
||||
"""
|
||||
一般文件下载,通常是即用即下的角色命座图片、技能图片、抽卡大图、圣遗物图片等
|
||||
|
||||
* ``param url: str`` 指定下载链接
|
||||
* ``param local: Union[Path, str] = ""`` 指定本地目标路径,传入类型为 ``Path`` 时视为保存文件完整路径,传入类型为 ``str`` 时视为保存文件子文件夹名(默认下载至插件资源根目录)
|
||||
- ``return: Union[Path, None]`` 本地文件地址,出错时返回空
|
||||
* ``param url: str`` 下载链接
|
||||
* ``param local: Union[Path, str] = ""`` 下载路径,传入类型为 ``Path`` 时视为保存文件完整路径,传入类型为 ``str`` 时视为保存文件子文件夹名(默认下载至插件资源根目录)
|
||||
* ``param retry: int = 3`` 下载失败重试次数
|
||||
- ``return: Union[Path, None]`` 本地文件路径,出错时返回空
|
||||
"""
|
||||
if not url.startswith("http"):
|
||||
url = DOWNLOAD_MIRROR + url + ".png"
|
||||
if not isinstance(local, Path):
|
||||
d = (LOCAL_DIR / local) if local else LOCAL_DIR
|
||||
if not d.exists():
|
||||
@ -203,9 +304,8 @@ async def download(url: str, local: Union[Path, str] = "") -> Union[Path, None]:
|
||||
# 本地文件存在时便不再下载,JSON 文件除外
|
||||
if f.exists() and ".json" not in f.name:
|
||||
return f
|
||||
client = AsyncClient()
|
||||
retryCnt = 3
|
||||
while retryCnt:
|
||||
client, retry = AsyncClient(), 3
|
||||
while retry:
|
||||
try:
|
||||
async with client.stream(
|
||||
"GET", url, headers={"user-agent": "NoneBot-GsPanel"}
|
||||
@ -215,7 +315,45 @@ async def download(url: str, local: Union[Path, str] = "") -> Union[Path, None]:
|
||||
fb.write(chunk)
|
||||
return f
|
||||
except Exception as e:
|
||||
print(f"面板资源 {f.name} 下载出错 {type(e)}:{e}")
|
||||
retryCnt -= 1
|
||||
await asyncio.sleep(2)
|
||||
retry -= 1
|
||||
if retry:
|
||||
await asyncio.sleep(2)
|
||||
else:
|
||||
logger.opt(exception=e).error(f"面板资源 {f.name} 下载出错")
|
||||
return None
|
||||
|
||||
|
||||
async def uidHelper(qq: Union[str, int], uid: str = "") -> str:
|
||||
"""
|
||||
UID 助手,根据 QQ 获取对应原神 UID,也可传入 UID 更新指定 QQ 的绑定情况
|
||||
|
||||
* ``param qq: Union[str, int]`` 操作 QQ
|
||||
* ``param uid: str = ""`` 操作 UID,默认不传入以查找该值,传入则视为绑定/更新
|
||||
- ``return: str``指定 QQ 绑定的原神 UID,绑定/更新时返回操作结果
|
||||
"""
|
||||
qq = str(qq)
|
||||
cfgFile = LOCAL_DIR / "qq-uid.json"
|
||||
uidCfg = json.loads(cfgFile.read_text(encoding="utf-8"))
|
||||
if uid:
|
||||
uidCfg[qq] = uid
|
||||
cfgFile.write_text(
|
||||
json.dumps(uidCfg, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
return "已{} QQ{} 的 UID 为 {}".format("更新" if qq in uidCfg else "绑定", qq, uid)
|
||||
return uidCfg.get(qq, "")
|
||||
|
||||
|
||||
async def aliasWho(input: str) -> str:
|
||||
"""角色别名,未找到别名配置的原样返回"""
|
||||
for char in CHAR_ALIAS:
|
||||
if (input in char) or (input in CHAR_ALIAS[char]):
|
||||
return char
|
||||
return input
|
||||
|
||||
|
||||
async def aliasTeam(input: str) -> Union[str, List]:
|
||||
"""队伍别名,未找到别名配置的原样返回"""
|
||||
for team in TEAM_ALIAS:
|
||||
if (input == team) or (input in TEAM_ALIAS[team].get("alias", [])):
|
||||
return TEAM_ALIAS[team]["chars"] # type: List
|
||||
return input
|
||||
|
675
gspanel/data_convert.py
Normal file
@ -0,0 +1,675 @@
|
||||
import json
|
||||
from time import time
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
# from nonebot.log import logger
|
||||
from ci import logger
|
||||
|
||||
from gspanel.__utils__ import (
|
||||
CALC_RULES,
|
||||
CHAR_DATA,
|
||||
ELEM,
|
||||
GROW_VALUE,
|
||||
HASH_TRANS,
|
||||
MAIN_AFFIXS,
|
||||
POS,
|
||||
PROP,
|
||||
RANK_MAP,
|
||||
RELIC_APPEND,
|
||||
SKILL,
|
||||
SUB_AFFIXS,
|
||||
getServer,
|
||||
kStr,
|
||||
vStr,
|
||||
)
|
||||
|
||||
|
||||
async def getRelicConfig(char: str, base: Dict = {}) -> Tuple[Dict, Dict, Dict]:
|
||||
"""
|
||||
指定角色圣遗物计算配置获取,包括词条评分权重、词条数值原始权重、各位置圣遗物总分理论最高分和主词条理论最高得分
|
||||
|
||||
* ``param char: str`` 角色名
|
||||
* ``param base: Dict = {}`` 角色的基础数值,可由 Enka 返回获得,格式为 ``{"生命值": 1, "攻击力": 1, "防御力": 1}``
|
||||
- ``return: Tuple[Dict, Dict, Dict]`` 词条评分权重、词条数值原始权重、各位置圣遗物最高得分
|
||||
"""
|
||||
affixWeight = CALC_RULES.get(char, {"攻击力百分比": 75, "暴击率": 100, "暴击伤害": 100})
|
||||
# 词条评分权重的 key 排序影响最优主词条选择
|
||||
# 通过特定排序使同等权重时生命攻击防御固定值词条优先级最低
|
||||
# key 的原始排序为 生命值攻击力防御力百分比、暴击率、暴击伤害、元素精通、元素伤害加成、物理伤害加成、元素充能效率
|
||||
# 注:已经忘了最初为什么这样写了,但总之就是顺序有影响+现在这样写能用
|
||||
affixWeight = dict(
|
||||
sorted(
|
||||
affixWeight.items(),
|
||||
key=lambda item: (
|
||||
item[1],
|
||||
"暴击" in item[0],
|
||||
"加成" in item[0],
|
||||
"元素" in item[0],
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
)
|
||||
# 计算词条数值原始权重
|
||||
# 是一种与词条数值的乘积在百位数级别的东西,后续据此计算最终得分
|
||||
# 非百分比的生命攻击防御词条也按百分比词条的 affixWeight 权重计算
|
||||
pointMark = {k: v / GROW_VALUE[k] for k, v in affixWeight.items()}
|
||||
if pointMark.get("攻击力百分比"):
|
||||
pointMark["攻击力"] = pointMark["攻击力百分比"] / base.get("攻击力", 1020) * 100
|
||||
if pointMark.get("防御力百分比"):
|
||||
pointMark["防御力"] = pointMark["防御力百分比"] / base.get("防御力", 300) * 100
|
||||
if pointMark.get("生命值百分比"):
|
||||
pointMark["生命值"] = pointMark["生命值百分比"] / base.get("生命值", 400) * 100
|
||||
# 各位置圣遗物的总分理论最高分、主词条理论最高得分
|
||||
maxMark = {"1": {}, "2": {}, "3": {}, "4": {}, "5": {}}
|
||||
for posIdx in range(1, 6):
|
||||
# 主词条最高得分
|
||||
if posIdx <= 2:
|
||||
# 花和羽不计算主词条得分
|
||||
mainAffix = "生命值" if posIdx == 1 else "攻击力"
|
||||
maxMark[str(posIdx)]["main"] = 0
|
||||
maxMark[str(posIdx)]["total"] = 0
|
||||
else:
|
||||
# 沙杯头计算该位置评分权重最高的词条得分
|
||||
avalMainAffix = {
|
||||
k: v for k, v in affixWeight.items() if k in MAIN_AFFIXS[str(posIdx)]
|
||||
}
|
||||
# logger.debug(
|
||||
# "{} 的主词条推荐顺序为:\n{}".format(
|
||||
# list(POS.values())[posIdx - 1],
|
||||
# " / ".join(f"{k}[{v}]" for k, v in avalMainAffix.items()),
|
||||
# )
|
||||
# )
|
||||
mainAffix = list(avalMainAffix)[0]
|
||||
maxMark[str(posIdx)]["main"] = affixWeight[mainAffix]
|
||||
maxMark[str(posIdx)]["total"] = affixWeight[mainAffix] * 2
|
||||
# 副词条最高得分
|
||||
maxSubAffixs = {
|
||||
k: v
|
||||
for k, v in affixWeight.items()
|
||||
if k in SUB_AFFIXS and k != mainAffix and affixWeight.get(k)
|
||||
}
|
||||
# logger.debug(
|
||||
# "{} 的副词条推荐顺序为:\n{}".format(
|
||||
# list(POS.values())[posIdx - 1],
|
||||
# " / ".join(f"{k}[{v}]" for k, v in maxSubAffixs.items()),
|
||||
# )
|
||||
# )
|
||||
# 副词条中评分权重最高的词条得分大幅提升
|
||||
maxMark[str(posIdx)]["total"] += sum(
|
||||
affixWeight[k] * (1 if kIdx else 6)
|
||||
for kIdx, k in enumerate(list(maxSubAffixs)[0:4])
|
||||
)
|
||||
logger.info(
|
||||
(
|
||||
"「{}」圣遗物评分依据:"
|
||||
"\n\t词条评分权重 affixWeight\n\t{}"
|
||||
"\n\t词条数值原始权重 pointMark\n\t{}"
|
||||
"\n\t各位置圣遗物最高得分 maxMark\n\t{}"
|
||||
).format(
|
||||
char,
|
||||
" / ".join(f"{k}[{v}]" for k, v in affixWeight.items()),
|
||||
" / ".join(f"{k}[{v}]" for k, v in pointMark.items()),
|
||||
" / ".join(
|
||||
f"{list(POS.values())[int(k)-1]}>主词条[{v['main']}]总分[{v['total']}]"
|
||||
for k, v in maxMark.items()
|
||||
),
|
||||
)
|
||||
)
|
||||
return affixWeight, pointMark, maxMark
|
||||
|
||||
|
||||
def getRelicRank(score: float) -> str:
|
||||
"""圣遗物评级获取"""
|
||||
# 在角色等级较低(基础数值较低)时评级可能显示为 "ERR"
|
||||
# 注:角色等级较低时不为 "ERR" 的评分也有可能出错
|
||||
return [r[0] for r in RANK_MAP if score <= r[1]][0] if score <= 66 else "ERR"
|
||||
|
||||
|
||||
async def calcRelicMark(
|
||||
relicData: Dict, charElement: str, affixWeight: Dict, pointMark: Dict, maxMark: Dict
|
||||
) -> Dict:
|
||||
"""
|
||||
指定角色圣遗物评分计算
|
||||
|
||||
* ``param relicData: Dict`` 圣遗物数据
|
||||
* ``param charElement: str`` 角色的中文元素属性
|
||||
* ``param affixWeight: Dict`` 角色的词条评分权重,由 ``getRelicConfig()`` 获取
|
||||
* ``param pointMark: Dict`` 角色的词条数值原始权重,由 ``getRelicConfig()`` 获取
|
||||
* ``param maxMark: Dict`` 角色的各位置圣遗物最高得分,由 ``getRelicConfig()`` 获取
|
||||
- ``return: Dict`` 圣遗物评分结果
|
||||
"""
|
||||
posIdx, relicLevel = str(relicData["pos"]), relicData["level"]
|
||||
mainProp, subProps = relicData["main"], relicData["sub"]
|
||||
# 主词条得分、主词条收益系数(百分数)
|
||||
if posIdx in ["1", "2"]:
|
||||
calcMain, calcMainPct = 0.0, 100
|
||||
else:
|
||||
# 角色元素属性与伤害属性不同时权重为 0,不影响物理伤害得分
|
||||
_mainPointMark: float = pointMark.get(
|
||||
mainProp["prop"].replace(charElement, ""), 0
|
||||
)
|
||||
_point: float = _mainPointMark * mainProp["value"]
|
||||
# 主词条与副词条的得分计算规则一致,但只取 25%
|
||||
calcMain = _point * 46.6 / 6 / 100 / 4
|
||||
# 主词条收益系数用于沙杯头位置主词条不正常时的圣遗物总分惩罚,最多扣除 50% 总分
|
||||
_punishPct: float = _point / maxMark[posIdx]["main"] / 2 / 4
|
||||
calcMainPct = 100 - 50 * (1 - _punishPct)
|
||||
# 副词条得分
|
||||
calcSubs = []
|
||||
for s in subProps:
|
||||
_subPointMark: float = pointMark.get(s["prop"], 0)
|
||||
calcSub: float = _subPointMark * s["value"] * 46.6 / 6 / 100
|
||||
# 副词条 CSS 样式
|
||||
_awKey = f"{s['prop']}百分比" if s["prop"] in ["生命值", "攻击力", "防御力"] else s["prop"]
|
||||
_subAffixWeight: int = affixWeight.get(_awKey, 0)
|
||||
subStyleClass = (
|
||||
("great" if _subAffixWeight > 79 else "use") if calcSub else "unuse"
|
||||
)
|
||||
# [词条名, 词条数值, 词条得分]
|
||||
calcSubs.append([subStyleClass, calcSub])
|
||||
# 总分对齐系数(百分数),按满分 66 对齐各位置圣遗物的总分
|
||||
calcTotalPct: float = 66 / (maxMark[posIdx]["total"] * 46.6 / 6 / 100) * 100
|
||||
# 最终圣遗物总分
|
||||
_total = calcMain + sum(s[1] for s in calcSubs)
|
||||
calcTotal = _total * calcMainPct * calcTotalPct / 10000
|
||||
# 强化歪次数
|
||||
realAppendPropIdList: List[int] = (
|
||||
relicData["_appendPropIdList"][-(relicLevel // 4) :] if (relicLevel // 4) else []
|
||||
)
|
||||
# logger.debug(
|
||||
# "{} 强化记录:\n{}".format(
|
||||
# list(POS.values())[int(posIdx) - 1],
|
||||
# " / ".join(
|
||||
# PROP.get(RELIC_APPEND[str(x)], RELIC_APPEND[str(x)])
|
||||
# for x in realAppendPropIdList
|
||||
# ),
|
||||
# )
|
||||
# )
|
||||
notHit = len(
|
||||
[
|
||||
x
|
||||
for x in realAppendPropIdList
|
||||
if not pointMark.get(PROP.get(RELIC_APPEND[str(x)], RELIC_APPEND[str(x)]))
|
||||
]
|
||||
)
|
||||
return {
|
||||
"rank": getRelicRank(calcTotal),
|
||||
"total": calcTotal,
|
||||
"nohit": notHit,
|
||||
"main": round(calcMain, 1),
|
||||
"sub": [{"style": subRes[0], "goal": round(subRes[1], 1)} for subRes in calcSubs],
|
||||
"main_pct": round(calcMainPct, 1),
|
||||
"total_pct": round(calcTotalPct, 1),
|
||||
}
|
||||
|
||||
|
||||
async def transFromEnka(avatarInfo: Dict, ts: int = 0) -> Dict:
|
||||
"""
|
||||
转换 Enka.Network 角色查询数据为内部格式
|
||||
|
||||
* ``param avatarInfo: Dict`` Enka.Network 角色查询数据,取自 ``data["avatarInfoList"]`` 列表
|
||||
* ``param ts: int = 0`` 数据时间戳
|
||||
- ``return: Dict`` 内部格式角色数据,用于本地缓存等
|
||||
"""
|
||||
charData = CHAR_DATA[str(avatarInfo["avatarId"])]
|
||||
res = {
|
||||
"id": avatarInfo["avatarId"], # type: int
|
||||
"rarity": 5 if "QUALITY_ORANGE" in charData["QualityType"] else 4,
|
||||
"name": charData["NameCN"],
|
||||
"slogan": charData["Slogan"],
|
||||
"element": ELEM[charData["Element"]], # 中文单字
|
||||
"cons": len(avatarInfo.get("talentIdList", [])), # type: int
|
||||
"fetter": avatarInfo["fetterInfo"]["expLevel"], # type: int
|
||||
"level": int(avatarInfo["propMap"]["4001"]["val"]), # type: int
|
||||
"icon": charData["iconName"],
|
||||
"gachaAvatarImg": charData["Costumes"][str(avatarInfo["costumeId"])]["art"]
|
||||
if avatarInfo.get("costumeId")
|
||||
else charData["iconName"].replace("UI_AvatarIcon_", "UI_Gacha_AvatarImg_"),
|
||||
"baseProp": { # type: float
|
||||
"生命值": avatarInfo["fightPropMap"]["1"],
|
||||
"攻击力": avatarInfo["fightPropMap"]["4"],
|
||||
"防御力": avatarInfo["fightPropMap"]["7"],
|
||||
},
|
||||
"fightProp": { # type: float
|
||||
"生命值": avatarInfo["fightPropMap"]["2000"],
|
||||
# "攻击力": avatarInfo["fightPropMap"]["2001"],
|
||||
"攻击力": avatarInfo["fightPropMap"]["4"]
|
||||
* (1 + avatarInfo["fightPropMap"].get("6", 0))
|
||||
+ avatarInfo["fightPropMap"].get("5", 0),
|
||||
"防御力": avatarInfo["fightPropMap"]["2002"],
|
||||
"暴击率": avatarInfo["fightPropMap"]["20"] * 100,
|
||||
"暴击伤害": avatarInfo["fightPropMap"]["22"] * 100,
|
||||
"治疗加成": avatarInfo["fightPropMap"]["26"] * 100,
|
||||
"元素精通": avatarInfo["fightPropMap"]["28"],
|
||||
"元素充能效率": avatarInfo["fightPropMap"]["23"] * 100,
|
||||
"物理伤害加成": avatarInfo["fightPropMap"]["30"] * 100,
|
||||
"火元素伤害加成": avatarInfo["fightPropMap"]["40"] * 100,
|
||||
"水元素伤害加成": avatarInfo["fightPropMap"]["42"] * 100,
|
||||
"风元素伤害加成": avatarInfo["fightPropMap"]["44"] * 100,
|
||||
"雷元素伤害加成": avatarInfo["fightPropMap"]["41"] * 100,
|
||||
"草元素伤害加成": avatarInfo["fightPropMap"]["43"] * 100,
|
||||
"冰元素伤害加成": avatarInfo["fightPropMap"]["46"] * 100,
|
||||
"岩元素伤害加成": avatarInfo["fightPropMap"]["45"] * 100,
|
||||
},
|
||||
"skills": {},
|
||||
"consts": [],
|
||||
"weapon": {},
|
||||
"relics": [],
|
||||
"relicSet": {},
|
||||
"relicCalc": {},
|
||||
"damage": {}, # 预留
|
||||
"time": ts or int(time()),
|
||||
}
|
||||
# 技能数据
|
||||
skills = {"a": {}, "e": {}, "q": {}}
|
||||
extraLevels = {
|
||||
k[-1]: v for k, v in avatarInfo.get("proudSkillExtraLevelMap", {}).items()
|
||||
}
|
||||
for idx, skillId in enumerate(charData["SkillOrder"]):
|
||||
# 实际技能等级、显示技能等级
|
||||
level = avatarInfo["skillLevelMap"][str(skillId)]
|
||||
currentLvl = level + extraLevels.get(list(SKILL)[idx], 0)
|
||||
skills[list(SKILL.values())[idx]] = {
|
||||
"style": "extra" if currentLvl > level else "",
|
||||
"icon": charData["Skills"][str(skillId)],
|
||||
"level": currentLvl,
|
||||
"originLvl": level,
|
||||
}
|
||||
res["skills"] = skills
|
||||
# 命座数据
|
||||
consts = []
|
||||
for cIdx, consImgName in enumerate(charData["Consts"]):
|
||||
consts.append(
|
||||
{
|
||||
"style": "off" if cIdx + 1 > res["cons"] else "",
|
||||
"icon": consImgName,
|
||||
}
|
||||
)
|
||||
res["consts"] = consts
|
||||
# 装备数据
|
||||
affixWeight, pointMark, maxMark = await getRelicConfig(
|
||||
charData["NameCN"], res["baseProp"]
|
||||
)
|
||||
relicsMark, relicsCnt, relicSet = 0.0, 0, {}
|
||||
for equip in avatarInfo["equipList"]:
|
||||
if equip["flat"]["itemType"] == "ITEM_WEAPON":
|
||||
weaponSub: str = equip["flat"]["weaponStats"][-1]["appendPropId"]
|
||||
weaponSubValue = equip["flat"]["weaponStats"][-1]["statValue"]
|
||||
res["weapon"] = {
|
||||
"id": equip["itemId"],
|
||||
"rarity": equip["flat"]["rankLevel"], # type: int
|
||||
"name": HASH_TRANS.get(equip["flat"]["nameTextMapHash"], "缺少翻译"),
|
||||
"affix": list(equip["weapon"].get("affixMap", {"_": 0}).values())[0] + 1,
|
||||
"level": equip["weapon"]["level"], # type: int
|
||||
"icon": equip["flat"]["icon"],
|
||||
"main": equip["flat"]["weaponStats"][0]["statValue"], # type: int
|
||||
"sub": {
|
||||
"prop": PROP[weaponSub].replace("百分比", ""),
|
||||
"value": f"{weaponSubValue}{'' if weaponSub.endswith('ELEMENT_MASTERY') else '%'}",
|
||||
}
|
||||
if weaponSub != "FIGHT_PROP_BASE_ATTACK"
|
||||
else {},
|
||||
}
|
||||
elif equip["flat"]["itemType"] == "ITEM_RELIQUARY":
|
||||
mainProp: Dict = equip["flat"]["reliquaryMainstat"]
|
||||
subProps: List = equip["flat"].get("reliquarySubstats", [])
|
||||
posIdx = list(POS.keys()).index(equip["flat"]["equipType"]) + 1
|
||||
relicData = {
|
||||
"pos": posIdx,
|
||||
"rarity": equip["flat"]["rankLevel"],
|
||||
"name": HASH_TRANS.get(equip["flat"]["nameTextMapHash"], "缺少翻译"),
|
||||
"setName": HASH_TRANS.get(equip["flat"]["setNameTextMapHash"], "缺少翻译"),
|
||||
"level": equip["reliquary"]["level"] - 1,
|
||||
"main": {
|
||||
"prop": PROP[mainProp["mainPropId"]],
|
||||
"value": mainProp["statValue"],
|
||||
},
|
||||
"sub": [
|
||||
{"prop": PROP[s["appendPropId"]], "value": s["statValue"]}
|
||||
for s in subProps
|
||||
],
|
||||
"calc": {},
|
||||
"icon": equip["flat"]["icon"],
|
||||
"_appendPropIdList": equip["reliquary"].get("appendPropIdList", []),
|
||||
}
|
||||
relicData["calc"] = await calcRelicMark(
|
||||
relicData, res["element"], affixWeight, pointMark, maxMark
|
||||
)
|
||||
# 分数计算完毕后再将词条名称、数值转为适合 HTML 渲染的格式
|
||||
relicData["main"]["value"] = vStr(
|
||||
relicData["main"]["prop"], relicData["main"]["value"]
|
||||
)
|
||||
relicData["main"]["prop"] = kStr(relicData["main"]["prop"])
|
||||
relicData["sub"] = [
|
||||
{"prop": kStr(s["prop"]), "value": vStr(s["prop"], s["value"])}
|
||||
for s in relicData["sub"]
|
||||
]
|
||||
# 额外数据处理
|
||||
relicData["calc"]["total"] = round(relicData["calc"]["total"], 1)
|
||||
relicData.pop("_appendPropIdList")
|
||||
relicSet[relicData["setName"]] = relicSet.get(relicData["setName"], 0) + 1
|
||||
res["relics"].append(relicData)
|
||||
# 累积圣遗物套装评分和计数器
|
||||
relicsMark += relicData["calc"]["total"]
|
||||
relicsCnt += 1
|
||||
# 圣遗物套装
|
||||
res["relicSet"] = relicSet
|
||||
res["relicCalc"] = {
|
||||
"rank": getRelicRank(relicsMark / relicsCnt) if relicsCnt else "NaN",
|
||||
"total": round(relicsMark, 1),
|
||||
}
|
||||
return res
|
||||
|
||||
|
||||
async def transToTeyvat(avatarsData: List[Dict], uid: str) -> Dict:
|
||||
"""
|
||||
转换内部格式角色数据为 Teyvat Helper 请求格式
|
||||
|
||||
* ``param avatarsData: List[Dict]`` 内部格式角色数据,由 ``transFromEnka()`` 获取
|
||||
* ``param uid: str`` 角色所属用户 UID
|
||||
- ``return: Dict`` Teyvat Helper 请求格式角色数据
|
||||
"""
|
||||
res = {"uid": uid, "role_data": []}
|
||||
if uid[0] not in ["1", "2"]:
|
||||
res["server"] = getServer(uid)
|
||||
|
||||
for avatarData in avatarsData:
|
||||
name = avatarData["name"]
|
||||
cons = avatarData["cons"]
|
||||
weapon = avatarData["weapon"]
|
||||
baseProp = avatarData["baseProp"]
|
||||
fightProp = avatarData["fightProp"]
|
||||
skills = avatarData["skills"]
|
||||
relics = avatarData["relics"]
|
||||
relicSet = avatarData["relicSet"]
|
||||
|
||||
# dataFix from https://github.com/yoimiya-kokomi/miao-plugin/blob/ac27075276154ef5a87a458697f6e5492bd323bd/components/profile-data/enka-data.js#L186
|
||||
if name == "雷电将军":
|
||||
_thunderDmg = fightProp["雷元素伤害加成"]
|
||||
_recharge = fightProp["元素充能效率"]
|
||||
fightProp["雷元素伤害加成"] = max(0, _thunderDmg - (_recharge - 100) * 0.4)
|
||||
if name == "莫娜":
|
||||
_waterDmg = fightProp["水元素伤害加成"]
|
||||
_recharge = fightProp["元素充能效率"]
|
||||
fightProp["水元素伤害加成"] = max(0, _waterDmg - _recharge * 0.2)
|
||||
if name == "妮露" and cons == 6:
|
||||
_count = float(fightProp["生命值"] / 1000)
|
||||
_crit = fightProp["暴击率"]
|
||||
_critDmg = fightProp["暴击伤害"]
|
||||
fightProp["暴击率"] = max(5, _crit - min(30, _count * 0.6))
|
||||
fightProp["暴击伤害"] = max(50, _critDmg - min(60, _count * 1.2))
|
||||
if weapon["name"] in ["息灾", "波乱月白经津", "雾切之回光", "猎人之径"]:
|
||||
for elem in ["火", "水", "雷", "风", "冰", "岩", "草"]:
|
||||
_origin = fightProp[f"{elem}元素伤害加成"]
|
||||
fightProp[f"{elem}元素伤害加成"] = max(
|
||||
0, _origin - 12 - 12 * (weapon["affix"] - 1) / 4
|
||||
)
|
||||
|
||||
# 圣遗物数据
|
||||
artifacts = []
|
||||
for a in relics:
|
||||
tData = {
|
||||
"artifacts_name": a["name"],
|
||||
"artifacts_type": list(POS.values())[a["pos"] - 1],
|
||||
"level": a["level"],
|
||||
"maintips": kStr(a["main"]["prop"], reverse=True),
|
||||
"mainvalue": a["main"]["value"],
|
||||
}
|
||||
tData.update(
|
||||
{
|
||||
f"tips{sIdx + 1}": "{}+{}".format(
|
||||
kStr(s["prop"], reverse=True), s["value"]
|
||||
)
|
||||
for sIdx, s in enumerate(a["sub"])
|
||||
}
|
||||
)
|
||||
artifacts.append(tData)
|
||||
|
||||
# 单个角色最终结果
|
||||
res["role_data"].append(
|
||||
{
|
||||
"uid": uid,
|
||||
"role": name,
|
||||
"role_class": cons,
|
||||
"level": int(avatarData["level"]),
|
||||
"weapon": weapon["name"],
|
||||
"weapon_level": weapon["level"],
|
||||
"weapon_class": f"精炼{weapon['affix']}阶",
|
||||
"hp": int(fightProp["生命值"]),
|
||||
"base_hp": int(baseProp["生命值"]),
|
||||
"attack": int(fightProp["攻击力"]),
|
||||
"base_attack": int(baseProp["攻击力"]),
|
||||
"defend": int(fightProp["防御力"]),
|
||||
"base_defend": int(baseProp["防御力"]),
|
||||
"element": round(fightProp["元素精通"]),
|
||||
"crit": f"{round(fightProp['暴击率'], 1)}%",
|
||||
"crit_dmg": f"{round(fightProp['暴击伤害'], 1)}%",
|
||||
"heal": f"{round(fightProp['治疗加成'], 1)}%",
|
||||
"recharge": f"{round(fightProp['元素充能效率'], 1)}%",
|
||||
"fire_dmg": f"{round(fightProp['火元素伤害加成'], 1)}%",
|
||||
"water_dmg": f"{round(fightProp['水元素伤害加成'], 1)}%",
|
||||
"thunder_dmg": f"{round(fightProp['雷元素伤害加成'], 1)}%",
|
||||
"wind_dmg": f"{round(fightProp['风元素伤害加成'], 1)}%",
|
||||
"ice_dmg": f"{round(fightProp['冰元素伤害加成'], 1)}%",
|
||||
"rock_dmg": f"{round(fightProp['岩元素伤害加成'], 1)}%",
|
||||
"grass_dmg": f"{round(fightProp['草元素伤害加成'], 1)}%",
|
||||
"physical_dmg": f"{round(fightProp['物理伤害加成'], 1)}%",
|
||||
"artifacts": "+".join(
|
||||
f"{k}{4 if v >= 4 else (2 if v >= 2 else 1)}"
|
||||
for k, v in relicSet.items()
|
||||
if (v >= 2) or ("之人" in k)
|
||||
),
|
||||
"ability1": skills["a"]["level"],
|
||||
"ability2": skills["e"]["level"],
|
||||
"ability3": skills["q"]["level"],
|
||||
"artifacts_detail": artifacts,
|
||||
}
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
async def simplDamageRes(damage: Dict) -> Dict:
|
||||
"""
|
||||
转换角色伤害计算请求数据为精简格式
|
||||
|
||||
* ``param damage: Dict`` 角色伤害计算请求数据,由 ``queryDamageApi()["result"][int]`` 获取
|
||||
- ``return: Dict`` 精简格式伤害数据,出错时返回 ``{}``
|
||||
"""
|
||||
res = {"level": damage["zdl_result"] or "NaN", "data": [], "buff": []}
|
||||
for key in ["damage_result_arr", "damage_result_arr2"]:
|
||||
for dmgDetail in damage[key]:
|
||||
dmgTitle = "{}{}".format(
|
||||
f"[{damage['zdl_result2']}]<br>" if key == "damage_result_arr2" else "",
|
||||
dmgDetail["title"],
|
||||
)
|
||||
if "期望" in str(dmgDetail["value"]) or not dmgDetail.get("expect"):
|
||||
dmgCrit, dmgExp = "-", str(dmgDetail["value"]).replace("期望", "")
|
||||
else:
|
||||
dmgCrit = str(dmgDetail["value"])
|
||||
dmgExp = str(dmgDetail["expect"]).replace("期望", "")
|
||||
res["data"].append([dmgTitle, dmgCrit, dmgExp])
|
||||
for buff in damage["bonus"]:
|
||||
# damage["bonus"]: {"0": {}, "2": {}, ...}
|
||||
# damage["bonus"]: [{}, {}, ...]
|
||||
intro = damage["bonus"][buff]["intro"] if isinstance(buff, str) else buff["intro"]
|
||||
buffTitle, buffDetail = intro.split(":")
|
||||
if buffTitle not in ["注", "备注"]:
|
||||
res["buff"].append([buffTitle, buffDetail])
|
||||
return res
|
||||
|
||||
|
||||
async def simplFightProp(
|
||||
fightProp: Dict, baseProp: Dict, char: str, element: str
|
||||
) -> Dict[str, Dict]:
|
||||
"""
|
||||
转换角色面板数据为 HTML 模板需求格式
|
||||
|
||||
* ``param fightProp: Dict`` 角色面板数据,由 ``transFromEnka()["fightProp"]`` 获取
|
||||
* ``param baseProp: Dict`` 角色基础数据,由 ``transFromEnka()["baseProp"]`` 获取
|
||||
* ``param element: str`` 角色元素属性
|
||||
- ``return: Dict[str, Dict]`` HTML 模板需求格式面板数据
|
||||
"""
|
||||
affixWeight = CALC_RULES.get(char, {"攻击力百分比": 75, "暴击率": 100, "暴击伤害": 100})
|
||||
|
||||
# 排列伤害加成
|
||||
prefer = (
|
||||
element if affixWeight.get("元素伤害加成", 0) > affixWeight.get("物理伤害加成", 0) else "物"
|
||||
)
|
||||
damages = sorted(
|
||||
[{"k": k, "v": v} for k, v in fightProp.items() if str(k).endswith("伤害加成")],
|
||||
key=lambda x: (x["v"], x["k"][0] == prefer),
|
||||
reverse=True,
|
||||
)
|
||||
for unuseDmg in damages[1:]:
|
||||
fightProp.pop(unuseDmg["k"])
|
||||
|
||||
# 生成模板渲染所需数据
|
||||
res = {}
|
||||
for propTitle, propValue in fightProp.items():
|
||||
# 跳过无效治疗加成
|
||||
if propTitle == "治疗加成" and not propValue and not affixWeight.get(propTitle):
|
||||
continue
|
||||
# 整理渲染数据
|
||||
res[propTitle] = {
|
||||
"value": f"{round(propValue, 1)}%"
|
||||
if propTitle not in ["生命值", "攻击力", "防御力", "元素精通"]
|
||||
else round(propValue),
|
||||
"weight": max(affixWeight.get("元素伤害加成", 0), affixWeight.get("物理伤害加成", 0))
|
||||
if propTitle.endswith("伤害加成")
|
||||
else affixWeight.get(propTitle) or affixWeight.get(f"{propTitle}百分比", 0),
|
||||
}
|
||||
# 补充基础数值
|
||||
if propTitle in ["生命值", "攻击力", "防御力"]:
|
||||
res[propTitle]["detail"] = [
|
||||
round(baseProp[propTitle]),
|
||||
round(res[propTitle]["value"] - baseProp[propTitle]),
|
||||
]
|
||||
# 标记异常伤害加成
|
||||
if propTitle.endswith("伤害加成"):
|
||||
if (
|
||||
propTitle[0] not in ["物", element]
|
||||
or affixWeight.get(propTitle[-6:], 0) != res[propTitle]["weight"]
|
||||
):
|
||||
res[propTitle]["error"] = True
|
||||
|
||||
return res
|
||||
|
||||
|
||||
async def simplTeamDamageRes(raw: Dict, rolesData: Dict) -> Dict:
|
||||
"""
|
||||
转换队伍伤害计算请求数据为精简格式
|
||||
|
||||
* ``param raw: Dict`` 队伍伤害计算请求数据,由 ``queryDamageApi(*, "team")["result"]`` 获取
|
||||
* ``param rolesData: Dict`` 角色数据,键为角色中文名,值为内部格式
|
||||
- ``return: Dict`` 精简格式伤害数据。出错时返回 ``{"error": "错误信息"}``
|
||||
"""
|
||||
s = (
|
||||
str(raw["zdl_tips0"])
|
||||
.replace("你的队伍", "")
|
||||
.replace("秒内造成总伤害", "-")
|
||||
.replace(",DPS为:", "")
|
||||
)
|
||||
tm, total = s.split("-")
|
||||
|
||||
pieData, pieColor = [], []
|
||||
for x in raw["chart_data"]:
|
||||
char, damage = x["name"].split("\n")
|
||||
pieData.append({"char": char, "damage": float(damage.replace("W", ""))})
|
||||
pieColor.append(x["label"]["color"])
|
||||
pieData = sorted(pieData, key=lambda x: x["damage"], reverse=True)
|
||||
# 寻找伤害最高的角色元素属性,跳过绽放等伤害来源
|
||||
elem = [
|
||||
rolesData[_source["char"]]["element"]
|
||||
for _source in pieData
|
||||
if rolesData.get(_source["char"])
|
||||
][0]
|
||||
|
||||
avatars = {}
|
||||
for role in raw["role_list"]:
|
||||
panelData = rolesData[role["role"]]
|
||||
avatars[role["role"]] = {
|
||||
"rarity": role["role_star"],
|
||||
"icon": panelData["icon"],
|
||||
"name": role["role"],
|
||||
"elem": panelData["element"],
|
||||
"cons": role["role_class"],
|
||||
"level": role["role_level"].replace("Lv", ""),
|
||||
"weapon": {
|
||||
"icon": panelData["weapon"]["icon"],
|
||||
"level": panelData["weapon"]["level"],
|
||||
"rarity": panelData["weapon"]["rarity"],
|
||||
"affix": panelData["weapon"]["affix"],
|
||||
},
|
||||
"sets": {
|
||||
[r for r in panelData["relics"] if r["setName"] == k][0]["icon"].split(
|
||||
"_"
|
||||
)[-2]: (2 if v < 4 else 4)
|
||||
for k, v in panelData["relicSet"].items()
|
||||
if v >= 2 # 暂未排版 祭X之人 单件套装
|
||||
},
|
||||
"cp": round(panelData["fightProp"]["暴击率"], 1),
|
||||
"cd": round(panelData["fightProp"]["暴击伤害"], 1),
|
||||
"key_prop": role["key_ability"],
|
||||
"key_value": role["key_value"],
|
||||
"skills": [
|
||||
{"icon": skill["icon"], "style": skill["style"], "level": skill["level"]}
|
||||
for _, skill in panelData["skills"].items()
|
||||
],
|
||||
}
|
||||
|
||||
for rechargeData in raw["recharge_info"]:
|
||||
name, tmp = rechargeData["recharge"].split("共获取同色球")
|
||||
same, diff = tmp.split("个,异色球")
|
||||
if len(diff.split("个,无色球")) == 2:
|
||||
# 暂未排版无色球
|
||||
diff = diff.split("个,无色球")[0]
|
||||
avatars[name]["recharge"] = {
|
||||
"pct": rechargeData["rate"],
|
||||
"same": round(float(same), 1),
|
||||
"diff": round(float(diff.replace("个", "")), 1),
|
||||
}
|
||||
|
||||
damages = []
|
||||
for step in raw["advice"]:
|
||||
if not step.get("content"):
|
||||
logger.error(f"奇怪的伤害:{step}")
|
||||
continue
|
||||
# content: "4.2s 雷神e协同,暴击:3016,不暴击:1565,期望:2343"
|
||||
t, s = step["content"].split(" ")
|
||||
if len(s.split(",")) == 1:
|
||||
# "3.89s 万叶q染色为:雷"
|
||||
a = s.split(",")[0]
|
||||
d = ["-", "-", "-"]
|
||||
else:
|
||||
a, dmgs = s.split(",")
|
||||
if len(dmgs.split(",")) == 1:
|
||||
d = ["-", "-", dmgs.split(",")[0].split(":")[-1]]
|
||||
else:
|
||||
d = [dd.split(":")[-1] for dd in dmgs.split(",")]
|
||||
damages.append([t.replace("s", ""), a.upper(), *d])
|
||||
|
||||
buffs = []
|
||||
for buff in raw["buff"]:
|
||||
if not buff.get("content"):
|
||||
logger.error(f"奇怪的 Buff:{buff}")
|
||||
continue
|
||||
# buff: "1.5s 风套-怪物雷抗减少-40%"
|
||||
t, tmp = buff["content"].split(" ", 1)
|
||||
b, bd = tmp.split("-", 1)
|
||||
buffs.append([t.replace("s", ""), b.upper(), bd.upper()])
|
||||
|
||||
return {
|
||||
"uid": raw["uid"],
|
||||
"elem": elem,
|
||||
"rank": raw["zdl_tips2"],
|
||||
"dps": raw["zdl_result"],
|
||||
"tm": tm,
|
||||
"total": total,
|
||||
"pie_data": json.dumps(pieData, ensure_ascii=False),
|
||||
"pie_color": json.dumps(pieColor),
|
||||
"avatars": avatars,
|
||||
"actions": raw["combo_intro"].split(","),
|
||||
"damages": damages,
|
||||
"buffs": buffs,
|
||||
}
|
@ -1,569 +1,342 @@
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
|
||||
# from io import BytesIO
|
||||
# from PIL import Image
|
||||
from copy import deepcopy
|
||||
from time import time
|
||||
from typing import Dict, List, Literal, Tuple
|
||||
from typing import Dict, List, Literal, Union
|
||||
|
||||
from httpx import AsyncClient, HTTPError
|
||||
|
||||
from gspanel.__utils__ import (
|
||||
DMG,
|
||||
ELEM,
|
||||
EXPIRE_SEC,
|
||||
GROW_VALUE,
|
||||
LOCAL_DIR,
|
||||
MAIN_AFFIXS,
|
||||
POS,
|
||||
PROP,
|
||||
SKILL,
|
||||
SUB_AFFIXS,
|
||||
download,
|
||||
getBrowser,
|
||||
kStr,
|
||||
vStr,
|
||||
# from nonebot import require
|
||||
# from nonebot.log import logger
|
||||
from ci import logger
|
||||
|
||||
# require("nonebot_plugin_htmlrender")
|
||||
# from nonebot_plugin_htmlrender import template_to_pic
|
||||
from defs.browser import template_to_pic
|
||||
|
||||
from gspanel.__utils__ import LOCAL_DIR, SCALE_FACTOR, TPL_VERSION, download
|
||||
from gspanel.data_convert import (
|
||||
simplDamageRes,
|
||||
simplFightProp,
|
||||
simplTeamDamageRes,
|
||||
transFromEnka,
|
||||
transToTeyvat,
|
||||
)
|
||||
|
||||
|
||||
async def getRawData(
|
||||
uid: str,
|
||||
charId: str = "000",
|
||||
refresh: bool = False,
|
||||
name2id=None,
|
||||
source: Literal["enka", "mgg"] = "enka",
|
||||
) -> Dict:
|
||||
async def queryPanelApi(uid: str, source: Literal["enka", "mgg"] = "enka") -> Dict:
|
||||
"""
|
||||
Enka.Network API 原神游戏内角色展柜原始数据获取
|
||||
原神游戏内角色展柜数据请求
|
||||
|
||||
* ``param uid: str`` 指定查询用户 UID
|
||||
* ``param charId: str = "000"`` 指定查询角色 ID
|
||||
* ``param refresh: bool = False`` 指定是否强制刷新数据
|
||||
* ``param name2id: Dict = {}`` 角色 ID 与中文名转换所需资源
|
||||
* ``param source: Literal["enka", "mgg"] = "enka"`` 指定查询接口
|
||||
- ``return: Dict`` 查询结果。出错时返回 ``{"error": "错误信息"}``
|
||||
* ``param uid: str`` 查询用户 UID
|
||||
* ``param source: Literal["enka", "mgg"] = "enka"`` 查询接口
|
||||
- ``return: Dict`` 查询结果,出错时返回 ``{"error": "错误信息"}``
|
||||
"""
|
||||
if name2id is None:
|
||||
name2id = json.loads((LOCAL_DIR / "name2id.json").read_text(encoding="utf-8"))
|
||||
cache = LOCAL_DIR / "cache" / f"{uid}__data.json"
|
||||
print(f"checking cache for {uid}'s {charId}")
|
||||
# 缓存文件存在且未过期、未要求刷新、查询角色存在于缓存中,三个条件均满足时才返回缓存
|
||||
if cache.exists() and (not refresh):
|
||||
cacheData = json.loads(cache.read_text(encoding="utf-8"))
|
||||
avalCharIds = [
|
||||
str(c["avatarId"]) for c in cacheData["playerInfo"]["showAvatarInfoList"]
|
||||
]
|
||||
if int(time()) - cacheData["time"] > EXPIRE_SEC:
|
||||
pass
|
||||
elif charId in avalCharIds:
|
||||
return [
|
||||
c for c in cacheData["avatarInfoList"] if str(c["avatarId"]) == charId
|
||||
][0]
|
||||
elif charId == "000":
|
||||
return {
|
||||
"list": [
|
||||
[
|
||||
nameCn
|
||||
for nameCn, cId in name2id.items()
|
||||
if cId == str(x["avatarId"])
|
||||
][0]
|
||||
for x in cacheData["playerInfo"]["showAvatarInfoList"]
|
||||
if x["avatarId"] not in [10000005, 10000007]
|
||||
]
|
||||
}
|
||||
# 请求最新数据
|
||||
root = "https://enka.network" if source == "enka" else "https://enka.minigg.cn"
|
||||
enkaMirrors = [
|
||||
"https://enka.network",
|
||||
"https://enka.minigg.cn",
|
||||
"https://enka.microgg.cn",
|
||||
]
|
||||
async with AsyncClient() as client:
|
||||
resJson = {}
|
||||
for idx, root in enumerate(enkaMirrors):
|
||||
try:
|
||||
res = await client.get(
|
||||
url=f"{root}/u/{uid}/__data.json",
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7",
|
||||
"Cache-Control": "no-cache",
|
||||
"Cookie": "locale=zh-CN",
|
||||
"Referer": "https://enka.network/",
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Linux; Android 12; Nexus 5) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/102.0.0.0 Mobile Safari/537.36"
|
||||
),
|
||||
},
|
||||
timeout=20.0,
|
||||
)
|
||||
resJson = res.json()
|
||||
break
|
||||
except (HTTPError, json.decoder.JSONDecodeError) as e:
|
||||
if idx == len(enkaMirrors) - 1:
|
||||
logger.opt(exception=e).error("面板数据接口无法访问或返回错误")
|
||||
return {"error": f"[{e.__class__.__name__}] 暂时无法访问面板数据接口.."}
|
||||
else:
|
||||
logger.info(f"从 {root} 获取面板失败,正在自动切换镜像重试...")
|
||||
if not resJson.get("playerInfo"):
|
||||
return {"error": f"玩家 {uid} 返回信息不全,接口可能正在维护.."}
|
||||
if not resJson.get("avatarInfoList"):
|
||||
return {"error": f"玩家 {uid} 的角色展柜详细数据已隐藏!"}
|
||||
if not resJson["playerInfo"].get("showAvatarInfoList"):
|
||||
return {"error": f"玩家 {uid} 的角色展柜内还没有角色哦!"}
|
||||
return resJson
|
||||
|
||||
|
||||
async def queryDamageApi(body: Dict, mode: Literal["single", "team"] = "single") -> Dict:
|
||||
"""
|
||||
角色伤害计算数据请求(提瓦特小助手)
|
||||
|
||||
* ``param body: Dict`` 查询角色数据
|
||||
* ``param mode: Literal["single", "team"] = "single"`` 查询接口类型,默认请求角色伤害接口,传入 ``"team"`` 请求队伍伤害接口
|
||||
- ``return: Dict`` 查询结果,出错时返回 ``{}``
|
||||
"""
|
||||
apiMap = {
|
||||
"single": "https://api.lelaer.com/ys/getDamageResult.php",
|
||||
"team": "https://api.lelaer.com/ys/getTeamResult.php",
|
||||
}
|
||||
async with AsyncClient() as client:
|
||||
try:
|
||||
res = await client.get(
|
||||
url=f"{root}/u/{uid}/__data.json",
|
||||
res = await client.post(
|
||||
apiMap[mode],
|
||||
json=body,
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7",
|
||||
"Cache-Control": "no-cache",
|
||||
"Cookie": "locale=zh-CN",
|
||||
"Referer": "https://enka.network/",
|
||||
"User-Agent": ( # "Miao-Plugin/3.0",
|
||||
"Mozilla/5.0 (Linux; Android 12; Nexus 5) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/102.0.0.0 Mobile Safari/537.36"
|
||||
"referer": "https://servicewechat.com/wx2ac9dce11213c3a8/192/page-frame.html",
|
||||
"user-agent": (
|
||||
"Mozilla/5.0 (Linux; Android 12; SM-G977N Build/SP1A.210812.016; wv) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 "
|
||||
"XWEB/4375 MMWEBSDK/20221011 Mobile Safari/537.36 MMWEBID/4357 "
|
||||
"MicroMessenger/8.0.30.2244(0x28001E44) WeChat/arm64 Weixin GPVersion/1 "
|
||||
"NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android"
|
||||
),
|
||||
},
|
||||
timeout=20.0,
|
||||
)
|
||||
resJson = res.json()
|
||||
resJson["time"] = int(time())
|
||||
if not resJson.get("playerInfo"):
|
||||
raise HTTPError("返回信息不全")
|
||||
if not resJson["playerInfo"].get("showAvatarInfoList"):
|
||||
return {"error": f"UID{uid} 的角色展柜内还没有角色哦!"}
|
||||
if not resJson.get("avatarInfoList"):
|
||||
return {"error": f"UID{uid} 的角色展柜详细数据已隐藏!"}
|
||||
(LOCAL_DIR / "cache" / f"{uid}__data.json").write_text(
|
||||
json.dumps(resJson, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
# 返回 Enka.Network API 查询结果
|
||||
if charId == "000":
|
||||
return {
|
||||
"list": [
|
||||
[
|
||||
nameCn
|
||||
for nameCn, cId in name2id.items()
|
||||
if cId == str(x["avatarId"])
|
||||
][0]
|
||||
for x in resJson["playerInfo"]["showAvatarInfoList"]
|
||||
if x["avatarId"] not in [10000005, 10000007]
|
||||
]
|
||||
}
|
||||
elif [x for x in resJson["avatarInfoList"] if str(x["avatarId"]) == charId]:
|
||||
return [
|
||||
x for x in resJson["avatarInfoList"] if str(x["avatarId"]) == charId
|
||||
][0]
|
||||
else:
|
||||
return {"error": f"UID{uid} 的最新数据中未发现该角色!"}
|
||||
except (HTTPError or json.decoder.JSONDecodeError):
|
||||
return {"error": "暂时无法访问面板数据接口.."}
|
||||
return res.json()
|
||||
except (HTTPError, json.decoder.JSONDecodeError) as e:
|
||||
logger.opt(exception=e).error("提瓦特小助手接口无法访问或返回错误")
|
||||
return {}
|
||||
except Exception as e:
|
||||
# 出错时返回 {"error": "错误信息"}
|
||||
print(f"请求 Enka.Network 出错 {type(e)}:{e}")
|
||||
return {"error": f"[{e.__class__.__name__}]面板数据处理出错辣.."}
|
||||
logger.opt(exception=e).error("提瓦特小助手接口错误")
|
||||
return {}
|
||||
|
||||
|
||||
async def getAffixCfg(char: str, base: Dict) -> Tuple[Dict, Dict, Dict]:
|
||||
async def getAvatarData(uid: str, char: str = "全部") -> Dict:
|
||||
"""
|
||||
指定角色词条配置获取,包括词条评分权重、词条数值原始权重、各位置圣遗物总分理论最高分和主词条理论最高得分
|
||||
角色数据获取(内部格式)
|
||||
|
||||
* ``param char: str`` 指定角色名
|
||||
* ``param base: Dict`` 指定角色的基础数值,可由 Enka 返回直接传入,格式为 ``{"生命值": 1, "攻击力": 1, "防御力": 1}``
|
||||
- ``return: Tuple[Dict, Dict, Dict]`` 词条评分权重、词条数值原始权重、各位置圣遗物最高得分
|
||||
"""
|
||||
allCfg = json.loads((LOCAL_DIR / "calc-rule.json").read_text(encoding="utf-8"))
|
||||
assert isinstance(allCfg, Dict)
|
||||
affixWeight = allCfg.get(char, {"攻击力百分比": 75, "暴击率": 100, "暴击伤害": 100})
|
||||
affixWeight = dict(sorted(affixWeight.items(), key=lambda item: (item[1], "暴击" in item[0], "加成" in item[0], "元素" in item[0]), reverse=True))
|
||||
|
||||
pointMark = {k: v / GROW_VALUE[k] for k, v in affixWeight.items()}
|
||||
if pointMark.get("攻击力百分比"):
|
||||
pointMark["攻击力"] = pointMark["攻击力百分比"] / base.get("攻击力", 1020) * 100
|
||||
if pointMark.get("防御力百分比"):
|
||||
pointMark["防御力"] = pointMark["防御力百分比"] / base.get("防御力", 300) * 100
|
||||
if pointMark.get("生命值百分比"):
|
||||
pointMark["生命值"] = pointMark["生命值百分比"] / base.get("生命值", 400) * 100
|
||||
maxMark = {"1": {}, "2": {}, "3": {}, "4": {}, "5": {}}
|
||||
for posIdx in range(1, 5 + 1):
|
||||
if posIdx <= 2:
|
||||
mainAffix = "生命值" if posIdx == 1 else "攻击力"
|
||||
maxMark[str(posIdx)]["main"] = 0
|
||||
maxMark[str(posIdx)]["total"] = 0
|
||||
else:
|
||||
avalMainAffix = {k: v for k, v in affixWeight.items() if k in MAIN_AFFIXS[str(posIdx)]}
|
||||
|
||||
print(f"posIdx:{posIdx} mainAffix:\n{avalMainAffix}")
|
||||
mainAffix = list(avalMainAffix)[0]
|
||||
maxMark[str(posIdx)]["main"] = affixWeight[mainAffix]
|
||||
maxMark[str(posIdx)]["total"] = affixWeight[mainAffix] * 2
|
||||
maxSubAffixs = {k: v for k, v in affixWeight.items() if k in SUB_AFFIXS and k != mainAffix and affixWeight.get(k)}
|
||||
|
||||
print(f"posIdx:{posIdx} subAffix:\n{maxSubAffixs}")
|
||||
maxMark[str(posIdx)]["total"] += sum(affixWeight[k] * (1 if kIdx else 6) for kIdx, k in enumerate(list(maxSubAffixs)[:4]))
|
||||
|
||||
print(f"「{char}」角色词条配置:\naffixWeight:\n {affixWeight}\npointMark:\n {pointMark}\nmaxMark:\n {maxMark}")
|
||||
|
||||
return affixWeight, pointMark, maxMark
|
||||
|
||||
|
||||
async def getPanelMsg(uid: str, char: str = "all", refresh: bool = False) -> Dict:
|
||||
"""
|
||||
原神游戏内角色展柜消息生成,针对原始数据进行文本翻译和结构重排。
|
||||
|
||||
* ``param uid: str`` 指定查询用户 UID
|
||||
* ``param char: str = "all"`` 指定查询角色
|
||||
* ``param refresh: bool = False`` 指定是否强制刷新数据
|
||||
* ``param uid: str`` 查询用户 UID
|
||||
* ``param char: str = "全部"`` 查询角色名
|
||||
- ``return: Dict`` 查询结果。出错时返回 ``{"error": "错误信息"}``
|
||||
"""
|
||||
# 获取查询角色 ID
|
||||
name2id = json.loads((LOCAL_DIR / "name2id.json").read_text(encoding="utf-8"))
|
||||
charId = "000" if char == "all" else name2id.get(char, "阿巴")
|
||||
if not charId.isdigit():
|
||||
return {"error": f"「{char}」是哪个角色?"}
|
||||
# 获取面板数据
|
||||
raw = await getRawData(uid, charId=charId, refresh=refresh, name2id=name2id)
|
||||
if raw.get("error"):
|
||||
return raw
|
||||
if char == "all":
|
||||
return {
|
||||
"msg": f"成功获取了 UID{uid} 的{'、'.join(raw['list'])}等 {len(raw['list'])} 位角色数据!"
|
||||
}
|
||||
|
||||
# 加载模板、翻译等资源
|
||||
tpl = (LOCAL_DIR / "tpl.html").read_text(encoding="utf-8")
|
||||
loc = json.loads((LOCAL_DIR / "TextMapCHS.json").read_text(encoding="utf-8"))
|
||||
characters = json.loads((LOCAL_DIR / "characters.json").read_text(encoding="utf-8"))
|
||||
propData, equipData = raw["fightPropMap"], raw["equipList"]
|
||||
# 加载角色数据(抽卡图片、命座、技能图标配置等)
|
||||
charData = characters[str(raw["avatarId"])]
|
||||
# 加载角色词条配置
|
||||
base = {"生命值": propData["1"], "攻击力": propData["4"], "防御力": propData["7"]}
|
||||
affixWeight, pointMark, maxMark = await getAffixCfg(char, base)
|
||||
dlTasks = [] # 所有待下载任务
|
||||
|
||||
# 准备好了吗,要开始了哦!
|
||||
|
||||
# 元素背景
|
||||
tpl = tpl.replace("{{elem_type}}", f"elem_{ELEM[charData['Element']]}")
|
||||
|
||||
# 角色大图
|
||||
charImgName = (
|
||||
charData["Costumes"][str(raw["costumeId"])]["art"]
|
||||
if raw.get("costumeId")
|
||||
else charData["SideIconName"].replace(
|
||||
"UI_AvatarIcon_Side", "UI_Gacha_AvatarImg"
|
||||
)
|
||||
)
|
||||
charImg = LOCAL_DIR / char / f"{charImgName}.png"
|
||||
dlTasks.append(
|
||||
download(f"https://enka.network/ui/{charImgName}.png", local=charImg)
|
||||
)
|
||||
tpl = tpl.replace("{{char_img}}", str(charImg.resolve().as_posix()))
|
||||
|
||||
# 角色信息
|
||||
tpl = tpl.replace(
|
||||
"<!--char_info-->",
|
||||
f"""
|
||||
<div class="char-name">{char}</div>
|
||||
<div class="char-lv">
|
||||
<span class="uid">UID {uid}</span>
|
||||
Lv.{raw["propMap"]["4001"]["val"]}
|
||||
<span class="fetter">♥ {raw["fetterInfo"]["expLevel"]}</span>
|
||||
</div>
|
||||
""",
|
||||
)
|
||||
|
||||
# 命座数据
|
||||
consActivated, consHtml = len(raw.get("talentIdList", [])), []
|
||||
for cIdx, consImgName in enumerate(charData["Consts"]):
|
||||
# 图像下载及模板替换
|
||||
consImg = LOCAL_DIR / char / f"{consImgName}.png"
|
||||
dlTasks.append(
|
||||
download(f"https://enka.network/ui/{consImgName}.png", local=consImg)
|
||||
)
|
||||
consHtml.append(
|
||||
f"""
|
||||
<div class="cons-item">
|
||||
<div class="talent-icon {"off" if cIdx + 1 > consActivated else ""}">
|
||||
<div class="talent-icon-img" style="background-image:url({str(consImg.resolve().as_posix())})"></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
tpl = tpl.replace("<!--cons_data-->", "".join(consHtml))
|
||||
|
||||
# 技能数据
|
||||
extraLevels = {k[-1]: v for k, v in raw.get("proudSkillExtraLevelMap", {}).items()}
|
||||
for idx, skillId in enumerate(charData["SkillOrder"]):
|
||||
# 实际技能等级、显示技能等级
|
||||
level = raw["skillLevelMap"][str(skillId)]
|
||||
currentLvl = level + extraLevels.get(list(SKILL)[idx], 0)
|
||||
# 图像下载及模板替换
|
||||
skillImgName = charData["Skills"][str(skillId)]
|
||||
skillImg = LOCAL_DIR / char / f"{skillImgName}.png"
|
||||
dlTasks.append(
|
||||
download(f"https://enka.network/ui/{skillImgName}.png", local=skillImg)
|
||||
)
|
||||
tpl = tpl.replace(
|
||||
f"<!--skill_{list(SKILL.values())[idx]}-->",
|
||||
f"""
|
||||
<div class="talent-icon {"talent-plus" if currentLvl > level else ""} {"talent-crown" if level == 10 else ""}">
|
||||
<div class="talent-icon-img" style="background-image:url({str(skillImg.resolve().as_posix())})"></div>
|
||||
<span>{currentLvl}</span>
|
||||
</div>
|
||||
""",
|
||||
)
|
||||
|
||||
# 面板数据
|
||||
# 显示物理伤害加成或元素伤害加成中数值最高者
|
||||
phyDmg = round(propData["30"] * 100, 1)
|
||||
elemDmg = sorted(
|
||||
[{"type": DMG[d], "value": round(propData[d] * 100, 1)} for d in DMG],
|
||||
key=lambda x: x["value"],
|
||||
reverse=True,
|
||||
)[0]
|
||||
if phyDmg > elemDmg["value"]:
|
||||
dmgType, dmgValue = "物理伤害加成", phyDmg
|
||||
elif elemDmg["value"] == 0:
|
||||
dmgType, dmgValue = f"{ELEM[charData['Element']]}元素伤害加成", 0
|
||||
# 总是先读取一遍缓存
|
||||
cache = LOCAL_DIR / "cache" / f"{uid}.json"
|
||||
if cache.exists():
|
||||
cacheData = json.loads(cache.read_text(encoding="utf-8"))
|
||||
nextQueryTime = cacheData["next"]
|
||||
else:
|
||||
dmgType, dmgValue = f"{elemDmg['type']}元素伤害加成", elemDmg["value"]
|
||||
# 模板替换,奶妈角色额外显示治疗加成,元素伤害异常时评分权重显示提醒
|
||||
tpl = tpl.replace(
|
||||
"<!--fight_prop-->",
|
||||
f"""
|
||||
<li>生命值
|
||||
{("<code>" + str(affixWeight["生命值百分比"]) + "</code>") if affixWeight.get("生命值百分比") else ""}
|
||||
<strong>{round(propData["2000"])}</strong>
|
||||
<span><font>{round(propData["1"])}</font>+{round(propData["2000"] - propData["1"])}</span>
|
||||
</li>
|
||||
<li>攻击力
|
||||
{("<code>" + str(affixWeight["攻击力百分比"]) + "</code>") if affixWeight.get("攻击力百分比") else ""}
|
||||
<strong>{round(propData["2001"])}</strong>
|
||||
<span><font>{round(propData["4"])}</font>+{round(propData["2001"] - propData["4"])}</span>
|
||||
</li>
|
||||
<li>防御力
|
||||
{("<code>" + str(affixWeight["防御力百分比"]) + "</code>") if affixWeight.get("防御力百分比") else ""}
|
||||
<strong>{round(propData["2002"])}</strong>
|
||||
<span><font>{round(propData["7"])}</font>+{round(propData["2002"] - propData["7"])}</span>
|
||||
</li>
|
||||
<li>暴击率
|
||||
{("<code>" + str(affixWeight["暴击率"]) + "</code>") if affixWeight.get("暴击率") else ""}
|
||||
<strong>{round(propData["20"] * 100, 1)}%</strong>
|
||||
</li>
|
||||
<li>暴击伤害
|
||||
{("<code>" + str(affixWeight["暴击伤害"]) + "</code>") if affixWeight.get("暴击伤害") else ""}
|
||||
<strong>{round(propData["22"] * 100, 1)}%</strong>
|
||||
</li>
|
||||
<li>元素精通
|
||||
{("<code>" + str(affixWeight["元素精通"]) + "</code>") if affixWeight.get("元素精通") else ""}
|
||||
<strong>{round(propData["28"])}</strong>
|
||||
</li>
|
||||
{f'''<li>治疗加成
|
||||
{("<code>" + str(affixWeight["治疗加成"]) + "</code>")}
|
||||
<strong>{round(propData["26"] * 100, 1)}%</strong>
|
||||
</li>''' if affixWeight.get("治疗加成") else ""}
|
||||
<li>元素充能效率
|
||||
{("<code>" + str(affixWeight["元素充能效率"]) + "</code>") if affixWeight.get("元素充能效率") else ""}
|
||||
<strong>{round(propData["23"] * 100, 1)}%</strong>
|
||||
</li>
|
||||
<li>{dmgType}
|
||||
{
|
||||
(
|
||||
"<code" +
|
||||
(
|
||||
' style="background-color: rgba(240, 6, 6, 0.7)"'
|
||||
if dmgType[0] not in ["物", ELEM[charData['Element']]]
|
||||
else ""
|
||||
) + ">" + str(affixWeight[dmgType[-6:]]) + "</code>"
|
||||
)
|
||||
if affixWeight.get(dmgType[-6:])
|
||||
else ""
|
||||
}
|
||||
<strong>{dmgValue}%</strong>
|
||||
</li>
|
||||
""",
|
||||
)
|
||||
cacheData, nextQueryTime = {}, 0
|
||||
|
||||
# 装备数据(圣遗物、武器)
|
||||
equipsMark, equipsCnt = 0.0, 0
|
||||
for equip in equipData:
|
||||
if equip["flat"]["itemType"] == "ITEM_WEAPON":
|
||||
# 武器精炼等级
|
||||
affixCnt = list(equip["weapon"].get("affixMap", {".": 0}).values())[0] + 1
|
||||
# 图像下载及模板替换
|
||||
weaponImgName = equip["flat"]["icon"]
|
||||
weaponImg = LOCAL_DIR / "weapon" / f"{weaponImgName}.png"
|
||||
dlTasks.append(
|
||||
download(
|
||||
f"https://enka.network/ui/{weaponImgName}.png", local=weaponImg
|
||||
)
|
||||
)
|
||||
tpl = tpl.replace(
|
||||
"<!--weapon-->",
|
||||
f"""
|
||||
<img src="{str(weaponImg.resolve())}" />
|
||||
<div class="head">
|
||||
<strong>{loc.get(equip["flat"]["nameTextMapHash"], "缺少翻译")}</strong>
|
||||
<div class="star star-{equip["flat"]["rankLevel"]}"></div>
|
||||
<span>Lv.{equip["weapon"]["level"]} <span class="affix affix-{affixCnt}">精{affixCnt}</span></span>
|
||||
</div>
|
||||
""",
|
||||
)
|
||||
elif equip["flat"]["itemType"] == "ITEM_RELIQUARY":
|
||||
mainProp = equip["flat"]["reliquaryMainstat"] # type: Dict
|
||||
subProps = equip["flat"].get("reliquarySubstats", []) # type: List
|
||||
posIdx = POS.index(equip["flat"]["equipType"]) + 1
|
||||
# 主词条得分(与副词条计算规则一致,但只取 25%),角色元素属性与伤害属性不同时不得分,不影响物理伤害得分
|
||||
calcMain = (
|
||||
0.0
|
||||
if posIdx < 3
|
||||
else pointMark.get(
|
||||
PROP[mainProp["mainPropId"]].replace(ELEM[charData["Element"]], ""),
|
||||
0,
|
||||
)
|
||||
* mainProp["statValue"]
|
||||
* 46.6
|
||||
/ 6
|
||||
/ 100
|
||||
/ 4
|
||||
)
|
||||
# 副词条得分
|
||||
calcSubs = [
|
||||
# [词条名, 词条数值, 词条得分]
|
||||
[
|
||||
PROP[s["appendPropId"]],
|
||||
s["statValue"],
|
||||
pointMark.get(PROP[s["appendPropId"]], 0)
|
||||
* s["statValue"]
|
||||
* 46.6
|
||||
/ 6
|
||||
/ 100,
|
||||
]
|
||||
for s in subProps
|
||||
]
|
||||
# 主词条收益系数(百分数),沙杯头位置主词条不正常时对圣遗物总分进行惩罚,最多扣除 50% 总分
|
||||
calcMainPct = (
|
||||
100
|
||||
if posIdx < 3
|
||||
else (
|
||||
100
|
||||
- 50
|
||||
* (
|
||||
1
|
||||
- pointMark.get(
|
||||
PROP[mainProp["mainPropId"]].replace(
|
||||
ELEM[charData["Element"]], ""
|
||||
),
|
||||
0,
|
||||
if int(time()) <= nextQueryTime:
|
||||
logger.info("UID{} 的角色展柜数据刷新冷却还有 {} 秒!".format(uid, nextQueryTime - int(time())))
|
||||
else:
|
||||
logger.info(f"UID{uid} 的角色展柜数据正在刷新!")
|
||||
newData = await queryPanelApi(uid)
|
||||
if not cacheData and newData.get("error"):
|
||||
return newData
|
||||
elif not newData.get("error"):
|
||||
avatarsCache = {str(x["id"]): x for x in cacheData.get("avatars", [])}
|
||||
now, wait4Dmg, avatars, avatarIdsNew = int(time()), {}, [], []
|
||||
for newAvatar in newData["avatarInfoList"]:
|
||||
if newAvatar["avatarId"] in [10000005, 10000007]:
|
||||
logger.info("旅行者面板查询暂未支持!")
|
||||
continue
|
||||
tmp, gotDmg = await transFromEnka(newAvatar, now), False
|
||||
|
||||
if str(tmp["id"]) in avatarsCache:
|
||||
# 保留旧的伤害计算数据
|
||||
avatarsCache[str(tmp["id"])].pop("time")
|
||||
cacheDmg = avatarsCache[str(tmp["id"])].pop("damage")
|
||||
nowStat = {
|
||||
k: v for k, v in tmp.items() if k not in ["damage", "time"]
|
||||
}
|
||||
if cacheDmg and avatarsCache[str(tmp["id"])] == nowStat:
|
||||
logger.info(f"UID{uid} 的 {tmp['name']} 伤害计算结果无需刷新!")
|
||||
tmp["damage"], gotDmg = cacheDmg, True
|
||||
else:
|
||||
logger.info(
|
||||
"UID{} 的 {} 数据发生变化:\n{}\n{}".format(
|
||||
uid, tmp["name"], avatarsCache[str(tmp["id"])], nowStat
|
||||
)
|
||||
)
|
||||
* mainProp["statValue"]
|
||||
/ maxMark[str(posIdx)]["main"]
|
||||
/ 2
|
||||
/ 4
|
||||
avatarIdsNew.append(tmp["id"])
|
||||
avatars.append(tmp)
|
||||
if not gotDmg:
|
||||
wait4Dmg[str(len(avatars) - 1)] = tmp
|
||||
|
||||
if wait4Dmg:
|
||||
logger.info(
|
||||
"正在为 UID{} 的 {} 重新请求伤害计算接口".format(
|
||||
uid, "/".join(f"[{aI}]{a['name']}" for aI, a in wait4Dmg.items())
|
||||
)
|
||||
)
|
||||
)
|
||||
# 总分对齐系数(百分数),按满分 66 对齐各位置圣遗物的总分
|
||||
calcTotalPct = 66 / (maxMark[str(posIdx)]["total"] * 46.6 / 6 / 100) * 100
|
||||
# 最终圣遗物总分
|
||||
calcTotal = (
|
||||
(calcMain + sum(s[2] for s in calcSubs))
|
||||
* calcMainPct
|
||||
/ 100
|
||||
* calcTotalPct
|
||||
/ 100
|
||||
)
|
||||
# 最终圣遗物评级
|
||||
calcRankStr = (
|
||||
[
|
||||
r[0]
|
||||
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 calcTotal <= r[1]
|
||||
][0]
|
||||
if calcTotal <= 66
|
||||
else "E"
|
||||
)
|
||||
# 累积圣遗物套装评分和计数器
|
||||
equipsMark += calcTotal
|
||||
equipsCnt += 1
|
||||
# 图像下载及模板替换
|
||||
artiImgName = equip["flat"]["icon"]
|
||||
artiImg = LOCAL_DIR / "artifacts" / f"{artiImgName}.png"
|
||||
dlTasks.append(
|
||||
download(f"https://enka.network/ui/{artiImgName}.png", local=artiImg)
|
||||
)
|
||||
tpl = tpl.replace(
|
||||
f"<!--arti_{posIdx}-->",
|
||||
f"""
|
||||
<div class="arti-icon">
|
||||
<img src="{str(artiImg.resolve())}" />
|
||||
<span>+{equip["reliquary"]["level"] - 1}</span>
|
||||
</div>
|
||||
<div class="head">
|
||||
<strong>{loc.get(equip["flat"]["nameTextMapHash"], "缺少翻译")}</strong>
|
||||
<span class="mark mark-{calcRankStr}"><span>{round(calcTotal, 1)}分</span> - {calcRankStr}</span>
|
||||
</div>
|
||||
<ul class="detail attr">
|
||||
<li class="arti-main">
|
||||
<span class="title">{kStr(PROP[mainProp["mainPropId"]])}</span>
|
||||
<span class="val">+{vStr(PROP[mainProp["mainPropId"]], mainProp["statValue"])}</span>
|
||||
<span class="{"mark" if calcMain else "val"}"> {round(calcMain, 1) if calcMain else "-"} </span>
|
||||
</li>
|
||||
{"".join(
|
||||
'''<li class="{}"><span class="title">{}</span><span class="val">+{}</span>
|
||||
<span class="mark">{}</span>
|
||||
</li>'''.format(
|
||||
"great" if affixWeight.get(f'{s[0]}百分比' if s[0] in ["生命值", "攻击力", "防御力"] else s[0], 0) > 79.9 else ("useful" if s[2] else "nouse"),
|
||||
kStr(s[0]), vStr(s[0], s[1]), round(s[2], 1)
|
||||
) for s in calcSubs
|
||||
)}
|
||||
</ul>
|
||||
<ul class="detail attr mark-calc">
|
||||
{f'''
|
||||
<li class="result">
|
||||
<span class="title">主词条收益系数</span>
|
||||
<span class="val">
|
||||
* {round(calcMainPct, 1)}%
|
||||
</span>
|
||||
</li>''' if posIdx >= 3 else ""}
|
||||
<li class="result">
|
||||
<span class="title">总分对齐系数</span>
|
||||
<span class="val">* {round(calcTotalPct, 1)}%</span>
|
||||
</li>
|
||||
</ul>
|
||||
""",
|
||||
)
|
||||
# 深拷贝避免转换对上下文中的 avatars 产生影响
|
||||
wtf = deepcopy([a for _, a in wait4Dmg.items()])
|
||||
teyvatBody = await transToTeyvat(wtf, uid)
|
||||
teyvatRaw = await queryDamageApi(teyvatBody)
|
||||
if teyvatRaw.get("code", "x") != 200 or len(wait4Dmg) != len(
|
||||
teyvatRaw.get("result", [])
|
||||
):
|
||||
logger.error(
|
||||
(
|
||||
f"UID{uid} 的 {len(wait4Dmg)} 位角色伤害计算请求失败!"
|
||||
f"\n>>>> [提瓦特返回] {teyvatRaw}"
|
||||
)
|
||||
)
|
||||
else:
|
||||
for dmgIdx, dmgData in enumerate(teyvatRaw.get("result", [])):
|
||||
aIdx = int(list(wait4Dmg.keys())[dmgIdx])
|
||||
avatars[aIdx]["damage"] = await simplDamageRes(dmgData)
|
||||
|
||||
# # 评分时间
|
||||
# tpl = tpl.replace("<!--time-->", f"@ {strftime('%m-%d %H:%M', localtime(raw['time']))}")
|
||||
# 圣遗物总分
|
||||
equipsMarkLevel = (
|
||||
[
|
||||
r[0]
|
||||
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],
|
||||
cacheData["avatars"] = [
|
||||
*avatars,
|
||||
*[
|
||||
aData
|
||||
for _, aData in avatarsCache.items()
|
||||
if aData["id"] not in avatarIdsNew
|
||||
],
|
||||
]
|
||||
if equipsMark / equipsCnt <= r[1]
|
||||
][0]
|
||||
if equipsCnt and equipsMark <= 66 * equipsCnt
|
||||
else "E"
|
||||
)
|
||||
tpl = tpl.replace("{{total_mark_lvl}}", equipsMarkLevel)
|
||||
tpl = tpl.replace("{{total_mark}}", str(round(equipsMark, 1)))
|
||||
cacheData["next"] = now + newData["ttl"]
|
||||
cache.write_text(
|
||||
json.dumps(cacheData, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
|
||||
# 下载所有图片
|
||||
# 获取所需角色数据
|
||||
if char == "全部":
|
||||
return cacheData
|
||||
searchRes = [x for x in cacheData["avatars"] if x["name"] == char]
|
||||
return (
|
||||
{
|
||||
"error": "玩家 {} 游戏内展柜中的 {} 位角色中没有 {}!".format(
|
||||
uid, len(cacheData["avatars"]), char
|
||||
)
|
||||
}
|
||||
if not searchRes
|
||||
else searchRes[0]
|
||||
)
|
||||
|
||||
|
||||
async def getPanel(uid: str, char: str = "全部") -> Union[bytes, str]:
|
||||
"""
|
||||
原神游戏内角色展柜消息生成入口
|
||||
|
||||
* ``param uid: str`` 查询用户 UID
|
||||
* ``param char: str = "全部"`` 查询角色
|
||||
- ``return: Union[bytes, str]`` 查询结果。一般返回图片字节,出错时返回错误信息字符串
|
||||
"""
|
||||
# 获取面板数据
|
||||
data = await getAvatarData(uid, char)
|
||||
if data.get("error"):
|
||||
return data["error"]
|
||||
if char == "全部":
|
||||
return "成功获取了 UID{} 的{}等 {} 位角色数据!".format(
|
||||
uid, "、".join(a["name"] for a in data["avatars"]), len(data["avatars"])
|
||||
)
|
||||
|
||||
# 图标下载任务
|
||||
dlTasks = [
|
||||
download(data["icon"], local=char),
|
||||
download(data["gachaAvatarImg"], local=char),
|
||||
*[download(sData["icon"], local=char) for _, sData in data["skills"].items()],
|
||||
*[download(conData["icon"], local=char) for conData in data["consts"]],
|
||||
download(data["weapon"]["icon"], local="weapon"),
|
||||
*[download(relicData["icon"], local="artifacts") for relicData in data["relics"]],
|
||||
]
|
||||
await asyncio.gather(*dlTasks)
|
||||
dlTasks.clear()
|
||||
|
||||
# 渲染截图
|
||||
tmpFile = LOCAL_DIR / f"{uid}-{char}.html"
|
||||
tmpFile.write_text(tpl, encoding="utf-8")
|
||||
print("启动浏览器截图..")
|
||||
browser = await getBrowser()
|
||||
if not browser:
|
||||
return {"error": "无法生成图片!"}
|
||||
data["fightProp"] = await simplFightProp(
|
||||
data["fightProp"], data["baseProp"], char, data["element"]
|
||||
)
|
||||
htmlBase = str(LOCAL_DIR.resolve())
|
||||
return await template_to_pic(
|
||||
template_path=htmlBase,
|
||||
template_name=f"panel-{TPL_VERSION}.html",
|
||||
templates={"css": TPL_VERSION, "uid": uid, "data": data},
|
||||
pages={
|
||||
"device_scale_factor": SCALE_FACTOR,
|
||||
"viewport": {"width": 600, "height": 300},
|
||||
"base_url": f"file://{htmlBase}",
|
||||
},
|
||||
wait=2,
|
||||
)
|
||||
|
||||
|
||||
async def getTeam(uid: str, chars: List[str] = []) -> Union[bytes, str]:
|
||||
"""
|
||||
队伍伤害消息生成入口
|
||||
|
||||
* ``param uid: str`` 查询用户 UID
|
||||
* ``param chars: List[str] = []`` 查询角色,为空默认数据中前四个
|
||||
- ``return: Union[bytes, str]`` 查询结果。一般返回图片字节,出错时返回错误信息字符串
|
||||
"""
|
||||
# 获取面板数据
|
||||
data = await getAvatarData(uid, "全部")
|
||||
if data.get("error"):
|
||||
return data["error"]
|
||||
|
||||
if chars:
|
||||
extract = [a for a in data["avatars"] if a["name"] in chars]
|
||||
if len(extract) != len(chars):
|
||||
gotThis = [a["name"] for a in extract]
|
||||
return "玩家 {} 的最新数据中未发现{}!".format(
|
||||
uid, "、".join(c for c in chars if c not in gotThis)
|
||||
)
|
||||
elif len(data["avatars"]) >= 4:
|
||||
extract = data["avatars"][:4]
|
||||
logger.info(
|
||||
"UID{} 未指定队伍,自动选择面板中前 4 位进行计算:{} ...".format(
|
||||
uid, "、".join(a["name"] for a in extract)
|
||||
)
|
||||
)
|
||||
else:
|
||||
return f"玩家 {uid} 的面板数据甚至不足以组成一支队伍呢!"
|
||||
|
||||
# 图片下载任务
|
||||
for tmp in extract:
|
||||
dlTasks = [
|
||||
download(tmp["icon"], local=tmp["name"]),
|
||||
*[
|
||||
download(sData["icon"], local=tmp["name"])
|
||||
for _, sData in tmp["skills"].items()
|
||||
],
|
||||
download(tmp["weapon"]["icon"], local="weapon"),
|
||||
*[
|
||||
download(
|
||||
f"UI_RelicIcon_{relicData['icon'].split('_')[-2]}_4",
|
||||
local="artifacts",
|
||||
)
|
||||
for relicData in tmp["relics"]
|
||||
],
|
||||
]
|
||||
await asyncio.gather(*dlTasks)
|
||||
dlTasks.clear()
|
||||
|
||||
teyvatBody = await transToTeyvat(deepcopy(extract), uid)
|
||||
teyvatRaw = await queryDamageApi(teyvatBody, "team")
|
||||
if teyvatRaw.get("code", "x") != 200 or not teyvatRaw.get("result"):
|
||||
logger.error(
|
||||
(f"UID{uid} 的 {len(extract)} 位角色队伍伤害计算请求失败!" f"\n>>>> [提瓦特返回] {teyvatRaw}")
|
||||
)
|
||||
return f"玩家 {uid} 队伍伤害计算失败,接口可能发生变动!" if teyvatRaw else "啊哦,队伍伤害计算小程序状态异常!"
|
||||
try:
|
||||
page = await browser.new_page()
|
||||
await page.set_viewport_size({"width": 1000, "height": 1500})
|
||||
await page.goto("file://" + str(tmpFile.resolve()), timeout=50000)
|
||||
card = await page.query_selector("body")
|
||||
assert card is not None
|
||||
picBytes = await card.screenshot(timeout=50000)
|
||||
bio = io.BytesIO(picBytes)
|
||||
bio.name = "card.png"
|
||||
print(f"图片大小 {len(picBytes)} 字节")
|
||||
# _ = Image.open(BytesIO(picBytes)).save(
|
||||
# str(tmpFile.resolve()).replace(".html", ".png")
|
||||
# )
|
||||
await page.close()
|
||||
tmpFile.unlink(missing_ok=True)
|
||||
return {"pic": bio}
|
||||
data = await simplTeamDamageRes(
|
||||
teyvatRaw["result"], {a["name"]: a for a in extract}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"生成角色圣遗物评分图片失败 {type(e)}:{e}")
|
||||
return {"error": "生成角色圣遗物评分总图失败!"}
|
||||
logger.opt(exception=e).error("队伍伤害数据解析出错")
|
||||
return f"[{e.__class__.__name__}] 队伍伤害数据解析出错咯"
|
||||
|
||||
htmlBase = str(LOCAL_DIR.resolve())
|
||||
return await template_to_pic(
|
||||
template_path=htmlBase,
|
||||
template_name=f"team-{TPL_VERSION}.html",
|
||||
templates={"css": TPL_VERSION, "data": data},
|
||||
pages={
|
||||
"device_scale_factor": SCALE_FACTOR,
|
||||
"viewport": {"width": 600, "height": 300},
|
||||
"base_url": f"file://{htmlBase}",
|
||||
},
|
||||
wait=2,
|
||||
)
|
||||
|
@ -3,6 +3,8 @@ from pyrogram.types import Message
|
||||
from defs.bind import check_bind, set_bind, remove_bind
|
||||
from ci import app, me
|
||||
|
||||
uidStart = ["1", "2", "5", "6", "7", "8", "9"]
|
||||
|
||||
|
||||
@app.on_message(filters.command(["bind", f"bind@{me['result']['username']}"]) & filters.private)
|
||||
async def bind_command(_: Client, message: Message):
|
||||
@ -11,12 +13,14 @@ async def bind_command(_: Client, message: Message):
|
||||
return await message.reply("使用 `/info [uid(可选)]` 查询角色缓存状态", quote=True)
|
||||
else:
|
||||
return await message.reply("请使用 <code>/bind [uid]</code> 绑定游戏 uid", quote=True)
|
||||
if not message.command[1].isdigit():
|
||||
if message.command[1] == "remove":
|
||||
uid = message.command[1]
|
||||
if not uid.isdigit():
|
||||
if uid == "remove":
|
||||
remove_bind(message.from_user.id)
|
||||
return await message.reply("已解除绑定", quote=True)
|
||||
return await message.reply("uid 非数字", quote=True)
|
||||
uid = message.command[1]
|
||||
elif uid[0] not in uidStart or len(uid) != 9:
|
||||
return await message.reply("uid 格式错误", quote=True)
|
||||
set_bind(message.from_user.id, uid)
|
||||
await message.reply(f"绑定成功,您绑定的游戏 uid 为:{uid}\n\n"
|
||||
f"请将角色放入展柜,开启显示详细信息后,使用 /refresh 刷新数据。", quote=True)
|
||||
|
@ -13,7 +13,6 @@ async def answer_callback(_: Client, callback_query: CallbackQuery):
|
||||
uid = data[0]
|
||||
char = callback_query.data.split("|")[1] if len(data) > 1 else None
|
||||
data = Player(uid)
|
||||
data.restore()
|
||||
if not data.all_char:
|
||||
return await callback_query.answer("没有可展示的角色,可能是数据未刷新", show_alert=True)
|
||||
if not char:
|
||||
@ -21,9 +20,9 @@ async def answer_callback(_: Client, callback_query: CallbackQuery):
|
||||
InputMediaPhoto(media=f"resources{sep}Kitsune.png",
|
||||
caption=f"请选择 {data.name} 的一个角色:"),
|
||||
reply_markup=data.gen_keyboard())
|
||||
if char_data := next((i for i in data.all_char if i.get("name", "") == char), None):
|
||||
if char_data := next((i for i in data.all_char if i.name == char), None):
|
||||
await callback_query.message.edit_media(
|
||||
InputMediaPhoto(media=char_data["file_id"]),
|
||||
InputMediaPhoto(media=char_data.file_id),
|
||||
reply_markup=data.gen_back())
|
||||
else:
|
||||
return await callback_query.answer("没有可展示的角色,可能是数据未刷新", show_alert=True)
|
||||
|
@ -13,5 +13,10 @@ async def info_command(_: Client, message: Message):
|
||||
if len(message.command) > 1 and message.command[1].isnumeric():
|
||||
uid = message.command[1]
|
||||
data = Player(uid)
|
||||
data.restore()
|
||||
return await message.reply(f"游戏 uid 为 {uid} 的角色缓存有:\n\n" f"{data.gen_all_char()}", quote=True) if data.all_char else await message.reply("没有可展示的角色,可能是数据未刷新", quote=True)
|
||||
return await message.reply(
|
||||
f"游戏 uid 为 {uid} 的角色缓存有:\n\n{data.gen_all_char()}",
|
||||
quote=True,
|
||||
) if data.all_char else await message.reply(
|
||||
"没有可展示的角色,可能是数据未刷新",
|
||||
quote=True,
|
||||
)
|
||||
|
@ -20,18 +20,20 @@ async def answer_callback(_: Client, query: InlineQuery):
|
||||
switch_pm_parameter="start",
|
||||
)
|
||||
data = Player(uid)
|
||||
data.restore()
|
||||
if not data.all_char:
|
||||
return await query.answer(
|
||||
results=[],
|
||||
switch_pm_text=f'{emoji.CROSS_MARK} 没有搜索到任何结果',
|
||||
switch_pm_parameter="start",
|
||||
)
|
||||
inline_data = []
|
||||
for i in data.all_char:
|
||||
inline_data.append(InlineQueryResultCachedPhoto(photo_file_id=i["file_id"],
|
||||
title=i["name"],
|
||||
description=data.name))
|
||||
await query.answer(inline_data,
|
||||
switch_pm_text=f'{emoji.KEY} 搜索到了 {len(data.all_char)} 个角色',
|
||||
switch_pm_parameter="start")
|
||||
inline_data = [
|
||||
InlineQueryResultCachedPhoto(
|
||||
photo_file_id=i.file_id, title=i.name, description=data.name
|
||||
)
|
||||
for i in data.all_char
|
||||
]
|
||||
await query.answer(
|
||||
inline_data,
|
||||
switch_pm_text=f'{emoji.KEY} 搜索到了 {len(data.all_char)} 个角色',
|
||||
switch_pm_parameter="start",
|
||||
)
|
||||
|
@ -20,10 +20,11 @@ async def search_command(_: Client, message: Message):
|
||||
if not uid:
|
||||
return await message.reply("请使用 /search [uid] 或 /bind [uid] 绑定账号后搜索", quote=True)
|
||||
data = Player(uid)
|
||||
data.restore()
|
||||
if not data.all_char:
|
||||
return await message.reply("没有可展示的角色,可能是数据未刷新", quote=True)
|
||||
await message.reply_photo(f"resources{sep}Kitsune.png",
|
||||
caption=f"请选择 {data.name} 的一个角色:",
|
||||
quote=True,
|
||||
reply_markup=data.gen_keyboard())
|
||||
await message.reply_photo(
|
||||
f"resources{sep}Kitsune.png",
|
||||
caption=f"请选择 {data.name} 的一个角色:",
|
||||
quote=True,
|
||||
reply_markup=data.gen_keyboard(),
|
||||
)
|
||||
|
@ -1,5 +1,8 @@
|
||||
httpx
|
||||
pyrogram==2.0.49
|
||||
httpx~=0.23.0
|
||||
pyrogram==2.0.97
|
||||
TGCrypto
|
||||
sqlitedict
|
||||
playwright
|
||||
sqlitedict~=2.0.0
|
||||
playwright~=1.25.2
|
||||
uvicorn~=0.20.0
|
||||
Jinja2~=3.1.2
|
||||
pydantic~=1.10.4
|
||||
|