✨ 新增幻想真境剧诗
31
GenshinUID/genshinuid_poetry_abyss/__init__.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from gsuid_core.sv import SV
|
||||||
|
from gsuid_core.bot import Bot
|
||||||
|
from gsuid_core.models import Event
|
||||||
|
from gsuid_core.utils.error_reply import UID_HINT
|
||||||
|
|
||||||
|
from ..utils.convert import get_uid
|
||||||
|
from .draw_poetry_abyss import draw_poetry_abyss_img
|
||||||
|
|
||||||
|
sv_poetry_abyss = SV('查询幻想真境剧诗')
|
||||||
|
|
||||||
|
|
||||||
|
@sv_poetry_abyss.on_command(
|
||||||
|
('查询幻想真境剧诗', '幻想真境剧诗', '新深渊', '查询新深渊', '真剧诗'),
|
||||||
|
block=True,
|
||||||
|
)
|
||||||
|
async def send_poetry_abyss_info(bot: Bot, ev: Event):
|
||||||
|
name = ''.join(re.findall('[\u4e00-\u9fa5]', ev.text))
|
||||||
|
if name:
|
||||||
|
return
|
||||||
|
|
||||||
|
await bot.logger.info('开始执行[幻想真境剧诗]')
|
||||||
|
uid, user_id = await get_uid(bot, ev, True)
|
||||||
|
if uid is None:
|
||||||
|
return await bot.send(UID_HINT)
|
||||||
|
await bot.logger.info('[幻想真境剧诗]uid: {}'.format(uid))
|
||||||
|
|
||||||
|
im = await draw_poetry_abyss_img(uid, ev)
|
||||||
|
|
||||||
|
await bot.send(im)
|
212
GenshinUID/genshinuid_poetry_abyss/draw_poetry_abyss.py
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
from gsuid_core.models import Event
|
||||||
|
from gsuid_core.utils.error_reply import get_error
|
||||||
|
from gsuid_core.utils.image.image_tools import get_avatar_with_ring
|
||||||
|
|
||||||
|
from ..utils.mys_api import mys_api
|
||||||
|
from ..utils.colors import first_color
|
||||||
|
from ..utils.image.convert import convert_img
|
||||||
|
from ..utils.image.image_tools import add_footer
|
||||||
|
from ..utils.resource.download_url import download_file
|
||||||
|
from ..utils.resource.RESOURCE_PATH import ICON_PATH, CHAR_CARD_PATH
|
||||||
|
from ..utils.resource.generate_char_card import create_single_char_card
|
||||||
|
from ..utils.fonts.genshin_fonts import (
|
||||||
|
gs_font_20,
|
||||||
|
gs_font_28,
|
||||||
|
gs_font_36,
|
||||||
|
gs_font_40,
|
||||||
|
gs_font_50,
|
||||||
|
)
|
||||||
|
|
||||||
|
TEXT_PATH = Path(__file__).parent / 'texture2d'
|
||||||
|
DIFFICULTY_MAP = {
|
||||||
|
1: '简单模式',
|
||||||
|
2: '普通模式',
|
||||||
|
3: '困难模式',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def timestamp_to_str(timestamp: float):
|
||||||
|
dt = datetime.fromtimestamp(timestamp)
|
||||||
|
return dt.strftime('%Y.%m.%d %H:%M:%S')
|
||||||
|
|
||||||
|
|
||||||
|
async def draw_buff(
|
||||||
|
data: List,
|
||||||
|
startb: int,
|
||||||
|
stage: Image.Image,
|
||||||
|
stage_draw: ImageDraw.ImageDraw,
|
||||||
|
_type: str,
|
||||||
|
):
|
||||||
|
if _type == '神秘收获':
|
||||||
|
y = 206
|
||||||
|
else:
|
||||||
|
y = 253
|
||||||
|
|
||||||
|
for index_choice, choice in enumerate(data):
|
||||||
|
name = f'{choice["name"]}.png'
|
||||||
|
path = ICON_PATH / name
|
||||||
|
if not path.exists():
|
||||||
|
await download_file(choice['icon'], 8, name)
|
||||||
|
icon = Image.open(path).resize((45, 45)).convert('RGBA')
|
||||||
|
buff = Image.open(TEXT_PATH / 'buff.png')
|
||||||
|
buff.paste(icon, (-3, -4), icon)
|
||||||
|
stage.paste(buff, (startb + index_choice * 44, y), buff)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
stage_draw.text(
|
||||||
|
(startb, y + 21), f'暂无{_type}', (68, 59, 45), gs_font_20, 'lm'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def draw_poetry_abyss_img(uid: str, ev: Event) -> Union[str, bytes]:
|
||||||
|
data = await mys_api.get_poetry_abyss_data(uid)
|
||||||
|
if isinstance(data, int):
|
||||||
|
return get_error(data)
|
||||||
|
|
||||||
|
if not data['is_unlock'] or not data['data']:
|
||||||
|
return '[幻想真境剧诗] 你还没有解锁该模式!'
|
||||||
|
|
||||||
|
data = data['data'][0]
|
||||||
|
round_data = data['detail']['rounds_data']
|
||||||
|
stat_data = data['stat']
|
||||||
|
|
||||||
|
start_time = timestamp_to_str(float(data['schedule']['start_time']))
|
||||||
|
end_time = timestamp_to_str(float(data['schedule']['end_time']))
|
||||||
|
|
||||||
|
w, h = 1000, 1040 + 50 + (len(round_data) // 2) * 330
|
||||||
|
|
||||||
|
img = Image.new('RGBA', (w, h), (22, 18, 20))
|
||||||
|
|
||||||
|
title = Image.open(TEXT_PATH / 'title.png').convert('RGBA')
|
||||||
|
status = Image.open(TEXT_PATH / 'status.png').convert('RGBA')
|
||||||
|
title_draw = ImageDraw.Draw(title)
|
||||||
|
status_draw = ImageDraw.Draw(status)
|
||||||
|
|
||||||
|
avatar = await get_avatar_with_ring(ev, 278)
|
||||||
|
title.paste(avatar, (361, 115), avatar)
|
||||||
|
title_draw.text((500, 473), f'UID {uid}', 'white', gs_font_36, 'mm')
|
||||||
|
|
||||||
|
difficulty = DIFFICULTY_MAP.get(stat_data['difficulty_id'], '困难模式')
|
||||||
|
max_round_id = stat_data['max_round_id']
|
||||||
|
is_gold = (
|
||||||
|
True
|
||||||
|
if stat_data['difficulty_id'] == 3 and max_round_id == 8
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
time = f'{start_time} ~ {end_time}'
|
||||||
|
avatar_bonus = stat_data['avatar_bonus_num']
|
||||||
|
rent_cnt = stat_data['rent_cnt']
|
||||||
|
coin_num = stat_data['coin_num']
|
||||||
|
|
||||||
|
if is_gold:
|
||||||
|
status_icon = Image.open(TEXT_PATH / 'yes.png')
|
||||||
|
else:
|
||||||
|
status_icon = Image.open(TEXT_PATH / 'no.png')
|
||||||
|
|
||||||
|
status_icon = status_icon.convert('RGBA')
|
||||||
|
status.paste(status_icon, (560, 111), status_icon)
|
||||||
|
status_draw.text((462, 132), difficulty, (236, 233, 229), gs_font_36, 'mm')
|
||||||
|
status_draw.text((500, 189), time, (139, 137, 133), gs_font_20, 'mm')
|
||||||
|
|
||||||
|
status_draw.text(
|
||||||
|
(220, 398), f'第{max_round_id}幕', (68, 59, 45), gs_font_50, 'mm'
|
||||||
|
)
|
||||||
|
status_draw.text(
|
||||||
|
(416, 398), f'{avatar_bonus}', (68, 59, 45), gs_font_50, 'mm'
|
||||||
|
)
|
||||||
|
status_draw.text((604, 398), f'{rent_cnt}', (68, 59, 45), gs_font_50, 'mm')
|
||||||
|
status_draw.text((793, 398), f'{coin_num}', (68, 59, 45), gs_font_50, 'mm')
|
||||||
|
|
||||||
|
flower_yes = Image.open(TEXT_PATH / 'flower_yes.png').convert('RGBA')
|
||||||
|
flower_no = Image.open(TEXT_PATH / 'flower_no.png').convert('RGBA')
|
||||||
|
medals = stat_data['get_medal_round_list']
|
||||||
|
|
||||||
|
while len(medals) < 8:
|
||||||
|
medals.append(0)
|
||||||
|
|
||||||
|
for index, medal in enumerate(medals):
|
||||||
|
flower = flower_yes if medal else flower_no
|
||||||
|
status.paste(flower, (449 + 42 * index, 270), flower)
|
||||||
|
|
||||||
|
img.paste(title, (0, 0), title)
|
||||||
|
img.paste(status, (0, 474), status)
|
||||||
|
|
||||||
|
for i, r in enumerate(round_data):
|
||||||
|
is_get_medal = r['is_get_medal']
|
||||||
|
round_id = r['round_id']
|
||||||
|
_medal = flower_yes if is_get_medal else flower_no
|
||||||
|
|
||||||
|
if i % 2 == 0:
|
||||||
|
stage = Image.open(TEXT_PATH / 'stage.png')
|
||||||
|
stage_draw = ImageDraw.Draw(stage)
|
||||||
|
stage_draw.text(
|
||||||
|
(160, 54), f'第{round_id}幕', (68, 59, 45), gs_font_28, 'lm'
|
||||||
|
)
|
||||||
|
stage.paste(_medal, (97, 32), _medal)
|
||||||
|
startx = 92
|
||||||
|
startb = 116
|
||||||
|
else:
|
||||||
|
stage_draw.text(
|
||||||
|
(830, 54), f'第{round_id}幕', (68, 59, 45), gs_font_28, 'rm'
|
||||||
|
)
|
||||||
|
stage.paste(_medal, (866, 32), _medal)
|
||||||
|
startx = 520
|
||||||
|
startb = 552
|
||||||
|
|
||||||
|
for index_char, char in enumerate(r['avatars']):
|
||||||
|
char_id = char['avatar_id']
|
||||||
|
char_type = char['avatar_type']
|
||||||
|
char_pic_path = CHAR_CARD_PATH / f'{char_id}.png'
|
||||||
|
# 不存在自动下载
|
||||||
|
if not char_pic_path.exists():
|
||||||
|
await create_single_char_card(char_id)
|
||||||
|
char_card = Image.open(char_pic_path).convert('RGBA')
|
||||||
|
char_card_draw = ImageDraw.Draw(char_card)
|
||||||
|
char_card_draw.text(
|
||||||
|
(128, 280),
|
||||||
|
f'Lv.{char["level"]}',
|
||||||
|
font=gs_font_40,
|
||||||
|
fill=first_color,
|
||||||
|
anchor='mm',
|
||||||
|
)
|
||||||
|
char_card = char_card.resize((89, 108), Image.Resampling.LANCZOS)
|
||||||
|
stage.paste(
|
||||||
|
char_card,
|
||||||
|
(startx + 97 * index_char, 88),
|
||||||
|
char_card,
|
||||||
|
)
|
||||||
|
if char_type != 1:
|
||||||
|
char_tag = Image.open(TEXT_PATH / f'{char_type}.png')
|
||||||
|
stage.paste(
|
||||||
|
char_tag,
|
||||||
|
(startx + 16 + 97 * index_char, 75),
|
||||||
|
char_tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
await draw_buff(
|
||||||
|
r['choice_cards'],
|
||||||
|
startb,
|
||||||
|
stage,
|
||||||
|
stage_draw,
|
||||||
|
'神秘收获',
|
||||||
|
)
|
||||||
|
await draw_buff(
|
||||||
|
r['buffs'],
|
||||||
|
startb,
|
||||||
|
stage,
|
||||||
|
stage_draw,
|
||||||
|
'奇妙助益',
|
||||||
|
)
|
||||||
|
|
||||||
|
if i % 2 == 1:
|
||||||
|
img.paste(stage, (0, 1040 + 330 * (i // 2)), stage)
|
||||||
|
|
||||||
|
img = add_footer(img, 1000)
|
||||||
|
|
||||||
|
res = await convert_img(img)
|
||||||
|
return res
|
BIN
GenshinUID/genshinuid_poetry_abyss/texture2d/2.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
GenshinUID/genshinuid_poetry_abyss/texture2d/3.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
GenshinUID/genshinuid_poetry_abyss/texture2d/buff.png
Normal file
After Width: | Height: | Size: 776 B |
BIN
GenshinUID/genshinuid_poetry_abyss/texture2d/flower_no.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
GenshinUID/genshinuid_poetry_abyss/texture2d/flower_yes.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
GenshinUID/genshinuid_poetry_abyss/texture2d/no.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
GenshinUID/genshinuid_poetry_abyss/texture2d/stage.png
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
GenshinUID/genshinuid_poetry_abyss/texture2d/status.png
Normal file
After Width: | Height: | Size: 327 KiB |
BIN
GenshinUID/genshinuid_poetry_abyss/texture2d/title.png
Normal file
After Width: | Height: | Size: 480 KiB |
BIN
GenshinUID/genshinuid_poetry_abyss/texture2d/yes.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
@ -1,2 +1,3 @@
|
|||||||
base_url = 'https://api-takumi-record.mihoyo.com/'
|
base_url = 'https://api-takumi-record.mihoyo.com'
|
||||||
widget_url = f'{base_url}game_record/genshin/aapi/widget/v2'
|
widget_url = f'{base_url}/game_record/genshin/aapi/widget/v2'
|
||||||
|
new_abyss_url = f'{base_url}/game_record/app/genshin/api/role_combat'
|
||||||
|
@ -73,3 +73,97 @@ class FakeResin(WidgetResin):
|
|||||||
transformer: Transformer
|
transformer: Transformer
|
||||||
daily_task: DayilyTask
|
daily_task: DayilyTask
|
||||||
archon_quest_progress: ArchonProgress
|
archon_quest_progress: ArchonProgress
|
||||||
|
|
||||||
|
|
||||||
|
class PoetryAbyssLinks(TypedDict):
|
||||||
|
lineup_link: str
|
||||||
|
lineup_link_pc: str
|
||||||
|
strategy_link: str
|
||||||
|
lineup_publish_link: str
|
||||||
|
lineup_publish_link_pc: str
|
||||||
|
|
||||||
|
|
||||||
|
class PoetryAbyssAvatar(TypedDict):
|
||||||
|
avatar_id: int
|
||||||
|
avatar_type: int
|
||||||
|
name: str
|
||||||
|
element: str
|
||||||
|
image: str
|
||||||
|
level: int
|
||||||
|
rarity: int
|
||||||
|
|
||||||
|
|
||||||
|
class PoetryAbyssChoiceCard(TypedDict):
|
||||||
|
icon: str
|
||||||
|
name: str
|
||||||
|
desc: str
|
||||||
|
is_enhanced: bool
|
||||||
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class PoetryAbyssBuff(TypedDict):
|
||||||
|
icon: str
|
||||||
|
name: str
|
||||||
|
desc: str
|
||||||
|
is_enhanced: bool
|
||||||
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class PoetryAbyssDateTime(TypedDict):
|
||||||
|
year: int
|
||||||
|
month: int
|
||||||
|
day: int
|
||||||
|
hour: int
|
||||||
|
minute: int
|
||||||
|
second: int
|
||||||
|
|
||||||
|
|
||||||
|
class PoetryAbyssSchedule(TypedDict):
|
||||||
|
start_time: int
|
||||||
|
end_time: int
|
||||||
|
schedule_type: int
|
||||||
|
schedule_id: int
|
||||||
|
start_date_time: PoetryAbyssDateTime
|
||||||
|
end_date_time: PoetryAbyssDateTime
|
||||||
|
|
||||||
|
|
||||||
|
class PoetryAbyssDetailStat(TypedDict):
|
||||||
|
difficulty_id: int
|
||||||
|
max_round_id: int
|
||||||
|
heraldry: int
|
||||||
|
get_medal_round_list: List[int]
|
||||||
|
medal_num: int
|
||||||
|
coin_num: int
|
||||||
|
avatar_bonus_num: int
|
||||||
|
rent_cnt: int
|
||||||
|
|
||||||
|
|
||||||
|
class RoundData(TypedDict):
|
||||||
|
avatars: List[PoetryAbyssAvatar]
|
||||||
|
choice_cards: List[PoetryAbyssChoiceCard]
|
||||||
|
buffs: List[PoetryAbyssBuff]
|
||||||
|
is_get_medal: bool
|
||||||
|
round_id: int
|
||||||
|
finish_time: int
|
||||||
|
finish_date_time: PoetryAbyssDateTime
|
||||||
|
detail_stat: PoetryAbyssDetailStat
|
||||||
|
|
||||||
|
|
||||||
|
class PoetryAbyssDetail(TypedDict):
|
||||||
|
rounds_data: List[RoundData]
|
||||||
|
detail_stat: PoetryAbyssDetailStat
|
||||||
|
backup_avatars: List[PoetryAbyssAvatar]
|
||||||
|
|
||||||
|
|
||||||
|
class PoetryAbyssData(TypedDict):
|
||||||
|
detail: PoetryAbyssDetail
|
||||||
|
stat: PoetryAbyssDetailStat
|
||||||
|
schedule: PoetryAbyssSchedule
|
||||||
|
has_data: bool
|
||||||
|
has_detail_data: bool
|
||||||
|
|
||||||
|
|
||||||
|
class PoetryAbyssDatas(TypedDict):
|
||||||
|
data: List[PoetryAbyssData]
|
||||||
|
is_unlock: bool
|
||||||
|
links: PoetryAbyssLinks
|
||||||
|
@ -2,10 +2,10 @@ from copy import deepcopy
|
|||||||
from typing import Dict, Union, cast
|
from typing import Dict, Union, cast
|
||||||
|
|
||||||
from gsuid_core.utils.api.mys_api import _MysApi
|
from gsuid_core.utils.api.mys_api import _MysApi
|
||||||
from gsuid_core.utils.api.mys.tools import get_web_ds_token
|
from gsuid_core.utils.api.mys.tools import get_ds_token, get_web_ds_token
|
||||||
|
|
||||||
from .api import widget_url
|
from .api import widget_url, new_abyss_url
|
||||||
from .models import WidgetResin
|
from .models import WidgetResin, PoetryAbyssDatas
|
||||||
|
|
||||||
|
|
||||||
class GsMysAPI(_MysApi):
|
class GsMysAPI(_MysApi):
|
||||||
@ -28,3 +28,30 @@ class GsMysAPI(_MysApi):
|
|||||||
if isinstance(data, Dict):
|
if isinstance(data, Dict):
|
||||||
data = cast(WidgetResin, data['data'])
|
data = cast(WidgetResin, data['data'])
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
async def get_poetry_abyss_data(
|
||||||
|
self, uid: str
|
||||||
|
) -> Union[PoetryAbyssDatas, int]:
|
||||||
|
server_id = self.RECOGNIZE_SERVER.get(uid[0])
|
||||||
|
HEADER = deepcopy(self._HEADER)
|
||||||
|
ck = await self.get_ck(uid)
|
||||||
|
if ck is None:
|
||||||
|
return -51
|
||||||
|
HEADER['Cookie'] = ck
|
||||||
|
params = {
|
||||||
|
'server': server_id,
|
||||||
|
'role_id': uid,
|
||||||
|
'need_detail': True,
|
||||||
|
}
|
||||||
|
HEADER['DS'] = get_ds_token(
|
||||||
|
'&'.join([f'{k}={v}' for k, v in params.items()])
|
||||||
|
)
|
||||||
|
data = await self._mys_request(
|
||||||
|
new_abyss_url,
|
||||||
|
'GET',
|
||||||
|
HEADER,
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
if isinstance(data, Dict):
|
||||||
|
data = cast(PoetryAbyssDatas, data['data'])
|
||||||
|
return data
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
GenshinUID_version = '4.7.1'
|
GenshinUID_version = '4.7.2'
|
||||||
Genshin_version = '4.7.0'
|
Genshin_version = '4.7.0'
|
||||||
|