Skip to content

Commit

Permalink
✨ 新增限定五星分析数据
Browse files Browse the repository at this point in the history
  • Loading branch information
monsterxcn committed Oct 25, 2022
1 parent 360ed74 commit e9afaf9
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 55 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ cp -r data/gachalogs /path/to/bot/data/
| `链接` / `地址` / `url` || 指定导出祈愿历史记录链接 |
| `饼干` / `ck` / `cookie` || 指定导出米哈游通行证 Cookie |

![导出示意图](https://user-images.githubusercontent.com/22407052/187933780-64fa0be4-a43f-40f1-9fa9-88e033e9d372.png)
![导出示意图](https://user-images.githubusercontent.com/22407052/197698116-40d247c1-3224-4419-a262-2a9989d82f8b.PNG)

- `抽卡记录删除` / `logdel` / `ckjldc`

Expand Down
29 changes: 21 additions & 8 deletions nonebot_plugin_gachalogs/__meta__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import json
from datetime import datetime
from pathlib import Path

from httpx import stream # AsyncClient
from httpx import stream
from nonebot import get_driver
from pytz import timezone

cfg = get_driver().config

Expand All @@ -23,18 +26,15 @@
LOCAL_DIR.mkdir(parents=True, exist_ok=True)

# 绘图字体
# auto download font use httpx AsyncClient
# async with AsyncClient() as client:
# font_content = await client.get("", timeout=10)
# with open(PIL_FONT, "wb") as f:
# f.write(font_content.content)
PIL_FONT = (
(Path(cfg.gachalogs_font))
if hasattr(cfg, "gachalogs_font")
else (LOCAL_DIR / "LXGW-Bold.ttf")
)
if not PIL_FONT.exists():
with stream("GET", "https://cdn.monsterx.cn/bot/LXGW-Bold.ttf", verify=False) as r:
with stream(
"GET", "https://cdn.monsterx.cn/bot/gachalogs/LXGW-Bold.ttf", verify=False
) as r:
with open(PIL_FONT, "wb") as f:
for chunk in r.iter_bytes():
f.write(chunk)
Expand All @@ -45,11 +45,24 @@
)
if not PIE_FONT.exists():
with stream(
"GET", "https://cdn.monsterx.cn/bot/LXGW-Bold-minipie.ttf", verify=False
"GET", "https://cdn.monsterx.cn/bot/gachalogs/LXGW-Bold-minipie.ttf", verify=False
) as r:
with open(PIE_FONT, "wb") as f:
for chunk in r.iter_bytes():
f.write(chunk)
# 卡池信息
_pools = LOCAL_DIR / "GachaEvent.json"
if (not _pools.exists()) or (
timezone("Asia/Shanghai").localize(datetime.now())
> datetime.fromisoformat(json.loads(_pools.read_text(encoding="utf-8"))[-1]["To"])
):
with stream(
"GET", "https://cdn.monsterx.cn/bot/gachalogs/GachaEvent.json", verify=False
) as r:
with open(_pools, "wb") as f:
for chunk in r.iter_bytes():
f.write(chunk)
POOL_INFO = json.loads(_pools.read_text(encoding="utf-8"))

# 抽卡链接地址
ROOT_URL = "https://hk4e-api.mihoyo.com/event/gacha_info/api/getGachaLog"
Expand Down
4 changes: 2 additions & 2 deletions nonebot_plugin_gachalogs/data_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import json
from pathlib import Path
from time import localtime, strftime, time
from typing import Dict, List, Literal
from typing import Dict, Generator, List, Literal

from nonebot.log import logger
from xlsxwriter import Workbook
Expand All @@ -14,7 +14,7 @@
from .data_source import logsHelper


def gnrtId():
def gnrtId() -> Generator[str, None, None]:
"""生成物品 ID"""
id = 1000000000000000000
while True:
Expand Down
183 changes: 143 additions & 40 deletions nonebot_plugin_gachalogs/data_render.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from base64 import b64encode
from copy import deepcopy
from datetime import datetime
from io import BytesIO
from math import floor
from pathlib import PosixPath
from time import localtime, strftime
from typing import Dict, List, Literal, Tuple, Union

import matplotlib.pyplot as plt
from matplotlib import font_manager as fm
from numpy import average
from nonebot.log import logger
from numpy import average, isnan
from PIL import Image, ImageDraw, ImageFont
from pytz import timezone

from .__meta__ import GACHA_TYPE, PIE_FONT, PIL_FONT
from .__meta__ import GACHA_TYPE, PIE_FONT, PIL_FONT, POOL_INFO


def percent(a: int, b: int, rt: Literal["pct", "rgb"] = "pct") -> str:
Expand Down Expand Up @@ -71,7 +73,7 @@ async def colorfulFive(
* ``param isWeapon: bool = False`` 是否为武器祈愿活动
- ``return: Image.Image`` Pillow 图片对象
"""
ImageSize = (maxWidth, 400)
ImageSize = (maxWidth, 1000)
coordX, coordY = 0, 0 # 绘制坐标
fontPadding = 10 # 字体行间距
result = Image.new("RGBA", ImageSize, "#f9f9f9")
Expand All @@ -91,38 +93,102 @@ async def colorfulFive(
wordW = fs(fontSize).getsize(word)[0]
if coordX + wordW <= maxWidth:
# 当前行绘制未超过最大宽度限制,正常绘制
tDraw.text((coordX, coordY), word, font=fs(fontSize), fill=color)
# 偏移 X 轴绘制坐标使物品名称与自己的抽数间隔半个空格、抽数与下一个物品名称间隔一个空格
coordX += (wordW + spaceW) if word[0] == "[" else (wordW + spaceW / 2)
tDraw.text(
(coordX, coordY),
word,
font=fs(fontSize),
fill=color,
stroke_width=int(item["up"]),
stroke_fill=color,
)
# 偏移 X 轴绘制坐标使物品名称与自己的抽数间隔 1/4 个空格、抽数与下一个物品名称间隔一个空格
coordX += (wordW + spaceW) if word[0] == "[" else int(wordW + spaceW / 4)
elif word[0] == "[":
# 当前行绘制超过最大宽度限制,且当前绘制为抽数,换行绘制(保证 [ 与数字不分离)
coordX, coordY = 0, (coordY + stepY) # 偏移绘制坐标至下行行首
tDraw.text((coordX, coordY), word, font=fs(fontSize), fill=color)
tDraw.text(
(coordX, coordY),
word,
font=fs(fontSize),
fill=color,
stroke_width=int(item["up"]),
stroke_fill=color,
)
coordX = wordW + spaceW # 偏移 X 轴绘制坐标使抽数与下一个物品名称间隔一个空格
else:
# 当前行绘制超过最大宽度限制,且当前绘制为物品名称,逐字绘制直至超限后换行绘制
aval = int((maxWidth - coordX) / spaceW) # 当前行可绘制的最大字符数
splitStr = [item["name"][:aval], item["name"][aval:]]
for i in range(len(splitStr)):
s = splitStr[i]
tDraw.text((coordX, coordY), s, font=fs(fontSize), fill=color)
tDraw.text(
(coordX, coordY),
s,
font=fs(fontSize),
fill=color,
stroke_width=int(item["up"]),
stroke_fill=color,
)
if i == 0:
# 当前行绘制完毕,偏移绘制坐标至下行行首
coordX, coordY = 0, (coordY + stepY)
else:
# 下一行绘制完毕,偏移 X 轴绘制坐标使物品名称与自己的抽数间隔半个空格
# 下一行绘制完毕,偏移 X 轴绘制坐标使物品名称与自己的抽数间隔 1/4 个空格
partW = fs(fontSize).getsize(s)[0]
coordX = partW + spaceW / 2
# 所有五星物品数据绘制完毕,绘制五星概率统计结果
coordY += stepY + fontPadding * 2 # 偏移 Y 轴绘制坐标至下两行行首(空出一行)
coordX = int(partW + spaceW / 4)
# 所有五星物品数据绘制完毕
# 绘制五星概率统计结果
star5Avg = average([item["count"] for item in star5Data])
tDraw.text((0, coordY), "五星平均抽数:", font=fs(fontSize), fill="black")
tDraw.text(
(indent1st, coordY),
f"{star5Avg:.2f}",
font=fs(fontSize),
fill=percent(round(star5Avg), 80 if isWeapon else 90, "rgb"),
if not isnan(star5Avg):
coordY += stepY + fontPadding * 2 # 偏移 Y 轴绘制坐标至下两行行首(空出一行)
tDraw.text((0, coordY), "五星平均抽数:", font=fs(fontSize), fill="black")
tDraw.text(
(indent1st, coordY),
f"{star5Avg:.2f}",
font=fs(fontSize),
fill=percent(round(star5Avg), 80 if isWeapon else 90, "rgb"),
)
if len(star5Data) > 1:
startW = maxWidth
for extreme in [
str(max(x["count"] for x in star5Data)),
" 最非 ",
str(min(x["count"] for x in star5Data)),
"最欧 ",
]:
tDraw.text(
(startW - fs(fontSize).getlength(extreme), coordY),
extreme,
font=fs(fontSize),
fill=percent(int(extreme), 80 if isWeapon else 90, "rgb")
if extreme.isdigit()
else "black",
)
startW -= fs(fontSize).getlength(extreme)
# 绘制限定五星概率统计结果
upStar5Avg = average(
[
(
item["count"]
+ (
star5Data[iIdx - 1]["count"]
if iIdx >= 1 and not star5Data[iIdx - 1]["up"]
else 0
)
)
for iIdx, item in enumerate(star5Data)
if item["up"]
]
)
if not isnan(upStar5Avg):
coordY += stepY + fontPadding * 2 # 偏移 Y 轴绘制坐标至下两行行首(空出一行)
tDraw.text((0, coordY), "限定五星平均抽数:", font=fs(fontSize), fill="black")
tDraw.text(
(indent1st + spaceW * 2, coordY), # 多出 2 个字宽度
f"{upStar5Avg:.2f}",
font=fs(fontSize),
fill=percent(round(upStar5Avg), 160 if isWeapon else 180, "rgb"),
)
# 裁剪图片
result = result.crop((0, 0, maxWidth, coordY + fH))
return result
Expand Down Expand Up @@ -157,6 +223,7 @@ async def calcStat(gachaLogs: Dict) -> Dict:
gachaList = gachaLogs[banner]
gachaList.reverse() # 千万不可以 gachaList.sort(key=lambda item: item["time"], reverse=False)
counter, pityCounter = 0, 0 # 总抽数计数器、保底计数器
upCounter = {} # UP 物品计数器
for item in gachaList:
counter += 1 # 总抽数计数器递增
pityCounter += 1 # 保底计数器递增
Expand All @@ -169,8 +236,39 @@ async def calcStat(gachaLogs: Dict) -> Dict:
else:
t = "cntChar" if itemType == "角色" else "cntWeapon"
gachaStat[t + str(rankType)] += 1 # 对应星级对应类型物品总数递增
# 对应星级对应类型物品 UP 总数递增
gotUpStar5 = False
if int(banner) in [301, 302]:
gachaTime = timezone("Asia/Shanghai").localize(
datetime.strptime(item["time"], "%Y-%m-%d %H:%M:%S")
)
belongTo = [
p
for p in POOL_INFO
if p["Type"] == int(item["gacha_type"])
and gachaTime >= datetime.fromisoformat(p["From"])
and gachaTime <= datetime.fromisoformat(p["To"])
]
if not belongTo or len(belongTo) > 1:
logger.error(
"卡池 {} 异常的 UP 判断:{}({}) in {}".format(
banner,
item["name"],
item["time"],
"/".join(bp["Name"] for bp in belongTo)
if belongTo
else "null",
)
)
elif item["name"] in belongTo[0]["UpPurpleList"]:
upCounter["cntUp4"] = 1 + upCounter.get("cntUp4", 0)
elif item["name"] in belongTo[0]["UpOrangeList"]:
upCounter["cntUp5"] = 1 + upCounter.get("cntUp5", 0)
gotUpStar5 = True
if rankType == 5:
gachaStat["star5"].append({"name": itemName, "count": pityCounter})
gachaStat["star5"].append(
{"name": itemName, "count": pityCounter, "up": gotUpStar5}
)
pityCounter = 0 # 重置保底计数器
# 计算未出五星抽数
star5Cnts = [
Expand All @@ -182,6 +280,9 @@ async def calcStat(gachaLogs: Dict) -> Dict:
timeNow = strftime("%Y-%m-%d %H:%M:%S", localtime())
gachaStat["startTime"] = gachaList[0].get("time", timeNow)
gachaStat["endTime"] = gachaList[-1].get("time", timeNow)
# UP 物品总数
if upCounter:
gachaStat.update(upCounter)
# 更新待返回统计数据
stat[banner] = gachaStat
return stat
Expand Down Expand Up @@ -215,9 +316,8 @@ async def drawPie(
sizes = [p["total"] for p in partMap if p["total"]]
explode = [(0.05 if "五星" in p["label"] else 0) for p in partMap if p["total"]]
# 绘制饼图
textprops = {"fontproperties": fm.FontProperties(fname=PIE_FONT, size=18)} # type: ignore
fig, ax = plt.subplots()
fmProp = fm.FontProperties(fname=PosixPath(PIE_FONT))
txtProp = {"fontsize": 16, "fontproperties": fmProp}
ax.set_facecolor("#f9f9f9")
ax.pie(
sizes,
Expand All @@ -230,7 +330,7 @@ async def drawPie(
radius=0.7,
explode=explode,
shadow=False,
textprops=txtProp,
textprops=textprops,
)
ax.axis("equal")
plt.tight_layout()
Expand Down Expand Up @@ -318,29 +418,34 @@ async def gnrtGachaInfo(rawData: Dict, uid: str) -> bytes:
tDraw.text((startW, poolImgH), totalStr6, font=fs(25), fill="black")
poolImgH += fs(25).getsize(totalStr1)[1] + 20 * 2
# 绘制概率统计
totalStar = {
"五星": {
totalList = [
{
"rank": "五星",
"cnt": poolStat["cntWeapon5"] + poolStat["cntChar5"],
"cntUp": poolStat.get("cntUp5", 0),
"color": "#C0713D",
},
"四星": {
{
"rank": "四星",
"cnt": poolStat["cntWeapon4"] + poolStat["cntChar4"],
"cntUp": poolStat.get("cntUp4", 0),
"color": "#A65FE2",
},
"三星": {"cnt": poolStat["cntStar3"], "color": "#4D8DF7"},
}
probList = [
{"level": key, "total": value["cnt"], "color": value["color"]}
for key, value in totalStar.items()
if value["cnt"] > 0
{"rank": "三星", "cnt": poolStat["cntStar3"], "color": "#4D8DF7"},
]
for item in probList:
cntStr = f"{item['level']}{item['total']} 次"
probStr = f"[{item['total'] / poolTotal * 100:.2f}%]"
for item in totalList:
if not item["cnt"]:
continue
cntStr = "{}:{} 次{}".format(
item["rank"],
item["cnt"],
f"({item['cntUp']} 次限定)" if item.get("cntUp") else "",
)
probStr = f"[{item['cnt'] / poolTotal * 100:.2f}%]"
tDraw.text((20, poolImgH), cntStr, font=fs(25), fill=item["color"])
probStrW = fs(25).getsize(probStr)[0]
tDraw.text(
(int(400 - probStrW), poolImgH),
(int((480 if int(banner) in [301, 302] else 400) - probStrW), poolImgH),
probStr,
font=fs(25),
fill=item["color"],
Expand All @@ -351,10 +456,8 @@ async def gnrtGachaInfo(rawData: Dict, uid: str) -> bytes:
star5Data = poolStat["star5"]
if star5Data:
statPic = await colorfulFive(star5Data, 25, 460, isWeapon)
statPicW, statPicH = statPic.size
statPicCoord = (int((500 - statPicW) / 2), poolImgH)
poolImg.paste(statPic, statPicCoord, statPic)
poolImgH += statPicH
poolImg.paste(statPic, (20, poolImgH), statPic)
poolImgH += statPic.size[1]
# 绘制完成
poolImgH += 20
poolImg = poolImg.crop((0, 0, 500, poolImgH))
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]
name = "nonebot-plugin-gachalogs"
version = "0.2.0"
description = "Genshin gacha logs analysis plugin for NoneBot2"
version = "0.2.1"
description = "Genshin gacha history analysis plugin for NoneBot2"
authors = ["monsterxcn <monsterxcn@gmail.com>"]
documentation = "https://github.com/monsterxcn/nonebot-plugin-gachalogs#readme"
license = "MIT"
Expand All @@ -16,6 +16,7 @@ nonebot-adapter-onebot = ">=2.0.0b1"
httpx = ">=0.20.0, <1.0.0"
matplotlib = ">=3.5.1"
xlsxwriter = ">=3.0.2"
pytz = "*"

[tool.poetry.dev-dependencies]

Expand Down

0 comments on commit e9afaf9

Please sign in to comment.