├── .gitignore ├── LICENSE ├── README.md ├── bot.py ├── data └── fishgame │ ├── fish_data.json │ ├── fish_item.json │ └── gacha.json ├── requirements.txt └── src ├── data_access ├── plugin_manager.py └── redis.py ├── libraries ├── apexlegends_api.py ├── auto_naga_api.py ├── fishgame.py ├── fishgame_util.py ├── gosen_choyen.py ├── image.py ├── maimai_best_40.py ├── maimai_best_50.py ├── maimaidx_music.py ├── pokemon_img.py └── tool.py └── plugins ├── apex_legends.py ├── auto_naga.py ├── covid_positive.py ├── fishgame.py ├── huozi.py ├── mahjong_character.py ├── maimaidx.py ├── poke.py ├── public.py └── reserve.py /.gitignore: -------------------------------------------------------------------------------- 1 | src/static 2 | .idea 3 | .code 4 | venv 5 | .env 6 | __pycache__ 7 | official_config.json 8 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Diving-Fish 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mai bot 使用指南 2 | 3 | 此 README 提供了最低程度的 mai bot 教程与支持。 4 | 5 | **建议您至少拥有一定的编程基础之后再尝试使用本工具。** 6 | 7 | ## Step 1. 安装 Python 8 | 9 | 请自行前往 https://www.python.org/ 下载 Python 3 版本(> 3.7)并将其添加到环境变量(在安装过程中勾选 Add Python to system PATH)。对大多数用户来说,您应该下载 Windows installer (64-bit)。 10 | 11 | 在 Linux 系统上,可能需要其他方法安装 Python 3,请自行查找。 12 | 13 | ## Step 2. 运行项目 14 | 15 | 建议使用 git 对此项目进行版本管理。您也可以直接在本界面下载代码的压缩包进行运行。 16 | 17 | 在运行代码之前,您需要从[此链接](https://www.diving-fish.com/maibot/static.zip)下载资源文件并解压到`src`文件夹中。 18 | 19 | > 资源文件仅供学习交流使用,请自觉在下载 24 小时内删除资源文件。 20 | 21 | 在此之后,**您需要打开控制台,并切换到该项目所在的目录。** 22 | 在 Windows 10 系统上,您可以直接在项目的根目录(即 bot.py)文件所在的位置按下 Shift + 右键,点击【在此处打开 PowerShell 窗口】。 23 | 如果您使用的是更旧的操作系统(比如 Windows 7),请自行查找关于`Command Prompt`,`Powershell`以及`cd`命令的教程。 24 | 25 | 之后,在打开的控制台中输入 26 | ``` 27 | python --version 28 | ``` 29 | 控制台应该会打印出 Python 的版本。如果提示找不到 `python` 命令,请检查环境变量或干脆重装 Python,**并务必勾选 Add Python to system PATH**。 30 | 31 | 之后,输入 32 | ``` 33 | pip install -r requirements.txt 34 | ``` 35 | 安装依赖完成后,运行 36 | ``` 37 | python bot.py 38 | ``` 39 | 运行项目。如果输出如下所示的内容,代表运行成功: 40 | ``` 41 | 08-02 11:26:48 [INFO] nonebot | NoneBot is initializing... 42 | 08-02 11:26:48 [INFO] nonebot | Current Env: prod 43 | 08-02 11:26:49 [INFO] nonebot | Succeeded to import "maimaidx" 44 | 08-02 11:26:49 [INFO] nonebot | Succeeded to import "public" 45 | 08-02 11:26:49 [INFO] nonebot | Running NoneBot... 46 | 08-02 11:26:49 [INFO] uvicorn | Started server process [5268] 47 | 08-02 11:26:49 [INFO] uvicorn | Waiting for application startup. 48 | 08-02 11:26:49 [INFO] uvicorn | Application startup complete. 49 | 08-02 11:26:49 [INFO] uvicorn | Uvicorn running on http://127.0.0.1:10219 (Press CTRL+C to quit) 50 | ``` 51 | **运行成功后请勿关闭此窗口,后续需要与 CQ-HTTP 连接。** 52 | 53 | ## Step 3. 连接 CQ-HTTP 54 | 55 | 前往 https://github.com/Mrs4s/go-cqhttp > Releases,下载适合自己操作系统的可执行文件。 56 | go-cqhttp 在初次启动时会询问代理方式,选择反向 websocket 代理即可。 57 | 58 | 之后用任何文本编辑器打开`config.yml`文件,设置反向 ws 地址、上报方式: 59 | ```yml 60 | message: 61 | post-format: array 62 | 63 | servers: 64 | - ws-reverse: 65 | universal: ws://127.0.0.1:10219/onebot/v11/ws 66 | ``` 67 | 然后设置您的 QQ 号和密码。您也可以不设置密码,选择扫码登陆的方式。 68 | 69 | 登陆成功后,后台应该会发送一条类似的信息: 70 | ``` 71 | 08-02 11:50:51 [INFO] nonebot | WebSocket Connection from CQHTTP Bot 114514 Accepted! 72 | ``` 73 | 至此,您可以和对应的 QQ 号聊天并使用 mai bot 的所有功能了。 74 | 75 | ## FAQ 76 | 77 | 不是 Windows 系统该怎么办? 78 | > 请自行查阅其他系统上的 Python 安装方式。cqhttp提供了其他系统的可执行文件,您也可以自行配置 golang module 环境进行编译。 79 | 80 | 配置 nonebot 或 cq-http 过程中出错? 81 | > 请查阅 https://github.com/nonebot/nonebot2 以及 https://github.com/Mrs4s/go-cqhttp 中的文档。 82 | 83 | 部分消息发不出来? 84 | > 被风控了。解决方式:换号或者让这个号保持登陆状态和一定的聊天频率,持续一段时间。 85 | 86 | ## 说明 87 | 88 | 本 bot 提供了如下功能: 89 | 90 | 命令 | 功能 91 | --- | --- 92 | help | 查看帮助文档 93 | 今日舞萌 | 查看今天的舞萌运势 94 | XXXmaimaiXXX什么 | 随机一首歌 95 | 随个[dx/标准][绿黄红紫白]<难度> | 随机一首指定条件的乐曲 96 | 查歌<乐曲标题的一部分> | 查询符合条件的乐曲 97 | [绿黄红紫白]id<歌曲编号> | 查询乐曲信息或谱面信息 98 | 定数查歌 <定数>
定数查歌 <定数下限> <定数上限> | 查询定数对应的乐曲 99 | 分数线 <难度+歌曲id> <分数线> | 展示歌曲的分数线 100 | 101 | ## License 102 | 103 | MIT 104 | 105 | 您可以自由使用本项目的代码用于商业或非商业的用途,但必须附带 MIT 授权协议。 106 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from collections import defaultdict 4 | 5 | import nonebot 6 | from nonebot.adapters.onebot.v11 import Adapter 7 | 8 | 9 | # Custom your logger 10 | # 11 | # from nonebot.log import logger, default_format 12 | # logger.add("error.log", 13 | # rotation="00:00", 14 | # diagnose=False, 15 | # level="ERROR", 16 | # format=default_format) 17 | 18 | # You can pass some keyword args config to init function 19 | nonebot.init() 20 | # nonebot.load_builtin_plugins() 21 | app = nonebot.get_asgi() 22 | 23 | driver = nonebot.get_driver() 24 | driver.register_adapter(Adapter) 25 | driver.config.help_text = {} 26 | 27 | 28 | nonebot.load_plugins("src/plugins") 29 | nonebot.load_plugin("nonebot_plugin_guild_patch") 30 | 31 | # Modify some config / config depends on loaded configs 32 | # 33 | # config = driver.config 34 | # do something... 35 | 36 | 37 | if __name__ == "__main__": 38 | nonebot.run() 39 | -------------------------------------------------------------------------------- /data/fishgame/fish_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "泥塘跳跳鱼", 4 | "detail": "栖息于浑浊的浅水沼泽,凭借强健的尾鳍可跃出水面1米高。体表覆盖粘液以抵御寄生虫,常被新手钓客视为练习目标。杂食性,尤其喜爱啃食腐烂植物根茎。", 5 | "rarity": "R", 6 | "std_power": 0, 7 | "base_probability": 0.0005, 8 | "exp": 10 9 | }, 10 | { 11 | "name": "锈斑小鲫鱼", 12 | "detail": "因鳞片氧化产生的铁锈色斑纹得名,群居于缓流河床底部。嗅觉灵敏,会循着发酵饵料聚集。肉质略带金属味,却是制作鱼露的优质原料。", 13 | "rarity": "R", 14 | "std_power": 5, 15 | "base_probability": 0.0005, 16 | "exp": 15 17 | }, 18 | { 19 | "name": "枯叶伪装鲦", 20 | "detail": "进化出与腐烂树叶完全一致的形态,静止时连鳃部都会模拟叶脉颤动。仅在被鱼钩触碰的瞬间才会急速逃窜,考验钓客的反应速度。", 21 | "rarity": "R", 22 | "std_power": 10, 23 | "base_probability": 0.0005, 24 | "exp": 20 25 | }, 26 | { 27 | "name": "荧光青鳉", 28 | "detail": "夜晚池塘的常驻居民,鱼边缘分布着共生发光菌群。会被同类光芒吸引聚集成光带,可作为天然光源帮助钓客寻找钓点。", 29 | "rarity": "R", 30 | "std_power": 15, 31 | "base_probability": 0.0005, 32 | "exp": 25 33 | }, 34 | { 35 | "name": "湍流红尾鳟", 36 | "detail": "适应急流环境的勇猛鱼种,鲜红的尾鳍可在高速游动时威慑天敌。牙齿锐利,常能咬断普通钓线,需配合钢芯鱼线捕获。", 37 | "rarity": "R", 38 | "std_power": 20, 39 | "base_probability": 0.0005, 40 | "exp": 30 41 | }, 42 | { 43 | "name": "雾隐灯笼鲇", 44 | "detail": "头部延伸出灯笼状发光器官,用于在浓雾中吸引浮游生物。钓获时分泌的黏液会让鱼竿握把持续12小时散发薄荷清香。", 45 | "rarity": "R", 46 | "std_power": 25, 47 | "base_probability": 0.0005, 48 | "exp": 35 49 | }, 50 | { 51 | "name": "螺旋纹螺蛳鲤", 52 | "detail": "背鳍至尾部生有螺旋状花纹,以螺蛳为食的专家。下颚骨骼特化为螺旋锥形,能轻易钻开螺壳,空螺壳常被工匠用作装饰材料。", 53 | "rarity": "R", 54 | "std_power": 30, 55 | "base_probability": 0.0005, 56 | "exp": 40 57 | }, 58 | { 59 | "name": "银鳞刀背鲌", 60 | "detail": "生活在开阔水域的银白色掠食者,背鳍边缘锋利如手术刀。集群游动时会形成银色旋风光影,但大量出现可能预示地震将至。", 61 | "rarity": "R", 62 | "std_power": 40, 63 | "base_probability": 0.0005, 64 | "exp": 50 65 | }, 66 | { 67 | "name": "岩缝刺须鳅", 68 | "detail": "扁平身体能钻入毫米级岩缝,嘴边六对感应须可探测水流变化。受惊时从鳃部喷射酸液,需佩戴防护手套才能安全摘钩。", 69 | "rarity": "R", 70 | "std_power": 50, 71 | "base_probability": 0.0005, 72 | "exp": 60 73 | }, 74 | { 75 | "name": "气泡河豚", 76 | "detail": "受威胁时吸入空气膨胀成球,同时从鳃孔高速喷射气泡束。干燥后的膨胀皮囊可制成浮力极强的天然救生装置。", 77 | "rarity": "R", 78 | "std_power": 60, 79 | "base_probability": 0.0005, 80 | "exp": 70 81 | }, 82 | { 83 | "name": "腐木寄生鲶", 84 | "detail": "幼鱼期便嵌入沉木,身体逐渐木质化与宿主融为一体。钓获时必须连木段一起取下,其鱼鳔干燥后可制成持久熏香。", 85 | "rarity": "R", 86 | "std_power": 70, 87 | "base_probability": 0.0005, 88 | "exp": 80 89 | }, 90 | { 91 | "name": "彩虹鳟鱼", 92 | "detail": "鳞片折射出七色微光的小型淡水鱼,喜欢吞食水草,钓获时常会吐出胃中未消化的植物残渣。", 93 | "rarity": "R", 94 | "std_power": 80, 95 | "base_probability": 0.0005, 96 | "exp": 90 97 | }, 98 | { 99 | "name": "铁甲泥鳅", 100 | "detail": "覆盖着锈迹斑斑硬壳的底栖生物,喜欢藏身淤泥中,咬钩时会疯狂旋转挣脱。", 101 | "rarity": "R", 102 | "std_power": 100, 103 | "base_probability": 0.0005, 104 | "exp": 110 105 | }, 106 | { 107 | "name": "月光鲦", 108 | "detail": "只在夜晚浮现的银白色小鱼群,游动时如碎银洒落湖面,传闻是月神遗落的饰品。", 109 | "rarity": "R", 110 | "std_power": 120, 111 | "base_probability": 0.0005, 112 | "exp": 130 113 | }, 114 | { 115 | "name": "爆炎食人鱼", 116 | "detail": "脾气暴躁的红色小鱼,鱼鳃喷出微弱火星,会成群结队啃咬鱼竿。", 117 | "rarity": "R", 118 | "std_power": 140, 119 | "base_probability": 0.0005, 120 | "exp": 150 121 | }, 122 | { 123 | "name": "翡翠龙纹鲈", 124 | "detail": "仅存于千年冷泉的珍稀物种,鳞片透射翡翠光泽,天然形成的纹路酷似龙鳞。需用寒铁鱼钩配合零度以下饵料方可引诱,传说其鱼骨能雕琢成护身符。", 125 | "rarity": "SR", 126 | "std_power": 25, 127 | "base_probability": 0.0003, 128 | "exp": 50 129 | }, 130 | { 131 | "name": "月影纱衣鳜", 132 | "detail": "每逢月圆之夜现身湖心,半透明鱼身随月光强度改变透明度。使用银制鱼钩且不携带照明设备时,咬钩概率提升300%。", 133 | "rarity": "SR", 134 | "std_power": 45, 135 | "base_probability": 0.0003, 136 | "exp": 90 137 | }, 138 | { 139 | "name": "雷霆闪鳞鳗", 140 | "detail": "雷暴天气时从深海跃至近海,通体鳞片蓄积大气静电。成功钓获可使鱼竿附魔「雷纹」,24小时内增加麻痹特效触发率。", 141 | "rarity": "SR", 142 | "std_power": 60, 143 | "base_probability": 0.0003, 144 | "exp": 120 145 | }, 146 | { 147 | "name": "水晶珊瑚蝶鱼", 148 | "detail": "与发光珊瑚共生的美丽鱼种,受损的鱼鳍会在48小时内再生。当携带至少3种R级珊瑚鱼时,会主动跟随玩家指引方向。", 149 | "rarity": "SR", 150 | "std_power": 80, 151 | "base_probability": 0.0003, 152 | "exp": 160 153 | }, 154 | { 155 | "name": "时砂回溯鲟", 156 | "detail": "脊椎中嵌着微型沙漏的古老生物,每次咬钩都会随机倒退或前进游戏内时间1-5分钟。鱼卵被证实含有逆转细胞衰老的物质。", 157 | "rarity": "SR", 158 | "std_power": 100, 159 | "base_probability": 0.0003, 160 | "exp": 200 161 | }, 162 | { 163 | "name": "幽灵船长的诅咒鲛", 164 | "detail": "出没于深海沉船残骸的诡异生物,眼球能发出幽蓝磷光。鱼鳍形似破烂船帆,咬钩时会发出类似哀嚎的次声波。传闻钓获者将继承沉船宝藏的线索。", 165 | "rarity": "SR", 166 | "std_power": 120, 167 | "base_probability": 0.0003, 168 | "exp": 240 169 | }, 170 | { 171 | "name": "霜晶龙鲤", 172 | "detail": "通体冰蓝的优雅鲤鱼,鳞片如水晶般剔透,触碰时会冻结周围的水面。", 173 | "rarity": "SR", 174 | "std_power": 140, 175 | "base_probability": 0.0003, 176 | "exp": 280 177 | }, 178 | { 179 | "name": "幽灵灯笼鱼", 180 | "detail": "半透明的深海怪鱼,头顶悬浮幽绿光球,能诱骗猎物主动游向它的巨口。", 181 | "rarity": "SR", 182 | "std_power": 160, 183 | "base_probability": 0.0003, 184 | "exp": 320 185 | }, 186 | { 187 | "name": "雷霆箭鱼", 188 | "detail": "以闪电速度穿梭于暴风雨海域的长喙猎手,冲刺时会在身后留下短暂的电光轨迹。", 189 | "rarity": "SR", 190 | "std_power": 180, 191 | "base_probability": 0.0003, 192 | "exp": 360 193 | }, 194 | { 195 | "name": "翡翠王鲑", 196 | "detail": "通体翠绿如帝王翡翠的巨型鲑鱼,仅在水质纯净的瀑布潭中现身,鱼鳞象征钓手荣耀。", 197 | "rarity": "SR", 198 | "std_power": 200, 199 | "base_probability": 0.0003, 200 | "exp": 400 201 | }, 202 | { 203 | "name": "熔岩火鳞鱼", 204 | "detail": "诞生于活火山地脉的奇迹生物,通体覆盖灼热黑曜石状鳞甲。需特制耐火鱼线在岩浆喷口垂钓,上钩时会引起局部水温沸腾。学者地核能量具象化的存在。", 205 | "rarity": "SSR", 206 | "std_power": 150, 207 | "base_probability": 0.0001, 208 | "exp": 450 209 | }, 210 | { 211 | "name": "永夜帝王鲟", 212 | "detail": "栖息于无光深渊的漆黑巨鲟,暗金色瞳孔能窥见灵魂,鱼骨被视作“不朽权杖”的原料。", 213 | "rarity": "SSR", 214 | "std_power": 175, 215 | "base_probability": 0.0001, 216 | "exp": 525 217 | }, 218 | { 219 | "name": "炽天使火鳐", 220 | "detail": "背部长有火焰状鳍翼的圣洁鳐鱼,传说它掠过水域会净化一切污染,千年仅现身一次。", 221 | "rarity": "SSR", 222 | "std_power": 195, 223 | "base_probability": 0.0001, 224 | "exp": 585 225 | }, 226 | { 227 | "name": "星海歌姬水母", 228 | "detail": "伞盖中流淌银河光晕的梦幻水母,触须颤动时会发出惑人心智的歌声,钓获者需通过精神检定。", 229 | "rarity": "SSR", 230 | "std_power": 220, 231 | "base_probability": 0.0001, 232 | "exp": 660 233 | }, 234 | { 235 | "name": "永生者·九尾天狐鲤", 236 | "detail": "被神道教奉为轮回化身的圣鱼,九条尾鳍分别镌刻不同符文。仅现身于献祭百种鱼类的圣池,钓获时可触发日月同辉异象。其鳞片研磨的粉末据说能逆转生死。", 237 | "rarity": "SSR", 238 | "std_power": 240, 239 | "base_probability": 0.0001, 240 | "exp": 720 241 | }, 242 | { 243 | "name": "虚空幻鲲", 244 | "detail": "能在现实与虚空裂隙间游动的神秘巨兽,身体若隐若现,钓获者将获得“时空眷顾”传说成就。", 245 | "rarity": "SSR", 246 | "std_power": 270, 247 | "base_probability": 0.0001, 248 | "exp": 810 249 | }, 250 | { 251 | "name": "创世之鳞·奥米茄", 252 | "detail": "游弋在世界本源之湖的至高存在,身躯由流动的星河构成。当玩家功德值满级且持有全部SSR鱼种时,有0.0001%概率遇见。成功钓起将永久改变游戏天气与物理法则。", 253 | "rarity": "SSR", 254 | "std_power": 333, 255 | "base_probability": 0.00005, 256 | "exp": 999 257 | } 258 | ] -------------------------------------------------------------------------------- /data/fishgame/fish_item.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "食料", 5 | "rarity": 1, 6 | "description": "使用后,在 30 分钟内,【R】鱼的出现概率将大幅提高", 7 | "price": 5 8 | }, 9 | { 10 | "id": 2, 11 | "name": "白银食料", 12 | "rarity": 2, 13 | "description": "使用后,在 60 分钟内,【R】鱼和【SR】鱼的出现概率将大幅提高", 14 | "price": 50 15 | }, 16 | { 17 | "id": 3, 18 | "name": "黄金食料", 19 | "rarity": 3, 20 | "description": "使用后,在 120 分钟内,【R】鱼、【SR】鱼和【SSR】鱼的出现概率将大幅提高" 21 | }, 22 | { 23 | "id": 4, 24 | "name": "渔力强化剂", 25 | "rarity": 1, 26 | "description": "使用后,下一次捕鱼时渔力提升 10 点" 27 | }, 28 | { 29 | "id": 5, 30 | "name": "白银渔力强化剂", 31 | "rarity": 2, 32 | "description": "使用后,下 2 次捕鱼时渔力提升 20 点,效果不与其他强化剂叠加" 33 | }, 34 | { 35 | "id": 6, 36 | "name": "黄金渔力强化剂", 37 | "rarity": 3, 38 | "description": "使用后,下 4 次捕鱼时渔力提升 40 点,效果不与其他强化剂叠加" 39 | }, 40 | { 41 | "id": 7, 42 | "name": "渔获加成卡", 43 | "rarity": 1, 44 | "description": "使用后,在 20 分钟内捕鱼时,获得的渔获提升 25%" 45 | }, 46 | { 47 | "id": 8, 48 | "name": "白银渔获加成卡", 49 | "rarity": 2, 50 | "description": "使用后,在 20 分钟内捕鱼时,获得的渔获提升 50%,效果不与其他加成卡叠加" 51 | }, 52 | { 53 | "id": 9, 54 | "name": "黄金渔获加成卡", 55 | "rarity": 3, 56 | "description": "使用后,在 20 分钟内捕鱼时,获得的渔获提升 100%,效果不与其他加成卡叠加" 57 | }, 58 | { 59 | "id": 101, 60 | "name": "机械抓手", 61 | "rarity": 1, 62 | "equipable": true, 63 | "type": "tool", 64 | "power": 5, 65 | "description": "能够稍微提升渔力的便捷工具" 66 | }, 67 | { 68 | "id": 102, 69 | "name": "纤维编织网", 70 | "rarity": 2, 71 | "equipable": true, 72 | "type": "tool", 73 | "power": 10, 74 | "description": "能够提升渔力的便捷工具" 75 | }, 76 | { 77 | "id": 103, 78 | "name": "探测罗盘", 79 | "rarity": 2, 80 | "equipable": true, 81 | "type": "tool", 82 | "power": 20, 83 | "description": "能够快速定位鱼群的工具" 84 | }, 85 | { 86 | "id": 104, 87 | "name": "24 号球衣", 88 | "rarity": 3, 89 | "equipable": true, 90 | "type": "tool", 91 | "power": 40, 92 | "description": "充满曼巴精神的装备,能够提升工作效率,提升大量渔力" 93 | }, 94 | { 95 | "id": 105, 96 | "name": "波浪形态", 97 | "rarity": 4, 98 | "equipable": true, 99 | "type": "tool", 100 | "power": 60, 101 | "description": "你就是水本身,能够极大幅提升渔力" 102 | }, 103 | { 104 | "id": 201, 105 | "name": "青铜渔具", 106 | "rarity": 1, 107 | "equipable": true, 108 | "type": "rod", 109 | "power": 10, 110 | "description": "青铜打造的渔具,能够提升渔力", 111 | "price": 50 112 | }, 113 | { 114 | "id": 202, 115 | "name": "白银渔具", 116 | "rarity": 2, 117 | "equipable": true, 118 | "type": "rod", 119 | "power": 20, 120 | "description": "白银打造的渔具,能够提升渔力", 121 | "price": 200 122 | }, 123 | { 124 | "id": 203, 125 | "name": "黄金渔具", 126 | "rarity": 3, 127 | "equipable": true, 128 | "type": "rod", 129 | "power": 40, 130 | "description": "黄金打造的渔具,能够提升渔力", 131 | "price": 1000 132 | }, 133 | { 134 | "id": 204, 135 | "name": "钻石渔具", 136 | "rarity": 4, 137 | "equipable": true, 138 | "type": "rod", 139 | "power": 80, 140 | "description": "钻石打造的渔具,能够提升渔力", 141 | "price": 5000 142 | } 143 | ] -------------------------------------------------------------------------------- /data/fishgame/gacha.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "score", 4 | "value": 10, 5 | "weight": 40 6 | }, 7 | { 8 | "type": "score", 9 | "value": 20, 10 | "weight": 20 11 | }, 12 | { 13 | "type": "score", 14 | "value": 50, 15 | "weight": 5 16 | }, 17 | { 18 | "type": "score", 19 | "value": 100, 20 | "weight": 2 21 | }, 22 | { 23 | "type": "item", 24 | "value": 1, 25 | "weight": 10 26 | }, 27 | { 28 | "type": "item", 29 | "value": 2, 30 | "weight": 5 31 | }, 32 | { 33 | "type": "item", 34 | "value": 3, 35 | "weight": 2 36 | }, 37 | { 38 | "type": "item", 39 | "value": 4, 40 | "weight": 10 41 | }, 42 | { 43 | "type": "item", 44 | "value": 5, 45 | "weight": 5 46 | }, 47 | { 48 | "type": "item", 49 | "value": 6, 50 | "weight": 2 51 | }, 52 | { 53 | "type": "item", 54 | "value": 7, 55 | "weight": 10 56 | }, 57 | { 58 | "type": "item", 59 | "value": 8, 60 | "weight": 5 61 | }, 62 | { 63 | "type": "item", 64 | "value": 9, 65 | "weight": 2 66 | }, 67 | { 68 | "type": "item", 69 | "value": 101, 70 | "weight": 4 71 | }, 72 | { 73 | "type": "item", 74 | "value": 102, 75 | "weight": 2 76 | }, 77 | { 78 | "type": "item", 79 | "value": 103, 80 | "weight": 1 81 | }, 82 | { 83 | "type": "item", 84 | "value": 104, 85 | "weight": 0.5 86 | }, 87 | { 88 | "type": "item", 89 | "value": 105, 90 | "weight": 0.2 91 | } 92 | ] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.1 2 | aiosignal==1.2.0 3 | anyio==3.6.1 4 | APScheduler==3.10.0 5 | arrow==1.2.3 6 | async-timeout==4.0.2 7 | attrs==22.1.0 8 | backports.zoneinfo==0.2.1 9 | beautifulsoup4==4.11.2 10 | binaryornot==0.4.4 11 | bs4==0.0.1 12 | certifi==2022.6.15 13 | chardet==5.0.0 14 | charset-normalizer==2.1.1 15 | click==8.1.3 16 | colorama==0.4.5 17 | cookiecutter==1.7.3 18 | fastapi==0.79.1 19 | frozenlist==1.3.1 20 | h11==0.12.0 21 | httpcore==0.15.0 22 | httptools==0.4.0 23 | httpx==0.23.0 24 | idna==3.3 25 | Jinja2==3.1.2 26 | jinja2-time==0.2.0 27 | loguru==0.6.0 28 | MarkupSafe==2.1.1 29 | msgpack==1.0.4 30 | multidict==6.0.2 31 | nb-cli==0.6.7 32 | nonebot-adapter-onebot==2.1.3 33 | nonebot-plugin-guild-patch==0.2.3 34 | nonebot2==2.0.0 35 | Pillow==9.3.0 36 | poyo==0.5.0 37 | prompt-toolkit==3.0.31 38 | pydantic==1.10.10 39 | pyfiglet==0.8.post1 40 | pygtrie==2.5.0 41 | python-dateutil==2.8.2 42 | python-dotenv==0.21.0 43 | python-slugify==6.1.2 44 | pytz==2022.7.1 45 | pytz-deprecation-shim==0.1.0.post0 46 | PyYAML==6.0 47 | qq-botpy==1.1.2 48 | redis==4.4.0 49 | requests==2.28.1 50 | rfc3986==1.5.0 51 | six==1.16.0 52 | sniffio==1.3.0 53 | soupsieve==2.3.2.post1 54 | starlette==0.19.1 55 | text-unidecode==1.3 56 | tomli==2.0.1 57 | tomlkit==0.10.2 58 | typing_extensions==4.3.0 59 | tzdata==2022.7 60 | tzlocal==4.2 61 | urllib3==1.26.12 62 | uvicorn==0.18.3 63 | uvloop==0.16.0 64 | watchfiles==0.16.1 65 | wcwidth==0.2.5 66 | websockets==10.3 67 | win32-setctime==1.1.0 68 | yarl==1.8.1 69 | -------------------------------------------------------------------------------- /src/data_access/plugin_manager.py: -------------------------------------------------------------------------------- 1 | from src.data_access.redis import redis_global 2 | from hashlib import sha256 3 | import json 4 | 5 | 6 | def get_string_hash(s): 7 | return sha256(bytes(s, encoding='utf-8')).hexdigest() 8 | 9 | 10 | class PluginManager: 11 | def __init__(self) -> None: 12 | self.metadata = {} 13 | 14 | def register_plugin(self, metadata) -> None: 15 | self.metadata[metadata["name"]] = metadata 16 | 17 | def __get_group_key(self, group_id) -> str: 18 | return get_string_hash("chiyuki" + str(group_id)) 19 | 20 | def get_all(self, group_id): 21 | status = {} 22 | for key, meta in self.metadata.items(): 23 | status[key] = meta["enable"] 24 | redis_data = redis_global.get(self.__get_group_key(group_id)) 25 | try: 26 | obj = json.loads(redis_data) 27 | for k, v in obj.items(): 28 | status[k] = v 29 | except Exception: 30 | pass 31 | return status 32 | 33 | def get_enable(self, group_id, plugin_name) -> bool: 34 | return self.get_all(group_id)[plugin_name] 35 | 36 | def set_enable(self, group_id, plugin_name, enable) -> None: 37 | status = self.get_all(group_id) 38 | status[plugin_name] = enable 39 | redis_global.set(self.__get_group_key(group_id), json.dumps(status)) 40 | 41 | def get_groups(self, plugin_name): 42 | groups = [] 43 | for group_id in redis_global.keys("chiyuki*"): 44 | status = self.get_all(group_id) 45 | if status[plugin_name]: 46 | groups.append(group_id) 47 | return groups 48 | 49 | plugin_manager = PluginManager() -------------------------------------------------------------------------------- /src/data_access/redis.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import json 3 | 4 | redis_global = redis.Redis(host='localhost', port=6379, decode_responses=True) 5 | 6 | class RedisData: 7 | def __init__(self, key, type_loader=str, type_serializer=str, default=''): 8 | self.key = key 9 | value = redis_global.get(self.key) 10 | if value == None: 11 | self.submit_data = default 12 | else: 13 | self.submit_data = type_loader(value) 14 | self.loader = type_loader 15 | self.serializer = type_serializer 16 | self.data = self.submit_data 17 | 18 | def set(self, data): 19 | self.data = data 20 | 21 | def save(self, *args, ex=None, px=None, nx=False, xx=False): 22 | if len(args) != 0: 23 | self.set(args[0]) 24 | self.submit_data = self.data 25 | redis_global.set(self.key, self.serializer(self.submit_data), ex, px, nx, xx) 26 | 27 | 28 | class NumberRedisData(RedisData): 29 | def __init__(self, key): 30 | super().__init__(key, int, str, default=0) 31 | 32 | 33 | class ListRedisData(RedisData): 34 | def __init__(self, key): 35 | super().__init__(key, lambda s : json.loads(s), lambda lst : json.dumps(lst), default=[]) 36 | if type(self.data) != type([]): 37 | raise Exception(f"{self.__dict__} is not a list") 38 | 39 | 40 | class DictRedisData(RedisData): 41 | def __init__(self, key, default={}): 42 | super().__init__(key, lambda s : json.loads(s), lambda dct : json.dumps(dct), default=default) 43 | if type(self.data) != type({}): 44 | raise Exception(f"{self.__dict__} is not a dict") 45 | -------------------------------------------------------------------------------- /src/libraries/apexlegends_api.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from collections import defaultdict 3 | from urllib.parse import quote 4 | from bs4 import BeautifulSoup 5 | import json 6 | 7 | YOUR_API_KEY = "" # will be set then 8 | 9 | def set_apex_token(token): 10 | global YOUR_API_KEY 11 | YOUR_API_KEY = token 12 | 13 | class ApexLegendsAPI: 14 | @staticmethod 15 | async def player_statistics_uid(uid): 16 | async with aiohttp.ClientSession() as session: 17 | async with session.get(f"https://api.mozambiquehe.re/bridge?auth={YOUR_API_KEY}&uid={uid}&platform=PC") as resp: 18 | return await resp.text(), resp.status 19 | 20 | @staticmethod 21 | async def map_rotation(): 22 | async with aiohttp.ClientSession() as session: 23 | async with session.get(f"https://api.mozambiquehe.re/maprotation?auth={YOUR_API_KEY}") as resp: 24 | return await resp.text(), resp.status 25 | 26 | @staticmethod 27 | async def crafting(): 28 | async with aiohttp.ClientSession() as session: 29 | async with session.get(f"https://api.mozambiquehe.re/crafting?auth={YOUR_API_KEY}") as resp: 30 | return await resp.text(), resp.status 31 | 32 | @staticmethod 33 | async def search_player(player_name): 34 | res = defaultdict(lambda: { 35 | "name": "", 36 | "level": "", 37 | "selected": "", 38 | "RP": 0, 39 | }) 40 | # 此函数将通过橘子 ID 和用户昵称查找可能的用户。 41 | async with aiohttp.ClientSession() as session: 42 | async with session.get(f"https://api.mozambiquehe.re/bridge?auth={YOUR_API_KEY}&player={player_name}&platform=PC") as resp: 43 | try: 44 | obj = json.loads(await resp.text(encoding='utf-8')) 45 | if 'global' in obj: 46 | uid = obj['global']['uid'] 47 | res[uid]['name'] = obj['global']['name'] 48 | res[uid]['level'] = obj['global']['level'] + obj['global']['levelPrestige'] * 500 49 | res[uid]['selected'] = obj['legends']['selected']['LegendName'] 50 | res[uid]['rp'] = obj['global']['rank']['rankScore'] 51 | except Exception: 52 | pass 53 | async with session.get("https://apexlegendsstatus.com/profile/search/gnivid"): 54 | # get cookie, then: 55 | async with session.get(f"https://apexlegendsstatus.com/core/interface?token=HARDCODED&platform=search&player={quote(player_name)}") as resp2: 56 | txt = await resp2.text() 57 | with open('a.html', 'w', encoding='utf-8') as fw: 58 | fw.write(txt) 59 | soup = BeautifulSoup(txt, 'html.parser') 60 | for o in soup.find_all(class_="col-lg-2 col-md-3 col-sm-4 col-xs-12"): 61 | container = list(o.children)[0] 62 | a = container.find('a') 63 | uid: str = a.attrs['href'] 64 | platform = uid.split('/')[-2] 65 | uid = uid.split('/')[-1] 66 | if platform != 'PC': 67 | continue 68 | uid = int(uid) 69 | if uid in res: 70 | continue 71 | try: 72 | ps = list(a.find_all('p')) 73 | res[uid]['name'] = ps[0].text 74 | b1, b2 = list(ps[1].find_all('b')) 75 | res[uid]['level'] = int(b1.text) + int(b2.text) * 500 76 | res[uid]['selected'] = ps[2].find('b').text 77 | res[uid]['rp'] = int(ps[3].find('b').text.replace(',', '')) 78 | except ValueError: 79 | # 说明用户没有在 apex legends status 平台上搜索过 80 | # 如果它能化成 UID (origin or steam),那就用 bridge 抓一下,然后返回数据,否则就算了 81 | resp3, txt = await ApexLegendsAPI.player_statistics_uid(uid) 82 | obj = json.loads(resp3) 83 | if 'global' in obj: 84 | uid = obj['global']['uid'] 85 | res[uid]['name'] = obj['global']['name'] 86 | res[uid]['level'] = obj['global']['level'] + obj['global']['levelPrestige'] * 500 87 | res[uid]['selected'] = obj['legends']['selected']['LegendName'] 88 | res[uid]['rp'] = obj['global']['rank']['rankScore'] 89 | return res 90 | -------------------------------------------------------------------------------- /src/libraries/auto_naga_api.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from src.data_access.redis import redis_global 3 | import json 4 | from typing import Dict, List, Optional, Union 5 | import aiohttp 6 | from src.data_access.redis import * 7 | 8 | BASE_URL = "https://www.diving-fish.com/api/auto_naga" 9 | NAGA_SECRET = "" # will be set then 10 | 11 | def set_naga_secret(secret): 12 | global NAGA_SECRET 13 | NAGA_SECRET = secret 14 | 15 | class AutoNaga: 16 | def __init__(self) -> None: 17 | self.majsoul_analyze_queue = [] 18 | self.majsoul_url_map = {} 19 | self.majsoul_data_map = {} 20 | 21 | async def cost_np(self, user_id, np) -> bool: 22 | cache_data = NumberRedisData(f'naga_np_{user_id}') 23 | if cache_data.data > np: 24 | cache_data.save(cache_data.data - np) 25 | return True, cache_data.data, np 26 | return False, cache_data.data, np 27 | 28 | async def get_np(self, user_id) -> int: 29 | cache_data = NumberRedisData(f'naga_np_{user_id}') 30 | return cache_data.data 31 | 32 | async def add_np(self, user_id, np) -> None: 33 | cache_data = NumberRedisData(f'naga_np_{user_id}') 34 | cache_data.save(cache_data.data + np) 35 | 36 | async def order(self, custom: bool, data: Union[str, List], player_types: str) -> Dict: 37 | body = { 38 | "secret": NAGA_SECRET, 39 | "custom": custom, 40 | "player_types": player_types 41 | } 42 | if custom: 43 | body['haihus']: List = data 44 | else: 45 | body['tenhou_url']: str = data 46 | 47 | print(data) 48 | async with aiohttp.ClientSession() as session: 49 | async with session.post(f"{BASE_URL}/order", json=body) as resp: 50 | return json.loads(await resp.text()) 51 | 52 | # 雀魂需要做缓存 53 | async def convert_majsoul(self, majsoul_url: str) -> Dict: 54 | body = { 55 | "secret": NAGA_SECRET, 56 | "majsoul_url": majsoul_url 57 | } 58 | 59 | prefixes = ['https://game.maj-soul.com/1/?paipu=', 'https://game.maj-soul.net/1/?paipu='] # 暂时只考虑支持国服 60 | 61 | haihu_id = '' 62 | 63 | for prefix in prefixes: 64 | if majsoul_url.startswith(prefix): 65 | haihu_id = majsoul_url[len(prefix):] 66 | break 67 | 68 | if haihu_id == '': 69 | return {"status": 400, "message": "majsoul_url is not valid"} 70 | 71 | cache_data = DictRedisData(f'convert_cache_{haihu_id}') 72 | 73 | index_data = DictRedisData('majsoul_convert_index_map') 74 | 75 | if len(cache_data.data) == 0: 76 | async with aiohttp.ClientSession() as session: 77 | async with session.post(f"{BASE_URL}/convert_majsoul", json=body) as resp: 78 | print(await resp.text()) 79 | data = json.loads(await resp.text()) 80 | if data['status'] == 200: 81 | majsoul_convert_index = redis_global.incr('majsoul_convert_index') 82 | cache_data.save({ 83 | 'majsoul_convert_index': majsoul_convert_index, 84 | 'message': data['message'] 85 | }, ex=30 * 24 * 60 * 60) # 1 month 86 | index_data.data[majsoul_convert_index] = f'convert_cache_{haihu_id}' 87 | index_data.save() 88 | 89 | return {"status": 200, "message": cache_data.data['message'], "index": cache_data.data['majsoul_convert_index']} 90 | 91 | async def order_report_list(self) -> Dict: 92 | async with aiohttp.ClientSession() as session: 93 | async with session.get(f"{BASE_URL}/order_report_list") as resp: 94 | return json.loads(await resp.text()) 95 | 96 | async def get_tenhou_custom_url(self, custom_no, index): 97 | index_data = DictRedisData('majsoul_convert_index_map') 98 | cache_data = DictRedisData(index_data.data[str(custom_no)]) 99 | return cache_data.data['message'][index] 100 | 101 | async def find_paipu(self, custom: bool, data: str) -> (int, str, str): # code, link, message 102 | # data will be a time format if custom, or a tenhou url 103 | if custom: 104 | ts = datetime.strptime(data, '%Y-%m-%dT%H:%M:%S').timestamp() 105 | json_data = await self.order_report_list() 106 | orders = list(filter(lambda e: e[0][:6] == "custom", json_data['order'])) 107 | reports = list(filter(lambda e: e[0][:6] == "custom", json_data['report'])) 108 | min_time = 10 109 | url = '' 110 | haihu = ["", 0, [2, 2, 2], 0] 111 | # 首先检测 order 是否成功 112 | for o in orders: 113 | ts2 = datetime.strptime(o[0].split('_')[2], '%Y-%m-%dT%H:%M:%S').timestamp() 114 | if abs(ts - ts2) < min_time: 115 | haihu = o 116 | min_time = abs(ts - ts2) 117 | 118 | print(haihu) 119 | if haihu[0] != "": 120 | if haihu[1] == 3: 121 | return 2, "", "牌谱解析失败" 122 | elif haihu[1] == 0: 123 | for r in reports: 124 | if r[0] == haihu[0]: 125 | break 126 | return 0, f'https://naga.dmv.nico/htmls/{r[2]}.html', "" 127 | else: 128 | return 1, "", "牌谱正在解析中" 129 | return -1, "", "未找到牌谱" 130 | 131 | else: 132 | query_str = data[data.index('?')+1:] 133 | query = [a.split('=') for a in query_str.split('&')] 134 | json_data = await self.order_report_list() 135 | orders = list(filter(lambda e: e[0][:6] != "custom", json_data['order'])) 136 | reports = list(filter(lambda e: e[0][:6] != "custom", json_data['report'])) 137 | haihu = ["", 0, [2, 2, 2], 0] 138 | for o in orders: 139 | if o[0] == query[0][1]: 140 | haihu = o 141 | break 142 | 143 | print(haihu) 144 | if haihu[0] != "": 145 | if haihu[1] == 3: 146 | return 2, "", "牌谱解析失败" 147 | elif haihu[1] == 0: 148 | for r in reports: 149 | if r[0] == haihu[0]: 150 | break 151 | return 0, f'https://naga.dmv.nico/htmls/{r[2]}.html', "" 152 | else: 153 | return 1, "", "牌谱正在解析中" 154 | return -1, "", "未找到牌谱" 155 | 156 | return -1, "", "未找到牌谱" 157 | 158 | auto_naga = AutoNaga() 159 | -------------------------------------------------------------------------------- /src/libraries/fishgame.py: -------------------------------------------------------------------------------- 1 | from src.data_access.redis import DictRedisData, redis_global 2 | import hashlib 3 | import json 4 | import random 5 | import time 6 | import string 7 | 8 | def generate_mixed_gibberish(length=10): 9 | # 中文字符的 Unicode 范围:常用汉字范围大约在 0x4E00-0x9FFF 10 | result = [] 11 | 12 | for _ in range(length): 13 | # 随机决定是生成中文还是英文字符 14 | if random.random() < 0.5: # 50% 的概率生成中文 15 | # 随机选择一个 Unicode 编码点,生成一个中文字符 16 | char_code = random.randint(0x4E00, 0x9FFF) 17 | result.append(chr(char_code)) 18 | else: # 50% 的概率生成英文 19 | # 随机选择英文字母(大小写)或数字 20 | choices = string.ascii_letters + string.digits 21 | result.append(random.choice(choices)) 22 | 23 | return ''.join(result) 24 | 25 | def md5(s): 26 | return hashlib.md5(s.encode()).hexdigest() 27 | 28 | 29 | def buff_available(buff): 30 | expire = buff.get('expire', 0) 31 | times = buff.get('time', 999) 32 | return times > 0 and (expire == 0 or expire > time.time()) 33 | 34 | 35 | players = {} 36 | 37 | 38 | with open('data/fishgame/fish_data.json', 'r', encoding='utf-8') as f: 39 | fish_data = json.load(f) 40 | 41 | with open('data/fishgame/fish_item.json', 'r', encoding='utf-8') as f: 42 | fish_item = json.load(f) 43 | 44 | def get_item_by_id(item_id): 45 | for item in fish_item: 46 | if item['id'] == item_id: 47 | return item 48 | return None 49 | 50 | with open('data/fishgame/gacha.json', 'r', encoding='utf-8') as f: 51 | gacha_data = json.load(f) 52 | 53 | 54 | class FishPlayer(DictRedisData): 55 | def __init__(self, qq): 56 | self.qq = qq 57 | token = f'fishgame_user_data_{md5(str(qq))}' 58 | super().__init__(token, default=FishPlayer.default_user_data()) 59 | 60 | @staticmethod 61 | def try_get(qq): 62 | token = f'fishgame_user_data_{md5(str(qq))}' 63 | if redis_global.get(token) == None: 64 | return None 65 | return FishPlayer(qq) 66 | 67 | @staticmethod 68 | def default_user_data(): 69 | return { 70 | "name": "渔者", 71 | "level": 1, 72 | "exp": 0, 73 | "gold": 0, 74 | "score": 0, 75 | "fish_log": [], 76 | "bag": [{ 77 | "id": 1, 78 | "name": "食料", 79 | "rarity": 1, 80 | "description": "使用后,在 30 分钟内,【R】鱼的出现概率将大幅提高" 81 | }], 82 | "buff": [] 83 | } 84 | 85 | def refresh_buff(self): 86 | self.data['buff'] = list(filter(buff_available, self.data['buff'])) 87 | 88 | def pop_item(self, item_index): 89 | item = self.bag[item_index] 90 | if item.get('count', 1) > 1: 91 | self.bag[item_index]['count'] -= 1 92 | else: 93 | self.bag.pop(item_index) 94 | 95 | def sort_bag(self): 96 | # 合并所有非装备类物品 97 | new_bag = {} 98 | for item in self.bag: 99 | if item['id'] < 100: 100 | if item['id'] in new_bag: 101 | new_bag[item['id']]['count'] += item.get('count', 1) 102 | else: 103 | new_bag[item['id']] = item 104 | new_bag[item['id']]['count'] = item.get('count', 1) 105 | # 装备类物品 106 | equips = sorted(filter(lambda x: x['id'] >= 100, self.bag), key=lambda x: 0 if x.get('equipped', False) else 1) 107 | consumables = sorted(list(new_bag.values()), key=lambda x: x['id']) 108 | self.data['bag'] = consumables + list(equips) 109 | self.save() 110 | 111 | @property 112 | def name(self): 113 | return self.data.get('name', '渔者') 114 | 115 | @property 116 | def level(self): 117 | return self.data['level'] 118 | 119 | @property 120 | def exp(self): 121 | return self.data['exp'] 122 | 123 | @property 124 | def gold(self): 125 | return self.data['gold'] 126 | 127 | @property 128 | def score(self): 129 | return self.data['score'] 130 | 131 | @property 132 | def fish_log(self): 133 | return self.data['fish_log'] 134 | 135 | @property 136 | def bag(self): 137 | return self.data['bag'] 138 | 139 | @property 140 | def buff(self): 141 | return self.data['buff'] 142 | 143 | @property 144 | def power(self): 145 | base_power = self.level 146 | for equipment in self.bag: 147 | if equipment.get('equipped', False): 148 | base_power += equipment.get('power', 0) 149 | for buff in self.buff: 150 | base_power += buff.get('power', 0) 151 | return base_power 152 | 153 | @property 154 | def equipment(self): 155 | equipment = {} 156 | for item in self.bag: 157 | if item.get('equipped', False) and item.get('type', '') != '': 158 | equipment[item['type']] = item 159 | return equipment 160 | 161 | @staticmethod 162 | def get_target_exp(level): 163 | base = level + 19 164 | return int(base * 1.1 ** (level / 10)) 165 | 166 | def handle_level_up(self): 167 | level_up = False 168 | target_exp = self.get_target_exp(self.level) 169 | while self.exp >= target_exp: 170 | self.data['level'] += 1 171 | self.data['exp'] -= target_exp 172 | target_exp = self.get_target_exp(self.level) 173 | level_up = True 174 | if level_up: 175 | return f"\n等级提升至 {self.level} 级!" 176 | return '' 177 | 178 | 179 | class FishGame(DictRedisData): 180 | def __init__(self, group_id=0): 181 | token = f'fishgame_group_data_{group_id}' 182 | super().__init__(token, default=FishGame.default_group_data()) 183 | self.average_power = 0 184 | self.current_fish = None 185 | self.try_list = [] 186 | self.leave_time = 0 187 | 188 | @staticmethod 189 | def default_group_data(): 190 | return { 191 | "fish_log": [], 192 | "buff": [], 193 | "day": 0, 194 | "feed_time": 0 195 | } 196 | 197 | def refresh_buff(self): 198 | self.data['buff'] = list(filter(buff_available, self.data['buff'])) 199 | if self.data.get('day', 0) != time.localtime().tm_mday: 200 | self.data['day'] = time.localtime().tm_mday 201 | self.data['feed_time'] = 0 202 | 203 | def is_fish_caught(self, name): 204 | for fish in self.data['fish_log']: 205 | if fish['name'] == name: 206 | return True 207 | return False 208 | 209 | def get_buff_for_rarity(self, rarity): 210 | value = 0 211 | for buff in self.data['buff']: 212 | if rarity == buff.get('rarity', ''): 213 | value += buff.get('bonus', 0) 214 | return value 215 | 216 | def update_average_power(self, qq_list): 217 | p = 0 218 | count = 0 219 | for qq in qq_list: 220 | player = None 221 | if qq in players: 222 | player = players[qq] 223 | else: 224 | player = FishPlayer.try_get(qq) 225 | if player is None: 226 | continue 227 | players[qq] = player 228 | print(player.power) 229 | p += player.power 230 | count += 1 231 | self.average_power = p / count 232 | 233 | def count_down(self): 234 | if self.current_fish is None: 235 | return False 236 | self.leave_time -= 1 237 | if self.leave_time == 0: 238 | self.current_fish = None 239 | self.try_list = [] 240 | return True 241 | return False 242 | 243 | def simulate_spawn_fish(self): 244 | self.refresh_buff() 245 | # calculate base probability 246 | prob_dist = list(map(lambda x: x['base_probability'] * (1 + self.get_buff_for_rarity(x['rarity'])), fish_data)) 247 | all_prob = sum(prob_dist) 248 | for i in range(len(prob_dist)): 249 | power = fish_data[i]['std_power'] 250 | # 平均 power 每低于 std_power 5 点,概率乘 0.9 251 | prob_dist[i] *= 0.9 ** max(0, (power - self.average_power) / 5) 252 | print(prob_dist[i]) 253 | # normalize 254 | all_prob2 = sum(prob_dist) 255 | prob_dist = list(map(lambda x: x * all_prob / all_prob2, prob_dist)) 256 | s = '' 257 | for i in range(len(fish_data)): 258 | # check this fish has been caught before 259 | if self.is_fish_caught(fish_data[i]['name']): 260 | s += f'{fish_data[i]["name"]}【{fish_data[i]["rarity"]}】(难度{fish_data[i]['std_power']}): {prob_dist[i]*100:.4f}%\n' 261 | else: 262 | s += f'??????【{fish_data[i]["rarity"]}】: {prob_dist[i]*100:.4f}%\n' 263 | return s 264 | 265 | 266 | def spawn_fish(self): 267 | self.refresh_buff() 268 | # self.current_fish = { 269 | # "name": generate_mixed_gibberish(), 270 | # "detail": "███████████████████████████████████████", 271 | # "rarity": "UR", 272 | # "std_power": 444, 273 | # "base_probability": 0.0003, 274 | # "exp": 7 275 | # } 276 | # return self.current_fish 277 | if self.current_fish is not None: 278 | return self.current_fish 279 | # calculate base probability 280 | prob_dist = list(map(lambda x: x['base_probability'] * (1 + self.get_buff_for_rarity(x['rarity'])), fish_data)) 281 | all_prob = sum(prob_dist) 282 | for i in range(len(prob_dist)): 283 | power = fish_data[i]['std_power'] 284 | # 平均 power 每低于 std_power 5 点,概率乘 0.9 285 | prob_dist[i] *= 0.9 ** max(0, (power - self.average_power) / 15) 286 | # normalize 287 | all_prob2 = sum(prob_dist) 288 | prob_dist = list(map(lambda x: x * all_prob / all_prob2, prob_dist)) 289 | # random 0 - 1 290 | r = random.random() 291 | print(f'random: {r}, target: {all_prob}') 292 | for i in range(len(prob_dist)): 293 | if r < prob_dist[i]: 294 | self.current_fish = fish_data[i] 295 | self.data['fish_log'].append(fish_data[i]) 296 | self.save() 297 | self.leave_time = 5 298 | return fish_data[i] 299 | r -= prob_dist[i] 300 | return None 301 | 302 | def catch_fish(self, player: FishPlayer): 303 | player.refresh_buff() 304 | if self.current_fish is None: 305 | return { 306 | "code": -1, 307 | "message": "当前没有鱼" 308 | } 309 | if player.qq in self.try_list: 310 | return { 311 | "code": -2, 312 | "message": "你已经尝试过捕捉这条鱼了" 313 | } 314 | self.try_list.append(player.qq) 315 | fish = self.current_fish 316 | success_rate = 60 317 | diff = player.power - fish['std_power'] 318 | if diff > 0: 319 | success_rate += (40 - 40 * 0.9 ** (diff / 5)) 320 | else: 321 | success_rate *= 0.9 ** (-diff / 5) 322 | if str(player.qq) == '2300756578': 323 | success_rate = 999.99 324 | 325 | for i, buff in enumerate(player.data['buff']): 326 | if buff.get('time', 0) > 0: 327 | player.data['buff'][i]['time'] -= 1 328 | 329 | if random.random() < success_rate / 100: 330 | player.data['fish_log'].append(fish) 331 | fishing_bonus = 1 332 | for buff in player.buff: 333 | fishing_bonus += buff.get('fishing_bonus', 0) 334 | value = int(fish['exp'] * fishing_bonus) 335 | player.data['exp'] += value 336 | player.data['gold'] += value 337 | msg = f"捕获 {fish['name']}【{fish['rarity']}】 成功(成功率{success_rate:.2f}%),获得了 {value} 经验和金币" 338 | msg += player.handle_level_up() 339 | player.save() 340 | self.current_fish = None 341 | self.try_list = [] 342 | return { 343 | "code": 0, 344 | "message": msg 345 | } 346 | else: 347 | player.data['exp'] += 1 348 | player.save() 349 | msg = f"捕获 {fish['name']}【{fish['rarity']}】 失败(成功率{success_rate:.2f}%),但至少你获得了 1 经验" 350 | flee_rate = { 351 | 'R': 0.2, 352 | 'SR': 0.5, 353 | 'SSR': 0.8, 354 | 'UR': 0 355 | } 356 | if random.random() < flee_rate[fish['rarity']] + len(self.try_list) * 0.1: 357 | self.current_fish = None 358 | self.try_list = [] 359 | msg += f"\n{fish['name']}【{fish['rarity']}】逃走了..." 360 | return { 361 | "code": 1, 362 | "message": msg 363 | } 364 | 365 | def gacha(self, player: FishPlayer, ten_time=False): 366 | need_gold = 100 if ten_time else 10 367 | if player.gold < need_gold: 368 | return { 369 | "code": -1, 370 | "message": "金币不足" 371 | } 372 | player.data['gold'] -= need_gold 373 | result = [] 374 | if ten_time: 375 | for i in range(11): 376 | res = self.gacha_pick() 377 | if res['type'] == 'score': 378 | player.data['score'] += res['value'] 379 | result.append({"name": f"{res['value']} 积分", "description": "可以使用积分在积分商城兑换奖励"}) 380 | elif res['type'] == 'item': 381 | player.bag.append(get_item_by_id(res['value'])) 382 | result.append(get_item_by_id(res['value'])) 383 | else: 384 | res = self.gacha_pick() 385 | if res['type'] == 'score': 386 | player.data['score'] += res['value'] 387 | result.append({"name": f"{res['value']} 积分", "description": "可以使用积分在积分商城兑换奖励"}) 388 | elif res['type'] == 'item': 389 | player.bag.append(get_item_by_id(res['value'])) 390 | result.append(get_item_by_id(res['value'])) 391 | player.save() 392 | return { 393 | "code": 0, 394 | "message": result 395 | } 396 | 397 | def gacha_pick(self): 398 | all_weight = sum(map(lambda x: x['weight'], gacha_data)) 399 | r = random.random() * all_weight 400 | for item in gacha_data: 401 | if r < item['weight']: 402 | return item 403 | r -= item['weight'] 404 | return None 405 | 406 | def get_shop(self): 407 | return list(filter(lambda x: x.get('price', 0) != 0, fish_item)) 408 | 409 | def shop_buy(self, player: FishPlayer, id): 410 | good = get_item_by_id(id) 411 | if good is None or good.get('price', 0) == 0: 412 | return { 413 | "code": -2, 414 | "message": "未找到该商品" 415 | } 416 | if player.gold < good.get('price'): 417 | return { 418 | "code": -1, 419 | "message": "金币不足" 420 | } 421 | player.data['gold'] -= good.get('price') 422 | player.bag.append(good) 423 | player.save() 424 | return { 425 | "code": 0, 426 | "message": f"购买 {good.get('name')} 成功" 427 | } 428 | 429 | def get_status(self): 430 | self.refresh_buff() 431 | s = f'当前池子已经来过 {len(self.data['fish_log'])} 条鱼了' 432 | if self.current_fish is not None: 433 | s += f'\n当前池子中有一条 {self.current_fish["name"]}【{self.current_fish["rarity"]}】!' 434 | for buff in self.data['buff']: 435 | if buff.get('expire', 0) > 0: 436 | s += f'\n【{buff["rarity"]}】种类鱼出现概率 +{100 * buff["bonus"]}% 效果剩余 {int(buff["expire"] - time.time())} 秒' 437 | return { 438 | "code": 0, 439 | "message": s 440 | } 441 | 442 | def use_item(self, player: FishPlayer, item_index): 443 | self.refresh_buff() 444 | if item_index >= len(player.bag): 445 | return { 446 | "code": -1, 447 | "message": "背包中没有该物品" 448 | } 449 | item = player.bag[item_index] 450 | if item.get('equipable', False): 451 | if item.get('equipped', False): 452 | player.bag[item_index]['equipped'] = False 453 | player.save() 454 | return { 455 | "code": 0, 456 | "message": f"卸下 {item.get('name')} 成功" 457 | } 458 | for i in range(len(player.bag)): 459 | if player.bag[i].get('type', '') == item.get('type', '') and player.bag[i].get('equipped', False): 460 | player.bag[i]['equipped'] = False 461 | player.bag[item_index]['equipped'] = True 462 | player.save() 463 | return { 464 | "code": 0, 465 | "message": f"装备 {item.get('name')} 成功" 466 | } 467 | if self.data['feed_time'] >= 5 and item['id'] <= 3: 468 | return { 469 | "code": -2, 470 | "message": "鱼已经吃饱了,明天再喂吧" 471 | } 472 | if item['id'] == 1: 473 | player.pop_item(item_index) 474 | self.data['buff'] = [ 475 | { 476 | "rarity": "R", 477 | "bonus": 40, 478 | "expire": time.time() + 1800 479 | } 480 | ] 481 | self.data['feed_time'] += 1 482 | elif item['id'] == 2: 483 | player.pop_item(item_index) 484 | self.data['buff'] = [ 485 | { 486 | "rarity": "SR", 487 | "bonus": 100, 488 | "expire": time.time() + 3600 489 | }, 490 | { 491 | "rarity": "R", 492 | "bonus": 60, 493 | "expire": time.time() + 3600 494 | } 495 | ] 496 | self.data['feed_time'] += 1 497 | elif item['id'] == 3: 498 | player.pop_item(item_index) 499 | self.data['buff'] = [ 500 | { 501 | "rarity": "SSR", 502 | "bonus": 200, 503 | "expire": time.time() + 7200 504 | }, 505 | { 506 | "rarity": "SR", 507 | "bonus": 120, 508 | "expire": time.time() + 7200 509 | }, 510 | { 511 | "rarity": "R", 512 | "bonus": 60, 513 | "expire": time.time() + 7200 514 | } 515 | ] 516 | self.data['feed_time'] += 1 517 | elif item['id'] == 4: 518 | player.pop_item(item_index) 519 | player.data['buff'] = list(filter(lambda buff: buff.get('power', 0) == 0, player.data['buff'])) 520 | player.data['buff'].append({ 521 | "power": 10, 522 | "time": 1 523 | }) 524 | elif item['id'] == 5: 525 | player.pop_item(item_index) 526 | player.data['buff'] = list(filter(lambda buff: buff.get('power', 0) == 0, player.data['buff'])) 527 | player.data['buff'].append({ 528 | "power": 20, 529 | "time": 2 530 | }) 531 | elif item['id'] == 6: 532 | player.pop_item(item_index) 533 | player.data['buff'] = list(filter(lambda buff: buff.get('power', 0) == 0, player.data['buff'])) 534 | player.data['buff'].append({ 535 | "power": 40, 536 | "time": 4 537 | }) 538 | elif item['id'] == 7: 539 | player.pop_item(item_index) 540 | player.data['buff'] = list(filter(lambda buff: buff.get('fishing_bonus', 0) == 0, player.data['buff'])) 541 | player.data['buff'].append({ 542 | "fishing_bonus": 0.25, 543 | "expire": time.time() + 1200 544 | }) 545 | elif item['id'] == 8: 546 | player.pop_item(item_index) 547 | player.data['buff'] = list(filter(lambda buff: buff.get('fishing_bonus', 0) == 0, player.data['buff'])) 548 | player.data['buff'].append({ 549 | "fishing_bonus": 0.5, 550 | "expire": time.time() + 1200 551 | }) 552 | elif item['id'] == 9: 553 | player.pop_item(item_index) 554 | player.data['buff'] = list(filter(lambda buff: buff.get('fishing_bonus', 0) == 0, player.data['buff'])) 555 | player.data['buff'].append({ 556 | "fishing_bonus": 1, 557 | "expire": time.time() + 1200 558 | }) 559 | else: 560 | return { 561 | "code": -2, 562 | "message": "该物品效果暂未实装!" 563 | } 564 | self.save() 565 | player.save() 566 | extra = '' 567 | if item['id'] <= 3: 568 | extra += f'\n今天还能投 {5 - self.data["feed_time"]} 次食料' 569 | return { 570 | "code": 0, 571 | "message": f"使用 {item.get('name')} 成功" + extra 572 | } -------------------------------------------------------------------------------- /src/libraries/fishgame_util.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | import textwrap 3 | from src.libraries.fishgame import FishPlayer 4 | import math 5 | from io import BytesIO 6 | import aiohttp 7 | 8 | async def get_qq_avatar(qq_number: int, size: int = 160) -> Image.Image: 9 | """ 10 | 异步获取QQ用户头像并转换为PIL Image对象 11 | 12 | 参数: 13 | qq_number (int): QQ号码 14 | size (int): 头像尺寸,可选值为 40, 100, 140, 640 等,默认为 640 15 | 16 | 返回: 17 | PIL.Image.Image: 头像图片的PIL对象 18 | 19 | 异常: 20 | ValueError: 当QQ号无效时抛出 21 | aiohttp.ClientError: 网络请求失败时抛出 22 | IOError: 图片处理失败时抛出 23 | """ 24 | 25 | # 构建QQ头像URL 26 | avatar_url = f"https://q1.qlogo.cn/g?b=qq&nk={qq_number}&s={size}" 27 | 28 | try: 29 | async with aiohttp.ClientSession() as session: 30 | async with session.get(avatar_url) as response: 31 | if response.status != 200: 32 | raise aiohttp.ClientError(f"获取头像失败,状态码: {response.status}") 33 | 34 | # 读取图片数据 35 | image_data = await response.read() 36 | 37 | # 转换为PIL Image对象 38 | img = Image.open(BytesIO(image_data)) 39 | # 确保图片完全加载 40 | img.load() 41 | return img 42 | 43 | except aiohttp.ClientError as e: 44 | raise aiohttp.ClientError(f"网络请求错误: {e}") 45 | except IOError as e: 46 | raise IOError(f"图片处理错误: {e}") 47 | 48 | def create_character_panel(player: FishPlayer, avatar_img: Image.Image): 49 | # 创建一个800x600的图像 50 | width, height = 800, 600 51 | image = Image.new("RGB", (width, height), (40, 42, 54)) # 深色背景 52 | draw = ImageDraw.Draw(image) 53 | 54 | # 加载字体 55 | font_path = "src/static/poke/LXGWWenKai-Regular.ttf" 56 | title_font = ImageFont.truetype(font_path, 32) 57 | header_font = ImageFont.truetype(font_path, 18) 58 | regular_font = ImageFont.truetype(font_path, 16) 59 | small_font = ImageFont.truetype(font_path, 14) 60 | 61 | # 绘制标题 62 | draw.text((width/2, 30), "钓鱼玩家信息", fill=(248, 248, 242), font=title_font, anchor="mm") 63 | 64 | # 绘制基本信息区域 65 | draw.rectangle([(50, 60), (width-50, 160)], fill=(68, 71, 90), outline=(98, 114, 164), width=2) 66 | 67 | # 添加角色头像框 68 | avatar_size = 80 69 | draw.rectangle([(60, 70), (60+avatar_size, 70+avatar_size)], fill=(98, 114, 164), outline=(189, 147, 249), width=2) 70 | 71 | # bilt avatar 72 | avatar_img = avatar_img.resize((avatar_size, avatar_size)) 73 | image.paste(avatar_img, (60, 70)) 74 | 75 | # 基本信息 76 | draw.text((150, 67), player.name, fill=(248, 248, 242), font=header_font) 77 | draw.text((150, 92), f"渔力: {player.power}", fill=(248, 248, 242), font=regular_font) 78 | draw.text((150, 112), f"经验: {player.exp}/{player.get_target_exp(player.level)}", 79 | fill=(248, 248, 242), font=regular_font) 80 | draw.text((150, 132), f"金币: {player.gold}", fill=(248, 248, 242), font=regular_font) 81 | 82 | # 绘制经验条 83 | # exp_bar_width = 200 84 | # exp_bar_height = 10 85 | # exp_ratio = min(character_data['exp'] / character_data['next_level_exp'], 1) 86 | # draw.rectangle([(250, 125), (250+exp_bar_width, 125+exp_bar_height)], fill=(68, 71, 90), outline=(98, 114, 164)) 87 | # draw.rectangle([(250, 125), (250+exp_bar_width*exp_ratio, 125+exp_bar_height)], fill=(80, 250, 123)) 88 | 89 | # 绘制称号 90 | draw.text((width-60, 75), f"Lv{player.level}", fill=(241, 250, 140), font=header_font, anchor="rt") 91 | draw.text((width-60, 100), f"总渔获: {len(player.fish_log)}条", fill=(248, 248, 242), font=regular_font, anchor="rt") 92 | draw.text((width-60, 125), f"积分: {player.score}", fill=(241, 250, 140), font=header_font, anchor="rt") 93 | 94 | # 装备区域 95 | draw.rectangle([(50, 170), (width-50, 300)], fill=(68, 71, 90), outline=(98, 114, 164), width=2) 96 | draw.text((60, 180), "当前装备", fill=(189, 147, 249), font=header_font) 97 | 98 | # 定义装备栏位置 99 | equipment_slots = [ 100 | {"name": "渔具", "x": 70, "y": 210, "item": player.equipment.get("rod")}, 101 | {"name": "工具", "x": 70, "y": 250, "item": player.equipment.get("tool")} 102 | ] 103 | 104 | # 绘制装备栏 105 | for slot in equipment_slots: 106 | # 装备名称 107 | draw.text((slot["x"], slot["y"]), f"{slot['name']}:", fill=(248, 248, 242), font=regular_font) 108 | 109 | item = slot["item"] 110 | if item: 111 | # 获取稀有度对应的颜色 112 | rarity_color = (255, 255, 255) # 默认白色 113 | if item.get("rarity") == 2: 114 | rarity_color = (139, 233, 253) # 稀有 - 蓝色 115 | elif item.get("rarity") == 3: 116 | rarity_color = (255, 121, 198) # 史诗 - 紫色 117 | elif item.get("rarity") == 4: 118 | rarity_color = (241, 250, 140) # 传说 - 黄色 119 | 120 | # 绘制装备信息 121 | item_text = f"{item['name']} (渔力+{item.get('power', 0)})" 122 | draw.text((slot["x"]+60, slot["y"]), item_text, fill=rarity_color, font=regular_font) 123 | 124 | # 显示装备描述 125 | desc = item.get("description", "") 126 | wrapped_desc = textwrap.wrap(desc, width=60) 127 | if wrapped_desc: 128 | draw.text((slot["x"]+60, slot["y"]+20), wrapped_desc[0], fill=(248, 248, 242), font=small_font) 129 | else: 130 | draw.text((slot["x"]+60, slot["y"]), "未装备", fill=(98, 114, 164), font=regular_font) 131 | 132 | # 近期捕获鱼记录区域 133 | draw.rectangle([(50, 310), (width-50, 540)], fill=(68, 71, 90), outline=(98, 114, 164), width=2) 134 | draw.text((60, 320), "近期捕获鱼记录", fill=(189, 147, 249), font=header_font) 135 | 136 | # 绘制渔获记录 137 | y_pos = 350 138 | for i, fish in enumerate(player.fish_log[max(0, len(player.fish_log)-4):]): 139 | if i >= 4: # 最多显示6条记录 140 | break 141 | 142 | # 获取鱼的稀有度对应的颜色 143 | fish_color = (255, 255, 255) # 默认白色 144 | if fish["rarity"] == "R": 145 | fish_color = (139, 233, 253) # R - 蓝色 146 | elif fish["rarity"] == "SR": 147 | fish_color = (255, 121, 198) # SR - 紫色 148 | elif fish["rarity"] == "SSR": 149 | fish_color = (241, 250, 140) # SSR - 黄色 150 | 151 | draw.text((60, y_pos), f"- {fish['name']} [{fish['rarity']}]", fill=fish_color, font=regular_font) 152 | 153 | # 显示鱼的描述(缩短版) 154 | desc = fish["detail"] 155 | desc_index = len(desc) 156 | while True: 157 | if draw.textlength(desc[:desc_index], font=small_font) < 680: 158 | break 159 | desc_index -= 1 160 | if desc_index == len(desc): 161 | draw.text((60, y_pos+20), desc, fill=(248, 48, 242), font=small_font) 162 | draw.text((60, y_pos+20), desc[:desc_index-1] + '...', fill=(248, 248, 242), font=small_font) 163 | 164 | # 显示经验值 165 | draw.text((width-60, y_pos), f"${fish['exp']}", fill=(80, 250, 123), font=regular_font, anchor="rt") 166 | 167 | y_pos += 45 168 | 169 | # 底部提示信息 170 | draw.line([(50, height-50), (width-50, height-50)], fill=(98, 114, 164), width=2) 171 | draw.text((width/2, height-25), "捕鱼达人 v0.1", 172 | fill=(98, 114, 164), font=regular_font, anchor="mm") 173 | 174 | return image 175 | 176 | 177 | def create_inventory_panel(items_data, page, max_page): 178 | width, height = 800, 600 179 | image = Image.new('RGB', (width, height), (40, 42, 54)) # 深色背景 180 | draw = ImageDraw.Draw(image) 181 | 182 | # 加载字体 183 | font_path = "src/static/poke/LXGWWenKai-Regular.ttf" 184 | title_font = ImageFont.truetype(font_path, 32) 185 | header_font = ImageFont.truetype(font_path, 18) 186 | regular_font = ImageFont.truetype(font_path, 16) 187 | small_font = ImageFont.truetype(font_path, 14) 188 | 189 | # 绘制标题和边框 190 | draw.text((width//2, 45), "背包", fill=(248, 248, 242), font=title_font, anchor="mm") 191 | draw.rectangle([(40, 10), (width-40, height-10)], outline=(98, 114, 164), width=2) 192 | 193 | def get_rarity_color(rarity): 194 | if rarity == 1: 195 | return (255, 255, 255) # 普通 - 白色 196 | elif rarity == 2: 197 | return (139, 233, 253) # 稀有 - 蓝色 198 | elif rarity == 3: 199 | return (255, 121, 198) # 史诗 - 紫色 200 | elif rarity == 4: 201 | return (241, 250, 140) # 传说 - 黄色 202 | return (255, 255, 255) 203 | 204 | # 绘制背包栏位 205 | slots_per_row = 2 206 | slot_width = (width - 100) // slots_per_row 207 | slot_height = 90 208 | padding = 15 209 | 210 | # 显示的物品数量(最多10个) 211 | display_items = items_data[:10] 212 | 213 | for i, item in enumerate(display_items): 214 | row = i // slots_per_row 215 | col = i % slots_per_row 216 | 217 | x1 = 50 + col * slot_width 218 | y1 = 70 + row * (slot_height + padding) 219 | x2 = x1 + slot_width - 10 220 | y2 = y1 + slot_height 221 | 222 | # 获取物品信息 223 | item_id = item["id"] 224 | count = item.get('count', 1) 225 | item_name = item["name"] + (f" ×{count}" if count > 1 else "") 226 | item_rarity = item["rarity"] 227 | item_desc = item["description"] 228 | is_equipable = item.get("equipable", False) 229 | 230 | # 绘制物品栏背景 231 | rarity_color = get_rarity_color(item_rarity) 232 | draw.rectangle([(x1, y1), (x2, y2)], fill=(68, 71, 90), outline=(98, 114, 164), width=2) 233 | 234 | # 左侧小图标区域 235 | icon_size = 50 236 | draw.rectangle([(x1+10, y1+15), (x1+10+icon_size, y1+15+icon_size)], 237 | fill=(50, 53, 70), outline=rarity_color) 238 | 239 | # 物品编号 240 | draw.text((x1+35, y1+40), f"{i + 1 + page * 10 - 10}", fill=(248, 248, 242), font=regular_font, anchor="mm") 241 | 242 | # 物品名称 243 | name_x = x1 + icon_size + 20 244 | draw.text((name_x, y1+12), item_name, fill=rarity_color, font=header_font) 245 | 246 | current_y = y1+12+22 247 | 248 | # 装备信息(如果可装备) 249 | if is_equipable: 250 | power = item.get("power", 0) 251 | item_type = item.get("type", "未知") 252 | 253 | # 装备类型中文转换 254 | if item_type == "tool": 255 | type_text = "工具" 256 | elif item_type == "rod": 257 | type_text = "渔具" 258 | else: 259 | type_text = item_type 260 | 261 | if item.get('equipped', False): 262 | equip_text = f"[已装备] 类型: {type_text} | 渔力: {power}" 263 | draw.text((name_x, current_y), equip_text, fill=(250, 250, 70), font=small_font) 264 | else: 265 | equip_text = f"[可装备] 类型: {type_text} | 渔力: {power}" 266 | draw.text((name_x, current_y), equip_text, fill=(80, 250, 123), font=small_font) 267 | current_y += 18 268 | 269 | # 物品描述(处理换行) 270 | desc_width = x2 - name_x - 10 # 描述文本可用宽度 271 | desc_index = 0 272 | while desc_index < len(item_desc): 273 | if draw.textlength(item_desc[:desc_index], font=small_font) < desc_width: 274 | desc_index += 1 275 | continue 276 | desc_index -= 1 277 | draw.text((name_x, current_y), item_desc[:desc_index], fill=(248, 248, 242), font=small_font) 278 | current_y += 18 279 | item_desc = item_desc[desc_index:] 280 | desc_index = 0 281 | 282 | draw.text((name_x, current_y), item_desc, fill=(248, 248, 242), font=small_font) 283 | 284 | # 容量信息 285 | draw.text((width-50, 45), f"第 {page} 页 / 共 {max_page} 页", 286 | fill=(248, 248, 242), font=regular_font, anchor="rt") 287 | 288 | # 容量信息 289 | draw.text((50, 45), f"Usage:使用 <道具编号>", 290 | fill=(124, 124, 124), font=regular_font, anchor="lt") 291 | 292 | return image 293 | 294 | def create_gacha_panel(items_data): 295 | # 计算所需的面板尺寸 296 | items_count = len(items_data) 297 | items_per_row = 4 # 每行最多4个物品 298 | rows = math.ceil(items_count / items_per_row) 299 | 300 | # 设置面板大小 301 | padding = 20 302 | item_width = 170 303 | item_height = 210 304 | 305 | width = padding + (item_width + padding) * min(items_count, items_per_row) 306 | height = 150 + (item_height + padding) * rows + 50 # 标题高度 + 物品高度 + 底部信息 307 | 308 | # 创建画布 309 | image = Image.new("RGB", (width, height), (40, 42, 54)) 310 | draw = ImageDraw.Draw(image) 311 | 312 | # 加载字体 313 | title_font = ImageFont.truetype("src/static/poke/LXGWWenKai-Regular.ttf", 32) 314 | header_font = ImageFont.truetype("src/static/poke/LXGWWenKai-Regular.ttf", 18) 315 | regular_font = ImageFont.truetype("src/static/poke/LXGWWenKai-Regular.ttf", 16) 316 | small_font = ImageFont.truetype("src/static/poke/LXGWWenKai-Regular.ttf", 14) 317 | tiny_font = ImageFont.truetype("src/static/poke/LXGWWenKai-Regular.ttf", 12) 318 | 319 | # 绘制标题 320 | draw.text((width/2, 40), "抽卡结果", fill=(248, 248, 242), font=title_font, anchor="mm") 321 | draw.line([(padding, 80), (width-padding, 80)], fill=(98, 114, 164), width=2) 322 | 323 | # 获取稀有度对应的颜色 324 | def get_rarity_color(rarity): 325 | if rarity == 1: 326 | return (255, 255, 255) # 普通 - 白色 327 | elif rarity == 2: 328 | return (139, 233, 253) # 稀有 - 蓝色 329 | elif rarity == 3: 330 | return (255, 121, 198) # 史诗 - 紫色 331 | elif rarity == 4: 332 | return (241, 250, 140) # 传说 - 黄色 333 | return (255, 255, 255) # 默认 - 白色 334 | 335 | # 手动文本换行函数 336 | def wrap_text(text, font, max_width): 337 | lines = [] 338 | current_line = "" 339 | 340 | for char in text: 341 | test_line = current_line + char 342 | line_width = draw.textlength(test_line, font=font) 343 | 344 | if line_width <= max_width: 345 | current_line = test_line 346 | else: 347 | lines.append(current_line) 348 | current_line = char 349 | 350 | if current_line: # 添加最后一行 351 | lines.append(current_line) 352 | 353 | return lines 354 | 355 | # 绘制抽卡结果项目 356 | for i, item in enumerate(items_data): 357 | row = i // items_per_row 358 | col = i % items_per_row 359 | 360 | x = padding + col * (item_width + padding) 361 | y = 100 + row * (item_height + padding) 362 | 363 | # 获取物品信息 364 | item_id = item.get("id", "") 365 | item_name = item.get("name", "未知") 366 | item_rarity = item.get("rarity", 0) # 积分项没有稀有度 367 | item_desc = item.get("description", "") 368 | is_equipable = item.get("equipable", False) 369 | is_points = "积分" in item_name 370 | 371 | # 绘制项目背景 372 | rarity_color = get_rarity_color(item_rarity) if not is_points else (255, 184, 108) # 积分用橙色 373 | draw.rectangle([(x, y), (x+item_width, y+item_height)], 374 | fill=(68, 71, 90), outline=rarity_color, width=3) 375 | 376 | # 随机ID区域或者积分标识 377 | if is_points: 378 | # 如果是积分奖励 379 | icon_size = 60 380 | icon_x = x + (item_width - icon_size) // 2 381 | icon_y = y + 30 382 | # 绘制一个圆形或图标表示积分 383 | draw.ellipse([(icon_x, icon_y), (icon_x+icon_size, icon_y+icon_size)], 384 | fill=(255, 184, 108), outline=(255, 184, 108)) 385 | 386 | # 修复:使用更小的字体来防止积分文字重叠 387 | draw.text((icon_x+icon_size//2, icon_y+icon_size//2), "积分", 388 | fill=(40, 42, 54), font=tiny_font, anchor="mm") 389 | 390 | # # 积分数量 391 | # points_amount = item_name.split()[0] 392 | # draw.text((x+item_width//2, icon_y+icon_size+20), 393 | # f"{points_amount}", fill=(255, 184, 108), 394 | # font=title_font, anchor="mm") 395 | else: 396 | # 普通物品 397 | icon_size = 60 398 | icon_x = x + (item_width - icon_size) // 2 399 | icon_y = y + 30 400 | # 绘制一个方形图标 401 | draw.rectangle([(icon_x, icon_y), (icon_x+icon_size, icon_y+icon_size)], 402 | fill=(50, 53, 70), outline=rarity_color) 403 | 404 | # 物品ID 405 | if item_id: 406 | draw.text((icon_x+icon_size//2, icon_y+icon_size//2), f"{item_id}", 407 | fill=(248, 248, 242), font=header_font, anchor="mm") 408 | 409 | # 物品名称 410 | name_y = y + 110 411 | draw.text((x+item_width//2, name_y), item_name, 412 | fill=rarity_color, font=header_font, anchor="mm") 413 | 414 | # 装备信息(如果可装备) 415 | if is_equipable: 416 | power = item.get("power", 0) 417 | item_type = item.get("type", "未知") 418 | 419 | # 装备类型中文转换 420 | if item_type == "tool": 421 | type_text = "工具" 422 | elif item_type == "rod": 423 | type_text = "渔具" 424 | else: 425 | type_text = item_type 426 | 427 | equip_text = f"{type_text} | 渔力+{power}" 428 | draw.text((x+item_width//2, name_y+25), equip_text, 429 | fill=(80, 250, 123), font=small_font, anchor="mm") 430 | 431 | # 物品描述(使用自定义换行函数) 432 | desc_y = name_y + (45 if is_equipable else 25) 433 | 434 | # 设置最大文本宽度略小于项目宽度 435 | max_text_width = item_width - 20 436 | wrapped_text = wrap_text(item_desc, small_font, max_text_width) 437 | 438 | # 最多显示3行描述 439 | for j, line in enumerate(wrapped_text[:3]): 440 | draw.text((x+item_width//2, desc_y+j*20), line, 441 | fill=(248, 248, 242), font=small_font, anchor="mm") 442 | 443 | # 如果描述被截断,添加省略号 444 | if len(wrapped_text) > 3: 445 | last_line = wrapped_text[2] 446 | # 检查最后一行是否需要添加省略号 447 | if j == 2: # 到达了第三行 448 | # 确保省略号不会导致文本溢出 449 | last_line_width = draw.textlength(last_line, font=small_font) 450 | ellipsis_width = draw.textlength("...", font=small_font) 451 | 452 | if last_line_width + ellipsis_width > max_text_width: 453 | # 需要截断最后一行并添加省略号 454 | truncated_line = last_line 455 | while draw.textlength(truncated_line + "...", font=small_font) > max_text_width: 456 | truncated_line = truncated_line[:-1] 457 | 458 | draw.text((x+item_width//2, desc_y+2*20), 459 | truncated_line + "...", fill=(248, 248, 242), 460 | font=small_font, anchor="mm") 461 | else: 462 | # 直接添加省略号 463 | draw.text((x+item_width//2, desc_y+2*20), 464 | last_line + "...", fill=(248, 248, 242), 465 | font=small_font, anchor="mm") 466 | 467 | # 底部信息 468 | draw.line([(padding, height-50), (width-padding, height-50)], fill=(98, 114, 164), width=2) 469 | draw.text((width/2, height-25), 470 | f"抽取了 {items_count} 件物品", 471 | fill=(98, 114, 164), font=regular_font, anchor="mm") 472 | 473 | return image 474 | 475 | def create_shop_panel(shop_items): 476 | # 基本设置 477 | padding = 20 478 | item_width = 200 479 | item_height = 220 480 | items_per_row = 4 # 每行最多4个商品 481 | 482 | # 计算面板尺寸 483 | items_count = len(shop_items) 484 | rows = math.ceil(items_count / items_per_row) 485 | width = padding + (item_width + padding) * min(items_count, items_per_row) 486 | height = 100 + (item_height + padding) * rows + 80 # 标题100px, 底部80px 487 | 488 | # 创建图像和绘图对象 489 | image = Image.new('RGB', (width, height), color=(40, 42, 54)) 490 | draw = ImageDraw.Draw(image) 491 | 492 | # 加载字体 493 | title_font = ImageFont.truetype("src/static/poke/LXGWWenKai-Regular.ttf", 32) 494 | header_font = ImageFont.truetype("src/static/poke/LXGWWenKai-Regular.ttf", 18) 495 | price_font = ImageFont.truetype("src/static/poke/LXGWWenKai-Regular.ttf", 20) 496 | regular_font = ImageFont.truetype("src/static/poke/LXGWWenKai-Regular.ttf", 16) 497 | small_font = ImageFont.truetype("src/static/poke/LXGWWenKai-Regular.ttf", 14) 498 | tiny_font = ImageFont.truetype("src/static/poke/LXGWWenKai-Regular.ttf", 12) 499 | 500 | # 绘制标题 501 | draw.text((width/2, 40), "金币商城", fill=(248, 248, 242), font=title_font, anchor="mm") 502 | draw.line([(padding, 80), (width-padding, 80)], fill=(98, 114, 164), width=2) 503 | 504 | # 获取稀有度对应的颜色 505 | def get_rarity_color(rarity): 506 | if rarity == 1: 507 | return (255, 255, 255) # 普通 - 白色 508 | elif rarity == 2: 509 | return (139, 233, 253) # 稀有 - 蓝色 510 | elif rarity == 3: 511 | return (255, 121, 198) # 史诗 - 紫色 512 | elif rarity == 4: 513 | return (241, 250, 140) # 传说 - 黄色 514 | return (255, 255, 255) # 默认 - 白色 515 | 516 | # 手动文本换行函数 517 | def wrap_text(text, font, max_width): 518 | lines = [] 519 | current_line = "" 520 | 521 | for char in text: 522 | test_line = current_line + char 523 | line_width = draw.textlength(test_line, font=font) 524 | 525 | if line_width <= max_width: 526 | current_line = test_line 527 | else: 528 | lines.append(current_line) 529 | current_line = char 530 | 531 | if current_line: # 添加最后一行 532 | lines.append(current_line) 533 | 534 | return lines 535 | 536 | # 绘制商品 537 | for i, item in enumerate(shop_items): 538 | row = i // items_per_row 539 | col = i % items_per_row 540 | 541 | x = padding + col * (item_width + padding) 542 | y = 100 + row * (item_height + padding) 543 | 544 | # 获取商品信息 545 | item_id = item.get("id", "") 546 | item_name = item.get("name", "未知") 547 | item_rarity = item.get("rarity", 1) 548 | item_desc = item.get("description", "") 549 | item_price = item.get("price", 0) 550 | item_stock = item.get("stock", "无限") 551 | is_equipable = item.get("equipable", False) 552 | is_limited = item_stock != "无限" 553 | 554 | # 绘制商品背景 555 | rarity_color = get_rarity_color(item_rarity) 556 | draw.rectangle([(x, y), (x+item_width, y+item_height)], 557 | fill=(68, 71, 90), outline=rarity_color, width=3) 558 | 559 | # 商品图标 560 | icon_size = 60 561 | icon_x = x + (item_width - icon_size) // 2 562 | icon_y = y + 20 563 | 564 | # 绘制一个方形图标 565 | draw.rectangle([(icon_x, icon_y), (icon_x+icon_size, icon_y+icon_size)], 566 | fill=(50, 53, 70), outline=rarity_color) 567 | 568 | # 物品ID 569 | if item_id: 570 | draw.text((icon_x+icon_size//2, icon_y+icon_size//2), f"{item_id}", 571 | fill=(248, 248, 242), font=header_font, anchor="mm") 572 | 573 | # 物品名称 574 | name_y = y + 100 575 | draw.text((x+item_width//2, name_y), item_name, 576 | fill=rarity_color, font=header_font, anchor="mm") 577 | 578 | # 装备信息(如果可装备) 579 | if is_equipable: 580 | power = item.get("power", 0) 581 | item_type = item.get("type", "未知") 582 | 583 | # 装备类型中文转换 584 | if item_type == "tool": 585 | type_text = "工具" 586 | elif item_type == "rod": 587 | type_text = "渔具" 588 | else: 589 | type_text = item_type 590 | 591 | equip_text = f"{type_text} | 渔力+{power}" 592 | draw.text((x+item_width//2, name_y+20), equip_text, 593 | fill=(80, 250, 123), font=small_font, anchor="mm") 594 | 595 | # 物品描述 596 | desc_y = name_y + (40 if is_equipable else 20) 597 | 598 | # 设置最大文本宽度略小于项目宽度 599 | max_text_width = item_width - 20 600 | wrapped_text = wrap_text(item_desc, small_font, max_text_width) 601 | 602 | # 最多显示2行描述(商店里需要给价格留位置) 603 | for j, line in enumerate(wrapped_text[:3]): 604 | draw.text((x+item_width//2, desc_y+j*20), line, 605 | fill=(248, 248, 242), font=small_font, anchor="mm") 606 | 607 | # 如果描述被截断,添加省略号 608 | if len(wrapped_text) > 3: 609 | last_line = wrapped_text[1] 610 | # 检查最后一行是否需要添加省略号 611 | if j == 1: # 到达了第二行 612 | # 确保省略号不会导致文本溢出 613 | last_line_width = draw.textlength(last_line, font=small_font) 614 | ellipsis_width = draw.textlength("...", font=small_font) 615 | 616 | if last_line_width + ellipsis_width > max_text_width: 617 | # 需要截断最后一行并添加省略号 618 | truncated_line = last_line 619 | while draw.textlength(truncated_line + "...", font=small_font) > max_text_width: 620 | truncated_line = truncated_line[:-1] 621 | 622 | draw.text((x+item_width//2, desc_y+1*20), 623 | truncated_line + "...", fill=(248, 248, 242), 624 | font=small_font, anchor="mm") 625 | else: 626 | # 直接添加省略号 627 | draw.text((x+item_width//2, desc_y+1*20), 628 | last_line + "...", fill=(248, 248, 242), 629 | font=small_font, anchor="mm") 630 | 631 | # 绘制价格和库存信息 632 | price_y = y + item_height - 45 633 | 634 | # 价格区域 635 | price_bg_height = 30 636 | price_bg_y = price_y - 5 637 | draw.rectangle([(x+10, price_bg_y), (x+item_width-10, price_bg_y+price_bg_height)], 638 | fill=(60, 62, 80), outline=None) 639 | 640 | # 价格文字 641 | price_icon_color = (255, 184, 108) # 积分颜色 - 橙色 642 | price_text = f"{item_price}" 643 | 644 | # 绘制积分图标 645 | draw.ellipse([(x+20, price_y), (x+20+20, price_y+20)], 646 | fill=price_icon_color) 647 | draw.text((x+20+10, price_y+10), "G", 648 | fill=(40, 42, 54), font=tiny_font, anchor="mm") 649 | 650 | # 绘制价格 651 | draw.text((x+50, price_y+10), price_text, 652 | fill=price_icon_color, font=price_font, anchor="lm") 653 | 654 | # 绘制库存信息(如果有限制) 655 | if is_limited: 656 | stock_text = f"库存: {item_stock}" 657 | draw.text((x+item_width-20, price_y+10), stock_text, 658 | fill=(248, 248, 242), font=small_font, anchor="rm") 659 | 660 | # 底部信息 661 | draw.line([(padding, height-60), (width-padding, height-60)], fill=(98, 114, 164), width=2) 662 | draw.text((width/2, height-35), 663 | "Usage: 商店购买 <商品编号>", 664 | fill=(98, 114, 164), font=regular_font, anchor="mm") 665 | 666 | # 积分余额显示 667 | # balance_text = "当前金币: 150" 668 | # draw.text((width-padding, height-35), 669 | # balance_text, fill=(255, 184, 108), 670 | # font=regular_font, anchor="rm") 671 | 672 | return image 673 | -------------------------------------------------------------------------------- /src/libraries/gosen_choyen.py: -------------------------------------------------------------------------------- 1 | # Author: Diving_Fish 2 | 3 | from typing import Tuple, List 4 | from PIL import Image, ImageDraw, ImageFont, ImageColor 5 | 6 | # Define your font here. 7 | __font_path = "./src/static/msyhbd.ttc" 8 | __font = ImageFont.truetype(__font_path, 100, encoding='utf-8') 9 | 10 | 11 | class VerticalColorGradient: 12 | def __init__(self): 13 | self.colors: List[Tuple[float, Tuple[int, int, int]]] = [] 14 | 15 | def add_color_stop(self, ratio, color: Tuple[int, int, int]): 16 | self.colors.append((ratio, color)) 17 | 18 | def get_color(self, ratio) -> Tuple[int, int, int]: 19 | if len(self.colors) == 0: 20 | return 0, 0, 0 21 | for i in range(len(self.colors)): 22 | if i + 1 >= len(self.colors): 23 | return self.colors[i][1] 24 | elif ratio <= self.colors[i + 1][0]: 25 | t = 1 - (self.colors[i + 1][0] - ratio) / (self.colors[i + 1][0] - self.colors[i][0]) 26 | r = self.colors[i + 1][1][0] * t + self.colors[i][1][0] * (1 - t) 27 | g = self.colors[i + 1][1][1] * t + self.colors[i][1][1] * (1 - t) 28 | b = self.colors[i + 1][1][2] * t + self.colors[i][1][2] * (1 - t) 29 | return int(r + 0.5), int(g + 0.5), int(b + 0.5) 30 | 31 | 32 | def get_vcg_bg(width, height, vcg: VerticalColorGradient, offset=0): 33 | im = Image.new('RGBA', (width, height)) 34 | draw = ImageDraw.Draw(im) 35 | for i in range(height): 36 | if i < offset: 37 | color = (0, 0, 0, 0) 38 | else: 39 | color = vcg.get_color((i - offset) / (height - offset)) 40 | draw.line((0, i, width - 1, i), fill=(color[0], color[1], color[2], 255), width=1) 41 | return im 42 | 43 | 44 | def canvas(text, stroke_width): 45 | im = Image.new('RGBA', (500, 500), (255, 255, 255, 255)) 46 | draw = ImageDraw.Draw(im) 47 | size = draw.textsize(text, __font, stroke_width=stroke_width) 48 | im = im.resize(size) 49 | draw = ImageDraw.Draw(im) 50 | return im, draw 51 | 52 | 53 | def get_text_mask(text): 54 | im, draw = canvas(text, 0) 55 | draw.text((0, 0), text, fill=(0, 0, 0, 0), font=__font) 56 | return im 57 | 58 | 59 | def get_text_stroke_mask(text, stroke_width): 60 | im, draw = canvas(text, stroke_width) 61 | draw.text((0, 0), text, fill=(255, 255, 255, 255), font=__font, stroke_width=stroke_width, stroke_fill=(0, 0, 0, 0)) 62 | return im 63 | 64 | 65 | def vcg_text(text, vcg, stroke_width=0, offset=0): 66 | if stroke_width: 67 | im1 = get_text_stroke_mask(text, stroke_width) 68 | im2 = get_vcg_bg(im1.size[0], im1.size[1], vcg, offset) 69 | im3 = Image.new('RGBA', im2.size, (0, 0, 0, 0)) 70 | im2.paste(im3, mask=im1) 71 | return im2 72 | else: 73 | im1 = get_text_mask(text) 74 | im2 = get_vcg_bg(im1.size[0], im1.size[1], vcg, offset) 75 | im3 = Image.new('RGBA', im2.size, (0, 0, 0, 0)) 76 | im2.paste(im3, mask=im1) 77 | return im2 78 | 79 | 80 | def red_text(text): 81 | uv1 = VerticalColorGradient() 82 | uv1.add_color_stop(0, (0, 0, 0)) 83 | i1 = vcg_text(text, uv1, stroke_width=17, offset=20) 84 | i1 = i1.transform(i1.size, Image.AFFINE, (1, 0, -4, 0, 1, -3)) 85 | 86 | uv2 = VerticalColorGradient() 87 | uv2.add_color_stop(0.0, (0,15,36)) 88 | uv2.add_color_stop(0.10, (255,255,255)) 89 | uv2.add_color_stop(0.18, (55,58,59)) 90 | uv2.add_color_stop(0.25, (55,58,59)) 91 | uv2.add_color_stop(0.5, (200,200,200)) 92 | uv2.add_color_stop(0.75, (55,58,59)) 93 | uv2.add_color_stop(0.85, (25,20,31)) 94 | uv2.add_color_stop(0.91, (240,240,240)) 95 | uv2.add_color_stop(0.95, (166,175,194)) 96 | uv2.add_color_stop(1, (50,50,50)) 97 | i2 = vcg_text(text, uv2, stroke_width=14, offset=20) 98 | i2 = i2.transform(i2.size, Image.AFFINE, (1, 0, -4, 0, 1, -3)) 99 | 100 | uv3 = VerticalColorGradient() 101 | uv3.add_color_stop(0, (0, 0, 0)) 102 | i3 = vcg_text(text, uv3, stroke_width=10, offset=20) 103 | 104 | uv4 = VerticalColorGradient() 105 | uv4.add_color_stop(0, (253,241,0)) 106 | uv4.add_color_stop(0.25, (245,253,187)) 107 | uv4.add_color_stop(0.4, (255,255,255)) 108 | uv4.add_color_stop(0.75, (253,219,9)) 109 | uv4.add_color_stop(0.9, (127,53,0)) 110 | uv4.add_color_stop(1, (243,196,11)) 111 | i4 = vcg_text(text, uv4, 8, 20) 112 | i4 = i4.transform(i4.size, Image.AFFINE, (1, 0, 0, 0, 1, 2)) 113 | 114 | uv5 = VerticalColorGradient() 115 | uv5.add_color_stop(0, (0, 0, 0)) 116 | i5 = vcg_text(text, uv5, stroke_width=4, offset=20) 117 | i5 = i5.transform(i5.size, Image.AFFINE, (1, 0, 0, 0, 1, 2)) 118 | 119 | uv6 = VerticalColorGradient() 120 | uv6.add_color_stop(0, (255, 255, 255)) 121 | i6 = vcg_text(text, uv6, stroke_width=4, offset=20) 122 | i6 = i6.transform(i6.size, Image.AFFINE, (1, 0, 0, 0, 1, 2)) 123 | 124 | red_fill_vcg = VerticalColorGradient() 125 | red_fill_vcg.add_color_stop(0, (255, 100, 0)) 126 | red_fill_vcg.add_color_stop(0.5, (123, 0, 0)) 127 | red_fill_vcg.add_color_stop(0.51, (240, 0, 0)) 128 | red_fill_vcg.add_color_stop(1, (5, 0, 0)) 129 | im = vcg_text(text, red_fill_vcg, 0, 20) 130 | im = im.transform(im.size, Image.AFFINE, (1, 0, 4, 0, 1, 8)) 131 | 132 | uv7 = VerticalColorGradient() 133 | uv7.add_color_stop(0, (230, 0, 0)) 134 | uv7.add_color_stop(0.5, (230, 0, 0)) 135 | uv7.add_color_stop(0.51, (240, 0, 0)) 136 | uv7.add_color_stop(1, (5, 0, 0)) 137 | i7 = vcg_text(text, red_fill_vcg, 1, 20) 138 | i7 = i7.transform(i7.size, Image.AFFINE, (1, 0, 4, 0, 1, 8)) 139 | 140 | l = [i2, i3, i4, i5, i6, im, i7] 141 | for elem in l: 142 | x = i1.size[0] - elem.size[0] 143 | y = i1.size[1] - elem.size[1] 144 | i1.paste(elem, box=(int(x / 2), int(y / 2)), mask=elem) 145 | return i1.transform(i1.size, Image.AFFINE, (1, 0.4, 0, 0, 1, 0)) 146 | 147 | 148 | def sliver_text(text): 149 | uv1 = VerticalColorGradient() 150 | uv1.add_color_stop(0, (0, 0, 0)) 151 | i1 = vcg_text(text, uv1, stroke_width=17, offset=20) 152 | i1 = i1.transform(i1.size, Image.AFFINE, (1, 0, -4, 0, 1, -3)) 153 | 154 | uv2 = VerticalColorGradient() 155 | uv2.add_color_stop(0, (0,15,36)) 156 | uv2.add_color_stop(0.25, (250,250,250)) 157 | uv2.add_color_stop(0.5, (150,150,150)) 158 | uv2.add_color_stop(0.75, (55,58,59)) 159 | uv2.add_color_stop(0.85, (25,20,31)) 160 | uv2.add_color_stop(0.91, (240,240,240)) 161 | uv2.add_color_stop(0.95, (166,175,194)) 162 | uv2.add_color_stop(1, (50,50,50)) 163 | i2 = vcg_text(text, uv2, stroke_width=14, offset=20) 164 | i2 = i2.transform(i2.size, Image.AFFINE, (1, 0, -4, 0, 1, -3)) 165 | 166 | uv3 = VerticalColorGradient() 167 | uv3.add_color_stop(0, (16, 25, 58)) 168 | i3 = vcg_text(text, uv3, stroke_width=12, offset=20) 169 | 170 | uv4 = VerticalColorGradient() 171 | uv4.add_color_stop(0, (221, 221, 221)) 172 | i4 = vcg_text(text, uv4, stroke_width=7, offset=20) 173 | i4 = i4.transform(i4.size, Image.AFFINE, (1, 0, 0, 0, 1, 0)) 174 | 175 | uv5 = VerticalColorGradient() 176 | uv5.add_color_stop(0, (16,25,58)) 177 | uv5.add_color_stop(0.03, (255,255,255)) 178 | uv5.add_color_stop(0.08, (16,25,58)) 179 | uv5.add_color_stop(0.2, (16,25,58)) 180 | uv5.add_color_stop(1, (16,25,58)) 181 | i5 = vcg_text(text, uv5, stroke_width=6, offset=20) 182 | 183 | uv6 = VerticalColorGradient() 184 | uv6.add_color_stop(0, (245,246,248)) 185 | uv6.add_color_stop(0.15, (255,255,255)) 186 | uv6.add_color_stop(0.35, (195,213,220)) 187 | uv6.add_color_stop(0.5, (160,190,201)) 188 | uv6.add_color_stop(0.51, (160,190,201)) 189 | uv6.add_color_stop(0.52, (196,215,222)) 190 | uv6.add_color_stop(1.0, (255,255,255)) 191 | i6 = vcg_text(text, uv6, offset=20) 192 | i6 = i6.transform(i6.size, Image.AFFINE, (1, 0, 6, 0, 1, 8)) 193 | 194 | l = [i2, i3, i4, i5, i6] 195 | for elem in l: 196 | x = i1.size[0] - elem.size[0] 197 | y = i1.size[1] - elem.size[1] 198 | i1.paste(elem, box=(int(x / 2), int(y / 2)), mask=elem) 199 | return i1.transform(i1.size, Image.AFFINE, (1, 0.4, 0, 0, 1, 0)) 200 | 201 | 202 | def generate(red, sliver, offset=-1): 203 | r = red_text(" " + red) 204 | s = sliver_text(" " + sliver) 205 | if offset == -1: 206 | offset = max(0, int(r.size[0] - s.size[0] / 2 - 60)) 207 | i = Image.new('RGB', (max(r.size[0], s.size[0] + offset), r.size[1] + s.size[1]), (255, 255, 255)) 208 | i.paste(r, box=(0, 0), mask=r.split()[3]) 209 | i.paste(s, box=(offset, r.size[1]), mask=s.split()[3]) 210 | return i -------------------------------------------------------------------------------- /src/libraries/image.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from io import BytesIO 3 | 4 | from PIL import ImageFont, ImageDraw, Image 5 | 6 | 7 | path = 'src/static/high_eq_image.png' 8 | fontpath = "src/static/poke/LXGWWenKai-Regular.ttf" 9 | 10 | 11 | def draw_text(img_pil, text, offset_x): 12 | draw = ImageDraw.Draw(img_pil) 13 | font = ImageFont.truetype(fontpath, 48) 14 | width, height = draw.textsize(text, font) 15 | x = 5 16 | if width > 390: 17 | font = ImageFont.truetype(fontpath, int(390 * 48 / width)) 18 | width, height = draw.textsize(text, font) 19 | else: 20 | x = int((400 - width) / 2) 21 | draw.rectangle((x + offset_x - 2, 360, x + 2 + width + offset_x, 360 + height * 1.2), fill=(0, 0, 0, 255)) 22 | draw.text((x + offset_x, 360), text, font=font, fill=(255, 255, 255, 255)) 23 | 24 | 25 | def text_to_image(text): 26 | font = ImageFont.truetype(fontpath, 24) 27 | padding = 10 28 | margin = 4 29 | text_list = text.split('\n') 30 | max_width = 0 31 | for text in text_list: 32 | l, t, r, b = font.getbbox(text) 33 | w = r - l 34 | h = b - t 35 | max_width = max(max_width, w) 36 | wa = max_width + padding * 2 37 | ha = h * len(text_list) + margin * (len(text_list) - 1) + padding * 2 38 | i = Image.new('RGB', (wa, ha), color=(255, 255, 255)) 39 | draw = ImageDraw.Draw(i) 40 | for j in range(len(text_list)): 41 | text = text_list[j] 42 | draw.text((padding, padding + j * (margin + h)), text, font=font, fill=(0, 0, 0)) 43 | return i 44 | 45 | 46 | def image_to_base64(img, format='PNG'): 47 | output_buffer = BytesIO() 48 | img.save(output_buffer, format) 49 | byte_data = output_buffer.getvalue() 50 | base64_str = base64.b64encode(byte_data) 51 | return base64_str 52 | -------------------------------------------------------------------------------- /src/libraries/maimai_best_40.py: -------------------------------------------------------------------------------- 1 | # Author: xyb, Diving_Fish 2 | import asyncio 3 | import os 4 | import math 5 | from typing import Optional, Dict, List 6 | 7 | import aiohttp 8 | from PIL import Image, ImageDraw, ImageFont, ImageFilter 9 | from src.libraries.maimaidx_music import get_cover_len5_id, total_list 10 | 11 | 12 | scoreRank = 'D C B BB BBB A AA AAA S S+ SS SS+ SSS SSS+'.split(' ') 13 | combo = ' FC FC+ AP AP+'.split(' ') 14 | diffs = 'Basic Advanced Expert Master Re:Master'.split(' ') 15 | 16 | 17 | class ChartInfo(object): 18 | def __init__(self, idNum:str, diff:int, tp:str, achievement:float, ra:int, comboId:int, scoreId:int, 19 | title:str, ds:float, lv:str): 20 | self.idNum = idNum 21 | self.diff = diff 22 | self.tp = tp 23 | self.achievement = achievement 24 | self.ra = ra 25 | self.comboId = comboId 26 | self.scoreId = scoreId 27 | self.title = title 28 | self.ds = ds 29 | self.lv = lv 30 | 31 | def __str__(self): 32 | return '%-50s' % f'{self.title} [{self.tp}]' + f'{self.ds}\t{diffs[self.diff]}\t{self.ra}' 33 | 34 | def __eq__(self, other): 35 | return self.ra == other.ra 36 | 37 | def __lt__(self, other): 38 | return self.ra < other.ra 39 | 40 | @classmethod 41 | def from_json(cls, data): 42 | rate = ['d', 'c', 'b', 'bb', 'bbb', 'a', 'aa', 'aaa', 's', 'sp', 'ss', 'ssp', 'sss', 'sssp'] 43 | ri = rate.index(data["rate"]) 44 | fc = ['', 'fc', 'fcp', 'ap', 'app'] 45 | fi = fc.index(data["fc"]) 46 | return cls( 47 | idNum=total_list.by_title(data["title"]).id, 48 | title=data["title"], 49 | diff=data["level_index"], 50 | ra=data["ra"], 51 | ds=data["ds"], 52 | comboId=fi, 53 | scoreId=ri, 54 | lv=data["level"], 55 | achievement=data["achievements"], 56 | tp=data["type"] 57 | ) 58 | 59 | 60 | 61 | class BestList(object): 62 | 63 | def __init__(self, size:int): 64 | self.data = [] 65 | self.size = size 66 | 67 | def push(self, elem:ChartInfo): 68 | if len(self.data) >= self.size and elem < self.data[-1]: 69 | return 70 | self.data.append(elem) 71 | self.data.sort() 72 | self.data.reverse() 73 | while(len(self.data) > self.size): 74 | del self.data[-1] 75 | 76 | def pop(self): 77 | del self.data[-1] 78 | 79 | def __str__(self): 80 | return '[\n\t' + ', \n\t'.join([str(ci) for ci in self.data]) + '\n]' 81 | 82 | def __len__(self): 83 | return len(self.data) 84 | 85 | def __getitem__(self, index): 86 | return self.data[index] 87 | 88 | 89 | class DrawBest(object): 90 | 91 | def __init__(self, sdBest:BestList, dxBest:BestList, userName:str, playerRating:int, musicRating:int): 92 | self.sdBest = sdBest 93 | self.dxBest = dxBest 94 | self.userName = self._stringQ2B(userName) 95 | self.playerRating = playerRating 96 | self.musicRating = musicRating 97 | self.rankRating = self.playerRating - self.musicRating 98 | self.pic_dir = 'src/static/mai/pic/' 99 | self.cover_dir = 'src/static/mai/cover/' 100 | self.img = Image.open(self.pic_dir + 'UI_TTR_BG_Base_Plus.png').convert('RGBA') 101 | self.ROWS_IMG = [2] 102 | for i in range(6): 103 | self.ROWS_IMG.append(116 + 96 * i) 104 | self.COLOUMS_IMG = [] 105 | for i in range(6): 106 | self.COLOUMS_IMG.append(2 + 172 * i) 107 | for i in range(4): 108 | self.COLOUMS_IMG.append(888 + 172 * i) 109 | self.draw() 110 | 111 | def _Q2B(self, uchar): 112 | """单个字符 全角转半角""" 113 | inside_code = ord(uchar) 114 | if inside_code == 0x3000: 115 | inside_code = 0x0020 116 | else: 117 | inside_code -= 0xfee0 118 | if inside_code < 0x0020 or inside_code > 0x7e: #转完之后不是半角字符返回原来的字符 119 | return uchar 120 | return chr(inside_code) 121 | 122 | def _stringQ2B(self, ustring): 123 | """把字符串全角转半角""" 124 | return "".join([self._Q2B(uchar) for uchar in ustring]) 125 | 126 | def _getCharWidth(self, o) -> int: 127 | widths = [ 128 | (126, 1), (159, 0), (687, 1), (710, 0), (711, 1), (727, 0), (733, 1), (879, 0), (1154, 1), (1161, 0), 129 | (4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 1), (8426, 0), (9000, 1), (9002, 2), (11021, 1), 130 | (12350, 2), (12351, 1), (12438, 2), (12442, 0), (19893, 2), (19967, 1), (55203, 2), (63743, 1), 131 | (64106, 2), (65039, 1), (65059, 0), (65131, 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2), 132 | (120831, 1), (262141, 2), (1114109, 1), 133 | ] 134 | if o == 0xe or o == 0xf: 135 | return 0 136 | for num, wid in widths: 137 | if o <= num: 138 | return wid 139 | return 1 140 | 141 | def _coloumWidth(self, s:str): 142 | res = 0 143 | for ch in s: 144 | res += self._getCharWidth(ord(ch)) 145 | return res 146 | 147 | def _changeColumnWidth(self, s:str, len:int) -> str: 148 | res = 0 149 | sList = [] 150 | for ch in s: 151 | res += self._getCharWidth(ord(ch)) 152 | if res <= len: 153 | sList.append(ch) 154 | return ''.join(sList) 155 | 156 | def _resizePic(self, img:Image.Image, time:float): 157 | return img.resize((int(img.size[0] * time), int(img.size[1] * time))) 158 | 159 | def _findRaPic(self) -> str: 160 | num = '10' 161 | if self.playerRating < 1000: 162 | num = '01' 163 | elif self.playerRating < 2000: 164 | num = '02' 165 | elif self.playerRating < 3000: 166 | num = '03' 167 | elif self.playerRating < 4000: 168 | num = '04' 169 | elif self.playerRating < 5000: 170 | num = '05' 171 | elif self.playerRating < 6000: 172 | num = '06' 173 | elif self.playerRating < 7000: 174 | num = '07' 175 | elif self.playerRating < 8000: 176 | num = '08' 177 | elif self.playerRating < 8500: 178 | num = '09' 179 | return f'UI_CMN_DXRating_S_{num}.png' 180 | 181 | def _drawRating(self, ratingBaseImg:Image.Image): 182 | COLOUMS_RATING = [86, 100, 115, 130, 145] 183 | theRa = self.playerRating 184 | i = 4 185 | while theRa: 186 | digit = theRa % 10 187 | theRa = theRa // 10 188 | digitImg = Image.open(self.pic_dir + f'UI_NUM_Drating_{digit}.png').convert('RGBA') 189 | digitImg = self._resizePic(digitImg, 0.6) 190 | ratingBaseImg.paste(digitImg, (COLOUMS_RATING[i] - 2, 9), mask=digitImg.split()[3]) 191 | i = i - 1 192 | return ratingBaseImg 193 | 194 | def _drawBestList(self, img:Image.Image, sdBest:BestList, dxBest:BestList): 195 | itemW = 164 196 | itemH = 88 197 | Color = [(69, 193, 36), (255, 186, 1), (255, 90, 102), (134, 49, 200), (217, 197, 233)] 198 | levelTriagle = [(itemW, 0), (itemW - 27, 0), (itemW, 27)] 199 | rankPic = 'D C B BB BBB A AA AAA S Sp SS SSp SSS SSSp'.split(' ') 200 | comboPic = ' FC FCp AP APp'.split(' ') 201 | imgDraw = ImageDraw.Draw(img) 202 | titleFontName = 'src/static/adobe_simhei.otf' 203 | for num in range(0, len(sdBest)): 204 | i = num // 5 205 | j = num % 5 206 | chartInfo = sdBest[num] 207 | pngPath = self.cover_dir + f'{get_cover_len5_id(chartInfo.idNum)}.png' 208 | if not os.path.exists(pngPath): 209 | pngPath = self.cover_dir + '01000.png' 210 | temp = Image.open(pngPath).convert('RGB') 211 | temp = self._resizePic(temp, itemW / temp.size[0]) 212 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 213 | temp = temp.filter(ImageFilter.GaussianBlur(3)) 214 | temp = temp.point(lambda p: int(p * 0.72)) 215 | 216 | tempDraw = ImageDraw.Draw(temp) 217 | tempDraw.polygon(levelTriagle, Color[chartInfo.diff]) 218 | font = ImageFont.truetype(titleFontName, 16, encoding='utf-8') 219 | title = chartInfo.title 220 | if self._coloumWidth(title) > 15: 221 | title = self._changeColumnWidth(title, 14) + '...' 222 | tempDraw.text((8, 8), title, 'white', font) 223 | font = ImageFont.truetype(titleFontName, 14, encoding='utf-8') 224 | 225 | tempDraw.text((7, 28), f'{"%.4f" % chartInfo.achievement}%', 'white', font) 226 | rankImg = Image.open(self.pic_dir + f'UI_GAM_Rank_{rankPic[chartInfo.scoreId]}.png').convert('RGBA') 227 | rankImg = self._resizePic(rankImg, 0.3) 228 | temp.paste(rankImg, (88, 28), rankImg.split()[3]) 229 | if chartInfo.comboId: 230 | comboImg = Image.open(self.pic_dir + f'UI_MSS_MBase_Icon_{comboPic[chartInfo.comboId]}_S.png').convert('RGBA') 231 | comboImg = self._resizePic(comboImg, 0.45) 232 | temp.paste(comboImg, (119, 27), comboImg.split()[3]) 233 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 12, encoding='utf-8') 234 | tempDraw.text((8, 44), f'Base: {chartInfo.ds} -> {chartInfo.ra}', 'white', font) 235 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 18, encoding='utf-8') 236 | tempDraw.text((8, 60), f'#{num + 1}', 'white', font) 237 | 238 | recBase = Image.new('RGBA', (itemW, itemH), 'black') 239 | recBase = recBase.point(lambda p: int(p * 0.8)) 240 | img.paste(recBase, (self.COLOUMS_IMG[j] + 5, self.ROWS_IMG[i + 1] + 5)) 241 | img.paste(temp, (self.COLOUMS_IMG[j] + 4, self.ROWS_IMG[i + 1] + 4)) 242 | for num in range(len(sdBest), sdBest.size): 243 | i = num // 5 244 | j = num % 5 245 | temp = Image.open(self.cover_dir + f'01000.png').convert('RGB') 246 | temp = self._resizePic(temp, itemW / temp.size[0]) 247 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 248 | temp = temp.filter(ImageFilter.GaussianBlur(1)) 249 | img.paste(temp, (self.COLOUMS_IMG[j] + 4, self.ROWS_IMG[i + 1] + 4)) 250 | for num in range(0, len(dxBest)): 251 | i = num // 3 252 | j = num % 3 253 | chartInfo = dxBest[num] 254 | pngPath = self.cover_dir + f'{get_cover_len5_id(chartInfo.idNum)}.png' 255 | if not os.path.exists(pngPath): 256 | pngPath = self.cover_dir + '01000.png' 257 | temp = Image.open(pngPath).convert('RGB') 258 | temp = self._resizePic(temp, itemW / temp.size[0]) 259 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 260 | temp = temp.filter(ImageFilter.GaussianBlur(3)) 261 | temp = temp.point(lambda p: int(p * 0.72)) 262 | 263 | tempDraw = ImageDraw.Draw(temp) 264 | tempDraw.polygon(levelTriagle, Color[chartInfo.diff]) 265 | font = ImageFont.truetype(titleFontName, 16, encoding='utf-8') 266 | title = chartInfo.title 267 | if self._coloumWidth(title) > 15: 268 | title = self._changeColumnWidth(title, 14) + '...' 269 | tempDraw.text((8, 8), title, 'white', font) 270 | font = ImageFont.truetype(titleFontName, 14, encoding='utf-8') 271 | 272 | tempDraw.text((7, 28), f'{"%.4f" % chartInfo.achievement}%', 'white', font) 273 | rankImg = Image.open(self.pic_dir + f'UI_GAM_Rank_{rankPic[chartInfo.scoreId]}.png').convert('RGBA') 274 | rankImg = self._resizePic(rankImg, 0.3) 275 | temp.paste(rankImg, (88, 28), rankImg.split()[3]) 276 | if chartInfo.comboId: 277 | comboImg = Image.open(self.pic_dir + f'UI_MSS_MBase_Icon_{comboPic[chartInfo.comboId]}_S.png').convert( 278 | 'RGBA') 279 | comboImg = self._resizePic(comboImg, 0.45) 280 | temp.paste(comboImg, (119, 27), comboImg.split()[3]) 281 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 12, encoding='utf-8') 282 | tempDraw.text((8, 44), f'Base: {chartInfo.ds} -> {chartInfo.ra}', 'white', font) 283 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 18, encoding='utf-8') 284 | tempDraw.text((8, 60), f'#{num + 1}', 'white', font) 285 | 286 | recBase = Image.new('RGBA', (itemW, itemH), 'black') 287 | recBase = recBase.point(lambda p: int(p * 0.8)) 288 | img.paste(recBase, (self.COLOUMS_IMG[j + 6] + 5, self.ROWS_IMG[i + 1] + 5)) 289 | img.paste(temp, (self.COLOUMS_IMG[j + 6] + 4, self.ROWS_IMG[i + 1] + 4)) 290 | for num in range(len(dxBest), dxBest.size): 291 | i = num // 3 292 | j = num % 3 293 | temp = Image.open(self.cover_dir + f'01000.png').convert('RGB') 294 | temp = self._resizePic(temp, itemW / temp.size[0]) 295 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 296 | temp = temp.filter(ImageFilter.GaussianBlur(1)) 297 | img.paste(temp, (self.COLOUMS_IMG[j + 6] + 4, self.ROWS_IMG[i + 1] + 4)) 298 | 299 | def draw(self): 300 | splashLogo = Image.open(self.pic_dir + 'UI_CMN_TabTitle_MaimaiTitle_Ver214.png').convert('RGBA') 301 | splashLogo = self._resizePic(splashLogo, 0.65) 302 | self.img.paste(splashLogo, (10, 10), mask=splashLogo.split()[3]) 303 | 304 | ratingBaseImg = Image.open(self.pic_dir + self._findRaPic()).convert('RGBA') 305 | ratingBaseImg = self._drawRating(ratingBaseImg) 306 | ratingBaseImg = self._resizePic(ratingBaseImg, 0.85) 307 | self.img.paste(ratingBaseImg, (240, 8), mask=ratingBaseImg.split()[3]) 308 | 309 | namePlateImg = Image.open(self.pic_dir + 'UI_TST_PlateMask.png').convert('RGBA') 310 | namePlateImg = namePlateImg.resize((285, 40)) 311 | namePlateDraw = ImageDraw.Draw(namePlateImg) 312 | font1 = ImageFont.truetype('src/static/msyh.ttc', 28, encoding='unic') 313 | namePlateDraw.text((12, 4), ' '.join(list(self.userName)), 'black', font1) 314 | nameDxImg = Image.open(self.pic_dir + 'UI_CMN_Name_DX.png').convert('RGBA') 315 | nameDxImg = self._resizePic(nameDxImg, 0.9) 316 | namePlateImg.paste(nameDxImg, (230, 4), mask=nameDxImg.split()[3]) 317 | self.img.paste(namePlateImg, (240, 40), mask=namePlateImg.split()[3]) 318 | 319 | shougouImg = Image.open(self.pic_dir + 'UI_CMN_Shougou_Rainbow.png').convert('RGBA') 320 | shougouDraw = ImageDraw.Draw(shougouImg) 321 | font2 = ImageFont.truetype('src/static/adobe_simhei.otf', 14, encoding='utf-8') 322 | playCountInfo = f'底分: {self.musicRating} + 段位分: {self.rankRating}' 323 | shougouImgW, shougouImgH = shougouImg.size 324 | playCountInfoW, playCountInfoH = shougouDraw.textsize(playCountInfo, font2) 325 | textPos = ((shougouImgW - playCountInfoW - font2.getoffset(playCountInfo)[0]) / 2, 5) 326 | shougouDraw.text((textPos[0] - 1, textPos[1]), playCountInfo, 'black', font2) 327 | shougouDraw.text((textPos[0] + 1, textPos[1]), playCountInfo, 'black', font2) 328 | shougouDraw.text((textPos[0], textPos[1] - 1), playCountInfo, 'black', font2) 329 | shougouDraw.text((textPos[0], textPos[1] + 1), playCountInfo, 'black', font2) 330 | shougouDraw.text((textPos[0] - 1, textPos[1] - 1), playCountInfo, 'black', font2) 331 | shougouDraw.text((textPos[0] + 1, textPos[1] - 1), playCountInfo, 'black', font2) 332 | shougouDraw.text((textPos[0] - 1, textPos[1] + 1), playCountInfo, 'black', font2) 333 | shougouDraw.text((textPos[0] + 1, textPos[1] + 1), playCountInfo, 'black', font2) 334 | shougouDraw.text(textPos, playCountInfo, 'white', font2) 335 | shougouImg = self._resizePic(shougouImg, 1.05) 336 | self.img.paste(shougouImg, (240, 83), mask=shougouImg.split()[3]) 337 | 338 | self._drawBestList(self.img, self.sdBest, self.dxBest) 339 | 340 | authorBoardImg = Image.open(self.pic_dir + 'UI_CMN_MiniDialog_01.png').convert('RGBA') 341 | authorBoardImg = self._resizePic(authorBoardImg, 0.35) 342 | authorBoardDraw = ImageDraw.Draw(authorBoardImg) 343 | authorBoardDraw.text((31, 28), ' Generated By\nXybBot & Chiyuki', 'black', font2) 344 | self.img.paste(authorBoardImg, (1224, 19), mask=authorBoardImg.split()[3]) 345 | 346 | dxImg = Image.open(self.pic_dir + 'UI_RSL_MBase_Parts_01.png').convert('RGBA') 347 | self.img.paste(dxImg, (890, 65), mask=dxImg.split()[3]) 348 | sdImg = Image.open(self.pic_dir + 'UI_RSL_MBase_Parts_02.png').convert('RGBA') 349 | self.img.paste(sdImg, (758, 65), mask=sdImg.split()[3]) 350 | 351 | # self.img.show() 352 | 353 | def getDir(self): 354 | return self.img 355 | 356 | 357 | def computeRa(ds: float, achievement:float) -> int: 358 | baseRa = 15.0 359 | if achievement >= 50 and achievement < 60: 360 | baseRa = 5.0 361 | elif achievement < 70: 362 | baseRa = 6.0 363 | elif achievement < 75: 364 | baseRa = 7.0 365 | elif achievement < 80: 366 | baseRa = 7.5 367 | elif achievement < 90: 368 | baseRa = 8.0 369 | elif achievement < 94: 370 | baseRa = 9.0 371 | elif achievement < 97: 372 | baseRa = 9.4 373 | elif achievement < 98: 374 | baseRa = 10.0 375 | elif achievement < 99: 376 | baseRa = 11.0 377 | elif achievement < 99.5: 378 | baseRa = 12.0 379 | elif achievement < 99.99: 380 | baseRa = 13.0 381 | elif achievement < 100: 382 | baseRa = 13.5 383 | elif achievement < 100.5: 384 | baseRa = 14.0 385 | 386 | return math.floor(ds * (min(100.5, achievement) / 100) * baseRa) 387 | 388 | 389 | async def generate(payload: Dict) -> (Optional[Image.Image], bool): 390 | async with aiohttp.request("POST", "https://www.diving-fish.com/api/maimaidxprober/query/player", json=payload) as resp: 391 | if resp.status == 400: 392 | return None, 400 393 | if resp.status == 403: 394 | return None, 403 395 | sd_best = BestList(25) 396 | dx_best = BestList(15) 397 | obj = await resp.json() 398 | dx: List[Dict] = obj["charts"]["dx"] 399 | sd: List[Dict] = obj["charts"]["sd"] 400 | for c in sd: 401 | sd_best.push(ChartInfo.from_json(c)) 402 | for c in dx: 403 | dx_best.push(ChartInfo.from_json(c)) 404 | pic = DrawBest(sd_best, dx_best, obj["nickname"], obj["rating"] + obj["additional_rating"], obj["rating"]).getDir() 405 | return pic, 0 406 | -------------------------------------------------------------------------------- /src/libraries/maimai_best_50.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import math 4 | from typing import Optional, Dict, List, Tuple 5 | 6 | import aiohttp 7 | from PIL import Image, ImageDraw, ImageFont, ImageFilter 8 | from src.libraries.maimaidx_music import total_list, get_cover_len5_id 9 | 10 | 11 | scoreRank = 'D C B BB BBB A AA AAA S S+ SS SS+ SSS SSS+'.split(' ') 12 | combo = ' FC FC+ AP AP+'.split(' ') 13 | diffs = 'Basic Advanced Expert Master Re:Master'.split(' ') 14 | 15 | 16 | class ChartInfo(object): 17 | def __init__(self, idNum:str, diff:int, tp:str, achievement:float, ra:int, comboId:int, scoreId:int, 18 | title:str, ds:float, lv:str): 19 | self.idNum = idNum 20 | self.diff = diff 21 | self.tp = tp 22 | self.achievement = achievement 23 | self.ra = computeRa(ds,achievement) 24 | self.comboId = comboId 25 | self.scoreId = scoreId 26 | self.title = title 27 | self.ds = ds 28 | self.lv = lv 29 | 30 | def __str__(self): 31 | return '%-50s' % f'{self.title} [{self.tp}]' + f'{self.ds}\t{diffs[self.diff]}\t{self.ra}' 32 | 33 | def __eq__(self, other): 34 | return self.ra == other.ra 35 | 36 | def __lt__(self, other): 37 | return self.ra < other.ra 38 | 39 | @classmethod 40 | def from_json(cls, data): 41 | rate = ['d', 'c', 'b', 'bb', 'bbb', 'a', 'aa', 'aaa', 's', 'sp', 'ss', 'ssp', 'sss', 'sssp'] 42 | ri = rate.index(data["rate"]) 43 | fc = ['', 'fc', 'fcp', 'ap', 'app'] 44 | fi = fc.index(data["fc"]) 45 | return cls( 46 | idNum=total_list.by_title(data["title"]).id, 47 | title=data["title"], 48 | diff=data["level_index"], 49 | ra=data["ra"], 50 | ds=data["ds"], 51 | comboId=fi, 52 | scoreId=ri, 53 | lv=data["level"], 54 | achievement=data["achievements"], 55 | tp=data["type"] 56 | ) 57 | 58 | 59 | 60 | class BestList(object): 61 | 62 | def __init__(self, size:int): 63 | self.data = [] 64 | self.size = size 65 | 66 | def push(self, elem:ChartInfo): 67 | if len(self.data) >= self.size and elem < self.data[-1]: 68 | return 69 | self.data.append(elem) 70 | self.data.sort() 71 | self.data.reverse() 72 | while(len(self.data) > self.size): 73 | del self.data[-1] 74 | 75 | def pop(self): 76 | del self.data[-1] 77 | 78 | def __str__(self): 79 | return '[\n\t' + ', \n\t'.join([str(ci) for ci in self.data]) + '\n]' 80 | 81 | def __len__(self): 82 | return len(self.data) 83 | 84 | def __getitem__(self, index): 85 | return self.data[index] 86 | 87 | 88 | class DrawBest(object): 89 | 90 | def __init__(self, sdBest:BestList, dxBest:BestList, userName:str): 91 | self.sdBest = sdBest 92 | self.dxBest = dxBest 93 | self.userName = self._stringQ2B(userName) 94 | self.sdRating = 0 95 | self.dxRating = 0 96 | for sd in sdBest: 97 | self.sdRating += computeRa(sd.ds, sd.achievement) 98 | for dx in dxBest: 99 | self.dxRating += computeRa(dx.ds, dx.achievement) 100 | self.playerRating = self.sdRating + self.dxRating 101 | self.pic_dir = 'src/static/mai/pic/' 102 | self.cover_dir = 'src/static/mai/cover/' 103 | self.img = Image.open(self.pic_dir + 'UI_TTR_BG_Base_Plus.png').convert('RGBA') 104 | self.ROWS_IMG = [2] 105 | for i in range(6): 106 | self.ROWS_IMG.append(116 + 96 * i) 107 | self.COLOUMS_IMG = [] 108 | for i in range(8): 109 | self.COLOUMS_IMG.append(2 + 138 * i) 110 | for i in range(4): 111 | self.COLOUMS_IMG.append(988 + 138 * i) 112 | self.draw() 113 | 114 | def _Q2B(self, uchar): 115 | """单个字符 全角转半角""" 116 | inside_code = ord(uchar) 117 | if inside_code == 0x3000: 118 | inside_code = 0x0020 119 | else: 120 | inside_code -= 0xfee0 121 | if inside_code < 0x0020 or inside_code > 0x7e: #转完之后不是半角字符返回原来的字符 122 | return uchar 123 | return chr(inside_code) 124 | 125 | def _stringQ2B(self, ustring): 126 | """把字符串全角转半角""" 127 | return "".join([self._Q2B(uchar) for uchar in ustring]) 128 | 129 | def _getCharWidth(self, o) -> int: 130 | widths = [ 131 | (126, 1), (159, 0), (687, 1), (710, 0), (711, 1), (727, 0), (733, 1), (879, 0), (1154, 1), (1161, 0), 132 | (4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 1), (8426, 0), (9000, 1), (9002, 2), (11021, 1), 133 | (12350, 2), (12351, 1), (12438, 2), (12442, 0), (19893, 2), (19967, 1), (55203, 2), (63743, 1), 134 | (64106, 2), (65039, 1), (65059, 0), (65131, 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2), 135 | (120831, 1), (262141, 2), (1114109, 1), 136 | ] 137 | if o == 0xe or o == 0xf: 138 | return 0 139 | for num, wid in widths: 140 | if o <= num: 141 | return wid 142 | return 1 143 | 144 | def _coloumWidth(self, s:str): 145 | res = 0 146 | for ch in s: 147 | res += self._getCharWidth(ord(ch)) 148 | return res 149 | 150 | def _changeColumnWidth(self, s:str, len:int) -> str: 151 | res = 0 152 | sList = [] 153 | for ch in s: 154 | res += self._getCharWidth(ord(ch)) 155 | if res <= len: 156 | sList.append(ch) 157 | return ''.join(sList) 158 | 159 | def _resizePic(self, img:Image.Image, time:float): 160 | return img.resize((int(img.size[0] * time), int(img.size[1] * time))) 161 | 162 | def _findRaPic(self) -> str: 163 | num = '10' 164 | if self.playerRating < 1000: 165 | num = '01' 166 | elif self.playerRating < 2000: 167 | num = '02' 168 | elif self.playerRating < 4000: 169 | num = '03' 170 | elif self.playerRating < 7000: 171 | num = '04' 172 | elif self.playerRating < 10000: 173 | num = '05' 174 | elif self.playerRating < 12000: 175 | num = '06' 176 | elif self.playerRating < 13000: 177 | num = '07' 178 | elif self.playerRating < 14500: 179 | num = '08' 180 | elif self.playerRating < 15000: 181 | num = '09' 182 | return f'UI_CMN_DXRating_S_{num}.png' 183 | 184 | def _drawRating(self, ratingBaseImg:Image.Image): 185 | COLOUMS_RATING = [86, 100, 115, 130, 145] 186 | theRa = self.playerRating 187 | i = 4 188 | while theRa: 189 | digit = theRa % 10 190 | theRa = theRa // 10 191 | digitImg = Image.open(self.pic_dir + f'UI_NUM_Drating_{digit}.png').convert('RGBA') 192 | digitImg = self._resizePic(digitImg, 0.6) 193 | ratingBaseImg.paste(digitImg, (COLOUMS_RATING[i] - 2, 9), mask=digitImg.split()[3]) 194 | i = i - 1 195 | return ratingBaseImg 196 | 197 | def _drawBestList(self, img:Image.Image, sdBest:BestList, dxBest:BestList): 198 | itemW = 131 199 | itemH = 88 200 | Color = [(69, 193, 36), (255, 186, 1), (255, 90, 102), (134, 49, 200), (217, 197, 233)] 201 | levelTriagle = [(itemW, 0), (itemW - 27, 0), (itemW, 27)] 202 | rankPic = 'D C B BB BBB A AA AAA S Sp SS SSp SSS SSSp'.split(' ') 203 | comboPic = ' FC FCp AP APp'.split(' ') 204 | imgDraw = ImageDraw.Draw(img) 205 | titleFontName = 'src/static/adobe_simhei.otf' 206 | for num in range(0, len(sdBest)): 207 | i = num // 7 208 | j = num % 7 209 | chartInfo = sdBest[num] 210 | pngPath = self.cover_dir + f'{get_cover_len5_id(chartInfo.idNum)}.png' 211 | if not os.path.exists(pngPath): 212 | pngPath = self.cover_dir + '01000.png' 213 | temp = Image.open(pngPath).convert('RGB') 214 | temp = self._resizePic(temp, itemW / temp.size[0]) 215 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 216 | temp = temp.filter(ImageFilter.GaussianBlur(3)) 217 | temp = temp.point(lambda p: int(p * 0.72)) 218 | 219 | tempDraw = ImageDraw.Draw(temp) 220 | tempDraw.polygon(levelTriagle, Color[chartInfo.diff]) 221 | font = ImageFont.truetype(titleFontName, 16, encoding='utf-8') 222 | title = chartInfo.title 223 | if self._coloumWidth(title) > 15: 224 | title = self._changeColumnWidth(title, 12) + '...' 225 | tempDraw.text((8, 8), title, 'white', font) 226 | font = ImageFont.truetype(titleFontName, 12, encoding='utf-8') 227 | 228 | tempDraw.text((7, 28), f'{"%.4f" % chartInfo.achievement}%', 'white', font) 229 | rankImg = Image.open(self.pic_dir + f'UI_GAM_Rank_{rankPic[chartInfo.scoreId]}.png').convert('RGBA') 230 | rankImg = self._resizePic(rankImg, 0.3) 231 | temp.paste(rankImg, (72, 28), rankImg.split()[3]) 232 | if chartInfo.comboId: 233 | comboImg = Image.open(self.pic_dir + f'UI_MSS_MBase_Icon_{comboPic[chartInfo.comboId]}_S.png').convert('RGBA') 234 | comboImg = self._resizePic(comboImg, 0.45) 235 | temp.paste(comboImg, (103, 27), comboImg.split()[3]) 236 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 12, encoding='utf-8') 237 | tempDraw.text((8, 44), f'Base: {chartInfo.ds} -> {computeRa(chartInfo.ds, chartInfo.achievement)}', 'white', font) 238 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 18, encoding='utf-8') 239 | tempDraw.text((8, 60), f'#{num + 1}', 'white', font) 240 | 241 | recBase = Image.new('RGBA', (itemW, itemH), 'black') 242 | recBase = recBase.point(lambda p: int(p * 0.8)) 243 | img.paste(recBase, (self.COLOUMS_IMG[j] + 5, self.ROWS_IMG[i + 1] + 5)) 244 | img.paste(temp, (self.COLOUMS_IMG[j] + 4, self.ROWS_IMG[i + 1] + 4)) 245 | for num in range(len(sdBest), sdBest.size): 246 | i = num // 7 247 | j = num % 7 248 | temp = Image.open(self.cover_dir + f'01000.png').convert('RGB') 249 | temp = self._resizePic(temp, itemW / temp.size[0]) 250 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 251 | temp = temp.filter(ImageFilter.GaussianBlur(1)) 252 | img.paste(temp, (self.COLOUMS_IMG[j] + 4, self.ROWS_IMG[i + 1] + 4)) 253 | for num in range(0, len(dxBest)): 254 | i = num // 3 255 | j = num % 3 256 | chartInfo = dxBest[num] 257 | pngPath = self.cover_dir + f'{get_cover_len5_id(chartInfo.idNum)}.png' 258 | if not os.path.exists(pngPath): 259 | pngPath = self.cover_dir + '01000.png' 260 | temp = Image.open(pngPath).convert('RGB') 261 | temp = self._resizePic(temp, itemW / temp.size[0]) 262 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 263 | temp = temp.filter(ImageFilter.GaussianBlur(3)) 264 | temp = temp.point(lambda p: int(p * 0.72)) 265 | 266 | tempDraw = ImageDraw.Draw(temp) 267 | tempDraw.polygon(levelTriagle, Color[chartInfo.diff]) 268 | font = ImageFont.truetype(titleFontName, 14, encoding='utf-8') 269 | title = chartInfo.title 270 | if self._coloumWidth(title) > 13: 271 | title = self._changeColumnWidth(title, 12) + '...' 272 | tempDraw.text((8, 8), title, 'white', font) 273 | font = ImageFont.truetype(titleFontName, 12, encoding='utf-8') 274 | 275 | tempDraw.text((7, 28), f'{"%.4f" % chartInfo.achievement}%', 'white', font) 276 | rankImg = Image.open(self.pic_dir + f'UI_GAM_Rank_{rankPic[chartInfo.scoreId]}.png').convert('RGBA') 277 | rankImg = self._resizePic(rankImg, 0.3) 278 | temp.paste(rankImg, (72, 28), rankImg.split()[3]) 279 | if chartInfo.comboId: 280 | comboImg = Image.open(self.pic_dir + f'UI_MSS_MBase_Icon_{comboPic[chartInfo.comboId]}_S.png').convert( 281 | 'RGBA') 282 | comboImg = self._resizePic(comboImg, 0.45) 283 | temp.paste(comboImg, (103, 27), comboImg.split()[3]) 284 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 12, encoding='utf-8') 285 | tempDraw.text((8, 44), f'Base: {chartInfo.ds} -> {chartInfo.ra}', 'white', font) 286 | font = ImageFont.truetype('src/static/adobe_simhei.otf', 18, encoding='utf-8') 287 | tempDraw.text((8, 60), f'#{num + 1}', 'white', font) 288 | 289 | recBase = Image.new('RGBA', (itemW, itemH), 'black') 290 | recBase = recBase.point(lambda p: int(p * 0.8)) 291 | img.paste(recBase, (self.COLOUMS_IMG[j + 8] + 5, self.ROWS_IMG[i + 1] + 5)) 292 | img.paste(temp, (self.COLOUMS_IMG[j + 8] + 4, self.ROWS_IMG[i + 1] + 4)) 293 | for num in range(len(dxBest), dxBest.size): 294 | i = num // 3 295 | j = num % 3 296 | temp = Image.open(self.cover_dir + f'01000.png').convert('RGB') 297 | temp = self._resizePic(temp, itemW / temp.size[0]) 298 | temp = temp.crop((0, (temp.size[1] - itemH) / 2, itemW, (temp.size[1] + itemH) / 2)) 299 | temp = temp.filter(ImageFilter.GaussianBlur(1)) 300 | img.paste(temp, (self.COLOUMS_IMG[j + 8] + 4, self.ROWS_IMG[i + 1] + 4)) 301 | 302 | def draw(self): 303 | splashLogo = Image.open(self.pic_dir + 'UI_CMN_TabTitle_MaimaiTitle_Ver214.png').convert('RGBA') 304 | splashLogo = self._resizePic(splashLogo, 0.65) 305 | self.img.paste(splashLogo, (10, 10), mask=splashLogo.split()[3]) 306 | 307 | ratingBaseImg = Image.open(self.pic_dir + self._findRaPic()).convert('RGBA') 308 | ratingBaseImg = self._drawRating(ratingBaseImg) 309 | ratingBaseImg = self._resizePic(ratingBaseImg, 0.85) 310 | self.img.paste(ratingBaseImg, (240, 8), mask=ratingBaseImg.split()[3]) 311 | 312 | namePlateImg = Image.open(self.pic_dir + 'UI_TST_PlateMask.png').convert('RGBA') 313 | namePlateImg = namePlateImg.resize((285, 40)) 314 | namePlateDraw = ImageDraw.Draw(namePlateImg) 315 | font1 = ImageFont.truetype('src/static/msyh.ttc', 28, encoding='unic') 316 | namePlateDraw.text((12, 4), ' '.join(list(self.userName)), 'black', font1) 317 | nameDxImg = Image.open(self.pic_dir + 'UI_CMN_Name_DX.png').convert('RGBA') 318 | nameDxImg = self._resizePic(nameDxImg, 0.9) 319 | namePlateImg.paste(nameDxImg, (230, 4), mask=nameDxImg.split()[3]) 320 | self.img.paste(namePlateImg, (240, 40), mask=namePlateImg.split()[3]) 321 | 322 | shougouImg = Image.open(self.pic_dir + 'UI_CMN_Shougou_Rainbow.png').convert('RGBA') 323 | shougouDraw = ImageDraw.Draw(shougouImg) 324 | font2 = ImageFont.truetype('src/static/adobe_simhei.otf', 14, encoding='utf-8') 325 | playCountInfo = f'SD: {self.sdRating} + DX: {self.dxRating} = {self.playerRating}' 326 | shougouImgW, shougouImgH = shougouImg.size 327 | playCountInfoW, playCountInfoH = shougouDraw.textsize(playCountInfo, font2) 328 | textPos = ((shougouImgW - playCountInfoW - font2.getoffset(playCountInfo)[0]) / 2, 5) 329 | shougouDraw.text((textPos[0] - 1, textPos[1]), playCountInfo, 'black', font2) 330 | shougouDraw.text((textPos[0] + 1, textPos[1]), playCountInfo, 'black', font2) 331 | shougouDraw.text((textPos[0], textPos[1] - 1), playCountInfo, 'black', font2) 332 | shougouDraw.text((textPos[0], textPos[1] + 1), playCountInfo, 'black', font2) 333 | shougouDraw.text((textPos[0] - 1, textPos[1] - 1), playCountInfo, 'black', font2) 334 | shougouDraw.text((textPos[0] + 1, textPos[1] - 1), playCountInfo, 'black', font2) 335 | shougouDraw.text((textPos[0] - 1, textPos[1] + 1), playCountInfo, 'black', font2) 336 | shougouDraw.text((textPos[0] + 1, textPos[1] + 1), playCountInfo, 'black', font2) 337 | shougouDraw.text(textPos, playCountInfo, 'white', font2) 338 | shougouImg = self._resizePic(shougouImg, 1.05) 339 | self.img.paste(shougouImg, (240, 83), mask=shougouImg.split()[3]) 340 | 341 | self._drawBestList(self.img, self.sdBest, self.dxBest) 342 | 343 | authorBoardImg = Image.open(self.pic_dir + 'UI_CMN_MiniDialog_01.png').convert('RGBA') 344 | authorBoardImg = self._resizePic(authorBoardImg, 0.35) 345 | authorBoardDraw = ImageDraw.Draw(authorBoardImg) 346 | authorBoardDraw.text((31, 28), ' Generated By\nXybBot & Chiyuki', 'black', font2) 347 | self.img.paste(authorBoardImg, (1224, 19), mask=authorBoardImg.split()[3]) 348 | 349 | dxImg = Image.open(self.pic_dir + 'UI_RSL_MBase_Parts_01.png').convert('RGBA') 350 | self.img.paste(dxImg, (988, 65), mask=dxImg.split()[3]) 351 | sdImg = Image.open(self.pic_dir + 'UI_RSL_MBase_Parts_02.png').convert('RGBA') 352 | self.img.paste(sdImg, (865, 65), mask=sdImg.split()[3]) 353 | 354 | # self.img.show() 355 | 356 | def getDir(self): 357 | return self.img 358 | 359 | 360 | def computeRa(ds: float, achievement: float) -> int: 361 | baseRa = 22.4 362 | if achievement < 50: 363 | baseRa = 7.0 364 | elif achievement < 60: 365 | baseRa = 8.0 366 | elif achievement < 70: 367 | baseRa = 9.6 368 | elif achievement < 75: 369 | baseRa = 11.2 370 | elif achievement < 80: 371 | baseRa = 12.0 372 | elif achievement < 90: 373 | baseRa = 13.6 374 | elif achievement < 94: 375 | baseRa = 15.2 376 | elif achievement < 97: 377 | baseRa = 16.8 378 | elif achievement < 98: 379 | baseRa = 20.0 380 | elif achievement < 99: 381 | baseRa = 20.3 382 | elif achievement < 99.5: 383 | baseRa = 20.8 384 | elif achievement < 100: 385 | baseRa = 21.1 386 | elif achievement < 100.5: 387 | baseRa = 21.6 388 | 389 | return math.floor(ds * (min(100.5, achievement) / 100) * baseRa) 390 | 391 | 392 | async def generate50(payload: Dict) -> Tuple[Optional[Image.Image], bool]: 393 | async with aiohttp.request("POST", "https://www.diving-fish.com/api/maimaidxprober/query/player", json=payload) as resp: 394 | if resp.status == 400: 395 | return None, 400 396 | if resp.status == 403: 397 | return None, 403 398 | sd_best = BestList(35) 399 | dx_best = BestList(15) 400 | obj = await resp.json() 401 | dx: List[Dict] = obj["charts"]["dx"] 402 | sd: List[Dict] = obj["charts"]["sd"] 403 | for c in sd: 404 | sd_best.push(ChartInfo.from_json(c)) 405 | for c in dx: 406 | dx_best.push(ChartInfo.from_json(c)) 407 | pic = DrawBest(sd_best, dx_best, obj["nickname"]).getDir() 408 | return pic, 0 409 | -------------------------------------------------------------------------------- /src/libraries/maimaidx_music.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | from typing import Dict, List, Optional, Union, Tuple, Any 4 | from copy import deepcopy 5 | 6 | import requests 7 | 8 | def get_cover_len5_id(mid) -> str: 9 | mid = int(mid) 10 | if mid > 10000 and mid <= 11000: 11 | mid -= 10000 12 | return f'{mid:05d}' 13 | 14 | def cross(checker: List[Any], elem: Optional[Union[Any, List[Any]]], diff): 15 | ret = False 16 | diff_ret = [] 17 | if not elem or elem is Ellipsis: 18 | return True, diff 19 | if isinstance(elem, List): 20 | for _j in (range(len(checker)) if diff is Ellipsis else diff): 21 | if _j >= len(checker): 22 | continue 23 | __e = checker[_j] 24 | if __e in elem: 25 | diff_ret.append(_j) 26 | ret = True 27 | elif isinstance(elem, Tuple): 28 | for _j in (range(len(checker)) if diff is Ellipsis else diff): 29 | if _j >= len(checker): 30 | continue 31 | __e = checker[_j] 32 | if elem[0] <= __e <= elem[1]: 33 | diff_ret.append(_j) 34 | ret = True 35 | else: 36 | for _j in (range(len(checker)) if diff is Ellipsis else diff): 37 | if _j >= len(checker): 38 | continue 39 | __e = checker[_j] 40 | if elem == __e: 41 | return True, [_j] 42 | return ret, diff_ret 43 | 44 | 45 | def in_or_equal(checker: Any, elem: Optional[Union[Any, List[Any]]]): 46 | if elem is Ellipsis: 47 | return True 48 | if isinstance(elem, List): 49 | return checker in elem 50 | elif isinstance(elem, Tuple): 51 | return elem[0] <= checker <= elem[1] 52 | else: 53 | return checker == elem 54 | 55 | 56 | class Chart(Dict): 57 | tap: Optional[int] = None 58 | slide: Optional[int] = None 59 | hold: Optional[int] = None 60 | touch: Optional[int] = None 61 | brk: Optional[int] = None 62 | charter: Optional[int] = None 63 | 64 | def __getattribute__(self, item): 65 | if item == 'tap': 66 | return self['notes'][0] 67 | elif item == 'hold': 68 | return self['notes'][1] 69 | elif item == 'slide': 70 | return self['notes'][2] 71 | elif item == 'touch': 72 | return self['notes'][3] if len(self['notes']) == 5 else 0 73 | elif item == 'brk': 74 | return self['notes'][-1] 75 | elif item == 'charter': 76 | return self['charter'] 77 | return super().__getattribute__(item) 78 | 79 | 80 | class Music(Dict): 81 | id: Optional[str] = None 82 | title: Optional[str] = None 83 | ds: Optional[List[float]] = None 84 | level: Optional[List[str]] = None 85 | genre: Optional[str] = None 86 | type: Optional[str] = None 87 | bpm: Optional[float] = None 88 | version: Optional[str] = None 89 | charts: Optional[Chart] = None 90 | release_date: Optional[str] = None 91 | artist: Optional[str] = None 92 | 93 | diff: List[int] = [] 94 | 95 | def __getattribute__(self, item): 96 | if item in {'genre', 'artist', 'release_date', 'bpm', 'version'}: 97 | if item == 'version': 98 | return self['basic_info']['from'] 99 | return self['basic_info'][item] 100 | elif item in self: 101 | return self[item] 102 | return super().__getattribute__(item) 103 | 104 | 105 | class MusicList(List[Music]): 106 | def by_id(self, music_id: str) -> Optional[Music]: 107 | for music in self: 108 | if music.id == music_id: 109 | return music 110 | return None 111 | 112 | def by_title(self, music_title: str) -> Optional[Music]: 113 | for music in self: 114 | if music.title == music_title: 115 | return music 116 | return None 117 | 118 | def random(self): 119 | return random.choice(self) 120 | 121 | def filter(self, 122 | *, 123 | level: Optional[Union[str, List[str]]] = ..., 124 | ds: Optional[Union[float, List[float], Tuple[float, float]]] = ..., 125 | title_search: Optional[str] = ..., 126 | genre: Optional[Union[str, List[str]]] = ..., 127 | bpm: Optional[Union[float, List[float], Tuple[float, float]]] = ..., 128 | type: Optional[Union[str, List[str]]] = ..., 129 | diff: List[int] = ..., 130 | ): 131 | new_list = MusicList() 132 | for music in self: 133 | diff2 = diff 134 | music = deepcopy(music) 135 | ret, diff2 = cross(music.level, level, diff2) 136 | if not ret: 137 | continue 138 | ret, diff2 = cross(music.ds, ds, diff2) 139 | if not ret: 140 | continue 141 | if not in_or_equal(music.genre, genre): 142 | continue 143 | if not in_or_equal(music.type, type): 144 | continue 145 | if not in_or_equal(music.bpm, bpm): 146 | continue 147 | if title_search is not Ellipsis and title_search.lower() not in music.title.lower(): 148 | continue 149 | music.diff = diff2 150 | new_list.append(music) 151 | return new_list 152 | 153 | 154 | obj = requests.get('https://www.diving-fish.com/api/maimaidxprober/music_data').json() 155 | total_list: MusicList = MusicList(obj) 156 | for __i in range(len(total_list)): 157 | total_list[__i] = Music(total_list[__i]) 158 | for __j in range(len(total_list[__i].charts)): 159 | total_list[__i].charts[__j] = Chart(total_list[__i].charts[__j]) 160 | -------------------------------------------------------------------------------- /src/libraries/pokemon_img.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | import os 3 | import json 4 | import re 5 | from PIL import Image, ImageFont, ImageDraw 6 | 7 | # Const values 8 | 9 | STATIC_FILE_DIR = "src/static/poke/" 10 | FILE_CACHE_DIR = "src/static/poke/imgcache/" 11 | COVER_DIR = "src/static/poke/covers/" 12 | FONT_PATH = STATIC_FILE_DIR + "LXGWWenKai-Regular.ttf" 13 | 14 | title_style = ImageFont.truetype(FONT_PATH, 80, encoding="utf-8") 15 | chapter_style = ImageFont.truetype(FONT_PATH, 56, encoding="utf-8") 16 | subtitle_style = ImageFont.truetype(FONT_PATH, 40, encoding="utf-8") 17 | type_style = ImageFont.truetype(FONT_PATH, 48, encoding="utf-8") 18 | stat_style = ImageFont.truetype(FONT_PATH, 40, encoding="utf-8") 19 | text_style = ImageFont.truetype(FONT_PATH, 32, encoding="utf-8") 20 | comment_style = ImageFont.truetype(FONT_PATH, 28, encoding="utf-8") 21 | small_style = ImageFont.truetype(FONT_PATH, 24, encoding="utf-8") 22 | desc_style = ImageFont.truetype(FONT_PATH, 20, encoding="utf-8") 23 | 24 | ability_cn = {} 25 | pokemon_dict = {} 26 | pokemon_cn = {} 27 | pokemon_cn_to_eng = {} 28 | move_cn = {} 29 | pm_move_dict = {} 30 | move_list = {} 31 | pic_dict = {} 32 | item_list = [] 33 | type_dmg_tbl = [ 34 | [1, 1, 1, 1, 1, 0.5, 1, 0, 0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1], 35 | [2, 1, 0.5, 0.5, 1, 2, 0.5, 0, 2, 1, 1, 1, 1, 0.5, 2, 1, 2, 0.5], 36 | [1, 2, 1, 1, 1, 0.5, 2, 1, 0.5, 1, 1, 2, 0.5, 1, 1, 1, 1, 1], 37 | [1, 1, 1, 0.5, 0.5, 0.5, 1, 0.5, 0, 1, 1, 2, 1, 1, 1, 1, 1, 2], 38 | [1, 1, 0, 2, 1, 2, 0.5, 1, 2, 2, 1, 0.5, 2, 1, 1, 1, 1, 1], 39 | [1, 0.5, 2, 1, 0.5, 1, 2, 1, 0.5, 2, 1, 1, 1, 1, 2, 1, 1, 1], 40 | [1, 0.5, 0.5, 0.5, 1, 1, 1, 0.5, 0.5, 0.5, 1, 2, 1, 2, 1, 1, 2, 0.5], 41 | [0, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 0.5, 1], 42 | [1, 1, 1, 1, 1, 2, 1, 1, 0.5, 0.5, 0.5, 1, 0.5, 1, 2, 1, 1, 2], 43 | [1, 1, 1, 1, 1, 0.5, 2, 1, 2, 0.5, 0.5, 2, 1, 1, 2, 0.5, 1, 1], 44 | [1, 1, 1, 1, 2, 2, 1, 1, 1, 2, 0.5, 0.5, 1, 1, 1, 0.5, 1, 1], 45 | [1, 1, 0.5, 0.5, 2, 2, 0.5, 1, 0.5, 0.5, 2, 0.5, 1, 1, 1, 0.5, 1, 1], 46 | [1, 1, 2, 1, 0, 1, 1, 1, 1, 1, 2, 0.5, 0.5, 1, 1, 0.5, 1, 1], 47 | [1, 2, 1, 2, 1, 1, 1, 1, 0.5, 1, 1, 1, 1, 0.5, 1, 1, 0, 1], 48 | [1, 1, 2, 1, 2, 1, 1, 1, 0.5, 0.5, 0.5, 2, 1, 1, 0.5, 2, 1, 1], 49 | [1, 1, 1, 1, 1, 1, 1, 1, 0.5, 1, 1, 1, 1, 1, 1, 2, 1, 0], 50 | [1, 0.5, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 0.5, 0.5], 51 | [1, 2, 1, 0.5, 1, 1, 1, 1, 0.5, 0.5, 1, 1, 1, 1, 1, 2, 2, 1]] 52 | type_tbl = { 53 | "Normal": ["一般", "#BBBBAA", "#E7E7D8", "普"], 54 | "Fighting": ["格斗", "#BB5544", "#DD9988", "斗"], 55 | "Flying": ["飞行", "#6699FF", "#99BBFF", "飞"], 56 | "Poison": ["毒", "#AA5599", "#C689BA", "毒"], 57 | "Ground": ["地面", "#DDBB55", "#F1DDA0", "地"], 58 | "Rock": ["岩石", "#BBAA66", "#E1D08C", "岩"], 59 | "Bug": ["虫", "#AABB22", "#DAEC44", "虫"], 60 | "Ghost": ["幽灵", "#6666BB", "#9F9FEC", "鬼"], 61 | "Steel": ["钢", "#AAAABB", "#DFDFE1", "钢"], 62 | "Fire": ["火", "#FF4422", "#FF927D", "火"], 63 | "Water": ["水", "#3399FF", "#77BBFF", "水"], 64 | "Grass": ["草", "#77CC55", "#BDFFA3", "草"], 65 | "Electric": ["电", "#FFCC33", "#FAE078", "电"], 66 | "Psychic": ["超能力", "#FF5599", "#FF9CC4", "超"], 67 | "Ice": ["冰", "#77DDFF", "#DBF6FF", "冰"], 68 | "Dragon": ["龙", "#7766EE", "#A194FF", "龙"], 69 | "Dark": ["恶", "#775544", "#BDA396", "恶"], 70 | "Fairy": ["妖精", "#FFAAFF", "#FBCBFB", "妖"], 71 | } 72 | 73 | 74 | def type_cn_to_eng(type): 75 | for k, v in type_tbl.items(): 76 | if v[0] == type: 77 | return k 78 | return "" 79 | 80 | 81 | def get_type_scale(target1, target2, attacker): 82 | result = 1 83 | at = list(type_tbl.keys()).index(attacker) 84 | ta1 = list(type_tbl.keys()).index(target1) 85 | result *= type_dmg_tbl[at][ta1] 86 | if target2 is not None: 87 | result *= type_dmg_tbl[at][list(type_tbl.keys()).index(target2)] 88 | return result 89 | 90 | 91 | def get_type(name): 92 | for type_eng, type_v in type_tbl.items(): 93 | if type_eng.lower() == name.lower() or type_v[0].lower() == name.lower() or type_v[1].lower() == name.lower(): 94 | return type_eng, type_v 95 | return "Normal", type_tbl["Normal"] 96 | 97 | 98 | def get_type_scale_s(target1, target2, attacker): 99 | color_tbl = { 100 | "4": "#D32F2F", 101 | "2": "#F44336", 102 | "1": "#333333", 103 | "1/2": "#4CAF50", 104 | "1/4": "#A5D6A7", 105 | "0": "#888888" 106 | } 107 | r = get_type_scale(target1, target2, attacker) 108 | if r == 1.0: 109 | r = "1" 110 | elif r == 0.5: 111 | r = "1/2" 112 | elif r == 0.25: 113 | r = "1/4" 114 | elif r == 0.0: 115 | r = "0" 116 | else: 117 | r = str(r) 118 | return r + "×", color_tbl[r] 119 | 120 | 121 | def tc(type): 122 | return type_tbl[type][1] 123 | 124 | 125 | def tc2(type): 126 | return type_tbl[type][2] 127 | 128 | 129 | def tn(type): 130 | return type_tbl[type][0] 131 | 132 | 133 | with open(STATIC_FILE_DIR + "MoveList.txt", encoding='utf-8') as f: 134 | for line in f.read().split('\n'): 135 | arr = line.split('\t') 136 | move_cn[arr[3]] = arr 137 | try: 138 | move_list[int(arr[0])] = arr 139 | except ValueError: 140 | pass 141 | 142 | with open(STATIC_FILE_DIR + "AbilityList.txt", encoding='utf-8') as f: 143 | for line in f.read().split('\n'): 144 | arr = line.split('\t') 145 | ability_cn[arr[3]] = arr 146 | 147 | with open(STATIC_FILE_DIR + "PokeList.txt", encoding='utf-8') as f: 148 | for line in f.read().split('\n'): 149 | arr = line.split('\t') 150 | pokemon_cn[arr[3]] = arr 151 | pokemon_cn_to_eng[arr[1]] = arr[3] 152 | 153 | with open(STATIC_FILE_DIR + "ItemList.txt", encoding='utf-8') as f: 154 | for line in f.read().split('\n'): 155 | arr = line.split('\t') 156 | item_list.append(arr) 157 | 158 | with open(STATIC_FILE_DIR + "move_list.json", encoding='utf-8') as f: 159 | pm_move_dict = json.load(f) 160 | 161 | with open(STATIC_FILE_DIR + "pokemon_dict.json", encoding='utf-8') as f: 162 | pokemon_dict = json.load(f) 163 | 164 | with open(STATIC_FILE_DIR + "pic_dict.json", encoding='utf-8') as f: 165 | pic_dict = json.load(f) 166 | 167 | 168 | def get_move(id): 169 | move = move_list[id] 170 | return { 171 | "id": id, 172 | "name": move[1].strip(), 173 | "type": type_cn_to_eng(move[4].strip()), 174 | "genre": move[5].strip(), 175 | "power": move[6].strip(), 176 | "acc": move[7].strip(), 177 | "pp": move[8].strip(), 178 | "desc": move[9].strip(), 179 | "usual": len(move) > 10 180 | } 181 | 182 | 183 | def get_item(name): 184 | for item in item_list: 185 | for v in item: 186 | if name == v: 187 | return item 188 | return None 189 | 190 | 191 | def get_pokemon(pokename, index=0): 192 | if pokename in pokemon_cn_to_eng: 193 | name = pokemon_cn_to_eng[pokename] 194 | else: 195 | name = pokename 196 | if name not in pokemon_dict: 197 | return None, None 198 | poke = pokemon_dict[name][index] 199 | res = { 200 | "name": pokemon_cn[name][1], 201 | "jpname": pokemon_cn[name][2], 202 | "engname": pokemon_cn[name][3], 203 | "ability_list": [], 204 | "moves": { 205 | "by_leveling_up": [], 206 | "all": [], 207 | }, 208 | "type": [], 209 | "stats": [] 210 | } 211 | for ab in poke["ability"]: 212 | ab_cn = ability_cn[ab] 213 | res["ability_list"].append({ 214 | "name": ab_cn[1], 215 | "hidden": False, 216 | "description": ab_cn[4] 217 | }) 218 | if poke["hiddenability"] != "": 219 | ab = poke["hiddenability"] 220 | ab_cn = ability_cn[ab] 221 | res["ability_list"].append({ 222 | "name": ab_cn[1], 223 | "hidden": True, 224 | "description": ab_cn[4] 225 | }) 226 | res["type"] = poke["type"] 227 | res["stats"] = poke["stats"] 228 | moves = pm_move_dict[name] 229 | for move in moves["byLevelingUp"]: 230 | res["moves"]["by_leveling_up"].append({ 231 | "level": move[0], 232 | "move": get_move(move[1]) 233 | }) 234 | for move in moves["all"]: 235 | if get_move(move) not in res["moves"]["all"]: 236 | res["moves"]["all"].append(get_move(move)) 237 | res["moves"]["all"].sort(key=lambda elem: elem["id"]) 238 | return res, pokemon_dict[name] 239 | 240 | 241 | def get_length_of_val(val): 242 | return min(val * 2, 450) 243 | 244 | def calc_stat(ev, level, iv, bp, modifier, is_hp=False): 245 | if is_hp: 246 | return int((int(ev * 2 + iv + bp / 4) * level) / 100 + 10 + level) 247 | else: 248 | return int(((int(ev * 2 + iv + bp / 4) * level) / 100 + 5) * modifier) 249 | 250 | def get_min_max(pokedata, level): 251 | minv = [0, 0, 0, 0, 0, 0] 252 | maxv = [0, 0, 0, 0, 0, 0] 253 | if pokedata["name"] == "脱壳忍者": 254 | minv[0] = 1 255 | maxv[0] = 1 256 | else: 257 | minv[0] = calc_stat(pokedata["stats"][0], level, 0, 0, 1, True) 258 | maxv[0] = calc_stat(pokedata["stats"][0], level, 31, 252, 1, True) 259 | for i in range(1, 6): 260 | minv[i] = calc_stat(pokedata["stats"][i], level, 0, 0, 0.9) 261 | maxv[i] = calc_stat(pokedata["stats"][i], level, 31, 252, 1.1) 262 | return minv, maxv 263 | 264 | def get_image(pokename, index=0, force_generate=False): 265 | data, others = get_pokemon(pokename, index) 266 | if data is None: 267 | return None, None 268 | 269 | filepath = FILE_CACHE_DIR + data['engname'] + (f"_{index}" if index > 0 else "") + ".png" 270 | if os.path.exists(filepath) and not force_generate: 271 | return filepath, others 272 | 273 | cover = Image.open(f"{COVER_DIR}{data['engname']}.png") 274 | cover = cover.resize((500, 500), Image.Resampling.BILINEAR) 275 | 276 | im = Image.new("RGBA", (1200, 4000)) 277 | draw = ImageDraw.Draw(im) 278 | draw.rectangle((0, 0, 1200, 4000), tc(data["type"][0])) 279 | draw.rounded_rectangle((16, 16, 1200 - 16, 4000 - 16), 20, tc2(data["type"][0])) 280 | draw.text((40, 24), data["name"], fill="#333333", font=title_style) 281 | draw.text((52 + len(data["name"] * 80), 64), data["jpname"] + " " + data["engname"], fill="#333333", font=subtitle_style) 282 | draw.text((40, 128), tn(data["type"][0]), fill=tc(data["type"][0]), stroke_fill="#888888", stroke_width=2, font=type_style) 283 | if len(data["type"]) == 2: 284 | draw.text((56 + len(tn(data["type"][0])) * 48, 128), tn(data["type"][1]), fill=tc(data["type"][1]), stroke_fill="#888888", stroke_width=2, font=type_style) 285 | lst = ['HP', '攻击', '防御', '特攻', '特防', '速度', '总计'] 286 | colors = ["#97C87A", "#FAE192", "#FBB977", "#A2D4DA", "#89A9CD", "#C39CD8", "#dddddd"] 287 | data["stats"].append(sum(data["stats"])) 288 | for i in range(len(data["stats"]) - 1): 289 | val = data["stats"][i] 290 | draw.text((40, 200 + i * 56), lst[i], stroke_fill="#888888", stroke_width=2, fill=colors[i], font=stat_style) 291 | draw.rectangle((135, 211 + i * 56, 145 + get_length_of_val(val), 233 + i * 56), "#888888") 292 | draw.rectangle((136, 212 + i * 56, 144 + get_length_of_val(val), 232 + i * 56), colors[i]) 293 | draw.text((160 + get_length_of_val(val), 200 + i * 56), str(data["stats"][i]), stroke_fill="#888888", stroke_width=2, fill=colors[i], font=stat_style) 294 | 295 | i = 6 296 | val = data["stats"][i] 297 | draw.text((40, 200 + i * 56), lst[i], stroke_fill="#888888", stroke_width=2, fill=colors[i], font=stat_style) 298 | draw.rectangle((135, 211 + i * 56, 145 + int(val * 0.33), 233 + i * 56), "#888888") 299 | draw.rectangle((136, 212 + i * 56, 144 + int(val * 0.33), 232 + i * 56), colors[i]) 300 | draw.text((160 + int(val * 0.33), 200 + i * 56), str(data["stats"][i]), stroke_fill="#888888", stroke_width=2, fill=colors[i], font=stat_style) 301 | 302 | minv, maxv = get_min_max(data, 50) 303 | minv100, maxv100 = get_min_max(data, 100) 304 | 305 | draw.text((40, 600), "能力值范围", fill="#333333", font=chapter_style) 306 | 307 | draw.rectangle((30, 670, 170 * 6 + 142 + 2, 672 + 104), "#333333") 308 | 309 | draw.rectangle((32, 672, 142, 672 + 50), fill=colors[6]) 310 | draw.text((40, 680), "50级", fill="#333333", font=text_style) 311 | for i in range(6): 312 | length = draw.textlength(f"{minv[i]} - {maxv[i]}", font=text_style) 313 | draw.rectangle((170 * i + 142, 672, 170 * (i + 1) + 142, 672 + 50), fill=colors[i]) 314 | draw.text((170 * i + 142 + 85 - int(length / 2), 680), f"{minv[i]} - {maxv[i]}", fill="#333333", font=text_style) 315 | 316 | draw.rectangle((32, 672 + 52, 142, 672 + 102), fill=colors[6]) 317 | draw.text((40, 680 + 52), "100级", fill="#333333", font=text_style) 318 | for i in range(6): 319 | length = draw.textlength(f"{minv100[i]} - {maxv100[i]}", font=text_style) 320 | draw.rectangle((170 * i + 142, 672 + 52, 170 * (i + 1) + 142, 672 + 102), fill=colors[i]) 321 | draw.text((170 * i + 142 + 85 - int(length / 2), 680 + 52), f"{minv100[i]} - {maxv100[i]}", fill="#333333", font=text_style) 322 | 323 | current_y = 790 324 | 325 | draw.text((40, current_y), "属性抗性", fill="#333333", font=chapter_style) 326 | 327 | current_y += 76 328 | 329 | current_x = 60 330 | 331 | for k in type_tbl: 332 | draw.rectangle((current_x, current_y, current_x + 60, current_y + 60), fill=type_tbl[k][1]) 333 | draw.text((current_x + 30, current_y + 12), tn(k)[:2], anchor="ma", fill="#333333", font=comment_style) 334 | draw.rectangle((current_x, current_y + 60, current_x + 60, current_y + 120), fill=type_tbl[k][2]) 335 | s, f = get_type_scale_s(data["type"][0], None if len(data["type"]) == 1 else data["type"][1], k) 336 | draw.text((current_x + 30, current_y + 76), s, anchor="ma", fill=f, font=small_style, stroke_fill="#888888", stroke_width=1) 337 | current_x += 60 338 | 339 | current_y += 132 340 | 341 | draw.text((40, current_y), "特性", fill="#333333", font=chapter_style) 342 | current_y += 76 343 | for i in range(len(data["ability_list"])): 344 | ab = data["ability_list"][i] 345 | draw.text((40, current_y), ab["name"], fill=("#3F51B5" if ab["hidden"] else "#333333"), font=text_style, stroke_fill="#888888", stroke_width=1) 346 | desc = ab["description"] 347 | current = "" 348 | while len(desc) != 0: 349 | current += desc[0] 350 | desc = desc[1:] 351 | if draw.textlength(current, font=text_style) > 920 and len(desc) > 0: 352 | draw.text((240, current_y), current, fill=("#3F51B5" if ab["hidden"] else "#333333"), font=text_style, stroke_fill="#888888", stroke_width=1) 353 | current_y += 50 354 | current = "" 355 | draw.text((240, current_y), current, fill=("#3F51B5" if ab["hidden"] else "#333333"), font=text_style, stroke_fill="#888888", stroke_width=1) 356 | current_y += 50 357 | 358 | draw.text((40, current_y), "招式列表", fill="#333333", font=chapter_style) 359 | current_y += 16 + 56 360 | draw.text((40, current_y), f"仅显示整体使用率较高的招式,如果需要完整招式请输入“完整招式 {data['name']}”", fill="#555555", font=comment_style) 361 | current_y += 16 + 32 362 | 363 | def draw_move(move, current_y, dark): 364 | desc = move["desc"] 365 | if len(desc) > 33: 366 | desc = desc[:32] + "..." 367 | draw.rectangle((48, current_y - 2, 1160, current_y + 30), fill=("#ffffffff" if dark else "#eeeeeeee")) 368 | draw.text((50 + 80, current_y), move["name"], fill="#333333", font=desc_style, anchor="ma") 369 | draw.text((200 + 30, current_y), tn(move["type"]), fill=tc(move["type"]), font=desc_style, anchor="ma") 370 | draw.text((270, current_y), move["genre"], fill="#333333", font=desc_style) 371 | draw.text((320 + 30, current_y), move["power"], fill="#333333", font=desc_style, anchor="ma") 372 | draw.text((380 + 30, current_y), move["acc"], fill="#333333", font=desc_style, anchor="ma") 373 | draw.text((440 + 30, current_y), move["pp"], fill="#333333", font=desc_style, anchor="ma") 374 | draw.text((500, current_y), desc, fill="#333333", font=desc_style) 375 | return current_y + 30 376 | 377 | dark = False 378 | draw.rectangle((48, current_y - 2, 1160, current_y + 30), fill=("#ffffffff" if dark else "#eeeeeeee")) 379 | draw.text((50 + 80, current_y), "名称", fill="#333333", font=desc_style, anchor="ma") 380 | draw.text((200 + 30, current_y), "属性", fill="#333333", font=desc_style, anchor="ma") 381 | draw.text((270, current_y), "分类", fill="#333333", font=desc_style) 382 | draw.text((320 + 30, current_y), "威力", fill="#333333", font=desc_style, anchor="ma") 383 | draw.text((380 + 30, current_y), "命中", fill="#333333", font=desc_style, anchor="ma") 384 | draw.text((440 + 30, current_y), "PP", fill="#333333", font=desc_style, anchor="ma") 385 | draw.text(((500 + 1160) / 2, current_y), "描述", fill="#333333", font=desc_style, anchor="ma") 386 | 387 | current_y += 30 388 | 389 | dark = True 390 | for move in data["moves"]["all"]: 391 | if move["usual"]: 392 | current_y = draw_move(move, current_y, dark) 393 | dark = not dark 394 | 395 | im.paste(cover, (640, 120), cover) 396 | 397 | im2 = Image.new("RGBA", (1200, current_y + 50)) 398 | draw = ImageDraw.Draw(im2) 399 | draw.rectangle((0, 0, 1200, current_y + 50), tc(data["type"][0])) 400 | draw.rounded_rectangle((16, 16, 1200 - 16, current_y + 50 - 16), 20, tc2(data["type"][0])) 401 | 402 | im2.paste(im.crop((0, 0, 1200, current_y)), (0, 0)) 403 | 404 | return im2, others 405 | 406 | 407 | def generate_cache(): 408 | j = 0 409 | for name, poke_datas in pokemon_dict.items(): 410 | j += 1 411 | for i in range(len(poke_datas)): 412 | filepath = FILE_CACHE_DIR + name + (f"_{i}" if i > 0 else "") + ".png" 413 | try: 414 | get_image(name, i, True)[0].save(filepath) 415 | print(f"{j}-{i}") 416 | except Exception as e: 417 | print(f"err: {j}-{i}, {str(e)}") 418 | 419 | 420 | def get_token(text, index): 421 | s = "" 422 | for i in range(index, len(text)): 423 | if text[i] == " ": 424 | return s, i+1 425 | s += text[i] 426 | return s, i+1 427 | 428 | 429 | def parse_pokemon(text, index): 430 | result = { 431 | "pokemon": None, 432 | "item": [], 433 | "ability": [], 434 | "bps": [0, 0, 0, 0, 0, 0], 435 | "ivs": [31, 31, 31, 31, 31, 31], 436 | "high": 0, # 性格修正+ 437 | "low": 0, # 性格修正- 438 | "tera": None, 439 | "mods": 0, # 能力变化 440 | "percent": 100 # 能力变化百分比 441 | } 442 | while True: 443 | token, index = get_token(text, index) 444 | if token == "": 445 | return result, index 446 | item = get_item(token) 447 | # parse item 448 | if item is not None: 449 | result["item"] = item 450 | continue 451 | # parse ability 452 | pass 453 | # parse mods 454 | reg = "([+|-][0-6])" 455 | grp = re.match(reg, token) 456 | if grp: 457 | result["mods"] = int(grp.group(0)) 458 | continue 459 | # parse mods percent 460 | reg = "([0-9]+)%" 461 | grp = re.match(reg, token) 462 | if grp: 463 | result["percent"] = float(grp.group(1)) 464 | continue 465 | 466 | flag = False 467 | # parse ivs 468 | tbl = ['ivhp', 'ivatk', 'ivdef', 'ivspa', 'ivspd', 'ivspe', 469 | 'ivh', 'iva', 'ivb', 'ivc', 'ivd', 'ivs', 470 | 'ivhp', '攻', 'ivdef', 'ivspa', 'ivspd', '速'] 471 | for i, t in enumerate(tbl): 472 | if t in token.lower(): 473 | t_re = token.lower().replace(t, "") 474 | result["ivs"][i % 6] = max(0, min(31, int(t_re))) 475 | flag = True 476 | break 477 | if flag: 478 | continue 479 | # parse bps 480 | tbl = ['hp', 'atk', 'def', 'spa', 'spd', 'spe', 481 | 'h', 'a', 'b', 'c', 'd', 's', 482 | '生命', '攻击', '防御', '特攻', '特防', '速度'] 483 | for i, t in enumerate(tbl): 484 | if t in token.lower(): 485 | t_re = token.lower().replace(t, "") 486 | if "+" in t_re: 487 | t_re = t_re.replace("+", "") 488 | if i % 6 != 0: 489 | result["high"] = i % 6 490 | elif "-" in t_re: 491 | t_re = t_re.replace("-", "") 492 | if i % 6 != 0: 493 | result["low"] = i % 6 494 | result["bps"][i % 6] = max(0, min(252, int(t_re))) 495 | flag = True 496 | break 497 | if flag: 498 | continue 499 | # parse tera 500 | if "太晶" in token: 501 | tera_type, _ = get_type(token.replace("太晶", "")) 502 | if tera_type != None: 503 | result["tera"] = tera_type 504 | continue 505 | # parse pokemon 506 | for poke in pokemon_cn.values(): 507 | for value in poke: 508 | s = f"{value}([0-9]?)" 509 | regex = re.match(s, token) 510 | if regex: 511 | pmi = regex.group(1) 512 | if pmi == "": 513 | pmi = 0 514 | else: 515 | pmi = int(pmi) - 1 516 | pokemon, _ = get_pokemon(poke[1], pmi) 517 | result["pokemon"] = { 518 | "name": pokemon["name"], 519 | "stats": pokemon["stats"], 520 | "type": pokemon["type"] 521 | } 522 | return result, index 523 | 524 | 525 | def parse_move(text, index): 526 | result = { 527 | "name": "", 528 | "bp": 0, 529 | "category": 0, # 0 for physical, 1 for special, 2 for ... 530 | "type": "Normal" 531 | } 532 | token, index = get_token(text, index) 533 | # parse existed move 534 | for move in move_list.values(): 535 | if token.lower() == move[1].lower() or token.lower() == move[2].lower() or token.lower() == move[3].lower(): 536 | result["name"] = move[1] 537 | try: 538 | result["bp"] = int(move[6]) 539 | except Exception: 540 | result["bp"] = 0 541 | result["type"], _ = get_type(move[4]) 542 | result["category"] = ["物理", "特殊", "变化"].index(move[5]) 543 | return result, index 544 | # parse unexisted move 545 | result["name"] = "自定义" 546 | # parse type 547 | for type_eng, type_v in type_tbl.items(): 548 | if type_eng in token: 549 | token = token.replace(type_eng, "") 550 | result["type"] = type_eng 551 | break 552 | if type_v[0] in token: 553 | token = token.replace(type_v[0], "") 554 | result["type"] = type_eng 555 | break 556 | if type_v[3] in token: 557 | token = token.replace(type_v[3], "") 558 | result["type"] = type_eng 559 | break 560 | if "物理" in token: 561 | token = token.replace("物理", "") 562 | result["category"] = 0 563 | elif "特殊" in token: 564 | token = token.replace("特殊", "") 565 | result["category"] = 1 566 | elif "变化" in token: 567 | token = token.replace("变化", "") 568 | result["category"] = 2 569 | try: 570 | result["bp"] = int(token) 571 | except Exception: 572 | result["bp"] = 0 573 | result["name"] += f'({type_tbl[result["type"]][0]} {["物理", "特殊", "变化"][result["category"]]} {result["bp"]} BP)' 574 | return result, index 575 | 576 | 577 | def parse_other(text, index): 578 | res = [[], [], [], 1] 579 | while index != len(text): 580 | value, index = get_token(text, index) 581 | if 'hit' in value and value[-3:] == 'hit': 582 | res[3] = int(value[:-3]) 583 | continue 584 | elif 'bp' in value and value[-2:] == 'bp': 585 | res[0].append(float(value[:-2])) 586 | continue 587 | elif value[-1] == 'p': 588 | res[1].append(float(value[:-1])) 589 | continue 590 | else: 591 | res[2].append(float(value)) 592 | continue 593 | return res 594 | 595 | 596 | def calculate_damage(text): 597 | 598 | def poke_round(val): 599 | if val % 1 > 0.5: 600 | return int(val + 1) 601 | return int(val) 602 | 603 | result = { 604 | "percent": [], 605 | "int": [], 606 | "range": [], 607 | "hp": 0 608 | } 609 | text = text.strip() 610 | attacker, index = parse_pokemon(text, 0) 611 | move, index = parse_move(text, index) 612 | target, index = parse_pokemon(text, index) 613 | mods = parse_other(text, index) 614 | print(mods) 615 | 616 | cat_addition = move["category"] * 2 617 | if cat_addition == 4: 618 | return result 619 | modifier = 1.0 620 | if attacker["high"] == 1 + cat_addition: 621 | modifier = 1.1 622 | elif attacker["low"] == 1 + cat_addition: 623 | modifier = 0.9 624 | attack = calc_stat(attacker["pokemon"]["stats"][1 + cat_addition], 50, attacker["ivs"][1 + cat_addition], attacker["bps"][1 + cat_addition], modifier) 625 | attack = poke_round(attack * attacker["percent"] / 100) 626 | stat_mod = attacker["mods"] 627 | if stat_mod >= 0: 628 | attack = int(attack * (2 + stat_mod) / 2) 629 | else: 630 | attack = int(attack * 2 / (2 - stat_mod)) 631 | 632 | modifier = 1.0 633 | if target["high"] == 2 + cat_addition: 634 | modifier = 1.1 635 | elif target["low"] == 2 + cat_addition: 636 | modifier = 0.9 637 | defense = calc_stat(target["pokemon"]["stats"][2 + cat_addition], 50, target["ivs"][2 + cat_addition], target["bps"][2 + cat_addition], modifier) 638 | defense = poke_round(defense * target["percent"] / 100) 639 | stat_mod = target["mods"] 640 | if stat_mod >= 0: 641 | defense = int(defense * (2 + stat_mod) / 2) 642 | else: 643 | defense = int(defense * 2 / (2 - stat_mod)) 644 | 645 | target_hp = calc_stat(target["pokemon"]["stats"][0], 50, target["ivs"][0], target["bps"][0], 1, True) 646 | result["hp"] = target_hp 647 | 648 | base_power = move["bp"] 649 | for mod in mods[0]: 650 | base_power *= int(mod * 4096) / 4096 651 | base_power = int(base_power) 652 | 653 | damage = int(int(((int((2 * 50) / 5 + 2) * base_power) * attack) / defense) / 50 + 2) 654 | attack_type = json.loads(json.dumps(attacker["pokemon"]["type"])) 655 | attack_type.append(attacker["tera"]) 656 | 657 | if target["tera"] is not None: 658 | defend_type1 = target["tera"] 659 | defend_type2 = None 660 | else: 661 | defend_type1 = target["pokemon"]["type"][0] 662 | defend_type2 = None if len(target["pokemon"]["type"]) == 1 else target["pokemon"]["type"][1] 663 | 664 | for mod in mods[1]: 665 | damage *= int(mod * 4096) / 4096 666 | 667 | damage_tbl = [] 668 | for i in range(16): 669 | new_dmg = int(damage * (85 + i) / 100) 670 | effectiveness = get_type_scale(defend_type1, defend_type2, move["type"]) 671 | stab = 4096 672 | for a in attack_type: 673 | if a == move["type"]: 674 | stab += 2048 675 | new_dmg = new_dmg * stab / 4096 676 | new_dmg = int(poke_round(new_dmg) * effectiveness) 677 | for mod in mods[2]: 678 | new_dmg *= int(mod * 4096) / 4096 679 | new_dmg = poke_round(max(1, new_dmg)) 680 | damage_tbl.append(new_dmg) 681 | 682 | result["int"] = damage_tbl 683 | if mods[3] != 1: 684 | result["range"] = [damage_tbl[0] * mods[3], damage_tbl[15] * mods[3]] 685 | for dmg in result["range"]: 686 | result["percent"].append(int(dmg * 1000 / target_hp) / 10) 687 | else: 688 | for dmg in result["int"]: 689 | result["percent"].append(int(dmg * 1000 / target_hp) / 10) 690 | 691 | return result 692 | -------------------------------------------------------------------------------- /src/libraries/tool.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def hash(qq: int): 5 | days = int(time.strftime("%d", time.localtime(time.time()))) + 31 * int( 6 | time.strftime("%m", time.localtime(time.time()))) + 77 7 | return (days * qq) >> 8 8 | -------------------------------------------------------------------------------- /src/plugins/apex_legends.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, on_regex, get_driver, get_bot 2 | from nonebot.log import logger 3 | from nonebot.params import CommandArg, EventMessage 4 | from nonebot.adapters import Event 5 | from nonebot.adapters.onebot.v11 import Message, MessageSegment 6 | 7 | from src.data_access.plugin_manager import plugin_manager 8 | from src.data_access.redis import redis_global 9 | from src.libraries.tool import hash 10 | from src.libraries.image import * 11 | from src.libraries.apexlegends_api import ApexLegendsAPI, set_apex_token 12 | from collections import defaultdict 13 | import asyncio 14 | import re 15 | import time 16 | import json 17 | 18 | set_apex_token(get_driver().config.apexlegendsstatus_token) 19 | 20 | __plugin_meta = { 21 | "name": "APEX", 22 | "enable": False, 23 | "help_text": "" 24 | } 25 | 26 | plugin_manager.register_plugin(__plugin_meta) 27 | 28 | async def __group_checker(event: Event): 29 | if hasattr(event, 'message_type') and event.message_type == 'channel': 30 | return False 31 | elif not hasattr(event, 'group_id'): 32 | return False 33 | else: 34 | return plugin_manager.get_enable(event.group_id, __plugin_meta["name"]) 35 | 36 | def apex_list(): 37 | lst = redis_global.get("apex_list") 38 | if not lst: 39 | lst = [] 40 | else: 41 | lst = json.loads(lst) 42 | return lst 43 | 44 | def get_rank_text(rank): 45 | div_text = rank['rankDiv'] 46 | if div_text == 0: 47 | div_text = "" 48 | else: 49 | div_text = " " + str(div_text) 50 | return f"{rank['rankName']}{div_text}" 51 | 52 | 53 | apex_add = on_command('apex添加', rule=__group_checker) 54 | 55 | @apex_add.handle() 56 | async def _(event: Event, message: Message = CommandArg()): 57 | uid = str(message) 58 | bridge_str, status = await ApexLegendsAPI.player_statistics_uid(uid) 59 | if status != 200: 60 | await apex_add.send(f"未直接找到{uid},正在尝试搜索,可能需要花费数秒到一分钟,请耐心等待……") 61 | res = await ApexLegendsAPI.search_player(uid) 62 | if (len(res) > 0): 63 | s = '' 64 | i = 0 65 | for uid2, data in res.items(): 66 | i += 1 67 | s += f"{data['name']}({uid2}), {data['level']}级,上次使用{data['selected']},{data['rp']}RP\n" 68 | if i == 20: 69 | break 70 | await apex_add.send(f"搜索到了以下玩家:\n" + s + "如果需要添加,请重新输入 “apex添加 ”,如还未找到,请尝试直接搜索橘子ID") 71 | else: 72 | await apex_add.send(f"未搜索到{uid}") 73 | return 74 | bridge = json.loads(bridge_str) 75 | lst = apex_list() 76 | gid = getattr(event, "group_id", -1) 77 | for u, g in lst: 78 | if int(uid) == u and gid == g: 79 | await apex_add.send(f"已经添加过了") 80 | return 81 | lst.append([int(uid), gid]) 82 | redis_global.set("apex_list", json.dumps(lst)) 83 | redis_global.set(f"apexcache_{uid}", bridge_str) 84 | await apex_add.send(f"添加成功:{uid}({bridge['global']['name']})") 85 | return 86 | 87 | 88 | apex_del = on_command('apex删除', rule=__group_checker) 89 | 90 | @apex_del.handle() 91 | async def _(event: Event, message: Message = CommandArg()): 92 | uid = str(message) 93 | try: 94 | lst = apex_list() 95 | gid = getattr(event, "group_id", -1) 96 | del_count = 0 97 | for i in range(len(lst) - 1, -1, -1): 98 | u, g = lst[i] 99 | if (gid == -1 or gid == g) and int(uid) == u: 100 | del lst[i] 101 | del_count += 1 102 | if del_count == 0: 103 | await apex_del.send(f"未找到{uid}") 104 | else: 105 | await apex_del.send(f"删除成功") 106 | redis_global.set("apex_list", json.dumps(lst)) 107 | except ValueError: 108 | await apex_del.send(f"未找到{uid}") 109 | return 110 | 111 | apex_query = on_command('apex查询', rule=__group_checker) 112 | 113 | @apex_query.handle() 114 | async def _(event: Event, message: Message = CommandArg()): 115 | uid_or_name = str(message) 116 | q_uid = 0 117 | lst = apex_list() 118 | for uid, _ in lst: 119 | if str(uid) == uid_or_name: 120 | q_uid = uid 121 | break 122 | cache = json.loads(redis_global.get(f"apexcache_{uid}")) 123 | if cache["global"]["name"] == uid_or_name: 124 | q_uid = cache["global"]["uid"] 125 | if q_uid == 0: 126 | await apex_query.send("请先添加监视之后再查询") 127 | return 128 | bridge_str, status = await ApexLegendsAPI.player_statistics_uid(q_uid) 129 | if status != 200: 130 | await apex_query.send(f"查询暂时错误: {q_uid}") 131 | return 132 | redis_global.set(f"apexcache_{q_uid}", bridge_str) 133 | bridge = json.loads(bridge_str) 134 | await apex_query.send(f"""{bridge['global']['name']} - {bridge['global']['uid']} 135 | Status: {bridge['realtime']['currentStateAsText']} 136 | {get_rank_text(bridge['global']['rank'])} - {bridge['global']['rank']['rankScore']}RP""") 137 | 138 | apex_show_list = on_command('apex列表', rule=__group_checker) 139 | 140 | @apex_show_list.handle() 141 | async def _(event: Event, message: Message = CommandArg()): 142 | lst = apex_list() 143 | s = "以下是本群添加的apex玩家列表:" 144 | for uid, gid in lst: 145 | if getattr(event, "group_id", 100) == gid: 146 | cache = json.loads(redis_global.get(f"apexcache_{uid}")) 147 | print(f"{cache['global']['name']}({uid})") 148 | s += f"\n{cache['global']['name']}({uid})" 149 | await apex_show_list.send(s) 150 | 151 | async def apex_auto_update(): 152 | while True: 153 | try: 154 | t = time.time_ns() 155 | group_message_dict = defaultdict(lambda: []) 156 | uid_to_gid_dict = defaultdict(lambda: []) 157 | for uid, group_id in apex_list(): 158 | uid_to_gid_dict[uid].append(group_id) 159 | for uid, gid_list in uid_to_gid_dict.items(): 160 | bridge_str, status = await ApexLegendsAPI.player_statistics_uid(uid) 161 | if status == 200: 162 | cache_bridge = json.loads(redis_global.get(f"apexcache_{uid}")) 163 | cache_rank = cache_bridge["global"]["rank"] 164 | cache_realtime_data = cache_bridge["realtime"] 165 | bridge = json.loads(bridge_str) 166 | realtime_data = bridge["realtime"] 167 | rank = bridge["global"]["rank"] 168 | redis_global.set(f"apexcache_{uid}", bridge_str) 169 | if len(gid_list) > 0: 170 | # 检查 RP 变动 171 | if cache_rank["rankScore"] != rank["rankScore"]: 172 | cache_rank_text = get_rank_text(cache_rank) 173 | rank_text = get_rank_text(rank) 174 | rp_text = rank["rankScore"] - cache_rank["rankScore"] 175 | if -200 < rp_text < 1000: 176 | if rp_text > 0: 177 | rp_text = f"+{rp_text}" 178 | sub_s = f"{bridge['global']['name']} {rp_text}RP ({rank['rankScore']}RP)" 179 | if cache_rank_text != rank_text: 180 | sub_s += f", rank changed to {rank_text}" 181 | for gid in gid_list: 182 | group_message_dict[gid].append(sub_s) 183 | # 检查是否开了一局新游戏 184 | if realtime_data["currentState"] == "inMatch": 185 | if cache_realtime_data["currentState"] != "inMatch" or \ 186 | (cache_realtime_data["currentState"] == "inMatch" and cache_realtime_data["currentStateSecsAgo"] > realtime_data["currentStateSecsAgo"]): 187 | for gid in gid_list: 188 | group_message_dict[gid].append(f"{bridge['global']['name']} started a game as {realtime_data['selectedLegend']}") 189 | await asyncio.sleep(1) 190 | for gid, messages in group_message_dict.items(): 191 | if plugin_manager.get_enable(gid, __plugin_meta["name"]): 192 | await get_bot().send_msg(message_type="group", group_id=gid, message='\n'.join(messages)) 193 | t = int((time.time_ns() - t) / 1e9) 194 | logger.success("APEX 数据自动更新完成") 195 | await asyncio.sleep(120 - t) 196 | except Exception: 197 | pass 198 | 199 | get_driver().on_startup(lambda: asyncio.get_running_loop().create_task(apex_auto_update())) 200 | -------------------------------------------------------------------------------- /src/plugins/auto_naga.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, on_regex, get_driver, get_bot 2 | from nonebot.log import logger 3 | from nonebot.params import CommandArg, EventMessage 4 | from nonebot.adapters import Event 5 | from nonebot.adapters.onebot.v11 import Message, MessageSegment 6 | 7 | from src.data_access.plugin_manager import plugin_manager 8 | from src.data_access.redis import * 9 | from src.libraries.tool import hash 10 | from src.libraries.image import * 11 | from src.libraries.auto_naga_api import auto_naga, set_naga_secret 12 | from collections import defaultdict 13 | import asyncio 14 | import re 15 | import time 16 | import json 17 | 18 | set_naga_secret(get_driver().config.auto_naga_secret) 19 | 20 | __plugin_meta = { 21 | "name": "NAGA", 22 | "enable": False, 23 | "help_text": "ms2th <雀魂牌谱链接>\nnaga解析 <雀魂牌谱编号> <小局编号> [解析种类]\nnaga解析 <天凤牌谱链接> [解析种类]\nnp查询\nthurl <自定义牌谱编号> <小局编号>" + 24 | "\n解析种类:填写用逗号隔开的数字,例如0,1,3,若不填写默认为2,4\n0 - ω(副露派)\n1 - γ(守備重視)\n2 - ニシキ(バランス)\n3 - ヒバカリ(超門前派)\n4 - カガシ(超副露派)" 25 | } 26 | 27 | plugin_manager.register_plugin(__plugin_meta) 28 | 29 | async def __group_checker(event: Event): 30 | if hasattr(event, 'message_type') and event.message_type == 'channel': 31 | return False 32 | elif not hasattr(event, 'group_id'): 33 | return True 34 | else: 35 | return plugin_manager.get_enable(event.group_id, __plugin_meta["name"]) 36 | 37 | 38 | tbl = ['东 1 局','东 2 局','东 3 局','东 4 局','南 1 局','南 2 局','南 3 局','南 4 局','西 1 局','西 2 局','西 3 局','西 4 局'] 39 | 40 | 41 | ms2th = on_command('ms2th', aliases={'雀魂牌譜:', '雀魂牌谱:'}) 42 | 43 | @ms2th.handle() 44 | async def _(event: Event, message: Message = CommandArg()): 45 | await ms2th.send(f"正在转换雀魂牌谱,可能需要一定时间,请耐心等待……") 46 | # data = await auto_naga.convert_majsoul(str(message).split('_')[0]) # 有消息称会封号,排除掉玩家视角的 URL 信息 47 | print(message) 48 | data = await auto_naga.convert_majsoul(str(message)) 49 | if data['status'] != 200: 50 | await ms2th.send(data['message']) 51 | return 52 | lst = [] 53 | for i, log in enumerate(data['message']): 54 | lst.append(f"{i} - {tbl[log['log'][0][0][0]]} {log['log'][0][0][1]} 本场") 55 | txt = f"牌谱编号{data['index']}\n{log['title'][0]} {log['title'][1]}\n{' '.join(log['name'])}\n" + '\n'.join(lst) 56 | await ms2th.send(Message([ 57 | MessageSegment("image", { 58 | "file": f"base64://{str(image_to_base64(text_to_image(txt)), encoding='utf-8')}" 59 | }) 60 | ])) 61 | await ms2th.send(f"请输入 naga解析 {data['index']} 0 以解析某小局或\nnaga解析 {data['index']} 0-{i} 来解析所有对局") 62 | 63 | 64 | naga_account = on_command('np查询') 65 | 66 | @naga_account.handle() 67 | async def _(event: Event, message: Message = CommandArg()): 68 | await naga_account.send(f'{event.user_id} 剩余 NP: {await auto_naga.get_np(event.user_id)}') 69 | 70 | 71 | naga_add = on_command('np充值') 72 | 73 | @naga_add.handle() 74 | async def _(event: Event, message: Message = CommandArg()): 75 | if str(event.user_id) not in get_driver().config.superusers: 76 | await naga.send('您没有权限使用此命令') 77 | return 78 | args = str(message).strip().split(' ') 79 | await auto_naga.add_np(int(args[0]), int(args[1])) 80 | await naga_add.send(f'{int(args[0])} 充值成功,剩余 NP: {await auto_naga.get_np(int(args[0]))}') 81 | 82 | 83 | def check_type_valid(s): 84 | s = s.strip().split(',') 85 | for i in s: 86 | if not i.isdigit() or int(i) < 0 or int(i) > 4: 87 | return False, 0 88 | if len(s) <= 2: 89 | return True, 1 90 | elif len(s) == 3: 91 | return True, 1.1 92 | elif len(s) == 4: 93 | return True, 1.2 94 | return True, 1.3 95 | 96 | 97 | naga = on_command('naga解析') 98 | 99 | @naga.handle() 100 | async def _(event: Event, message: Message = CommandArg()): 101 | lst = str(message).strip().replace('&', '&').split(' ') 102 | # read arguments 103 | try: 104 | if len(lst) == 0: 105 | raise Exception() 106 | elif 'tenhou.net' in lst[0]: 107 | if len(lst) == 2: 108 | player_types = lst[-1] 109 | valid, cost_adjust = check_type_valid(player_types) 110 | if not valid: 111 | raise Exception() 112 | else: 113 | player_types = '2,4' 114 | cost_adjust = 1 115 | custom = False 116 | np_enough, remaining, required = await auto_naga.cost_np(event.user_id, int(50 * cost_adjust)) 117 | if not np_enough: 118 | await naga.send(f'您的 NP 不足,剩余 {remaining} NP,需要 {required} NP') 119 | return 120 | data = await auto_naga.order(False, lst[0], player_types) 121 | print(data) 122 | if data['status'] == 400: 123 | if data['msg'] == 'すでに解析済みの牌譜です': 124 | await naga.send('检查到已解析过此天凤牌谱,正在查找缓存中……') 125 | else: 126 | await naga.send('天凤牌谱解析失败,请检查牌谱链接是否正确') 127 | return 128 | else: 129 | await naga.send(f'解析申请已提交,请稍等数十秒到一分钟,已扣除 {required} NP,剩余 {remaining} NP') 130 | elif lst[0].isdigit(): 131 | if len(lst) == 3: 132 | player_types = lst[-1] 133 | valid, cost_adjust = check_type_valid(player_types) 134 | if not valid: 135 | raise Exception() 136 | else: 137 | player_types = '2,4' 138 | cost_adjust = 1 139 | custom = True 140 | index_data = DictRedisData('majsoul_convert_index_map') 141 | haihu_no = lst[0] 142 | print(index_data.data) 143 | if haihu_no not in index_data.data: 144 | await naga.send('未找到该编号的雀魂牌谱') 145 | return 146 | lst2 = [] 147 | for i in lst[1].split(','): 148 | if '-' in i: 149 | lst2.extend(range(int(i.split('-')[0]), int(i.split('-')[1]) + 1)) 150 | else: 151 | lst2.append(int(i)) 152 | haihu_data = DictRedisData(index_data.data[haihu_no]).data['message'] 153 | haihus = [] 154 | lst2.sort() 155 | lst2 = list(set(lst2)) 156 | for i in lst2: 157 | if i >= len(haihu_data): 158 | await naga.send(f'小局编号{i}不存在') 159 | return 160 | haihus.append(haihu_data[i]) 161 | np_enough, remaining, required = await auto_naga.cost_np(event.user_id, int(len(haihus) * 10 * cost_adjust)) 162 | if not np_enough: 163 | await naga.send(f'您的 NP 不足,剩余 {remaining} NP,需要 {required} NP') 164 | return 165 | data = await auto_naga.order(True, haihus, player_types) 166 | if data['status'] in [400, 405]: 167 | with open('naga_error.txt', 'a') as fw: 168 | fw.write(json.dumps(data, ensure_ascii=False) + '\n') 169 | await naga.send('自定义牌谱解析失败,请检查牌谱链接是否正确') 170 | return 171 | else: 172 | ts = data["current"] 173 | await naga.send(f'解析申请已提交,请稍等数十秒到一分钟,已扣除 {required} NP,剩余 {remaining} NP') 174 | else: 175 | raise Exception() 176 | except Exception as e: 177 | await naga.send('Usage:\nnaga解析 <雀魂牌谱编号> <小局编号> [解析种类]\nnaga解析 <天凤牌谱链接> [解析种类]') 178 | raise e 179 | return 180 | 181 | timeout = 0 182 | while timeout < 60: 183 | timeout += 1 184 | await asyncio.sleep(1) 185 | code, link, msg = await auto_naga.find_paipu(custom, lst[0] if not custom else ts) 186 | if code == 0: 187 | await naga.send(f'解析完成,牌谱链接:{link}') 188 | return 189 | elif code == 2: 190 | await naga.send(msg) 191 | return 192 | 193 | await naga.send('解析超时,请稍后重试') 194 | return 195 | 196 | 197 | custom_th_url = on_command('thurl') 198 | 199 | @custom_th_url.handle() 200 | async def _(event: Event, message: Message = CommandArg()): 201 | lst = str(message).strip().split(' ') 202 | try: 203 | data = await auto_naga.get_tenhou_custom_url(int(lst[0]), int(lst[1])) 204 | await custom_th_url.send(("http://tenhou.net/6/json=" + json.dumps(data, ensure_ascii=False)).replace(' ', '')) 205 | except Exception as e: 206 | print(e) 207 | await custom_th_url.send('Usage: thurl <自定义牌谱编号> <小局编号>') 208 | -------------------------------------------------------------------------------- /src/plugins/covid_positive.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, on_regex 2 | from nonebot.params import RawCommand 3 | from nonebot.adapters import Event 4 | from nonebot.adapters.onebot.v11 import Message, MessageSegment, Bot 5 | 6 | from src.data_access.plugin_manager import plugin_manager, get_string_hash 7 | from src.data_access.redis import redis_global 8 | from src.libraries.tool import hash 9 | from src.libraries.maimaidx_music import * 10 | from src.libraries.image import * 11 | from src.libraries.maimai_best_40 import generate 12 | from src.libraries.maimai_best_50 import generate50 13 | import re 14 | 15 | __plugin_meta = { 16 | "name": "阳性报备", 17 | "enable": False, 18 | "help_text": "" 19 | } 20 | 21 | plugin_manager.register_plugin(__plugin_meta) 22 | 23 | async def __group_checker(event: Event): 24 | if hasattr(event, 'message_type') and event.message_type == 'channel': 25 | return False 26 | elif not hasattr(event, 'group_id'): 27 | return False 28 | else: 29 | return plugin_manager.get_enable(event.group_id, __plugin_meta["name"]) 30 | 31 | 32 | impositive = on_command('我阳了', rule=__group_checker) 33 | 34 | @impositive.handle() 35 | async def _(event: Event): 36 | v = redis_global.get(get_string_hash("covid" + str(event.group_id))) 37 | if not v: 38 | v = "[]" 39 | data = json.loads(v) 40 | if event.user_id not in data: 41 | data.append(event.user_id) 42 | redis_global.set(get_string_hash("covid" + str(event.group_id)), json.dumps(data)) 43 | await impositive.finish("你阳了") 44 | 45 | 46 | imnotpositive = on_command('我没阳', rule=__group_checker) 47 | 48 | @imnotpositive.handle() 49 | async def _(event: Event): 50 | v = redis_global.get(get_string_hash("covid" + str(event.group_id))) 51 | if not v: 52 | v = "[]" 53 | data = json.loads(v) 54 | if event.user_id in data: 55 | del data[data.index(event.user_id)] 56 | redis_global.set(get_string_hash("covid" + str(event.group_id)), json.dumps(data)) 57 | await impositive.finish("你没阳") 58 | 59 | 60 | hepositive = on_command('他阳了', rule=__group_checker) 61 | 62 | @hepositive.handle() 63 | async def _(event: Event): 64 | v = redis_global.get(get_string_hash("covid" + str(event.group_id))) 65 | if not v: 66 | v = "[]" 67 | data = json.loads(v) 68 | for message in event.original_message: 69 | print(message) 70 | if message.type == 'at': 71 | user_id = message.data['qq'] 72 | if int(user_id) not in data: 73 | data.append(int(user_id)) 74 | redis_global.set(get_string_hash("covid" + str(event.group_id)), json.dumps(data)) 75 | await hepositive.finish("他阳了") 76 | 77 | 78 | whopositive = on_command('谁阳了', rule=__group_checker) 79 | 80 | @whopositive.handle() 81 | async def _(bot: Bot, event: Event): 82 | s = "现在阳的人有这些:" 83 | v = redis_global.get(get_string_hash("covid" + str(event.group_id))) 84 | if not v: 85 | v = "[]" 86 | data = json.loads(v) 87 | for uid in data: 88 | user = await bot.get_group_member_info(group_id=event.group_id, user_id=uid) 89 | if user: 90 | s += f"\n{user['card'] if user['card'] != '' else user['nickname']}({uid})" 91 | await whopositive.finish(s) 92 | -------------------------------------------------------------------------------- /src/plugins/fishgame.py: -------------------------------------------------------------------------------- 1 | from nonebot import get_bot, on_command, on_regex 2 | from nonebot.params import CommandArg, EventMessage 3 | from nonebot.adapters import Event 4 | from nonebot.adapters.onebot.v11 import Message, MessageSegment 5 | 6 | from src.libraries.image import image_to_base64 7 | from src.data_access.plugin_manager import plugin_manager 8 | from src.libraries.fishgame import * 9 | from src.libraries.fishgame_util import * 10 | import re 11 | import time 12 | 13 | 14 | __plugin_meta = { 15 | "name": "捕鱼", 16 | "enable": False, 17 | "help_text": """以成为捕鱼达人为目标吧! 18 | 群里会随机出现鱼,在出现的时候【捕鱼】即可! 19 | 鱼3分钟就会离开,请尽快捕获! 20 | 可用命令列表: 21 | 捕鱼 在鱼出现时进行捕鱼 22 | 面板 查看自己的角色面板 23 | 背包 [页数] 查看自己的背包列表 24 | 使用 <道具编号> 使用道具 25 | 单抽/十连 使用每抽10金币的价格进行抽奖 26 | 状态 查看池子状态 27 | 商店 查看商店 28 | 商店购买 <商品编号> 购买商品""", 29 | } 30 | 31 | plugin_manager.register_plugin(__plugin_meta) 32 | 33 | async def __group_checker(event: Event): 34 | if hasattr(event, 'message_type') and event.message_type == 'channel': 35 | return True 36 | elif not hasattr(event, 'group_id'): 37 | return True 38 | else: 39 | return plugin_manager.get_enable(event.group_id, __plugin_meta["name"]) 40 | 41 | 42 | from nonebot_plugin_apscheduler import scheduler 43 | 44 | fish_games = {} 45 | 46 | @scheduler.scheduled_job("cron", minute="*/1", jitter=30) 47 | async def try_spawn_fish(): 48 | group_list = await get_bot().get_group_list() 49 | print(group_list) 50 | # 如果不在 8 到 24 点则不尝试生成 51 | if 1 <= time.localtime().tm_hour < 8: 52 | return 53 | for obj in group_list: 54 | group = obj['group_id'] 55 | if not plugin_manager.get_enable(group, __plugin_meta["name"]): 56 | continue 57 | if group not in fish_games: 58 | fish_games[group] = FishGame(group) 59 | qq_list = await get_bot().get_group_member_list(group_id=group) 60 | qq_list = [str(qq['user_id']) for qq in qq_list] 61 | game: FishGame = fish_games[group] 62 | game.update_average_power(qq_list) 63 | print(f'{group} 尝试刷鱼') 64 | if game.current_fish is not None: 65 | leave = game.count_down() 66 | if leave: 67 | await get_bot().send_msg(message_type="group", group_id=group, message=f"鱼离开了...") 68 | else: 69 | fish = game.spawn_fish() 70 | if fish is not None: 71 | if fish['rarity'] == 'UR': 72 | await get_bot().send_msg(message_type="group", group_id=group, message=f"{fish['name']}【{fish['rarity']}】 █████!\n使用【████】指███████获████!") 73 | else: 74 | await get_bot().send_msg(message_type="group", group_id=group, message=f"{fish['name']}【{fish['rarity']}】 出现了!\n使用【捕鱼】指令进行捕获吧!") 75 | 76 | 77 | panel = on_command('面板', rule=__group_checker) 78 | 79 | @panel.handle() 80 | async def _(event: Event): 81 | group = event.group_id 82 | if group not in fish_games: 83 | fish_games[group] = FishGame(group) 84 | # game: FishGame = fish_games[group] 85 | player = FishPlayer(str(event.user_id)) 86 | player.refresh_buff() 87 | info = await get_bot().get_group_member_info(group_id=group, user_id=event.user_id) 88 | player.data['name'] = info['card'] if (info['card'] != '' and info['card'] is not None) else info['nickname'] 89 | avatar = await get_qq_avatar(event.user_id) 90 | character_panel = create_character_panel(player, avatar) 91 | await panel.send(Message([ 92 | MessageSegment.reply(event.message_id), 93 | MessageSegment("image", { 94 | "file": f"base64://{str(image_to_base64(character_panel), encoding='utf-8')}" 95 | }) 96 | ])) 97 | 98 | bag = on_command('背包', rule=__group_checker) 99 | 100 | @bag.handle() 101 | async def _(event: Event, message: Message = CommandArg()): 102 | player = FishPlayer(str(event.user_id)) 103 | player.sort_bag() 104 | page = 1 105 | if message: 106 | page = int(str(message)) 107 | bag_panel = create_inventory_panel(player.bag[(page - 1) * 10:page * 10], page, (len(player.bag) - 1) // 10 + 1) 108 | await bag.send(Message([ 109 | MessageSegment.reply(event.message_id), 110 | MessageSegment("image", { 111 | "file": f"base64://{str(image_to_base64(bag_panel), encoding='utf-8')}" 112 | }) 113 | ])) 114 | 115 | catch = on_command('捕鱼', aliases={'捕捉', '捕获'}, rule=__group_checker) 116 | 117 | @catch.handle() 118 | async def _(event: Event, message: Message = EventMessage()): 119 | if str(message) != '捕鱼': 120 | return 121 | group = event.group_id 122 | if group not in fish_games: 123 | fish_games[group] = FishGame(group) 124 | game: FishGame = fish_games[group] 125 | player = FishPlayer(str(event.user_id)) 126 | res = game.catch_fish(player) 127 | await catch.send(Message([ 128 | MessageSegment.reply(event.message_id), 129 | MessageSegment.text(res['message']) 130 | ])) 131 | 132 | draw = on_command('单抽', aliases={'十连'}, rule=__group_checker) 133 | 134 | @draw.handle() 135 | async def _(event: Event, message: Message = EventMessage()): 136 | msg = str(message).strip() 137 | if msg == '单抽': 138 | ten_time = False 139 | elif msg == '十连': 140 | ten_time = True 141 | else: 142 | return 143 | group = event.group_id 144 | if group not in fish_games: 145 | fish_games[group] = FishGame(group) 146 | game: FishGame = fish_games[group] 147 | player = FishPlayer(str(event.user_id)) 148 | res = game.gacha(player, ten_time) 149 | if res['code'] == 0: 150 | gacha_panel = create_gacha_panel(res['message']) 151 | await draw.send(Message([ 152 | MessageSegment.reply(event.message_id), 153 | MessageSegment("image", { 154 | "file": f"base64://{str(image_to_base64(gacha_panel), encoding='utf-8')}" 155 | }) 156 | ])) 157 | else: 158 | await draw.send(Message([ 159 | MessageSegment.reply(event.message_id), 160 | MessageSegment.text(res['message']) 161 | ])) 162 | 163 | shop = on_command('商店', rule=__group_checker) 164 | 165 | @shop.handle() 166 | async def _(event: Event, message: Message = EventMessage()): 167 | msg = str(message).strip() 168 | if msg != '商店': 169 | return 170 | group = event.group_id 171 | if group not in fish_games: 172 | fish_games[group] = FishGame(group) 173 | game: FishGame = fish_games[group] 174 | shop_panel = create_shop_panel(game.get_shop()) 175 | await draw.send(Message([ 176 | MessageSegment.reply(event.message_id), 177 | MessageSegment("image", { 178 | "file": f"base64://{str(image_to_base64(shop_panel), encoding='utf-8')}" 179 | }) 180 | ])) 181 | 182 | buy = on_command('商店购买', rule=__group_checker) 183 | 184 | @buy.handle() 185 | async def _(event: Event, message: Message = CommandArg()): 186 | try: 187 | id = int(str(message).strip()) 188 | group = event.group_id 189 | if group not in fish_games: 190 | fish_games[group] = FishGame(group) 191 | game: FishGame = fish_games[group] 192 | player = FishPlayer(str(event.user_id)) 193 | res = game.shop_buy(player, id) 194 | await catch.send(Message([ 195 | MessageSegment.reply(event.message_id), 196 | MessageSegment.text(res['message']) 197 | ])) 198 | except Exception as e: 199 | await catch.send(Message([ 200 | MessageSegment.reply(event.message_id), 201 | MessageSegment.text('未找到该商品') 202 | ])) 203 | raise e 204 | 205 | use = on_command('使用', rule=__group_checker) 206 | 207 | @use.handle() 208 | async def _(event: Event, message: Message = CommandArg()): 209 | try: 210 | id = int(str(message).strip()) 211 | group = event.group_id 212 | if group not in fish_games: 213 | fish_games[group] = FishGame(group) 214 | game: FishGame = fish_games[group] 215 | player = FishPlayer(str(event.user_id)) 216 | res = game.use_item(player, id-1) 217 | await catch.send(Message([ 218 | MessageSegment.reply(event.message_id), 219 | MessageSegment.text(res['message']) 220 | ])) 221 | except Exception as e: 222 | await catch.send(Message([ 223 | MessageSegment.reply(event.message_id), 224 | MessageSegment.text('未找到该道具') 225 | ])) 226 | raise e 227 | 228 | status = on_command('状态', rule=__group_checker) 229 | 230 | @status.handle() 231 | async def _(event: Event): 232 | group = event.group_id 233 | if group not in fish_games: 234 | fish_games[group] = FishGame(group) 235 | game: FishGame = fish_games[group] 236 | player = FishPlayer(str(event.user_id)) 237 | player.refresh_buff() 238 | s = "" 239 | for buff in player.buff: 240 | if buff.get('power', 0) > 0: 241 | s += f"渔力+{buff['power']}," 242 | elif buff.get('fishing_bonus', 0) > 0: 243 | s += f"渔获+{buff['fishing_bonus']*100}%," 244 | if buff.get('time', None) is not None: 245 | s += f"剩余{buff['time']}次\n" 246 | elif buff.get('expire', None) is not None: 247 | s += f"剩余{int(buff['expire'] - time.time())}秒\n" 248 | res = game.get_status()['message'] + '\n当前刷新鱼概率:\n' 249 | simulate = game.simulate_spawn_fish() 250 | await status.send(Message([ 251 | MessageSegment.reply(event.message_id), 252 | MessageSegment.text(s + res + simulate) 253 | ])) -------------------------------------------------------------------------------- /src/plugins/huozi.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, on_regex 2 | from nonebot.params import RawCommand, CommandArg 3 | from nonebot.adapters import Event 4 | from nonebot.adapters.onebot.v11 import Message, MessageSegment, Bot 5 | 6 | from src.data_access.plugin_manager import plugin_manager, get_string_hash 7 | from src.data_access.redis import redis_global 8 | from src.libraries.tool import hash 9 | from src.libraries.maimaidx_music import * 10 | from src.libraries.image import * 11 | from src.libraries.maimai_best_40 import generate 12 | from src.libraries.maimai_best_50 import generate50 13 | import re 14 | import aiohttp 15 | 16 | __plugin_meta = { 17 | "name": "活字印刷", 18 | "enable": True, 19 | "help_text": "" 20 | } 21 | 22 | plugin_manager.register_plugin(__plugin_meta) 23 | 24 | async def __group_checker(event: Event): 25 | if hasattr(event, 'message_type') and event.message_type == 'channel': 26 | return False 27 | elif not hasattr(event, 'group_id'): 28 | return True 29 | else: 30 | return plugin_manager.get_enable(event.group_id, __plugin_meta["name"]) 31 | 32 | 33 | huozi = on_command('活字印刷', rule=__group_checker) 34 | 35 | 36 | @huozi.handle() 37 | async def _(event: Event, message: Message = CommandArg()): 38 | msg = str(message) 39 | if len(msg) > 40: 40 | await huozi.send("最多活字印刷 40 个字符噢") 41 | return 42 | payload = { 43 | "rawData": msg, 44 | "inYsddMode": True, 45 | "norm": True, 46 | "reverse": False, 47 | "speedMult": 1.0, 48 | "pitchMult": 1.0 49 | } 50 | async with aiohttp.request("POST", "https://www.diving-fish.com/api/huozi/make", json=payload) as resp: 51 | if resp.status == 404: 52 | await huozi.send("生成失败!") 53 | else: 54 | obj = await resp.json() 55 | await huozi.send(Message([ 56 | MessageSegment("record", { 57 | "file": f"https://www.diving-fish.com/api/huozi/get/{obj['id']}.mp3" 58 | }) 59 | ])) -------------------------------------------------------------------------------- /src/plugins/mahjong_character.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, on_regex 2 | from nonebot.params import RawCommand, CommandArg 3 | from nonebot.adapters import Event 4 | from nonebot.adapters.onebot.v11 import Message, MessageSegment, Bot 5 | 6 | from src.data_access.plugin_manager import plugin_manager, get_string_hash 7 | from src.data_access.redis import redis_global 8 | from src.libraries.tool import hash 9 | from src.libraries.maimaidx_music import * 10 | from src.libraries.image import * 11 | from src.libraries.maimai_best_40 import generate 12 | from src.libraries.maimai_best_50 import generate50 13 | import re 14 | 15 | __plugin_meta = { 16 | "name": "麻将字符", 17 | "enable": True, 18 | "help_text": "" 19 | } 20 | 21 | plugin_manager.register_plugin(__plugin_meta) 22 | 23 | async def __group_checker(event: Event): 24 | if hasattr(event, 'message_type') and event.message_type == 'channel': 25 | return False 26 | elif not hasattr(event, 'group_id'): 27 | return True 28 | else: 29 | return plugin_manager.get_enable(event.group_id, __plugin_meta["name"]) 30 | 31 | 32 | mc_tbl = { 33 | 'z': '🀀🀁🀂🀃🀆🀅🀄', 34 | 'm': '🀇🀈🀉🀊🀋🀌🀍🀎🀏', 35 | 's': '🀐🀑🀒🀓🀔🀕🀖🀗🀘', 36 | 'p': '🀙🀚🀛🀜🀝🀞🀟🀠🀡' 37 | } 38 | 39 | mahjong_char = on_command('麻将字符', rule=__group_checker) 40 | 41 | 42 | @mahjong_char.handle() 43 | async def _(event: Event, message: Message = CommandArg()): 44 | argv = str(message).split(' ') 45 | res = [''] * len(argv) 46 | s = [] 47 | try: 48 | for i, arg in enumerate(argv): 49 | for c in arg: 50 | if c in '0123456789': 51 | s.append(int(c)) 52 | elif c in 'mspz': 53 | res[i] += ''.join([mc_tbl[c][v-1] for v in s]) 54 | s = [] 55 | except Exception: 56 | await mahjong_char.send('请检查输入') 57 | await mahjong_char.send(' '.join(res)) -------------------------------------------------------------------------------- /src/plugins/maimaidx.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, on_regex 2 | from nonebot.params import CommandArg, EventMessage 3 | from nonebot.adapters import Event 4 | from nonebot.adapters.onebot.v11 import Message, MessageSegment 5 | 6 | from src.data_access.plugin_manager import plugin_manager 7 | from src.libraries.tool import hash 8 | from src.libraries.maimaidx_music import * 9 | from src.libraries.image import * 10 | from src.libraries.maimai_best_40 import generate 11 | from src.libraries.maimai_best_50 import generate50 12 | import re 13 | 14 | 15 | __plugin_meta = { 16 | "name": "舞萌", 17 | "enable": True, 18 | "help_text": """可用命令如下: 19 | 今日舞萌 查看今天的舞萌运势 20 | XXXmaimaiXXX什么 随机一首歌 21 | 随个[dx/标准][绿黄红紫白]<难度> 随机一首指定条件的乐曲 22 | 查歌<乐曲标题的一部分> 查询符合条件的乐曲 23 | [绿黄红紫白]id<歌曲编号> 查询乐曲信息或谱面信息 24 | <歌曲别名>是什么歌 查询乐曲别名对应的乐曲 25 | 定数查歌 <定数> 查询定数对应的乐曲 26 | 定数查歌 <定数下限> <定数上限> 27 | 分数线 <难度+歌曲id> <分数线> 详情请输入“分数线 帮助”查看""", 28 | } 29 | 30 | plugin_manager.register_plugin(__plugin_meta) 31 | 32 | async def __group_checker(event: Event): 33 | if hasattr(event, 'message_type') and event.message_type == 'channel': 34 | return True 35 | elif not hasattr(event, 'group_id'): 36 | return True 37 | else: 38 | return plugin_manager.get_enable(event.group_id, __plugin_meta["name"]) 39 | 40 | 41 | def song_txt(music: Music): 42 | return Message([ 43 | MessageSegment("text", { 44 | "text": f"{music.id}. {music.title}\n" 45 | }), 46 | MessageSegment("image", { 47 | "file": f"https://www.diving-fish.com/covers/{get_cover_len5_id(music.id)}.png" 48 | }), 49 | MessageSegment("text", { 50 | "text": f"\n{'/'.join(music.level)}" 51 | }) 52 | ]) 53 | 54 | 55 | def inner_level_q(ds1, ds2=None): 56 | result_set = [] 57 | diff_label = ['Bas', 'Adv', 'Exp', 'Mst', 'ReM'] 58 | if ds2 is not None: 59 | music_data = total_list.filter(ds=(ds1, ds2)) 60 | else: 61 | music_data = total_list.filter(ds=ds1) 62 | for music in sorted(music_data, key = lambda i: int(i['id'])): 63 | for i in music.diff: 64 | result_set.append((music['id'], music['title'], music['ds'][i], diff_label[i], music['level'][i])) 65 | return result_set 66 | 67 | 68 | inner_level = on_command('inner_level ', aliases={'定数查歌 '}, rule=__group_checker) 69 | 70 | 71 | @inner_level.handle() 72 | async def _(event: Event, message: Message = CommandArg()): 73 | argv = str(message).strip().split(" ") 74 | if len(argv) > 2 or len(argv) == 0: 75 | await inner_level.finish("命令格式为\n定数查歌 <定数>\n定数查歌 <定数下限> <定数上限>") 76 | return 77 | if len(argv) == 1: 78 | result_set = inner_level_q(float(argv[0])) 79 | else: 80 | result_set = inner_level_q(float(argv[0]), float(argv[1])) 81 | if len(result_set) > 50: 82 | await inner_level.finish(f"结果过多({len(result_set)} 条),请缩小搜索范围。") 83 | return 84 | s = "" 85 | for elem in result_set: 86 | s += f"{elem[0]}. {elem[1]} {elem[3]} {elem[4]}({elem[2]})\n" 87 | await inner_level.finish(s.strip()) 88 | 89 | 90 | spec_rand = on_regex(r"^随个(?:dx|sd|标准)?[绿黄红紫白]?[0-9]+\+?", rule=__group_checker) 91 | 92 | 93 | @spec_rand.handle() 94 | async def _(event: Event, message: Message = EventMessage()): 95 | level_labels = ['绿', '黄', '红', '紫', '白'] 96 | regex = "随个((?:dx|sd|标准))?([绿黄红紫白]?)([0-9]+\+?)" 97 | res = re.match(regex, str(message).lower()) 98 | try: 99 | if res.groups()[0] == "dx": 100 | tp = ["DX"] 101 | elif res.groups()[0] == "sd" or res.groups()[0] == "标准": 102 | tp = ["SD"] 103 | else: 104 | tp = ["SD", "DX"] 105 | level = res.groups()[2] 106 | if res.groups()[1] == "": 107 | music_data = total_list.filter(level=level, type=tp) 108 | else: 109 | music_data = total_list.filter(level=level, diff=['绿黄红紫白'.index(res.groups()[1])], type=tp) 110 | if len(music_data) == 0: 111 | rand_result = "没有这样的乐曲哦。" 112 | else: 113 | rand_result = song_txt(music_data.random()) 114 | await spec_rand.send(rand_result) 115 | except Exception as e: 116 | print(e) 117 | await spec_rand.finish("随机命令错误,请检查语法") 118 | 119 | 120 | mr = on_regex(r".*maimai.*什么", rule=__group_checker) 121 | 122 | 123 | @mr.handle() 124 | async def _(): 125 | await mr.finish(song_txt(total_list.random())) 126 | 127 | 128 | search_music = on_regex(r"^查歌.+", rule=__group_checker) 129 | 130 | 131 | @search_music.handle() 132 | async def _(event: Event, message: Message = EventMessage()): 133 | regex = "查歌(.+)" 134 | name = re.match(regex, str(message)).groups()[0].strip() 135 | if name == "": 136 | return 137 | res = total_list.filter(title_search=name) 138 | if len(res) == 0: 139 | await search_music.send("没有找到这样的乐曲。") 140 | elif len(res) < 50: 141 | search_result = "" 142 | for music in sorted(res, key = lambda i: int(i['id'])): 143 | search_result += f"{music['id']}. {music['title']}\n" 144 | await search_music.finish(Message([ 145 | MessageSegment("text", { 146 | "text": search_result.strip() 147 | })])) 148 | else: 149 | await search_music.send(f"结果过多({len(res)} 条),请缩小查询范围。") 150 | 151 | 152 | query_chart = on_regex(r"^([绿黄红紫白]?)id([0-9]+)", rule=__group_checker) 153 | 154 | 155 | @query_chart.handle() 156 | async def _(event: Event, message: Message = EventMessage()): 157 | regex = "([绿黄红紫白]?)id([0-9]+)" 158 | groups = re.match(regex, str(message)).groups() 159 | level_labels = ['绿', '黄', '红', '紫', '白'] 160 | if groups[0] != "": 161 | try: 162 | level_index = level_labels.index(groups[0]) 163 | level_name = ['Basic', 'Advanced', 'Expert', 'Master', 'Re: MASTER'] 164 | name = groups[1] 165 | music = total_list.by_id(name) 166 | chart = music['charts'][level_index] 167 | ds = music['ds'][level_index] 168 | level = music['level'][level_index] 169 | file = f"https://www.diving-fish.com/covers/{get_cover_len5_id(music['id'])}.png" 170 | if len(chart['notes']) == 4: 171 | msg = f'''{level_name[level_index]} {level}({ds}) 172 | TAP: {chart['notes'][0]} 173 | HOLD: {chart['notes'][1]} 174 | SLIDE: {chart['notes'][2]} 175 | BREAK: {chart['notes'][3]} 176 | 谱师: {chart['charter']}''' 177 | else: 178 | msg = f'''{level_name[level_index]} {level}({ds}) 179 | TAP: {chart['notes'][0]} 180 | HOLD: {chart['notes'][1]} 181 | SLIDE: {chart['notes'][2]} 182 | TOUCH: {chart['notes'][3]} 183 | BREAK: {chart['notes'][4]} 184 | 谱师: {chart['charter']}''' 185 | await query_chart.send(Message([ 186 | MessageSegment("text", { 187 | "text": f"{music['id']}. {music['title']}\n" 188 | }), 189 | MessageSegment("image", { 190 | "file": f"{file}" 191 | }), 192 | MessageSegment("text", { 193 | "text": msg 194 | }) 195 | ])) 196 | except Exception: 197 | await query_chart.send("未找到该谱面") 198 | else: 199 | name = groups[1] 200 | music = total_list.by_id(name) 201 | try: 202 | file =f"https://www.diving-fish.com/covers/{get_cover_len5_id(music['id'])}.png" 203 | await query_chart.send(Message([ 204 | MessageSegment("text", { 205 | "text": f"{music['id']}. {music['title']}\n" 206 | }), 207 | MessageSegment("image", { 208 | "file": f"{file}" 209 | }), 210 | MessageSegment("text", { 211 | "text": f"艺术家: {music['basic_info']['artist']}\n分类: {music['basic_info']['genre']}\nBPM: {music['basic_info']['bpm']}\n版本: {music['basic_info']['from']}\n难度: {'/'.join(music['level'])}" 212 | }) 213 | ])) 214 | except Exception: 215 | await query_chart.send("未找到该乐曲") 216 | 217 | 218 | wm_list = ['拼机', '推分', '越级', '下埋', '夜勤', '练底力', '练手法', '打旧框', '干饭', '抓绝赞', '收歌'] 219 | 220 | 221 | jrwm = on_command('今日舞萌', aliases={'今日mai'}, rule=__group_checker) 222 | 223 | 224 | @jrwm.handle() 225 | async def _(event: Event, message: Message = CommandArg()): 226 | qq = int(event.sender_id) 227 | h = hash(qq) 228 | rp = h % 100 229 | wm_value = [] 230 | for i in range(11): 231 | wm_value.append(h & 3) 232 | h >>= 2 233 | s = f"今日人品值:{rp}\n" 234 | for i in range(11): 235 | if wm_value[i] == 3: 236 | s += f'宜 {wm_list[i]}\n' 237 | elif wm_value[i] == 0: 238 | s += f'忌 {wm_list[i]}\n' 239 | s += "千雪提醒您:打机时不要大力拍打或滑动哦\n今日推荐歌曲:" 240 | music = total_list[h % len(total_list)] 241 | await jrwm.finish(Message([MessageSegment("text", {"text": s})] + song_txt(music))) 242 | 243 | query_score = on_command('分数线', rule=__group_checker) 244 | 245 | 246 | @query_score.handle() 247 | async def _(event: Event, message: Message = CommandArg()): 248 | r = "([绿黄红紫白])(id)?([0-9]+)" 249 | argv = str(message).strip().split(" ") 250 | if len(argv) == 1 and argv[0] == '帮助': 251 | s = '''此功能为查找某首歌分数线设计。 252 | 命令格式:分数线 <难度+歌曲id> <分数线> 253 | 例如:分数线 紫799 100 254 | 命令将返回分数线允许的 TAP GREAT 容错以及 BREAK 50落等价的 TAP GREAT 数。 255 | 以下为 TAP GREAT 的对应表: 256 | GREAT/GOOD/MISS 257 | TAP\t1/2.5/5 258 | HOLD\t2/5/10 259 | SLIDE\t3/7.5/15 260 | TOUCH\t1/2.5/5 261 | BREAK\t5/12.5/25(外加200落)''' 262 | await query_score.send(Message([ 263 | MessageSegment("image", { 264 | "file": f"base64://{str(image_to_base64(text_to_image(s)), encoding='utf-8')}" 265 | }) 266 | ])) 267 | elif len(argv) == 2: 268 | try: 269 | grp = re.match(r, argv[0]).groups() 270 | level_labels = ['绿', '黄', '红', '紫', '白'] 271 | level_labels2 = ['Basic', 'Advanced', 'Expert', 'Master', 'Re:MASTER'] 272 | level_index = level_labels.index(grp[0]) 273 | chart_id = grp[2] 274 | line = float(argv[1]) 275 | music = total_list.by_id(chart_id) 276 | chart: Dict[Any] = music['charts'][level_index] 277 | tap = int(chart['notes'][0]) 278 | slide = int(chart['notes'][2]) 279 | hold = int(chart['notes'][1]) 280 | touch = int(chart['notes'][3]) if len(chart['notes']) == 5 else 0 281 | brk = int(chart['notes'][-1]) 282 | total_score = 500 * tap + slide * 1500 + hold * 1000 + touch * 500 + brk * 2500 283 | break_bonus = 0.01 / brk 284 | break_50_reduce = total_score * break_bonus / 4 285 | reduce = 101 - line 286 | if reduce <= 0 or reduce >= 101: 287 | raise ValueError 288 | await query_chart.send(f'''{music['title']} {level_labels2[level_index]} 289 | 分数线 {line}% 允许的最多 TAP GREAT 数量为 {(total_score * reduce / 10000):.2f}(每个-{10000 / total_score:.4f}%), 290 | BREAK 50落(一共{brk}个)等价于 {(break_50_reduce / 100):.3f} 个 TAP GREAT(-{break_50_reduce / total_score * 100:.4f}%)''') 291 | except Exception: 292 | await query_chart.send("格式错误,输入“分数线 帮助”以查看帮助信息") 293 | 294 | 295 | best_40_pic = on_command('b40', rule=__group_checker) 296 | 297 | 298 | @best_40_pic.handle() 299 | async def _(event: Event, message: Message = CommandArg()): 300 | username = str(message).strip() 301 | if username == "": 302 | payload = {'qq': str(event.sender_id)} 303 | else: 304 | payload = {'username': username} 305 | img, success = await generate(payload) 306 | if success == 400: 307 | if hasattr(event, 'message_type') and event.message_type == 'guild': 308 | await best_40_pic.send("在 qq 频道中无法获取您的 qq 号,请输入 【qq绑定 】 以绑定 qq 号") 309 | else: 310 | await best_40_pic.send("未找到此玩家,请确保此玩家的用户名和查分器中的用户名相同。") 311 | elif success == 403: 312 | await best_40_pic.send("该用户禁止了其他人获取数据。") 313 | else: 314 | await best_40_pic.send(Message([ 315 | MessageSegment("image", { 316 | "file": f"base64://{str(image_to_base64(img), encoding='utf-8')}" 317 | }) 318 | ])) 319 | 320 | best_50_pic = on_command('b50', rule=__group_checker) 321 | 322 | 323 | @best_50_pic.handle() 324 | async def _(event: Event, message: Message = CommandArg()): 325 | username = str(message).strip() 326 | if username == "": 327 | payload = {'qq': str(event.sender_id),'b50':True} 328 | else: 329 | payload = {'username': username,'b50': True} 330 | img, success = await generate50(payload) 331 | if success == 400: 332 | if hasattr(event, 'message_type') and event.message_type == 'guild': 333 | await best_50_pic.send("在 qq 频道中无法获取您的 qq 号,请输入 【qq绑定 】 以绑定 qq 号") 334 | else: 335 | await best_50_pic.send("未找到此玩家,请确保此玩家的用户名和查分器中的用户名相同。") 336 | elif success == 403: 337 | await best_50_pic.send("该用户禁止了其他人获取数据。") 338 | else: 339 | await best_50_pic.send(Message([ 340 | MessageSegment("image", { 341 | "file": f"base64://{str(image_to_base64(img), encoding='utf-8')}" 342 | }) 343 | ])) 344 | -------------------------------------------------------------------------------- /src/plugins/poke.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, on_notice, get_driver 2 | from nonebot.params import CommandArg, EventMessage 3 | from nonebot.typing import T_State 4 | from nonebot.adapters.onebot.v11 import Message, Event, Bot, MessageSegment 5 | from nonebot.exception import IgnoredException 6 | from nonebot.message import event_preprocessor 7 | from src.libraries.image import * 8 | from src.libraries.pokemon_img import get_image, calculate_damage 9 | from src.data_access.plugin_manager import plugin_manager 10 | import os 11 | 12 | __plugin_meta = { 13 | "name": "宝可梦", 14 | "enable": True, 15 | "help_text": "", 16 | } 17 | 18 | plugin_manager.register_plugin(__plugin_meta) 19 | 20 | async def __group_checker(event: Event): 21 | if hasattr(event, 'message_type') and event.message_type == 'channel': 22 | return False 23 | elif not hasattr(event, 'group_id'): 24 | return True 25 | else: 26 | return plugin_manager.get_enable(event.group_id, __plugin_meta["name"]) 27 | 28 | pokemon = on_command('poke', rule=__group_checker) 29 | 30 | @pokemon.handle() 31 | async def _(event: Event, message: Message = CommandArg()): 32 | argv = str(message).strip().split(" ") 33 | flag = False 34 | if len(argv) == 1: 35 | flag = True 36 | argv.append(1) 37 | try: 38 | data, others = get_image(argv[0], int(argv[1]) - 1) 39 | except Exception as e: 40 | await pokemon.send("查询出错:" + str(e)) 41 | return 42 | if type(data) == type(""): 43 | await pokemon.send(Message([ 44 | MessageSegment("image", { 45 | "file": f"file:///{os.getcwd()}/{data}" 46 | }) 47 | ])) 48 | elif data is not None: 49 | await pokemon.send(Message([ 50 | MessageSegment("image", { 51 | "file": f"base64://{str(image_to_base64(data), encoding='utf-8')}" 52 | }) 53 | ])) 54 | else: 55 | await pokemon.send(f"未找到名为【{argv[0]}】的宝可梦~") 56 | return 57 | if len(others) > 1 and flag: 58 | s = f"""该宝可梦存在其他形态,如果需要查询对应形态请输入poke {argv[0]} <编号>,编号列表如下:\n""" 59 | for j, o in enumerate(others): 60 | s += f"{j+1}. {o['name']}\n" 61 | await pokemon.send(s.strip()) 62 | 63 | 64 | dmgcalc = on_command("伤害计算", rule=__group_checker) 65 | 66 | @dmgcalc.handle() 67 | async def _(event: Event, message: Message = CommandArg()): 68 | argv = str(message).strip() 69 | if argv == "帮助": 70 | s = '''伤害计算格式为 71 | 伤害计算 [攻击方宝可梦] [招式] [防御方宝可梦] [额外加成] 72 | 宝可梦均为 50 级 73 | 74 | 宝可梦可以设置如下参数,以空格隔开,最后一个参数为宝可梦: 75 | 252C/252+SpA/252-特攻:设置努力值/性格修正 76 | 31ivatk/31iva:设置个体值 77 | 太晶火:设置太晶属性 78 | +0~6:设置能力变化 79 | 150%:设置对应项百分比的能力变化,例如 80 | 81 | 招式可以使用已存在的招式,也可以自由设定威力不同的招式,例如“150飞行物理” 82 | 欺诈、精神冲击、打草结、重磅冲撞等招式并未实现,请采用换算方式计算 83 | 84 | 由于物品、招式和特性均没有实现,所以引入额外加成的概念: 85 | 额外加成均为倍率,使用空格隔开,倍率后可以附加不同的字母代表不同的乘算方式,具体如下 86 | 无后缀:最终乘区内计算的加成,比如生命宝珠(1.3) 87 | p后缀:代表在属性克制和本系加成之前、但是在技能威力之后计算的乘区,比如晴天(1.5p) 88 | bp后缀:代表直接计算在威力区的加成,例如沙暴下岩石系精灵的特防,讲究眼镜、讲究头带 89 | 拍落(1.5bp),技术高手(1.5bp),妖精皮肤(1.2bp),双打对战(0.75bp),木炭(1.2bp) 90 | hit后缀:代表多次攻击,例如种子机关枪(5hit),鼠数儿(10hit) 91 | 92 | 几个例子: 93 | 极限特攻的冰伊布携带讲究眼镜,对沙暴下的班基拉斯使用冰冻光束: 94 | 伤害计算 252+spa 150% 冰伊布 冰冻光束 150% 班基拉斯 95 | 晴天下双打对战,77%生命值、携带生命宝珠的极限特攻煤炭龟,对极限特耐的多龙巴鲁托使用喷火: 96 | 伤害计算 252+spa 煤炭龟 喷火 252hp 252+spd 多龙巴鲁托 0.77bp 0.75bp 1.3 1.5p 97 | 极限物攻、技师斗笠菇对无耐久吃吼霸使用五段种子机关枪: 98 | 伤害计算 252+atk 斗笠菇 种子机关枪 吃吼霸 1.5bp 5hit''' 99 | await dmgcalc.send(Message([ 100 | MessageSegment("image", { 101 | "file": f"base64://{str(image_to_base64(text_to_image(s)), encoding='utf-8')}" 102 | }) 103 | ])) 104 | return 105 | try: 106 | result = calculate_damage(argv) 107 | except Exception as e: 108 | await dmgcalc.send("格式错误,请使用 伤害计算 帮助 查看命令格式\n" + str(e)) 109 | return 110 | if len(result['percent']) == 2: 111 | s = f"> {message}: {result['range'][0]} - {result['range'][1]} ({result['percent'][0]}% - {result['percent'][1]}%)\n" 112 | s += "单次可能的伤害值:" + ", ".join([str(r) for r in result["int"]]) 113 | else: 114 | s = f"> {message}: {result['int'][0]} - {result['int'][15]} ({result['percent'][0]}% - {result['percent'][15]}%)\n" 115 | s += "可能的伤害值:" + ", ".join([str(r) for r in result["int"]]) 116 | await dmgcalc.send(s) 117 | 118 | -------------------------------------------------------------------------------- /src/plugins/public.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from nonebot import on_command, on_notice, get_driver 3 | from nonebot.log import logger 4 | from nonebot.params import CommandArg, EventMessage 5 | from nonebot.typing import T_State 6 | from nonebot.matcher import Matcher 7 | from nonebot.adapters.onebot.v11 import Message, Event, Bot, MessageSegment 8 | from nonebot.exception import IgnoredException 9 | from nonebot.adapters.onebot.v11.exception import ActionFailed 10 | from nonebot.message import event_preprocessor, run_postprocessor 11 | from src.libraries.image import * 12 | from src.data_access.redis import NumberRedisData 13 | import time 14 | import random 15 | 16 | from src.data_access.plugin_manager import plugin_manager 17 | 18 | 19 | def is_channel_message(event): 20 | return hasattr(event, 'message_type') and event.message_type == 'guild' 21 | 22 | @event_preprocessor 23 | async def preprocessor(bot, event, state): 24 | if is_channel_message(event): 25 | qq = NumberRedisData(f'channel_bind_{event.user_id}') 26 | setattr(event, 'sender_id', qq.data if qq.data != 0 else event.user_id) 27 | elif hasattr(event, 'user_id'): 28 | setattr(event, 'sender_id', event.user_id) 29 | print(event.__dict__) 30 | 31 | if hasattr(event, 'message_type') and event.message_type == "private" and event.sub_type != "friend": 32 | raise IgnoredException("not reply group temp message") 33 | 34 | 35 | last_call_time = 0 36 | last_fail_time = 0 37 | failed_count = 0 38 | 39 | @run_postprocessor 40 | async def _(bot: Bot, event, matcher: Matcher, exception: Optional[Exception]): 41 | global last_call_time 42 | global last_fail_time 43 | global failed_count 44 | if not exception: 45 | return 46 | 47 | try: 48 | raise exception 49 | except ActionFailed as err: 50 | print(err.__dict__) 51 | if err.info['msg'] == 'SEND_MSG_API_ERROR': 52 | if time.time() - last_fail_time < 180: 53 | logger.error(f"Send message error, count {failed_count}") 54 | last_fail_time = time.time() 55 | failed_count += 1 56 | if failed_count > 5 and time.time() - last_call_time > 300: 57 | last_call_time = time.time() 58 | await bot.send_msg(message_type="private", user_id=2300756578, message="爹,救我") 59 | await bot.send_msg(message_type="private", user_id=1131604199, message="妈,救我") 60 | return 61 | else: 62 | last_fail_time = time.time() 63 | failed_count = 0 64 | else: 65 | raise err 66 | 67 | help = on_command('help') 68 | 69 | 70 | @help.handle() 71 | async def _(bot: Bot, event: Event, state: T_State, message: Message = CommandArg()): 72 | arg = str(message).strip() 73 | if arg == '': 74 | s = '可用插件帮助列表:' 75 | for name, value in plugin_manager.get_all(event.group_id).items(): 76 | if value: 77 | s += f'\nhelp {name}' 78 | await help.send(s) 79 | return 80 | 81 | try: 82 | if not plugin_manager.get_enable(event.group_id, arg): 83 | raise Exception("插件已禁用") 84 | meta = plugin_manager.metadata[arg] 85 | await help.send(Message([ 86 | MessageSegment("image", { 87 | "file": f"base64://{str(image_to_base64(text_to_image(meta['help_text'])), encoding='utf-8')}" 88 | }) 89 | ])) 90 | except Exception: 91 | await help.send("未找到插件或插件未启用") 92 | 93 | 94 | async def _group_poke(bot: Bot, event: Event) -> bool: 95 | value = (event.notice_type == "notify" and event.sub_type == "poke" and event.target_id == int(bot.self_id)) 96 | return value 97 | 98 | 99 | poke = on_notice(rule=_group_poke, priority=10, block=True) 100 | 101 | 102 | @poke.handle() 103 | async def _(bot: Bot, event: Event, state: T_State): 104 | if event.__getattribute__('group_id') is None: 105 | event.__delattr__('group_id') 106 | await poke.send(Message([ 107 | MessageSegment("poke", { 108 | "qq": f"{event.sender_id}" 109 | }) 110 | ])) 111 | 112 | 113 | plugin_manage = on_command("插件管理") 114 | 115 | 116 | @plugin_manage.handle() 117 | async def _(bot: Bot, event: Event, message: Message = CommandArg()): 118 | if not hasattr(event, 'group_id'): 119 | await plugin_manage.send("千雪私聊默认启用所有功能,请于群聊中使用此命令") 120 | return 121 | is_superuser = str(event.user_id) in get_driver().config.superusers 122 | print(is_superuser) 123 | is_group_admin = (await bot.get_group_member_info(group_id=event.group_id, user_id=event.user_id))['role'] != "member" 124 | argv = str(message).strip() 125 | print(argv) 126 | if argv == "": 127 | s = '\n'.join([f"{'√' if v else '×'}{k}" for k, v in plugin_manager.get_all(event.group_id).items()]) 128 | await plugin_manage.send("当前插件列表:\n" + s) 129 | return 130 | if not is_superuser and not is_group_admin: 131 | await plugin_manage.send("权限不足,请检查权限后再试") 132 | return 133 | args = argv.split(' ') 134 | try: 135 | if len(args) != 2: 136 | raise Exception 137 | if args[1] not in plugin_manager.get_all(event.group_id): 138 | await plugin_manage.send(f"未找到名为{args[1]}的插件") 139 | return 140 | if args[0] == "启用": 141 | plugin_manager.set_enable(event.group_id, args[1], True) 142 | elif args[0] == "禁用": 143 | plugin_manager.set_enable(event.group_id, args[1], False) 144 | else: 145 | raise Exception 146 | s = '\n'.join([f"{'√' if v else '×'}{k}" for k, v in plugin_manager.get_all(event.group_id).items()]) 147 | await plugin_manage.send("修改成功,当前插件列表:\n" + s) 148 | return 149 | except Exception: 150 | await plugin_manage.send("格式错误,正确格式为:插件管理 启用/禁用 [插件名]") 151 | 152 | 153 | is_alive = on_command("千雪") 154 | 155 | 156 | @is_alive.handle() 157 | async def _(bot: Bot, message: Message = CommandArg()): 158 | print(str(message).strip()) 159 | if str(message).strip() == "": 160 | await is_alive.finish("我在") 161 | 162 | 163 | shuffle = on_command('shuffle') 164 | 165 | @shuffle.handle() 166 | async def _(event: Event, message: Message = CommandArg()): 167 | try: 168 | num = int(str(message).strip()) 169 | except Exception: 170 | await shuffle.send("Usage: shuffle ") 171 | if num > 200: 172 | await shuffle.send("number should be lower than 200") 173 | else: 174 | a = list(range(1, num + 1)) 175 | random.shuffle(a) 176 | await shuffle.send(', '.join([str(b) for b in a])) 177 | 178 | 179 | channel_bind = on_command('qq绑定') 180 | 181 | @channel_bind.handle() 182 | async def _(event: Event, message: Message = CommandArg()): 183 | if not is_channel_message(event): 184 | return 185 | try: 186 | val = int(str(message).strip()) 187 | qq = NumberRedisData(f'channel_bind_{event.user_id}') 188 | qq.save(val) 189 | await channel_bind.send(f'已绑定频道用户 ID {event.user_id} 到 QQ 号 {val}') 190 | except Exception: 191 | await channel_bind.send('绑定的 qq 格式错误,请重试') 192 | -------------------------------------------------------------------------------- /src/plugins/reserve.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, on_regex, get_driver, get_bot 2 | from nonebot.log import logger 3 | from nonebot.params import CommandArg, EventMessage 4 | from nonebot.adapters import Event 5 | from nonebot.adapters.onebot.v11 import Message, MessageSegment 6 | 7 | from src.data_access.plugin_manager import plugin_manager 8 | from src.data_access.redis import * 9 | from src.libraries.tool import hash 10 | from src.libraries.image import * 11 | from collections import defaultdict 12 | import asyncio 13 | import re 14 | import time 15 | import json 16 | 17 | __plugin_meta = { 18 | "name": "预定", 19 | "enable": False, 20 | "help_text": "有没有xx/什么时候xx - 预定某项目\n没有xx/不xx了 - 退出预定某项目\nxx来人 - bot帮你喊人" 21 | } 22 | 23 | plugin_manager.register_plugin(__plugin_meta) 24 | 25 | async def __group_checker(event: Event): 26 | if hasattr(event, 'message_type') and event.message_type == 'channel': 27 | return False 28 | elif not hasattr(event, 'group_id'): 29 | return False 30 | else: 31 | return plugin_manager.get_enable(event.group_id, __plugin_meta["name"]) 32 | 33 | 34 | reserve_data = defaultdict(lambda: {'id': [], 'date': None}) 35 | 36 | def add_id_to_dict(data, uid): 37 | # by ChatGPT 38 | # 获取当前时间戳的日期 39 | current_date = time.strftime('%Y-%m-%d', time.localtime()) 40 | if current_date == data.get('date'): 41 | # 如果当前时间和dict里面记录的时间戳为同一天,则在数组中添加id 42 | if uid not in data['id']: 43 | data['id'].append(uid) 44 | else: 45 | # 否则清空数组再加入id 46 | data['id'] = [uid] 47 | data['date'] = current_date 48 | 49 | 50 | on_reserve = on_regex(r"^(有没有|什么时候)(.+)$", rule=__group_checker) 51 | 52 | @on_reserve.handle() 53 | async def _(event: Event, message: Message = EventMessage()): 54 | regex = "^(有没有|什么时候)(.+)" 55 | name = re.match(regex, str(message)).groups()[1].strip() 56 | data = reserve_data[name + str(event.group_id)] 57 | add_id_to_dict(data, str(event.user_id)) 58 | await on_reserve.send(f'已预约{name},现在一共有{len(data["id"])}人') 59 | 60 | 61 | on_cancel = on_regex(r"^(没有(.+)|不(.+)了)$", rule=__group_checker) 62 | @on_cancel.handle() 63 | async def _(event: Event, message: Message = EventMessage()): 64 | regex = "^(没有(.+)|不(.+)了)" 65 | lst = re.match(regex, str(message)).groups() 66 | name = lst[1] if lst[2] is None else lst[2] 67 | if name + str(event.group_id) in reserve_data: 68 | data = reserve_data[name + str(event.group_id)] 69 | if str(event.user_id) not in data['id']: 70 | await on_cancel.send(f'你又没说要来你叫什么') 71 | else: 72 | del data['id'][data['id'].index(str(event.user_id))] 73 | await on_cancel.send(f'不来以后都别来了') 74 | 75 | 76 | on_call = on_regex(r"^(.+)来人$", rule=__group_checker) 77 | @on_call.handle() 78 | async def _(event: Event, message: Message = EventMessage()): 79 | regex = "^(.+)来人" 80 | name = re.match(regex, str(message)).groups()[0].strip() 81 | if name + str(event.group_id) in reserve_data: 82 | data = reserve_data[name + str(event.group_id)] 83 | if len(data['id']) > 0: 84 | msgs = [] 85 | for uid in data['id']: 86 | msgs.append(MessageSegment("at", { 87 | "qq": uid 88 | })) 89 | await on_call.send(Message(msgs)) 90 | else: 91 | await on_call.send(f"没人{name}") 92 | else: 93 | await on_call.send(f"没人{name}") 94 | 95 | 96 | 97 | --------------------------------------------------------------------------------