koishi-plugin-dwrgbwiki 1.3.2 → 2.0.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/basicdata.js +407 -0
- package/cangtou.js +166 -0
- package/codex.js +524 -0
- package/gacha.js +1498 -1501
- package/index.js +923 -406
- package/lib.js +1453 -1453
- package/package.json +32 -28
- package/readme.md +223 -125
- package/wiki.js +96 -0
package/basicdata.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const CAMP_META = {
|
|
4
|
+
hunter: { label: '监管者', file: 'hunter_data.csv', enc: 'utf8' },
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const FALLBACK_COLUMNS = {
|
|
8
|
+
hunter: ['模型体积', '移动速度', '转向速度', '跨过窗户', '摧毁木板', '被砸眩晕', '普攻前摇', '普攻距离最小值', '普攻距离最大值', '普攻空刀后摇', '普攻擦刀', '蓄力前摇', '蓄力距离最小值', '蓄力距离最大值', '蓄力空刀后摇', '蓄力擦刀', '牵气球时长', '挣脱眩晕时长', '牵气球攻击前摇', '气球刀最小距离', '气球刀最大距离', '恐惧半径', '弹刀时长'],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CACHE_TTL = 15 * 60 * 1000
|
|
12
|
+
const cache = new Map()
|
|
13
|
+
|
|
14
|
+
function esc(s) {
|
|
15
|
+
return String(s == null ? '' : s)
|
|
16
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
17
|
+
.replace(/"/g, '"').replace(/'/g, ''')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function campLabel(camp) {
|
|
21
|
+
return CAMP_META[camp] ? CAMP_META[camp].label : camp
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function baseName(name) {
|
|
25
|
+
return String(name)
|
|
26
|
+
.replace(/["“”]/g, '')
|
|
27
|
+
.replace(/[((].*?[))]$/, '')
|
|
28
|
+
.trim()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function fetchTextRaw(fullUrl, timeout) {
|
|
32
|
+
const hit = cache.get(fullUrl)
|
|
33
|
+
if (hit && Date.now() - hit.ts < hit.ttl) return hit.buf
|
|
34
|
+
const ctrl = new AbortController()
|
|
35
|
+
const timer = setTimeout(() => ctrl.abort(), timeout)
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(fullUrl, { signal: ctrl.signal })
|
|
38
|
+
if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + fullUrl)
|
|
39
|
+
const buf = Buffer.from(await res.arrayBuffer())
|
|
40
|
+
cache.set(fullUrl, { buf, ts: Date.now(), ttl: CACHE_TTL })
|
|
41
|
+
return buf
|
|
42
|
+
} finally {
|
|
43
|
+
clearTimeout(timer)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseCSV(text) {
|
|
48
|
+
const lines = text.trim().split('\n').filter(l => l.trim())
|
|
49
|
+
if (lines.length < 2) throw new Error('CSV 数据为空')
|
|
50
|
+
const rawHeaders = lines[0].split(',').map(h => h.trim()).filter(Boolean)
|
|
51
|
+
const columns = []
|
|
52
|
+
rawHeaders.forEach((h, i) => {
|
|
53
|
+
if (i === 0) return
|
|
54
|
+
const m = h.match(/^(.*?)\(([01])\)$/)
|
|
55
|
+
if (m) {
|
|
56
|
+
columns.push({ name: m[1], raw: h, idx: i, sortType: m[2] === '1' ? 'desc' : 'asc' })
|
|
57
|
+
} else {
|
|
58
|
+
columns.push({ name: h, raw: h, idx: i, sortType: 'asc' })
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
const rows = []
|
|
62
|
+
lines.slice(1).forEach(line => {
|
|
63
|
+
const vals = line.split(',').map(v => {
|
|
64
|
+
const s = v.trim().replace(/^"|"$/g, '')
|
|
65
|
+
if (s !== '' && !isNaN(parseFloat(s)) && isFinite(s)) return parseFloat(s)
|
|
66
|
+
return s
|
|
67
|
+
})
|
|
68
|
+
rows.push({ name: vals[0], vals })
|
|
69
|
+
})
|
|
70
|
+
return { columns, rows }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function loadCamp(camp, config) {
|
|
74
|
+
const meta = CAMP_META[camp] || CAMP_META.hunter
|
|
75
|
+
const base = (config.dataSource || 'https://website.homeworkkun.top').replace(/\/+$/, '')
|
|
76
|
+
const url = base + '/' + encodeURI('WIKI/基础数据排名/' + meta.file)
|
|
77
|
+
const buf = await fetchTextRaw(url, config.timeout || 15000)
|
|
78
|
+
return parseCSV(buf.toString('utf8'))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveColumn(data, q) {
|
|
82
|
+
const s = String(q || '').trim()
|
|
83
|
+
if (!s) return null
|
|
84
|
+
const exact = data.columns.find(c => c.name === s)
|
|
85
|
+
if (exact) return exact
|
|
86
|
+
const matches = data.columns.filter(c => c.name.includes(s) || s.includes(c.name))
|
|
87
|
+
return matches.length === 1 ? matches[0] : null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findColumnCandidates(data, q) {
|
|
91
|
+
const s = String(q || '').trim()
|
|
92
|
+
const names = data.columns.map(c => c.name)
|
|
93
|
+
if (!s) return names
|
|
94
|
+
const hit = names.filter(n => n.includes(s))
|
|
95
|
+
return hit.length ? hit : names.filter(n => s.includes(n))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveChar(data, q) {
|
|
99
|
+
const s = String(q || '').trim()
|
|
100
|
+
if (!s) return null
|
|
101
|
+
const raw = data.rows.find(r => r.name === s)
|
|
102
|
+
if (raw) return raw
|
|
103
|
+
const exact = data.rows.filter(r => baseName(r.name) === s)
|
|
104
|
+
if (exact.length) {
|
|
105
|
+
const plain = exact.find(r => baseName(r.name) === r.name.replace(/["“”]/g, ''))
|
|
106
|
+
return plain || exact[0]
|
|
107
|
+
}
|
|
108
|
+
const subs = data.rows.filter(r => r.name.includes(s) || s.includes(r.name))
|
|
109
|
+
return subs.length === 1 ? subs[0] : null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findCharCandidates(data, q) {
|
|
113
|
+
const s = String(q || '').trim()
|
|
114
|
+
const names = []
|
|
115
|
+
const seen = new Set()
|
|
116
|
+
data.rows.forEach(r => {
|
|
117
|
+
const b = baseName(r.name)
|
|
118
|
+
if (!seen.has(b)) { seen.add(b); names.push(b) }
|
|
119
|
+
})
|
|
120
|
+
if (!s) return names
|
|
121
|
+
const hit = names.filter(n => n.includes(s))
|
|
122
|
+
return (hit.length ? hit : names.filter(n => s.includes(n))).slice(0, 10)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function computeRanking(data, col, label) {
|
|
126
|
+
const numeric = [], nonNumeric = []
|
|
127
|
+
data.rows.forEach(row => {
|
|
128
|
+
const v = row.vals[col.idx]
|
|
129
|
+
const item = { name: row.name, value: v }
|
|
130
|
+
if (typeof v === 'number' && !isNaN(v)) numeric.push(item)
|
|
131
|
+
else nonNumeric.push(item)
|
|
132
|
+
})
|
|
133
|
+
let avg = 0
|
|
134
|
+
if (numeric.length) avg = numeric.reduce((sum, x) => sum + x.value, 0) / numeric.length
|
|
135
|
+
const dir = col.sortType === 'desc' ? -1 : 1
|
|
136
|
+
numeric.sort((a, b) => (a.value - b.value) * dir)
|
|
137
|
+
let rank = 1
|
|
138
|
+
for (let i = 0; i < numeric.length;) {
|
|
139
|
+
let j = i
|
|
140
|
+
while (j < numeric.length && numeric[j].value === numeric[i].value) j++
|
|
141
|
+
for (let k = i; k < j; k++) numeric[k].rank = rank
|
|
142
|
+
rank += j - i
|
|
143
|
+
i = j
|
|
144
|
+
}
|
|
145
|
+
const rows = numeric.concat(nonNumeric.map(x => ({ ...x, rank: null })))
|
|
146
|
+
rows.forEach(x => {
|
|
147
|
+
if (typeof x.value === 'number' && !isNaN(x.value)) {
|
|
148
|
+
x.deviation = avg === 0
|
|
149
|
+
? '-'
|
|
150
|
+
: (() => {
|
|
151
|
+
const d = ((x.value - avg) / Math.abs(avg)) * 100
|
|
152
|
+
return (d >= 0 ? '+' : '') + d.toFixed(1) + '%'
|
|
153
|
+
})()
|
|
154
|
+
} else {
|
|
155
|
+
x.deviation = null
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
return { col, avg, total: data.rows.length, rows, campLabel: label || '' }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function computeCharacter(data, row) {
|
|
162
|
+
const items = data.columns.map(col => {
|
|
163
|
+
const value = row.vals[col.idx]
|
|
164
|
+
const num = typeof value === 'number' && !isNaN(value)
|
|
165
|
+
const item = { col, value, num }
|
|
166
|
+
if (num) {
|
|
167
|
+
const r = computeRanking(data, col, '')
|
|
168
|
+
const entry = r.rows.find(x => x.name === row.name)
|
|
169
|
+
if (entry) {
|
|
170
|
+
item.rank = entry.rank
|
|
171
|
+
item.total = r.rows.filter(x => x.rank != null).length
|
|
172
|
+
} else {
|
|
173
|
+
item.rank = null
|
|
174
|
+
item.total = null
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return item
|
|
178
|
+
})
|
|
179
|
+
return { row, items, numCount: items.filter(i => i.num).length }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function renderName(name) {
|
|
183
|
+
const m = String(name).match(/^(.*?)[((](.+?)[))]$/)
|
|
184
|
+
if (m) {
|
|
185
|
+
return esc(m[1]) + '<span class="morph">(' + esc(m[2]) + ')</span>'
|
|
186
|
+
}
|
|
187
|
+
return esc(name)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildRankingHtml(r, config, highlight) {
|
|
191
|
+
const width = config.width || 1300
|
|
192
|
+
const half = Math.ceil(r.rows.length / 2)
|
|
193
|
+
const left = r.rows.slice(0, half)
|
|
194
|
+
const right = r.rows.slice(half)
|
|
195
|
+
|
|
196
|
+
const hlName = highlight ? String(highlight).trim() : ''
|
|
197
|
+
const hlBase = hlName ? baseName(hlName) : ''
|
|
198
|
+
const hlRow = hlName ? r.rows.find(x => baseName(x.name) === hlBase || x.name === hlName) : null
|
|
199
|
+
|
|
200
|
+
const rowHtml = (rows) => rows.map(row => {
|
|
201
|
+
const num = typeof row.value === 'number' && !isNaN(row.value)
|
|
202
|
+
const isHl = hlBase && baseName(row.name) === hlBase
|
|
203
|
+
return '<tr' + (isHl ? ' class="hl"' : '') + '>' +
|
|
204
|
+
'<td class="c-rank">' + (row.rank != null ? row.rank : '-') + '</td>' +
|
|
205
|
+
'<td class="c-name">' + renderName(row.name) + (isHl ? '<span class="hl-tag">你查询的角色</span>' : '') + '</td>' +
|
|
206
|
+
'<td class="c-val">' + (num ? row.value.toFixed(2) : '<span class="na">' + esc(row.value || '-') + '</span>') + '</td>' +
|
|
207
|
+
'<td class="c-dev">' + (row.deviation != null ? row.deviation : '-') + '</td>' +
|
|
208
|
+
'</tr>'
|
|
209
|
+
}).join('')
|
|
210
|
+
|
|
211
|
+
const noteBar = hlRow
|
|
212
|
+
? '<div class="hl-note">已高亮:' + renderName(hlRow.name) + ' · 该要素第 ' + (hlRow.rank != null ? hlRow.rank : '-') + ' 名' + (typeof hlRow.value === 'number' ? '(' + hlRow.value.toFixed(2) + ')' : '') + '</div>'
|
|
213
|
+
: ''
|
|
214
|
+
|
|
215
|
+
const tableHtml = (rows, title) =>
|
|
216
|
+
'<div class="table-col">' +
|
|
217
|
+
(title ? '<div class="table-title">' + title + '</div>' : '') +
|
|
218
|
+
'<table><thead><tr><th class="c-rank">排名</th><th class="c-name">角色名</th><th class="c-val">' + esc(r.col.name) + '</th><th class="c-dev">距平</th></tr></thead><tbody>' +
|
|
219
|
+
rowHtml(rows) +
|
|
220
|
+
'</tbody></table></div>'
|
|
221
|
+
|
|
222
|
+
return `<!DOCTYPE html>
|
|
223
|
+
<html lang="zh-CN">
|
|
224
|
+
<head>
|
|
225
|
+
<meta charset="UTF-8">
|
|
226
|
+
<style>
|
|
227
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
228
|
+
body { background:#0d1117; color:#c9d1d9; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:16px; }
|
|
229
|
+
.page { width:100%; margin:0 auto; }
|
|
230
|
+
.title-bar { display:flex; justify-content:center; align-items:center; gap:8px; flex-wrap:wrap; padding:6px 18px; background:rgba(22,27,34,.9); border:1px solid #30363d; border-radius:20px; font-size:20px; margin-bottom:14px; }
|
|
231
|
+
.title-bar .sep { color:#30363d; }
|
|
232
|
+
.title-bar .metric-tag { color:#58a6ff; font-weight:600; }
|
|
233
|
+
.card { background:#161b22; border:1px solid #30363d; border-radius:18px; padding:20px; }
|
|
234
|
+
.hl-note { text-align:center; font-size:14px; color:#ffd76d; font-weight:600; margin:0 0 14px; }
|
|
235
|
+
.tables { display:flex; gap:20px; align-items:flex-start; flex-wrap:wrap; }
|
|
236
|
+
.table-col { flex:1; min-width:320px; }
|
|
237
|
+
.table-title { text-align:center; font-size:14px; color:#d2991d; font-weight:700; margin-bottom:8px; letter-spacing:1px; }
|
|
238
|
+
table { width:100%; border-collapse:collapse; }
|
|
239
|
+
thead th { background:#21262d; color:#8b949e; font-size:12px; font-weight:600; padding:8px 10px; text-align:center; border:1px solid #30363d; letter-spacing:.05em; }
|
|
240
|
+
td { padding:7px 10px; font-size:13px; border:1px solid #21262d; text-align:center; }
|
|
241
|
+
tr:nth-child(even) td { background:rgba(255,255,255,.02); }
|
|
242
|
+
tr.hl td { background:rgba(255,211,109,.10) !important; }
|
|
243
|
+
tr.hl td.c-rank { color:#ffd76d; }
|
|
244
|
+
tr.hl td.c-name { color:#ffd76d; font-weight:700; }
|
|
245
|
+
tr.hl td.c-val { color:#ffd76d; }
|
|
246
|
+
.c-rank { color:#f85149; font-weight:700; font-family:'SF Mono',Consolas,monospace; width:48px; }
|
|
247
|
+
.c-name { text-align:left; font-weight:600; color:#e6edf3; white-space:nowrap; }
|
|
248
|
+
.c-name .morph { color:#8ab4f8; font-size:.85em; font-weight:400; margin-left:.2em; }
|
|
249
|
+
.hl-tag { display:inline-block; margin-left:6px; padding:1px 6px; font-size:10px; color:#0d1117; background:#ffd76d; border-radius:4px; font-weight:700; vertical-align:middle; }
|
|
250
|
+
.c-val { font-family:'SF Mono',Consolas,monospace; color:#79c0ff; }
|
|
251
|
+
.c-dev { font-family:'SF Mono',Consolas,monospace; color:#8b949e; }
|
|
252
|
+
.c-dev:not(:empty) { color:#3fb950; }
|
|
253
|
+
.na { color:#484f58; font-style:italic; }
|
|
254
|
+
.foot { text-align:center; color:#8b949e; font-size:12px; margin-top:14px; opacity:.8; }
|
|
255
|
+
</style>
|
|
256
|
+
</head>
|
|
257
|
+
<body>
|
|
258
|
+
<div class="page">
|
|
259
|
+
<div class="title-bar">
|
|
260
|
+
<span>${esc(r.campLabel)}</span><span class="sep">·</span>
|
|
261
|
+
<span>基础数据</span><span class="sep">·</span>
|
|
262
|
+
<span>${esc(r.col.name)}</span><span class="sep">·</span>
|
|
263
|
+
<span class="metric-tag">${r.col.sortType === 'desc' ? '越大越优' : '越小越优'} · 平均 ${r.avg.toFixed(2)} · ${r.total} 位</span>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="card">
|
|
266
|
+
${noteBar}
|
|
267
|
+
<div class="tables">
|
|
268
|
+
${tableHtml(left, left.length ? '排名 1-' + left.length : '')}
|
|
269
|
+
${tableHtml(right, right.length ? '排名 ' + (half + 1) + '-' + r.rows.length : '')}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
<div class="foot">基础数据排名 · 数据根据 BWIKI 基础数据计算 · 无数据不计入排名</div>
|
|
273
|
+
</div>
|
|
274
|
+
</body>
|
|
275
|
+
</html>`
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildCharacterHtml(d, config) {
|
|
279
|
+
const width = config.width || 1300
|
|
280
|
+
const { row, items } = d
|
|
281
|
+
const charName = row.name.replace(/["“”]/g, '')
|
|
282
|
+
const nameHtml = renderName(charName)
|
|
283
|
+
|
|
284
|
+
const cellHtml = items.map(it => {
|
|
285
|
+
const up = it.col.sortType === 'desc'
|
|
286
|
+
const rankText = it.num && it.rank != null ? '#' + it.rank + ' / ' + it.total : '--'
|
|
287
|
+
return '<div class="cell">' +
|
|
288
|
+
'<div class="cell-top"><span class="cell-label">' + esc(it.col.name) + '</span><span class="cell-dir ' + (up ? 'up' : 'down') + '">' + (up ? '↑ 越大越优' : '↓ 越小越优') + '</span></div>' +
|
|
289
|
+
'<div class="cell-val">' + (it.num ? it.value.toFixed(2) : '<span class="na">' + esc(it.value || '无数据') + '</span>') + '</div>' +
|
|
290
|
+
'<div class="cell-rank">' + rankText + '</div>' +
|
|
291
|
+
'</div>'
|
|
292
|
+
}).join('')
|
|
293
|
+
|
|
294
|
+
return `<!DOCTYPE html>
|
|
295
|
+
<html lang="zh-CN">
|
|
296
|
+
<head>
|
|
297
|
+
<meta charset="UTF-8">
|
|
298
|
+
<style>
|
|
299
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
300
|
+
body { background:#0d1117; color:#c9d1d9; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:16px; }
|
|
301
|
+
.page { width:100%; margin:0 auto; }
|
|
302
|
+
.title-bar { display:flex; justify-content:center; align-items:center; gap:8px; flex-wrap:wrap; padding:6px 18px; background:rgba(22,27,34,.9); border:1px solid #30363d; border-radius:20px; font-size:20px; margin-bottom:14px; }
|
|
303
|
+
.title-bar .sep { color:#30363d; }
|
|
304
|
+
.title-bar .metric-tag { color:#58a6ff; font-weight:600; }
|
|
305
|
+
.card { background:#161b22; border:1px solid #30363d; border-radius:18px; padding:22px 24px; }
|
|
306
|
+
.hero { display:flex; flex-direction:column; align-items:center; gap:8px; text-align:center; margin-bottom:16px; }
|
|
307
|
+
.char-avatar { width:84px; height:84px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:38px; font-weight:700; color:#e6edf3; background:linear-gradient(135deg,#1c2333,#22304a); border:2px solid rgba(88,166,255,.35); box-shadow:0 0 30px rgba(88,166,255,.15); }
|
|
308
|
+
.char-name { font-size:30px; font-weight:800; letter-spacing:1px; color:#e6edf3; }
|
|
309
|
+
.char-name .morph { color:#8ab4f8; font-size:.75em; font-weight:500; margin-left:.25em; }
|
|
310
|
+
.char-sub { font-size:13px; color:#8b949e; }
|
|
311
|
+
.note { display:flex; align-items:center; gap:8px; font-size:13px; line-height:1.6; color:#d2991d; background:rgba(210,153,29,.08); border:1px solid rgba(210,153,29,.25); border-radius:10px; padding:10px 14px; margin-bottom:16px; }
|
|
312
|
+
.note b { color:#ffd76d; }
|
|
313
|
+
.grid { display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:10px; }
|
|
314
|
+
.cell { background:rgba(255,255,255,.03); border:1px solid #21262d; border-radius:12px; padding:10px 14px; }
|
|
315
|
+
.cell-top { display:flex; justify-content:space-between; align-items:center; gap:8px; margin-bottom:6px; }
|
|
316
|
+
.cell-label { font-size:13px; color:#8b949e; font-weight:600; }
|
|
317
|
+
.cell-dir { font-size:10px; font-weight:700; padding:1px 6px; border-radius:4px; flex-shrink:0; }
|
|
318
|
+
.cell-dir.up { color:#79c0ff; background:rgba(88,166,255,.12); }
|
|
319
|
+
.cell-dir.down { color:#ffa657; background:rgba(255,166,87,.12); }
|
|
320
|
+
.cell-val { font-size:22px; font-weight:800; font-family:'SF Mono',Consolas,monospace; color:#e6edf3; }
|
|
321
|
+
.cell-val .na { font-size:15px; color:#484f58; font-weight:400; font-style:italic; }
|
|
322
|
+
.cell-rank { font-size:13px; color:#58a6ff; font-family:'SF Mono',Consolas,monospace; margin-top:4px; }
|
|
323
|
+
.foot { text-align:center; color:#8b949e; font-size:12px; margin-top:14px; opacity:.8; }
|
|
324
|
+
</style>
|
|
325
|
+
</head>
|
|
326
|
+
<body>
|
|
327
|
+
<div class="page">
|
|
328
|
+
<div class="title-bar">
|
|
329
|
+
<span>基础数据</span><span class="sep">·</span>
|
|
330
|
+
<span>${esc(campLabel('hunter'))}</span><span class="sep">·</span>
|
|
331
|
+
<span class="metric-tag">全部项目 · 阵营内排名</span>
|
|
332
|
+
</div>
|
|
333
|
+
<div class="card">
|
|
334
|
+
<div class="hero">
|
|
335
|
+
<div class="char-avatar">${esc(charName.charAt(0))}</div>
|
|
336
|
+
<div class="char-name">${nameHtml}</div>
|
|
337
|
+
<div class="char-sub">${d.items.length} 项数据 · ${d.numCount} 项有数值 · 其余无数据/未公布</div>
|
|
338
|
+
</div>
|
|
339
|
+
<div class="note">
|
|
340
|
+
<b>排名规则:</b>距离类数据(普攻距离、蓄力距离、气球刀距离)数值<b>越大</b>排名越高;其他数据数值<b>越小</b>排名越高。数值相同排名并列。
|
|
341
|
+
</div>
|
|
342
|
+
<div class="grid">${cellHtml}</div>
|
|
343
|
+
</div>
|
|
344
|
+
<div class="foot">数据根据 BWIKI 基础数据计算</div>
|
|
345
|
+
</div>
|
|
346
|
+
</body>
|
|
347
|
+
</html>`
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function buildTabsHtml(camps, config) {
|
|
351
|
+
const width = config.width || 1300
|
|
352
|
+
const sections = camps.map(camp => {
|
|
353
|
+
const names = camp.columns && camp.columns.length ? camp.columns : FALLBACK_COLUMNS[camp.key]
|
|
354
|
+
const chips = names.map(n => '<span class="chip">' + esc(n) + '</span>').join('')
|
|
355
|
+
return '<div class="camp-block"><div class="camp-label">' + esc(camp.label) + '(' + names.length + ')</div><div class="chips">' + chips + '</div></div>'
|
|
356
|
+
}).join('')
|
|
357
|
+
return `<!DOCTYPE html>
|
|
358
|
+
<html lang="zh-CN">
|
|
359
|
+
<head>
|
|
360
|
+
<meta charset="UTF-8">
|
|
361
|
+
<style>
|
|
362
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
363
|
+
body { background:#0d1117; color:#c9d1d9; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:16px; }
|
|
364
|
+
.page { width:100%; margin:0 auto; }
|
|
365
|
+
.hero { display:flex; flex-direction:column; align-items:center; gap:8px; text-align:center; margin-bottom:14px; }
|
|
366
|
+
.hero-title { font-size:28px; font-weight:800; letter-spacing:1px; background:linear-gradient(90deg,#79c0ff,#ffd76d); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
|
|
367
|
+
.hero-sub { font-size:13px; color:#8b949e; }
|
|
368
|
+
.card { background:#161b22; border:1px solid #30363d; border-radius:18px; padding:20px 22px; }
|
|
369
|
+
.camp-block { margin-bottom:16px; }
|
|
370
|
+
.camp-block:last-child { margin-bottom:0; }
|
|
371
|
+
.camp-label { font-size:15px; font-weight:700; color:#ffd76d; margin-bottom:8px; display:flex; align-items:center; gap:8px; }
|
|
372
|
+
.camp-label::before { content:''; width:4px; height:15px; border-radius:2px; background:linear-gradient(#ffd76d,#ff8c00); }
|
|
373
|
+
.chips { display:flex; flex-wrap:wrap; gap:7px; }
|
|
374
|
+
.chip { padding:5px 12px; font-size:13px; color:#c9d1d9; background:rgba(88,166,255,.08); border:1px solid rgba(88,166,255,.22); border-radius:999px; }
|
|
375
|
+
.chip:nth-child(4n+1) { background:rgba(210,153,29,.08); border-color:rgba(210,153,29,.3); }
|
|
376
|
+
.foot { text-align:center; color:#8b949e; font-size:12px; margin-top:14px; opacity:.8; }
|
|
377
|
+
</style>
|
|
378
|
+
</head>
|
|
379
|
+
<body>
|
|
380
|
+
<div class="page">
|
|
381
|
+
<div class="hero">
|
|
382
|
+
<div class="hero-title">基础数据排名</div>
|
|
383
|
+
<div class="hero-sub">「基础数据 <项目>」查排名 · 「基础数据 <项目> <角色>」高亮该角色 · 「基础数据 <角色>」查看全部项目</div>
|
|
384
|
+
</div>
|
|
385
|
+
<div class="card">${sections}</div>
|
|
386
|
+
<div class="foot">项目为上方标签名 · 数据根据 BWIKI 基础数据计算</div>
|
|
387
|
+
</div>
|
|
388
|
+
</body>
|
|
389
|
+
</html>`
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
module.exports = {
|
|
393
|
+
CAMP_META,
|
|
394
|
+
campLabel,
|
|
395
|
+
baseName,
|
|
396
|
+
loadCamp,
|
|
397
|
+
parseCSV,
|
|
398
|
+
resolveColumn,
|
|
399
|
+
resolveChar,
|
|
400
|
+
findColumnCandidates,
|
|
401
|
+
findCharCandidates,
|
|
402
|
+
computeRanking,
|
|
403
|
+
computeCharacter,
|
|
404
|
+
buildRankingHtml,
|
|
405
|
+
buildCharacterHtml,
|
|
406
|
+
buildTabsHtml,
|
|
407
|
+
}
|
package/cangtou.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const CACHE_TTL = 60 * 60 * 1000
|
|
4
|
+
const FILES = [
|
|
5
|
+
'char_mapping.json',
|
|
6
|
+
'first_letter_char_mapping.json',
|
|
7
|
+
'full_pinyin_char_mapping.json',
|
|
8
|
+
'char_first_letter_mapping.json',
|
|
9
|
+
'char_full_pinyin_mapping.json',
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
let dataCache = null
|
|
13
|
+
let cacheTs = 0
|
|
14
|
+
|
|
15
|
+
async function fetchText(url, timeout) {
|
|
16
|
+
const ctrl = new AbortController()
|
|
17
|
+
const timer = setTimeout(() => ctrl.abort(), timeout)
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(url, { signal: ctrl.signal })
|
|
20
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
21
|
+
return await res.text()
|
|
22
|
+
} finally {
|
|
23
|
+
clearTimeout(timer)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function loadData(config) {
|
|
28
|
+
if (dataCache && Date.now() - cacheTs < CACHE_TTL) return dataCache
|
|
29
|
+
const base = (config.dataSource || 'https://website.homeworkkun.top').replace(/\/+$/, '')
|
|
30
|
+
const timeout = config.timeout || 15000
|
|
31
|
+
const results = await Promise.all(FILES.map(async f => {
|
|
32
|
+
const url = base + '/' + encodeURI('WIKI/图片获取/cangtou/' + f)
|
|
33
|
+
return JSON.parse(await fetchText(url, timeout))
|
|
34
|
+
}))
|
|
35
|
+
dataCache = {
|
|
36
|
+
charMapping: results[0],
|
|
37
|
+
firstLetterCharMapping: results[1],
|
|
38
|
+
fullPinyinCharMapping: results[2],
|
|
39
|
+
charFirstLetterMapping: results[3],
|
|
40
|
+
charFullPinyinMapping: results[4],
|
|
41
|
+
}
|
|
42
|
+
cacheTs = Date.now()
|
|
43
|
+
return dataCache
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pickItems(d, chars, typeFilter) {
|
|
47
|
+
const seen = new Set()
|
|
48
|
+
const all = []
|
|
49
|
+
for (const c of chars) {
|
|
50
|
+
for (const it of (d.charMapping[c] || [])) {
|
|
51
|
+
if (typeFilter !== '所有' && it.type !== typeFilter) continue
|
|
52
|
+
if (seen.has(it.item_full_title)) continue
|
|
53
|
+
seen.add(it.item_full_title)
|
|
54
|
+
all.push(it)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return all
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildSegments(d, text, mode) {
|
|
61
|
+
const segs = []
|
|
62
|
+
for (const ch of String(text || '')) {
|
|
63
|
+
if (/[\u4e00-\u9fff]/.test(ch)) {
|
|
64
|
+
if (mode === '谐音') {
|
|
65
|
+
const py = d.charFullPinyinMapping[ch] || ''
|
|
66
|
+
segs.push({ input: ch, tag: py, chars: py ? (d.fullPinyinCharMapping[py] || []) : [] })
|
|
67
|
+
} else {
|
|
68
|
+
const initial = d.charFirstLetterMapping[ch] || ''
|
|
69
|
+
segs.push({ input: ch, tag: initial, chars: initial ? (d.firstLetterCharMapping[initial] || []) : [] })
|
|
70
|
+
}
|
|
71
|
+
} else if (/[a-z]/i.test(ch) && mode !== '谐音') {
|
|
72
|
+
const l = ch.toLowerCase()
|
|
73
|
+
segs.push({ input: ch, tag: l, chars: d.firstLetterCharMapping[l] || [] })
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return segs
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolve(d, text, mode, typeFilter) {
|
|
80
|
+
return buildSegments(d, text, mode).slice(0, 50).map(seg => {
|
|
81
|
+
const items = pickItems(d, seg.chars, typeFilter)
|
|
82
|
+
return Object.assign({}, seg, { item: items.length ? items[Math.floor(Math.random() * items.length)] : null })
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveAll(d, text, mode, typeFilter) {
|
|
87
|
+
return buildSegments(d, text, mode).slice(0, 10).map(seg => {
|
|
88
|
+
return Object.assign({}, seg, { items: pickItems(d, seg.chars, typeFilter) })
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function itemText(item) {
|
|
93
|
+
return item.item_display_title + '(' + item.role + '·' + item.quality + '·' + item.type + ')'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildText(result, text, mode) {
|
|
97
|
+
const lines = []
|
|
98
|
+
if (!result.length) return '未识别到可用的汉字或字母'
|
|
99
|
+
lines.push('【藏头·' + mode + '】' + String(text || '').trim())
|
|
100
|
+
result.forEach(seg => {
|
|
101
|
+
const line = mode === '谐音'
|
|
102
|
+
? seg.input + '(' + seg.tag + ')'
|
|
103
|
+
: seg.tag.toUpperCase() + ' ' + seg.input
|
|
104
|
+
lines.push(line + ' → ' + (seg.item ? itemText(seg.item) : '无匹配'))
|
|
105
|
+
})
|
|
106
|
+
return lines.join('\n')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function esc(s) {
|
|
110
|
+
return String(s == null ? '' : s)
|
|
111
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
112
|
+
.replace(/"/g, '"').replace(/'/g, ''')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildFullHtml(result, text, mode, config) {
|
|
116
|
+
const width = config.width || 1300
|
|
117
|
+
const blocks = result.map(seg => {
|
|
118
|
+
const head = mode === '谐音'
|
|
119
|
+
? esc(seg.input) + '<span class="pinyin">(' + esc(seg.tag) + ')</span>'
|
|
120
|
+
: '<span class="initial">' + esc(seg.tag.toUpperCase()) + '</span> ' + esc(seg.input)
|
|
121
|
+
const body = seg.items && seg.items.length
|
|
122
|
+
? seg.items.map(it => '<span class="chip">' + esc(it.item_display_title) + '<span class="meta">' + esc(it.role + ' · ' + it.quality + ' · ' + it.type) + '</span></span>').join('')
|
|
123
|
+
: '<span class="none">无匹配</span>'
|
|
124
|
+
return '<div class="block"><div class="b-head">' + head + '<span class="cnt">' + (seg.items ? seg.items.length : 0) + ' 个</span></div><div class="chips">' + body + '</div></div>'
|
|
125
|
+
}).join('')
|
|
126
|
+
|
|
127
|
+
return `<!DOCTYPE html>
|
|
128
|
+
<html lang="zh-CN">
|
|
129
|
+
<head>
|
|
130
|
+
<meta charset="UTF-8">
|
|
131
|
+
<style>
|
|
132
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
133
|
+
body { background:#0d1117; color:#c9d1d9; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:16px; }
|
|
134
|
+
.page { width:100%; margin:0 auto; }
|
|
135
|
+
.title-bar { display:flex; justify-content:center; align-items:center; gap:10px; flex-wrap:wrap; padding:8px 20px; background:rgba(22,27,34,.9); border:1px solid #30363d; border-radius:20px; font-size:30px; margin-bottom:14px; }
|
|
136
|
+
.title-bar .sep { color:#30363d; }
|
|
137
|
+
.card { background:#161b22; border:1px solid #30363d; border-radius:18px; padding:20px 24px; }
|
|
138
|
+
.block { margin-bottom:18px; }
|
|
139
|
+
.block:last-child { margin-bottom:0; }
|
|
140
|
+
.b-head { font-size:26px; font-weight:800; color:#ffd76d; margin-bottom:10px; display:flex; align-items:center; gap:12px; }
|
|
141
|
+
.b-head .initial { color:#79c0ff; font-family:'SF Mono',Consolas,monospace; }
|
|
142
|
+
.b-head .pinyin { font-size:18px; color:#8b949e; font-weight:400; font-family:'SF Mono',Consolas,monospace; margin-left:4px; }
|
|
143
|
+
.b-head .cnt { font-size:16px; font-weight:400; color:#8b949e; margin-left:auto; }
|
|
144
|
+
.chips { display:flex; flex-wrap:wrap; gap:8px; }
|
|
145
|
+
.chip { display:inline-flex; align-items:baseline; gap:6px; padding:5px 14px; font-size:20px; font-weight:600; color:#e6edf3; background:rgba(88,166,255,.07); border:1px solid rgba(88,166,255,.22); border-radius:999px; }
|
|
146
|
+
.chip .meta { font-size:14px; font-weight:400; color:#8b949e; }
|
|
147
|
+
.chip:nth-child(3n+1) { background:rgba(210,153,29,.07); border-color:rgba(210,153,29,.28); }
|
|
148
|
+
.none { font-size:20px; color:#8b949e; }
|
|
149
|
+
.foot { text-align:center; color:#8b949e; font-size:12px; margin-top:14px; opacity:.8; }
|
|
150
|
+
</style>
|
|
151
|
+
</head>
|
|
152
|
+
<body>
|
|
153
|
+
<div class="page">
|
|
154
|
+
<div class="title-bar">
|
|
155
|
+
<span>藏头完整</span><span class="sep">·</span>
|
|
156
|
+
<span>${esc(mode)}</span><span class="sep">·</span>
|
|
157
|
+
<span>${esc(String(text || '').trim())}</span>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="card">${blocks || '<div class="none">未识别到可用的汉字或字母</div>'}</div>
|
|
160
|
+
<div class="foot">数据根据 BWIKI 时装/随身物品池计算</div>
|
|
161
|
+
</div>
|
|
162
|
+
</body>
|
|
163
|
+
</html>`
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { loadData, resolve, resolveAll, buildText, buildFullHtml }
|