koishi-plugin-dwrgbwiki 1.3.0 → 1.3.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/gacha.js +1501 -0
- package/index.js +163 -6
- package/lib.js +19 -11
- package/package.json +2 -1
package/gacha.js
ADDED
|
@@ -0,0 +1,1501 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
// ===== 常量 =====
|
|
7
|
+
|
|
8
|
+
const RESOURCE_TYPES = {
|
|
9
|
+
inspiration: '灵感',
|
|
10
|
+
clue: '线索',
|
|
11
|
+
fragments: '碎片',
|
|
12
|
+
rank_treasure: '排位珍宝',
|
|
13
|
+
huoqi: '火漆',
|
|
14
|
+
nightingale: '夜莺祝福',
|
|
15
|
+
ink: '墨迹',
|
|
16
|
+
manuscript: '未竟手稿',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CACHE_TTL = 15 * 60 * 1000
|
|
20
|
+
const cache = new Map()
|
|
21
|
+
|
|
22
|
+
// ===== 内部工具 =====
|
|
23
|
+
|
|
24
|
+
async function fetchText(fullUrl, timeout) {
|
|
25
|
+
const hit = cache.get(fullUrl)
|
|
26
|
+
if (hit && Date.now() - hit.ts < hit.ttl) return hit.text
|
|
27
|
+
const ctrl = new AbortController()
|
|
28
|
+
const timer = setTimeout(() => ctrl.abort(), timeout || 15000)
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(fullUrl, { signal: ctrl.signal })
|
|
31
|
+
if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + fullUrl)
|
|
32
|
+
const text = await res.text()
|
|
33
|
+
cache.set(fullUrl, { text, ts: Date.now(), ttl: CACHE_TTL })
|
|
34
|
+
return text
|
|
35
|
+
} finally {
|
|
36
|
+
clearTimeout(timer)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function fetchJson(fullUrl, timeout) {
|
|
41
|
+
return JSON.parse(await fetchText(fullUrl, timeout))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function esc(s) {
|
|
45
|
+
return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ===== 数据源解析(与 lib.js 相同的双源逻辑) =====
|
|
49
|
+
|
|
50
|
+
const KNOWN_SOURCES = [
|
|
51
|
+
'https://website.homeworkkun.top',
|
|
52
|
+
'https://web.homeworkkun.top',
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
async function resolveSource(config) {
|
|
56
|
+
const primary = (config && config.dataSource || KNOWN_SOURCES[0]).replace(/\/+$/, '')
|
|
57
|
+
const candidates = [primary]
|
|
58
|
+
KNOWN_SOURCES.forEach(s => { const x = s.replace(/\/+$/, ''); if (!candidates.includes(x)) candidates.push(x) })
|
|
59
|
+
let lastErr = null
|
|
60
|
+
for (const base of candidates) {
|
|
61
|
+
try {
|
|
62
|
+
const text = await fetchText(url(base, '/抽卡/more/list.json'), config.timeout)
|
|
63
|
+
const j = JSON.parse(text)
|
|
64
|
+
if (Array.isArray(j)) return { base, sourceLabel: base.replace(/^https?:\/\//, '') }
|
|
65
|
+
} catch (e) { lastErr = e }
|
|
66
|
+
}
|
|
67
|
+
throw lastErr || new Error('所有数据源均不可用')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function url(base, path2) {
|
|
71
|
+
return base.replace(/\/+$/, '') + '/' + String(path2).replace(/^\/+/, '')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ===== 资源持久化(优先 Koishi 数据库,回退文件) =====
|
|
75
|
+
|
|
76
|
+
class Store {
|
|
77
|
+
constructor(ctx, file) {
|
|
78
|
+
this.ctx = ctx || null
|
|
79
|
+
this.file = file || ''
|
|
80
|
+
this.cache = new Map() // userId -> 用户数据对象(内存缓存)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getUser(userId) {
|
|
84
|
+
const key = String(userId)
|
|
85
|
+
if (this.cache.has(key)) return this.cache.get(key)
|
|
86
|
+
let user = null
|
|
87
|
+
if (this.ctx && this.ctx.database) {
|
|
88
|
+
try {
|
|
89
|
+
const rows = await this.ctx.database.get('gacha_data', { id: key })
|
|
90
|
+
if (rows && rows.length && rows[0].data) user = JSON.parse(rows[0].data)
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error('[gacha] 数据库读取失败,尝试文件:', e.message)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!user && this.file) {
|
|
96
|
+
try {
|
|
97
|
+
if (fs.existsSync(this.file)) {
|
|
98
|
+
const all = JSON.parse(fs.readFileSync(this.file, 'utf8'))
|
|
99
|
+
user = all && all.users ? all.users[key] : null
|
|
100
|
+
}
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error('[gacha] 文件读取失败:', e.message)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!user || typeof user !== 'object') user = { resources: {}, owned: {}, pools: {}, daily: {} }
|
|
106
|
+
this.cache.set(key, user)
|
|
107
|
+
return user
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async save(userId) {
|
|
111
|
+
const key = String(userId)
|
|
112
|
+
const user = this.cache.get(key)
|
|
113
|
+
if (!user) return
|
|
114
|
+
if (this.ctx && this.ctx.database) {
|
|
115
|
+
try {
|
|
116
|
+
await this.ctx.database.upsert('gacha_data', [{ id: key, data: JSON.stringify(user) }])
|
|
117
|
+
return
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.error('[gacha] 数据库写入失败,回退文件:', e.message)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (this.file) {
|
|
123
|
+
try {
|
|
124
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true })
|
|
125
|
+
let all = { users: {} }
|
|
126
|
+
if (fs.existsSync(this.file)) {
|
|
127
|
+
try { all = JSON.parse(fs.readFileSync(this.file, 'utf8')) } catch (e) { all = { users: {} } }
|
|
128
|
+
}
|
|
129
|
+
if (!all.users) all.users = {}
|
|
130
|
+
all.users[key] = user
|
|
131
|
+
const tmp = this.file + '.tmp'
|
|
132
|
+
fs.writeFileSync(tmp, JSON.stringify(all))
|
|
133
|
+
fs.renameSync(tmp, this.file)
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error('[gacha] 文件保存失败:', e.message)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async resetUser(userId) {
|
|
141
|
+
const key = String(userId)
|
|
142
|
+
const user = { resources: {}, owned: {}, pools: {}, daily: {} }
|
|
143
|
+
this.cache.set(key, user)
|
|
144
|
+
if (this.ctx && this.ctx.database) {
|
|
145
|
+
try {
|
|
146
|
+
await this.ctx.database.upsert('gacha_data', [{ id: key, data: JSON.stringify(user) }])
|
|
147
|
+
return
|
|
148
|
+
} catch (e) { /* 忽略 */ }
|
|
149
|
+
}
|
|
150
|
+
if (this.file) await this.save(key)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ===== 卡池数据加载 =====
|
|
155
|
+
|
|
156
|
+
// 卡池类型定义
|
|
157
|
+
// type: 数据类型标识; listPath: list.json 路径; poolPath: pool.json 路径模板
|
|
158
|
+
const POOL_TYPES = {
|
|
159
|
+
jinghua: { label: '精华', listPath: '/抽卡/more/list.json', poolPath: '/抽卡/pools/{id}/pool.json', probPath: '/抽卡/pools/{id}/possibility.json', listFilter: v => v.version === 'V1.html' },
|
|
160
|
+
paivei: { label: '排位珍宝', listPath: '/抽卡/more/list.json', poolPath: '/抽卡/pools/{id}/pool.json', probPath: '/抽卡/pools/{id}/possibility.json', listFilter: v => v.version === 'V2.html' },
|
|
161
|
+
xingyun: { label: '幸运之骰', listPath: '/抽卡/XYZT/list.json', poolPath: '/抽卡/XYZT/{id}/pool.json', probPath: '/抽卡/XYZT/{id}/possibility.json' },
|
|
162
|
+
jianying: { label: '鉴影寻宝', listPath: '/抽卡/JYXB/list.json', poolPath: '/抽卡/JYXB/{id}/pool.json', probPath: '/抽卡/JYXB/{id}/possibility.json' },
|
|
163
|
+
huanxiang: { label: '幻象迷途', listPath: '/抽卡/HXMT/pool.json', poolPath: '/抽卡/HXMT/pool.json', probPath: '/抽卡/HXMT/possibility.json' },
|
|
164
|
+
jiaye: { label: '加页手记', listPath: '/抽卡/jiayeshouji/list.json', poolPath: '/抽卡/jiayeshouji/{id}/pool.json' },
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const POOL_TYPE_ALIAS = {
|
|
168
|
+
精华: 'jinghua', 排位: 'paivei', 排位珍宝: 'paivei', 幸运: 'xingyun', 幸运之骰: 'xingyun',
|
|
169
|
+
鉴影: 'jianying', 鉴影寻宝: 'jianying', 幻象: 'huanxiang', 幻象迷途: 'huanxiang', 加页: 'jiaye', 加页手记: 'jiaye',
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function loadPoolList(base, type, timeout) {
|
|
173
|
+
const def = POOL_TYPES[type]
|
|
174
|
+
const j = await fetchJson(url(base, def.listPath), timeout)
|
|
175
|
+
if (Array.isArray(j)) return j
|
|
176
|
+
return [j]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function loadPoolConfig(base, type, id, timeout) {
|
|
180
|
+
const def = POOL_TYPES[type]
|
|
181
|
+
const j = await fetchJson(url(base, def.poolPath.replace('{id}', id)), timeout)
|
|
182
|
+
return j
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function loadProbConfig(base, type, id, timeout) {
|
|
186
|
+
const def = POOL_TYPES[type]
|
|
187
|
+
if (!def.probPath) return null
|
|
188
|
+
try {
|
|
189
|
+
return await fetchJson(url(base, def.probPath.replace('{id}', id)), timeout)
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function findPoolId(list, query) {
|
|
196
|
+
const q = String(query || '').trim().toLowerCase()
|
|
197
|
+
if (!q) return null
|
|
198
|
+
const exact = list.find(x => String(x.id).toLowerCase() === q)
|
|
199
|
+
if (exact) return exact.id
|
|
200
|
+
const num = q.replace(/^第/, '').replace(/期$/, '').replace(/季$/, '')
|
|
201
|
+
const byNum = list.find(x => String(x.id).toLowerCase() === num.toLowerCase() || String(x.id).toLowerCase().replace(/^[a-z]+/i, '') === num.toLowerCase())
|
|
202
|
+
if (byNum) return byNum.id
|
|
203
|
+
const byName = list.find(x => String(x.name || '').includes(q) || q.includes(String(x.name || '').toLowerCase()))
|
|
204
|
+
if (byName) return byName.id
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ===== 抽卡核心 =====
|
|
209
|
+
|
|
210
|
+
// 通用:物品唯一标识
|
|
211
|
+
function itemKey(item) {
|
|
212
|
+
return [item.name, item.type, item.rarity, item.img || ''].join('|')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 记录已抽到物品(存储名称/稀有度/数量/图片,兼容旧版 true 数据),返回是否为重复
|
|
216
|
+
function markOwned(user, item) {
|
|
217
|
+
const key = itemKey(item)
|
|
218
|
+
const prev = user.owned[key]
|
|
219
|
+
const isRepeat = !!prev
|
|
220
|
+
user.owned[key] = {
|
|
221
|
+
name: item.name,
|
|
222
|
+
type: item.type,
|
|
223
|
+
rarity: item.rarity,
|
|
224
|
+
img: item.img || item.icon || '',
|
|
225
|
+
count: (prev && typeof prev === 'object' ? prev.count || 0 : 0) + 1,
|
|
226
|
+
}
|
|
227
|
+
return isRepeat
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 通用状态字段
|
|
231
|
+
function defaultPoolState() {
|
|
232
|
+
return {
|
|
233
|
+
total: 0,
|
|
234
|
+
pityGold: 0, pityPurple: 0, pityBlue: 0, pityFrame: 0,
|
|
235
|
+
lastAPullName: null, consecutiveAPullCount: 0,
|
|
236
|
+
discountRate: 1, discountName: null,
|
|
237
|
+
itemCounts: {},
|
|
238
|
+
pityCounts: {},
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------- V1 精华 ----------
|
|
243
|
+
|
|
244
|
+
function v1Rarity(prob, state, diffA) {
|
|
245
|
+
const p = prob.probabilities || prob
|
|
246
|
+
if (diffA !== 3) {
|
|
247
|
+
const goldChance = p.S[state.pityGold - 1] != null ? p.S[state.pityGold - 1] : p.S[p.S.length - 1]
|
|
248
|
+
if (Math.random() < goldChance) return 'S'
|
|
249
|
+
}
|
|
250
|
+
const purpleChance = p.A[state.pityPurple - 1] != null ? p.A[state.pityPurple - 1] : p.A[p.A.length - 1]
|
|
251
|
+
if (Math.random() < purpleChance) return 'A'
|
|
252
|
+
const blueChance = p.B[state.pityBlue - 1] != null ? p.B[state.pityBlue - 1] : p.B[p.B.length - 1]
|
|
253
|
+
if (Math.random() < blueChance) return 'B'
|
|
254
|
+
const totalCD = (p.C_chance || 0) + (p.D_chance || 0)
|
|
255
|
+
if (totalCD > 0 && Math.random() * totalCD < (p.C_chance || 0)) return 'C'
|
|
256
|
+
return 'D'
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function selectAItem(pool, state, user, diffA) {
|
|
260
|
+
const aPool = pool.items.A || []
|
|
261
|
+
if (!aPool.length) return { name: '未知A级物品', type: '未知', rarity: 'A' }
|
|
262
|
+
if (diffA === 0 || diffA === 3) {
|
|
263
|
+
const unowned = aPool.filter(item => !user.owned[itemKey({ ...item, rarity: 'A' })])
|
|
264
|
+
const pool2 = unowned.length > 0 ? unowned : aPool
|
|
265
|
+
return pool2[Math.floor(Math.random() * pool2.length)]
|
|
266
|
+
}
|
|
267
|
+
if (diffA === 1) {
|
|
268
|
+
if (state.consecutiveAPullCount >= 2 && state.lastAPullName) {
|
|
269
|
+
const diff = aPool.filter(item => item.name !== state.lastAPullName)
|
|
270
|
+
const pool2 = diff.length > 0 ? diff : aPool
|
|
271
|
+
return pool2[Math.floor(Math.random() * pool2.length)]
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return aPool[Math.floor(Math.random() * aPool.length)]
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function pickItem(pool, r, user, state, diffA) {
|
|
278
|
+
let template
|
|
279
|
+
if (r === 'A') {
|
|
280
|
+
template = selectAItem(pool, state, user, diffA)
|
|
281
|
+
if (diffA === 1) {
|
|
282
|
+
if (template.name === state.lastAPullName) state.consecutiveAPullCount++
|
|
283
|
+
else { state.lastAPullName = template.name; state.consecutiveAPullCount = 1 }
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
const items = pool.items[r] || []
|
|
287
|
+
template = items.length > 0 ? items[Math.floor(Math.random() * items.length)] : { name: '未知' + r + '级物品', type: '未知', rarity: r }
|
|
288
|
+
}
|
|
289
|
+
return { ...template, rarity: r }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function v1Draw(pool, prob, state, user) {
|
|
293
|
+
const diffA = pool.diff_A != null ? pool.diff_A : 0
|
|
294
|
+
const pity = pool.pitySettings || {}
|
|
295
|
+
const gold = pity.gold || 0, purple = pity.purple || 0, blue = pity.blue || 0
|
|
296
|
+
|
|
297
|
+
state.total++
|
|
298
|
+
if (diffA !== 3) state.pityGold++
|
|
299
|
+
state.pityPurple++
|
|
300
|
+
state.pityBlue++
|
|
301
|
+
|
|
302
|
+
let r = null
|
|
303
|
+
if (diffA !== 3 && state.pityGold >= gold) r = 'S'
|
|
304
|
+
else if (state.pityPurple >= purple) r = 'A'
|
|
305
|
+
else if (state.pityBlue >= blue) r = 'B'
|
|
306
|
+
if (!r) r = v1Rarity(prob, state, diffA)
|
|
307
|
+
|
|
308
|
+
const item = pickItem(pool, r, user, state, diffA)
|
|
309
|
+
if (r === 'S' && diffA !== 3) state.pityGold = 0
|
|
310
|
+
else if (r === 'A') state.pityPurple = 0
|
|
311
|
+
else if (r === 'B') state.pityBlue = 0
|
|
312
|
+
|
|
313
|
+
const isRepeat = markOwned(user, item)
|
|
314
|
+
return { item, isRepeat, fragments: isRepeat ? (item.repeat || 0) : 0 }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------- V2 排位珍宝(含 F 级) ----------
|
|
318
|
+
|
|
319
|
+
function v2Rarity(prob, state, pool) {
|
|
320
|
+
const p = prob.probabilities || prob
|
|
321
|
+
const goldChance = p.S[state.pityGold - 1] != null ? p.S[state.pityGold - 1] : p.S[p.S.length - 1]
|
|
322
|
+
if (Math.random() < goldChance) return 'S'
|
|
323
|
+
const purpleChance = p.A[state.pityPurple - 1] != null ? p.A[state.pityPurple - 1] : p.A[p.A.length - 1]
|
|
324
|
+
if (Math.random() < purpleChance) return 'A'
|
|
325
|
+
const blueChance = p.B[state.pityBlue - 1] != null ? p.B[state.pityBlue - 1] : p.B[p.B.length - 1]
|
|
326
|
+
if (Math.random() < blueChance) return 'B'
|
|
327
|
+
if (p.F && p.F.length > 0) {
|
|
328
|
+
const frameChance = p.F[state.pityFrame - 1] != null ? p.F[state.pityFrame - 1] : (p.F_chance || 0)
|
|
329
|
+
if (Math.random() < frameChance) return 'F'
|
|
330
|
+
}
|
|
331
|
+
const totalCD = (p.C_chance || 0) + (p.D_chance || 0)
|
|
332
|
+
if (totalCD > 0 && Math.random() * totalCD < (p.C_chance || 0)) return 'C'
|
|
333
|
+
return 'D'
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function v2Draw(pool, prob, state, user) {
|
|
337
|
+
const pity = pool.pitySettings || {}
|
|
338
|
+
const gold = pity.gold || 0, purple = pity.purple || 0, blue = pity.blue || 0
|
|
339
|
+
const frame = pool.frameSettings || {}
|
|
340
|
+
const frameThreshold = frame.pityThreshold || 0
|
|
341
|
+
const diffA = pool.diff_A != null ? pool.diff_A : 0
|
|
342
|
+
|
|
343
|
+
state.total++
|
|
344
|
+
state.pityGold++
|
|
345
|
+
state.pityPurple++
|
|
346
|
+
state.pityBlue++
|
|
347
|
+
state.pityFrame++
|
|
348
|
+
|
|
349
|
+
let r = v2Rarity(prob, state, pool)
|
|
350
|
+
if (state.pityGold >= gold) r = 'S'
|
|
351
|
+
else if (state.pityPurple >= purple) r = r !== 'S' ? 'A' : r
|
|
352
|
+
else if (state.pityBlue >= blue) r = (r !== 'S' && r !== 'A') ? 'B' : r
|
|
353
|
+
else if (frame.enabled && state.pityFrame >= frameThreshold && r !== 'S' && r !== 'A' && r !== 'B') r = 'F'
|
|
354
|
+
|
|
355
|
+
let item
|
|
356
|
+
if (r === 'F') {
|
|
357
|
+
item = { ...(frame.item || {}), rarity: 'F' }
|
|
358
|
+
} else {
|
|
359
|
+
item = pickItem(pool, r, user, state, diffA)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (r === 'S') state.pityGold = 0
|
|
363
|
+
else if (r === 'A') state.pityPurple = 0
|
|
364
|
+
else if (r === 'B') state.pityBlue = 0
|
|
365
|
+
else if (r === 'F') state.pityFrame = 0
|
|
366
|
+
|
|
367
|
+
const isRepeat = markOwned(user, item)
|
|
368
|
+
return { item, isRepeat, fragments: isRepeat ? (item.repeat || 0) : 0 }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ---------- XYZT 幸运之骰 ----------
|
|
372
|
+
|
|
373
|
+
function xingyunDraw(pool, prob, state, user) {
|
|
374
|
+
state.total++
|
|
375
|
+
const completeDrawLimit = pool.completeDrawLimit || 0
|
|
376
|
+
const tiers = (prob && prob.tiers) || []
|
|
377
|
+
|
|
378
|
+
// 达到强制完成限制时,优先从未收集物品中抽取
|
|
379
|
+
if (completeDrawLimit > 0 && state.total >= completeDrawLimit) {
|
|
380
|
+
const allNames = Object.keys(pool.items || {})
|
|
381
|
+
const uncollected = allNames.filter(name => {
|
|
382
|
+
const it = pool.items[name]
|
|
383
|
+
return !user.owned[itemKey({ ...it, name, rarity: it.rarity })]
|
|
384
|
+
})
|
|
385
|
+
if (uncollected.length > 0) {
|
|
386
|
+
const name = uncollected[Math.floor(Math.random() * uncollected.length)]
|
|
387
|
+
const it = pool.items[name]
|
|
388
|
+
return { item: { ...it, name, rarity: it.rarity }, isRepeat: false, fragments: 0, guaranteed: true }
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (tiers.length > 0) {
|
|
393
|
+
const rand = Math.random()
|
|
394
|
+
let cumulative = 0
|
|
395
|
+
let tier = null
|
|
396
|
+
for (const t of tiers) {
|
|
397
|
+
cumulative += t.probability || 0
|
|
398
|
+
if (rand < cumulative) { tier = t; break }
|
|
399
|
+
}
|
|
400
|
+
if (tier) {
|
|
401
|
+
const totalWeight = (tier.items || []).reduce((s, it) => s + (it.weight || 1), 0)
|
|
402
|
+
let r = Math.random() * totalWeight
|
|
403
|
+
let picked = null
|
|
404
|
+
for (const ti of tier.items || []) {
|
|
405
|
+
r -= (ti.weight || 1)
|
|
406
|
+
if (r < 0) { picked = ti; break }
|
|
407
|
+
}
|
|
408
|
+
const fallback = (tier.items || [])[0]
|
|
409
|
+
const name = (picked || fallback || {}).name
|
|
410
|
+
if (name && pool.items[name]) {
|
|
411
|
+
const it = pool.items[name]
|
|
412
|
+
const final = { ...it, name, rarity: it.rarity }
|
|
413
|
+
const isRepeat = markOwned(user, final)
|
|
414
|
+
return { item: final, isRepeat, fragments: isRepeat ? (it.repeat || 0) : 0 }
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 后备:全物品随机
|
|
420
|
+
const names = Object.keys(pool.items || {})
|
|
421
|
+
const name = names[Math.floor(Math.random() * names.length)]
|
|
422
|
+
const it = pool.items[name] || { type: '未知', rarity: 'D' }
|
|
423
|
+
const final = { ...it, name, rarity: it.rarity }
|
|
424
|
+
const isRepeat = markOwned(user, final)
|
|
425
|
+
return { item: final, isRepeat, fragments: isRepeat ? (it.repeat || 0) : 0 }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ---------- JYXB 鉴影寻宝 ----------
|
|
429
|
+
|
|
430
|
+
function jyxbRarity(prob, state, isOld) {
|
|
431
|
+
const p = (prob && prob.probabilities) || {}
|
|
432
|
+
const rand = Math.random()
|
|
433
|
+
if (isOld) {
|
|
434
|
+
if (rand < (p.A ? p.A[0] : 0.01)) return 'A'
|
|
435
|
+
if (rand < ((p.A ? p.A[0] : 0.01) + (p.B ? p.B[0] : 0.05))) return 'B'
|
|
436
|
+
if (rand < ((p.A ? p.A[0] : 0.01) + (p.B ? p.B[0] : 0.05) + (p.C ? p.C[0] : 0.3))) return 'C'
|
|
437
|
+
return 'D'
|
|
438
|
+
}
|
|
439
|
+
if (rand < (p.A ? p.A[0] : 0.05)) return 'A'
|
|
440
|
+
if (rand < ((p.A ? p.A[0] : 0.05) + (p.B ? p.B[0] : 0.2))) return 'B'
|
|
441
|
+
return 'C'
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function jyxbDraw(pool, prob, state, user, isOld) {
|
|
445
|
+
state.total++
|
|
446
|
+
state.pityA++
|
|
447
|
+
let r = jyxbRarity(prob, state, isOld)
|
|
448
|
+
if (state.pityA >= (pool.pitySettings?.gold || 80)) { r = 'A'; state.pityA = 0 }
|
|
449
|
+
const items = pool.items[r] || []
|
|
450
|
+
const item = items.length > 0 ? items[Math.floor(Math.random() * items.length)] : { name: '未知' + r + '级', type: '奖励', rarity: r }
|
|
451
|
+
const final = { ...item, rarity: r }
|
|
452
|
+
const isRepeat = markOwned(user, final)
|
|
453
|
+
|
|
454
|
+
let fragments = 0, clue = 0
|
|
455
|
+
const match = String(item.name || '').match(/\*(\d+)/)
|
|
456
|
+
const amount = match ? parseInt(match[1]) : 0
|
|
457
|
+
if (r === 'A') {
|
|
458
|
+
if (isRepeat) fragments = 200
|
|
459
|
+
} else if (r === 'B') {
|
|
460
|
+
if (isOld) fragments = amount || 10
|
|
461
|
+
else fragments = amount || 10
|
|
462
|
+
} else if (r === 'C') {
|
|
463
|
+
if (isOld) fragments = amount || 10
|
|
464
|
+
else clue = amount || 10
|
|
465
|
+
} else if (r === 'D') {
|
|
466
|
+
clue = amount || 10
|
|
467
|
+
}
|
|
468
|
+
return { item: final, isRepeat, fragments, clue }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ---------- HXMT 幻象迷途 ----------
|
|
472
|
+
|
|
473
|
+
function hxmtRarity(prob) {
|
|
474
|
+
const p = prob.probabilities || prob
|
|
475
|
+
const rand = Math.random()
|
|
476
|
+
let cumulative = 0
|
|
477
|
+
const order = ['senior', 'intermediate', 'junior', 'basic', 'discount']
|
|
478
|
+
for (const tier of order) {
|
|
479
|
+
cumulative += p[tier] || 0
|
|
480
|
+
if (rand < cumulative) return tier
|
|
481
|
+
}
|
|
482
|
+
return 'discount'
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function hxmtDraw(pool, prob, state, user) {
|
|
486
|
+
state.total++
|
|
487
|
+
const r = hxmtRarity(prob)
|
|
488
|
+
const items = pool.items[r] || []
|
|
489
|
+
const item = items.length > 0 ? items[Math.floor(Math.random() * items.length)] : { name: '未知奖励', type: '奖励', rarity: r }
|
|
490
|
+
const final = { ...item, rarity: r }
|
|
491
|
+
let fragments = 0, clue = 0, nightingale = 0
|
|
492
|
+
|
|
493
|
+
if (r === 'discount') {
|
|
494
|
+
// 折扣不参与重复
|
|
495
|
+
} else {
|
|
496
|
+
const isRepeat = markOwned(user, final)
|
|
497
|
+
final.isRepeat = isRepeat
|
|
498
|
+
if (isRepeat) {
|
|
499
|
+
const reward = (pool.repeatRewards && pool.repeatRewards[r]) || 0
|
|
500
|
+
nightingale = reward
|
|
501
|
+
final.repeatBonus = reward
|
|
502
|
+
}
|
|
503
|
+
if (item.type === '线索') {
|
|
504
|
+
const m = String(item.name || '').match(/(\d+)/)
|
|
505
|
+
if (m) clue = parseInt(m[1])
|
|
506
|
+
} else if (item.type === '碎片') {
|
|
507
|
+
const m = String(item.name || '').match(/(\d+)/)
|
|
508
|
+
if (m) fragments = parseInt(m[1])
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return { item: final, fragments, clue, nightingale }
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ---------- jiayeshouji 加页手记 ----------
|
|
515
|
+
|
|
516
|
+
function jiayeDraw(pool, prob, state, user) {
|
|
517
|
+
state.total++
|
|
518
|
+
const allItems = []
|
|
519
|
+
for (const cat of Object.keys(pool.items || {})) {
|
|
520
|
+
for (const it of pool.items[cat] || []) {
|
|
521
|
+
allItems.push({ ...it, cat })
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (!allItems.length) return { item: { name: '未知', type: '未知', rarity: 'D' }, isRepeat: false, manuscript: 0, fragments: 0 }
|
|
525
|
+
|
|
526
|
+
// 增加所有带保底物品的保底计数
|
|
527
|
+
for (const it of allItems) {
|
|
528
|
+
if (it.pityStart) state.pityCounts[it.id || it.name] = (state.pityCounts[it.id || it.name] || 0) + 1
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const maxCount = (it) => {
|
|
532
|
+
const key = it.id || it.name
|
|
533
|
+
return (state.itemCounts[key] || 0) >= (it.maxCount || it.max || Infinity)
|
|
534
|
+
}
|
|
535
|
+
const effWeight = (it) => {
|
|
536
|
+
if (!it.pityStart) return it.weight || 1
|
|
537
|
+
const pc = state.pityCounts[it.id || it.name] || 0
|
|
538
|
+
if (pc <= it.pityStart) return it.weight || 1
|
|
539
|
+
return (it.weight || 1) + (pc - it.pityStart) * (it.pityInc || 10)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 保底触发检查
|
|
543
|
+
for (const it of allItems) {
|
|
544
|
+
if (it.pityGuarantee && !maxCount(it)) {
|
|
545
|
+
const pc = state.pityCounts[it.id || it.name] || 0
|
|
546
|
+
if (pc >= it.pityGuarantee) return applyJiayeItem(pool, it, state, user, true)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 加权随机
|
|
551
|
+
const available = []
|
|
552
|
+
let weightSum = 0
|
|
553
|
+
for (const it of allItems) {
|
|
554
|
+
if (maxCount(it)) continue
|
|
555
|
+
const w = effWeight(it)
|
|
556
|
+
available.push({ item: it, weight: w })
|
|
557
|
+
weightSum += w
|
|
558
|
+
}
|
|
559
|
+
if (weightSum <= 0) {
|
|
560
|
+
const last = allItems[allItems.length - 1]
|
|
561
|
+
return applyJiayeItem(pool, last, state, user, false)
|
|
562
|
+
}
|
|
563
|
+
let rand = Math.random() * weightSum
|
|
564
|
+
let selected = available[available.length - 1]
|
|
565
|
+
for (const a of available) {
|
|
566
|
+
rand -= a.weight
|
|
567
|
+
if (rand < 0) { selected = a; break }
|
|
568
|
+
}
|
|
569
|
+
return applyJiayeItem(pool, selected.item, state, user, false)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function applyJiayeItem(pool, it, state, user, guaranteed) {
|
|
573
|
+
const key = it.id || it.name
|
|
574
|
+
if (it.pityStart) state.pityCounts[key] = 0
|
|
575
|
+
const currentCount = state.itemCounts[key] || 0
|
|
576
|
+
let manuscript = 0
|
|
577
|
+
let fragments = 0
|
|
578
|
+
if (it.isManuscriptDrop) {
|
|
579
|
+
manuscript += 20
|
|
580
|
+
state.itemCounts[key] = currentCount + 1
|
|
581
|
+
} else if (currentCount >= (it.maxCount || it.max || Infinity)) {
|
|
582
|
+
manuscript += 20
|
|
583
|
+
} else {
|
|
584
|
+
state.itemCounts[key] = currentCount + 1
|
|
585
|
+
}
|
|
586
|
+
state.manuscript = (state.manuscript || 0) + 5
|
|
587
|
+
manuscript += 5
|
|
588
|
+
const final = { ...it, rarity: it.rarity || 'D', isGuaranteed: guaranteed }
|
|
589
|
+
return { item: final, isRepeat: currentCount >= (it.maxCount || it.max || Infinity), manuscript, fragments }
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ===== 抽卡入口 =====
|
|
593
|
+
|
|
594
|
+
const DRAWERS = {
|
|
595
|
+
jinghua: { draw: v1Draw, prob: true, isOld: false },
|
|
596
|
+
paivei: { draw: v2Draw, prob: true, isOld: false },
|
|
597
|
+
xingyun: { draw: xingyunDraw, prob: true, isOld: false },
|
|
598
|
+
jianying: { draw: jyxbDraw, prob: true, isOld: true },
|
|
599
|
+
huanxiang: { draw: hxmtDraw, prob: true, isOld: false },
|
|
600
|
+
jiaye: { draw: jiayeDraw, prob: false, isOld: false },
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function gachaPull(config, store, userId, poolType, poolQuery, count) {
|
|
604
|
+
const type = POOL_TYPE_ALIAS[poolType] || poolType
|
|
605
|
+
if (!POOL_TYPES[type]) return { ok: false, error: '未知的抽卡类型:' + poolType }
|
|
606
|
+
const times = Math.min(10, Math.max(1, parseInt(count) || 10))
|
|
607
|
+
|
|
608
|
+
// 卡池未指定时使用配置中的默认卡池(幻象迷途固定 HXMT 无需配置)
|
|
609
|
+
if (type !== 'huanxiang' && (!poolQuery || !String(poolQuery).trim())) {
|
|
610
|
+
const defKey = 'gachaDefault' + type.charAt(0).toUpperCase() + type.slice(1)
|
|
611
|
+
poolQuery = (config && config[defKey]) || ''
|
|
612
|
+
if (!poolQuery) {
|
|
613
|
+
return { ok: false, error: '未指定卡池,请在配置中设置「抽卡-' + (POOL_TYPES[type].label || type) + ' 默认卡池编号」或手动指定' }
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const src = await resolveSource(config)
|
|
618
|
+
const list = await loadPoolList(src.base, type, config.timeout)
|
|
619
|
+
let poolId = null
|
|
620
|
+
if (type === 'huanxiang') {
|
|
621
|
+
poolId = 'HXMT'
|
|
622
|
+
} else {
|
|
623
|
+
poolId = findPoolId(list, poolQuery)
|
|
624
|
+
if (!poolId) {
|
|
625
|
+
const names = list.map(x => x.id).join('、')
|
|
626
|
+
return { ok: false, error: '未找到卡池「' + poolQuery + '」,可用:' + names }
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const pool = await loadPoolConfig(src.base, type, poolId, config.timeout)
|
|
631
|
+
const prob = await loadProbConfig(src.base, type, poolId, config.timeout)
|
|
632
|
+
const drawer = DRAWERS[type]
|
|
633
|
+
|
|
634
|
+
const user = await store.getUser(userId)
|
|
635
|
+
if (!user.pools[poolId]) user.pools[poolId] = defaultPoolState()
|
|
636
|
+
const state = user.pools[poolId]
|
|
637
|
+
|
|
638
|
+
// 每日次数限制:每种抽卡类型每天最多 limit 次(可配置)
|
|
639
|
+
const limit = Math.max(1, Number(config.gachaDailyLimit) || 2)
|
|
640
|
+
const today = new Date()
|
|
641
|
+
const todayStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0')
|
|
642
|
+
if (!user.daily) user.daily = {}
|
|
643
|
+
const daily = user.daily[type] || { date: todayStr, count: 0 }
|
|
644
|
+
if (daily.date !== todayStr) { daily.date = todayStr; daily.count = 0 }
|
|
645
|
+
if (daily.count >= limit) {
|
|
646
|
+
return { ok: false, limit: true, error: '今日' + (POOL_TYPES[type].label || type) + '抽卡次数已达上限(每种每天限 ' + limit + ' 次),明天再来吧' }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const results = []
|
|
650
|
+
let totalFragments = 0, totalClue = 0, totalNightingale = 0, totalManuscript = 0
|
|
651
|
+
for (let i = 0; i < times; i++) {
|
|
652
|
+
const res = drawer.draw(pool, prob, state, user, drawer.isOld)
|
|
653
|
+
results.push(res)
|
|
654
|
+
totalFragments += res.fragments || 0
|
|
655
|
+
totalClue += res.clue || 0
|
|
656
|
+
totalNightingale += res.nightingale || 0
|
|
657
|
+
totalManuscript += res.manuscript || 0
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (totalFragments > 0) user.resources.fragments = (user.resources.fragments || 0) + totalFragments
|
|
661
|
+
if (totalClue > 0) user.resources.clue = (user.resources.clue || 0) + totalClue
|
|
662
|
+
if (totalNightingale > 0) user.resources.nightingale = (user.resources.nightingale || 0) + totalNightingale
|
|
663
|
+
if (totalManuscript > 0) user.resources.manuscript = (user.resources.manuscript || 0) + totalManuscript
|
|
664
|
+
|
|
665
|
+
// HXMT 每抽返还 + 里程碑奖励
|
|
666
|
+
if (type === 'huanxiang') {
|
|
667
|
+
const pullReward = pool.pullReward || 20
|
|
668
|
+
const totalPullReward = times * pullReward
|
|
669
|
+
user.resources.nightingale = (user.resources.nightingale || 0) + totalPullReward
|
|
670
|
+
totalNightingale += totalPullReward
|
|
671
|
+
let milestoneBonus = 0
|
|
672
|
+
for (const m of pool.milestoneRewards || []) {
|
|
673
|
+
if (state.total >= m.pulls && (state.lastMilestone || 0) < m.pulls) milestoneBonus += m.reward
|
|
674
|
+
}
|
|
675
|
+
state.lastMilestone = (pool.milestoneRewards || []).filter(m => state.total >= m.pulls).reduce((mx, m) => Math.max(mx, m.pulls), state.lastMilestone || 0)
|
|
676
|
+
if (milestoneBonus > 0) {
|
|
677
|
+
user.resources.nightingale = (user.resources.nightingale || 0) + milestoneBonus
|
|
678
|
+
totalNightingale += milestoneBonus
|
|
679
|
+
state.milestoneBonus = milestoneBonus
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 记录本次抽卡次数
|
|
684
|
+
daily.count++
|
|
685
|
+
user.daily[type] = daily
|
|
686
|
+
|
|
687
|
+
await store.save(userId)
|
|
688
|
+
|
|
689
|
+
const rarityMap = { S: '稀世', A: '奇珍', B: '独特', C: '罕见', D: '普通', F: '头像框' }
|
|
690
|
+
const itemList = results.map(r => {
|
|
691
|
+
const item = { ...r.item }
|
|
692
|
+
item.rarityLabel = rarityMap[item.rarity] || item.rarity
|
|
693
|
+
item.isRepeat = r.isRepeat
|
|
694
|
+
item.gainFragments = r.fragments || 0
|
|
695
|
+
item.gainClue = r.clue || 0
|
|
696
|
+
item.gainNightingale = r.nightingale || 0
|
|
697
|
+
item.gainManuscript = r.manuscript || 0
|
|
698
|
+
return item
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
ok: true,
|
|
703
|
+
poolId,
|
|
704
|
+
poolName: pool.name || poolId,
|
|
705
|
+
type,
|
|
706
|
+
times,
|
|
707
|
+
dailyLeft: limit - daily.count,
|
|
708
|
+
items: itemList,
|
|
709
|
+
totalFragments, totalClue, totalNightingale, totalManuscript,
|
|
710
|
+
pity: {
|
|
711
|
+
gold: state.pityGold, purple: state.pityPurple, blue: state.pityBlue, frame: state.pityFrame, total: state.total,
|
|
712
|
+
},
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ===== 资源查询 =====
|
|
717
|
+
|
|
718
|
+
const RARITY_LABEL = { S: '稀世', A: '奇珍', B: '独特', C: '罕见', D: '普通', F: '头像框' }
|
|
719
|
+
const RARITY_ORDER = { S: 0, A: 1, B: 2, C: 3, D: 4, F: 5 }
|
|
720
|
+
|
|
721
|
+
async function gachaResources(store, userId) {
|
|
722
|
+
const user = await store.getUser(userId)
|
|
723
|
+
const out = []
|
|
724
|
+
for (const key of Object.keys(RESOURCE_TYPES)) {
|
|
725
|
+
const val = user.resources[key] || 0
|
|
726
|
+
out.push({ key, name: RESOURCE_TYPES[key], value: val })
|
|
727
|
+
}
|
|
728
|
+
// 已抽到的物品(新格式:对象含 name/rarity/count/img;旧格式 true 则从 key 中解析)
|
|
729
|
+
const owned = []
|
|
730
|
+
for (const k of Object.keys(user.owned || {})) {
|
|
731
|
+
const v = user.owned[k]
|
|
732
|
+
if (v && typeof v === 'object') {
|
|
733
|
+
owned.push({ name: v.name || k.split('|')[0], rarity: v.rarity || k.split('|')[2] || '', img: v.img || k.split('|')[3] || '', count: v.count || 1 })
|
|
734
|
+
} else {
|
|
735
|
+
const parts = k.split('|')
|
|
736
|
+
owned.push({ name: parts[0], rarity: parts[2] || '', img: parts[3] || '', count: 1 })
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
owned.sort((a, b) => (RARITY_ORDER[a.rarity] ?? 9) - (RARITY_ORDER[b.rarity] ?? 9) || String(a.name).localeCompare(String(b.name), 'zh'))
|
|
740
|
+
return { ok: true, resources: out, owned }
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ===== 抽卡资源/已抽物品 图片 HTML =====
|
|
744
|
+
|
|
745
|
+
function buildResourcesHtml(data, config) {
|
|
746
|
+
const width = config.gachaWidth || config.width || 1300
|
|
747
|
+
const cardW = Math.min(width, 1700)
|
|
748
|
+
const colors = {
|
|
749
|
+
S: '#e8c668', A: '#b568e8', B: '#689de8', C: '#65a30d', D: '#9ca3af', F: '#689de8',
|
|
750
|
+
}
|
|
751
|
+
const labels = {
|
|
752
|
+
S: '稀世', A: '奇珍', B: '独特', C: '罕见', D: '普通', F: '头像框',
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const resHtml = (data.resources || []).map(r =>
|
|
756
|
+
'<span class="res">' + esc(r.name) + ' <b>' + r.value + '</b></span>'
|
|
757
|
+
).join('')
|
|
758
|
+
|
|
759
|
+
const owned = data.owned || []
|
|
760
|
+
const colNum = owned.length <= 3 ? (owned.length || 1) : 10
|
|
761
|
+
const cardHtml = owned.map(it => {
|
|
762
|
+
const color = colors[it.rarity] || '#9ca3af'
|
|
763
|
+
const label = labels[it.rarity] || it.rarity
|
|
764
|
+
const imgHtml = it.img
|
|
765
|
+
? '<img src="' + esc(it.img) + '" onerror="this.style.visibility=\'hidden\'">'
|
|
766
|
+
: '<div class="noimg">' + esc(String(it.name).charAt(0)) + '</div>'
|
|
767
|
+
return '<div class="card" style="--c:' + color + '">' +
|
|
768
|
+
'<div class="card-img">' + imgHtml + '</div>' +
|
|
769
|
+
'<div class="card-name">' + esc(it.name) + '</div>' +
|
|
770
|
+
'<div class="card-type" style="color:' + color + '">' + esc(label) + (it.count > 1 ? ' ×' + it.count : '') + '</div>' +
|
|
771
|
+
'</div>'
|
|
772
|
+
}).join('')
|
|
773
|
+
|
|
774
|
+
const emptyHtml = owned.length === 0
|
|
775
|
+
? '<div class="empty">还没有抽到任何物品,快去抽卡吧!</div>'
|
|
776
|
+
: ''
|
|
777
|
+
|
|
778
|
+
return `<!DOCTYPE html>
|
|
779
|
+
<html lang="zh-CN">
|
|
780
|
+
<head>
|
|
781
|
+
<meta charset="UTF-8">
|
|
782
|
+
<style>
|
|
783
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
784
|
+
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:30px 20px; color:#e6edf3;
|
|
785
|
+
background:radial-gradient(1200px 600px at 50% -10%, #1b2a4a 0%, #10182b 45%, #0a0e17 100%); }
|
|
786
|
+
.page { max-width:${cardW}px; margin:0 auto; }
|
|
787
|
+
.head { text-align:center; margin-bottom:20px; }
|
|
788
|
+
.title { font-size:26px; font-weight:800; letter-spacing:1px; color:#fff;
|
|
789
|
+
background:linear-gradient(90deg, #79c0ff, #ffd76d); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
|
|
790
|
+
.sub { font-size:13px; color:#8b949e; margin-top:6px; }
|
|
791
|
+
.res-bar { display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin-bottom:22px; }
|
|
792
|
+
.res { font-size:13px; color:#8b949e; background:rgba(139,148,158,.08); border:1px solid rgba(139,148,158,.25); border-radius:8px; padding:5px 14px; }
|
|
793
|
+
.res b { color:#e6edf3; margin-left:4px; }
|
|
794
|
+
.grid { display:grid; grid-template-columns:repeat(${colNum}, 1fr); gap:14px; }
|
|
795
|
+
.card { background:rgba(22,27,34,.75); border:1px solid rgba(255,255,255,.09); border-radius:14px; padding:12px 8px;
|
|
796
|
+
text-align:center; overflow:hidden; }
|
|
797
|
+
.card-img { width:54px; height:54px; margin:0 auto 8px; background:#111318; border-radius:10px; display:flex; align-items:center; justify-content:center; overflow:hidden; }
|
|
798
|
+
.card-img img { max-width:100%; max-height:100%; object-fit:contain; filter:drop-shadow(0 0 10px var(--c)) drop-shadow(0 0 20px var(--c)); }
|
|
799
|
+
.noimg { width:100%; height:100%; display:flex; align-items:center; justify-content:center; font-size:22px; font-weight:800; color:var(--c); }
|
|
800
|
+
.card-name { font-size:14.5px; font-weight:700; color:#fff; margin-bottom:3px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
801
|
+
.card-type { font-size:13px; }
|
|
802
|
+
.empty { text-align:center; color:#8b949e; font-size:15px; padding:40px 0; }
|
|
803
|
+
.foot { text-align:center; color:#8b949e; font-size:12px; margin-top:18px; opacity:.75; }
|
|
804
|
+
</style>
|
|
805
|
+
</head>
|
|
806
|
+
<body>
|
|
807
|
+
<div class="page">
|
|
808
|
+
<div class="head">
|
|
809
|
+
<div class="title">抽卡资源</div>
|
|
810
|
+
<div class="sub">已抽物品 ${owned.length} 种</div>
|
|
811
|
+
</div>
|
|
812
|
+
<div class="res-bar">${resHtml}</div>
|
|
813
|
+
${emptyHtml}
|
|
814
|
+
<div class="grid">${cardHtml}</div>
|
|
815
|
+
<div class="foot">抽卡模拟</div>
|
|
816
|
+
</div>
|
|
817
|
+
</body>
|
|
818
|
+
</html>`
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// 重置用户今日抽卡/探索次数为默认值(保留全部资源与卡池进度)
|
|
822
|
+
async function gachaResetDaily(store, userId) {
|
|
823
|
+
const user = await store.getUser(userId)
|
|
824
|
+
if (!user.daily) user.daily = {}
|
|
825
|
+
const today = new Date()
|
|
826
|
+
const todayStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0')
|
|
827
|
+
for (const key of Object.keys(user.daily)) {
|
|
828
|
+
user.daily[key] = { date: todayStr, count: 0 }
|
|
829
|
+
}
|
|
830
|
+
await store.save(userId)
|
|
831
|
+
return { ok: true }
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ===== 抽卡结果图片 HTML =====
|
|
835
|
+
|
|
836
|
+
function buildGachaHtml(data, config) {
|
|
837
|
+
const width = config.gachaWidth || config.width || 1300
|
|
838
|
+
const cardW = Math.min(width, 1100)
|
|
839
|
+
const colors = {
|
|
840
|
+
S: '#e8c668', A: '#b568e8', B: '#689de8', C: '#65a30d', D: '#9ca3af', F: '#689de8',
|
|
841
|
+
senior: '#e8c668', intermediate: '#689de8', junior: '#10b981', basic: '#6b7280', discount: '#f59e0b',
|
|
842
|
+
}
|
|
843
|
+
const labels = {
|
|
844
|
+
S: '稀世', A: '奇珍', B: '独特', C: '罕见', D: '普通', F: '头像框',
|
|
845
|
+
senior: '稀世', intermediate: '奇珍', junior: '罕见', basic: '普通', discount: '折扣',
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const colNum = data.items.length === 1 ? 1 : (data.items.length <= 3 ? data.items.length : 6)
|
|
849
|
+
const cardHtml = data.items.map((it, idx) => {
|
|
850
|
+
const color = colors[it.rarity] || '#9ca3af'
|
|
851
|
+
const label = labels[it.rarity] || it.rarityLabel || it.rarity
|
|
852
|
+
const repeatHtml = it.isRepeat
|
|
853
|
+
? '<div class="repeat">重复转化' + (it.gainFragments || it.gainManuscript || it.gainNightingale || it.gainClue ? ' <b>+' + (it.gainFragments || it.gainManuscript || it.gainNightingale || it.gainClue) + (it.gainFragments ? '碎片' : it.gainManuscript ? '手稿' : it.gainNightingale ? '夜莺祝福' : '线索') + '</b>' : '') + '</div>'
|
|
854
|
+
: ''
|
|
855
|
+
return '<div class="card" style="--c:' + color + '">' +
|
|
856
|
+
'<div class="card-img"><img src="' + esc(it.img || it.icon || '') + '" onerror="this.style.visibility=\'hidden\'"></div>' +
|
|
857
|
+
'<div class="card-body">' +
|
|
858
|
+
'<div class="card-name">' + esc(it.name) + '</div>' +
|
|
859
|
+
'<div class="card-type" style="color:' + color + '">' + esc(label) + (it.type ? ' · ' + esc(it.type) : '') + '</div>' +
|
|
860
|
+
repeatHtml +
|
|
861
|
+
'</div>' +
|
|
862
|
+
'</div>'
|
|
863
|
+
}).join('')
|
|
864
|
+
|
|
865
|
+
const gains = []
|
|
866
|
+
if (data.totalFragments) gains.push('碎片 +' + data.totalFragments)
|
|
867
|
+
if (data.totalClue) gains.push('线索 +' + data.totalClue)
|
|
868
|
+
if (data.totalNightingale) gains.push('夜莺祝福 +' + data.totalNightingale)
|
|
869
|
+
if (data.totalManuscript) gains.push('未竟手稿 +' + data.totalManuscript)
|
|
870
|
+
const gainHtml = gains.length ? '<div class="gains">' + gains.map(g => '<span>' + g + '</span>').join('') + '</div>' : ''
|
|
871
|
+
|
|
872
|
+
return `<!DOCTYPE html>
|
|
873
|
+
<html lang="zh-CN">
|
|
874
|
+
<head>
|
|
875
|
+
<meta charset="UTF-8">
|
|
876
|
+
<style>
|
|
877
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
878
|
+
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:30px 20px; color:#e6edf3;
|
|
879
|
+
background:radial-gradient(1200px 600px at 50% -10%, #1b2a4a 0%, #10182b 45%, #0a0e17 100%); }
|
|
880
|
+
.page { max-width:${cardW}px; margin:0 auto; }
|
|
881
|
+
.head { text-align:center; margin-bottom:20px; }
|
|
882
|
+
.pool-name { font-size:26px; font-weight:800; letter-spacing:1px; color:#fff;
|
|
883
|
+
background:linear-gradient(90deg, #79c0ff, #ffd76d); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
|
|
884
|
+
.pool-sub { font-size:13px; color:#8b949e; margin-top:6px; }
|
|
885
|
+
.grid { display:grid; grid-template-columns:repeat(${colNum}, 1fr); gap:14px; }
|
|
886
|
+
.card { background:rgba(22,27,34,.75); border:1px solid rgba(255,255,255,.09); border-radius:14px; padding:12px 14px;
|
|
887
|
+
display:flex; align-items:center; gap:14px; overflow:hidden; }
|
|
888
|
+
.card-img { width:62px; height:62px; flex-shrink:0; background:#111318; border-radius:10px; display:flex; align-items:center; justify-content:center; overflow:hidden; }
|
|
889
|
+
.card-img img { max-width:100%; max-height:100%; object-fit:contain; filter:drop-shadow(0 0 10px var(--c)) drop-shadow(0 0 20px var(--c)); }
|
|
890
|
+
.card-body { min-width:0; }
|
|
891
|
+
.card-name { font-size:17px; font-weight:700; color:#fff; margin-bottom:3px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
892
|
+
.card-type { font-size:14px; }
|
|
893
|
+
.repeat { margin-top:6px; font-size:12.5px; color:#f85149; background:rgba(248,81,73,.08); border:1px solid rgba(248,81,73,.2); border-radius:6px; padding:3px 8px; display:inline-block; }
|
|
894
|
+
.gains { display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin-top:20px; }
|
|
895
|
+
.gains span { font-size:13px; font-weight:700; color:#3fb950; background:rgba(63,185,80,.08); border:1px solid rgba(63,185,80,.25); border-radius:8px; padding:5px 14px; }
|
|
896
|
+
.foot { text-align:center; color:#8b949e; font-size:12px; margin-top:18px; opacity:.75; }
|
|
897
|
+
.pity { text-align:center; color:#8b949e; font-size:12px; margin-top:8px; }
|
|
898
|
+
.daily { display:inline-block; font-size:12.5px; color:#8b949e; background:rgba(139,148,158,.08); border:1px solid rgba(139,148,158,.25);
|
|
899
|
+
border-radius:8px; padding:4px 12px; margin-top:10px; }
|
|
900
|
+
</style>
|
|
901
|
+
</head>
|
|
902
|
+
<body>
|
|
903
|
+
<div class="page">
|
|
904
|
+
<div class="head">
|
|
905
|
+
<div class="pool-name">${esc(data.poolName)}</div>
|
|
906
|
+
<div class="pool-sub">抽取 ${data.times} 次</div>
|
|
907
|
+
<div>
|
|
908
|
+
<span class="daily">今日${esc(POOL_TYPES[data.type] ? POOL_TYPES[data.type].label : data.type)}剩余次数:${data.dailyLeft}</span>
|
|
909
|
+
</div>
|
|
910
|
+
</div>
|
|
911
|
+
<div class="grid">${cardHtml}</div>
|
|
912
|
+
${gainHtml}
|
|
913
|
+
<div class="pity">保底进度:稀世 ${data.pity.gold} 抽 / 奇珍 ${data.pity.purple} 抽 / 独特 ${data.pity.blue} 抽 / 累计 ${data.pity.total} 抽</div>
|
|
914
|
+
<div class="foot">抽卡模拟</div>
|
|
915
|
+
</div>
|
|
916
|
+
</body>
|
|
917
|
+
</html>`
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// ===== 加页手记·探索 =====
|
|
921
|
+
|
|
922
|
+
const EXPLORE_DIFFS = ['新手', '简单', '普通', '困难']
|
|
923
|
+
|
|
924
|
+
async function loadExploreData(base, timeout) {
|
|
925
|
+
const [weapons, cizhang, cfg, monsters, shenshi, miyan] = await Promise.all([
|
|
926
|
+
fetchJson(url(base, '/抽卡/jiayeshouji/weapon.json'), timeout).catch(() => []),
|
|
927
|
+
fetchJson(url(base, '/抽卡/jiayeshouji/cizhang.json'), timeout).catch(() => []),
|
|
928
|
+
fetchJson(url(base, '/抽卡/jiayeshouji/explore.json'), timeout).catch(() => ({})),
|
|
929
|
+
fetchJson(url(base, '/抽卡/jiayeshouji/monster.json'), timeout).catch(() => []),
|
|
930
|
+
fetchJson(url(base, '/抽卡/jiayeshouji/shenshi.json'), timeout).catch(() => []),
|
|
931
|
+
fetchJson(url(base, '/抽卡/jiayeshouji/miyan.json'), timeout).catch(() => ({})),
|
|
932
|
+
])
|
|
933
|
+
return { weapons, cizhang, cfg, monsters, shenshi, miyan }
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// 复刻 explore.html 的完整探索模拟(自动模式:自动交换、自动夜莺、快速完成)
|
|
937
|
+
function exploreRun(data, diff, equipped) {
|
|
938
|
+
const { weapons, cizhang, cfg, monsters, shenshi, miyan } = data
|
|
939
|
+
const log = []
|
|
940
|
+
const push = (msg, cls) => log.push({ msg, cls: cls || '' })
|
|
941
|
+
const rnd = (min, max) => min + Math.floor(Math.random() * (max - min + 1))
|
|
942
|
+
const fmt = n => (n || 0).toLocaleString()
|
|
943
|
+
|
|
944
|
+
// 难度
|
|
945
|
+
const costs = cfg.difficultyCosts || {}
|
|
946
|
+
const diffCost = costs[diff] || 0
|
|
947
|
+
const te = (cfg.totalEvents && cfg.totalEvents[diff]) || { min: 15, max: 30 }
|
|
948
|
+
const totalEvents = Math.max(3, rnd(te.min, te.max))
|
|
949
|
+
const minVal = (cfg.minCizhangValue || {})[diff] || 0
|
|
950
|
+
const em = cfg.equipmentMultipliers || {}
|
|
951
|
+
const lightSources = em.lightSources || ['提灯', '谎言之眼']
|
|
952
|
+
const hasLight = lightSources.some(s => equipped.includes(s))
|
|
953
|
+
|
|
954
|
+
// 装备效果叠乘
|
|
955
|
+
const weaponCfg = em.weapons || {}
|
|
956
|
+
let deathMult = 1, eventMult = 1
|
|
957
|
+
const noEquipMult = equipped.length === 0 ? (em.noEquipMultiplier || 1.5) : 1
|
|
958
|
+
const darkMult = hasLight ? 1 : (em.noLightPenalty || 1.3)
|
|
959
|
+
const equipSet = [...new Set(equipped)]
|
|
960
|
+
for (const name of equipSet) {
|
|
961
|
+
const c = weaponCfg[name]
|
|
962
|
+
if (!c) continue
|
|
963
|
+
const count = Math.min(c.maxStacks || 99, equipped.filter(n => n === name).length)
|
|
964
|
+
if (c.deathMult !== undefined) deathMult *= Math.pow(c.deathMult, count)
|
|
965
|
+
if (c.eventMult !== undefined) eventMult *= Math.pow(c.eventMult, count)
|
|
966
|
+
}
|
|
967
|
+
const totalEventsEff = Math.max(3, Math.round(totalEvents * eventMult))
|
|
968
|
+
const deathMultiplier = noEquipMult * deathMult * darkMult
|
|
969
|
+
|
|
970
|
+
// 装备价值(出货加成)
|
|
971
|
+
const equipTotal = equipped.reduce((s, n) => s + ((weapons.find(w => w.name === n) || {}).price || 0), 0)
|
|
972
|
+
const eb = cfg.equipBonus || { inkPerBonus: 10000, maxBonus: 30 }
|
|
973
|
+
const equipBonus = Math.min(eb.maxBonus || 30, Math.floor(equipTotal / (eb.inkPerBonus || 10000)))
|
|
974
|
+
|
|
975
|
+
// 状态
|
|
976
|
+
const st = {
|
|
977
|
+
events: 0, totalEvents: totalEventsEff, cizhang: [], hasSecret: false,
|
|
978
|
+
treasureChests: 0, dead: false, treasureRoomFound: false, treasureRoomPending: false,
|
|
979
|
+
pianoFound: false, charBlock: 0, smokePots: 0, smokeUsed: 0,
|
|
980
|
+
nightingaleUsed: false, nightingaleItem: null, killLog: {}, lostEquipValue: 0,
|
|
981
|
+
}
|
|
982
|
+
st.charBlock = equipSet.some(n => (weaponCfg[n] || {}).blockDeath) ? 1 : 0
|
|
983
|
+
st.smokePots = equipped.filter(n => n === '祝祷烟炉').length
|
|
984
|
+
const eventCounts = {}
|
|
985
|
+
|
|
986
|
+
push('📜 探索开始!难度:<b>' + diff + '</b>' + (diffCost > 0 ? '(入场费 ' + fmt(diffCost) + ' 墨迹)' : '') + ',事件数 ' + totalEventsEff)
|
|
987
|
+
push('🎒 携带装备:' + (equipped.length ? equipSet.join('、') : '无(怪物致命率×1.5)'))
|
|
988
|
+
if (!hasLight) push('⚠️ 未携带' + lightSources.join('/') + ':黑暗笼罩,死亡概率 ×' + darkMult)
|
|
989
|
+
if (st.charBlock > 0) push('🛡️ 角色守护:可抵挡 1 次致命伤害')
|
|
990
|
+
if (st.smokePots > 0) push('🕯 祝祷烟炉 ×' + st.smokePots + ':可抵挡致命伤害')
|
|
991
|
+
if (minVal > 0) push('⚠️ 需辞章价值达到 ' + fmt(minVal) + ' 墨迹才能成功撤离')
|
|
992
|
+
push('━━━━━━━━━━━━━━━━━━━━')
|
|
993
|
+
|
|
994
|
+
const addCizhang = (source) => {
|
|
995
|
+
const srcCfg = cfg[source] || {}
|
|
996
|
+
let diffProbs = (srcCfg.cizhangProbs && srcCfg.cizhangProbs[diff]) || (cfg.cizhangProbabilities || {})[diff] || { '华彩': 0.5, '稀世': 2, '奇珍': 8, '独特': 89.5 }
|
|
997
|
+
const adj = {
|
|
998
|
+
'华彩': (diffProbs['华彩'] || 0) + equipBonus * ((eb.华彩Increment !== undefined ? eb.华彩Increment : 0.15) || 0),
|
|
999
|
+
'稀世': (diffProbs['稀世'] || 0) + equipBonus * ((eb.稀世Increment !== undefined ? eb.稀世Increment : 0.3) || 0),
|
|
1000
|
+
'奇珍': (diffProbs['奇珍'] || 0) + equipBonus * ((eb.奇珍Increment !== undefined ? eb.奇珍Increment : 0.2) || 0),
|
|
1001
|
+
'独特': Math.max(0, (diffProbs['独特'] || 0) - equipBonus * Math.abs((eb.独特Decrement !== undefined ? eb.独特Decrement : 0.65) || 0)),
|
|
1002
|
+
}
|
|
1003
|
+
const total = adj['华彩'] + adj['稀世'] + adj['奇珍'] + adj['独特']
|
|
1004
|
+
if (total <= 0) return
|
|
1005
|
+
let r = Math.random() * total, rarity
|
|
1006
|
+
if (r < adj['华彩']) rarity = '华彩'
|
|
1007
|
+
else if (r < adj['华彩'] + adj['稀世']) rarity = '稀世'
|
|
1008
|
+
else if (r < adj['华彩'] + adj['稀世'] + adj['奇珍']) rarity = '奇珍'
|
|
1009
|
+
else rarity = '独特'
|
|
1010
|
+
const rate140w = cfg.rate140w !== undefined ? cfg.rate140w : 0.2
|
|
1011
|
+
if (rarity === '华彩' && Math.random() < rate140w) rarity = '华彩+'
|
|
1012
|
+
const pool = cizhang.filter(c => c.rarity === rarity)
|
|
1013
|
+
if (!pool.length) return
|
|
1014
|
+
const totalWeight = pool.reduce((s, c) => s + (c.weight || 1), 0)
|
|
1015
|
+
let rand = Math.random() * totalWeight
|
|
1016
|
+
const picked = pool.find(c => { rand -= (c.weight || 1); return rand < 0 }) || pool[0]
|
|
1017
|
+
st.cizhang.push(picked)
|
|
1018
|
+
push(' ↳ ' + picked.name + ' (' + rarity + '·' + fmt(picked.value) + '墨迹)', 'gain')
|
|
1019
|
+
// 夜莺自动带回:第一个超过 10 万价值的物品
|
|
1020
|
+
if (!st.nightingaleUsed && picked.value >= 100000) {
|
|
1021
|
+
st.nightingaleUsed = true
|
|
1022
|
+
st.nightingaleItem = picked
|
|
1023
|
+
st.cizhang = st.cizhang.filter(c => c !== picked)
|
|
1024
|
+
push('🕊 <b>夜莺</b>自动带回 <b>' + picked.name + '</b>(' + rarity + '·' + fmt(picked.value) + '墨迹),死亡也不影响!', 'night')
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const pickMonster = (forcedName) => {
|
|
1029
|
+
let monster
|
|
1030
|
+
if (forcedName) monster = monsters.find(m => m.name === forcedName)
|
|
1031
|
+
else if (monsters.length) {
|
|
1032
|
+
const totalW = monsters.reduce((s, m) => s + ((m.weights && m.weights[diff]) || 0), 0)
|
|
1033
|
+
if (totalW > 0) {
|
|
1034
|
+
let r = Math.random() * totalW
|
|
1035
|
+
for (const m of monsters) {
|
|
1036
|
+
r -= (m.weights && m.weights[diff]) || 0
|
|
1037
|
+
if (r <= 0) { monster = m; break }
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (!monster) monster = monsters[monsters.length - 1]
|
|
1041
|
+
}
|
|
1042
|
+
return monster
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const doCombat = (monster, mName, forced) => {
|
|
1046
|
+
const count = monster ? rnd((monster.countRange && monster.countRange[diff] || { min: 1, max: 1 }).min, (monster.countRange && monster.countRange[diff] || { min: 1, max: 1 }).max) : 1
|
|
1047
|
+
const secretMult = st.hasSecret ? 1.5 : 1
|
|
1048
|
+
const monsterKill = monster ? ((monster.killChance && monster.killChance[diff]) || 0.1) : 0.1
|
|
1049
|
+
const killChance = Math.min(1, monsterKill * deathMultiplier * secretMult)
|
|
1050
|
+
const encText = monster && monster.encounters && monster.encounters.length ? monster.encounters[Math.floor(Math.random() * monster.encounters.length)] : '遭遇 <b>' + mName + '</b>'
|
|
1051
|
+
const countText = count > 1 ? '你遇到了<b>' + mName + '</b>×' + count : '你遇到了<b>' + mName + '</b>'
|
|
1052
|
+
if (Math.random() < killChance) {
|
|
1053
|
+
if (st.charBlock > 0) {
|
|
1054
|
+
st.charBlock = 0
|
|
1055
|
+
push('👹 第' + st.events + '次:' + encText + countText + '——<span class="red">致命一击!</span>但被角色抵挡!', 'block')
|
|
1056
|
+
} else if (st.smokePots > 0) {
|
|
1057
|
+
st.smokePots--
|
|
1058
|
+
st.smokeUsed++
|
|
1059
|
+
push('👹 第' + st.events + '次:' + encText + countText + '——<span class="red">致命一击!</span>但被祝祷烟炉抵挡!(剩余 ' + st.smokePots + ' 个)', 'block')
|
|
1060
|
+
} else {
|
|
1061
|
+
st.dead = true
|
|
1062
|
+
st.lostEquipValue = equipped.filter(e => { const w = weapons.find(x => x.name === e); return w && w.type !== 'character' }).reduce((s, n) => s + ((weapons.find(x => x.name === n) || {}).price || 0), 0)
|
|
1063
|
+
push('👹 第' + st.events + '次:' + encText + countText + '——<span class="red">你死了!</span>装备全部丢失。', 'dead')
|
|
1064
|
+
}
|
|
1065
|
+
} else if (Math.random() < 0.5) {
|
|
1066
|
+
push('👹 第' + st.events + '次:' + encText + countText + '——<span class="green">击败怪物!</span>')
|
|
1067
|
+
st.killLog[mName] = (st.killLog[mName] || 0) + count
|
|
1068
|
+
} else {
|
|
1069
|
+
push('👹 第' + st.events + '次:' + encText + countText + '——成功逃脱!')
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// 绅士交换(自动接受)
|
|
1074
|
+
const doExchange = (monster) => {
|
|
1075
|
+
if (!shenshi.length || !st.cizhang.length) return false
|
|
1076
|
+
const cheapest = [...st.cizhang].sort((a, b) => a.value - b.value)[0]
|
|
1077
|
+
const offer = shenshi[Math.floor(Math.random() * shenshi.length)]
|
|
1078
|
+
const idx = st.cizhang.indexOf(cheapest)
|
|
1079
|
+
if (idx !== -1) st.cizhang.splice(idx, 1)
|
|
1080
|
+
st.cizhang.push(offer)
|
|
1081
|
+
push('🤝 第' + st.events + '次:你给了缄默的绅士「' + cheapest.name + '」(' + cheapest.rarity + '·' + fmt(cheapest.value) + '),绅士回赠「' + offer.name + '」(' + offer.rarity + '·' + fmt(offer.value) + ')', 'ex')
|
|
1082
|
+
doCombat(monster, '缄默的绅士')
|
|
1083
|
+
return true
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// 秘言箱(自动:选最便宜独特 2~5 件提交)
|
|
1087
|
+
const doMiyan = () => {
|
|
1088
|
+
if (!miyan || !st.cizhang.length) {
|
|
1089
|
+
push('📦 第' + st.events + '次:发现秘言箱——但你没有可提交的物品。')
|
|
1090
|
+
return
|
|
1091
|
+
}
|
|
1092
|
+
const dudu = st.cizhang
|
|
1093
|
+
.map((c, i) => ({ ...c, _idx: i }))
|
|
1094
|
+
.filter(c => c.rarity === '独特')
|
|
1095
|
+
.sort((a, b) => a.value - b.value)
|
|
1096
|
+
.slice(0, 5)
|
|
1097
|
+
if (dudu.length < 2) {
|
|
1098
|
+
push('📦 第' + st.events + '次:发现秘言箱——但独特物品不足 2 件。')
|
|
1099
|
+
return
|
|
1100
|
+
}
|
|
1101
|
+
const selected = dudu.slice(0, Math.max(2, Math.min(5, dudu.length)))
|
|
1102
|
+
const indices = selected.map(c => c._idx).sort((a, b) => b - a)
|
|
1103
|
+
const offered = indices.map(i => st.cizhang[i])
|
|
1104
|
+
const totalValue = offered.reduce((s, c) => s + c.value, 0)
|
|
1105
|
+
indices.forEach(i => st.cizhang.splice(i, 1))
|
|
1106
|
+
const rarityOrder = ['独特', '奇珍', '稀世', '华彩']
|
|
1107
|
+
const maxRarity = offered.reduce((best, c) => {
|
|
1108
|
+
const ri = rarityOrder.indexOf(c.rarity)
|
|
1109
|
+
const bi = rarityOrder.indexOf(best)
|
|
1110
|
+
return ri > bi ? c.rarity : best
|
|
1111
|
+
}, '独特')
|
|
1112
|
+
const situation = miyan[maxRarity === '华彩' || maxRarity === '稀世' ? 'situation3' : maxRarity === '奇珍' ? 'situation2' : 'situation1']
|
|
1113
|
+
if (!situation) return
|
|
1114
|
+
let level = situation[0]
|
|
1115
|
+
for (const lv of situation) if (totalValue >= (lv.value || 0)) level = lv
|
|
1116
|
+
const probs = { ...level }
|
|
1117
|
+
delete probs.value; delete probs.combine_level; delete probs.desc
|
|
1118
|
+
const totalProb = Object.values(probs).reduce((s, v) => s + v, 0)
|
|
1119
|
+
let r = Math.random() * totalProb
|
|
1120
|
+
let pickedRarity = '独特'
|
|
1121
|
+
for (const [rar, prob] of Object.entries(probs)) {
|
|
1122
|
+
r -= prob
|
|
1123
|
+
if (r <= 0) { pickedRarity = rar; break }
|
|
1124
|
+
}
|
|
1125
|
+
let resultItem
|
|
1126
|
+
if (pickedRarity === '弧光') {
|
|
1127
|
+
resultItem = { name: '弧光', rarity: '奇珍', value: 5000 }
|
|
1128
|
+
} else {
|
|
1129
|
+
const pool = (miyan[pickedRarity] || []).map(c => ({ ...c }))
|
|
1130
|
+
for (const offeredItem of offered) {
|
|
1131
|
+
const sp = (miyan.special || []).find(s => s.name === offeredItem.name)
|
|
1132
|
+
if (sp) {
|
|
1133
|
+
const target = pool.find(c => c.name === sp.cizhang && c.rarity === sp.rarity)
|
|
1134
|
+
if (target) target.weight = (target.weight || 0) + (sp.weight || 0)
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
const totalWeight = pool.reduce((s, c) => s + (c.weight || 1), 0)
|
|
1138
|
+
let rand = Math.random() * totalWeight
|
|
1139
|
+
resultItem = pool.find(c => { rand -= (c.weight || 1); return rand < 0 }) || pool[pool.length - 1]
|
|
1140
|
+
resultItem = { ...resultItem }
|
|
1141
|
+
}
|
|
1142
|
+
st.cizhang.push(resultItem)
|
|
1143
|
+
push('📦 第' + st.events + '次:你向秘言箱提交了 ' + offered.length + ' 件独特物品(总价值' + fmt(totalValue) + '),秘言箱回赠「' + resultItem.name + '」(' + resultItem.rarity + '·' + fmt(resultItem.value) + ')', 'ex')
|
|
1144
|
+
if (!st.nightingaleUsed && resultItem.value >= 100000) {
|
|
1145
|
+
st.nightingaleUsed = true
|
|
1146
|
+
st.nightingaleItem = resultItem
|
|
1147
|
+
st.cizhang = st.cizhang.filter(c => c !== resultItem)
|
|
1148
|
+
push('🕊 <b>夜莺</b>自动带回 <b>' + resultItem.name + '</b>(' + resultItem.rarity + '·' + fmt(resultItem.value) + '墨迹)', 'night')
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const enterTreasureRoom = () => {
|
|
1153
|
+
st.treasureRoomFound = true
|
|
1154
|
+
st.treasureRoomPending = false
|
|
1155
|
+
st.hasSecret = false
|
|
1156
|
+
const secretIdx = st.cizhang.findIndex(c => c.name === '缪斯的秘密')
|
|
1157
|
+
if (secretIdx !== -1) st.cizhang.splice(secretIdx, 1)
|
|
1158
|
+
const chestCount = (cfg.treasureRoom && cfg.treasureRoom.chestCount) || 4
|
|
1159
|
+
push('🚪 第' + st.events + '次:进入宝藏房——消耗缪斯的秘密,打开' + chestCount + '个宝箱!', 'gold')
|
|
1160
|
+
for (let b = 1; b <= chestCount; b++) {
|
|
1161
|
+
push(' 📦 宝箱 #' + b + '——<span class="gold">获得辞章!</span>')
|
|
1162
|
+
addCizhang('treasureRoom')
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// 事件循环
|
|
1167
|
+
while (st.events < st.totalEvents && !st.dead) {
|
|
1168
|
+
st.events++
|
|
1169
|
+
const baseWeights = (cfg.eventWeights || {})[diff] || { vase: 50, chest: 20, roadside: 15, piano: 5, treasureRoom: 3, monster: 22 }
|
|
1170
|
+
const caps = (cfg.eventCaps || {})[diff] || {}
|
|
1171
|
+
const eventTypes = ['vase', 'regularBox', 'roadside', 'piano', 'treasureRoom', 'antiqueCabinet1', 'antiqueCabinet2', 'exquisiteCabinet', 'miyan', 'monster']
|
|
1172
|
+
const weights = {}
|
|
1173
|
+
let totalWeight = 0
|
|
1174
|
+
for (const t of eventTypes) {
|
|
1175
|
+
const cap = caps[t]
|
|
1176
|
+
if (cap !== undefined && (eventCounts[t] || 0) >= cap) weights[t] = 0
|
|
1177
|
+
else weights[t] = baseWeights[t] || 0
|
|
1178
|
+
totalWeight += weights[t]
|
|
1179
|
+
}
|
|
1180
|
+
if (totalWeight <= 0) {
|
|
1181
|
+
push('第' + st.events + '次:所有事件已达上限,继续前行...')
|
|
1182
|
+
continue
|
|
1183
|
+
}
|
|
1184
|
+
const r = Math.random() * totalWeight
|
|
1185
|
+
let cum = 0, pickedType = 'monster'
|
|
1186
|
+
for (const t of eventTypes) {
|
|
1187
|
+
cum += weights[t]
|
|
1188
|
+
if (r < cum) { pickedType = t; break }
|
|
1189
|
+
}
|
|
1190
|
+
eventCounts[pickedType] = (eventCounts[pickedType] || 0) + 1
|
|
1191
|
+
|
|
1192
|
+
if (pickedType === 'vase') {
|
|
1193
|
+
if (Math.random() < ((cfg.vase && cfg.vase.cizhangChance && cfg.vase.cizhangChance[diff]) != null ? cfg.vase.cizhangChance[diff] : 0.25)) {
|
|
1194
|
+
push('🏺 第' + st.events + '次:打碎花瓶——<span class="gold">发现辞章!</span>')
|
|
1195
|
+
addCizhang('vase')
|
|
1196
|
+
} else {
|
|
1197
|
+
push('🏺 第' + st.events + '次:打碎花瓶——空无一物...')
|
|
1198
|
+
}
|
|
1199
|
+
} else if (pickedType === 'regularBox') {
|
|
1200
|
+
if (Math.random() < ((cfg.regularBox && cfg.regularBox.cizhangChance && cfg.regularBox.cizhangChance[diff]) != null ? cfg.regularBox.cizhangChance[diff] : 0.4)) {
|
|
1201
|
+
push('📦 第' + st.events + '次:打开普通的箱子——<span class="gold">发现辞章!</span>')
|
|
1202
|
+
addCizhang('regularBox')
|
|
1203
|
+
} else {
|
|
1204
|
+
push('📦 第' + st.events + '次:打开普通的箱子——空无一物...')
|
|
1205
|
+
}
|
|
1206
|
+
} else if (pickedType === 'antiqueCabinet1') {
|
|
1207
|
+
push('🗄 第' + st.events + '次:打开上锁的古董柜子——<span class="gold">获得辞章!</span>')
|
|
1208
|
+
addCizhang('antiqueCabinet1')
|
|
1209
|
+
if (Math.random() < ((cfg.antiqueCabinet1 && cfg.antiqueCabinet1.arcLightChance && cfg.antiqueCabinet1.arcLightChance[diff]) || 0)) {
|
|
1210
|
+
const arcItem = cizhang.find(c => c.name === '弧光')
|
|
1211
|
+
if (arcItem) { st.cizhang.push({ ...arcItem }); push(' ↳ <span class="purple">✨ 发现了额外的弧光!</span>', 'gain') }
|
|
1212
|
+
}
|
|
1213
|
+
} else if (pickedType === 'antiqueCabinet2') {
|
|
1214
|
+
push('🗄 第' + st.events + '次:打开上锁的古董柜子——<span class="gold">获得辞章!</span>')
|
|
1215
|
+
addCizhang('antiqueCabinet2')
|
|
1216
|
+
if (Math.random() < ((cfg.antiqueCabinet2 && cfg.antiqueCabinet2.arcLightChance && cfg.antiqueCabinet2.arcLightChance[diff]) || 0)) {
|
|
1217
|
+
const arcItem = cizhang.find(c => c.name === '弧光')
|
|
1218
|
+
if (arcItem) { st.cizhang.push({ ...arcItem }); push(' ↳ <span class="purple">✨ 发现了额外的弧光!</span>', 'gain') }
|
|
1219
|
+
}
|
|
1220
|
+
} else if (pickedType === 'exquisiteCabinet') {
|
|
1221
|
+
push('🗝 第' + st.events + '次:打开上锁的精致柜子——<span class="gold">获得辞章!</span>')
|
|
1222
|
+
addCizhang('exquisiteCabinet')
|
|
1223
|
+
if (Math.random() < ((cfg.exquisiteCabinet && cfg.exquisiteCabinet.arcLightChance && cfg.exquisiteCabinet.arcLightChance[diff]) || 0)) {
|
|
1224
|
+
const arcItem = cizhang.find(c => c.name === '弧光')
|
|
1225
|
+
if (arcItem) { st.cizhang.push({ ...arcItem }); push(' ↳ <span class="purple">✨ 发现了额外的弧光!</span>', 'gain') }
|
|
1226
|
+
}
|
|
1227
|
+
} else if (pickedType === 'roadside') {
|
|
1228
|
+
if (Math.random() < ((cfg.roadside && cfg.roadside.cizhangChance && cfg.roadside.cizhangChance[diff]) != null ? cfg.roadside.cizhangChance[diff] : 0.1)) {
|
|
1229
|
+
push('🍀 第' + st.events + '次:路边捡到物品——<span class="gold">发现辞章!</span>')
|
|
1230
|
+
addCizhang('roadside')
|
|
1231
|
+
} else {
|
|
1232
|
+
push('🍀 第' + st.events + '次:路边捡到物品——不值钱的小玩意儿...')
|
|
1233
|
+
}
|
|
1234
|
+
} else if (pickedType === 'piano') {
|
|
1235
|
+
if (st.pianoFound) {
|
|
1236
|
+
push('🎹 第' + st.events + '次:再次路过琴房——但已取走了缪斯的秘密。')
|
|
1237
|
+
} else {
|
|
1238
|
+
st.pianoFound = true
|
|
1239
|
+
st.hasSecret = true
|
|
1240
|
+
const secretValue = (cfg.piano && cfg.piano.rewardInk) || 10000
|
|
1241
|
+
st.cizhang.push({ name: '缪斯的秘密', rarity: '奇珍', value: secretValue })
|
|
1242
|
+
push('🎹 第' + st.events + '次:进入琴房——<span class="ms">获得「缪斯的秘密」!(价值' + fmt(secretValue) + '墨迹)</span>', 'gain')
|
|
1243
|
+
if (st.treasureRoomPending) {
|
|
1244
|
+
push(' ↳ <span class="gold">记起宝藏房位置,立刻折返!</span>')
|
|
1245
|
+
enterTreasureRoom()
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
} else if (pickedType === 'treasureRoom') {
|
|
1249
|
+
if (st.treasureRoomFound || st.treasureRoomPending) {
|
|
1250
|
+
push('🚪 第' + st.events + '次:再次路过宝藏房——但已被搜刮一空。')
|
|
1251
|
+
} else if (!st.hasSecret) {
|
|
1252
|
+
st.treasureRoomPending = true
|
|
1253
|
+
push('🚪 第' + st.events + '次:发现宝藏房——<span class="red">但没有缪斯的秘密,标记位置等待钥匙...</span>')
|
|
1254
|
+
} else {
|
|
1255
|
+
enterTreasureRoom()
|
|
1256
|
+
}
|
|
1257
|
+
} else if (pickedType === 'miyan') {
|
|
1258
|
+
doMiyan()
|
|
1259
|
+
} else {
|
|
1260
|
+
// 怪物
|
|
1261
|
+
const monster = pickMonster()
|
|
1262
|
+
const mName = monster ? monster.name : '未知怪物'
|
|
1263
|
+
if (monster && mName === '缄默的绅士' && doExchange(monster)) continue
|
|
1264
|
+
if (monster && mName === '无名布偶') {
|
|
1265
|
+
const encText = monster.encounters && monster.encounters.length ? monster.encounters[Math.floor(Math.random() * monster.encounters.length)] : ''
|
|
1266
|
+
push('🧸 第' + st.events + '次:' + encText + '它抬起头盯着你,你感到一阵不安...')
|
|
1267
|
+
continue
|
|
1268
|
+
}
|
|
1269
|
+
doCombat(monster, mName)
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// 结算
|
|
1274
|
+
push('━━━━━━━━━━━━━━━━━━━━')
|
|
1275
|
+
const nightingaleValue = st.nightingaleItem ? st.nightingaleItem.value : 0
|
|
1276
|
+
const result = { ok: true, diff, dead: false, success: false, carried: [], carriedValue: 0, nightingale: st.nightingaleItem, killLog: st.killLog, log }
|
|
1277
|
+
const ev = () => {
|
|
1278
|
+
const arcItems = st.cizhang.filter(c => c.name === '弧光')
|
|
1279
|
+
const nonArc = st.cizhang.filter(c => c.name !== '弧光')
|
|
1280
|
+
const sortedNonArc = [...nonArc].sort((a, b) => b.value - a.value)
|
|
1281
|
+
const top5 = sortedNonArc.slice(0, 5)
|
|
1282
|
+
const arcTotal = arcItems.reduce((s, c) => s + c.value, 0)
|
|
1283
|
+
return { arcItems, arcCount: arcItems.length, arcTotal, sortedNonArc, top5, top5Value: top5.reduce((s, c) => s + c.value, 0), effectiveTotal: arcTotal + top5.reduce((s, c) => s + c.value, 0) }
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (st.dead) {
|
|
1287
|
+
result.dead = true
|
|
1288
|
+
push('<span class="red">💀 探索失败——装备丢失。</span>')
|
|
1289
|
+
if (st.lostEquipValue > 0) push('📦 损失装备价值:' + fmt(st.lostEquipValue) + ' 墨迹')
|
|
1290
|
+
if (diffCost > 0) push('💸 入场费 −' + fmt(diffCost) + ' 墨迹')
|
|
1291
|
+
if (nightingaleValue > 0) {
|
|
1292
|
+
push('🕊 夜莺带回了 <b>' + st.nightingaleItem.name + '</b>(' + st.nightingaleItem.rarity + '·+' + fmt(nightingaleValue) + '墨迹)', 'night')
|
|
1293
|
+
result.carried.push(st.nightingaleItem)
|
|
1294
|
+
result.carriedValue += nightingaleValue
|
|
1295
|
+
}
|
|
1296
|
+
} else {
|
|
1297
|
+
const e = ev()
|
|
1298
|
+
const top6NonArc = e.sortedNonArc.slice(0, 6)
|
|
1299
|
+
const top6Value = top6NonArc.reduce((s, c) => s + c.value, 0)
|
|
1300
|
+
const passWithArc = e.arcCount > 0 && e.effectiveTotal >= minVal
|
|
1301
|
+
const passWithoutArc = (nightingaleValue + top6Value) >= minVal
|
|
1302
|
+
if (!passWithArc && !passWithoutArc) {
|
|
1303
|
+
const totalCheck = Math.max(e.effectiveTotal, nightingaleValue + top6Value)
|
|
1304
|
+
push('<span class="red">⏳ 时间耗尽!背包总价值 ' + fmt(totalCheck) + ' 不足 ' + fmt(minVal) + ',未能撤离。</span>')
|
|
1305
|
+
push('<span class="red">💀 装备全部丢失。</span>')
|
|
1306
|
+
const lostEquip = equipped.filter(x => { const w = weapons.find(y => y.name === x); return w && w.type !== 'character' }).reduce((s, n) => s + ((weapons.find(x => x.name === n) || {}).price || 0), 0)
|
|
1307
|
+
if (lostEquip > 0) push('📦 损失装备价值:' + fmt(lostEquip) + ' 墨迹')
|
|
1308
|
+
if (diffCost > 0) push('💸 入场费 −' + fmt(diffCost) + ' 墨迹')
|
|
1309
|
+
if (nightingaleValue > 0) {
|
|
1310
|
+
push('🕊 夜莺带回了 <b>' + st.nightingaleItem.name + '</b>(+' + fmt(nightingaleValue) + '墨迹)', 'night')
|
|
1311
|
+
result.carried.push(st.nightingaleItem)
|
|
1312
|
+
result.carriedValue += nightingaleValue
|
|
1313
|
+
}
|
|
1314
|
+
} else {
|
|
1315
|
+
result.success = true
|
|
1316
|
+
const takeArc = passWithArc
|
|
1317
|
+
let totalValue = 0
|
|
1318
|
+
if (takeArc) {
|
|
1319
|
+
result.carried.push(...e.top5)
|
|
1320
|
+
if (e.arcCount > 0) result.carried.push({ name: '弧光 ×' + e.arcCount, rarity: '奇珍', value: e.arcTotal })
|
|
1321
|
+
totalValue = e.top5Value + e.arcTotal
|
|
1322
|
+
} else {
|
|
1323
|
+
result.carried.push(...top6NonArc)
|
|
1324
|
+
totalValue = top6Value
|
|
1325
|
+
}
|
|
1326
|
+
result.carriedValue = totalValue
|
|
1327
|
+
push('<span class="gold">🏆 探索成功!带出 ' + result.carried.length + ' 件辞章:</span>')
|
|
1328
|
+
result.carried.forEach(c => push(' · ' + c.name + ' (' + c.rarity + '·' + fmt(c.value) + '墨迹)'))
|
|
1329
|
+
push('💰 辞章价值 +' + fmt(totalValue) + ' 墨迹')
|
|
1330
|
+
const baseEquipValue = equipped.filter(x => { const w = weapons.find(y => y.name === x); return w && w.type !== 'character' }).reduce((s, n) => s + ((weapons.find(x => x.name === n) || {}).price || 0), 0)
|
|
1331
|
+
const smokeWep = weapons.find(x => x.name === '祝祷烟炉')
|
|
1332
|
+
const smokeDeduct = (smokeWep ? smokeWep.price : 0) * (st.smokeUsed || 0)
|
|
1333
|
+
const dep = cfg.equipDepreciation || { min: 0.05, max: 0.12 }
|
|
1334
|
+
const depRate = dep.min + Math.random() * (dep.max - dep.min)
|
|
1335
|
+
const depreciation = Math.round(baseEquipValue * depRate)
|
|
1336
|
+
if (baseEquipValue > 0) push('📦 装备带出 +' + fmt(baseEquipValue) + ' 墨迹')
|
|
1337
|
+
if (smokeDeduct > 0) push('🕯 祝祷烟炉消耗 −' + fmt(smokeDeduct) + ' 墨迹')
|
|
1338
|
+
if (depreciation > 0) push('🔧 装备维修 −' + fmt(depreciation) + ' 墨迹')
|
|
1339
|
+
if (diffCost > 0) push('💸 入场费 −' + fmt(diffCost) + ' 墨迹')
|
|
1340
|
+
if (nightingaleValue > 0) {
|
|
1341
|
+
push('🕊 夜莺带回了 <b>' + st.nightingaleItem.name + '</b>(+' + fmt(nightingaleValue) + '墨迹)', 'night')
|
|
1342
|
+
result.carriedValue += nightingaleValue
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
const killEntries = Object.entries(st.killLog || {})
|
|
1347
|
+
if (killEntries.length > 0) {
|
|
1348
|
+
killEntries.sort((a, b) => b[1] - a[1])
|
|
1349
|
+
push('━━━━━━━━━━━━━━━━━━━━')
|
|
1350
|
+
push('⚔️ <b>怪物击杀统计:</b>')
|
|
1351
|
+
killEntries.forEach(([name, count]) => push(' · ' + name + ' ×' + count))
|
|
1352
|
+
}
|
|
1353
|
+
result.totalEvents = st.totalEvents
|
|
1354
|
+
result.diffCost = diffCost
|
|
1355
|
+
result.minValue = minVal
|
|
1356
|
+
return result
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// 探索入口(每日限制 + 数据加载 + 模拟)
|
|
1360
|
+
async function gachaExplore(config, store, userId, diffArg) {
|
|
1361
|
+
const diff = EXPLORE_DIFFS.includes(String(diffArg || '').trim()) ? String(diffArg).trim() : '困难'
|
|
1362
|
+
|
|
1363
|
+
const user = await store.getUser(userId)
|
|
1364
|
+
if (!user.daily) user.daily = {}
|
|
1365
|
+
const today = new Date()
|
|
1366
|
+
const todayStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0')
|
|
1367
|
+
const limit = Math.max(1, Number(config.gachaDailyLimit) || 2)
|
|
1368
|
+
const daily = user.daily['explore'] || { date: todayStr, count: 0 }
|
|
1369
|
+
if (daily.date !== todayStr) { daily.date = todayStr; daily.count = 0 }
|
|
1370
|
+
if (daily.count >= limit) {
|
|
1371
|
+
return { ok: false, limit: true, error: '今日探索次数已达上限(每天限 ' + limit + ' 次),明天再来吧' }
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const src = await resolveSource(config)
|
|
1375
|
+
const data = await loadExploreData(src.base, config.timeout)
|
|
1376
|
+
if (!data.weapons.length || !data.cizhang.length) {
|
|
1377
|
+
return { ok: false, error: '探索数据加载失败' }
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// 自动装备方案(保证可玩性:光源 + 角色 + 保命装)
|
|
1381
|
+
const equipped = ['信使', '提灯', '提神嗅盒', '提神嗅盒', '急救针', '祝祷烟炉']
|
|
1382
|
+
|
|
1383
|
+
const result = exploreRun(data, diff, equipped)
|
|
1384
|
+
daily.count++
|
|
1385
|
+
user.daily['explore'] = daily
|
|
1386
|
+
await store.save(userId)
|
|
1387
|
+
|
|
1388
|
+
result.dailyLeft = limit - daily.count
|
|
1389
|
+
result.diff = diff
|
|
1390
|
+
return result
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// 探索结果图片 HTML
|
|
1394
|
+
function buildExploreHtml(data, config) {
|
|
1395
|
+
const width = config.gachaWidth || config.width || 1300
|
|
1396
|
+
const cardW = Math.min(width, 1000)
|
|
1397
|
+
const lines = data.log.map(l => {
|
|
1398
|
+
let cls = 'line'
|
|
1399
|
+
if (l.cls === 'gain') cls = 'line gain'
|
|
1400
|
+
else if (l.cls === 'gold') cls = 'line gold'
|
|
1401
|
+
else if (l.cls === 'red' || l.cls === 'dead') cls = 'line red'
|
|
1402
|
+
else if (l.cls === 'green') cls = 'line green'
|
|
1403
|
+
else if (l.cls === 'night') cls = 'line night'
|
|
1404
|
+
else if (l.cls === 'ex') cls = 'line ex'
|
|
1405
|
+
else if (l.cls === 'block') cls = 'line block'
|
|
1406
|
+
else if (l.cls === 'purple') cls = 'line purple'
|
|
1407
|
+
else if (l.cls === 'ms') cls = 'line ms'
|
|
1408
|
+
return '<div class="' + cls + '">' + l.msg + '</div>'
|
|
1409
|
+
}).join('')
|
|
1410
|
+
|
|
1411
|
+
const statusHtml = data.dead
|
|
1412
|
+
? '<div class="status bad">💀 探索失败</div>'
|
|
1413
|
+
: data.success
|
|
1414
|
+
? '<div class="status good">🏆 探索成功</div>'
|
|
1415
|
+
: '<div class="status bad">⏳ 未能撤离</div>'
|
|
1416
|
+
const nightHtml = data.nightingale
|
|
1417
|
+
? '<div class="night-bar">🕊 夜莺带回:<b>' + esc(data.nightingale.name) + '</b>(' + (data.nightingale.rarity || '') + '·' + esc(String(data.nightingale.value)) + '墨迹)</div>'
|
|
1418
|
+
: ''
|
|
1419
|
+
|
|
1420
|
+
return `<!DOCTYPE html>
|
|
1421
|
+
<html lang="zh-CN">
|
|
1422
|
+
<head>
|
|
1423
|
+
<meta charset="UTF-8">
|
|
1424
|
+
<style>
|
|
1425
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
1426
|
+
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:28px 18px; color:#d1d5db;
|
|
1427
|
+
background:radial-gradient(1200px 600px at 50% -10%, #1b2a4a 0%, #10182b 45%, #0a0e17 100%); }
|
|
1428
|
+
.page { max-width:${cardW}px; margin:0 auto; }
|
|
1429
|
+
.head { text-align:center; margin-bottom:18px; }
|
|
1430
|
+
.title { font-size:26px; font-weight:800; letter-spacing:1px; color:#fff;
|
|
1431
|
+
background:linear-gradient(90deg, #79c0ff, #ffd76d); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
|
|
1432
|
+
.sub { font-size:13px; color:#8b949e; margin-top:6px; }
|
|
1433
|
+
.status { display:inline-block; font-size:14px; font-weight:700; border-radius:8px; padding:5px 16px; margin-top:10px; }
|
|
1434
|
+
.status.good { color:#3fb950; background:rgba(63,185,80,.1); border:1px solid rgba(63,185,80,.3); }
|
|
1435
|
+
.status.bad { color:#f85149; background:rgba(248,81,73,.1); border:1px solid rgba(248,81,73,.3); }
|
|
1436
|
+
.daily { display:inline-block; font-size:12.5px; color:#8b949e; background:rgba(139,148,158,.08); border:1px solid rgba(139,148,158,.25);
|
|
1437
|
+
border-radius:8px; padding:4px 12px; margin-left:8px; }
|
|
1438
|
+
.log { background:rgba(22,27,34,.75); border:1px solid rgba(255,255,255,.09); border-radius:14px; padding:18px 20px; }
|
|
1439
|
+
.line { font-size:13px; line-height:1.9; color:#c9d1d9; }
|
|
1440
|
+
.line.gain { color:#e6edf3; }
|
|
1441
|
+
.line.gold { color:#e8c668; }
|
|
1442
|
+
.line.red, .line.dead { color:#f85149; }
|
|
1443
|
+
.line.green { color:#3fb950; }
|
|
1444
|
+
.line.night { color:#b568e8; font-weight:700; }
|
|
1445
|
+
.line.ex { color:#ffd76d; }
|
|
1446
|
+
.line.block { color:#4ade80; }
|
|
1447
|
+
.line.purple { color:#b568e8; }
|
|
1448
|
+
.line.ms { color:#f0c060; }
|
|
1449
|
+
.line b { color:#fff; }
|
|
1450
|
+
.night-bar { margin-top:14px; font-size:13.5px; color:#b568e8; font-weight:700; text-align:center;
|
|
1451
|
+
background:rgba(181,104,232,.08); border:1px solid rgba(181,104,232,.3); border-radius:10px; padding:8px 14px; }
|
|
1452
|
+
.foot { text-align:center; color:#8b949e; font-size:12px; margin-top:16px; opacity:.75; }
|
|
1453
|
+
</style>
|
|
1454
|
+
</head>
|
|
1455
|
+
<body>
|
|
1456
|
+
<div class="page">
|
|
1457
|
+
<div class="head">
|
|
1458
|
+
<div class="title">加页手记·探索(${esc(data.diff)})</div>
|
|
1459
|
+
<div class="sub">事件 ${data.totalEvents} 次 · 入场费 ${esc(String(data.diffCost || 0))} 墨迹</div>
|
|
1460
|
+
<div>
|
|
1461
|
+
${statusHtml}
|
|
1462
|
+
<span class="daily">今日剩余:${data.dailyLeft} 次</span>
|
|
1463
|
+
</div>
|
|
1464
|
+
</div>
|
|
1465
|
+
<div class="log">${lines}</div>
|
|
1466
|
+
${nightHtml}
|
|
1467
|
+
<div class="foot">加页手记探索</div>
|
|
1468
|
+
</div>
|
|
1469
|
+
</body>
|
|
1470
|
+
</html>`
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// ===== 导出 =====
|
|
1474
|
+
|
|
1475
|
+
module.exports = {
|
|
1476
|
+
KNOWN_SOURCES,
|
|
1477
|
+
RESOURCE_TYPES,
|
|
1478
|
+
POOL_TYPES,
|
|
1479
|
+
POOL_TYPE_ALIAS,
|
|
1480
|
+
Store,
|
|
1481
|
+
resolveSource,
|
|
1482
|
+
gachaPull,
|
|
1483
|
+
gachaResources,
|
|
1484
|
+
gachaResetDaily,
|
|
1485
|
+
gachaExplore,
|
|
1486
|
+
buildGachaHtml,
|
|
1487
|
+
buildExploreHtml,
|
|
1488
|
+
buildResourcesHtml,
|
|
1489
|
+
exploreRun,
|
|
1490
|
+
loadExploreData,
|
|
1491
|
+
itemKey,
|
|
1492
|
+
markOwned,
|
|
1493
|
+
RARITY_LABEL,
|
|
1494
|
+
defaultPoolState,
|
|
1495
|
+
v1Draw,
|
|
1496
|
+
v2Draw,
|
|
1497
|
+
xingyunDraw,
|
|
1498
|
+
jyxbDraw,
|
|
1499
|
+
hxmtDraw,
|
|
1500
|
+
jiayeDraw,
|
|
1501
|
+
}
|