🚧 sr查询绘图完成
画了三个小时,终于画好了 @KimigaiiWuyi @qwerdvd 画图还有些需要改,比如并发画图、图片保存、出现错误码的情况、多UID、指定UID等情况
@ -1,4 +1,4 @@
|
||||
from typing import Any, List, TypedDict
|
||||
from typing import Any, Dict, List, Optional, TypedDict
|
||||
|
||||
|
||||
class RoleBasicInfo(TypedDict):
|
||||
@ -119,3 +119,86 @@ class SignList(TypedDict):
|
||||
biz: str
|
||||
resign: bool
|
||||
short_extra_award: SignExtraAward
|
||||
|
||||
|
||||
#####################
|
||||
# 基础信息 角色信息 #
|
||||
####################
|
||||
|
||||
|
||||
class Stats(TypedDict):
|
||||
active_days: int
|
||||
avatar_num: int
|
||||
achievement_num: int
|
||||
chest_num: int
|
||||
abyss_process: str
|
||||
|
||||
|
||||
class AvatarListItem(TypedDict):
|
||||
id: int
|
||||
level: int
|
||||
name: str
|
||||
element: str
|
||||
icon: str
|
||||
rarity: int
|
||||
rank: int
|
||||
is_chosen: bool
|
||||
|
||||
|
||||
class RoleIndex(TypedDict):
|
||||
stats: Stats
|
||||
avatar_list: List[AvatarListItem]
|
||||
|
||||
|
||||
################
|
||||
# 角色详细信息 #
|
||||
################
|
||||
|
||||
|
||||
class Equip(TypedDict):
|
||||
id: int
|
||||
level: int
|
||||
rank: int
|
||||
name: str
|
||||
desc: str
|
||||
icon: str
|
||||
|
||||
|
||||
class RelicsItem(TypedDict):
|
||||
id: int
|
||||
level: int
|
||||
pos: int
|
||||
name: str
|
||||
desc: str
|
||||
icon: str
|
||||
rarity: int
|
||||
|
||||
|
||||
class RanksItem(TypedDict):
|
||||
id: int
|
||||
pos: int
|
||||
name: str
|
||||
icon: str
|
||||
desc: str
|
||||
is_unlocked: bool
|
||||
|
||||
|
||||
class AvatarListItemDetail(TypedDict):
|
||||
id: int
|
||||
level: int
|
||||
name: str
|
||||
element: str
|
||||
icon: str
|
||||
rarity: int
|
||||
rank: int
|
||||
image: str
|
||||
equip: Optional[Equip]
|
||||
relics: List[RelicsItem]
|
||||
ornaments: List
|
||||
ranks: List[RanksItem]
|
||||
|
||||
|
||||
class AvatarInfo(TypedDict):
|
||||
avatar_list: List[AvatarListItemDetail]
|
||||
equip_wiki: Dict[str, str]
|
||||
relic_wiki: Dict
|
||||
|
@ -1,10 +1,17 @@
|
||||
from gsuid_core.sv import SV
|
||||
from gsuid_core.bot import Bot
|
||||
from gsuid_core.models import Event
|
||||
import re
|
||||
from .draw_roleinfo_card import get_role_img
|
||||
|
||||
sv_get_info = SV('sr查询信息')
|
||||
|
||||
|
||||
@sv_get_info.on_command(('sr', 'sruid'))
|
||||
async def send_role_info(bot: Bot, ev: Event):
|
||||
await bot.send("WIP 前面的区域,以后再来探索吧")
|
||||
name = ''.join(re.findall('[\u4e00-\u9fa5]', ev.text))
|
||||
if name:
|
||||
return
|
||||
|
||||
await bot.logger.info('开始执行[sr查询信息]')
|
||||
await bot.send(await get_role_img(bot.bot_id, ev.user_id))
|
||||
|
208
StarRailUID/starrailuid_roleinfo/draw_roleinfo_card.py
Normal file
@ -0,0 +1,208 @@
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from gsuid_core.logger import logger
|
||||
|
||||
from ..utils.api import get_sqla
|
||||
from ..utils.mys_api import mys_api
|
||||
from .utils import get_icon, wrap_list
|
||||
from ..utils.image.convert import convert_img
|
||||
from ..utils.fonts.starrail_fonts import (
|
||||
sr_font_24,
|
||||
sr_font_30,
|
||||
sr_font_36,
|
||||
)
|
||||
|
||||
TEXT_PATH = Path(__file__).parent / 'texture2D'
|
||||
|
||||
bg1 = Image.open(TEXT_PATH / 'bg1.png')
|
||||
bg2 = Image.open(TEXT_PATH / 'bg2.png')
|
||||
bg3 = Image.open(TEXT_PATH / 'bg3.png')
|
||||
user_avatar = (
|
||||
Image.open(TEXT_PATH / "200101.png").resize((220, 220)).convert("RGBA")
|
||||
)
|
||||
char_bg_4 = Image.open(TEXT_PATH / 'rarity4_bg.png').convert("RGBA")
|
||||
char_bg_5 = Image.open(TEXT_PATH / 'rarity5_bg.png').convert("RGBA")
|
||||
circle = Image.open(TEXT_PATH / 'char_weapon_bg.png').convert("RGBA")
|
||||
|
||||
bg_color = (248, 248, 248)
|
||||
white_color = (255, 255, 255)
|
||||
color_color = (40, 18, 7)
|
||||
first_color = (22, 8, 31)
|
||||
|
||||
elements = {
|
||||
"ice": Image.open(TEXT_PATH / "IconNatureColorIce.png").convert("RGBA"),
|
||||
"fire": Image.open(TEXT_PATH / "IconNatureColorFire.png").convert("RGBA"),
|
||||
"imaginary": Image.open(
|
||||
TEXT_PATH / "IconNatureColorImaginary.png"
|
||||
).convert("RGBA"),
|
||||
"quantum": Image.open(TEXT_PATH / "IconNatureColorQuantum.png").convert(
|
||||
"RGBA"
|
||||
),
|
||||
"lightning": Image.open(TEXT_PATH / "IconNatureColorThunder.png").convert(
|
||||
"RGBA"
|
||||
),
|
||||
"wind": Image.open(TEXT_PATH / "IconNatureColorWind.png").convert("RGBA"),
|
||||
"physical": Image.open(TEXT_PATH / "IconNaturePhysical.png").convert(
|
||||
"RGBA"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def get_role_img(bot_id: str, user_id: str):
|
||||
sqla = get_sqla(bot_id)
|
||||
uid_list: List = await sqla.get_bind_sruid_list(user_id)
|
||||
logger.info(f'[每日信息]UID: {uid_list}')
|
||||
# 进行校验UID是否绑定CK
|
||||
useable_uid_list = []
|
||||
for uid in uid_list:
|
||||
status = await sqla.get_user_cookie(uid)
|
||||
if status is not None:
|
||||
useable_uid_list.append(uid)
|
||||
uid = useable_uid_list[0]
|
||||
res = await convert_img(await draw_role_card(uid))
|
||||
logger.info(f'[每日信息]可用UID: {useable_uid_list}')
|
||||
if not useable_uid_list:
|
||||
return '请先绑定一个可用CK & UID再来查询哦~'
|
||||
return res
|
||||
|
||||
|
||||
def _lv(level: int) -> str:
|
||||
return f"Lv.0{level}" if level < 10 else f"Lv.{level}"
|
||||
|
||||
|
||||
async def draw_role_card(sr_uid: str) -> Image.Image:
|
||||
role_basic_info = await mys_api.get_role_basic_info(sr_uid)
|
||||
role_index = await mys_api.get_role_index(sr_uid)
|
||||
stats = role_index['stats']
|
||||
avatars = role_index['avatar_list']
|
||||
|
||||
# 名称
|
||||
nickname = role_basic_info['nickname']
|
||||
|
||||
# 基本状态
|
||||
active_days = stats['active_days']
|
||||
avater_num = stats['avatar_num']
|
||||
achievement_num = stats['achievement_num']
|
||||
chest_num = stats['chest_num']
|
||||
level = role_basic_info['level']
|
||||
|
||||
# 忘却之庭
|
||||
abyss_process = stats['abyss_process']
|
||||
|
||||
# 角色武器
|
||||
details = (await mys_api.get_avatar_info(sr_uid, avatars[0]['id']))[
|
||||
'avatar_list'
|
||||
]
|
||||
equips: Dict[int, Optional[str]] = {}
|
||||
for detail in details:
|
||||
equip = detail['equip']
|
||||
equips[detail['id']] = equip['icon'] if equip is not None else None # type: ignore
|
||||
|
||||
img_bg1 = bg1.copy()
|
||||
bg1_draw = ImageDraw.Draw(img_bg1)
|
||||
|
||||
# 写Nickname
|
||||
bg1_draw.text(
|
||||
(400, 85), nickname, font=sr_font_36, fill=white_color, anchor='mm'
|
||||
)
|
||||
# 写UID
|
||||
bg1_draw.text(
|
||||
(400, 165),
|
||||
f"UID {sr_uid}",
|
||||
font=sr_font_30,
|
||||
fill=white_color,
|
||||
anchor='mm',
|
||||
)
|
||||
# 贴头像
|
||||
img_bg1.paste(user_avatar, (286, 213), mask=user_avatar)
|
||||
|
||||
# 写基本信息
|
||||
bg1_draw.text(
|
||||
(143, 590),
|
||||
str(active_days),
|
||||
font=sr_font_36,
|
||||
fill=white_color,
|
||||
anchor='mm',
|
||||
) # 活跃天数
|
||||
bg1_draw.text(
|
||||
(270, 590),
|
||||
str(avater_num),
|
||||
font=sr_font_36,
|
||||
fill=white_color,
|
||||
anchor='mm',
|
||||
) # 解锁角色
|
||||
bg1_draw.text(
|
||||
(398, 590),
|
||||
str(achievement_num),
|
||||
font=sr_font_36,
|
||||
fill=white_color,
|
||||
anchor='mm',
|
||||
) # 达成成就
|
||||
bg1_draw.text(
|
||||
(525, 590),
|
||||
str(chest_num),
|
||||
font=sr_font_36,
|
||||
fill=white_color,
|
||||
anchor='mm',
|
||||
) # 战利品开启
|
||||
bg1_draw.text(
|
||||
(666, 590), str(level), font=sr_font_36, fill=white_color, anchor='mm'
|
||||
) # 开拓等级
|
||||
|
||||
# 画忘却之庭
|
||||
bg1_draw.text(
|
||||
(471, 722),
|
||||
abyss_process,
|
||||
font=sr_font_30,
|
||||
fill=first_color,
|
||||
anchor='mm',
|
||||
)
|
||||
|
||||
# 角色部分 每五个一组
|
||||
lines = []
|
||||
for five_avatars in wrap_list(avatars, 5):
|
||||
line = bg2.copy()
|
||||
x = 70
|
||||
for avatar in five_avatars:
|
||||
char_bg = (
|
||||
char_bg_4 if avatar['rarity'] == 4 else char_bg_5
|
||||
).copy()
|
||||
char_draw = ImageDraw.Draw(char_bg)
|
||||
char_icon = await get_icon(avatar['icon'])
|
||||
element_icon = elements[avatar['element']]
|
||||
|
||||
char_bg.paste(char_icon, (4, 8), mask=char_icon)
|
||||
char_bg.paste(element_icon, (81, 10), mask=element_icon)
|
||||
|
||||
if equip := equips[avatar['id']]:
|
||||
char_bg.paste(circle, (0, 0), mask=circle)
|
||||
equip_icon = (await get_icon(equip)).resize((48, 48))
|
||||
char_bg.paste(equip_icon, (9, 80), mask=equip_icon)
|
||||
|
||||
char_draw.text(
|
||||
(60, 146),
|
||||
_lv(avatar['level']),
|
||||
font=sr_font_24,
|
||||
fill=color_color,
|
||||
anchor='mm',
|
||||
)
|
||||
|
||||
line.paste(char_bg, (x, 0))
|
||||
x += 135
|
||||
lines.append(line)
|
||||
|
||||
# 绘制总图
|
||||
img = Image.new("RGBA", (800, 880 + len(lines) * 200), bg_color)
|
||||
img.paste(img_bg1, (0, 0))
|
||||
|
||||
y = 810
|
||||
for line in lines:
|
||||
img.paste(line, (0, y), mask=line)
|
||||
y += 200
|
||||
|
||||
img.paste(bg3, (0, len(lines) * 200 + 810))
|
||||
|
||||
return img
|
BIN
StarRailUID/starrailuid_roleinfo/texture2D/200101.png
Normal file
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
BIN
StarRailUID/starrailuid_roleinfo/texture2D/bg1.png
Normal file
After Width: | Height: | Size: 576 KiB |
BIN
StarRailUID/starrailuid_roleinfo/texture2D/bg2.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
StarRailUID/starrailuid_roleinfo/texture2D/bg3.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
StarRailUID/starrailuid_roleinfo/texture2D/char_weapon_bg.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
StarRailUID/starrailuid_roleinfo/texture2D/rarity4_bg.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
StarRailUID/starrailuid_roleinfo/texture2D/rarity5_bg.png
Normal file
After Width: | Height: | Size: 26 KiB |
18
StarRailUID/starrailuid_roleinfo/utils.py
Normal file
@ -0,0 +1,18 @@
|
||||
from io import BytesIO
|
||||
from typing import List, TypeVar, Generator
|
||||
|
||||
from PIL import Image
|
||||
from aiohttp import ClientSession
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def wrap_list(lst: List[T], n: int) -> Generator[List[T], None, None]:
|
||||
for i in range(0, len(lst), n):
|
||||
yield lst[i : i + n]
|
||||
|
||||
|
||||
async def get_icon(url: str) -> Image.Image:
|
||||
async with ClientSession() as client:
|
||||
async with client.get(url) as resp:
|
||||
return Image.open(BytesIO(await resp.read())).convert("RGBA")
|
@ -19,6 +19,8 @@ from ..sruid_utils.api.mys.models import (
|
||||
MonthlyAward,
|
||||
DailyNoteData,
|
||||
RoleBasicInfo,
|
||||
RoleIndex,
|
||||
AvatarInfo
|
||||
)
|
||||
|
||||
RECOGNIZE_SERVER = {
|
||||
@ -113,6 +115,27 @@ class _MysApi(BaseMysApi):
|
||||
data = cast(DailyNoteData, data['data'])
|
||||
return data
|
||||
|
||||
async def get_role_index(self, uid: str) -> Union[RoleIndex, int]:
|
||||
data = await self.simple_mys_req('STAR_RAIL_INDEX_URL', uid)
|
||||
if isinstance(data, Dict):
|
||||
data = cast(RoleIndex, data['data'])
|
||||
return data
|
||||
|
||||
async def get_avatar_info(
|
||||
self, uid: str, avatar_id: int, need_wiki: bool = False
|
||||
) -> Union[AvatarInfo, int]:
|
||||
data = await self.simple_mys_req(
|
||||
'STAR_RAIL_AVATAR_INFO_URL',
|
||||
uid,
|
||||
params={
|
||||
"id": avatar_id,
|
||||
"need_wiki": "true" if need_wiki else "false"
|
||||
},
|
||||
)
|
||||
if isinstance(data, Dict):
|
||||
data = cast(AvatarInfo, data['data'])
|
||||
return data
|
||||
|
||||
async def get_sign_list(self, uid) -> Union[SignList, int]:
|
||||
# is_os = self.check_os(uid)
|
||||
is_os = False
|
||||
@ -283,7 +306,7 @@ class _MysApi(BaseMysApi):
|
||||
server_id = 'cn_qd01' if is_os else 'prod_gf_cn'
|
||||
else:
|
||||
server_id = RECOGNIZE_SERVER.get(uid[0])
|
||||
is_os = False if int(uid[0]) < 6 else True
|
||||
is_os = int(uid[0]) >= 6
|
||||
ex_params = '&'.join([f'{k}={v}' for k, v in params.items()])
|
||||
if is_os:
|
||||
_URL = _API[f'{URL}_OS']
|
||||
@ -292,8 +315,9 @@ class _MysApi(BaseMysApi):
|
||||
else:
|
||||
_URL = _API[URL]
|
||||
HEADER = copy.deepcopy(self._HEADER)
|
||||
param_str = f'role_id={uid}&server={server_id}'
|
||||
HEADER['DS'] = get_ds_token(
|
||||
ex_params if ex_params else f'role_id={uid}&server={server_id}'
|
||||
f"{ex_params}&{param_str}" if ex_params else param_str
|
||||
)
|
||||
HEADER.update(header)
|
||||
if cookie is not None:
|
||||
@ -303,11 +327,12 @@ class _MysApi(BaseMysApi):
|
||||
if ck is None:
|
||||
return -51
|
||||
HEADER['Cookie'] = ck
|
||||
param_dict = {'server': server_id, 'role_id': uid}
|
||||
data = await self._mys_request(
|
||||
url=_URL,
|
||||
method='GET',
|
||||
header=HEADER,
|
||||
params=params if params else {'server': server_id, 'role_id': uid},
|
||||
params={**params, **param_dict} if params else param_dict,
|
||||
use_proxy=True if is_os else False,
|
||||
)
|
||||
return data
|
||||
|