@sdd330dev/jy-skill 0.3.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +41 -8
- package/README.md +54 -10
- package/SKILL.md +32 -15
- package/assets/game-config.json +63 -2
- package/assets/templates.json +168 -9
- package/package.json +11 -1
- package/references/agent-handbook.md +49 -10
- package/references/host-adapters.md +182 -0
- package/references/player-guide.md +5 -4
- package/save/.gitkeep +0 -0
- package/scripts/build-feishu-card.ts +53 -0
- package/scripts/choice-prompt.ts +179 -0
- package/scripts/config-loader.ts +182 -16
- package/scripts/event-engine.ts +205 -0
- package/scripts/game-engine.ts +683 -40
- package/scripts/game-logic.ts +5 -0
- package/scripts/game-types.ts +149 -0
- package/scripts/mcp-server.ts +161 -0
- package/scripts/persistence.ts +100 -31
package/scripts/config-loader.ts
CHANGED
|
@@ -5,14 +5,44 @@
|
|
|
5
5
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import type { NpcCard } from './game-types';
|
|
8
9
|
|
|
9
10
|
const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
11
|
const ASSETS_DIR = join(ROOT_DIR, 'assets');
|
|
11
12
|
|
|
13
|
+
/** 地图名归一化(game-config 与 templates 不一致时) */
|
|
14
|
+
const MAP_ALIASES: Record<string, string> = {
|
|
15
|
+
终南山全真教: '全真教',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function normalizeMapName(name: string): string {
|
|
19
|
+
return MAP_ALIASES[name] ?? name;
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
// ============================================================================
|
|
13
23
|
// 类型
|
|
14
24
|
// ============================================================================
|
|
15
25
|
|
|
26
|
+
export interface MapEventCondition {
|
|
27
|
+
type: string;
|
|
28
|
+
params: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MapEventAction {
|
|
32
|
+
type: string;
|
|
33
|
+
params: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MapEvent {
|
|
37
|
+
id: string;
|
|
38
|
+
triggerType: string;
|
|
39
|
+
x?: number;
|
|
40
|
+
y?: number;
|
|
41
|
+
npcName?: string;
|
|
42
|
+
conditions: MapEventCondition[];
|
|
43
|
+
actions: MapEventAction[];
|
|
44
|
+
}
|
|
45
|
+
|
|
16
46
|
export interface MapInfo {
|
|
17
47
|
npcs: string[];
|
|
18
48
|
shops: string[];
|
|
@@ -20,6 +50,11 @@ export interface MapInfo {
|
|
|
20
50
|
npcDialogs: Record<string, string>;
|
|
21
51
|
encounterRate?: number;
|
|
22
52
|
encounterEnemies?: string[];
|
|
53
|
+
description?: string;
|
|
54
|
+
atmosphere?: string;
|
|
55
|
+
dangerLevel?: 'safe' | 'cautious' | 'dangerous';
|
|
56
|
+
npcCards?: Record<string, NpcCard>;
|
|
57
|
+
events?: MapEvent[];
|
|
23
58
|
}
|
|
24
59
|
|
|
25
60
|
export interface ItemConfig {
|
|
@@ -35,6 +70,13 @@ export interface ItemConfig {
|
|
|
35
70
|
useAddMp: number;
|
|
36
71
|
useAddStamina: number;
|
|
37
72
|
useDePoison: number;
|
|
73
|
+
useAddAttack?: number;
|
|
74
|
+
useAddAgility?: number;
|
|
75
|
+
useAddDefence?: number;
|
|
76
|
+
useAddPoison?: number;
|
|
77
|
+
skillId?: number;
|
|
78
|
+
needIQ?: number;
|
|
79
|
+
needExp?: number;
|
|
38
80
|
}
|
|
39
81
|
|
|
40
82
|
export interface SkillConfig {
|
|
@@ -43,6 +85,7 @@ export interface SkillConfig {
|
|
|
43
85
|
desc: string;
|
|
44
86
|
mpCost: number;
|
|
45
87
|
damageType: number;
|
|
88
|
+
poison?: number;
|
|
46
89
|
levels: number[][];
|
|
47
90
|
}
|
|
48
91
|
|
|
@@ -56,10 +99,17 @@ export interface CharacterConfig {
|
|
|
56
99
|
source: string;
|
|
57
100
|
}
|
|
58
101
|
|
|
102
|
+
export interface DialogChoiceConfig {
|
|
103
|
+
text: string;
|
|
104
|
+
nextId: string;
|
|
105
|
+
actions?: MapEventAction[];
|
|
106
|
+
}
|
|
107
|
+
|
|
59
108
|
export interface DialogConfig {
|
|
60
109
|
id: string;
|
|
61
110
|
speaker: string;
|
|
62
111
|
text: string;
|
|
112
|
+
choices?: DialogChoiceConfig[];
|
|
63
113
|
}
|
|
64
114
|
|
|
65
115
|
export interface EnemyTemplate {
|
|
@@ -67,6 +117,19 @@ export interface EnemyTemplate {
|
|
|
67
117
|
attack: number;
|
|
68
118
|
defence: number;
|
|
69
119
|
solo?: boolean;
|
|
120
|
+
onHitPoison?: number;
|
|
121
|
+
onHitHurt?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface TemplateMapEntry {
|
|
125
|
+
npcs: string[];
|
|
126
|
+
shops: string[];
|
|
127
|
+
connections: string[];
|
|
128
|
+
encounters?: number | { rate: number; enemies: string[] };
|
|
129
|
+
description?: string;
|
|
130
|
+
atmosphere?: string;
|
|
131
|
+
dangerLevel?: 'safe' | 'cautious' | 'dangerous';
|
|
132
|
+
npcCards?: Record<string, NpcCard>;
|
|
70
133
|
}
|
|
71
134
|
|
|
72
135
|
export interface GameTemplates {
|
|
@@ -80,15 +143,7 @@ export interface GameTemplates {
|
|
|
80
143
|
};
|
|
81
144
|
startLocation: string;
|
|
82
145
|
enemies: Record<string, EnemyTemplate | { characterId: number }>;
|
|
83
|
-
maps?: Record<
|
|
84
|
-
string,
|
|
85
|
-
{
|
|
86
|
-
npcs: string[];
|
|
87
|
-
shops: string[];
|
|
88
|
-
connections: string[];
|
|
89
|
-
encounters?: number | { rate: number; enemies: string[] };
|
|
90
|
-
}
|
|
91
|
-
>;
|
|
146
|
+
maps?: Record<string, TemplateMapEntry>;
|
|
92
147
|
}
|
|
93
148
|
|
|
94
149
|
function parseEncounters(
|
|
@@ -101,6 +156,17 @@ function parseEncounters(
|
|
|
101
156
|
return { encounterRate: encounters.rate, encounterEnemies: [...encounters.enemies] };
|
|
102
157
|
}
|
|
103
158
|
|
|
159
|
+
function deriveDangerLevel(
|
|
160
|
+
templateMap: TemplateMapEntry | undefined,
|
|
161
|
+
encounterConfig: Pick<MapInfo, 'encounterRate' | 'encounterEnemies'>,
|
|
162
|
+
events: MapEvent[],
|
|
163
|
+
): 'safe' | 'cautious' | 'dangerous' {
|
|
164
|
+
if (templateMap?.dangerLevel) return templateMap.dangerLevel;
|
|
165
|
+
if (encounterConfig.encounterRate && encounterConfig.encounterRate > 0) return 'dangerous';
|
|
166
|
+
if (events.some((e) => e.triggerType === 'interact')) return 'cautious';
|
|
167
|
+
return 'safe';
|
|
168
|
+
}
|
|
169
|
+
|
|
104
170
|
// ============================================================================
|
|
105
171
|
// 加载
|
|
106
172
|
// ============================================================================
|
|
@@ -114,6 +180,7 @@ const mapsByName = new Map<string, MapInfo>();
|
|
|
114
180
|
const itemsByName = new Map<string, ItemConfig>();
|
|
115
181
|
const itemsById = new Map<number, ItemConfig>();
|
|
116
182
|
const skillsByName = new Map<string, SkillConfig>();
|
|
183
|
+
const skillsById = new Map<number, SkillConfig>();
|
|
117
184
|
const charactersById = new Map<number, CharacterConfig>();
|
|
118
185
|
const charactersByName = new Map<string, CharacterConfig>();
|
|
119
186
|
const dialogsById = new Map<string, DialogConfig>();
|
|
@@ -136,6 +203,7 @@ function buildMapsFromConfig(): void {
|
|
|
136
203
|
maps: Array<{
|
|
137
204
|
id: number;
|
|
138
205
|
name: string;
|
|
206
|
+
desc?: string;
|
|
139
207
|
npcs: Array<{
|
|
140
208
|
roleId: number;
|
|
141
209
|
dialogId: string;
|
|
@@ -143,13 +211,14 @@ function buildMapsFromConfig(): void {
|
|
|
143
211
|
shopItems: number[];
|
|
144
212
|
}>;
|
|
145
213
|
connections: Array<{ targetMapId: number }>;
|
|
214
|
+
events?: MapEvent[];
|
|
146
215
|
}>;
|
|
147
216
|
dialogs: Record<string, DialogConfig>;
|
|
148
217
|
}>('game-config.json');
|
|
149
218
|
|
|
150
219
|
const mapIdToName = new Map<number, string>();
|
|
151
220
|
for (const map of gameConfig.maps) {
|
|
152
|
-
mapIdToName.set(map.id, map.name);
|
|
221
|
+
mapIdToName.set(map.id, normalizeMapName(map.name));
|
|
153
222
|
}
|
|
154
223
|
|
|
155
224
|
for (const [id, dialog] of Object.entries(gameConfig.dialogs)) {
|
|
@@ -159,7 +228,8 @@ function buildMapsFromConfig(): void {
|
|
|
159
228
|
const templateMaps = templates.maps ?? {};
|
|
160
229
|
|
|
161
230
|
for (const map of gameConfig.maps) {
|
|
162
|
-
const
|
|
231
|
+
const mapName = normalizeMapName(map.name);
|
|
232
|
+
const templateMap = templateMaps[mapName as keyof typeof templateMaps];
|
|
163
233
|
const npcDialogs: Record<string, string> = {};
|
|
164
234
|
const npcNames: string[] = [];
|
|
165
235
|
const shopNames = new Set<string>();
|
|
@@ -182,18 +252,25 @@ function buildMapsFromConfig(): void {
|
|
|
182
252
|
? [...templateMap.connections]
|
|
183
253
|
: map.connections
|
|
184
254
|
.map((c) => mapIdToName.get(c.targetMapId))
|
|
185
|
-
.filter((name): name is string => Boolean(name))
|
|
255
|
+
.filter((name): name is string => Boolean(name))
|
|
256
|
+
.map(normalizeMapName);
|
|
186
257
|
|
|
187
258
|
const shops =
|
|
188
|
-
templateMap && templateMap.shops
|
|
259
|
+
templateMap && templateMap.shops?.length > 0 ? [...templateMap.shops] : [...shopNames];
|
|
189
260
|
|
|
190
261
|
const encounterConfig = parseEncounters(templateMap?.encounters);
|
|
262
|
+
const events = map.events ?? [];
|
|
191
263
|
|
|
192
|
-
mapsByName.set(
|
|
264
|
+
mapsByName.set(mapName, {
|
|
193
265
|
npcs: templateMap?.npcs?.length ? [...templateMap.npcs] : npcNames,
|
|
194
266
|
shops,
|
|
195
267
|
connections,
|
|
196
268
|
npcDialogs,
|
|
269
|
+
description: templateMap?.description ?? map.desc,
|
|
270
|
+
atmosphere: templateMap?.atmosphere,
|
|
271
|
+
dangerLevel: deriveDangerLevel(templateMap, encounterConfig, events),
|
|
272
|
+
npcCards: templateMap?.npcCards ? { ...templateMap.npcCards } : undefined,
|
|
273
|
+
events: events.length > 0 ? [...events] : undefined,
|
|
197
274
|
...encounterConfig,
|
|
198
275
|
});
|
|
199
276
|
}
|
|
@@ -207,6 +284,10 @@ function buildMapsFromConfig(): void {
|
|
|
207
284
|
shops: [...map.shops],
|
|
208
285
|
connections: [...map.connections],
|
|
209
286
|
npcDialogs: {},
|
|
287
|
+
description: map.description,
|
|
288
|
+
atmosphere: map.atmosphere,
|
|
289
|
+
dangerLevel: deriveDangerLevel(map, encounterConfig, []),
|
|
290
|
+
npcCards: map.npcCards ? { ...map.npcCards } : undefined,
|
|
210
291
|
...encounterConfig,
|
|
211
292
|
});
|
|
212
293
|
}
|
|
@@ -241,6 +322,7 @@ function loadSkills(): void {
|
|
|
241
322
|
const data = loadJson<{ skills: SkillConfig[] }>('skills.json');
|
|
242
323
|
for (const skill of data.skills) {
|
|
243
324
|
skillsByName.set(skill.name, skill);
|
|
325
|
+
skillsById.set(skill.id, skill);
|
|
244
326
|
}
|
|
245
327
|
}
|
|
246
328
|
|
|
@@ -277,6 +359,7 @@ export function resetConfigsForTest(): void {
|
|
|
277
359
|
itemsByName.clear();
|
|
278
360
|
itemsById.clear();
|
|
279
361
|
skillsByName.clear();
|
|
362
|
+
skillsById.clear();
|
|
280
363
|
charactersById.clear();
|
|
281
364
|
charactersByName.clear();
|
|
282
365
|
dialogsById.clear();
|
|
@@ -293,7 +376,7 @@ export function getTemplates(): GameTemplates {
|
|
|
293
376
|
|
|
294
377
|
export function getMap(name: string): MapInfo | undefined {
|
|
295
378
|
initConfigs();
|
|
296
|
-
return mapsByName.get(name);
|
|
379
|
+
return mapsByName.get(normalizeMapName(name));
|
|
297
380
|
}
|
|
298
381
|
|
|
299
382
|
export function getAllMapNames(): string[] {
|
|
@@ -306,11 +389,37 @@ export function getItem(name: string): ItemConfig | undefined {
|
|
|
306
389
|
return itemsByName.get(name);
|
|
307
390
|
}
|
|
308
391
|
|
|
392
|
+
export function getItemById(id: number): ItemConfig | undefined {
|
|
393
|
+
initConfigs();
|
|
394
|
+
return itemsById.get(id);
|
|
395
|
+
}
|
|
396
|
+
|
|
309
397
|
export function getSkill(name: string): SkillConfig | undefined {
|
|
310
398
|
initConfigs();
|
|
311
399
|
return skillsByName.get(name);
|
|
312
400
|
}
|
|
313
401
|
|
|
402
|
+
export function getSkillById(id: number): SkillConfig | undefined {
|
|
403
|
+
initConfigs();
|
|
404
|
+
return skillsById.get(id);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** damageType 数字 → calculateStaminaCost 字符串键 */
|
|
408
|
+
export function mapDamageTypeToStaminaCost(damageType: number): string {
|
|
409
|
+
switch (damageType) {
|
|
410
|
+
case 1:
|
|
411
|
+
return 'absorbMp';
|
|
412
|
+
case 2:
|
|
413
|
+
return 'poison';
|
|
414
|
+
case 3:
|
|
415
|
+
return 'depoison';
|
|
416
|
+
case 4:
|
|
417
|
+
return 'heal';
|
|
418
|
+
default:
|
|
419
|
+
return 'normal';
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
314
423
|
export function getSkillAttackAtLevel(skillName: string, levelIndex = 0): number {
|
|
315
424
|
const skill = getSkill(skillName);
|
|
316
425
|
if (!skill || !skill.levels.length) return 0;
|
|
@@ -328,6 +437,42 @@ export function getDialog(dialogId: string): DialogConfig | undefined {
|
|
|
328
437
|
return dialogsById.get(dialogId);
|
|
329
438
|
}
|
|
330
439
|
|
|
440
|
+
export function getMapEvents(mapName: string): MapEvent[] {
|
|
441
|
+
initConfigs();
|
|
442
|
+
return getMap(mapName)?.events ?? [];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function getLocationMeta(mapName: string): {
|
|
446
|
+
description: string;
|
|
447
|
+
atmosphere: string;
|
|
448
|
+
dangerLevel: 'safe' | 'cautious' | 'dangerous';
|
|
449
|
+
} {
|
|
450
|
+
initConfigs();
|
|
451
|
+
const map = getMap(mapName);
|
|
452
|
+
return {
|
|
453
|
+
description: map?.description ?? `${mapName},江湖一处所在。`,
|
|
454
|
+
atmosphere: map?.atmosphere ?? '',
|
|
455
|
+
dangerLevel: map?.dangerLevel ?? 'safe',
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function getNpcCard(location: string, npcName: string): NpcCard | undefined {
|
|
460
|
+
initConfigs();
|
|
461
|
+
const map = getMap(location);
|
|
462
|
+
const card = map?.npcCards?.[npcName];
|
|
463
|
+
if (card) return { ...card, name: card.name ?? npcName };
|
|
464
|
+
|
|
465
|
+
const char = getCharacterByName(npcName);
|
|
466
|
+
if (char) {
|
|
467
|
+
return {
|
|
468
|
+
name: char.name,
|
|
469
|
+
persona: `${char.name},出自${char.source}。`,
|
|
470
|
+
knowledge: [],
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
|
|
331
476
|
export function getEnemyTemplate(enemyName: string): EnemyTemplate | undefined {
|
|
332
477
|
initConfigs();
|
|
333
478
|
const entry = templates.enemies[enemyName];
|
|
@@ -344,7 +489,21 @@ export function isArmor(item: ItemConfig): boolean {
|
|
|
344
489
|
}
|
|
345
490
|
|
|
346
491
|
export function isConsumable(item: ItemConfig): boolean {
|
|
347
|
-
return
|
|
492
|
+
return (
|
|
493
|
+
item.useAddHp > 0 ||
|
|
494
|
+
item.useAddMp > 0 ||
|
|
495
|
+
item.useAddStamina > 0 ||
|
|
496
|
+
item.useDePoison > 0 ||
|
|
497
|
+
(item.useAddAttack ?? 0) > 0 ||
|
|
498
|
+
(item.useAddAgility ?? 0) > 0 ||
|
|
499
|
+
(item.useAddDefence ?? 0) > 0 ||
|
|
500
|
+
(item.useAddPoison ?? 0) > 0 ||
|
|
501
|
+
isSkillBook(item)
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function isSkillBook(item: ItemConfig): boolean {
|
|
506
|
+
return item.type === 2 && (item.skillId ?? 0) > 0;
|
|
348
507
|
}
|
|
349
508
|
|
|
350
509
|
/** 校验 assets 完整性(CLI / CI 用) */
|
|
@@ -389,6 +548,13 @@ export function validateAssets(): string[] {
|
|
|
389
548
|
errors.push(`Map "${mapName}" connection target unknown: ${conn}`);
|
|
390
549
|
}
|
|
391
550
|
}
|
|
551
|
+
if (map.npcCards) {
|
|
552
|
+
for (const npcName of Object.keys(map.npcCards)) {
|
|
553
|
+
if (!map.npcs.includes(npcName)) {
|
|
554
|
+
errors.push(`Map "${mapName}" npcCards key "${npcName}" not in npcs list`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
392
558
|
}
|
|
393
559
|
|
|
394
560
|
for (const enemyName of Object.keys(templates.enemies)) {
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 事件引擎 — 处理 game-config.json 中的地图事件与对话动作
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { GameState, EventResult, DialogChoice } from './game-types';
|
|
6
|
+
import type { MapEvent, MapEventCondition, MapEventAction, DialogConfig } from './config-loader';
|
|
7
|
+
import { getDialog, getItemById, getMapEvents, getMap } from './config-loader';
|
|
8
|
+
|
|
9
|
+
export interface EventContext {
|
|
10
|
+
mapName?: string;
|
|
11
|
+
eventId?: string;
|
|
12
|
+
npcName?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function evaluateConditions(state: GameState, conditions: MapEventCondition[]): boolean {
|
|
16
|
+
if (conditions.length === 0) return true;
|
|
17
|
+
return conditions.every((c) => evaluateCondition(state, c));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function evaluateCondition(state: GameState, condition: MapEventCondition): boolean {
|
|
21
|
+
const params = condition.params;
|
|
22
|
+
switch (condition.type) {
|
|
23
|
+
case 'flag': {
|
|
24
|
+
const flag = params.flag as string;
|
|
25
|
+
const expected = params.value as boolean | number;
|
|
26
|
+
const actual = state.flags[flag];
|
|
27
|
+
if (expected === false) return actual === undefined || actual === false;
|
|
28
|
+
return actual === expected;
|
|
29
|
+
}
|
|
30
|
+
case 'level': {
|
|
31
|
+
const min = (params.min as number) ?? 0;
|
|
32
|
+
return state.character.level >= min;
|
|
33
|
+
}
|
|
34
|
+
case 'item': {
|
|
35
|
+
const itemId = params.itemId as number;
|
|
36
|
+
const item = getItemById(itemId);
|
|
37
|
+
if (!item) return false;
|
|
38
|
+
const inv = state.inventory.items.find((i) => i.name === item.name);
|
|
39
|
+
const minCount = (params.count as number) ?? 1;
|
|
40
|
+
return (inv?.count ?? 0) >= minCount;
|
|
41
|
+
}
|
|
42
|
+
case 'quest': {
|
|
43
|
+
const questId = params.questId as string;
|
|
44
|
+
return state.completedQuests.includes(questId);
|
|
45
|
+
}
|
|
46
|
+
default:
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function checkEvents(
|
|
52
|
+
state: GameState,
|
|
53
|
+
trigger: 'auto' | 'interact' | 'talk',
|
|
54
|
+
ctx: EventContext = {},
|
|
55
|
+
): MapEvent[] {
|
|
56
|
+
const mapName = ctx.mapName ?? state.location;
|
|
57
|
+
const events = getMapEvents(mapName);
|
|
58
|
+
return events.filter((event) => {
|
|
59
|
+
if (event.triggerType !== trigger) return false;
|
|
60
|
+
if (ctx.eventId && event.id !== ctx.eventId) return false;
|
|
61
|
+
if (trigger === 'talk' && ctx.npcName && event.npcName && event.npcName !== ctx.npcName) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return evaluateConditions(state, event.conditions);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function dialogToChoices(dialog: DialogConfig): DialogChoice[] {
|
|
69
|
+
if (!dialog.choices?.length) return [];
|
|
70
|
+
return dialog.choices.map((c, index) => ({
|
|
71
|
+
text: c.text,
|
|
72
|
+
nextId: c.nextId,
|
|
73
|
+
index,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function dialogToResult(dialog: DialogConfig): EventResult {
|
|
78
|
+
return {
|
|
79
|
+
type: 'dialog',
|
|
80
|
+
message: `${dialog.speaker}:「${dialog.text}」`,
|
|
81
|
+
dialogId: dialog.id,
|
|
82
|
+
choices: dialogToChoices(dialog),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function processEventAction(state: GameState, action: MapEventAction): EventResult | null {
|
|
87
|
+
const params = action.params;
|
|
88
|
+
switch (action.type) {
|
|
89
|
+
case 'setFlag': {
|
|
90
|
+
const flag = params.flag as string;
|
|
91
|
+
state.flags[flag] = params.value as boolean | number;
|
|
92
|
+
return { type: 'setFlag', flag, message: '' };
|
|
93
|
+
}
|
|
94
|
+
case 'dialog': {
|
|
95
|
+
const dialogId = params.dialogId as string;
|
|
96
|
+
const dialog = getDialog(dialogId);
|
|
97
|
+
if (!dialog) return { type: 'message', message: '(对话缺失)' };
|
|
98
|
+
return dialogToResult(dialog);
|
|
99
|
+
}
|
|
100
|
+
case 'addItem': {
|
|
101
|
+
const itemId = params.itemId as number;
|
|
102
|
+
const count = (params.count as number) ?? 1;
|
|
103
|
+
const item = getItemById(itemId);
|
|
104
|
+
if (!item) return { type: 'message', message: '(物品缺失)' };
|
|
105
|
+
const existing = state.inventory.items.find((i) => i.name === item.name);
|
|
106
|
+
if (existing) {
|
|
107
|
+
existing.count += count;
|
|
108
|
+
} else {
|
|
109
|
+
state.inventory.items.push({
|
|
110
|
+
id: String(item.id),
|
|
111
|
+
name: item.name,
|
|
112
|
+
count,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return { type: 'addItem', itemName: item.name, message: `获得了${item.name}×${count}` };
|
|
116
|
+
}
|
|
117
|
+
case 'battle': {
|
|
118
|
+
const enemyName = (params.enemyName as string) ?? '山贼';
|
|
119
|
+
return { type: 'battle', enemyName, message: `⚔️ 遭遇${enemyName}!` };
|
|
120
|
+
}
|
|
121
|
+
case 'heal': {
|
|
122
|
+
state.character.hp = state.character.maxHp;
|
|
123
|
+
state.character.mp = state.character.maxMp;
|
|
124
|
+
state.character.stamina = 100;
|
|
125
|
+
state.character.poison = 0;
|
|
126
|
+
state.character.hurt = 0;
|
|
127
|
+
return { type: 'heal', message: '休息完毕,状态全满' };
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function processEvent(state: GameState, event: MapEvent): EventResult[] {
|
|
135
|
+
const results: EventResult[] = [];
|
|
136
|
+
for (const action of event.actions) {
|
|
137
|
+
const result = processEventAction(state, action);
|
|
138
|
+
if (result) {
|
|
139
|
+
if (result.message) results.push(result);
|
|
140
|
+
else if (result.type !== 'setFlag') results.push(result);
|
|
141
|
+
else results.push(result);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return results.filter((r) => r.message || r.type === 'dialog' || r.type === 'battle');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function processDialogChoiceActions(
|
|
148
|
+
state: GameState,
|
|
149
|
+
actions: MapEventAction[],
|
|
150
|
+
): EventResult[] {
|
|
151
|
+
const results: EventResult[] = [];
|
|
152
|
+
for (const action of actions) {
|
|
153
|
+
const result = processEventAction(state, action);
|
|
154
|
+
if (result) results.push(result);
|
|
155
|
+
}
|
|
156
|
+
return results;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function runTriggeredEvents(
|
|
160
|
+
state: GameState,
|
|
161
|
+
trigger: 'auto' | 'interact' | 'talk',
|
|
162
|
+
ctx: EventContext = {},
|
|
163
|
+
): EventResult[] {
|
|
164
|
+
const matched = checkEvents(state, trigger, ctx);
|
|
165
|
+
const allResults: EventResult[] = [];
|
|
166
|
+
for (const event of matched) {
|
|
167
|
+
allResults.push(...processEvent(state, event));
|
|
168
|
+
}
|
|
169
|
+
return allResults;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function formatEventResults(results: EventResult[]): string {
|
|
173
|
+
return results
|
|
174
|
+
.map((r) => r.message)
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
.join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function getInteractEventLabel(event: MapEvent): string {
|
|
180
|
+
for (const action of event.actions) {
|
|
181
|
+
if (action.type === 'dialog') {
|
|
182
|
+
const dialogId = action.params.dialogId as string;
|
|
183
|
+
const dialog = getDialog(dialogId);
|
|
184
|
+
if (dialog) return dialog.text.slice(0, 20);
|
|
185
|
+
}
|
|
186
|
+
if (action.type === 'addItem') return '探索此处';
|
|
187
|
+
}
|
|
188
|
+
return '探索';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function getInteractEventsForMap(mapName: string): MapEvent[] {
|
|
192
|
+
return getMapEvents(mapName).filter((e) => e.triggerType === 'interact');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function hasInteractEvents(mapName: string): boolean {
|
|
196
|
+
return getInteractEventsForMap(mapName).length > 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function getMapDangerHint(mapName: string): string | undefined {
|
|
200
|
+
const map = getMap(mapName);
|
|
201
|
+
if (!map) return undefined;
|
|
202
|
+
if (map.dangerLevel === 'dangerous') return '此地行路需当心,或有歹人埋伏';
|
|
203
|
+
if (map.dangerLevel === 'cautious') return '此地不宜大意,多加留神';
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|