This commit is contained in:
xtaodada 2023-01-14 21:59:43 +08:00
parent d80ae3f47c
commit 9308b109cb
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
33 changed files with 2752 additions and 699 deletions

5
ci.py
View File

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

Binary file not shown.

BIN
data/font/tttgbnumber.ttf Normal file

Binary file not shown.

16
data/g2plot.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
data/imgs/bg-anemo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

BIN
data/imgs/bg-cryo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
data/imgs/bg-dendro.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

BIN
data/imgs/bg-electro.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
data/imgs/bg-geo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
data/imgs/bg-hydro.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
data/imgs/bg-pyro.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
data/imgs/talent-anemo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
data/imgs/talent-cryo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
data/imgs/talent-dendro.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
data/imgs/talent-geo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
data/imgs/talent-hydro.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
data/imgs/talent-pyro.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

1114
data/panel-0.2.7.css Normal file

File diff suppressed because it is too large Load Diff

137
data/panel-0.2.7.html Normal file
View 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
View 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
View 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] = []

View File

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

View File

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

View File

@ -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
View 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,
}

View File

@ -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">&hearts; {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,
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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