├── .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 |
--------------------------------------------------------------------------------