@sdd330dev/jy-skill 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/AGENTS.md +148 -0
  2. package/LICENSE +21 -0
  3. package/README.md +177 -0
  4. package/SKILL.md +208 -0
  5. package/assets/characters/0.json +56 -0
  6. package/assets/characters/1.json +60 -0
  7. package/assets/characters/10.json +56 -0
  8. package/assets/characters/100.json +38 -0
  9. package/assets/characters/101.json +38 -0
  10. package/assets/characters/102.json +38 -0
  11. package/assets/characters/103.json +38 -0
  12. package/assets/characters/104.json +38 -0
  13. package/assets/characters/105.json +38 -0
  14. package/assets/characters/106.json +38 -0
  15. package/assets/characters/107.json +38 -0
  16. package/assets/characters/108.json +38 -0
  17. package/assets/characters/109.json +38 -0
  18. package/assets/characters/11.json +56 -0
  19. package/assets/characters/110.json +38 -0
  20. package/assets/characters/12.json +52 -0
  21. package/assets/characters/13.json +56 -0
  22. package/assets/characters/14.json +52 -0
  23. package/assets/characters/15.json +56 -0
  24. package/assets/characters/16.json +56 -0
  25. package/assets/characters/17.json +52 -0
  26. package/assets/characters/18.json +43 -0
  27. package/assets/characters/19.json +56 -0
  28. package/assets/characters/2.json +56 -0
  29. package/assets/characters/20.json +56 -0
  30. package/assets/characters/200.json +48 -0
  31. package/assets/characters/201.json +48 -0
  32. package/assets/characters/202.json +56 -0
  33. package/assets/characters/21.json +52 -0
  34. package/assets/characters/22.json +52 -0
  35. package/assets/characters/23.json +56 -0
  36. package/assets/characters/24.json +43 -0
  37. package/assets/characters/25.json +52 -0
  38. package/assets/characters/26.json +56 -0
  39. package/assets/characters/27.json +52 -0
  40. package/assets/characters/28.json +52 -0
  41. package/assets/characters/29.json +52 -0
  42. package/assets/characters/3.json +60 -0
  43. package/assets/characters/30.json +52 -0
  44. package/assets/characters/300.json +38 -0
  45. package/assets/characters/301.json +38 -0
  46. package/assets/characters/31.json +56 -0
  47. package/assets/characters/32.json +47 -0
  48. package/assets/characters/33.json +47 -0
  49. package/assets/characters/34.json +52 -0
  50. package/assets/characters/35.json +56 -0
  51. package/assets/characters/36.json +52 -0
  52. package/assets/characters/37.json +52 -0
  53. package/assets/characters/38.json +56 -0
  54. package/assets/characters/39.json +52 -0
  55. package/assets/characters/4.json +56 -0
  56. package/assets/characters/40.json +56 -0
  57. package/assets/characters/41.json +52 -0
  58. package/assets/characters/42.json +52 -0
  59. package/assets/characters/43.json +52 -0
  60. package/assets/characters/44.json +52 -0
  61. package/assets/characters/45.json +56 -0
  62. package/assets/characters/46.json +56 -0
  63. package/assets/characters/47.json +47 -0
  64. package/assets/characters/48.json +52 -0
  65. package/assets/characters/49.json +38 -0
  66. package/assets/characters/5.json +60 -0
  67. package/assets/characters/50.json +38 -0
  68. package/assets/characters/51.json +38 -0
  69. package/assets/characters/52.json +38 -0
  70. package/assets/characters/53.json +38 -0
  71. package/assets/characters/54.json +38 -0
  72. package/assets/characters/55.json +38 -0
  73. package/assets/characters/56.json +38 -0
  74. package/assets/characters/57.json +38 -0
  75. package/assets/characters/58.json +38 -0
  76. package/assets/characters/59.json +38 -0
  77. package/assets/characters/6.json +56 -0
  78. package/assets/characters/60.json +38 -0
  79. package/assets/characters/61.json +38 -0
  80. package/assets/characters/62.json +38 -0
  81. package/assets/characters/63.json +38 -0
  82. package/assets/characters/64.json +38 -0
  83. package/assets/characters/65.json +38 -0
  84. package/assets/characters/66.json +38 -0
  85. package/assets/characters/67.json +38 -0
  86. package/assets/characters/68.json +38 -0
  87. package/assets/characters/69.json +38 -0
  88. package/assets/characters/7.json +64 -0
  89. package/assets/characters/70.json +38 -0
  90. package/assets/characters/71.json +38 -0
  91. package/assets/characters/72.json +38 -0
  92. package/assets/characters/73.json +38 -0
  93. package/assets/characters/74.json +38 -0
  94. package/assets/characters/75.json +38 -0
  95. package/assets/characters/76.json +38 -0
  96. package/assets/characters/77.json +38 -0
  97. package/assets/characters/78.json +38 -0
  98. package/assets/characters/79.json +38 -0
  99. package/assets/characters/8.json +60 -0
  100. package/assets/characters/80.json +38 -0
  101. package/assets/characters/81.json +38 -0
  102. package/assets/characters/82.json +38 -0
  103. package/assets/characters/83.json +38 -0
  104. package/assets/characters/84.json +38 -0
  105. package/assets/characters/85.json +38 -0
  106. package/assets/characters/86.json +38 -0
  107. package/assets/characters/87.json +38 -0
  108. package/assets/characters/88.json +38 -0
  109. package/assets/characters/89.json +38 -0
  110. package/assets/characters/9.json +56 -0
  111. package/assets/characters/90.json +38 -0
  112. package/assets/characters/91.json +38 -0
  113. package/assets/characters/92.json +38 -0
  114. package/assets/characters/93.json +38 -0
  115. package/assets/characters/94.json +38 -0
  116. package/assets/characters/95.json +38 -0
  117. package/assets/characters/96.json +38 -0
  118. package/assets/characters/97.json +38 -0
  119. package/assets/characters/98.json +38 -0
  120. package/assets/characters/99.json +38 -0
  121. package/assets/characters/index.json +584 -0
  122. package/assets/game-config.json +855 -0
  123. package/assets/items.json +1060 -0
  124. package/assets/skills.json +829 -0
  125. package/assets/templates.json +127 -0
  126. package/package.json +80 -0
  127. package/references/agent-handbook.md +308 -0
  128. package/references/game-design.md +124 -0
  129. package/references/player-guide.md +176 -0
  130. package/scripts/config-loader.ts +408 -0
  131. package/scripts/game-engine.ts +617 -0
  132. package/scripts/game-logic.ts +153 -0
  133. package/scripts/game-types.ts +46 -0
  134. package/scripts/install-skill.mjs +115 -0
  135. package/scripts/persistence.ts +135 -0
@@ -0,0 +1,617 @@
1
+ /**
2
+ * 金庸群侠传 · 游戏引擎
3
+ *
4
+ * 智能体唯一入口:所有游戏操作通过本文件导出函数执行。
5
+ */
6
+
7
+ import {
8
+ calculateDamage as calcDamage,
9
+ getExpForLevel,
10
+ calculateMpCost,
11
+ calculatePoisonDamage,
12
+ calculateHurtDamage,
13
+ DEFAULT_ATTRIBUTES,
14
+ MAX_STAMINA,
15
+ MAX_LEVEL,
16
+ } from './game-logic';
17
+ import {
18
+ initConfigs,
19
+ getTemplates,
20
+ getMap,
21
+ getItem,
22
+ getSkill,
23
+ getSkillAttackAtLevel,
24
+ getDialog,
25
+ getEnemyTemplate,
26
+ isWeapon,
27
+ isArmor,
28
+ isConsumable,
29
+ } from './config-loader';
30
+ import type { GameState, BattleEnemy, Character } from './game-types';
31
+ import {
32
+ loadGameState as persistenceLoadGameState,
33
+ saveGameState,
34
+ deleteSave,
35
+ loadOrCreateGame as persistenceLoadOrCreateGame,
36
+ getSavePath,
37
+ } from './persistence';
38
+ import type { LoadGameResult } from './persistence';
39
+
40
+ export type { GameState, BattleEnemy, Character, LoadGameResult };
41
+ export { saveGameState, deleteSave, getSavePath };
42
+
43
+ initConfigs();
44
+
45
+ function autoSave(state: GameState): void {
46
+ saveGameState(state);
47
+ }
48
+
49
+ // ============================================================================
50
+ // 初始化
51
+ // ============================================================================
52
+
53
+ function buildCharacter(name: string): Character {
54
+ const tpl = getTemplates();
55
+ const attrs = { ...DEFAULT_ATTRIBUTES };
56
+ return {
57
+ name,
58
+ level: 1,
59
+ exp: 0,
60
+ hp: attrs.maxHp,
61
+ maxHp: attrs.maxHp,
62
+ mp: attrs.maxMp,
63
+ maxMp: attrs.maxMp,
64
+ stamina: MAX_STAMINA,
65
+ poison: 0,
66
+ hurt: 0,
67
+ attributes: attrs,
68
+ equipment: { weapon: null, armor: null },
69
+ skills: [...(tpl.defaultCharacter.skills ?? ['基本拳法'])],
70
+ skillLevels: Object.fromEntries(
71
+ (tpl.defaultCharacter.skills ?? ['基本拳法']).map((s) => [s, 0]),
72
+ ),
73
+ };
74
+ }
75
+
76
+ export function createNewGame(name: string): GameState {
77
+ const tpl = getTemplates();
78
+ return {
79
+ character: buildCharacter(name),
80
+ team: [],
81
+ inventory: {
82
+ silver: tpl.defaultInventory.silver,
83
+ items: tpl.defaultInventory.items.map((i) => ({ ...i })),
84
+ },
85
+ location: tpl.startLocation ?? '小村',
86
+ week: 1,
87
+ flags: {},
88
+ visitedMaps: [tpl.startLocation ?? '小村'],
89
+ completedQuests: [],
90
+ };
91
+ }
92
+
93
+ export function loadGameState(): GameState | null {
94
+ return persistenceLoadGameState();
95
+ }
96
+
97
+ export function loadOrCreateGame(
98
+ createNewGameFn: (name: string) => GameState,
99
+ name = '主角',
100
+ ): LoadGameResult {
101
+ return persistenceLoadOrCreateGame(createNewGameFn, name);
102
+ }
103
+
104
+ export function restartGame(name = '主角'): GameState {
105
+ deleteSave();
106
+ const state = createNewGame(name);
107
+ saveGameState(state);
108
+ return state;
109
+ }
110
+
111
+ // ============================================================================
112
+ // 状态查询
113
+ // ============================================================================
114
+
115
+ export function getStatus(state: GameState): string {
116
+ const c = state.character;
117
+ const expLine =
118
+ c.level >= MAX_LEVEL
119
+ ? `经验: ${c.exp}(已满级)`
120
+ : `经验: ${c.exp}/${getExpForLevel(c.level + 1)}`;
121
+ return [
122
+ `👤 ${c.name} | Lv.${c.level} | ${expLine}`,
123
+ `❤️ ${c.hp}/${c.maxHp} | 💠 ${c.mp}/${c.maxMp} | ⚡ ${c.stamina}/${MAX_STAMINA}`,
124
+ c.poison > 0 ? `🧪 中毒: ${c.poison}` : null,
125
+ c.hurt > 0 ? `💊 受伤: ${c.hurt}` : null,
126
+ `💰 ${state.inventory.silver} | 📍 ${state.location} | 📅 第${state.week}周`,
127
+ ]
128
+ .filter(Boolean)
129
+ .join('\n');
130
+ }
131
+
132
+ export function getLocationInfo(state: GameState): string {
133
+ const map = getMap(state.location);
134
+ if (!map) return '当前位置未知';
135
+
136
+ const lines = [`📍 ${state.location}`];
137
+ if (map.connections.length > 0) {
138
+ lines.push(`可前往: ${map.connections.join('、')}`);
139
+ }
140
+ if (map.npcs.length > 0) {
141
+ lines.push(`人物: ${map.npcs.join('、')}`);
142
+ }
143
+ if (map.shops.length > 0) {
144
+ lines.push(`可购: ${map.shops.join('、')}`);
145
+ }
146
+ if (map.encounterRate && map.encounterRate > 0) {
147
+ lines.push('此地行路需当心,或有歹人埋伏');
148
+ }
149
+ return lines.join('\n');
150
+ }
151
+
152
+ export function getInventory(state: GameState): string {
153
+ const items = state.inventory.items;
154
+ if (items.length === 0) return `💰 银两: ${state.inventory.silver}\n\n📦 背包空空如也`;
155
+
156
+ const lines = items.map((i) => `- ${i.name} ×${i.count}`);
157
+ return `💰 银两: ${state.inventory.silver}\n\n📦 物品:\n${lines.join('\n')}`;
158
+ }
159
+
160
+ export function getSkills(state: GameState): string {
161
+ const skills = state.character.skills;
162
+ if (skills.length === 0) return '🥋 还没有学会任何武功';
163
+ return `🥋 武功:\n${skills.map((s) => `- ${s}`).join('\n')}`;
164
+ }
165
+
166
+ // ============================================================================
167
+ // 移动
168
+ // ============================================================================
169
+
170
+ export function moveTo(
171
+ state: GameState,
172
+ destination: string,
173
+ ): { success: boolean; message: string; encounter?: string } {
174
+ const map = getMap(state.location);
175
+ if (!map) return { success: false, message: '当前位置未知' };
176
+
177
+ if (destination === 'random') {
178
+ if (map.connections.length === 0) {
179
+ return { success: false, message: '无处可去' };
180
+ }
181
+ destination = map.connections[Math.floor(Math.random() * map.connections.length)];
182
+ }
183
+
184
+ if (!map.connections.includes(destination)) {
185
+ return { success: false, message: `从${state.location}无法直达${destination}` };
186
+ }
187
+
188
+ state.location = destination;
189
+ state.character.stamina = Math.max(0, state.character.stamina - 5);
190
+ state.week++;
191
+ advanceWeekEffects(state);
192
+
193
+ if (!state.visitedMaps.includes(destination)) {
194
+ state.visitedMaps.push(destination);
195
+ }
196
+
197
+ const destMap = getMap(destination);
198
+ let encounter: string | undefined;
199
+ if (
200
+ destMap?.encounterRate &&
201
+ destMap.encounterRate > 0 &&
202
+ destMap.encounterEnemies &&
203
+ destMap.encounterEnemies.length > 0 &&
204
+ Math.random() * 100 < destMap.encounterRate
205
+ ) {
206
+ const pool = destMap.encounterEnemies;
207
+ encounter = pool[Math.floor(Math.random() * pool.length)];
208
+ }
209
+
210
+ autoSave(state);
211
+
212
+ let message = `你来到了${destination}`;
213
+ if (encounter) {
214
+ message += `。暗处传来脚步声——似乎有${encounter}埋伏!`;
215
+ }
216
+ return { success: true, message, encounter };
217
+ }
218
+
219
+ // ============================================================================
220
+ // NPC 交互
221
+ // ============================================================================
222
+
223
+ export function talkTo(state: GameState, npcName: string): { success: boolean; message: string } {
224
+ const map = getMap(state.location);
225
+ if (!map) return { success: false, message: '当前位置未知' };
226
+
227
+ if (npcName === 'random') {
228
+ if (map.npcs.length === 0) return { success: false, message: '这里没有人' };
229
+ npcName = map.npcs[Math.floor(Math.random() * map.npcs.length)];
230
+ }
231
+
232
+ if (!map.npcs.includes(npcName)) {
233
+ return { success: false, message: `${state.location}没有${npcName}` };
234
+ }
235
+
236
+ const dialogId = map.npcDialogs[npcName];
237
+ if (dialogId) {
238
+ const dialog = getDialog(dialogId);
239
+ if (dialog) {
240
+ return { success: true, message: `${dialog.speaker}:「${dialog.text}」` };
241
+ }
242
+ }
243
+
244
+ return { success: true, message: `你和${npcName}聊了起来` };
245
+ }
246
+
247
+ // ============================================================================
248
+ // 商店
249
+ // ============================================================================
250
+
251
+ export function buyItem(state: GameState, itemName: string): { success: boolean; message: string } {
252
+ const item = getItem(itemName);
253
+ if (!item) return { success: false, message: `没有${itemName}出售` };
254
+
255
+ const map = getMap(state.location);
256
+ if (!map || !map.shops.includes(itemName)) {
257
+ return { success: false, message: `${state.location}没有卖${itemName}` };
258
+ }
259
+
260
+ if (state.inventory.silver < item.price) {
261
+ return {
262
+ success: false,
263
+ message: `银两不足,需要${item.price},只有${state.inventory.silver}`,
264
+ };
265
+ }
266
+
267
+ state.inventory.silver -= item.price;
268
+ const existing = state.inventory.items.find((i) => i.name === itemName);
269
+ if (existing) {
270
+ existing.count++;
271
+ } else {
272
+ state.inventory.items.push({
273
+ id: String(item.id),
274
+ name: itemName,
275
+ count: 1,
276
+ });
277
+ }
278
+
279
+ autoSave(state);
280
+ return { success: true, message: `购买了${itemName},花费${item.price}银两` };
281
+ }
282
+
283
+ // ============================================================================
284
+ // 物品
285
+ // ============================================================================
286
+
287
+ export function useItem(state: GameState, itemName: string): { success: boolean; message: string } {
288
+ const inv = state.inventory.items.find((i) => i.name === itemName);
289
+ if (!inv || inv.count <= 0) {
290
+ return { success: false, message: `没有${itemName}` };
291
+ }
292
+
293
+ const item = getItem(itemName);
294
+ if (!item || !isConsumable(item)) {
295
+ return { success: false, message: `${itemName}无法使用` };
296
+ }
297
+
298
+ const c = state.character;
299
+ const parts: string[] = [];
300
+ let hpGain = 0;
301
+ let mpGain = 0;
302
+ let staminaGain = 0;
303
+ let poisonReduced = 0;
304
+
305
+ if (item.useAddHp > 0) {
306
+ hpGain = Math.min(item.useAddHp, c.maxHp - c.hp);
307
+ if (hpGain > 0) parts.push(`恢复${hpGain}生命`);
308
+ }
309
+ if (item.useAddMp > 0) {
310
+ mpGain = Math.min(item.useAddMp, c.maxMp - c.mp);
311
+ if (mpGain > 0) parts.push(`恢复${mpGain}内力`);
312
+ }
313
+ if (item.useAddStamina > 0) {
314
+ staminaGain = Math.min(item.useAddStamina, MAX_STAMINA - c.stamina);
315
+ if (staminaGain > 0) parts.push(`恢复${staminaGain}体力`);
316
+ }
317
+ if (item.useDePoison > 0 && c.poison > 0) {
318
+ poisonReduced = Math.min(item.useDePoison, c.poison);
319
+ parts.push('解除中毒');
320
+ }
321
+
322
+ if (hpGain + mpGain + staminaGain + poisonReduced === 0) {
323
+ if (item.useDePoison > 0 && c.poison <= 0) {
324
+ return { success: false, message: '你没有中毒,无需使用解毒丸' };
325
+ }
326
+ return { success: false, message: `${itemName}当前无需使用` };
327
+ }
328
+
329
+ c.hp += hpGain;
330
+ c.mp += mpGain;
331
+ c.stamina += staminaGain;
332
+ c.poison -= poisonReduced;
333
+
334
+ inv.count--;
335
+ if (inv.count <= 0) {
336
+ state.inventory.items = state.inventory.items.filter((i) => i.name !== itemName);
337
+ }
338
+
339
+ autoSave(state);
340
+ return { success: true, message: `使用${itemName},${parts.join(',')}` };
341
+ }
342
+
343
+ // ============================================================================
344
+ // 装备
345
+ // ============================================================================
346
+
347
+ export function equipItem(
348
+ state: GameState,
349
+ itemName: string,
350
+ ): { success: boolean; message: string } {
351
+ const inv = state.inventory.items.find((i) => i.name === itemName);
352
+ if (!inv || inv.count <= 0) {
353
+ return { success: false, message: `没有${itemName}` };
354
+ }
355
+
356
+ const item = getItem(itemName);
357
+ if (!item) return { success: false, message: `未知物品${itemName}` };
358
+
359
+ if (isWeapon(item)) {
360
+ state.character.equipment.weapon = itemName;
361
+ autoSave(state);
362
+ return { success: true, message: `装备了${itemName}` };
363
+ }
364
+
365
+ if (isArmor(item)) {
366
+ state.character.equipment.armor = itemName;
367
+ autoSave(state);
368
+ return { success: true, message: `装备了${itemName}` };
369
+ }
370
+
371
+ return { success: false, message: `${itemName}不可装备` };
372
+ }
373
+
374
+ // ============================================================================
375
+ // 武功
376
+ // ============================================================================
377
+
378
+ export function learnSkill(
379
+ state: GameState,
380
+ skillName: string,
381
+ ): { success: boolean; message: string } {
382
+ if (!getSkill(skillName)) {
383
+ return { success: false, message: `江湖上没有${skillName}这门武功` };
384
+ }
385
+ if (state.character.skills.includes(skillName)) {
386
+ return { success: false, message: `已经学会了${skillName}` };
387
+ }
388
+
389
+ state.character.skills.push(skillName);
390
+ state.character.skillLevels[skillName] = 0;
391
+ autoSave(state);
392
+ return { success: true, message: `学会了${skillName}` };
393
+ }
394
+
395
+ // ============================================================================
396
+ // 休息
397
+ // ============================================================================
398
+
399
+ export function rest(state: GameState): { success: boolean; message: string } {
400
+ state.character.hp = state.character.maxHp;
401
+ state.character.mp = state.character.maxMp;
402
+ state.character.stamina = MAX_STAMINA;
403
+ state.character.poison = 0;
404
+ state.character.hurt = 0;
405
+
406
+ autoSave(state);
407
+ return { success: true, message: '休息完毕,状态全满' };
408
+ }
409
+
410
+ // ============================================================================
411
+ // 战斗
412
+ // ============================================================================
413
+
414
+ export function startBattle(
415
+ state: GameState,
416
+ enemyName: string,
417
+ ): { success: boolean; message: string; enemies?: BattleEnemy[] } {
418
+ const entry = getTemplates().enemies[enemyName];
419
+ const template = getEnemyTemplate(enemyName);
420
+ if (!template) {
421
+ return { success: false, message: `没有遇到${enemyName}` };
422
+ }
423
+
424
+ const solo =
425
+ (entry && 'solo' in entry && entry.solo === true) ||
426
+ enemyName === '老虎' ||
427
+ enemyName === '毒蛇';
428
+ const count = solo ? 1 : Math.floor(Math.random() * 3) + 1;
429
+ const enemies: BattleEnemy[] = Array.from({ length: count }, (_, i) => ({
430
+ name: count === 1 ? enemyName : `${enemyName}${i + 1}`,
431
+ hp: template.hp,
432
+ maxHp: template.hp,
433
+ attack: template.attack,
434
+ defence: template.defence,
435
+ }));
436
+
437
+ return { success: true, message: `⚔️ 遭遇${count}个${enemyName}!`, enemies };
438
+ }
439
+
440
+ export function attackEnemy(
441
+ state: GameState,
442
+ enemies: BattleEnemy[],
443
+ targetIndex: number,
444
+ ): { message: string; enemyDefeated: boolean; playerDamage: number } {
445
+ const target = enemies[targetIndex];
446
+ if (!target || target.hp <= 0) {
447
+ return { message: '目标无效', enemyDefeated: false, playerDamage: 0 };
448
+ }
449
+
450
+ const damage = calcDamage(
451
+ getEffectiveAttack(state),
452
+ 0,
453
+ target.defence,
454
+ state.character.attributes.ambidextrous,
455
+ state.character.attributes.martialKnowledge,
456
+ );
457
+
458
+ target.hp = Math.max(0, target.hp - damage);
459
+ state.character.stamina = Math.max(0, state.character.stamina - 3);
460
+
461
+ const defeated = target.hp <= 0;
462
+ if (defeated) {
463
+ grantBattleExp(state, target.maxHp);
464
+ }
465
+
466
+ autoSave(state);
467
+ return {
468
+ message: `攻击${target.name},造成${damage}点伤害${defeated ? ',击败!' : ''}`,
469
+ enemyDefeated: defeated,
470
+ playerDamage: 0,
471
+ };
472
+ }
473
+
474
+ export function useSkillInBattle(
475
+ state: GameState,
476
+ enemies: BattleEnemy[],
477
+ skillName: string,
478
+ targetIndex: number,
479
+ ): { success: boolean; message: string } {
480
+ if (!state.character.skills.includes(skillName)) {
481
+ return { success: false, message: `没有学会${skillName}` };
482
+ }
483
+
484
+ const skill = getSkill(skillName);
485
+ if (!skill) return { success: false, message: `未知武功${skillName}` };
486
+
487
+ const target = enemies[targetIndex];
488
+ if (!target || target.hp <= 0) {
489
+ return { success: false, message: '目标无效' };
490
+ }
491
+
492
+ const levelIndex = state.character.skillLevels[skillName] ?? 0;
493
+ const mpCost = calculateMpCost(skill.mpCost, levelIndex);
494
+ if (state.character.mp < mpCost) {
495
+ return { success: false, message: `内力不足,需要${mpCost}点内力` };
496
+ }
497
+
498
+ const skillAttack = getSkillAttackAtLevel(skillName, levelIndex);
499
+ const damage = calcDamage(
500
+ getEffectiveAttack(state),
501
+ skillAttack,
502
+ target.defence,
503
+ state.character.attributes.ambidextrous,
504
+ state.character.attributes.martialKnowledge,
505
+ );
506
+
507
+ target.hp = Math.max(0, target.hp - damage);
508
+ state.character.mp = Math.max(0, state.character.mp - mpCost);
509
+ state.character.stamina = Math.max(0, state.character.stamina - 3);
510
+
511
+ const defeated = target.hp <= 0;
512
+ if (defeated) {
513
+ grantBattleExp(state, target.maxHp);
514
+ }
515
+
516
+ autoSave(state);
517
+ return {
518
+ success: true,
519
+ message: `使用${skillName}攻击${target.name},造成${damage}点伤害${defeated ? ',击败!' : ''}`,
520
+ };
521
+ }
522
+
523
+ export function enemyAttack(
524
+ state: GameState,
525
+ enemies: BattleEnemy[],
526
+ ): { message: string; playerDefeated: boolean } {
527
+ const aliveEnemies = enemies.filter((e) => e.hp > 0);
528
+ if (aliveEnemies.length === 0) return { message: '', playerDefeated: false };
529
+
530
+ const enemy = aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)];
531
+ const damage = Math.max(1, enemy.attack - getEffectiveDefence(state));
532
+
533
+ state.character.hp = Math.max(0, state.character.hp - damage);
534
+
535
+ autoSave(state);
536
+ return {
537
+ message: `${enemy.name}攻击你,造成${damage}点伤害`,
538
+ playerDefeated: state.character.hp <= 0,
539
+ };
540
+ }
541
+
542
+ // ============================================================================
543
+ // 状态辅助(原 game-state.ts 合并)
544
+ // ============================================================================
545
+
546
+ export function advanceWeek(state: GameState): void {
547
+ state.week++;
548
+ advanceWeekEffects(state);
549
+ autoSave(state);
550
+ }
551
+
552
+ function advanceWeekEffects(state: GameState): void {
553
+ if (state.character.poison > 0) {
554
+ const dmg = calculatePoisonDamage(state.character.poison);
555
+ state.character.hp = Math.max(1, state.character.hp - dmg);
556
+ }
557
+ if (state.character.hurt > 0) {
558
+ const dmg = calculateHurtDamage(state.character.hurt);
559
+ state.character.hp = Math.max(1, state.character.hp - dmg);
560
+ }
561
+ }
562
+
563
+ export function isDead(state: GameState): boolean {
564
+ return state.character.hp <= 0;
565
+ }
566
+
567
+ // ============================================================================
568
+ // 内部
569
+ // ============================================================================
570
+
571
+ function getEffectiveAttack(state: GameState): number {
572
+ let attack = state.character.attributes.attack;
573
+ const weapon = state.character.equipment.weapon;
574
+ if (weapon) {
575
+ const item = getItem(weapon);
576
+ if (item) attack += item.addAttack;
577
+ }
578
+ return attack;
579
+ }
580
+
581
+ function getEffectiveDefence(state: GameState): number {
582
+ let defence = state.character.attributes.defence;
583
+ const armor = state.character.equipment.armor;
584
+ if (armor) {
585
+ const item = getItem(armor);
586
+ if (item) defence += item.addDefence;
587
+ }
588
+ return defence;
589
+ }
590
+
591
+ function grantBattleExp(state: GameState, enemyMaxHp: number): void {
592
+ state.character.exp += 10 + enemyMaxHp / 10;
593
+ checkLevelUp(state);
594
+ }
595
+
596
+ function checkLevelUp(state: GameState): void {
597
+ const c = state.character;
598
+ let needed = getExpForLevel(c.level + 1);
599
+
600
+ while (c.exp >= needed && c.level < MAX_LEVEL) {
601
+ c.level++;
602
+ c.exp -= needed;
603
+
604
+ const iq = c.attributes.iq;
605
+ const attrGain = Math.floor(Math.random() * (Math.floor((iq - 10) / 20) + 2)) + 1;
606
+
607
+ c.maxHp += (c.attributes.hpInc + Math.floor(Math.random() * 4)) * 3;
608
+ c.maxMp += (9 - attrGain) * 4;
609
+ c.attributes.attack += attrGain;
610
+ c.attributes.agility += attrGain;
611
+ c.attributes.defence += attrGain;
612
+
613
+ c.hp = c.maxHp;
614
+ c.mp = c.maxMp;
615
+ needed = getExpForLevel(c.level + 1);
616
+ }
617
+ }