feat✨: 添加游戏抽卡功能

This commit is contained in:
HibiKier
2024-07-28 03:37:37 +08:00
parent 86048e041c
commit aa7a8271f3
17 changed files with 4039 additions and 1 deletions
+289
View File
@@ -0,0 +1,289 @@
import asyncio
import traceback
from dataclasses import dataclass
from typing import Any
import nonebot
from cn2an import cn2an
from nonebot import on_keyword, on_message, on_regex
from nonebot.log import logger
from nonebot.matcher import Matcher
from nonebot.params import RegexGroup
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from nonebot.typing import T_Handler
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_saa import Text
from nonebot_plugin_session import EventSession
from zhenxun.configs.config import Config
from zhenxun.configs.utils import PluginExtraData
from .handles.azur_handle import AzurHandle
from .handles.ba_handle import BaHandle
from .handles.base_handle import BaseHandle
from .handles.fgo_handle import FgoHandle
from .handles.genshin_handle import GenshinHandle
from .handles.guardian_handle import GuardianHandle
from .handles.onmyoji_handle import OnmyojiHandle
from .handles.pcr_handle import PcrHandle
from .handles.pretty_handle import PrettyHandle
from .handles.prts_handle import PrtsHandle
from .rule import rule
__plugin_meta__ = PluginMetadata(
name="游戏抽卡",
description="就算是模拟抽卡也不能改变自己是个非酋",
usage="""
usage:
模拟赛马娘,原神,明日方舟,坎公骑冠剑,公主连结(国/台),碧蓝航线,FGO,阴阳师,碧蓝档案进行抽卡
指令:
原神[1-180]抽: 原神常驻池
原神角色[1-180]抽: 原神角色UP池子
原神角色2池[1-180]抽: 原神角色UP池子
原神武器[1-180]抽: 原神武器UP池子
重置原神抽卡: 清空当前卡池的抽卡次数[即从0开始计算UP概率]
方舟[1-300]抽: 方舟卡池,当有当期UP时指向UP池
赛马娘[1-200]抽: 赛马娘卡池,当有当期UP时指向UP池
坎公骑冠剑[1-300]抽: 坎公骑冠剑卡池,当有当期UP时指向UP池
pcr/公主连接[1-300]抽: 公主连接卡池
碧蓝航线/碧蓝[重型/轻型/特型/活动][1-300]抽: 碧蓝航线重型/轻型/特型/活动卡池
fgo[1-300]抽: fgo卡池 (已失效)
阴阳师[1-300]抽: 阴阳师卡池
ba/碧蓝档案[1-200]抽:碧蓝档案卡池 (已失效)
* 以上指令可以通过 XX一井 来指定最大抽取数量 *
* 示例:原神一井 *
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.1",
menu_type="抽卡相关",
superuser_help="""
更新方舟信息
重载方舟卡池
更新原神信息
重载原神卡池
更新赛马娘信息
重载赛马娘卡池
更新坎公骑冠剑信息
更新碧蓝航线信息
更新fgo信息
更新阴阳师信息
""",
).dict(),
)
_hidden = on_message(rule=lambda: False)
@dataclass
class Game:
keywords: set[str]
handle: BaseHandle
flag: bool
config_name: str
max_count: int = 300 # 一次最大抽卡数
reload_time: int | None = None # 重载UP池时间(小时)
has_other_pool: bool = False
games = (
Game(
{"azur", "碧蓝航线"},
AzurHandle(),
Config.get_config("draw_card", "AZUR_FLAG", True),
"AZUR_FLAG",
),
Game(
{"fgo", "命运冠位指定"},
FgoHandle(),
Config.get_config("draw_card", "FGO_FLAG", True),
"FGO_FLAG",
),
Game(
{"genshin", "原神"},
GenshinHandle(),
Config.get_config("draw_card", "GENSHIN_FLAG", True),
"GENSHIN_FLAG",
max_count=180,
reload_time=18,
has_other_pool=True,
),
Game(
{"guardian", "坎公骑冠剑"},
GuardianHandle(),
Config.get_config("draw_card", "GUARDIAN_FLAG", True),
"GUARDIAN_FLAG",
reload_time=4,
),
Game(
{"onmyoji", "阴阳师"},
OnmyojiHandle(),
Config.get_config("draw_card", "ONMYOJI_FLAG", True),
"ONMYOJI_FLAG",
),
Game(
{"pcr", "公主连结", "公主连接", "公主链接", "公主焊接"},
PcrHandle(),
Config.get_config("draw_card", "PCR_FLAG", True),
"PCR_FLAG",
),
Game(
{"pretty", "马娘", "赛马娘"},
PrettyHandle(),
Config.get_config("draw_card", "PRETTY_FLAG", True),
"PRETTY_FLAG",
max_count=200,
reload_time=4,
),
Game(
{"prts", "方舟", "明日方舟"},
PrtsHandle(),
Config.get_config("draw_card", "PRTS_FLAG", True),
"PRTS_FLAG",
reload_time=4,
),
Game(
{"ba", "碧蓝档案"},
BaHandle(),
Config.get_config("draw_card", "BA_FLAG", True),
"BA_FLAG",
),
)
def create_matchers():
def draw_handler(game: Game) -> T_Handler:
async def handler(
session: EventSession,
args: tuple[Any, ...] = RegexGroup(),
):
pool_name, pool_type_, num, unit = args
if num == "单":
num = 1
else:
try:
num = int(cn2an(num, mode="smart"))
except ValueError:
await Text("必!须!是!数!字!").finish(reply=True)
if unit == "井":
num *= game.max_count
if num < 1:
await Text("虚空抽卡???").finish(reply=True)
elif num > game.max_count:
await Text("一井都满不足不了你嘛!快爬开!").finish(reply=True)
pool_name = (
pool_name.replace("池", "")
.replace("武器", "arms")
.replace("角色", "char")
.replace("卡牌", "card")
.replace("卡", "card")
)
try:
if pool_type_ in ["2池", "二池"]:
pool_name = pool_name + "1"
res = await game.handle.draw(
num, pool_name=pool_name, user_id=session.id1
)
logger.info(
f"游戏抽卡 类型: {list(game.keywords)[1]} 卡池: {pool_name} 数量: {num}",
"游戏抽卡",
session=session,
)
except:
logger.warning(traceback.format_exc())
await Text("出错了...").finish(reply=True)
await res.send()
return handler
def update_handler(game: Game) -> T_Handler:
async def handler(matcher: Matcher):
await game.handle.update_info()
await matcher.finish("更新完成!")
return handler
def reload_handler(game: Game) -> T_Handler:
async def handler(matcher: Matcher):
res = await game.handle.reload_pool()
if res:
await res.send()
return handler
def reset_handler(game: Game) -> T_Handler:
async def handler(matcher: Matcher, session: EventSession):
if not session.id1:
await Text("获取用户id失败...").finish()
if game.handle.reset_count(session.id1):
await Text("重置成功!").send()
return handler
def scheduled_job(game: Game) -> T_Handler:
async def handler():
await game.handle.reload_pool()
return handler
for game in games:
pool_pattern = r"([^\s单0-9零一二三四五六七八九百十]{0,3})"
num_pattern = r"(单|[0-9零一二三四五六七八九百十]{1,3})"
unit_pattern = r"([抽|井|连])"
pool_type = "()"
if game.has_other_pool:
pool_type = r"([2二]池)?"
draw_regex = r".*?(?:{})\s*{}\s*{}\s*{}\s*{}".format(
"|".join(game.keywords), pool_pattern, pool_type, num_pattern, unit_pattern
)
update_keywords = {f"更新{keyword}信息" for keyword in game.keywords}
reload_keywords = {f"重载{keyword}卡池" for keyword in game.keywords}
reset_keywords = {f"重置{keyword}抽卡" for keyword in game.keywords}
on_regex(draw_regex, priority=5, block=True, rule=rule(game)).append_handler(
draw_handler(game)
)
on_keyword(
update_keywords, priority=1, block=True, permission=SUPERUSER
).append_handler(update_handler(game))
on_keyword(
reload_keywords, priority=1, block=True, permission=SUPERUSER
).append_handler(reload_handler(game))
on_keyword(reset_keywords, priority=5, block=True).append_handler(
reset_handler(game)
)
if game.reload_time:
scheduler.add_job(
scheduled_job(game), trigger="cron", hour=game.reload_time, minute=1
)
create_matchers()
# 更新资源
@scheduler.scheduled_job(
"cron",
hour=4,
minute=1,
)
async def _():
tasks = []
for game in games:
if game.flag:
tasks.append(asyncio.ensure_future(game.handle.update_info()))
await asyncio.gather(*tasks)
driver = nonebot.get_driver()
@driver.on_startup
async def _():
tasks = []
for game in games:
if game.flag:
game.handle.init_data()
if not game.handle.data_exists():
tasks.append(asyncio.ensure_future(game.handle.update_info()))
await asyncio.gather(*tasks)
+203
View File
@@ -0,0 +1,203 @@
import nonebot
import ujson as json
from pydantic import BaseModel, Extra, ValidationError
from zhenxun.configs.config import Config as AConfig
from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
from zhenxun.services.log import logger
# 原神
class GenshinConfig(BaseModel, extra=Extra.ignore):
GENSHIN_FIVE_P: float = 0.006
GENSHIN_FOUR_P: float = 0.051
GENSHIN_THREE_P: float = 0.43
GENSHIN_G_FIVE_P: float = 0.016
GENSHIN_G_FOUR_P: float = 0.13
I72_ADD: float = 0.0585
# 明日方舟
class PrtsConfig(BaseModel, extra=Extra.ignore):
PRTS_SIX_P: float = 0.02
PRTS_FIVE_P: float = 0.08
PRTS_FOUR_P: float = 0.48
PRTS_THREE_P: float = 0.42
# 赛马娘
class PrettyConfig(BaseModel, extra=Extra.ignore):
PRETTY_THREE_P: float = 0.03
PRETTY_TWO_P: float = 0.18
PRETTY_ONE_P: float = 0.79
# 坎公骑冠剑
class GuardianConfig(BaseModel, extra=Extra.ignore):
GUARDIAN_THREE_CHAR_P: float = 0.0275
GUARDIAN_TWO_CHAR_P: float = 0.19
GUARDIAN_ONE_CHAR_P: float = 0.7825
GUARDIAN_THREE_CHAR_UP_P: float = 0.01375
GUARDIAN_THREE_CHAR_OTHER_P: float = 0.01375
GUARDIAN_EXCLUSIVE_ARMS_P: float = 0.03
GUARDIAN_FIVE_ARMS_P: float = 0.03
GUARDIAN_FOUR_ARMS_P: float = 0.09
GUARDIAN_THREE_ARMS_P: float = 0.27
GUARDIAN_TWO_ARMS_P: float = 0.58
GUARDIAN_EXCLUSIVE_ARMS_UP_P: float = 0.01
GUARDIAN_EXCLUSIVE_ARMS_OTHER_P: float = 0.02
# 公主连结
class PcrConfig(BaseModel, extra=Extra.ignore):
PCR_THREE_P: float = 0.025
PCR_TWO_P: float = 0.18
PCR_ONE_P: float = 0.795
PCR_G_THREE_P: float = 0.025
PCR_G_TWO_P: float = 0.975
# 碧蓝航线
class AzurConfig(BaseModel, extra=Extra.ignore):
AZUR_FIVE_P: float = 0.012
AZUR_FOUR_P: float = 0.07
AZUR_THREE_P: float = 0.12
AZUR_TWO_P: float = 0.51
AZUR_ONE_P: float = 0.3
# 命运-冠位指定
class FgoConfig(BaseModel, extra=Extra.ignore):
FGO_SERVANT_FIVE_P: float = 0.01
FGO_SERVANT_FOUR_P: float = 0.03
FGO_SERVANT_THREE_P: float = 0.4
FGO_CARD_FIVE_P: float = 0.04
FGO_CARD_FOUR_P: float = 0.12
FGO_CARD_THREE_P: float = 0.4
# 阴阳师
class OnmyojiConfig(BaseModel, extra=Extra.ignore):
ONMYOJI_SP: float = 0.0025
ONMYOJI_SSR: float = 0.01
ONMYOJI_SR: float = 0.2
ONMYOJI_R: float = 0.7875
# 碧蓝档案
class BaConfig(BaseModel, extra=Extra.ignore):
BA_THREE_P: float = 0.025
BA_TWO_P: float = 0.185
BA_ONE_P: float = 0.79
BA_G_TWO_P: float = 0.975
class Config(BaseModel, extra=Extra.ignore):
# 开关
PRTS_FLAG: bool = True
GENSHIN_FLAG: bool = True
PRETTY_FLAG: bool = True
GUARDIAN_FLAG: bool = True
PCR_FLAG: bool = True
AZUR_FLAG: bool = True
FGO_FLAG: bool = True
ONMYOJI_FLAG: bool = True
BA_FLAG: bool = True
# 其他配置
PCR_TAI: bool = False
SEMAPHORE: int = 5
# 抽卡概率
prts: PrtsConfig = PrtsConfig()
genshin: GenshinConfig = GenshinConfig()
pretty: PrettyConfig = PrettyConfig()
guardian: GuardianConfig = GuardianConfig()
pcr: PcrConfig = PcrConfig()
azur: AzurConfig = AzurConfig()
fgo: FgoConfig = FgoConfig()
onmyoji: OnmyojiConfig = OnmyojiConfig()
ba: BaConfig = BaConfig()
driver = nonebot.get_driver()
# DRAW_PATH = Path("data/draw_card").absolute()
DRAW_PATH = IMAGE_PATH / "draw_card"
# try:
# DRAW_PATH = Path(global_config.draw_path).absolute()
# except:
# pass
config_path = DATA_PATH / "draw_card" / "draw_card_config" / "draw_card_config.json"
draw_config: Config = Config()
for game_flag, game_name in zip(
[
"PRTS_FLAG",
"GENSHIN_FLAG",
"PRETTY_FLAG",
"GUARDIAN_FLAG",
"PCR_FLAG",
"AZUR_FLAG",
"FGO_FLAG",
"ONMYOJI_FLAG",
"PCR_TAI",
"BA_FLAG",
],
[
"明日方舟",
"原神",
"赛马娘",
"坎公骑冠剑",
"公主连结",
"碧蓝航线",
"命运-冠位指定(FGO)",
"阴阳师",
"pcr台服卡池",
"碧蓝档案",
],
):
AConfig.add_plugin_config(
"draw_card",
game_flag,
True,
help=f"{game_name} 抽卡开关",
default_value=True,
type=bool,
)
AConfig.add_plugin_config(
"draw_card",
"SEMAPHORE",
5,
help=f"异步数据下载数量限制",
default_value=5,
type=int,
)
AConfig.set_name("draw_card", "游戏抽卡")
@driver.on_startup
def check_config():
global draw_config
if not config_path.exists():
config_path.parent.mkdir(parents=True, exist_ok=True)
draw_config = Config()
logger.warning("draw_card:配置文件不存在,已重新生成配置文件...")
else:
with config_path.open("r", encoding="utf8") as fp:
data = json.load(fp)
try:
draw_config = Config.parse_obj({**data})
except ValidationError:
draw_config = Config()
logger.warning("draw_card:配置文件格式错误,已重新生成配置文件...")
with config_path.open("w", encoding="utf8") as fp:
json.dump(
draw_config.dict(),
fp,
indent=4,
ensure_ascii=False,
)
+149
View File
@@ -0,0 +1,149 @@
from typing import Optional, TypeVar, Generic
from pydantic import BaseModel
from cachetools import TTLCache
class BaseUserCount(BaseModel):
count: int = 0 # 当前抽卡次数
TCount = TypeVar("TCount", bound="BaseUserCount")
class DrawCountManager(Generic[TCount]):
"""
抽卡统计保底
"""
def __init__(
self, game_draw_count_rule: tuple, star2name: tuple, max_draw_count: int
):
"""
初始化保底统计
例如:DrawCountManager((10, 90, 180), ("4", "5", "5"))
抽卡保底需要的次数和返回的对应名称,例如星级等
:param game_draw_count_rule:抽卡规则
:param star2name:星级对应的名称
:param max_draw_count:最大累计抽卡次数,当下次单次抽卡超过该次数时将会清空数据
"""
# 只有保底
# 超过60秒重置抽卡次数
self._data: TTLCache[int, TCount] = TTLCache(maxsize=1000, ttl=60)
self._guarantee_tuple = game_draw_count_rule
self._star2name = star2name
self._max_draw_count = max_draw_count
@classmethod
def get_count_class(cls) -> TCount:
raise NotImplementedError
def _get_count(self, key: int) -> TCount:
if self._data.get(key) is None:
self._data[key] = self.get_count_class()
else:
self._data[key] = self._data[key]
return self._data[key]
def increase(self, key: int, value: int = 1):
"""
用户抽卡次数加1
"""
self._get_count(key).count += value
def get_max_guarantee(self):
"""
获取最大保底抽卡次数
"""
return self._guarantee_tuple[-1]
def get_user_count(self, key: int) -> int:
"""
获取当前抽卡次数
"""
return self._get_count(key).count
def reset(self, key: int):
"""
清空记录
"""
self._data.pop(key, None)
class GenshinUserCount(BaseUserCount):
five_index: int = 0 # 获取五星时的抽卡次数
four_index: int = 0 # 获取四星时的抽卡次数
is_up: bool = False # 下次五星是否必定为up
class GenshinCountManager(DrawCountManager[GenshinUserCount]):
@classmethod
def get_count_class(cls) -> GenshinUserCount:
return GenshinUserCount()
def set_is_up(self, key: int, value: bool):
"""
设置下次是否必定up
"""
self._get_count(key).is_up = value
def is_up(self, key: int) -> bool:
"""
判断该次保底是否必定为up
"""
return self._get_count(key).is_up
def get_user_five_index(self, key: int) -> int:
"""
获取用户上次获取五星的次数
"""
return self._get_count(key).five_index
def get_user_four_index(self, key: int) -> int:
"""
获取用户上次获取四星的次数
"""
return self._get_count(key).four_index
def mark_five_index(self, key: int):
"""
标记用户该次次数为五星
"""
self._get_count(key).five_index = self._get_count(key).count
def mark_four_index(self, key: int):
"""
标记用户该次次数为四星
"""
self._get_count(key).four_index = self._get_count(key).count
def check_count(self, key: int, count: int):
"""
检查用户该次抽卡次数累计是否超过最大限制次数
"""
if self._get_count(key).count + count > self._max_draw_count:
self._data.pop(key, None)
def get_user_guarantee_count(self, key: int) -> int:
user = self._get_count(key)
return (
self.get_max_guarantee()
- (user.count % self.get_max_guarantee() - user.five_index)
) % self.get_max_guarantee() or self.get_max_guarantee()
def check(self, key: int) -> Optional[int]:
"""
是否保底
"""
# print(self._data)
user = self._get_count(key)
if user.count - user.five_index == 90:
user.five_index = user.count
return 5
if user.count - user.four_index == 10:
user.four_index = user.count
return 4
return None
@@ -0,0 +1,307 @@
import random
from urllib.parse import unquote
import dateparser
import ujson as json
from lxml import etree
from nonebot_plugin_saa import Image, MessageFactory, Text
from PIL import ImageDraw
from pydantic import ValidationError
from zhenxun.services.log import logger
from zhenxun.utils.image_utils import BuildImage
from ..config import draw_config
from ..util import cn2py, load_font, remove_prohibited_str
from .base_handle import BaseData, BaseHandle
from .base_handle import UpChar as _UpChar
from .base_handle import UpEvent as _UpEvent
class AzurChar(BaseData):
type_: str # 舰娘类型
@property
def star_str(self) -> str:
return ["白", "蓝", "紫", "金"][self.star - 1]
class UpChar(_UpChar):
type_: str # 舰娘类型
class UpEvent(_UpEvent):
up_char: list[UpChar] # up对象
class AzurHandle(BaseHandle[AzurChar]):
def __init__(self):
super().__init__("azur", "碧蓝航线")
self.max_star = 4
self.config = draw_config.azur
self.ALL_CHAR: list[AzurChar] = []
self.UP_EVENT: UpEvent | None = None
def get_card(self, pool_name: str, **kwargs) -> AzurChar:
if pool_name == "轻型":
type_ = ["驱逐", "轻巡", "维修"]
elif pool_name == "重型":
type_ = ["重巡", "战列", "战巡", "重炮"]
else:
type_ = ["维修", "潜艇", "重巡", "轻航", "航母"]
up_pool_flag = pool_name == "活动"
# Up
up_ship = (
[x for x in self.UP_EVENT.up_char if x.zoom > 0] if self.UP_EVENT else []
)
# print(up_ship)
acquire_char = None
if up_ship and up_pool_flag:
up_zoom: list[tuple[float, float]] = [(0, up_ship[0].zoom / 100)]
# 初始化概率
cur_ = up_ship[0].zoom / 100
for i in range(len(up_ship)):
try:
up_zoom.append((cur_, cur_ + up_ship[i + 1].zoom / 100))
cur_ += up_ship[i + 1].zoom / 100
except IndexError:
pass
rand = random.random()
# 抽取up
for i, zoom in enumerate(up_zoom):
if zoom[0] <= rand <= zoom[1]:
try:
acquire_char = [
x for x in self.ALL_CHAR if x.name == up_ship[i].name
][0]
except IndexError:
pass
# 没有up或者未抽取到up
if not acquire_char:
star = self.get_star(
# [4, 3, 2, 1],
[4, 3, 2, 2],
[
self.config.AZUR_FOUR_P,
self.config.AZUR_THREE_P,
self.config.AZUR_TWO_P,
self.config.AZUR_ONE_P,
],
)
acquire_char = random.choice(
[
x
for x in self.ALL_CHAR
if x.star == star and x.type_ in type_ and not x.limited
]
)
return acquire_char
async def draw(self, count: int, **kwargs) -> MessageFactory:
index2card = self.get_cards(count, **kwargs)
cards = [card[0] for card in index2card]
up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else []
result = self.format_result(index2card, **{**kwargs, "up_list": up_list})
gen_img = await self.generate_img(cards)
return MessageFactory([Image(gen_img.pic2bytes()), Text(result)])
async def generate_card_img(self, card: AzurChar) -> BuildImage:
sep_w = 5
sep_t = 5
sep_b = 20
w = 100
h = 100
bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b)
frame_path = str(self.img_path / f"{card.star}_star.png")
frame = BuildImage(w, h, background=frame_path)
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
img = BuildImage(w, h, background=img_path)
# 加圆角
await frame.circle_corner(6)
await img.circle_corner(6)
await bg.paste(img, (sep_w, sep_t))
await bg.paste(frame, (sep_w, sep_t))
# 加名字
text = card.name[:6] + "..." if len(card.name) > 7 else card.name
font = load_font(fontsize=14)
text_w, text_h = BuildImage.get_text_size(text, font)
draw = ImageDraw.Draw(bg.markImg)
draw.text(
(sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2),
text,
font=font,
fill=["#808080", "#3b8bff", "#8000ff", "#c90", "#ee494c"][card.star - 1],
)
return bg
def _init_data(self):
self.ALL_CHAR = [
AzurChar(
name=value["名称"],
star=int(value["星级"]),
limited="可以建造" not in value["获取途径"],
type_=value["类型"],
)
for value in self.load_data().values()
]
self.load_up_char()
def load_up_char(self):
try:
data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
self.UP_EVENT = UpEvent.parse_obj(data.get("char", {}))
except ValidationError:
logger.warning(f"{self.game_name}_up_char 解析出错")
def dump_up_char(self):
if self.UP_EVENT:
data = {"char": json.loads(self.UP_EVENT.json())}
self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
async def _update_info(self):
info = {}
# 更新图鉴
url = "https://wiki.biligame.com/blhx/舰娘图鉴"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} 出错")
return
dom = etree.HTML(result, etree.HTMLParser())
contents = dom.xpath(
"//div[@class='mw-body-content mw-content-ltr']/div[@class='mw-parser-output']"
)
for index, content in enumerate(contents):
char_list = content.xpath("./div[@id='CardSelectTr']/div")
for char in char_list:
try:
name = char.xpath("./span/a/text()")[0]
frame = char.xpath("./div/div/a/img/@alt")[0]
avatar = char.xpath("./div/img/@srcset")[0]
except IndexError:
continue
member_dict = {
"名称": remove_prohibited_str(name),
"头像": unquote(str(avatar).split(" ")[-2]),
"星级": self.parse_star(frame),
"类型": char.xpath("./@data-param1")[0].split(",")[-1],
}
info[member_dict["名称"]] = member_dict
# 更新额外信息
for key in info.keys():
# TODO: 各种舰娘·改获取错误
url = f"https://wiki.biligame.com/blhx/{key}"
result = await self.get_url(url)
if not result:
info[key]["获取途径"] = []
logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
continue
try:
dom = etree.HTML(result, etree.HTMLParser())
time = dom.xpath(
"//table[@class='wikitable sv-general']/tbody[1]/tr[4]/td[2]//text()"
)[0]
sources = []
if "无法建造" in time:
sources.append("无法建造")
elif "活动已关闭" in time:
sources.append("活动限定")
else:
sources.append("可以建造")
info[key]["获取途径"] = sources
except IndexError:
info[key]["获取途径"] = []
logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
self.dump_data(info)
logger.info(f"{self.game_name_cn} 更新成功")
# 下载头像
for value in info.values():
await self.download_img(value["头像"], value["名称"])
# 下载头像框
idx = 1
BLHX_URL = "https://patchwiki.biligame.com/images/blhx"
for url in [
"/1/15/pxho13xsnkyb546tftvh49etzdh74cf.png",
"/a/a9/k8t7nx6c8pan5vyr8z21txp45jxeo66.png",
"/a/a5/5whkzvt200zwhhx0h0iz9qo1kldnidj.png",
"/a/a2/ptog1j220x5q02hytpwc8al7f229qk9.png",
"/6/6d/qqv5oy3xs40d3055cco6bsm0j4k4gzk.png",
]:
await self.download_img(BLHX_URL + url, f"{idx}_star")
idx += 1
await self.update_up_char()
@staticmethod
def parse_star(star: str) -> int:
if star in ["舰娘头像外框普通.png", "舰娘头像外框白色.png"]:
return 1
elif star in ["舰娘头像外框稀有.png", "舰娘头像外框蓝色.png"]:
return 2
elif star in ["舰娘头像外框精锐.png", "舰娘头像外框紫色.png"]:
return 3
elif star in ["舰娘头像外框超稀有.png", "舰娘头像外框金色.png"]:
return 4
elif star in ["舰娘头像外框海上传奇.png", "舰娘头像外框彩色.png"]:
return 5
elif star in [
"舰娘头像外框最高方案.png",
"舰娘头像外框决战方案.png",
"舰娘头像外框超稀有META.png",
"舰娘头像外框精锐META.png",
]:
return 6
else:
return 6
async def update_up_char(self):
url = "https://wiki.biligame.com/blhx/游戏活动表"
result = await self.get_url(url)
if not result:
logger.warning(f"{self.game_name_cn}获取活动表出错")
return
try:
dom = etree.HTML(result, etree.HTMLParser())
dd = dom.xpath("//div[@class='timeline2']/dl/dd/a")[0]
url = "https://wiki.biligame.com" + dd.xpath("./@href")[0]
title = dd.xpath("string(.)")
result = await self.get_url(url)
if not result:
logger.warning(f"{self.game_name_cn}获取活动页面出错")
return
dom = etree.HTML(result, etree.HTMLParser())
timer = dom.xpath("//span[@class='eventTimer']")[0]
start_time = dateparser.parse(timer.xpath("./@data-start")[0])
end_time = dateparser.parse(timer.xpath("./@data-end")[0])
ships = dom.xpath("//table[@class='shipinfo']")
up_chars = []
for ship in ships:
name = ship.xpath("./tbody/tr/td[2]/p/a/@title")[0]
type_ = ship.xpath("./tbody/tr/td[2]/p/small/text()")[0] # 舰船类型
try:
p = float(str(ship.xpath(".//sup/text()")[0]).strip("%"))
except (IndexError, ValueError):
p = 0
star = self.parse_star(
ship.xpath("./tbody/tr/td[1]/div/div/div/a/img/@alt")[0]
)
up_chars.append(
UpChar(name=name, star=star, limited=False, zoom=p, type_=type_)
)
self.UP_EVENT = UpEvent(
title=title,
pool_img="",
start_time=start_time,
end_time=end_time,
up_char=up_chars,
)
self.dump_up_char()
except Exception as e:
logger.warning(f"{self.game_name_cn}UP更新出错", e=e)
async def _reload_pool(self) -> MessageFactory | None:
await self.update_up_char()
self.load_up_char()
if self.UP_EVENT:
return MessageFactory(
[Text(f"重载成功!\n当前活动:{self.UP_EVENT.title}")]
)
@@ -0,0 +1,153 @@
import random
from PIL import ImageDraw
from zhenxun.services.log import logger
from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.image_utils import BuildImage
from ..config import draw_config
from ..util import cn2py, load_font
from .base_handle import BaseData, BaseHandle
class BaChar(BaseData):
pass
class BaHandle(BaseHandle[BaChar]):
def __init__(self):
super().__init__("ba", "碧蓝档案")
self.max_star = 3
self.config = draw_config.ba
self.ALL_CHAR: list[BaChar] = []
def get_card(self, mode: int = 1) -> BaChar:
if mode == 2:
star = self.get_star(
[3, 2], [self.config.BA_THREE_P, self.config.BA_G_TWO_P]
)
else:
star = self.get_star(
[3, 2, 1],
[self.config.BA_THREE_P, self.config.BA_TWO_P, self.config.BA_ONE_P],
)
chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited]
return random.choice(chars)
def get_cards(self, count: int, **kwargs) -> list[tuple[BaChar, int]]:
card_list = []
card_count = 0 # 保底计算
for i in range(count):
card_count += 1
# 十连保底
if card_count == 10:
card = self.get_card(2)
card_count = 0
else:
card = self.get_card(1)
if card.star > self.max_star - 2:
card_count = 0
card_list.append((card, i + 1))
return card_list
async def generate_card_img(self, card: BaChar) -> BuildImage:
sep_w = 5
sep_h = 5
star_h = 15
img_w = 90
img_h = 100
font_h = 20
bar_h = 20
bar_w = 90
bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5")
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
img = BuildImage(img_w, img_h, background=img_path)
bar = BuildImage(bar_w, bar_h, color="#6495ED")
await bg.paste(img, (sep_w, sep_h))
await bg.paste(bar, (sep_w, img_h - bar_h + sep_h))
if card.star == 1:
star_path = str(self.img_path / "star-1.png")
star_w = 15
elif card.star == 2:
star_path = str(self.img_path / "star-2.png")
star_w = 30
else:
star_path = str(self.img_path / "star-3.png")
star_w = 45
star = BuildImage(star_w, star_h, background=star_path)
await bg.paste(star, (img_w // 2 - 15 * (card.star - 1) // 2, img_h - star_h))
text = card.name[:5] + "..." if len(card.name) > 6 else card.name
font = load_font(fontsize=14)
text_w, text_h = BuildImage.get_text_size(text, font)
draw = ImageDraw.Draw(bg.markImg)
draw.text(
(sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2),
text,
font=font,
fill="gray",
)
return bg
def _init_data(self):
self.ALL_CHAR = [
BaChar(
name=value["名称"],
star=int(value["星级"]),
limited=True if "(" in key else False,
)
for key, value in self.load_data().items()
]
def title2star(self, title: int):
if title == "Star-3.png":
return 3
elif title == "Star-2.png":
return 2
else:
return 1
async def _update_info(self):
# TODO: ba获取链接失效
info = {}
url = "https://lonqie.github.io/SchaleDB/data/cn/students.min.json?v=49"
result = (await AsyncHttpx.get(url)).json()
if not result:
logger.warning(f"更新 {self.game_name_cn} 出错")
return
else:
for char in result:
try:
name = char["Name"]
avatar = (
"https://github.com/lonqie/SchaleDB/raw/main/images/student/icon/"
+ char["CollectionTexture"]
+ ".png"
)
star = char["StarGrade"]
except IndexError:
continue
member_dict = {
"头像": avatar,
"名称": name,
"星级": star,
}
info[member_dict["名称"]] = member_dict
self.dump_data(info)
logger.info(f"{self.game_name_cn} 更新成功")
# 下载头像
for value in info.values():
await self.download_img(value["头像"], value["名称"])
# 下载星星
await self.download_img(
"https://patchwiki.biligame.com/images/bluearchive/thumb/e/e0/82nj2x9sxko473g7782r14fztd4zyky.png/15px-Star-1.png",
"star-1",
)
await self.download_img(
"https://patchwiki.biligame.com/images/bluearchive/thumb/0/0b/msaff2g0zk6nlyl1rrn7n1ri4yobcqc.png/30px-Star-2.png",
"star-2",
)
await self.download_img(
"https://patchwiki.biligame.com/images/bluearchive/thumb/8/8a/577yv79x1rwxk8efdccpblo0lozl158.png/46px-Star-3.png",
"star-3",
)
@@ -0,0 +1,296 @@
import asyncio
import random
from asyncio.exceptions import TimeoutError
from datetime import datetime
from typing import Generic, TypeVar
import aiohttp
import anyio
import ujson as json
from nonebot_plugin_saa import Image
from nonebot_plugin_saa import Image as SaaImage
from nonebot_plugin_saa import MessageFactory, Text
from PIL import Image
from pydantic import BaseModel, Extra
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.log import logger
from zhenxun.utils.image_utils import BuildImage
from ..config import DRAW_PATH, draw_config
from ..util import circled_number, cn2py
class BaseData(BaseModel, extra=Extra.ignore):
name: str # 名字
star: int # 星级
limited: bool # 限定
def __eq__(self, other: "BaseData"):
return self.name == other.name
def __hash__(self):
return hash(self.name)
@property
def star_str(self) -> str:
return "".join(["★" for _ in range(self.star)])
class UpChar(BaseData):
zoom: float # up提升倍率
class UpEvent(BaseModel):
title: str # up池标题
pool_img: str # up池封面
start_time: datetime | None # 开始时间
end_time: datetime | None # 结束时间
up_char: list[UpChar] # up对象
TC = TypeVar("TC", bound="BaseData")
class BaseHandle(Generic[TC]):
def __init__(self, game_name: str, game_name_cn: str):
self.game_name = game_name
self.game_name_cn = game_name_cn
self.max_star = 1 # 最大星级
self.game_card_color: str = "#ffffff"
self.data_path = DATA_PATH / "draw_card"
self.img_path = DRAW_PATH / f"{self.game_name}"
self.up_path = DATA_PATH / "draw_card" / "draw_card_up"
self.img_path.mkdir(parents=True, exist_ok=True)
self.up_path.mkdir(parents=True, exist_ok=True)
self.data_files: list[str] = [f"{self.game_name}.json"]
async def draw(self, count: int, **kwargs) -> MessageFactory:
index2card = self.get_cards(count, **kwargs)
cards = [card[0] for card in index2card]
result = self.format_result(index2card)
gen_img = await self.generate_img(cards)
return MessageFactory([SaaImage(gen_img.pic2bytes()), Text(result)])
# 抽取卡池
def get_card(self, **kwargs) -> TC:
raise NotImplementedError
def get_cards(self, count: int, **kwargs) -> list[tuple[TC, int]]:
return [(self.get_card(**kwargs), i) for i in range(count)]
# 获取星级
@staticmethod
def get_star(star_list: list[int], probability_list: list[float]) -> int:
return random.choices(star_list, weights=probability_list, k=1)[0]
def format_result(self, index2card: list[tuple[TC, int]], **kwargs) -> str:
card_list = [card[0] for card in index2card]
results = [
self.format_star_result(card_list, **kwargs),
self.format_max_star(index2card, **kwargs),
self.format_max_card(card_list, **kwargs),
]
results = [rst for rst in results if rst]
return "\n".join(results)
def format_star_result(self, card_list: list[TC], **kwargs) -> str:
star_dict: dict[str, int] = {} # 记录星级及其次数
card_list_sorted = sorted(card_list, key=lambda c: c.star, reverse=True)
for card in card_list_sorted:
try:
star_dict[card.star_str] += 1
except KeyError:
star_dict[card.star_str] = 1
rst = ""
for star_str, count in star_dict.items():
rst += f"[{star_str}×{count}] "
return rst.strip()
def format_max_star(
self, card_list: list[tuple[TC, int]], up_list: list[str] = [], **kwargs
) -> str:
up_list = up_list or kwargs.get("up_list", [])
rst = ""
for card, index in card_list:
if card.star == self.max_star:
if card.name in up_list:
rst += f"第 {index} 抽获取UP {card.name}\n"
else:
rst += f"第 {index} 抽获取 {card.name}\n"
return rst.strip()
def format_max_card(self, card_list: list[TC], **kwargs) -> str:
card_dict: dict[TC, int] = {} # 记录卡牌抽取次数
for card in card_list:
try:
card_dict[card] += 1
except KeyError:
card_dict[card] = 1
max_count = max(card_dict.values())
max_card = list(card_dict.keys())[list(card_dict.values()).index(max_count)]
if max_count <= 1:
return ""
return f"抽取到最多的是{max_card.name},共抽取了{max_count}次"
async def generate_img(
self,
cards: list[TC],
num_per_line: int = 5,
max_per_line: tuple[int, int] = (40, 10),
) -> BuildImage:
"""
生成统计图片
cards: 卡牌列表
num_per_line: 单行角色显示数量
max_per_line: 当card_list超过一定数值时,更改单行数量
"""
if len(cards) > max_per_line[0]:
num_per_line = max_per_line[1]
if len(cards) > 90:
card_dict: dict[TC, int] = {} # 记录卡牌抽取次数
for card in cards:
try:
card_dict[card] += 1
except KeyError:
card_dict[card] = 1
card_list = list(card_dict)
num_list = list(card_dict.values())
else:
card_list = cards
num_list = [1] * len(cards)
card_imgs: list[BuildImage] = []
for card, num in zip(card_list, num_list):
card_img = await self.generate_card_img(card)
# 数量 > 1 时加数字上标
if num > 1:
label = circled_number(num)
label_w = int(min(card_img.width, card_img.height) / 7)
label = label.resize(
(
int(label_w * label.width / label.height),
label_w,
),
Image.ANTIALIAS, # type: ignore
)
await card_img.paste(label)
card_imgs.append(card_img)
# img_w = card_imgs[0].width
# img_h = card_imgs[0].height
# if len(card_imgs) < num_per_line:
# w = img_w * len(card_imgs)
# else:
# w = img_w * num_per_line
# h = img_h * math.ceil(len(card_imgs) / num_per_line)
# img = BuildImage(w, h, img_w, img_h, color=self.game_card_color)
# for card_img in card_imgs:
# await img.paste(card_img)
return await BuildImage.auto_paste(card_imgs, 10, color=self.game_card_color) # type: ignore
async def generate_card_img(self, card: TC) -> BuildImage:
img = str(self.img_path / f"{cn2py(card.name)}.png")
return BuildImage(100, 100, background=img)
def load_data(self, filename: str = "") -> dict:
if not filename:
filename = f"{self.game_name}.json"
filepath = self.data_path / filename
if not filepath.exists():
return {}
with filepath.open("r", encoding="utf8") as f:
return json.load(f)
def dump_data(self, data: dict, filename: str = ""):
if not filename:
filename = f"{self.game_name}.json"
filepath = self.data_path / filename
with filepath.open("w", encoding="utf8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
def data_exists(self) -> bool:
for file in self.data_files:
if not (self.data_path / file).exists():
return False
return True
def _init_data(self):
raise NotImplementedError
def init_data(self):
try:
self._init_data()
except Exception as e:
logger.warning(f"{self.game_name_cn} 导入角色数据错误:{type(e)}:{e}")
async def _update_info(self):
raise NotImplementedError
def client(self) -> aiohttp.ClientSession:
headers = {
"User-Agent": '"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)"'
}
return aiohttp.ClientSession(headers=headers)
async def update_info(self):
try:
async with asyncio.Semaphore(draw_config.SEMAPHORE):
async with self.client() as session:
self.session = session
await self._update_info()
except Exception as e:
logger.warning(f"{self.game_name_cn} 更新数据错误:{type(e)}:{e}")
self.init_data()
async def get_url(self, url: str) -> str:
result = ""
retry = 5
for i in range(retry):
try:
async with self.session.get(url, timeout=10) as response:
result = await response.text()
break
except TimeoutError:
logger.warning(f"访问 {url} 超时, 重试 {i + 1}/{retry}")
await asyncio.sleep(1)
return result
async def download_img(self, url: str, name: str) -> bool:
img_path = self.img_path / f"{cn2py(name)}.png"
if img_path.exists():
return True
try:
async with self.session.get(url, timeout=10) as response:
async with await anyio.open_file(img_path, "wb") as f:
await f.write(await response.read())
return True
except TimeoutError:
logger.warning(
f"下载 {self.game_name_cn} 图片超时,名称:{name},url:{url}"
)
return False
except:
logger.warning(
f"下载 {self.game_name_cn} 链接错误,名称:{name},url:{url}"
)
return False
async def _reload_pool(self) -> MessageFactory | None:
return None
async def reload_pool(self) -> MessageFactory | None:
try:
async with self.client() as session:
self.session = session
return await self._reload_pool()
except Exception as e:
logger.warning(f"{self.game_name_cn} 重载UP池错误", e=e)
def reset_count(self, user_id: str) -> bool:
return False
@@ -0,0 +1,223 @@
import random
import ujson as json
from lxml import etree
from PIL import ImageDraw
from zhenxun.services.log import logger
from zhenxun.utils.image_utils import BuildImage
from ..config import draw_config
from ..util import cn2py, load_font, remove_prohibited_str
from .base_handle import BaseData, BaseHandle
class FgoData(BaseData):
pass
class FgoChar(FgoData):
pass
class FgoCard(FgoData):
pass
class FgoHandle(BaseHandle[FgoData]):
def __init__(self):
super().__init__("fgo", "命运-冠位指定")
self.data_files.append("fgo_card.json")
self.max_star = 5
self.config = draw_config.fgo
self.ALL_CHAR: list[FgoChar] = []
self.ALL_CARD: list[FgoCard] = []
def get_card(self, mode: int = 1) -> FgoData:
if mode == 1:
star = self.get_star(
[8, 7, 6, 5, 4, 3],
[
self.config.FGO_SERVANT_FIVE_P,
self.config.FGO_SERVANT_FOUR_P,
self.config.FGO_SERVANT_THREE_P,
self.config.FGO_CARD_FIVE_P,
self.config.FGO_CARD_FOUR_P,
self.config.FGO_CARD_THREE_P,
],
)
elif mode == 2:
star = self.get_star(
[5, 4], [self.config.FGO_CARD_FIVE_P, self.config.FGO_CARD_FOUR_P]
)
else:
star = self.get_star(
[8, 7, 6],
[
self.config.FGO_SERVANT_FIVE_P,
self.config.FGO_SERVANT_FOUR_P,
self.config.FGO_SERVANT_THREE_P,
],
)
if star > 5:
star -= 3
chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited]
else:
chars = [x for x in self.ALL_CARD if x.star == star and not x.limited]
return random.choice(chars)
def get_cards(self, count: int, **kwargs) -> list[tuple[FgoData, int]]:
card_list = [] # 获取所有角色
servant_count = 0 # 保底计算
card_count = 0 # 保底计算
for i in range(count):
servant_count += 1
card_count += 1
if card_count == 9: # 四星卡片保底
mode = 2
elif servant_count == 10: # 三星从者保底
mode = 3
else: # 普通抽
mode = 1
card = self.get_card(mode)
if isinstance(card, FgoCard) and card.star > self.max_star - 2:
card_count = 0
if isinstance(card, FgoChar):
servant_count = 0
card_list.append((card, i + 1))
return card_list
async def generate_card_img(self, card: FgoData) -> BuildImage:
sep_w = 5
sep_t = 5
sep_b = 20
w = 128
h = 140
bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b)
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
img = BuildImage(w, h, background=img_path)
await bg.paste(img, (sep_w, sep_t))
# 加名字
text = card.name[:6] + "..." if len(card.name) > 7 else card.name
font = load_font(fontsize=16)
text_w, text_h = BuildImage.get_text_size(text, font)
draw = ImageDraw.Draw(bg.markImg)
draw.text(
(sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2),
text,
font=font,
fill="gray",
)
return bg
def _init_data(self):
self.ALL_CHAR = [
FgoChar(
name=value["名称"],
star=int(value["星级"]),
limited=(
True
if not (
"圣晶石召唤" in value["入手方式"]
or "圣晶石召唤(Story卡池)" in value["入手方式"]
)
else False
),
)
for value in self.load_data().values()
]
self.ALL_CARD = [
FgoCard(name=value["名称"], star=int(value["星级"]), limited=False)
for value in self.load_data("fgo_card.json").values()
]
async def _update_info(self):
# TODO: fgo获取链接失效
fgo_info = {}
for i in range(500):
url = f"http://fgo.vgtime.com/servant/ajax?card=&wd=&ids=&sort=12777&o=desc&pn={i}"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} page {i} 出错")
continue
fgo_data = json.loads(result)
if int(fgo_data["nums"]) <= 0:
break
for x in fgo_data["data"]:
name = remove_prohibited_str(x["name"])
member_dict = {
"id": x["id"],
"card_id": x["charid"],
"头像": x["icon"],
"名称": remove_prohibited_str(x["name"]),
"职阶": x["classes"],
"星级": int(x["star"]),
"hp": x["lvmax4hp"],
"atk": x["lvmax4atk"],
"card_quick": x["cardquick"],
"card_arts": x["cardarts"],
"card_buster": x["cardbuster"],
"宝具": x["tprop"],
}
fgo_info[name] = member_dict
# 更新额外信息
for key in fgo_info.keys():
url = f'http://fgo.vgtime.com/servant/{fgo_info[key]["id"]}'
result = await self.get_url(url)
if not result:
fgo_info[key]["入手方式"] = ["圣晶石召唤"]
logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
continue
try:
dom = etree.HTML(result, etree.HTMLParser())
obtain = dom.xpath(
"//table[contains(string(.),'入手方式')]/tr[8]/td[3]/text()"
)[0]
obtain = str(obtain).strip()
if "限时活动免费获取 活动结束后无法获得" in obtain:
obtain = ["活动获取"]
elif "非限时UP无法获得" in obtain:
obtain = ["限时召唤"]
else:
if "&" in obtain:
obtain = obtain.split("&")
else:
obtain = obtain.split(" ")
obtain = [s.strip() for s in obtain if s.strip()]
fgo_info[key]["入手方式"] = obtain
except IndexError:
fgo_info[key]["入手方式"] = ["圣晶石召唤"]
logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
self.dump_data(fgo_info)
logger.info(f"{self.game_name_cn} 更新成功")
# fgo_card.json
fgo_card_info = {}
for i in range(500):
url = f"http://fgo.vgtime.com/equipment/ajax?wd=&ids=&sort=12958&o=desc&pn={i}"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn}卡牌 page {i} 出错")
continue
fgo_data = json.loads(result)
if int(fgo_data["nums"]) <= 0:
break
for x in fgo_data["data"]:
name = remove_prohibited_str(x["name"])
member_dict = {
"id": x["id"],
"card_id": x["equipid"],
"头像": x["icon"],
"名称": name,
"星级": int(x["star"]),
"hp": x["lvmax_hp"],
"atk": x["lvmax_atk"],
"skill_e": str(x["skill_e"]).split("<br />")[:-1],
}
fgo_card_info[name] = member_dict
self.dump_data(fgo_card_info, "fgo_card.json")
logger.info(f"{self.game_name_cn} 卡牌更新成功")
# 下载头像
for value in fgo_info.values():
await self.download_img(value["头像"], value["名称"])
for value in fgo_card_info.values():
await self.download_img(value["头像"], value["名称"])
@@ -0,0 +1,465 @@
import random
from datetime import datetime, timedelta
from urllib.parse import unquote
import dateparser
import ujson as json
from lxml import etree
from nonebot_plugin_saa import Image as SaaImage
from nonebot_plugin_saa import MessageFactory, Text
from PIL import Image, ImageDraw
from pydantic import ValidationError
from zhenxun.services.log import logger
from zhenxun.utils.image_utils import BuildImage
from ..config import draw_config
from ..count_manager import GenshinCountManager
from ..util import cn2py, load_font, remove_prohibited_str
from .base_handle import BaseData, BaseHandle, UpChar, UpEvent
class GenshinData(BaseData):
pass
class GenshinChar(GenshinData):
pass
class GenshinArms(GenshinData):
pass
class GenshinHandle(BaseHandle[GenshinData]):
def __init__(self):
super().__init__("genshin", "原神")
self.data_files.append("genshin_arms.json")
self.max_star = 5
self.game_card_color = "#ebebeb"
self.config = draw_config.genshin
self.ALL_CHAR: list[GenshinData] = []
self.ALL_ARMS: list[GenshinData] = []
self.UP_CHAR: UpEvent | None = None
self.UP_CHAR_LIST: UpEvent | None = []
self.UP_ARMS: UpEvent | None = None
self.count_manager = GenshinCountManager((10, 90), ("4", "5"), 180)
# 抽取卡池
def get_card(
self,
pool_name: str,
mode: int = 1,
add: float = 0.0,
is_up: bool = False,
card_index: int = 0,
):
"""
mode 1:普通抽 2:四星保底 3:五星保底
"""
if mode == 1:
star = self.get_star(
[5, 4, 3],
[
self.config.GENSHIN_FIVE_P + add,
self.config.GENSHIN_FOUR_P,
self.config.GENSHIN_THREE_P,
],
)
elif mode == 2:
star = self.get_star(
[5, 4],
[self.config.GENSHIN_G_FIVE_P + add, self.config.GENSHIN_G_FOUR_P],
)
else:
star = 5
if pool_name == "char":
up_event = self.UP_CHAR_LIST[card_index]
all_list = self.ALL_CHAR + [
x for x in self.ALL_ARMS if x.star == star and x.star < 5
]
elif pool_name == "arms":
up_event = self.UP_ARMS
all_list = self.ALL_ARMS + [
x for x in self.ALL_CHAR if x.star == star and x.star < 5
]
else:
up_event = None
all_list = self.ALL_ARMS + self.ALL_CHAR
acquire_char = None
# 是否UP
if up_event and star > 3:
# 获取up角色列表
up_list = [x.name for x in up_event.up_char if x.star == star]
# 成功获取up角色
if random.random() < 0.5 or is_up:
up_name = random.choice(up_list)
try:
acquire_char = [x for x in all_list if x.name == up_name][0]
except IndexError:
pass
if not acquire_char:
chars = [x for x in all_list if x.star == star and not x.limited]
acquire_char = random.choice(chars)
return acquire_char
def get_cards(
self, count: int, user_id: int, pool_name: str, card_index: int = 0
) -> list[tuple[GenshinData, int]]:
card_list = [] # 获取角色列表
add = 0.0
count_manager = self.count_manager
count_manager.check_count(user_id, count) # 检查次数累计
pool = self.UP_CHAR_LIST[card_index] if pool_name == "char" else self.UP_ARMS
for i in range(count):
count_manager.increase(user_id)
star = count_manager.check(user_id) # 是否有四星或五星保底
if (
count_manager.get_user_count(user_id)
- count_manager.get_user_five_index(user_id)
) % count_manager.get_max_guarantee() >= 72:
add += draw_config.genshin.I72_ADD
if star:
if star == 4:
card = self.get_card(pool_name, 2, add=add, card_index=card_index)
else:
card = self.get_card(
pool_name,
3,
add,
count_manager.is_up(user_id),
card_index=card_index,
)
else:
card = self.get_card(
pool_name,
1,
add,
count_manager.is_up(user_id),
card_index=card_index,
)
# print(f"{count_manager.get_user_count(user_id)}:",
# count_manager.get_user_five_index(user_id), star, card.star, add)
# 四星角色
if card.star == 4:
count_manager.mark_four_index(user_id)
# 五星角色
elif card.star == self.max_star:
add = 0
count_manager.mark_five_index(user_id) # 记录五星保底
count_manager.mark_four_index(user_id) # 记录四星保底
if pool and card.name in [
x.name for x in pool.up_char if x.star == self.max_star
]:
count_manager.set_is_up(user_id, True)
else:
count_manager.set_is_up(user_id, False)
card_list.append((card, count_manager.get_user_count(user_id)))
return card_list
async def generate_card_img(self, card: GenshinData) -> BuildImage:
sep_w = 10
sep_h = 5
frame_w = 112
frame_h = 132
img_w = 106
img_h = 106
bg = BuildImage(frame_w + sep_w * 2, frame_h + sep_h * 2, color="#EBEBEB")
frame_path = str(self.img_path / "avatar_frame.png")
frame = Image.open(frame_path)
# 加名字
text = card.name
font = load_font(fontsize=14)
text_w, text_h = BuildImage.get_text_size(text, font)
draw = ImageDraw.Draw(frame)
draw.text(
((frame_w - text_w) / 2, frame_h - 15 - text_h / 2),
text,
font=font,
fill="gray",
)
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
img = BuildImage(img_w, img_h, background=img_path)
if isinstance(card, GenshinArms):
# 武器卡背景不是透明的,切去上方两个圆弧
r = 12
circle = Image.new("L", (r * 2, r * 2), 0)
alpha = Image.new("L", img.size, 255)
alpha.paste(circle, (-r - 3, -r - 3)) # 左上角
alpha.paste(circle, (img_h - r + 3, -r - 3)) # 右上角
img.markImg.putalpha(alpha)
star_path = str(self.img_path / f"{card.star}_star.png")
star = Image.open(star_path)
await bg.paste(frame, (sep_w, sep_h))
await bg.paste(img, (sep_w + 3, sep_h + 3))
await bg.paste(star, (sep_w + int((frame_w - star.width) / 2), sep_h - 6))
return bg
def format_pool_info(self, pool_name: str, card_index: int = 0) -> str:
info = ""
up_event = None
if pool_name == "char":
up_event = self.UP_CHAR_LIST[card_index]
elif pool_name == "arms":
up_event = self.UP_ARMS
if up_event:
star5_list = [x.name for x in up_event.up_char if x.star == 5]
star4_list = [x.name for x in up_event.up_char if x.star == 4]
if star5_list:
info += f"五星UP:{' '.join(star5_list)}\n"
if star4_list:
info += f"四星UP:{' '.join(star4_list)}\n"
info = f"当前up池:{up_event.title}\n{info}"
return info.strip()
async def draw(
self, count: int, user_id: int, pool_name: str = "", **kwargs
) -> Text | MessageFactory:
card_index = 0
if "1" in pool_name:
card_index = 1
pool_name = pool_name.replace("1", "")
index2cards = self.get_cards(count, user_id, pool_name, card_index)
cards = [card[0] for card in index2cards]
up_event = None
if pool_name == "char":
if card_index == 1 and len(self.UP_CHAR_LIST) == 1:
return Text("当前没有第二个角色UP池")
up_event = self.UP_CHAR_LIST[card_index]
elif pool_name == "arms":
up_event = self.UP_ARMS
up_list = [x.name for x in up_event.up_char] if up_event else []
result = self.format_star_result(cards)
result += (
"\n" + max_star_str
if (max_star_str := self.format_max_star(index2cards, up_list=up_list))
else ""
)
result += f"\n距离保底发还剩 {self.count_manager.get_user_guarantee_count(user_id)} 抽"
# result += "\n【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】"
pool_info = self.format_pool_info(pool_name, card_index)
img = await self.generate_img(cards)
bk = BuildImage(img.width, img.height + 50, font_size=20, color="#ebebeb")
await bk.paste(img)
await bk.text(
(0, img.height + 10),
"【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】",
)
return MessageFactory([Text(pool_info), SaaImage(bk.pic2bytes()), Text(result)])
def _init_data(self):
self.ALL_CHAR = [
GenshinChar(
name=value["名称"],
star=int(value["星级"]),
limited=value["常驻/限定"] == "限定UP",
)
for key, value in self.load_data().items()
if "旅行者" not in key
]
self.ALL_ARMS = [
GenshinArms(
name=value["名称"],
star=int(value["星级"]),
limited="祈愿" not in value["获取途径"],
)
for value in self.load_data("genshin_arms.json").values()
]
self.load_up_char()
def load_up_char(self):
try:
data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char", {})))
self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char1", {})))
self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {}))
except ValidationError:
logger.warning(f"{self.game_name}_up_char 解析出错")
def dump_up_char(self):
if self.UP_CHAR_LIST and self.UP_ARMS:
data = {
"char": json.loads(self.UP_CHAR_LIST[0].json()),
"arms": json.loads(self.UP_ARMS.json()),
}
if len(self.UP_CHAR_LIST) > 1:
data["char1"] = json.loads(self.UP_CHAR_LIST[1].json())
self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
async def _update_info(self):
# genshin.json
char_info = {}
url = "https://wiki.biligame.com/ys/角色筛选"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} 出错")
else:
dom = etree.HTML(result, etree.HTMLParser())
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
for char in char_list:
try:
name = char.xpath("./td[1]/a/@title")[0]
avatar = char.xpath("./td[1]/a/img/@srcset")[0]
star = char.xpath("./td[3]/text()")[0]
except IndexError:
continue
member_dict = {
"头像": unquote(str(avatar).split(" ")[-2]),
"名称": remove_prohibited_str(name),
"星级": int(str(star).strip()[:1]),
}
char_info[member_dict["名称"]] = member_dict
# 更新额外信息
for key in char_info.keys():
result = await self.get_url(f"https://wiki.biligame.com/ys/{key}")
if not result:
char_info[key]["常驻/限定"] = "未知"
logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
continue
try:
dom = etree.HTML(result, etree.HTMLParser())
limit = dom.xpath(
"//table[contains(string(.),'常驻/限定')]/tbody/tr[6]/td/text()"
)[0]
char_info[key]["常驻/限定"] = str(limit).strip()
except IndexError:
char_info[key]["常驻/限定"] = "未知"
logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
self.dump_data(char_info)
logger.info(f"{self.game_name_cn} 更新成功")
# genshin_arms.json
arms_info = {}
url = "https://wiki.biligame.com/ys/武器图鉴"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} 出错")
else:
dom = etree.HTML(result, etree.HTMLParser())
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
for char in char_list:
try:
name = char.xpath("./td[1]/a/@title")[0]
avatar = char.xpath("./td[1]/a/img/@srcset")[0]
star = char.xpath("./td[4]/img/@alt")[0]
sources = str(char.xpath("./td[5]/text()")[0]).split(",")
except IndexError:
continue
member_dict = {
"头像": unquote(str(avatar).split(" ")[-2]),
"名称": remove_prohibited_str(name),
"星级": int(str(star).strip()[:1]),
"获取途径": [s.strip() for s in sources if s.strip()],
}
arms_info[member_dict["名称"]] = member_dict
self.dump_data(arms_info, "genshin_arms.json")
logger.info(f"{self.game_name_cn} 武器更新成功")
# 下载头像
for value in char_info.values():
await self.download_img(value["头像"], value["名称"])
for value in arms_info.values():
await self.download_img(value["头像"], value["名称"])
# 下载星星
idx = 1
YS_URL = "https://patchwiki.biligame.com/images/ys"
for url in [
"/1/13/7xzg7tgf8dsr2hjpmdbm5gn9wvzt2on.png",
"/b/bc/sd2ige6d7lvj7ugfumue3yjg8gyi0d1.png",
"/e/ec/l3mnhy56pyailhn3v7r873htf2nofau.png",
"/9/9c/sklp02ffk3aqszzvh8k1c3139s0awpd.png",
"/c/c7/qu6xcndgj6t14oxvv7yz2warcukqv1m.png",
]:
await self.download_img(YS_URL + url, f"{idx}_star")
idx += 1
# 下载头像框
await self.download_img(
YS_URL + "/2/2e/opbcst4xbtcq0i4lwerucmosawn29ti.png", f"avatar_frame"
)
await self.update_up_char()
async def update_up_char(self):
self.UP_CHAR_LIST = []
url = "https://wiki.biligame.com/ys/祈愿"
result = await self.get_url(url)
if not result:
logger.warning(f"{self.game_name_cn}获取祈愿页面出错")
return
dom = etree.HTML(result, etree.HTMLParser())
tables = dom.xpath(
"//div[@class='mw-parser-output']/div[@class='row']/div/table[@class='wikitable']/tbody"
)
if not tables or len(tables) < 2:
logger.warning(f"{self.game_name_cn}获取活动祈愿出错")
return
try:
for index, table in enumerate(tables):
title = table.xpath("./tr[1]/th/img/@title")[0]
title = str(title).split("」")[0] + "」" if "」" in title else title
pool_img = str(table.xpath("./tr[1]/th/img/@srcset")[0]).split(" ")[-2]
time = table.xpath("./tr[2]/td/text()")[0]
star5_list = table.xpath("./tr[3]/td/a/@title")
star4_list = table.xpath("./tr[4]/td/a/@title")
start, end = str(time).split("~")
start_time = dateparser.parse(start)
end_time = dateparser.parse(end)
if not start_time and end_time:
start_time = end_time - timedelta(days=20)
if start_time and end_time and start_time <= datetime.now() <= end_time:
up_event = UpEvent(
title=title,
pool_img=pool_img,
start_time=start_time,
end_time=end_time,
up_char=[
UpChar(name=name, star=5, limited=False, zoom=50)
for name in star5_list
]
+ [
UpChar(name=name, star=4, limited=False, zoom=50)
for name in star4_list
],
)
if "神铸赋形" not in title:
self.UP_CHAR_LIST.append(up_event)
else:
self.UP_ARMS = up_event
if self.UP_CHAR_LIST and self.UP_ARMS:
self.dump_up_char()
char_title = " & ".join([x.title for x in self.UP_CHAR_LIST])
logger.info(
f"成功获取{self.game_name_cn}当前up信息...当前up池: {char_title} & {self.UP_ARMS.title}"
)
except Exception as e:
logger.warning(f"{self.game_name_cn}UP更新出错", e=e)
def reset_count(self, user_id: str) -> bool:
self.count_manager.reset(user_id)
return True
async def _reload_pool(self) -> MessageFactory | None:
await self.update_up_char()
self.load_up_char()
if self.UP_CHAR_LIST and self.UP_ARMS:
if len(self.UP_CHAR_LIST) > 1:
return MessageFactory(
[
Text(
f"重载成功!\n当前UP池子:{self.UP_CHAR_LIST[0].title} & {self.UP_CHAR_LIST[1].title} & {self.UP_ARMS.title}"
),
Image(self.UP_CHAR_LIST[0].pool_img),
Image(self.UP_CHAR_LIST[1].pool_img),
Image(self.UP_ARMS.pool_img),
]
)
return MessageFactory(
[
Text(
f"重载成功!\n当前UP池子:{char_title} & {self.UP_ARMS.title}"
),
Image(self.UP_CHAR_LIST[0].pool_img),
Image(self.UP_ARMS.pool_img),
]
)
@@ -0,0 +1,399 @@
import random
import re
from datetime import datetime
from urllib.parse import unquote
import dateparser
import ujson as json
from lxml import etree
from nonebot_plugin_saa import Image, MessageFactory, Text
from PIL import ImageDraw
from pydantic import ValidationError
from zhenxun.services.log import logger
from zhenxun.utils.image_utils import BuildImage
from ..config import draw_config
from ..util import cn2py, load_font, remove_prohibited_str
from .base_handle import BaseData, BaseHandle, UpChar, UpEvent
class GuardianData(BaseData):
pass
class GuardianChar(GuardianData):
pass
class GuardianArms(GuardianData):
pass
class GuardianHandle(BaseHandle[GuardianData]):
def __init__(self):
super().__init__("guardian", "坎公骑冠剑")
self.data_files.append("guardian_arms.json")
self.config = draw_config.guardian
self.ALL_CHAR: list[GuardianChar] = []
self.ALL_ARMS: list[GuardianArms] = []
self.UP_CHAR: UpEvent | None = None
self.UP_ARMS: UpEvent | None = None
def get_card(self, pool_name: str, mode: int = 1) -> GuardianData:
if pool_name == "char":
if mode == 1:
star = self.get_star(
[3, 2, 1],
[
self.config.GUARDIAN_THREE_CHAR_P,
self.config.GUARDIAN_TWO_CHAR_P,
self.config.GUARDIAN_ONE_CHAR_P,
],
)
else:
star = self.get_star(
[3, 2],
[
self.config.GUARDIAN_THREE_CHAR_P,
self.config.GUARDIAN_TWO_CHAR_P,
],
)
up_event = self.UP_CHAR
self.max_star = 3
all_data = self.ALL_CHAR
else:
if mode == 1:
star = self.get_star(
[5, 4, 3, 2],
[
self.config.GUARDIAN_FIVE_ARMS_P,
self.config.GUARDIAN_FOUR_ARMS_P,
self.config.GUARDIAN_THREE_ARMS_P,
self.config.GUARDIAN_TWO_ARMS_P,
],
)
else:
star = self.get_star(
[5, 4],
[
self.config.GUARDIAN_FIVE_ARMS_P,
self.config.GUARDIAN_FOUR_ARMS_P,
],
)
up_event = self.UP_ARMS
self.max_star = 5
all_data = self.ALL_ARMS
acquire_char = None
# 是否UP
if up_event and star == self.max_star and pool_name:
# 获取up角色列表
up_list = [x.name for x in up_event.up_char if x.star == star]
# 成功获取up角色
if random.random() < 0.5:
up_name = random.choice(up_list)
try:
acquire_char = [x for x in all_data if x.name == up_name][0]
except IndexError:
pass
if not acquire_char:
chars = [x for x in all_data if x.star == star and not x.limited]
acquire_char = random.choice(chars)
return acquire_char
def get_cards(self, count: int, pool_name: str) -> list[tuple[GuardianData, int]]:
card_list = []
card_count = 0 # 保底计算
for i in range(count):
card_count += 1
# 十连保底
if card_count == 10:
card = self.get_card(pool_name, 2)
card_count = 0
else:
card = self.get_card(pool_name, 1)
if card.star > self.max_star - 2:
card_count = 0
card_list.append((card, i + 1))
return card_list
def format_pool_info(self, pool_name: str) -> str:
info = ""
up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS
if up_event:
if pool_name == "char":
up_list = [x.name for x in up_event.up_char if x.star == 3]
info += f'三星UP:{" ".join(up_list)}\n'
else:
up_list = [x.name for x in up_event.up_char if x.star == 5]
info += f'五星UP:{" ".join(up_list)}\n'
info = f"当前up池:{up_event.title}\n{info}"
return info.strip()
async def draw(self, count: int, pool_name: str, **kwargs) -> MessageFactory:
index2card = self.get_cards(count, pool_name)
cards = [card[0] for card in index2card]
up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS
up_list = [x.name for x in up_event.up_char] if up_event else []
result = self.format_result(index2card, up_list=up_list)
pool_info = self.format_pool_info(pool_name)
img = await self.generate_img(cards)
return MessageFactory([Text(pool_info), Image(img.pic2bytes()), Text(result)])
async def generate_card_img(self, card: GuardianData) -> BuildImage:
sep_w = 1
sep_h = 1
block_w = 170
block_h = 90
img_w = 90
img_h = 90
if isinstance(card, GuardianChar):
block_color = "#2e2923"
font_color = "#e2ccad"
star_w = 90
star_h = 30
star_name = f"{card.star}_star.png"
frame_path = ""
else:
block_color = "#EEE4D5"
font_color = "#A65400"
star_w = 45
star_h = 45
star_name = f"{card.star}_star_rank.png"
frame_path = str(self.img_path / "avatar_frame.png")
bg = BuildImage(block_w + sep_w * 2, block_h + sep_h * 2, color="#F6F4ED")
block = BuildImage(block_w, block_h, color=block_color)
star_path = str(self.img_path / star_name)
star = BuildImage(star_w, star_h, background=star_path)
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
img = BuildImage(img_w, img_h, background=img_path)
await block.paste(img, (0, 0))
if frame_path:
frame = BuildImage(img_w, img_h, background=frame_path)
await block.paste(frame, (0, 0))
await block.paste(
star,
(int((block_w + img_w - star_w) / 2), block_h - star_h - 30),
)
# 加名字
text = card.name[:4] + "..." if len(card.name) > 5 else card.name
font = load_font(fontsize=14)
text_w, _ = BuildImage.get_text_size(text, font)
draw = ImageDraw.Draw(block.markImg)
draw.text(
((block_w + img_w - text_w) / 2, 55),
text,
font=font,
fill=font_color,
)
await bg.paste(block, (sep_w, sep_h))
return bg
def _init_data(self):
self.ALL_CHAR = [
GuardianChar(name=value["名称"], star=int(value["星级"]), limited=False)
for value in self.load_data().values()
]
self.ALL_ARMS = [
GuardianArms(name=value["名称"], star=int(value["星级"]), limited=False)
for value in self.load_data("guardian_arms.json").values()
]
self.load_up_char()
def load_up_char(self):
try:
data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
self.UP_CHAR = UpEvent.parse_obj(data.get("char", {}))
self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {}))
except ValidationError:
logger.warning(f"{self.game_name}_up_char 解析出错")
def dump_up_char(self):
if self.UP_CHAR and self.UP_ARMS:
data = {
"char": json.loads(self.UP_CHAR.json()),
"arms": json.loads(self.UP_ARMS.json()),
}
self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
async def _update_info(self):
# guardian.json
guardian_info = {}
url = "https://wiki.biligame.com/gt/英雄筛选表"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} 出错")
else:
dom = etree.HTML(result, etree.HTMLParser())
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
for char in char_list:
try:
# name = char.xpath("./td[1]/a/@title")[0]
# avatar = char.xpath("./td[1]/a/img/@src")[0]
# star = char.xpath("./td[1]/span/img/@alt")[0]
name = char.xpath("./th[1]/a[1]/@title")[0]
avatar = char.xpath("./th[1]/a/img/@src")[0]
star = char.xpath("./th[1]/span/img/@alt")[0]
except IndexError:
continue
member_dict = {
"头像": unquote(str(avatar)),
"名称": remove_prohibited_str(name),
"星级": int(str(star).split(" ")[0].replace("Rank", "")),
}
guardian_info[member_dict["名称"]] = member_dict
self.dump_data(guardian_info)
logger.info(f"{self.game_name_cn} 更新成功")
# guardian_arms.json
guardian_arms_info = {}
url = "https://wiki.biligame.com/gt/武器"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} 武器出错")
else:
dom = etree.HTML(result, etree.HTMLParser())
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
for char in char_list:
try:
name = char.xpath("./td[2]/a/@title")[0]
avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0]
url = char.xpath("./td[3]/img/@srcset")[0]
if r := re.search(r"Rank-mini-star_(\d).png", url):
star = r.group(1)
else:
continue
except IndexError:
continue
member_dict = {
"头像": unquote(str(avatar)),
"名称": remove_prohibited_str(name),
"星级": int(str(star).strip()),
}
guardian_arms_info[member_dict["名称"]] = member_dict
self.dump_data(guardian_arms_info, "guardian_arms.json")
logger.info(f"{self.game_name_cn} 武器更新成功")
url = "https://wiki.biligame.com/gt/盾牌"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} 盾牌出错")
else:
dom = etree.HTML(result, etree.HTMLParser())
char_list = dom.xpath(
"//div[@class='resp-tabs-container']/div[2]/div/table[1]/tbody/tr"
)
for char in char_list:
try:
name = char.xpath("./td[2]/a/@title")[0]
avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0]
star = char.xpath("./td[3]/text()")[0]
except IndexError:
continue
member_dict = {
"头像": unquote(str(avatar)),
"名称": remove_prohibited_str(name),
"星级": int(str(star).strip()),
}
guardian_arms_info[member_dict["名称"]] = member_dict
self.dump_data(guardian_arms_info, "guardian_arms.json")
logger.info(f"{self.game_name_cn} 盾牌更新成功")
# 下载头像
for value in guardian_info.values():
await self.download_img(value["头像"], value["名称"])
for value in guardian_arms_info.values():
await self.download_img(value["头像"], value["名称"])
# 下载星星
idx = 1
GT_URL = "https://patchwiki.biligame.com/images/gt"
for url in [
"/4/4b/ardr3bi2yf95u4zomm263tc1vke6i3i.png",
"/5/55/6vow7lh76gzus6b2g9cfn325d1sugca.png",
"/b/b9/du8egrd2vyewg0cuyra9t8jh0srl0ds.png",
]:
await self.download_img(GT_URL + url, f"{idx}_star")
idx += 1
# 另一种星星
idx = 1
for url in [
"/6/66/4e2tfa9kvhfcbikzlyei76i9crva145.png",
"/1/10/r9ihsuvycgvsseyneqz4xs22t53026m.png",
"/7/7a/o0k86ru9k915y04azc26hilxead7xp1.png",
"/c/c9/rxz99asysz0rg391j3b02ta09mnpa7v.png",
"/2/2a/sfxz0ucv1s6ewxveycz9mnmrqs2rw60.png",
]:
await self.download_img(GT_URL + url, f"{idx}_star_rank")
idx += 1
# 头像框
await self.download_img(
GT_URL + "/8/8e/ogbqslbhuykjhnc8trtoa0p0nhfzohs.png", f"avatar_frame"
)
await self.update_up_char()
async def update_up_char(self):
url = "https://wiki.biligame.com/gt/首页"
result = await self.get_url(url)
if not result:
logger.warning(f"{self.game_name_cn}获取公告出错")
return
try:
dom = etree.HTML(result, etree.HTMLParser())
announcement = dom.xpath(
"//div[@class='mw-parser-output']/div/div[3]/div[2]/div/div[2]/div[3]"
)[0]
title = announcement.xpath("./font/p/b/text()")[0]
match = re.search(r"从(.*?)开始.*?至(.*?)结束", title)
if not match:
logger.warning(f"{self.game_name_cn}找不到UP时间")
return
start, end = match.groups()
start_time = dateparser.parse(start.replace("月", "/").replace("日", ""))
end_time = dateparser.parse(end.replace("月", "/").replace("日", ""))
if not (start_time and end_time) or not (
start_time <= datetime.now() <= end_time
):
return
divs = announcement.xpath("./font/div")
char_index = 0
arms_index = 0
for index, div in enumerate(divs):
if div.xpath("string(.)") == "角色":
char_index = index
elif div.xpath("string(.)") == "武器":
arms_index = index
chars = divs[char_index + 1 : arms_index]
arms = divs[arms_index + 1 :]
up_chars = []
up_arms = []
for char in chars:
name = char.xpath("./p/a/@title")[0]
up_chars.append(UpChar(name=name, star=3, limited=False, zoom=0))
for arm in arms:
name = arm.xpath("./p/a/@title")[0]
up_arms.append(UpChar(name=name, star=5, limited=False, zoom=0))
self.UP_CHAR = UpEvent(
title=title,
pool_img="",
start_time=start_time,
end_time=end_time,
up_char=up_chars,
)
self.UP_ARMS = UpEvent(
title=title,
pool_img="",
start_time=start_time,
end_time=end_time,
up_char=up_arms,
)
self.dump_up_char()
logger.info(f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}")
except Exception as e:
logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}")
async def _reload_pool(self) -> MessageFactory | None:
await self.update_up_char()
self.load_up_char()
if self.UP_CHAR and self.UP_ARMS:
return MessageFactory(
[Text(f"重载成功!\n当前UP池子:{self.UP_CHAR.title}")]
)
@@ -0,0 +1,178 @@
import random
import ujson as json
from lxml import etree
from PIL import Image, ImageDraw
from PIL.Image import Image as IMG
from zhenxun.services.log import logger
from zhenxun.utils.image_utils import BuildImage
from ..config import draw_config
from ..util import cn2py, load_font, remove_prohibited_str
from .base_handle import BaseData, BaseHandle
class OnmyojiChar(BaseData):
@property
def star_str(self) -> str:
return ["N", "R", "SR", "SSR", "SP"][self.star - 1]
class OnmyojiHandle(BaseHandle[OnmyojiChar]):
def __init__(self):
super().__init__("onmyoji", "阴阳师")
self.max_star = 5
self.config = draw_config.onmyoji
self.ALL_CHAR: list[OnmyojiChar] = []
def get_card(self, **kwargs) -> OnmyojiChar:
star = self.get_star(
[5, 4, 3, 2],
[
self.config.ONMYOJI_SP,
self.config.ONMYOJI_SSR,
self.config.ONMYOJI_SR,
self.config.ONMYOJI_R,
],
)
chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited]
return random.choice(chars)
def format_max_star(self, card_list: list[tuple[OnmyojiChar, int]]) -> str:
rst = ""
for card, index in card_list:
if card.star == self.max_star:
rst += f"第 {index} 抽获取SP {card.name}\n"
elif card.star == self.max_star - 1:
rst += f"第 {index} 抽获取SSR {card.name}\n"
return rst.strip()
@staticmethod
def star_label(star: int) -> IMG:
text, color1, color2 = [
("N", "#7E7E82", "#F5F6F7"),
("R", "#014FA8", "#37C6FD"),
("SR", "#6E0AA4", "#E94EFD"),
("SSR", "#E5511D", "#FAF905"),
("SP", "#FA1F2D", "#FFBBAF"),
][star - 1]
w = 200
h = 110
# 制作渐变色图片
base = Image.new("RGBA", (w, h), color1)
top = Image.new("RGBA", (w, h), color2)
mask = Image.new("L", (w, h))
mask_data = []
for y in range(h):
mask_data.extend([int(255 * (y / h))] * w)
mask.putdata(mask_data)
base.paste(top, (0, 0), mask)
# 透明图层
font = load_font("gorga.otf", 100)
alpha = Image.new("L", (w, h))
draw = ImageDraw.Draw(alpha)
draw.text((20, -30), text, fill="white", font=font)
base.putalpha(alpha)
# stroke
bg = Image.new("RGBA", (w, h))
draw = ImageDraw.Draw(bg)
draw.text(
(20, -30),
text,
font=font,
fill="gray",
stroke_width=3,
stroke_fill="gray",
)
bg.paste(base, (0, 0), base)
return bg
async def generate_img(self, card_list: list[OnmyojiChar]) -> BuildImage:
return await super().generate_img(card_list, num_per_line=10)
async def generate_card_img(self, card: OnmyojiChar) -> BuildImage:
bg = BuildImage(73, 240, color="#F1EFE9")
img_path = str(self.img_path / f"{cn2py(card.name)}_mark_btn.png")
img = BuildImage(0, 0, background=img_path)
img = Image.open(img_path).convert("RGBA")
label = self.star_label(card.star).resize((60, 33), Image.ANTIALIAS)
await bg.paste(img, (0, 0))
await bg.paste(label, (0, 135))
font = load_font("msyh.ttf", 16)
draw = ImageDraw.Draw(bg.markImg)
text = "\n".join([t for t in card.name[:4]])
_, text_h = font.getsize_multiline(text, spacing=0)
draw.text(
(40, 150 + (90 - text_h) / 2), text, font=font, fill="gray", spacing=0
)
return bg
def _init_data(self):
self.ALL_CHAR = [
OnmyojiChar(
name=value["名称"],
star=["N", "R", "SR", "SSR", "SP"].index(value["星级"]) + 1,
limited=(
True
if key
in [
"奴良陆生",
"卖药郎",
"鬼灯",
"阿香",
"蜜桃&芥子",
"犬夜叉",
"杀生丸",
"桔梗",
"朽木露琪亚",
"黑崎一护",
"灶门祢豆子",
"灶门炭治郎",
]
else False
),
)
for key, value in self.load_data().items()
]
async def _update_info(self):
info = {}
url = "https://yys.res.netease.com/pc/zt/20161108171335/js/app/all_shishen.json?v74="
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} 出错")
return
data = json.loads(result)
for x in data:
name = remove_prohibited_str(x["name"])
member_dict = {
"id": x["id"],
"名称": name,
"星级": x["level"],
}
info[name] = member_dict
# logger.info(f"{name} is update...")
# 更新头像
for key in info.keys():
url = f'https://yys.163.com/shishen/{info[key]["id"]}.html'
result = await self.get_url(url)
if not result:
info[key]["头像"] = ""
continue
try:
dom = etree.HTML(result, etree.HTMLParser())
avatar = dom.xpath("//div[@class='pic_wrap']/img/@src")[0]
avatar = "https:" + avatar
info[key]["头像"] = avatar
except IndexError:
info[key]["头像"] = ""
logger.warning(f"{self.game_name_cn} 获取头像错误 {key}")
self.dump_data(info)
logger.info(f"{self.game_name_cn} 更新成功")
# 下载头像
for value in info.values():
await self.download_img(value["头像"], value["名称"])
# 下载书签形式的头像
url = f"https://yys.res.netease.com/pc/zt/20161108171335/data/mark_btn/{value['id']}.png"
await self.download_img(url, value["名称"] + "_mark_btn")
@@ -0,0 +1,149 @@
import random
from urllib.parse import unquote
from lxml import etree
from PIL import ImageDraw
from zhenxun.services.log import logger
from zhenxun.utils.image_utils import BuildImage
from ..config import draw_config
from ..util import cn2py, load_font, remove_prohibited_str
from .base_handle import BaseData, BaseHandle
class PcrChar(BaseData):
pass
class PcrHandle(BaseHandle[PcrChar]):
def __init__(self):
super().__init__("pcr", "公主连结")
self.max_star = 3
self.config = draw_config.pcr
self.ALL_CHAR: list[PcrChar] = []
def get_card(self, mode: int = 1) -> PcrChar:
if mode == 2:
star = self.get_star(
[3, 2], [self.config.PCR_G_THREE_P, self.config.PCR_G_TWO_P]
)
else:
star = self.get_star(
[3, 2, 1],
[self.config.PCR_THREE_P, self.config.PCR_TWO_P, self.config.PCR_ONE_P],
)
chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited]
return random.choice(chars)
def get_cards(self, count: int, **kwargs) -> list[tuple[PcrChar, int]]:
card_list = []
card_count = 0 # 保底计算
for i in range(count):
card_count += 1
# 十连保底
if card_count == 10:
card = self.get_card(2)
card_count = 0
else:
card = self.get_card(1)
if card.star > self.max_star - 2:
card_count = 0
card_list.append((card, i + 1))
return card_list
async def generate_card_img(self, card: PcrChar) -> BuildImage:
sep_w = 5
sep_h = 5
star_h = 15
img_w = 90
img_h = 90
font_h = 20
bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5")
star_path = str(self.img_path / "star.png")
star = BuildImage(star_h, star_h, background=star_path)
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
img = BuildImage(img_w, img_h, background=img_path)
await bg.paste(img, (sep_w, sep_h))
for i in range(card.star):
await bg.paste(star, (sep_w + img_w - star_h * (i + 1), sep_h))
# 加名字
text = card.name[:5] + "..." if len(card.name) > 6 else card.name
font = load_font(fontsize=14)
text_w, text_h = BuildImage.get_text_size(text, font)
draw = ImageDraw.Draw(bg.markImg)
draw.text(
(sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2),
text,
font=font,
fill="gray",
)
return bg
def _init_data(self):
self.ALL_CHAR = [
PcrChar(
name=value["名称"],
star=int(value["星级"]),
limited=True if "(" in key else False,
)
for key, value in self.load_data().items()
]
async def _update_info(self):
info = {}
if draw_config.PCR_TAI:
url = "https://wiki.biligame.com/pcr/角色图鉴"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} 出错")
return
dom = etree.HTML(result, etree.HTMLParser())
# TODO: PCR台湾更新失败
char_list = dom.xpath(
"//*[@id='CardSelectCard']/div[@class='unit-icon trcard']"
)
for char in char_list:
try:
name = char.xpath("./a/@title")[0]
avatar = char.xpath("./a/img/@srcset")[0]
star = len(char.xpath("./div[1]/img"))
except IndexError:
continue
member_dict = {
"头像": unquote(str(avatar).split(" ")[-2]),
"名称": remove_prohibited_str(name),
"星级": star,
}
info[member_dict["名称"]] = member_dict
else:
url = "https://wiki.biligame.com/pcr/角色筛选表"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} 出错")
return
dom = etree.HTML(result, etree.HTMLParser())
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
for char in char_list:
try:
name = char.xpath("./td[1]/a/@title")[0]
avatar = char.xpath("./td[1]/a/img/@srcset")[0]
star = char.xpath("./td[4]/text()")[0]
except IndexError:
continue
member_dict = {
"头像": unquote(str(avatar).split(" ")[-2]),
"名称": remove_prohibited_str(name),
"星级": int(str(star).strip()),
}
info[member_dict["名称"]] = member_dict
self.dump_data(info)
logger.info(f"{self.game_name_cn} 更新成功")
# 下载头像
for value in info.values():
await self.download_img(value["头像"], value["名称"])
# 下载星星
await self.download_img(
"https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png",
"star",
)
@@ -0,0 +1,422 @@
import random
import re
from datetime import datetime
from urllib.parse import unquote
import dateparser
import ujson as json
from bs4 import BeautifulSoup
from lxml import etree
from nonebot_plugin_saa import Image, MessageFactory, Text
from PIL import ImageDraw
from pydantic import ValidationError
from zhenxun.services.log import logger
from zhenxun.utils.image_utils import BuildImage
from ..config import draw_config
from ..util import cn2py, load_font, remove_prohibited_str
from .base_handle import BaseData, BaseHandle, UpChar, UpEvent
class PrettyData(BaseData):
pass
class PrettyChar(PrettyData):
pass
class PrettyCard(PrettyData):
@property
def star_str(self) -> str:
return ["R", "SR", "SSR"][self.star - 1]
class PrettyHandle(BaseHandle[PrettyData]):
def __init__(self):
super().__init__("pretty", "赛马娘")
self.data_files.append("pretty_card.json")
self.max_star = 3
self.game_card_color = "#eff2f5"
self.config = draw_config.pretty
self.ALL_CHAR: list[PrettyChar] = []
self.ALL_CARD: list[PrettyCard] = []
self.UP_CHAR: UpEvent | None = None
self.UP_CARD: UpEvent | None = None
def get_card(self, pool_name: str, mode: int = 1) -> PrettyData:
if mode == 1:
star = self.get_star(
[3, 2, 1],
[
self.config.PRETTY_THREE_P,
self.config.PRETTY_TWO_P,
self.config.PRETTY_ONE_P,
],
)
else:
star = self.get_star(
[3, 2], [self.config.PRETTY_THREE_P, self.config.PRETTY_TWO_P]
)
up_pool = None
if pool_name == "char":
up_pool = self.UP_CHAR
all_list = self.ALL_CHAR
else:
up_pool = self.UP_CARD
all_list = self.ALL_CARD
all_char = [x for x in all_list if x.star == star and not x.limited]
acquire_char = None
# 有UP池子
if up_pool and star in [x.star for x in up_pool.up_char]:
up_list = [x.name for x in up_pool.up_char if x.star == star]
# 抽到UP
if random.random() < 1 / len(all_char) * (0.7 / 0.1385):
up_name = random.choice(up_list)
try:
acquire_char = [x for x in all_list if x.name == up_name][0]
except IndexError:
pass
if not acquire_char:
acquire_char = random.choice(all_char)
return acquire_char
def get_cards(self, count: int, pool_name: str) -> list[tuple[PrettyData, int]]:
card_list = []
card_count = 0 # 保底计算
for i in range(count):
card_count += 1
# 十连保底
if card_count == 10:
card = self.get_card(pool_name, 2)
card_count = 0
else:
card = self.get_card(pool_name, 1)
if card.star > self.max_star - 2:
card_count = 0
card_list.append((card, i + 1))
return card_list
def format_pool_info(self, pool_name: str) -> str:
info = ""
up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD
if up_event:
star3_list = [x.name for x in up_event.up_char if x.star == 3]
star2_list = [x.name for x in up_event.up_char if x.star == 2]
star1_list = [x.name for x in up_event.up_char if x.star == 1]
if star3_list:
if pool_name == "char":
info += f'三星UP:{" ".join(star3_list)}\n'
else:
info += f'SSR UP:{" ".join(star3_list)}\n'
if star2_list:
if pool_name == "char":
info += f'二星UP:{" ".join(star2_list)}\n'
else:
info += f'SR UP:{" ".join(star2_list)}\n'
if star1_list:
if pool_name == "char":
info += f'一星UP:{" ".join(star1_list)}\n'
else:
info += f'R UP:{" ".join(star1_list)}\n'
info = f"当前up池:{up_event.title}\n{info}"
return info.strip()
async def draw(self, count: int, pool_name: str, **kwargs) -> MessageFactory:
pool_name = "char" if not pool_name else pool_name
index2card = self.get_cards(count, pool_name)
cards = [card[0] for card in index2card]
up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD
up_list = [x.name for x in up_event.up_char] if up_event else []
result = self.format_result(index2card, up_list=up_list)
pool_info = self.format_pool_info(pool_name)
img = await self.generate_img(cards)
return MessageFactory([Text(pool_info), Image(img.pic2bytes()), Text(result)])
async def generate_card_img(self, card: PrettyData) -> BuildImage:
if isinstance(card, PrettyChar):
star_h = 30
img_w = 200
img_h = 219
font_h = 50
bg = BuildImage(img_w, img_h + font_h, color="#EFF2F5")
star_path = str(self.img_path / "star.png")
star = BuildImage(star_h, star_h, background=star_path)
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
img = BuildImage(img_w, img_h, background=img_path)
star_w = star_h * card.star
for i in range(card.star):
await bg.paste(star, (int((img_w - star_w) / 2) + star_h * i, 0))
await bg.paste(img, (0, 0))
# 加名字
text = card.name[:5] + "..." if len(card.name) > 6 else card.name
font = load_font(fontsize=30)
text_w, _ = font.getsize(text)
draw = ImageDraw.Draw(bg.markImg)
draw.text(
((img_w - text_w) / 2, img_h),
text,
font=font,
fill="gray",
)
return bg
else:
sep_w = 10
img_w = 200
img_h = 267
font_h = 75
bg = BuildImage(img_w + sep_w * 2, img_h + font_h, color="#EFF2F5")
label_path = str(self.img_path / f"{card.star}_label.png")
label = BuildImage(40, 40, background=label_path)
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
img = BuildImage(img_w, img_h, background=img_path)
await bg.paste(img, (sep_w, 0))
await bg.paste(label, (30, 3))
# 加名字
text = ""
texts = []
font = load_font(fontsize=25)
for t in card.name:
if BuildImage.get_text_size((text + t), font)[0] > 190:
texts.append(text)
text = ""
if len(texts) >= 2:
texts[-1] += "..."
break
else:
text += t
if text:
texts.append(text)
text = "\n".join(texts)
text_w, _ = font.getsize_multiline(text)
draw = ImageDraw.Draw(bg.markImg)
draw.text(
((img_w - text_w) / 2, img_h),
text,
font=font,
align="center",
fill="gray",
)
return bg
def _init_data(self):
self.ALL_CHAR = [
PrettyChar(
name=value["名称"],
star=int(value["初始星级"]),
limited=False,
)
for value in self.load_data().values()
]
self.ALL_CARD = [
PrettyCard(
name=value["中文名"],
star=["R", "SR", "SSR"].index(value["稀有度"]) + 1,
limited=True if "卡池" not in value["获取方式"] else False,
)
for value in self.load_data("pretty_card.json").values()
]
self.load_up_char()
def load_up_char(self):
try:
data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
self.UP_CHAR = UpEvent.parse_obj(data.get("char", {}))
self.UP_CARD = UpEvent.parse_obj(data.get("card", {}))
except ValidationError:
logger.warning(f"{self.game_name}_up_char 解析出错")
def dump_up_char(self):
if self.UP_CHAR and self.UP_CARD:
data = {
"char": json.loads(self.UP_CHAR.json()),
"card": json.loads(self.UP_CARD.json()),
}
self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
async def _update_info(self):
# pretty.json
pretty_info = {}
url = "https://wiki.biligame.com/umamusume/赛马娘图鉴"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} 出错")
else:
dom = etree.HTML(result, etree.HTMLParser())
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
for char in char_list:
try:
name = char.xpath("./td[1]/a/@title")[0]
avatar = char.xpath("./td[1]/a/img/@srcset")[0]
star = len(char.xpath("./td[3]/img"))
except IndexError:
continue
member_dict = {
"头像": unquote(str(avatar).split(" ")[-2]),
"名称": remove_prohibited_str(name),
"初始星级": star,
}
pretty_info[member_dict["名称"]] = member_dict
self.dump_data(pretty_info)
logger.info(f"{self.game_name_cn} 更新成功")
# pretty_card.json
pretty_card_info = {}
url = "https://wiki.biligame.com/umamusume/支援卡图鉴"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} 卡牌出错")
else:
dom = etree.HTML(result, etree.HTMLParser())
char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
for char in char_list:
try:
name = char.xpath("./td[1]/div/a/@title")[0]
name_cn = char.xpath("./td[3]/a/text()")[0]
avatar = char.xpath("./td[1]/div/a/img/@srcset")[0]
star = str(char.xpath("./td[5]/text()")[0]).strip()
sources = str(char.xpath("./td[7]/text()")[0]).strip()
except IndexError:
continue
member_dict = {
"头像": unquote(str(avatar).split(" ")[-2]),
"名称": remove_prohibited_str(name),
"中文名": remove_prohibited_str(name_cn),
"稀有度": star,
"获取方式": [sources] if sources else [],
}
pretty_card_info[member_dict["中文名"]] = member_dict
self.dump_data(pretty_card_info, "pretty_card.json")
logger.info(f"{self.game_name_cn} 卡牌更新成功")
# 下载头像
for value in pretty_info.values():
await self.download_img(value["头像"], value["名称"])
for value in pretty_card_info.values():
await self.download_img(value["头像"], value["中文名"])
# 下载星星
PRETTY_URL = "https://patchwiki.biligame.com/images/umamusume"
await self.download_img(
PRETTY_URL + "/1/13/e1hwjz4vmhtvk8wlyb7c0x3ld1s2ata.png", "star"
)
# 下载稀有度标志
idx = 1
for url in [
"/f/f7/afqs7h4snmvovsrlifq5ib8vlpu2wvk.png",
"/3/3b/d1jmpwrsk4irkes1gdvoos4ic6rmuht.png",
"/0/06/q23szwkbtd7pfkqrk3wcjlxxt9z595o.png",
]:
await self.download_img(PRETTY_URL + url, f"{idx}_label")
idx += 1
await self.update_up_char()
async def update_up_char(self):
announcement_url = "https://wiki.biligame.com/umamusume/公告"
result = await self.get_url(announcement_url)
if not result:
logger.warning(f"{self.game_name_cn}获取公告出错")
return
dom = etree.HTML(result, etree.HTMLParser())
announcements = dom.xpath("//div[@id='mw-content-text']/div/div/span/a")
title = ""
url = ""
for announcement in announcements:
try:
title = announcement.xpath("./@title")[0]
url = "https://wiki.biligame.com/" + announcement.xpath("./@href")[0]
if re.match(r".*?\d{8}$", title) or re.match(
r"^\d{1,2}月\d{1,2}日.*?", title
):
break
except IndexError:
continue
if not title:
logger.warning(f"{self.game_name_cn}未找到新UP公告")
return
result = await self.get_url(url)
if not result:
logger.warning(f"{self.game_name_cn}获取UP公告出错")
return
try:
start_time = None
end_time = None
char_img = ""
card_img = ""
up_chars = []
up_cards = []
soup = BeautifulSoup(result, "lxml")
heads = soup.find_all("span", {"class": "mw-headline"})
for head in heads:
if "时间" in head.text:
time = head.find_next("p").text.split("\n")[0]
if "~" in time:
start, end = time.split("~")
start_time = dateparser.parse(start)
end_time = dateparser.parse(end)
elif "赛马娘" in head.text:
char_img = head.find_next("a", {"class": "image"}).find("img")[
"src"
]
lines = str(head.find_next("p").text).split("\n")
chars = [
line
for line in lines
if "★" in line and "(" in line and ")" in line
]
for char in chars:
star = char.count("★")
name = re.split(r"[()]", char)[-2].strip()
up_chars.append(
UpChar(name=name, star=star, limited=False, zoom=70)
)
elif "支援卡" in head.text:
card_img = head.find_next("a", {"class": "image"}).find("img")[
"src"
]
lines = str(head.find_next("p").text).split("\n")
cards = [
line
for line in lines
if "R" in line and "(" in line and ")" in line
]
for card in cards:
star = 3 if "SSR" in card else 2 if "SR" in card else 1
name = re.split(r"[()]", card)[-2].strip()
up_cards.append(
UpChar(name=name, star=star, limited=False, zoom=70)
)
if start_time and end_time:
if start_time <= datetime.now() <= end_time:
self.UP_CHAR = UpEvent(
title=title,
pool_img=char_img,
start_time=start_time,
end_time=end_time,
up_char=up_chars,
)
self.UP_CARD = UpEvent(
title=title,
pool_img=card_img,
start_time=start_time,
end_time=end_time,
up_char=up_cards,
)
self.dump_up_char()
logger.info(
f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}"
)
except Exception as e:
logger.warning(f"{self.game_name_cn}UP更新出错", e=e)
async def _reload_pool(self) -> MessageFactory | None:
await self.update_up_char()
self.load_up_char()
if self.UP_CHAR and self.UP_CARD:
return MessageFactory(
[
Text(f"重载成功!\n当前UP池子:{self.UP_CHAR.title}"),
Image(self.UP_CHAR.pool_img),
Image(self.UP_CARD.pool_img),
]
)
@@ -0,0 +1,343 @@
import random
import re
from datetime import datetime
from urllib.parse import unquote
import dateparser
import ujson as json
from lxml import etree
from lxml.etree import _Element
from nonebot_plugin_saa import Image, MessageFactory, Text
from PIL import ImageDraw
from pydantic import ValidationError
from zhenxun.services.log import logger
from zhenxun.utils.image_utils import BuildImage
from ..config import draw_config
from ..util import cn2py, load_font, remove_prohibited_str
from .base_handle import BaseData, BaseHandle, UpChar, UpEvent
class Operator(BaseData):
recruit_only: bool # 公招限定
event_only: bool # 活动获得干员
core_only: bool # 中坚干员
# special_only: bool # 升变/异格干员
class PrtsHandle(BaseHandle[Operator]):
def __init__(self):
super().__init__(game_name="prts", game_name_cn="明日方舟")
self.max_star = 6
self.game_card_color = "#eff2f5"
self.config = draw_config.prts
self.ALL_OPERATOR: list[Operator] = []
self.UP_EVENT: UpEvent | None = None
def get_card(self, add: float) -> Operator:
star = self.get_star(
star_list=[6, 5, 4, 3],
probability_list=[
self.config.PRTS_SIX_P + add,
self.config.PRTS_FIVE_P,
self.config.PRTS_FOUR_P,
self.config.PRTS_THREE_P,
],
)
all_operators = [
x
for x in self.ALL_OPERATOR
if x.star == star
and not any([x.limited, x.recruit_only, x.event_only, x.core_only])
]
acquire_operator = None
if self.UP_EVENT:
up_operators = [x for x in self.UP_EVENT.up_char if x.star == star]
# UPs
try:
zooms = [x.zoom for x in up_operators]
zoom_sum = sum(zooms)
if random.random() < zoom_sum:
up_name = random.choices(up_operators, weights=zooms, k=1)[0].name
acquire_operator = [
x for x in self.ALL_OPERATOR if x.name == up_name
][0]
except IndexError:
pass
if not acquire_operator:
acquire_operator = random.choice(all_operators)
return acquire_operator
def get_cards(self, count: int, **kwargs) -> list[tuple[Operator, int]]:
card_list = [] # 获取所有角色
add = 0.0
count_idx = 0
for i in range(count):
count_idx += 1
card = self.get_card(add)
if card.star == self.max_star:
add = 0.0
count_idx = 0
elif count_idx > 50:
add += 0.02
card_list.append((card, i + 1))
return card_list
def format_pool_info(self) -> str:
info = ""
if self.UP_EVENT:
star6_list = [x.name for x in self.UP_EVENT.up_char if x.star == 6]
star5_list = [x.name for x in self.UP_EVENT.up_char if x.star == 5]
star4_list = [x.name for x in self.UP_EVENT.up_char if x.star == 4]
if star6_list:
info += f"六星UP:{' '.join(star6_list)}\n"
if star5_list:
info += f"五星UP:{' '.join(star5_list)}\n"
if star4_list:
info += f"四星UP:{' '.join(star4_list)}\n"
info = f"当前up池: {self.UP_EVENT.title}\n{info}"
return info.strip()
async def draw(self, count: int, **kwargs) -> MessageFactory:
index2card = self.get_cards(count)
"""这里cards修复了抽卡图文不符的bug"""
cards = [card[0] for card in index2card]
up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else []
result = self.format_result(index2card, up_list=up_list)
pool_info = self.format_pool_info()
img = await self.generate_img(cards)
return MessageFactory([Text(pool_info), Image(img.pic2bytes()), Text(result)])
async def generate_card_img(self, card: Operator) -> BuildImage:
sep_w = 5
sep_h = 5
star_h = 15
img_w = 120
img_h = 120
font_h = 20
bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5")
star_path = str(self.img_path / "star.png")
star = BuildImage(star_h, star_h, background=star_path)
img_path = str(self.img_path / f"{cn2py(card.name)}.png")
img = BuildImage(img_w, img_h, background=img_path)
await bg.paste(img, (sep_w, sep_h))
for i in range(card.star):
await bg.paste(star, (sep_w + img_w - 5 - star_h * (i + 1), sep_h))
# 加名字
text = card.name[:7] + "..." if len(card.name) > 8 else card.name
font = load_font(fontsize=16)
text_w, text_h = BuildImage.get_text_size(text, font)
draw = ImageDraw.Draw(bg.markImg)
draw.text(
(sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2),
text,
font=font,
fill="gray",
)
return bg
def _init_data(self):
self.ALL_OPERATOR = [
Operator(
name=value["名称"],
star=int(value["星级"]),
limited="标准寻访" not in value["获取途径"]
and "中坚寻访" not in value["获取途径"],
recruit_only=(
True
if "标准寻访" not in value["获取途径"]
and "中坚寻访" not in value["获取途径"]
and "公开招募" in value["获取途径"]
else False
),
event_only=True if "活动获取" in value["获取途径"] else False,
core_only=(
True
if "标准寻访" not in value["获取途径"]
and "中坚寻访" in value["获取途径"]
else False
),
)
for key, value in self.load_data().items()
if "阿米娅" not in key
]
self.load_up_char()
def load_up_char(self):
try:
data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
"""这里的 waring 有点模糊,更新游戏信息时没有up池的情况下也会报错,所以细分了一下"""
if not data:
logger.warning(f"当前无UP池或 {self.game_name}_up_char.json 文件不存在")
else:
self.UP_EVENT = UpEvent.parse_obj(data.get("char", {}))
except ValidationError:
logger.warning(f"{self.game_name}_up_char 解析出错")
def dump_up_char(self):
if self.UP_EVENT:
data = {"char": json.loads(self.UP_EVENT.json())}
self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
async def _update_info(self):
"""更新信息"""
info = {}
url = "https://wiki.biligame.com/arknights/干员数据表"
result = await self.get_url(url)
if not result:
logger.warning(f"更新 {self.game_name_cn} 出错")
return
dom = etree.HTML(result, etree.HTMLParser())
char_list: list[_Element] = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
for char in char_list:
try:
avatar = char.xpath("./td[1]/div/div/div/a/img/@srcset")[0]
name = char.xpath("./td[1]/center/a/text()")[0]
star = char.xpath("./td[2]/text()")[0]
"""这里sources修好了干员获取标签有问题的bug,如三星只能抽到卡缇就是这个原因"""
sources = [_.strip("\n") for _ in char.xpath("./td[7]/text()")]
except IndexError:
continue
member_dict = {
"头像": unquote(str(avatar).split(" ")[-2]),
"名称": remove_prohibited_str(str(name).strip()),
"星级": int(str(star).strip()),
"获取途径": sources,
}
info[member_dict["名称"]] = member_dict
self.dump_data(info)
logger.info(f"{self.game_name_cn} 更新成功")
# 下载头像
for value in info.values():
await self.download_img(value["头像"], value["名称"])
# 下载星星
await self.download_img(
"https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png",
"star",
)
await self.update_up_char()
async def update_up_char(self):
"""重载卡池"""
announcement_url = "https://ak.hypergryph.com/news.html"
result = await self.get_url(announcement_url)
if not result:
logger.warning(f"{self.game_name_cn}获取公告出错")
return
dom = etree.HTML(result, etree.HTMLParser())
activity_urls = dom.xpath(
"//ol[@class='articlelist' and @data-category-key='ACTIVITY']/li/a/@href"
)
start_time = None
end_time = None
up_chars = []
pool_img = ""
for activity_url in activity_urls[:10]: # 减少响应时间, 10个就够了
activity_url = f"https://ak.hypergryph.com{activity_url}"
result = await self.get_url(activity_url)
if not result:
logger.warning(f"{self.game_name_cn}获取公告 {activity_url} 出错")
continue
"""因为鹰角的前端太自由了,这里重写了匹配规则以尽可能避免因为前端乱七八糟而导致的重载失败"""
dom = etree.HTML(result, etree.HTMLParser())
contents = dom.xpath(
"//div[@class='article-content']/p/text() | //div[@class='article-content']/p/span/text() | //div[@class='article-content']/div[@class='media-wrap image-wrap']/img/@src"
)
title = ""
time = ""
chars: list[str] = []
for index, content in enumerate(contents):
if re.search("(.*)(寻访|复刻).*?开启", content):
title = re.split(r"[【】]", content)
title = "".join(title[1:-1]) if "-" in title else title[1]
lines = [
contents[index - 2 + _] for _ in range(8)
] # 从 -2 开始是因为xpath获取的时间有的会在寻访开启这一句之前
lines.append("") # 防止IndexError,加个空字符串
for idx, line in enumerate(lines):
match = re.search(
r"(\d{1,2}月\d{1,2}日.*?-.*?\d{1,2}月\d{1,2}日.*?$)", line
)
if match:
time = match.group(1)
"""因为 <p> 的诡异排版,所以有了下面的一段"""
if ("★★" in line and "%" in line) or (
"★★" in line and "%" in lines[idx + 1]
):
(
chars.append(line)
if ("★★" in line and "%" in line)
else chars.append(line + lines[idx + 1])
)
if not time:
continue
start, end = (
time.replace("月", "/").replace("日", " ").split("-")[:2]
) # 日替换为空格是因为有日后面不接空格的情况,导致 split 出问题
start_time = dateparser.parse(start)
end_time = dateparser.parse(end)
pool_img = contents[index - 2]
r"""两类格式:用/分割,用\分割;★+(概率)+名字,★+名字+(概率)"""
for char in chars:
star = char.split("(")[0].count("★")
name = (
re.split(r"[:(]", char)[1]
if "★(" not in char
else re.split("):", char)[1]
) # 有的括号在前面有的在后面
dual_up = False
if "\\" in name:
names = name.split("\\")
dual_up = True
elif "/" in name:
names = name.split("/")
dual_up = True
else:
names = [name] # 既有用/分割的,又有用\分割的
names = [name.replace("[限定]", "").strip() for name in names]
zoom = 1
if "权值" in char:
zoom = 0.03
else:
match = re.search(r"(占.*?的.*?(\d+).*?%)", char)
if dual_up == True:
zoom = float(match.group(1)) / 2
else:
zoom = float(match.group(1))
zoom = zoom / 100 if zoom > 1 else zoom
for name in names:
up_chars.append(
UpChar(name=name, star=star, limited=False, zoom=zoom)
)
break # 这里break会导致个问题:如果一个公告里有两个池子,会漏掉下面的池子,比如 5.19 的定向寻访。但目前我也没啥好想法解决
if title and start_time and end_time:
if start_time <= datetime.now() <= end_time:
self.UP_EVENT = UpEvent(
title=title,
pool_img=pool_img,
start_time=start_time,
end_time=end_time,
up_char=up_chars,
)
self.dump_up_char()
logger.info(
f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}"
)
break
async def _reload_pool(self) -> MessageFactory | None:
await self.update_up_char()
self.load_up_char()
if self.UP_EVENT:
return MessageFactory(
[
Text(f"重载成功!\n当前UP池子:{self.UP_EVENT.title}"),
Image(self.UP_EVENT.pool_img),
]
)
+10
View File
@@ -0,0 +1,10 @@
from nonebot.internal.rule import Rule
from zhenxun.configs.config import Config
def rule(game) -> Rule:
async def _rule() -> bool:
return Config.get_config("draw_card", game.config_name, True)
return Rule(_rule)
+61
View File
@@ -0,0 +1,61 @@
import platform
from pathlib import Path
import pypinyin
from PIL import Image, ImageDraw, ImageFont
from PIL.Image import Image as IMG
from PIL.ImageFont import FreeTypeFont
from zhenxun.configs.path_config import FONT_PATH
from zhenxun.utils._build_image import BuildImage
dir_path = Path(__file__).parent.absolute()
def cn2py(word) -> str:
"""保存声调,防止出现类似方舟干员红与吽拼音相同声调不同导致红照片无法保存的问题"""
temp = ""
for i in pypinyin.pinyin(word, style=pypinyin.Style.TONE3):
temp += "".join(i)
return temp
# 移除windows和linux下特殊字符
def remove_prohibited_str(name: str) -> str:
if platform.system().lower() == "windows":
tmp = ""
for i in name:
if i not in ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]:
tmp += i
name = tmp
else:
name = name.replace("/", "\\")
return name
def load_font(fontname: str = "msyh.ttf", fontsize: int = 16) -> FreeTypeFont:
return ImageFont.truetype(
str(FONT_PATH / f"{fontname}"), fontsize, encoding="utf-8"
)
def circled_number(num: int) -> IMG:
font = load_font(fontsize=450)
text = str(num)
text_w = BuildImage.get_text_size(text, font=font)[0]
w = 240 + text_w
w = w if w >= 500 else 500
img = Image.new("RGBA", (w, 500))
draw = ImageDraw.Draw(img)
draw.ellipse(((0, 0), (500, 500)), fill="red")
draw.ellipse(((w - 500, 0), (w, 500)), fill="red")
draw.rectangle(((250, 0), (w - 250, 500)), fill="red")
draw.text(
(120, -60),
text,
font=font,
fill="white",
stroke_width=10,
stroke_fill="white",
)
return img