Support signal_log avatar weapon command

This commit is contained in:
omg-xtao 2024-07-05 22:10:36 +08:00 committed by GitHub
parent 528a1a49a4
commit d072dc57ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 3205 additions and 3988 deletions

359
core/dependence/assets.py Normal file
View File

@ -0,0 +1,359 @@
import asyncio
from pathlib import Path
from ssl import SSLZeroReturnError
from typing import Optional, List, Dict
from aiofiles import open as async_open
from httpx import AsyncClient, HTTPError
from core.base_service import BaseService
from modules.wiki.base import WikiModel
from modules.wiki.models.avatar import Avatar
from modules.wiki.models.weapon import Weapon
from modules.wiki.models.buddy import Buddy
from modules.wiki.models.equipment_suit import EquipmentSuit
from utils.const import PROJECT_ROOT
from utils.log import logger
from utils.typedefs import StrOrURL, StrOrInt
ASSETS_PATH = PROJECT_ROOT.joinpath("resources/assets")
ASSETS_PATH.mkdir(exist_ok=True, parents=True)
DATA_MAP = {
"avatar": WikiModel.BASE_URL + "avatars.json",
"weapon": WikiModel.BASE_URL + "weapons.json",
"buddy": WikiModel.BASE_URL + "buddy.json",
"equipment_suit": WikiModel.BASE_URL + "equipment_suits.json",
}
class AssetsServiceError(Exception):
pass
class AssetsCouldNotFound(AssetsServiceError):
def __init__(self, message: str, target: str):
self.message = message
self.target = target
super().__init__(f"{message}: target={target}")
class _AssetsService:
client: Optional[AsyncClient] = None
def __init__(self, client: Optional[AsyncClient] = None) -> None:
self.client = client
async def _download(self, url: StrOrURL, path: Path, retry: int = 5) -> Optional[Path]:
"""从 url 下载图标至 path"""
if not url:
return None
logger.debug("正在从 %s 下载图标至 %s", url, path)
headers = None
for time in range(retry):
try:
response = await self.client.get(url, follow_redirects=False, headers=headers)
except Exception as error: # pylint: disable=W0703
if not isinstance(error, (HTTPError, SSLZeroReturnError)):
logger.error(error) # 打印未知错误
if time != retry - 1: # 未达到重试次数
await asyncio.sleep(1)
else:
raise error
continue
if response.status_code != 200: # 判定页面是否正常
return None
async with async_open(path, "wb") as file:
await file.write(response.content) # 保存图标
return path.resolve()
class _AvatarAssets(_AssetsService):
path: Path
data: List[Avatar]
name_map: Dict[str, Avatar]
id_map: Dict[int, Avatar]
def __init__(self, client: Optional[AsyncClient] = None) -> None:
super().__init__(client)
self.path = ASSETS_PATH.joinpath("agent")
self.path.mkdir(exist_ok=True, parents=True)
async def initialize(self):
logger.info("正在初始化角色素材图标")
html = await self.client.get(DATA_MAP["avatar"])
self.data = [Avatar(**data) for data in html.json()]
self.name_map = {icon.name: icon for icon in self.data}
self.id_map = {icon.id: icon for icon in self.data}
tasks = []
for icon in self.data:
base_path = self.path / f"{icon.id}"
base_path.mkdir(exist_ok=True, parents=True)
gacha_path = base_path / "gacha.png"
icon_path = base_path / "icon.png"
normal_path = base_path / "normal.png"
if not gacha_path.exists():
tasks.append(self._download(icon.gacha, gacha_path))
if not icon_path.exists():
tasks.append(self._download(icon.icon_, icon_path))
if not normal_path.exists():
tasks.append(self._download(icon.normal, normal_path))
if len(tasks) >= 100:
await asyncio.gather(*tasks)
tasks = []
if tasks:
await asyncio.gather(*tasks)
logger.info("角色素材图标初始化完成")
def get_path(self, icon: Avatar, name: str, ext: str = "png") -> Path:
path = self.path / f"{icon.id}"
path.mkdir(exist_ok=True, parents=True)
return path / f"{name}.{ext}"
def get_by_id(self, id_: int) -> Optional[Avatar]:
return self.id_map.get(id_, None)
def get_by_name(self, name: str) -> Optional[Avatar]:
return self.name_map.get(name, None)
def get_target(self, target: StrOrInt, second_target: StrOrInt = None) -> Avatar:
data = None
if isinstance(target, int):
data = self.get_by_id(target)
elif isinstance(target, str):
data = self.get_by_name(target)
if data is None:
if second_target:
return self.get_target(second_target)
raise AssetsCouldNotFound("角色素材图标不存在", target)
return data
def gacha(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
icon = self.get_target(target, second_target)
return self.get_path(icon, "gacha")
def icon(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
icon = self.get_target(target, second_target)
return self.get_path(icon, "icon")
def normal(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
icon = self.get_target(target, second_target)
return self.get_path(icon, "normal")
class _WeaponAssets(_AssetsService):
path: Path
data: List[Weapon]
name_map: Dict[str, Weapon]
id_map: Dict[int, Weapon]
def __init__(self, client: Optional[AsyncClient] = None) -> None:
super().__init__(client)
self.path = ASSETS_PATH.joinpath("engines")
self.path.mkdir(exist_ok=True, parents=True)
async def initialize(self):
logger.info("正在初始化武器素材图标")
html = await self.client.get(DATA_MAP["weapon"])
self.data = [Weapon(**data) for data in html.json()]
self.name_map = {icon.name: icon for icon in self.data}
self.id_map = {icon.id: icon for icon in self.data}
tasks = []
for icon in self.data:
base_path = self.path / f"{icon.id}"
base_path.mkdir(exist_ok=True, parents=True)
icon_path = base_path / "icon.webp"
if not icon_path.exists():
tasks.append(self._download(icon.icon, icon_path))
if len(tasks) >= 100:
await asyncio.gather(*tasks)
tasks = []
if tasks:
await asyncio.gather(*tasks)
logger.info("武器素材图标初始化完成")
def get_path(self, icon: Weapon, name: str) -> Path:
path = self.path / f"{icon.id}"
path.mkdir(exist_ok=True, parents=True)
return path / f"{name}.webp"
def get_by_id(self, id_: int) -> Optional[Weapon]:
return self.id_map.get(id_, None)
def get_by_name(self, name: str) -> Optional[Weapon]:
return self.name_map.get(name, None)
def get_target(self, target: StrOrInt, second_target: StrOrInt = None) -> Optional[Weapon]:
if isinstance(target, int):
return self.get_by_id(target)
elif isinstance(target, str):
return self.get_by_name(target)
if second_target:
return self.get_target(second_target)
raise AssetsCouldNotFound("武器素材图标不存在", target)
def icon(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
icon = self.get_target(target, second_target)
return self.get_path(icon, "icon")
class _BuddyAssets(_AssetsService):
path: Path
data: List[Buddy]
id_map: Dict[int, Buddy]
name_map: Dict[str, Buddy]
def __init__(self, client: Optional[AsyncClient] = None) -> None:
super().__init__(client)
self.path = ASSETS_PATH.joinpath("buddy")
self.path.mkdir(exist_ok=True, parents=True)
async def initialize(self):
logger.info("正在初始化邦布素材图标")
html = await self.client.get(DATA_MAP["buddy"])
self.data = [Buddy(**data) for data in html.json()]
self.id_map = {icon.id: icon for icon in self.data}
self.name_map = {icon.name: icon for icon in self.data}
tasks = []
for icon in self.data:
webp_path = self.path / f"{icon.id}.webp"
png_path = self.path / f"{icon.id}.png"
if not webp_path.exists() and icon.webp:
tasks.append(self._download(icon.webp, webp_path))
if not png_path.exists() and icon.png:
tasks.append(self._download(icon.png, png_path))
if len(tasks) >= 100:
await asyncio.gather(*tasks)
tasks = []
if tasks:
await asyncio.gather(*tasks)
logger.info("邦布素材图标初始化完成")
def get_path(self, icon: Buddy, ext: str) -> Path:
path = self.path / f"{icon.id}.{ext}"
return path
def get_by_id(self, id_: int) -> Optional[Buddy]:
return self.id_map.get(id_, None)
def get_by_name(self, name: str) -> Optional[Buddy]:
return self.name_map.get(name, None)
def get_target(self, target: StrOrInt, second_target: StrOrInt = None) -> Optional[Buddy]:
if isinstance(target, int):
return self.get_by_id(target)
elif isinstance(target, str):
return self.get_by_name(target)
if second_target:
return self.get_target(second_target)
raise AssetsCouldNotFound("邦布素材图标不存在", target)
def webp(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
icon = self.get_target(target, second_target)
return self.get_path(icon, "webp")
def png(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
icon = self.get_target(target, second_target)
return self.get_path(icon, "png")
def icon(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
icon = self.get_target(target, second_target)
webp_path = self.get_path(icon, "webp")
png_path = self.get_path(icon, "png")
if webp_path.exists():
return webp_path
if png_path.exists():
return png_path
raise AssetsCouldNotFound("邦布素材图标不存在", target)
class _EquipmentSuitAssets(_AssetsService):
path: Path
data: List[EquipmentSuit]
id_map: Dict[int, EquipmentSuit]
name_map: Dict[str, EquipmentSuit]
def __init__(self, client: Optional[AsyncClient] = None) -> None:
super().__init__(client)
self.path = ASSETS_PATH.joinpath("equipment_suit")
self.path.mkdir(exist_ok=True, parents=True)
async def initialize(self):
logger.info("正在初始化驱动盘素材图标")
html = await self.client.get(DATA_MAP["equipment_suit"])
self.data = [EquipmentSuit(**data) for data in html.json()]
self.id_map = {theme.id: theme for theme in self.data}
self.name_map = {theme.name: theme for theme in self.data}
tasks = []
for theme in self.data:
path = self.path / f"{theme.id}.webp"
if not path.exists():
tasks.append(self._download(theme.icon, path))
if len(tasks) >= 100:
await asyncio.gather(*tasks)
tasks = []
if tasks:
await asyncio.gather(*tasks)
logger.info("驱动盘素材图标初始化完成")
def get_path(self, theme: EquipmentSuit, ext: str) -> Path:
path = self.path / f"{theme.id}.{ext}"
return path
def get_by_id(self, id_: int) -> Optional[EquipmentSuit]:
return self.id_map.get(id_, None)
def get_by_name(self, name_: str) -> Optional[EquipmentSuit]:
return self.name_map.get(name_, None)
def get_target(self, target: StrOrInt, second_target: StrOrInt = None) -> Optional[EquipmentSuit]:
if isinstance(target, int):
return self.get_by_id(target)
elif isinstance(target, str):
return self.get_by_name(target)
if second_target:
return self.get_target(second_target)
raise AssetsCouldNotFound("驱动盘素材图标不存在", target)
def icon(self, target: StrOrInt, second_target: StrOrInt = None) -> Path:
theme = self.get_target(target, second_target)
webp_path = self.get_path(theme, "webp")
if webp_path.exists():
return webp_path
raise AssetsCouldNotFound("驱动盘素材图标不存在", target)
class AssetsService(BaseService.Dependence):
"""asset服务
用于储存和管理 asset :
当对应的 asset (如某角色图标)不存在时该服务会先查找本地
若本地不存在则从网络上下载若存在则返回其路径
"""
client: Optional[AsyncClient] = None
avatar: _AvatarAssets
"""角色"""
weapon: _WeaponAssets
"""武器"""
buddy: _BuddyAssets
"""邦布"""
equipment_suit: _EquipmentSuitAssets
"""驱动盘"""
def __init__(self):
self.client = AsyncClient(timeout=60.0)
self.avatar = _AvatarAssets(self.client)
self.weapon = _WeaponAssets(self.client)
self.buddy = _BuddyAssets(self.client)
self.equipment_suit = _EquipmentSuitAssets(self.client)
async def initialize(self): # pylint: disable=W0221
await self.avatar.initialize()
await self.weapon.initialize()
await self.buddy.initialize()
await self.equipment_suit.initialize()

View File

@ -0,0 +1 @@
"""GameService"""

View File

@ -0,0 +1,61 @@
from typing import List
from core.base_service import BaseService
from core.dependence.redisdb import RedisDB
__all__ = [
"GameCache",
"GameCacheForAvatar",
"GameCacheForStrategy",
"GameCacheForBuddy",
"GameCacheForWeapon",
"GameCacheForEquipmentSuit",
]
class GameCache:
qname: str
def __init__(self, redis: RedisDB, ttl: int = 3600):
self.client = redis.client
self.ttl = ttl
async def get_url_list(self, character_name: str):
qname = f"{self.qname}:{character_name}"
return [str(str_data, encoding="utf-8") for str_data in await self.client.lrange(qname, 0, -1)][::-1]
async def set_url_list(self, character_name: str, str_list: List[str]):
qname = f"{self.qname}:{character_name}"
await self.client.ltrim(qname, 1, 0)
await self.client.lpush(qname, *str_list)
await self.client.expire(qname, self.ttl)
return await self.client.llen(qname)
async def get_file(self, character_name: str):
qname = f"{self.qname}:{character_name}"
return await self.client.get(qname)
async def set_file(self, character_name: str, file: str):
qname = f"{self.qname}:{character_name}"
await self.client.set(qname, file)
await self.client.expire(qname, self.ttl)
class GameCacheForAvatar(BaseService.Component, GameCache):
qname = "game:avatar"
class GameCacheForStrategy(BaseService.Component, GameCache):
qname = "game:strategy"
class GameCacheForBuddy(BaseService.Component, GameCache):
qname = "game:buddy"
class GameCacheForWeapon(BaseService.Component, GameCache):
qname = "game:weapon"
class GameCacheForEquipmentSuit(BaseService.Component, GameCache):
qname = "game:relics"

View File

@ -0,0 +1,66 @@
from core.base_service import BaseService
from core.services.game.cache import (
GameCacheForAvatar,
GameCacheForStrategy,
GameCacheForBuddy,
GameCacheForWeapon,
GameCacheForEquipmentSuit,
)
__all__ = "GameCacheService"
class GameCacheService(BaseService):
def __init__(
self,
avatar_cache: GameCacheForAvatar,
strategy_cache: GameCacheForStrategy,
buddy_cache: GameCacheForBuddy,
weapon_cache: GameCacheForWeapon,
equipment_suit_cache: GameCacheForEquipmentSuit,
):
self.avatar_cache = avatar_cache
self.strategy_cache = strategy_cache
self.buddy_cache = buddy_cache
self.weapon_cache = weapon_cache
self.equipment_suit_cache = equipment_suit_cache
async def get_avatar_cache(self, character_name: str) -> str:
cache = await self.avatar_cache.get_file(character_name)
if cache is not None:
return cache.decode("utf-8")
async def set_avatar_cache(self, character_name: str, file: str) -> None:
await self.avatar_cache.set_file(character_name, file)
async def get_strategy_cache(self, character_name: str) -> str:
cache = await self.strategy_cache.get_file(character_name)
if cache is not None:
return cache.decode("utf-8")
async def set_strategy_cache(self, character_name: str, file: str) -> None:
await self.strategy_cache.set_file(character_name, file)
async def get_buddy_cache(self, character_name: str) -> str:
cache = await self.buddy_cache.get_file(character_name)
if cache is not None:
return cache.decode("utf-8")
async def set_buddy_cache(self, character_name: str, file: str) -> None:
await self.buddy_cache.set_file(character_name, file)
async def get_weapon_cache(self, weapon_name: str) -> str:
cache = await self.weapon_cache.get_file(weapon_name)
if cache is not None:
return cache.decode("utf-8")
async def set_weapon_cache(self, weapon_name: str, file: str) -> None:
await self.weapon_cache.set_file(weapon_name, file)
async def get_equipment_suit_cache(self, relics_name: str) -> str:
cache = await self.equipment_suit_cache.get_file(relics_name)
if cache is not None:
return cache.decode("utf-8")
async def set_equipment_suit_cache(self, relics_name: str, file: str) -> None:
await self.equipment_suit_cache.set_file(relics_name, file)

View File

@ -0,0 +1 @@
"""WikiService"""

View File

@ -0,0 +1,44 @@
from core.base_service import BaseService
from modules.wiki.character import Character
from modules.wiki.weapon import Weapon
from modules.wiki.buddy import Buddy
from modules.wiki.raider import Raider
from modules.wiki.equipment_suit import EquipmentSuit
from utils.log import logger
__all__ = ["WikiService"]
class WikiService(BaseService):
def __init__(self):
self.character = Character()
self.weapon = Weapon()
self.buddy = Buddy()
self.raider = Raider()
self.equipment_suit = EquipmentSuit()
async def initialize(self) -> None:
logger.info("正在加载 Wiki 数据")
try:
await self.character.read()
await self.weapon.read()
await self.buddy.read()
await self.raider.read()
await self.equipment_suit.read()
except Exception as e:
logger.error("加载 Wiki 数据失败", exc_info=e)
logger.info("加载 Wiki 数据完成")
async def refresh_wiki(self) -> None:
logger.info("正在重新获取Wiki")
logger.info("正在重新获取角色信息")
await self.character.refresh()
logger.info("正在重新获取武器信息")
await self.weapon.refresh()
logger.info("正在重新获取邦布信息")
await self.buddy.refresh()
logger.info("正在重新获取攻略信息")
await self.raider.refresh()
logger.info("正在重新获取驱动盘信息")
await self.equipment_suit.refresh()
logger.info("刷新成功")

View File

@ -1,19 +1,16 @@
from metadata.pool.pool_100 import POOL_100
from metadata.pool.pool_200 import POOL_200
from metadata.pool.pool_301 import POOL_301
from metadata.pool.pool_302 import POOL_302
from metadata.pool.pool_500 import POOL_500
from metadata.pool.pool_1 import POOL_1
from metadata.pool.pool_2 import POOL_2
from metadata.pool.pool_3 import POOL_3
from metadata.pool.pool_5 import POOL_5
def get_pool_by_id(pool_type):
if pool_type == 100:
return POOL_100
if pool_type == 200:
return POOL_200
if pool_type in [301, 400]:
return POOL_301
if pool_type == 302:
return POOL_302
if pool_type == 500:
return POOL_500
if pool_type == 1:
return POOL_1
if pool_type == 2:
return POOL_2
if pool_type == 3:
return POOL_3
if pool_type == 5:
return POOL_5
return None

3
metadata/pool/pool_1.py Normal file
View File

@ -0,0 +1,3 @@
POOL_1 = [
{"five": ["热门卡司"], "four": [], "from": "2024-07-04 06:00:00", "name": "热门卡司", "to": "2050-09-15 17:59:59"}
]

View File

@ -1,3 +0,0 @@
POOL_100 = [
{"five": ["新手池"], "four": [], "from": "2020-09-15 06:00:00", "name": "新手池", "to": "2050-09-15 17:59:59"}
]

9
metadata/pool/pool_2.py Normal file
View File

@ -0,0 +1,9 @@
POOL_2 = [
{
"five": ["艾莲"],
"four": ["苍角", "安东"],
"name": "慵懒逐浪",
"from": "2024-07-04 06:00:00",
"to": "2024-07-24 11:59:59",
},
]

View File

@ -1,3 +0,0 @@
POOL_200 = [
{"five": ["常驻池"], "four": [], "from": "2020-09-15 06:00:00", "name": "奔行世间", "to": "2050-09-15 17:59:59"}
]

9
metadata/pool/pool_3.py Normal file
View File

@ -0,0 +1,9 @@
POOL_3 = [
{
"five": ["深海访客"],
"four": ["含羞恶面", "旋钻机-赤轴"],
"name": "喧哗奏鸣",
"from": "2024-07-04 06:00:00",
"to": "2024-07-24 11:59:59",
},
]

View File

@ -1,471 +0,0 @@
POOL_301 = [
{
"five": ["希格雯", "芙宁娜"],
"four": ["诺艾尔", "嘉明", "罗莎莉亚"],
"name": "柔柔海露心|众水的颂诗",
"from": "2024-06-25 18:00:00",
"to": "2024-07-16 14:59:59",
},
{
"five": ["克洛琳德", "艾尔海森"],
"four": ["赛索斯", "班尼特", "托马"],
"name": "流霆贯夜|敕诫枢谋",
"from": "2024-06-05 06:00:00",
"to": "2024-06-25 17:59:59",
},
{
"five": ["流浪者", "白术"],
"four": ["莱依拉", "珐露珊", "北斗"],
"name": "余火变相|心珠循琅",
"from": "2024-05-14 18:00:00",
"to": "2024-06-04 14:59:59",
},
{
"five": ["阿蕾奇诺", "林尼"],
"four": ["菲米尼", "琳妮特", "香菱"],
"name": "炉边烬影|光与影的戏术",
"from": "2024-04-24 06:00:00",
"to": "2024-05-14 17:59:59",
},
{
"five": ["那维莱特", "枫原万叶"],
"four": ["芭芭拉", "行秋", "烟绯"],
"name": "谕告的潮音|红叶逐荒波",
"from": "2024-04-02 18:00:00",
"to": "2024-04-23 14:59:59",
},
{
"five": ["千织", "荒泷一斗"],
"four": ["五郎", "云堇", "多莉"],
"name": "千云绘羽织|鬼门斗宴",
"from": "2024-03-13 06:00:00",
"to": "2024-04-02 17:59:59",
},
{
"five": ["", "八重神子"],
"four": ["瑶瑶", "辛焱", "凝光"],
"name": "烟火之邀|华紫樱绯",
"from": "2024-02-20 18:00:00",
"to": "2024-03-12 14:59:59",
},
{
"five": ["闲云", "纳西妲"],
"four": ["嘉明", "珐露珊", "诺艾尔"],
"name": "云府鹤行|月草的赐慧",
"from": "2024-01-31 06:00:00",
"to": "2024-02-20 17:59:59",
},
{
"five": ["雷电将军", "宵宫"],
"four": ["夏沃蕾", "九条裟罗", "班尼特"],
"name": "影寂天下人|琉焰华舞",
"from": "2024-01-09 18:00:00",
"to": "2024-01-30 14:59:59",
},
{
"five": ["娜维娅", "神里绫华"],
"four": ["砂糖", "罗莎莉亚", "坎蒂丝"],
"name": "刺玫的铭誓|白鹭之庭",
"from": "2023-12-20 06:00:00",
"to": "2024-01-09 17:59:59",
},
{
"five": ["赛诺", "神里绫人"],
"four": ["绮良良", "久岐忍", "香菱"],
"name": "雳裁冥昭|苍流踏花",
"from": "2023-11-28 18:00:00",
"to": "2023-12-19 14:59:59",
},
{
"five": ["芙宁娜", "白术"],
"four": ["夏洛蒂", "柯莱", "北斗"],
"from": "2023-11-08 06:00:00",
"name": "众水的颂诗|心珠循琅",
"to": "2023-11-28 17:59:59",
},
{
"five": ["莱欧斯利", "温迪"],
"four": ["重云", "托马", "多莉"],
"from": "2023-10-17 18:00:00",
"name": "劫中泛滥|杯装之诗",
"to": "2023-11-07 14:59:59",
},
{
"five": ["那维莱特", "胡桃"],
"four": ["菲谢尔", "行秋", "迪奥娜"],
"from": "2023-09-27 06:00:00",
"name": "谕告的潮音|雪霁梅香",
"to": "2023-10-17 17:59:59",
},
{
"five": ["钟离", "达达利亚"],
"four": ["菲米尼", "早柚", "诺艾尔"],
"from": "2023-09-05 18:00:00",
"name": "陵薮市朝|暂别冬都",
"to": "2023-09-26 14:59:59",
},
{
"five": ["林尼", "夜兰"],
"four": ["琳妮特", "班尼特", "芭芭拉"],
"from": "2023-08-16 06:00:00",
"name": "光与影的戏术|素霓伣天",
"to": "2023-09-05 17:59:59",
},
{
"five": ["珊瑚宫心海", "流浪者"],
"four": ["珐露珊", "罗莎莉亚", "烟绯"],
"from": "2023-07-25 18:00:00",
"name": "浮岳虹珠|余火变相",
"to": "2023-08-15 14:59:59",
},
{
"five": ["优菈", "可莉"],
"four": ["米卡", "雷泽", "托马"],
"from": "2023-07-05 06:00:00",
"name": "浪涌之瞬|闪焰的驻足",
"to": "2023-07-25 17:59:59",
},
{
"five": ["艾尔海森", "枫原万叶"],
"four": ["瑶瑶", "鹿野院平藏", "香菱"],
"from": "2023-06-13 18:00:00",
"name": "敕诫枢谋|叶落风随",
"to": "2023-07-04 14:59:59",
},
{
"five": ["宵宫", "八重神子"],
"four": ["绮良良", "云堇", "重云"],
"from": "2023-05-24 06:00:00",
"name": "琉焰华舞|浮世笑百姿",
"to": "2023-06-13 17:59:59",
},
{
"five": ["白术", "甘雨"],
"four": ["卡维", "坎蒂丝", "菲谢尔"],
"from": "2023-05-02 18:00:00",
"name": "心珠循琅|浮生孰来",
"to": "2023-05-23 14:59:59",
},
{
"five": ["纳西妲", "妮露"],
"four": ["久岐忍", "多莉", "莱依拉"],
"from": "2023-04-12 06:00:00",
"name": "月草的赐慧|翩舞歈莲",
"to": "2023-05-02 17:59:59",
},
{
"five": ["申鹤", "神里绫华"],
"four": ["米卡", "砂糖", "迪奥娜"],
"from": "2023-03-21 18:00:00",
"name": "孤辰茕怀|白鹭霜华",
"to": "2023-04-11 14:59:59",
},
{
"five": ["迪希雅", "赛诺"],
"four": ["班尼特", "芭芭拉", "柯莱"],
"from": "2023-03-01 06:00:00",
"name": "烈阳烁金|雳裁冥昭",
"to": "2023-03-21 17:59:59",
},
{
"five": ["胡桃", "夜兰"],
"four": ["行秋", "凝光", "北斗"],
"from": "2023-02-07 18:00:00",
"name": "赤团开时|素霓伣天",
"to": "2023-02-28 14:59:59",
},
{
"five": ["艾尔海森", ""],
"four": ["瑶瑶", "云堇", "辛焱"],
"from": "2023-01-18 06:00:00",
"name": "敕诫枢谋|烟火之邀",
"to": "2023-02-07 17:59:59",
},
{
"five": ["雷电将军", "神里绫人"],
"four": ["罗莎莉亚", "早柚", "九条裟罗"],
"from": "2022-12-27 18:00:00",
"name": "影寂天下人|苍流踏花",
"to": "2023-01-17 14:59:59",
},
{
"five": ["流浪者", "荒泷一斗"],
"four": ["珐露珊", "五郎", "烟绯"],
"from": "2022-12-07 06:00:00",
"name": "余火变相|鬼门斗宴",
"to": "2022-12-27 17:59:59",
},
{
"five": ["八重神子", "达达利亚"],
"four": ["莱依拉", "托马", "鹿野院平藏"],
"from": "2022-11-18 18:00:00",
"name": "华紫樱绯|暂别冬都",
"to": "2022-12-06 14:59:59",
},
{
"five": ["纳西妲", "宵宫"],
"four": ["雷泽", "诺艾尔", "班尼特"],
"from": "2022-11-2 06:00:00",
"name": "月草的赐慧|焰色天河",
"to": "2022-11-18 17:59:59",
},
{
"five": ["妮露", "阿贝多"],
"four": ["北斗", "芭芭拉", "香菱"],
"from": "2022-10-14 18:00:00",
"name": "翩舞歈莲|深秘之息",
"to": "2022-11-01 14:59:59",
},
{
"five": ["赛诺", "温迪"],
"four": ["久岐忍", "早柚", "坎蒂丝"],
"from": "2022-09-28 06:00:00",
"name": "雳裁冥昭|杯装之诗",
"to": "2022-10-14 17:59:59",
},
{
"five": ["甘雨", "心海"],
"four": ["行秋", "砂糖", "多莉"],
"from": "2022-09-09 18:00:00",
"name": "浮生孰来|浮岳虹珠",
"to": "2022-09-27 14:59:59",
},
{
"five": ["提纳里", "钟离"],
"four": ["云堇", "辛焱", "班尼特"],
"from": "2022-08-24 06:00:00",
"name": "巡御蘙荟|陵薮市朝",
"to": "2022-09-09 17:59:59",
},
{
"five": ["宵宫"],
"four": ["云堇", "辛焱", "班尼特"],
"from": "2022-08-02 18:00:00",
"name": "焰色天河",
"to": "2022-08-23 14:59:59",
},
{
"five": ["枫原万叶", "可莉"],
"four": ["凝光", "鹿野院平藏", "托马"],
"from": "2022-07-13 06:00:00",
"name": "红叶逐荒波",
"to": "2022-08-02 17:59:59",
},
{
"five": ["荒泷一斗"],
"four": ["烟绯", "芭芭拉", "诺艾尔"],
"from": "2022-06-21 18:00:00",
"name": "鬼门斗宴",
"to": "2022-07-12 14:59:59",
},
{
"five": ["夜兰", ""],
"four": ["烟绯", "芭芭拉", "诺艾尔"],
"from": "2022-05-31 06:00:00",
"name": "素霓伣天|烟火之邀",
"to": "2022-06-21 17:59:59",
},
{
"five": ["神里绫华"],
"four": ["罗莎莉亚", "早柚", "雷泽"],
"from": "2022-04-19 17:59:59",
"name": "白鹭之庭",
"to": "2022-05-31 05:59:59",
},
{
"five": ["神里绫人", "温迪"],
"four": ["香菱", "砂糖", "云堇"],
"from": "2022-03-30 06:00:00",
"name": "苍流踏花|杯装之诗",
"to": "2022-04-19 17:59:59",
},
{
"five": ["雷电将军", "珊瑚宫心海"],
"four": ["辛焱", "九条裟罗", "班尼特"],
"from": "2022-03-08 18:00:00",
"name": "影寂天下人|浮岳虹珠",
"to": "2022-03-29 14:59:59",
},
{
"five": ["八重神子"],
"four": ["菲谢尔", "迪奥娜", "托马"],
"from": "2022-02-16 06:00:00",
"name": "华紫樱绯",
"to": "2022-03-08 17:59:59",
},
{
"five": ["甘雨", "钟离"],
"four": ["行秋", "北斗", "烟绯"],
"from": "2022-01-25 18:00:00",
"name": "浮生孰来|陵薮市朝",
"to": "2022-02-15 14:59:59",
},
{
"five": ["申鹤", ""],
"four": ["云堇", "凝光", "重云"],
"from": "2022-01-05 06:00:00",
"name": "出尘入世|烟火之邀",
"to": "2022-01-25 17:59:59",
},
{
"five": ["荒泷一斗"],
"four": ["五郎", "芭芭拉", "香菱"],
"from": "2021-12-14 18:00:00",
"name": "鬼门斗宴",
"to": "2022-01-04 14:59:59",
},
{
"five": ["阿贝多", "优菈"],
"four": ["班尼特", "诺艾尔", "罗莎莉亚"],
"from": "2021-11-24 06:00:00",
"name": "深秘之息|浪涌之瞬",
"to": "2021-12-14 17:59:59",
},
{
"five": ["胡桃"],
"four": ["托马", "迪奥娜", "早柚"],
"from": "2021-11-02 18:00:00",
"name": "赤团开时",
"to": "2021-11-23 14:59:59",
},
{
"five": ["达达利亚"],
"four": ["凝光", "重云", "烟绯"],
"from": "2021-10-13 06:00:00",
"name": "暂别冬都",
"to": "2021-11-02 17:59:59",
},
{
"five": ["珊瑚宫心海"],
"four": ["罗莎莉亚", "北斗", "行秋"],
"from": "2021-09-21 18:00:00",
"name": "浮岳虹珠",
"to": "2021-10-12 14:59:59",
},
{
"five": ["雷电将军"],
"four": ["九条裟罗", "香菱", "砂糖"],
"from": "2021-09-01 06:00:00",
"name": "影寂天下人",
"to": "2021-09-21 17:59:59",
},
{
"five": ["宵宫"],
"four": ["早柚", "迪奥娜", "辛焱"],
"from": "2021-08-10 18:00:00",
"name": "焰色天河",
"to": "2021-08-31 14:59:59",
},
{
"five": ["神里绫华"],
"four": ["凝光", "重云", "烟绯"],
"from": "2021-07-21 06:00:00",
"name": "白鹭之庭",
"to": "2021-08-10 17:59:59",
},
{
"five": ["枫原万叶"],
"four": ["罗莎莉亚", "班尼特", "雷泽"],
"from": "2021-06-29 18:00:00",
"name": "红叶逐荒波",
"to": "2021-07-20 14:59:59",
},
{
"five": ["可莉"],
"four": ["芭芭拉", "砂糖", "菲谢尔"],
"from": "2021-06-09 06:00:00",
"name": "逃跑的太阳",
"to": "2021-06-29 17:59:59",
},
{
"five": ["优菈"],
"four": ["辛焱", "行秋", "北斗"],
"from": "2021-05-18 18:00:00",
"name": "浪沫的旋舞",
"to": "2021-06-08 14:59:59",
},
{
"five": ["钟离"],
"four": ["烟绯", "诺艾尔", "迪奥娜"],
"from": "2021-04-28 06:00:00",
"name": "陵薮市朝",
"to": "2021-05-18 17:59:59",
},
{
"five": ["达达利亚"],
"four": ["罗莎莉亚", "芭芭拉", "菲谢尔"],
"from": "2021-04-06 18:00:00",
"name": "暂别冬都",
"to": "2021-04-27 14:59:59",
},
{
"five": ["温迪"],
"four": ["砂糖", "雷泽", "诺艾尔"],
"from": "2021-03-17 06:00:00",
"name": "杯装之诗",
"to": "2021-04-06 15:59:59",
},
{
"five": ["胡桃"],
"four": ["行秋", "香菱", "重云"],
"from": "2021-03-02 18:00:00",
"name": "赤团开时",
"to": "2021-03-16 14:59:59",
},
{
"five": ["刻晴"],
"four": ["凝光", "班尼特", "芭芭拉"],
"from": "2021-02-17 18:00:00",
"name": "鱼龙灯昼",
"to": "2021-03-02 15:59:59",
},
{
"five": [""],
"four": ["迪奥娜", "北斗", "辛焱"],
"from": "2021-02-03 06:00:00",
"name": "烟火之邀",
"to": "2021-02-17 15:59:59",
},
{
"five": ["甘雨"],
"four": ["香菱", "行秋", "诺艾尔"],
"from": "2021-01-12 18:00:00",
"name": "浮生孰来",
"to": "2021-02-02 14:59:59",
},
{
"five": ["阿贝多"],
"four": ["菲谢尔", "砂糖", "班尼特"],
"from": "2020-12-23 06:00:00",
"name": "深秘之息",
"to": "2021-01-12 15:59:59",
},
{
"five": ["钟离"],
"four": ["辛焱", "雷泽", "重云"],
"from": "2020-12-01 18:00:00",
"name": "陵薮市朝",
"to": "2020-12-22 14:59:59",
},
{
"five": ["达达利亚"],
"four": ["迪奥娜", "北斗", "凝光"],
"from": "2020-11-11 06:00:00",
"name": "暂别冬都",
"to": "2020-12-01 15:59:59",
},
{
"five": ["可莉"],
"four": ["行秋", "诺艾尔", "砂糖"],
"from": "2020-10-20 18:00:00",
"name": "闪焰的驻足",
"to": "2020-11-10 14:59:59",
},
{
"five": ["温迪"],
"four": ["芭芭拉", "菲谢尔", "香菱"],
"from": "2020-9-28 06:00:00",
"name": "杯装之诗",
"to": "2020-10-18 17:59:59",
},
]

View File

@ -1,464 +0,0 @@
POOL_302 = [
{
"five": ["白雨心弦", "静水流涌之辉"],
"four": ["千岩古剑", "匣里龙吟", "西风长枪", "西风秘典", "祭礼弓"],
"name": "神铸赋形",
"from": "2024-06-25 18:00:00",
"to": "2024-07-16 14:59:59",
},
{
"five": ["赦罪", "裁叶萃光"],
"four": ["千岩长枪", "祭礼剑", "祭礼大剑", "流浪乐章", "绝弦"],
"name": "神铸赋形",
"from": "2024-06-05 06:00:00",
"to": "2024-06-25 17:59:59",
},
{
"five": ["图莱杜拉的回忆", "碧落之珑"],
"four": ["勘探钻机", "测距规", "西风剑", "雨裁", "祭礼残章"],
"name": "神铸赋形",
"from": "2024-05-14 18:00:00",
"to": "2024-06-04 14:59:59",
},
{
"five": ["赤月之形", "最初的大魔术"],
"four": ["船坞长剑", "便携动力锯", "匣里灭辰", "昭心", "西风猎弓"],
"name": "神铸赋形",
"from": "2024-04-24 06:00:00",
"to": "2024-05-14 17:59:59",
},
{
"five": ["万世流涌大典", "苍古自由之誓"],
"four": ["暗巷的酒与诗", "幽夜华尔兹", "笛剑", "西风大剑", "西风长枪"],
"name": "神铸赋形",
"from": "2024-04-02 18:00:00",
"to": "2024-04-23 14:59:59",
},
{
"five": ["有乐御簾切", "赤角石溃杵"],
"four": ["暗巷闪光", "暗巷猎手", "钟剑", "匣里灭辰", "西风秘典"],
"name": "神铸赋形",
"from": "2024-03-13 06:00:00",
"to": "2024-04-02 17:59:59",
},
{
"five": ["神乐之真意", "和璞鸢"],
"four": ["千岩古剑", "匣里龙吟", "西风长枪", "流浪乐章", "绝弦"],
"name": "神铸赋形",
"from": "2024-02-20 18:00:00",
"to": "2024-03-12 14:59:59",
},
{
"five": ["鹤鸣余音", "千夜浮梦"],
"four": ["千岩长枪", "祭礼剑", "祭礼大剑", "祭礼残章", "祭礼弓"],
"name": "神铸赋形",
"from": "2024-01-31 06:00:00",
"to": "2024-02-20 17:59:59",
},
{
"five": ["薙草之稻光", "飞雷之弦振"],
"four": ["断浪长鳍", "西风剑", "雨裁", "昭心", "弓藏"],
"name": "神铸赋形",
"from": "2024-01-09 18:00:00",
"to": "2024-01-30 14:59:59",
},
{
"five": ["裁断", "雾切之回光"],
"four": ["恶王丸", "曚云之月", "笛剑", "匣里灭辰", "西风秘典"],
"name": "神铸赋形",
"from": "2023-12-20 06:00:00",
"to": "2024-01-09 17:59:59",
},
{
"five": ["赤沙之杖", "波乱月白经津"],
"four": ["西风猎弓", "流浪乐章", "西风长枪", "西风大剑", "匣里龙吟"],
"name": "神铸赋形",
"from": "2023-11-28 18:00:00",
"to": "2023-12-19 14:59:59",
},
{
"five": ["静水流涌之辉", "碧落之珑"],
"four": ["祭礼剑", "钟剑", "匣里灭辰", "祭礼残", "绝弦"],
"from": "2023-11-08 06:00:00",
"name": "神铸赋形",
"to": "2023-11-28 17:59:59",
},
{
"five": ["金流监督", "终末嗟叹之诗"],
"four": ["勘探钻机", "测距规", "西风剑", "雨裁", "昭心"],
"from": "2023-10-17 18:00:00",
"name": "神铸赋形",
"to": "2023-11-07 14:59:59",
},
{
"five": ["万世流涌大典", "护摩之杖"],
"four": ["船坞长剑", "便携动力锯", "幽夜华尔兹", "西风长枪", "西风秘典"],
"from": "2023-09-27 06:00:00",
"name": "神铸赋形",
"to": "2023-10-17 17:59:59",
},
{
"five": ["贯虹之槊", "冬极白星"],
"four": ["笛剑", "祭礼大剑", "匣里灭辰", "流浪乐章", "弓藏"],
"from": "2023-09-05 18:00:00",
"name": "神铸赋形",
"to": "2023-09-26 14:59:59",
},
{
"five": ["最初的大魔术", "若水"],
"four": ["祭礼剑", "西风大剑", "西风长枪", "祭礼残章", "祭礼弓"],
"from": "2023-08-16 06:00:00",
"name": "神铸赋形",
"to": "2023-09-05 17:59:59",
},
{
"five": ["不灭月华", "图莱杜拉的回忆"],
"four": ["暗巷的酒与诗", "匣里龙吟", "钟剑", "匣里灭辰", "西风猎弓"],
"from": "2023-07-25 18:00:00",
"name": "神铸赋形",
"to": "2023-08-15 14:59:59",
},
{
"five": ["松籁响起之时", "四风原典"],
"four": ["暗巷闪光", "暗巷猎手", "雨裁", "西风长枪", "昭心"],
"from": "2023-07-05 06:00:00",
"name": "神铸赋形",
"to": "2023-07-25 17:59:59",
},
{
"five": ["裁叶萃光", "苍古自由之誓"],
"four": ["断浪长鳍", "曚云之月", "西风剑", "祭礼大剑", "西风秘典"],
"from": "2023-06-13 18:00:00",
"name": "神铸赋形",
"to": "2023-07-04 14:59:59",
},
{
"five": ["飞雷之弦振", "神乐之真意"],
"four": ["恶王丸", "笛剑", "匣里灭辰", "流浪乐章", "弓藏"],
"from": "2023-05-24 06:00:00",
"name": "神铸赋形",
"to": "2023-06-13 17:59:59",
},
{
"five": ["碧落之珑", "阿莫斯之弓"],
"four": ["玛海菈的水色", "流浪的晚星", "匣里龙吟", "西风长枪", "祭礼弓"],
"from": "2023-05-02 18:00:00",
"name": "神铸赋形",
"to": "2023-05-23 14:59:59",
},
{
"five": ["千夜浮梦", "圣显之钥"],
"four": ["西福斯的月光", "西风大剑", "匣里灭辰", "祭礼残章", "绝弦"],
"from": "2023-04-12 06:00:00",
"name": "神铸赋形",
"to": "2023-05-02 17:59:59",
},
{
"five": ["息灾", "雾切之回光"],
"four": ["暗巷的酒与诗", "祭礼剑", "钟剑", "西风长枪", "西风猎弓"],
"from": "2023-03-21 18:00:00",
"name": "神铸赋形",
"to": "2023-04-11 14:59:59",
},
{
"five": ["苇海信标", "赤沙之杖"],
"four": ["暗巷闪光", "暗巷猎手", "祭礼大剑", "匣里灭辰", "昭心"],
"from": "2023-03-01 06:00:00",
"name": "神铸赋形",
"to": "2023-03-21 17:59:59",
},
{
"five": ["护摩之杖", "若水"],
"four": ["千岩古剑", "西风剑", "匣里灭辰", "西风秘典", "弓藏"],
"from": "2023-02-07 18:00:00",
"name": "神铸赋形",
"to": "2023-02-28 14:59:59",
},
{
"five": ["裁叶萃光", "和璞鸢"],
"four": ["千岩长枪", "笛剑", "雨裁", "流浪乐章", "祭礼弓"],
"from": "2023-01-18 06:00:00",
"name": "神铸赋形",
"to": "2023-02-07 17:59:59",
},
{
"five": ["薙草之稻光", "波乱月白经津"],
"four": ["恶王丸", "曚云之月", "匣里龙吟", "西风长枪", "祭礼残章"],
"from": "2022-12-27 18:00:00",
"name": "神铸赋形",
"to": "2023-01-17 14:59:59",
},
{
"five": ["图莱杜拉的回忆", "赤角石溃杵"],
"four": ["祭礼剑", "西风大剑", "断浪长鳍", "昭心", "西风猎弓"],
"from": "2022-12-07 06:00:00",
"name": "神铸赋形",
"to": "2022-12-27 17:59:59",
},
{
"five": ["神乐之真意", "冬极白星"],
"four": ["西风剑", "钟剑", "匣里灭辰", "西风秘典", "绝弦"],
"from": "2022-11-18 18:00:00",
"name": "神铸赋形",
"to": "2022-12-06 14:59:59",
},
{
"five": ["千夜浮梦", "飞雷之弦振"],
"four": ["笛剑", "祭礼大剑", "西风长枪", "流浪乐章", "弓藏"],
"from": "2022-11-2 06:00:00",
"name": "神铸赋形",
"to": "2022-11-18 17:59:59",
},
{
"five": ["圣显之钥", "磐岩结绿"],
"four": ["西福斯的月光", "雨裁", "匣里灭辰", "流浪的晚星", "祭礼弓"],
"from": "2022-10-14 18:00:00",
"name": "神铸赋形",
"to": "2022-11-01 14:59:59",
},
{
"five": ["赤沙之杖", "终末嗟叹之诗"],
"four": ["匣里龙吟", "玛海菈的水色", "西风长枪", "祭礼残章", "西风猎弓"],
"from": "2022-09-28 06:00:00",
"name": "神铸赋形",
"to": "2022-10-14 17:59:59",
},
{
"five": ["阿莫斯之弓", "不灭月华"],
"four": ["祭礼剑", "西风大剑", "匣里灭辰", "昭心", "弓藏"],
"from": "2022-09-09 18:00:00",
"name": "神铸赋形",
"to": "2022-09-27 14:59:59",
},
{
"five": ["猎人之径", "贯虹之槊"],
"four": ["西风剑", "钟剑", "西风长枪", "西风秘典", "绝弦"],
"from": "2022-08-24 06:00:00",
"name": "神铸赋形",
"to": "2022-09-09 17:59:59",
},
{
"five": ["飞雷之弦振", "斫峰之刃"],
"four": ["暗巷的酒与诗", "暗巷猎手", "笛剑", "祭礼大剑", "匣里灭辰"],
"from": "2022-08-02 18:00:00",
"name": "神铸赋形",
"to": "2022-08-23 14:59:59",
},
{
"five": ["苍古自由之誓", "四风原典"],
"four": ["千岩古剑", "匣里龙吟", "匣里灭辰", "祭礼残章", "绝弦"],
"from": "2022-07-13 06:00:00",
"name": "神铸赋形",
"to": "2022-08-02 17:59:59",
},
{
"five": ["赤角石溃杵", "尘世之锁"],
"four": ["千岩古剑", "匣里龙吟", "匣里灭辰", "祭礼残章", "绝弦"],
"from": "2022-06-21 18:00:00",
"name": "神铸赋形",
"to": "2022-07-12 14:59:59",
},
{
"five": ["若水", "和璞鸢"],
"four": ["千岩长枪", "祭礼剑", "西风大剑", "昭心", "祭礼弓"],
"from": "2022-05-31 06:00:00",
"name": "神铸赋形",
"to": "2022-06-21 17:59:59",
},
{
"five": ["雾切之回光", "无工之剑"],
"four": ["西风剑", "钟剑", "西风长枪", "西风秘典", "西风猎弓"],
"from": "2022-04-19 17:59:59",
"name": "神铸赋形",
"to": "2022-05-31 05:59:59",
},
{
"five": ["波乱月白经津", "终末嗟叹之诗"],
"four": ["弓藏", "笛剑", "流浪乐章", "匣里灭辰", "祭礼大剑"],
"from": "2022-03-30 06:00:00",
"name": "神铸赋形",
"to": "2022-04-19 17:59:59",
},
{
"five": ["薙草之稻光", "不灭月华"],
"four": ["恶王丸", "曚云之月", "匣里龙吟", "西风长枪", "祭礼残章"],
"from": "2022-03-08 18:00:00",
"name": "神铸赋形",
"to": "2022-03-29 14:59:59",
},
{
"five": ["神乐之真意", "磐岩结绿"],
"four": ["祭礼剑", "雨裁", "断浪长鳍", "昭心", "绝弦"],
"from": "2022-02-16 06:00:00",
"name": "神铸赋形",
"to": "2022-03-08 17:59:59",
},
{
"five": ["贯虹之槊", "阿莫斯之弓"],
"four": ["西风剑", "千岩古剑", "匣里灭辰", "西风秘典", "祭礼弓"],
"from": "2022-01-25 18:00:00",
"name": "神铸赋形",
"to": "2022-02-15 14:59:59",
},
{
"five": ["息灾", "和璞鸢"],
"four": ["笛剑", "西风大剑", "千岩长枪", "流浪乐章", "西风猎弓"],
"from": "2022-01-05 06:00:00",
"name": "神铸赋形",
"to": "2022-01-25 17:59:59",
},
{
"five": ["赤角石溃杵", "天空之翼"],
"four": ["暗巷闪光", "钟剑", "西风长枪", "祭礼残章", "幽夜华尔兹"],
"from": "2021-12-14 18:00:00",
"name": "神铸赋形",
"to": "2022-01-04 14:59:59",
},
{
"five": ["苍古自由之誓", "松籁响起之时"],
"four": ["匣里龙吟", "祭礼大剑", "匣里灭辰", "暗巷的酒与诗", "暗巷猎手"],
"from": "2021-11-24 06:00:00",
"name": "神铸赋形",
"to": "2021-12-14 17:59:59",
},
{
"five": ["护摩之杖", "终末嗟叹之诗"],
"four": ["祭礼剑", "雨裁", "断浪长鳍", "流浪乐章", "曚云之月"],
"from": "2021-11-02 18:00:00",
"name": "神铸赋形",
"to": "2021-11-23 14:59:59",
},
{
"five": ["冬极白星", "尘世之锁"],
"four": ["西风剑", "恶王丸", "西风长枪", "昭心", "弓藏"],
"from": "2021-10-13 06:00:00",
"name": "神铸赋形",
"to": "2021-11-02 17:59:59",
},
{
"five": ["不灭月华", "磐岩结绿"],
"four": ["笛剑", "西风大剑", "匣里灭辰", "西风秘典", "绝弦"],
"from": "2021-09-21 18:00:00",
"name": "神铸赋形",
"to": "2021-10-12 14:59:59",
},
{
"five": ["薙草之稻光", "无工之剑"],
"four": ["匣里龙吟", "钟剑", "西风长枪", "流浪乐章", "祭礼弓"],
"from": "2021-09-01 06:00:00",
"name": "神铸赋形",
"to": "2021-09-21 17:59:59",
},
{
"five": ["飞雷之弦振", "天空之刃"],
"four": ["祭礼剑", "雨裁", "匣里灭辰", "祭礼残章", "西风猎弓"],
"from": "2021-08-10 18:00:00",
"name": "神铸赋形",
"to": "2021-08-31 14:59:59",
},
{
"five": ["雾切之回光", "天空之脊"],
"four": ["西风剑", "祭礼大剑", "西风长枪", "西风秘典", "绝弦"],
"from": "2021-07-21 06:00:00",
"name": "神铸赋形",
"to": "2021-08-10 17:59:59",
},
{
"five": ["苍古自由之誓", "天空之卷"],
"four": ["暗巷闪光", "西风大剑", "匣里灭辰", "暗巷的酒与诗", "暗巷猎手"],
"from": "2021-06-29 18:00:00",
"name": "神铸赋形",
"to": "2021-07-20 14:59:59",
},
{
"five": ["天空之傲", "四风原典"],
"four": ["匣里龙吟", "钟剑", "西风长枪", "流浪乐章", "幽夜华尔兹"],
"from": "2021-06-09 06:00:00",
"name": "神铸赋形",
"to": "2021-06-29 17:59:59",
},
{
"five": ["松籁响起之时", "风鹰剑"],
"four": ["祭礼剑", "雨裁", "匣里灭辰", "祭礼残章", "弓藏"],
"from": "2021-05-18 18:00:00",
"name": "神铸赋形",
"to": "2021-06-08 14:59:59",
},
{
"five": ["斫峰之刃", "尘世之锁"],
"four": ["笛剑", "千岩古剑", "祭礼弓", "昭心", "千岩长枪"],
"from": "2021-04-28 06:00:00",
"name": "神铸赋形",
"to": "2021-05-18 17:59:59",
},
{
"five": ["天空之翼", "四风原典"],
"four": ["西风剑", "祭礼大剑", "暗巷猎手", "西风秘典", "西风长枪"],
"from": "2021-04-06 18:00:00",
"name": "神铸赋形",
"to": "2021-04-27 14:59:59",
},
{
"five": ["终末嗟叹之诗", "天空之刃"],
"four": ["暗巷闪光", "西风大剑", "西风猎弓", "暗巷的酒与诗", "匣里灭辰"],
"from": "2021-03-17 06:00:00",
"name": "神铸赋形",
"to": "2021-04-06 15:59:59",
},
{
"five": ["护摩之杖", "狼的末路"],
"four": ["匣里龙吟", "千岩古剑", "祭礼弓", "流浪乐章", "千岩长枪"],
"from": "2021-02-23 18:00:00",
"name": "神铸赋形",
"to": "2021-03-16 14:59:59",
},
{
"five": ["磐岩结绿", "和璞鸢"],
"four": ["笛剑", "祭礼大剑", "弓藏", "昭心", "西风长枪"],
"from": "2021-02-03 06:00:00",
"name": "神铸赋形",
"to": "2021-02-23 15:59:59",
},
{
"five": ["阿莫斯之弓", "天空之傲"],
"four": ["祭礼剑", "钟剑", "匣里灭辰", "昭心", "西风猎弓"],
"from": "2021-01-12 18:00:00",
"name": "神铸赋形",
"to": "2021-02-02 14:59:59",
},
{
"five": ["斫峰之刃", "天空之卷"],
"four": ["西风剑", "西风大剑", "西风长枪", "祭礼残章", "绝弦"],
"from": "2020-12-23 06:00:00",
"name": "神铸赋形",
"to": "2021-01-12 15:59:59",
},
{
"five": ["贯虹之槊", "无工之剑"],
"four": ["匣里龙吟", "钟剑", "西风秘典", "西风猎弓", "匣里灭辰"],
"from": "2020-12-01 18:00:00",
"name": "神铸赋形",
"to": "2020-12-22 14:59:59",
},
{
"five": ["天空之翼", "尘世之锁"],
"four": ["笛剑", "雨裁", "昭心", "弓藏", "西风长枪"],
"from": "2020-11-11 06:00:00",
"name": "神铸赋形",
"to": "2020-12-01 15:59:59",
},
{
"five": ["四风原典", "狼的末路"],
"four": ["祭礼剑", "祭礼大剑", "祭礼残章", "祭礼弓", "匣里灭辰"],
"from": "2020-10-20 18:00:00",
"name": "神铸赋形",
"to": "2020-11-10 14:59:59",
},
{
"five": ["风鹰剑", "阿莫斯之弓"],
"four": ["祭礼剑", "祭礼大剑", "祭礼残章", "祭礼弓", "匣里灭辰"],
"from": "2020-09-28 06:00:00",
"name": "神铸赋形",
"to": "2020-10-18 17:59:59",
},
]

3
metadata/pool/pool_5.py Normal file
View File

@ -0,0 +1,3 @@
POOL_5 = [
{"five": ["卓越搭档"], "four": [], "from": "2024-07-04 06:00:00", "name": "卓越搭档", "to": "2050-09-15 17:59:59"}
]

View File

@ -1,9 +0,0 @@
POOL_500 = [
{
"five": ["晨风之诗"],
"four": [],
"name": "晨风之诗",
"from": "2024-03-13 06:00:00",
"to": "2024-04-02 17:59:59",
},
]

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,12 @@
from simnet.models.genshin.wish import BannerType
from simnet.models.zzz.wish import ZZZBannerType
PAIMONMOE_VERSION = 3
UIGF_VERSION = "v3.0"
ZZZGF_VERSION = "v1.0"
GACHA_TYPE_LIST = {
BannerType.NOVICE: "新手祈愿",
BannerType.PERMANENT: "常驻祈愿",
BannerType.WEAPON: "武器祈愿",
BannerType.CHARACTER1: "角色祈愿",
BannerType.CHARACTER2: "角色祈愿",
BannerType.CHRONICLED: "集录祈愿",
ZZZBannerType.STANDARD: "常驻调频",
ZZZBannerType.CHARACTER: "代理人调频",
ZZZBannerType.WEAPON: "音擎调频",
ZZZBannerType.BANGBOO: "邦布调频",
}
GACHA_TYPE_LIST_REVERSE = {v: k for k, v in GACHA_TYPE_LIST.items()}

View File

@ -3,20 +3,17 @@ import contextlib
import datetime
import json
from concurrent.futures import ThreadPoolExecutor
from os import PathLike
from pathlib import Path
from typing import Dict, IO, List, Optional, Tuple, Union, TYPE_CHECKING
from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
import aiofiles
from openpyxl import load_workbook
from simnet import GenshinClient, Region
from simnet import ZZZClient, Region
from simnet.errors import AuthkeyTimeout, InvalidAuthkey
from simnet.models.genshin.wish import BannerType
from simnet.utils.player import recognize_genshin_server
from simnet.models.zzz.wish import ZZZBannerType
from simnet.utils.player import recognize_zzz_server
from metadata.pool.pool import get_pool_by_id
from metadata.shortname import roleToId, weaponToId
from modules.gacha_log.const import GACHA_TYPE_LIST, PAIMONMOE_VERSION
from modules.gacha_log.const import GACHA_TYPE_LIST
from modules.gacha_log.error import (
GachaLogAccountNotFound,
GachaLogAuthkeyTimeout,
@ -25,7 +22,6 @@ from modules.gacha_log.error import (
GachaLogInvalidAuthkey,
GachaLogMixedProvider,
GachaLogNotFound,
PaimonMoeGachaLogFileError,
)
from modules.gacha_log.models import (
FiveStarItem,
@ -33,12 +29,10 @@ from modules.gacha_log.models import (
GachaItem,
GachaLogInfo,
ImportType,
ItemType,
Pool,
UIGFGachaType,
UIGFInfo,
UIGFItem,
UIGFModel,
ZZZGFInfo,
ZZZGFItem,
ZZZGFModel,
)
from utils.const import PROJECT_ROOT
from utils.uid import mask_number
@ -47,7 +41,7 @@ if TYPE_CHECKING:
from core.dependence.assets import AssetsService
GACHA_LOG_PATH = PROJECT_ROOT.joinpath("data", "apihelper", "gacha_log")
GACHA_LOG_PATH = PROJECT_ROOT.joinpath("data", "apihelper", "signal_log")
GACHA_LOG_PATH.mkdir(parents=True, exist_ok=True)
@ -70,11 +64,11 @@ class GachaLog:
async def load_history_info(
self, user_id: str, uid: str, only_status: bool = False
) -> Tuple[Optional[GachaLogInfo], bool]:
"""读取历史抽卡记录数据
"""读取历史调频记录数据
:param user_id: 用户id
:param uid: 原神uid
:param only_status: 是否只读取状态
:return: 抽卡记录数据
:return: 调频记录数据
"""
file_path = self.gacha_log_path / f"{user_id}-{uid}.json"
if only_status:
@ -87,7 +81,7 @@ class GachaLog:
return GachaLogInfo(user_id=user_id, uid=uid, update_time=datetime.datetime.now()), False
async def remove_history_info(self, user_id: str, uid: str) -> bool:
"""删除历史抽卡记录数据
"""删除历史调频记录数据
:param user_id: 用户id
:param uid: 原神uid
:return: 是否删除成功
@ -125,10 +119,10 @@ class GachaLog:
return False
async def save_gacha_log_info(self, user_id: str, uid: str, info: GachaLogInfo):
"""保存抽卡记录数据
"""保存调频记录数据
:param user_id: 用户id
:param uid: 玩家uid
:param info: 抽卡记录数据
:param info: 调频记录数据
"""
save_path = self.gacha_log_path / f"{user_id}-{uid}.json"
save_path_bak = self.gacha_log_path / f"{user_id}-{uid}.json.bak"
@ -141,29 +135,31 @@ class GachaLog:
# 写入数据
await self.save_json(save_path, info.json())
async def gacha_log_to_uigf(self, user_id: str, uid: str) -> Optional[Path]:
"""抽卡日记转换为 UIGF 格式
async def gacha_log_to_zzzgf(self, user_id: str, uid: str) -> Optional[Path]:
"""调频日记转换为 ZZZGF 格式
:param user_id: 用户ID
:param uid: 游戏UID
:return: 转换是否成功转换信息UIGF文件目录
:return: 转换是否成功转换信息ZZZGF 文件目录
"""
data, state = await self.load_history_info(user_id, uid)
if not state:
raise GachaLogNotFound
save_path = self.gacha_log_path / f"{user_id}-{uid}-uigf.json"
info = UIGFModel(info=UIGFInfo(uid=uid, export_app=ImportType.PaiGram.value, export_app_version="v3"), list=[])
save_path = self.gacha_log_path / f"{user_id}-{uid}-zzzgf.json"
info = ZZZGFModel(
info=ZZZGFInfo(uid=uid, export_app=ImportType.PaiGram.value, export_app_version="v4"), list=[]
)
for items in data.item_list.values():
for item in items:
info.list.append(
UIGFItem(
ZZZGFItem(
id=item.id,
name=item.name,
gacha_id=item.gacha_id,
gacha_type=item.gacha_type,
item_id=roleToId(item.name) if item.item_type == "角色" else weaponToId(item.name),
item_id=item.item_id,
item_type=item.item_type,
rank_type=item.rank_type,
time=item.time.strftime("%Y-%m-%d %H:%M:%S"),
uigf_gacha_type=item.gacha_type if item.gacha_type != "400" else "301",
)
)
await self.save_json(save_path, json.loads(info.json()))
@ -178,11 +174,11 @@ class GachaLog:
if total > 50:
if total <= five_star * 15:
raise GachaLogFileError(
"检测到您将要导入的抽卡记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。"
"检测到您将要导入的调频记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。"
)
if four_star < five_star:
raise GachaLogFileError(
"检测到您将要导入的抽卡记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。"
"检测到您将要导入的调频记录中五星数量过多,可能是由于文件错误导致的,请检查后重新导入。"
)
return True
except Exception as exc: # pylint: disable=W0703
@ -192,7 +188,7 @@ class GachaLog:
def import_data_backend(all_items: List[GachaItem], gacha_log: GachaLogInfo, temp_id_data: Dict) -> int:
new_num = 0
for item_info in all_items:
pool_name = GACHA_TYPE_LIST[BannerType(int(item_info.gacha_type))]
pool_name = GACHA_TYPE_LIST[ZZZBannerType(int(item_info.gacha_type))]
if pool_name not in temp_id_data:
temp_id_data[pool_name] = []
if pool_name not in gacha_log.item_list:
@ -219,11 +215,6 @@ class GachaLog:
all_items = [GachaItem(**i) for i in data["list"]]
await self.verify_data(all_items)
gacha_log, status = await self.load_history_info(str(user_id), uid)
if import_type == ImportType.PAIMONMOE:
if status and gacha_log.get_import_type != ImportType.PAIMONMOE:
raise GachaLogMixedProvider
elif status and gacha_log.get_import_type == ImportType.PAIMONMOE:
raise GachaLogMixedProvider
# 将唯一 id 放入临时数据中,加快查找速度
temp_id_data = {
pool_name: [i.id for i in pool_data] for pool_name, pool_data in gacha_log.item_list.items()
@ -244,20 +235,21 @@ class GachaLog:
await self.save_gacha_log_info(str(user_id), uid, gacha_log)
return new_num
except GachaLogAccountNotFound as e:
raise GachaLogAccountNotFound("导入失败,文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同") from e
raise GachaLogAccountNotFound("导入失败,文件包含的调频记录所属 uid 与你当前绑定的 uid 不同") from e
except GachaLogMixedProvider as e:
raise GachaLogMixedProvider from e
except Exception as exc:
raise GachaLogException from exc
@staticmethod
def get_game_client(player_id: int) -> GenshinClient:
if recognize_genshin_server(player_id) in ["cn_gf01", "cn_qd01"]:
return GenshinClient(player_id=player_id, region=Region.CHINESE, lang="zh-cn")
return GenshinClient(player_id=player_id, region=Region.OVERSEAS, lang="zh-cn")
def get_game_client(player_id: int) -> ZZZClient:
if recognize_zzz_server(player_id) in ["prod_gf_cn"]:
return ZZZClient(player_id=player_id, region=Region.CHINESE, lang="zh-cn")
else:
return ZZZClient(player_id=player_id, region=Region.OVERSEAS, lang="zh-cn")
async def get_gacha_log_data(self, user_id: int, player_id: int, authkey: str) -> int:
"""使用authkey获取抽卡记录数据,并合并旧数据
"""使用authkey获取调频记录数据,并合并旧数据
:param user_id: 用户id
:param player_id: 玩家id
:param authkey: authkey
@ -265,10 +257,8 @@ class GachaLog:
"""
new_num = 0
gacha_log, _ = await self.load_history_info(str(user_id), str(player_id))
if gacha_log.get_import_type == ImportType.PAIMONMOE:
raise GachaLogMixedProvider
# 将唯一 id 放入临时数据中,加快查找速度
temp_id_data = {pool_name: [i.id for i in pool_data] for pool_name, pool_data in gacha_log.item_list.items()}
temp_id_data = {pool_name: {i.id: i for i in pool_data} for pool_name, pool_data in gacha_log.item_list.items()}
client = self.get_game_client(player_id)
try:
for pool_id, pool_name in GACHA_TYPE_LIST.items():
@ -277,7 +267,9 @@ class GachaLog:
item = GachaItem(
id=str(data.id),
name=data.name,
gacha_id=str(data.banner_id),
gacha_type=str(data.banner_type.value),
item_id=str(data.item_id),
item_type=data.type,
rank_type=str(data.rarity),
time=datetime.datetime(
@ -291,13 +283,17 @@ class GachaLog:
)
if pool_name not in temp_id_data:
temp_id_data[pool_name] = []
temp_id_data[pool_name] = {}
if pool_name not in gacha_log.item_list:
gacha_log.item_list[pool_name] = []
if item.id not in temp_id_data[pool_name]:
if item.id not in temp_id_data[pool_name].keys():
gacha_log.item_list[pool_name].append(item)
temp_id_data[pool_name].append(item.id)
temp_id_data[pool_name][item.id] = item
new_num += 1
else:
old_item: GachaItem = temp_id_data[pool_name][item.id]
old_item.gacha_id = item.gacha_id
old_item.item_id = item.item_id
except AuthkeyTimeout as exc:
raise GachaLogAuthkeyTimeout from exc
except InvalidAuthkey as exc:
@ -307,56 +303,64 @@ class GachaLog:
for i in gacha_log.item_list.values():
i.sort(key=lambda x: (x.time, x.id))
gacha_log.update_time = datetime.datetime.now()
gacha_log.import_type = ImportType.UIGF.value
gacha_log.import_type = ImportType.PaiGram.value
await self.save_gacha_log_info(str(user_id), str(player_id), gacha_log)
return new_num
@staticmethod
def check_avatar_up(name: str, gacha_time: datetime.datetime) -> bool:
if name in {"莫娜", "七七", "迪卢克", "", "迪希雅"}:
if name in {"莱卡恩", "猫又", "格莉丝", "丽娜", "「11号」", "珂蕾妲"}:
return False
if name == "刻晴":
start_time = datetime.datetime.strptime("2021-02-17 18:00:00", "%Y-%m-%d %H:%M:%S")
end_time = datetime.datetime.strptime("2021-03-02 15:59:59", "%Y-%m-%d %H:%M:%S")
if not start_time < gacha_time < end_time:
return False
elif name == "提纳里":
start_time = datetime.datetime.strptime("2022-08-24 06:00:00", "%Y-%m-%d %H:%M:%S")
end_time = datetime.datetime.strptime("2022-09-09 17:59:59", "%Y-%m-%d %H:%M:%S")
if not start_time < gacha_time < end_time:
return False
return True
async def get_all_5_star_items(self, data: List[GachaItem], assets: "AssetsService", pool_name: str = "角色祈愿"):
async def get_all_5_star_items(self, data: List[GachaItem], assets: "AssetsService", pool_name: str = "代理人调频"):
"""
获取所有5星角色
:param data: 抽卡记录
获取所有5星代理人
:param data: 调频记录
:param assets: 资源服务
:param pool_name: 池子名称
:return: 5角色列表
:return: 5星代理人列表
"""
count = 0
result = []
for item in data:
count += 1
if item.rank_type == "5":
if item.item_type == "角色" and pool_name in {"角色祈愿", "常驻祈愿", "新手祈愿", "集录祈愿"}:
if item.item_type == "代理人" and pool_name in {"代理人调频", "常驻调频"}:
if pool_name == "代理人调频":
isUp, isBig = (
self.check_avatar_up(item.name, item.time),
(not result[-1].isUp) if result else False,
)
else:
isUp, isBig = False, False
data = {
"name": item.name,
"icon": (await assets.avatar(roleToId(item.name)).icon()).as_uri(),
"icon": assets.avatar.normal(item.name).as_uri(),
"count": count,
"type": "角色",
"isUp": self.check_avatar_up(item.name, item.time) if pool_name == "角色祈愿" else False,
"isBig": (not result[-1].isUp) if result and pool_name == "角色祈愿" else False,
"type": "代理人",
"isUp": isUp,
"isBig": isBig,
"time": item.time,
}
result.append(FiveStarItem.construct(**data))
elif item.item_type == "武器" and pool_name in {"武器祈愿", "常驻祈愿", "新手祈愿", "集录祈愿"}:
elif item.item_type == "音擎" and pool_name in {"音擎调频", "常驻调频"}:
data = {
"name": item.name,
"icon": (await assets.weapon(weaponToId(item.name)).icon()).as_uri(),
"icon": assets.weapon.icon(item.name).as_uri(),
"count": count,
"type": "武器",
"type": "音擎",
"isUp": False,
"isBig": False,
"time": item.time,
}
result.append(FiveStarItem.construct(**data))
elif item.item_type == "邦布" and pool_name in {"邦布调频"}:
data = {
"name": item.name,
"icon": assets.buddy.icon(item.name).as_uri(),
"count": count,
"type": "邦布",
"isUp": False,
"isBig": False,
"time": item.time,
@ -370,7 +374,7 @@ class GachaLog:
async def get_all_4_star_items(data: List[GachaItem], assets: "AssetsService"):
"""
获取 no_fout_star
:param data: 抽卡记录
:param data: 调频记录
:param assets: 资源服务
:return: no_fout_star
"""
@ -379,21 +383,30 @@ class GachaLog:
for item in data:
count += 1
if item.rank_type == "4":
if item.item_type == "角色":
if item.item_type == "代理人":
data = {
"name": item.name,
"icon": (await assets.avatar(roleToId(item.name)).icon()).as_uri(),
"icon": assets.avatar.normal(item.name).as_uri(),
"count": count,
"type": "角色",
"type": "代理人",
"time": item.time,
}
result.append(FourStarItem.construct(**data))
elif item.item_type == "武器":
elif item.item_type == "音擎":
data = {
"name": item.name,
"icon": (await assets.weapon(weaponToId(item.name)).icon()).as_uri(),
"icon": assets.weapon.icon(item.name).as_uri(),
"count": count,
"type": "武器",
"type": "音擎",
"time": item.time,
}
result.append(FourStarItem.construct(**data))
elif item.item_type == "邦布":
data = {
"name": item.name,
"icon": assets.buddy.icon(item.name).as_uri(),
"count": count,
"type": "邦布",
"time": item.time,
}
result.append(FourStarItem.construct(**data))
@ -402,7 +415,7 @@ class GachaLog:
return result, count
@staticmethod
def get_301_pool_data(total: int, all_five: List[FiveStarItem], no_five_star: int, no_four_star: int):
def get_2_pool_data(total: int, all_five: List[FiveStarItem], no_five_star: int, no_four_star: int):
# 总共五星
five_star = len(all_five)
five_star_up = len([i for i in all_five if i.isUp])
@ -435,20 +448,20 @@ class GachaLog:
{"num": no_four_star, "unit": "", "lable": "未出四星"},
{"num": five_star_const, "unit": "", "lable": "五星常驻"},
{"num": up_avg, "unit": "", "lable": "UP平均"},
{"num": up_cost, "unit": "", "lable": "UP花费原石"},
{"num": up_cost, "unit": "", "lable": "UP花费星琼"},
],
]
@staticmethod
def get_200_pool_data(
def get_1_pool_data(
total: int, all_five: List[FiveStarItem], all_four: List[FourStarItem], no_five_star: int, no_four_star: int
):
# 总共五星
five_star = len(all_five)
# 五星平均
five_star_avg = round((total - no_five_star) / five_star, 2) if five_star != 0 else 0
# 五星武器
five_star_weapon = len([i for i in all_five if i.type == "武器"])
# 五星音擎
five_star_weapon = len([i for i in all_five if i.type == "音擎"])
# 总共四星
four_star = len(all_four)
# 四星平均
@ -462,7 +475,7 @@ class GachaLog:
{"num": no_five_star, "unit": "", "lable": "未出五星"},
{"num": five_star, "unit": "", "lable": "五星"},
{"num": five_star_avg, "unit": "", "lable": "五星平均"},
{"num": five_star_weapon, "unit": "", "lable": "五星武器"},
{"num": five_star_weapon, "unit": "", "lable": "五星音擎"},
{"num": no_four_star, "unit": "", "lable": "未出四星"},
{"num": four_star, "unit": "", "lable": "四星"},
{"num": four_star_avg, "unit": "", "lable": "四星平均"},
@ -471,15 +484,15 @@ class GachaLog:
]
@staticmethod
def get_302_pool_data(
def get_3_pool_data(
total: int, all_five: List[FiveStarItem], all_four: List[FourStarItem], no_five_star: int, no_four_star: int
):
# 总共五星
five_star = len(all_five)
# 五星平均
five_star_avg = round((total - no_five_star) / five_star, 2) if five_star != 0 else 0
# 四星武器
four_star_weapon = len([i for i in all_four if i.type == "武器"])
# 四星音擎
four_star_weapon = len([i for i in all_four if i.type == "音擎"])
# 总共四星
four_star = len(all_four)
# 四星平均
@ -493,38 +506,7 @@ class GachaLog:
{"num": no_five_star, "unit": "", "lable": "未出五星"},
{"num": five_star, "unit": "", "lable": "五星"},
{"num": five_star_avg, "unit": "", "lable": "五星平均"},
{"num": four_star_weapon, "unit": "", "lable": "四星武器"},
{"num": no_four_star, "unit": "", "lable": "未出四星"},
{"num": four_star, "unit": "", "lable": "四星"},
{"num": four_star_avg, "unit": "", "lable": "四星平均"},
{"num": four_star_max_count, "unit": four_star_max, "lable": "四星最多"},
],
]
@staticmethod
def get_500_pool_data(
total: int, all_five: List[FiveStarItem], all_four: List[FourStarItem], no_five_star: int, no_four_star: int
):
# 总共五星
five_star = len(all_five)
# 五星平均
five_star_avg = round((total - no_five_star) / five_star, 2) if five_star != 0 else 0
# 四星角色
four_star_character = len([i for i in all_four if i.type == "角色"])
# 总共四星
four_star = len(all_four)
# 四星平均
four_star_avg = round((total - no_four_star) / four_star, 2) if four_star != 0 else 0
# 四星最多
four_star_name_list = [i.name for i in all_four]
four_star_max = max(four_star_name_list, key=four_star_name_list.count) if four_star_name_list else ""
four_star_max_count = four_star_name_list.count(four_star_max)
return [
[
{"num": no_five_star, "unit": "", "lable": "未出五星"},
{"num": five_star, "unit": "", "lable": "五星"},
{"num": five_star_avg, "unit": "", "lable": "五星平均"},
{"num": four_star_character, "unit": "", "lable": "四星角色"},
{"num": four_star_weapon, "unit": "", "lable": "四星"},
{"num": no_four_star, "unit": "", "lable": "未出四星"},
{"num": four_star, "unit": "", "lable": "四星"},
{"num": four_star_avg, "unit": "", "lable": "四星平均"},
@ -535,7 +517,7 @@ class GachaLog:
@staticmethod
def count_fortune(pool_name: str, summon_data, weapon: bool = False):
"""
角色 武器
代理人 音擎
50以下 45以下
50-60 45-55
60-70 55-65
@ -557,9 +539,9 @@ class GachaLog:
return f"{pool_name} · 非"
return pool_name
async def get_analysis(self, user_id: int, player_id: int, pool: BannerType, assets: "AssetsService"):
async def get_analysis(self, user_id: int, player_id: int, pool: ZZZBannerType, assets: "AssetsService"):
"""
获取抽卡记录分析数据
获取调频记录分析数据
:param user_id: 用户id
:param player_id: 玩家id
:param pool: 池子类型
@ -579,17 +561,14 @@ class GachaLog:
all_five, no_five_star = await self.get_all_5_star_items(data, assets, pool_name)
all_four, no_four_star = await self.get_all_4_star_items(data, assets)
summon_data = None
if pool in [BannerType.CHARACTER1, BannerType.CHARACTER2, BannerType.NOVICE]:
summon_data = self.get_301_pool_data(total, all_five, no_five_star, no_four_star)
if pool == ZZZBannerType.CHARACTER:
summon_data = self.get_2_pool_data(total, all_five, no_five_star, no_four_star)
pool_name = self.count_fortune(pool_name, summon_data)
elif pool == BannerType.WEAPON:
summon_data = self.get_302_pool_data(total, all_five, all_four, no_five_star, no_four_star)
elif pool in [ZZZBannerType.WEAPON, ZZZBannerType.BANGBOO]:
summon_data = self.get_3_pool_data(total, all_five, all_four, no_five_star, no_four_star)
pool_name = self.count_fortune(pool_name, summon_data, True)
elif pool == BannerType.PERMANENT:
summon_data = self.get_200_pool_data(total, all_five, all_four, no_five_star, no_four_star)
pool_name = self.count_fortune(pool_name, summon_data)
elif pool == BannerType.CHRONICLED:
summon_data = self.get_500_pool_data(total, all_five, all_four, no_five_star, no_four_star)
elif pool == ZZZBannerType.STANDARD:
summon_data = self.get_1_pool_data(total, all_five, all_four, no_five_star, no_four_star)
pool_name = self.count_fortune(pool_name, summon_data)
last_time = data[0].time.strftime("%Y-%m-%d %H:%M")
first_time = data[-1].time.strftime("%Y-%m-%d %H:%M")
@ -606,9 +585,9 @@ class GachaLog:
}
async def get_pool_analysis(
self, user_id: int, player_id: int, pool: BannerType, assets: "AssetsService", group: bool
self, user_id: int, player_id: int, pool: ZZZBannerType, assets: "AssetsService", group: bool
) -> dict:
"""获取抽卡记录分析数据
"""获取调频记录分析数据
:param user_id: 用户id
:param player_id: 玩家id
:param pool: 池子类型
@ -648,14 +627,14 @@ class GachaLog:
)
pool_data = [i for i in pool_data if i["count"] > 0]
return {
"uid": player_id,
"uid": mask_number(player_id),
"typeName": pool_name,
"pool": pool_data[:6] if group else pool_data,
"hasMore": len(pool_data) > 6,
}
async def get_all_five_analysis(self, user_id: int, player_id: int, assets: "AssetsService") -> dict:
"""获取五星抽卡记录分析数据
"""获取五星调频记录分析数据
:param user_id: 用户id
:param player_id: 玩家id
:param assets: 资源服务
@ -689,133 +668,8 @@ class GachaLog:
for up_pool in pools
]
return {
"uid": player_id,
"uid": mask_number(player_id),
"typeName": "五星列表",
"pool": pool_data,
"hasMore": False,
}
@staticmethod
def convert_xlsx_to_uigf(file: Union[str, PathLike, IO[bytes]], zh_dict: Dict) -> Dict:
"""转换 paimone.moe 或 非小酋 导出 xlsx 数据为 UIGF 格式
:param file: 导出的 xlsx 文件
:param zh_dict:
:return: UIGF 格式数据
"""
def from_paimon_moe(
uigf_gacha_type: UIGFGachaType, item_type: str, name: str, date_string: str, rank_type: int, _id: int
) -> UIGFItem:
item_type = ItemType.CHARACTER if item_type == "Character" else ItemType.WEAPON
return UIGFItem(
id=str(_id),
name=zh_dict[name],
gacha_type=uigf_gacha_type,
item_type=item_type,
rank_type=str(rank_type),
time=date_string,
uigf_gacha_type=uigf_gacha_type,
)
def from_uigf(
uigf_gacha_type: str,
gacha__type: str,
item_type: str,
name: str,
date_string: str,
rank_type: str,
_id: str,
) -> UIGFItem:
return UIGFItem(
id=_id,
name=name,
gacha_type=gacha__type,
item_type=item_type,
rank_type=rank_type,
time=date_string,
uigf_gacha_type=uigf_gacha_type,
)
def from_fxq(
uigf_gacha_type: UIGFGachaType, item_type: str, name: str, date_string: str, rank_type: int, _id: int
) -> UIGFItem:
item_type = ItemType.CHARACTER if item_type == "角色" else ItemType.WEAPON
return UIGFItem(
id=str(_id),
name=name,
gacha_type=uigf_gacha_type,
item_type=item_type,
rank_type=str(rank_type),
time=date_string,
uigf_gacha_type=uigf_gacha_type,
)
wb = load_workbook(file)
wb_len = len(wb.worksheets)
if wb_len == 6:
import_type = ImportType.PAIMONMOE
elif wb_len == 5:
import_type = ImportType.UIGF
elif wb_len == 4:
import_type = ImportType.FXQ
else:
raise GachaLogFileError("xlsx 格式错误")
paimonmoe_sheets = {
UIGFGachaType.BEGINNER: "Beginners' Wish",
UIGFGachaType.STANDARD: "Standard",
UIGFGachaType.CHARACTER: "Character Event",
UIGFGachaType.WEAPON: "Weapon Event",
}
fxq_sheets = {
UIGFGachaType.BEGINNER: "新手祈愿",
UIGFGachaType.STANDARD: "常驻祈愿",
UIGFGachaType.CHARACTER: "角色活动祈愿",
UIGFGachaType.WEAPON: "武器活动祈愿",
}
data = UIGFModel(info=UIGFInfo(export_app=import_type.value), list=[])
if import_type == ImportType.PAIMONMOE:
ws = wb["Information"]
if ws["B2"].value != PAIMONMOE_VERSION:
raise PaimonMoeGachaLogFileError(file_version=ws["B2"].value, support_version=PAIMONMOE_VERSION)
count = 1
for gacha_type in paimonmoe_sheets:
ws = wb[paimonmoe_sheets[gacha_type]]
for row in ws.iter_rows(min_row=2, values_only=True):
if row[0] is None:
break
data.list.append(from_paimon_moe(gacha_type, row[0], row[1], row[2], row[3], count))
count += 1
elif import_type == ImportType.UIGF:
ws = wb["原始数据"]
type_map = {}
count = 0
for row in ws["1"]:
if row.value is None:
break
type_map[row.value] = count
count += 1
for row in ws.iter_rows(min_row=2, values_only=True):
if row[0] is None:
break
data.list.append(
from_uigf(
row[type_map["uigf_gacha_type"]],
row[type_map["gacha_type"]],
row[type_map["item_type"]],
row[type_map["name"]],
row[type_map["time"]],
row[type_map["rank_type"]],
row[type_map["id"]],
)
)
else:
for gacha_type in fxq_sheets:
ws = wb[fxq_sheets[gacha_type]]
for row in ws.iter_rows(min_row=2, values_only=True):
if row[0] is None:
break
data.list.append(from_fxq(gacha_type, row[2], row[1], row[0], row[3], row[6]))
return json.loads(data.json())

View File

@ -4,15 +4,13 @@ from typing import Any, Dict, List, Union
from pydantic import BaseModel, validator
from metadata.shortname import not_real_roles, roleToId, weaponToId
from modules.gacha_log.const import UIGF_VERSION
from metadata.shortname import not_real_roles, roleToId, weaponToId, buddyToId
from modules.gacha_log.const import ZZZGF_VERSION
class ImportType(Enum):
PaiGram = "PaiGram"
PAIMONMOE = "PAIMONMOE"
FXQ = "FXQ"
UIGF = "UIGF"
ZZZGF = "ZZZGF"
UNKNOWN = "UNKNOWN"
@ -37,27 +35,29 @@ class FourStarItem(BaseModel):
class GachaItem(BaseModel):
id: str
name: str
gacha_id: str = ""
gacha_type: str
item_id: str = ""
item_type: str
rank_type: str
time: datetime.datetime
@validator("name")
def name_validator(cls, v):
if item_id := (roleToId(v) or weaponToId(v)):
if item_id := (roleToId(v) or weaponToId(v) or buddyToId(v)):
if item_id not in not_real_roles:
return v
raise ValueError(f"Invalid name {v}")
@validator("gacha_type")
def check_gacha_type(cls, v):
if v not in {"100", "200", "301", "302", "400", "500"}:
raise ValueError(f"gacha_type must be 200, 301, 302, 400, 500, invalid value: {v}")
if v not in {"1", "2", "3", "5"}:
raise ValueError(f"gacha_type must be 1, 2, 3 or 5, invalid value: {v}")
return v
@validator("item_type")
def check_item_type(cls, item):
if item not in {"角色", "武器"}:
if item not in {"代理人", "音擎", "邦布"}:
raise ValueError(f"error item type {item}")
return item
@ -74,11 +74,10 @@ class GachaLogInfo(BaseModel):
update_time: datetime.datetime
import_type: str = ""
item_list: Dict[str, List[GachaItem]] = {
"角色祈愿": [],
"武器祈愿": [],
"常驻祈愿": [],
"新手祈愿": [],
"集录祈愿": [],
"代理人调频": [],
"音擎调频": [],
"常驻调频": [],
"邦布调频": [],
}
@property
@ -131,57 +130,47 @@ class Pool:
class ItemType(Enum):
CHARACTER = "角色"
WEAPON = "武器"
CHARACTER = "代理人"
WEAPON = "音擎"
BANGBOO = "邦布"
class UIGFGachaType(Enum):
BEGINNER = "100"
STANDARD = "200"
CHARACTER = "301"
WEAPON = "302"
CHARACTER2 = "400"
CHRONICLED = "500"
class ZZZGFGachaType(Enum):
STANDARD = "1"
CHARACTER = "2"
WEAPON = "3"
BANGBOO = "5"
class UIGFItem(BaseModel):
class ZZZGFItem(BaseModel):
id: str
name: str
count: str = "1"
gacha_type: UIGFGachaType
gacha_id: str = ""
gacha_type: ZZZGFGachaType
item_id: str = ""
item_type: ItemType
rank_type: str
time: str
uigf_gacha_type: UIGFGachaType
class UIGFInfo(BaseModel):
class ZZZGFInfo(BaseModel):
uid: str = "0"
lang: str = "zh-cn"
region_time_zone: int = 8
export_time: str = ""
export_timestamp: int = 0
export_app: str = ""
export_app_version: str = ""
uigf_version: str = UIGF_VERSION
region_time_zone: int = 8
zzzgf_version: str = ZZZGF_VERSION
def __init__(self, **data: Any):
data["region_time_zone"] = data.get("region_time_zone", UIGFInfo.get_region_time_zone(data.get("uid", "0")))
super().__init__(**data)
if not self.export_time:
self.export_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.export_timestamp = int(datetime.datetime.now().timestamp())
@staticmethod
def get_region_time_zone(uid: str) -> int:
if uid.startswith("6"):
return -5
if uid.startswith("7"):
return 1
return 8
class UIGFModel(BaseModel):
info: UIGFInfo
list: List[UIGFItem]
class ZZZGFModel(BaseModel):
info: ZZZGFInfo
list: List[ZZZGFItem]

View File

@ -1,254 +1,39 @@
import asyncio
import re
from abc import abstractmethod
from asyncio import Queue
from multiprocessing import Value
from ssl import SSLZeroReturnError
from typing import AsyncIterator, ClassVar, List, Optional, Tuple, Union
from pathlib import Path
from typing import List, Dict
import anyio
from bs4 import BeautifulSoup
from httpx import URL, AsyncClient, HTTPError, Response
from pydantic import BaseConfig as PydanticBaseConfig
from pydantic import BaseModel as PydanticBaseModel
from utils.log import logger
try:
import ujson as jsonlib
except ImportError:
import json as jsonlib
__all__ = ["Model", "WikiModel", "HONEY_HOST"]
HONEY_HOST = URL("https://gensh.honeyhunterworld.com/")
import aiofiles
import ujson as jsonlib
from httpx import AsyncClient
class Model(PydanticBaseModel):
"""基类"""
class WikiModel:
BASE_URL = "https://zzz-res.paimon.vip/data/"
BASE_PATH = Path("data/wiki-zzz")
BASE_PATH.mkdir(parents=True, exist_ok=True)
def __new__(cls, *args, **kwargs):
# 让每次new的时候都解析
cls.update_forward_refs()
return super(Model, cls).__new__(cls) # pylint: disable=E1120
def __init__(self):
self.client = AsyncClient(timeout=120.0)
class Config(PydanticBaseConfig):
# 使用 ujson 作为解析库
json_dumps = jsonlib.dumps
json_loads = jsonlib.loads
class WikiModel(Model):
# noinspection PyUnresolvedReferences
"""wiki所用到的基类
Attributes:
id (:obj:`int`): ID
name (:obj:`str`): 名称
rarity (:obj:`int`): 星级
_client (:class:`httpx.AsyncClient`): 发起 http 请求的 client
"""
_client: ClassVar[AsyncClient] = AsyncClient()
id: str
name: str
rarity: int
async def remote_get(self, url: str):
return await self.client.get(url)
@staticmethod
@abstractmethod
def scrape_urls() -> List[URL]:
"""爬取的目标网页集合
例如有关武器的页面有:
[单手剑](https://genshin.honeyhunterworld.com/fam_sword/?lang=CHS)
[双手剑](https://genshin.honeyhunterworld.com/fam_claymore/?lang=CHS)
[长柄武器](https://genshin.honeyhunterworld.com/fam_polearm/?lang=CHS)
这个函数就是返回这些页面的网址所组成的 List
"""
@classmethod
async def _client_get(cls, url: Union[URL, str], retry_times: int = 5, sleep: float = 1) -> Response:
"""用自己的 client 发起 get 请求的快捷函数
Args:
url: 发起请求的 url
retry_times: 发生错误时的重复次数不能小于 0 .
sleep: 发生错误后等待重试的时间单位为秒
Returns:
返回对应的请求
Raises:
请求所需要的异常
"""
for _ in range(retry_times):
try:
return await cls._client.get(url, follow_redirects=True)
except (HTTPError, SSLZeroReturnError):
await anyio.sleep(sleep)
return await cls._client.get(url, follow_redirects=True) # 防止 retry_times 等于 0 的时候无法发生请求
@classmethod
@abstractmethod
async def _parse_soup(cls, soup: BeautifulSoup) -> "WikiModel":
"""解析 soup 生成对应 WikiModel
Args:
soup: 需要解析的 soup
Returns:
返回对应的 WikiModel
"""
@classmethod
async def _scrape(cls, url: Union[URL, str]) -> "WikiModel":
"""从 url 中爬取数据,并返回对应的 Model
Args:
url: 目标 url. 可以为字符串 str , 也可以为 httpx.URL
Returns:
返回对应的 WikiModel
"""
response = await cls._client_get(url)
return await cls._parse_soup(BeautifulSoup(response.text, "lxml"))
@classmethod
async def get_by_id(cls, id_: str) -> "WikiModel":
"""通过ID获取Model
Args:
id_: 目标 ID
Returns:
返回对应的 WikiModel
"""
return await cls._scrape(await cls.get_url_by_id(id_))
@classmethod
async def get_by_name(cls, name: str) -> Optional["WikiModel"]:
"""通过名称获取Model
Args:
name: 目标名
Returns:
返回对应的 WikiModel
"""
url = await cls.get_url_by_name(name)
return None if url is None else await cls._scrape(url)
@classmethod
async def get_full_data(cls) -> List["WikiModel"]:
"""获取全部数据的 Model
Returns:
返回能爬到的所有的 Model 所组成的 List
"""
return [i async for i in cls.full_data_generator()]
@classmethod
async def full_data_generator(cls) -> AsyncIterator["WikiModel"]:
"""Model 生成器
这是一个异步生成器该函数在使用时会爬取所有数据并将其转为对应的 Model然后存至一个队列中
当有需要时再一个一个地迭代取出
Returns:
返回能爬到的所有的 WikiModel 所组成的 List
"""
queue: Queue["WikiModel"] = Queue() # 存放 Model 的队列
signal = Value("i", 0) # 一个用于异步任务同步的信号
async def task(u):
# 包装的爬虫任务
try:
await queue.put(await cls._scrape(u)) # 爬取一条数据,并将其放入队列中
except Exception as exc: # pylint: disable=W0703
logger.error("爬取数据出现异常 %s", str(exc))
logger.debug("异常信息", exc_info=exc)
finally:
signal.value -= 1 # 信号量减少 1 ,说明该爬虫任务已经完成
for _, url in await cls.get_name_list(with_url=True): # 遍历爬取所有需要爬取的页面
signal.value += 1 # 信号量增加 1 ,说明有一个爬虫任务被添加
asyncio.create_task(task(url)) # 创建一个爬虫任务
while signal.value > 0 or not queue.empty(): # 当还有未完成的爬虫任务或存放数据的队列不为空时
yield await queue.get() # 取出并返回一个存放的 Model
def __str__(self) -> str:
return f"<{self.__class__.__name__} {super(WikiModel, self).__str__()}>"
def __repr__(self) -> str:
return self.__str__()
async def dump(datas, path: Path):
async with aiofiles.open(path, "w", encoding="utf-8") as f:
await f.write(jsonlib.dumps(datas, indent=4, ensure_ascii=False))
@staticmethod
async def get_url_by_id(id_: str) -> URL:
"""根据 id 获取对应的 url
async def read(path: Path) -> List[Dict]:
async with aiofiles.open(path, "r", encoding="utf-8") as f:
datas = jsonlib.loads(await f.read())
return datas
例如神里绫华的ID为 ayaka_002对应的数据页url为 https://genshin.honeyhunterworld.com/ayaka_002/?lang=CHS
@staticmethod
async def save_file(data, path: Path):
async with aiofiles.open(path, "wb") as f:
await f.write(data)
Args:
id_ : 实列ID
Returns:
返回对应的 url
"""
return HONEY_HOST.join(f"{id_}/?lang=CHS")
@classmethod
async def _name_list_generator(cls, *, with_url: bool = False) -> AsyncIterator[Union[str, Tuple[str, URL]]]:
"""一个 Model 的名称 和 其对应 url 的异步生成器
Args:
with_url: 是否返回相应的 url
Returns:
返回对应的名称列表 或者 名称与url 的列表
"""
urls = cls.scrape_urls()
queue: Queue[Union[str, Tuple[str, URL]]] = Queue() # 存放 Model 的队列
signal = Value("i", len(urls)) # 一个用于异步任务同步的信号,初始值为存放所需要爬取的页面数
async def task(page: URL):
"""包装的爬虫任务"""
response = await cls._client_get(page)
# 从页面中获取对应的 chaos data (未处理的json格式字符串)
chaos_data = re.findall(r"sortable_data\.push\((.*?)\);\s*sortable_cur_page", response.text)[0]
json_data = jsonlib.loads(chaos_data) # 转为 json
for data in json_data: # 遍历 json
data_name = re.findall(r">(.*)<", data[1])[0].strip() # 获取 Model 的名称
if with_url: # 如果需要返回对应的 url
data_url = HONEY_HOST.join(re.findall(r"\"(.*?)\"", data[0])[0])
await queue.put((data_name, data_url))
else:
await queue.put(data_name)
signal.value = signal.value - 1 # 信号量减少 1 ,说明该爬虫任务已经完成
for url in urls: # 遍历需要爬出的页面
asyncio.create_task(task(url)) # 添加爬虫任务
while signal.value > 0 or not queue.empty(): # 当还有未完成的爬虫任务或存放数据的队列不为空时
yield await queue.get() # 取出并返回一个存放的 Model
@classmethod
async def get_name_list(cls, *, with_url: bool = False) -> List[Union[str, Tuple[str, URL]]]:
"""获取全部 Model 的 名称
Returns:
返回能爬到的所有的 Model 的名称所组成的 List
"""
return [i async for i in cls._name_list_generator(with_url=with_url)]
@classmethod
async def get_url_by_name(cls, name: str) -> Optional[URL]:
"""通过 Model 的名称获取对应的 url
Args:
name: 实列名
Returns:
若有对应的实列则返回对应的 url; 若没有, 则返回 None
"""
async for n, url in cls._name_list_generator(with_url=True):
if name == n:
return url
@property
@abstractmethod
def icon(self):
"""返回此 Model 的图标链接"""
@staticmethod
async def read_file(path: Path):
async with aiofiles.open(path, "rb") as f:
return await f.read()

46
modules/wiki/buddy.py Normal file
View File

@ -0,0 +1,46 @@
from typing import List, Dict, Optional
from modules.wiki.base import WikiModel
from modules.wiki.models.buddy import Buddy as BuddyModel
class Buddy(WikiModel):
buddy_url = WikiModel.BASE_URL + "buddy.json"
buddy_path = WikiModel.BASE_PATH / "buddy.json"
def __init__(self):
super().__init__()
self.all_buddys: List[BuddyModel] = []
self.all_buddys_map: Dict[int, BuddyModel] = {}
self.all_buddys_name: Dict[str, BuddyModel] = {}
def clear_class_data(self) -> None:
self.all_buddys.clear()
self.all_buddys_map.clear()
self.all_buddys_name.clear()
async def refresh(self):
datas = await self.remote_get(self.buddy_url)
await self.dump(datas.json(), self.buddy_path)
await self.read()
async def read(self):
if not self.buddy_path.exists():
await self.refresh()
return
datas = await WikiModel.read(self.buddy_path)
self.clear_class_data()
for data in datas:
m = BuddyModel(**data)
self.all_buddys.append(m)
self.all_buddys_map[m.id] = m
self.all_buddys_name[m.name] = m
def get_by_id(self, cid: int) -> Optional[BuddyModel]:
return self.all_buddys_map.get(cid)
def get_by_name(self, name: str) -> Optional[BuddyModel]:
return self.all_buddys_name.get(name)
def get_name_list(self) -> List[str]:
return list(self.all_buddys_name.keys())

View File

@ -1,199 +1,46 @@
import re
from typing import List, Optional
from typing import List, Dict, Optional
from bs4 import BeautifulSoup
from httpx import URL
from modules.wiki.base import HONEY_HOST, Model, WikiModel
from modules.wiki.other import Association, Element, WeaponType
class Birth(Model):
"""生日
Attributes:
day:
month:
"""
day: int
month: int
class CharacterAscension(Model):
"""角色的突破材料
Attributes:
level: 等级突破材料
skill: 技能/天赋培养材料
"""
level: List[str] = []
skill: List[str] = []
class CharacterState(Model):
"""角色属性值
Attributes:
level: 等级
HP: 生命
ATK: 攻击力
DEF: 防御力
CR: 暴击率
CD: 暴击伤害
bonus: 突破属性
"""
level: str
HP: int
ATK: float
DEF: float
CR: str
CD: str
bonus: str
class CharacterIcon(Model):
icon: str
side: str
gacha: str
splash: Optional[str]
from modules.wiki.base import WikiModel
from modules.wiki.models.avatar import Avatar
class Character(WikiModel):
"""角色
Attributes:
title: 称号
occupation: 所属
association: 地区
weapon_type: 武器类型
element: 元素
birth: 生日
constellation: 命之座
cn_cv: 中配
jp_cv: 日配
en_cv: 英配
kr_cv: 韩配
description: 描述
"""
avatar_url = WikiModel.BASE_URL + "avatars.json"
avatar_path = WikiModel.BASE_PATH / "avatars.json"
id: str
title: str
occupation: str
association: Association
weapon_type: WeaponType
element: Element
birth: Optional[Birth]
constellation: str
cn_cv: str
jp_cv: str
en_cv: str
kr_cv: str
description: str
ascension: CharacterAscension
def __init__(self):
super().__init__()
self.all_avatars: List[Avatar] = []
self.all_avatars_map: Dict[int, Avatar] = {}
self.all_avatars_name: Dict[str, Avatar] = {}
stats: List[CharacterState]
def clear_class_data(self) -> None:
self.all_avatars.clear()
self.all_avatars_map.clear()
self.all_avatars_name.clear()
@classmethod
def scrape_urls(cls) -> List[URL]:
return [HONEY_HOST.join("fam_chars/?lang=CHS")]
async def refresh(self):
datas = await self.remote_get(self.avatar_url)
await self.dump(datas.json(), self.avatar_path)
await self.read()
@classmethod
async def _parse_soup(cls, soup: BeautifulSoup) -> "Character":
"""解析角色页"""
soup = soup.select(".wp-block-post-content")[0]
tables = soup.find_all("table")
table_rows = tables[0].find_all("tr")
async def read(self):
if not self.avatar_path.exists():
await self.refresh()
return
datas = await WikiModel.read(self.avatar_path)
self.clear_class_data()
for data in datas:
m = Avatar(**data)
self.all_avatars.append(m)
self.all_avatars_map[m.id] = m
self.all_avatars_name[m.name] = m
def get_table_text(row_num: int) -> str:
"""一个快捷函数,用于返回表格对应行的最后一个单元格中的文本"""
return table_rows[row_num].find_all("td")[-1].text.replace("\xa0", "")
def get_by_id(self, cid: int) -> Optional[Avatar]:
return self.all_avatars_map.get(cid)
id_ = re.findall(r"img/(.*?_\d+)_.*", table_rows[0].find("img").attrs["src"])[0]
name = get_table_text(0)
if name != "旅行者": # 如果角色名不是 旅行者
title = get_table_text(1)
occupation = get_table_text(2)
association = Association.convert(get_table_text(3).lower().title())
rarity = len(table_rows[4].find_all("img"))
weapon_type = WeaponType[get_table_text(5)]
element = Element[get_table_text(6)]
birth = Birth(day=int(get_table_text(7)), month=int(get_table_text(8)))
constellation = get_table_text(10)
cn_cv = get_table_text(11)
jp_cv = get_table_text(12)
en_cv = get_table_text(13)
kr_cv = get_table_text(14)
else:
name = "" if id_.endswith("5") else ""
title = get_table_text(0)
occupation = get_table_text(1)
association = Association.convert(get_table_text(2).lower().title())
rarity = len(table_rows[3].find_all("img"))
weapon_type = WeaponType[get_table_text(4)]
element = Element[get_table_text(5)]
birth = None
constellation = get_table_text(7)
cn_cv = get_table_text(8)
jp_cv = get_table_text(9)
en_cv = get_table_text(10)
kr_cv = get_table_text(11)
description = get_table_text(-3)
ascension = CharacterAscension(
level=[
target[0]
for i in table_rows[-2].find_all("a")
if (target := re.findall(r"/(.*)/", i.attrs["href"])) # 过滤掉错误的材料(honey网页的bug)
],
skill=[re.findall(r"/(.*)/", i.attrs["href"])[0] for i in table_rows[-1].find_all("a")],
)
stats = []
for row in tables[2].find_all("tr")[1:]:
cells = row.find_all("td")
stats.append(
CharacterState(
level=cells[0].text,
HP=cells[1].text,
ATK=cells[2].text,
DEF=cells[3].text,
CR=cells[4].text,
CD=cells[5].text,
bonus=cells[6].text,
)
)
return Character(
id=id_,
name=name,
title=title,
occupation=occupation,
association=association,
weapon_type=weapon_type,
element=element,
birth=birth,
constellation=constellation,
cn_cv=cn_cv,
jp_cv=jp_cv,
rarity=rarity,
en_cv=en_cv,
kr_cv=kr_cv,
description=description,
ascension=ascension,
stats=stats,
)
def get_by_name(self, name: str) -> Optional[Avatar]:
return self.all_avatars_name.get(name)
@classmethod
async def get_url_by_name(cls, name: str) -> Optional[URL]:
# 重写此函数的目的是处理主角名字的 ID
_map = {"": "playergirl_007", "": "playerboy_005"}
if (id_ := _map.get(name)) is not None:
return await cls.get_url_by_id(id_)
return await super(Character, cls).get_url_by_name(name)
@property
def icon(self) -> CharacterIcon:
return CharacterIcon(
icon=str(HONEY_HOST.join(f"/img/{self.id}_icon.webp")),
side=str(HONEY_HOST.join(f"/img/{self.id}_side_icon.webp")),
gacha=str(HONEY_HOST.join(f"/img/{self.id}_gacha_card.webp")),
splash=str(HONEY_HOST.join(f"/img/{self.id}_gacha_splash.webp")),
)
def get_name_list(self) -> List[str]:
return list(self.all_avatars_name.keys())

View File

@ -0,0 +1,46 @@
from typing import List, Dict, Optional
from modules.wiki.base import WikiModel
from modules.wiki.models.equipment_suit import EquipmentSuit as EquipmentSuitModel
class EquipmentSuit(WikiModel):
equipment_suit_url = WikiModel.BASE_URL + "equipment_suits.json"
equipment_suit_path = WikiModel.BASE_PATH / "equipment_suits.json"
def __init__(self):
super().__init__()
self.all_equipment_suits: List[EquipmentSuitModel] = []
self.all_equipment_suits_map: Dict[int, EquipmentSuitModel] = {}
self.all_equipment_suits_name: Dict[str, EquipmentSuitModel] = {}
def clear_class_data(self) -> None:
self.all_equipment_suits.clear()
self.all_equipment_suits_map.clear()
self.all_equipment_suits_name.clear()
async def refresh(self):
datas = await self.remote_get(self.equipment_suit_url)
await self.dump(datas.json(), self.equipment_suit_path)
await self.read()
async def read(self):
if not self.equipment_suit_path.exists():
await self.refresh()
return
datas = await WikiModel.read(self.equipment_suit_path)
self.clear_class_data()
for data in datas:
m = EquipmentSuitModel(**data)
self.all_equipment_suits.append(m)
self.all_equipment_suits_map[m.id] = m
self.all_equipment_suits_name[m.name] = m
def get_by_id(self, cid: int) -> Optional[EquipmentSuitModel]:
return self.all_equipment_suits_map.get(cid)
def get_by_name(self, name: str) -> Optional[EquipmentSuitModel]:
return self.all_equipment_suits_name.get(name)
def get_name_list(self) -> List[str]:
return list(self.all_equipment_suits_name.keys())

View File

@ -1,81 +0,0 @@
import re
from typing import List, Optional, Tuple, Union
from bs4 import BeautifulSoup
from httpx import URL
from modules.wiki.base import HONEY_HOST, WikiModel
__all__ = ["Material"]
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
class Material(WikiModel):
# noinspection PyUnresolvedReferences
"""武器、角色培养素材
Attributes:
type: 类型
weekdays: 每周开放的时间
source: 获取方式
description: 描述
"""
type: str
source: Optional[List[str]] = None
weekdays: Optional[List[int]] = None
description: str
@staticmethod
def scrape_urls() -> List[URL]:
weapon = [HONEY_HOST.join(f"fam_wep_{i}/?lang=CHS") for i in ["primary", "secondary", "common"]]
talent = [HONEY_HOST.join(f"fam_talent_{i}/?lang=CHS") for i in ["book", "boss", "common", "reward"]]
return weapon + talent
@classmethod
async def get_name_list(cls, *, with_url: bool = False) -> List[Union[str, Tuple[str, URL]]]:
return list(sorted(set(await super(Material, cls).get_name_list(with_url=with_url)), key=lambda x: x[0]))
@classmethod
async def _parse_soup(cls, soup: BeautifulSoup) -> "Material":
"""解析突破素材页"""
soup = soup.select(".wp-block-post-content")[0]
tables = soup.find_all("table")
table_rows = tables[0].find_all("tr")
def get_table_row(target: str):
"""一个便捷函数,用于返回对应表格头的对应行的最后一个单元格中的文本"""
for row in table_rows:
if target in row.find("td").text:
return row.find_all("td")[-1]
return None
def get_table_text(row_num: int) -> str:
"""一个便捷函数,用于返回表格对应行的最后一个单元格中的文本"""
return table_rows[row_num].find_all("td")[-1].text.replace("\xa0", "")
id_ = re.findall(r"/img/(.*?)\.webp", str(table_rows[0]))[0]
name = get_table_text(0)
rarity = len(table_rows[3].find_all("img"))
type_ = get_table_text(1)
if (item_source := get_table_row("Item Source")) is not None:
item_source = list(
# filter 在这里的作用是过滤掉为空的数据
filter(lambda x: x, item_source.encode_contents().decode().split("<br/>"))
)
if (alter_source := get_table_row("Alternative Item")) is not None:
alter_source = list(
# filter 在这里的作用是过滤掉为空的数据
filter(lambda x: x, alter_source.encode_contents().decode().split("<br/>"))
)
source = list(sorted(set((item_source or []) + (alter_source or []))))
if (weekdays := get_table_row("Weekday")) is not None:
weekdays = [*(WEEKDAYS.index(weekdays.text.replace("\xa0", "").split(",")[0]) + 3 * i for i in range(2)), 6]
description = get_table_text(-1)
return Material(
id=id_, name=name, rarity=rarity, type=type_, description=description, source=source, weekdays=weekdays
)
@property
def icon(self) -> str:
return str(HONEY_HOST.join(f"/img/{self.id}.webp"))

View File

View File

@ -0,0 +1,38 @@
from typing import List
from pydantic import BaseModel
from .enums import ZZZElementType, ZZZSpeciality, ZZZRank
class Avatar(BaseModel, frozen=False):
id: int
""" 角色ID """
name: str
""" 中文名称 """
name_en: str
""" 英文名称 """
name_full: str
""" 中文全称 """
name_short: str
""" 英文简称 """
rank: ZZZRank = ZZZRank.NULL
""" 星级 """
element: ZZZElementType
""" 元素 """
speciality: ZZZSpeciality
""" 特性 """
icon: List[str] = ["", "", ""]
""" 图标 """
@property
def icon_(self) -> str:
return self.icon[0]
@property
def normal(self) -> str:
return self.icon[1]
@property
def gacha(self) -> str:
return self.icon[2]

View File

@ -0,0 +1,24 @@
from pydantic import BaseModel
from .enums import ZZZRank
class Buddy(BaseModel):
id: int
""""邦布ID"""
name: str
"""名称"""
name_en: str
"""英文名称"""
icon: str = ""
"""图标"""
rank: ZZZRank = ZZZRank.NULL
""" 星级 """
@property
def webp(self) -> str:
return self.icon if self.icon.endswith("webp") else ""
@property
def png(self) -> str:
return self.icon if self.icon.endswith("png") else ""

View File

@ -0,0 +1,49 @@
from enum import Enum, IntEnum
class ZZZElementType(IntEnum):
"""ZZZ element type."""
NULL = 1
""""""
PHYSICAL = 200
""" 物理 """
FIRE = 201
""""""
ICE = 202
""""""
ELECTRIC = 203
""""""
ETHER = 205
""" 以太 """
class ZZZSpeciality(IntEnum):
"""ZZZ agent compatible speciality."""
ATTACK = 1
""" 强攻 """
STUN = 2
""" 击破 """
ANOMALY = 3
""" 异常 """
SUPPORT = 4
""" 支援 """
DEFENSE = 5
""" 防护 """
class ZZZRank(str, Enum):
"""ZZZ Rank"""
S = "S"
A = "A"
B = "B"
C = "C"
D = "D"
NULL = "NULL"
@property
def int(self):
value_map = {"S": 5, "A": 4, "B": 3, "C": 2, "D": 1, "NULL": 0}
return value_map[self.value]

View File

@ -0,0 +1,22 @@
from pydantic import BaseModel
from .enums import ZZZRank
class EquipmentSuit(BaseModel):
id: int
"""驱动盘套装ID"""
name: str
"""套装名称"""
name_en: str
"""英文套装名称"""
icon: str = ""
"""套装图标"""
desc_2: str
"""2套描述"""
desc_4: str
"""4套描述"""
story: str
"""套装故事"""
rank: ZZZRank = ZZZRank.NULL
""" 星级 """

View File

@ -0,0 +1,18 @@
from pydantic import BaseModel
from .enums import ZZZRank
class Weapon(BaseModel):
id: int
""""武器ID"""
name: str
"""名称"""
name_en: str
"""英文名称"""
description: str
"""描述"""
icon: str = ""
"""图标"""
rank: ZZZRank
"""稀有度"""

View File

@ -124,7 +124,6 @@ class Association(Enum):
Inazuma = "稻妻"
Liyue = "璃月"
Mondstadt = "蒙德"
Fontaine = "枫丹"
@classmethod
def convert(cls, string: str) -> Optional[Self]:

69
modules/wiki/raider.py Normal file
View File

@ -0,0 +1,69 @@
import asyncio
from typing import List, Dict
from metadata.shortname import roleToName, weaponToName
from modules.wiki.base import WikiModel
class Raider(WikiModel):
raider_url = "https://raw.githubusercontent.com/PaiGramTeam/zzz-atlas/master"
raider_path = WikiModel.BASE_PATH / "raiders"
raider_role_path = WikiModel.BASE_PATH / "raiders" / "role"
raider_guide_for_role_path = WikiModel.BASE_PATH / "raiders" / "guide_for_role"
raider_light_cone_path = WikiModel.BASE_PATH / "raiders" / "weapon"
raider_relic_path = WikiModel.BASE_PATH / "raiders" / "relic"
raider_info_path = WikiModel.BASE_PATH / "raiders" / "path.json"
raider_role_path.mkdir(parents=True, exist_ok=True)
raider_guide_for_role_path.mkdir(parents=True, exist_ok=True)
raider_light_cone_path.mkdir(parents=True, exist_ok=True)
raider_relic_path.mkdir(parents=True, exist_ok=True)
name_map = {
"角色": "role",
"音擎": "weapon",
"角色攻略": "guide_for_role",
}
def __init__(self):
super().__init__()
self.all_role_raiders: List[str] = []
self.all_guide_for_role_raiders: List[str] = []
self.all_light_cone_raiders: List[str] = []
self.all_relic_raiders: List[str] = []
def clear_class_data(self) -> None:
self.all_role_raiders.clear()
self.all_guide_for_role_raiders.clear()
self.all_light_cone_raiders.clear()
self.all_relic_raiders.clear()
async def refresh_task(self, name: str, path: str = "", start: str = ""):
photo = await self.remote_get(f"{self.raider_url}{path}")
await self.save_file(photo.content, self.raider_path / start / f"{name}.png")
async def refresh(self):
datas = await self.remote_get(self.raider_url + "/path.json")
data = datas.json()
new_data = {}
for key, start in self.name_map.items():
new_data[start] = list(data[key].keys())
tasks = []
for name, path in data[key].items():
if key in {"角色", "角色攻略"}:
name = roleToName(name)
else:
name = weaponToName(name)
tasks.append(self.refresh_task(name, path, start))
await asyncio.gather(*tasks)
await self.dump(new_data, self.raider_info_path)
await self.read()
async def read(self):
if not self.raider_info_path.exists():
await self.refresh()
return
datas: Dict[str, List] = await WikiModel.read(self.raider_info_path) # noqa
self.clear_class_data()
self.all_role_raiders.extend(datas["role"])
self.all_guide_for_role_raiders.extend(datas["guide_for_role"])
self.all_light_cone_raiders.extend(datas["weapon"])
# self.all_relic_raiders.extend(datas["relic"])

View File

@ -1,146 +1,46 @@
import itertools
import re
from typing import List, Optional, Tuple, Union
from typing import List, Dict, Optional
from bs4 import BeautifulSoup
from httpx import URL
from modules.wiki.base import HONEY_HOST, Model, WikiModel
from modules.wiki.other import AttributeType, WeaponType
__all__ = ["Weapon", "WeaponAffix", "WeaponAttribute"]
class WeaponAttribute(Model):
"""武器词条"""
type: AttributeType
value: str
class WeaponAffix(Model):
"""武器技能
Attributes:
name: 技能名
description: 技能描述
"""
name: str
description: List[str]
class WeaponState(Model):
level: str
ATK: float
bonus: Optional[str]
class WeaponIcon(Model):
icon: str
awakened: str
gacha: str
from modules.wiki.base import WikiModel
from modules.wiki.models.weapon import Weapon as WeaponModel
class Weapon(WikiModel):
"""武器
weapon_url = WikiModel.BASE_URL + "weapons.json"
weapon_path = WikiModel.BASE_PATH / "weapons.json"
Attributes:
weapon_type: 武器类型
attack: 基础攻击力
attribute:
affix: 武器技能
description: 描述
ascension: 突破材料
story: 武器故事
"""
def __init__(self):
super().__init__()
self.all_weapons: List[WeaponModel] = []
self.all_weapons_map: Dict[int, WeaponModel] = {}
self.all_weapons_name: Dict[str, WeaponModel] = {}
weapon_type: WeaponType
attack: float
attribute: Optional[WeaponAttribute]
affix: Optional[WeaponAffix]
description: str
ascension: List[str]
story: Optional[str]
def clear_class_data(self) -> None:
self.all_weapons.clear()
self.all_weapons_map.clear()
self.all_weapons_name.clear()
stats: List[WeaponState]
async def refresh(self):
datas = await self.remote_get(self.weapon_url)
await self.dump(datas.json(), self.weapon_path)
await self.read()
@staticmethod
def scrape_urls() -> List[URL]:
return [HONEY_HOST.join(f"fam_{i.lower()}/?lang=CHS") for i in WeaponType.__members__]
async def read(self):
if not self.weapon_path.exists():
await self.refresh()
return
datas = await WikiModel.read(self.weapon_path)
self.clear_class_data()
for data in datas:
m = WeaponModel(**data)
self.all_weapons.append(m)
self.all_weapons_map[m.id] = m
self.all_weapons_name[m.name] = m
@classmethod
async def _parse_soup(cls, soup: BeautifulSoup) -> "Weapon":
"""解析武器页"""
soup = soup.select(".wp-block-post-content")[0]
tables = soup.find_all("table")
table_rows = tables[0].find_all("tr")
def get_by_id(self, cid: int) -> Optional[WeaponModel]:
return self.all_weapons_map.get(cid)
def get_table_text(row_num: int) -> str:
"""一个快捷函数,用于返回表格对应行的最后一个单元格中的文本"""
return table_rows[row_num].find_all("td")[-1].text.replace("\xa0", "")
def get_by_name(self, name: str) -> Optional[WeaponModel]:
return self.all_weapons_name.get(name)
def find_table(select: str):
"""一个快捷函数,用于寻找对应表格头的表格"""
return list(filter(lambda x: select in " ".join(x.attrs["class"]), tables))
id_ = re.findall(r"/img/(.*?)_gacha", str(table_rows[0]))[0]
weapon_type = WeaponType[get_table_text(1).split(",")[-1].strip()]
name = get_table_text(0)
rarity = len(table_rows[2].find_all("img"))
attack = float(get_table_text(4))
ascension = [re.findall(r"/(.*)/", tag.attrs["href"])[0] for tag in table_rows[-1].find_all("a")]
if rarity > 2: # 如果是 3 星及其以上的武器
attribute = WeaponAttribute(
type=AttributeType.convert(tables[2].find("thead").find("tr").find_all("td")[2].text.split(" ")[1]),
value=get_table_text(6),
)
affix = WeaponAffix(
name=get_table_text(7), description=[i.find_all("td")[1].text for i in tables[3].find_all("tr")[1:]]
)
description = get_table_text(9)
if story_table := find_table("quotes"):
story = story_table[0].text.strip()
else:
story = None
else: # 如果是 2 星及其以下的武器
attribute = affix = None
description = get_table_text(5)
story = tables[-1].text.strip()
stats = []
for row in tables[2].find_all("tr")[1:]:
cells = row.find_all("td")
if rarity > 2:
stats.append(WeaponState(level=cells[0].text, ATK=cells[1].text, bonus=cells[2].text))
else:
stats.append(WeaponState(level=cells[0].text, ATK=cells[1].text))
return Weapon(
id=id_,
name=name,
rarity=rarity,
attack=attack,
attribute=attribute,
affix=affix,
weapon_type=weapon_type,
story=story,
stats=stats,
description=description,
ascension=ascension,
)
@classmethod
async def get_name_list(cls, *, with_url: bool = False) -> List[Union[str, Tuple[str, URL]]]:
# 重写此函数的目的是名字去重,例如单手剑页面中有三个 “「一心传」名刀”
name_list = [i async for i in cls._name_list_generator(with_url=with_url)]
if with_url:
return [(i[0], list(i[1])[0][1]) for i in itertools.groupby(name_list, lambda x: x[0])]
return [i[0] for i in itertools.groupby(name_list, lambda x: x)]
@property
def icon(self) -> WeaponIcon:
return WeaponIcon(
icon=str(HONEY_HOST.join(f"/img/{self.id}.webp")),
awakened=str(HONEY_HOST.join(f"/img/{self.id}_awaken_icon.webp")),
gacha=str(HONEY_HOST.join(f"/img/{self.id}_gacha_icon.webp")),
)
def get_name_list(self) -> List[str]:
return list(self.all_weapons_name.keys())

View File

@ -2212,11 +2212,11 @@ name = "simnet"
version = "0.1.22"
requires_python = "<4.0,>=3.8"
git = "https://github.com/PaiGramTeam/SIMNet"
revision = "05fcb568d6c1fe44a4f917c996198bfe62a00053"
revision = "074939d8818e6073be4a918b25d7deadd43a5b7b"
summary = "Modern API wrapper for Genshin Impact & Honkai: Star Rail built on asyncio and pydantic."
groups = ["default"]
dependencies = [
"httpx<1.0.0,>=0.25.0",
"httpx>=0.25.0",
"pydantic<2.0.0,>=1.10.7",
]

View File

@ -212,7 +212,7 @@ class BindAccountPlugin(Plugin.Conversation):
return ConversationHandler.END
try:
async with self.helper.public_genshin(user.id, region=region, uid=player_id) as client:
player_stats = await client.get_genshin_user(player_id)
player_stats = await client.get_zzz_user(player_id)
except TooManyRequestPublicCookies:
await message.reply_text("用户查询次数过多,请稍后重试", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END

View File

@ -21,6 +21,11 @@ class SetCommandPlugin(Plugin):
user_command = [
BotCommand("cancel", "取消操作(解决一切玄学问题)"),
BotCommand("help_raw", "查看文本帮助"),
# gacha_log 相关
BotCommand("signal_log", "查看调频记录"),
BotCommand("signal_log_import", "导入调频记录"),
BotCommand("signal_log_export", "导出调频记录"),
BotCommand("signal_log_delete", "删除调频记录"),
# Cookie 查询类
BotCommand("sign", "米游社绝区零每日签到"),
BotCommand("dailynote_tasks", "自动便笺提醒"),
@ -33,6 +38,9 @@ class SetCommandPlugin(Plugin):
group_command = [
# 通用
BotCommand("help", "帮助"),
# Wiki 类
BotCommand("weapon", "查看音擎图鉴"),
BotCommand("avatar", "查询角色攻略"),
# UID 查询类
BotCommand("stats", "玩家统计查询"),
# Cookie 查询类

19
plugins/admin/wiki.py Normal file
View File

@ -0,0 +1,19 @@
from telegram import Update
from telegram.ext import CallbackContext
from core.plugin import Plugin, handler
from core.services.wiki.services import WikiService
class WikiPlugin(Plugin):
"""有关WIKI操作"""
def __init__(self, wiki_service: WikiService):
self.wiki_service = wiki_service
@handler.command("refresh_wiki", block=False, admin=True)
async def refresh_wiki(self, update: Update, _: CallbackContext):
message = update.effective_message
msg = await message.reply_text("正在刷新Wiki缓存请稍等")
await self.wiki_service.refresh_wiki()
await msg.edit_text("刷新Wiki缓存成功")

View File

@ -1,27 +1,31 @@
import asyncio
from typing import Awaitable, Dict, List, cast, Tuple
from uuid import uuid4
from telegram import (
InlineQuery,
InlineQueryResultArticle,
InlineQueryResultPhoto,
InlineQueryResultCachedPhoto,
InlineQueryResultCachedDocument,
InputTextMessageContent,
Update,
InlineQueryResultsButton,
InlineKeyboardButton,
InlineKeyboardMarkup,
InlineKeyboardButton,
InlineQueryResultPhoto,
)
from telegram.constants import ParseMode
from telegram.error import BadRequest
from telegram.ext import CallbackContext, ContextTypes
from core.dependence.assets import AssetsService
from core.plugin import Plugin, handler
from core.services.cookies import CookiesService
from core.services.players import PlayersService
from core.services.search.services import SearchServices
from core.services.wiki.services import WikiService
from gram_core.config import config
from gram_core.plugin.methods.inline_use_data import IInlineUseData
from gram_core.services.cookies import CookiesService
from gram_core.services.players import PlayersService
from utils.log import logger
@ -30,12 +34,20 @@ class Inline(Plugin):
def __init__(
self,
asset_service: AssetsService,
search_service: SearchServices,
wiki_service: WikiService,
cookies_service: CookiesService,
players_service: PlayersService,
):
self.asset_service = asset_service
self.wiki_service = wiki_service
self.weapons_list: List[Dict[str, str]] = []
self.characters_list: List[Dict[str, str]] = []
self.characters_material_list: List[Dict[str, str]] = []
self.characters_guide_list: List[Dict[str, str]] = []
self.light_cone_list: List[Dict[str, str]] = []
self.relics_list: List[Dict[str, str]] = []
self.refresh_task: List[Awaitable] = []
self.search_service = search_service
self.cookies_service = cookies_service
@ -44,6 +56,84 @@ class Inline(Plugin):
self.inline_use_data_map: Dict[str, IInlineUseData] = {}
self.img_url = "https://i.dawnlab.me/b1bdf9cc3061d254f038e557557694bc.jpg"
async def initialize(self):
async def task_light_cone():
logger.info("Inline 模块正在获取武器列表")
light_cone_datas: Dict[str, str] = {}
light_cone_datas_name: Dict[str, str] = {}
for light_cone in self.asset_service.weapon.data:
light_cone_datas[light_cone.name] = light_cone.icon
light_cone_datas_name[str(light_cone.id)] = light_cone.name
# 武器列表
for lid in self.wiki_service.raider.all_light_cone_raiders:
if lid not in light_cone_datas_name:
continue
light_cone = light_cone_datas_name[lid]
if light_cone in light_cone_datas:
self.light_cone_list.append({"name": light_cone, "icon": light_cone_datas[light_cone]})
else:
logger.warning(f"未找到武器 {light_cone} 的图标inline 不显示此武器")
logger.success("Inline 模块获取武器列表完成")
async def task_relics():
logger.info("Inline 模块正在获取驱动盘列表")
relics_datas: Dict[str, str] = {}
relics_datas_name: Dict[str, str] = {}
for relics in self.wiki_service.equipment_suit.all_equipment_suits:
relics_datas[relics.name] = relics.icon
relics_datas_name[str(relics.id)] = relics.name
for rid in self.wiki_service.raider.all_relic_raiders:
if rid not in relics_datas_name:
continue
relics = relics_datas_name[rid]
if relics in relics_datas:
self.relics_list.append({"name": relics, "icon": relics_datas[relics]})
else:
logger.warning(f"未找到驱动盘 {relics} 的图标inline 不显示此驱动盘")
logger.success("Inline 模块获取驱动盘列表完成")
async def task_characters():
logger.info("Inline 模块正在获取角色列表")
datas: Dict[str, str] = {}
datas_name: Dict[str, str] = {}
for character in self.asset_service.avatar.data:
datas[character.name] = character.normal
datas_name[str(character.id)] = character.name
def get_character(_cid: str) -> str:
if _cid in datas_name:
return datas_name[_cid]
# 角色攻略
for cid in self.wiki_service.raider.all_role_raiders:
character = get_character(cid)
if not character:
continue
if character in datas:
self.characters_list.append({"name": character, "icon": datas[character]})
else:
for key, value in datas.items():
if character.startswith(key) or character.endswith(key):
self.characters_list.append({"name": character, "icon": value})
break
# 角色攻略
for cid in self.wiki_service.raider.all_guide_for_role_raiders:
character = get_character(cid)
if not character:
continue
if character in datas:
self.characters_guide_list.append({"name": character, "icon": datas[character]})
else:
for key, value in datas.items():
if character.startswith(key) or character.endswith(key):
self.characters_guide_list.append({"name": character, "icon": value})
break
logger.success("Inline 模块获取角色列表成功")
self.refresh_task.append(asyncio.create_task(task_characters()))
self.refresh_task.append(asyncio.create_task(task_light_cone()))
self.refresh_task.append(asyncio.create_task(task_relics()))
async def init_inline_use_data(self):
if self.inline_use_data:
return
@ -159,22 +249,22 @@ class Inline(Plugin):
results_list = []
args = query.split(" ")
if args[0] == "":
results_list.append(
InlineQueryResultArticle(
id=str(uuid4()),
title="武器图鉴查询",
description="输入武器名称即可查询武器图鉴",
input_message_content=InputTextMessageContent("武器图鉴查询"),
temp_data = [
("武器图鉴查询", "输入武器名称即可查询武器图鉴"),
("角色攻略查询", "输入角色名即可查询角色攻略图鉴"),
# ("角色图鉴查询", "输入角色名即可查询角色图鉴"),
# ("角色培养素材查询", "输入角色名即可查询角色培养素材图鉴"),
# ("驱动盘套装查询", "输入驱动盘套装名称即可查询驱动盘套装图鉴"),
]
for i in temp_data:
results_list.append(
InlineQueryResultArticle(
id=str(uuid4()),
title=i[0],
description=i[1],
input_message_content=InputTextMessageContent(i[0]),
)
)
)
results_list.append(
InlineQueryResultArticle(
id=str(uuid4()),
title="角色攻略查询",
description="输入角色名即可查询角色攻略",
input_message_content=InputTextMessageContent("角色攻略查询"),
)
)
results_list.append(
InlineQueryResultArticle(
id=str(uuid4()),
@ -188,33 +278,31 @@ class Inline(Plugin):
elif args[0] == "功能":
return
else:
if args[0] == "查看武器列表并查询":
for weapon in self.weapons_list:
name = weapon["name"]
icon = weapon["icon"]
results_list.append(
InlineQueryResultArticle(
id=str(uuid4()),
title=name,
description=f"查看武器列表并查询 {name}",
thumbnail_url=icon,
input_message_content=InputTextMessageContent(
f"武器查询{name}", parse_mode=ParseMode.MARKDOWN_V2
),
)
)
elif args[0] == "查看角色攻略列表并查询":
for character in self.characters_list:
if args[0] in [
# "查看角色攻略列表并查询",
"查看角色图鉴列表并查询",
"查看音擎列表并查询",
# "查看驱动盘套装列表并查询",
# "查看角色培养素材列表并查询",
]:
temp_data = {
# "查看角色攻略列表并查询": (self.characters_list, "角色攻略查询"),
"查看角色图鉴列表并查询": (self.characters_guide_list, "角色图鉴查询"),
# "查看角色培养素材列表并查询": (self.characters_material_list, "角色培养素材查询"),
"查看音擎列表并查询": (self.light_cone_list, "音擎图鉴查询"),
# "查看驱动盘套装列表并查询": (self.relics_list, "驱动盘套装查询"),
}[args[0]]
for character in temp_data[0]:
name = character["name"]
icon = character["icon"]
results_list.append(
InlineQueryResultArticle(
id=str(uuid4()),
title=name,
description=f"查看角色攻略列表并查询 {name}",
description=f"{args[0]} {name}",
thumbnail_url=icon,
input_message_content=InputTextMessageContent(
f"角色攻略查询{name}", parse_mode=ParseMode.MARKDOWN_V2
f"{temp_data[1]}{name}", parse_mode=ParseMode.MARKDOWN_V2
),
)
)
@ -226,35 +314,44 @@ class Inline(Plugin):
id=str(uuid4()),
title=f"当前查询内容为 {args[0]}",
description="如果无查看图片描述 这是正常的 客户端问题",
thumbnail_url="https://www.miyoushe.com/_nuxt/img/game-ys.dfc535b.jpg",
thumbnail_url="https://www.miyoushe.com/_nuxt/img/game-sr.4f80911.jpg",
input_message_content=InputTextMessageContent(
f"当前查询内容为 {args[0]}\n如果无查看图片描述 这是正常的 客户端问题"
),
)
)
for simple_search_result in simple_search_results:
description = simple_search_result.description
if len(description) >= 10:
description = description[:10]
item = None
if simple_search_result.photo_file_id:
description = simple_search_result.description
if len(description) >= 10:
description = description[:10]
results_list.append(
InlineQueryResultCachedPhoto(
id=str(uuid4()),
title=simple_search_result.title,
photo_file_id=simple_search_result.photo_file_id,
description=description,
caption=simple_search_result.caption,
parse_mode=simple_search_result.parse_mode,
)
item = InlineQueryResultCachedPhoto(
id=str(uuid4()),
title=simple_search_result.title,
photo_file_id=simple_search_result.photo_file_id,
description=description,
caption=simple_search_result.caption,
parse_mode=simple_search_result.parse_mode,
)
elif simple_search_result.document_file_id:
item = InlineQueryResultCachedDocument(
id=str(uuid4()),
title=simple_search_result.title,
document_file_id=simple_search_result.document_file_id,
description=description,
caption=simple_search_result.caption,
parse_mode=simple_search_result.parse_mode,
)
if item:
results_list.append(item)
if not results_list:
results_list.append(
InlineQueryResultArticle(
id=str(uuid4()),
title="好像找不到问题呢",
description="这个问题我也不知道,因为我就是个应急食品。",
input_message_content=InputTextMessageContent("这个问题我也不知道,因为我就是个应急食品。"),
description="这个问题我也不知道",
input_message_content=InputTextMessageContent("这个问题我也不知道"),
)
)
try:

View File

@ -8,7 +8,6 @@ from typing import Optional, Tuple, List, TYPE_CHECKING
from httpx import TimeoutException
from simnet import Game
from simnet.errors import BadRequest as SimnetBadRequest, AlreadyClaimed, InvalidCookies, TimedOut as SimnetTimedOut
from simnet.utils.player import recognize_genshin_server
from sqlalchemy.orm.exc import StaleDataError
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.constants import ParseMode
@ -114,10 +113,7 @@ class SignSystem(Plugin):
title: Optional[str] = "签到结果",
) -> str:
if is_sleep:
if recognize_genshin_server(client.player_id) in ("cn_gf01", "cn_qd01"):
await asyncio.sleep(random.randint(10, 300)) # nosec
else:
await asyncio.sleep(random.randint(0, 3)) # nosec
await asyncio.sleep(random.randint(0, 3)) # nosec
try:
rewards = await client.get_monthly_rewards(game=Game.GENSHIN, lang="zh-cn")
except SimnetBadRequest as error:
@ -246,7 +242,7 @@ class SignSystem(Plugin):
logger.warning("UID[%s] 已经签到", client.player_id)
if is_raise:
raise error
result = "今天旅行者已经签到过了~"
result = "今天绳匠已经签到过了~"
except SimnetBadRequest as error:
logger.warning("UID %s 签到失败API返回信息为 %s", client.player_id, str(error))
if is_raise:
@ -256,7 +252,7 @@ class SignSystem(Plugin):
result = "OK"
else:
logger.info("UID[%s] 已经签到", client.player_id)
result = "今天旅行者已经签到过了~"
result = "今天绳匠已经签到过了~"
logger.info("UID[%s] 签到结果 %s", client.player_id, result)
reward = rewards[daily_reward_info.claimed_rewards - (1 if daily_reward_info.signed_in else 0)]
today = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
@ -300,7 +296,7 @@ class SignSystem(Plugin):
text = "自动签到执行失败Cookie无效"
sign_db.status = TaskStatusEnum.INVALID_COOKIES
except AlreadyClaimed:
text = "今天旅行者已经签到过了~"
text = "今天绳匠已经签到过了~"
sign_db.status = TaskStatusEnum.ALREADY_CLAIMED
except SimnetBadRequest as exc:
text = f"自动签到执行失败API返回信息为 {str(exc)}"

86
plugins/zzz/avatars.py Normal file
View File

@ -0,0 +1,86 @@
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.constants import ChatAction, ParseMode
from telegram.ext import CallbackContext, filters
from core.plugin import Plugin, handler
from core.services.game.services import GameCacheService
from core.services.search.models import StrategyEntry
from core.services.search.services import SearchServices
from core.services.wiki.services import WikiService
from metadata.shortname import roleToName, roleToTag
from utils.log import logger
class AvatarsPlugin(Plugin):
"""角色图鉴查询"""
KEYBOARD = [
[InlineKeyboardButton(text="查看角色图鉴列表并查询", switch_inline_query_current_chat="查看角色图鉴列表并查询")]
]
def __init__(
self,
cache_service: GameCacheService = None,
wiki_service: WikiService = None,
search_service: SearchServices = None,
):
self.cache_service = cache_service
self.wiki_service = wiki_service
self.search_service = search_service
@handler.command(command="avatar", block=False)
@handler.message(filters=filters.Regex("^角色图鉴查询(.*)"), block=False)
async def command_start(self, update: Update, context: CallbackContext) -> None:
message = update.effective_message
args = self.get_args(context)
if len(args) >= 1:
character_name = args[0]
else:
reply_message = await message.reply_text(
"请回复你要查询的图鉴的角色名", reply_markup=InlineKeyboardMarkup(self.KEYBOARD)
)
if filters.ChatType.GROUPS.filter(reply_message):
self.add_delete_message_job(message)
self.add_delete_message_job(reply_message)
return
character_name = roleToName(character_name)
file_path = self.wiki_service.raider.raider_guide_for_role_path / f"{character_name}.png"
if not file_path.exists():
reply_message = await message.reply_text(
f"没有找到 {character_name} 的图鉴", reply_markup=InlineKeyboardMarkup(self.KEYBOARD)
)
if filters.ChatType.GROUPS.filter(reply_message):
self.add_delete_message_job(message)
self.add_delete_message_job(reply_message)
return
self.log_user(update, logger.info, "查询角色图鉴命令请求 || 参数 %s", character_name)
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
caption = "From 米游社@听语惊花"
if file_id := await self.cache_service.get_avatar_cache(character_name):
await message.reply_document(
document=file_id,
caption=caption,
filename=f"{character_name}.png",
parse_mode=ParseMode.HTML,
)
else:
reply_photo = await message.reply_document(
document=open(file_path, "rb"),
caption=caption,
filename=f"{character_name}.png",
parse_mode=ParseMode.HTML,
)
if reply_photo.document:
tags = roleToTag(character_name)
photo_file_id = reply_photo.document.file_id
await self.cache_service.set_avatar_cache(character_name, photo_file_id)
entry = StrategyEntry(
key=f"plugin:avatar:{character_name}",
title=character_name,
description=f"{character_name} 角色图鉴",
tags=tags,
caption=caption,
parse_mode="HTML",
document_file_id=photo_file_id,
)
await self.search_service.add_entry(entry)

596
plugins/zzz/signal_log.py Normal file
View File

@ -0,0 +1,596 @@
from functools import partial
from io import BytesIO
from typing import Optional, TYPE_CHECKING, List, Union, Tuple, Dict
from simnet import ZZZClient, Region
from simnet.models.zzz.wish import ZZZBannerType
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove
from telegram.constants import ChatAction
from telegram.ext import ConversationHandler, filters
from telegram.helpers import create_deep_linked_url
from core.dependence.assets import AssetsService
from core.plugin import Plugin, conversation, handler
from core.services.cookies import CookiesService
from core.services.players import PlayersService
from core.services.template.models import FileType
from core.services.template.services import TemplateService
from gram_core.basemodel import RegionEnum
from gram_core.config import config
from gram_core.plugin.methods.inline_use_data import IInlineUseData
from modules.gacha_log.const import ZZZGF_VERSION, GACHA_TYPE_LIST_REVERSE
from modules.gacha_log.error import (
GachaLogAccountNotFound,
GachaLogAuthkeyTimeout,
GachaLogFileError,
GachaLogInvalidAuthkey,
GachaLogMixedProvider,
GachaLogNotFound,
)
from modules.gacha_log.helpers import from_url_get_authkey
from modules.gacha_log.log import GachaLog
from modules.gacha_log.migrate import GachaLogMigrate
from modules.gacha_log.models import GachaLogInfo
from plugins.tools.genshin import PlayerNotFoundError
from utils.const import RESOURCE_DIR
from utils.log import logger
try:
import ujson as jsonlib
except ImportError:
import json as jsonlib
if TYPE_CHECKING:
from telegram import Update, Message, User, Document
from telegram.ext import ContextTypes
from gram_core.services.players.models import Player
from gram_core.services.template.models import RenderResult
INPUT_URL, INPUT_FILE, CONFIRM_DELETE = range(10100, 10103)
WAITING = f"{config.notice.bot_name}正在从服务器获取数据,请稍后"
WISHLOG_NOT_FOUND = f"{config.notice.bot_name}没有找到你的调频记录,快来私聊{config.notice.bot_name}导入吧~"
class WishLogPlugin(Plugin.Conversation):
"""调频记录导入/导出/分析"""
IMPORT_HINT = (
"<b>开始导入祈愿历史记录:请通过 https://zzz.rng.moe/en/tracker/import 获取调频记录链接后发送给我"
"(非 zzz.rng.moe 导出的文件数据)</b>\n\n"
f"> 你还可以向{config.notice.bot_name}发送从其他工具导出的 ZZZGF {ZZZGF_VERSION} 标准的记录文件\n"
"> 在绑定 Cookie 时添加 stoken 可能有特殊效果哦(仅限国服)\n"
"<b>注意:导入的数据将会与旧数据进行合并。</b>"
)
def __init__(
self,
template_service: TemplateService,
players_service: PlayersService,
assets: AssetsService,
cookie_service: CookiesService,
):
self.template_service = template_service
self.players_service = players_service
self.assets_service = assets
self.cookie_service = cookie_service
self.gacha_log = GachaLog()
self.wish_photo = None
async def get_player_id(self, user_id: int, player_id: int, offset: int) -> int:
"""获取绑定的游戏ID"""
logger.debug("尝试获取已绑定的绝区零账号")
player = await self.players_service.get_player(user_id, player_id=player_id, offset=offset)
if player is None:
raise PlayerNotFoundError(user_id)
return player.player_id
async def _refresh_user_data(
self, user: "User", player_id: int, data: dict = None, authkey: str = None, verify_uid: bool = True
) -> str:
"""刷新用户数据
:param user: 用户
:param data: 数据
:param authkey: 认证密钥
:return: 返回信息
"""
try:
logger.debug("尝试获取已绑定的绝区零账号")
if authkey:
new_num = await self.gacha_log.get_gacha_log_data(user.id, player_id, authkey)
return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条调频记录"
if data:
new_num = await self.gacha_log.import_gacha_log_data(user.id, player_id, data, verify_uid)
return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条调频记录"
except GachaLogNotFound:
return WISHLOG_NOT_FOUND
except GachaLogAccountNotFound:
return "导入失败,可能文件包含的调频记录所属 uid 与你当前绑定的 uid 不同"
except GachaLogFileError:
return "导入失败,数据格式错误"
except GachaLogInvalidAuthkey:
return "更新数据失败authkey 无效"
except GachaLogAuthkeyTimeout:
return "更新数据失败authkey 已经过期"
except GachaLogMixedProvider:
return "导入失败,你已经通过其他方式导入过调频记录了,本次无法导入"
except PlayerNotFoundError:
logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id)
return config.notice.user_not_found
async def import_from_file(
self, user: "User", player_id: int, message: "Message", document: "Document" = None
) -> None:
if not document:
document = message.document
# TODO: 使用 mimetype 判断文件类型
if document.file_name.endswith(".json"):
file_type = "json"
else:
await message.reply_text("文件格式错误,请发送符合 ZZZGF 标准的调频记录文件")
return
if document.file_size > 5 * 1024 * 1024:
await message.reply_text("文件过大,请发送小于 5 MB 的文件")
return
try:
out = BytesIO()
await (await document.get_file()).download_to_memory(out=out)
if file_type == "json":
# bytesio to json
data = jsonlib.loads(out.getvalue().decode("utf-8"))
else:
await message.reply_text("文件解析失败,请检查文件")
return
except GachaLogFileError:
await message.reply_text("文件解析失败,请检查文件是否符合 ZZZGF 标准")
return
except (KeyError, IndexError, ValueError):
await message.reply_text("文件解析失败,请检查文件编码是否正确或符合 ZZZGF 标准")
return
except Exception as exc:
logger.error("文件解析失败 %s", repr(exc))
await message.reply_text("文件解析失败,请检查文件是否符合 ZZZGF 标准")
return
await message.reply_chat_action(ChatAction.TYPING)
reply = await message.reply_text("文件解析成功,正在导入数据", reply_markup=ReplyKeyboardRemove())
await message.reply_chat_action(ChatAction.TYPING)
try:
text = await self._refresh_user_data(user, player_id, data=data, verify_uid=file_type == "json")
except Exception as exc: # pylint: disable=W0703
logger.error("文件解析失败 %s", repr(exc))
text = "文件解析失败,请检查文件是否符合 ZZZGF 标准"
await reply.edit_text(text)
async def can_gen_authkey(self, user_id: int, player_id: int) -> bool:
player_info = await self.players_service.get_player(user_id, region=RegionEnum.HYPERION, player_id=player_id)
if player_info is not None:
cookies = await self.cookie_service.get(user_id, account_id=player_info.account_id)
if (
cookies is not None
and cookies.data
and "stoken" in cookies.data
and next((value for key, value in cookies.data.items() if key in ["ltuid", "login_uid"]), None)
):
return True
return False
async def gen_authkey(self, uid: int) -> Optional[str]:
player_info = await self.players_service.get_player(uid, region=RegionEnum.HYPERION)
if player_info is not None:
cookies = await self.cookie_service.get(uid, account_id=player_info.account_id)
if cookies is not None and cookies.data and "stoken" in cookies.data:
if stuid := next((value for key, value in cookies.data.items() if key in ["ltuid", "login_uid"]), None):
cookies.data["stuid"] = stuid
async with ZZZClient(
cookies=cookies.data, region=Region.CHINESE, lang="zh-cn", player_id=player_info.player_id
) as client:
return await client.get_authkey_by_stoken("webview_gacha")
@conversation.entry_point
@handler.command(command="signal_log_import", filters=filters.ChatType.PRIVATE, block=False)
@handler.message(filters=filters.Regex("^导入调频记录(.*)") & filters.ChatType.PRIVATE, block=False)
@handler.command(command="start", filters=filters.Regex("signal_log_import$"), block=False)
async def command_start(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int:
uid, offset = self.get_real_uid_or_offset(update)
message = update.effective_message
user = update.effective_user
player_id = await self.get_player_id(user.id, uid, offset)
context.chat_data["uid"] = player_id
logger.info("用户 %s[%s] 导入调频记录命令请求", user.full_name, user.id)
keyboard = None
if await self.can_gen_authkey(user.id, player_id):
keyboard = ReplyKeyboardMarkup([["自动导入"], ["退出"]], one_time_keyboard=True)
await message.reply_text(self.IMPORT_HINT, parse_mode="html", reply_markup=keyboard)
return INPUT_URL
@conversation.state(state=INPUT_URL)
@handler.message(filters=~filters.COMMAND, block=False)
async def import_data_from_message(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int:
message = update.effective_message
user = update.effective_user
player_id = context.chat_data["uid"]
if message.document:
await self.import_from_file(user, player_id, message)
return ConversationHandler.END
if not message.text:
await message.reply_text("请发送文件或链接")
return INPUT_URL
if message.text == "自动导入":
authkey = await self.gen_authkey(user.id)
if not authkey:
await message.reply_text(
"自动生成 authkey 失败,请尝试通过其他方式导入。", reply_markup=ReplyKeyboardRemove()
)
return ConversationHandler.END
elif message.text == "退出":
await message.reply_text("取消导入跃迁记录", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
else:
authkey = from_url_get_authkey(message.text)
reply = await message.reply_text(WAITING, reply_markup=ReplyKeyboardRemove())
await message.reply_chat_action(ChatAction.TYPING)
text = await self._refresh_user_data(user, player_id, authkey=authkey)
self.add_delete_message_job(reply, delay=1)
await message.reply_text(text, reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
@conversation.entry_point
@handler.command(command="signal_log_delete", filters=filters.ChatType.PRIVATE, block=False)
@handler.message(filters=filters.Regex("^删除调频记录(.*)") & filters.ChatType.PRIVATE, block=False)
async def command_start_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int:
uid, offset = self.get_real_uid_or_offset(update)
message = update.effective_message
user = update.effective_user
logger.info("用户 %s[%s] 删除调频记录命令请求", user.full_name, user.id)
try:
player_id = await self.get_player_id(user.id, uid, offset)
context.chat_data["uid"] = player_id
except PlayerNotFoundError:
logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id)
await message.reply_text(config.notice.user_not_found)
return ConversationHandler.END
_, status = await self.gacha_log.load_history_info(str(user.id), str(player_id), only_status=True)
if not status:
await message.reply_text("你还没有导入调频记录哦~")
return ConversationHandler.END
await message.reply_text(
"你确定要删除调频记录吗?(此项操作无法恢复),如果确定请发送 ”确定“,发送其他内容取消"
)
return CONFIRM_DELETE
@conversation.state(state=CONFIRM_DELETE)
@handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False)
async def command_confirm_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int:
message = update.effective_message
user = update.effective_user
if message.text == "确定":
status = await self.gacha_log.remove_history_info(str(user.id), str(context.chat_data["uid"]))
await message.reply_text("调频记录已删除" if status else "调频记录删除失败")
return ConversationHandler.END
await message.reply_text("已取消")
return ConversationHandler.END
@handler.command(command="signal_log_force_delete", block=False, admin=True)
async def command_signal_log_force_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"):
uid, offset = self.get_real_uid_or_offset(update)
message = update.effective_message
args = self.get_args(context)
if not args:
await message.reply_text("请指定用户ID")
return
try:
cid = int(args[0])
if cid < 0:
raise ValueError("Invalid cid")
player_id = await self.get_player_id(cid, uid, offset)
_, status = await self.gacha_log.load_history_info(str(cid), str(player_id), only_status=True)
if not status:
await message.reply_text("该用户还没有导入调频记录")
return
status = await self.gacha_log.remove_history_info(str(cid), str(player_id))
await message.reply_text("调频记录已强制删除" if status else "调频记录删除失败")
except GachaLogNotFound:
await message.reply_text("该用户还没有导入调频记录")
except PlayerNotFoundError:
await message.reply_text("该用户暂未绑定账号")
except (ValueError, IndexError):
await message.reply_text("用户ID 不合法")
@handler.command(command="signal_log_export", filters=filters.ChatType.PRIVATE, block=False)
@handler.message(filters=filters.Regex("^导出调频记录(.*)") & filters.ChatType.PRIVATE, block=False)
async def command_start_export(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None:
uid, offset = self.get_real_uid_or_offset(update)
message = update.effective_message
user = update.effective_user
logger.info("用户 %s[%s] 导出调频记录命令请求", user.full_name, user.id)
try:
await message.reply_chat_action(ChatAction.TYPING)
player_id = await self.get_player_id(user.id, uid, offset)
path = await self.gacha_log.gacha_log_to_zzzgf(str(user.id), str(player_id))
await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT)
await message.reply_document(
document=open(path, "rb+"), caption=f"调频记录导出文件 - ZZZGF {ZZZGF_VERSION}"
)
except GachaLogNotFound:
logger.info("未找到用户 %s[%s] 的调频记录", user.full_name, user.id)
buttons = [
[
InlineKeyboardButton(
"点我导入", url=create_deep_linked_url(context.bot.username, "signal_log_import")
)
]
]
await message.reply_text(WISHLOG_NOT_FOUND, reply_markup=InlineKeyboardMarkup(buttons))
except GachaLogAccountNotFound:
await message.reply_text("导入失败,可能文件包含的调频记录所属 uid 与你当前绑定的 uid 不同")
except GachaLogFileError:
await message.reply_text("导入失败,数据格式错误")
except PlayerNotFoundError:
logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id)
await message.reply_text(config.notice.user_not_found)
async def rander_wish_log_analysis(
self, user_id: int, player_id: int, pool_type: ZZZBannerType
) -> Union[str, "RenderResult"]:
data = await self.gacha_log.get_analysis(user_id, player_id, pool_type, self.assets_service)
if isinstance(data, str):
return data
await self.add_theme_data(data, player_id)
png_data = await self.template_service.render(
"zzz/gacha_log/gacha_log.html",
data,
full_page=True,
file_type=FileType.DOCUMENT if len(data.get("fiveLog")) > 300 else FileType.PHOTO,
query_selector=".body_box",
)
return png_data
@staticmethod
def gen_button(user_id: int, uid: int, info: "GachaLogInfo") -> List[List[InlineKeyboardButton]]:
buttons = []
pools = []
skip_pools = []
for k, v in info.item_list.items():
if k in skip_pools:
continue
if not v:
continue
pools.append(k)
# 2 个一组
for i in range(0, len(pools), 2):
row = []
for pool in pools[i : i + 2]:
for k, v in {"log": "", "count": "(按卡池)"}.items():
row.append(
InlineKeyboardButton(
f"{pool.replace('祈愿', '')}{v}",
callback_data=f"get_wish_log|{user_id}|{uid}|{k}|{pool}",
)
)
buttons.append(row)
buttons.append([InlineKeyboardButton("五星调频统计", callback_data=f"get_wish_log|{user_id}|{uid}|count|five")])
return buttons
async def wish_log_pool_choose(self, user_id: int, player_id: int, message: "Message"):
await message.reply_chat_action(ChatAction.TYPING)
gacha_log, status = await self.gacha_log.load_history_info(str(user_id), str(player_id))
if not status:
raise GachaLogNotFound
buttons = self.gen_button(user_id, player_id, gacha_log)
if isinstance(self.wish_photo, str):
photo = self.wish_photo
else:
photo = open("resources/img/wish.jpg", "rb")
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
reply_message = await message.reply_photo(
photo=photo,
caption="请选择你要查询的卡池",
reply_markup=InlineKeyboardMarkup(buttons),
)
if reply_message.photo:
self.wish_photo = reply_message.photo[-1].file_id
async def wish_log_pool_send(self, user_id: int, uid: int, pool_type: "ZZZBannerType", message: "Message"):
await message.reply_chat_action(ChatAction.TYPING)
png_data = await self.rander_wish_log_analysis(user_id, uid, pool_type)
if isinstance(png_data, str):
reply = await message.reply_text(png_data)
if filters.ChatType.GROUPS.filter(message):
self.add_delete_message_job(reply)
self.add_delete_message_job(message)
else:
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
if png_data.file_type == FileType.DOCUMENT:
await png_data.reply_document(message, filename="调频统计.png")
else:
await png_data.reply_photo(message)
@handler.command(command="signal_log", block=False)
@handler.message(filters=filters.Regex("^调频记录?(光锥|角色|常驻|新手)$"), block=False)
async def command_start_analysis(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None:
user_id = await self.get_real_user_id(update)
uid, offset = self.get_real_uid_or_offset(update)
message = update.effective_message
pool_type = None
if args := self.get_args(context):
if "角色" in args:
pool_type = ZZZBannerType.CHARACTER
elif "武器" in args:
pool_type = ZZZBannerType.WEAPON
elif "常驻" in args:
pool_type = ZZZBannerType.STANDARD
elif "邦布" in args:
pool_type = ZZZBannerType.BANGBOO
self.log_user(update, logger.info, "调频记录命令请求 || 参数 %s", pool_type.name if pool_type else None)
try:
player_id = await self.get_player_id(user_id, uid, offset)
if pool_type is None:
await self.wish_log_pool_choose(user_id, player_id, message)
else:
await self.wish_log_pool_send(user_id, player_id, pool_type, message)
except GachaLogNotFound:
self.log_user(update, logger.info, "未找到调频记录")
buttons = [
[
InlineKeyboardButton(
"点我导入", url=create_deep_linked_url(context.bot.username, "signal_log_import")
)
]
]
await message.reply_text(
WISHLOG_NOT_FOUND,
reply_markup=InlineKeyboardMarkup(buttons),
)
@handler.callback_query(pattern=r"^get_wish_log\|", block=False)
async def get_wish_log(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
callback_query = update.callback_query
user = callback_query.from_user
message = callback_query.message
async def get_wish_log_callback(
callback_query_data: str,
) -> Tuple[str, str, int, int]:
_data = callback_query_data.split("|")
_user_id = int(_data[1])
_uid = int(_data[2])
_t = _data[3]
_result = _data[4]
logger.debug(
"callback_query_data函数返回 result[%s] user_id[%s] uid[%s] show_type[%s]",
_result,
_user_id,
_uid,
_t,
)
return _result, _t, _user_id, _uid
try:
pool, show_type, user_id, uid = await get_wish_log_callback(callback_query.data)
except IndexError:
await callback_query.answer("按钮数据已过期,请重新获取。", show_alert=True)
self.add_delete_message_job(message, delay=1)
return
if user.id != user_id:
await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True)
return
if show_type == "count":
await self.get_wish_log_count(update, user_id, uid, pool)
else:
await self.get_wish_log_log(update, user_id, uid, pool)
async def get_wish_log_log(self, update: "Update", user_id: int, uid: int, pool: str):
callback_query = update.callback_query
message = callback_query.message
pool_type = GACHA_TYPE_LIST_REVERSE.get(pool)
await message.reply_chat_action(ChatAction.TYPING)
try:
png_data = await self.rander_wish_log_analysis(user_id, uid, pool_type)
except GachaLogNotFound:
png_data = "未找到调频记录"
if isinstance(png_data, str):
await callback_query.answer(png_data, show_alert=True)
self.add_delete_message_job(message, delay=1)
else:
await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False)
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
if png_data.file_type == FileType.DOCUMENT:
await png_data.reply_document(message, filename="调频统计.png")
self.add_delete_message_job(message, delay=1)
else:
await png_data.edit_media(message)
async def get_wish_log_count(self, update: "Update", user_id: int, uid: int, pool: str):
callback_query = update.callback_query
message = callback_query.message
all_five = pool == "five"
group = filters.ChatType.GROUPS.filter(message)
pool_type = GACHA_TYPE_LIST_REVERSE.get(pool)
await message.reply_chat_action(ChatAction.TYPING)
try:
if all_five:
png_data = await self.gacha_log.get_all_five_analysis(user_id, uid, self.assets_service)
else:
png_data = await self.gacha_log.get_pool_analysis(user_id, uid, pool_type, self.assets_service, group)
except GachaLogNotFound:
png_data = "未找到调频记录"
if isinstance(png_data, str):
await callback_query.answer(png_data, show_alert=True)
self.add_delete_message_job(message, delay=1)
else:
await self.add_theme_data(png_data, uid)
await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False)
document = False
if png_data["hasMore"] and not group:
document = True
png_data["hasMore"] = False
await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT if document else ChatAction.UPLOAD_PHOTO)
png = await self.template_service.render(
"zzz/gacha_count/gacha_count.html",
png_data,
full_page=True,
query_selector=".body_box",
file_type=FileType.DOCUMENT if document else FileType.PHOTO,
)
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
if document:
await png.reply_document(message, filename="调频统计.png")
self.add_delete_message_job(message, delay=1)
else:
await png.edit_media(message)
async def add_theme_data(self, data: Dict, player_id: int):
res = RESOURCE_DIR / "img"
data["avatar"] = (res / "avatar.png").as_uri()
data["background"] = (res / "home.png").as_uri()
return data
@staticmethod
async def get_migrate_data(
old_user_id: int, new_user_id: int, old_players: List["Player"]
) -> Optional[GachaLogMigrate]:
return await GachaLogMigrate.create(old_user_id, new_user_id, old_players)
async def wish_log_use_by_inline(
self, update: "Update", context: "ContextTypes.DEFAULT_TYPE", pool_type: "ZZZBannerType"
):
callback_query = update.callback_query
user = update.effective_user
user_id = user.id
uid = IInlineUseData.get_uid_from_context(context)
self.log_user(update, logger.info, "调频记录命令请求 || 参数 %s", pool_type.name if pool_type else None)
notice = None
try:
render_result = await self.rander_wish_log_analysis(user_id, uid, pool_type)
if isinstance(render_result, str):
notice = render_result
else:
await render_result.edit_inline_media(callback_query, filename="调频统计.png")
except GachaLogNotFound:
self.log_user(update, logger.info, "未找到调频记录")
notice = "未找到调频记录"
if notice:
await callback_query.answer(notice, show_alert=True)
async def get_inline_use_data(self) -> List[Optional[IInlineUseData]]:
types = {
"代理人": ZZZBannerType.CHARACTER,
"音擎": ZZZBannerType.WEAPON,
"邦布": ZZZBannerType.BANGBOO,
"常驻": ZZZBannerType.STANDARD,
}
data = []
for k, v in types.items():
data.append(
IInlineUseData(
text=f"{k}调频",
hash=f"signal_log_{v.value}",
callback=partial(self.wish_log_use_by_inline, pool_type=v),
player=True,
)
)
return data

View File

@ -10,6 +10,7 @@ from core.services.template.models import RenderResult
from core.services.template.services import TemplateService
from gram_core.plugin.methods.inline_use_data import IInlineUseData
from plugins.tools.genshin import GenshinHelper
from utils.const import RESOURCE_DIR
from utils.log import logger
from utils.uid import mask_number
@ -60,9 +61,6 @@ class PlayerStatsPlugins(Plugin):
uid = client.player_id
user_info = await client.get_zzz_user(uid)
# 因为需要替换线上图片地址为本地地址,先克隆数据,避免修改原数据
user_info = user_info.copy(deep=True)
data = {
"uid": mask_number(uid),
"stats": user_info.stats,
@ -74,11 +72,11 @@ class PlayerStatsPlugins(Plugin):
("式舆防卫战防线", "cur_period_zone_layer_count"),
("获得邦布数", "buddy_num"),
],
"style": random.choice(["mondstadt", "liyue"]), # nosec
"style": "main", # nosec
}
await self.add_theme_data(data, uid)
return await self.template_service.render(
"zzz/stats/stats.jinja2",
"zzz/stats/stats.html",
data,
{"width": 650, "height": 400},
full_page=True,
@ -113,6 +111,12 @@ class PlayerStatsPlugins(Plugin):
return
await render_result.edit_inline_media(callback_query)
async def add_theme_data(self, data, player_id: int):
res = RESOURCE_DIR / "img"
data["avatar"] = (res / "avatar.png").as_uri()
data["background"] = (res / "home.png").as_uri()
return data
async def get_inline_use_data(self) -> List[Optional[IInlineUseData]]:
return [
IInlineUseData(

86
plugins/zzz/weapon.py Normal file
View File

@ -0,0 +1,86 @@
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.constants import ChatAction, ParseMode
from telegram.ext import CallbackContext, filters
from core.plugin import Plugin, handler
from core.services.game.services import GameCacheService
from core.services.search.models import StrategyEntry
from core.services.search.services import SearchServices
from core.services.wiki.services import WikiService
from metadata.shortname import weaponToTag, weaponToName
from utils.log import logger
class WeaponPlugin(Plugin):
"""音擎图鉴查询"""
KEYBOARD = [
[InlineKeyboardButton(text="查看音擎列表并查询", switch_inline_query_current_chat="查看音擎列表并查询")]
]
def __init__(
self,
cache_service: GameCacheService = None,
wiki_service: WikiService = None,
search_service: SearchServices = None,
):
self.cache_service = cache_service
self.wiki_service = wiki_service
self.search_service = search_service
@handler.command(command="weapon", block=False)
@handler.message(filters=filters.Regex("^音擎图鉴查询(.*)"), block=False)
async def command_start(self, update: Update, context: CallbackContext) -> None:
message = update.effective_message
args = self.get_args(context)
if len(args) >= 1:
weapon_name = args[0]
else:
reply_message = await message.reply_text(
"请回复你要查询的音擎名称", reply_markup=InlineKeyboardMarkup(self.KEYBOARD)
)
if filters.ChatType.GROUPS.filter(reply_message):
self.add_delete_message_job(message)
self.add_delete_message_job(reply_message)
return
weapon_name = weaponToName(weapon_name)
file_path = self.wiki_service.raider.raider_light_cone_path / f"{weapon_name}.png"
if not file_path.exists():
reply_message = await message.reply_text(
f"没有找到 {weapon_name} 的图鉴", reply_markup=InlineKeyboardMarkup(self.KEYBOARD)
)
if filters.ChatType.GROUPS.filter(reply_message):
self.add_delete_message_job(message)
self.add_delete_message_job(reply_message)
return
self.log_user(update, logger.info, "查询音擎图鉴命令请求 || 参数 %s", weapon_name)
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
caption = "From 米游社@听语惊花"
if file_id := await self.cache_service.get_weapon_cache(weapon_name):
await message.reply_photo(
photo=file_id,
caption=caption,
filename=f"{weapon_name}.png",
parse_mode=ParseMode.HTML,
)
else:
reply_photo = await message.reply_photo(
photo=open(file_path, "rb"),
caption=caption,
filename=f"{weapon_name}.png",
parse_mode=ParseMode.HTML,
)
if reply_photo.photo:
tags = weaponToTag(weapon_name)
photo_file_id = reply_photo.photo[0].file_id
await self.cache_service.set_weapon_cache(weapon_name, photo_file_id)
entry = StrategyEntry(
key=f"plugin:strategy:{weapon_name}",
title=weapon_name,
description=f"{weapon_name} 音擎图鉴",
tags=tags,
caption=caption,
parse_mode="HTML",
photo_file_id=photo_file_id,
)
await self.search_service.add_entry(entry)

View File

@ -88,7 +88,7 @@ rich==13.7.1
sentry-sdk==2.7.1
setuptools==70.2.0
shellingham==1.5.4
simnet @ git+https://github.com/PaiGramTeam/SIMNet@05fcb568d6c1fe44a4f917c996198bfe62a00053
simnet @ git+https://github.com/PaiGramTeam/SIMNet@074939d8818e6073be4a918b25d7deadd43a5b7b
six==1.16.0
smmap==5.0.1
sniffio==1.3.1

BIN
resources/img/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
resources/img/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 423 KiB

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
<link rel="shortcut icon" href="#"/>
<link rel="stylesheet" type="text/css" href="gacha_count.css"/>
<link rel="preload" href="../../fonts/HYWenHei-85W.ttf" as="font">
<link rel="preload" href="./../../fonts/tttgbnumber.ttf" as="font">
<link rel="preload" href="./../abyss/background/roleStarBg5.png" as="image">
<link rel="preload" href="./../abyss/background/roleStarBg4.png" as="image">
<style>
.head_box {
background-position-x: 42px;
background: #fff url(../gacha_log/img/提纳里.png) no-repeat;
background-size: auto 101%;
}
</style>
<title></title>
</head>
<body id="container" class="body_box">
<div class="container">
<div class="head_box">
<div class="id_text">ID: 10001</div>
<h2 class="day_text">抽卡统计-角色祈愿</h2>
<img class="genshin_logo" src="./../../bot/help/background/genshin.png"/>
</div>
<div class="pool_box">
<div class="title_box">
<div class="name_box">
<div class="title"><h2>「枫原万叶、可莉」</h2></div>
<span class="label label_301">98抽</span>
</div>
<span class="date">2022-08-02 - 2022-08-02</span>
</div>
<div class="list_box">
<div class="item">
<div class="bg5"></div>
<span class="num life5">20</span>
<img class="role_img" src=""/>
</div>
</div>
</div>
<div class="hasMore">*完整数据请私聊查看</div>
<div class="logo">Template By Yunzai-Bot</div>
</div>
</body>
</html>

View File

@ -0,0 +1,226 @@
@font-face {
font-family: "tttgbnumber";
src: url("./../../fonts/tttgbnumber.ttf");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "HYWenHei-55W";
src: url("../../fonts/HYWenHei-85W.ttf");
font-weight: normal;
font-style: normal;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
.header {
/*background: #e0dad3 url(../gacha_log/img/starrail.png) no-repeat right;*/
box-shadow: 0 0 8px #72a2ae79;
background-size: cover;
background-position: top;
}
.frame {
border-color: #cdbea8;
color: white;
}
body {
font-size: 16px;
width: 1286px;
color: #1E1F20;
transform: scale(1.5);
transform-origin: 0 0;
}
.container {
width: 1286px;
padding: 20px 15px 10px 15px;
background-color: #F5F6FB;
}
.head_box {
border-radius: 15px;
font-family: tttgbnumber, serif;
padding: 10px 20px;
position: relative;
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
}
.head_box .id_text {
font-size: 24px;
}
.head_box .day_text {
font-size: 20px;
}
.head_box .starrail_logo {
position: absolute;
top: 5px;
right: 30px;
width: 120px;
}
.base_info {
position: relative;
padding-left: 10px;
}
.uid {
font-family: tttgbnumber, serif;
}
.pool_box {
font-family: HYWenHei-55W, serif;
border-radius: 12px;
margin-top: 20px;
margin-bottom: 20px;
padding: 10px 5px 5px 5px;
background: #FFF;
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
position: relative;
}
.title_box {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.title {
white-space: nowrap;
max-width: 210px;
overflow: hidden;
}
.name_box {
display: flex;
align-items: center;
flex: 1;
}
.title_box .date {
margin-right: 10px;
}
.list_box {
display: flex;
flex-wrap: wrap;
}
.item {
margin: 0 0 10px 10px;
border-radius: 7px;
overflow: hidden;
box-shadow: 0 2px 6px 0 rgb(132 93 90 / 30%);
height: 70px;
width: 70px;
background: #E9E5DC;
position: relative;
}
.item .role_img {
width: 100%;
overflow: hidden;
background-size: 100%;
background-repeat: no-repeat;
position: absolute;
top: 0;
/* filter: contrast(95%); */
}
.item .num {
position: absolute;
top: 0;
right: 0;
z-index: 9;
font-size: 18px;
text-align: center;
color: #FFF;
border-radius: 3px;
padding: 1px 5px;
background: rgb(0 0 0 / 50%);
font-family: "tttgbnumber", serif;
}
.label_301 {
background-color: rgb(235 106 75);
}
.label_302 {
background-color: #E69449;
}
.label_200 {
background-color: #757CC8;
}
.label {
color: #FFF;
border-radius: 10px;
font-size: 16px;
padding: 2px 7px;
vertical-align: 2px;
}
.bg5 {
background-image: url(./../../genshin/abyss/background/roleStarBg5.png);
width: 100%;
height: 70px;
/* filter: brightness(1.1); */
background-size: 100%;
background-repeat: no-repeat;
}
.bg4 {
width: 100%;
height: 70px;
background-image: url(./../../genshin/abyss/background/roleStarBg4.png);
background-size: 100%;
background-repeat: no-repeat;
}
.list_box .item .life1 {
background-color: #62A8EA;
}
.list_box .item .life2 {
background-color: #62A8EA;
}
.list_box .item .life3 {
background-color: #45B97C;
}
.list_box .item .life4 {
background-color: #45B97C;
}
.list_box .item .life5 {
background-color: #FF5722;
}
.list_box .item .life6 {
background-color: #FF5722;
}
.logo {
font-size: 14px;
font-family: "tttgbnumber", serif;
text-align: center;
color: #7994A7;
}
.hasMore {
font-size: 12px;
margin: -6px 0 10px 6px;
color: #7F858A;
}

View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
<link rel="shortcut icon" href="#"/>
<link rel="stylesheet" type="text/css" href="gacha_count.css"/>
<link rel="preload" href="../../fonts/HYWenHei-85W.ttf" as="font">
<link rel="preload" href="./../../fonts/tttgbnumber.ttf" as="font">
<link rel="preload" href="./../../genshin/abyss/background/roleStarBg5.png" as="image">
<link rel="preload" href="./../../genshin/abyss/background/roleStarBg4.png" as="image">
<link rel="preload" href="../gacha_log/img/starrail.png" as="image" />
<script src="../../js/tailwindcss-3.1.8.js"></script>
<title></title>
</head>
<body id="container" class="body_box">
<div class="container">
<div class="info_box">
<div class="header p-2 rounded-xl mb-6" style='background-image: url("{{ background }}")'>
<div class="frame p-4 rounded-lg border-solid border-2 flex items-center">
<img class="w-16 h-16 rounded-full mr-4" src="{{ avatar }}" alt="Avatar">
<div>
<h2 class="font-bold italic">ID: {{ uid }}</h2>
<h2 class="italic">
跃迁统计-{{ typeName }}
</h2>
</div>
</div>
</div>
</div>
{% for val in pool %}
<div class="pool_box">
<div class="title_box">
<div class="name_box">
<div class="title text-lg font-bold"><h2>「{{ val.name }}」</h2></div>
<span class="label label_301">{{ val.count }}抽</span>
</div>
{% if typeName != "常驻跃迁" %}
<span class="date">{{ val.start }} - {{ val.end }}</span>
{% endif %}
</div>
<div class="list_box">
{% for v in val.list %}
<div class="item">
<div class="bg{{ v.rank_type }}"></div>
<span class="num {% if v.count>=5 and v.rank_type == 5 %}life5{% endif %}">{{ v.count }}</span>
<img class="role_img" src="{{ v.icon }}" alt=""/>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% if hasMore %}
<div class="hasMore">*完整数据请私聊查看</div>
{% endif %}
<div class="logo">Template By Yunzai-Bot x Generated By PamGram</div>
</div>
</body>
</html>

View File

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
<link rel="shortcut icon" href="#"/>
<link rel="stylesheet" type="text/css" href="gacha_log.css"/>
<link rel="preload" href="./../../fonts/tttgbnumber.ttf" as="font">
<link rel="preload" href="./img/提纳里.png" as="image">
<link rel="preload" href="./../abyss/background/roleStarBg5.png" as="image">
<style>
.head_box {
background-position-x: 42px;
background: #fff url(./img/提纳里.png) no-repeat;
background-size: auto 101%;
}
</style>
<title></title>
</head>
<body id="container" class="body_box">
<div class="container">
<div class="info_box">
<div class="head_box">
<div class="id_text">
ID: 10001
</div>
<h2 class="day_text">
81抽
<span class="label label_301">角色祈愿池 · 欧</span>
</h2>
<img class="genshin_logo" src="./../../bot/help/background/genshin.png" alt=""/>
</div>
<div class="data_box">
<div class="tab_lable">数据总览</div>
<div class="data_line">
<div class="data_line_item">
<div class="num">1<span class="unit"></span></div>
<div class="lable">未出五星</div>
</div>
</div>
<div class="line_box">
<span class="line"></span>
<span class="text">五星历史 2022-10-07 01:10 ~ 2022-10-07 23:10</span>
<span class="line"></span>
</div>
<div class="card_list">
<div class="item star5">
<span class="minimum">UP</span>
<img class="role" src="" alt=""/>
<!-- <div class="num">{{val.num}}</div>-->
<div class="num_name">80</div>
</div>
</div>
<div class="line_box">
<span class="line"></span>
<span class="text">四星最近历史</span>
<span class="line"></span>
</div>
<div class="card_list">
<div class="item star4">
<img class="role" src="" alt=""/>
<!-- <div class="num">{{val.num}}</div>-->
<div class="num_name">10</div>
</div>
</div>
</div>
<div class="logo"> Template By Yunzai-Bot</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,345 @@
@font-face {
font-family: "tttgbnumber";
src: url("./../../fonts/tttgbnumber.ttf");
font-weight: normal;
font-style: normal;
}
.header {
/*background: #e0dad3 url(./img/starrail.png) no-repeat right;*/
box-shadow: 0 0 8px #72a2ae79;
background-size: cover;
background-position: top;
}
.frame {
border-color: #cdbea8;
color: white;
}
body {
font-size: 18px;
color: #1e1f20;
font-family: PingFangSC-Medium, PingFang SC, sans-serif;
transform: scale(1.5);
transform-origin: 0 0;
width: 980px
}
.container {
width: 980px;
padding: 20px 15px 10px 15px;
background-color: #f5f6fb;
}
.head_box {
border-radius: 9999px;
font-family: tttgbnumber, sans-serif;
padding: 10px 20px;
position: relative;
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
}
.head_box .id_text {
font-size: 24px;
}
.head_box .day_text {
font-size: 20px;
}
.head_box .starrail_logo {
position: absolute;
top: 10px;
right: 30px;
width: 120px;
}
.logo {
font-size: 12px;
font-family: "tttgbnumber", serif;
text-align: center;
color: #7994a7;
position: relative;
padding-left: 10px;
}
.data_box {
border-radius: 15px;
margin-top: 20px;
margin-bottom: 10px;
padding: 20px 0 5px 10px;
background: #fff;
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
position: relative;
}
.tab_lable {
position: absolute;
top: -10px;
left: -8px;
background: #d4b98c;
color: #fff;
font-size: 14px;
padding: 3px 10px;
border-radius: 15px 0 15px 15px;
z-index: 20;
}
.data_line {
display: flex;
justify-content: space-around;
margin-bottom: 14px;
padding-right: 10px;
}
.data_line_item {
width: 100px;
text-align: center;
/* margin: 0 20px; */
}
.num {
font-family: tttgbnumber, serif;
font-size: 24px;
}
.num .unit {
font-size: 12px;
}
.data_box .lable {
font-size: 14px;
color: #7f858a;
line-height: 1;
margin-top: 3px;
}
.info_box_border {
border-radius: 15px;
/* margin-top: 20px; */
margin-bottom: 20px;
padding: 6px 0 5px 10px;
background: #fff;
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
position: relative;
}
.card_list {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
}
.card_list .item {
margin: 0 8px 10px 0;
border-radius: 7px;
box-shadow: 0 2px 6px 0 rgb(132 93 90 / 30%);
height: 90px;
position: relative;
overflow: hidden;
background: #e7e5d9;
}
.card_list .item img {
width: 70px;
height: 70px;
border-radius: 7px 7px 20px 0;
}
.card_list .item.star5 img {
background-image: url(./../../genshin/abyss/background/roleStarBg5.png);
width: 100%;
height: 70px;
/* filter: brightness(1.1); */
background-size: 100%;
background-repeat: no-repeat;
}
.card_list .item.star4 img {
width: 100%;
height: 70px;
background-image: url(./../../genshin/abyss/background/roleStarBg4.png);
background-size: 100%;
background-repeat: no-repeat;
}
.card_list .item .num {
position: absolute;
top: 0;
right: 0;
z-index: 9;
font-size: 18px;
text-align: center;
color: #fff;
border-radius: 3px;
padding: 1px 5px;
background: rgb(0 0 0 / 50%);
font-family: "tttgbnumber", serif;
}
.card_list .item .name,
.card_list .item .num_name {
position: absolute;
top: 71px;
left: 0;
z-index: 9;
font-size: 12px;
text-align: center;
width: 100%;
height: 16px;
line-height: 18px;
}
.card_list .item .num_name {
font-family: "tttgbnumber", serif;
font-size: 16px;
}
.base_info {
position: relative;
padding-left: 10px;
margin: 5px 10px;
}
.uid::before {
content: " ";
position: absolute;
width: 5px;
height: 24px;
border-radius: 1px;
left: 0;
top: 0;
background: #d3bc8d;
}
.label_301 {
background-color: rgb(235 106 75);
}
.label_302 {
background-color: #e69449;
}
.label_200 {
background-color: #757cc8;
}
.label {
color: #fff!important;
border-radius: 10px;
font-size: 12px;
padding: 2px 7px;
vertical-align: 2px;
}
.ritem {
display: flex;
font-size: 12px;
margin-bottom: 5px;
}
.info_role {
display: flex;
flex-wrap: wrap;
padding: 0 0 5px 9px;
}
.ritem .role {
width: 20px;
height: 20px;
background-color: #ffb285;
border-radius: 100%;
}
.ritem .weapon_box {
overflow: hidden;
width: 20px;
height: 20px;
border-radius: 100%;
}
.ritem .weapon {
width: 20px;
height: 20px;
background-color: #ffb285;
border-radius: 100%;
transform: scale(1.5);
-webkit-transform: scale(1.5);
}
.ritem .role_text {
margin: 2px 3px 0 2px;
display: flex;
align-items: baseline;
}
.ritem .role_name {
width: 24px;
white-space: nowrap;
overflow: hidden;
}
.ritem .role_num {
width: 24px;
}
.line_box {
height: 32px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #7d7d7d;
padding-bottom: 5px;
}
.line_box .line {
height: 2px;
flex-grow: 1;
background-color: #ebebeb;
margin: 0 10px;
}
.red {
color: #f21000;
}
.orange {
color: #ff8d00;
}
.green {
color: #12d88c;
}
.blue {
color: #4169e1;
}
.purple {
color: #7500ff;
}
.minimum {
position: absolute;
top: 0;
right: 0;
z-index: 9;
font-size: 12px;
text-align: center;
color: #fff;
border-radius: 3px;
padding: 1px 3px;
background-color: rgb(0 0 0 / 80%);
font-family: "tttgbnumber", serif;
}
.hasMore {
font-size: 12px;
margin: 6px 0;
color: #7f858a;
}

View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<link rel="stylesheet" type="text/css" href="gacha_log.css" />
<link type="text/css" href="../../styles/public.css" rel="stylesheet" />
<link rel="preload" href="./img/starrail.png" as="image" />
<script src="../../js/tailwindcss-3.1.8.js"></script>
<title>Title</title>
</head>
<body id="container" class="body_box">
<div class="container">
<div class="info_box">
<div class="header p-2 rounded-xl mb-6" style='background-image: url("{{ background }}")'>
<div class="frame p-4 rounded-lg border-solid border-2 flex items-center">
<img class="w-16 h-16 rounded-full mr-4" src="{{ avatar }}" alt="Avatar">
<div>
<h2 class="font-bold italic">ID: {{ uid }}</h2>
<h2 class="italic">
{{ allNum }}抽
<span class="label text-neutral-600 label_{{type}}"
>{{ typeName }}</span
>
</h2>
</div>
</div>
</div>
<div class="data_box">
<div class="tab_lable">数据总览</div>
{% for val in line %}
<div class="data_line">
{% for item in val %}
<div class="data_line_item">
<div class="num">
{{item.num}}<span class="unit">{{item.unit}}</span>
</div>
<div class="lable">{{item.lable}}</div>
</div>
{% endfor %}
</div>
{% endfor %}
<div class="line_box">
<span class="line"></span>
<span class="text">五星历史 {{firstTime}} ~ {{lastTime}}</span>
<span class="line"></span>
</div>
<div class="card_list">
{% for val in fiveLog %}
<div class="item star5">
{% if val.isUp %}
<span class="minimum">UP</span>
{% endif %}
<img class="role" src="{{ val.icon }}" alt="" />
<!-- <div class="num">{{val.num}}</div>-->
<div class="num_name">{{ val.count }}</div>
</div>
{% endfor %}
</div>
<div class="line_box">
<span class="line"></span>
<span class="text">四星最近历史</span>
<span class="line"></span>
</div>
<div class="card_list">
{% for val in fourLog %}
<div class="item star4">
<img class="role" src="{{ val.icon }}" alt="" />
<!-- <div class="num">{{val.num}}</div>-->
<div class="num_name">{{ val.count }}</div>
</div>
{% endfor %}
</div>
</div>
<div class="logo">Template By Yunzai-Bot x Generated By PamGram</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

View File

@ -1,62 +0,0 @@
:root {
--primary: #ffeabd;
}
body {
background-color: #f5f6fb;
}
.header {
background-image: url(../background/liyue.png);
box-shadow: 0 0 16px rgb(255 233 144 / 50%);
}
.box {
background-color: #9c433d;
box-shadow: 0 0 16px rgb(255 233 144 / 50%);
}
.box-title {
background-color: rgb(255, 200, 122, 0.1);
--tw-ring-color: #ff9966;
}
.pointer-bar {
width: 95%;
height: 8px;
display: inline-block;
background-color: rgb(0, 0, 0, 0.1);
border-radius: 0.25rem;
}
.pointer-progress-bar {
border-radius: 0.25rem;
height: 100%;
background: linear-gradient(to bottom, #f5efcd, #f8eabd, #ffdf90);
}
.name {
background: linear-gradient(to bottom, #ffffff, #ffeabd, #ffdf90);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
.uid {
color: var(--primary);
background: linear-gradient(to right, rgb(0, 0, 0, 0), #cc6666, rgb(0, 0, 0, 0));
}
.about {
background-color: #e0dad3;
color: #8a4d30;
}
.box-stats {
color: var(--primary);
}
.box-stats-label {
color: var(--primary) !important;
opacity: 0.65;
}

View File

@ -1,256 +0,0 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Title</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<link href="./liyue.css" rel="stylesheet" />
<link type="text/css" href="../../../styles/public.css" rel="stylesheet" />
<script src="../../../js/tailwindcss-3.1.8.js"></script>
</head>
<body class="text-neutral-600">
<div class="mx-auto max-w-[600px] py-8">
<div class="header p-6 flex mb-8 rounded-xl bg-cover justify-between">
<div class="flex flex-col items-center justify-center">
<h1 class="text-4xl italic name mb-2 px-2">
小何
<span class="text-lg">lv.58</span>
</h1>
<h1 class="italic uid px-10">UID - 125324176</h1>
</div>
</div>
<div class="box pt-4 rounded-xl overflow-hidden">
<div>
<h2 class="box-title text-center text-xl ring text-neutral-100 p-1">
数据总览
</h2>
<div class="p-6 grid grid-cols-4 gap-4 text-center">
<div class="">
<div class="text-xl box-stats">493</div>
<div class="text-neutral-400 box-stats-label">活跃天数</div>
</div>
<div class="">
<div class="text-xl box-stats">536</div>
<div class="text-neutral-400 box-stats-label">成就达成数</div>
</div>
<div class="">
<div class="text-xl box-stats">38</div>
<div class="text-neutral-400 box-stats-label">获取角色数</div>
</div>
<div class="">
<div class="text-xl box-stats">12-3</div>
<div class="text-neutral-400 box-stats-label">深境螺旋</div>
</div>
<div class="">
<div class="text-xl box-stats">227</div>
<div class="text-neutral-400 box-stats-label">解锁传送点</div>
</div>
<div class="">
<div class="text-xl box-stats">41</div>
<div class="text-neutral-400 box-stats-label">解锁秘境</div>
</div>
<div class="">
<div class="text-xl box-stats">58</div>
<div class="text-neutral-400 box-stats-label">奇馈宝箱数</div>
</div>
<div class="">
<div class="text-xl box-stats">128</div>
<div class="text-neutral-400 box-stats-label">华丽宝箱数</div>
</div>
<div class="">
<div class="text-xl box-stats">316</div>
<div class="text-neutral-400 box-stats-label">珍贵宝箱数</div>
</div>
<div class="">
<div class="text-xl box-stats">1184</div>
<div class="text-neutral-400 box-stats-label">精致宝箱数</div>
</div>
<div class="">
<div class="text-xl box-stats">1594</div>
<div class="text-neutral-400 box-stats-label">普通宝箱数</div>
</div>
<div class="">
<div class="text-xl box-stats">65</div>
<div class="text-neutral-400 box-stats-label">风神瞳</div>
</div>
<div class="">
<div class="text-xl box-stats">131</div>
<div class="text-neutral-400 box-stats-label">岩神瞳</div>
</div>
<div class="">
<div class="text-xl box-stats">180</div>
<div class="text-neutral-400 box-stats-label">雷神瞳</div>
</div>
<div class="">
<div class="text-xl box-stats">80</div>
<div class="text-neutral-400 box-stats-label">草神瞳</div>
</div>
</div>
</div>
<div>
<h2 class="box-title text-center text-xl ring text-neutral-100 p-1">
世界探索
</h2>
<div class="p-6 grid grid-cols-4 gap-4 text-center text-neutral-100">
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="
background-image: url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_Xumi.png');
"
>
<img
class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_Xumi.png"
/>
<div class="text-sm w-full truncate">须弥</div>
<div class="text-xs">28.0%</div>
<div class="pointer-bar">
<div class="pointer-progress-bar" style="width: 28%"></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="
background-image: url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_ChasmsMaw.png');
"
>
<img
class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_ChasmsMaw.png"
/>
<div class="text-sm w-full truncate">层岩巨渊·地下矿区</div>
<div class="text-xs">98.7%</div>
<div class="pointer-bar">
<div class="pointer-progress-bar" style="width: 98.7%"></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="
background-image: url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_ChasmsMaw.png');
"
>
<img
class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_ChasmsMaw.png"
/>
<div class="text-sm w-full truncate">层岩巨渊</div>
<div class="text-xs">92.9%</div>
<div class="pointer-bar">
<div class="pointer-progress-bar" style="width: 92.9%"></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="
background-image: url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_Enkanomiya.png');
"
>
<img
class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_Enkanomiya.png"
/>
<div class="text-sm w-full truncate">渊下宫</div>
<div class="text-xs">98.3%</div>
<div class="pointer-bar">
<div class="pointer-progress-bar" style="width: 98.3%"></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="
background-image: url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_Daoqi.png');
"
>
<img
class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_Daoqi.png"
/>
<div class="text-sm w-full truncate">稻妻</div>
<div class="text-xs">100.0%</div>
<div class="pointer-bar">
<div class="pointer-progress-bar" style="width: 100%"></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="
background-image: url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_Dragonspine.png');
"
>
<img
class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_Dragonspine.png"
/>
<div class="text-sm w-full truncate">龙脊雪山</div>
<div class="text-xs">83.6%</div>
<div class="pointer-bar">
<div class="pointer-progress-bar" style="width: 83.6%"></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="
background-image: url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_Liyue.png');
"
>
<img
class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_Liyue.png"
/>
<div class="text-sm w-full truncate">璃月</div>
<div class="text-xs">95.9%</div>
<div class="pointer-bar">
<div class="pointer-progress-bar" style="width: 95.9%"></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="
background-image: url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_Mengde.png');
"
>
<img
class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_Mengde.png"
/>
<div class="text-sm w-full truncate">蒙德</div>
<div class="text-xs">100.0%</div>
<div class="pointer-bar">
<div class="pointer-progress-bar" style="width: 100%"></div>
</div>
</div>
</div>
</div>
<div class="about text-center leading-8 text-xs opacity-50">
所有数据会有一小时延迟 以游戏内为准 此处仅供参考
</div>
</div>
</div>
</body>
</html>

View File

@ -1,49 +0,0 @@
body {
background-color: #f5f6fb;
}
.header {
background-image: url(../background/mondstadt.png);
box-shadow: 0 0 8px rgb(123 242 248 / 50%);
}
.box {
background-color: #fdfdf3;
box-shadow: 0 0 8px rgb(123 242 248 / 50%);
}
.box-title {
background-color: #43849abb;
--tw-ring-color: #43849a;
}
.pointer-bar {
width: 95%;
height: 8px;
display: inline-block;
background-color: rgb(0, 0, 0, 0.2);
border-radius: 0.25rem;
}
.pointer-progress-bar {
border-radius: 0.25rem;
height: 100%;
background: #fff6e2;
}
.name {
background: linear-gradient(to bottom, #66bbee, #5ddddd, #55dddd);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
.uid {
color: #fff;
background: linear-gradient(to right, rgb(0, 0, 0, 0), #5ddddd, rgb(0, 0, 0, 0));
}
.about {
background-color: #e0dad3;
color: #8a4d30;
}

View File

@ -1,261 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Title</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<link href="./mondstadt.css" rel="stylesheet" />
<link type="text/css" href="../../../styles/public.css" rel="stylesheet" />
<script src="../../../js/tailwindcss-3.1.8.js"></script>
</head>
<body class="text-neutral-600">
<div class="mx-auto max-w-[600px] py-8">
<div class="header p-6 flex mb-8 rounded-xl bg-cover justify-between">
<div class="flex flex-col items-center justify-center">
<h1 class="text-4xl italic name mb-2 px-2">
小何
<span class="text-lg">lv.58</span>
</h1>
<h1 class="italic uid px-10">UID - 125324176</h1>
</div>
</div>
<div class="box pt-4 rounded-xl overflow-hidden">
<div>
<h2 class="box-title text-center text-xl ring text-neutral-100 p-1">
数据总览
</h2>
<div class="p-6 grid grid-cols-4 gap-4 text-center">
<div class="">
<div class="text-xl">491</div>
<div class="text-neutral-400">活跃天数</div>
</div>
<div class="">
<div class="text-xl">536</div>
<div class="text-neutral-400">成就达成数</div>
</div>
<div class="">
<div class="text-xl">38</div>
<div class="text-neutral-400">获取角色数</div>
</div>
<div class="">
<div class="text-xl">12-3</div>
<div class="text-neutral-400">深境螺旋</div>
</div>
<div class="">
<div class="text-xl">227</div>
<div class="text-neutral-400">解锁传送点</div>
</div>
<div class="">
<div class="text-xl">41</div>
<div class="text-neutral-400">解锁秘境</div>
</div>
<div class="">
<div class="text-xl">58</div>
<div class="text-neutral-400">奇馈宝箱数</div>
</div>
<div class="">
<div class="text-xl">127</div>
<div class="text-neutral-400">华丽宝箱数</div>
</div>
<div class="">
<div class="text-xl">316</div>
<div class="text-neutral-400">珍贵宝箱数</div>
</div>
<div class="">
<div class="text-xl">1180</div>
<div class="text-neutral-400">精致宝箱数</div>
</div>
<div class="">
<div class="text-xl">1591</div>
<div class="text-neutral-400">普通宝箱数</div>
</div>
<div class="">
<div class="text-xl">65</div>
<div class="text-neutral-400">风神瞳</div>
</div>
<div class="">
<div class="text-xl">131</div>
<div class="text-neutral-400">岩神瞳</div>
</div>
<div class="">
<div class="text-xl">180</div>
<div class="text-neutral-400">雷神瞳</div>
</div>
<div class="">
<div class="text-xl">79</div>
<div class="text-neutral-400">草神瞳</div>
</div>
</div>
</div>
<div>
<h2 class="box-title text-center text-xl ring text-neutral-100 p-1">
世界探索
</h2>
<div class="p-6 grid grid-cols-4 gap-4 text-center text-neutral-100">
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="background-image:
url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_Xumi.png');"
>
<img class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_Xumi.png" />
<div class="text-sm w-full truncate">须弥</div>
<div class="text-xs">26.0%</div>
<div class="pointer-bar">
<div
class="pointer-progress-bar"
style="width: 26.0%"
></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="background-image:
url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_ChasmsMaw.png');"
>
<img class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_ChasmsMaw.png" />
<div class="text-sm w-full truncate">层岩巨渊·地下矿区</div>
<div class="text-xs">98.7%</div>
<div class="pointer-bar">
<div
class="pointer-progress-bar"
style="width: 98.7%"
></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="background-image:
url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_ChasmsMaw.png');"
>
<img class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_ChasmsMaw.png" />
<div class="text-sm w-full truncate">层岩巨渊</div>
<div class="text-xs">92.9%</div>
<div class="pointer-bar">
<div
class="pointer-progress-bar"
style="width: 92.9%"
></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="background-image:
url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_Enkanomiya.png');"
>
<img class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_Enkanomiya.png" />
<div class="text-sm w-full truncate">渊下宫</div>
<div class="text-xs">98.3%</div>
<div class="pointer-bar">
<div
class="pointer-progress-bar"
style="width: 98.3%"
></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="background-image:
url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_Daoqi.png');"
>
<img class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_Daoqi.png" />
<div class="text-sm w-full truncate">稻妻</div>
<div class="text-xs">100.0%</div>
<div class="pointer-bar">
<div
class="pointer-progress-bar"
style="width: 100.0%"
></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="background-image:
url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_Dragonspine.png');"
>
<img class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_Dragonspine.png" />
<div class="text-sm w-full truncate">龙脊雪山</div>
<div class="text-xs">83.6%</div>
<div class="pointer-bar">
<div
class="pointer-progress-bar"
style="width: 83.6%"
></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="background-image:
url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_Liyue.png');"
>
<img class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_Liyue.png" />
<div class="text-sm w-full truncate">璃月</div>
<div class="text-xs">95.9%</div>
<div class="pointer-bar">
<div
class="pointer-progress-bar"
style="width: 95.9%"
></div>
</div>
</div>
<div
class="w-full flex flex-col items-center rounded-lg p-2 space-y-1 bg-cover"
style="background-image:
url('https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterCover_Mengde.png');"
>
<img class="w-3/5"
src="https://upload-bbs.mihoyo.com/game_record/genshin/city_icon/UI_ChapterIcon_Mengde.png" />
<div class="text-sm w-full truncate">蒙德</div>
<div class="text-xs">100.0%</div>
<div class="pointer-bar">
<div
class="pointer-progress-bar"
style="width: 100.0%"
></div>
</div>
</div>
</div>
</div>
<div class="about text-center leading-8 text-xs opacity-50">
所有数据会有一小时延迟 以游戏内为准 此处仅供参考
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

View File

@ -1,62 +0,0 @@
:root {
--primary: #ffeabd;
}
body {
background-color: #f5f6fb;
}
.header {
background-image: url(./background/liyue.png);
box-shadow: 0 0 16px rgb(255 233 144 / 50%);
}
.box {
background-color: #9c433d;
box-shadow: 0 0 16px rgb(255 233 144 / 50%);
}
.box-title {
background-color: rgb(255, 200, 122, 0.1);
--tw-ring-color: #ff9966;
}
.pointer-bar {
width: 95%;
height: 8px;
display: inline-block;
background-color: rgb(0, 0, 0, 0.1);
border-radius: 0.25rem;
}
.pointer-progress-bar {
border-radius: 0.25rem;
height: 100%;
background: linear-gradient(to bottom, #f5efcd, #f8eabd, #ffdf90);
}
.name {
background: linear-gradient(to bottom, #ffffff, #ffeabd, #ffdf90);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
.uid {
color: var(--primary);
background: linear-gradient(to right, rgb(0, 0, 0, 0), #cc6666, rgb(0, 0, 0, 0));
}
.about {
background-color: #e0dad3;
color: #8a4d30;
}
.box-stats {
color: var(--primary);
}
.box-stats-label {
color: var(--primary) !important;
opacity: 0.65;
}

View File

@ -0,0 +1,56 @@
body {
background-color: #f5f6fb;
}
.header {
/*background-image: url(../../bot/help/background/header.png);*/
box-shadow: 0 0 8px #72a2ae79;
}
.box {
background-color: #f4f2e4;
box-shadow: 0 0 8px #72a2ae79;
}
.pointer-bar {
width: 95%;
height: 8px;
display: inline-block;
background-color: rgb(0, 0, 0, 0.2);
border-radius: 0.25rem;
}
.pointer-progress-bar {
border-radius: 0.25rem;
height: 100%;
background: #fff6e2;
}
.name {
color: #ffffee;
text-shadow: 0 0.08em 0.1em #00000093, 0 0.1em 0.3em rgba(0, 0, 0, 0.4);
}
.uid {
color: #fff;
background: linear-gradient(
to right,
rgb(0, 0, 0, 0),
#3f7587 25%,
#3f7587 75%,
rgb(0, 0, 0, 0)
);
}
.about {
background-color: #e0dad3;
color: #8a4d30;
}
.frame-pic {
border-color: #fdfdf356;
}
.frame {
border-color: #cdbea8;
}

View File

@ -1,49 +0,0 @@
body {
background-color: #f5f6fb;
}
.header {
background-image: url(background/mondstadt.png);
box-shadow: 0 0 8px rgb(123 242 248 / 50%);
}
.box {
background-color: #fdfdf3;
box-shadow: 0 0 8px rgb(123 242 248 / 50%);
}
.box-title {
background-color: #43849abb;
--tw-ring-color: #43849a;
}
.pointer-bar {
width: 95%;
height: 8px;
display: inline-block;
background-color: rgb(0, 0, 0, 0.2);
border-radius: 0.25rem;
}
.pointer-progress-bar {
border-radius: 0.25rem;
height: 100%;
background: #fff6e2;
}
.name {
background: linear-gradient(to bottom, #66bbee, #5ddddd, #55dddd);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
.uid {
color: #fff;
background: linear-gradient(to right, rgb(0, 0, 0, 0), #5ddddd, rgb(0, 0, 0, 0));
}
.about {
background-color: #e0dad3;
color: #8a4d30;
}

View File

@ -1,92 +0,0 @@
body {
background-repeat: no-repeat;
background-size: cover;
background-position: center;
background-attachment: fixed;
}
#container {
}
.account-center-header {
padding: 10px 8px;
background-color: rgba(225, 225, 225, 0.5);
/*background-image: url("./0.jpg");
background-size: cover;*/
}
.user-avatar {
width: 100%;
height: 100%;
}
.user-info-1 {
padding: 10px 8px;
background-color: rgba(225, 225, 225, 0.5);
}
.world-exploration {
padding: 10px 8px;
background-color: rgba(225, 225, 225, 0.5);
}
.teapot {
padding: 10px 8px;
background-color: rgba(225, 225, 225, 0.5);
}
.account-center-header-avatar {
width: 120px;
height: 120px;
}
.teapot-info-icon {
height: 96px;
overflow: hidden;
}
.teapot-info-img {
width: 100%;
}
.world-exploration-info {
border: 2px solid rgb(0, 0, 0, 0.2);
}
.world-exploration-info-icon {
height: 96px;
overflow: hidden;
}
.world-exploration-info-img {
filter: brightness(0);
height: 100%;
}
.world-exploration-info-hr-1 {
width: 95%;
}
.world-exploration-info-pointer-bar-body {
width: 95%;
height: 4px;
display: inline-block;
border-radius: 2px;
background-color: rgb(0, 0, 0, 0.3);
}
.world-exploration-info-pointer-progress-bar {
height: 100%;
background-color: #000000;
border-radius: 2px;
}
.background-color {
background-color: rgb(225, 225, 225, 0.75);
}
.teapot-info-name {
background-color: rgba(225, 225, 225, 0.5);
}

View File

@ -1,183 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
<title>Title</title>
<link type="text/css" href="../../styles/tailwind.min.css" rel="stylesheet">
<link type="text/css" href="./info.css" rel="stylesheet">
<link type="text/css" href="../../styles/public.css" rel="stylesheet">
<style>
body {
background-image: url({{background_image}});
}
</style>
</head>
<body>
<div class="background-color">
<div class="container mx-auto px-4 py-4" id="container">
<div class="account-center-header flex rounded-xl mb-4">
<div class="account-center-user p-4 flex-grow">
<div class="account-center-user-title pb-4">
<span class="account-center-user-name text-4xl">{{name}}</span>
</div>
<div class="account-center-user-uid pb-4">
<span class="account-center-user-name text-2xl">{{uid}}</span>
</div>
</div>
<div class="account-center-header-avatar mr-8 mt-2 flex-shrink">
<img class="user-avatar rounded-full " src="{{user_avatar}}">
</div>
</div>
<div class="user-info-1 rounded-xl text-center mb-4">
<div class="base-user-info-1 flex flex-wrap text-center">
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">活跃天数</div>
<div class="user-base-info-value text-4xl p-1 ">{{action_day_number}}</div>
</div>
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">成就达成数</div>
<div class="user-base-info-value text-4xl p-1 ">{{achievement_number}}</div>
</div>
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">获取角色数</div>
<div class="user-base-info-value text-4xl p-1 ">{{avatar_number}}</div>
</div>
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">深境螺旋</div>
<div class="user-base-info-value text-4xl p-1 ">{{spiral_abyss}}</div>
</div>
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">解锁传送点</div>
<div class="user-base-info-value text-4xl p-1 ">{{way_point_number}}</div>
</div>
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">解锁秘境</div>
<div class="user-base-info-value text-4xl p-1 ">{{domain_number}}</div>
</div>
</div>
<div class="base-user-info-2 flex flex-wrap text-center">
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">奇馈宝箱数</div>
<div class="user-base-info-value text-4xl p-1 ">{{magic_chest_number}}</div>
</div>
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">华丽宝箱数</div>
<div class="user-base-info-value text-4xl p-1 ">{{luxurious_number}}</div>
</div>
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">珍贵宝箱数</div>
<div class="user-base-info-value text-4xl p-1 ">{{precious_chest_number}}</div>
</div>
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">精致宝箱数</div>
<div class="user-base-info-value text-4xl p-1 ">{{exquisite_chest_number}}</div>
</div>
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">普通宝箱数</div>
<div class="user-base-info-value text-4xl p-1 ">{{common_chest_number}}</div>
</div>
</div>
<div class="base-user-info-1 flex flex-wrap text-center">
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">风神瞳</div>
<div class="user-base-info-value text-4xl p-1 ">{{anemoculus_number}}</div>
</div>
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">岩神瞳</div>
<div class="user-base-info-value text-4xl p-1 ">{{geoculus_number}}</div>
</div>
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">雷神瞳</div>
<div class="user-base-info-value text-4xl p-1 ">{{electroculus_number}}</div>
</div>
<div class="user-base-info ml-4 p-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-xl p-1">草神瞳</div>
<div class="user-base-info-value text-4xl p-1 ">{{dendroculi_number}}</div>
</div>
</div>
</div>
<div class="user-info-2 flex ">
<div class="world-exploration rounded-xl float-left flex-1 mr-4">
<div class="world-exploration-title pl-2 ">
<h1 class="text-2xl">世界探索</h1>
</div>
<div class="world-exploration-list pt-2 px-4">
{% for world_exploration in world_exploration_list %}
<div class="world-exploration-info mt-2 flex rounded-xl">
<div class="world-exploration-info-icon flex-shrink">
<img class="world-exploration-info-img" src="{{world_exploration.icon}}">
</div>
<div class="world-exploration-info-info flex-grow pt-1 pl-2 pb-1">
<div class="world-exploration-info-name pb-1">
<p>{{world_exploration.name}}</p>
</div>
<div class="world-exploration-info-hr pb-1">
<HR class="world-exploration-info-hr-1" color=#000000 style="height: 2px">
</div>
<div class="world-exploration-info-s">
<p>探索度:{{world_exploration.exploration_percentage}}%</p>
</div>
<div class="world-exploration-info-pointer">
<div class="world-exploration-info-pointer-bar-body">
<div class="world-exploration-info-pointer-progress-bar"
style="width: {{world_exploration.exploration_percentage}}%"></div>
</div>
</div>
<div class="world-exploration-info-offerings-list flex">
{% for offering in world_exploration.offerings %}
<div class="world-exploration-info-other-1 flex-1">
<p>{{offering.data}}</p>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="teapot rounded-xl float-right flex-1">
<div class="teapot-title pl-2">
<h1 class="text-2xl">尘歌壶</h1>
</div>
<div class="teapot-info-base flex rounded-xl text-center pt-2">
<div class="user-base-info py-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-base p-1">信任等阶级</div>
<div class="user-base-info-value text-4xl p-1">{{teapot_level}}</div>
</div>
<div class="user-base-info py-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-base p-1">最高洞天仙力</div>
<div class="user-base-info-value text-4xl p-1">{{teapot_comfort_num}}</div>
</div>
<div class="user-base-info py-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-base p-1">获得摆设数</div>
<div class="user-base-info-value text-4xl p-1">{{teapot_item_num}}</div>
</div>
<div class="user-base-info py-2 rounded-xl flex-1">
<div class="user-base-info-title-name text-base p-1">历史访问数</div>
<div class="user-base-info-value text-4xl p-1">{{teapot_visit_num}}</div>
</div>
</div>
<div class="teapot-info-title text-center pt-2">
<h1 class="text-base">已解锁的洞天</h1>
</div>
<div class="teapot-info-list flex-col px-4">
{% for teapot in teapot_list %}
<div class="teapot-info pt-4">
<div class="teapot-info-icon rounded-xl relative">
<div class="teapot-info-icon">
<img class="teapot-info-img" src="{{teapot.icon}}">
</div>
<div class="teapot-info-name absolute right-0 top-0 px-2 text-base rounded-bl-lg">
{{teapot.name}}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<link href="./{{style}}.css" rel="stylesheet"/>
<link type="text/css" href="../../styles/public.css" rel="stylesheet"/>
<script src="../../js/tailwindcss-3.1.8.js"></script>
</head>
<body class="text-neutral-600">
<div class="mx-auto max-w-[600px] py-8">
<div class="header p-2 rounded-xl bg-cover mb-6" style='background-image: url("{{ background }}")'>
<div
class="frame-pic p-4 flex items-center rounded-lg border-solid border-2"
>
<img class="w-16 h-16 rounded-full mr-4" src="{{ avatar }}" alt="Avatar">
<div class="flex flex-col items-center justify-center">
<h1 class="name text-4xl italic mb-2 px-2 text-shadow">
{{ nickname }}
</h1>
<h1 class="italic uid px-10">UID - {{ uid }}</h1>
</div>
</div>
</div>
<div class="box p-2 rounded-xl overflow-hidden">
<div class="frame rounded-lg border-solid border-2">
<h2
class="title font-semibold pt-4 text-center text-xl text-neutral-700 p-1"
>
<img src="./items/star.png" class="inline-block w-4"/>
数据总览
</h2>
<div class="p-6 grid grid-cols-4 gap-4 text-center">
{% for label, key in stats_labels %}
<div class="">
{% set value = stats[key] %}
{% if value == "" %}
{% set value = "-" %}
{% endif %}
<div class="text-xl box-stats">{{ value }}</div>
<div class="text-neutral-400 box-stats-label">{{ label }}</div>
</div>
{% endfor %}
{% if rogue %}
{% for label, key in rogue_labels %}
<div class="">
<div class="text-xl box-stats">{{ rogue[key] }}</div>
<div class="text-neutral-400 box-stats-label">{{ label }}</div>
</div>
{% endfor %}
{% endif %}
{% if ledger %}
{% for label, key in ledger_labels %}
<div class="">
<div class="text-xl box-stats">{{ ledger[key] }}</div>
<div class="text-neutral-400 box-stats-label">{{ label }}</div>
</div>
{% endfor %}
{% endif %}
</div>
<div class="about text-center leading-8 text-xs opacity-50">
所有数据会有一小时延迟 以游戏内为准 此处仅供参考
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,45 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Title</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<link href="./{{style}}.css" rel="stylesheet" />
<link type="text/css" href="../../styles/public.css" rel="stylesheet" />
<script src="../../js/tailwindcss-3.1.8.js"></script>
</head>
<body class="text-neutral-600">
<div class="mx-auto max-w-[600px] py-8">
<div class="header p-6 flex mb-8 rounded-xl bg-cover justify-between">
<div class="flex flex-col items-center justify-center">
<h1 class="text-4xl italic name mb-2 px-2">
{{ nickname }}
</h1>
<h1 class="italic uid px-10">UID - {{ uid }}</h1>
</div>
</div>
<div class="box pt-4 rounded-xl overflow-hidden">
<div>
<h2 class="box-title text-center text-xl ring text-neutral-100 p-1">
数据总览
</h2>
<div class="p-6 grid grid-cols-4 gap-4 text-center">
{% for label, key in stats_labels %}
<div class="">
<div class="text-xl box-stats">{{ stats[key] }}</div>
<div class="text-neutral-400 box-stats-label">{{ label }}</div>
</div>
{% endfor %}
</div>
</div>
<div class="about text-center leading-8 text-xs opacity-50">
所有数据会有一小时延迟 以游戏内为准 此处仅供参考
</div>
</div>
</div>
</body>
</html>