This commit is contained in:
xtaodada 2022-09-03 22:50:36 +08:00
parent 1d72f6016c
commit 2c259856fa
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
47 changed files with 283743 additions and 2 deletions

7
.gitignore vendored
View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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">&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
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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
httpx
pyrogram==2.0.49
TGCrypto
sqlitedict
playwright

BIN
resources/Kitsune.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

276419
resources/TextMapCHS.json Normal file

File diff suppressed because it is too large Load Diff

2690
resources/cache/190182329__data.json vendored Normal file

File diff suppressed because it is too large Load Diff

439
resources/calc-rule.json Normal file
View 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

File diff suppressed because it is too large Load Diff

BIN
resources/font/HYWH-65W.ttf Normal file

Binary file not shown.

BIN
resources/font/NZBZ.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
resources/imgs/card-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
resources/imgs/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

56
resources/name2id.json Normal file
View 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
View 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
View 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
View 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>