diff --git a/GenshinUID/genshinuid_guide/__init__.py b/GenshinUID/genshinuid_guide/__init__.py index 769ad199..c8d92721 100644 --- a/GenshinUID/genshinuid_guide/__init__.py +++ b/GenshinUID/genshinuid_guide/__init__.py @@ -4,6 +4,7 @@ from gsuid_core.sv import SV from gsuid_core.bot import Bot from gsuid_core.models import Event +from gsuid_core.logger import logger from gsuid_core.message_models import Button from gsuid_core.segment import MessageSegment @@ -16,9 +17,11 @@ from ..utils.resource.RESOURCE_PATH import REF_PATH from .get_bbs_post_guide import get_material_way_post from ..utils.map.name_covert import alias_to_char_name +from .draw_poetry_abyss_pic import draw_poetry_abyss_image sv_char_guide = SV('查询角色攻略') sv_abyss_review = SV('查询深渊阵容') +sv_poetry_abyss_review = SV('查询剧诗深渊阵容') sv_bbs_post_guide = SV('查询BBS攻略') @@ -57,6 +60,13 @@ async def send_bluekun_pic(bot: Bot, ev: Event): await bot.logger.warning('未找到{}参考面板图片'.format(name)) +@sv_poetry_abyss_review.on_command(('剧诗版本深渊', '剧诗深渊阵容')) +async def send_poetry_abyss_review(bot: Bot, ev: Event): + im = await draw_poetry_abyss_image() + logger.info('[剧诗版本深渊] 获得深渊信息成功!') + await bot.send(im) + + @sv_abyss_review.on_command(('版本深渊', '深渊阵容', '深渊怪物')) async def send_abyss_review(bot: Bot, ev: Event): floor = '12' @@ -88,7 +98,7 @@ async def send_abyss_review(bot: Bot, ev: Event): d = Button(f'♾️版本深渊{adv_version}', f'深渊概览{adv_version}') await bot.send_option(im, [c, d]) elif isinstance(im, List): - mes = [MessageSegment.text(str(msg)) for msg in im] + mes = [MessageSegment.text(str(msg)) for msg in im] # type: ignore await bot.send(MessageSegment.node(mes)) elif isinstance(im, str): await bot.send(im) diff --git a/GenshinUID/genshinuid_guide/draw_poetry_abyss_pic.py b/GenshinUID/genshinuid_guide/draw_poetry_abyss_pic.py new file mode 100644 index 00000000..c2920a53 --- /dev/null +++ b/GenshinUID/genshinuid_guide/draw_poetry_abyss_pic.py @@ -0,0 +1,222 @@ +import re +from pathlib import Path +from datetime import datetime + +from PIL import Image, ImageDraw +from gsuid_core.utils.error_reply import get_error +from gsuid_core.utils.image.convert import convert_img + +from ..utils.image.image_tools import add_footer +from ..utils.api.hakush.request import hakush_api +from ..utils.map.name_covert import avatar_id_to_name +from ..utils.resource.RESOURCE_PATH import CHAR_CARD_PATH, MONSTER_ICON_PATH +from ..utils.fonts.genshin_fonts import ( + gs_font_20, + gs_font_22, + gs_font_26, + gs_font_28, + gs_font_30, + gs_font_38, +) + +TEXT_PATH = Path(__file__).parent / 'texture2d2' + + +def remove_angle_brackets(text: str) -> str: + cleaned_text = re.sub(r'<.*?>', '', text) + return cleaned_text + + +def is_current_time_in_range(start_time: str, end_time: str) -> bool: + start_datetime = datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S") + end_datetime = datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S") + + current_datetime = datetime.now() + + return start_datetime <= current_datetime <= end_datetime + + +async def draw_poetry_abyss_image(): + data = await hakush_api.get_hakush_rolecombats() + if isinstance(data, int): + return get_error(data) + + poetry_id = 0 + for _id in data: + item = data[_id] + begin = item['begin'] + end = item['end'] + if is_current_time_in_range(begin, end): + poetry_id = _id + break + else: + return '没有找到当前的活动!' + + pdata = await hakush_api.get_hakush_rolecombat(poetry_id) + if isinstance(pdata, int): + return get_error(pdata) + + w, h = 1000, 1270 + 50 + + monster_list = list(pdata['DifficultyConfig'].values())[-1] + monster_room = monster_list['Room'] + + for room_id in monster_room: + monster = monster_room[room_id] + if 'Title' in monster: + h += 301 + else: + h += 145 + + time = f"{pdata['BeginTime']} ~ {pdata['EndTime']}" + img = Image.new('RGBA', (w, h), (22, 18, 20, 255)) + + title = Image.open(TEXT_PATH / 'title.png') + title_draw = ImageDraw.Draw(title) + title_draw.text( + (500, 473), + f'剧诗ID:{poetry_id} 详细信息', + (255, 255, 255), + font=gs_font_38, + anchor='mm', + ) + img.paste(title, (0, 0), title) + + avatar_bg = Image.open(TEXT_PATH / 'avatar_bg.png') + avatar_draw = ImageDraw.Draw(avatar_bg) + + avatar_draw.text( + (500, 184), + time, + (139, 137, 133), + font=gs_font_22, + anchor='mm', + ) + + avatar_draw.text( + (500, 474), + pdata['AvatarConfig']['BuffAvatarList'][0]['Desc'][16:-9], + (139, 137, 133), + font=gs_font_26, + anchor='mm', + ) + + for cindex, char in enumerate(pdata['AvatarConfig']['BuffAvatarList']): + char_id = str(char['Id']) + char_img = Image.open(CHAR_CARD_PATH / f'{char_id}.png') + char_img = char_img.resize((102, 124)) + char_name = await avatar_id_to_name(char_id) + char_draw = ImageDraw.Draw(char_img) + char_draw.text( + (51, 111), + char_name, + (10, 10, 10), + font=gs_font_20, + anchor='mm', + ) + avatar_bg.paste(char_img, (121 + cindex * 130, 302), char_img) + + for iindex, char_id in enumerate( + pdata['AvatarConfig']['InviteAvatarList'] + ): + char_id = str(char_id) + char_img = Image.open(CHAR_CARD_PATH / f'{char_id}.png') + char_img = char_img.resize((102, 124)) + char_name = await avatar_id_to_name(char_id) + char_draw = ImageDraw.Draw(char_img) + char_draw.text( + (51, 111), + char_name, + (10, 10, 10), + font=gs_font_20, + anchor='mm', + ) + avatar_bg.paste(char_img, (252 + iindex * 130, 576), char_img) + + img.paste(avatar_bg, (0, 482), avatar_bg) + + point = 1270 + for room_id in monster_room: + monster = monster_room[room_id] + if 'Title' in monster: + monster_bg = Image.open(TEXT_PATH / 'long_monster_bg.png') + monster_draw = ImageDraw.Draw(monster_bg) + desc = remove_angle_brackets(monster['Desc']) + desc = desc[:26] + '...' + monster_draw.text( + (245, 69), + f'{monster["Title"]}', + 'black', + font=gs_font_30, + anchor='lm', + ) + for mindex, m in enumerate(monster['MonsterPreviewList']): + monster_icon = m['Icon'] + monster_img = await hakush_api.get_hakush_ui( + monster_icon, MONSTER_ICON_PATH + ) + monster_img = monster_img.resize((90, 90)) + monster_name = m['Name'] + if '·' in monster_name: + monster_name = monster_name.split('·')[-1] + + monster_name = monster_name[:6] + + monster_hp = int(m['Hp']) + mimg = Image.new( + 'RGBA', + (250, 90), + (219, 209, 203), + ) + ming_draw = ImageDraw.Draw(mimg) + + mimg.paste(monster_img, (0, 0), monster_img) + ming_draw.text( + (100, 30), + f'{monster_name}', + (36, 36, 36), + font=gs_font_22, + anchor='lm', + ) + + ming_draw.text( + (100, 57), + f'HP {monster_hp}', + (90, 90, 90), + font=gs_font_22, + anchor='lm', + ) + monster_bg.paste(mimg, (126 + mindex * 257, 158), mimg) + else: + monster_bg = Image.open(TEXT_PATH / 'short_monster_bg.png') + monster_draw = ImageDraw.Draw(monster_bg) + desc = f'怪物等级 Lv{monster["MonsterLevel"]}' + + monster_draw.text( + (131, 69), + f'第{room_id}幕', + 'black', + font=gs_font_30, + anchor='lm', + ) + + monster_draw.text( + (131, 114), + desc, + (99, 99, 99), + font=gs_font_28, + anchor='lm', + ) + + if 'Title' not in monster: + point -= 12 + + img.paste(monster_bg, (0, point), monster_bg) + if 'Title' not in monster: + point += 145 + else: + point += 301 + + img = add_footer(img, 1000) + res = await convert_img(img) + return res diff --git a/GenshinUID/genshinuid_guide/texture2d2/avatar_bg.png b/GenshinUID/genshinuid_guide/texture2d2/avatar_bg.png new file mode 100644 index 00000000..f2a2b0d1 Binary files /dev/null and b/GenshinUID/genshinuid_guide/texture2d2/avatar_bg.png differ diff --git a/GenshinUID/genshinuid_guide/texture2d2/long_monster_bg.png b/GenshinUID/genshinuid_guide/texture2d2/long_monster_bg.png new file mode 100644 index 00000000..7e468dbb Binary files /dev/null and b/GenshinUID/genshinuid_guide/texture2d2/long_monster_bg.png differ diff --git a/GenshinUID/genshinuid_guide/texture2d2/short_monster_bg.png b/GenshinUID/genshinuid_guide/texture2d2/short_monster_bg.png new file mode 100644 index 00000000..c8debf7b Binary files /dev/null and b/GenshinUID/genshinuid_guide/texture2d2/short_monster_bg.png differ diff --git a/GenshinUID/genshinuid_guide/texture2d2/title.png b/GenshinUID/genshinuid_guide/texture2d2/title.png new file mode 100644 index 00000000..3513aaf8 Binary files /dev/null and b/GenshinUID/genshinuid_guide/texture2d2/title.png differ diff --git a/GenshinUID/genshinuid_help/help.json b/GenshinUID/genshinuid_help/help.json index 9e7f1079..2a35e31d 100644 --- a/GenshinUID/genshinuid_help/help.json +++ b/GenshinUID/genshinuid_help/help.json @@ -507,6 +507,14 @@ "need_sk": false, "need_admin": false }, + { + "name": "剧诗版本深渊", + "desc": "获取当前版本剧诗深渊阵容", + "eg": "剧诗版本深渊", + "need_ck": false, + "need_sk": false, + "need_admin": false + }, { "name": "查成就", "desc": "查询这个成就的攻略", diff --git a/GenshinUID/utils/api/hakush/api.py b/GenshinUID/utils/api/hakush/api.py new file mode 100644 index 00000000..99bfd2c4 --- /dev/null +++ b/GenshinUID/utils/api/hakush/api.py @@ -0,0 +1,5 @@ +HAKUSH_D_API = 'https://api.hakush.in/gi/data' +HAKUSH_U_API = 'https://api.hakush.in/gi/UI' + +HAKUSH_ROLECOMBATS_API = f'{HAKUSH_D_API}/rolecombat.json' +HAKUSH_ROLECOMBAT_API = f'{HAKUSH_D_API}/zh/rolecombat/' + '{}.json' diff --git a/GenshinUID/utils/api/hakush/models.py b/GenshinUID/utils/api/hakush/models.py new file mode 100644 index 00000000..1a7acef4 --- /dev/null +++ b/GenshinUID/utils/api/hakush/models.py @@ -0,0 +1,58 @@ +from typing import Dict, List, TypedDict + + +class RoleCombatEvent(TypedDict): + begin: str + end: str + element: List[int] + invite: List[int] + buff: List[int] + live_begin: str + live_end: str + + +class BuffAvatar(TypedDict): + Id: int + Desc: str + + +class MonsterPreview(TypedDict): + Id: int + Hp: float + Name: str + Icon: str + + +class RoomConfig(TypedDict): + MonsterLevel: int + Title: str + Desc: str + MonsterPreviewList: List[MonsterPreview] + + +class DifficultyLevel(TypedDict): + CanInvitePool: int + BossMaxRoomNumber: int + MinimumAvatarLevel: int + Room: Dict[str, RoomConfig] + CanInvitePoolAdd: int + + +class AvatarConfig(TypedDict): + ElementList: List[int] + InviteAvatarList: List[int] + BuffAvatarList: List[BuffAvatar] + + +class ShopItem(TypedDict): + Id: int + Name: str + Desc: str + + +class RoleCombatData(TypedDict): + BeginTime: str + EndTime: str + AvatarConfig: AvatarConfig + DifficultyConfig: Dict[str, DifficultyLevel] + ShopConfig: List[ShopItem] diff --git a/GenshinUID/utils/api/hakush/request.py b/GenshinUID/utils/api/hakush/request.py new file mode 100644 index 00000000..a16f97e1 --- /dev/null +++ b/GenshinUID/utils/api/hakush/request.py @@ -0,0 +1,87 @@ +import io +import json +from pathlib import Path +from typing import Any, Dict, Union, Literal, Optional, cast + +from PIL import Image +from httpx import AsyncClient +from gsuid_core.logger import logger + +from .models import RoleCombatData, RoleCombatEvent +from .api import HAKUSH_U_API, HAKUSH_ROLECOMBAT_API, HAKUSH_ROLECOMBATS_API + + +class _HakushAPI: + ssl_verify = True + _HEADER = {'User-Agent': 'GenshinUID & GsCore'} + + async def get_hakush_rolecombats( + self, + ) -> Union[Dict[str, RoleCombatEvent], int]: + data = await self._hakush_request(HAKUSH_ROLECOMBATS_API) + if isinstance(data, Dict): + return cast(Dict[str, RoleCombatEvent], data) + else: + return -500 + + async def get_hakush_rolecombat( + self, id: str + ) -> Union[RoleCombatData, int]: + data = await self._hakush_request(HAKUSH_ROLECOMBAT_API.format(id)) + if isinstance(data, Dict): + return cast(RoleCombatData, data) + else: + return -500 + + async def get_hakush_ui( + self, ui_name: str, save_path: Path + ) -> Image.Image: + png_file_path = save_path / f'{ui_name}.png' + if png_file_path.exists(): + return Image.open(png_file_path) + url = f'{HAKUSH_U_API}/{ui_name}.webp' + data = await self._hakush_request(url) + if isinstance(data, bytes): + webp_stream = io.BytesIO(data) + with Image.open(webp_stream) as img: + img.save(str(png_file_path), 'PNG') + return img + else: + return Image.new('RGBA', (256, 256)) + + async def _hakush_request( + self, + url: str, + method: Literal['GET', 'POST'] = 'GET', + header: Dict[str, Any] = _HEADER, + params: Optional[Dict[str, Any]] = None, + _json: Optional[Dict[str, Any]] = None, + ) -> Union[Dict, int, bytes]: + async with AsyncClient(timeout=None) as client: + logger.debug(f'[HAKUSH] 正在请求{url}') + resp = await client.request( + method, + url, + headers=header, + params=params, + json=_json, + ) + try: + raw_data = await resp.json() + except: # noqa + try: + raw_data = json.loads(resp.text) + except: # noqa + try: + raw_data = resp.content + except: # noqa + raw_data = { + 'retcode': -999, + 'data': resp.text, + } + if not isinstance(raw_data, bytes): + logger.debug(raw_data) + return raw_data + + +hakush_api = _HakushAPI()