@sophiacyl/dsh-plugin-waves 0.0.0-stage → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/DSH-GO-VERIFICATION.md +57 -0
  2. package/ICON.png +0 -0
  3. package/LICENSE +674 -0
  4. package/README.md +251 -2
  5. package/cordis.patch.yml +8 -0
  6. package/install.ps1 +0 -0
  7. package/install.sh +209 -0
  8. package/lib/client.js +1511 -0
  9. package/lib/host/aliashint.js +70 -0
  10. package/lib/host/attachments.js +25 -0
  11. package/lib/host/command.js +34 -0
  12. package/lib/host/consts.js +16 -0
  13. package/lib/host/dataguide.js +35 -0
  14. package/lib/host/imagebridge.js +41 -0
  15. package/lib/host/native-result.js +91 -0
  16. package/lib/host/native.js +191 -0
  17. package/lib/host/proxy.js +59 -0
  18. package/lib/host/results.js +67 -0
  19. package/lib/host/service.js +263 -0
  20. package/lib/host/tool.js +185 -0
  21. package/lib/host/troubleshoot.js +31 -0
  22. package/lib/host/worldbook.js +80 -0
  23. package/lib/index.js +74 -0
  24. package/locale/en.json +6 -0
  25. package/locale/zh.json +6 -0
  26. package/package.json +136 -4
  27. package/service/ICON.png +0 -0
  28. package/service/ai/AI_DATA.md +221 -0
  29. package/service/knowledge/additional/manual.jsonl +5 -0
  30. package/service/knowledge/wiki/baike.jsonl +1231 -0
  31. package/service/knowledge/wiki/chengjiu.jsonl +36 -0
  32. package/service/knowledge/wiki/gonglve.jsonl +383 -0
  33. package/service/knowledge/wiki/huodong.jsonl +427 -0
  34. package/service/knowledge/wiki/renwu.jsonl +412 -0
  35. package/service/knowledge/wiki/tujian.jsonl +2889 -0
  36. package/service/knowledge/wiki/zixun.jsonl +47 -0
  37. package/service/plugins/__init__.py +0 -0
  38. package/service/pyproject.toml +40 -0
  39. package/service/rover/__init__.py +0 -0
  40. package/service/rover/ai_core/__init__.py +0 -0
  41. package/service/rover/ai_core/models.py +50 -0
  42. package/service/rover/ai_core/rag/__init__.py +5 -0
  43. package/service/rover/ai_core/rag/knowledge.py +23 -0
  44. package/service/rover/ai_core/register.py +86 -0
  45. package/service/rover/ai_core/resource.py +6 -0
  46. package/service/rover/ai_core/skills/__init__.py +0 -0
  47. package/service/rover/ai_core/skills/operations.py +8 -0
  48. package/service/rover/api.py +720 -0
  49. package/service/rover/api_render.py +161 -0
  50. package/service/rover/app_life.py +154 -0
  51. package/service/rover/aps.py +47 -0
  52. package/service/rover/avatar.py +85 -0
  53. package/service/rover/bot.py +133 -0
  54. package/service/rover/builtin.py +152 -0
  55. package/service/rover/cards/__init__.py +26 -0
  56. package/service/rover/cards/_shared.py +129 -0
  57. package/service/rover/cards/char_list.py +258 -0
  58. package/service/rover/cards/char_panel.py +1353 -0
  59. package/service/rover/cards/echo_list.py +206 -0
  60. package/service/rover/cards/matrix_roles.py +25 -0
  61. package/service/rover/cards/rank_boards.py +714 -0
  62. package/service/rover/cards/rank_total.py +664 -0
  63. package/service/rover/compat.py +215 -0
  64. package/service/rover/config.py +163 -0
  65. package/service/rover/custom_panel.py +220 -0
  66. package/service/rover/data_store.py +31 -0
  67. package/service/rover/gss.py +19 -0
  68. package/service/rover/handler.py +191 -0
  69. package/service/rover/help/__init__.py +0 -0
  70. package/service/rover/help/draw_new_plugin_help.py +332 -0
  71. package/service/rover/help/model.py +16 -0
  72. package/service/rover/help/utils.py +7 -0
  73. package/service/rover/hooks.py +420 -0
  74. package/service/rover/html_actions.py +236 -0
  75. package/service/rover/i18n.py +19 -0
  76. package/service/rover/knowledge.py +235 -0
  77. package/service/rover/logger.py +102 -0
  78. package/service/rover/main.py +33 -0
  79. package/service/rover/media.py +222 -0
  80. package/service/rover/message_models.py +23 -0
  81. package/service/rover/models.py +92 -0
  82. package/service/rover/native_media.py +64 -0
  83. package/service/rover/ocr.py +79 -0
  84. package/service/rover/pool.py +43 -0
  85. package/service/rover/restart.py +36 -0
  86. package/service/rover/segment.py +143 -0
  87. package/service/rover/server.py +131 -0
  88. package/service/rover/status/__init__.py +0 -0
  89. package/service/rover/status/models.py +10 -0
  90. package/service/rover/status/plugin_status.py +17 -0
  91. package/service/rover/subscribe.py +148 -0
  92. package/service/rover/sv.py +403 -0
  93. package/service/rover/templates/char_list.html +182 -0
  94. package/service/rover/templates/char_panel.html +465 -0
  95. package/service/rover/templates/echo_list.html +221 -0
  96. package/service/rover/templates/rank_all_total.html +193 -0
  97. package/service/rover/templates/rank_board.html +107 -0
  98. package/service/rover/templates/rank_level_total.html +57 -0
  99. package/service/rover/templates/rank_matrix_total.html +66 -0
  100. package/service/rover/templates/rank_phantom_total.html +180 -0
  101. package/service/rover/templates/rank_slash_total.html +38 -0
  102. package/service/rover/trigger.py +98 -0
  103. package/service/rover/utils/__init__.py +0 -0
  104. package/service/rover/utils/boardcast/__init__.py +0 -0
  105. package/service/rover/utils/boardcast/models.py +15 -0
  106. package/service/rover/utils/cookie_manager/__init__.py +0 -0
  107. package/service/rover/utils/cookie_manager/qrlogin.py +24 -0
  108. package/service/rover/utils/database/__init__.py +0 -0
  109. package/service/rover/utils/database/base_models.py +1771 -0
  110. package/service/rover/utils/database/models.py +80 -0
  111. package/service/rover/utils/database/startup.py +60 -0
  112. package/service/rover/utils/default_bg/bg.jpg +0 -0
  113. package/service/rover/utils/download_resource/__init__.py +0 -0
  114. package/service/rover/utils/download_resource/download_core.py +164 -0
  115. package/service/rover/utils/download_resource/download_file.py +46 -0
  116. package/service/rover/utils/fonts/__init__.py +0 -0
  117. package/service/rover/utils/fonts/fonts.py +33 -0
  118. package/service/rover/utils/image/__init__.py +0 -0
  119. package/service/rover/utils/image/convert.py +51 -0
  120. package/service/rover/utils/image/image_tools.py +30 -0
  121. package/service/rover/utils/image/utils.py +70 -0
  122. package/service/rover/utils/plugins_config/__init__.py +0 -0
  123. package/service/rover/utils/plugins_config/gs_config.py +506 -0
  124. package/service/rover/utils/plugins_config/models.py +182 -0
  125. package/service/rover/web_app.py +3 -0
  126. package/service/rover/webconsole/__init__.py +0 -0
  127. package/service/rover/webconsole/mount_app.py +26 -0
  128. package/service/tools/migrate.py +351 -0
  129. package/service/tools/regression.py +87 -0
  130. package/service/tools/smoke.py +63 -0
  131. package/service/tools/update_plugins.py +212 -0
  132. package/service/tools/verify_actions.py +104 -0
  133. package/service/tools/verify_card.py +82 -0
  134. package/service/tools/wwdata.py +665 -0
  135. package/tests/compat.test.js +181 -0
  136. package/tests/imagebridge.test.js +58 -0
  137. package/tests/media.test.js +68 -0
  138. package/tests/native.test.js +127 -0
  139. package/tests/runtime.test.js +69 -0
  140. package/tests/service.test.js +48 -0
@@ -0,0 +1,714 @@
1
+ """练度总排行 / 无尽总排行 / 矩阵总排行(含单队) 的 HTML 卡片。
2
+
3
+ 取数沿用原绘图函数, 版式按 PIL 版逐坐标搬运; 构图出错时回退原图。
4
+ """
5
+
6
+ import asyncio
7
+ import base64
8
+ import importlib
9
+ import time
10
+ from io import BytesIO
11
+ from typing import Any, Dict, List, Optional, Tuple
12
+
13
+ from rover.cards import _shared
14
+ from rover.logger import logger
15
+
16
+ _HEADER_H = 510
17
+ _TITLE_H = 500
18
+ _ROW_H = 110
19
+
20
+ _scratch: Dict[str, Any] = {}
21
+
22
+
23
+ def _plugin(module, path: str):
24
+ """按插件根包取模块, 不写死插件的导入路径。"""
25
+ return importlib.import_module(f"{module.__name__.rsplit('.', 2)[0]}.{path}")
26
+
27
+
28
+ def _sibling(module, name: str):
29
+ return importlib.import_module(f"{module.__name__.rsplit('.', 1)[0]}.{name}")
30
+
31
+
32
+ def _font_css_url(module) -> str:
33
+ try:
34
+ return _shared.font_css_url(_plugin(module, "utils.render_utils"))
35
+ except Exception:
36
+ return ""
37
+
38
+
39
+ def _css(color: Any, alpha: Optional[float] = None) -> str:
40
+ """插件里的颜色有 (r,g,b) 元组和 '#RRGGBB'/名字两种写法, 统一成 CSS。"""
41
+ if color is None:
42
+ return "inherit"
43
+ if isinstance(color, str):
44
+ return color
45
+ r, g, b = color[0], color[1], color[2]
46
+ if alpha is None and len(color) > 3:
47
+ alpha = color[3] / 255
48
+ if alpha is None:
49
+ return f"rgb({r},{g},{b})"
50
+ return f"rgba({r},{g},{b},{alpha:.3f})"
51
+
52
+
53
+ def _data_uri(image, quality: int = 0) -> str:
54
+ buffer = BytesIO()
55
+ if quality:
56
+ image.save(buffer, format="WEBP", quality=quality)
57
+ mime = "webp"
58
+ else:
59
+ image.save(buffer, format="PNG")
60
+ mime = "png"
61
+ return f"data:image/{mime};base64," + base64.b64encode(buffer.getvalue()).decode()
62
+
63
+
64
+ class _Assets:
65
+ """重复出现的图(角色头像/合鸣/徽章底)只内联一次, 行内按 class 引用。"""
66
+
67
+ def __init__(self) -> None:
68
+ self.items: List[Dict[str, str]] = []
69
+ self._index: Dict[Any, str] = {}
70
+
71
+ def add(self, key: Any, image, quality: int = 0) -> str:
72
+ if image is None:
73
+ return ""
74
+ name = self._index.get(key)
75
+ if name is None:
76
+ name = f"g{len(self.items)}"
77
+ self.items.append({"cls": name, "uri": _data_uri(image, quality)})
78
+ self._index[key] = name
79
+ return name
80
+
81
+
82
+ def _draw_ctx(module):
83
+ """fit_text 要一个画布量文字宽度, 全局借一张 1x1。"""
84
+ ctx = _scratch.get("draw")
85
+ if ctx is None:
86
+ ctx = module.ImageDraw.Draw(module.Image.new("RGBA", (1, 1)))
87
+ _scratch["draw"] = ctx
88
+ return ctx
89
+
90
+
91
+ def _fit_name(module, text: str, max_width: int) -> Tuple[int, str]:
92
+ fonts = tuple(getattr(module, f"waves_font_{s}") for s in (20, 18, 16, 14, 12))
93
+ font, fitted = module.fit_text(_draw_ctx(module), str(text), max_width, fonts)
94
+ return int(font.size), fitted
95
+
96
+
97
+ def _mask_gradient(mask, width: int, height: int) -> str:
98
+ """标题遮罩: 照 PIL 的等比放大 + 底部裁切采样透明度, 转成竖向渐变。"""
99
+ resized = mask.convert("RGBA").resize((width, mask.height * width // mask.width))
100
+ band = resized.crop((0, resized.height - height, width, resized.height)).getchannel("A")
101
+ x = width // 2
102
+ solid = 0
103
+ for y in range(height):
104
+ if band.getpixel((x, y)) < 255:
105
+ break
106
+ solid = y
107
+ stops = [f"rgb(0,0,0) {solid}px"]
108
+ for y in range(solid, height, 8):
109
+ stops.append(f"rgba(0,0,0,{band.getpixel((x, y)) / 255:.3f}) {y}px")
110
+ stops.append(f"rgba(0,0,0,0) {height}px")
111
+ return "linear-gradient(to bottom, " + ", ".join(stops) + ")"
112
+
113
+
114
+ def _title_image(module, name: str, width: int):
115
+ """标题底图: 大图直接裁, 小图(矩阵)按宽度等比放大后再裁/垫。"""
116
+ image = module.Image.open(module.TEXT_PATH / name).convert("RGBA")
117
+ if name.endswith(".png"):
118
+ image = image.resize((width, int(image.height * width / image.width)))
119
+ if image.height <= _TITLE_H:
120
+ padded = module.Image.new("RGBA", (width, _TITLE_H), (0, 0, 0, 0))
121
+ padded.paste(image, (0, _TITLE_H - image.height))
122
+ return padded
123
+ return image.crop((0, 0, width, _TITLE_H))
124
+
125
+
126
+ def _header(module, title_name: str, title_file: str, width: int, subs: List[str]) -> dict:
127
+ icon = module.get_ICON().resize((128, 128))
128
+ return {
129
+ "title": title_name,
130
+ "art": _data_uri(_title_image(module, title_file, width).convert("RGB"), 82),
131
+ "icon": _data_uri(icon),
132
+ "mask": _mask_gradient(
133
+ module.Image.open(module.TEXT_PATH / "char_mask.png"), width, _TITLE_H
134
+ ),
135
+ "subs": [s for s in subs if s],
136
+ }
137
+
138
+
139
+ def _rank_slot(module, assets: _Assets, rank_id: int) -> dict:
140
+ """名次: 前三名贴牌, 其余色块底板, 位数越多底板越宽(照原版三档)。"""
141
+ badge = _sibling(module, "rank_badge")
142
+ slot: Dict[str, Any] = {"medal": assets.add(("medal", rank_id), badge._load_badge(rank_id))}
143
+ if slot["medal"]:
144
+ return slot
145
+ if rank_id > 1000:
146
+ slot["box"] = {"left": 10, "w": 100, "text": "999+"}
147
+ elif rank_id > 999:
148
+ slot["box"] = {"left": 10, "w": 100, "text": str(rank_id)}
149
+ elif rank_id > 99:
150
+ slot["box"] = {"left": 25, "w": 75, "text": str(rank_id)}
151
+ else:
152
+ slot["box"] = {"left": 40, "w": 50, "text": str(rank_id)}
153
+ return slot
154
+
155
+
156
+ def _bot_badge(module, assets: _Assets, background: str, name: str, x: int, y: int) -> Optional[dict]:
157
+ """bot 主人徽章: 底图按 388x72 → 208x39 等比缩放后居中, 名字压在中间。"""
158
+ if not name:
159
+ return None
160
+ try:
161
+ image = _plugin(module, "utils.image").get_bot_bg(background or "")
162
+ except Exception as e:
163
+ logger.debug(f"[鸣潮·榜单卡] 徽章底图取用失败: {e}")
164
+ image = None
165
+ badge: Dict[str, Any] = {"name": name, "cls": "", "w": 0, "h": 0, "x": x, "y": y}
166
+ if image is not None:
167
+ badge["cls"] = assets.add(("botbg", background or "", id(image)), image)
168
+ badge["w"] = round(image.width * 208 / 388)
169
+ badge["h"] = round(image.height * 39 / 72)
170
+ return badge
171
+
172
+
173
+ def _char_command(module, char_id: Any) -> str:
174
+ """自己那一行的角色头像点开面板。"""
175
+ try:
176
+ name = _plugin(module, "utils.name_convert").char_id_to_char_name(str(char_id))
177
+ except Exception:
178
+ return ""
179
+ if not name:
180
+ return ""
181
+ try:
182
+ prefix = _plugin(module, "wutheringwaves_config").PREFIX
183
+ except Exception:
184
+ prefix = "ww"
185
+ return f"{prefix}{name}面板"
186
+
187
+
188
+ def _base_context(module, width: int, bar, bg) -> dict:
189
+ return {
190
+ "card_w": width,
191
+ "bar": _data_uri(bar),
192
+ "bg": _data_uri(bg.convert("RGB"), 72),
193
+ "font_css_url": _font_css_url(module),
194
+ "grey": _css(module.GREY),
195
+ "gold": _css(getattr(module, "SPECIAL_GOLD", (234, 183, 4))),
196
+ }
197
+
198
+
199
+ def _now() -> str:
200
+ return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
201
+
202
+
203
+ # ---------------------------------------------------------------- 练度总排行
204
+
205
+ _LEVEL_CHAR_SIZE = 40
206
+ _LEVEL_CHAR_SPACING = 45
207
+ _LEVEL_CHAR_X = 570
208
+ _LEVEL_CHAR_Y = 35
209
+
210
+
211
+ def _level_char_mask(module) -> str:
212
+ """角色头像用的小遮罩: 底部一段渐隐, 按百分比写以适配任意尺寸。"""
213
+ mask = module.Image.open(module.TEXT_PATH / "char_mask.png").convert("RGBA")
214
+ band = mask.resize((_LEVEL_CHAR_SIZE, _LEVEL_CHAR_SIZE)).getchannel("A")
215
+ stops = []
216
+ for y in range(_LEVEL_CHAR_SIZE):
217
+ stops.append(f"rgba(0,0,0,{band.getpixel((_LEVEL_CHAR_SIZE // 2, y)) / 255:.3f}) {y * 100 / _LEVEL_CHAR_SIZE:.1f}%")
218
+ return "linear-gradient(to bottom, " + ", ".join(stops) + ")"
219
+
220
+
221
+ async def _build_level_rank(module, bot, ev, pages):
222
+ self_uid = await module.WavesBind.get_uid_by_game(ev.user_id, ev.bot_id) or ""
223
+ item = module.TotalRankRequest(
224
+ page=pages,
225
+ page_num=module.RANK_PAGE_SIZE,
226
+ waves_id=self_uid,
227
+ version=module.get_version(dynamic=True, waves_id=self_uid, pages=pages),
228
+ )
229
+ rank_info = await module.get_rank(item)
230
+ if not rank_info:
231
+ return "获取练度总排行失败"
232
+ if rank_info.message and not rank_info.data:
233
+ return rank_info.message
234
+ if not rank_info.data or not rank_info.data.score_details:
235
+ return "暂无排行数据"
236
+
237
+ details = rank_info.data.score_details
238
+ avatars = await asyncio.gather(
239
+ *[module.get_avatar(d.user_id, getattr(d, "sender_avatar", "")) for d in details]
240
+ )
241
+
242
+ top_chars = {}
243
+ for detail in details:
244
+ chars = sorted(
245
+ detail.char_score_details or [], key=lambda c: c.phantom_score, reverse=True
246
+ )[:10]
247
+ top_chars[detail.waves_id] = chars
248
+ char_ids = {module.randomize_special_char_id(c.char_id) for cs in top_chars.values() for c in cs}
249
+ char_avatars = dict(
250
+ zip(char_ids, await asyncio.gather(*[module.get_square_avatar(i) for i in char_ids]))
251
+ )
252
+
253
+ assets = _Assets()
254
+ width = 1300
255
+ rows: List[dict] = []
256
+ for detail, avatar in zip(details, avatars):
257
+ is_self = bool(self_uid) and detail.waves_id == self_uid
258
+ name_size, name_text = _fit_name(module, detail.kuro_name, 130)
259
+ best = top_chars.get(detail.waves_id) or []
260
+ chars: List[dict] = []
261
+ for index, char in enumerate(best):
262
+ display_id = module.randomize_special_char_id(char.char_id)
263
+ icon = char_avatars.get(display_id)
264
+ if icon is None:
265
+ continue
266
+ chars.append(
267
+ {
268
+ "x": _LEVEL_CHAR_X + index * _LEVEL_CHAR_SPACING,
269
+ "cls": assets.add(("char", display_id), icon.resize((40, 40)), 88),
270
+ "score": f"{int(char.phantom_score)}",
271
+ "cmd": _char_command(module, display_id) if is_self else "",
272
+ }
273
+ )
274
+ row = {
275
+ "avatar": _data_uri(avatar, 80),
276
+ "name": name_text,
277
+ "name_size": name_size,
278
+ "count": len(detail.char_score_details or []),
279
+ "uid": f"特征码: {module.hide_uid(detail.waves_id, user_pref='on' if detail.hide_uid else '')}",
280
+ "uid_x": 350,
281
+ "uid_color": _css(module.RED) if is_self else "white",
282
+ "badge": _bot_badge(
283
+ module, assets, getattr(detail, "background", ""), getattr(detail, "alias_name", ""), 346, 61
284
+ ),
285
+ "chars": chars,
286
+ "best": f"{int(best[0].phantom_score)}" if best else "",
287
+ "total": f"{detail.total_score:.1f}",
288
+ }
289
+ row.update(_rank_slot(module, assets, detail.rank))
290
+ rows.append(row)
291
+
292
+ context = _base_context(
293
+ module, width, module.Image.open(module.TEXT_PATH / "bar1.png"),
294
+ module.get_custom_waves_bg(bg="bg9", crop=False),
295
+ )
296
+ context.update(
297
+ header=_header(module, "#练度总排行", "totalrank.jpg", width, [_now()]),
298
+ cond={
299
+ "line1": "1. 综合所有角色的声骸分数。具备声骸套装的角色,全量刷新面板后更新排行位置。",
300
+ "line2": "2. 显示前10个最强角色,刷新单角色影响此处显示但不计入总分。",
301
+ "notes": "排行标准:以所有角色评分(分数>=175)总和为排序的综合排名",
302
+ },
303
+ char_mask=_level_char_mask(module),
304
+ red=_css(module.RED),
305
+ rows=rows,
306
+ assets=assets.items,
307
+ )
308
+ return context
309
+
310
+
311
+ # ---------------------------------------------------------------- 无尽总排行
312
+
313
+ _SLASH_CHAR_SIZE = 45
314
+ _SLASH_HALF_X = 570
315
+ _SLASH_HALF_SPACING = 250
316
+
317
+
318
+ async def _build_slash_rank(module, bot, ev, page):
319
+ waves_id = await module.WavesBind.get_uid_by_game(ev.user_id, ev.bot_id) or ""
320
+ item = module.SlashRankItem(
321
+ page=page,
322
+ page_num=module.RANK_PAGE_SIZE,
323
+ waves_id=waves_id,
324
+ version=module.get_version(dynamic=True, waves_id=waves_id, pages=page),
325
+ )
326
+ rank_info = await module.get_rank(item)
327
+ if not rank_info:
328
+ return "获取排行失败"
329
+ if rank_info.message and not rank_info.data:
330
+ return rank_info.message
331
+ if not rank_info.data or not rank_info.data.rank_list:
332
+ return "暂无排行数据"
333
+
334
+ rank_list = rank_info.data.rank_list
335
+ avatars = await asyncio.gather(
336
+ *[module.get_avatar(r.user_id, getattr(r, "sender_avatar", "")) for r in rank_list]
337
+ )
338
+
339
+ char_ids = {
340
+ module.randomize_special_char_id(c.char_id)
341
+ for r in rank_list
342
+ for half in r.half_list
343
+ for c in half.char_detail
344
+ }
345
+ char_avatars = dict(
346
+ zip(char_ids, await asyncio.gather(*[module.get_square_avatar(i) for i in char_ids]))
347
+ )
348
+ buff_urls = {half.buff_icon for r in rank_list for half in r.half_list if half.buff_icon}
349
+ buff_icons = dict(
350
+ zip(
351
+ buff_urls,
352
+ await asyncio.gather(
353
+ *[module.pic_download_from_url(module.SLASH_PATH, u) for u in buff_urls],
354
+ return_exceptions=True,
355
+ ),
356
+ )
357
+ )
358
+
359
+ assets = _Assets()
360
+ width = 1300
361
+ rows: List[dict] = []
362
+ for rank, avatar in zip(rank_list, avatars):
363
+ is_self = bool(waves_id) and rank.waves_id == waves_id
364
+ name_size, name_text = _fit_name(module, rank.kuro_name, 130)
365
+ gold_total = sum(
366
+ c.chain + 1
367
+ for half in rank.half_list
368
+ for c in half.char_detail
369
+ if c.chain >= 0 and module.is_limited_5star(c.char_id)
370
+ )
371
+ halves: List[dict] = []
372
+ for half_index, half in enumerate(rank.half_list):
373
+ offset = 10 if half_index == 0 else 0
374
+ base_x = _SLASH_HALF_X + half_index * _SLASH_HALF_SPACING + offset
375
+ chars: List[dict] = []
376
+ for role_index, char in enumerate(half.char_detail):
377
+ display_id = module.randomize_special_char_id(char.char_id)
378
+ if module.get_char_model(display_id) is None:
379
+ continue
380
+ icon = char_avatars.get(display_id)
381
+ if icon is None:
382
+ continue
383
+ chars.append(
384
+ {
385
+ "x": base_x + role_index * 50,
386
+ "cls": assets.add(("char", display_id), icon.resize((45, 45)), 88),
387
+ "chain": char.chain,
388
+ "chain_color": _css(module.CHAIN_COLOR[char.chain], 0.9) if char.chain != -1 else "",
389
+ "cmd": _char_command(module, display_id) if is_self else "",
390
+ }
391
+ )
392
+ buff = buff_icons.get(half.buff_icon)
393
+ halves.append(
394
+ {
395
+ "chars": chars,
396
+ "buff": assets.add(("buff", half.buff_icon), buff if not isinstance(buff, BaseException) else None, 88),
397
+ "buff_x": 720 + half_index * _SLASH_HALF_SPACING + offset,
398
+ "buff_color": _css(module.COLOR_QUALITY[half.buff_quality]),
399
+ "score": f"{half.score}",
400
+ "score_x": 670 + half_index * _SLASH_HALF_SPACING + offset,
401
+ "score_color": _css(module.get_score_color(half.score)),
402
+ }
403
+ )
404
+ row = {
405
+ "avatar": _data_uri(avatar, 80),
406
+ "name": name_text,
407
+ "name_size": name_size,
408
+ "gold": f"角色限定{gold_total}金",
409
+ "uid": f"特征码: {module.hide_uid(rank.waves_id, user_pref='on' if rank.hide_uid else '')}",
410
+ "uid_x": 350,
411
+ "uid_color": _css(module.RED) if is_self else "white",
412
+ "badge": _bot_badge(
413
+ module, assets, getattr(rank, "background", ""), rank.alias_name or "", 346, 61
414
+ ),
415
+ "halves": halves,
416
+ "score": f"{rank.score}",
417
+ "score_color": _css(module.get_score_color(rank.score)),
418
+ }
419
+ row.update(_rank_slot(module, assets, rank.rank))
420
+ rows.append(row)
421
+
422
+ period = ""
423
+ if rank_info.data.start_date:
424
+ rank_dt = module.parse_rank_date(rank_info.data.start_date)
425
+ if rank_dt:
426
+ period = f"第{module.get_slash_period_number(rank_dt)}期"
427
+
428
+ context = _base_context(
429
+ module, width, module.Image.open(module.TEXT_PATH / "bar1.png"),
430
+ module.get_waves_bg(bg="bg9", crop=False),
431
+ )
432
+ context.update(
433
+ header=_header(module, "#无尽总排行", "slash.jpg", width, [period, _now()]),
434
+ rows=rows,
435
+ assets=assets.items,
436
+ )
437
+ return context
438
+
439
+
440
+ # ---------------------------------------------------------------- 矩阵总排行
441
+
442
+
443
+ def _crystal_gradient(module) -> str:
444
+ colors = _sibling(module, "_colors").CRYSTAL_COLORS
445
+ return "linear-gradient(90deg, " + ", ".join(_css(c) for c in colors) + ")"
446
+
447
+
448
+ def _matrix_score_style(module, color) -> str:
449
+ return "" if color == module.CRYSTAL_SENTINEL else _css(color)
450
+
451
+
452
+ async def _build_matrix_rank(module, bot, ev, single_team, page, char_ids):
453
+ waves_id = await module.WavesBind.get_uid_by_game(ev.user_id, ev.bot_id) or ""
454
+ item = module.MatrixRankItem(
455
+ page=page,
456
+ page_num=module.RANK_PAGE_SIZE,
457
+ waves_id=waves_id,
458
+ version=module.get_version(dynamic=True, waves_id=waves_id, pages=page),
459
+ single_team=single_team,
460
+ char_ids=char_ids or [],
461
+ )
462
+ rank_info = await module.get_rank(item)
463
+ if not rank_info:
464
+ return "获取矩阵排行失败"
465
+ if rank_info.message and not rank_info.data:
466
+ return rank_info.message
467
+ if not rank_info.data or not rank_info.data.rank_list:
468
+ return "暂无该队伍的排行数据" if char_ids else "暂无排行数据"
469
+
470
+ rank_list = rank_info.data.rank_list
471
+ width = module.MATRIX_SINGLE_TOTAL_WIDTH if single_team else module.MATRIX_TOTAL_WIDTH
472
+ board_7digit = any(r.score >= 1_000_000 for r in rank_list)
473
+ board_6digit = any(r.score >= 100_000 for r in rank_list)
474
+ team_limit = 1 if single_team else 2
475
+ char_size = 55 if single_team else 45
476
+ char_spacing = 60 if single_team else 50
477
+ char_y = 28 if single_team else 20
478
+ buff_size = 60 if single_team else 50
479
+ buff_y = 25 if single_team else 15
480
+ chain_size = 20 if single_team else 15
481
+ team_base_x = (580 if board_6digit else 600) if single_team else 575
482
+ team_spacing = 232 if (not single_team and board_7digit) else 250
483
+
484
+ avatars = await asyncio.gather(
485
+ *[module.get_avatar(r.user_id, getattr(r, "sender_avatar", "")) for r in rank_list]
486
+ )
487
+ shown_teams = [
488
+ sorted(r.teams, key=lambda t: t.score, reverse=True)[:team_limit] for r in rank_list
489
+ ]
490
+ ids = set()
491
+ icon_urls = set()
492
+ for teams in shown_teams:
493
+ for team in teams:
494
+ ids.update(module.randomize_special_char_id(c.char_id) for c in team.char_detail)
495
+ if team.buff_icon:
496
+ icon_urls.add(team.buff_icon)
497
+ if not team.char_detail:
498
+ icon_urls.update(team.role_icons)
499
+ char_avatars = dict(
500
+ zip(ids, await asyncio.gather(*[module.get_square_avatar(i) for i in ids]))
501
+ )
502
+ downloads = dict(
503
+ zip(
504
+ icon_urls,
505
+ await asyncio.gather(
506
+ *[module.pic_download_from_url(module.MATRIX_PATH, u) for u in icon_urls],
507
+ return_exceptions=True,
508
+ ),
509
+ )
510
+ )
511
+
512
+ assets = _Assets()
513
+ rows: List[dict] = []
514
+ for rank, avatar, shown in zip(rank_list, avatars, shown_teams):
515
+ is_self = bool(waves_id) and rank.waves_id == waves_id
516
+ name_size, name_text = _fit_name(
517
+ module,
518
+ rank.kuro_name,
519
+ module.MATRIX_SINGLE_NAME_MAX_WIDTH if single_team else module.MATRIX_TOTAL_NAME_MAX_WIDTH,
520
+ )
521
+ uid_text = module.hide_uid(rank.waves_id, user_pref="on" if rank.hide_uid else "")
522
+ teams: List[dict] = []
523
+ for team_index, team in enumerate(shown):
524
+ base_x = team_base_x + team_index * team_spacing
525
+ chars: List[dict] = []
526
+ for role_index, char in enumerate(team.char_detail):
527
+ display_id = module.randomize_special_char_id(char.char_id)
528
+ if module.get_char_model(display_id) is None:
529
+ continue
530
+ icon = char_avatars.get(display_id)
531
+ if icon is None:
532
+ continue
533
+ chars.append(
534
+ {
535
+ "x": base_x + role_index * char_spacing,
536
+ "cls": assets.add(("char", display_id, char_size), icon.resize((char_size, char_size)), 88),
537
+ "chain": char.chain,
538
+ "chain_color": _css(module.CHAIN_COLOR[char.chain], 0.9) if char.chain != -1 else "",
539
+ "round": False,
540
+ "cmd": _char_command(module, display_id) if is_self else "",
541
+ }
542
+ )
543
+ if not team.char_detail and team.role_icons:
544
+ for role_index, url in enumerate(team.role_icons):
545
+ icon = downloads.get(url)
546
+ if icon is None or isinstance(icon, BaseException):
547
+ continue
548
+ chars.append(
549
+ {
550
+ "x": base_x + role_index * char_spacing,
551
+ "cls": assets.add(("url", url, char_size), icon.resize((char_size, char_size)), 88),
552
+ "chain": -1,
553
+ "chain_color": "",
554
+ "round": True,
555
+ "cmd": "",
556
+ }
557
+ )
558
+ filled = len(team.char_detail) or len(team.role_icons)
559
+ buff = downloads.get(team.buff_icon)
560
+ score_color = module.get_team_score_color(team.score)
561
+ teams.append(
562
+ {
563
+ "chars": chars,
564
+ "holes": [base_x + i * char_spacing for i in range(filled, 3)],
565
+ "buff": assets.add(
566
+ ("buff", team.buff_icon, buff_size),
567
+ buff.resize((buff_size, buff_size)) if buff is not None and not isinstance(buff, BaseException) else None,
568
+ 88,
569
+ ),
570
+ "buff_x": base_x + char_spacing * 3 + 10,
571
+ "score": f"{'最高单队得分' if team_index == 0 else '次高单队得分'}: {team.score}",
572
+ "score_x": base_x + 105,
573
+ "score_color": _matrix_score_style(module, score_color),
574
+ }
575
+ )
576
+ score_color = (
577
+ module.get_team_score_color(rank.score) if single_team else module.get_score_color(rank.score)
578
+ )
579
+ team_count = rank.team_count if rank.team_count else len(rank.teams)
580
+ row = {
581
+ "avatar": _data_uri(avatar, 80),
582
+ "name": name_text,
583
+ "name_size": name_size,
584
+ "uid": f"特征码: {uid_text}" if single_team else uid_text,
585
+ "uid_x": 350 if single_team else 210,
586
+ "uid_color": _css(module.RED) if is_self else "white",
587
+ "gold": module._get_matrix_team_char_gold_count(rank.teams) if single_team else 0,
588
+ "team_count": f"上场队伍数量: {team_count}" if team_count else "",
589
+ "badge": _bot_badge(
590
+ module,
591
+ assets,
592
+ getattr(rank, "background", ""),
593
+ rank.alias_name or "",
594
+ 346 if single_team else 350,
595
+ 60,
596
+ ),
597
+ "teams": teams,
598
+ "score": f"{rank.score}",
599
+ "score_x": 950 if single_team else 1140,
600
+ "score_color": _matrix_score_style(module, score_color),
601
+ }
602
+ row.update(_rank_slot(module, assets, rank.rank))
603
+ rows.append(row)
604
+
605
+ period = ""
606
+ if rank_info.data.start_date:
607
+ rank_dt = module.parse_rank_date(rank_info.data.start_date)
608
+ if rank_dt:
609
+ period = f"第{module.get_matrix_period_number(rank_dt)}期"
610
+ subs = [period, _now()]
611
+ if period:
612
+ subs.append(module._matrix_team_label(char_ids))
613
+
614
+ context = _base_context(
615
+ module, width, module._get_matrix_rank_bar(width), module.get_waves_bg(bg="bg9", crop=False)
616
+ )
617
+ context.update(
618
+ header=_header(
619
+ module,
620
+ "#矩阵单队总排行" if single_team else "#矩阵总排行",
621
+ "matrix.png",
622
+ width,
623
+ subs,
624
+ ),
625
+ single_team=single_team,
626
+ char_size=char_size,
627
+ char_y=char_y,
628
+ chain_size=chain_size,
629
+ buff_size=buff_size,
630
+ buff_y=buff_y,
631
+ hole_first_y=19 if single_team else 16,
632
+ hole_second_y=35 if single_team else 32,
633
+ crystal=_crystal_gradient(module),
634
+ red=_css(module.RED),
635
+ rows=rows,
636
+ assets=assets.items,
637
+ )
638
+ return context
639
+
640
+
641
+ # ---------------------------------------------------------------- 补丁挂载
642
+
643
+
644
+ def _render(template_name: str, context: dict):
645
+ card = _shared.render(template_name, context)
646
+ logger.info(f"[鸣潮·榜单卡] {template_name} {len(card) / 1024 / 1024:.2f}MB")
647
+ return card
648
+
649
+
650
+ def _bind(module, name: str, func) -> None:
651
+ """功能模块 import 时已按值绑定, 包内同名引用一并换掉。"""
652
+ import sys
653
+
654
+ setattr(module, name, func)
655
+ package = sys.modules.get(module.__name__.rsplit(".", 1)[0])
656
+ if package is not None and hasattr(package, name):
657
+ setattr(package, name, func)
658
+
659
+
660
+ def patch_level_rank(module) -> None:
661
+ original = module.draw_total_rank
662
+
663
+ async def draw_total_rank(bot, ev, pages):
664
+ try:
665
+ context = await _build_level_rank(module, bot, ev, pages)
666
+ if isinstance(context, str):
667
+ return context
668
+ return _render("rank_level_total.html", context)
669
+ except Exception as e:
670
+ logger.exception(f"[鸣潮·榜单卡] 练度总排行渲染失败, 回退原图: {e}")
671
+ return await original(bot, ev, pages)
672
+
673
+ _bind(module, "draw_total_rank", draw_total_rank)
674
+
675
+
676
+ def patch_slash_rank(module) -> None:
677
+ original = module.draw_all_slash_rank_card
678
+
679
+ async def draw_all_slash_rank_card(bot, ev, page: int = 1):
680
+ try:
681
+ context = await _build_slash_rank(module, bot, ev, page)
682
+ if isinstance(context, str):
683
+ return context
684
+ return _render("rank_slash_total.html", context)
685
+ except Exception as e:
686
+ logger.exception(f"[鸣潮·榜单卡] 无尽总排行渲染失败, 回退原图: {e}")
687
+ return await original(bot, ev, page)
688
+
689
+ _bind(module, "draw_all_slash_rank_card", draw_all_slash_rank_card)
690
+
691
+
692
+ def patch_matrix_rank(module) -> None:
693
+ original = module.draw_all_matrix_rank_card
694
+
695
+ async def draw_all_matrix_rank_card(bot, ev, single_team=False, page=1, char_ids=None):
696
+ try:
697
+ context = await _build_matrix_rank(module, bot, ev, single_team, page, char_ids)
698
+ if isinstance(context, str):
699
+ return context
700
+ return _render("rank_matrix_total.html", context)
701
+ except Exception as e:
702
+ logger.exception(f"[鸣潮·榜单卡] 矩阵总排行渲染失败, 回退原图: {e}")
703
+ return await original(bot, ev, single_team, page, char_ids)
704
+
705
+ _bind(module, "draw_all_matrix_rank_card", draw_all_matrix_rank_card)
706
+
707
+
708
+ PATCHES = {
709
+ "XutheringWavesUID": {
710
+ "wutheringwaves_rank.draw_total_rank_card": patch_level_rank,
711
+ "wutheringwaves_rank.slash_rank": patch_slash_rank,
712
+ "wutheringwaves_rank.matrix_rank": patch_matrix_rank,
713
+ }
714
+ }