update
7
.gitignore
vendored
@ -158,5 +158,8 @@ cython_debug/
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
.idea/
|
||||
data/
|
||||
bot.sessio*
|
||||
resources/data/*
|
||||
config.ini
|
||||
|
41
ci.py
Normal file
@ -0,0 +1,41 @@
|
||||
from configparser import RawConfigParser
|
||||
from os import mkdir, sep
|
||||
from os.path import exists
|
||||
|
||||
from pyrogram import Client
|
||||
from httpx import AsyncClient, get
|
||||
from sqlitedict import SqliteDict
|
||||
|
||||
try:
|
||||
import uvloop
|
||||
uvloop.install()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# init folders
|
||||
if not exists("data"):
|
||||
mkdir("data")
|
||||
sqlite = SqliteDict(f"data{sep}data.sqlite", autocommit=True)
|
||||
# 读取配置文件
|
||||
config = RawConfigParser()
|
||||
config.read("config.ini")
|
||||
bot_token: str = ""
|
||||
api_id: int = 0
|
||||
api_hash: str = ""
|
||||
channel_id: int = 0
|
||||
admin_id: int = 0
|
||||
bot_token = config.get("basic", "bot_token", fallback=bot_token)
|
||||
channel_id = config.get("basic", "channel_id", fallback=channel_id)
|
||||
admin_id = config.get("basic", "admin_id", fallback=admin_id)
|
||||
api_id = config.get("pyrogram", "api_id", fallback=api_id)
|
||||
api_hash = config.get("pyrogram", "api_hash", fallback=api_hash)
|
||||
guess_time = 30 # 猜语音游戏持续时间
|
||||
""" Init httpx client """
|
||||
# 使用自定义 UA
|
||||
headers = {
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"
|
||||
}
|
||||
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"})
|
8
config.gen.ini
Normal file
@ -0,0 +1,8 @@
|
||||
[pyrogram]
|
||||
api_id = 12345
|
||||
api_hash = 0123456789abc0123456789abc
|
||||
|
||||
[basic]
|
||||
bot_token = 111:abc
|
||||
channel_id = 0
|
||||
admin_id = 0
|
25
defs/bind.py
Normal file
@ -0,0 +1,25 @@
|
||||
from ci import sqlite
|
||||
|
||||
|
||||
def get_bind_list() -> dict:
|
||||
return sqlite.get("bind", {})
|
||||
|
||||
|
||||
def get_bind_uid(uid: int) -> str:
|
||||
return get_bind_list().get(uid, None)
|
||||
|
||||
|
||||
def set_bind(uid: int, player: str):
|
||||
data = get_bind_list()
|
||||
data[uid] = player
|
||||
sqlite["bind"] = data
|
||||
|
||||
|
||||
def remove_bind(uid: int):
|
||||
data = get_bind_list()
|
||||
data.pop(uid, None)
|
||||
sqlite["bind"] = data
|
||||
|
||||
|
||||
def check_bind(uid: int) -> bool:
|
||||
return get_bind_uid(uid) is not None
|
102
defs/player.py
Normal file
@ -0,0 +1,102 @@
|
||||
import json
|
||||
import time
|
||||
|
||||
from datetime import datetime
|
||||
from os.path import exists
|
||||
from typing import List
|
||||
|
||||
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
||||
from ci import channel_id, app, sqlite
|
||||
from gspanel.__utils__ import LOCAL_DIR
|
||||
from gspanel.data_source import getRawData, getPanelMsg
|
||||
|
||||
|
||||
def gen_char_dict(name: str, file_id: str) -> dict:
|
||||
return {"name": name, "file_id": file_id, "time": int(time.time())}
|
||||
|
||||
|
||||
class Player:
|
||||
name: str = ""
|
||||
uid: str = ""
|
||||
all_char: List[dict] = []
|
||||
time: int = 0
|
||||
|
||||
def __init__(self, uid: str):
|
||||
self.uid = uid
|
||||
self.time = 0
|
||||
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:
|
||||
data = json.load(fp)
|
||||
self.name = 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", "")
|
||||
|
||||
async def update_char(self):
|
||||
all_char = await getRawData(self.uid)
|
||||
all_char = all_char.get("list", [])
|
||||
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)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
continue
|
||||
return f"成功缓存了 UID{self.uid} 的{'、'.join(all_char)}等 {len(all_char)} 位角色数据!"
|
||||
|
||||
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", [])
|
||||
|
||||
def gen_keyboard(self) -> InlineKeyboardMarkup:
|
||||
data = []
|
||||
temp_ = []
|
||||
num = 0
|
||||
for i in self.all_char:
|
||||
name = i.get("name", "")
|
||||
temp_.append(InlineKeyboardButton(name, callback_data=f"{self.uid}|{name}"))
|
||||
num += 1
|
||||
if num == 3:
|
||||
data.append(temp_)
|
||||
temp_ = []
|
||||
num = 0
|
||||
if temp_:
|
||||
data.append(temp_)
|
||||
return InlineKeyboardMarkup(data)
|
||||
|
||||
def gen_back(self) -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup([[InlineKeyboardButton("返回", callback_data=self.uid)]])
|
||||
|
||||
@staticmethod
|
||||
def parse_time(time_stamp: int) -> str:
|
||||
return datetime.strftime(datetime.fromtimestamp(time_stamp), '%Y-%m-%d %H:%M:%S')
|
||||
|
||||
def gen_all_char(self) -> str:
|
||||
if not self.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"
|
||||
return text
|
19
defs/refresh.py
Normal file
@ -0,0 +1,19 @@
|
||||
from defs.player import Player
|
||||
from ci import sqlite
|
||||
import time
|
||||
|
||||
|
||||
async def refresh_player(uid: str) -> str:
|
||||
data = Player(uid)
|
||||
data.restore()
|
||||
if data.time + 60 * 5 > int(time.time()):
|
||||
return "刷新过快,请稍等一会儿再试"
|
||||
text = await data.update_char()
|
||||
if not text:
|
||||
return "数据刷新失败,请重试"
|
||||
try:
|
||||
data.update_name()
|
||||
except FileNotFoundError:
|
||||
return "数据刷新失败,请重试"
|
||||
sqlite[uid] = data.export()
|
||||
return text
|
221
gspanel/__utils__.py
Normal file
@ -0,0 +1,221 @@
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
from httpx import AsyncClient
|
||||
from playwright.async_api import Browser, async_playwright
|
||||
|
||||
GROW_VALUE = { # 词条成长值
|
||||
"暴击率": 3.89,
|
||||
"暴击伤害": 7.77,
|
||||
"元素精通": 23.31,
|
||||
"攻击力百分比": 5.83,
|
||||
"生命值百分比": 5.83,
|
||||
"防御力百分比": 7.29,
|
||||
"元素充能效率": 6.48,
|
||||
"元素伤害加成": 5.825,
|
||||
"物理伤害加成": 7.288,
|
||||
"治疗加成": 4.487,
|
||||
}
|
||||
MAIN_AFFIXS = { # 可能的主词条
|
||||
"3": "攻击力百分比,防御力百分比,生命值百分比,元素精通,元素充能效率".split(","), # EQUIP_SHOES
|
||||
"4": "攻击力百分比,防御力百分比,生命值百分比,元素精通,元素伤害加成,物理伤害加成".split(","), # EQUIP_RING
|
||||
"5": "攻击力百分比,防御力百分比,生命值百分比,元素精通,治疗加成,暴击率,暴击伤害".split(","), # EQUIP_DRESS
|
||||
}
|
||||
SUB_AFFIXS = "攻击力,攻击力百分比,防御力,防御力百分比,生命值,生命值百分比,元素精通,元素充能效率,暴击率,暴击伤害".split(",")
|
||||
# STAR = {"QUALITY_ORANGE": 5, "QUALITY_PURPLE": 4}
|
||||
ELEM = {
|
||||
"Fire": "火",
|
||||
"Water": "水",
|
||||
"Wind": "风",
|
||||
"Electric": "雷",
|
||||
"Grass": "草",
|
||||
"Ice": "冰",
|
||||
"Rock": "岩",
|
||||
}
|
||||
POS = ["EQUIP_BRACER", "EQUIP_NECKLACE", "EQUIP_SHOES", "EQUIP_RING", "EQUIP_DRESS"]
|
||||
SKILL = {"1": "a", "2": "e", "9": "q"}
|
||||
DMG = {
|
||||
"40": "火",
|
||||
"41": "雷",
|
||||
"42": "水",
|
||||
"43": "草",
|
||||
"44": "风",
|
||||
"45": "岩",
|
||||
"46": "冰",
|
||||
}
|
||||
PROP = {
|
||||
"FIGHT_PROP_BASE_ATTACK": "基础攻击力",
|
||||
"FIGHT_PROP_HP": "生命值",
|
||||
"FIGHT_PROP_ATTACK": "攻击力",
|
||||
"FIGHT_PROP_DEFENSE": "防御力",
|
||||
"FIGHT_PROP_HP_PERCENT": "生命值百分比",
|
||||
"FIGHT_PROP_ATTACK_PERCENT": "攻击力百分比",
|
||||
"FIGHT_PROP_DEFENSE_PERCENT": "防御力百分比",
|
||||
"FIGHT_PROP_CRITICAL": "暴击率",
|
||||
"FIGHT_PROP_CRITICAL_HURT": "暴击伤害",
|
||||
"FIGHT_PROP_CHARGE_EFFICIENCY": "元素充能效率",
|
||||
"FIGHT_PROP_HEAL_ADD": "治疗加成",
|
||||
"FIGHT_PROP_ELEMENT_MASTERY": "元素精通",
|
||||
"FIGHT_PROP_PHYSICAL_ADD_HURT": "物理伤害加成",
|
||||
"FIGHT_PROP_FIRE_ADD_HURT": "火元素伤害加成",
|
||||
"FIGHT_PROP_ELEC_ADD_HURT": "雷元素伤害加成",
|
||||
"FIGHT_PROP_WATER_ADD_HURT": "水元素伤害加成",
|
||||
"FIGHT_PROP_WIND_ADD_HURT": "风元素伤害加成",
|
||||
"FIGHT_PROP_ICE_ADD_HURT": "冰元素伤害加成",
|
||||
"FIGHT_PROP_ROCK_ADD_HURT": "岩元素伤害加成",
|
||||
}
|
||||
|
||||
_browser: Optional[Browser] = None
|
||||
|
||||
EXPIRE_SEC = 60 * 5
|
||||
LOCAL_DIR = Path("resources")
|
||||
if not (LOCAL_DIR / "cache").exists():
|
||||
(LOCAL_DIR / "cache").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def kStr(prop: str) -> str:
|
||||
"""转换词条名称为简短形式"""
|
||||
return (
|
||||
prop.replace("百分比", "")
|
||||
.replace("元素充能", "充能")
|
||||
.replace("元素伤害", "伤")
|
||||
.replace("物理伤害", "物伤")
|
||||
)
|
||||
|
||||
|
||||
def vStr(prop: str, value: Union[int, float]) -> str:
|
||||
"""转换词条数值为字符串形式"""
|
||||
if prop in {"生命值", "攻击力", "防御力", "元素精通"}:
|
||||
return str(value)
|
||||
else:
|
||||
return f"{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
|
||||
|
||||
|
||||
async def getBrowser(**kwargs) -> Optional[Browser]:
|
||||
return _browser or await initBrowser(**kwargs)
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
print("正在检查面板插件所需资源...")
|
||||
# 仅首次启用插件下载的文件
|
||||
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",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/bg-dendro.jpg",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/bg-electro.jpg",
|
||||
"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",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/talent-electro.png",
|
||||
"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",
|
||||
]
|
||||
tasks = []
|
||||
for r in initRes:
|
||||
d = r.replace("https://cdn.monsterx.cn/bot/gspanel/", "").split("/")[0]
|
||||
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("面板插件所需资源检查完毕!")
|
||||
|
||||
|
||||
async def download(url: str, local: Union[Path, str] = "") -> Union[Path, None]:
|
||||
"""
|
||||
一般文件下载,通常是即用即下的角色命座图片、技能图片、抽卡大图、圣遗物图片等
|
||||
|
||||
* ``param url: str`` 指定下载链接
|
||||
* ``param local: Union[Path, str] = ""`` 指定本地目标路径,传入类型为 ``Path`` 时视为保存文件完整路径,传入类型为 ``str`` 时视为保存文件子文件夹名(默认下载至插件资源根目录)
|
||||
- ``return: Union[Path, None]`` 本地文件地址,出错时返回空
|
||||
"""
|
||||
if not isinstance(local, Path):
|
||||
d = (LOCAL_DIR / local) if local else LOCAL_DIR
|
||||
if not d.exists():
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
f = d / url.split("/")[-1]
|
||||
else:
|
||||
if not local.parent.exists():
|
||||
local.parent.mkdir(parents=True, exist_ok=True)
|
||||
f = local
|
||||
# 本地文件存在时便不再下载,JSON 文件除外
|
||||
if f.exists() and ".json" not in f.name:
|
||||
return f
|
||||
client = AsyncClient()
|
||||
retryCnt = 3
|
||||
while retryCnt:
|
||||
try:
|
||||
async with client.stream(
|
||||
"GET", url, headers={"user-agent": "NoneBot-GsPanel"}
|
||||
) as res:
|
||||
with open(f, "wb") as fb:
|
||||
async for chunk in res.aiter_bytes():
|
||||
fb.write(chunk)
|
||||
return f
|
||||
except Exception as e:
|
||||
print(f"面板资源 {f.name} 下载出错 {type(e)}:{e}")
|
||||
retryCnt -= 1
|
||||
await asyncio.sleep(2)
|
||||
return None
|
569
gspanel/data_source.py
Normal file
@ -0,0 +1,569 @@
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
|
||||
# from io import BytesIO
|
||||
# from PIL import Image
|
||||
from time import time
|
||||
from typing import Dict, List, Literal, Tuple
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
async def getRawData(
|
||||
uid: str,
|
||||
charId: str = "000",
|
||||
refresh: bool = False,
|
||||
name2id=None,
|
||||
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": "错误信息"}``
|
||||
"""
|
||||
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
|
||||
if 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"
|
||||
async with AsyncClient() as client:
|
||||
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": ( # "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"
|
||||
),
|
||||
},
|
||||
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": "暂时无法访问面板数据接口.."}
|
||||
except Exception as e:
|
||||
# 出错时返回 {"error": "错误信息"}
|
||||
print(f"请求 Enka.Network 出错 {type(e)}:{e}")
|
||||
return {"error": f"[{e.__class__.__name__}]面板数据处理出错辣.."}
|
||||
|
||||
|
||||
async def getAffixCfg(char: str, base: Dict) -> Tuple[Dict, Dict, 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`` 指定是否强制刷新数据
|
||||
- ``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
|
||||
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>
|
||||
""",
|
||||
)
|
||||
|
||||
# 装备数据(圣遗物、武器)
|
||||
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,
|
||||
)
|
||||
* mainProp["statValue"]
|
||||
/ maxMark[str(posIdx)]["main"]
|
||||
/ 2
|
||||
/ 4
|
||||
)
|
||||
)
|
||||
)
|
||||
# 总分对齐系数(百分数),按满分 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>
|
||||
""",
|
||||
)
|
||||
|
||||
# # 评分时间
|
||||
# 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],
|
||||
]
|
||||
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)))
|
||||
|
||||
# 下载所有图片
|
||||
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": "无法生成图片!"}
|
||||
try:
|
||||
page = await browser.new_page()
|
||||
await page.set_viewport_size({"width": 1000, "height": 1500})
|
||||
await page.goto("file://" + str(tmpFile.resolve()), timeout=5000)
|
||||
card = await page.query_selector("body")
|
||||
assert card is not None
|
||||
picBytes = await card.screenshot(timeout=5000)
|
||||
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}
|
||||
except Exception as e:
|
||||
print(f"生成角色圣遗物评分图片失败 {type(e)}:{e}")
|
||||
return {"error": "生成角色圣遗物评分总图失败!"}
|
8
main.py
Normal file
@ -0,0 +1,8 @@
|
||||
import logging
|
||||
from ci import app
|
||||
|
||||
# 日志记录
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logging.getLogger("pyrogram").setLevel(logging.CRITICAL)
|
||||
logging.info("Bot 已启动")
|
||||
app.run()
|
20
plugins/admin.py
Normal file
@ -0,0 +1,20 @@
|
||||
from pyrogram import filters, Client
|
||||
from pyrogram.types import Message
|
||||
from ci import app
|
||||
from defs.refresh import refresh_player
|
||||
from gspanel.__utils__ import fetchInitRes
|
||||
|
||||
|
||||
@app.on_message(filters.command("refresh_admin") & filters.private)
|
||||
async def refresh_admin_command(_: Client, message: Message):
|
||||
if message.from_user.id != 347437156:
|
||||
return
|
||||
await fetchInitRes()
|
||||
if len(message.command) == 1:
|
||||
return await message.reply("请输入 uid", quote=True)
|
||||
if not message.command[1].isnumeric():
|
||||
return await message.reply("请输入正确的 uid", quote=True)
|
||||
uid = message.command[1]
|
||||
msg = await message.reply("正在刷新数据,请稍等。。。", quote=True)
|
||||
text = await refresh_player(uid)
|
||||
await msg.edit(text)
|
22
plugins/bind.py
Normal file
@ -0,0 +1,22 @@
|
||||
from pyrogram import filters, Client
|
||||
from pyrogram.types import Message
|
||||
from defs.bind import check_bind, set_bind, remove_bind
|
||||
from ci import app, me
|
||||
|
||||
|
||||
@app.on_message(filters.command(["bind", f"bind@{me['result']['username']}"]) & filters.private)
|
||||
async def bind_command(_: Client, message: Message):
|
||||
if len(message.command) == 1:
|
||||
if check_bind(message.from_user.id):
|
||||
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":
|
||||
remove_bind(message.from_user.id)
|
||||
return await message.reply("已解除绑定", quote=True)
|
||||
return await message.reply("uid 非数字", quote=True)
|
||||
uid = message.command[1]
|
||||
set_bind(message.from_user.id, uid)
|
||||
await message.reply(f"绑定成功,您绑定的游戏 uid 为:{uid}\n\n"
|
||||
f"请将角色放入展柜,开启显示详细信息后,使用 /refresh 刷新数据。", quote=True)
|
29
plugins/callback.py
Normal file
@ -0,0 +1,29 @@
|
||||
from os import sep
|
||||
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import CallbackQuery, InputMediaPhoto
|
||||
|
||||
from ci import app
|
||||
from defs.player import Player
|
||||
|
||||
|
||||
@app.on_callback_query()
|
||||
async def answer_callback(_: Client, callback_query: CallbackQuery):
|
||||
data = callback_query.data.split("|")
|
||||
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:
|
||||
return await callback_query.message.edit_media(
|
||||
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):
|
||||
await callback_query.message.edit_media(
|
||||
InputMediaPhoto(media=char_data["file_id"]),
|
||||
reply_markup=data.gen_back())
|
||||
else:
|
||||
return await callback_query.answer("没有可展示的角色,可能是数据未刷新", show_alert=True)
|
17
plugins/info.py
Normal file
@ -0,0 +1,17 @@
|
||||
from pyrogram import filters, Client
|
||||
from pyrogram.types import Message
|
||||
from defs.bind import check_bind, get_bind_uid
|
||||
from defs.player import Player
|
||||
from ci import app, me
|
||||
|
||||
|
||||
@app.on_message(filters.command(["info", f"info@{me['result']['username']}"]) & filters.private)
|
||||
async def info_command(_: Client, message: Message):
|
||||
if not check_bind(message.from_user.id):
|
||||
return await message.reply("请使用 <code>/bind [uid]</code> 绑定游戏 uid", quote=True)
|
||||
uid = get_bind_uid(message.from_user.id)
|
||||
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)
|
37
plugins/inline.py
Normal file
@ -0,0 +1,37 @@
|
||||
from pyrogram import Client, emoji
|
||||
from pyrogram.types import InlineQuery, InlineQueryResultCachedPhoto
|
||||
|
||||
from ci import app
|
||||
from defs.bind import check_bind, get_bind_uid
|
||||
from defs.player import Player
|
||||
|
||||
|
||||
@app.on_inline_query()
|
||||
async def answer_callback(_: Client, query: InlineQuery):
|
||||
uid = None
|
||||
if check_bind(query.from_user.id):
|
||||
uid = get_bind_uid(query.from_user.id)
|
||||
if query.query:
|
||||
uid = query.query
|
||||
if not uid:
|
||||
return await query.answer(
|
||||
results=[],
|
||||
switch_pm_text=f'{emoji.CROSS_MARK} 没有搜索到任何结果',
|
||||
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")
|
17
plugins/refresh.py
Normal file
@ -0,0 +1,17 @@
|
||||
from pyrogram import filters, Client
|
||||
from pyrogram.types import Message
|
||||
from defs.bind import check_bind, get_bind_uid
|
||||
from ci import app, me
|
||||
from defs.refresh import refresh_player
|
||||
|
||||
|
||||
@app.on_message(filters.command(["refresh", f"refresh@{me['result']['username']}"]) & filters.private)
|
||||
async def refresh_command(_: Client, message: Message):
|
||||
if not check_bind(message.from_user.id):
|
||||
return await message.reply("请使用 <code>/bind [uid]</code> 绑定游戏 uid", quote=True)
|
||||
uid = get_bind_uid(message.from_user.id)
|
||||
if len(message.command) > 1 and message.command[1].isnumeric():
|
||||
uid = message.command[1]
|
||||
msg = await message.reply("正在刷新数据,请稍等。。。", quote=True)
|
||||
text = await refresh_player(uid)
|
||||
await msg.edit(text)
|
29
plugins/search.py
Normal file
@ -0,0 +1,29 @@
|
||||
from os import sep
|
||||
|
||||
from pyrogram import filters, Client
|
||||
from pyrogram.types import Message
|
||||
|
||||
from defs.bind import check_bind, get_bind_uid
|
||||
from ci import app, me
|
||||
from defs.player import Player
|
||||
|
||||
|
||||
@app.on_message(filters.command(["search", f"search@{me['result']['username']}"]))
|
||||
async def search_command(_: Client, message: Message):
|
||||
if message.sender_chat or not message.from_user:
|
||||
return
|
||||
uid = None
|
||||
if check_bind(message.from_user.id):
|
||||
uid = get_bind_uid(message.from_user.id)
|
||||
if len(message.command) > 1 and message.command[1].isnumeric():
|
||||
uid = message.command[1]
|
||||
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())
|
25
plugins/start.py
Normal file
@ -0,0 +1,25 @@
|
||||
from pyrogram import filters, Client
|
||||
from pyrogram.types import Message
|
||||
from ci import app, me
|
||||
|
||||
des = """
|
||||
你好!{} 我是 [{}]({})
|
||||
|
||||
> 请先使用 `/bind [uid]` 绑定游戏 uid 进行更新数据,然后使用 `/search [uid(可选)]` 获取角色卡片。
|
||||
|
||||
我基于公共 API 提供的数据来合成图片,支持以下数据:
|
||||
`
|
||||
- 等级、天赋、武器、面板数据、圣遗物
|
||||
`
|
||||
角色数据基于 [enka](https://enka.shinshin.moe)
|
||||
图片模板基于 [nonebot-plugin-gspanel](https://github.com/monsterxcn/nonebot-plugin-gspanel)
|
||||
"""
|
||||
|
||||
|
||||
@app.on_message(filters.command(["start", f"start@{me['result']['username']}"]) & filters.private)
|
||||
async def start_command(_: Client, message: Message):
|
||||
await message.reply(des.format(message.from_user.mention(),
|
||||
me["result"]["first_name"],
|
||||
f"https://t.me/{me['result']['username']}"),
|
||||
disable_web_page_preview=True,
|
||||
quote=True)
|
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
httpx
|
||||
pyrogram==2.0.49
|
||||
TGCrypto
|
||||
sqlitedict
|
||||
playwright
|
BIN
resources/Kitsune.png
Normal file
After Width: | Height: | Size: 971 KiB |
276419
resources/TextMapCHS.json
Normal file
2690
resources/cache/190182329__data.json
vendored
Normal file
439
resources/calc-rule.json
Normal file
@ -0,0 +1,439 @@
|
||||
{
|
||||
"神里绫人": {
|
||||
"生命值百分比": 50,
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 30
|
||||
},
|
||||
"八重神子": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"申鹤": {
|
||||
"攻击力百分比": 100,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"云堇": {
|
||||
"防御力百分比": 100,
|
||||
"暴击率": 50,
|
||||
"暴击伤害": 50,
|
||||
"元素伤害加成": 40,
|
||||
"元素充能效率": 90
|
||||
},
|
||||
"荒泷一斗": {
|
||||
"攻击力百分比": 50,
|
||||
"防御力百分比": 100,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 30
|
||||
},
|
||||
"五郎": {
|
||||
"攻击力百分比": 50,
|
||||
"防御力百分比": 100,
|
||||
"暴击率": 50,
|
||||
"暴击伤害": 50,
|
||||
"元素伤害加成": 25,
|
||||
"元素充能效率": 90
|
||||
},
|
||||
"班尼特": {
|
||||
"生命值百分比": 100,
|
||||
"攻击力百分比": 50,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 80,
|
||||
"元素充能效率": 55,
|
||||
"治疗加成": 100
|
||||
},
|
||||
"枫原万叶": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"雷电将军": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 75,
|
||||
"元素充能效率": 90
|
||||
},
|
||||
"行秋": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 75
|
||||
},
|
||||
"钟离": {
|
||||
"生命值百分比": 80,
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"物理伤害加成": 50,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"钟离-血牛": {
|
||||
"生命值百分比": 100,
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 75,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"神里绫华": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 30
|
||||
},
|
||||
"香菱": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"胡桃": {
|
||||
"生命值百分比": 80,
|
||||
"攻击力百分比": 50,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100
|
||||
},
|
||||
"甘雨": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100
|
||||
},
|
||||
"甘雨-永冻": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"温迪": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"珊瑚宫心海": {
|
||||
"生命值百分比": 100,
|
||||
"攻击力百分比": 50,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55,
|
||||
"治疗加成": 100
|
||||
},
|
||||
"莫娜": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 75
|
||||
},
|
||||
"阿贝多": {
|
||||
"防御力百分比": 100,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100
|
||||
},
|
||||
"迪奥娜": {
|
||||
"生命值百分比": 100,
|
||||
"攻击力百分比": 50,
|
||||
"暴击率": 50,
|
||||
"暴击伤害": 50,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 90,
|
||||
"治疗加成": 100
|
||||
},
|
||||
"优菈": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 40,
|
||||
"物理伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"达达利亚": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 30
|
||||
},
|
||||
"魈": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"宵宫": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100
|
||||
},
|
||||
"九条裟罗": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"琴": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"物理伤害加成": 100,
|
||||
"元素充能效率": 55,
|
||||
"治疗加成": 100
|
||||
},
|
||||
"菲谢尔": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"物理伤害加成": 60
|
||||
},
|
||||
"罗莎莉亚": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 70,
|
||||
"物理伤害加成": 80,
|
||||
"元素充能效率": 30
|
||||
},
|
||||
"可莉": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 30
|
||||
},
|
||||
"凝光": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 30
|
||||
},
|
||||
"北斗": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"刻晴": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"物理伤害加成": 100
|
||||
},
|
||||
"托马": {
|
||||
"生命值百分比": 100,
|
||||
"攻击力百分比": 50,
|
||||
"暴击率": 50,
|
||||
"暴击伤害": 50,
|
||||
"元素伤害加成": 75,
|
||||
"元素充能效率": 90
|
||||
},
|
||||
"迪卢克": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100
|
||||
},
|
||||
"芭芭拉": {
|
||||
"生命值百分比": 100,
|
||||
"攻击力百分比": 50,
|
||||
"暴击率": 50,
|
||||
"暴击伤害": 50,
|
||||
"元素伤害加成": 80,
|
||||
"元素充能效率": 55,
|
||||
"治疗加成": 100
|
||||
},
|
||||
"芭芭拉-暴力": {
|
||||
"生命值百分比": 50,
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55,
|
||||
"治疗加成": 50
|
||||
},
|
||||
"诺艾尔": {
|
||||
"攻击力百分比": 50,
|
||||
"防御力百分比": 90,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 70
|
||||
},
|
||||
"旅行者": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"重云": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"七七": {
|
||||
"攻击力百分比": 100,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 60,
|
||||
"物理伤害加成": 70,
|
||||
"元素充能效率": 55,
|
||||
"治疗加成": 100
|
||||
},
|
||||
"凯亚": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"物理伤害加成": 100,
|
||||
"元素充能效率": 30
|
||||
},
|
||||
"烟绯": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 30
|
||||
},
|
||||
"早柚": {
|
||||
"攻击力百分比": 50,
|
||||
"暴击率": 50,
|
||||
"暴击伤害": 50,
|
||||
"元素精通": 100,
|
||||
"元素伤害加成": 80,
|
||||
"元素充能效率": 55,
|
||||
"治疗加成": 100
|
||||
},
|
||||
"安柏": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"物理伤害加成": 100
|
||||
},
|
||||
"丽莎": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100
|
||||
},
|
||||
"埃洛伊": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100
|
||||
},
|
||||
"辛焱": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 50,
|
||||
"物理伤害加成": 100
|
||||
},
|
||||
"砂糖": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 100,
|
||||
"元素伤害加成": 50,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"雷泽": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 50,
|
||||
"物理伤害加成": 100
|
||||
},
|
||||
"夜兰": {
|
||||
"生命值百分比": 80,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55
|
||||
},
|
||||
"久岐忍": {
|
||||
"生命值百分比": 100,
|
||||
"攻击力百分比": 50,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 55,
|
||||
"治疗加成": 100
|
||||
},
|
||||
"鹿野院平藏": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 30
|
||||
},
|
||||
"提纳里": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 90,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 30
|
||||
},
|
||||
"柯莱": {
|
||||
"攻击力百分比": 75,
|
||||
"暴击率": 100,
|
||||
"暴击伤害": 100,
|
||||
"元素精通": 75,
|
||||
"元素伤害加成": 100,
|
||||
"元素充能效率": 30
|
||||
}
|
||||
}
|
1971
resources/characters.json
Normal file
BIN
resources/font/HYWH-65W.ttf
Normal file
BIN
resources/font/NZBZ.ttf
Normal file
BIN
resources/font/tttgbnumber.ttf
Normal file
BIN
resources/font/华文中宋.TTF
Normal file
BIN
resources/imgs/bg-anemo.jpg
Normal file
After Width: | Height: | Size: 187 KiB |
BIN
resources/imgs/bg-cryo.jpg
Normal file
After Width: | Height: | Size: 173 KiB |
BIN
resources/imgs/bg-dendro.jpg
Normal file
After Width: | Height: | Size: 202 KiB |
BIN
resources/imgs/bg-electro.jpg
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
resources/imgs/bg-geo.jpg
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
resources/imgs/bg-hydro.jpg
Normal file
After Width: | Height: | Size: 182 KiB |
BIN
resources/imgs/bg-pyro.jpg
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
resources/imgs/card-bg.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
resources/imgs/star.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
resources/imgs/talent-anemo.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/imgs/talent-cryo.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/imgs/talent-dendro.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/imgs/talent-electro.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
resources/imgs/talent-geo.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
resources/imgs/talent-hydro.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/imgs/talent-pyro.png
Normal file
After Width: | Height: | Size: 39 KiB |
56
resources/name2id.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"神里绫华": "10000002",
|
||||
"琴": "10000003",
|
||||
"旅行者": "10000007-708",
|
||||
"丽莎": "10000006",
|
||||
"芭芭拉": "10000014",
|
||||
"凯亚": "10000015",
|
||||
"迪卢克": "10000016",
|
||||
"雷泽": "10000020",
|
||||
"安柏": "10000021",
|
||||
"温迪": "10000022",
|
||||
"香菱": "10000023",
|
||||
"北斗": "10000024",
|
||||
"行秋": "10000025",
|
||||
"魈": "10000026",
|
||||
"凝光": "10000027",
|
||||
"可莉": "10000029",
|
||||
"钟离": "10000030",
|
||||
"菲谢尔": "10000031",
|
||||
"班尼特": "10000032",
|
||||
"达达利亚": "10000033",
|
||||
"诺艾尔": "10000034",
|
||||
"七七": "10000035",
|
||||
"重云": "10000036",
|
||||
"甘雨": "10000037",
|
||||
"阿贝多": "10000038",
|
||||
"迪奥娜": "10000039",
|
||||
"莫娜": "10000041",
|
||||
"刻晴": "10000042",
|
||||
"砂糖": "10000043",
|
||||
"辛焱": "10000044",
|
||||
"罗莎莉亚": "10000045",
|
||||
"胡桃": "10000046",
|
||||
"枫原万叶": "10000047",
|
||||
"烟绯": "10000048",
|
||||
"宵宫": "10000049",
|
||||
"托马": "10000050",
|
||||
"优菈": "10000051",
|
||||
"雷电将军": "10000052",
|
||||
"早柚": "10000053",
|
||||
"珊瑚宫心海": "10000054",
|
||||
"五郎": "10000055",
|
||||
"九条裟罗": "10000056",
|
||||
"荒泷一斗": "10000057",
|
||||
"八重神子": "10000058",
|
||||
"鹿野院平藏": "10000059",
|
||||
"夜兰": "10000060",
|
||||
"埃洛伊": "10000062",
|
||||
"申鹤": "10000063",
|
||||
"云堇": "10000064",
|
||||
"久岐忍": "10000065",
|
||||
"神里绫人": "10000066",
|
||||
"柯莱": "10000067",
|
||||
"多莉": "10000068",
|
||||
"提纳里": "10000069"
|
||||
}
|
860
resources/style.css
Normal file
@ -0,0 +1,860 @@
|
||||
@font-face {
|
||||
font-family: HWZS;
|
||||
src: url(font/华文中宋.TTF);
|
||||
font-weight: 400;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Number;
|
||||
src: url(font/tttgbnumber.ttf);
|
||||
font-weight: 400;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: NZBZ;
|
||||
src: url(font/NZBZ.ttf);
|
||||
font-weight: 400;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: YS;
|
||||
src: url(font/HYWH-65W.ttf);
|
||||
font-weight: 400;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
user-select: none
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 18px;
|
||||
color: #1e1f20;
|
||||
font-family: Number, YS, PingFangSC-Medium, PingFang SC, sans-serif;
|
||||
transform: scale(1.4);
|
||||
transform-origin: 0 0;
|
||||
width: 600px
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 600px;
|
||||
padding: 20px 15px 10px 15px;
|
||||
background-size: contain
|
||||
}
|
||||
|
||||
.copyright {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
text-shadow: 1px 1px 1px #000;
|
||||
margin: 10px 0
|
||||
}
|
||||
|
||||
.copyright .version {
|
||||
font-size: 13px;
|
||||
color: #d3bc8e;
|
||||
display: inline-block;
|
||||
padding: 0 3px
|
||||
}
|
||||
|
||||
.cons {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0 5px;
|
||||
border-radius: 4px
|
||||
}
|
||||
|
||||
.cons-0 {
|
||||
background: #666;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons-1 {
|
||||
background: #5cbac2;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons-2 {
|
||||
background: #339d61;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons-3 {
|
||||
background: #3e95b9;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons-4 {
|
||||
background: #3955b7;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons-5 {
|
||||
background: #531ba9cf;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons-6 {
|
||||
background: #ff5722;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons2-0 {
|
||||
border-radius: 4px;
|
||||
background: #666;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons2-1 {
|
||||
border-radius: 4px;
|
||||
background: #71b1b7;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons2-2 {
|
||||
border-radius: 4px;
|
||||
background: #369961;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons2-3 {
|
||||
border-radius: 4px;
|
||||
background: #4596b9;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons2-4 {
|
||||
border-radius: 4px;
|
||||
background: #4560b9;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons2-5 {
|
||||
border-radius: 4px;
|
||||
background: #531ba9cf;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.cons2-6 {
|
||||
border-radius: 4px;
|
||||
background: #ff5722;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
/* .elem-anemo .talent-icon {
|
||||
background-image: url(imgs/talent-anemo.png)
|
||||
}
|
||||
|
||||
.anemo-bg,
|
||||
.elem-anemo .elem-bg {
|
||||
background-image: url(imgs/bg-anemo.jpg)
|
||||
} */
|
||||
|
||||
.cont {
|
||||
border-radius: 10px;
|
||||
background: url(imgs/card-bg.png) top left repeat-x;
|
||||
background-size: auto 100%;
|
||||
margin: 5px 15px 5px 10px;
|
||||
position: relative;
|
||||
box-shadow: 0 0 1px 0 #ccc, 2px 2px 4px 0 rgba(50, 50, 50, .8);
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
.cont-title {
|
||||
background: rgba(0, 0, 0, .4);
|
||||
box-shadow: 0 0 1px 0 #fff;
|
||||
color: #d3bc8e;
|
||||
padding: 10px 20px;
|
||||
text-align: left;
|
||||
border-radius: 10px 10px 0 0
|
||||
}
|
||||
|
||||
.cont-title span {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
margin-left: 10px;
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
.cont-body {
|
||||
padding: 10px 15px;
|
||||
font-size: 12px;
|
||||
background: rgba(0, 0, 0, .5);
|
||||
box-shadow: 0 0 1px 0 #fff;
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
.font-NZBZ {
|
||||
font-family: Number, "印品南征北战NZBZ体", NZBZ, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif
|
||||
}
|
||||
|
||||
body {
|
||||
width: 600px
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 600px;
|
||||
padding: 0;
|
||||
background-size: cover;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.basic {
|
||||
position: relative;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.main-pic {
|
||||
width: 1000px;
|
||||
height: 500px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
margin-left: -360px;
|
||||
position: relative;
|
||||
z-index: 2
|
||||
}
|
||||
|
||||
.detail {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 5px;
|
||||
color: #fff;
|
||||
z-index: 3
|
||||
}
|
||||
|
||||
.char-name {
|
||||
margin-right: 5px;
|
||||
font-size: 40px;
|
||||
font-family: Number, "印品南征北战NZBZ体", NZBZ, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif;
|
||||
text-shadow: 0 0 3px #000, 2px 2px 4px rgba(0, 0, 0, .7);
|
||||
text-align: right
|
||||
}
|
||||
|
||||
.char-lv {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 0 3px #000, 2px 2px 4px rgba(0, 0, 0, .7);
|
||||
text-align: right
|
||||
}
|
||||
|
||||
.char-lv .uid {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.char-lv .fetter {
|
||||
border-radius: 4px;
|
||||
background: #ff5722;
|
||||
color: #fff;
|
||||
padding: 0.2em 0.3em 0.1em 0.2em;
|
||||
}
|
||||
|
||||
.attr {
|
||||
border-radius: 4px;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.detail li {
|
||||
width: 300px;
|
||||
font-size: 17px;
|
||||
list-style: none;
|
||||
padding: 0 30px;
|
||||
position: relative;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-shadow: 0 0 1px rgba(0, 0, 0, .5)
|
||||
}
|
||||
|
||||
.detail .attr li span {
|
||||
right: 30px;
|
||||
bottom: -12px;
|
||||
text-align: right;
|
||||
font-size: 10px;
|
||||
opacity: .7
|
||||
}
|
||||
|
||||
.detail .attr li span font {
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.detail li:nth-child(even) {
|
||||
background: rgba(0, 0, 0, .4)
|
||||
}
|
||||
|
||||
.detail li:nth-child(odd) {
|
||||
background: rgba(50, 50, 50, .4)
|
||||
}
|
||||
|
||||
.detail li strong {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
text-align: right;
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
.detail li span {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: left;
|
||||
width: 75px;
|
||||
display: inline-block;
|
||||
color: #82ff9a;
|
||||
font-size: 15px
|
||||
}
|
||||
|
||||
.detail .attr li code,
|
||||
.detail.attr li code {
|
||||
color: #ffe699;
|
||||
background-color: rgba(0, 0, 0, .7);
|
||||
white-space: normal;
|
||||
font-size: .9em;
|
||||
margin-left: .5em;
|
||||
padding: 0 .2em;
|
||||
border-radius: 5px
|
||||
}
|
||||
|
||||
.detail .attr li code.error,
|
||||
.detail.attr li code.error {
|
||||
background-color: rgba(240, 6, 6, 0.7) !important;
|
||||
}
|
||||
|
||||
.talent-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
padding: 5px;
|
||||
display: table;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
z-index: 90
|
||||
}
|
||||
|
||||
.talent-icon .talent-icon-img,
|
||||
.talent-icon img {
|
||||
width: 46%;
|
||||
height: 46%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -22% 0 0 -23%;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center
|
||||
}
|
||||
|
||||
.talent-icon span {
|
||||
background: #fff;
|
||||
width: 34px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
font-size: 17px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 50%;
|
||||
margin-left: -15px;
|
||||
color: #000;
|
||||
box-shadow: 0 0 5px 0 #000
|
||||
}
|
||||
|
||||
.talent-icon.talent-plus span {
|
||||
background: #45deff;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 .08em .1em #000, 0 .1em .3em rgb(0 0 0 / 90%)
|
||||
}
|
||||
|
||||
.char-talents {
|
||||
display: flex;
|
||||
width: 300px;
|
||||
margin: 0 0 20px 0
|
||||
}
|
||||
|
||||
.char-cons {
|
||||
display: flex;
|
||||
width: 250px;
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 20px
|
||||
}
|
||||
|
||||
.char-cons .talent-item,
|
||||
.char-talents .talent-item {
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.char-cons .talent-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 0 -5px
|
||||
}
|
||||
|
||||
.char-cons .talent-icon.off {
|
||||
filter: grayscale(100%);
|
||||
opacity: .4
|
||||
}
|
||||
|
||||
.elem_火 .talent-icon {
|
||||
background-image: url(imgs/talent-pyro.png);
|
||||
}
|
||||
|
||||
.elem_火 .container {
|
||||
background-image: url(imgs/bg-pyro.jpg);
|
||||
}
|
||||
|
||||
.elem_水 .talent-icon {
|
||||
background-image: url(imgs/talent-hydro.png);
|
||||
}
|
||||
|
||||
.elem_水 .container {
|
||||
background-image: url(imgs/bg-hydro.jpg);
|
||||
}
|
||||
|
||||
.elem_风 .talent-icon {
|
||||
background-image: url(imgs/talent-anemo.png)
|
||||
}
|
||||
|
||||
.elem_风 .container {
|
||||
background-image: url(imgs/bg-anemo.jpg)
|
||||
}
|
||||
|
||||
.elem_雷 .talent-icon {
|
||||
background-image: url(imgs/talent-electro.png);
|
||||
}
|
||||
|
||||
.elem_雷 .container {
|
||||
background-image: url(imgs/bg-electro.jpg);
|
||||
}
|
||||
|
||||
.elem_草 .talent-icon {
|
||||
background-image: url(imgs/talent-dendro.png);
|
||||
}
|
||||
|
||||
.elem_草 .container {
|
||||
background-image: url(imgs/bg-dendro.jpg);
|
||||
}
|
||||
|
||||
.elem_冰 .talent-icon {
|
||||
background-image: url(imgs/talent-cryo.png);
|
||||
}
|
||||
|
||||
.elem_冰 .container {
|
||||
background-image: url(imgs/bg-cryo.jpg);
|
||||
}
|
||||
|
||||
.elem_岩 .talent-icon {
|
||||
background-image: url(imgs/talent-geo.png);
|
||||
}
|
||||
|
||||
.elem_岩 .container {
|
||||
background-image: url(imgs/bg-geo.jpg);
|
||||
}
|
||||
|
||||
.cont {
|
||||
border-radius: 10px;
|
||||
background: url(imgs/card-bg.png) top left repeat-x;
|
||||
background-size: auto 100%;
|
||||
margin: 5px 15px 5px 10px;
|
||||
position: relative;
|
||||
box-shadow: 0 0 1px 0 #ccc, 2px 2px 4px 0 rgba(50, 50, 50, .8);
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
.cont-title {
|
||||
background: rgba(0, 0, 0, .4);
|
||||
color: #d3bc8e;
|
||||
padding: 10px 20px;
|
||||
text-align: left
|
||||
}
|
||||
|
||||
.cont-title span {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
margin-left: 10px;
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
.artis {
|
||||
display: flex;
|
||||
width: 600px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 5px;
|
||||
padding: 0 5px
|
||||
}
|
||||
|
||||
.artis .item {
|
||||
width: 185px;
|
||||
border-radius: 10px;
|
||||
background: url(imgs/card-bg.png) top left repeat-x;
|
||||
background-size: auto 100%;
|
||||
margin: 5px;
|
||||
position: relative;
|
||||
box-shadow: 5px 5px 5px rgb(0 0 0 / 30%);
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.artis .item .arti-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 3px
|
||||
}
|
||||
|
||||
.artis .item .arti-icon span {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
margin-left: 5px;
|
||||
background: rgba(0, 0, 0, .5);
|
||||
border-radius: 5px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
padding: 0 3px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
display: block
|
||||
}
|
||||
|
||||
.artis .item .arti-icon img {
|
||||
width: 60px;
|
||||
height: 60px
|
||||
}
|
||||
|
||||
.artis .head {
|
||||
color: #fff;
|
||||
padding: 12px 0 8px 68px
|
||||
}
|
||||
|
||||
.artis .head strong {
|
||||
font-size: 15px;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.artis .head span {
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
.mark-ACE,
|
||||
.mark-ACE² {
|
||||
color: #e85656;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mark-SSS,
|
||||
.mark-SS {
|
||||
color: #ffe699;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mark-S,
|
||||
.mark-A {
|
||||
color: #d699ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.arti-main {
|
||||
color: #fff;
|
||||
padding: 6px 15px
|
||||
}
|
||||
|
||||
.artis ul.detail {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
position: initial;
|
||||
display: table
|
||||
}
|
||||
|
||||
.artis ul.detail li {
|
||||
padding: 0 3px;
|
||||
font-size: 14px;
|
||||
position: initial;
|
||||
width: 100%;
|
||||
display: table-row;
|
||||
line-height: 26px;
|
||||
height: 26px
|
||||
}
|
||||
|
||||
.artis ul.detail li.great span.title {
|
||||
color: #ffe699
|
||||
}
|
||||
|
||||
.artis ul.detail li.nouse span {
|
||||
color: #888
|
||||
}
|
||||
|
||||
.artis ul.detail li.arti-main {
|
||||
font-size: 16px;
|
||||
padding: 3px 3px;
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.artis ul.detail li span {
|
||||
position: initial;
|
||||
display: table-cell;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.artis ul.detail li span.title {
|
||||
text-align: left;
|
||||
padding-left: 10px
|
||||
}
|
||||
|
||||
.artis ul.detail li span.val {
|
||||
text-align: right;
|
||||
padding-right: 10px
|
||||
}
|
||||
|
||||
.artis .weapon .star {
|
||||
height: 20px;
|
||||
width: 100px;
|
||||
background: url(imgs/star.png) no-repeat;
|
||||
background-size: 100px 100px;
|
||||
transform: scale(.8);
|
||||
transform-origin: 100px 10px;
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.artis .weapon .star.star-2 {
|
||||
background-position: 0 -20px
|
||||
}
|
||||
|
||||
.artis .weapon .star.star-3 {
|
||||
background-position: 0 -40px
|
||||
}
|
||||
|
||||
.artis .weapon .star.star-4 {
|
||||
background-position: 0 -60px
|
||||
}
|
||||
|
||||
.artis .weapon .star.star-5 {
|
||||
background-position: 0 -80px
|
||||
}
|
||||
|
||||
.artis .weapon {
|
||||
overflow: hidden;
|
||||
height: 97px
|
||||
}
|
||||
|
||||
.artis .weapon img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
z-index: 2
|
||||
}
|
||||
|
||||
.artis .weapon .head {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
text-align: right;
|
||||
padding: 13px 12px 13px 0;
|
||||
z-index: 3
|
||||
}
|
||||
|
||||
.artis .weapon .head strong {
|
||||
font-size: 18px;
|
||||
margin-bottom: 3px;
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
.artis .weapon .head>span {
|
||||
display: block
|
||||
}
|
||||
|
||||
.artis .weapon span {
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
.artis .weapon .affix {
|
||||
color: #000;
|
||||
padding: 0 7px;
|
||||
border-radius: 4px;
|
||||
margin-left: 5px;
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
.artis .weapon .affix-1 {
|
||||
box-shadow: 0 0 4px 0 #a3a3a3 inset;
|
||||
background: #ebebebaa
|
||||
}
|
||||
|
||||
.artis .weapon .affix-2 {
|
||||
box-shadow: 0 0 4px 0 #51b72fbd inset;
|
||||
background: #ddffdeaa
|
||||
}
|
||||
|
||||
.artis .weapon .affix-3 {
|
||||
box-shadow: 0 0 4px 0 #396cdecf inset;
|
||||
background: #ddebffaa
|
||||
}
|
||||
|
||||
.artis .weapon .affix-4 {
|
||||
box-shadow: 0 0 4px 0 #c539debf inset;
|
||||
background: #ffddf0aa
|
||||
}
|
||||
|
||||
.artis .weapon .affix-5 {
|
||||
box-shadow: 0 0 4px 0 #deaf39 inset;
|
||||
background: #fff6dd
|
||||
}
|
||||
|
||||
.artis .arti-stat {
|
||||
height: 85px
|
||||
}
|
||||
|
||||
.arti-stat {
|
||||
height: 90px;
|
||||
margin-top: 10px;
|
||||
padding: 13px 10px;
|
||||
display: table
|
||||
}
|
||||
|
||||
.arti-stat>div {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.arti-stat strong {
|
||||
display: block;
|
||||
height: 40px;
|
||||
font-size: 30px;
|
||||
line-height: 40px
|
||||
}
|
||||
|
||||
.arti-stat span {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: #bbb
|
||||
}
|
||||
|
||||
.char-优菈 .main-pic {
|
||||
margin-left: -175px
|
||||
}
|
||||
|
||||
.char-烟绯 .main-pic {
|
||||
margin-left: -135px
|
||||
}
|
||||
|
||||
.char-香菱 .main-pic {
|
||||
margin-left: -195px
|
||||
}
|
||||
|
||||
.char-迪奥娜 .main-pic {
|
||||
margin-left: -180px
|
||||
}
|
||||
|
||||
.char-可莉 .main-pic {
|
||||
margin-left: -210px
|
||||
}
|
||||
|
||||
.char-凝光 .main-pic {
|
||||
margin-left: -320px
|
||||
}
|
||||
|
||||
.char-班尼特 .main-pic {
|
||||
margin-left: -220px
|
||||
}
|
||||
|
||||
.container,
|
||||
body {
|
||||
width: 650px
|
||||
}
|
||||
|
||||
.container>.cont {
|
||||
margin-left: 15px
|
||||
}
|
||||
|
||||
.basic .detail .cont {
|
||||
margin: 10px 0
|
||||
}
|
||||
|
||||
.basic:after {
|
||||
display: none
|
||||
}
|
||||
|
||||
.arti-stat {
|
||||
width: 100%;
|
||||
margin-right: 0
|
||||
}
|
||||
|
||||
.arti-icon {
|
||||
width: 30px
|
||||
}
|
||||
|
||||
.artis {
|
||||
width: 650px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 0 10px
|
||||
}
|
||||
|
||||
.artis ul.detail li span {
|
||||
width: initial
|
||||
}
|
||||
|
||||
.artis ul.detail li span.mark {
|
||||
text-align: right;
|
||||
padding-right: 10px
|
||||
}
|
||||
|
||||
.artis ul.detail li span.mark:after {
|
||||
content: "分";
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
transform: scale(.8);
|
||||
margin-right: -1px
|
||||
}
|
||||
|
||||
.artis .mark-calc {
|
||||
background: rgba(0, 0, 0, .35);
|
||||
border-radius: 0 0 5px 5px;
|
||||
border-top-style: dashed;
|
||||
border-top-width: 1px
|
||||
}
|
||||
|
||||
.artis .mark-calc li.result {
|
||||
background: #2e353e;
|
||||
height: 30px;
|
||||
line-height: 30px
|
||||
}
|
||||
|
||||
.artis .mark-calc li.result .mark {
|
||||
font-size: 18px
|
||||
}
|
||||
|
||||
.artis .item {
|
||||
height: inherit;
|
||||
width: 200px
|
||||
}
|
||||
|
||||
.artis div .item.weapon {
|
||||
padding-top: 100px;
|
||||
margin-bottom: 15px
|
||||
}
|
||||
|
||||
.artis div .item.arti-stat {
|
||||
padding: 24px 10px
|
||||
}
|
25
resources/template.json
Normal file
@ -0,0 +1,25 @@
|
||||
[
|
||||
"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",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/bg-dendro.jpg",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/bg-electro.jpg",
|
||||
"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",
|
||||
"https://cdn.monsterx.cn/bot/gspanel/imgs/talent-electro.png",
|
||||
"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/calc-rule.json"
|
||||
]
|
84
resources/tpl.html
Normal file
@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cn">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="preload" href="./font/HYWH-65W.ttf" as="font" type="font/ttf">
|
||||
<link rel="preload" href="./font/NZBZ.ttf" as="font" type="font/ttf">
|
||||
<link rel="preload" href="./font/tttgbnumber.ttf" as="font" type="font/ttf">
|
||||
<link rel="stylesheet" type="text/css" href="./style.css" />
|
||||
<title>Designed by Miao-Plugin</title>
|
||||
</head>
|
||||
|
||||
<body class="{{elem_type}} default-mode" style=transform:scale(1.3)>
|
||||
<div class="container elem-bg" id="container">
|
||||
|
||||
<div class="basic">
|
||||
<div class="main-pic" style="background-image:url({{char_img}})"></div>
|
||||
<div class="detail">
|
||||
<!--char_info-->
|
||||
<div class="char-talents">
|
||||
<div class="talent-item">
|
||||
<!--skill_a-->
|
||||
</div>
|
||||
<div class="talent-item">
|
||||
<!--skill_e-->
|
||||
</div>
|
||||
<div class="talent-item">
|
||||
<!--skill_q-->
|
||||
</div>
|
||||
</div>
|
||||
<ul class="attr">
|
||||
<!--fight_prop-->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="char-cons">
|
||||
<!--cons_data-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="artis">
|
||||
|
||||
<div>
|
||||
<div class="item weapon">
|
||||
<!--weapon-->
|
||||
</div>
|
||||
<div class="item arti-stat">
|
||||
<div><strong class="mark-{{total_mark_lvl}}">{{total_mark_lvl}}</strong><span>圣遗物评级</span></div>
|
||||
<div><strong>{{total_mark}}</strong><span>圣遗物总分</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item arti 1">
|
||||
<!--arti_1-->
|
||||
</div>
|
||||
|
||||
<div class="item arti 2">
|
||||
<!--arti_2-->
|
||||
</div>
|
||||
|
||||
<div class="item arti 3">
|
||||
<!--arti_3-->
|
||||
</div>
|
||||
|
||||
|
||||
<div class="item arti 4">
|
||||
<!--arti_4-->
|
||||
</div>
|
||||
|
||||
<div class="item arti 5">
|
||||
<!--arti_5-->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="copyright">Enka.Network × NoneBot × Miao-Plugin
|
||||
<!--time-->
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|