@sophiacyl/dsh-plugin-waves 0.0.0-stage → 0.1.0-beta.2
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.
- package/DSH-GO-VERIFICATION.md +57 -0
- package/ICON.png +0 -0
- package/LICENSE +674 -0
- package/README.md +251 -2
- package/cordis.patch.yml +8 -0
- package/install.ps1 +0 -0
- package/install.sh +209 -0
- package/lib/client.js +1511 -0
- package/lib/host/aliashint.js +70 -0
- package/lib/host/attachments.js +25 -0
- package/lib/host/command.js +34 -0
- package/lib/host/consts.js +16 -0
- package/lib/host/dataguide.js +35 -0
- package/lib/host/imagebridge.js +41 -0
- package/lib/host/native-result.js +91 -0
- package/lib/host/native.js +191 -0
- package/lib/host/proxy.js +59 -0
- package/lib/host/results.js +67 -0
- package/lib/host/service.js +263 -0
- package/lib/host/tool.js +185 -0
- package/lib/host/troubleshoot.js +31 -0
- package/lib/host/worldbook.js +80 -0
- package/lib/index.js +74 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +136 -4
- package/service/ICON.png +0 -0
- package/service/ai/AI_DATA.md +221 -0
- package/service/knowledge/additional/manual.jsonl +5 -0
- package/service/knowledge/wiki/baike.jsonl +1231 -0
- package/service/knowledge/wiki/chengjiu.jsonl +36 -0
- package/service/knowledge/wiki/gonglve.jsonl +383 -0
- package/service/knowledge/wiki/huodong.jsonl +427 -0
- package/service/knowledge/wiki/renwu.jsonl +412 -0
- package/service/knowledge/wiki/tujian.jsonl +2889 -0
- package/service/knowledge/wiki/zixun.jsonl +47 -0
- package/service/plugins/__init__.py +0 -0
- package/service/pyproject.toml +40 -0
- package/service/rover/__init__.py +0 -0
- package/service/rover/ai_core/__init__.py +0 -0
- package/service/rover/ai_core/models.py +50 -0
- package/service/rover/ai_core/rag/__init__.py +5 -0
- package/service/rover/ai_core/rag/knowledge.py +23 -0
- package/service/rover/ai_core/register.py +86 -0
- package/service/rover/ai_core/resource.py +6 -0
- package/service/rover/ai_core/skills/__init__.py +0 -0
- package/service/rover/ai_core/skills/operations.py +8 -0
- package/service/rover/api.py +720 -0
- package/service/rover/api_render.py +161 -0
- package/service/rover/app_life.py +154 -0
- package/service/rover/aps.py +47 -0
- package/service/rover/avatar.py +85 -0
- package/service/rover/bot.py +133 -0
- package/service/rover/builtin.py +152 -0
- package/service/rover/cards/__init__.py +26 -0
- package/service/rover/cards/_shared.py +129 -0
- package/service/rover/cards/char_list.py +258 -0
- package/service/rover/cards/char_panel.py +1353 -0
- package/service/rover/cards/echo_list.py +206 -0
- package/service/rover/cards/matrix_roles.py +25 -0
- package/service/rover/cards/rank_boards.py +714 -0
- package/service/rover/cards/rank_total.py +664 -0
- package/service/rover/compat.py +215 -0
- package/service/rover/config.py +163 -0
- package/service/rover/custom_panel.py +220 -0
- package/service/rover/data_store.py +31 -0
- package/service/rover/gss.py +19 -0
- package/service/rover/handler.py +191 -0
- package/service/rover/help/__init__.py +0 -0
- package/service/rover/help/draw_new_plugin_help.py +332 -0
- package/service/rover/help/model.py +16 -0
- package/service/rover/help/utils.py +7 -0
- package/service/rover/hooks.py +420 -0
- package/service/rover/html_actions.py +236 -0
- package/service/rover/i18n.py +19 -0
- package/service/rover/knowledge.py +235 -0
- package/service/rover/logger.py +102 -0
- package/service/rover/main.py +33 -0
- package/service/rover/media.py +222 -0
- package/service/rover/message_models.py +23 -0
- package/service/rover/models.py +92 -0
- package/service/rover/native_media.py +64 -0
- package/service/rover/ocr.py +79 -0
- package/service/rover/pool.py +43 -0
- package/service/rover/restart.py +36 -0
- package/service/rover/segment.py +143 -0
- package/service/rover/server.py +131 -0
- package/service/rover/status/__init__.py +0 -0
- package/service/rover/status/models.py +10 -0
- package/service/rover/status/plugin_status.py +17 -0
- package/service/rover/subscribe.py +148 -0
- package/service/rover/sv.py +403 -0
- package/service/rover/templates/char_list.html +182 -0
- package/service/rover/templates/char_panel.html +465 -0
- package/service/rover/templates/echo_list.html +221 -0
- package/service/rover/templates/rank_all_total.html +193 -0
- package/service/rover/templates/rank_board.html +107 -0
- package/service/rover/templates/rank_level_total.html +57 -0
- package/service/rover/templates/rank_matrix_total.html +66 -0
- package/service/rover/templates/rank_phantom_total.html +180 -0
- package/service/rover/templates/rank_slash_total.html +38 -0
- package/service/rover/trigger.py +98 -0
- package/service/rover/utils/__init__.py +0 -0
- package/service/rover/utils/boardcast/__init__.py +0 -0
- package/service/rover/utils/boardcast/models.py +15 -0
- package/service/rover/utils/cookie_manager/__init__.py +0 -0
- package/service/rover/utils/cookie_manager/qrlogin.py +24 -0
- package/service/rover/utils/database/__init__.py +0 -0
- package/service/rover/utils/database/base_models.py +1771 -0
- package/service/rover/utils/database/models.py +80 -0
- package/service/rover/utils/database/startup.py +60 -0
- package/service/rover/utils/default_bg/bg.jpg +0 -0
- package/service/rover/utils/download_resource/__init__.py +0 -0
- package/service/rover/utils/download_resource/download_core.py +164 -0
- package/service/rover/utils/download_resource/download_file.py +46 -0
- package/service/rover/utils/fonts/__init__.py +0 -0
- package/service/rover/utils/fonts/fonts.py +33 -0
- package/service/rover/utils/image/__init__.py +0 -0
- package/service/rover/utils/image/convert.py +51 -0
- package/service/rover/utils/image/image_tools.py +30 -0
- package/service/rover/utils/image/utils.py +70 -0
- package/service/rover/utils/plugins_config/__init__.py +0 -0
- package/service/rover/utils/plugins_config/gs_config.py +506 -0
- package/service/rover/utils/plugins_config/models.py +182 -0
- package/service/rover/web_app.py +3 -0
- package/service/rover/webconsole/__init__.py +0 -0
- package/service/rover/webconsole/mount_app.py +26 -0
- package/service/tools/migrate.py +351 -0
- package/service/tools/regression.py +87 -0
- package/service/tools/smoke.py +63 -0
- package/service/tools/update_plugins.py +212 -0
- package/service/tools/verify_actions.py +104 -0
- package/service/tools/verify_card.py +82 -0
- package/service/tools/wwdata.py +665 -0
- package/tests/compat.test.js +181 -0
- package/tests/imagebridge.test.js +58 -0
- package/tests/media.test.js +68 -0
- package/tests/native.test.js +127 -0
- package/tests/runtime.test.js +69 -0
- package/tests/service.test.js +48 -0
|
@@ -0,0 +1,664 @@
|
|
|
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
|
|
12
|
+
|
|
13
|
+
from rover.cards import _shared
|
|
14
|
+
from rover.logger import logger
|
|
15
|
+
|
|
16
|
+
# char_mask.png 的竖向渐变(实测采样): 410px 起淡出, 500px 全透
|
|
17
|
+
_MASK_GRADIENT = (
|
|
18
|
+
"linear-gradient(to bottom, rgba(0,0,0,1) 0px, rgba(0,0,0,1) 410px,"
|
|
19
|
+
" rgba(0,0,0,.84) 435px, rgba(0,0,0,.6) 450px, rgba(0,0,0,.32) 465px,"
|
|
20
|
+
" rgba(0,0,0,.11) 480px, rgba(0,0,0,0) 500px)"
|
|
21
|
+
)
|
|
22
|
+
_HEADER_H = 500
|
|
23
|
+
# 标题图与人物立绘在原版里整体左移 300px 后再上遮罩
|
|
24
|
+
_TITLE_DX = -300
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _plugin(module, path: str):
|
|
28
|
+
"""按插件根包取兄弟模块, 不写死插件的导入路径。"""
|
|
29
|
+
return importlib.import_module(f"{module.__name__.rsplit('.', 2)[0]}.{path}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _sibling(module, name: str):
|
|
33
|
+
return importlib.import_module(f"{module.__name__.rsplit('.', 1)[0]}.{name}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _prefix(module) -> str:
|
|
37
|
+
try:
|
|
38
|
+
return _plugin(module, "wutheringwaves_config").PREFIX
|
|
39
|
+
except Exception:
|
|
40
|
+
return "ww"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _font_css_url(module) -> str:
|
|
44
|
+
try:
|
|
45
|
+
return _shared.font_css_url(_plugin(module, "utils.render_utils"))
|
|
46
|
+
except Exception:
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _css(color: Any, alpha: Optional[float] = None) -> str:
|
|
51
|
+
"""插件里的颜色有 (r,g,b) 元组和 '#RRGGBB' 两种写法, 统一成 CSS。"""
|
|
52
|
+
if color is None:
|
|
53
|
+
return "inherit"
|
|
54
|
+
if isinstance(color, str):
|
|
55
|
+
return color
|
|
56
|
+
r, g, b = color[0], color[1], color[2]
|
|
57
|
+
if alpha is None and len(color) > 3:
|
|
58
|
+
alpha = color[3] / 255
|
|
59
|
+
if alpha is None:
|
|
60
|
+
return f"rgb({r},{g},{b})"
|
|
61
|
+
return f"rgba({r},{g},{b},{alpha:.3f})"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _data_uri(image, quality: int = 0) -> str:
|
|
65
|
+
buffer = BytesIO()
|
|
66
|
+
if quality:
|
|
67
|
+
image.save(buffer, format="WEBP", quality=quality)
|
|
68
|
+
mime = "webp"
|
|
69
|
+
else:
|
|
70
|
+
image.save(buffer, format="PNG")
|
|
71
|
+
mime = "png"
|
|
72
|
+
return f"data:image/{mime};base64," + base64.b64encode(buffer.getvalue()).decode()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _Assets:
|
|
76
|
+
"""重复出现的图(武器/评级/合鸣/徽章底)只内联一次, 行内按 class 引用。"""
|
|
77
|
+
|
|
78
|
+
def __init__(self) -> None:
|
|
79
|
+
self.items: List[Dict[str, str]] = []
|
|
80
|
+
self._index: Dict[Any, str] = {}
|
|
81
|
+
|
|
82
|
+
def add(self, key: Any, image, quality: int = 0) -> str:
|
|
83
|
+
if image is None:
|
|
84
|
+
return ""
|
|
85
|
+
name = self._index.get(key)
|
|
86
|
+
if name is None:
|
|
87
|
+
name = f"g{len(self.items)}"
|
|
88
|
+
self.items.append({"cls": name, "uri": _data_uri(image, quality)})
|
|
89
|
+
self._index[key] = name
|
|
90
|
+
return name
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _bot_badge(module, assets: _Assets, background: str, name: str) -> Optional[dict]:
|
|
94
|
+
"""bot 主人徽章: 底图按 388x72 → 208x39 等比缩放后居中, 名字压在中间。"""
|
|
95
|
+
if not name:
|
|
96
|
+
return None
|
|
97
|
+
try:
|
|
98
|
+
image = _plugin(module, "utils.image").get_bot_bg(background or "")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.debug(f"[鸣潮·排行卡] 徽章底图取用失败: {e}")
|
|
101
|
+
image = None
|
|
102
|
+
badge: Dict[str, Any] = {"name": name, "cls": "", "w": 0, "h": 0}
|
|
103
|
+
if image is not None:
|
|
104
|
+
badge["cls"] = assets.add(("botbg", background or "", id(image)), image)
|
|
105
|
+
badge["w"] = round(image.width * 208 / 388)
|
|
106
|
+
badge["h"] = round(image.height * 39 / 72)
|
|
107
|
+
return badge
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _rank_box(rank_id: int) -> dict:
|
|
111
|
+
"""非前三名的名次底板: 位数越多底板越宽(照原版三档)。"""
|
|
112
|
+
if rank_id > 1000:
|
|
113
|
+
return {"left": 10, "w": 100, "text": "999+"}
|
|
114
|
+
if rank_id > 999:
|
|
115
|
+
return {"left": 10, "w": 100, "text": str(rank_id)}
|
|
116
|
+
if rank_id > 99:
|
|
117
|
+
return {"left": 25, "w": 75, "text": str(rank_id)}
|
|
118
|
+
return {"left": 40, "w": 50, "text": str(rank_id)}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def _header_context(module, char_id: str, title_name: str, pile_x: int) -> dict:
|
|
122
|
+
"""人物立绘头部: 标题底图 + 立绘 + 遮罩, 坐标照原版整体左移 300。"""
|
|
123
|
+
version = module.get_version()
|
|
124
|
+
header: Dict[str, Any] = {
|
|
125
|
+
"title_bg": _data_uri(module.TITLE_II, 85),
|
|
126
|
+
"logo": _data_uri(module.logo_img),
|
|
127
|
+
"title_dx": _TITLE_DX,
|
|
128
|
+
"title_name": title_name,
|
|
129
|
+
"version": f"v{version}",
|
|
130
|
+
"version_x": 240 + 31 * len(title_name),
|
|
131
|
+
"time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
|
|
132
|
+
"mask": _MASK_GRADIENT,
|
|
133
|
+
"pile": None,
|
|
134
|
+
}
|
|
135
|
+
try:
|
|
136
|
+
pile, _ = await module.get_role_pile_default(char_id, custom=True)
|
|
137
|
+
header["pile"] = {
|
|
138
|
+
"uri": _data_uri(pile, 82),
|
|
139
|
+
"x": pile_x,
|
|
140
|
+
"y": -120,
|
|
141
|
+
"w": pile.width,
|
|
142
|
+
"h": pile.height,
|
|
143
|
+
}
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.debug(f"[鸣潮·排行卡] 立绘取用失败 char_id={char_id}: {e}")
|
|
146
|
+
return header
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _background(module, quality: int = 72) -> str:
|
|
150
|
+
bg = module.get_custom_waves_bg(bg="bg3", crop=False)
|
|
151
|
+
return _data_uri(bg.convert("RGB"), quality)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------- 角色总排行
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def _build_all_rank(module, bot, ev, char, rank_type, pages, modal, group_uids):
|
|
158
|
+
self_uid = ""
|
|
159
|
+
is_self_ck = False
|
|
160
|
+
try:
|
|
161
|
+
self_uid = await module.WavesBind.get_uid_by_game(ev.user_id, ev.bot_id)
|
|
162
|
+
is_self_ck, _ = await module.waves_api.get_ck_result(self_uid, ev.user_id, ev.bot_id)
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
char_id = module.char_name_to_char_id(char)
|
|
167
|
+
if not char_id:
|
|
168
|
+
return "未找到指定角色, 请检查输入是否正确!"
|
|
169
|
+
char_name = module.alias_to_char_name(char)
|
|
170
|
+
char_model = module.get_char_model(char_id)
|
|
171
|
+
if not char_model:
|
|
172
|
+
return f"[鸣潮] 角色名【{char_name}】暂未适配!\n"
|
|
173
|
+
attribute_name = module.ATTRIBUTE_ID_MAP[char_model.attributeId]
|
|
174
|
+
|
|
175
|
+
rank_type_num = 3 if rank_type == "综合评分" else (2 if rank_type == "伤害" else 1)
|
|
176
|
+
page_num = module.RANK_PAGE_SIZE
|
|
177
|
+
if not modal:
|
|
178
|
+
options = module.get_modal_options(int(char_id))
|
|
179
|
+
if options:
|
|
180
|
+
role = await module.find_role_detail(self_uid, char_id) if self_uid else None
|
|
181
|
+
modal = module.get_role_modal(role) if role else module.get_default_modal(int(char_id))
|
|
182
|
+
|
|
183
|
+
is_group = group_uids is not None
|
|
184
|
+
if is_group:
|
|
185
|
+
resp = await module.get_cards_rank(
|
|
186
|
+
module.CardsRankRequest(
|
|
187
|
+
char_id=int(char_id),
|
|
188
|
+
rank_type=rank_type_num,
|
|
189
|
+
modal=modal,
|
|
190
|
+
waves_ids=[str(u) for u in group_uids if u],
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
if not resp:
|
|
194
|
+
return "获取群排行失败"
|
|
195
|
+
if not resp.data or not resp.data.details:
|
|
196
|
+
return "暂无排行数据"
|
|
197
|
+
details = [d for d in resp.data.details if d.overall_score > 0]
|
|
198
|
+
if not details:
|
|
199
|
+
return "[鸣潮] 群内暂无该角色综合评分数据\n需【登录】并【刷新单角色面板】上传后才会上榜"
|
|
200
|
+
details.sort(key=lambda d: d.overall_score, reverse=True)
|
|
201
|
+
for i, d in enumerate(details):
|
|
202
|
+
d.rank = i + 1
|
|
203
|
+
self_entry = next((d for d in details if self_uid and d.waves_id == self_uid), None)
|
|
204
|
+
details, _, page_count, page_item_count = module.paginate_group_rank(
|
|
205
|
+
details, pages, self_entry.rank if self_entry else None, self_entry
|
|
206
|
+
)
|
|
207
|
+
if page_item_count == 0:
|
|
208
|
+
return module.group_rank_empty_page_message(pages, page_count)
|
|
209
|
+
else:
|
|
210
|
+
item = module.RankItem(
|
|
211
|
+
char_id=int(char_id),
|
|
212
|
+
page=pages,
|
|
213
|
+
page_num=page_num,
|
|
214
|
+
rank_type=rank_type_num,
|
|
215
|
+
waves_id=self_uid,
|
|
216
|
+
version=module.get_version(
|
|
217
|
+
dynamic=True, waves_id=self_uid, char_id=char_id, rank_type=rank_type, pages=pages
|
|
218
|
+
),
|
|
219
|
+
modal=modal,
|
|
220
|
+
)
|
|
221
|
+
rank_info = await module.get_rank(item)
|
|
222
|
+
if not rank_info:
|
|
223
|
+
return "获取排行失败"
|
|
224
|
+
if rank_info.message and not rank_info.data:
|
|
225
|
+
return rank_info.message
|
|
226
|
+
if not rank_info.data or not rank_info.data.details:
|
|
227
|
+
return "暂无排行数据"
|
|
228
|
+
details = rank_info.data.details
|
|
229
|
+
|
|
230
|
+
assets = _Assets()
|
|
231
|
+
avatars = await asyncio.gather(
|
|
232
|
+
*[
|
|
233
|
+
module.get_avatar(rank.user_id, getattr(rank, "sender_avatar", ""), char_id=rank.char_id)
|
|
234
|
+
for rank in details
|
|
235
|
+
]
|
|
236
|
+
)
|
|
237
|
+
mask_uid = None
|
|
238
|
+
if is_group:
|
|
239
|
+
mask_uid = await module.build_uid_masker(
|
|
240
|
+
[(d.waves_id, d.user_id) for d in details], ev.bot_id
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
attribute_icon = assets.add(
|
|
244
|
+
("attr", attribute_name),
|
|
245
|
+
(await module.get_attribute(attribute_name, is_simple=True)).resize((40, 40)).convert("RGBA"),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
prefix = _prefix(module)
|
|
249
|
+
rows: List[dict] = []
|
|
250
|
+
total_score = 0.0
|
|
251
|
+
total_damage = 0.0
|
|
252
|
+
avg_num = 0
|
|
253
|
+
damage_name = ""
|
|
254
|
+
valid_pairs = [(r, a) for r, a in zip(details, avatars) if r.rank > 0]
|
|
255
|
+
|
|
256
|
+
for index, (rank, avatar) in enumerate(valid_pairs):
|
|
257
|
+
damage_name = rank.expected_name
|
|
258
|
+
weapon_model = module.get_weapon_model(rank.weapon_id)
|
|
259
|
+
if not weapon_model:
|
|
260
|
+
logger.warning("[鸣潮·排行卡] 武器无法找到, 可能暂未适配")
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
weapon_icon = module.crop_center_img(await module.get_square_weapon(rank.weapon_id), 110, 110)
|
|
264
|
+
weapon_bg = module.get_weapon_icon_bg(weapon_model.starLevel, module.TEXT_PATH)
|
|
265
|
+
weapon_bg.paste(weapon_icon, (10, 20), weapon_icon)
|
|
266
|
+
|
|
267
|
+
score_value = rank.overall_score if rank_type == "综合评分" else rank.phantom_score
|
|
268
|
+
score: Optional[dict] = None
|
|
269
|
+
if score_value > 0.0:
|
|
270
|
+
grade = (
|
|
271
|
+
module.get_panel_score_grade(score_value)
|
|
272
|
+
if rank_type == "综合评分"
|
|
273
|
+
else rank.phantom_score_bg
|
|
274
|
+
)
|
|
275
|
+
emblem = module.Image.open(module.TEXT_PATH / f"score_{grade}.png")
|
|
276
|
+
score = {
|
|
277
|
+
"cls": assets.add(("score", grade), emblem),
|
|
278
|
+
"value": f"{int(score_value * 100) / 100:.2f}",
|
|
279
|
+
"label": "综合评分" if rank_type == "综合评分" else "声骸分数",
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if rank.sonata_name:
|
|
283
|
+
sonata_label = module.get_sonata_label(rank.sonata_name)
|
|
284
|
+
sonata_cls = assets.add(
|
|
285
|
+
("sonata", rank.sonata_name), await module.get_sonata_effect_image(rank.sonata_name, 50)
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
sonata_label, sonata_cls = "合鸣效果", ""
|
|
289
|
+
|
|
290
|
+
if is_group:
|
|
291
|
+
uid_text = mask_uid(rank.waves_id, rank.user_id)
|
|
292
|
+
else:
|
|
293
|
+
uid_text = module.hide_uid(rank.waves_id, user_pref="on" if rank.hide_uid else "")
|
|
294
|
+
|
|
295
|
+
rows.append(
|
|
296
|
+
{
|
|
297
|
+
"rank": rank.rank,
|
|
298
|
+
"medal": assets.add(
|
|
299
|
+
("medal", rank.rank), _sibling(module, "rank_badge")._load_badge(rank.rank)
|
|
300
|
+
),
|
|
301
|
+
"box": _rank_box(rank.rank),
|
|
302
|
+
"avatar": _data_uri(avatar, 80),
|
|
303
|
+
"chain": module.get_chain_name(rank.chain),
|
|
304
|
+
"chain_color": _css(module.CHAIN_COLOR[rank.chain], 0.9),
|
|
305
|
+
"level": f"Lv.{rank.level}",
|
|
306
|
+
"attribute": attribute_icon,
|
|
307
|
+
"uid": f"特征码: {uid_text}",
|
|
308
|
+
"uid_color": _css(module.RED) if is_self_ck and self_uid == rank.waves_id else "white",
|
|
309
|
+
# 点行按对方 uid 查这个角色的面板(对方未公开则由命令自己回提示)
|
|
310
|
+
"cmd": f"{_shared.prefix()}{rank.waves_id}{char_name}面板" if rank.waves_id else "",
|
|
311
|
+
"name": str(rank.kuro_name),
|
|
312
|
+
"badge": _bot_badge(module, assets, getattr(rank, "background", ""), rank.alias_name),
|
|
313
|
+
"score": score,
|
|
314
|
+
"sonata": sonata_cls,
|
|
315
|
+
"sonata_label": sonata_label,
|
|
316
|
+
"sonata_size": 14 if len(sonata_label) > 4 else 16,
|
|
317
|
+
"weapon": {
|
|
318
|
+
"cls": assets.add(("weapon", rank.weapon_id, weapon_model.starLevel), weapon_bg),
|
|
319
|
+
"name": weapon_model.name,
|
|
320
|
+
"level": f"Lv.{rank.weapon_level}/90",
|
|
321
|
+
"reson": f"{rank.weapon_reson_level}阶",
|
|
322
|
+
"reson_color": _css(module.WEAPON_RESONLEVEL_COLOR[rank.weapon_reson_level], 0.8),
|
|
323
|
+
},
|
|
324
|
+
"damage": f"{rank.expected_damage:,.0f}",
|
|
325
|
+
"damage_name": rank.expected_name,
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if index + 1 + (pages - 1) * page_num == rank.rank:
|
|
330
|
+
total_score += rank.overall_score if rank_type == "综合评分" else rank.phantom_score
|
|
331
|
+
total_damage += rank.expected_damage
|
|
332
|
+
avg_num += 1
|
|
333
|
+
|
|
334
|
+
title_char = module.SPECIAL_CHAR_NAME.get(char_id, char_name)
|
|
335
|
+
title_name = f"{title_char}{rank_type}{'群排行' if is_group else '总排行'}"
|
|
336
|
+
|
|
337
|
+
modal_options = module.get_modal_options(int(char_id))
|
|
338
|
+
if rank_type == "伤害":
|
|
339
|
+
notes = "排行标准:以期望伤害(计算暴击率的伤害,不代表实际伤害) 为排序的排名"
|
|
340
|
+
elif rank_type == "综合评分":
|
|
341
|
+
notes = "综合评分为个性化标准,按各自配置估算得出,仅供参考,非公平对比。"
|
|
342
|
+
else:
|
|
343
|
+
notes = "排行标准:以声骸分数(声骸评分高,不代表实际伤害高) 为排序的排名"
|
|
344
|
+
cond = {
|
|
345
|
+
"height": 170 if modal_options else 130,
|
|
346
|
+
"line1": "1. 声骸套装为常规套装",
|
|
347
|
+
"line2": "2. 登录用户&刷新单角色面板" if rank_type == "综合评分" else "2. 登录用户&刷新面板",
|
|
348
|
+
"modal": "",
|
|
349
|
+
"modal_current": "",
|
|
350
|
+
"notes": notes,
|
|
351
|
+
}
|
|
352
|
+
if modal_options:
|
|
353
|
+
names = "/".join(o["name"] for o in modal_options)
|
|
354
|
+
cond["modal"] = f"支持模态: {prefix}{char}总排行 {names}"
|
|
355
|
+
current = module.get_modal_name(int(char_id), modal)
|
|
356
|
+
if current:
|
|
357
|
+
cond["modal_current"] = f"当前模态: {current}"
|
|
358
|
+
|
|
359
|
+
header = await _header_context(module, char_id, title_name, 600)
|
|
360
|
+
header["stats"] = [
|
|
361
|
+
{
|
|
362
|
+
"x": 300,
|
|
363
|
+
"value": f"{total_score / avg_num:.1f}" if avg_num else "0",
|
|
364
|
+
"label": "平均综合评分" if rank_type == "综合评分" else "平均声骸分数",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
"x": 490,
|
|
368
|
+
"value": f"{total_damage / avg_num:,.0f}" if avg_num else "0",
|
|
369
|
+
"label": "平均治疗量" if "治疗" in damage_name else "平均伤害",
|
|
370
|
+
},
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
"card_w": 1300,
|
|
375
|
+
"header_h": _HEADER_H,
|
|
376
|
+
"row_h": 110,
|
|
377
|
+
"bg": _background(module),
|
|
378
|
+
"bar": _data_uri(module.Image.open(module.TEXT_PATH / "bar1.png")),
|
|
379
|
+
"font_css_url": _font_css_url(module),
|
|
380
|
+
"gold": _css(module.SPECIAL_GOLD),
|
|
381
|
+
"grey": _css(module.GREY),
|
|
382
|
+
"header": header,
|
|
383
|
+
"cond": cond,
|
|
384
|
+
"rows": rows,
|
|
385
|
+
"assets": assets.items,
|
|
386
|
+
"cmd_score": f"{prefix}{char_name}评分总排行",
|
|
387
|
+
"cmd_damage": f"{prefix}{char_name}总排行",
|
|
388
|
+
"cmd_phantom": f"{prefix}{char_name}声骸总排行",
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ---------------------------------------------------------------- 声骸总排行
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _phantom_geometry(module) -> dict:
|
|
396
|
+
"""行内坐标全部取原模块实测常量, 保证与 PIL 版逐像素一致。"""
|
|
397
|
+
return {
|
|
398
|
+
"width": module.WIDTH,
|
|
399
|
+
"item_h": module.ITEM_H,
|
|
400
|
+
"rank_x": module._RANK_X,
|
|
401
|
+
"medal_y": module._RANK_MEDAL_Y,
|
|
402
|
+
"medal_size": module._RANK_MEDAL_SIZE[0],
|
|
403
|
+
"box_y": module._RANK_BOX_Y,
|
|
404
|
+
"box_h": module._RANK_BOX_SIZE[1],
|
|
405
|
+
"box_cx": module._RANK_X + module._RANK_BOX_SIZE[0] // 2,
|
|
406
|
+
"user_x": module.USER_INFO_X,
|
|
407
|
+
"name_y": module._USER_NAME_Y,
|
|
408
|
+
"name_max": module._USER_NAME_MAX_WIDTH,
|
|
409
|
+
"uid_y": module._USER_UID_Y,
|
|
410
|
+
"badge_x": module._BOT_BADGE_POS[0],
|
|
411
|
+
"badge_y": module._BOT_BADGE_POS[1],
|
|
412
|
+
"thumb_x": module.PHANTOM_THUMB_X - module._THUMB_SHIFT,
|
|
413
|
+
"icon_y": module._PHANTOM_ICON_Y,
|
|
414
|
+
"icon_size": module._PHANTOM_ICON_SIZE[0],
|
|
415
|
+
"fetter_dx": 58,
|
|
416
|
+
"fetter_y": module._FETTER_ICON_POS[1],
|
|
417
|
+
"fetter_size": module._FETTER_ICON_SIZE[0],
|
|
418
|
+
"pip_size": module._PROMOTE_SIZE[0],
|
|
419
|
+
"pip_y": module._PROMOTE_Y,
|
|
420
|
+
"name_col_max": module._NAME_MAX,
|
|
421
|
+
"emblem_x": module._EMBLEM_X,
|
|
422
|
+
"emblem_y": module._EMBLEM_Y,
|
|
423
|
+
"emblem_size": module._EMBLEM_SIZE[0],
|
|
424
|
+
"score_x": module._SCORE_NUM_X,
|
|
425
|
+
"score_y": module._SCORE_NUM_Y,
|
|
426
|
+
"score_label_x": module._SCORE_LABEL_X,
|
|
427
|
+
"score_label_y": module._SCORE_LABEL_Y,
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _name_size(name: str, max_width: int) -> int:
|
|
432
|
+
"""词条名照原版逐级缩字号(18/16/14), 汉字宽度按字号估算。"""
|
|
433
|
+
for size in (18, 16, 14):
|
|
434
|
+
if len(name) * size <= max_width:
|
|
435
|
+
return size
|
|
436
|
+
return 14
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _phantom_props(module, detail, calc_map, geo) -> List[dict]:
|
|
440
|
+
"""词条 2 列表: 名左对齐, 数值与单词条分各按固定右边界对齐。"""
|
|
441
|
+
entries = module.build_entries(detail.main_props, detail.sub_props, calc_map)
|
|
442
|
+
props: List[dict] = []
|
|
443
|
+
for i, entry in enumerate(entries[:6]):
|
|
444
|
+
name, value, pscore, is_main, name_color, num_color = entry
|
|
445
|
+
row, col = divmod(i, 2)
|
|
446
|
+
name_x = module._COL_NAME_X[col]
|
|
447
|
+
score_right = module._COL_SCORE_RIGHT[col]
|
|
448
|
+
props.append(
|
|
449
|
+
{
|
|
450
|
+
"name": name,
|
|
451
|
+
"name_size": _name_size(name, geo["name_col_max"]),
|
|
452
|
+
"value": value,
|
|
453
|
+
"score": f"+{pscore:.2f}",
|
|
454
|
+
"name_x": name_x,
|
|
455
|
+
"value_right": geo["width"] - module._COL_VALUE_RIGHT[col],
|
|
456
|
+
"score_right": geo["width"] - score_right,
|
|
457
|
+
"y": module._PROP_ROW_Y[row],
|
|
458
|
+
"name_color": _css(name_color),
|
|
459
|
+
"value_color": _css(num_color),
|
|
460
|
+
"score_color": _css(module.SCORE_POS if pscore > 0 else module.SCORE_ZERO),
|
|
461
|
+
"highlight": {"left": name_x - 6, "w": score_right - name_x + 12} if is_main else None,
|
|
462
|
+
}
|
|
463
|
+
)
|
|
464
|
+
return props
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
async def _build_phantom_rank(module, bot, ev, char, pages):
|
|
468
|
+
char_id = module.char_name_to_char_id(char)
|
|
469
|
+
if not char_id:
|
|
470
|
+
return "未找到指定角色, 请检查输入是否正确!"
|
|
471
|
+
char = module.char_id_to_char_name(char_id) or char
|
|
472
|
+
self_uid = await module.WavesBind.get_uid_by_game(ev.user_id, ev.bot_id) or ""
|
|
473
|
+
|
|
474
|
+
rank_info = await module.get_phantom_total_rank(
|
|
475
|
+
module.PhantomTotalRankRequest(
|
|
476
|
+
char_id=int(char_id),
|
|
477
|
+
page=pages,
|
|
478
|
+
page_num=module.RANK_PAGE_SIZE,
|
|
479
|
+
waves_id=self_uid,
|
|
480
|
+
version=module.get_version(dynamic=True, waves_id=self_uid, pages=pages),
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
if not rank_info:
|
|
484
|
+
return "获取声骸总排行失败"
|
|
485
|
+
if rank_info.message and not rank_info.data:
|
|
486
|
+
return rank_info.message
|
|
487
|
+
if not rank_info.data or not rank_info.data.rank_list:
|
|
488
|
+
return "暂无排行数据"
|
|
489
|
+
|
|
490
|
+
ranking = rank_info.data.rank_list
|
|
491
|
+
details = list(ranking)
|
|
492
|
+
self_entry = getattr(rank_info.data, "self_entry", None)
|
|
493
|
+
if self_uid and self_entry is not None:
|
|
494
|
+
if self_entry.rank and self_entry.rank > 0:
|
|
495
|
+
self_row = self_entry
|
|
496
|
+
else:
|
|
497
|
+
self_row = await module._build_local_self_row(self_uid, char_id, self_entry)
|
|
498
|
+
if self_row is not None:
|
|
499
|
+
details.append(self_row)
|
|
500
|
+
if not details:
|
|
501
|
+
return f"[鸣潮] 暂无【{char}】声骸总排行数据"
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
calc_map = module.get_calc_map({}, char, char_id)
|
|
505
|
+
except Exception as e:
|
|
506
|
+
logger.debug(f"[鸣潮·排行卡] 词条权重取用失败 char_id={char_id}: {e}")
|
|
507
|
+
calc_map = None
|
|
508
|
+
|
|
509
|
+
avatars, phantom_icons, fetter_icons = await asyncio.gather(
|
|
510
|
+
asyncio.gather(
|
|
511
|
+
*[module.get_avatar(d.user_id, getattr(d, "sender_avatar", ""), char_id=d.char_id) for d in details]
|
|
512
|
+
),
|
|
513
|
+
asyncio.gather(*[module._safe_phantom_icon(d) for d in details]),
|
|
514
|
+
asyncio.gather(*[module._safe_fetter_icon(d.set) for d in details]),
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
geo = _phantom_geometry(module)
|
|
518
|
+
assets = _Assets()
|
|
519
|
+
rows: List[dict] = []
|
|
520
|
+
for detail, avatar, phantom_icon, fetter_icon in zip(details, avatars, phantom_icons, fetter_icons):
|
|
521
|
+
avatar_box = None
|
|
522
|
+
if avatar is not None:
|
|
523
|
+
bbox = avatar.getchannel("A").getbbox()
|
|
524
|
+
if bbox is not None:
|
|
525
|
+
avatar_box = {
|
|
526
|
+
"uri": _data_uri(avatar, 80),
|
|
527
|
+
"x": module._AVATAR_VISIBLE_X - bbox[0],
|
|
528
|
+
"y": module.FRAME_TOP
|
|
529
|
+
+ (module.FRAME_BOTTOM - module.FRAME_TOP - (bbox[3] - bbox[1])) // 2
|
|
530
|
+
- bbox[1],
|
|
531
|
+
"w": avatar.width,
|
|
532
|
+
"h": avatar.height,
|
|
533
|
+
}
|
|
534
|
+
emblem = None
|
|
535
|
+
try:
|
|
536
|
+
emblem = module.Image.open(module.TEXT_PATH / f"score_{detail.grade}.png").resize(
|
|
537
|
+
module._EMBLEM_SIZE
|
|
538
|
+
)
|
|
539
|
+
except Exception as e:
|
|
540
|
+
logger.debug(f"[鸣潮·排行卡] 评级贴图缺失 grade={detail.grade}: {e}")
|
|
541
|
+
|
|
542
|
+
pip_gap = 3
|
|
543
|
+
pip_step = geo["pip_size"] + pip_gap
|
|
544
|
+
pips_w = detail.cost * geo["pip_size"] + max(detail.cost - 1, 0) * pip_gap
|
|
545
|
+
pips_x = geo["thumb_x"] + (geo["icon_size"] - pips_w) // 2
|
|
546
|
+
|
|
547
|
+
rows.append(
|
|
548
|
+
{
|
|
549
|
+
"rank": detail.rank,
|
|
550
|
+
"medal": assets.add(("medal", detail.rank), module._load_badge(detail.rank)),
|
|
551
|
+
"box": _phantom_rank_box(module, detail.rank),
|
|
552
|
+
"avatar": avatar_box,
|
|
553
|
+
"name": str(detail.kuro_name),
|
|
554
|
+
"uid": f"特征码: {module.hide_uid(detail.waves_id, user_pref='on' if detail.hide_uid else '')}",
|
|
555
|
+
"uid_color": _css(module.RED) if detail.waves_id == self_uid else "white",
|
|
556
|
+
"badge": _bot_badge(
|
|
557
|
+
module, assets, getattr(detail, "background", ""), getattr(detail, "alias_name", "")
|
|
558
|
+
),
|
|
559
|
+
"icon": assets.add(("phantom", detail.phantom_id), phantom_icon, 85),
|
|
560
|
+
"fetter": assets.add(("fetter", detail.set), fetter_icon),
|
|
561
|
+
"pips": [pips_x + pip_step * i for i in range(detail.cost)],
|
|
562
|
+
"props": _phantom_props(module, detail, calc_map, geo),
|
|
563
|
+
"emblem": assets.add(("grade", detail.grade), emblem),
|
|
564
|
+
"score": f"{detail.score:.2f}",
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
avg_value = sum(d.score for d in ranking) / len(ranking) if ranking else 0
|
|
569
|
+
title_char = module.SPECIAL_CHAR_NAME.get(char_id, char)
|
|
570
|
+
title_name = f"{title_char}声骸总排行"
|
|
571
|
+
header = await _header_context(module, char_id, title_name, geo["width"] - 700)
|
|
572
|
+
header["stats"] = [{"x": 300, "value": f"{avg_value:.1f}", "label": "平均单声骸分数"}]
|
|
573
|
+
|
|
574
|
+
prefix = _prefix(module)
|
|
575
|
+
return {
|
|
576
|
+
"card_w": geo["width"],
|
|
577
|
+
"header_h": _HEADER_H,
|
|
578
|
+
"geo": geo,
|
|
579
|
+
"bg": _background(module),
|
|
580
|
+
"bar": _data_uri(module.draw_phantom_bar_bg(module.Image.open(module.TEXT_PATH / "bar1.png"))),
|
|
581
|
+
"pip": _data_uri(module.promote_icon.resize(module._PROMOTE_SIZE)),
|
|
582
|
+
"font_css_url": _font_css_url(module),
|
|
583
|
+
"gold": _css(module.SPECIAL_GOLD),
|
|
584
|
+
"grey": _css(module.GREY),
|
|
585
|
+
"header": header,
|
|
586
|
+
"cond": {
|
|
587
|
+
"height": 130,
|
|
588
|
+
"line1": "1. 声骸满 5 副词条 & 常规有效套装",
|
|
589
|
+
"line2": "2. 登录用户 & 刷新面板",
|
|
590
|
+
"notes": "排行标准:以单声骸分数排序 (评分高不代表实际伤害高)",
|
|
591
|
+
},
|
|
592
|
+
"rows": rows,
|
|
593
|
+
"assets": assets.items,
|
|
594
|
+
"cmd_damage": f"{prefix}{char}总排行",
|
|
595
|
+
"cmd_score": f"{prefix}{char}评分总排行",
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _phantom_rank_box(module, rank_id: int) -> dict:
|
|
600
|
+
"""声骸榜名次底板: 金框外空间有限, 4 位数字换小一号字。"""
|
|
601
|
+
if rank_id > 999:
|
|
602
|
+
return {"text": "999+" if rank_id > 1000 else str(rank_id), "w": 88, "size": 30}
|
|
603
|
+
if rank_id > 99:
|
|
604
|
+
return {"text": str(rank_id), "w": 72, "size": 34}
|
|
605
|
+
return {"text": str(rank_id), "w": module._RANK_BOX_SIZE[0], "size": 34}
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
# ---------------------------------------------------------------- 补丁挂载
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _render(template_name: str, context: dict):
|
|
612
|
+
card = _shared.render(template_name, context)
|
|
613
|
+
logger.info(f"[鸣潮·排行卡] {template_name} {len(card) / 1024 / 1024:.2f}MB")
|
|
614
|
+
return card
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _bind(module, name: str, func) -> None:
|
|
618
|
+
"""功能模块 import 时已按值绑定, 包内同名引用一并换掉。"""
|
|
619
|
+
import sys
|
|
620
|
+
|
|
621
|
+
setattr(module, name, func)
|
|
622
|
+
package = sys.modules.get(module.__name__.rsplit(".", 1)[0])
|
|
623
|
+
if package is not None and hasattr(package, name):
|
|
624
|
+
setattr(package, name, func)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def patch_all_rank(module) -> None:
|
|
628
|
+
original = module.draw_all_rank_card
|
|
629
|
+
|
|
630
|
+
async def draw_all_rank_card(bot, ev, char, rank_type, pages, modal="", group_uids=None):
|
|
631
|
+
try:
|
|
632
|
+
context = await _build_all_rank(module, bot, ev, char, rank_type, pages, modal, group_uids)
|
|
633
|
+
if isinstance(context, str):
|
|
634
|
+
return context
|
|
635
|
+
return _render("rank_all_total.html", context)
|
|
636
|
+
except Exception as e:
|
|
637
|
+
logger.exception(f"[鸣潮·排行卡] 角色总排行渲染失败, 回退原图: {e}")
|
|
638
|
+
return await original(bot, ev, char, rank_type, pages, modal, group_uids)
|
|
639
|
+
|
|
640
|
+
_bind(module, "draw_all_rank_card", draw_all_rank_card)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def patch_phantom_rank(module) -> None:
|
|
644
|
+
original = module.draw_phantom_total_rank
|
|
645
|
+
|
|
646
|
+
async def draw_phantom_total_rank(bot, ev, char, pages):
|
|
647
|
+
try:
|
|
648
|
+
context = await _build_phantom_rank(module, bot, ev, char, pages)
|
|
649
|
+
if isinstance(context, str):
|
|
650
|
+
return context
|
|
651
|
+
return _render("rank_phantom_total.html", context)
|
|
652
|
+
except Exception as e:
|
|
653
|
+
logger.exception(f"[鸣潮·排行卡] 声骸总排行渲染失败, 回退原图: {e}")
|
|
654
|
+
return await original(bot, ev, char, pages)
|
|
655
|
+
|
|
656
|
+
_bind(module, "draw_phantom_total_rank", draw_phantom_total_rank)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
PATCHES = {
|
|
660
|
+
"XutheringWavesUID": {
|
|
661
|
+
"wutheringwaves_rank.draw_all_rank_card": patch_all_rank,
|
|
662
|
+
"wutheringwaves_rank.draw_phantom_total_rank_card": patch_phantom_rank,
|
|
663
|
+
}
|
|
664
|
+
}
|