koishi-plugin-dwrgbwiki 1.0.0 → 1.2.0
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/index.js +151 -17
- package/lib.js +672 -8
- package/package.json +1 -1
- package/readme.md +76 -2
package/index.js
CHANGED
|
@@ -5,13 +5,30 @@ const lib = require('./lib')
|
|
|
5
5
|
|
|
6
6
|
const HELP = lib.buildHelp()
|
|
7
7
|
|
|
8
|
+
const MAIN_HELP = [
|
|
9
|
+
'第五人格BWIKI数据查询机器人',
|
|
10
|
+
'',
|
|
11
|
+
'支持指令:',
|
|
12
|
+
' 胜率 <角色名> [赛季] [周数] [排序要素] [段位]',
|
|
13
|
+
' 查询角色胜率,例:胜率 小女孩',
|
|
14
|
+
' 地图 <地图名> [赛季] [周数] [段位] [阵营]',
|
|
15
|
+
' 查询地图胜率,例:地图 军工厂',
|
|
16
|
+
' 模仿者 <角色名> [日期] [模式]',
|
|
17
|
+
' 查询模仿者游戏数据,例:模仿者 侦探',
|
|
18
|
+
' 勋章 <角色名> [日期] [模式]',
|
|
19
|
+
' 查询角色勋章分数线,例:勋章 小女孩',
|
|
20
|
+
'',
|
|
21
|
+
'发送“胜率”等单独指令可查看对应完整帮助',
|
|
22
|
+
'数据均根据第五人格官方小程序数据计算',
|
|
23
|
+
].join('\n')
|
|
24
|
+
|
|
8
25
|
module.exports = {
|
|
9
26
|
name: 'dwrgbwiki',
|
|
10
27
|
description: '第五人格BWIKI编辑组测试用组件,纯AI生成喵',
|
|
11
28
|
|
|
12
29
|
Config: Schema.object({
|
|
13
|
-
dataSource: Schema.
|
|
14
|
-
.description('
|
|
30
|
+
dataSource: Schema.union(lib.KNOWN_SOURCES)
|
|
31
|
+
.description('数据源地址,可在两个镜像站点之间切换')
|
|
15
32
|
.default('https://website.homeworkkun.top'),
|
|
16
33
|
tryAlternate: Schema.boolean()
|
|
17
34
|
.description('主数据源失败时自动尝试备用数据源')
|
|
@@ -19,6 +36,9 @@ module.exports = {
|
|
|
19
36
|
defaultPart: Schema.union(['8', '-127'])
|
|
20
37
|
.description('默认段位(8=巅七,-127=全段位)')
|
|
21
38
|
.default('8'),
|
|
39
|
+
mapDefaultCamp: Schema.union(['1', '2'])
|
|
40
|
+
.description('地图查询默认阵营(1=监管者,2=求生者)')
|
|
41
|
+
.default('1'),
|
|
22
42
|
showBadge: Schema.boolean()
|
|
23
43
|
.description('显示勋章分数据(获取失败时自动隐藏)')
|
|
24
44
|
.default(true),
|
|
@@ -26,8 +46,17 @@ module.exports = {
|
|
|
26
46
|
.description('勋章日增长率天数')
|
|
27
47
|
.default(7),
|
|
28
48
|
width: Schema.number().min(400).max(4000)
|
|
29
|
-
.description('图片宽度(px
|
|
49
|
+
.description('图片宽度(px),其他指令未单独设置时使用该值')
|
|
30
50
|
.default(1000),
|
|
51
|
+
mapWidth: Schema.number().min(400).max(4000)
|
|
52
|
+
.description('地图指令图片宽度(px),0=使用默认宽度')
|
|
53
|
+
.default(0),
|
|
54
|
+
mimicWidth: Schema.number().min(400).max(4000)
|
|
55
|
+
.description('模仿者指令图片宽度(px),0=使用默认宽度')
|
|
56
|
+
.default(0),
|
|
57
|
+
badgeWidth: Schema.number().min(400).max(4000)
|
|
58
|
+
.description('勋章指令图片宽度(px),0=使用默认宽度')
|
|
59
|
+
.default(0),
|
|
31
60
|
scale: Schema.number().min(1).max(4)
|
|
32
61
|
.description('截图倍率(越大越清晰)')
|
|
33
62
|
.default(2),
|
|
@@ -37,30 +66,135 @@ module.exports = {
|
|
|
37
66
|
}),
|
|
38
67
|
|
|
39
68
|
apply(ctx, config) {
|
|
69
|
+
ctx.command('BWIKI机器人', '显示当前支持的指令列表')
|
|
70
|
+
.action(async ({ session }) => {
|
|
71
|
+
return helpImage(ctx, config, MAIN_HELP)
|
|
72
|
+
})
|
|
73
|
+
|
|
40
74
|
ctx.command('胜率 <角色名> [条件:text]', '查询角色胜率并生成图片')
|
|
41
75
|
.usage(HELP)
|
|
42
76
|
.example('胜率 小女孩')
|
|
43
77
|
.example('胜率 小女孩 第43赛季 第3周 使用率')
|
|
44
78
|
.example('胜率 小女孩 43 3 胜率')
|
|
45
79
|
.action(async ({ session }, name, cond) => {
|
|
46
|
-
if (!name || !name.trim()) return HELP
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
if (!name || !name.trim()) return helpImage(ctx, config, HELP)
|
|
81
|
+
return run(ctx, config, lib.collect, lib.buildHtml, name.trim(), cond, 'width')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
ctx.command('地图 <地图名> [条件:text]', '查询地图胜率并生成图片')
|
|
85
|
+
.usage(MAP_HELP)
|
|
86
|
+
.example('地图 军工厂')
|
|
87
|
+
.example('地图 红教堂 S44 3 求生者')
|
|
88
|
+
.example('地图 月亮河公园 全赛季')
|
|
89
|
+
.action(async ({ session }, name, cond) => {
|
|
90
|
+
if (!name || !name.trim()) return helpImage(ctx, config, MAP_HELP)
|
|
91
|
+
return run(ctx, config, lib.collectMap, lib.buildMapHtml, name.trim(), cond, 'mapWidth')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
ctx.command('模仿者 <角色名> [条件:text]', '查询模仿者游戏角色数据并生成图片')
|
|
95
|
+
.usage(MIMIC_HELP)
|
|
96
|
+
.example('模仿者 侦探')
|
|
97
|
+
.example('模仿者 药剂师 260531 低级场')
|
|
98
|
+
.action(async ({ session }, name, cond) => {
|
|
99
|
+
if (!name || !name.trim()) return helpImage(ctx, config, MIMIC_HELP)
|
|
100
|
+
return run(ctx, config, lib.collectMimic, lib.buildMimicHtml, name.trim(), cond, 'mimicWidth')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
ctx.command('勋章 <角色名> [条件:text]', '查询角色勋章分数线并生成图片')
|
|
104
|
+
.usage(BADGE_HELP)
|
|
105
|
+
.example('勋章 小女孩')
|
|
106
|
+
.example('勋章 小女孩 20250917 7日增长率')
|
|
107
|
+
.example('勋章 小女孩 赛季同比增长率')
|
|
108
|
+
.action(async ({ session }, name, cond) => {
|
|
109
|
+
if (!name || !name.trim()) return helpImage(ctx, config, BADGE_HELP)
|
|
110
|
+
return run(ctx, config, lib.collectBadge, lib.buildBadgeHtml, name.trim(), cond, 'badgeWidth')
|
|
60
111
|
})
|
|
61
112
|
},
|
|
62
113
|
}
|
|
63
114
|
|
|
115
|
+
async function run(ctx, config, collectFn, buildFn, name, cond, widthKey) {
|
|
116
|
+
if (!ctx.puppeteer) {
|
|
117
|
+
return '未检测到 puppeteer 服务,请先安装并启用 koishi-plugin-puppeteer'
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const result = await collectFn(config, name, cond)
|
|
121
|
+
if (!result.ok) return errorImage(ctx, config, result.error)
|
|
122
|
+
const width = (config[widthKey] && config[widthKey] > 0) ? config[widthKey] : config.width
|
|
123
|
+
const html = buildFn(result, { ...config, avatarBase: result.base, width })
|
|
124
|
+
const image = await capture(ctx, html, { ...config, width })
|
|
125
|
+
return [h.image(image)]
|
|
126
|
+
} catch (e) {
|
|
127
|
+
ctx.logger('dwrgbwiki').warn(e)
|
|
128
|
+
return errorImage(ctx, config, '查询失败:' + (e && e.message ? e.message : e))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function helpImage(ctx, config, text) {
|
|
133
|
+
if (!ctx.puppeteer) return text
|
|
134
|
+
try {
|
|
135
|
+
const html = lib.buildHelpHtml(text, { ...config, width: config.width })
|
|
136
|
+
const image = await capture(ctx, html, config)
|
|
137
|
+
return [h.image(image)]
|
|
138
|
+
} catch (e) {
|
|
139
|
+
ctx.logger('dwrgbwiki').warn(e)
|
|
140
|
+
return text
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function errorImage(ctx, config, text) {
|
|
145
|
+
if (!ctx.puppeteer || !text) return text
|
|
146
|
+
try {
|
|
147
|
+
const html = lib.buildHelpHtml(text, { ...config, width: config.width })
|
|
148
|
+
const image = await capture(ctx, html, config)
|
|
149
|
+
return [h.image(image)]
|
|
150
|
+
} catch (e) {
|
|
151
|
+
ctx.logger('dwrgbwiki').warn(e)
|
|
152
|
+
return text
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const MAP_HELP = [
|
|
157
|
+
'查询地图胜率并生成图片',
|
|
158
|
+
'',
|
|
159
|
+
'用法:地图 <地图名> [赛季] [周数] [段位] [阵营]',
|
|
160
|
+
'',
|
|
161
|
+
'示例:',
|
|
162
|
+
' 地图 军工厂',
|
|
163
|
+
' 地图 红教堂 S44 3 求生者',
|
|
164
|
+
' 地图 月亮河公园 全赛季 全段位',
|
|
165
|
+
'',
|
|
166
|
+
'阵营:监管者(默认)、求生者;赛季:S44 / 第44赛季;周数:第3周 / 3',
|
|
167
|
+
'段位:巅七(默认)、七阶、六阶、五阶、四阶、三阶、二阶、一阶、全段位',
|
|
168
|
+
'赛季、周数不填默认使用最新赛季最新周',
|
|
169
|
+
].join('\n')
|
|
170
|
+
|
|
171
|
+
const MIMIC_HELP = [
|
|
172
|
+
'查询模仿者游戏角色数据并生成图片',
|
|
173
|
+
'',
|
|
174
|
+
'用法:模仿者 <角色名> [日期] [模式]',
|
|
175
|
+
'',
|
|
176
|
+
'示例:',
|
|
177
|
+
' 模仿者 侦探',
|
|
178
|
+
' 模仿者 药剂师 260531 低级场',
|
|
179
|
+
'',
|
|
180
|
+
'日期:YYMMDD(如 260531),不填默认最新日期',
|
|
181
|
+
'模式:低级场、10人高级场、12人高级场、所有模式(默认)',
|
|
182
|
+
].join('\n')
|
|
183
|
+
|
|
184
|
+
const BADGE_HELP = [
|
|
185
|
+
'查询角色勋章分数线并生成图片',
|
|
186
|
+
'',
|
|
187
|
+
'用法:勋章 <角色名> [日期] [模式]',
|
|
188
|
+
'',
|
|
189
|
+
'示例:',
|
|
190
|
+
' 勋章 小女孩',
|
|
191
|
+
' 勋章 小女孩 20250917 7日增长率',
|
|
192
|
+
' 勋章 小女孩 赛季同比增长率',
|
|
193
|
+
'',
|
|
194
|
+
'日期:YYYYMMDD(如 20250917),不填默认最新日期',
|
|
195
|
+
'模式:勋章分数(默认)、距平百分比、1/3/7/15/30日增长率、赛季同比增长率',
|
|
196
|
+
].join('\n')
|
|
197
|
+
|
|
64
198
|
async function capture(ctx, html, config) {
|
|
65
199
|
const page = await ctx.puppeteer.page()
|
|
66
200
|
try {
|
package/lib.js
CHANGED
|
@@ -30,6 +30,24 @@ const PART_ORDER = [8, 7, 6, 5, 4, 3, 2, 1]
|
|
|
30
30
|
|
|
31
31
|
const RATE_METRICS = ['win_rate_without_draw', 'win_rate', 'ping_rate', 'lose_rate', 'use_rate', 'ban_rate']
|
|
32
32
|
|
|
33
|
+
const MAP_CAMPS = { 1: '监管者', 2: '求生者' }
|
|
34
|
+
const MAP_CAMP_LABELS = Object.keys(MAP_CAMPS).reduce((m, k) => { m[MAP_CAMPS[k]] = k; return m }, {})
|
|
35
|
+
|
|
36
|
+
const MIMIC_MODES = { '67': '低级场', '68': '10人高级场', '73': '12人高级场', 'all_mode': '所有模式' }
|
|
37
|
+
const MIMIC_MODE_LABELS = Object.keys(MIMIC_MODES).reduce((m, k) => { m[MIMIC_MODES[k]] = k; return m }, {})
|
|
38
|
+
|
|
39
|
+
const BADGE_MODES = {
|
|
40
|
+
original: '勋章分数',
|
|
41
|
+
anomaly: '距平百分比',
|
|
42
|
+
'1d': '1日增长率',
|
|
43
|
+
'3d': '3日增长率',
|
|
44
|
+
'7d': '7日增长率',
|
|
45
|
+
'15d': '15日增长率',
|
|
46
|
+
'30d': '30日增长率',
|
|
47
|
+
season: '赛季同比增长率',
|
|
48
|
+
}
|
|
49
|
+
const BADGE_MODE_LABELS = Object.keys(BADGE_MODES).reduce((m, k) => { m[BADGE_MODES[k]] = k; return m }, {})
|
|
50
|
+
|
|
33
51
|
const METRIC_LABELS = {}
|
|
34
52
|
Object.keys(METRICS).forEach(k => { METRIC_LABELS[METRICS[k]] = k })
|
|
35
53
|
|
|
@@ -60,7 +78,7 @@ function winRateNoDraw(w, p) {
|
|
|
60
78
|
|
|
61
79
|
function getMetricVal(item, metric, badgeScores) {
|
|
62
80
|
if (metric.startsWith('badge_')) {
|
|
63
|
-
const tier = metric.slice(
|
|
81
|
+
const tier = metric.slice(6)
|
|
64
82
|
const s = badgeScores && badgeScores[item.name] ? badgeScores[item.name][tier] : null
|
|
65
83
|
return s == null || isNaN(s) ? null : s
|
|
66
84
|
}
|
|
@@ -292,6 +310,125 @@ async function loadBadgeAll(base, config, meta, dataFile) {
|
|
|
292
310
|
}
|
|
293
311
|
}
|
|
294
312
|
|
|
313
|
+
// ===== 地图数据 =====
|
|
314
|
+
|
|
315
|
+
async function loadMapData(base, timeout) {
|
|
316
|
+
const files = await fetchJson(url(base, '/胜率数据/map/mapdata.json'), timeout)
|
|
317
|
+
if (!Array.isArray(files)) throw new Error('地图文件列表解析失败')
|
|
318
|
+
const all = {}
|
|
319
|
+
for (const f of files) {
|
|
320
|
+
if (!/^MAP_S\d+\.json$/.test(f)) continue
|
|
321
|
+
let j
|
|
322
|
+
try { j = await fetchJson(url(base, '/胜率数据/map/' + f), timeout) } catch (e) { continue }
|
|
323
|
+
;(j.data || []).forEach(item => {
|
|
324
|
+
const season = 'S' + item.season
|
|
325
|
+
const isFullSeason = item.week_num === null || item.week_num == -127
|
|
326
|
+
const weekKey = isFullSeason ? 'full_season' : 'W' + item.week_num
|
|
327
|
+
const partKey = String(item.part) === '-127' ? 'all_tiers' : 'tier' + item.part
|
|
328
|
+
const campId = String(item.camp_id)
|
|
329
|
+
const spaceName = item.space_name
|
|
330
|
+
if (!spaceName) return
|
|
331
|
+
if (!all[season]) all[season] = { end_time: '' }
|
|
332
|
+
if (!all[season][weekKey]) all[season][weekKey] = {}
|
|
333
|
+
if (isFullSeason && item.end_time && item.end_time > all[season].end_time) all[season].end_time = item.end_time
|
|
334
|
+
if (!all[season][weekKey][partKey]) all[season][weekKey][partKey] = {}
|
|
335
|
+
if (!all[season][weekKey][partKey][campId]) all[season][weekKey][partKey][campId] = {}
|
|
336
|
+
const slot = all[season][weekKey][partKey][campId]
|
|
337
|
+
if (!slot[spaceName]) slot[spaceName] = {
|
|
338
|
+
win_rate: { sum: 0, count: 0 }, ping_rate: { sum: 0, count: 0 },
|
|
339
|
+
fail_rate: { sum: 0, count: 0 }, space_no: item.space_no,
|
|
340
|
+
}
|
|
341
|
+
const s = slot[spaceName]
|
|
342
|
+
s.win_rate.sum += item.win_rate || 0; s.win_rate.count++
|
|
343
|
+
s.ping_rate.sum += item.ping_rate || 0; s.ping_rate.count++
|
|
344
|
+
s.fail_rate.sum += item.fail_rate || 0; s.fail_rate.count++
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
const out = {}
|
|
348
|
+
Object.keys(all).forEach(season => {
|
|
349
|
+
out[season] = { end_time: all[season].end_time }
|
|
350
|
+
Object.keys(all[season]).forEach(weekKey => {
|
|
351
|
+
if (weekKey === 'end_time') return
|
|
352
|
+
out[season][weekKey] = {}
|
|
353
|
+
Object.keys(all[season][weekKey]).forEach(partKey => {
|
|
354
|
+
out[season][weekKey][partKey] = {}
|
|
355
|
+
Object.keys(all[season][weekKey][partKey]).forEach(campId => {
|
|
356
|
+
const camp = {}
|
|
357
|
+
Object.keys(all[season][weekKey][partKey][campId]).forEach(mapName => {
|
|
358
|
+
const s = all[season][weekKey][partKey][campId][mapName]
|
|
359
|
+
camp[mapName] = {
|
|
360
|
+
win_rate: s.win_rate.count ? s.win_rate.sum / s.win_rate.count : null,
|
|
361
|
+
ping_rate: s.ping_rate.count ? s.ping_rate.sum / s.ping_rate.count : null,
|
|
362
|
+
fail_rate: s.fail_rate.count ? s.fail_rate.sum / s.fail_rate.count : null,
|
|
363
|
+
space_no: s.space_no,
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
out[season][weekKey][partKey][campId] = camp
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
return out
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function parseMapArgs(rest) {
|
|
375
|
+
const tokens = String(rest || '').trim().split(/\s+/).filter(Boolean)
|
|
376
|
+
const r = { season: null, week: null, part: null, camp: null }
|
|
377
|
+
tokens.forEach(t => {
|
|
378
|
+
let m
|
|
379
|
+
if ((m = t.match(/^s\s*(\d{1,3})\s*赛季$/i))) { r.season = 'S' + m[1]; return }
|
|
380
|
+
if ((m = t.match(/^s\s*(\d{1,3})$/i))) { r.season = 'S' + m[1]; return }
|
|
381
|
+
if ((m = t.match(/^第?\s*(\d{1,3})\s*赛季$/i))) { r.season = 'S' + m[1]; return }
|
|
382
|
+
if ((m = t.match(/^第?\s*(\d{1,2})\s*周$/))) { r.week = m[1]; return }
|
|
383
|
+
if (MAP_CAMP_LABELS[t]) { r.camp = MAP_CAMP_LABELS[t]; return }
|
|
384
|
+
if ((m = t.match(/^([12])$/))) { r.camp = m[1]; return }
|
|
385
|
+
const pm = Object.keys(PARTS).find(k => PARTS[k] === t)
|
|
386
|
+
if (pm != null) { r.part = parseInt(pm); return }
|
|
387
|
+
if ((m = t.match(/^(\d{1,2})$/))) { r.week = m[1]; return }
|
|
388
|
+
})
|
|
389
|
+
return r
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ===== 模仿者游戏数据 =====
|
|
393
|
+
|
|
394
|
+
async function loadMimicFileList(base, timeout) {
|
|
395
|
+
const text = await fetchText(url(base, '/胜率数据/模仿者游戏/fileList.js'), timeout)
|
|
396
|
+
const m = text.match(/\[([\s\S]*)\]/)
|
|
397
|
+
if (!m) return []
|
|
398
|
+
let files = []
|
|
399
|
+
try { files = new Function('return [' + m[1] + ']')() } catch (e) { return [] }
|
|
400
|
+
return files.filter(f => typeof f === 'string' && /^data_\d{6}\.json$/.test(f)).sort()
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function loadMimicConfig(base, timeout) {
|
|
404
|
+
const j = await fetchJson(url(base, '/胜率数据/模仿者游戏/config.json'), timeout)
|
|
405
|
+
const nameToId = {}
|
|
406
|
+
Object.keys(j || {}).forEach(id => { const n = String(j[id]); if (n) nameToId[n] = id })
|
|
407
|
+
return { idToName: j || {}, nameToId }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function parseMimicArgs(rest) {
|
|
411
|
+
const tokens = String(rest || '').trim().split(/\s+/).filter(Boolean)
|
|
412
|
+
const r = { date: null, mode: null }
|
|
413
|
+
tokens.forEach(t => {
|
|
414
|
+
if (/^\d{6}$/.test(t)) { r.date = t; return }
|
|
415
|
+
if (MIMIC_MODE_LABELS[t]) { r.mode = MIMIC_MODE_LABELS[t]; return }
|
|
416
|
+
})
|
|
417
|
+
return r
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ===== 勋章数据 =====
|
|
421
|
+
|
|
422
|
+
function parseBadgeArgs(rest) {
|
|
423
|
+
const tokens = String(rest || '').trim().split(/\s+/).filter(Boolean)
|
|
424
|
+
const r = { date: null, mode: 'original' }
|
|
425
|
+
tokens.forEach(t => {
|
|
426
|
+
if (/^\d{8}$/.test(t)) { r.date = t; return }
|
|
427
|
+
if (BADGE_MODE_LABELS[t] != null) { r.mode = BADGE_MODE_LABELS[t]; return }
|
|
428
|
+
})
|
|
429
|
+
return r
|
|
430
|
+
}
|
|
431
|
+
|
|
295
432
|
// ===== 数据计算 =====
|
|
296
433
|
|
|
297
434
|
function uniqueBy(data, camp, part) {
|
|
@@ -396,6 +533,7 @@ function computeCard(opts) {
|
|
|
396
533
|
return {
|
|
397
534
|
entry, camp, part,
|
|
398
535
|
rank: rank[entry.name] || 0, total,
|
|
536
|
+
ranking: sorted,
|
|
399
537
|
six, allRanks, changeRanks, partsGrid, prevEntry,
|
|
400
538
|
}
|
|
401
539
|
}
|
|
@@ -447,6 +585,241 @@ function findSimilar(data, name) {
|
|
|
447
585
|
return names.filter(n => n.includes(name) || name.includes(n)).slice(0, 8)
|
|
448
586
|
}
|
|
449
587
|
|
|
588
|
+
// ===== 地图查询主流程 =====
|
|
589
|
+
|
|
590
|
+
async function collectMap(config, mapQuery, cond) {
|
|
591
|
+
const parsed = parseMapArgs(cond)
|
|
592
|
+
const src = await resolveSource(config)
|
|
593
|
+
const { base, meta } = src
|
|
594
|
+
const sourceLabel = base.replace(/^https?:\/\//, '')
|
|
595
|
+
|
|
596
|
+
const mapData = await loadMapData(base, config.timeout)
|
|
597
|
+
const seasons = Object.keys(mapData).filter(s => /^S\d+$/.test(s))
|
|
598
|
+
.sort((a, b) => parseInt(a.substring(1)) - parseInt(b.substring(1)))
|
|
599
|
+
if (!seasons.length) return { ok: false, error: '地图数据为空' }
|
|
600
|
+
|
|
601
|
+
const seasonName = parsed.season && mapData[parsed.season] ? parsed.season : seasons[seasons.length - 1]
|
|
602
|
+
const seasonObj = mapData[seasonName]
|
|
603
|
+
const weekKeys = Object.keys(seasonObj).filter(w => w !== 'end_time')
|
|
604
|
+
|
|
605
|
+
let weekKey = 'full_season'
|
|
606
|
+
if (parsed.week != null) {
|
|
607
|
+
const wk = 'W' + parsed.week
|
|
608
|
+
if (!weekKeys.includes(wk)) return { ok: false, error: seasonName + ' 无第' + parsed.week + '周地图数据' }
|
|
609
|
+
weekKey = wk
|
|
610
|
+
} else {
|
|
611
|
+
const ws = weekKeys.filter(w => /^W\d+$/.test(w))
|
|
612
|
+
.sort((a, b) => parseInt(a.substring(1)) - parseInt(b.substring(1)))
|
|
613
|
+
if (ws.length) weekKey = ws[ws.length - 1]
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const part = parsed.part != null ? parsed.part : parseInt(config.defaultPart)
|
|
617
|
+
const partKey = part === -127 ? 'all_tiers' : 'tier' + part
|
|
618
|
+
const camp = parsed.camp != null ? parseInt(parsed.camp) : (config.mapDefaultCamp ? parseInt(config.mapDefaultCamp) : 1)
|
|
619
|
+
|
|
620
|
+
const slot = seasonObj[weekKey] && seasonObj[weekKey][partKey] && seasonObj[weekKey][partKey][camp]
|
|
621
|
+
if (!slot || !Object.keys(slot).length) {
|
|
622
|
+
return { ok: false, error: seasonName + ' ' + (weekKey === 'full_season' ? '全赛季' : '第' + weekKey.substring(1) + '周') + ' ' + (PARTS[part] || part) + ' ' + (MAP_CAMPS[camp] || camp) + ' 暂无地图数据' }
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const mapNames = Object.keys(slot)
|
|
626
|
+
const exact = mapNames.find(n => n === mapQuery)
|
|
627
|
+
const match = exact || mapNames.find(n => n.includes(mapQuery) || mapQuery.includes(n))
|
|
628
|
+
if (!match) {
|
|
629
|
+
return { ok: false, error: '未找到地图「' + mapQuery + '」\n相近地图:' + mapNames.slice(0, 8).join('、') }
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const cur = slot[match]
|
|
633
|
+
const nodraw = (cur.win_rate != null && cur.fail_rate != null && cur.win_rate + cur.fail_rate > 0)
|
|
634
|
+
? cur.win_rate / (cur.win_rate + cur.fail_rate) * 100 : null
|
|
635
|
+
|
|
636
|
+
let prev = null
|
|
637
|
+
const seasonNum = parseInt(seasonName.substring(1))
|
|
638
|
+
const prevSeason = 'S' + (seasonNum - 1)
|
|
639
|
+
if (mapData[prevSeason]) {
|
|
640
|
+
const prevSlot = mapData[prevSeason][weekKey] && mapData[prevSeason][weekKey][partKey] && mapData[prevSeason][weekKey][partKey][camp]
|
|
641
|
+
const p = prevSlot && prevSlot[match]
|
|
642
|
+
if (p) {
|
|
643
|
+
const pn = (p.win_rate != null && p.fail_rate != null && p.win_rate + p.fail_rate > 0)
|
|
644
|
+
? p.win_rate / (p.win_rate + p.fail_rate) * 100 : null
|
|
645
|
+
prev = { win_rate: p.win_rate, ping_rate: p.ping_rate, fail_rate: p.fail_rate, win_rate_nodraw: pn }
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
ok: true, base, sourceLabel,
|
|
651
|
+
mapName: match, spaceNo: cur.space_no,
|
|
652
|
+
seasonName,
|
|
653
|
+
weekLabel: weekKey === 'full_season' ? '全赛季' : '第' + weekKey.substring(1) + '周',
|
|
654
|
+
partName: PARTS[part] || String(part),
|
|
655
|
+
campName: MAP_CAMPS[camp] || String(camp),
|
|
656
|
+
prevLabel: prev ? prevSeason + ' 同周' : null,
|
|
657
|
+
win_rate: cur.win_rate, ping_rate: cur.ping_rate, fail_rate: cur.fail_rate,
|
|
658
|
+
win_rate_nodraw: nodraw,
|
|
659
|
+
prev,
|
|
660
|
+
endTime: seasonObj.end_time || null,
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ===== 模仿者查询主流程 =====
|
|
665
|
+
|
|
666
|
+
async function collectMimic(config, name, cond) {
|
|
667
|
+
const parsed = parseMimicArgs(cond)
|
|
668
|
+
const src = await resolveSource(config)
|
|
669
|
+
const { base, meta } = src
|
|
670
|
+
const sourceLabel = base.replace(/^https?:\/\//, '')
|
|
671
|
+
|
|
672
|
+
const files = await loadMimicFileList(base, config.timeout)
|
|
673
|
+
if (!files.length) return { ok: false, error: '模仿者数据文件为空' }
|
|
674
|
+
|
|
675
|
+
let file
|
|
676
|
+
if (parsed.date) {
|
|
677
|
+
file = files.find(f => f.includes(parsed.date))
|
|
678
|
+
if (!file) {
|
|
679
|
+
return { ok: false, error: '未找到日期 ' + parsed.date + ' 的模仿者数据(可用 ' + files[0].substring(5, 11) + ' ~ ' + files[files.length - 1].substring(5, 11) + ')' }
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
file = files[files.length - 1]
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const j = await fetchJson(url(base, '/胜率数据/模仿者游戏/json/' + file), config.timeout)
|
|
686
|
+
const allPlayerData = j.all_player_data || {}
|
|
687
|
+
const cfg = await loadMimicConfig(base, config.timeout)
|
|
688
|
+
|
|
689
|
+
const ids = Object.keys(cfg.idToName)
|
|
690
|
+
const exact = ids.find(id => cfg.idToName[id] === name)
|
|
691
|
+
let charId = exact
|
|
692
|
+
if (!charId) {
|
|
693
|
+
const fuzzy = ids.filter(id => cfg.idToName[id].includes(name) || name.includes(cfg.idToName[id]))
|
|
694
|
+
if (fuzzy.length === 1) charId = fuzzy[0]
|
|
695
|
+
else if (fuzzy.length > 1) {
|
|
696
|
+
return { ok: false, error: '角色名「' + name + '」匹配到多个角色:' + fuzzy.map(id => cfg.idToName[id]).join('、') }
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (!charId) {
|
|
700
|
+
return { ok: false, error: '未找到模仿者角色「' + name + '」\n可尝试:' + ids.map(id => cfg.idToName[id]).slice(0, 12).join('、') }
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const campId = parseInt(charId)
|
|
704
|
+
const campName = campId < 200 ? '侦探团' : (campId < 300 ? '模仿者' : '中立')
|
|
705
|
+
|
|
706
|
+
const modes = []
|
|
707
|
+
Object.keys(MIMIC_MODES).forEach(modeKey => {
|
|
708
|
+
const md = allPlayerData[modeKey] && allPlayerData[modeKey][charId]
|
|
709
|
+
if (md) modes.push({ key: modeKey, label: MIMIC_MODES[modeKey], ...md })
|
|
710
|
+
})
|
|
711
|
+
if (!modes.length) return { ok: false, error: '角色「' + cfg.idToName[charId] + '」在该日期无数据' }
|
|
712
|
+
|
|
713
|
+
let primary = modes
|
|
714
|
+
let modeLabel = '所有模式'
|
|
715
|
+
if (parsed.mode) {
|
|
716
|
+
primary = modes.filter(m => m.key === parsed.mode)
|
|
717
|
+
if (!primary.length) return { ok: false, error: '模式「' + (MIMIC_MODES[parsed.mode] || parsed.mode) + '」无数据' }
|
|
718
|
+
modeLabel = MIMIC_MODES[parsed.mode]
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
ok: true, base, sourceLabel, file,
|
|
723
|
+
name: cfg.idToName[charId], charId,
|
|
724
|
+
campName, campId,
|
|
725
|
+
dateLabel: '20' + file.substring(5, 7) + '.' + file.substring(7, 9) + '.' + file.substring(9, 11),
|
|
726
|
+
modes, primary: primary[0], modeLabel,
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ===== 勋章查询主流程 =====
|
|
731
|
+
|
|
732
|
+
async function collectBadge(config, name, cond) {
|
|
733
|
+
const parsed = parseBadgeArgs(cond)
|
|
734
|
+
const src = await resolveSource(config)
|
|
735
|
+
const { base, meta } = src
|
|
736
|
+
const sourceLabel = base.replace(/^https?:\/\//, '')
|
|
737
|
+
|
|
738
|
+
const files = await loadBadgeFiles(base, config.timeout)
|
|
739
|
+
if (!files.length) return { ok: false, error: '勋章数据文件为空' }
|
|
740
|
+
|
|
741
|
+
let curDate = parsed.date
|
|
742
|
+
if (!curDate) {
|
|
743
|
+
curDate = files[files.length - 1].replace('Badge_', '').replace('.json', '')
|
|
744
|
+
}
|
|
745
|
+
const curFile = 'Badge_' + curDate + '.json'
|
|
746
|
+
if (!files.includes(curFile)) {
|
|
747
|
+
return { ok: false, error: '未找到日期 ' + curDate + ' 的勋章数据(最新 ' + files[files.length - 1].replace('Badge_', '').replace('.json', '') + ')' }
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const cur = await loadBadgeMap(base, curFile, config.timeout)
|
|
751
|
+
const campNames = ['', '监管者', '求生者']
|
|
752
|
+
|
|
753
|
+
const names = Object.keys(cur.map)
|
|
754
|
+
const exact = names.find(n => n === name)
|
|
755
|
+
let heroName = exact
|
|
756
|
+
if (!heroName) {
|
|
757
|
+
const fuzzy = names.filter(n => n.includes(name) || name.includes(n))
|
|
758
|
+
if (fuzzy.length === 1) heroName = fuzzy[0]
|
|
759
|
+
else if (fuzzy.length > 1) {
|
|
760
|
+
return { ok: false, error: '角色名「' + name + '」匹配到多个角色:' + fuzzy.join('、') }
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (!heroName) return { ok: false, error: '未找到角色「' + name + '」\n相近角色:' + names.slice(0, 8).join('、') }
|
|
764
|
+
|
|
765
|
+
const campId = parseInt(cur.camps[heroName]) || 2
|
|
766
|
+
const curScores = cur.map[heroName] || {}
|
|
767
|
+
|
|
768
|
+
const delta = {}
|
|
769
|
+
let prevDate = null
|
|
770
|
+
if (parsed.mode === 'anomaly') {
|
|
771
|
+
BADGE_TIERS.forEach(t => {
|
|
772
|
+
const cv = curScores[t.key]
|
|
773
|
+
let sum = 0, cnt = 0
|
|
774
|
+
Object.keys(cur.map).forEach(nm => {
|
|
775
|
+
if (cur.camps[nm] !== String(campId)) return
|
|
776
|
+
const v = cur.map[nm][t.key]
|
|
777
|
+
if (v) { sum += v; cnt++ }
|
|
778
|
+
})
|
|
779
|
+
const avg = cnt ? sum / cnt : null
|
|
780
|
+
delta[t.key] = avg > 0 && cv != null ? (cv - avg) / avg * 100 : null
|
|
781
|
+
})
|
|
782
|
+
} else if (parsed.mode === 'season') {
|
|
783
|
+
prevDate = seasonComparisonDate(parseInt(curDate), meta.seasonList)
|
|
784
|
+
const prevFile = prevDate ? pickBadgeFile(files, prevDate, 30) : null
|
|
785
|
+
if (prevFile) {
|
|
786
|
+
const prev = await loadBadgeMap(base, prevFile, config.timeout)
|
|
787
|
+
const p = prev.map[heroName] || {}
|
|
788
|
+
BADGE_TIERS.forEach(t => {
|
|
789
|
+
delta[t.key] = p[t.key] > 0 && curScores[t.key] != null ? (curScores[t.key] - p[t.key]) / p[t.key] * 100 : null
|
|
790
|
+
})
|
|
791
|
+
prevDate = prevFile.replace('Badge_', '').replace('.json', '')
|
|
792
|
+
}
|
|
793
|
+
} else if (/^\d+d$/.test(parsed.mode)) {
|
|
794
|
+
const days = parseInt(parsed.mode)
|
|
795
|
+
const prevTarget = dateAddDays(parseInt(curDate), -days)
|
|
796
|
+
const prevFile = pickBadgeFile(files, prevTarget, 3)
|
|
797
|
+
if (prevFile) {
|
|
798
|
+
const prev = await loadBadgeMap(base, prevFile, config.timeout)
|
|
799
|
+
const p = prev.map[heroName] || {}
|
|
800
|
+
BADGE_TIERS.forEach(t => {
|
|
801
|
+
delta[t.key] = p[t.key] > 0 && curScores[t.key] != null ? (curScores[t.key] - p[t.key]) / p[t.key] * 100 : null
|
|
802
|
+
})
|
|
803
|
+
prevDate = prevFile.replace('Badge_', '').replace('.json', '')
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const fmtDate = d => d ? String(d).substring(0, 4) + '.' + String(d).substring(4, 6) + '.' + String(d).substring(6, 8) : null
|
|
808
|
+
|
|
809
|
+
return {
|
|
810
|
+
ok: true, base, sourceLabel, file: curFile,
|
|
811
|
+
name: heroName, campName: campNames[campId] || '未知', campId,
|
|
812
|
+
dateLabel: fmtDate(curDate),
|
|
813
|
+
mode: parsed.mode, modeLabel: BADGE_MODES[parsed.mode] || parsed.mode,
|
|
814
|
+
tiers: BADGE_TIERS.map(t => ({
|
|
815
|
+
label: t.label, cls: t.cls,
|
|
816
|
+
val: curScores[t.key] != null ? curScores[t.key] : null,
|
|
817
|
+
delta: delta[t.key] != null ? delta[t.key] : null,
|
|
818
|
+
})),
|
|
819
|
+
prevDate: fmtDate(prevDate),
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
450
823
|
// ===== 主流程 =====
|
|
451
824
|
|
|
452
825
|
async function collect(config, name, cond) {
|
|
@@ -531,7 +904,6 @@ function buildHtml(data, config) {
|
|
|
531
904
|
|
|
532
905
|
const hl = getMetricVal(card.entry, metric, data.badgeScores)
|
|
533
906
|
const hlCls = metric.startsWith('badge_') ? 'plain' : (hl == null ? '' : (hl >= 0.5 ? 'high' : (hl >= 0.3 ? 'mid' : 'low')))
|
|
534
|
-
const rankSup = '#' + card.rank + '/' + card.total
|
|
535
907
|
|
|
536
908
|
const sixRows = card.six.map(m => {
|
|
537
909
|
const rk = card.allRanks[m.key][card.entry.name]
|
|
@@ -568,7 +940,8 @@ function buildHtml(data, config) {
|
|
|
568
940
|
const zoneLeft = '<div class="stat-block">' + sixRows + '</div>' + changeBlock +
|
|
569
941
|
'<div class="zone-sep"></div>' +
|
|
570
942
|
'<div class="stat-block"><div class="comp-title" style="color:var(--accent);margin-bottom:4px">各段位去平胜率(距平均)</div>' +
|
|
571
|
-
'<div class="allrank-grid">' + partsItems + '</div></div>'
|
|
943
|
+
'<div class="allrank-grid">' + partsItems + '</div></div>' +
|
|
944
|
+
'<div class="credit">@夜莺小姐档案馆</div>'
|
|
572
945
|
|
|
573
946
|
let zoneRight
|
|
574
947
|
if (badge) {
|
|
@@ -588,9 +961,11 @@ function buildHtml(data, config) {
|
|
|
588
961
|
'<div class="card-badge-tiers">' + tierItems + '</div></div>' +
|
|
589
962
|
growBlock('赛季同比增长 (' + short(badge.dates.prev) + '→' + short(badge.dates.cur) + ')', growItems(badge.season)) +
|
|
590
963
|
growBlock(badgeDays + '日增长率 (' + short(badge.dates.daily) + '→' + short(badge.dates.cur) + ')', growItems(badge.daily)) +
|
|
591
|
-
growBlock('距阵营平均值', growItems(badge.anomaly))
|
|
964
|
+
growBlock('距阵营平均值', growItems(badge.anomaly)) +
|
|
965
|
+
'<div class="cmd-hint">' + esc(buildHelp()) + '</div>'
|
|
592
966
|
} else {
|
|
593
|
-
zoneRight = '<div class="stat-block"><div class="comp-title" style="color:var(--text-secondary)">暂无勋章数据</div></div>'
|
|
967
|
+
zoneRight = '<div class="stat-block"><div class="comp-title" style="color:var(--text-secondary)">暂无勋章数据</div></div>' +
|
|
968
|
+
'<div class="cmd-hint">' + esc(buildHelp()) + '</div>'
|
|
594
969
|
}
|
|
595
970
|
|
|
596
971
|
const position = card.entry.position
|
|
@@ -598,6 +973,14 @@ function buildHtml(data, config) {
|
|
|
598
973
|
const avatarUrl = avatar ? avatar.replace(/\/+$/, '') + '/statics/角色头像/' + encodeURIComponent(name) + '.png' : ''
|
|
599
974
|
const campName = card.camp === 1 ? '监管者' : '求生者'
|
|
600
975
|
|
|
976
|
+
const rankRows = buildRankRows(card, metric, data.badgeScores)
|
|
977
|
+
const rankBlock = rankRows.length
|
|
978
|
+
? '<div class="zone-sep"></div>' +
|
|
979
|
+
'<div class="comp-title" style="color:var(--accent);margin-bottom:4px">本阵营排名(' + metricLabel + ')</div>' +
|
|
980
|
+
'<div class="rank-table"><div class="rank-row rank-header"><span class="rk-pos">排名</span><span class="rk-name">角色名</span><span class="rk-val">数据</span></div>' +
|
|
981
|
+
rankRows.join('') + '</div>'
|
|
982
|
+
: ''
|
|
983
|
+
|
|
601
984
|
const zoneCenter =
|
|
602
985
|
'<div class="card-avatar-wrap">' +
|
|
603
986
|
(avatarUrl
|
|
@@ -607,8 +990,9 @@ function buildHtml(data, config) {
|
|
|
607
990
|
'</div>' +
|
|
608
991
|
'<div class="card-char-name">' + esc(name) + '</div>' +
|
|
609
992
|
(position ? '<div class="card-sub">' + esc(position) + ' · ' + campName + '</div>' : '') +
|
|
610
|
-
'<div class="card-highlight ' + hlCls + '">' + fmtHigh(hl, metric) + '
|
|
611
|
-
'<div class="card-highlight-label">' + metricLabel + ' · ' + partName + '</div>'
|
|
993
|
+
'<div class="card-highlight ' + hlCls + '">' + fmtHigh(hl, metric) + '</div>' +
|
|
994
|
+
'<div class="card-highlight-label">' + metricLabel + ' · ' + partName + ' · #' + card.rank + '/' + card.total + '</div>' +
|
|
995
|
+
rankBlock
|
|
612
996
|
|
|
613
997
|
const titleBar =
|
|
614
998
|
'<span>' + seasonName + '</span><span class="sep">·</span>' +
|
|
@@ -642,8 +1026,18 @@ body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkM
|
|
|
642
1026
|
.card-highlight.mid { color:var(--yellow); }
|
|
643
1027
|
.card-highlight.low { color:var(--red); }
|
|
644
1028
|
.card-highlight.plain { color:var(--accent); }
|
|
645
|
-
.card-highlight .hl-rank { position:absolute; top:-0.15em; font-size:.35em; font-weight:700; color:var(--accent); white-space:nowrap; }
|
|
646
1029
|
.card-highlight-label { font-size:15px; color:var(--text-secondary); }
|
|
1030
|
+
.rank-table { width:100%; border:1px solid rgba(255,255,255,.08); border-radius:8px; overflow:hidden; }
|
|
1031
|
+
.rank-row { display:grid; grid-template-columns:48px 1fr 84px; gap:6px; align-items:center; padding:8px 12px; background:rgba(255,255,255,.02); border-bottom:1px solid rgba(255,255,255,.05); font-size:16px; }
|
|
1032
|
+
.rank-row:last-child { border-bottom:none; }
|
|
1033
|
+
.rank-row.self { background:rgba(88,166,255,.12); border-left:3px solid var(--accent); font-weight:700; }
|
|
1034
|
+
.rank-header { background:rgba(255,255,255,.05); font-size:13px; color:var(--text-secondary); font-weight:600; }
|
|
1035
|
+
.rk-pos { color:var(--accent); font-weight:700; font-variant-numeric:tabular-nums; }
|
|
1036
|
+
.rank-row.self .rk-pos { color:var(--yellow); }
|
|
1037
|
+
.rk-name { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
1038
|
+
.rk-val { text-align:right; font-variant-numeric:tabular-nums; font-weight:600; font-size:16px; }
|
|
1039
|
+
.rank-header .rk-pos, .rank-header .rk-val { color:var(--text-secondary); font-weight:600; }
|
|
1040
|
+
.credit { text-align:center; color:var(--text-secondary); font-size:12px; margin-top:14px; opacity:.7; }
|
|
647
1041
|
.stat-block { display:flex; flex-direction:column; gap:6px; }
|
|
648
1042
|
.stat-block + .stat-block { margin-top:12px; }
|
|
649
1043
|
.s-row { display:flex; align-items:center; justify-content:space-between; padding:8px 12px; background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.04); border-radius:6px; gap:10px; }
|
|
@@ -671,6 +1065,7 @@ body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkM
|
|
|
671
1065
|
.card-badge-item.tier-b .b-tier { color:var(--accent); }
|
|
672
1066
|
.card-badge-item.tier-c .b-tier { color:var(--text-secondary); }
|
|
673
1067
|
.foot { text-align:center; color:var(--text-secondary); font-size:12px; margin-top:10px; opacity:.7; }
|
|
1068
|
+
.cmd-hint { margin-top:16px; padding:8px 12px; border:1px solid rgba(255,255,255,.08); border-radius:8px; background:rgba(22,27,34,.5); white-space:pre-wrap; font-family:inherit; font-size:10px; line-height:1.55; color:var(--accent); }
|
|
674
1069
|
</style>
|
|
675
1070
|
</head>
|
|
676
1071
|
<body>
|
|
@@ -695,6 +1090,236 @@ function fmtHigh(v, metric) {
|
|
|
695
1090
|
return (v * 100).toFixed(2) + '%'
|
|
696
1091
|
}
|
|
697
1092
|
|
|
1093
|
+
function buildRankRows(card, metric, badgeScores) {
|
|
1094
|
+
const { rank, total, ranking } = card
|
|
1095
|
+
if (!rank || !total || !ranking || !ranking.length) return []
|
|
1096
|
+
const lo = Math.max(1, rank - 5)
|
|
1097
|
+
const hi = Math.min(total, rank + 5)
|
|
1098
|
+
const list = []
|
|
1099
|
+
if (1 < lo || 1 > hi) list.push(1)
|
|
1100
|
+
for (let i = lo; i <= hi; i++) list.push(i)
|
|
1101
|
+
if (total !== hi && total !== 1) list.push(total)
|
|
1102
|
+
return list.map(r => {
|
|
1103
|
+
const item = ranking[r - 1]
|
|
1104
|
+
if (!item) return ''
|
|
1105
|
+
const v = getMetricVal(item, metric, badgeScores)
|
|
1106
|
+
const self = r === rank
|
|
1107
|
+
return '<div class="rank-row' + (self ? ' self' : '') + '">' +
|
|
1108
|
+
'<span class="rk-pos">#' + r + '</span>' +
|
|
1109
|
+
'<span class="rk-name">' + esc(item.name) + '</span>' +
|
|
1110
|
+
'<span class="rk-val">' + fmtHigh(v, metric) + '</span></div>'
|
|
1111
|
+
})
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// ===== 地图 HTML =====
|
|
1115
|
+
|
|
1116
|
+
function buildMapHtml(data, config) {
|
|
1117
|
+
const width = config.width || 1000
|
|
1118
|
+
const d = (v, suffix) => v == null || isNaN(v) ? '--' : v.toFixed(2) + suffix
|
|
1119
|
+
const diffHtml = (cur, prev) => {
|
|
1120
|
+
if (cur == null || prev == null) return '<span class="stat-delta">--</span>'
|
|
1121
|
+
const diff = cur - prev
|
|
1122
|
+
const cls = Math.abs(diff) < 0.01 ? '' : (diff > 0 ? 'up' : 'down')
|
|
1123
|
+
return '<span class="stat-delta ' + cls + '">' + (diff > 0 ? '+' : '') + diff.toFixed(2) + '%</span>'
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const rows = [
|
|
1127
|
+
{ label: '胜率', cur: data.win_rate, prev: data.prev ? data.prev.win_rate : null },
|
|
1128
|
+
{ label: '平率', cur: data.ping_rate, prev: data.prev ? data.prev.ping_rate : null },
|
|
1129
|
+
{ label: '负率', cur: data.fail_rate, prev: data.prev ? data.prev.fail_rate : null },
|
|
1130
|
+
{ label: '去平胜率', cur: data.win_rate_nodraw, prev: data.prev ? data.prev.win_rate_nodraw : null },
|
|
1131
|
+
].map(r =>
|
|
1132
|
+
'<div class="s-row"><span class="s-label">' + r.label + '</span>' +
|
|
1133
|
+
'<span class="s-val">' + d(r.cur, '%') + '</span>' +
|
|
1134
|
+
'<span class="s-rank" style="text-align:right;min-width:110px">' + (data.prev ? diffHtml(r.cur, r.prev) : '<span class="stat-delta">--</span>') + '</span></div>'
|
|
1135
|
+
).join('')
|
|
1136
|
+
|
|
1137
|
+
const prevBlock = data.prev
|
|
1138
|
+
? '<div class="zone-sep"></div><div class="comp-title" style="color:var(--accent);margin-bottom:4px">对比上赛季(' + data.prevLabel + ')</div>'
|
|
1139
|
+
: ''
|
|
1140
|
+
|
|
1141
|
+
const titleBar =
|
|
1142
|
+
'<span>' + data.seasonName + '</span><span class="sep">·</span>' +
|
|
1143
|
+
'<span>' + data.weekLabel + '</span><span class="sep">·</span>' +
|
|
1144
|
+
'<span>' + data.partName + '</span><span class="sep">·</span>' +
|
|
1145
|
+
'<span>' + data.campName + '</span>'
|
|
1146
|
+
|
|
1147
|
+
const spaceNo = data.spaceNo != null ? '<div class="card-sub">地图编号 ' + esc(String(data.spaceNo)) + '</div>' : ''
|
|
1148
|
+
|
|
1149
|
+
return `<!DOCTYPE html>
|
|
1150
|
+
<html lang="zh-CN">
|
|
1151
|
+
<head>
|
|
1152
|
+
<meta charset="UTF-8">
|
|
1153
|
+
<style>
|
|
1154
|
+
:root { --bg:#0d1117; --surface:#161b22; --border:#30363d; --text:#c9d1d9; --text-secondary:#8b949e; --accent:#58a6ff; --green:#3fb950; --red:#f85149; --yellow:#d2991d; --card-bg:#1c2333; }
|
|
1155
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
1156
|
+
body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:16px; }
|
|
1157
|
+
.title-bar { display:flex; justify-content:center; align-items:center; gap:8px; padding:6px 18px; background:rgba(22,27,34,.9); border:1px solid var(--border); border-radius:20px; font-size:20px; margin-bottom:14px; white-space:nowrap; }
|
|
1158
|
+
.title-bar .sep { color:var(--border); }
|
|
1159
|
+
.card { background:var(--card-bg); border:1px solid var(--border); border-radius:18px; padding:22px 26px; max-width:560px; margin:0 auto; }
|
|
1160
|
+
.zone-center { display:flex; flex-direction:column; align-items:center; }
|
|
1161
|
+
.card-map-name { font-size:28px; font-weight:700; text-align:center; margin:4px 0 2px; }
|
|
1162
|
+
.card-sub { font-size:13px; color:var(--text-secondary); margin-bottom:10px; }
|
|
1163
|
+
.stat-block { display:flex; flex-direction:column; gap:6px; }
|
|
1164
|
+
.s-row { display:flex; align-items:center; justify-content:space-between; padding:8px 12px; background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.04); border-radius:6px; gap:10px; }
|
|
1165
|
+
.s-label { font-size:15px; color:var(--text-secondary); font-weight:600; width:90px; flex-shrink:0; }
|
|
1166
|
+
.s-val { font-size:18px; font-weight:700; font-variant-numeric:tabular-nums; flex:1; text-align:center; }
|
|
1167
|
+
.s-rank { font-size:15px; min-width:110px; text-align:right; }
|
|
1168
|
+
.stat-delta { font-size:14px; font-weight:600; font-variant-numeric:tabular-nums; }
|
|
1169
|
+
.stat-delta.up { color:var(--green); }
|
|
1170
|
+
.stat-delta.down { color:var(--red); }
|
|
1171
|
+
.zone-sep { margin:12px 0; border-top:1px solid rgba(255,255,255,.08); }
|
|
1172
|
+
.comp-title { font-size:15px; color:var(--green); font-weight:700; text-align:center; margin-bottom:8px; letter-spacing:1px; }
|
|
1173
|
+
.foot { text-align:center; color:var(--text-secondary); font-size:12px; margin-top:10px; opacity:.7; }
|
|
1174
|
+
</style>
|
|
1175
|
+
</head>
|
|
1176
|
+
<body>
|
|
1177
|
+
<div class="title-bar">${titleBar}</div>
|
|
1178
|
+
<div class="card">
|
|
1179
|
+
<div class="zone-center">
|
|
1180
|
+
<div class="card-map-name">${esc(data.mapName)}</div>
|
|
1181
|
+
${spaceNo}
|
|
1182
|
+
</div>
|
|
1183
|
+
<div class="stat-block">${rows}</div>
|
|
1184
|
+
${prevBlock}
|
|
1185
|
+
</div>
|
|
1186
|
+
<div class="foot">数据来源:${esc(data.sourceLabel)} · 数据均根据第五人格官方小程序数据计算</div>
|
|
1187
|
+
</body>
|
|
1188
|
+
</html>`
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// ===== 模仿者 HTML =====
|
|
1192
|
+
|
|
1193
|
+
function buildMimicHtml(data, config) {
|
|
1194
|
+
const pct = v => v == null || isNaN(v) ? '--' : (v * 100).toFixed(2) + '%'
|
|
1195
|
+
const p = data.primary
|
|
1196
|
+
const rows = [
|
|
1197
|
+
{ label: '胜率', val: pct(p.win_rate) },
|
|
1198
|
+
{ label: '出场率', val: pct(p.show_rate) },
|
|
1199
|
+
{ label: '投票正确率', val: pct(p.vote_correct_rate) },
|
|
1200
|
+
{ label: '被杀率', val: pct(p.bekill_rate) },
|
|
1201
|
+
].map(r =>
|
|
1202
|
+
'<div class="s-row"><span class="s-label">' + r.label + '</span>' +
|
|
1203
|
+
'<span class="s-val">' + r.val + '</span><span class="s-rank"></span></div>'
|
|
1204
|
+
).join('')
|
|
1205
|
+
|
|
1206
|
+
const otherModes = data.modes.map(m => {
|
|
1207
|
+
const isPrimary = m.key === p.key
|
|
1208
|
+
return '<div class="mode-item' + (isPrimary ? ' active' : '') + '">' +
|
|
1209
|
+
'<span class="mode-name">' + esc(m.label) + '</span>' +
|
|
1210
|
+
'<span class="mode-val">' + pct(m.win_rate) + '</span></div>'
|
|
1211
|
+
}).join('')
|
|
1212
|
+
|
|
1213
|
+
const titleBar =
|
|
1214
|
+
'<span>' + data.dateLabel + '</span><span class="sep">·</span>' +
|
|
1215
|
+
'<span>' + data.modeLabel + '</span><span class="sep">·</span>' +
|
|
1216
|
+
'<span>' + data.campName + '</span>'
|
|
1217
|
+
|
|
1218
|
+
return `<!DOCTYPE html>
|
|
1219
|
+
<html lang="zh-CN">
|
|
1220
|
+
<head>
|
|
1221
|
+
<meta charset="UTF-8">
|
|
1222
|
+
<style>
|
|
1223
|
+
:root { --bg:#0d1117; --surface:#161b22; --border:#30363d; --text:#c9d1d9; --text-secondary:#8b949e; --accent:#58a6ff; --green:#3fb950; --red:#f85149; --yellow:#d2991d; --card-bg:#1c2333; }
|
|
1224
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
1225
|
+
body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:16px; }
|
|
1226
|
+
.title-bar { display:flex; justify-content:center; align-items:center; gap:8px; padding:6px 18px; background:rgba(22,27,34,.9); border:1px solid var(--border); border-radius:20px; font-size:20px; margin-bottom:14px; white-space:nowrap; }
|
|
1227
|
+
.title-bar .sep { color:var(--border); }
|
|
1228
|
+
.card { background:var(--card-bg); border:1px solid var(--border); border-radius:18px; padding:22px 26px; max-width:560px; margin:0 auto; }
|
|
1229
|
+
.zone-center { display:flex; flex-direction:column; align-items:center; }
|
|
1230
|
+
.card-char-name { font-size:28px; font-weight:700; text-align:center; margin:4px 0 2px; }
|
|
1231
|
+
.card-camp { font-size:13px; color:var(--accent); margin-bottom:12px; }
|
|
1232
|
+
.stat-block { display:flex; flex-direction:column; gap:6px; }
|
|
1233
|
+
.s-row { display:flex; align-items:center; justify-content:space-between; padding:8px 12px; background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.04); border-radius:6px; gap:10px; }
|
|
1234
|
+
.s-label { font-size:15px; color:var(--text-secondary); font-weight:600; width:110px; flex-shrink:0; }
|
|
1235
|
+
.s-val { font-size:18px; font-weight:700; font-variant-numeric:tabular-nums; flex:1; text-align:center; }
|
|
1236
|
+
.s-rank { min-width:46px; }
|
|
1237
|
+
.zone-sep { margin:12px 0; border-top:1px solid rgba(255,255,255,.08); }
|
|
1238
|
+
.comp-title { font-size:15px; color:var(--green); font-weight:700; text-align:center; margin-bottom:8px; letter-spacing:1px; }
|
|
1239
|
+
.mode-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:6px; }
|
|
1240
|
+
.mode-item { display:flex; align-items:center; justify-content:space-between; padding:6px 10px; background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.04); border-radius:5px; }
|
|
1241
|
+
.mode-item.active { border-color:var(--accent); }
|
|
1242
|
+
.mode-name { font-size:13px; color:var(--text-secondary); font-weight:600; }
|
|
1243
|
+
.mode-val { font-size:15px; font-weight:700; font-variant-numeric:tabular-nums; }
|
|
1244
|
+
.foot { text-align:center; color:var(--text-secondary); font-size:12px; margin-top:10px; opacity:.7; }
|
|
1245
|
+
</style>
|
|
1246
|
+
</head>
|
|
1247
|
+
<body>
|
|
1248
|
+
<div class="title-bar">${titleBar}</div>
|
|
1249
|
+
<div class="card">
|
|
1250
|
+
<div class="zone-center">
|
|
1251
|
+
<div class="card-char-name">${esc(data.name)}</div>
|
|
1252
|
+
<div class="card-camp">${esc(data.campName)}</div>
|
|
1253
|
+
</div>
|
|
1254
|
+
<div class="stat-block">${rows}</div>
|
|
1255
|
+
<div class="zone-sep"></div>
|
|
1256
|
+
<div class="comp-title" style="color:var(--accent);margin-bottom:4px">各模式胜率</div>
|
|
1257
|
+
<div class="mode-grid">${otherModes}</div>
|
|
1258
|
+
</div>
|
|
1259
|
+
<div class="foot">数据来源:${esc(data.sourceLabel)} · 数据文件:${esc(data.file)}</div>
|
|
1260
|
+
</body>
|
|
1261
|
+
</html>`
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// ===== 勋章 HTML =====
|
|
1265
|
+
|
|
1266
|
+
function buildBadgeHtml(data, config) {
|
|
1267
|
+
const tierItems = data.tiers.map(t => {
|
|
1268
|
+
const deltaHtml = data.mode === 'original'
|
|
1269
|
+
? ''
|
|
1270
|
+
: '<div class="b-delta ' + (t.delta == null ? '' : (t.delta > 0 ? 'up' : (t.delta < 0 ? 'down' : ''))) + '">' +
|
|
1271
|
+
(t.delta == null ? '--' : (t.delta > 0 ? '+' : '') + t.delta.toFixed(1) + '%') + '</div>'
|
|
1272
|
+
return '<div class="card-badge-item ' + t.cls + '"><span class="b-tier">' + t.label + '</span>' +
|
|
1273
|
+
'<span class="b-val">' + (t.val != null ? t.val : '--') + '</span>' + deltaHtml + '</div>'
|
|
1274
|
+
}).join('')
|
|
1275
|
+
|
|
1276
|
+
const titleBar =
|
|
1277
|
+
'<span>' + data.dateLabel + '</span><span class="sep">·</span>' +
|
|
1278
|
+
'<span>' + data.modeLabel + '</span><span class="sep">·</span>' +
|
|
1279
|
+
'<span>' + data.campName + '</span>'
|
|
1280
|
+
|
|
1281
|
+
const footNote = data.prevDate && data.mode !== 'original'
|
|
1282
|
+
? '<div class="foot">对比日期:' + data.prevDate + ' · 数据来源:' + esc(data.sourceLabel) + '</div>'
|
|
1283
|
+
: '<div class="foot">数据来源:' + esc(data.sourceLabel) + '</div>'
|
|
1284
|
+
|
|
1285
|
+
return `<!DOCTYPE html>
|
|
1286
|
+
<html lang="zh-CN">
|
|
1287
|
+
<head>
|
|
1288
|
+
<meta charset="UTF-8">
|
|
1289
|
+
<style>
|
|
1290
|
+
:root { --bg:#0d1117; --surface:#161b22; --border:#30363d; --text:#c9d1d9; --text-secondary:#8b949e; --accent:#58a6ff; --green:#3fb950; --red:#f85149; --yellow:#d2991d; --card-bg:#1c2333; }
|
|
1291
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
1292
|
+
body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:16px; }
|
|
1293
|
+
.title-bar { display:flex; justify-content:center; align-items:center; gap:8px; padding:6px 18px; background:rgba(22,27,34,.9); border:1px solid var(--border); border-radius:20px; font-size:20px; margin-bottom:14px; white-space:nowrap; }
|
|
1294
|
+
.title-bar .sep { color:var(--border); }
|
|
1295
|
+
.card { background:var(--card-bg); border:1px solid var(--border); border-radius:18px; padding:22px 26px; max-width:560px; margin:0 auto; }
|
|
1296
|
+
.zone-center { display:flex; flex-direction:column; align-items:center; }
|
|
1297
|
+
.card-char-name { font-size:28px; font-weight:700; text-align:center; margin:4px 0 8px; }
|
|
1298
|
+
.card-badge-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:8px; }
|
|
1299
|
+
.card-badge-item { display:flex; flex-direction:column; align-items:center; gap:3px; padding:8px 3px; background:rgba(255,255,255,.02); border:1px solid rgba(255,255,255,.04); border-radius:5px; }
|
|
1300
|
+
.b-tier { font-size:13px; color:var(--text-secondary); font-weight:700; }
|
|
1301
|
+
.b-val { font-size:18px; font-weight:700; font-variant-numeric:tabular-nums; }
|
|
1302
|
+
.b-delta { font-size:12px; font-weight:600; font-variant-numeric:tabular-nums; }
|
|
1303
|
+
.b-delta.up { color:var(--green); }
|
|
1304
|
+
.b-delta.down { color:var(--red); }
|
|
1305
|
+
.card-badge-item.tier-s .b-tier { color:var(--yellow); }
|
|
1306
|
+
.card-badge-item.tier-a .b-tier { color:var(--green); }
|
|
1307
|
+
.card-badge-item.tier-b .b-tier { color:var(--accent); }
|
|
1308
|
+
.card-badge-item.tier-c .b-tier { color:var(--text-secondary); }
|
|
1309
|
+
.foot { text-align:center; color:var(--text-secondary); font-size:12px; margin-top:10px; opacity:.7; }
|
|
1310
|
+
</style>
|
|
1311
|
+
</head>
|
|
1312
|
+
<body>
|
|
1313
|
+
<div class="title-bar">${titleBar}</div>
|
|
1314
|
+
<div class="card">
|
|
1315
|
+
<div class="zone-center"><div class="card-char-name">${esc(data.name)}</div></div>
|
|
1316
|
+
<div class="card-badge-grid">${tierItems}</div>
|
|
1317
|
+
</div>
|
|
1318
|
+
${footNote}
|
|
1319
|
+
</body>
|
|
1320
|
+
</html>`
|
|
1321
|
+
}
|
|
1322
|
+
|
|
698
1323
|
function buildHelp() {
|
|
699
1324
|
return [
|
|
700
1325
|
'查询角色胜率并生成图片',
|
|
@@ -714,16 +1339,55 @@ function buildHelp() {
|
|
|
714
1339
|
].join('\n')
|
|
715
1340
|
}
|
|
716
1341
|
|
|
1342
|
+
function buildHelpHtml(text, config) {
|
|
1343
|
+
const width = config.width || 1000
|
|
1344
|
+
return `<!DOCTYPE html>
|
|
1345
|
+
<html lang="zh-CN">
|
|
1346
|
+
<head>
|
|
1347
|
+
<meta charset="UTF-8">
|
|
1348
|
+
<style>
|
|
1349
|
+
:root { --bg:#0d1117; --surface:#161b22; --border:#30363d; --text:#c9d1d9; --text-secondary:#8b949e; --accent:#58a6ff; --card-bg:#1c2333; }
|
|
1350
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
1351
|
+
body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:16px; }
|
|
1352
|
+
.card { background:var(--card-bg); border:1px solid var(--border); border-radius:18px; padding:22px 26px; max-width:${Math.min(width, 720)}px; margin:0 auto; }
|
|
1353
|
+
.help-title { font-size:22px; font-weight:700; color:var(--accent); text-align:center; margin-bottom:14px; }
|
|
1354
|
+
.help-text { font-size:15px; line-height:1.8; white-space:pre-wrap; word-break:break-all; }
|
|
1355
|
+
.foot { text-align:center; color:var(--text-secondary); font-size:12px; margin-top:10px; opacity:.7; }
|
|
1356
|
+
</style>
|
|
1357
|
+
</head>
|
|
1358
|
+
<body>
|
|
1359
|
+
<div class="card">
|
|
1360
|
+
<div class="help-title">指令帮助</div>
|
|
1361
|
+
<div class="help-text">${esc(text)}</div>
|
|
1362
|
+
</div>
|
|
1363
|
+
<div class="foot">数据均根据第五人格官方小程序数据计算</div>
|
|
1364
|
+
</body>
|
|
1365
|
+
</html>`
|
|
1366
|
+
}
|
|
1367
|
+
|
|
717
1368
|
module.exports = {
|
|
718
1369
|
KNOWN_SOURCES,
|
|
719
1370
|
METRICS,
|
|
720
1371
|
BADGE_TIERS,
|
|
721
1372
|
PARTS,
|
|
722
1373
|
PART_ORDER,
|
|
1374
|
+
MAP_CAMPS,
|
|
1375
|
+
MIMIC_MODES,
|
|
1376
|
+
BADGE_MODES,
|
|
723
1377
|
parseArgs,
|
|
1378
|
+
parseMapArgs,
|
|
1379
|
+
parseMimicArgs,
|
|
1380
|
+
parseBadgeArgs,
|
|
724
1381
|
resolveSource,
|
|
725
1382
|
collect,
|
|
1383
|
+
collectMap,
|
|
1384
|
+
collectMimic,
|
|
1385
|
+
collectBadge,
|
|
726
1386
|
buildHtml,
|
|
1387
|
+
buildMapHtml,
|
|
1388
|
+
buildMimicHtml,
|
|
1389
|
+
buildBadgeHtml,
|
|
727
1390
|
buildHelp,
|
|
1391
|
+
buildHelpHtml,
|
|
728
1392
|
findSimilar,
|
|
729
1393
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
第五人格BWIKI编辑组测试用组件,纯AI生成喵。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
查询第五人格角色胜率、地图胜率、模仿者游戏数据、勋章分数线,返回一张图片(样式参考 胜率数据站 animationshow.html 的角色卡片)。
|
|
6
6
|
|
|
7
7
|
需要安装 [koishi-plugin-puppeteer](https://www.npmjs.com/package/koishi-plugin-puppeteer) 用于截图。
|
|
8
8
|
|
|
9
9
|
## 用法
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
+
BWIKI机器人
|
|
12
13
|
胜率 <角色名> [赛季] [周数] [排序要素] [段位]
|
|
14
|
+
地图 <地图名> [赛季] [周数] [段位] [阵营]
|
|
15
|
+
模仿者 <角色名> [日期] [模式]
|
|
16
|
+
勋章 <角色名> [日期] [模式]
|
|
13
17
|
```
|
|
14
18
|
|
|
19
|
+
发送 `BWIKI机器人` 可查看当前支持的指令列表图片;发送 `胜率` 等单独指令可查看对应完整帮助。
|
|
20
|
+
|
|
21
|
+
### 胜率(角色胜率)
|
|
22
|
+
|
|
15
23
|
示例:
|
|
16
24
|
|
|
17
25
|
| 指令 | 含义 |
|
|
@@ -26,6 +34,48 @@
|
|
|
26
34
|
- 排序要素默认 `去平胜率`,可选:胜率、平率、负率、使用率、禁用率、S1/S10/A11/A100/B101/B300/C301/C500 勋章分
|
|
27
35
|
- 段位默认 `巅七`,可选:七阶、六阶、五阶、四阶、三阶、二阶、一阶、全段位
|
|
28
36
|
|
|
37
|
+
### 地图(地图胜率)
|
|
38
|
+
|
|
39
|
+
示例:
|
|
40
|
+
|
|
41
|
+
| 指令 | 含义 |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `地图 军工厂` | 最新赛季最新周,监管者视角 |
|
|
44
|
+
| `地图 红教堂 S44 3 求生者` | 指定赛季、周数与阵营 |
|
|
45
|
+
| `地图 月亮河公园 全赛季 全段位` | 赛季累计与全段位 |
|
|
46
|
+
| `地图 湖景` | 支持模糊匹配地图名 |
|
|
47
|
+
|
|
48
|
+
- 阵营默认 `监管者`,可选:求生者;赛季:S44 / 第44赛季;周数:第3周 / 3
|
|
49
|
+
- 段位默认 `巅七`,可选:七阶、六阶、五阶、四阶、三阶、二阶、一阶、全段位
|
|
50
|
+
- 显示胜率/平率/负率/去平胜率,以及对比上赛季同周的变化
|
|
51
|
+
|
|
52
|
+
### 模仿者(模仿者游戏数据)
|
|
53
|
+
|
|
54
|
+
示例:
|
|
55
|
+
|
|
56
|
+
| 指令 | 含义 |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `模仿者 侦探` | 最新日期,所有模式 |
|
|
59
|
+
| `模仿者 药剂师 260531 12人高级场` | 指定日期与模式 |
|
|
60
|
+
|
|
61
|
+
- 日期:YYMMDD(如 260531),不填默认最新日期
|
|
62
|
+
- 模式:低级场、10人高级场、12人高级场、所有模式(默认)
|
|
63
|
+
- 显示胜率/出场率/投票正确率/被杀率,以及各模式胜率对比;角色阵营分为侦探团/模仿者/中立
|
|
64
|
+
|
|
65
|
+
### 勋章(角色勋章分数线)
|
|
66
|
+
|
|
67
|
+
示例:
|
|
68
|
+
|
|
69
|
+
| 指令 | 含义 |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `勋章 小女孩` | 最新日期,勋章分数 |
|
|
72
|
+
| `勋章 小女孩 20250917 7日增长率` | 指定日期与模式 |
|
|
73
|
+
| `勋章 小女孩 赛季同比增长率` | 对比上赛季同日 |
|
|
74
|
+
|
|
75
|
+
- 日期:YYYYMMDD(如 20250917),不填默认最新日期
|
|
76
|
+
- 模式:勋章分数(默认)、距平百分比、1/3/7/15/30日增长率、赛季同比增长率
|
|
77
|
+
- 显示 S1/S10/A11/A100/B101/B300/C301/C500 八档勋章分数线
|
|
78
|
+
|
|
29
79
|
## 配置
|
|
30
80
|
|
|
31
81
|
| 配置项 | 说明 | 默认值 |
|
|
@@ -33,9 +83,13 @@
|
|
|
33
83
|
| `dataSource` | 数据源地址,可在两个镜像站点间切换 | `https://website.homeworkkun.top` |
|
|
34
84
|
| `tryAlternate` | 主数据源失败时自动尝试备用数据源 | `true` |
|
|
35
85
|
| `defaultPart` | 默认段位(8=巅七,-127=全段位) | `8` |
|
|
86
|
+
| `mapDefaultCamp` | 地图查询默认阵营(1=监管者,2=求生者) | `1` |
|
|
36
87
|
| `showBadge` | 显示勋章分数据 | `true` |
|
|
37
88
|
| `badgeDays` | 勋章日增长率天数 | `7` |
|
|
38
|
-
| `width` | 图片宽度(px
|
|
89
|
+
| `width` | 图片宽度(px),其他指令未单独设置时使用该值 | `1000` |
|
|
90
|
+
| `mapWidth` | 地图指令图片宽度(px),0=使用默认宽度 | `0` |
|
|
91
|
+
| `mimicWidth` | 模仿者指令图片宽度(px),0=使用默认宽度 | `0` |
|
|
92
|
+
| `badgeWidth` | 勋章指令图片宽度(px),0=使用默认宽度 | `0` |
|
|
39
93
|
| `scale` | 截图倍率 | `2` |
|
|
40
94
|
| `timeout` | 网络请求超时(毫秒) | `15000` |
|
|
41
95
|
|
|
@@ -43,9 +97,29 @@
|
|
|
43
97
|
|
|
44
98
|
## 图片内容
|
|
45
99
|
|
|
100
|
+
### 胜率
|
|
101
|
+
|
|
46
102
|
- 标题栏:赛季 · 周数 · 段位 · 排序要素
|
|
47
103
|
- 中央:角色头像、名称、定位/阵营、排序要素数值(含本阵营排名)、要素名
|
|
48
104
|
- 左侧:六项数据(去平胜率/胜率/平率/负率/使用率/禁用率)及阵营排名、对比上赛季变化值与变化排名、各段位去平胜率(距平均)
|
|
49
105
|
- 右侧:勋章分数(S1~C500)、赛季同比增长、日增长率、距阵营平均值
|
|
50
106
|
|
|
107
|
+
### 地图
|
|
108
|
+
|
|
109
|
+
- 标题栏:赛季 · 周数 · 段位 · 阵营
|
|
110
|
+
- 中央:地图名、地图编号
|
|
111
|
+
- 数据:胜率/平率/负率/去平胜率,及对比上赛季同周变化
|
|
112
|
+
|
|
113
|
+
### 模仿者
|
|
114
|
+
|
|
115
|
+
- 标题栏:日期 · 模式 · 阵营
|
|
116
|
+
- 中央:角色名、阵营
|
|
117
|
+
- 数据:胜率/出场率/投票正确率/被杀率,及各模式胜率对比
|
|
118
|
+
|
|
119
|
+
### 勋章
|
|
120
|
+
|
|
121
|
+
- 标题栏:日期 · 模式 · 阵营
|
|
122
|
+
- 中央:角色名
|
|
123
|
+
- 数据:S1~C500 八档勋章分数线,及所选模式(增长率/距平)计算值
|
|
124
|
+
|
|
51
125
|
数据均根据第五人格官方小程序数据计算。
|