@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.
Files changed (154) hide show
  1. package/CHANGELOG.md +686 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +181 -0
  4. package/README.md +221 -0
  5. package/bin/dingtalk-connector.js +858 -0
  6. package/bin/wizard-config.mjs +110 -0
  7. package/dist/accounts-BAzdqkAV.mjs +268 -0
  8. package/dist/accounts-BQptOmgB.mjs +2 -0
  9. package/dist/chunk-upload-BBQgGtcZ.mjs +193 -0
  10. package/dist/chunk-upload-DaLXXZH3.mjs +2 -0
  11. package/dist/common-C8pYKU_y.mjs +2 -0
  12. package/dist/common-Dt9n6fQN.mjs +101 -0
  13. package/dist/connection-DHHFFNQJ.mjs +423 -0
  14. package/dist/entry-bundled.d.mts +16 -0
  15. package/dist/entry-bundled.mjs +31 -0
  16. package/dist/game-xiyou-CqHt-6Q1.mjs +4271 -0
  17. package/dist/gateway-methods-C4tcgI7P.mjs +771 -0
  18. package/dist/gateway-methods-Ci31A3vg.mjs +2 -0
  19. package/dist/http-client-CpnJHB89.mjs +2 -0
  20. package/dist/http-client-DFWZgO1n.mjs +33 -0
  21. package/dist/index.d.mts +193 -0
  22. package/dist/index.mjs +45 -0
  23. package/dist/logger-BmJkQkm1.mjs +2 -0
  24. package/dist/logger-mZ9OSbmD.mjs +58 -0
  25. package/dist/media-C_SVin7s.mjs +2 -0
  26. package/dist/media-cz72EVS3.mjs +509 -0
  27. package/dist/message-handler-DESzFFDc.mjs +1971 -0
  28. package/dist/messaging-B6l1sRvX.mjs +1044 -0
  29. package/dist/runtime-DUgpo5zC.mjs +1422 -0
  30. package/dist/session-DJ4jYqPv.mjs +114 -0
  31. package/dist/utils-Bjh4r_qS.mjs +4 -0
  32. package/dist/utils-CIfI_3Jh.mjs +63 -0
  33. package/dist/utils-legacy-CALCPP1t.mjs +230 -0
  34. package/dist/utils-legacy-CFYDBM4r.mjs +3 -0
  35. package/docs/DEAP_AGENT_GUIDE.en.md +115 -0
  36. package/docs/DEAP_AGENT_GUIDE.md +115 -0
  37. package/docs/DINGTALK_MANUAL_SETUP.md +50 -0
  38. package/docs/MULTI_AGENT_SETUP.md +306 -0
  39. package/docs/RELEASE_NOTES_V0.7.10.md +40 -0
  40. package/docs/RELEASE_NOTES_V0.7.2.md +143 -0
  41. package/docs/RELEASE_NOTES_V0.7.3.md +149 -0
  42. package/docs/RELEASE_NOTES_V0.7.4.md +206 -0
  43. package/docs/RELEASE_NOTES_V0.7.5.md +267 -0
  44. package/docs/RELEASE_NOTES_V0.7.6.md +219 -0
  45. package/docs/RELEASE_NOTES_V0.7.7.md +122 -0
  46. package/docs/RELEASE_NOTES_V0.7.8.md +101 -0
  47. package/docs/RELEASE_NOTES_V0.7.9.md +65 -0
  48. package/docs/RELEASE_NOTES_V0.8.0.md +53 -0
  49. package/docs/RELEASE_NOTES_V0.8.1.md +47 -0
  50. package/docs/RELEASE_NOTES_V0.8.10.md +49 -0
  51. package/docs/RELEASE_NOTES_V0.8.11.md +51 -0
  52. package/docs/RELEASE_NOTES_V0.8.12.md +63 -0
  53. package/docs/RELEASE_NOTES_V0.8.13-beta.0.md +69 -0
  54. package/docs/RELEASE_NOTES_V0.8.13.md +62 -0
  55. package/docs/RELEASE_NOTES_V0.8.14.md +86 -0
  56. package/docs/RELEASE_NOTES_V0.8.16.md +40 -0
  57. package/docs/RELEASE_NOTES_V0.8.17.md +87 -0
  58. package/docs/RELEASE_NOTES_V0.8.18.md +64 -0
  59. package/docs/RELEASE_NOTES_V0.8.19.md +62 -0
  60. package/docs/RELEASE_NOTES_V0.8.2.md +55 -0
  61. package/docs/RELEASE_NOTES_V0.8.20.md +49 -0
  62. package/docs/RELEASE_NOTES_V0.8.3.md +63 -0
  63. package/docs/RELEASE_NOTES_V0.8.4.md +45 -0
  64. package/docs/RELEASE_NOTES_V0.8.7.md +49 -0
  65. package/docs/RELEASE_NOTES_V0.8.8.md +63 -0
  66. package/docs/RELEASE_NOTES_V0.8.9.md +81 -0
  67. package/docs/RELEASE_NOTES_v0.7.0.md +142 -0
  68. package/docs/RELEASE_NOTES_v0.7.1.md +74 -0
  69. package/docs/TROUBLESHOOTING.md +122 -0
  70. package/index.ts +77 -0
  71. package/openclaw.plugin.json +551 -0
  72. package/package.json +147 -0
  73. package/skills/dingtalk-channel-rules/SKILL.md +91 -0
  74. package/skills/dingtalk-troubleshoot/SKILL.md +93 -0
  75. package/skills/dws-cli/SKILL.md +129 -0
  76. package/skills/dws-cli/references/error-codes.md +95 -0
  77. package/skills/dws-cli/references/field-rules.md +105 -0
  78. package/skills/dws-cli/references/global-reference.md +104 -0
  79. package/skills/dws-cli/references/intent-guide.md +114 -0
  80. package/skills/dws-cli/references/products/aitable.md +452 -0
  81. package/skills/dws-cli/references/products/attendance.md +93 -0
  82. package/skills/dws-cli/references/products/calendar.md +217 -0
  83. package/skills/dws-cli/references/products/chat.md +292 -0
  84. package/skills/dws-cli/references/products/contact.md +108 -0
  85. package/skills/dws-cli/references/products/ding.md +57 -0
  86. package/skills/dws-cli/references/products/report.md +162 -0
  87. package/skills/dws-cli/references/products/simple.md +128 -0
  88. package/skills/dws-cli/references/products/todo.md +138 -0
  89. package/skills/dws-cli/references/products/workbench.md +39 -0
  90. package/skills/dws-cli/references/recovery-guide.md +94 -0
  91. package/src/channel.ts +588 -0
  92. package/src/config/accounts.ts +242 -0
  93. package/src/config/schema.ts +180 -0
  94. package/src/core/connection.ts +741 -0
  95. package/src/core/message-handler.ts +1788 -0
  96. package/src/core/provider.ts +111 -0
  97. package/src/core/state.ts +54 -0
  98. package/src/device-auth-config.ts +14 -0
  99. package/src/device-auth.ts +197 -0
  100. package/src/directory.ts +95 -0
  101. package/src/docs.ts +293 -0
  102. package/src/game-xiyou/achievement-engine.ts +252 -0
  103. package/src/game-xiyou/bounty-system.ts +315 -0
  104. package/src/game-xiyou/commands.ts +223 -0
  105. package/src/game-xiyou/drop-engine.ts +241 -0
  106. package/src/game-xiyou/encounter-system.ts +135 -0
  107. package/src/game-xiyou/escape-engine.ts +164 -0
  108. package/src/game-xiyou/exp-calculator.ts +139 -0
  109. package/src/game-xiyou/index.ts +479 -0
  110. package/src/game-xiyou/level-system.ts +91 -0
  111. package/src/game-xiyou/monster-pool.ts +180 -0
  112. package/src/game-xiyou/pity-counter.ts +114 -0
  113. package/src/game-xiyou/random-event-engine.ts +648 -0
  114. package/src/game-xiyou/renderer.ts +679 -0
  115. package/src/game-xiyou/storage.ts +218 -0
  116. package/src/game-xiyou/treasure-system.ts +105 -0
  117. package/src/game-xiyou/types.ts +582 -0
  118. package/src/game-xiyou/uid-resolver.ts +49 -0
  119. package/src/gateway-methods.ts +740 -0
  120. package/src/onboarding.ts +553 -0
  121. package/src/policy.ts +32 -0
  122. package/src/probe.ts +210 -0
  123. package/src/reply-dispatcher.ts +874 -0
  124. package/src/runtime.ts +32 -0
  125. package/src/sdk/helpers.ts +322 -0
  126. package/src/sdk/types.ts +519 -0
  127. package/src/secret-input.ts +19 -0
  128. package/src/services/media/audio.ts +54 -0
  129. package/src/services/media/chunk-upload.ts +296 -0
  130. package/src/services/media/common.ts +155 -0
  131. package/src/services/media/file.ts +75 -0
  132. package/src/services/media/image.ts +81 -0
  133. package/src/services/media/index.ts +10 -0
  134. package/src/services/media/video.ts +162 -0
  135. package/src/services/media.ts +1143 -0
  136. package/src/services/messaging/card.ts +604 -0
  137. package/src/services/messaging/index.ts +18 -0
  138. package/src/services/messaging/mentions.ts +267 -0
  139. package/src/services/messaging/send.ts +141 -0
  140. package/src/services/messaging.ts +1191 -0
  141. package/src/services/reply-markers.ts +55 -0
  142. package/src/targets.ts +45 -0
  143. package/src/types/index.ts +59 -0
  144. package/src/types/pdf-parse.d.ts +3 -0
  145. package/src/utils/agent.ts +63 -0
  146. package/src/utils/async.ts +51 -0
  147. package/src/utils/constants.ts +27 -0
  148. package/src/utils/http-client.ts +38 -0
  149. package/src/utils/index.ts +8 -0
  150. package/src/utils/logger.ts +78 -0
  151. package/src/utils/session.ts +147 -0
  152. package/src/utils/token.ts +93 -0
  153. package/src/utils/utils-legacy.ts +454 -0
  154. package/tsconfig.json +20 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * 概率掉落引擎(核心)
3
+ *
4
+ * 每次 dws CLI 成功执行后触发一次"降妖"事件。
5
+ * 流程:保底判定 → 随机品质 → 等级门槛降级 → 加权选妖 → 闪光判定 → 更新计数器
6
+ */
7
+
8
+ import { randomBytes } from 'crypto';
9
+ import type {
10
+ MonsterQuality, Monster, UserProfile, DropResult,
11
+ Buff, UserCollection, EscapeModifier,
12
+ } from './types.ts';
13
+ import {
14
+ DROP_RATES, QUALITY_LEVEL_GATES, QUALITY_ORDER,
15
+ } from './types.ts';
16
+ import { checkPityTrigger, updatePityCounters, getSoftPityBonuses } from './pity-counter.ts';
17
+ import { getMonstersByQuality, getWeeklyUpMonster, weightedRandomSelect } from './monster-pool.ts';
18
+ import { getQualityBoost, getQualityCap, isDropSuppressed } from './random-event-engine.ts';
19
+
20
+ /**
21
+ * 使用 crypto.randomBytes 生成安全随机数
22
+ */
23
+ function cryptoRandom(): number {
24
+ const buffer = randomBytes(4);
25
+ return buffer.readUInt32BE(0) / 0xFFFFFFFF;
26
+ }
27
+
28
+ /**
29
+ * 根据随机数和用户等级判定掉落品质
30
+ *
31
+ * v2: 集成软保底概率加成
32
+ */
33
+ function resolveQuality(roll: number, level: number, buffs: Buff[], softPityBonuses: Partial<Record<MonsterQuality, number>>): MonsterQuality {
34
+ // 计算 buff 加成后的掉落率
35
+ const rateBonus: Partial<Record<MonsterQuality, number>> = {};
36
+ for (const buff of buffs) {
37
+ switch (buff.effect) {
38
+ case 'epicRateBonus':
39
+ rateBonus.epic = (rateBonus.epic ?? 0) + buff.value;
40
+ break;
41
+ case 'rareRateBonus':
42
+ rateBonus.rare = (rateBonus.rare ?? 0) + buff.value;
43
+ break;
44
+ case 'legendaryRateBonus':
45
+ rateBonus.legendary = (rateBonus.legendary ?? 0) + buff.value;
46
+ break;
47
+ case 'shinyRateBonus':
48
+ rateBonus.shiny = (rateBonus.shiny ?? 0) + buff.value;
49
+ break;
50
+ case 'allRateBonus':
51
+ for (const quality of QUALITY_ORDER) {
52
+ if (quality !== 'normal' && quality !== 'fine') {
53
+ rateBonus[quality] = (rateBonus[quality] ?? 0) + buff.value;
54
+ }
55
+ }
56
+ break;
57
+ }
58
+ }
59
+
60
+ // 合并软保底加成
61
+ for (const [quality, bonus] of Object.entries(softPityBonuses)) {
62
+ const qualityKey = quality as MonsterQuality;
63
+ rateBonus[qualityKey] = (rateBonus[qualityKey] ?? 0) + (bonus ?? 0);
64
+ }
65
+
66
+ // 从高品质到低品质依次判定
67
+ let cumulative = 0;
68
+ const qualitiesHighToLow: MonsterQuality[] = ['shiny', 'legendary', 'epic', 'rare', 'fine', 'normal'];
69
+
70
+ for (const quality of qualitiesHighToLow) {
71
+ const baseRate = DROP_RATES[quality];
72
+ const bonus = rateBonus[quality] ?? 0;
73
+ cumulative += baseRate + bonus;
74
+
75
+ if (roll < cumulative) {
76
+ return quality;
77
+ }
78
+ }
79
+
80
+ return 'normal';
81
+ }
82
+
83
+ /**
84
+ * 应用等级门槛降级
85
+ */
86
+ function applyLevelGate(quality: MonsterQuality, level: number): MonsterQuality {
87
+ const gate = QUALITY_LEVEL_GATES[quality];
88
+ if (gate !== undefined && level < gate) {
89
+ // 降级到最高可用品质
90
+ const qualityIndex = QUALITY_ORDER.indexOf(quality);
91
+ for (let i = qualityIndex - 1; i >= 0; i--) {
92
+ const lowerQuality = QUALITY_ORDER[i];
93
+ const lowerGate = QUALITY_LEVEL_GATES[lowerQuality];
94
+ if (lowerGate === undefined || level >= lowerGate) {
95
+ return lowerQuality;
96
+ }
97
+ }
98
+ return 'normal';
99
+ }
100
+ return quality;
101
+ }
102
+
103
+ /**
104
+ * 检查玲珑宝塔 buff:普通掉落有概率升级为精良
105
+ */
106
+ function checkNormalUpgrade(quality: MonsterQuality, buffs: Buff[]): MonsterQuality {
107
+ if (quality !== 'normal') return quality;
108
+
109
+ const upgradeChance = buffs
110
+ .filter(b => b.effect === 'normalUpgrade')
111
+ .reduce((sum, b) => sum + b.value, 0);
112
+
113
+ if (upgradeChance > 0 && cryptoRandom() < upgradeChance) {
114
+ return 'fine';
115
+ }
116
+ return quality;
117
+ }
118
+
119
+ /**
120
+ * 将品质提升一级(用于月光宝盒事件)
121
+ */
122
+ function boostQuality(quality: MonsterQuality): MonsterQuality {
123
+ const index = QUALITY_ORDER.indexOf(quality);
124
+ if (index < 0 || index >= QUALITY_ORDER.length - 1) return quality;
125
+ return QUALITY_ORDER[index + 1];
126
+ }
127
+
128
+ /**
129
+ * 执行一次掉落
130
+ *
131
+ * v2: 集成软保底概率加成、事件品质提升/上限、禁止掉落检查
132
+ */
133
+ export function executeDrop(
134
+ product: string,
135
+ profile: UserProfile,
136
+ collection: UserCollection
137
+ ): DropResult {
138
+ const emptyResult: DropResult = {
139
+ monster: { id: '', name: '', emoji: '', quality: 'normal', origin: '', relatedProduct: null, captureQuote: '' },
140
+ isShiny: false, isNew: false, expGained: 0,
141
+ isPityTriggered: false, isUpMonster: false,
142
+ escaped: false, escapeRate: 0, escapeModifiers: [],
143
+ };
144
+
145
+ // v2: 检查是否被事件禁止掉落(五行山镇压)
146
+ if (isDropSuppressed(profile)) {
147
+ return emptyResult;
148
+ }
149
+
150
+ const pity = profile.pityCounters;
151
+ let isPityTriggered = false;
152
+ let quality: MonsterQuality;
153
+
154
+ // 1. 保底判定(优先级最高)
155
+ const pityQuality = checkPityTrigger(pity);
156
+ if (pityQuality) {
157
+ quality = pityQuality;
158
+ isPityTriggered = true;
159
+ } else {
160
+ // 2. 随机品质判定(v2: 含软保底加成)
161
+ const roll = cryptoRandom();
162
+ const softPityBonuses = getSoftPityBonuses(pity);
163
+ quality = resolveQuality(roll, profile.level, profile.buffs, softPityBonuses);
164
+ }
165
+
166
+ // 3. 等级门槛降级
167
+ quality = applyLevelGate(quality, profile.level);
168
+
169
+ // 4. 玲珑宝塔 buff 检查
170
+ quality = checkNormalUpgrade(quality, profile.buffs);
171
+
172
+ // v2: 事件品质上限(妖雾弥漫)
173
+ const qualityCap = getQualityCap(profile);
174
+ if (qualityCap) {
175
+ const capIndex = QUALITY_ORDER.indexOf(qualityCap);
176
+ const currentIndex = QUALITY_ORDER.indexOf(quality);
177
+ if (currentIndex > capIndex) {
178
+ quality = qualityCap;
179
+ }
180
+ }
181
+
182
+ // v2: 事件品质提升(月光宝盒)
183
+ const qualityBoostLevel = getQualityBoost(profile);
184
+ if (qualityBoostLevel > 0) {
185
+ for (let i = 0; i < qualityBoostLevel; i++) {
186
+ quality = boostQuality(quality);
187
+ }
188
+ quality = applyLevelGate(quality, profile.level);
189
+ }
190
+
191
+ // 5. 闪光判定(独立于品质判定)
192
+ let isShiny = false;
193
+ if (quality === 'shiny') {
194
+ isShiny = true;
195
+ const availableQualities = QUALITY_ORDER.filter(q => {
196
+ if (q === 'shiny') return false;
197
+ const gate = QUALITY_LEVEL_GATES[q];
198
+ return gate === undefined || profile.level >= gate;
199
+ });
200
+ quality = availableQualities[Math.floor(cryptoRandom() * availableQualities.length)] || 'normal';
201
+ } else if (profile.level >= 9) {
202
+ const shinyBonus = profile.buffs
203
+ .filter(b => b.effect === 'shinyRateBonus')
204
+ .reduce((sum, b) => sum + b.value, 0);
205
+ if (cryptoRandom() < 0.001 + shinyBonus) {
206
+ isShiny = true;
207
+ }
208
+ }
209
+
210
+ // 6. 在品质池中选择妖怪
211
+ const pool = getMonstersByQuality(quality);
212
+ const upMonster = getWeeklyUpMonster();
213
+ let monster: Monster;
214
+
215
+ if (pool.length === 0) {
216
+ const normalPool = getMonstersByQuality('normal');
217
+ monster = weightedRandomSelect(normalPool, product, null, cryptoRandom());
218
+ } else {
219
+ monster = weightedRandomSelect(pool, product, upMonster, cryptoRandom());
220
+ }
221
+
222
+ // 7. 判定是否为新妖怪
223
+ const isNew = !collection.entries.some(e =>
224
+ e.monsterId === monster.id && (isShiny ? e.isShiny : !e.isShiny)
225
+ );
226
+
227
+ // 8. 更新保底计数器
228
+ updatePityCounters(pity, quality, isShiny);
229
+
230
+ return {
231
+ monster,
232
+ isShiny,
233
+ isNew,
234
+ expGained: 0,
235
+ isPityTriggered,
236
+ isUpMonster: upMonster?.id === monster.id,
237
+ escaped: false,
238
+ escapeRate: 0,
239
+ escapeModifiers: [],
240
+ };
241
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * 神仙机缘系统
3
+ *
4
+ * 等级 ≥ 3(修行者)后解锁。
5
+ * 每次 dws CLI 成功执行,除了掉落妖怪外,还有独立概率触发"神仙机缘"事件。
6
+ */
7
+
8
+ import { randomBytes } from 'crypto';
9
+ import type { Encounter, EncounterType, Immortal, UserProfile, Buff, Treasure } from './types.ts';
10
+ import { ENCOUNTER_RATES } from './types.ts';
11
+ import { TREASURES_DATA } from './treasure-system.ts';
12
+
13
+ /**
14
+ * 神仙数据(内联)
15
+ */
16
+ const allImmortals: Immortal[] = [
17
+ { id: "G001", name: "菩提祖师", guidanceQuote: "悟性不错,但还差一个筋斗云的距离。", treasureId: "jintouyun", apprenticeBuff: { id: "putizu-apprentice", source: "apprentice", effect: "expMultiplier", value: 1.2 } },
18
+ { id: "G002", name: "观音菩萨", guidanceQuote: "救苦救难,先把待办清了。", treasureId: "jingping", apprenticeBuff: { id: "guanyin-apprentice", source: "apprentice", effect: "pityReduction", value: 0.5 } },
19
+ { id: "G003", name: "太上老君", guidanceQuote: "八卦炉里炼出来的,都是好东西。", treasureId: "zijinhulu", apprenticeBuff: { id: "laojun-apprentice", source: "apprentice", effect: "epicRateBonus", value: 0.01 } },
20
+ { id: "G004", name: "太白金星", guidanceQuote: "玉帝有旨,你的 KPI 不错。", treasureId: "pantao", apprenticeBuff: { id: "taibai-apprentice", source: "apprentice", effect: "signInMultiplier", value: 1.5 } },
21
+ { id: "G005", name: "哪吒三太子", guidanceQuote: "风火轮转得快,但别忘了刹车。", treasureId: "qiankunquan", apprenticeBuff: { id: "nezha-apprentice", source: "apprentice", effect: "comboLimitBonus", value: 1 } },
22
+ { id: "G006", name: "二郎真君", guidanceQuote: "第三只眼看穿一切 bug。", treasureId: "sanjiandao", apprenticeBuff: { id: "erlang-apprentice", source: "apprentice", effect: "rareRateBonus", value: 0.02 } },
23
+ { id: "G007", name: "托塔天王", guidanceQuote: "塔在手,妖魔走。", treasureId: "linglongta", apprenticeBuff: { id: "tuota-apprentice", source: "apprentice", effect: "allRateBonus", value: 0.005 } },
24
+ { id: "G008", name: "镇元大仙", guidanceQuote: "人参果,三千年一开花,三千年一结果。", treasureId: "renshenguo", apprenticeBuff: { id: "zhenyuan-apprentice", source: "apprentice", effect: "legendaryRateBonus", value: 0.003 } },
25
+ ];
26
+
27
+ function cryptoRandom(): number {
28
+ const buffer = randomBytes(4);
29
+ return buffer.readUInt32BE(0) / 0xFFFFFFFF;
30
+ }
31
+
32
+ /**
33
+ * 根据 ID 查找神仙
34
+ */
35
+ export function getImmortalById(immortalId: string): Immortal | undefined {
36
+ return allImmortals.find(i => i.id === immortalId);
37
+ }
38
+
39
+ /**
40
+ * 获取法宝名称
41
+ */
42
+ export function getTreasureName(treasureId: string): string {
43
+ const treasure = TREASURES_DATA.find(t => t.id === treasureId);
44
+ return treasure?.name ?? treasureId;
45
+ }
46
+
47
+ /**
48
+ * 获取法宝描述
49
+ */
50
+ export function getTreasureDescription(treasureId: string): string {
51
+ const treasure = TREASURES_DATA.find(t => t.id === treasureId);
52
+ return treasure?.description ?? '';
53
+ }
54
+
55
+ /**
56
+ * 检查是否触发机缘事件
57
+ *
58
+ * @returns 机缘事件,或 null(未触发)
59
+ */
60
+ export function checkEncounter(profile: UserProfile): Encounter | null {
61
+ // 等级 < 3 不触发
62
+ if (profile.level < 3) {
63
+ return null;
64
+ }
65
+
66
+ // 按概率从高到低判定
67
+ const encounterTypes: EncounterType[] = ['apprentice', 'treasure', 'guidance'];
68
+
69
+ for (const encounterType of encounterTypes) {
70
+ const rate = ENCOUNTER_RATES[encounterType];
71
+ if (cryptoRandom() < rate) {
72
+ // 随机选择一位神仙
73
+ const immortal = allImmortals[Math.floor(cryptoRandom() * allImmortals.length)];
74
+
75
+ const encounter: Encounter = {
76
+ immortalId: immortal.id,
77
+ type: encounterType,
78
+ occurredAt: Date.now(),
79
+ };
80
+
81
+ if (encounterType === 'treasure') {
82
+ encounter.treasureId = immortal.treasureId;
83
+ }
84
+
85
+ if (encounterType === 'apprentice') {
86
+ encounter.buffId = immortal.apprenticeBuff.id;
87
+ }
88
+
89
+ return encounter;
90
+ }
91
+ }
92
+
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * 应用机缘效果到用户档案
98
+ */
99
+ export function applyEncounterEffects(profile: UserProfile, encounter: Encounter): void {
100
+ // 记录机缘
101
+ profile.encounters.push(encounter);
102
+
103
+ const immortal = getImmortalById(encounter.immortalId);
104
+ if (!immortal) return;
105
+
106
+ if (encounter.type === 'treasure' && encounter.treasureId) {
107
+ // 赐宝:添加法宝到背包
108
+ if (!profile.treasures.includes(encounter.treasureId)) {
109
+ profile.treasures.push(encounter.treasureId);
110
+ }
111
+
112
+ // 如果法宝有永久 buff 效果,添加到 buffs
113
+ const treasure = TREASURES_DATA.find(t => t.id === encounter.treasureId);
114
+ if (treasure && !treasure.consumable) {
115
+ const existingBuff = profile.buffs.find(b => b.id === treasure.id);
116
+ if (!existingBuff) {
117
+ profile.buffs.push({
118
+ id: treasure.id,
119
+ source: 'treasure',
120
+ effect: treasure.effect,
121
+ value: treasure.value,
122
+ });
123
+ }
124
+ }
125
+ }
126
+
127
+ if (encounter.type === 'apprentice') {
128
+ // 收徒:添加永久 buff
129
+ const buff: Buff = { ...immortal.apprenticeBuff };
130
+ const existingBuff = profile.buffs.find(b => b.id === buff.id);
131
+ if (!existingBuff) {
132
+ profile.buffs.push(buff);
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * 妖怪逃跑引擎 (v2)
3
+ *
4
+ * 掉落不再等于收服。品质越高的妖怪越难降服。
5
+ * 逃跑率受连击、法宝、buff、产品关联等因素修正,最低不低于 5%。
6
+ * 保底触发的妖怪 100% 收服,不会逃跑。
7
+ */
8
+
9
+ import { randomBytes } from 'crypto';
10
+ import type {
11
+ MonsterQuality, Monster, UserProfile, DropResult, EscapeModifier,
12
+ } from './types.ts';
13
+ import { BASE_ESCAPE_RATES, MIN_ESCAPE_RATE } from './types.ts';
14
+
15
+ function cryptoRandom(): number {
16
+ const buffer = randomBytes(4);
17
+ return buffer.readUInt32BE(0) / 0xFFFFFFFF;
18
+ }
19
+
20
+ /**
21
+ * 计算逃跑率修正因子列表
22
+ */
23
+ function calculateEscapeModifiers(
24
+ monster: Monster,
25
+ profile: UserProfile,
26
+ product: string,
27
+ isBountyTarget: boolean
28
+ ): EscapeModifier[] {
29
+ const modifiers: EscapeModifier[] = [];
30
+
31
+ // 连击加成:≥5 时 -10%,≥10 时 -20%
32
+ if (profile.currentCombo >= 10) {
33
+ modifiers.push({ source: 'combo', value: 0.20, description: `连击 ×${profile.currentCombo} 加成` });
34
+ } else if (profile.currentCombo >= 5) {
35
+ modifiers.push({ source: 'combo', value: 0.10, description: `连击 ×${profile.currentCombo} 加成` });
36
+ }
37
+
38
+ // 法宝"玲珑宝塔":逃跑率 -15%
39
+ if (profile.buffs.some(b => b.id === 'linglongta')) {
40
+ modifiers.push({ source: 'treasure', value: 0.15, description: '玲珑宝塔' });
41
+ }
42
+
43
+ // 法宝"定海神针":逃跑率 -10%
44
+ if (profile.buffs.some(b => b.id === 'dinghaishenzhen')) {
45
+ modifiers.push({ source: 'treasure', value: 0.10, description: '定海神针' });
46
+ }
47
+
48
+ // 师徒 buff(二郎真君):逃跑率 -8%
49
+ if (profile.buffs.some(b => b.id === 'erlang-apprentice')) {
50
+ modifiers.push({ source: 'buff', value: 0.08, description: '二郎真君师徒' });
51
+ }
52
+
53
+ // 产品关联匹配:妖怪关联产品与当前命令产品一致时 -5%
54
+ if (monster.relatedProduct && monster.relatedProduct === product) {
55
+ modifiers.push({ source: 'product', value: 0.05, description: '产品关联匹配' });
56
+ }
57
+
58
+ // 悬赏令加成:悬赏目标妖怪逃跑率 -10%
59
+ if (isBountyTarget) {
60
+ modifiers.push({ source: 'bounty', value: 0.10, description: '悬赏令加成' });
61
+ }
62
+
63
+ // 活跃事件修正:逃跑率全局修正
64
+ for (const event of profile.activeEvents.currentEvents) {
65
+ if (event.effect.type === 'escape_rate_mod') {
66
+ const eventValue = Math.abs(event.effect.value);
67
+ if (event.effect.value < 0) {
68
+ modifiers.push({ source: 'event', value: eventValue, description: `${event.name} 减益` });
69
+ }
70
+ // 正值(增加逃跑率)在 calculateFinalEscapeRate 中处理
71
+ }
72
+ }
73
+
74
+ return modifiers;
75
+ }
76
+
77
+ /**
78
+ * 计算最终逃跑率
79
+ */
80
+ function calculateFinalEscapeRate(
81
+ quality: MonsterQuality,
82
+ isShiny: boolean,
83
+ modifiers: EscapeModifier[],
84
+ profile: UserProfile,
85
+ monsterId: string
86
+ ): number {
87
+ // 闪光妖怪使用闪光逃跑率
88
+ const baseRate = isShiny ? BASE_ESCAPE_RATES.shiny : BASE_ESCAPE_RATES[quality];
89
+
90
+ if (baseRate === 0) return 0; // 普通妖怪不逃跑
91
+
92
+ // 减去所有修正因子
93
+ const totalReduction = modifiers.reduce((sum, m) => sum + m.value, 0);
94
+
95
+ // 加上事件增加的逃跑率
96
+ let eventIncrease = 0;
97
+ for (const event of profile.activeEvents.currentEvents) {
98
+ if (event.effect.type === 'escape_rate_mod' && event.effect.value > 0) {
99
+ eventIncrease += event.effect.value;
100
+ }
101
+ }
102
+
103
+ // "冤家路窄":同一只妖怪连续逃跑 3 次,第 4 次逃跑率减半
104
+ const consecutiveEscapes = profile.escapeHistory[monsterId] ?? 0;
105
+ let consecutiveReduction = 0;
106
+ if (consecutiveEscapes >= 3) {
107
+ consecutiveReduction = (baseRate + eventIncrease - totalReduction) * 0.5;
108
+ }
109
+
110
+ const finalRate = baseRate + eventIncrease - totalReduction - consecutiveReduction;
111
+ return Math.max(MIN_ESCAPE_RATE, Math.min(finalRate, 0.95));
112
+ }
113
+
114
+ /**
115
+ * 判定妖怪是否逃跑,并更新追踪数据
116
+ *
117
+ * @returns 更新后的 DropResult(设置 escaped / escapeRate / escapeModifiers)
118
+ */
119
+ export function resolveEscape(
120
+ dropResult: DropResult,
121
+ profile: UserProfile,
122
+ product: string,
123
+ isBountyTarget: boolean = false
124
+ ): DropResult {
125
+ const { monster, isShiny, isPityTriggered } = dropResult;
126
+
127
+ // 保底触发的妖怪 100% 收服
128
+ if (isPityTriggered) {
129
+ return { ...dropResult, escaped: false, escapeRate: 0, escapeModifiers: [] };
130
+ }
131
+
132
+ // 普通妖怪不逃跑
133
+ if (monster.quality === 'normal' && !isShiny) {
134
+ return { ...dropResult, escaped: false, escapeRate: 0, escapeModifiers: [] };
135
+ }
136
+
137
+ const modifiers = calculateEscapeModifiers(monster, profile, product, isBountyTarget);
138
+ const escapeRate = calculateFinalEscapeRate(
139
+ monster.quality, isShiny, modifiers, profile, monster.id
140
+ );
141
+
142
+ const escaped = cryptoRandom() < escapeRate;
143
+
144
+ // 更新逃跑追踪
145
+ if (escaped) {
146
+ profile.escapeHistory[monster.id] = (profile.escapeHistory[monster.id] ?? 0) + 1;
147
+ profile.totalEscapes += 1;
148
+ } else {
149
+ // 收服成功,重置该妖怪的连续逃跑计数
150
+ profile.escapeHistory[monster.id] = 0;
151
+ }
152
+
153
+ return { ...dropResult, escaped, escapeRate, escapeModifiers: modifiers };
154
+ }
155
+
156
+ /**
157
+ * 检查"冤家路窄"效果:逃跑后的妖怪在接下来 10 次掉落内再次遇到的概率 ×2
158
+ * 返回应该额外加权的妖怪 ID 列表
159
+ */
160
+ export function getEscapedMonsterBoostIds(profile: UserProfile): string[] {
161
+ return Object.entries(profile.escapeHistory)
162
+ .filter(([_, count]) => count > 0)
163
+ .map(([monsterId]) => monsterId);
164
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * 修行值计算引擎
3
+ *
4
+ * 计算公式:总修行值 = (基础值 × 连击倍率 × 首次使用倍率 + 签到奖励) × buff 倍率
5
+ */
6
+
7
+ import type { UserProfile, ExpResult, Buff } from './types.ts';
8
+ import { PRODUCT_BASE_EXP, COMBO_MULTIPLIERS } from './types.ts';
9
+
10
+ /**
11
+ * 获取产品的基础修行值
12
+ */
13
+ function getBaseExp(product: string): number {
14
+ return PRODUCT_BASE_EXP[product] ?? 2;
15
+ }
16
+
17
+ /**
18
+ * 计算连击加成倍率
19
+ */
20
+ function getComboMultiplier(comboCount: number, buffs: Buff[]): number {
21
+ // 检查是否有连击上限提升 buff
22
+ const comboLimitBonus = buffs
23
+ .filter(b => b.effect === 'comboLimitBonus')
24
+ .reduce((sum, b) => sum + b.value, 0);
25
+
26
+ // 检查是否有连击加成 buff
27
+ const comboBonusFromBuffs = buffs
28
+ .filter(b => b.effect === 'comboBonus')
29
+ .reduce((sum, b) => sum + b.value, 0);
30
+
31
+ // 基础连击倍率
32
+ let baseMultiplier = 1.0;
33
+ for (const { threshold, multiplier } of COMBO_MULTIPLIERS) {
34
+ if (comboCount >= threshold) {
35
+ baseMultiplier = multiplier;
36
+ break;
37
+ }
38
+ }
39
+
40
+ // 如果有连击上限提升,最高倍率可以更高
41
+ if (comboLimitBonus > 0 && comboCount >= 10) {
42
+ baseMultiplier = Math.min(baseMultiplier + comboLimitBonus, 5.0);
43
+ }
44
+
45
+ return baseMultiplier + comboBonusFromBuffs;
46
+ }
47
+
48
+ /**
49
+ * 计算首次使用加成
50
+ */
51
+ function getFirstUseMultiplier(product: string, productUsage: Record<string, number>): number {
52
+ const usage = productUsage[product] ?? 0;
53
+ return usage === 0 ? 5 : 1;
54
+ }
55
+
56
+ /**
57
+ * 计算签到奖励
58
+ */
59
+ function getSignInBonus(profile: UserProfile): number {
60
+ const today = new Date().toISOString().slice(0, 10);
61
+ if (profile.lastSignInDate === today) {
62
+ return 0; // 今天已签到
63
+ }
64
+ return 10; // 每日首次 +10
65
+ }
66
+
67
+ /**
68
+ * 计算连续签到奖励
69
+ */
70
+ function getConsecutiveSignInBonus(profile: UserProfile, buffs: Buff[]): number {
71
+ const today = new Date().toISOString().slice(0, 10);
72
+ if (profile.lastSignInDate === today) {
73
+ return 0; // 今天已签到,不重复计算
74
+ }
75
+
76
+ const signInMultiplier = buffs
77
+ .filter(b => b.effect === 'signInMultiplier')
78
+ .reduce((max, b) => Math.max(max, b.value), 1);
79
+
80
+ const consecutiveDays = profile.consecutiveSignInDays + 1;
81
+ const bonus = Math.min(consecutiveDays * 2, 30);
82
+ return Math.floor(bonus * signInMultiplier);
83
+ }
84
+
85
+ /**
86
+ * 计算 buff 总倍率
87
+ */
88
+ function getBuffMultiplier(buffs: Buff[]): number {
89
+ return buffs
90
+ .filter(b => b.effect === 'expMultiplier')
91
+ .reduce((multiplier, b) => multiplier * b.value, 1.0);
92
+ }
93
+
94
+ /**
95
+ * 计算一次操作获得的总修行值
96
+ */
97
+ export function calculateExp(product: string, profile: UserProfile): ExpResult {
98
+ const baseExp = getBaseExp(product);
99
+ const comboMultiplier = getComboMultiplier(profile.currentCombo + 1, profile.buffs);
100
+ const firstUseMultiplier = getFirstUseMultiplier(product, profile.productUsage);
101
+ const signInBonus = getSignInBonus(profile);
102
+ const consecutiveSignInBonus = getConsecutiveSignInBonus(profile, profile.buffs);
103
+ const buffMultiplier = getBuffMultiplier(profile.buffs);
104
+
105
+ const totalExp = Math.floor(
106
+ (baseExp * comboMultiplier * firstUseMultiplier + signInBonus + consecutiveSignInBonus) * buffMultiplier
107
+ );
108
+
109
+ return {
110
+ baseExp,
111
+ comboMultiplier,
112
+ firstUseMultiplier,
113
+ signInBonus,
114
+ consecutiveSignInBonus,
115
+ buffMultiplier,
116
+ totalExp,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * 更新签到状态
122
+ */
123
+ export function updateSignInStatus(profile: UserProfile): void {
124
+ const today = new Date().toISOString().slice(0, 10);
125
+
126
+ if (profile.lastSignInDate === today) {
127
+ return; // 今天已签到
128
+ }
129
+
130
+ // 检查是否连续签到
131
+ const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
132
+ if (profile.lastSignInDate === yesterday) {
133
+ profile.consecutiveSignInDays += 1;
134
+ } else {
135
+ profile.consecutiveSignInDays = 1;
136
+ }
137
+
138
+ profile.lastSignInDate = today;
139
+ }