@jeik/dingtalk-connector 0.8.21-fix1
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/CHANGELOG.md +686 -0
- package/LICENSE +21 -0
- package/README.en.md +181 -0
- package/README.md +221 -0
- package/bin/dingtalk-connector.js +858 -0
- package/bin/wizard-config.mjs +110 -0
- package/dist/accounts-BAzdqkAV.mjs +268 -0
- package/dist/accounts-BQptOmgB.mjs +2 -0
- package/dist/chunk-upload-BBQgGtcZ.mjs +193 -0
- package/dist/chunk-upload-DaLXXZH3.mjs +2 -0
- package/dist/common-C8pYKU_y.mjs +2 -0
- package/dist/common-Dt9n6fQN.mjs +101 -0
- package/dist/connection-DHHFFNQJ.mjs +423 -0
- package/dist/entry-bundled.d.mts +16 -0
- package/dist/entry-bundled.mjs +31 -0
- package/dist/game-xiyou-CqHt-6Q1.mjs +4271 -0
- package/dist/gateway-methods-C4tcgI7P.mjs +771 -0
- package/dist/gateway-methods-Ci31A3vg.mjs +2 -0
- package/dist/http-client-CpnJHB89.mjs +2 -0
- package/dist/http-client-DFWZgO1n.mjs +33 -0
- package/dist/index.d.mts +193 -0
- package/dist/index.mjs +45 -0
- package/dist/logger-BmJkQkm1.mjs +2 -0
- package/dist/logger-mZ9OSbmD.mjs +58 -0
- package/dist/media-C_SVin7s.mjs +2 -0
- package/dist/media-cz72EVS3.mjs +509 -0
- package/dist/message-handler-DESzFFDc.mjs +1971 -0
- package/dist/messaging-B6l1sRvX.mjs +1044 -0
- package/dist/runtime-DUgpo5zC.mjs +1422 -0
- package/dist/session-DJ4jYqPv.mjs +114 -0
- package/dist/utils-Bjh4r_qS.mjs +4 -0
- package/dist/utils-CIfI_3Jh.mjs +63 -0
- package/dist/utils-legacy-CALCPP1t.mjs +230 -0
- package/dist/utils-legacy-CFYDBM4r.mjs +3 -0
- package/docs/DEAP_AGENT_GUIDE.en.md +115 -0
- package/docs/DEAP_AGENT_GUIDE.md +115 -0
- package/docs/DINGTALK_MANUAL_SETUP.md +50 -0
- package/docs/MULTI_AGENT_SETUP.md +306 -0
- package/docs/RELEASE_NOTES_V0.7.10.md +40 -0
- package/docs/RELEASE_NOTES_V0.7.2.md +143 -0
- package/docs/RELEASE_NOTES_V0.7.3.md +149 -0
- package/docs/RELEASE_NOTES_V0.7.4.md +206 -0
- package/docs/RELEASE_NOTES_V0.7.5.md +267 -0
- package/docs/RELEASE_NOTES_V0.7.6.md +219 -0
- package/docs/RELEASE_NOTES_V0.7.7.md +122 -0
- package/docs/RELEASE_NOTES_V0.7.8.md +101 -0
- package/docs/RELEASE_NOTES_V0.7.9.md +65 -0
- package/docs/RELEASE_NOTES_V0.8.0.md +53 -0
- package/docs/RELEASE_NOTES_V0.8.1.md +47 -0
- package/docs/RELEASE_NOTES_V0.8.10.md +49 -0
- package/docs/RELEASE_NOTES_V0.8.11.md +51 -0
- package/docs/RELEASE_NOTES_V0.8.12.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.13-beta.0.md +69 -0
- package/docs/RELEASE_NOTES_V0.8.13.md +62 -0
- package/docs/RELEASE_NOTES_V0.8.14.md +86 -0
- package/docs/RELEASE_NOTES_V0.8.16.md +40 -0
- package/docs/RELEASE_NOTES_V0.8.17.md +87 -0
- package/docs/RELEASE_NOTES_V0.8.18.md +64 -0
- package/docs/RELEASE_NOTES_V0.8.19.md +62 -0
- package/docs/RELEASE_NOTES_V0.8.2.md +55 -0
- package/docs/RELEASE_NOTES_V0.8.20.md +49 -0
- package/docs/RELEASE_NOTES_V0.8.3.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.4.md +45 -0
- package/docs/RELEASE_NOTES_V0.8.7.md +49 -0
- package/docs/RELEASE_NOTES_V0.8.8.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.9.md +81 -0
- package/docs/RELEASE_NOTES_v0.7.0.md +142 -0
- package/docs/RELEASE_NOTES_v0.7.1.md +74 -0
- package/docs/TROUBLESHOOTING.md +122 -0
- package/index.ts +77 -0
- package/openclaw.plugin.json +551 -0
- package/package.json +147 -0
- package/skills/dingtalk-channel-rules/SKILL.md +91 -0
- package/skills/dingtalk-troubleshoot/SKILL.md +93 -0
- package/skills/dws-cli/SKILL.md +129 -0
- package/skills/dws-cli/references/error-codes.md +95 -0
- package/skills/dws-cli/references/field-rules.md +105 -0
- package/skills/dws-cli/references/global-reference.md +104 -0
- package/skills/dws-cli/references/intent-guide.md +114 -0
- package/skills/dws-cli/references/products/aitable.md +452 -0
- package/skills/dws-cli/references/products/attendance.md +93 -0
- package/skills/dws-cli/references/products/calendar.md +217 -0
- package/skills/dws-cli/references/products/chat.md +292 -0
- package/skills/dws-cli/references/products/contact.md +108 -0
- package/skills/dws-cli/references/products/ding.md +57 -0
- package/skills/dws-cli/references/products/report.md +162 -0
- package/skills/dws-cli/references/products/simple.md +128 -0
- package/skills/dws-cli/references/products/todo.md +138 -0
- package/skills/dws-cli/references/products/workbench.md +39 -0
- package/skills/dws-cli/references/recovery-guide.md +94 -0
- package/src/channel.ts +588 -0
- package/src/config/accounts.ts +242 -0
- package/src/config/schema.ts +180 -0
- package/src/core/connection.ts +741 -0
- package/src/core/message-handler.ts +1788 -0
- package/src/core/provider.ts +111 -0
- package/src/core/state.ts +54 -0
- package/src/device-auth-config.ts +14 -0
- package/src/device-auth.ts +197 -0
- package/src/directory.ts +95 -0
- package/src/docs.ts +293 -0
- package/src/game-xiyou/achievement-engine.ts +252 -0
- package/src/game-xiyou/bounty-system.ts +315 -0
- package/src/game-xiyou/commands.ts +223 -0
- package/src/game-xiyou/drop-engine.ts +241 -0
- package/src/game-xiyou/encounter-system.ts +135 -0
- package/src/game-xiyou/escape-engine.ts +164 -0
- package/src/game-xiyou/exp-calculator.ts +139 -0
- package/src/game-xiyou/index.ts +479 -0
- package/src/game-xiyou/level-system.ts +91 -0
- package/src/game-xiyou/monster-pool.ts +180 -0
- package/src/game-xiyou/pity-counter.ts +114 -0
- package/src/game-xiyou/random-event-engine.ts +648 -0
- package/src/game-xiyou/renderer.ts +679 -0
- package/src/game-xiyou/storage.ts +218 -0
- package/src/game-xiyou/treasure-system.ts +105 -0
- package/src/game-xiyou/types.ts +582 -0
- package/src/game-xiyou/uid-resolver.ts +49 -0
- package/src/gateway-methods.ts +740 -0
- package/src/onboarding.ts +553 -0
- package/src/policy.ts +32 -0
- package/src/probe.ts +210 -0
- package/src/reply-dispatcher.ts +874 -0
- package/src/runtime.ts +32 -0
- package/src/sdk/helpers.ts +322 -0
- package/src/sdk/types.ts +519 -0
- package/src/secret-input.ts +19 -0
- package/src/services/media/audio.ts +54 -0
- package/src/services/media/chunk-upload.ts +296 -0
- package/src/services/media/common.ts +155 -0
- package/src/services/media/file.ts +75 -0
- package/src/services/media/image.ts +81 -0
- package/src/services/media/index.ts +10 -0
- package/src/services/media/video.ts +162 -0
- package/src/services/media.ts +1143 -0
- package/src/services/messaging/card.ts +604 -0
- package/src/services/messaging/index.ts +18 -0
- package/src/services/messaging/mentions.ts +267 -0
- package/src/services/messaging/send.ts +141 -0
- package/src/services/messaging.ts +1191 -0
- package/src/services/reply-markers.ts +55 -0
- package/src/targets.ts +45 -0
- package/src/types/index.ts +59 -0
- package/src/types/pdf-parse.d.ts +3 -0
- package/src/utils/agent.ts +63 -0
- package/src/utils/async.ts +51 -0
- package/src/utils/constants.ts +27 -0
- package/src/utils/http-client.ts +38 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +78 -0
- package/src/utils/session.ts +147 -0
- package/src/utils/token.ts +93 -0
- package/src/utils/utils-legacy.ts +454 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,4271 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { createHash, createHmac, randomBytes } from "crypto";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
//#region src/game-xiyou/uid-resolver.ts
|
|
6
|
+
/**
|
|
7
|
+
* UID 解析与绑定
|
|
8
|
+
*
|
|
9
|
+
* 优先级链:
|
|
10
|
+
* 1. 钉钉 userId(通过 connector 的 device-auth 获取)
|
|
11
|
+
* 2. 本机 fingerprint(兜底,基于 hostname + username 的 SHA256)
|
|
12
|
+
*/
|
|
13
|
+
const SALT = "xiyou-salt-2026";
|
|
14
|
+
/**
|
|
15
|
+
* 根据原始 UID 生成稳定的哈希标识
|
|
16
|
+
*/
|
|
17
|
+
function hashUid(rawUid) {
|
|
18
|
+
return createHash("sha256").update(rawUid + SALT).digest("hex").slice(0, 16);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 生成本机指纹作为兜底 UID
|
|
22
|
+
*/
|
|
23
|
+
function generateMachineFingerprint() {
|
|
24
|
+
return `machine:${os.hostname()}:${os.userInfo().username}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 解析用户 UID
|
|
28
|
+
*
|
|
29
|
+
* @param senderId - 钉钉消息的发送者 ID(优先使用)
|
|
30
|
+
* @returns 稳定的 UID 哈希值(16 位 hex)
|
|
31
|
+
*/
|
|
32
|
+
function resolveUid(senderId) {
|
|
33
|
+
return hashUid(senderId || generateMachineFingerprint());
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/game-xiyou/bounty-system.ts
|
|
37
|
+
/**
|
|
38
|
+
* 悬赏令系统 (v2)
|
|
39
|
+
*
|
|
40
|
+
* 每日刷新 3 张悬赏令(铜/银/金),为日常使用增加目标感和方向性。
|
|
41
|
+
* 使用基于 UID + 日期的种子随机,确保同一用户同一天结果一致。
|
|
42
|
+
*/
|
|
43
|
+
const BRONZE_POOL = [
|
|
44
|
+
{
|
|
45
|
+
id: "B001",
|
|
46
|
+
tier: "bronze",
|
|
47
|
+
descriptionTemplate: "成功执行 3 次任意 dws 命令",
|
|
48
|
+
reward: { exp: 15 },
|
|
49
|
+
condition: {
|
|
50
|
+
type: "command",
|
|
51
|
+
count: 3
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "B002",
|
|
56
|
+
tier: "bronze",
|
|
57
|
+
descriptionTemplate: "使用 {product} 成功执行 1 次命令",
|
|
58
|
+
reward: { exp: 10 },
|
|
59
|
+
condition: {
|
|
60
|
+
type: "command",
|
|
61
|
+
count: 1
|
|
62
|
+
},
|
|
63
|
+
hasProductPlaceholder: true
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "B003",
|
|
67
|
+
tier: "bronze",
|
|
68
|
+
descriptionTemplate: "收服 1 只任意妖怪",
|
|
69
|
+
reward: { exp: 10 },
|
|
70
|
+
condition: {
|
|
71
|
+
type: "capture",
|
|
72
|
+
count: 1
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "B004",
|
|
77
|
+
tier: "bronze",
|
|
78
|
+
descriptionTemplate: "达成 3 连击",
|
|
79
|
+
reward: { exp: 20 },
|
|
80
|
+
condition: {
|
|
81
|
+
type: "combo",
|
|
82
|
+
count: 3
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "B005",
|
|
87
|
+
tier: "bronze",
|
|
88
|
+
descriptionTemplate: "使用 2 种不同产品的命令",
|
|
89
|
+
reward: { exp: 15 },
|
|
90
|
+
condition: {
|
|
91
|
+
type: "product_variety",
|
|
92
|
+
count: 2
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "B006",
|
|
97
|
+
tier: "bronze",
|
|
98
|
+
descriptionTemplate: "收服 1 只精良及以上妖怪",
|
|
99
|
+
reward: { exp: 20 },
|
|
100
|
+
condition: {
|
|
101
|
+
type: "capture",
|
|
102
|
+
qualityMin: "fine",
|
|
103
|
+
count: 1
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "B007",
|
|
108
|
+
tier: "bronze",
|
|
109
|
+
descriptionTemplate: "成功执行 5 次任意 dws 命令",
|
|
110
|
+
reward: { exp: 25 },
|
|
111
|
+
condition: {
|
|
112
|
+
type: "command",
|
|
113
|
+
count: 5
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "B008",
|
|
118
|
+
tier: "bronze",
|
|
119
|
+
descriptionTemplate: "收服 2 只任意妖怪(不含逃跑)",
|
|
120
|
+
reward: { exp: 20 },
|
|
121
|
+
condition: {
|
|
122
|
+
type: "capture",
|
|
123
|
+
count: 2
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
const SILVER_POOL = [
|
|
128
|
+
{
|
|
129
|
+
id: "B101",
|
|
130
|
+
tier: "silver",
|
|
131
|
+
descriptionTemplate: "收服 1 只稀有及以上妖怪",
|
|
132
|
+
reward: { exp: 50 },
|
|
133
|
+
condition: {
|
|
134
|
+
type: "capture",
|
|
135
|
+
qualityMin: "rare",
|
|
136
|
+
count: 1
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "B102",
|
|
141
|
+
tier: "silver",
|
|
142
|
+
descriptionTemplate: "达成 5 连击",
|
|
143
|
+
reward: { exp: 40 },
|
|
144
|
+
condition: {
|
|
145
|
+
type: "combo",
|
|
146
|
+
count: 5
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "B103",
|
|
151
|
+
tier: "silver",
|
|
152
|
+
descriptionTemplate: "使用 3 种不同产品的命令",
|
|
153
|
+
reward: { exp: 35 },
|
|
154
|
+
condition: {
|
|
155
|
+
type: "product_variety",
|
|
156
|
+
count: 3
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "B104",
|
|
161
|
+
tier: "silver",
|
|
162
|
+
descriptionTemplate: "收服 1 只与 {product} 关联的妖怪",
|
|
163
|
+
reward: { exp: 30 },
|
|
164
|
+
condition: {
|
|
165
|
+
type: "capture",
|
|
166
|
+
count: 1
|
|
167
|
+
},
|
|
168
|
+
hasProductPlaceholder: true
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "B105",
|
|
172
|
+
tier: "silver",
|
|
173
|
+
descriptionTemplate: "成功执行 10 次任意 dws 命令",
|
|
174
|
+
reward: { exp: 50 },
|
|
175
|
+
condition: {
|
|
176
|
+
type: "command",
|
|
177
|
+
count: 10
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "B106",
|
|
182
|
+
tier: "silver",
|
|
183
|
+
descriptionTemplate: "收服 3 只不同品质的妖怪",
|
|
184
|
+
reward: { exp: 45 },
|
|
185
|
+
condition: {
|
|
186
|
+
type: "quality_variety",
|
|
187
|
+
count: 3
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "B107",
|
|
192
|
+
tier: "silver",
|
|
193
|
+
descriptionTemplate: "触发 1 次神仙机缘",
|
|
194
|
+
reward: { exp: 40 },
|
|
195
|
+
condition: {
|
|
196
|
+
type: "encounter",
|
|
197
|
+
count: 1
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: "B108",
|
|
202
|
+
tier: "silver",
|
|
203
|
+
descriptionTemplate: "收服 1 只图鉴中未拥有的新妖怪",
|
|
204
|
+
reward: { exp: 60 },
|
|
205
|
+
condition: {
|
|
206
|
+
type: "capture",
|
|
207
|
+
count: 1,
|
|
208
|
+
isNew: true
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
const GOLD_POOL = [
|
|
213
|
+
{
|
|
214
|
+
id: "B201",
|
|
215
|
+
tier: "gold",
|
|
216
|
+
descriptionTemplate: "收服 1 只史诗及以上妖怪",
|
|
217
|
+
reward: {
|
|
218
|
+
exp: 100,
|
|
219
|
+
treasureFragment: "random"
|
|
220
|
+
},
|
|
221
|
+
condition: {
|
|
222
|
+
type: "capture",
|
|
223
|
+
qualityMin: "epic",
|
|
224
|
+
count: 1
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: "B202",
|
|
229
|
+
tier: "gold",
|
|
230
|
+
descriptionTemplate: "达成 10 连击",
|
|
231
|
+
reward: { exp: 80 },
|
|
232
|
+
condition: {
|
|
233
|
+
type: "combo",
|
|
234
|
+
count: 10
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "B203",
|
|
239
|
+
tier: "gold",
|
|
240
|
+
descriptionTemplate: "使用 5 种不同产品的命令",
|
|
241
|
+
reward: { exp: 70 },
|
|
242
|
+
condition: {
|
|
243
|
+
type: "product_variety",
|
|
244
|
+
count: 5
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "B204",
|
|
249
|
+
tier: "gold",
|
|
250
|
+
descriptionTemplate: "单日收服 5 只不同妖怪",
|
|
251
|
+
reward: { exp: 100 },
|
|
252
|
+
condition: {
|
|
253
|
+
type: "capture",
|
|
254
|
+
count: 5
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: "B205",
|
|
259
|
+
tier: "gold",
|
|
260
|
+
descriptionTemplate: "收服本周 UP 妖怪",
|
|
261
|
+
reward: { exp: 120 },
|
|
262
|
+
condition: {
|
|
263
|
+
type: "capture",
|
|
264
|
+
count: 1,
|
|
265
|
+
isUpMonster: true
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: "B206",
|
|
270
|
+
tier: "gold",
|
|
271
|
+
descriptionTemplate: "触发 1 次赐宝机缘",
|
|
272
|
+
reward: { exp: 80 },
|
|
273
|
+
condition: {
|
|
274
|
+
type: "encounter",
|
|
275
|
+
count: 1
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: "B207",
|
|
280
|
+
tier: "gold",
|
|
281
|
+
descriptionTemplate: "连续成功 15 次不中断",
|
|
282
|
+
reward: { exp: 100 },
|
|
283
|
+
condition: {
|
|
284
|
+
type: "combo",
|
|
285
|
+
count: 15
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "B208",
|
|
290
|
+
tier: "gold",
|
|
291
|
+
descriptionTemplate: "收服 2 只稀有及以上妖怪",
|
|
292
|
+
reward: { exp: 90 },
|
|
293
|
+
condition: {
|
|
294
|
+
type: "capture",
|
|
295
|
+
qualityMin: "rare",
|
|
296
|
+
count: 2
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
];
|
|
300
|
+
const AVAILABLE_PRODUCTS = [
|
|
301
|
+
"aitable",
|
|
302
|
+
"calendar",
|
|
303
|
+
"chat",
|
|
304
|
+
"contact",
|
|
305
|
+
"todo",
|
|
306
|
+
"approval",
|
|
307
|
+
"attendance",
|
|
308
|
+
"report",
|
|
309
|
+
"ding",
|
|
310
|
+
"workbench",
|
|
311
|
+
"devdoc"
|
|
312
|
+
];
|
|
313
|
+
function hashString(input) {
|
|
314
|
+
return createHash("sha256").update(input).digest().readUInt32BE(0);
|
|
315
|
+
}
|
|
316
|
+
function seededRandom(seed) {
|
|
317
|
+
let state = seed;
|
|
318
|
+
return () => {
|
|
319
|
+
state = state * 1664525 + 1013904223 & 4294967295;
|
|
320
|
+
return (state >>> 0) / 4294967295;
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function pickRandom(pool, rng) {
|
|
324
|
+
return pool[Math.floor(rng() * pool.length)];
|
|
325
|
+
}
|
|
326
|
+
function getDayKey() {
|
|
327
|
+
return new Date((/* @__PURE__ */ new Date()).getTime() + 480 * 60 * 1e3).toISOString().slice(0, 10);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* 生成每日悬赏令
|
|
331
|
+
*/
|
|
332
|
+
function generateDailyBounties(profile) {
|
|
333
|
+
const today = getDayKey();
|
|
334
|
+
if (profile.dailyBounty && profile.dailyBounty.date === today) return profile.dailyBounty;
|
|
335
|
+
const rng = seededRandom(hashString(profile.uidHash + today + "bounty-salt"));
|
|
336
|
+
const state = {
|
|
337
|
+
date: today,
|
|
338
|
+
bounties: [
|
|
339
|
+
pickRandom(BRONZE_POOL, rng),
|
|
340
|
+
pickRandom(SILVER_POOL, rng),
|
|
341
|
+
pickRandom(GOLD_POOL, rng)
|
|
342
|
+
].map((template) => instantiateTemplate(template, rng)),
|
|
343
|
+
completedCount: 0
|
|
344
|
+
};
|
|
345
|
+
profile.dailyBounty = state;
|
|
346
|
+
return state;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* 将模板实例化为具体的悬赏令
|
|
350
|
+
*/
|
|
351
|
+
function instantiateTemplate(template, rng) {
|
|
352
|
+
let description = template.descriptionTemplate;
|
|
353
|
+
const condition = { ...template.condition };
|
|
354
|
+
if (template.hasProductPlaceholder) {
|
|
355
|
+
const product = pickRandom(AVAILABLE_PRODUCTS, rng);
|
|
356
|
+
description = description.replace("{product}", product);
|
|
357
|
+
condition.product = product;
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
id: template.id,
|
|
361
|
+
tier: template.tier,
|
|
362
|
+
description,
|
|
363
|
+
target: condition.count,
|
|
364
|
+
current: 0,
|
|
365
|
+
completed: false,
|
|
366
|
+
reward: { ...template.reward },
|
|
367
|
+
condition
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const QUALITY_RANK = {
|
|
371
|
+
normal: 0,
|
|
372
|
+
fine: 1,
|
|
373
|
+
rare: 2,
|
|
374
|
+
epic: 3,
|
|
375
|
+
legendary: 4,
|
|
376
|
+
shiny: 5
|
|
377
|
+
};
|
|
378
|
+
function isQualityAtLeast(actual, minimum) {
|
|
379
|
+
return QUALITY_RANK[actual] >= QUALITY_RANK[minimum];
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* 操作后更新悬赏令进度
|
|
383
|
+
*
|
|
384
|
+
* @returns 本次新完成的悬赏令列表
|
|
385
|
+
*/
|
|
386
|
+
function updateBountyProgress(profile, context) {
|
|
387
|
+
const bountyState = profile.dailyBounty;
|
|
388
|
+
if (!bountyState) return [];
|
|
389
|
+
const today = getDayKey();
|
|
390
|
+
if (bountyState.date !== today) return [];
|
|
391
|
+
const newlyCompleted = [];
|
|
392
|
+
for (const bounty of bountyState.bounties) {
|
|
393
|
+
if (bounty.completed) continue;
|
|
394
|
+
bounty.current;
|
|
395
|
+
updateSingleBountyProgress(bounty, context);
|
|
396
|
+
if (!bounty.completed && bounty.current >= bounty.target) {
|
|
397
|
+
bounty.completed = true;
|
|
398
|
+
bountyState.completedCount += 1;
|
|
399
|
+
newlyCompleted.push(bounty);
|
|
400
|
+
profile.totalExp += bounty.reward.exp;
|
|
401
|
+
profile.bountyHistory.totalCompleted += 1;
|
|
402
|
+
switch (bounty.tier) {
|
|
403
|
+
case "bronze":
|
|
404
|
+
profile.bountyHistory.bronzeCompleted += 1;
|
|
405
|
+
break;
|
|
406
|
+
case "silver":
|
|
407
|
+
profile.bountyHistory.silverCompleted += 1;
|
|
408
|
+
break;
|
|
409
|
+
case "gold":
|
|
410
|
+
profile.bountyHistory.goldCompleted += 1;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (bountyState.completedCount === 3) profile.bountyHistory.consecutiveFullClear += 1;
|
|
416
|
+
return newlyCompleted;
|
|
417
|
+
}
|
|
418
|
+
function updateSingleBountyProgress(bounty, ctx) {
|
|
419
|
+
const { condition } = bounty;
|
|
420
|
+
switch (condition.type) {
|
|
421
|
+
case "command":
|
|
422
|
+
if (ctx.commandSuccess) if (condition.product) {
|
|
423
|
+
if (ctx.product === condition.product) bounty.current += 1;
|
|
424
|
+
} else bounty.current += 1;
|
|
425
|
+
break;
|
|
426
|
+
case "capture":
|
|
427
|
+
if (ctx.dropResult && !ctx.dropResult.escaped) {
|
|
428
|
+
let matches = true;
|
|
429
|
+
if (condition.qualityMin) matches = matches && isQualityAtLeast(ctx.dropResult.monster.quality, condition.qualityMin);
|
|
430
|
+
if (condition.isUpMonster) matches = matches && ctx.dropResult.isUpMonster;
|
|
431
|
+
if (condition.isNew) matches = matches && ctx.dropResult.isNew;
|
|
432
|
+
if (condition.product) matches = matches && ctx.dropResult.monster.relatedProduct === condition.product;
|
|
433
|
+
if (matches) bounty.current += 1;
|
|
434
|
+
}
|
|
435
|
+
break;
|
|
436
|
+
case "combo":
|
|
437
|
+
bounty.current = Math.min(ctx.currentCombo, bounty.target);
|
|
438
|
+
break;
|
|
439
|
+
case "encounter":
|
|
440
|
+
if (ctx.encounterTriggered) if (bounty.id === "B206") {
|
|
441
|
+
if (ctx.encounterType === "treasure") bounty.current += 1;
|
|
442
|
+
} else bounty.current += 1;
|
|
443
|
+
break;
|
|
444
|
+
case "product_variety":
|
|
445
|
+
bounty.current = Math.min(ctx.todayProducts.size, bounty.target);
|
|
446
|
+
break;
|
|
447
|
+
case "quality_variety":
|
|
448
|
+
bounty.current = Math.min(ctx.todayQualities.size, bounty.target);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* 初始化默认的悬赏历史
|
|
454
|
+
*/
|
|
455
|
+
function createDefaultBountyHistory() {
|
|
456
|
+
return {
|
|
457
|
+
totalCompleted: 0,
|
|
458
|
+
bronzeCompleted: 0,
|
|
459
|
+
silverCompleted: 0,
|
|
460
|
+
goldCompleted: 0,
|
|
461
|
+
consecutiveFullClear: 0
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* 检查今日是否需要重置连续全清计数
|
|
466
|
+
* (如果昨天没有全部完成,重置为 0)
|
|
467
|
+
*/
|
|
468
|
+
function checkBountyDayReset(profile) {
|
|
469
|
+
const today = getDayKey();
|
|
470
|
+
if (profile.dailyBounty && profile.dailyBounty.date !== today) {
|
|
471
|
+
if (profile.dailyBounty.completedCount < 3) profile.bountyHistory.consecutiveFullClear = 0;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
//#endregion
|
|
475
|
+
//#region src/game-xiyou/random-event-engine.ts
|
|
476
|
+
/**
|
|
477
|
+
* 随机事件系统 (v2)
|
|
478
|
+
*
|
|
479
|
+
* 低概率触发的特殊剧情事件,打破日常节奏,制造惊喜和紧张感。
|
|
480
|
+
* 事件分为三类:增益(8%)、挑战(5%)、灾厄(3%)。
|
|
481
|
+
* 独立于掉落和机缘触发,同一事件 24 小时内不重复。
|
|
482
|
+
*/
|
|
483
|
+
function cryptoRandom$3() {
|
|
484
|
+
return randomBytes(4).readUInt32BE(0) / 4294967295;
|
|
485
|
+
}
|
|
486
|
+
const BLESSING_EVENTS = [
|
|
487
|
+
{
|
|
488
|
+
id: "EV001",
|
|
489
|
+
name: "蟠桃大会",
|
|
490
|
+
category: "blessing",
|
|
491
|
+
triggerRate: .015,
|
|
492
|
+
description: "接下来 10 次操作修行值 ×3",
|
|
493
|
+
flavorText: "王母娘娘设宴,蟠桃大会开席!修行值大涨!",
|
|
494
|
+
effect: {
|
|
495
|
+
type: "exp_multiplier",
|
|
496
|
+
value: 3,
|
|
497
|
+
targetCount: 10
|
|
498
|
+
},
|
|
499
|
+
duration: {
|
|
500
|
+
type: "operation_count",
|
|
501
|
+
total: 10,
|
|
502
|
+
remaining: 10
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
id: "EV002",
|
|
507
|
+
name: "月光宝盒",
|
|
508
|
+
category: "blessing",
|
|
509
|
+
triggerRate: .01,
|
|
510
|
+
description: "下一次掉落品质强制提升一级",
|
|
511
|
+
flavorText: "紫霞仙子留下的月光宝盒,时光倒流,命运改写!",
|
|
512
|
+
effect: {
|
|
513
|
+
type: "quality_boost",
|
|
514
|
+
value: 1
|
|
515
|
+
},
|
|
516
|
+
duration: {
|
|
517
|
+
type: "drop_count",
|
|
518
|
+
total: 1,
|
|
519
|
+
remaining: 1
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
id: "EV003",
|
|
524
|
+
name: "龙宫寻宝",
|
|
525
|
+
category: "blessing",
|
|
526
|
+
triggerRate: .015,
|
|
527
|
+
description: "立即额外触发一次掉落",
|
|
528
|
+
flavorText: "东海龙宫大门洞开,宝物任你挑选!",
|
|
529
|
+
effect: {
|
|
530
|
+
type: "extra_drop",
|
|
531
|
+
value: 1
|
|
532
|
+
},
|
|
533
|
+
duration: {
|
|
534
|
+
type: "instant",
|
|
535
|
+
total: 1,
|
|
536
|
+
remaining: 0
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
id: "EV004",
|
|
541
|
+
name: "仙人指路",
|
|
542
|
+
category: "blessing",
|
|
543
|
+
triggerRate: .02,
|
|
544
|
+
description: "下 5 次操作的产品关联权重 ×5",
|
|
545
|
+
flavorText: "南极仙翁路过,指了指前方:\"那边有好东西。\"",
|
|
546
|
+
effect: {
|
|
547
|
+
type: "product_weight",
|
|
548
|
+
value: 5,
|
|
549
|
+
targetCount: 5
|
|
550
|
+
},
|
|
551
|
+
duration: {
|
|
552
|
+
type: "operation_count",
|
|
553
|
+
total: 5,
|
|
554
|
+
remaining: 5
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: "EV005",
|
|
559
|
+
name: "蟠桃熟了",
|
|
560
|
+
category: "blessing",
|
|
561
|
+
triggerRate: .01,
|
|
562
|
+
description: "立即获得 30-100 随机修行值",
|
|
563
|
+
flavorText: "三千年一熟的蟠桃,今日恰好落入你手。",
|
|
564
|
+
effect: {
|
|
565
|
+
type: "exp_flat",
|
|
566
|
+
value: 0
|
|
567
|
+
},
|
|
568
|
+
duration: {
|
|
569
|
+
type: "instant",
|
|
570
|
+
total: 1,
|
|
571
|
+
remaining: 0
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
id: "EV006",
|
|
576
|
+
name: "土地公的宝箱",
|
|
577
|
+
category: "blessing",
|
|
578
|
+
triggerRate: .01,
|
|
579
|
+
description: "随机获得一件一次性法宝",
|
|
580
|
+
flavorText: "土地公从地下冒出来:\"大圣,这个给你!\"",
|
|
581
|
+
effect: {
|
|
582
|
+
type: "treasure_grant",
|
|
583
|
+
value: 1
|
|
584
|
+
},
|
|
585
|
+
duration: {
|
|
586
|
+
type: "instant",
|
|
587
|
+
total: 1,
|
|
588
|
+
remaining: 0
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
];
|
|
592
|
+
const CHALLENGE_EVENTS_DATA = [
|
|
593
|
+
{
|
|
594
|
+
id: "EV101",
|
|
595
|
+
name: "妖王入侵",
|
|
596
|
+
category: "challenge",
|
|
597
|
+
triggerRate: .015,
|
|
598
|
+
description: "接下来连续成功 5 次",
|
|
599
|
+
flavorText: "一股强大的妖气从远方袭来——\"哈哈哈,齐天大圣不过如此!\"",
|
|
600
|
+
effect: {
|
|
601
|
+
type: "exp_flat",
|
|
602
|
+
value: 0
|
|
603
|
+
},
|
|
604
|
+
duration: {
|
|
605
|
+
type: "operation_count",
|
|
606
|
+
total: 8,
|
|
607
|
+
remaining: 8
|
|
608
|
+
},
|
|
609
|
+
challengeCondition: {
|
|
610
|
+
type: "consecutive_success",
|
|
611
|
+
target: 5,
|
|
612
|
+
current: 0
|
|
613
|
+
},
|
|
614
|
+
successReward: {
|
|
615
|
+
exp: 80,
|
|
616
|
+
pityBonus: 5
|
|
617
|
+
},
|
|
618
|
+
failurePenalty: { expLoss: 30 },
|
|
619
|
+
operationLimit: 8
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
id: "EV102",
|
|
623
|
+
name: "火焰山",
|
|
624
|
+
category: "challenge",
|
|
625
|
+
triggerRate: .01,
|
|
626
|
+
description: "接下来 10 次操作中使用 ≥3 种不同产品",
|
|
627
|
+
flavorText: "火焰山烈焰滔天,唯有多方尝试方能通过!",
|
|
628
|
+
effect: {
|
|
629
|
+
type: "exp_flat",
|
|
630
|
+
value: 0
|
|
631
|
+
},
|
|
632
|
+
duration: {
|
|
633
|
+
type: "operation_count",
|
|
634
|
+
total: 10,
|
|
635
|
+
remaining: 10
|
|
636
|
+
},
|
|
637
|
+
challengeCondition: {
|
|
638
|
+
type: "product_variety",
|
|
639
|
+
target: 3,
|
|
640
|
+
current: 0
|
|
641
|
+
},
|
|
642
|
+
successReward: {
|
|
643
|
+
exp: 60,
|
|
644
|
+
escapeRateMod: -.05
|
|
645
|
+
},
|
|
646
|
+
failurePenalty: {
|
|
647
|
+
expLoss: 0,
|
|
648
|
+
comboReset: true
|
|
649
|
+
},
|
|
650
|
+
operationLimit: 10
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
id: "EV103",
|
|
654
|
+
name: "通天河阻路",
|
|
655
|
+
category: "challenge",
|
|
656
|
+
triggerRate: .01,
|
|
657
|
+
description: "接下来连续成功 3 次且收服 ≥1 只妖怪",
|
|
658
|
+
flavorText: "通天河水势汹涌,需要勇气和实力才能渡过!",
|
|
659
|
+
effect: {
|
|
660
|
+
type: "exp_flat",
|
|
661
|
+
value: 0
|
|
662
|
+
},
|
|
663
|
+
duration: {
|
|
664
|
+
type: "operation_count",
|
|
665
|
+
total: 5,
|
|
666
|
+
remaining: 5
|
|
667
|
+
},
|
|
668
|
+
challengeCondition: {
|
|
669
|
+
type: "consecutive_success",
|
|
670
|
+
target: 3,
|
|
671
|
+
current: 0
|
|
672
|
+
},
|
|
673
|
+
successReward: {
|
|
674
|
+
exp: 100,
|
|
675
|
+
extraDrop: true
|
|
676
|
+
},
|
|
677
|
+
failurePenalty: { expLoss: 20 },
|
|
678
|
+
operationLimit: 5
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
id: "EV104",
|
|
682
|
+
name: "真假美猴王",
|
|
683
|
+
category: "challenge",
|
|
684
|
+
triggerRate: .005,
|
|
685
|
+
description: "接下来 3 次掉落中选出\"真\"的那只",
|
|
686
|
+
flavorText: "六耳猕猴现身,真假难辨!",
|
|
687
|
+
effect: {
|
|
688
|
+
type: "exp_flat",
|
|
689
|
+
value: 0
|
|
690
|
+
},
|
|
691
|
+
duration: {
|
|
692
|
+
type: "drop_count",
|
|
693
|
+
total: 3,
|
|
694
|
+
remaining: 3
|
|
695
|
+
},
|
|
696
|
+
challengeCondition: {
|
|
697
|
+
type: "pick_correct",
|
|
698
|
+
target: 1,
|
|
699
|
+
current: 0
|
|
700
|
+
},
|
|
701
|
+
successReward: { exp: 100 },
|
|
702
|
+
failurePenalty: { expLoss: 0 },
|
|
703
|
+
operationLimit: 3
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
id: "EV105",
|
|
707
|
+
name: "盘丝洞迷阵",
|
|
708
|
+
category: "challenge",
|
|
709
|
+
triggerRate: .005,
|
|
710
|
+
description: "接下来 5 次操作必须使用 5 种不同产品",
|
|
711
|
+
flavorText: "蜘蛛精的丝网密布,每一步都不能重复!",
|
|
712
|
+
effect: {
|
|
713
|
+
type: "exp_flat",
|
|
714
|
+
value: 0
|
|
715
|
+
},
|
|
716
|
+
duration: {
|
|
717
|
+
type: "operation_count",
|
|
718
|
+
total: 5,
|
|
719
|
+
remaining: 5
|
|
720
|
+
},
|
|
721
|
+
challengeCondition: {
|
|
722
|
+
type: "product_variety",
|
|
723
|
+
target: 5,
|
|
724
|
+
current: 0
|
|
725
|
+
},
|
|
726
|
+
successReward: {
|
|
727
|
+
exp: 120,
|
|
728
|
+
monster: { qualityMin: "rare" }
|
|
729
|
+
},
|
|
730
|
+
failurePenalty: { expLoss: 50 },
|
|
731
|
+
operationLimit: 5
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
id: "EV106",
|
|
735
|
+
name: "比丘国救童",
|
|
736
|
+
category: "challenge",
|
|
737
|
+
triggerRate: .005,
|
|
738
|
+
description: "接下来 8 次操作中成功率 ≥80%",
|
|
739
|
+
flavorText: "比丘国的孩子们需要你的帮助!",
|
|
740
|
+
effect: {
|
|
741
|
+
type: "exp_flat",
|
|
742
|
+
value: 0
|
|
743
|
+
},
|
|
744
|
+
duration: {
|
|
745
|
+
type: "operation_count",
|
|
746
|
+
total: 8,
|
|
747
|
+
remaining: 8
|
|
748
|
+
},
|
|
749
|
+
challengeCondition: {
|
|
750
|
+
type: "success_rate",
|
|
751
|
+
target: 7,
|
|
752
|
+
current: 0
|
|
753
|
+
},
|
|
754
|
+
successReward: {
|
|
755
|
+
exp: 150,
|
|
756
|
+
treasureFragment: "random"
|
|
757
|
+
},
|
|
758
|
+
failurePenalty: { expLoss: 40 },
|
|
759
|
+
operationLimit: 8
|
|
760
|
+
}
|
|
761
|
+
];
|
|
762
|
+
const DISASTER_EVENTS = [
|
|
763
|
+
{
|
|
764
|
+
id: "EV201",
|
|
765
|
+
name: "黑风来袭",
|
|
766
|
+
category: "disaster",
|
|
767
|
+
triggerRate: .01,
|
|
768
|
+
description: "接下来 5 次掉落逃跑率 +20%",
|
|
769
|
+
flavorText: "黑风山的妖气蔓延而来——\"嘿嘿嘿,让你的妖怪都跑光!\"",
|
|
770
|
+
effect: {
|
|
771
|
+
type: "escape_rate_mod",
|
|
772
|
+
value: .2
|
|
773
|
+
},
|
|
774
|
+
duration: {
|
|
775
|
+
type: "drop_count",
|
|
776
|
+
total: 5,
|
|
777
|
+
remaining: 5
|
|
778
|
+
},
|
|
779
|
+
resolution: {
|
|
780
|
+
type: "consecutive_success",
|
|
781
|
+
count: 3,
|
|
782
|
+
description: "连续成功 3 次可提前解除"
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
id: "EV202",
|
|
787
|
+
name: "金蝉脱壳",
|
|
788
|
+
category: "disaster",
|
|
789
|
+
triggerRate: .005,
|
|
790
|
+
description: "随机一只已收服的精良妖怪逃跑",
|
|
791
|
+
flavorText: "一阵妖风吹过,你的妖怪图鉴闪了一下……",
|
|
792
|
+
effect: {
|
|
793
|
+
type: "monster_escape",
|
|
794
|
+
value: 1
|
|
795
|
+
},
|
|
796
|
+
duration: {
|
|
797
|
+
type: "instant",
|
|
798
|
+
total: 1,
|
|
799
|
+
remaining: 0
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
id: "EV203",
|
|
804
|
+
name: "妖雾弥漫",
|
|
805
|
+
category: "disaster",
|
|
806
|
+
triggerRate: .008,
|
|
807
|
+
description: "接下来 3 次掉落品质上限降为精良",
|
|
808
|
+
flavorText: "浓雾笼罩,视线模糊,只能看到近处的小妖……",
|
|
809
|
+
effect: {
|
|
810
|
+
type: "quality_cap",
|
|
811
|
+
value: 1
|
|
812
|
+
},
|
|
813
|
+
duration: {
|
|
814
|
+
type: "drop_count",
|
|
815
|
+
total: 3,
|
|
816
|
+
remaining: 3
|
|
817
|
+
},
|
|
818
|
+
resolution: {
|
|
819
|
+
type: "use_treasure",
|
|
820
|
+
treasureId: "zhaoyaojing",
|
|
821
|
+
description: "使用法宝\"照妖镜\"可立即解除"
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
id: "EV204",
|
|
826
|
+
name: "紧箍咒发作",
|
|
827
|
+
category: "disaster",
|
|
828
|
+
triggerRate: .004,
|
|
829
|
+
description: "接下来 5 次操作修行值减半",
|
|
830
|
+
flavorText: "头痛欲裂!唐僧又念紧箍咒了!",
|
|
831
|
+
effect: {
|
|
832
|
+
type: "exp_halve",
|
|
833
|
+
value: .5
|
|
834
|
+
},
|
|
835
|
+
duration: {
|
|
836
|
+
type: "operation_count",
|
|
837
|
+
total: 5,
|
|
838
|
+
remaining: 5
|
|
839
|
+
},
|
|
840
|
+
resolution: {
|
|
841
|
+
type: "complete_bounty",
|
|
842
|
+
description: "完成 1 张悬赏令可提前解除"
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
id: "EV205",
|
|
847
|
+
name: "五行山镇压",
|
|
848
|
+
category: "disaster",
|
|
849
|
+
triggerRate: .002,
|
|
850
|
+
description: "连击归零 + 接下来 3 次操作无掉落",
|
|
851
|
+
flavorText: "五行山从天而降,压得你动弹不得!",
|
|
852
|
+
effect: {
|
|
853
|
+
type: "no_drop",
|
|
854
|
+
value: 3
|
|
855
|
+
},
|
|
856
|
+
duration: {
|
|
857
|
+
type: "operation_count",
|
|
858
|
+
total: 3,
|
|
859
|
+
remaining: 3
|
|
860
|
+
},
|
|
861
|
+
resolution: {
|
|
862
|
+
type: "trigger_encounter",
|
|
863
|
+
description: "触发 1 次神仙机缘可提前解除"
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
id: "EV206",
|
|
868
|
+
name: "走火入魔",
|
|
869
|
+
category: "disaster",
|
|
870
|
+
triggerRate: .001,
|
|
871
|
+
description: "修行值 -100 + 保底计数器全部 -10",
|
|
872
|
+
flavorText: "修炼走火入魔,功力大损!",
|
|
873
|
+
effect: {
|
|
874
|
+
type: "exp_loss",
|
|
875
|
+
value: 100
|
|
876
|
+
},
|
|
877
|
+
duration: {
|
|
878
|
+
type: "instant",
|
|
879
|
+
total: 1,
|
|
880
|
+
remaining: 0
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
];
|
|
884
|
+
/**
|
|
885
|
+
* 创建默认的活跃事件状态
|
|
886
|
+
*/
|
|
887
|
+
function createDefaultActiveEventState() {
|
|
888
|
+
return {
|
|
889
|
+
currentEvents: [],
|
|
890
|
+
activeChallenge: null,
|
|
891
|
+
lastEventTriggerTime: {}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* 创建默认的事件统计
|
|
896
|
+
*/
|
|
897
|
+
function createDefaultEventStats() {
|
|
898
|
+
return {
|
|
899
|
+
totalTriggered: 0,
|
|
900
|
+
challengesCompleted: 0,
|
|
901
|
+
challengesFailed: 0,
|
|
902
|
+
disastersResolved: 0
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* 检查并触发随机事件
|
|
907
|
+
*
|
|
908
|
+
* @returns 触发的事件,或 null
|
|
909
|
+
*/
|
|
910
|
+
function checkRandomEvent(profile) {
|
|
911
|
+
const now = Date.now();
|
|
912
|
+
const oneDayMs = 1440 * 60 * 1e3;
|
|
913
|
+
const candidates = [
|
|
914
|
+
...BLESSING_EVENTS,
|
|
915
|
+
...CHALLENGE_EVENTS_DATA,
|
|
916
|
+
...DISASTER_EVENTS
|
|
917
|
+
];
|
|
918
|
+
for (const candidate of candidates) {
|
|
919
|
+
if (now - (profile.activeEvents.lastEventTriggerTime[candidate.id] ?? 0) < oneDayMs) continue;
|
|
920
|
+
if (candidate.category === "challenge" && profile.activeEvents.activeChallenge) continue;
|
|
921
|
+
if (cryptoRandom$3() < candidate.triggerRate) {
|
|
922
|
+
profile.activeEvents.lastEventTriggerTime[candidate.id] = now;
|
|
923
|
+
profile.eventStats.totalTriggered += 1;
|
|
924
|
+
const event = instantiateEvent(candidate, profile);
|
|
925
|
+
if (event.category === "challenge") profile.activeEvents.activeChallenge = event;
|
|
926
|
+
else if (event.duration.type !== "instant") profile.activeEvents.currentEvents.push(event);
|
|
927
|
+
profile.eventHistory.push({
|
|
928
|
+
eventId: event.id,
|
|
929
|
+
triggeredAt: now
|
|
930
|
+
});
|
|
931
|
+
return event;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
/** 一次性法宝 ID 池(用于 EV006 土地公的宝箱) */
|
|
937
|
+
const CONSUMABLE_TREASURE_IDS = ["pantao", "renshenguo"];
|
|
938
|
+
/**
|
|
939
|
+
* 实例化事件(处理随机值、即时效果等)
|
|
940
|
+
*
|
|
941
|
+
* 即时事件的效果在此函数中直接应用到 profile。
|
|
942
|
+
* 持续性事件仅初始化状态,效果由 tickActiveEvents / 查询函数处理。
|
|
943
|
+
*/
|
|
944
|
+
function instantiateEvent(template, profile) {
|
|
945
|
+
const event = JSON.parse(JSON.stringify(template));
|
|
946
|
+
switch (event.id) {
|
|
947
|
+
case "EV005": {
|
|
948
|
+
const expGain = Math.floor(30 + cryptoRandom$3() * 71);
|
|
949
|
+
event.effect.value = expGain;
|
|
950
|
+
profile.totalExp += expGain;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
case "EV006": {
|
|
954
|
+
const treasureId = CONSUMABLE_TREASURE_IDS[Math.floor(cryptoRandom$3() * CONSUMABLE_TREASURE_IDS.length)];
|
|
955
|
+
event.effect.value = 1;
|
|
956
|
+
if (!profile.treasures.includes(treasureId)) profile.treasures.push(treasureId);
|
|
957
|
+
event.description = `获得了一次性法宝:${treasureId === "pantao" ? "蟠桃" : "人参果"}`;
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
case "EV202":
|
|
961
|
+
event.effect.value = 1;
|
|
962
|
+
break;
|
|
963
|
+
case "EV205":
|
|
964
|
+
profile.currentCombo = 0;
|
|
965
|
+
break;
|
|
966
|
+
case "EV206":
|
|
967
|
+
profile.totalExp = Math.max(0, profile.totalExp - 100);
|
|
968
|
+
profile.pityCounters.sinceLastRare = Math.max(0, profile.pityCounters.sinceLastRare - 10);
|
|
969
|
+
profile.pityCounters.sinceLastEpic = Math.max(0, profile.pityCounters.sinceLastEpic - 10);
|
|
970
|
+
profile.pityCounters.sinceLastLegendary = Math.max(0, profile.pityCounters.sinceLastLegendary - 10);
|
|
971
|
+
profile.pityCounters.totalDropsWithoutShiny = Math.max(0, profile.pityCounters.totalDropsWithoutShiny - 10);
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
if (event.category === "challenge") {
|
|
975
|
+
const challengeEvent = event;
|
|
976
|
+
challengeEvent.progress = {
|
|
977
|
+
operationsUsed: 0,
|
|
978
|
+
operationLimit: challengeEvent.operationLimit,
|
|
979
|
+
conditionMet: false,
|
|
980
|
+
usedProducts: []
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
return event;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* 每次操作后更新活跃事件的持续时间和状态
|
|
987
|
+
*
|
|
988
|
+
* @returns 本次过期/完成的事件列表
|
|
989
|
+
*/
|
|
990
|
+
function tickActiveEvents(profile, operationSuccess, product, capturedMonster) {
|
|
991
|
+
const results = [];
|
|
992
|
+
const remainingEvents = [];
|
|
993
|
+
for (const event of profile.activeEvents.currentEvents) {
|
|
994
|
+
if (event.duration.type === "operation_count") event.duration.remaining -= 1;
|
|
995
|
+
if (event.category === "disaster" && event.resolution) {
|
|
996
|
+
if (checkResolution(event.resolution, profile, operationSuccess)) {
|
|
997
|
+
results.push({
|
|
998
|
+
event,
|
|
999
|
+
outcome: "resolved"
|
|
1000
|
+
});
|
|
1001
|
+
profile.eventStats.disastersResolved += 1;
|
|
1002
|
+
updateEventHistory(profile, event.id, "resolved");
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (event.duration.remaining <= 0) {
|
|
1007
|
+
results.push({
|
|
1008
|
+
event,
|
|
1009
|
+
outcome: "expired"
|
|
1010
|
+
});
|
|
1011
|
+
updateEventHistory(profile, event.id, "expired");
|
|
1012
|
+
} else remainingEvents.push(event);
|
|
1013
|
+
}
|
|
1014
|
+
profile.activeEvents.currentEvents = remainingEvents;
|
|
1015
|
+
const challenge = profile.activeEvents.activeChallenge;
|
|
1016
|
+
if (challenge) {
|
|
1017
|
+
challenge.progress.operationsUsed += 1;
|
|
1018
|
+
challenge.duration.remaining -= 1;
|
|
1019
|
+
updateChallengeProgress(challenge, operationSuccess, product, capturedMonster);
|
|
1020
|
+
if (challenge.progress.conditionMet) {
|
|
1021
|
+
results.push({
|
|
1022
|
+
event: challenge,
|
|
1023
|
+
outcome: "success"
|
|
1024
|
+
});
|
|
1025
|
+
applyChallengeReward(profile, challenge);
|
|
1026
|
+
profile.eventStats.challengesCompleted += 1;
|
|
1027
|
+
updateEventHistory(profile, challenge.id, "success");
|
|
1028
|
+
profile.activeEvents.activeChallenge = null;
|
|
1029
|
+
} else if (challenge.progress.operationsUsed >= challenge.progress.operationLimit) {
|
|
1030
|
+
results.push({
|
|
1031
|
+
event: challenge,
|
|
1032
|
+
outcome: "failure"
|
|
1033
|
+
});
|
|
1034
|
+
applyChallengePenalty(profile, challenge);
|
|
1035
|
+
profile.eventStats.challengesFailed += 1;
|
|
1036
|
+
updateEventHistory(profile, challenge.id, "failure");
|
|
1037
|
+
profile.activeEvents.activeChallenge = null;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return results;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* 掉落时递减 drop_count 类型事件的剩余次数
|
|
1044
|
+
*/
|
|
1045
|
+
function tickDropEvents(profile) {
|
|
1046
|
+
for (const event of profile.activeEvents.currentEvents) if (event.duration.type === "drop_count") event.duration.remaining -= 1;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* 获取当前活跃的修行值倍率修正
|
|
1050
|
+
*/
|
|
1051
|
+
function getActiveExpMultiplier(profile) {
|
|
1052
|
+
let multiplier = 1;
|
|
1053
|
+
for (const event of profile.activeEvents.currentEvents) {
|
|
1054
|
+
if (event.effect.type === "exp_multiplier") multiplier *= event.effect.value;
|
|
1055
|
+
if (event.effect.type === "exp_halve") multiplier *= event.effect.value;
|
|
1056
|
+
}
|
|
1057
|
+
return multiplier;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* 检查是否有品质提升事件
|
|
1061
|
+
*/
|
|
1062
|
+
function getQualityBoost(profile) {
|
|
1063
|
+
for (const event of profile.activeEvents.currentEvents) if (event.effect.type === "quality_boost" && event.duration.remaining > 0) return event.effect.value;
|
|
1064
|
+
return 0;
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* 检查是否有品质上限事件
|
|
1068
|
+
*/
|
|
1069
|
+
function getQualityCap(profile) {
|
|
1070
|
+
for (const event of profile.activeEvents.currentEvents) if (event.effect.type === "quality_cap" && event.duration.remaining > 0) return "fine";
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* 检查是否有禁止掉落事件
|
|
1075
|
+
*/
|
|
1076
|
+
function isDropSuppressed(profile) {
|
|
1077
|
+
return profile.activeEvents.currentEvents.some((e) => e.effect.type === "no_drop" && e.duration.remaining > 0);
|
|
1078
|
+
}
|
|
1079
|
+
function checkResolution(resolution, profile, operationSuccess) {
|
|
1080
|
+
switch (resolution.type) {
|
|
1081
|
+
case "consecutive_success": return profile.currentCombo >= (resolution.count ?? 3);
|
|
1082
|
+
case "use_treasure": return false;
|
|
1083
|
+
case "complete_bounty": return false;
|
|
1084
|
+
case "trigger_encounter": return false;
|
|
1085
|
+
default: return false;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* 手动解除灾厄事件(由外部系统调用)
|
|
1090
|
+
*/
|
|
1091
|
+
function resolveDisasterEvent(profile, resolutionType) {
|
|
1092
|
+
const index = profile.activeEvents.currentEvents.findIndex((e) => e.category === "disaster" && e.resolution?.type === resolutionType);
|
|
1093
|
+
if (index === -1) return null;
|
|
1094
|
+
const event = profile.activeEvents.currentEvents[index];
|
|
1095
|
+
profile.activeEvents.currentEvents.splice(index, 1);
|
|
1096
|
+
profile.eventStats.disastersResolved += 1;
|
|
1097
|
+
updateEventHistory(profile, event.id, "resolved");
|
|
1098
|
+
return event;
|
|
1099
|
+
}
|
|
1100
|
+
function updateChallengeProgress(challenge, operationSuccess, product, capturedMonster) {
|
|
1101
|
+
const usedProducts = challenge.progress.usedProducts ?? [];
|
|
1102
|
+
switch (challenge.challengeCondition.type) {
|
|
1103
|
+
case "consecutive_success":
|
|
1104
|
+
if (operationSuccess) challenge.challengeCondition.current += 1;
|
|
1105
|
+
else challenge.challengeCondition.current = 0;
|
|
1106
|
+
break;
|
|
1107
|
+
case "product_variety":
|
|
1108
|
+
if (!usedProducts.includes(product)) {
|
|
1109
|
+
usedProducts.push(product);
|
|
1110
|
+
challenge.progress.usedProducts = usedProducts;
|
|
1111
|
+
}
|
|
1112
|
+
challenge.challengeCondition.current = usedProducts.length;
|
|
1113
|
+
break;
|
|
1114
|
+
case "success_rate":
|
|
1115
|
+
if (operationSuccess) challenge.challengeCondition.current += 1;
|
|
1116
|
+
break;
|
|
1117
|
+
case "capture_count":
|
|
1118
|
+
if (capturedMonster) challenge.challengeCondition.current += 1;
|
|
1119
|
+
break;
|
|
1120
|
+
case "pick_correct":
|
|
1121
|
+
if (cryptoRandom$3() < 1 / 3) challenge.challengeCondition.current += 1;
|
|
1122
|
+
break;
|
|
1123
|
+
}
|
|
1124
|
+
if (challenge.challengeCondition.current >= challenge.challengeCondition.target) challenge.progress.conditionMet = true;
|
|
1125
|
+
}
|
|
1126
|
+
function applyChallengeReward(profile, challenge) {
|
|
1127
|
+
const reward = challenge.successReward;
|
|
1128
|
+
profile.totalExp += reward.exp;
|
|
1129
|
+
if (reward.pityBonus) {
|
|
1130
|
+
profile.pityCounters.sinceLastRare += reward.pityBonus;
|
|
1131
|
+
profile.pityCounters.sinceLastEpic += reward.pityBonus;
|
|
1132
|
+
profile.pityCounters.sinceLastLegendary += reward.pityBonus;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function applyChallengePenalty(profile, challenge) {
|
|
1136
|
+
const penalty = challenge.failurePenalty;
|
|
1137
|
+
profile.totalExp = Math.max(0, profile.totalExp - penalty.expLoss);
|
|
1138
|
+
if (penalty.comboReset) profile.currentCombo = 0;
|
|
1139
|
+
if (penalty.pityLoss) {
|
|
1140
|
+
profile.pityCounters.sinceLastRare = Math.max(0, profile.pityCounters.sinceLastRare - penalty.pityLoss);
|
|
1141
|
+
profile.pityCounters.sinceLastEpic = Math.max(0, profile.pityCounters.sinceLastEpic - penalty.pityLoss);
|
|
1142
|
+
profile.pityCounters.sinceLastLegendary = Math.max(0, profile.pityCounters.sinceLastLegendary - penalty.pityLoss);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function updateEventHistory(profile, eventId, outcome) {
|
|
1146
|
+
const entry = profile.eventHistory.find((e) => e.eventId === eventId && !e.outcome);
|
|
1147
|
+
if (entry) entry.outcome = outcome;
|
|
1148
|
+
}
|
|
1149
|
+
//#endregion
|
|
1150
|
+
//#region src/game-xiyou/storage.ts
|
|
1151
|
+
/**
|
|
1152
|
+
* JSON 持久化层
|
|
1153
|
+
*
|
|
1154
|
+
* 所有养成数据存储在 ~/.dingtalk-connector/gamification/ 目录下,
|
|
1155
|
+
* 按 UID 哈希分文件存储,支持 profile / collection / history 三类数据。
|
|
1156
|
+
*/
|
|
1157
|
+
const STORAGE_DIR = path.join(os.homedir(), ".dingtalk-connector", "gamification");
|
|
1158
|
+
const CHECKSUM_SECRET = "xiyou-hmac-secret-2026";
|
|
1159
|
+
function ensureStorageDir() {
|
|
1160
|
+
if (!fs.existsSync(STORAGE_DIR)) fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
|
1161
|
+
}
|
|
1162
|
+
function getProfilePath(uidHash) {
|
|
1163
|
+
return path.join(STORAGE_DIR, `profile-${uidHash}.json`);
|
|
1164
|
+
}
|
|
1165
|
+
function getCollectionPath(uidHash) {
|
|
1166
|
+
return path.join(STORAGE_DIR, `collection-${uidHash}.json`);
|
|
1167
|
+
}
|
|
1168
|
+
function getHistoryPath(uidHash) {
|
|
1169
|
+
return path.join(STORAGE_DIR, `history-${uidHash}.json`);
|
|
1170
|
+
}
|
|
1171
|
+
function computeChecksum(profile) {
|
|
1172
|
+
const payload = `${profile.uidHash}:${profile.totalExp}:${profile.level}:${profile.totalOperations}`;
|
|
1173
|
+
return createHmac("sha256", CHECKSUM_SECRET).update(payload).digest("hex").slice(0, 32);
|
|
1174
|
+
}
|
|
1175
|
+
function createDefaultProfile(uidHash) {
|
|
1176
|
+
const profile = {
|
|
1177
|
+
uidHash,
|
|
1178
|
+
level: 1,
|
|
1179
|
+
title: "凡人",
|
|
1180
|
+
totalExp: 0,
|
|
1181
|
+
totalOperations: 0,
|
|
1182
|
+
currentCombo: 0,
|
|
1183
|
+
maxCombo: 0,
|
|
1184
|
+
consecutiveSignInDays: 0,
|
|
1185
|
+
lastSignInDate: "",
|
|
1186
|
+
totalRecoveries: 0,
|
|
1187
|
+
consecutiveFailures: 0,
|
|
1188
|
+
productUsage: {},
|
|
1189
|
+
pityCounters: {
|
|
1190
|
+
sinceLastRare: 0,
|
|
1191
|
+
sinceLastEpic: 0,
|
|
1192
|
+
sinceLastLegendary: 0,
|
|
1193
|
+
totalDropsWithoutShiny: 0
|
|
1194
|
+
},
|
|
1195
|
+
buffs: [],
|
|
1196
|
+
settings: {
|
|
1197
|
+
enabled: false,
|
|
1198
|
+
showDropAnimation: true,
|
|
1199
|
+
muteNormalDrops: false
|
|
1200
|
+
},
|
|
1201
|
+
encounters: [],
|
|
1202
|
+
unlockedAchievements: [],
|
|
1203
|
+
treasures: [],
|
|
1204
|
+
consumedTreasures: [],
|
|
1205
|
+
createdAt: Date.now(),
|
|
1206
|
+
escapeHistory: {},
|
|
1207
|
+
totalEscapes: 0,
|
|
1208
|
+
dailyBounty: null,
|
|
1209
|
+
bountyHistory: createDefaultBountyHistory(),
|
|
1210
|
+
activeEvents: createDefaultActiveEventState(),
|
|
1211
|
+
eventStats: createDefaultEventStats(),
|
|
1212
|
+
eventHistory: []
|
|
1213
|
+
};
|
|
1214
|
+
return {
|
|
1215
|
+
...profile,
|
|
1216
|
+
checksum: computeChecksum(profile)
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function loadProfile(uidHash) {
|
|
1220
|
+
ensureStorageDir();
|
|
1221
|
+
const filePath = getProfilePath(uidHash);
|
|
1222
|
+
if (!fs.existsSync(filePath)) {
|
|
1223
|
+
const profile = createDefaultProfile(uidHash);
|
|
1224
|
+
saveProfile(profile);
|
|
1225
|
+
return profile;
|
|
1226
|
+
}
|
|
1227
|
+
try {
|
|
1228
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1229
|
+
return migrateProfile(JSON.parse(raw));
|
|
1230
|
+
} catch {
|
|
1231
|
+
const profile = createDefaultProfile(uidHash);
|
|
1232
|
+
saveProfile(profile);
|
|
1233
|
+
return profile;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* 补全旧版本 profile 中缺失的 v2 字段,确保向后兼容
|
|
1238
|
+
*/
|
|
1239
|
+
function migrateProfile(profile) {
|
|
1240
|
+
let migrated = false;
|
|
1241
|
+
if (!profile.escapeHistory) {
|
|
1242
|
+
profile.escapeHistory = {};
|
|
1243
|
+
migrated = true;
|
|
1244
|
+
}
|
|
1245
|
+
if (profile.totalEscapes == null) {
|
|
1246
|
+
profile.totalEscapes = 0;
|
|
1247
|
+
migrated = true;
|
|
1248
|
+
}
|
|
1249
|
+
if (profile.dailyBounty === void 0) {
|
|
1250
|
+
profile.dailyBounty = null;
|
|
1251
|
+
migrated = true;
|
|
1252
|
+
}
|
|
1253
|
+
if (!profile.bountyHistory) {
|
|
1254
|
+
profile.bountyHistory = createDefaultBountyHistory();
|
|
1255
|
+
migrated = true;
|
|
1256
|
+
}
|
|
1257
|
+
if (!profile.activeEvents) {
|
|
1258
|
+
profile.activeEvents = createDefaultActiveEventState();
|
|
1259
|
+
migrated = true;
|
|
1260
|
+
}
|
|
1261
|
+
if (!profile.eventStats) {
|
|
1262
|
+
profile.eventStats = createDefaultEventStats();
|
|
1263
|
+
migrated = true;
|
|
1264
|
+
}
|
|
1265
|
+
if (!profile.eventHistory) {
|
|
1266
|
+
profile.eventHistory = [];
|
|
1267
|
+
migrated = true;
|
|
1268
|
+
}
|
|
1269
|
+
if (migrated) saveProfile(profile);
|
|
1270
|
+
return profile;
|
|
1271
|
+
}
|
|
1272
|
+
function saveProfile(profile) {
|
|
1273
|
+
ensureStorageDir();
|
|
1274
|
+
const withChecksum = {
|
|
1275
|
+
...profile,
|
|
1276
|
+
checksum: computeChecksum(profile)
|
|
1277
|
+
};
|
|
1278
|
+
const filePath = getProfilePath(profile.uidHash);
|
|
1279
|
+
fs.writeFileSync(filePath, JSON.stringify(withChecksum, null, 2), "utf-8");
|
|
1280
|
+
}
|
|
1281
|
+
function createDefaultCollection(uidHash) {
|
|
1282
|
+
return {
|
|
1283
|
+
uidHash,
|
|
1284
|
+
entries: []
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
function loadCollection(uidHash) {
|
|
1288
|
+
ensureStorageDir();
|
|
1289
|
+
const filePath = getCollectionPath(uidHash);
|
|
1290
|
+
if (!fs.existsSync(filePath)) {
|
|
1291
|
+
const collection = createDefaultCollection(uidHash);
|
|
1292
|
+
saveCollection(collection);
|
|
1293
|
+
return collection;
|
|
1294
|
+
}
|
|
1295
|
+
try {
|
|
1296
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1297
|
+
return JSON.parse(raw);
|
|
1298
|
+
} catch {
|
|
1299
|
+
const collection = createDefaultCollection(uidHash);
|
|
1300
|
+
saveCollection(collection);
|
|
1301
|
+
return collection;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
function saveCollection(collection) {
|
|
1305
|
+
ensureStorageDir();
|
|
1306
|
+
const filePath = getCollectionPath(collection.uidHash);
|
|
1307
|
+
fs.writeFileSync(filePath, JSON.stringify(collection, null, 2), "utf-8");
|
|
1308
|
+
}
|
|
1309
|
+
const MAX_HISTORY_RECORDS = 500;
|
|
1310
|
+
function createDefaultHistory(uidHash) {
|
|
1311
|
+
return {
|
|
1312
|
+
uidHash,
|
|
1313
|
+
records: []
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function loadHistory(uidHash) {
|
|
1317
|
+
ensureStorageDir();
|
|
1318
|
+
const filePath = getHistoryPath(uidHash);
|
|
1319
|
+
if (!fs.existsSync(filePath)) return createDefaultHistory(uidHash);
|
|
1320
|
+
try {
|
|
1321
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1322
|
+
return JSON.parse(raw);
|
|
1323
|
+
} catch {
|
|
1324
|
+
return createDefaultHistory(uidHash);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
function saveHistory(history) {
|
|
1328
|
+
ensureStorageDir();
|
|
1329
|
+
if (history.records.length > MAX_HISTORY_RECORDS) history.records = history.records.slice(-MAX_HISTORY_RECORDS);
|
|
1330
|
+
const filePath = getHistoryPath(history.uidHash);
|
|
1331
|
+
fs.writeFileSync(filePath, JSON.stringify(history, null, 2), "utf-8");
|
|
1332
|
+
}
|
|
1333
|
+
//#endregion
|
|
1334
|
+
//#region src/game-xiyou/types.ts
|
|
1335
|
+
const QUALITY_LABELS = {
|
|
1336
|
+
normal: "🪨 普通",
|
|
1337
|
+
fine: "🌿 精良",
|
|
1338
|
+
rare: "💎 稀有",
|
|
1339
|
+
epic: "🔮 史诗",
|
|
1340
|
+
legendary: "👑 传说",
|
|
1341
|
+
shiny: "✨ 闪光"
|
|
1342
|
+
};
|
|
1343
|
+
const QUALITY_ORDER = [
|
|
1344
|
+
"normal",
|
|
1345
|
+
"fine",
|
|
1346
|
+
"rare",
|
|
1347
|
+
"epic",
|
|
1348
|
+
"legendary",
|
|
1349
|
+
"shiny"
|
|
1350
|
+
];
|
|
1351
|
+
const LEVEL_DEFINITIONS = [
|
|
1352
|
+
{
|
|
1353
|
+
level: 1,
|
|
1354
|
+
title: "凡人",
|
|
1355
|
+
requiredExp: 0,
|
|
1356
|
+
unlockDescription: "基础掉落池"
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
level: 2,
|
|
1360
|
+
title: "樵夫",
|
|
1361
|
+
requiredExp: 120
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
level: 3,
|
|
1365
|
+
title: "修行者",
|
|
1366
|
+
requiredExp: 320,
|
|
1367
|
+
unlockDescription: "解锁\"机缘\"系统(神仙随机现身)"
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
level: 4,
|
|
1371
|
+
title: "散仙",
|
|
1372
|
+
requiredExp: 800,
|
|
1373
|
+
unlockDescription: "掉落池扩展:加入稀有妖怪"
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
level: 5,
|
|
1377
|
+
title: "天兵",
|
|
1378
|
+
requiredExp: 2e3,
|
|
1379
|
+
unlockDescription: "解锁\"法宝\"系统"
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
level: 6,
|
|
1383
|
+
title: "天将",
|
|
1384
|
+
requiredExp: 4e3,
|
|
1385
|
+
unlockDescription: "掉落池扩展:加入史诗妖怪"
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
level: 7,
|
|
1389
|
+
title: "哪吒",
|
|
1390
|
+
requiredExp: 8e3,
|
|
1391
|
+
unlockDescription: "连击加成上限提升至 ×4.0"
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
level: 8,
|
|
1395
|
+
title: "二郎神",
|
|
1396
|
+
requiredExp: 16e3,
|
|
1397
|
+
unlockDescription: "掉落池扩展:加入传说妖怪"
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
level: 9,
|
|
1401
|
+
title: "齐天大圣",
|
|
1402
|
+
requiredExp: 32e3,
|
|
1403
|
+
unlockDescription: "解锁\"闪光\"掉落"
|
|
1404
|
+
},
|
|
1405
|
+
{
|
|
1406
|
+
level: 10,
|
|
1407
|
+
title: "斗战胜佛",
|
|
1408
|
+
requiredExp: 6e4,
|
|
1409
|
+
unlockDescription: "全图鉴解锁提示、专属称号色"
|
|
1410
|
+
}
|
|
1411
|
+
];
|
|
1412
|
+
const PRODUCT_BASE_EXP = {
|
|
1413
|
+
aitable: 3,
|
|
1414
|
+
calendar: 2,
|
|
1415
|
+
chat: 2,
|
|
1416
|
+
contact: 1,
|
|
1417
|
+
todo: 2,
|
|
1418
|
+
approval: 4,
|
|
1419
|
+
attendance: 2,
|
|
1420
|
+
report: 3,
|
|
1421
|
+
ding: 1,
|
|
1422
|
+
workbench: 3,
|
|
1423
|
+
devdoc: 1
|
|
1424
|
+
};
|
|
1425
|
+
/** v2: 保底阈值上调 */
|
|
1426
|
+
const PITY_THRESHOLDS = {
|
|
1427
|
+
rare: 30,
|
|
1428
|
+
epic: 80,
|
|
1429
|
+
legendary: 150,
|
|
1430
|
+
shiny: 800
|
|
1431
|
+
};
|
|
1432
|
+
/** v2: 软保底起始点 — 接近硬保底时概率逐步提升 */
|
|
1433
|
+
const SOFT_PITY_START = {
|
|
1434
|
+
rare: 20,
|
|
1435
|
+
epic: 60,
|
|
1436
|
+
legendary: 120,
|
|
1437
|
+
shiny: 600
|
|
1438
|
+
};
|
|
1439
|
+
/** v2: 软保底每次额外增加的概率 */
|
|
1440
|
+
const SOFT_PITY_RATE_PER_STEP = {
|
|
1441
|
+
rare: .03,
|
|
1442
|
+
epic: .02,
|
|
1443
|
+
legendary: .01,
|
|
1444
|
+
shiny: 5e-4
|
|
1445
|
+
};
|
|
1446
|
+
/** 各品质的基础逃跑率 */
|
|
1447
|
+
const BASE_ESCAPE_RATES = {
|
|
1448
|
+
normal: 0,
|
|
1449
|
+
fine: .1,
|
|
1450
|
+
rare: .25,
|
|
1451
|
+
epic: .4,
|
|
1452
|
+
legendary: .6,
|
|
1453
|
+
shiny: .75
|
|
1454
|
+
};
|
|
1455
|
+
/** 逃跑率的最低下限 */
|
|
1456
|
+
const MIN_ESCAPE_RATE = .05;
|
|
1457
|
+
const DROP_RATES = {
|
|
1458
|
+
shiny: .001,
|
|
1459
|
+
legendary: .009,
|
|
1460
|
+
epic: .04,
|
|
1461
|
+
rare: .1,
|
|
1462
|
+
fine: .25,
|
|
1463
|
+
normal: .6
|
|
1464
|
+
};
|
|
1465
|
+
/** 等级门槛:低于此等级的品质会降级 */
|
|
1466
|
+
const QUALITY_LEVEL_GATES = {
|
|
1467
|
+
rare: 4,
|
|
1468
|
+
epic: 6,
|
|
1469
|
+
legendary: 8,
|
|
1470
|
+
shiny: 9
|
|
1471
|
+
};
|
|
1472
|
+
/** 连击加成倍率 */
|
|
1473
|
+
const COMBO_MULTIPLIERS = [
|
|
1474
|
+
{
|
|
1475
|
+
threshold: 10,
|
|
1476
|
+
multiplier: 3
|
|
1477
|
+
},
|
|
1478
|
+
{
|
|
1479
|
+
threshold: 5,
|
|
1480
|
+
multiplier: 2
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
threshold: 3,
|
|
1484
|
+
multiplier: 1.5
|
|
1485
|
+
}
|
|
1486
|
+
];
|
|
1487
|
+
/** 机缘触发概率 */
|
|
1488
|
+
const ENCOUNTER_RATES = {
|
|
1489
|
+
guidance: .08,
|
|
1490
|
+
treasure: .03,
|
|
1491
|
+
apprentice: .005
|
|
1492
|
+
};
|
|
1493
|
+
//#endregion
|
|
1494
|
+
//#region src/game-xiyou/exp-calculator.ts
|
|
1495
|
+
/**
|
|
1496
|
+
* 获取产品的基础修行值
|
|
1497
|
+
*/
|
|
1498
|
+
function getBaseExp(product) {
|
|
1499
|
+
return PRODUCT_BASE_EXP[product] ?? 2;
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* 计算连击加成倍率
|
|
1503
|
+
*/
|
|
1504
|
+
function getComboMultiplier(comboCount, buffs) {
|
|
1505
|
+
const comboLimitBonus = buffs.filter((b) => b.effect === "comboLimitBonus").reduce((sum, b) => sum + b.value, 0);
|
|
1506
|
+
const comboBonusFromBuffs = buffs.filter((b) => b.effect === "comboBonus").reduce((sum, b) => sum + b.value, 0);
|
|
1507
|
+
let baseMultiplier = 1;
|
|
1508
|
+
for (const { threshold, multiplier } of COMBO_MULTIPLIERS) if (comboCount >= threshold) {
|
|
1509
|
+
baseMultiplier = multiplier;
|
|
1510
|
+
break;
|
|
1511
|
+
}
|
|
1512
|
+
if (comboLimitBonus > 0 && comboCount >= 10) baseMultiplier = Math.min(baseMultiplier + comboLimitBonus, 5);
|
|
1513
|
+
return baseMultiplier + comboBonusFromBuffs;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* 计算首次使用加成
|
|
1517
|
+
*/
|
|
1518
|
+
function getFirstUseMultiplier(product, productUsage) {
|
|
1519
|
+
return (productUsage[product] ?? 0) === 0 ? 5 : 1;
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* 计算签到奖励
|
|
1523
|
+
*/
|
|
1524
|
+
function getSignInBonus(profile) {
|
|
1525
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1526
|
+
if (profile.lastSignInDate === today) return 0;
|
|
1527
|
+
return 10;
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* 计算连续签到奖励
|
|
1531
|
+
*/
|
|
1532
|
+
function getConsecutiveSignInBonus(profile, buffs) {
|
|
1533
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1534
|
+
if (profile.lastSignInDate === today) return 0;
|
|
1535
|
+
const signInMultiplier = buffs.filter((b) => b.effect === "signInMultiplier").reduce((max, b) => Math.max(max, b.value), 1);
|
|
1536
|
+
const consecutiveDays = profile.consecutiveSignInDays + 1;
|
|
1537
|
+
const bonus = Math.min(consecutiveDays * 2, 30);
|
|
1538
|
+
return Math.floor(bonus * signInMultiplier);
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* 计算 buff 总倍率
|
|
1542
|
+
*/
|
|
1543
|
+
function getBuffMultiplier(buffs) {
|
|
1544
|
+
return buffs.filter((b) => b.effect === "expMultiplier").reduce((multiplier, b) => multiplier * b.value, 1);
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* 计算一次操作获得的总修行值
|
|
1548
|
+
*/
|
|
1549
|
+
function calculateExp(product, profile) {
|
|
1550
|
+
const baseExp = getBaseExp(product);
|
|
1551
|
+
const comboMultiplier = getComboMultiplier(profile.currentCombo + 1, profile.buffs);
|
|
1552
|
+
const firstUseMultiplier = getFirstUseMultiplier(product, profile.productUsage);
|
|
1553
|
+
const signInBonus = getSignInBonus(profile);
|
|
1554
|
+
const consecutiveSignInBonus = getConsecutiveSignInBonus(profile, profile.buffs);
|
|
1555
|
+
const buffMultiplier = getBuffMultiplier(profile.buffs);
|
|
1556
|
+
return {
|
|
1557
|
+
baseExp,
|
|
1558
|
+
comboMultiplier,
|
|
1559
|
+
firstUseMultiplier,
|
|
1560
|
+
signInBonus,
|
|
1561
|
+
consecutiveSignInBonus,
|
|
1562
|
+
buffMultiplier,
|
|
1563
|
+
totalExp: Math.floor((baseExp * comboMultiplier * firstUseMultiplier + signInBonus + consecutiveSignInBonus) * buffMultiplier)
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* 更新签到状态
|
|
1568
|
+
*/
|
|
1569
|
+
function updateSignInStatus(profile) {
|
|
1570
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1571
|
+
if (profile.lastSignInDate === today) return;
|
|
1572
|
+
const yesterday = (/* @__PURE__ */ new Date(Date.now() - 864e5)).toISOString().slice(0, 10);
|
|
1573
|
+
if (profile.lastSignInDate === yesterday) profile.consecutiveSignInDays += 1;
|
|
1574
|
+
else profile.consecutiveSignInDays = 1;
|
|
1575
|
+
profile.lastSignInDate = today;
|
|
1576
|
+
}
|
|
1577
|
+
//#endregion
|
|
1578
|
+
//#region src/game-xiyou/level-system.ts
|
|
1579
|
+
/**
|
|
1580
|
+
* 根据累计修行值计算当前等级
|
|
1581
|
+
*/
|
|
1582
|
+
function calculateLevel(totalExp) {
|
|
1583
|
+
let currentLevel = LEVEL_DEFINITIONS[0];
|
|
1584
|
+
for (const levelDef of LEVEL_DEFINITIONS) if (totalExp >= levelDef.requiredExp) currentLevel = levelDef;
|
|
1585
|
+
else break;
|
|
1586
|
+
return currentLevel;
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* 获取下一级的定义(如果已满级则返回 null)
|
|
1590
|
+
*/
|
|
1591
|
+
function getNextLevel(currentLevel) {
|
|
1592
|
+
const nextIndex = LEVEL_DEFINITIONS.findIndex((l) => l.level === currentLevel) + 1;
|
|
1593
|
+
if (nextIndex >= LEVEL_DEFINITIONS.length) return null;
|
|
1594
|
+
return LEVEL_DEFINITIONS[nextIndex];
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* 计算距离下一级还需要多少修行值
|
|
1598
|
+
*/
|
|
1599
|
+
function getExpToNextLevel(totalExp) {
|
|
1600
|
+
const nextLevel = getNextLevel(calculateLevel(totalExp).level);
|
|
1601
|
+
if (!nextLevel) return null;
|
|
1602
|
+
return nextLevel.requiredExp - totalExp;
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* 获取当前等级的进度百分比
|
|
1606
|
+
*/
|
|
1607
|
+
function getLevelProgress(totalExp) {
|
|
1608
|
+
const currentLevel = calculateLevel(totalExp);
|
|
1609
|
+
const nextLevel = getNextLevel(currentLevel.level);
|
|
1610
|
+
if (!nextLevel) return 100;
|
|
1611
|
+
const levelRange = nextLevel.requiredExp - currentLevel.requiredExp;
|
|
1612
|
+
const currentProgress = totalExp - currentLevel.requiredExp;
|
|
1613
|
+
return Math.floor(currentProgress / levelRange * 100);
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* 检查是否发生升级,返回升级结果
|
|
1617
|
+
*/
|
|
1618
|
+
function checkLevelUp(profile, expGained) {
|
|
1619
|
+
const previousLevel = calculateLevel(profile.totalExp);
|
|
1620
|
+
const newLevel = calculateLevel(profile.totalExp + expGained);
|
|
1621
|
+
if (newLevel.level <= previousLevel.level) return null;
|
|
1622
|
+
return {
|
|
1623
|
+
previousLevel: previousLevel.level,
|
|
1624
|
+
previousTitle: previousLevel.title,
|
|
1625
|
+
newLevel: newLevel.level,
|
|
1626
|
+
newTitle: newLevel.title,
|
|
1627
|
+
unlockDescription: newLevel.unlockDescription
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* 应用升级到 profile
|
|
1632
|
+
*/
|
|
1633
|
+
function applyLevelUp(profile) {
|
|
1634
|
+
const levelDef = calculateLevel(profile.totalExp);
|
|
1635
|
+
profile.level = levelDef.level;
|
|
1636
|
+
profile.title = levelDef.title;
|
|
1637
|
+
}
|
|
1638
|
+
//#endregion
|
|
1639
|
+
//#region src/game-xiyou/pity-counter.ts
|
|
1640
|
+
/**
|
|
1641
|
+
* 检查是否触发硬保底,返回应强制掉落的品质(如果有)
|
|
1642
|
+
*/
|
|
1643
|
+
function checkPityTrigger(counters) {
|
|
1644
|
+
if (counters.totalDropsWithoutShiny >= PITY_THRESHOLDS.shiny) return "shiny";
|
|
1645
|
+
if (counters.sinceLastLegendary >= PITY_THRESHOLDS.legendary) return "legendary";
|
|
1646
|
+
if (counters.sinceLastEpic >= PITY_THRESHOLDS.epic) return "epic";
|
|
1647
|
+
if (counters.sinceLastRare >= PITY_THRESHOLDS.rare) return "rare";
|
|
1648
|
+
return null;
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* v2: 计算软保底额外概率加成
|
|
1652
|
+
*
|
|
1653
|
+
* 在接近硬保底阈值时,对应品质的掉落概率逐步提升,
|
|
1654
|
+
* 避免"临门一脚"的漫长等待。
|
|
1655
|
+
*
|
|
1656
|
+
* @returns 各品质的额外概率加成 { rare: 0.06, epic: 0, ... }
|
|
1657
|
+
*/
|
|
1658
|
+
function getSoftPityBonuses(counters) {
|
|
1659
|
+
const bonuses = {};
|
|
1660
|
+
if (counters.sinceLastRare >= SOFT_PITY_START.rare) bonuses.rare = (counters.sinceLastRare - SOFT_PITY_START.rare) * SOFT_PITY_RATE_PER_STEP.rare;
|
|
1661
|
+
if (counters.sinceLastEpic >= SOFT_PITY_START.epic) bonuses.epic = (counters.sinceLastEpic - SOFT_PITY_START.epic) * SOFT_PITY_RATE_PER_STEP.epic;
|
|
1662
|
+
if (counters.sinceLastLegendary >= SOFT_PITY_START.legendary) bonuses.legendary = (counters.sinceLastLegendary - SOFT_PITY_START.legendary) * SOFT_PITY_RATE_PER_STEP.legendary;
|
|
1663
|
+
if (counters.totalDropsWithoutShiny >= SOFT_PITY_START.shiny) bonuses.shiny = (counters.totalDropsWithoutShiny - SOFT_PITY_START.shiny) * SOFT_PITY_RATE_PER_STEP.shiny;
|
|
1664
|
+
return bonuses;
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* 更新保底计数器
|
|
1668
|
+
*
|
|
1669
|
+
* 掉落后递增所有计数器,然后重置对应品质及以下的计数器
|
|
1670
|
+
*/
|
|
1671
|
+
function updatePityCounters(counters, droppedQuality, isShiny) {
|
|
1672
|
+
counters.sinceLastRare += 1;
|
|
1673
|
+
counters.sinceLastEpic += 1;
|
|
1674
|
+
counters.sinceLastLegendary += 1;
|
|
1675
|
+
counters.totalDropsWithoutShiny += 1;
|
|
1676
|
+
const qualityIndex = QUALITY_ORDER.indexOf(droppedQuality);
|
|
1677
|
+
if (isShiny || droppedQuality === "shiny") counters.totalDropsWithoutShiny = 0;
|
|
1678
|
+
if (qualityIndex >= QUALITY_ORDER.indexOf("legendary")) counters.sinceLastLegendary = 0;
|
|
1679
|
+
if (qualityIndex >= QUALITY_ORDER.indexOf("epic")) counters.sinceLastEpic = 0;
|
|
1680
|
+
if (qualityIndex >= QUALITY_ORDER.indexOf("rare")) counters.sinceLastRare = 0;
|
|
1681
|
+
}
|
|
1682
|
+
//#endregion
|
|
1683
|
+
//#region src/game-xiyou/monster-pool.ts
|
|
1684
|
+
/**
|
|
1685
|
+
* 妖怪数据(内联,避免运行时文件系统依赖)
|
|
1686
|
+
*/
|
|
1687
|
+
const allMonsters = [
|
|
1688
|
+
{
|
|
1689
|
+
id: "N001",
|
|
1690
|
+
name: "巡山小妖",
|
|
1691
|
+
emoji: "👹",
|
|
1692
|
+
quality: "normal",
|
|
1693
|
+
origin: "各山洞",
|
|
1694
|
+
relatedProduct: null,
|
|
1695
|
+
captureQuote: "大王叫我来巡山~"
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
id: "N002",
|
|
1699
|
+
name: "树精",
|
|
1700
|
+
emoji: "🌳",
|
|
1701
|
+
quality: "normal",
|
|
1702
|
+
origin: "荆棘岭",
|
|
1703
|
+
relatedProduct: "contact",
|
|
1704
|
+
captureQuote: "落叶归根,不过如此。"
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
id: "N003",
|
|
1708
|
+
name: "草头神",
|
|
1709
|
+
emoji: "🌿",
|
|
1710
|
+
quality: "normal",
|
|
1711
|
+
origin: "通天河畔",
|
|
1712
|
+
relatedProduct: "calendar",
|
|
1713
|
+
captureQuote: "时辰到了,该走了。"
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
id: "N004",
|
|
1717
|
+
name: "虾兵",
|
|
1718
|
+
emoji: "🦐",
|
|
1719
|
+
quality: "normal",
|
|
1720
|
+
origin: "东海",
|
|
1721
|
+
relatedProduct: "chat",
|
|
1722
|
+
captureQuote: "龙宫不是你想来就能来的!"
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
id: "N005",
|
|
1726
|
+
name: "蟹将",
|
|
1727
|
+
emoji: "🦀",
|
|
1728
|
+
quality: "normal",
|
|
1729
|
+
origin: "东海",
|
|
1730
|
+
relatedProduct: "chat",
|
|
1731
|
+
captureQuote: "横行霸道?那是我的专利。"
|
|
1732
|
+
},
|
|
1733
|
+
{
|
|
1734
|
+
id: "N006",
|
|
1735
|
+
name: "山神",
|
|
1736
|
+
emoji: "⛰️",
|
|
1737
|
+
quality: "normal",
|
|
1738
|
+
origin: "各处",
|
|
1739
|
+
relatedProduct: "attendance",
|
|
1740
|
+
captureQuote: "此山是我开,此树是我栽。"
|
|
1741
|
+
},
|
|
1742
|
+
{
|
|
1743
|
+
id: "N007",
|
|
1744
|
+
name: "土地公",
|
|
1745
|
+
emoji: "👴",
|
|
1746
|
+
quality: "normal",
|
|
1747
|
+
origin: "各处",
|
|
1748
|
+
relatedProduct: "contact",
|
|
1749
|
+
captureQuote: "大圣饶命,小神知无不言。"
|
|
1750
|
+
},
|
|
1751
|
+
{
|
|
1752
|
+
id: "N008",
|
|
1753
|
+
name: "夜叉",
|
|
1754
|
+
emoji: "👿",
|
|
1755
|
+
quality: "normal",
|
|
1756
|
+
origin: "水府",
|
|
1757
|
+
relatedProduct: "ding",
|
|
1758
|
+
captureQuote: "水底的消息,最快。"
|
|
1759
|
+
},
|
|
1760
|
+
{
|
|
1761
|
+
id: "N009",
|
|
1762
|
+
name: "狼妖",
|
|
1763
|
+
emoji: "🐺",
|
|
1764
|
+
quality: "normal",
|
|
1765
|
+
origin: "黄风岭",
|
|
1766
|
+
relatedProduct: "todo",
|
|
1767
|
+
captureQuote: "待办事项?我只待吃人。"
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
id: "N010",
|
|
1771
|
+
name: "蛇妖",
|
|
1772
|
+
emoji: "🐍",
|
|
1773
|
+
quality: "normal",
|
|
1774
|
+
origin: "蛇盘山",
|
|
1775
|
+
relatedProduct: "devdoc",
|
|
1776
|
+
captureQuote: "嘶——文档里藏着秘密。"
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
id: "N011",
|
|
1780
|
+
name: "鹿精",
|
|
1781
|
+
emoji: "🦌",
|
|
1782
|
+
quality: "normal",
|
|
1783
|
+
origin: "比丘国",
|
|
1784
|
+
relatedProduct: "report",
|
|
1785
|
+
captureQuote: "今日份的鹿茸报告。"
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
id: "N012",
|
|
1789
|
+
name: "兔精",
|
|
1790
|
+
emoji: "🐇",
|
|
1791
|
+
quality: "normal",
|
|
1792
|
+
origin: "天竺国",
|
|
1793
|
+
relatedProduct: "calendar",
|
|
1794
|
+
captureQuote: "月宫的日程,排得满满的。"
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
id: "N013",
|
|
1798
|
+
name: "鱼精",
|
|
1799
|
+
emoji: "🐟",
|
|
1800
|
+
quality: "normal",
|
|
1801
|
+
origin: "通天河",
|
|
1802
|
+
relatedProduct: "aitable",
|
|
1803
|
+
captureQuote: "河底的账本,一条不差。"
|
|
1804
|
+
},
|
|
1805
|
+
{
|
|
1806
|
+
id: "N014",
|
|
1807
|
+
name: "龟精",
|
|
1808
|
+
emoji: "🐢",
|
|
1809
|
+
quality: "normal",
|
|
1810
|
+
origin: "通天河",
|
|
1811
|
+
relatedProduct: "todo",
|
|
1812
|
+
captureQuote: "慢是慢了点,但待办一定完成。"
|
|
1813
|
+
},
|
|
1814
|
+
{
|
|
1815
|
+
id: "N015",
|
|
1816
|
+
name: "猪妖",
|
|
1817
|
+
emoji: "🐷",
|
|
1818
|
+
quality: "normal",
|
|
1819
|
+
origin: "福陵山",
|
|
1820
|
+
relatedProduct: "report",
|
|
1821
|
+
captureQuote: "日报?让我先睡一觉再说。"
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
id: "N016",
|
|
1825
|
+
name: "鸡精",
|
|
1826
|
+
emoji: "🐔",
|
|
1827
|
+
quality: "normal",
|
|
1828
|
+
origin: "毒敌山",
|
|
1829
|
+
relatedProduct: "attendance",
|
|
1830
|
+
captureQuote: "打鸣就是打卡,准时得很。"
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
id: "N017",
|
|
1834
|
+
name: "鼠精",
|
|
1835
|
+
emoji: "🐭",
|
|
1836
|
+
quality: "normal",
|
|
1837
|
+
origin: "无底洞",
|
|
1838
|
+
relatedProduct: "aitable",
|
|
1839
|
+
captureQuote: "数据?我最擅长搬运了。"
|
|
1840
|
+
},
|
|
1841
|
+
{
|
|
1842
|
+
id: "N018",
|
|
1843
|
+
name: "蝙蝠精",
|
|
1844
|
+
emoji: "🦇",
|
|
1845
|
+
quality: "normal",
|
|
1846
|
+
origin: "黄花观",
|
|
1847
|
+
relatedProduct: "ding",
|
|
1848
|
+
captureQuote: "暗夜传信,无声无息。"
|
|
1849
|
+
},
|
|
1850
|
+
{
|
|
1851
|
+
id: "N019",
|
|
1852
|
+
name: "石妖",
|
|
1853
|
+
emoji: "🪨",
|
|
1854
|
+
quality: "normal",
|
|
1855
|
+
origin: "花果山",
|
|
1856
|
+
relatedProduct: "workbench",
|
|
1857
|
+
captureQuote: "石头里蹦出来的,不止猴子。"
|
|
1858
|
+
},
|
|
1859
|
+
{
|
|
1860
|
+
id: "N020",
|
|
1861
|
+
name: "柳树精",
|
|
1862
|
+
emoji: "🌾",
|
|
1863
|
+
quality: "normal",
|
|
1864
|
+
origin: "荆棘岭",
|
|
1865
|
+
relatedProduct: "approval",
|
|
1866
|
+
captureQuote: "柳条一挥,审批盖章。"
|
|
1867
|
+
},
|
|
1868
|
+
{
|
|
1869
|
+
id: "R001",
|
|
1870
|
+
name: "黑风怪",
|
|
1871
|
+
emoji: "🐻",
|
|
1872
|
+
quality: "fine",
|
|
1873
|
+
origin: "黑风山",
|
|
1874
|
+
relatedProduct: "workbench",
|
|
1875
|
+
captureQuote: "这袈裟,归我了!"
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
id: "R002",
|
|
1879
|
+
name: "黄风怪",
|
|
1880
|
+
emoji: "🐀",
|
|
1881
|
+
quality: "fine",
|
|
1882
|
+
origin: "黄风岭",
|
|
1883
|
+
relatedProduct: "chat",
|
|
1884
|
+
captureQuote: "三昧神风,吹!"
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
id: "R003",
|
|
1888
|
+
name: "白骨精",
|
|
1889
|
+
emoji: "💀",
|
|
1890
|
+
quality: "fine",
|
|
1891
|
+
origin: "白虎岭",
|
|
1892
|
+
relatedProduct: "contact",
|
|
1893
|
+
captureQuote: "变化之术,通讯录里谁是谁?"
|
|
1894
|
+
},
|
|
1895
|
+
{
|
|
1896
|
+
id: "R004",
|
|
1897
|
+
name: "银角大王",
|
|
1898
|
+
emoji: "🦊",
|
|
1899
|
+
quality: "fine",
|
|
1900
|
+
origin: "平顶山",
|
|
1901
|
+
relatedProduct: "aitable",
|
|
1902
|
+
captureQuote: "紫金红葫芦,装!"
|
|
1903
|
+
},
|
|
1904
|
+
{
|
|
1905
|
+
id: "R005",
|
|
1906
|
+
name: "金角大王",
|
|
1907
|
+
emoji: "🦁",
|
|
1908
|
+
quality: "fine",
|
|
1909
|
+
origin: "平顶山",
|
|
1910
|
+
relatedProduct: "aitable",
|
|
1911
|
+
captureQuote: "幌金绳,捆!"
|
|
1912
|
+
},
|
|
1913
|
+
{
|
|
1914
|
+
id: "R006",
|
|
1915
|
+
name: "红孩儿",
|
|
1916
|
+
emoji: "🔥",
|
|
1917
|
+
quality: "fine",
|
|
1918
|
+
origin: "火云洞",
|
|
1919
|
+
relatedProduct: "ding",
|
|
1920
|
+
captureQuote: "三昧真火,DING!"
|
|
1921
|
+
},
|
|
1922
|
+
{
|
|
1923
|
+
id: "R007",
|
|
1924
|
+
name: "鼍龙",
|
|
1925
|
+
emoji: "🐊",
|
|
1926
|
+
quality: "fine",
|
|
1927
|
+
origin: "黑水河",
|
|
1928
|
+
relatedProduct: "approval",
|
|
1929
|
+
captureQuote: "我舅舅是西海龙王,审批通过!"
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
id: "R008",
|
|
1933
|
+
name: "蜘蛛精",
|
|
1934
|
+
emoji: "🕷️",
|
|
1935
|
+
quality: "fine",
|
|
1936
|
+
origin: "盘丝洞",
|
|
1937
|
+
relatedProduct: "todo",
|
|
1938
|
+
captureQuote: "七姐妹的待办,丝丝入扣。"
|
|
1939
|
+
},
|
|
1940
|
+
{
|
|
1941
|
+
id: "R009",
|
|
1942
|
+
name: "蝎子精",
|
|
1943
|
+
emoji: "🦂",
|
|
1944
|
+
quality: "fine",
|
|
1945
|
+
origin: "琵琶洞",
|
|
1946
|
+
relatedProduct: "attendance",
|
|
1947
|
+
captureQuote: "倒马毒桩,准时打卡。"
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
id: "R010",
|
|
1951
|
+
name: "六耳猕猴",
|
|
1952
|
+
emoji: "🐵",
|
|
1953
|
+
quality: "fine",
|
|
1954
|
+
origin: "—",
|
|
1955
|
+
relatedProduct: null,
|
|
1956
|
+
captureQuote: "真假难辨,你猜我是谁?"
|
|
1957
|
+
},
|
|
1958
|
+
{
|
|
1959
|
+
id: "R011",
|
|
1960
|
+
name: "奔波儿灞",
|
|
1961
|
+
emoji: "🐡",
|
|
1962
|
+
quality: "fine",
|
|
1963
|
+
origin: "乱石山碧波潭",
|
|
1964
|
+
relatedProduct: "chat",
|
|
1965
|
+
captureQuote: "跑腿送信,我最在行。"
|
|
1966
|
+
},
|
|
1967
|
+
{
|
|
1968
|
+
id: "R012",
|
|
1969
|
+
name: "灞波儿奔",
|
|
1970
|
+
emoji: "🐠",
|
|
1971
|
+
quality: "fine",
|
|
1972
|
+
origin: "乱石山碧波潭",
|
|
1973
|
+
relatedProduct: "chat",
|
|
1974
|
+
captureQuote: "消息必达,使命必成。"
|
|
1975
|
+
},
|
|
1976
|
+
{
|
|
1977
|
+
id: "R013",
|
|
1978
|
+
name: "独角兕大王",
|
|
1979
|
+
emoji: "🦏",
|
|
1980
|
+
quality: "fine",
|
|
1981
|
+
origin: "金兜山",
|
|
1982
|
+
relatedProduct: "calendar",
|
|
1983
|
+
captureQuote: "金刚琢套住你的日程。"
|
|
1984
|
+
},
|
|
1985
|
+
{
|
|
1986
|
+
id: "R014",
|
|
1987
|
+
name: "如意真仙",
|
|
1988
|
+
emoji: "🧙",
|
|
1989
|
+
quality: "fine",
|
|
1990
|
+
origin: "解阳山",
|
|
1991
|
+
relatedProduct: "report",
|
|
1992
|
+
captureQuote: "落胎泉的日报,概不外传。"
|
|
1993
|
+
},
|
|
1994
|
+
{
|
|
1995
|
+
id: "R015",
|
|
1996
|
+
name: "虎力大仙",
|
|
1997
|
+
emoji: "🐯",
|
|
1998
|
+
quality: "fine",
|
|
1999
|
+
origin: "车迟国",
|
|
2000
|
+
relatedProduct: "approval",
|
|
2001
|
+
captureQuote: "国师审批,一言九鼎。"
|
|
2002
|
+
},
|
|
2003
|
+
{
|
|
2004
|
+
id: "S001",
|
|
2005
|
+
name: "黄袍怪",
|
|
2006
|
+
emoji: "🐆",
|
|
2007
|
+
quality: "rare",
|
|
2008
|
+
origin: "碗子山",
|
|
2009
|
+
relatedProduct: "report",
|
|
2010
|
+
captureQuote: "百花羞的日报,我替她写了。"
|
|
2011
|
+
},
|
|
2012
|
+
{
|
|
2013
|
+
id: "S002",
|
|
2014
|
+
name: "金翅大鹏",
|
|
2015
|
+
emoji: "🦅",
|
|
2016
|
+
quality: "rare",
|
|
2017
|
+
origin: "狮驼岭",
|
|
2018
|
+
relatedProduct: "calendar",
|
|
2019
|
+
captureQuote: "翅膀一扇,九万里。日程?不存在的。"
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
id: "S003",
|
|
2023
|
+
name: "青牛精",
|
|
2024
|
+
emoji: "🐂",
|
|
2025
|
+
quality: "rare",
|
|
2026
|
+
origin: "金兜山",
|
|
2027
|
+
relatedProduct: "aitable",
|
|
2028
|
+
captureQuote: "金刚琢,套住你的表格。"
|
|
2029
|
+
},
|
|
2030
|
+
{
|
|
2031
|
+
id: "S004",
|
|
2032
|
+
name: "铁扇公主",
|
|
2033
|
+
emoji: "🪭",
|
|
2034
|
+
quality: "rare",
|
|
2035
|
+
origin: "翠云山",
|
|
2036
|
+
relatedProduct: "approval",
|
|
2037
|
+
captureQuote: "芭蕉扇一扇,审批全灭。"
|
|
2038
|
+
},
|
|
2039
|
+
{
|
|
2040
|
+
id: "S005",
|
|
2041
|
+
name: "牛魔王",
|
|
2042
|
+
emoji: "🐃",
|
|
2043
|
+
quality: "rare",
|
|
2044
|
+
origin: "积雷山",
|
|
2045
|
+
relatedProduct: "workbench",
|
|
2046
|
+
captureQuote: "我乃平天大圣,工作台归我管。"
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
id: "S006",
|
|
2050
|
+
name: "白鹿精",
|
|
2051
|
+
emoji: "🫎",
|
|
2052
|
+
quality: "rare",
|
|
2053
|
+
origin: "比丘国",
|
|
2054
|
+
relatedProduct: "attendance",
|
|
2055
|
+
captureQuote: "一千一百一十一个小儿的考勤。"
|
|
2056
|
+
},
|
|
2057
|
+
{
|
|
2058
|
+
id: "S007",
|
|
2059
|
+
name: "蜈蚣精",
|
|
2060
|
+
emoji: "🐛",
|
|
2061
|
+
quality: "rare",
|
|
2062
|
+
origin: "黄花观",
|
|
2063
|
+
relatedProduct: "todo",
|
|
2064
|
+
captureQuote: "千目待办,一个不漏。"
|
|
2065
|
+
},
|
|
2066
|
+
{
|
|
2067
|
+
id: "S008",
|
|
2068
|
+
name: "玉兔精",
|
|
2069
|
+
emoji: "🐰",
|
|
2070
|
+
quality: "rare",
|
|
2071
|
+
origin: "天竺国",
|
|
2072
|
+
relatedProduct: "calendar",
|
|
2073
|
+
captureQuote: "广寒宫的排班表,我说了算。"
|
|
2074
|
+
},
|
|
2075
|
+
{
|
|
2076
|
+
id: "S009",
|
|
2077
|
+
name: "金鼻白毛老鼠精",
|
|
2078
|
+
emoji: "🐁",
|
|
2079
|
+
quality: "rare",
|
|
2080
|
+
origin: "无底洞",
|
|
2081
|
+
relatedProduct: "contact",
|
|
2082
|
+
captureQuote: "无底洞的通讯录,深不见底。"
|
|
2083
|
+
},
|
|
2084
|
+
{
|
|
2085
|
+
id: "S010",
|
|
2086
|
+
name: "鹿力大仙",
|
|
2087
|
+
emoji: "🦬",
|
|
2088
|
+
quality: "rare",
|
|
2089
|
+
origin: "车迟国",
|
|
2090
|
+
relatedProduct: "ding",
|
|
2091
|
+
captureQuote: "呼风唤雨,DING 声如雷。"
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
id: "S011",
|
|
2095
|
+
name: "羊力大仙",
|
|
2096
|
+
emoji: "🐏",
|
|
2097
|
+
quality: "rare",
|
|
2098
|
+
origin: "车迟国",
|
|
2099
|
+
relatedProduct: "devdoc",
|
|
2100
|
+
captureQuote: "油锅里的文档,捞出来就是。"
|
|
2101
|
+
},
|
|
2102
|
+
{
|
|
2103
|
+
id: "S012",
|
|
2104
|
+
name: "荆棘岭十八公",
|
|
2105
|
+
emoji: "🌲",
|
|
2106
|
+
quality: "rare",
|
|
2107
|
+
origin: "荆棘岭",
|
|
2108
|
+
relatedProduct: "chat",
|
|
2109
|
+
captureQuote: "松竹梅桂,群聊四友。"
|
|
2110
|
+
},
|
|
2111
|
+
{
|
|
2112
|
+
id: "E001",
|
|
2113
|
+
name: "黄眉大王",
|
|
2114
|
+
emoji: "👺",
|
|
2115
|
+
quality: "epic",
|
|
2116
|
+
origin: "弥勒佛的童子",
|
|
2117
|
+
relatedProduct: "approval",
|
|
2118
|
+
captureQuote: "人种袋,把你的审批全装进来。"
|
|
2119
|
+
},
|
|
2120
|
+
{
|
|
2121
|
+
id: "E002",
|
|
2122
|
+
name: "大鹏金翅明王",
|
|
2123
|
+
emoji: "🦤",
|
|
2124
|
+
quality: "epic",
|
|
2125
|
+
origin: "如来舅舅",
|
|
2126
|
+
relatedProduct: "workbench",
|
|
2127
|
+
captureQuote: "狮驼国的工作台,三界最大。"
|
|
2128
|
+
},
|
|
2129
|
+
{
|
|
2130
|
+
id: "E003",
|
|
2131
|
+
name: "九灵元圣",
|
|
2132
|
+
emoji: "🦁",
|
|
2133
|
+
quality: "epic",
|
|
2134
|
+
origin: "太乙天尊坐骑",
|
|
2135
|
+
relatedProduct: "aitable",
|
|
2136
|
+
captureQuote: "九个头,九张表,哪个都不能少。"
|
|
2137
|
+
},
|
|
2138
|
+
{
|
|
2139
|
+
id: "E004",
|
|
2140
|
+
name: "赛太岁",
|
|
2141
|
+
emoji: "🐕",
|
|
2142
|
+
quality: "epic",
|
|
2143
|
+
origin: "观音坐骑金毛犼",
|
|
2144
|
+
relatedProduct: "attendance",
|
|
2145
|
+
captureQuote: "紫金铃一摇,全员到齐。"
|
|
2146
|
+
},
|
|
2147
|
+
{
|
|
2148
|
+
id: "E005",
|
|
2149
|
+
name: "青狮精",
|
|
2150
|
+
emoji: "🦁",
|
|
2151
|
+
quality: "epic",
|
|
2152
|
+
origin: "文殊菩萨坐骑",
|
|
2153
|
+
relatedProduct: "chat",
|
|
2154
|
+
captureQuote: "狮子吼,群消息已送达。"
|
|
2155
|
+
},
|
|
2156
|
+
{
|
|
2157
|
+
id: "E006",
|
|
2158
|
+
name: "白象精",
|
|
2159
|
+
emoji: "🐘",
|
|
2160
|
+
quality: "epic",
|
|
2161
|
+
origin: "普贤菩萨坐骑",
|
|
2162
|
+
relatedProduct: "todo",
|
|
2163
|
+
captureQuote: "长鼻一卷,待办清空。"
|
|
2164
|
+
},
|
|
2165
|
+
{
|
|
2166
|
+
id: "E007",
|
|
2167
|
+
name: "蠹虫精",
|
|
2168
|
+
emoji: "🪲",
|
|
2169
|
+
quality: "epic",
|
|
2170
|
+
origin: "比丘国国丈",
|
|
2171
|
+
relatedProduct: "report",
|
|
2172
|
+
captureQuote: "一千一百一十一份日报,全在这了。"
|
|
2173
|
+
},
|
|
2174
|
+
{
|
|
2175
|
+
id: "E008",
|
|
2176
|
+
name: "金鱼精",
|
|
2177
|
+
emoji: "🐠",
|
|
2178
|
+
quality: "epic",
|
|
2179
|
+
origin: "观音莲花池",
|
|
2180
|
+
relatedProduct: "calendar",
|
|
2181
|
+
captureQuote: "通天河的日程,年年祭祀。"
|
|
2182
|
+
},
|
|
2183
|
+
{
|
|
2184
|
+
id: "E009",
|
|
2185
|
+
name: "蟒蛇精",
|
|
2186
|
+
emoji: "🐉",
|
|
2187
|
+
quality: "epic",
|
|
2188
|
+
origin: "七绝山",
|
|
2189
|
+
relatedProduct: "devdoc",
|
|
2190
|
+
captureQuote: "七绝山的文档,毒气弥漫。"
|
|
2191
|
+
},
|
|
2192
|
+
{
|
|
2193
|
+
id: "E010",
|
|
2194
|
+
name: "灵感大王",
|
|
2195
|
+
emoji: "🐋",
|
|
2196
|
+
quality: "epic",
|
|
2197
|
+
origin: "通天河",
|
|
2198
|
+
relatedProduct: "ding",
|
|
2199
|
+
captureQuote: "金鱼一跃,DING 达四海。"
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
id: "L001",
|
|
2203
|
+
name: "混世魔王",
|
|
2204
|
+
emoji: "👑",
|
|
2205
|
+
quality: "legendary",
|
|
2206
|
+
origin: "花果山第一战",
|
|
2207
|
+
relatedProduct: null,
|
|
2208
|
+
captureQuote: "水帘洞,从此姓孙。"
|
|
2209
|
+
},
|
|
2210
|
+
{
|
|
2211
|
+
id: "L002",
|
|
2212
|
+
name: "牛魔王(魔化)",
|
|
2213
|
+
emoji: "🔱",
|
|
2214
|
+
quality: "legendary",
|
|
2215
|
+
origin: "大力牛魔王本相",
|
|
2216
|
+
relatedProduct: null,
|
|
2217
|
+
captureQuote: "平天大圣,不服来战!"
|
|
2218
|
+
},
|
|
2219
|
+
{
|
|
2220
|
+
id: "L003",
|
|
2221
|
+
name: "九头虫",
|
|
2222
|
+
emoji: "🐲",
|
|
2223
|
+
quality: "legendary",
|
|
2224
|
+
origin: "碧波潭万圣龙王驸马",
|
|
2225
|
+
relatedProduct: null,
|
|
2226
|
+
captureQuote: "九颗头颅,九种权限。"
|
|
2227
|
+
},
|
|
2228
|
+
{
|
|
2229
|
+
id: "L004",
|
|
2230
|
+
name: "百眼魔君",
|
|
2231
|
+
emoji: "👁️",
|
|
2232
|
+
quality: "legendary",
|
|
2233
|
+
origin: "蜈蚣精本相",
|
|
2234
|
+
relatedProduct: null,
|
|
2235
|
+
captureQuote: "千目金光,洞察一切数据。"
|
|
2236
|
+
},
|
|
2237
|
+
{
|
|
2238
|
+
id: "L005",
|
|
2239
|
+
name: "大鹏金翅(本相)",
|
|
2240
|
+
emoji: "🦅",
|
|
2241
|
+
quality: "legendary",
|
|
2242
|
+
origin: "遮天蔽日",
|
|
2243
|
+
relatedProduct: null,
|
|
2244
|
+
captureQuote: "三界之大,不过我翅膀之下。"
|
|
2245
|
+
},
|
|
2246
|
+
{
|
|
2247
|
+
id: "L006",
|
|
2248
|
+
name: "孙悟空(石猴)",
|
|
2249
|
+
emoji: "🐒",
|
|
2250
|
+
quality: "legendary",
|
|
2251
|
+
origin: "花果山水帘洞",
|
|
2252
|
+
relatedProduct: null,
|
|
2253
|
+
captureQuote: "俺老孙来也!"
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
id: "L007",
|
|
2257
|
+
name: "六耳猕猴(真身)",
|
|
2258
|
+
emoji: "🙈",
|
|
2259
|
+
quality: "legendary",
|
|
2260
|
+
origin: "混沌之中",
|
|
2261
|
+
relatedProduct: null,
|
|
2262
|
+
captureQuote: "天地间第二个齐天大圣。"
|
|
2263
|
+
},
|
|
2264
|
+
{
|
|
2265
|
+
id: "L008",
|
|
2266
|
+
name: "白骨夫人(真身)",
|
|
2267
|
+
emoji: "☠️",
|
|
2268
|
+
quality: "legendary",
|
|
2269
|
+
origin: "白虎岭深处",
|
|
2270
|
+
relatedProduct: null,
|
|
2271
|
+
captureQuote: "三打不死,方显真身。"
|
|
2272
|
+
}
|
|
2273
|
+
];
|
|
2274
|
+
/**
|
|
2275
|
+
* 获取所有妖怪
|
|
2276
|
+
*/
|
|
2277
|
+
function getAllMonsters() {
|
|
2278
|
+
return allMonsters;
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* 获取指定品质的妖怪列表
|
|
2282
|
+
*/
|
|
2283
|
+
function getMonstersByQuality(quality) {
|
|
2284
|
+
return allMonsters.filter((m) => m.quality === quality);
|
|
2285
|
+
}
|
|
2286
|
+
/**
|
|
2287
|
+
* 根据 ID 查找妖怪
|
|
2288
|
+
*/
|
|
2289
|
+
function getMonsterById(monsterId) {
|
|
2290
|
+
return allMonsters.find((m) => m.id === monsterId);
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* 获取所有妖怪的总数(不含闪光变体)
|
|
2294
|
+
*/
|
|
2295
|
+
function getTotalMonsterCount() {
|
|
2296
|
+
return allMonsters.length;
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* 获取本周 UP 妖怪
|
|
2300
|
+
*
|
|
2301
|
+
* 按周数轮换,从史诗和传说池中选择
|
|
2302
|
+
*/
|
|
2303
|
+
function getWeeklyUpMonster() {
|
|
2304
|
+
const upPool = allMonsters.filter((m) => m.quality === "epic" || m.quality === "legendary");
|
|
2305
|
+
if (upPool.length === 0) return null;
|
|
2306
|
+
return upPool[Math.floor(Date.now() / (10080 * 60 * 1e3)) % upPool.length];
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* 带权重的随机选择妖怪
|
|
2310
|
+
*
|
|
2311
|
+
* @param pool - 候选妖怪池
|
|
2312
|
+
* @param product - 当前 dws 产品(关联产品权重 ×3)
|
|
2313
|
+
* @param upMonster - 本周 UP 妖怪(权重 ×5)
|
|
2314
|
+
* @param randomValue - 随机数 [0, 1)
|
|
2315
|
+
*/
|
|
2316
|
+
function weightedRandomSelect(pool, product, upMonster, randomValue) {
|
|
2317
|
+
if (pool.length === 0) throw new Error("Monster pool is empty");
|
|
2318
|
+
if (pool.length === 1) return pool[0];
|
|
2319
|
+
const weights = pool.map((monster) => {
|
|
2320
|
+
let weight = 1;
|
|
2321
|
+
if (product && monster.relatedProduct === product) weight *= 3;
|
|
2322
|
+
if (upMonster && monster.id === upMonster.id) weight *= 5;
|
|
2323
|
+
return weight;
|
|
2324
|
+
});
|
|
2325
|
+
let target = randomValue * weights.reduce((sum, w) => sum + w, 0);
|
|
2326
|
+
for (let i = 0; i < pool.length; i++) {
|
|
2327
|
+
target -= weights[i];
|
|
2328
|
+
if (target <= 0) return pool[i];
|
|
2329
|
+
}
|
|
2330
|
+
return pool[pool.length - 1];
|
|
2331
|
+
}
|
|
2332
|
+
//#endregion
|
|
2333
|
+
//#region src/game-xiyou/drop-engine.ts
|
|
2334
|
+
/**
|
|
2335
|
+
* 概率掉落引擎(核心)
|
|
2336
|
+
*
|
|
2337
|
+
* 每次 dws CLI 成功执行后触发一次"降妖"事件。
|
|
2338
|
+
* 流程:保底判定 → 随机品质 → 等级门槛降级 → 加权选妖 → 闪光判定 → 更新计数器
|
|
2339
|
+
*/
|
|
2340
|
+
/**
|
|
2341
|
+
* 使用 crypto.randomBytes 生成安全随机数
|
|
2342
|
+
*/
|
|
2343
|
+
function cryptoRandom$2() {
|
|
2344
|
+
return randomBytes(4).readUInt32BE(0) / 4294967295;
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* 根据随机数和用户等级判定掉落品质
|
|
2348
|
+
*
|
|
2349
|
+
* v2: 集成软保底概率加成
|
|
2350
|
+
*/
|
|
2351
|
+
function resolveQuality(roll, level, buffs, softPityBonuses) {
|
|
2352
|
+
const rateBonus = {};
|
|
2353
|
+
for (const buff of buffs) switch (buff.effect) {
|
|
2354
|
+
case "epicRateBonus":
|
|
2355
|
+
rateBonus.epic = (rateBonus.epic ?? 0) + buff.value;
|
|
2356
|
+
break;
|
|
2357
|
+
case "rareRateBonus":
|
|
2358
|
+
rateBonus.rare = (rateBonus.rare ?? 0) + buff.value;
|
|
2359
|
+
break;
|
|
2360
|
+
case "legendaryRateBonus":
|
|
2361
|
+
rateBonus.legendary = (rateBonus.legendary ?? 0) + buff.value;
|
|
2362
|
+
break;
|
|
2363
|
+
case "shinyRateBonus":
|
|
2364
|
+
rateBonus.shiny = (rateBonus.shiny ?? 0) + buff.value;
|
|
2365
|
+
break;
|
|
2366
|
+
case "allRateBonus":
|
|
2367
|
+
for (const quality of QUALITY_ORDER) if (quality !== "normal" && quality !== "fine") rateBonus[quality] = (rateBonus[quality] ?? 0) + buff.value;
|
|
2368
|
+
break;
|
|
2369
|
+
}
|
|
2370
|
+
for (const [quality, bonus] of Object.entries(softPityBonuses)) {
|
|
2371
|
+
const qualityKey = quality;
|
|
2372
|
+
rateBonus[qualityKey] = (rateBonus[qualityKey] ?? 0) + (bonus ?? 0);
|
|
2373
|
+
}
|
|
2374
|
+
let cumulative = 0;
|
|
2375
|
+
for (const quality of [
|
|
2376
|
+
"shiny",
|
|
2377
|
+
"legendary",
|
|
2378
|
+
"epic",
|
|
2379
|
+
"rare",
|
|
2380
|
+
"fine",
|
|
2381
|
+
"normal"
|
|
2382
|
+
]) {
|
|
2383
|
+
const baseRate = DROP_RATES[quality];
|
|
2384
|
+
const bonus = rateBonus[quality] ?? 0;
|
|
2385
|
+
cumulative += baseRate + bonus;
|
|
2386
|
+
if (roll < cumulative) return quality;
|
|
2387
|
+
}
|
|
2388
|
+
return "normal";
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* 应用等级门槛降级
|
|
2392
|
+
*/
|
|
2393
|
+
function applyLevelGate(quality, level) {
|
|
2394
|
+
const gate = QUALITY_LEVEL_GATES[quality];
|
|
2395
|
+
if (gate !== void 0 && level < gate) {
|
|
2396
|
+
const qualityIndex = QUALITY_ORDER.indexOf(quality);
|
|
2397
|
+
for (let i = qualityIndex - 1; i >= 0; i--) {
|
|
2398
|
+
const lowerQuality = QUALITY_ORDER[i];
|
|
2399
|
+
const lowerGate = QUALITY_LEVEL_GATES[lowerQuality];
|
|
2400
|
+
if (lowerGate === void 0 || level >= lowerGate) return lowerQuality;
|
|
2401
|
+
}
|
|
2402
|
+
return "normal";
|
|
2403
|
+
}
|
|
2404
|
+
return quality;
|
|
2405
|
+
}
|
|
2406
|
+
/**
|
|
2407
|
+
* 检查玲珑宝塔 buff:普通掉落有概率升级为精良
|
|
2408
|
+
*/
|
|
2409
|
+
function checkNormalUpgrade(quality, buffs) {
|
|
2410
|
+
if (quality !== "normal") return quality;
|
|
2411
|
+
const upgradeChance = buffs.filter((b) => b.effect === "normalUpgrade").reduce((sum, b) => sum + b.value, 0);
|
|
2412
|
+
if (upgradeChance > 0 && cryptoRandom$2() < upgradeChance) return "fine";
|
|
2413
|
+
return quality;
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
2416
|
+
* 将品质提升一级(用于月光宝盒事件)
|
|
2417
|
+
*/
|
|
2418
|
+
function boostQuality(quality) {
|
|
2419
|
+
const index = QUALITY_ORDER.indexOf(quality);
|
|
2420
|
+
if (index < 0 || index >= QUALITY_ORDER.length - 1) return quality;
|
|
2421
|
+
return QUALITY_ORDER[index + 1];
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* 执行一次掉落
|
|
2425
|
+
*
|
|
2426
|
+
* v2: 集成软保底概率加成、事件品质提升/上限、禁止掉落检查
|
|
2427
|
+
*/
|
|
2428
|
+
function executeDrop(product, profile, collection) {
|
|
2429
|
+
const emptyResult = {
|
|
2430
|
+
monster: {
|
|
2431
|
+
id: "",
|
|
2432
|
+
name: "",
|
|
2433
|
+
emoji: "",
|
|
2434
|
+
quality: "normal",
|
|
2435
|
+
origin: "",
|
|
2436
|
+
relatedProduct: null,
|
|
2437
|
+
captureQuote: ""
|
|
2438
|
+
},
|
|
2439
|
+
isShiny: false,
|
|
2440
|
+
isNew: false,
|
|
2441
|
+
expGained: 0,
|
|
2442
|
+
isPityTriggered: false,
|
|
2443
|
+
isUpMonster: false,
|
|
2444
|
+
escaped: false,
|
|
2445
|
+
escapeRate: 0,
|
|
2446
|
+
escapeModifiers: []
|
|
2447
|
+
};
|
|
2448
|
+
if (isDropSuppressed(profile)) return emptyResult;
|
|
2449
|
+
const pity = profile.pityCounters;
|
|
2450
|
+
let isPityTriggered = false;
|
|
2451
|
+
let quality;
|
|
2452
|
+
const pityQuality = checkPityTrigger(pity);
|
|
2453
|
+
if (pityQuality) {
|
|
2454
|
+
quality = pityQuality;
|
|
2455
|
+
isPityTriggered = true;
|
|
2456
|
+
} else {
|
|
2457
|
+
const roll = cryptoRandom$2();
|
|
2458
|
+
const softPityBonuses = getSoftPityBonuses(pity);
|
|
2459
|
+
quality = resolveQuality(roll, profile.level, profile.buffs, softPityBonuses);
|
|
2460
|
+
}
|
|
2461
|
+
quality = applyLevelGate(quality, profile.level);
|
|
2462
|
+
quality = checkNormalUpgrade(quality, profile.buffs);
|
|
2463
|
+
const qualityCap = getQualityCap(profile);
|
|
2464
|
+
if (qualityCap) {
|
|
2465
|
+
const capIndex = QUALITY_ORDER.indexOf(qualityCap);
|
|
2466
|
+
if (QUALITY_ORDER.indexOf(quality) > capIndex) quality = qualityCap;
|
|
2467
|
+
}
|
|
2468
|
+
const qualityBoostLevel = getQualityBoost(profile);
|
|
2469
|
+
if (qualityBoostLevel > 0) {
|
|
2470
|
+
for (let i = 0; i < qualityBoostLevel; i++) quality = boostQuality(quality);
|
|
2471
|
+
quality = applyLevelGate(quality, profile.level);
|
|
2472
|
+
}
|
|
2473
|
+
let isShiny = false;
|
|
2474
|
+
if (quality === "shiny") {
|
|
2475
|
+
isShiny = true;
|
|
2476
|
+
const availableQualities = QUALITY_ORDER.filter((q) => {
|
|
2477
|
+
if (q === "shiny") return false;
|
|
2478
|
+
const gate = QUALITY_LEVEL_GATES[q];
|
|
2479
|
+
return gate === void 0 || profile.level >= gate;
|
|
2480
|
+
});
|
|
2481
|
+
quality = availableQualities[Math.floor(cryptoRandom$2() * availableQualities.length)] || "normal";
|
|
2482
|
+
} else if (profile.level >= 9) {
|
|
2483
|
+
const shinyBonus = profile.buffs.filter((b) => b.effect === "shinyRateBonus").reduce((sum, b) => sum + b.value, 0);
|
|
2484
|
+
if (cryptoRandom$2() < .001 + shinyBonus) isShiny = true;
|
|
2485
|
+
}
|
|
2486
|
+
const pool = getMonstersByQuality(quality);
|
|
2487
|
+
const upMonster = getWeeklyUpMonster();
|
|
2488
|
+
let monster;
|
|
2489
|
+
if (pool.length === 0) monster = weightedRandomSelect(getMonstersByQuality("normal"), product, null, cryptoRandom$2());
|
|
2490
|
+
else monster = weightedRandomSelect(pool, product, upMonster, cryptoRandom$2());
|
|
2491
|
+
const isNew = !collection.entries.some((e) => e.monsterId === monster.id && (isShiny ? e.isShiny : !e.isShiny));
|
|
2492
|
+
updatePityCounters(pity, quality, isShiny);
|
|
2493
|
+
return {
|
|
2494
|
+
monster,
|
|
2495
|
+
isShiny,
|
|
2496
|
+
isNew,
|
|
2497
|
+
expGained: 0,
|
|
2498
|
+
isPityTriggered,
|
|
2499
|
+
isUpMonster: upMonster?.id === monster.id,
|
|
2500
|
+
escaped: false,
|
|
2501
|
+
escapeRate: 0,
|
|
2502
|
+
escapeModifiers: []
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
//#endregion
|
|
2506
|
+
//#region src/game-xiyou/treasure-system.ts
|
|
2507
|
+
/**
|
|
2508
|
+
* 法宝数据(内联,导出供 encounter-system 引用)
|
|
2509
|
+
*/
|
|
2510
|
+
const TREASURES_DATA = [
|
|
2511
|
+
{
|
|
2512
|
+
id: "jintouyun",
|
|
2513
|
+
name: "筋斗云",
|
|
2514
|
+
source: "菩提祖师赐宝",
|
|
2515
|
+
description: "连击加成额外 +0.5",
|
|
2516
|
+
effect: "comboBonus",
|
|
2517
|
+
value: .5,
|
|
2518
|
+
consumable: false
|
|
2519
|
+
},
|
|
2520
|
+
{
|
|
2521
|
+
id: "jingping",
|
|
2522
|
+
name: "净瓶",
|
|
2523
|
+
source: "观音菩萨赐宝",
|
|
2524
|
+
description: "recovery 成功时额外 +5 修行值",
|
|
2525
|
+
effect: "expMultiplier",
|
|
2526
|
+
value: 1.1,
|
|
2527
|
+
consumable: false
|
|
2528
|
+
},
|
|
2529
|
+
{
|
|
2530
|
+
id: "zijinhulu",
|
|
2531
|
+
name: "紫金葫芦",
|
|
2532
|
+
source: "太上老君赐宝",
|
|
2533
|
+
description: "每日额外 1 次掉落机会",
|
|
2534
|
+
effect: "extraDrop",
|
|
2535
|
+
value: 1,
|
|
2536
|
+
consumable: false
|
|
2537
|
+
},
|
|
2538
|
+
{
|
|
2539
|
+
id: "pantao",
|
|
2540
|
+
name: "蟠桃",
|
|
2541
|
+
source: "太白金星赐宝",
|
|
2542
|
+
description: "使用后立即 +50 修行值",
|
|
2543
|
+
effect: "instantExp",
|
|
2544
|
+
value: 50,
|
|
2545
|
+
consumable: true
|
|
2546
|
+
},
|
|
2547
|
+
{
|
|
2548
|
+
id: "qiankunquan",
|
|
2549
|
+
name: "乾坤圈",
|
|
2550
|
+
source: "哪吒赐宝",
|
|
2551
|
+
description: "闪光概率 +0.05%",
|
|
2552
|
+
effect: "shinyRateBonus",
|
|
2553
|
+
value: 5e-4,
|
|
2554
|
+
consumable: false
|
|
2555
|
+
},
|
|
2556
|
+
{
|
|
2557
|
+
id: "sanjiandao",
|
|
2558
|
+
name: "三尖两刃刀",
|
|
2559
|
+
source: "二郎真君赐宝",
|
|
2560
|
+
description: "CLI 错误自动重试 +1 次",
|
|
2561
|
+
effect: "cliRetry",
|
|
2562
|
+
value: 1,
|
|
2563
|
+
consumable: false
|
|
2564
|
+
},
|
|
2565
|
+
{
|
|
2566
|
+
id: "linglongta",
|
|
2567
|
+
name: "玲珑宝塔",
|
|
2568
|
+
source: "托塔天王赐宝",
|
|
2569
|
+
description: "普通掉落 20% 概率升级为精良",
|
|
2570
|
+
effect: "normalUpgrade",
|
|
2571
|
+
value: .2,
|
|
2572
|
+
consumable: false
|
|
2573
|
+
},
|
|
2574
|
+
{
|
|
2575
|
+
id: "renshenguo",
|
|
2576
|
+
name: "人参果",
|
|
2577
|
+
source: "镇元大仙赐宝",
|
|
2578
|
+
description: "使用后立即 +200 修行值",
|
|
2579
|
+
effect: "instantExp",
|
|
2580
|
+
value: 200,
|
|
2581
|
+
consumable: true
|
|
2582
|
+
},
|
|
2583
|
+
{
|
|
2584
|
+
id: "dinghaishenzhen",
|
|
2585
|
+
name: "定海神针",
|
|
2586
|
+
source: "成就「齐天大圣」解锁",
|
|
2587
|
+
description: "所有掉落率 +1%",
|
|
2588
|
+
effect: "allRateBonus",
|
|
2589
|
+
value: .01,
|
|
2590
|
+
consumable: false
|
|
2591
|
+
},
|
|
2592
|
+
{
|
|
2593
|
+
id: "jingguzhou",
|
|
2594
|
+
name: "紧箍咒",
|
|
2595
|
+
source: "成就「西天取经」解锁",
|
|
2596
|
+
description: "保底计数器速度 ×1.5",
|
|
2597
|
+
effect: "pitySpeed",
|
|
2598
|
+
value: 1.5,
|
|
2599
|
+
consumable: false
|
|
2600
|
+
},
|
|
2601
|
+
{
|
|
2602
|
+
id: "bashanshan",
|
|
2603
|
+
name: "芭蕉扇",
|
|
2604
|
+
source: "收服铁扇公主后概率获得",
|
|
2605
|
+
description: "连续签到奖励 ×2",
|
|
2606
|
+
effect: "signInMultiplier",
|
|
2607
|
+
value: 2,
|
|
2608
|
+
consumable: false
|
|
2609
|
+
},
|
|
2610
|
+
{
|
|
2611
|
+
id: "zhaoyaojing",
|
|
2612
|
+
name: "照妖镜",
|
|
2613
|
+
source: "收服全部精良妖怪后解锁",
|
|
2614
|
+
description: "掉落时预览下一次的品质",
|
|
2615
|
+
effect: "previewNextQuality",
|
|
2616
|
+
value: 1,
|
|
2617
|
+
consumable: false
|
|
2618
|
+
}
|
|
2619
|
+
];
|
|
2620
|
+
const allTreasures = TREASURES_DATA;
|
|
2621
|
+
/**
|
|
2622
|
+
* 根据 ID 查找法宝
|
|
2623
|
+
*/
|
|
2624
|
+
function getTreasureById(treasureId) {
|
|
2625
|
+
return allTreasures.find((t) => t.id === treasureId);
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* 获取用户拥有的法宝列表(含详情)
|
|
2629
|
+
*/
|
|
2630
|
+
function getUserTreasures(profile) {
|
|
2631
|
+
return profile.treasures.map((id) => getTreasureById(id)).filter((t) => t !== void 0);
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* 获取用户可使用的一次性法宝
|
|
2635
|
+
*/
|
|
2636
|
+
function getConsumableTreasures(profile) {
|
|
2637
|
+
return getUserTreasures(profile).filter((t) => t.consumable && !profile.consumedTreasures.includes(t.id));
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* 使用一次性法宝
|
|
2641
|
+
*
|
|
2642
|
+
* @returns 使用结果描述,或 null(法宝不存在/已使用/不可消耗)
|
|
2643
|
+
*/
|
|
2644
|
+
function consumeTreasure(profile, treasureName) {
|
|
2645
|
+
const treasure = allTreasures.find((t) => t.name === treasureName);
|
|
2646
|
+
if (!treasure) return null;
|
|
2647
|
+
if (!profile.treasures.includes(treasure.id)) return null;
|
|
2648
|
+
if (!treasure.consumable) return null;
|
|
2649
|
+
if (profile.consumedTreasures.includes(treasure.id)) return null;
|
|
2650
|
+
profile.consumedTreasures.push(treasure.id);
|
|
2651
|
+
let expGained = 0;
|
|
2652
|
+
let message = "";
|
|
2653
|
+
if (treasure.effect === "instantExp") {
|
|
2654
|
+
expGained = treasure.value;
|
|
2655
|
+
profile.totalExp += expGained;
|
|
2656
|
+
message = `修行值 +${expGained}`;
|
|
2657
|
+
}
|
|
2658
|
+
return {
|
|
2659
|
+
treasure,
|
|
2660
|
+
expGained,
|
|
2661
|
+
message
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
//#endregion
|
|
2665
|
+
//#region src/game-xiyou/encounter-system.ts
|
|
2666
|
+
/**
|
|
2667
|
+
* 神仙机缘系统
|
|
2668
|
+
*
|
|
2669
|
+
* 等级 ≥ 3(修行者)后解锁。
|
|
2670
|
+
* 每次 dws CLI 成功执行,除了掉落妖怪外,还有独立概率触发"神仙机缘"事件。
|
|
2671
|
+
*/
|
|
2672
|
+
/**
|
|
2673
|
+
* 神仙数据(内联)
|
|
2674
|
+
*/
|
|
2675
|
+
const allImmortals = [
|
|
2676
|
+
{
|
|
2677
|
+
id: "G001",
|
|
2678
|
+
name: "菩提祖师",
|
|
2679
|
+
guidanceQuote: "悟性不错,但还差一个筋斗云的距离。",
|
|
2680
|
+
treasureId: "jintouyun",
|
|
2681
|
+
apprenticeBuff: {
|
|
2682
|
+
id: "putizu-apprentice",
|
|
2683
|
+
source: "apprentice",
|
|
2684
|
+
effect: "expMultiplier",
|
|
2685
|
+
value: 1.2
|
|
2686
|
+
}
|
|
2687
|
+
},
|
|
2688
|
+
{
|
|
2689
|
+
id: "G002",
|
|
2690
|
+
name: "观音菩萨",
|
|
2691
|
+
guidanceQuote: "救苦救难,先把待办清了。",
|
|
2692
|
+
treasureId: "jingping",
|
|
2693
|
+
apprenticeBuff: {
|
|
2694
|
+
id: "guanyin-apprentice",
|
|
2695
|
+
source: "apprentice",
|
|
2696
|
+
effect: "pityReduction",
|
|
2697
|
+
value: .5
|
|
2698
|
+
}
|
|
2699
|
+
},
|
|
2700
|
+
{
|
|
2701
|
+
id: "G003",
|
|
2702
|
+
name: "太上老君",
|
|
2703
|
+
guidanceQuote: "八卦炉里炼出来的,都是好东西。",
|
|
2704
|
+
treasureId: "zijinhulu",
|
|
2705
|
+
apprenticeBuff: {
|
|
2706
|
+
id: "laojun-apprentice",
|
|
2707
|
+
source: "apprentice",
|
|
2708
|
+
effect: "epicRateBonus",
|
|
2709
|
+
value: .01
|
|
2710
|
+
}
|
|
2711
|
+
},
|
|
2712
|
+
{
|
|
2713
|
+
id: "G004",
|
|
2714
|
+
name: "太白金星",
|
|
2715
|
+
guidanceQuote: "玉帝有旨,你的 KPI 不错。",
|
|
2716
|
+
treasureId: "pantao",
|
|
2717
|
+
apprenticeBuff: {
|
|
2718
|
+
id: "taibai-apprentice",
|
|
2719
|
+
source: "apprentice",
|
|
2720
|
+
effect: "signInMultiplier",
|
|
2721
|
+
value: 1.5
|
|
2722
|
+
}
|
|
2723
|
+
},
|
|
2724
|
+
{
|
|
2725
|
+
id: "G005",
|
|
2726
|
+
name: "哪吒三太子",
|
|
2727
|
+
guidanceQuote: "风火轮转得快,但别忘了刹车。",
|
|
2728
|
+
treasureId: "qiankunquan",
|
|
2729
|
+
apprenticeBuff: {
|
|
2730
|
+
id: "nezha-apprentice",
|
|
2731
|
+
source: "apprentice",
|
|
2732
|
+
effect: "comboLimitBonus",
|
|
2733
|
+
value: 1
|
|
2734
|
+
}
|
|
2735
|
+
},
|
|
2736
|
+
{
|
|
2737
|
+
id: "G006",
|
|
2738
|
+
name: "二郎真君",
|
|
2739
|
+
guidanceQuote: "第三只眼看穿一切 bug。",
|
|
2740
|
+
treasureId: "sanjiandao",
|
|
2741
|
+
apprenticeBuff: {
|
|
2742
|
+
id: "erlang-apprentice",
|
|
2743
|
+
source: "apprentice",
|
|
2744
|
+
effect: "rareRateBonus",
|
|
2745
|
+
value: .02
|
|
2746
|
+
}
|
|
2747
|
+
},
|
|
2748
|
+
{
|
|
2749
|
+
id: "G007",
|
|
2750
|
+
name: "托塔天王",
|
|
2751
|
+
guidanceQuote: "塔在手,妖魔走。",
|
|
2752
|
+
treasureId: "linglongta",
|
|
2753
|
+
apprenticeBuff: {
|
|
2754
|
+
id: "tuota-apprentice",
|
|
2755
|
+
source: "apprentice",
|
|
2756
|
+
effect: "allRateBonus",
|
|
2757
|
+
value: .005
|
|
2758
|
+
}
|
|
2759
|
+
},
|
|
2760
|
+
{
|
|
2761
|
+
id: "G008",
|
|
2762
|
+
name: "镇元大仙",
|
|
2763
|
+
guidanceQuote: "人参果,三千年一开花,三千年一结果。",
|
|
2764
|
+
treasureId: "renshenguo",
|
|
2765
|
+
apprenticeBuff: {
|
|
2766
|
+
id: "zhenyuan-apprentice",
|
|
2767
|
+
source: "apprentice",
|
|
2768
|
+
effect: "legendaryRateBonus",
|
|
2769
|
+
value: .003
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
];
|
|
2773
|
+
function cryptoRandom$1() {
|
|
2774
|
+
return randomBytes(4).readUInt32BE(0) / 4294967295;
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* 根据 ID 查找神仙
|
|
2778
|
+
*/
|
|
2779
|
+
function getImmortalById(immortalId) {
|
|
2780
|
+
return allImmortals.find((i) => i.id === immortalId);
|
|
2781
|
+
}
|
|
2782
|
+
/**
|
|
2783
|
+
* 获取法宝名称
|
|
2784
|
+
*/
|
|
2785
|
+
function getTreasureName(treasureId) {
|
|
2786
|
+
return TREASURES_DATA.find((t) => t.id === treasureId)?.name ?? treasureId;
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* 检查是否触发机缘事件
|
|
2790
|
+
*
|
|
2791
|
+
* @returns 机缘事件,或 null(未触发)
|
|
2792
|
+
*/
|
|
2793
|
+
function checkEncounter(profile) {
|
|
2794
|
+
if (profile.level < 3) return null;
|
|
2795
|
+
for (const encounterType of [
|
|
2796
|
+
"apprentice",
|
|
2797
|
+
"treasure",
|
|
2798
|
+
"guidance"
|
|
2799
|
+
]) {
|
|
2800
|
+
const rate = ENCOUNTER_RATES[encounterType];
|
|
2801
|
+
if (cryptoRandom$1() < rate) {
|
|
2802
|
+
const immortal = allImmortals[Math.floor(cryptoRandom$1() * allImmortals.length)];
|
|
2803
|
+
const encounter = {
|
|
2804
|
+
immortalId: immortal.id,
|
|
2805
|
+
type: encounterType,
|
|
2806
|
+
occurredAt: Date.now()
|
|
2807
|
+
};
|
|
2808
|
+
if (encounterType === "treasure") encounter.treasureId = immortal.treasureId;
|
|
2809
|
+
if (encounterType === "apprentice") encounter.buffId = immortal.apprenticeBuff.id;
|
|
2810
|
+
return encounter;
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
return null;
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* 应用机缘效果到用户档案
|
|
2817
|
+
*/
|
|
2818
|
+
function applyEncounterEffects(profile, encounter) {
|
|
2819
|
+
profile.encounters.push(encounter);
|
|
2820
|
+
const immortal = getImmortalById(encounter.immortalId);
|
|
2821
|
+
if (!immortal) return;
|
|
2822
|
+
if (encounter.type === "treasure" && encounter.treasureId) {
|
|
2823
|
+
if (!profile.treasures.includes(encounter.treasureId)) profile.treasures.push(encounter.treasureId);
|
|
2824
|
+
const treasure = TREASURES_DATA.find((t) => t.id === encounter.treasureId);
|
|
2825
|
+
if (treasure && !treasure.consumable) {
|
|
2826
|
+
if (!profile.buffs.find((b) => b.id === treasure.id)) profile.buffs.push({
|
|
2827
|
+
id: treasure.id,
|
|
2828
|
+
source: "treasure",
|
|
2829
|
+
effect: treasure.effect,
|
|
2830
|
+
value: treasure.value
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
if (encounter.type === "apprentice") {
|
|
2835
|
+
const buff = { ...immortal.apprenticeBuff };
|
|
2836
|
+
if (!profile.buffs.find((b) => b.id === buff.id)) profile.buffs.push(buff);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
//#endregion
|
|
2840
|
+
//#region src/game-xiyou/achievement-engine.ts
|
|
2841
|
+
/**
|
|
2842
|
+
* 成就数据(内联)
|
|
2843
|
+
*/
|
|
2844
|
+
const allAchievements = [
|
|
2845
|
+
{
|
|
2846
|
+
id: "A001",
|
|
2847
|
+
name: "初出茅庐",
|
|
2848
|
+
emoji: "🐒",
|
|
2849
|
+
description: "首次成功调用 dws CLI",
|
|
2850
|
+
category: "cultivation",
|
|
2851
|
+
condition: {
|
|
2852
|
+
type: "totalOperations",
|
|
2853
|
+
count: 1
|
|
2854
|
+
},
|
|
2855
|
+
expReward: 10
|
|
2856
|
+
},
|
|
2857
|
+
{
|
|
2858
|
+
id: "A002",
|
|
2859
|
+
name: "三天打鱼",
|
|
2860
|
+
emoji: "🔥",
|
|
2861
|
+
description: "连续 3 天签到",
|
|
2862
|
+
category: "cultivation",
|
|
2863
|
+
condition: {
|
|
2864
|
+
type: "consecutiveSignIn",
|
|
2865
|
+
days: 3
|
|
2866
|
+
},
|
|
2867
|
+
expReward: 15
|
|
2868
|
+
},
|
|
2869
|
+
{
|
|
2870
|
+
id: "A003",
|
|
2871
|
+
name: "七七四十九",
|
|
2872
|
+
emoji: "📅",
|
|
2873
|
+
description: "连续 49 天签到",
|
|
2874
|
+
category: "cultivation",
|
|
2875
|
+
condition: {
|
|
2876
|
+
type: "consecutiveSignIn",
|
|
2877
|
+
days: 49
|
|
2878
|
+
},
|
|
2879
|
+
expReward: 200
|
|
2880
|
+
},
|
|
2881
|
+
{
|
|
2882
|
+
id: "A004",
|
|
2883
|
+
name: "十连斩",
|
|
2884
|
+
emoji: "⚡",
|
|
2885
|
+
description: "单次连击达到 10 次",
|
|
2886
|
+
category: "cultivation",
|
|
2887
|
+
condition: {
|
|
2888
|
+
type: "maxCombo",
|
|
2889
|
+
count: 10
|
|
2890
|
+
},
|
|
2891
|
+
expReward: 50
|
|
2892
|
+
},
|
|
2893
|
+
{
|
|
2894
|
+
id: "A005",
|
|
2895
|
+
name: "五行山下",
|
|
2896
|
+
emoji: "🏔️",
|
|
2897
|
+
description: "累计 500 次成功调用",
|
|
2898
|
+
category: "cultivation",
|
|
2899
|
+
condition: {
|
|
2900
|
+
type: "totalOperations",
|
|
2901
|
+
count: 500
|
|
2902
|
+
},
|
|
2903
|
+
expReward: 100
|
|
2904
|
+
},
|
|
2905
|
+
{
|
|
2906
|
+
id: "A006",
|
|
2907
|
+
name: "八十一难",
|
|
2908
|
+
emoji: "🌋",
|
|
2909
|
+
description: "累计 81 次 recovery 成功",
|
|
2910
|
+
category: "cultivation",
|
|
2911
|
+
condition: {
|
|
2912
|
+
type: "totalRecoveries",
|
|
2913
|
+
count: 81
|
|
2914
|
+
},
|
|
2915
|
+
expReward: 300
|
|
2916
|
+
},
|
|
2917
|
+
{
|
|
2918
|
+
id: "A101",
|
|
2919
|
+
name: "妖怪猎人",
|
|
2920
|
+
emoji: "📖",
|
|
2921
|
+
description: "收服 10 种不同妖怪",
|
|
2922
|
+
category: "collection",
|
|
2923
|
+
condition: {
|
|
2924
|
+
type: "uniqueMonsters",
|
|
2925
|
+
count: 10
|
|
2926
|
+
},
|
|
2927
|
+
expReward: 30
|
|
2928
|
+
},
|
|
2929
|
+
{
|
|
2930
|
+
id: "A102",
|
|
2931
|
+
name: "半部西游",
|
|
2932
|
+
emoji: "📚",
|
|
2933
|
+
description: "收服 24 种不同妖怪",
|
|
2934
|
+
category: "collection",
|
|
2935
|
+
condition: {
|
|
2936
|
+
type: "uniqueMonsters",
|
|
2937
|
+
count: 24
|
|
2938
|
+
},
|
|
2939
|
+
expReward: 100
|
|
2940
|
+
},
|
|
2941
|
+
{
|
|
2942
|
+
id: "A103",
|
|
2943
|
+
name: "妖魔全书",
|
|
2944
|
+
emoji: "📜",
|
|
2945
|
+
description: "收服全部 48 种妖怪",
|
|
2946
|
+
category: "collection",
|
|
2947
|
+
condition: {
|
|
2948
|
+
type: "uniqueMonsters",
|
|
2949
|
+
count: 48
|
|
2950
|
+
},
|
|
2951
|
+
expReward: 500,
|
|
2952
|
+
titleReward: "妖魔克星"
|
|
2953
|
+
},
|
|
2954
|
+
{
|
|
2955
|
+
id: "A104",
|
|
2956
|
+
name: "闪光猎人",
|
|
2957
|
+
emoji: "✨",
|
|
2958
|
+
description: "收服 1 只闪光妖怪",
|
|
2959
|
+
category: "collection",
|
|
2960
|
+
condition: {
|
|
2961
|
+
type: "shinyMonsters",
|
|
2962
|
+
count: 1
|
|
2963
|
+
},
|
|
2964
|
+
expReward: 200
|
|
2965
|
+
},
|
|
2966
|
+
{
|
|
2967
|
+
id: "A105",
|
|
2968
|
+
name: "闪光大师",
|
|
2969
|
+
emoji: "🌈",
|
|
2970
|
+
description: "收服 5 只闪光妖怪",
|
|
2971
|
+
category: "collection",
|
|
2972
|
+
condition: {
|
|
2973
|
+
type: "shinyMonsters",
|
|
2974
|
+
count: 5
|
|
2975
|
+
},
|
|
2976
|
+
expReward: 500,
|
|
2977
|
+
titleReward: "欧皇"
|
|
2978
|
+
},
|
|
2979
|
+
{
|
|
2980
|
+
id: "A106",
|
|
2981
|
+
name: "全闪通关",
|
|
2982
|
+
emoji: "👑",
|
|
2983
|
+
description: "收服 10 只闪光妖怪",
|
|
2984
|
+
category: "collection",
|
|
2985
|
+
condition: {
|
|
2986
|
+
type: "shinyMonsters",
|
|
2987
|
+
count: 10
|
|
2988
|
+
},
|
|
2989
|
+
expReward: 1e3,
|
|
2990
|
+
titleReward: "天选之人"
|
|
2991
|
+
},
|
|
2992
|
+
{
|
|
2993
|
+
id: "A201",
|
|
2994
|
+
name: "表格大师",
|
|
2995
|
+
emoji: "📊",
|
|
2996
|
+
description: "aitable 相关命令成功 50 次",
|
|
2997
|
+
category: "product",
|
|
2998
|
+
condition: {
|
|
2999
|
+
type: "productUsage",
|
|
3000
|
+
product: "aitable",
|
|
3001
|
+
count: 50
|
|
3002
|
+
},
|
|
3003
|
+
expReward: 30
|
|
3004
|
+
},
|
|
3005
|
+
{
|
|
3006
|
+
id: "A202",
|
|
3007
|
+
name: "时间管理者",
|
|
3008
|
+
emoji: "📅",
|
|
3009
|
+
description: "calendar 相关命令成功 50 次",
|
|
3010
|
+
category: "product",
|
|
3011
|
+
condition: {
|
|
3012
|
+
type: "productUsage",
|
|
3013
|
+
product: "calendar",
|
|
3014
|
+
count: 50
|
|
3015
|
+
},
|
|
3016
|
+
expReward: 30
|
|
3017
|
+
},
|
|
3018
|
+
{
|
|
3019
|
+
id: "A203",
|
|
3020
|
+
name: "群聊达人",
|
|
3021
|
+
emoji: "💬",
|
|
3022
|
+
description: "chat 相关命令成功 50 次",
|
|
3023
|
+
category: "product",
|
|
3024
|
+
condition: {
|
|
3025
|
+
type: "productUsage",
|
|
3026
|
+
product: "chat",
|
|
3027
|
+
count: 50
|
|
3028
|
+
},
|
|
3029
|
+
expReward: 30
|
|
3030
|
+
},
|
|
3031
|
+
{
|
|
3032
|
+
id: "A204",
|
|
3033
|
+
name: "待办终结者",
|
|
3034
|
+
emoji: "✅",
|
|
3035
|
+
description: "todo 相关命令成功 50 次",
|
|
3036
|
+
category: "product",
|
|
3037
|
+
condition: {
|
|
3038
|
+
type: "productUsage",
|
|
3039
|
+
product: "todo",
|
|
3040
|
+
count: 50
|
|
3041
|
+
},
|
|
3042
|
+
expReward: 30
|
|
3043
|
+
},
|
|
3044
|
+
{
|
|
3045
|
+
id: "A205",
|
|
3046
|
+
name: "日报之王",
|
|
3047
|
+
emoji: "📝",
|
|
3048
|
+
description: "report 连续 30 天提交",
|
|
3049
|
+
category: "product",
|
|
3050
|
+
condition: {
|
|
3051
|
+
type: "consecutiveReport",
|
|
3052
|
+
days: 30
|
|
3053
|
+
},
|
|
3054
|
+
expReward: 100
|
|
3055
|
+
},
|
|
3056
|
+
{
|
|
3057
|
+
id: "A206",
|
|
3058
|
+
name: "全能战士",
|
|
3059
|
+
emoji: "🎯",
|
|
3060
|
+
description: "使用过所有 11 个产品",
|
|
3061
|
+
category: "product",
|
|
3062
|
+
condition: { type: "allProducts" },
|
|
3063
|
+
expReward: 200
|
|
3064
|
+
},
|
|
3065
|
+
{
|
|
3066
|
+
id: "A301",
|
|
3067
|
+
name: "夜猫子",
|
|
3068
|
+
emoji: "🌙",
|
|
3069
|
+
description: "凌晨 2:00-5:00 成功调用",
|
|
3070
|
+
category: "hidden",
|
|
3071
|
+
condition: { type: "nightOwl" },
|
|
3072
|
+
expReward: 20
|
|
3073
|
+
},
|
|
3074
|
+
{
|
|
3075
|
+
id: "A302",
|
|
3076
|
+
name: "非酋翻身",
|
|
3077
|
+
emoji: "🎰",
|
|
3078
|
+
description: "触发天命保底(150 次未出传说)",
|
|
3079
|
+
category: "hidden",
|
|
3080
|
+
condition: { type: "pityTriggered" },
|
|
3081
|
+
expReward: 100,
|
|
3082
|
+
titleReward: "大器晚成"
|
|
3083
|
+
},
|
|
3084
|
+
{
|
|
3085
|
+
id: "A303",
|
|
3086
|
+
name: "屡败屡战",
|
|
3087
|
+
emoji: "💀",
|
|
3088
|
+
description: "连续 10 次失败后第 11 次成功",
|
|
3089
|
+
category: "hidden",
|
|
3090
|
+
condition: {
|
|
3091
|
+
type: "consecutiveFailThenSuccess",
|
|
3092
|
+
failCount: 10
|
|
3093
|
+
},
|
|
3094
|
+
expReward: 50
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
id: "A304",
|
|
3098
|
+
name: "屠龙勇士",
|
|
3099
|
+
emoji: "🐉",
|
|
3100
|
+
description: "单日收服 3 只稀有及以上妖怪",
|
|
3101
|
+
category: "hidden",
|
|
3102
|
+
condition: {
|
|
3103
|
+
type: "dailyRareOrAbove",
|
|
3104
|
+
count: 3
|
|
3105
|
+
},
|
|
3106
|
+
expReward: 100
|
|
3107
|
+
},
|
|
3108
|
+
{
|
|
3109
|
+
id: "A305",
|
|
3110
|
+
name: "生日快乐",
|
|
3111
|
+
emoji: "🎂",
|
|
3112
|
+
description: "在账号注册日当天使用",
|
|
3113
|
+
category: "hidden",
|
|
3114
|
+
condition: { type: "birthday" },
|
|
3115
|
+
expReward: 50
|
|
3116
|
+
},
|
|
3117
|
+
{
|
|
3118
|
+
id: "A401",
|
|
3119
|
+
name: "逃跑大师",
|
|
3120
|
+
emoji: "💨",
|
|
3121
|
+
description: "累计被妖怪逃跑 50 次",
|
|
3122
|
+
category: "hidden",
|
|
3123
|
+
condition: {
|
|
3124
|
+
type: "totalEscapes",
|
|
3125
|
+
count: 50
|
|
3126
|
+
},
|
|
3127
|
+
expReward: 30
|
|
3128
|
+
},
|
|
3129
|
+
{
|
|
3130
|
+
id: "A402",
|
|
3131
|
+
name: "一网打尽",
|
|
3132
|
+
emoji: "🪤",
|
|
3133
|
+
description: "连续 10 次掉落无妖怪逃跑",
|
|
3134
|
+
category: "hidden",
|
|
3135
|
+
condition: {
|
|
3136
|
+
type: "consecutiveNoEscape",
|
|
3137
|
+
count: 10
|
|
3138
|
+
},
|
|
3139
|
+
expReward: 50
|
|
3140
|
+
},
|
|
3141
|
+
{
|
|
3142
|
+
id: "A403",
|
|
3143
|
+
name: "赏金猎人",
|
|
3144
|
+
emoji: "📜",
|
|
3145
|
+
description: "累计完成 30 张悬赏令",
|
|
3146
|
+
category: "hidden",
|
|
3147
|
+
condition: {
|
|
3148
|
+
type: "totalBountiesCompleted",
|
|
3149
|
+
count: 30
|
|
3150
|
+
},
|
|
3151
|
+
expReward: 100,
|
|
3152
|
+
titleReward: "赏金猎人"
|
|
3153
|
+
},
|
|
3154
|
+
{
|
|
3155
|
+
id: "A404",
|
|
3156
|
+
name: "金牌猎人",
|
|
3157
|
+
emoji: "🥇",
|
|
3158
|
+
description: "累计完成 10 张金令",
|
|
3159
|
+
category: "hidden",
|
|
3160
|
+
condition: {
|
|
3161
|
+
type: "goldBountiesCompleted",
|
|
3162
|
+
count: 10
|
|
3163
|
+
},
|
|
3164
|
+
expReward: 150
|
|
3165
|
+
},
|
|
3166
|
+
{
|
|
3167
|
+
id: "A405",
|
|
3168
|
+
name: "全勤猎人",
|
|
3169
|
+
emoji: "📅",
|
|
3170
|
+
description: "连续 7 天每日完成全部 3 张悬赏令",
|
|
3171
|
+
category: "hidden",
|
|
3172
|
+
condition: {
|
|
3173
|
+
type: "consecutiveFullClear",
|
|
3174
|
+
days: 7
|
|
3175
|
+
},
|
|
3176
|
+
expReward: 200
|
|
3177
|
+
},
|
|
3178
|
+
{
|
|
3179
|
+
id: "A406",
|
|
3180
|
+
name: "见多识广",
|
|
3181
|
+
emoji: "🎪",
|
|
3182
|
+
description: "累计触发 10 次随机事件",
|
|
3183
|
+
category: "hidden",
|
|
3184
|
+
condition: {
|
|
3185
|
+
type: "totalEventsTriggered",
|
|
3186
|
+
count: 10
|
|
3187
|
+
},
|
|
3188
|
+
expReward: 30
|
|
3189
|
+
},
|
|
3190
|
+
{
|
|
3191
|
+
id: "A407",
|
|
3192
|
+
name: "百战百胜",
|
|
3193
|
+
emoji: "⚔️",
|
|
3194
|
+
description: "累计完成 10 次挑战事件",
|
|
3195
|
+
category: "hidden",
|
|
3196
|
+
condition: {
|
|
3197
|
+
type: "challengesCompleted",
|
|
3198
|
+
count: 10
|
|
3199
|
+
},
|
|
3200
|
+
expReward: 100,
|
|
3201
|
+
titleReward: "战神"
|
|
3202
|
+
},
|
|
3203
|
+
{
|
|
3204
|
+
id: "A408",
|
|
3205
|
+
name: "劫后余生",
|
|
3206
|
+
emoji: "😈",
|
|
3207
|
+
description: "触发「走火入魔」后存活(修行值未归零)",
|
|
3208
|
+
category: "hidden",
|
|
3209
|
+
condition: { type: "survivedMadness" },
|
|
3210
|
+
expReward: 50,
|
|
3211
|
+
titleReward: "大难不死"
|
|
3212
|
+
},
|
|
3213
|
+
{
|
|
3214
|
+
id: "A409",
|
|
3215
|
+
name: "蟠桃常客",
|
|
3216
|
+
emoji: "🍑",
|
|
3217
|
+
description: "累计触发 3 次「蟠桃大会」",
|
|
3218
|
+
category: "hidden",
|
|
3219
|
+
condition: {
|
|
3220
|
+
type: "specificEventCount",
|
|
3221
|
+
eventId: "EV001",
|
|
3222
|
+
count: 3
|
|
3223
|
+
},
|
|
3224
|
+
expReward: 80
|
|
3225
|
+
},
|
|
3226
|
+
{
|
|
3227
|
+
id: "A410",
|
|
3228
|
+
name: "火焰山主",
|
|
3229
|
+
emoji: "🔥",
|
|
3230
|
+
description: "累计完成 3 次「火焰山」挑战",
|
|
3231
|
+
category: "hidden",
|
|
3232
|
+
condition: {
|
|
3233
|
+
type: "specificEventCount",
|
|
3234
|
+
eventId: "EV102",
|
|
3235
|
+
count: 3
|
|
3236
|
+
},
|
|
3237
|
+
expReward: 60
|
|
3238
|
+
},
|
|
3239
|
+
{
|
|
3240
|
+
id: "A411",
|
|
3241
|
+
name: "真假悟空",
|
|
3242
|
+
emoji: "🐒",
|
|
3243
|
+
description: "在「真假美猴王」事件中选对",
|
|
3244
|
+
category: "hidden",
|
|
3245
|
+
condition: {
|
|
3246
|
+
type: "specificChallengeSuccess",
|
|
3247
|
+
eventId: "EV104"
|
|
3248
|
+
},
|
|
3249
|
+
expReward: 100,
|
|
3250
|
+
titleReward: "火眼金睛"
|
|
3251
|
+
},
|
|
3252
|
+
{
|
|
3253
|
+
id: "A412",
|
|
3254
|
+
name: "否极泰来",
|
|
3255
|
+
emoji: "🌈",
|
|
3256
|
+
description: "灾厄事件结束后立即触发增益事件",
|
|
3257
|
+
category: "hidden",
|
|
3258
|
+
condition: { type: "disasterThenBlessing" },
|
|
3259
|
+
expReward: 200
|
|
3260
|
+
},
|
|
3261
|
+
{
|
|
3262
|
+
id: "A413",
|
|
3263
|
+
name: "化险为夷",
|
|
3264
|
+
emoji: "🛡️",
|
|
3265
|
+
description: "累计 5 次通过化解方式提前解除灾厄",
|
|
3266
|
+
category: "hidden",
|
|
3267
|
+
condition: {
|
|
3268
|
+
type: "disastersResolved",
|
|
3269
|
+
count: 5
|
|
3270
|
+
},
|
|
3271
|
+
expReward: 80
|
|
3272
|
+
}
|
|
3273
|
+
];
|
|
3274
|
+
/**
|
|
3275
|
+
* 获取所有成就
|
|
3276
|
+
*/
|
|
3277
|
+
function getAllAchievements() {
|
|
3278
|
+
return allAchievements;
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* 根据 ID 查找成就
|
|
3282
|
+
*/
|
|
3283
|
+
function getAchievementById(achievementId) {
|
|
3284
|
+
return allAchievements.find((a) => a.id === achievementId);
|
|
3285
|
+
}
|
|
3286
|
+
/**
|
|
3287
|
+
* 检查单个成就条件是否满足
|
|
3288
|
+
*/
|
|
3289
|
+
function isConditionMet(condition, profile, collection, todayRecords) {
|
|
3290
|
+
switch (condition.type) {
|
|
3291
|
+
case "totalOperations": return profile.totalOperations >= condition.count;
|
|
3292
|
+
case "consecutiveSignIn": return profile.consecutiveSignInDays >= condition.days;
|
|
3293
|
+
case "maxCombo": return profile.maxCombo >= condition.count;
|
|
3294
|
+
case "totalRecoveries": return profile.totalRecoveries >= condition.count;
|
|
3295
|
+
case "uniqueMonsters": return collection.entries.filter((e) => !e.isShiny).length >= condition.count;
|
|
3296
|
+
case "shinyMonsters": return collection.entries.filter((e) => e.isShiny).length >= condition.count;
|
|
3297
|
+
case "productUsage": return (profile.productUsage[condition.product] ?? 0) >= condition.count;
|
|
3298
|
+
case "allProducts": return Object.keys(PRODUCT_BASE_EXP).every((p) => (profile.productUsage[p] ?? 0) > 0);
|
|
3299
|
+
case "dailyOperations": return todayRecords.filter((r) => r.success).length >= condition.count;
|
|
3300
|
+
case "nightOwl": {
|
|
3301
|
+
const hour = (/* @__PURE__ */ new Date()).getHours();
|
|
3302
|
+
return hour >= 2 && hour < 5;
|
|
3303
|
+
}
|
|
3304
|
+
case "pityTriggered": return false;
|
|
3305
|
+
case "consecutiveFailThenSuccess": return profile.consecutiveFailures >= condition.failCount;
|
|
3306
|
+
case "dailyRareOrAbove": return todayRecords.filter((r) => {
|
|
3307
|
+
if (!r.monsterId) return false;
|
|
3308
|
+
return r.monsterId.startsWith("S") || r.monsterId.startsWith("E") || r.monsterId.startsWith("L");
|
|
3309
|
+
}).length >= condition.count;
|
|
3310
|
+
case "birthday": {
|
|
3311
|
+
const createdDate = new Date(profile.createdAt);
|
|
3312
|
+
const now = /* @__PURE__ */ new Date();
|
|
3313
|
+
return createdDate.getMonth() === now.getMonth() && createdDate.getDate() === now.getDate() && now.getFullYear() > createdDate.getFullYear();
|
|
3314
|
+
}
|
|
3315
|
+
case "consecutiveReport": return (profile.productUsage["report"] ?? 0) >= condition.days;
|
|
3316
|
+
case "totalEscapes": return profile.totalEscapes >= condition.count;
|
|
3317
|
+
case "consecutiveNoEscape": return false;
|
|
3318
|
+
case "totalBountiesCompleted": return profile.bountyHistory.totalCompleted >= condition.count;
|
|
3319
|
+
case "goldBountiesCompleted": return profile.bountyHistory.goldCompleted >= condition.count;
|
|
3320
|
+
case "consecutiveFullClear": return profile.bountyHistory.consecutiveFullClear >= condition.days;
|
|
3321
|
+
case "totalEventsTriggered": return profile.eventStats.totalTriggered >= condition.count;
|
|
3322
|
+
case "challengesCompleted": return profile.eventStats.challengesCompleted >= condition.count;
|
|
3323
|
+
case "survivedMadness": return profile.eventHistory.some((e) => e.eventId === "EV206") && profile.totalExp > 0;
|
|
3324
|
+
case "specificEventCount": return profile.eventHistory.filter((e) => e.eventId === condition.eventId).length >= condition.count;
|
|
3325
|
+
case "specificChallengeSuccess": return profile.eventHistory.some((e) => e.eventId === condition.eventId && e.outcome === "success");
|
|
3326
|
+
case "disasterThenBlessing": return false;
|
|
3327
|
+
case "disastersResolved": return profile.eventStats.disastersResolved >= condition.count;
|
|
3328
|
+
default: return false;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
/**
|
|
3332
|
+
* 检查所有未解锁的成就,返回新解锁的成就列表
|
|
3333
|
+
*/
|
|
3334
|
+
function checkAchievements(profile, collection, todayRecords) {
|
|
3335
|
+
const newlyUnlocked = [];
|
|
3336
|
+
for (const achievement of allAchievements) {
|
|
3337
|
+
if (profile.unlockedAchievements.includes(achievement.id)) continue;
|
|
3338
|
+
if (isConditionMet(achievement.condition, profile, collection, todayRecords)) {
|
|
3339
|
+
newlyUnlocked.push(achievement);
|
|
3340
|
+
profile.unlockedAchievements.push(achievement.id);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
return newlyUnlocked;
|
|
3344
|
+
}
|
|
3345
|
+
/**
|
|
3346
|
+
* 手动触发特殊成就(如保底触发)
|
|
3347
|
+
*/
|
|
3348
|
+
function triggerSpecialAchievement(profile, achievementId) {
|
|
3349
|
+
if (profile.unlockedAchievements.includes(achievementId)) return null;
|
|
3350
|
+
const achievement = getAchievementById(achievementId);
|
|
3351
|
+
if (!achievement) return null;
|
|
3352
|
+
profile.unlockedAchievements.push(achievementId);
|
|
3353
|
+
return achievement;
|
|
3354
|
+
}
|
|
3355
|
+
//#endregion
|
|
3356
|
+
//#region src/game-xiyou/escape-engine.ts
|
|
3357
|
+
/**
|
|
3358
|
+
* 妖怪逃跑引擎 (v2)
|
|
3359
|
+
*
|
|
3360
|
+
* 掉落不再等于收服。品质越高的妖怪越难降服。
|
|
3361
|
+
* 逃跑率受连击、法宝、buff、产品关联等因素修正,最低不低于 5%。
|
|
3362
|
+
* 保底触发的妖怪 100% 收服,不会逃跑。
|
|
3363
|
+
*/
|
|
3364
|
+
function cryptoRandom() {
|
|
3365
|
+
return randomBytes(4).readUInt32BE(0) / 4294967295;
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* 计算逃跑率修正因子列表
|
|
3369
|
+
*/
|
|
3370
|
+
function calculateEscapeModifiers(monster, profile, product, isBountyTarget) {
|
|
3371
|
+
const modifiers = [];
|
|
3372
|
+
if (profile.currentCombo >= 10) modifiers.push({
|
|
3373
|
+
source: "combo",
|
|
3374
|
+
value: .2,
|
|
3375
|
+
description: `连击 ×${profile.currentCombo} 加成`
|
|
3376
|
+
});
|
|
3377
|
+
else if (profile.currentCombo >= 5) modifiers.push({
|
|
3378
|
+
source: "combo",
|
|
3379
|
+
value: .1,
|
|
3380
|
+
description: `连击 ×${profile.currentCombo} 加成`
|
|
3381
|
+
});
|
|
3382
|
+
if (profile.buffs.some((b) => b.id === "linglongta")) modifiers.push({
|
|
3383
|
+
source: "treasure",
|
|
3384
|
+
value: .15,
|
|
3385
|
+
description: "玲珑宝塔"
|
|
3386
|
+
});
|
|
3387
|
+
if (profile.buffs.some((b) => b.id === "dinghaishenzhen")) modifiers.push({
|
|
3388
|
+
source: "treasure",
|
|
3389
|
+
value: .1,
|
|
3390
|
+
description: "定海神针"
|
|
3391
|
+
});
|
|
3392
|
+
if (profile.buffs.some((b) => b.id === "erlang-apprentice")) modifiers.push({
|
|
3393
|
+
source: "buff",
|
|
3394
|
+
value: .08,
|
|
3395
|
+
description: "二郎真君师徒"
|
|
3396
|
+
});
|
|
3397
|
+
if (monster.relatedProduct && monster.relatedProduct === product) modifiers.push({
|
|
3398
|
+
source: "product",
|
|
3399
|
+
value: .05,
|
|
3400
|
+
description: "产品关联匹配"
|
|
3401
|
+
});
|
|
3402
|
+
if (isBountyTarget) modifiers.push({
|
|
3403
|
+
source: "bounty",
|
|
3404
|
+
value: .1,
|
|
3405
|
+
description: "悬赏令加成"
|
|
3406
|
+
});
|
|
3407
|
+
for (const event of profile.activeEvents.currentEvents) if (event.effect.type === "escape_rate_mod") {
|
|
3408
|
+
const eventValue = Math.abs(event.effect.value);
|
|
3409
|
+
if (event.effect.value < 0) modifiers.push({
|
|
3410
|
+
source: "event",
|
|
3411
|
+
value: eventValue,
|
|
3412
|
+
description: `${event.name} 减益`
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
return modifiers;
|
|
3416
|
+
}
|
|
3417
|
+
/**
|
|
3418
|
+
* 计算最终逃跑率
|
|
3419
|
+
*/
|
|
3420
|
+
function calculateFinalEscapeRate(quality, isShiny, modifiers, profile, monsterId) {
|
|
3421
|
+
const baseRate = isShiny ? BASE_ESCAPE_RATES.shiny : BASE_ESCAPE_RATES[quality];
|
|
3422
|
+
if (baseRate === 0) return 0;
|
|
3423
|
+
const totalReduction = modifiers.reduce((sum, m) => sum + m.value, 0);
|
|
3424
|
+
let eventIncrease = 0;
|
|
3425
|
+
for (const event of profile.activeEvents.currentEvents) if (event.effect.type === "escape_rate_mod" && event.effect.value > 0) eventIncrease += event.effect.value;
|
|
3426
|
+
const consecutiveEscapes = profile.escapeHistory[monsterId] ?? 0;
|
|
3427
|
+
let consecutiveReduction = 0;
|
|
3428
|
+
if (consecutiveEscapes >= 3) consecutiveReduction = (baseRate + eventIncrease - totalReduction) * .5;
|
|
3429
|
+
const finalRate = baseRate + eventIncrease - totalReduction - consecutiveReduction;
|
|
3430
|
+
return Math.max(MIN_ESCAPE_RATE, Math.min(finalRate, .95));
|
|
3431
|
+
}
|
|
3432
|
+
/**
|
|
3433
|
+
* 判定妖怪是否逃跑,并更新追踪数据
|
|
3434
|
+
*
|
|
3435
|
+
* @returns 更新后的 DropResult(设置 escaped / escapeRate / escapeModifiers)
|
|
3436
|
+
*/
|
|
3437
|
+
function resolveEscape(dropResult, profile, product, isBountyTarget = false) {
|
|
3438
|
+
const { monster, isShiny, isPityTriggered } = dropResult;
|
|
3439
|
+
if (isPityTriggered) return {
|
|
3440
|
+
...dropResult,
|
|
3441
|
+
escaped: false,
|
|
3442
|
+
escapeRate: 0,
|
|
3443
|
+
escapeModifiers: []
|
|
3444
|
+
};
|
|
3445
|
+
if (monster.quality === "normal" && !isShiny) return {
|
|
3446
|
+
...dropResult,
|
|
3447
|
+
escaped: false,
|
|
3448
|
+
escapeRate: 0,
|
|
3449
|
+
escapeModifiers: []
|
|
3450
|
+
};
|
|
3451
|
+
const modifiers = calculateEscapeModifiers(monster, profile, product, isBountyTarget);
|
|
3452
|
+
const escapeRate = calculateFinalEscapeRate(monster.quality, isShiny, modifiers, profile, monster.id);
|
|
3453
|
+
const escaped = cryptoRandom() < escapeRate;
|
|
3454
|
+
if (escaped) {
|
|
3455
|
+
profile.escapeHistory[monster.id] = (profile.escapeHistory[monster.id] ?? 0) + 1;
|
|
3456
|
+
profile.totalEscapes += 1;
|
|
3457
|
+
} else profile.escapeHistory[monster.id] = 0;
|
|
3458
|
+
return {
|
|
3459
|
+
...dropResult,
|
|
3460
|
+
escaped,
|
|
3461
|
+
escapeRate,
|
|
3462
|
+
escapeModifiers: modifiers
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
//#endregion
|
|
3466
|
+
//#region src/game-xiyou/renderer.ts
|
|
3467
|
+
/**
|
|
3468
|
+
* 渲染普通掉落结果(追加到 agent 回复末尾)
|
|
3469
|
+
*
|
|
3470
|
+
* v3: 紧凑化渲染 — 用妖怪 emoji 图标 + 单行/双行展示,大幅减少纵向空间
|
|
3471
|
+
*/
|
|
3472
|
+
function renderDropResult(drop, expResult, collection) {
|
|
3473
|
+
if (!drop.monster.id) return "";
|
|
3474
|
+
const qualityLabel = drop.isShiny ? "✨" : QUALITY_LABELS[drop.monster.quality];
|
|
3475
|
+
const monsterEmoji = drop.monster.emoji ?? "🗡️";
|
|
3476
|
+
const totalMonsters = getTotalMonsterCount();
|
|
3477
|
+
const collectedCount = collection.entries.length;
|
|
3478
|
+
const tags = [];
|
|
3479
|
+
if (drop.isPityTriggered) tags.push("🔮保底");
|
|
3480
|
+
if (drop.isUpMonster) tags.push("📢UP");
|
|
3481
|
+
if (drop.isNew) tags.push("📖新发现");
|
|
3482
|
+
const tagStr = tags.length > 0 ? ` ${tags.join(" ")}` : "";
|
|
3483
|
+
if (drop.escaped) return `\n💨 ${monsterEmoji} ${qualityLabel} **${drop.monster.name}** 逃跑! *"${drop.monster.captureQuote}"* · +2${tagStr}`;
|
|
3484
|
+
if (drop.isShiny) return `\n🌈 ${monsterEmoji} ✨ **${drop.monster.name}** ✨ 闪光降临! *"${drop.monster.captureQuote}"* · +${expResult.totalExp} · 📖${collectedCount}/${totalMonsters}${tagStr}`;
|
|
3485
|
+
if (drop.monster.quality === "epic" || drop.monster.quality === "legendary") return `\n✦ ${monsterEmoji} ${qualityLabel} **${drop.monster.name}** · *${drop.monster.origin}* · *"${drop.monster.captureQuote}"* · +${expResult.totalExp} · 📖${collectedCount}/${totalMonsters}${tagStr}`;
|
|
3486
|
+
return `\n🗡️ ${monsterEmoji} ${qualityLabel} **${drop.monster.name}** · *"${drop.monster.captureQuote}"* · +${expResult.totalExp} · 📖${collectedCount}/${totalMonsters}${tagStr}`;
|
|
3487
|
+
}
|
|
3488
|
+
function renderLevelUp(levelUp) {
|
|
3489
|
+
const unlockPart = levelUp.unlockDescription ? ` · 🔓${levelUp.unlockDescription}` : "";
|
|
3490
|
+
return `\n⬆️ **升级!** ${levelUp.previousTitle} → **${levelUp.newTitle}** (Lv.${levelUp.newLevel})${unlockPart}`;
|
|
3491
|
+
}
|
|
3492
|
+
function renderEncounter(encounter) {
|
|
3493
|
+
const immortal = getImmortalById(encounter.immortalId);
|
|
3494
|
+
if (!immortal) return "";
|
|
3495
|
+
const typeTag = encounter.type === "guidance" ? "🤍点化" : encounter.type === "treasure" ? "💛赐宝" : "💜收徒";
|
|
3496
|
+
let extra = "";
|
|
3497
|
+
if (encounter.type === "treasure" && encounter.treasureId) extra = ` · 获得${getTreasureName(encounter.treasureId)}`;
|
|
3498
|
+
if (encounter.type === "apprentice") extra = " · 永久加成";
|
|
3499
|
+
return `\n☁️ ${typeTag} **${immortal.name}**:*"${immortal.guidanceQuote}"*${extra}`;
|
|
3500
|
+
}
|
|
3501
|
+
function renderNewAchievements(achievements) {
|
|
3502
|
+
if (achievements.length === 0) return "";
|
|
3503
|
+
return achievements.map((achievement) => {
|
|
3504
|
+
const titlePart = achievement.titleReward ? ` 🎖️「${achievement.titleReward}」` : "";
|
|
3505
|
+
return `\n🏆 ${achievement.emoji} **${achievement.name}** — *${achievement.description}* · +${achievement.expReward}${titlePart}`;
|
|
3506
|
+
}).join("");
|
|
3507
|
+
}
|
|
3508
|
+
/**
|
|
3509
|
+
* 渲染修行面板 (/修行)
|
|
3510
|
+
*/
|
|
3511
|
+
function renderProfilePanel(profile, collection) {
|
|
3512
|
+
const totalMonsters = getTotalMonsterCount();
|
|
3513
|
+
const collectedCount = collection.entries.length;
|
|
3514
|
+
const shinyCount = collection.entries.filter((e) => e.isShiny).length;
|
|
3515
|
+
const progress = getLevelProgress(profile.totalExp);
|
|
3516
|
+
const expToNext = getExpToNextLevel(profile.totalExp);
|
|
3517
|
+
const nextLevel = getNextLevel(profile.level);
|
|
3518
|
+
const upMonster = getWeeklyUpMonster();
|
|
3519
|
+
const allAchievementsList = getAllAchievements();
|
|
3520
|
+
const progressBar = renderEmojiBar(progress);
|
|
3521
|
+
const lines = [
|
|
3522
|
+
`### 🐒 西游妖魔榜 · 修行面板`,
|
|
3523
|
+
"",
|
|
3524
|
+
`**修行者 ID**:${profile.uidHash.slice(0, 8)}`,
|
|
3525
|
+
`**称号**:${profile.title} (Lv.${profile.level})`
|
|
3526
|
+
];
|
|
3527
|
+
if (nextLevel) lines.push(`**修行值**:${profile.totalExp.toLocaleString()} / ${nextLevel.requiredExp.toLocaleString()} (${progress}%)`, "", `${progressBar}`, "", `距离下一级「${nextLevel.title}」还需 ${expToNext?.toLocaleString()} 修行值`);
|
|
3528
|
+
else lines.push(`**修行值**:${profile.totalExp.toLocaleString()} (已满级)`, "", `${renderEmojiBar(100)}`);
|
|
3529
|
+
lines.push("", `#### 📊 统计`, `- **总操作**:${profile.totalOperations} 次`, `- **连击中**:${profile.currentCombo} 次${profile.currentCombo >= 3 ? ` (×${getComboDisplay(profile.currentCombo)})` : ""}`, `- **连续签到**:${profile.consecutiveSignInDays} 天`, `- **最高连击**:${profile.maxCombo} 次`);
|
|
3530
|
+
const qualityCounts = getQualityProgress(collection);
|
|
3531
|
+
lines.push("", `#### 📖 图鉴 ${collectedCount}/${totalMonsters} (${Math.floor(collectedCount / totalMonsters * 100)}%)`, "", `| 品质 | 进度 |`, `|------|------|`);
|
|
3532
|
+
for (const [label, collected, total] of qualityCounts) {
|
|
3533
|
+
const status = collected >= total ? " ✅" : "";
|
|
3534
|
+
lines.push(`| ${label} | ${collected}/${total}${status} |`);
|
|
3535
|
+
}
|
|
3536
|
+
if (shinyCount > 0) lines.push(`| ✨ 闪光 | ${shinyCount} |`);
|
|
3537
|
+
const pity = profile.pityCounters;
|
|
3538
|
+
lines.push("", `#### 🔮 保底状态`, `- **小保底**:${pity.sinceLastRare}/30 · **大保底**:${pity.sinceLastEpic}/80 · **天命**:${pity.sinceLastLegendary}/150`);
|
|
3539
|
+
if (upMonster) lines.push("", `📢 **本周 UP**:${QUALITY_LABELS[upMonster.quality]} ${upMonster.name} (权重 ×5)`);
|
|
3540
|
+
lines.push("", `🏆 **成就**:${profile.unlockedAchievements.length}/${allAchievementsList.length}`, `🎒 **法宝**:${profile.treasures.length} 件`);
|
|
3541
|
+
return lines.join("\n");
|
|
3542
|
+
}
|
|
3543
|
+
/**
|
|
3544
|
+
* 渲染图鉴面板 (/图鉴)
|
|
3545
|
+
*/
|
|
3546
|
+
function renderCollectionPanel(collection) {
|
|
3547
|
+
const allMonstersList = getAllMonsters();
|
|
3548
|
+
const totalMonsters = getTotalMonsterCount();
|
|
3549
|
+
const lines = [`### 📖 妖怪图鉴 · ${collection.entries.length}/${totalMonsters}`, ""];
|
|
3550
|
+
const qualityGroups = [
|
|
3551
|
+
{
|
|
3552
|
+
quality: "normal",
|
|
3553
|
+
label: QUALITY_LABELS.normal,
|
|
3554
|
+
monsters: allMonstersList.filter((m) => m.quality === "normal")
|
|
3555
|
+
},
|
|
3556
|
+
{
|
|
3557
|
+
quality: "fine",
|
|
3558
|
+
label: QUALITY_LABELS.fine,
|
|
3559
|
+
monsters: allMonstersList.filter((m) => m.quality === "fine")
|
|
3560
|
+
},
|
|
3561
|
+
{
|
|
3562
|
+
quality: "rare",
|
|
3563
|
+
label: QUALITY_LABELS.rare,
|
|
3564
|
+
monsters: allMonstersList.filter((m) => m.quality === "rare")
|
|
3565
|
+
},
|
|
3566
|
+
{
|
|
3567
|
+
quality: "epic",
|
|
3568
|
+
label: QUALITY_LABELS.epic,
|
|
3569
|
+
monsters: allMonstersList.filter((m) => m.quality === "epic")
|
|
3570
|
+
},
|
|
3571
|
+
{
|
|
3572
|
+
quality: "legendary",
|
|
3573
|
+
label: QUALITY_LABELS.legendary,
|
|
3574
|
+
monsters: allMonstersList.filter((m) => m.quality === "legendary")
|
|
3575
|
+
}
|
|
3576
|
+
];
|
|
3577
|
+
for (const group of qualityGroups) {
|
|
3578
|
+
const collected = group.monsters.filter((m) => collection.entries.some((e) => e.monsterId === m.id && !e.isShiny));
|
|
3579
|
+
const uncollectedCount = group.monsters.length - collected.length;
|
|
3580
|
+
lines.push(`#### ${group.label} ${collected.length}/${group.monsters.length}${collected.length >= group.monsters.length ? " ✅" : ""}`);
|
|
3581
|
+
if (collected.length > 0) lines.push(collected.map((m) => m.name).join(" · "));
|
|
3582
|
+
if (uncollectedCount > 0) lines.push(`${"❓".repeat(Math.min(uncollectedCount, 5))} *还有 ${uncollectedCount} 只未发现*`);
|
|
3583
|
+
lines.push("");
|
|
3584
|
+
}
|
|
3585
|
+
const shinyEntries = collection.entries.filter((e) => e.isShiny);
|
|
3586
|
+
lines.push(`#### ✨ 闪光 ${shinyEntries.length}`);
|
|
3587
|
+
if (shinyEntries.length > 0) {
|
|
3588
|
+
const shinyNames = shinyEntries.map((e) => {
|
|
3589
|
+
const monster = getMonsterById(e.monsterId);
|
|
3590
|
+
return monster ? `${monster.name} ✨` : e.monsterId;
|
|
3591
|
+
});
|
|
3592
|
+
lines.push(shinyNames.join(" · "));
|
|
3593
|
+
} else lines.push("*等级 ≥ 9 后解锁闪光掉落*");
|
|
3594
|
+
return lines.join("\n");
|
|
3595
|
+
}
|
|
3596
|
+
/**
|
|
3597
|
+
* 渲染成就面板 (/成就)
|
|
3598
|
+
*/
|
|
3599
|
+
function renderAchievementPanel(profile) {
|
|
3600
|
+
const allAchievementsList = getAllAchievements();
|
|
3601
|
+
const lines = [`### 🏆 成就列表 · ${profile.unlockedAchievements.length}/${allAchievementsList.length}`, ""];
|
|
3602
|
+
for (const category of [
|
|
3603
|
+
{
|
|
3604
|
+
key: "cultivation",
|
|
3605
|
+
label: "修行成就"
|
|
3606
|
+
},
|
|
3607
|
+
{
|
|
3608
|
+
key: "collection",
|
|
3609
|
+
label: "收集成就"
|
|
3610
|
+
},
|
|
3611
|
+
{
|
|
3612
|
+
key: "product",
|
|
3613
|
+
label: "产品成就"
|
|
3614
|
+
},
|
|
3615
|
+
{
|
|
3616
|
+
key: "hidden",
|
|
3617
|
+
label: "隐藏成就"
|
|
3618
|
+
}
|
|
3619
|
+
]) {
|
|
3620
|
+
const categoryAchievements = allAchievementsList.filter((a) => a.category === category.key);
|
|
3621
|
+
lines.push(`#### ${category.label}`);
|
|
3622
|
+
for (const achievement of categoryAchievements) {
|
|
3623
|
+
const unlocked = profile.unlockedAchievements.includes(achievement.id);
|
|
3624
|
+
const status = unlocked ? "✅" : "⬜";
|
|
3625
|
+
const desc = category.key === "hidden" && !unlocked ? "???" : achievement.description;
|
|
3626
|
+
lines.push(`- ${status} ${achievement.emoji} **${achievement.name}** — ${desc} (+${achievement.expReward})`);
|
|
3627
|
+
}
|
|
3628
|
+
lines.push("");
|
|
3629
|
+
}
|
|
3630
|
+
return lines.join("\n");
|
|
3631
|
+
}
|
|
3632
|
+
/**
|
|
3633
|
+
* 渲染法宝面板 (/法宝)
|
|
3634
|
+
*/
|
|
3635
|
+
function renderTreasurePanel(profile) {
|
|
3636
|
+
const treasures = getUserTreasures(profile);
|
|
3637
|
+
const consumable = getConsumableTreasures(profile);
|
|
3638
|
+
const lines = [`### 🎒 法宝背包 · ${treasures.length} 件`, ""];
|
|
3639
|
+
if (treasures.length === 0) {
|
|
3640
|
+
lines.push("*背包空空如也,等待神仙赐宝...*");
|
|
3641
|
+
return lines.join("\n");
|
|
3642
|
+
}
|
|
3643
|
+
for (const treasure of treasures) {
|
|
3644
|
+
const status = profile.consumedTreasures.includes(treasure.id) ? "(已使用)" : treasure.consumable ? "(可使用)" : "(永久生效)";
|
|
3645
|
+
lines.push(`- **${treasure.name}** ${status}`, ` ${treasure.description}`, ` *来源:${treasure.source}*`, "");
|
|
3646
|
+
}
|
|
3647
|
+
if (consumable.length > 0) lines.push("", `💡 发送 \`/使用 法宝名\` 来使用一次性法宝`);
|
|
3648
|
+
return lines.join("\n");
|
|
3649
|
+
}
|
|
3650
|
+
/**
|
|
3651
|
+
* 渲染保底面板 (/保底)
|
|
3652
|
+
*/
|
|
3653
|
+
function renderPityPanel(profile) {
|
|
3654
|
+
const pity = profile.pityCounters;
|
|
3655
|
+
const softPityHints = [];
|
|
3656
|
+
if (pity.sinceLastRare >= 20) softPityHints.push(`稀有软保底已激活 +${(pity.sinceLastRare - 20) * 3}%`);
|
|
3657
|
+
if (pity.sinceLastEpic >= 60) softPityHints.push(`史诗软保底已激活 +${(pity.sinceLastEpic - 60) * 2}%`);
|
|
3658
|
+
if (pity.sinceLastLegendary >= 120) softPityHints.push(`传说软保底已激活 +${(pity.sinceLastLegendary - 120) * 1}%`);
|
|
3659
|
+
const lines = [
|
|
3660
|
+
`### 🔮 保底计数器`,
|
|
3661
|
+
"",
|
|
3662
|
+
`| 保底类型 | 当前计数 | 触发阈值 | 进度 |`,
|
|
3663
|
+
`|---------|---------|---------|------|`,
|
|
3664
|
+
`| 小保底(稀有) | ${pity.sinceLastRare} | 30 | ${Math.floor(pity.sinceLastRare / 30 * 100)}% |`,
|
|
3665
|
+
`| 大保底(史诗) | ${pity.sinceLastEpic} | 80 | ${Math.floor(pity.sinceLastEpic / 80 * 100)}% |`,
|
|
3666
|
+
`| 天命保底(传说) | ${pity.sinceLastLegendary} | 150 | ${Math.floor(pity.sinceLastLegendary / 150 * 100)}% |`,
|
|
3667
|
+
`| 闪光保底 | ${pity.totalDropsWithoutShiny} | 800 | ${Math.floor(pity.totalDropsWithoutShiny / 800 * 100)}% |`
|
|
3668
|
+
];
|
|
3669
|
+
if (softPityHints.length > 0) lines.push("", `🌟 ${softPityHints.join(" · ")}`);
|
|
3670
|
+
lines.push("", `*保底计数器在对应品质或更高品质掉落后重置*`);
|
|
3671
|
+
return lines.join("\n");
|
|
3672
|
+
}
|
|
3673
|
+
/**
|
|
3674
|
+
* 渲染机缘面板 (/机缘)
|
|
3675
|
+
*/
|
|
3676
|
+
function renderEncounterPanel(profile) {
|
|
3677
|
+
const lines = [`### ☁️ 神仙机缘录`, ""];
|
|
3678
|
+
if (profile.level < 3) {
|
|
3679
|
+
lines.push("*等级 ≥ 3(修行者)后解锁机缘系统*");
|
|
3680
|
+
return lines.join("\n");
|
|
3681
|
+
}
|
|
3682
|
+
if (profile.encounters.length === 0) {
|
|
3683
|
+
lines.push("*尚未遇到任何神仙,继续修行吧...*");
|
|
3684
|
+
return lines.join("\n");
|
|
3685
|
+
}
|
|
3686
|
+
for (const encounter of profile.encounters) {
|
|
3687
|
+
const immortal = getImmortalById(encounter.immortalId);
|
|
3688
|
+
if (!immortal) continue;
|
|
3689
|
+
const typeLabel = encounter.type === "guidance" ? "🤍 点化" : encounter.type === "treasure" ? "💛 赐宝" : "💜 收徒";
|
|
3690
|
+
const date = new Date(encounter.occurredAt).toLocaleDateString("zh-CN");
|
|
3691
|
+
lines.push(`- ${typeLabel} **${immortal.name}** — ${date}`);
|
|
3692
|
+
if (encounter.type === "treasure" && encounter.treasureId) lines.push(` 赐宝:${getTreasureName(encounter.treasureId)}`);
|
|
3693
|
+
}
|
|
3694
|
+
return lines.join("\n");
|
|
3695
|
+
}
|
|
3696
|
+
/**
|
|
3697
|
+
* 渲染妖魔榜 (/妖魔榜)
|
|
3698
|
+
*/
|
|
3699
|
+
function renderLeaderboard(profile, collection) {
|
|
3700
|
+
const upMonster = getWeeklyUpMonster();
|
|
3701
|
+
const totalMonsters = getTotalMonsterCount();
|
|
3702
|
+
const lines = [`### 🐒 西游妖魔榜`, ""];
|
|
3703
|
+
if (upMonster) lines.push(`#### 📢 本周 UP`, `${QUALITY_LABELS[upMonster.quality]} **${upMonster.name}** · ${upMonster.origin}`, `> "${upMonster.captureQuote}"`, `*在对应品质池中掉落权重 ×5*`, "");
|
|
3704
|
+
lines.push(`#### 📊 掉落统计`, `- **总掉落**:${profile.totalOperations} 次`, `- **图鉴完成度**:${collection.entries.length}/${totalMonsters}`, `- **闪光收服**:${collection.entries.filter((e) => e.isShiny).length} 只`, "", `#### 🔮 保底状态`, `- 小保底:${profile.pityCounters.sinceLastRare}/30`, `- 大保底:${profile.pityCounters.sinceLastEpic}/80`, `- 天命:${profile.pityCounters.sinceLastLegendary}/150`);
|
|
3705
|
+
return lines.join("\n");
|
|
3706
|
+
}
|
|
3707
|
+
/**
|
|
3708
|
+
* 渲染法宝使用结果
|
|
3709
|
+
*/
|
|
3710
|
+
function renderTreasureUse(treasureName, expGained, currentExp, nextLevelExp) {
|
|
3711
|
+
return [
|
|
3712
|
+
"---",
|
|
3713
|
+
`${{
|
|
3714
|
+
"蟠桃": "🍑",
|
|
3715
|
+
"人参果": "🍐"
|
|
3716
|
+
}[treasureName] ?? "✨"} **使用了${treasureName}!**`,
|
|
3717
|
+
`修行值 +${expGained}${nextLevelExp ? ` · 当前 ${currentExp}/${nextLevelExp}` : ""}`,
|
|
3718
|
+
`> "仙物入腹,周身舒泰。"`
|
|
3719
|
+
].join("\n");
|
|
3720
|
+
}
|
|
3721
|
+
/**
|
|
3722
|
+
* 渲染群聊炫耀 (/炫耀)
|
|
3723
|
+
*/
|
|
3724
|
+
function renderShowOff(profile, collection) {
|
|
3725
|
+
const shinyCount = collection.entries.filter((e) => e.isShiny).length;
|
|
3726
|
+
const rarest = findRarestMonster(collection);
|
|
3727
|
+
const lines = [
|
|
3728
|
+
`### 🐒 ${profile.title} (Lv.${profile.level}) 的西游妖魔榜`,
|
|
3729
|
+
"",
|
|
3730
|
+
`图鉴:${collection.entries.length}/${getTotalMonsterCount()} · 闪光:${shinyCount}`
|
|
3731
|
+
];
|
|
3732
|
+
if (rarest) lines.push(`最稀有:${QUALITY_LABELS[rarest.quality]} ${rarest.name}`);
|
|
3733
|
+
lines.push("", `> "此人修为不浅,诸位小心。"`);
|
|
3734
|
+
return lines.join("\n");
|
|
3735
|
+
}
|
|
3736
|
+
/**
|
|
3737
|
+
* 渲染 emoji 进度条(8 格,用圆形符号表示)
|
|
3738
|
+
*
|
|
3739
|
+
* 示例:●●●●●○○○ 62%
|
|
3740
|
+
*/
|
|
3741
|
+
function renderEmojiBar(percent) {
|
|
3742
|
+
const total = 8;
|
|
3743
|
+
const filled = Math.round(percent / 100 * total);
|
|
3744
|
+
return "●".repeat(filled) + "○".repeat(total - filled);
|
|
3745
|
+
}
|
|
3746
|
+
function getComboDisplay(combo) {
|
|
3747
|
+
if (combo >= 10) return "3.0";
|
|
3748
|
+
if (combo >= 5) return "2.0";
|
|
3749
|
+
if (combo >= 3) return "1.5";
|
|
3750
|
+
return "1.0";
|
|
3751
|
+
}
|
|
3752
|
+
function getQualityProgress(collection) {
|
|
3753
|
+
const allMonstersList = getAllMonsters();
|
|
3754
|
+
return [
|
|
3755
|
+
{
|
|
3756
|
+
label: QUALITY_LABELS.normal,
|
|
3757
|
+
quality: "normal"
|
|
3758
|
+
},
|
|
3759
|
+
{
|
|
3760
|
+
label: QUALITY_LABELS.fine,
|
|
3761
|
+
quality: "fine"
|
|
3762
|
+
},
|
|
3763
|
+
{
|
|
3764
|
+
label: QUALITY_LABELS.rare,
|
|
3765
|
+
quality: "rare"
|
|
3766
|
+
},
|
|
3767
|
+
{
|
|
3768
|
+
label: QUALITY_LABELS.epic,
|
|
3769
|
+
quality: "epic"
|
|
3770
|
+
},
|
|
3771
|
+
{
|
|
3772
|
+
label: QUALITY_LABELS.legendary,
|
|
3773
|
+
quality: "legendary"
|
|
3774
|
+
}
|
|
3775
|
+
].map(({ label, quality }) => {
|
|
3776
|
+
const total = allMonstersList.filter((m) => m.quality === quality).length;
|
|
3777
|
+
return [
|
|
3778
|
+
label,
|
|
3779
|
+
allMonstersList.filter((m) => m.quality === quality && collection.entries.some((e) => e.monsterId === m.id && !e.isShiny)).length,
|
|
3780
|
+
total
|
|
3781
|
+
];
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3784
|
+
function findRarestMonster(collection) {
|
|
3785
|
+
for (const quality of [
|
|
3786
|
+
"legendary",
|
|
3787
|
+
"epic",
|
|
3788
|
+
"rare",
|
|
3789
|
+
"fine",
|
|
3790
|
+
"normal"
|
|
3791
|
+
]) {
|
|
3792
|
+
const entry = collection.entries.find((e) => {
|
|
3793
|
+
return getMonsterById(e.monsterId)?.quality === quality;
|
|
3794
|
+
});
|
|
3795
|
+
if (entry) return getMonsterById(entry.monsterId) ?? null;
|
|
3796
|
+
}
|
|
3797
|
+
return null;
|
|
3798
|
+
}
|
|
3799
|
+
const BOUNTY_TIER_LABELS = {
|
|
3800
|
+
bronze: "🥉 铜令",
|
|
3801
|
+
silver: "🥈 银令",
|
|
3802
|
+
gold: "🥇 金令"
|
|
3803
|
+
};
|
|
3804
|
+
/**
|
|
3805
|
+
* 渲染悬赏令面板 (/悬赏)
|
|
3806
|
+
*/
|
|
3807
|
+
function renderBountyPanel(profile) {
|
|
3808
|
+
const bountyState = profile.dailyBounty;
|
|
3809
|
+
if (!bountyState || bountyState.bounties.length === 0) return [
|
|
3810
|
+
`### 📜 今日悬赏令`,
|
|
3811
|
+
"",
|
|
3812
|
+
"*今日悬赏令尚未生成,执行一次 dws 命令即可刷新。*"
|
|
3813
|
+
].join("\n");
|
|
3814
|
+
const lines = [`### 📜 今日悬赏令`, ""];
|
|
3815
|
+
for (const bounty of bountyState.bounties) {
|
|
3816
|
+
const tierLabel = BOUNTY_TIER_LABELS[bounty.tier] ?? bounty.tier;
|
|
3817
|
+
const status = bounty.completed ? "✅" : "⬜";
|
|
3818
|
+
const progressPercent = Math.min(100, Math.floor(bounty.current / bounty.target * 100));
|
|
3819
|
+
const progressBar = renderEmojiBar(progressPercent);
|
|
3820
|
+
lines.push(`${status} **${tierLabel}**:${bounty.description}`, ` 奖励:+${bounty.reward.exp} 修行值`, ` 进度:${bounty.current}/${bounty.target} ${progressBar} ${progressPercent}%`, "");
|
|
3821
|
+
}
|
|
3822
|
+
const now = /* @__PURE__ */ new Date();
|
|
3823
|
+
const tomorrow = new Date(now);
|
|
3824
|
+
tomorrow.setHours(24, 0, 0, 0);
|
|
3825
|
+
const remainingMs = tomorrow.getTime() - now.getTime();
|
|
3826
|
+
const remainingHours = Math.floor(remainingMs / (3600 * 1e3));
|
|
3827
|
+
const remainingMinutes = Math.floor(remainingMs % (3600 * 1e3) / (60 * 1e3));
|
|
3828
|
+
lines.push(`⏰ 刷新倒计时:${remainingHours} 小时 ${remainingMinutes} 分`);
|
|
3829
|
+
const history = profile.bountyHistory;
|
|
3830
|
+
lines.push("", `#### 📊 悬赏历史`, `累计完成:${history.totalCompleted} 张 (🥉${history.bronzeCompleted} 🥈${history.silverCompleted} 🥇${history.goldCompleted})`, `连续全清:${history.consecutiveFullClear} 天`);
|
|
3831
|
+
return lines.join("\n");
|
|
3832
|
+
}
|
|
3833
|
+
/**
|
|
3834
|
+
* 渲染悬赏令完成通知(紧凑单行)
|
|
3835
|
+
*/
|
|
3836
|
+
function renderBountyComplete(bounty) {
|
|
3837
|
+
return `\n📜 **${BOUNTY_TIER_LABELS[bounty.tier] ?? bounty.tier}完成!** 「${bounty.description}」 · +${bounty.reward.exp}`;
|
|
3838
|
+
}
|
|
3839
|
+
const EVENT_CATEGORY_EMOJI = {
|
|
3840
|
+
blessing: "🌟",
|
|
3841
|
+
challenge: "⚔️",
|
|
3842
|
+
disaster: "😈"
|
|
3843
|
+
};
|
|
3844
|
+
/**
|
|
3845
|
+
* 渲染随机事件触发通知(紧凑版)
|
|
3846
|
+
*/
|
|
3847
|
+
function renderEventTrigger(event) {
|
|
3848
|
+
const emoji = EVENT_CATEGORY_EMOJI[event.category] ?? "🎲";
|
|
3849
|
+
const durationStr = event.duration.type !== "instant" ? ` (${event.duration.remaining}/${event.duration.total})` : "";
|
|
3850
|
+
if (event.category === "challenge") {
|
|
3851
|
+
const challenge = event;
|
|
3852
|
+
return `\n${emoji} **${event.name}** — ${event.description} · 🏆+${challenge.successReward.exp} 💀-${challenge.failurePenalty.expLoss} · ⏰${challenge.operationLimit}次`;
|
|
3853
|
+
}
|
|
3854
|
+
const resolvePart = event.category === "disaster" && event.resolution ? ` · 💡${event.resolution.description}` : "";
|
|
3855
|
+
return `\n${emoji} **${event.name}** — ${event.description}${durationStr}${resolvePart}`;
|
|
3856
|
+
}
|
|
3857
|
+
/**
|
|
3858
|
+
* 渲染挑战事件结果(紧凑单行)
|
|
3859
|
+
*/
|
|
3860
|
+
function renderChallengeResult(event, success) {
|
|
3861
|
+
if (success) {
|
|
3862
|
+
const pityPart = event.successReward.pityBonus ? ` · 保底+${event.successReward.pityBonus}` : "";
|
|
3863
|
+
return `\n🏆 **${event.name} · 挑战成功!** +${event.successReward.exp}${pityPart}`;
|
|
3864
|
+
}
|
|
3865
|
+
const comboPart = event.failurePenalty.comboReset ? " · 连击归零" : "";
|
|
3866
|
+
return `\n💀 **${event.name} · 挑战失败** -${event.failurePenalty.expLoss}${comboPart}`;
|
|
3867
|
+
}
|
|
3868
|
+
/**
|
|
3869
|
+
* 渲染灾厄事件化解通知
|
|
3870
|
+
*/
|
|
3871
|
+
function renderDisasterResolved(event) {
|
|
3872
|
+
return [
|
|
3873
|
+
"",
|
|
3874
|
+
"---",
|
|
3875
|
+
`🛡️ **${event.name} · 已化解!**`,
|
|
3876
|
+
`*灾厄消散,天地清明。*`
|
|
3877
|
+
].join("\n");
|
|
3878
|
+
}
|
|
3879
|
+
/**
|
|
3880
|
+
* 渲染事件面板 (/事件)
|
|
3881
|
+
*/
|
|
3882
|
+
function renderEventPanel(profile) {
|
|
3883
|
+
const activeState = profile.activeEvents;
|
|
3884
|
+
const lines = [`### 🎲 随机事件`, ""];
|
|
3885
|
+
if (activeState.currentEvents.length === 0 && !activeState.activeChallenge) lines.push("*当前没有活跃的随机事件。*");
|
|
3886
|
+
else {
|
|
3887
|
+
if (activeState.currentEvents.length > 0) {
|
|
3888
|
+
lines.push(`#### 当前生效`);
|
|
3889
|
+
for (const event of activeState.currentEvents) {
|
|
3890
|
+
const emoji = EVENT_CATEGORY_EMOJI[event.category] ?? "🎲";
|
|
3891
|
+
const remaining = event.duration.type !== "instant" ? ` (剩余 ${event.duration.remaining}/${event.duration.total})` : "";
|
|
3892
|
+
lines.push(`- ${emoji} **${event.name}**:${event.description}${remaining}`);
|
|
3893
|
+
if (event.resolution) lines.push(` 💡 化解:${event.resolution.description}`);
|
|
3894
|
+
}
|
|
3895
|
+
lines.push("");
|
|
3896
|
+
}
|
|
3897
|
+
if (activeState.activeChallenge) {
|
|
3898
|
+
const challenge = activeState.activeChallenge;
|
|
3899
|
+
lines.push(`#### ⚔️ 进行中的挑战`, `**${challenge.name}**:${challenge.description}`, `进度:${challenge.challengeCondition.current}/${challenge.challengeCondition.target}`, `操作次数:${challenge.progress.operationsUsed}/${challenge.progress.operationLimit}`, "");
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
const stats = profile.eventStats;
|
|
3903
|
+
lines.push(`#### 📊 事件统计`, `- **累计触发**:${stats.totalTriggered} 次`, `- **挑战完成**:${stats.challengesCompleted} 次`, `- **挑战失败**:${stats.challengesFailed} 次`, `- **灾厄化解**:${stats.disastersResolved} 次`);
|
|
3904
|
+
return lines.join("\n");
|
|
3905
|
+
}
|
|
3906
|
+
//#endregion
|
|
3907
|
+
//#region src/game-xiyou/commands.ts
|
|
3908
|
+
/** 养成系统支持的命令列表 */
|
|
3909
|
+
const GAMIFICATION_COMMANDS = [
|
|
3910
|
+
"/修行",
|
|
3911
|
+
"/图鉴",
|
|
3912
|
+
"/成就",
|
|
3913
|
+
"/法宝",
|
|
3914
|
+
"/使用",
|
|
3915
|
+
"/妖魔榜",
|
|
3916
|
+
"/机缘",
|
|
3917
|
+
"/保底",
|
|
3918
|
+
"/炫耀",
|
|
3919
|
+
"/悬赏",
|
|
3920
|
+
"/事件",
|
|
3921
|
+
"/西游"
|
|
3922
|
+
];
|
|
3923
|
+
/**
|
|
3924
|
+
* 检查消息是否是养成系统命令
|
|
3925
|
+
*/
|
|
3926
|
+
function isGamificationCommand(text) {
|
|
3927
|
+
const trimmed = text.trim();
|
|
3928
|
+
return GAMIFICATION_COMMANDS.some((cmd) => trimmed.startsWith(cmd));
|
|
3929
|
+
}
|
|
3930
|
+
/**
|
|
3931
|
+
* 处理养成系统命令,返回 Markdown 响应
|
|
3932
|
+
*
|
|
3933
|
+
* @returns Markdown 字符串,或 null(不是养成系统命令)
|
|
3934
|
+
*/
|
|
3935
|
+
function handleGamificationCommand(text, profile, collection, saveCallback) {
|
|
3936
|
+
const trimmed = text.trim();
|
|
3937
|
+
if (trimmed === "/修行") return renderProfilePanel(profile, collection);
|
|
3938
|
+
if (trimmed === "/图鉴") return renderCollectionPanel(collection);
|
|
3939
|
+
if (trimmed.startsWith("/图鉴 ")) return renderMonsterDetail(trimmed.slice(4).trim(), collection);
|
|
3940
|
+
if (trimmed === "/成就") return renderAchievementPanel(profile);
|
|
3941
|
+
if (trimmed === "/法宝") return renderTreasurePanel(profile);
|
|
3942
|
+
if (trimmed.startsWith("/使用 ")) return handleUseTreasure(profile, trimmed.slice(4).trim(), saveCallback);
|
|
3943
|
+
if (trimmed === "/妖魔榜") return renderLeaderboard(profile, collection);
|
|
3944
|
+
if (trimmed === "/机缘") return renderEncounterPanel(profile);
|
|
3945
|
+
if (trimmed === "/保底") return renderPityPanel(profile);
|
|
3946
|
+
if (trimmed === "/炫耀") return renderShowOff(profile, collection);
|
|
3947
|
+
if (trimmed === "/悬赏" || trimmed === "/悬赏 历史") return renderBountyPanel(profile);
|
|
3948
|
+
if (trimmed === "/事件" || trimmed === "/事件 历史") return renderEventPanel(profile);
|
|
3949
|
+
if (trimmed === "/西游" || trimmed === "/西游 --h" || trimmed === "/西游 -h" || trimmed === "/西游 help") return [
|
|
3950
|
+
`### 🐒 西游妖魔榜 · 命令一览`,
|
|
3951
|
+
``,
|
|
3952
|
+
`当前状态:${profile.settings.enabled ? "✅ 已开启" : "❌ 已关闭"}`,
|
|
3953
|
+
``,
|
|
3954
|
+
`| 命令 | 功能 |`,
|
|
3955
|
+
`|------|------|`,
|
|
3956
|
+
`| \`/修行\` | 查看个人修行面板(等级、修行值、连击、签到等) |`,
|
|
3957
|
+
`| \`/图鉴\` | 查看妖怪图鉴收集进度 |`,
|
|
3958
|
+
`| \`/图鉴 妖怪名\` | 查看指定妖怪详情 |`,
|
|
3959
|
+
`| \`/成就\` | 查看成就列表及解锁状态 |`,
|
|
3960
|
+
`| \`/法宝\` | 查看法宝背包 |`,
|
|
3961
|
+
`| \`/使用 法宝名\` | 使用一次性法宝(如蟠桃、人参果) |`,
|
|
3962
|
+
`| \`/妖魔榜\` | 查看本周 UP 妖怪、掉落统计、保底状态 |`,
|
|
3963
|
+
`| \`/机缘\` | 查看神仙机缘录 |`,
|
|
3964
|
+
`| \`/保底\` | 查看保底计数器详情(含软保底状态) |`,
|
|
3965
|
+
`| \`/炫耀\` | 生成炫耀卡片 |`,
|
|
3966
|
+
`| \`/悬赏\` | 查看今日悬赏令及历史统计 |`,
|
|
3967
|
+
`| \`/事件\` | 查看当前活跃事件及事件统计 |`,
|
|
3968
|
+
`| \`/西游 开启\` | 开启养成系统 |`,
|
|
3969
|
+
`| \`/西游 关闭\` | 关闭养成系统 |`,
|
|
3970
|
+
``,
|
|
3971
|
+
`> 关闭后,dws 命令执行不再触发降妖掉落,但已有数据会保留。`
|
|
3972
|
+
].join("\n");
|
|
3973
|
+
if (trimmed === "/西游 开启" || trimmed === "/西游 开") {
|
|
3974
|
+
profile.settings.enabled = true;
|
|
3975
|
+
saveCallback();
|
|
3976
|
+
return [
|
|
3977
|
+
`### 🐒 西游妖魔榜 · 已开启`,
|
|
3978
|
+
``,
|
|
3979
|
+
`✅ 养成系统已开启!每次使用钉钉产品能力都会触发降妖掉落。`,
|
|
3980
|
+
``,
|
|
3981
|
+
`发送 \`/修行\` 查看你的修行面板。`
|
|
3982
|
+
].join("\n");
|
|
3983
|
+
}
|
|
3984
|
+
if (trimmed === "/西游 关闭" || trimmed === "/西游 关") {
|
|
3985
|
+
profile.settings.enabled = false;
|
|
3986
|
+
saveCallback();
|
|
3987
|
+
return [
|
|
3988
|
+
`### 🐒 西游妖魔榜 · 已关闭`,
|
|
3989
|
+
``,
|
|
3990
|
+
`❌ 养成系统已关闭。dws 命令执行不再触发降妖掉落。`,
|
|
3991
|
+
``,
|
|
3992
|
+
`你的修行数据已保留,随时可以发送 \`/西游 开启\` 重新开启。`
|
|
3993
|
+
].join("\n");
|
|
3994
|
+
}
|
|
3995
|
+
return null;
|
|
3996
|
+
}
|
|
3997
|
+
/**
|
|
3998
|
+
* 渲染妖怪详情
|
|
3999
|
+
*/
|
|
4000
|
+
function renderMonsterDetail(monsterName, collection) {
|
|
4001
|
+
const monster = getAllMonsters().find((m) => m.name === monsterName);
|
|
4002
|
+
if (!monster) return `未找到名为「${monsterName}」的妖怪。`;
|
|
4003
|
+
const entry = collection.entries.find((e) => e.monsterId === monster.id && !e.isShiny);
|
|
4004
|
+
const shinyEntry = collection.entries.find((e) => e.monsterId === monster.id && e.isShiny);
|
|
4005
|
+
const lines = [
|
|
4006
|
+
`### ${QUALITY_LABELS[monster.quality]} ${monster.name}`,
|
|
4007
|
+
"",
|
|
4008
|
+
`- **出处**:${monster.origin}`,
|
|
4009
|
+
`- **关联产品**:${monster.relatedProduct ?? "任意"}`,
|
|
4010
|
+
`- **收服台词**:"${monster.captureQuote}"`,
|
|
4011
|
+
""
|
|
4012
|
+
];
|
|
4013
|
+
if (entry) lines.push(`✅ **已收服**`, `- 首次收服:${new Date(entry.firstCapturedAt).toLocaleDateString("zh-CN")}`, `- 收服次数:${entry.captureCount}`);
|
|
4014
|
+
else lines.push(`❌ **未收服**`);
|
|
4015
|
+
if (shinyEntry) lines.push("", `✨ **闪光变体已收服**`);
|
|
4016
|
+
return lines.join("\n");
|
|
4017
|
+
}
|
|
4018
|
+
/**
|
|
4019
|
+
* 处理使用法宝命令
|
|
4020
|
+
*/
|
|
4021
|
+
function handleUseTreasure(profile, treasureName, saveCallback) {
|
|
4022
|
+
const result = consumeTreasure(profile, treasureName);
|
|
4023
|
+
if (!result) return `无法使用「${treasureName}」。可能原因:未拥有、已使用、或不是一次性法宝。\n\n发送 \`/法宝\` 查看背包。`;
|
|
4024
|
+
const nextLevelExp = getNextLevel(profile.level)?.requiredExp ?? null;
|
|
4025
|
+
saveCallback();
|
|
4026
|
+
return renderTreasureUse(treasureName, result.expGained, profile.totalExp, nextLevelExp);
|
|
4027
|
+
}
|
|
4028
|
+
//#endregion
|
|
4029
|
+
//#region src/game-xiyou/index.ts
|
|
4030
|
+
/**
|
|
4031
|
+
* 西游妖魔榜养成系统 · 入口
|
|
4032
|
+
*
|
|
4033
|
+
* GamificationEngine 是养成系统的门面类,统一协调所有子系统。
|
|
4034
|
+
* 对外暴露两个核心方法:
|
|
4035
|
+
* - onDwsCommandResult(): 每次 dws CLI 命令执行后调用(成功或失败)
|
|
4036
|
+
* - handleCommand(): 处理聊天命令(/修行 /图鉴 等)
|
|
4037
|
+
*/
|
|
4038
|
+
let engineInstance = null;
|
|
4039
|
+
var GamificationEngine = class GamificationEngine {
|
|
4040
|
+
profile;
|
|
4041
|
+
collection;
|
|
4042
|
+
history;
|
|
4043
|
+
uidHash;
|
|
4044
|
+
constructor(senderId) {
|
|
4045
|
+
this.uidHash = resolveUid(senderId);
|
|
4046
|
+
this.profile = loadProfile(this.uidHash);
|
|
4047
|
+
this.collection = loadCollection(this.uidHash);
|
|
4048
|
+
this.history = loadHistory(this.uidHash);
|
|
4049
|
+
}
|
|
4050
|
+
/**
|
|
4051
|
+
* 获取或创建引擎实例
|
|
4052
|
+
*/
|
|
4053
|
+
static getInstance(senderId) {
|
|
4054
|
+
const uidHash = resolveUid(senderId);
|
|
4055
|
+
if (!engineInstance || engineInstance.uidHash !== uidHash) engineInstance = new GamificationEngine(senderId);
|
|
4056
|
+
return engineInstance;
|
|
4057
|
+
}
|
|
4058
|
+
/**
|
|
4059
|
+
* 强制重新加载数据(用于多用户场景)
|
|
4060
|
+
*/
|
|
4061
|
+
static getInstanceForUser(senderId) {
|
|
4062
|
+
return new GamificationEngine(senderId);
|
|
4063
|
+
}
|
|
4064
|
+
/**
|
|
4065
|
+
* 检查养成系统是否启用
|
|
4066
|
+
*/
|
|
4067
|
+
isEnabled() {
|
|
4068
|
+
return this.profile.settings.enabled;
|
|
4069
|
+
}
|
|
4070
|
+
/**
|
|
4071
|
+
* 检查消息是否是养成系统命令
|
|
4072
|
+
*/
|
|
4073
|
+
isCommand(text) {
|
|
4074
|
+
return isGamificationCommand(text);
|
|
4075
|
+
}
|
|
4076
|
+
/**
|
|
4077
|
+
* 处理聊天命令,返回 Markdown 响应
|
|
4078
|
+
*/
|
|
4079
|
+
handleCommand(text) {
|
|
4080
|
+
return handleGamificationCommand(text, this.profile, this.collection, () => this.save());
|
|
4081
|
+
}
|
|
4082
|
+
/**
|
|
4083
|
+
* dws CLI 命令执行后调用(核心方法)
|
|
4084
|
+
*
|
|
4085
|
+
* v2: 集成逃跑机制、悬赏令、随机事件系统
|
|
4086
|
+
*
|
|
4087
|
+
* @param product - dws 产品名(如 "aitable"、"calendar")
|
|
4088
|
+
* @param success - 命令是否成功
|
|
4089
|
+
* @param commandStr - 原始命令字符串(用于生成 hash)
|
|
4090
|
+
* @param isRecovery - 是否为 recovery 成功
|
|
4091
|
+
* @returns Markdown 字符串(追加到 agent 回复末尾),或空字符串
|
|
4092
|
+
*/
|
|
4093
|
+
onDwsCommandResult(product, success, commandStr = "", isRecovery = false) {
|
|
4094
|
+
if (!this.isEnabled()) return "";
|
|
4095
|
+
const commandHash = createHash("sha256").update(commandStr).digest("hex").slice(0, 16);
|
|
4096
|
+
checkBountyDayReset(this.profile);
|
|
4097
|
+
generateDailyBounties(this.profile);
|
|
4098
|
+
if (!success) {
|
|
4099
|
+
this.profile.currentCombo = 0;
|
|
4100
|
+
this.profile.consecutiveFailures += 1;
|
|
4101
|
+
tickActiveEvents(this.profile, false, product, false);
|
|
4102
|
+
this.save();
|
|
4103
|
+
return "";
|
|
4104
|
+
}
|
|
4105
|
+
this.profile.totalOperations += 1;
|
|
4106
|
+
this.profile.currentCombo += 1;
|
|
4107
|
+
if (this.profile.currentCombo > this.profile.maxCombo) this.profile.maxCombo = this.profile.currentCombo;
|
|
4108
|
+
this.profile.productUsage[product] = (this.profile.productUsage[product] ?? 0) + 1;
|
|
4109
|
+
if (isRecovery) this.profile.totalRecoveries += 1;
|
|
4110
|
+
updateSignInStatus(this.profile);
|
|
4111
|
+
const expResult = calculateExp(product, this.profile);
|
|
4112
|
+
const eventExpMultiplier = getActiveExpMultiplier(this.profile);
|
|
4113
|
+
expResult.totalExp = Math.floor(expResult.totalExp * eventExpMultiplier);
|
|
4114
|
+
const levelUp = checkLevelUp(this.profile, expResult.totalExp);
|
|
4115
|
+
this.profile.totalExp += expResult.totalExp;
|
|
4116
|
+
applyLevelUp(this.profile);
|
|
4117
|
+
let dropResult = executeDrop(product, this.profile, this.collection);
|
|
4118
|
+
dropResult.expGained = expResult.totalExp;
|
|
4119
|
+
if (dropResult.monster.id) dropResult = resolveEscape(dropResult, this.profile, product);
|
|
4120
|
+
if (dropResult.monster.id) tickDropEvents(this.profile);
|
|
4121
|
+
if (dropResult.monster.id && !dropResult.escaped) this.updateCollection(dropResult.monster.id, dropResult.isShiny, commandHash);
|
|
4122
|
+
if (dropResult.escaped) this.profile.totalExp += 2;
|
|
4123
|
+
const encounter = checkEncounter(this.profile);
|
|
4124
|
+
if (encounter) {
|
|
4125
|
+
applyEncounterEffects(this.profile, encounter);
|
|
4126
|
+
if (resolveDisasterEvent(this.profile, "trigger_encounter")) {}
|
|
4127
|
+
}
|
|
4128
|
+
const triggeredEvent = checkRandomEvent(this.profile);
|
|
4129
|
+
let extraDropResult = null;
|
|
4130
|
+
if (triggeredEvent) {
|
|
4131
|
+
if (triggeredEvent.effect.type === "extra_drop") {
|
|
4132
|
+
extraDropResult = executeDrop(product, this.profile, this.collection);
|
|
4133
|
+
if (extraDropResult.monster.id && !extraDropResult.escaped) this.updateCollection(extraDropResult.monster.id, extraDropResult.isShiny, commandHash);
|
|
4134
|
+
}
|
|
4135
|
+
if (triggeredEvent.id === "EV202") {
|
|
4136
|
+
const fineEntries = this.collection.entries.filter((e) => {
|
|
4137
|
+
return getMonsterById(e.monsterId)?.quality === "fine" && !e.isShiny;
|
|
4138
|
+
});
|
|
4139
|
+
if (fineEntries.length > 0) {
|
|
4140
|
+
const removedEntry = fineEntries[Math.floor(Math.random() * fineEntries.length)];
|
|
4141
|
+
this.collection.entries = this.collection.entries.filter((e) => e !== removedEntry);
|
|
4142
|
+
this.profile.escapeHistory[removedEntry.monsterId] = 1;
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
const capturedMonster = dropResult.monster.id !== "" && !dropResult.escaped;
|
|
4147
|
+
const eventResults = tickActiveEvents(this.profile, true, product, capturedMonster);
|
|
4148
|
+
if (eventResults.some((r) => r.event.category === "disaster" && (r.outcome === "expired" || r.outcome === "resolved")) && triggeredEvent?.category === "blessing") triggerSpecialAchievement(this.profile, "A412");
|
|
4149
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4150
|
+
const todayRecords = this.history.records.filter((r) => {
|
|
4151
|
+
return new Date(r.timestamp).toISOString().slice(0, 10) === today;
|
|
4152
|
+
});
|
|
4153
|
+
const todayProducts = /* @__PURE__ */ new Set();
|
|
4154
|
+
const todayQualities = /* @__PURE__ */ new Set();
|
|
4155
|
+
for (const record of todayRecords) {
|
|
4156
|
+
if (record.success) todayProducts.add(record.product);
|
|
4157
|
+
if (record.monsterId && !record.escaped) {
|
|
4158
|
+
const monster = getMonsterById(record.monsterId);
|
|
4159
|
+
if (monster) todayQualities.add(monster.quality);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
todayProducts.add(product);
|
|
4163
|
+
if (capturedMonster) todayQualities.add(dropResult.monster.quality);
|
|
4164
|
+
const bountyContext = {
|
|
4165
|
+
commandSuccess: true,
|
|
4166
|
+
product,
|
|
4167
|
+
dropResult: capturedMonster ? dropResult : void 0,
|
|
4168
|
+
encounterTriggered: encounter !== null,
|
|
4169
|
+
encounterType: encounter?.type,
|
|
4170
|
+
currentCombo: this.profile.currentCombo,
|
|
4171
|
+
todayProducts,
|
|
4172
|
+
todayQualities
|
|
4173
|
+
};
|
|
4174
|
+
const completedBounties = updateBountyProgress(this.profile, bountyContext);
|
|
4175
|
+
if (completedBounties.length > 0) resolveDisasterEvent(this.profile, "complete_bounty");
|
|
4176
|
+
const newAchievements = checkAchievements(this.profile, this.collection, todayRecords);
|
|
4177
|
+
if (dropResult.isPityTriggered) {
|
|
4178
|
+
const pityAchievement = triggerSpecialAchievement(this.profile, "A302");
|
|
4179
|
+
if (pityAchievement) newAchievements.push(pityAchievement);
|
|
4180
|
+
}
|
|
4181
|
+
if (this.profile.consecutiveFailures >= 10) {
|
|
4182
|
+
const failAchievement = triggerSpecialAchievement(this.profile, "A303");
|
|
4183
|
+
if (failAchievement) newAchievements.push(failAchievement);
|
|
4184
|
+
}
|
|
4185
|
+
if (triggeredEvent?.id === "EV206" && this.profile.totalExp > 0) {
|
|
4186
|
+
const madnessAchievement = triggerSpecialAchievement(this.profile, "A408");
|
|
4187
|
+
if (madnessAchievement) newAchievements.push(madnessAchievement);
|
|
4188
|
+
}
|
|
4189
|
+
for (const achievement of newAchievements) this.profile.totalExp += achievement.expReward;
|
|
4190
|
+
applyLevelUp(this.profile);
|
|
4191
|
+
this.profile.consecutiveFailures = 0;
|
|
4192
|
+
const historyRecord = {
|
|
4193
|
+
timestamp: Date.now(),
|
|
4194
|
+
product,
|
|
4195
|
+
commandHash,
|
|
4196
|
+
success: true,
|
|
4197
|
+
expGained: expResult.totalExp,
|
|
4198
|
+
monsterId: dropResult.monster.id || void 0,
|
|
4199
|
+
isShiny: dropResult.isShiny || void 0,
|
|
4200
|
+
escaped: dropResult.escaped || void 0,
|
|
4201
|
+
encounterId: encounter?.immortalId,
|
|
4202
|
+
achievementIds: newAchievements.length > 0 ? newAchievements.map((a) => a.id) : void 0,
|
|
4203
|
+
eventId: triggeredEvent?.id,
|
|
4204
|
+
completedBountyIds: completedBounties.length > 0 ? completedBounties.map((b) => b.id) : void 0
|
|
4205
|
+
};
|
|
4206
|
+
this.history.records.push(historyRecord);
|
|
4207
|
+
this.save();
|
|
4208
|
+
return this.renderOutput(dropResult, expResult, levelUp, encounter, newAchievements, completedBounties, triggeredEvent ?? null, eventResults);
|
|
4209
|
+
}
|
|
4210
|
+
/**
|
|
4211
|
+
* 更新图鉴
|
|
4212
|
+
*/
|
|
4213
|
+
updateCollection(monsterId, isShiny, commandHash) {
|
|
4214
|
+
const existingEntry = this.collection.entries.find((e) => e.monsterId === monsterId && e.isShiny === isShiny);
|
|
4215
|
+
if (existingEntry) existingEntry.captureCount += 1;
|
|
4216
|
+
else {
|
|
4217
|
+
const newEntry = {
|
|
4218
|
+
monsterId,
|
|
4219
|
+
firstCapturedAt: Date.now(),
|
|
4220
|
+
captureCount: 1,
|
|
4221
|
+
isShiny
|
|
4222
|
+
};
|
|
4223
|
+
this.collection.entries.push(newEntry);
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
/**
|
|
4227
|
+
* 渲染完整输出(追加到 agent 回复末尾的 Markdown)
|
|
4228
|
+
*
|
|
4229
|
+
* v3: 紧凑化 — 降妖结果 + 附属事件合并为双列表格,大幅减少纵向空间
|
|
4230
|
+
*
|
|
4231
|
+
* 布局策略:
|
|
4232
|
+
* - 只有降妖结果时:直接单行输出
|
|
4233
|
+
* - 降妖 + 附属事件时:用表格 左列=降妖 | 右列=附属事件列表
|
|
4234
|
+
* - 只有附属事件时:紧凑列表输出
|
|
4235
|
+
*/
|
|
4236
|
+
renderOutput(dropResult, expResult, levelUp, encounter, newAchievements, completedBounties, triggeredEvent, eventResults) {
|
|
4237
|
+
const dropLine = dropResult.monster.id && !(this.profile.settings.muteNormalDrops && dropResult.monster.quality === "normal" && !dropResult.isNew && !dropResult.isShiny && !dropResult.escaped) ? renderDropResult(dropResult, expResult, this.collection).trim() : "";
|
|
4238
|
+
const sideEvents = [];
|
|
4239
|
+
if (levelUp) sideEvents.push(renderLevelUp(levelUp).trim());
|
|
4240
|
+
if (encounter) sideEvents.push(renderEncounter(encounter).trim());
|
|
4241
|
+
if (triggeredEvent) sideEvents.push(renderEventTrigger(triggeredEvent).trim());
|
|
4242
|
+
for (const result of eventResults) {
|
|
4243
|
+
if (result.event.category === "challenge") {
|
|
4244
|
+
if (result.outcome === "success" || result.outcome === "failure") sideEvents.push(renderChallengeResult(result.event, result.outcome === "success").trim());
|
|
4245
|
+
}
|
|
4246
|
+
if (result.event.category === "disaster" && result.outcome === "resolved") sideEvents.push(renderDisasterResolved(result.event).trim());
|
|
4247
|
+
}
|
|
4248
|
+
for (const bounty of completedBounties) sideEvents.push(renderBountyComplete(bounty).trim());
|
|
4249
|
+
if (newAchievements.length > 0) sideEvents.push(renderNewAchievements(newAchievements).trim());
|
|
4250
|
+
if (!dropLine && sideEvents.length === 0) return "";
|
|
4251
|
+
if (dropLine && sideEvents.length > 0) return [
|
|
4252
|
+
"",
|
|
4253
|
+
"---",
|
|
4254
|
+
`| 🗡️ 降妖 | 📋 事件 |`,
|
|
4255
|
+
`|---|---|`,
|
|
4256
|
+
`| ${dropLine} | ${sideEvents.join(" · ")} |`
|
|
4257
|
+
].join("\n");
|
|
4258
|
+
if (dropLine) return `\n---\n${dropLine}`;
|
|
4259
|
+
return `\n---\n${sideEvents.join("\n")}`;
|
|
4260
|
+
}
|
|
4261
|
+
/**
|
|
4262
|
+
* 持久化所有数据
|
|
4263
|
+
*/
|
|
4264
|
+
save() {
|
|
4265
|
+
saveProfile(this.profile);
|
|
4266
|
+
saveCollection(this.collection);
|
|
4267
|
+
saveHistory(this.history);
|
|
4268
|
+
}
|
|
4269
|
+
};
|
|
4270
|
+
//#endregion
|
|
4271
|
+
export { GamificationEngine, isGamificationCommand };
|