@sdd330dev/jy-skill 0.3.1 → 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 +39 -8
- package/SKILL.md +32 -15
- package/assets/game-config.json +63 -2
- package/assets/templates.json +166 -7
- package/package.json +11 -1
- package/references/agent-handbook.md +47 -9
- package/references/host-adapters.md +182 -0
- package/scripts/build-feishu-card.ts +53 -0
- package/scripts/choice-prompt.ts +179 -0
- package/scripts/config-loader.ts +132 -14
- package/scripts/event-engine.ts +205 -0
- package/scripts/game-engine.ts +430 -13
- package/scripts/game-types.ts +142 -0
- package/scripts/mcp-server.ts +161 -0
- package/scripts/persistence.ts +61 -27
package/scripts/game-engine.ts
CHANGED
|
@@ -28,24 +28,92 @@ import {
|
|
|
28
28
|
getSkillAttackAtLevel,
|
|
29
29
|
getDialog,
|
|
30
30
|
getEnemyTemplate,
|
|
31
|
+
getNpcCard,
|
|
32
|
+
getLocationMeta,
|
|
31
33
|
isWeapon,
|
|
32
34
|
isArmor,
|
|
33
35
|
isConsumable,
|
|
34
36
|
isSkillBook,
|
|
35
37
|
mapDamageTypeToStaminaCost,
|
|
36
38
|
} from './config-loader';
|
|
37
|
-
import type {
|
|
39
|
+
import type {
|
|
40
|
+
GameState,
|
|
41
|
+
BattleEnemy,
|
|
42
|
+
Character,
|
|
43
|
+
TalkResult,
|
|
44
|
+
LocationDetail,
|
|
45
|
+
ActionOption,
|
|
46
|
+
ResolveOptionResult,
|
|
47
|
+
NpcCard,
|
|
48
|
+
NpcContext,
|
|
49
|
+
DialogChoice,
|
|
50
|
+
EventResult,
|
|
51
|
+
PlayerChoicePrompt,
|
|
52
|
+
McpElicitationParams,
|
|
53
|
+
FeishuInteractiveCard,
|
|
54
|
+
} from './game-types';
|
|
55
|
+
import {
|
|
56
|
+
runTriggeredEvents,
|
|
57
|
+
getInteractEventsForMap,
|
|
58
|
+
getInteractEventLabel,
|
|
59
|
+
getMapDangerHint,
|
|
60
|
+
processDialogChoiceActions,
|
|
61
|
+
evaluateConditions,
|
|
62
|
+
} from './event-engine';
|
|
38
63
|
import {
|
|
39
64
|
loadGameState as persistenceLoadGameState,
|
|
40
65
|
saveGameState,
|
|
41
66
|
deleteSave,
|
|
42
67
|
loadOrCreateGame as persistenceLoadOrCreateGame,
|
|
68
|
+
loadOrCreateGameForUser as persistenceLoadOrCreateGameForUser,
|
|
43
69
|
getSavePath,
|
|
70
|
+
setSaveUserId,
|
|
71
|
+
getSaveUserId,
|
|
44
72
|
} from './persistence';
|
|
45
73
|
import type { LoadGameResult } from './persistence';
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
import {
|
|
75
|
+
buildChoicePrompt as buildChoicePromptFromOptions,
|
|
76
|
+
toMcpElicitationParams,
|
|
77
|
+
fromElicitationResponse,
|
|
78
|
+
toFeishuInteractiveCard,
|
|
79
|
+
feishuCardToMessageContent,
|
|
80
|
+
parseFeishuButtonValue,
|
|
81
|
+
parseDialogChoiceValue,
|
|
82
|
+
parsePaginationValue,
|
|
83
|
+
isPaginationValue,
|
|
84
|
+
type BuildChoicePromptContext,
|
|
85
|
+
} from './choice-prompt';
|
|
86
|
+
|
|
87
|
+
export type {
|
|
88
|
+
GameState,
|
|
89
|
+
BattleEnemy,
|
|
90
|
+
Character,
|
|
91
|
+
LoadGameResult,
|
|
92
|
+
TalkResult,
|
|
93
|
+
LocationDetail,
|
|
94
|
+
ActionOption,
|
|
95
|
+
ResolveOptionResult,
|
|
96
|
+
NpcCard,
|
|
97
|
+
NpcContext,
|
|
98
|
+
DialogChoice,
|
|
99
|
+
EventResult,
|
|
100
|
+
PlayerChoicePrompt,
|
|
101
|
+
McpElicitationParams,
|
|
102
|
+
FeishuInteractiveCard,
|
|
103
|
+
};
|
|
104
|
+
export {
|
|
105
|
+
saveGameState,
|
|
106
|
+
deleteSave,
|
|
107
|
+
getSavePath,
|
|
108
|
+
setSaveUserId,
|
|
109
|
+
getSaveUserId,
|
|
110
|
+
toMcpElicitationParams,
|
|
111
|
+
fromElicitationResponse,
|
|
112
|
+
toFeishuInteractiveCard,
|
|
113
|
+
feishuCardToMessageContent,
|
|
114
|
+
parseFeishuButtonValue,
|
|
115
|
+
};
|
|
116
|
+
export type { BuildChoicePromptContext };
|
|
49
117
|
|
|
50
118
|
initConfigs();
|
|
51
119
|
|
|
@@ -119,7 +187,7 @@ function buildCharacter(name: string): Character {
|
|
|
119
187
|
|
|
120
188
|
export function createNewGame(name: string): GameState {
|
|
121
189
|
const tpl = getTemplates();
|
|
122
|
-
|
|
190
|
+
const state: GameState = {
|
|
123
191
|
character: buildCharacter(name),
|
|
124
192
|
team: [],
|
|
125
193
|
inventory: {
|
|
@@ -132,6 +200,9 @@ export function createNewGame(name: string): GameState {
|
|
|
132
200
|
visitedMaps: [tpl.startLocation ?? '小村'],
|
|
133
201
|
completedQuests: [],
|
|
134
202
|
};
|
|
203
|
+
runTriggeredEvents(state, 'auto', { mapName: state.location });
|
|
204
|
+
saveGameState(state);
|
|
205
|
+
return state;
|
|
135
206
|
}
|
|
136
207
|
|
|
137
208
|
export function loadGameState(): GameState | null {
|
|
@@ -175,11 +246,64 @@ export function getStatus(state: GameState): string {
|
|
|
175
246
|
.join('\n');
|
|
176
247
|
}
|
|
177
248
|
|
|
249
|
+
function personaToString(persona: NpcCard['persona']): string {
|
|
250
|
+
if (typeof persona === 'string') return persona;
|
|
251
|
+
const parts = [persona.archetype, persona.tone].filter(Boolean);
|
|
252
|
+
return parts.join(',') || '';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildNpcCard(location: string, npcName: string): NpcCard | undefined {
|
|
256
|
+
return getNpcCard(location, npcName);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function dialogToChoices(dialogId: string): DialogChoice[] {
|
|
260
|
+
const dialog = getDialog(dialogId);
|
|
261
|
+
if (!dialog?.choices?.length) return [];
|
|
262
|
+
return dialog.choices.map((c, index) => ({
|
|
263
|
+
text: c.text,
|
|
264
|
+
nextId: c.nextId,
|
|
265
|
+
index,
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function getLocationDetail(state: GameState): LocationDetail {
|
|
270
|
+
const map = getMap(state.location);
|
|
271
|
+
const meta = getLocationMeta(state.location);
|
|
272
|
+
if (!map) {
|
|
273
|
+
return {
|
|
274
|
+
name: state.location,
|
|
275
|
+
description: meta.description,
|
|
276
|
+
atmosphere: meta.atmosphere,
|
|
277
|
+
dangerLevel: meta.dangerLevel,
|
|
278
|
+
connections: [],
|
|
279
|
+
npcs: [],
|
|
280
|
+
shops: [],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
name: state.location,
|
|
286
|
+
description: meta.description,
|
|
287
|
+
atmosphere: meta.atmosphere,
|
|
288
|
+
dangerLevel: meta.dangerLevel,
|
|
289
|
+
connections: [...map.connections],
|
|
290
|
+
npcs: map.npcs.map((name) => {
|
|
291
|
+
const card = getNpcCard(state.location, name);
|
|
292
|
+
return { name, persona: card ? personaToString(card.persona) : undefined };
|
|
293
|
+
}),
|
|
294
|
+
shops: [...map.shops],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
178
298
|
export function getLocationInfo(state: GameState): string {
|
|
179
299
|
const map = getMap(state.location);
|
|
180
300
|
if (!map) return '当前位置未知';
|
|
181
301
|
|
|
302
|
+
const detail = getLocationDetail(state);
|
|
182
303
|
const lines = [`📍 ${state.location}`];
|
|
304
|
+
if (detail.description) {
|
|
305
|
+
lines.push(detail.description);
|
|
306
|
+
}
|
|
183
307
|
if (map.connections.length > 0) {
|
|
184
308
|
lines.push(`可前往: ${map.connections.join('、')}`);
|
|
185
309
|
}
|
|
@@ -189,12 +313,224 @@ export function getLocationInfo(state: GameState): string {
|
|
|
189
313
|
if (map.shops.length > 0) {
|
|
190
314
|
lines.push(`可购: ${map.shops.join('、')}`);
|
|
191
315
|
}
|
|
192
|
-
|
|
193
|
-
|
|
316
|
+
const dangerHint = getMapDangerHint(state.location);
|
|
317
|
+
if (dangerHint) {
|
|
318
|
+
lines.push(dangerHint);
|
|
194
319
|
}
|
|
195
320
|
return lines.join('\n');
|
|
196
321
|
}
|
|
197
322
|
|
|
323
|
+
export function getOptions(state: GameState): ActionOption[] {
|
|
324
|
+
const alive = assertAlive(state);
|
|
325
|
+
if (!alive.ok) {
|
|
326
|
+
return [{ id: 'status', label: '查看状态', category: 'status' }];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const map = getMap(state.location);
|
|
330
|
+
if (!map) {
|
|
331
|
+
return [{ id: 'status', label: '查看状态', category: 'status' }];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const options: ActionOption[] = [];
|
|
335
|
+
|
|
336
|
+
for (const npc of map.npcs) {
|
|
337
|
+
options.push({
|
|
338
|
+
id: `talk_${npc}`,
|
|
339
|
+
label: `和${npc}交谈`,
|
|
340
|
+
category: 'talk',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const conn of map.connections) {
|
|
345
|
+
options.push({
|
|
346
|
+
id: `move_${conn}`,
|
|
347
|
+
label: `前往${conn}`,
|
|
348
|
+
category: 'move',
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
for (const itemName of map.shops) {
|
|
353
|
+
const itemCfg = getItem(itemName);
|
|
354
|
+
const canAfford = state.inventory.silver >= (itemCfg?.price ?? Infinity);
|
|
355
|
+
options.push({
|
|
356
|
+
id: `buy_${itemName}`,
|
|
357
|
+
label: `购买${itemName}`,
|
|
358
|
+
category: 'shop',
|
|
359
|
+
hint: canAfford ? undefined : `(需${itemCfg?.price}两,现有${state.inventory.silver}两)`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const event of getInteractEventsForMap(state.location)) {
|
|
364
|
+
if (evaluateConditions(state, event.conditions)) {
|
|
365
|
+
options.push({
|
|
366
|
+
id: `interact_${event.id}`,
|
|
367
|
+
label: getInteractEventLabel(event),
|
|
368
|
+
category: 'interact',
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
options.push(
|
|
374
|
+
{ id: 'explore', label: '四处看看', category: 'explore' },
|
|
375
|
+
{ id: 'rest', label: '休息恢复', category: 'rest' },
|
|
376
|
+
{ id: 'status', label: '查看状态', category: 'status' },
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
return options;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function resolveOption(state: GameState, optionId: string): ResolveOptionResult {
|
|
383
|
+
if (isPaginationValue(optionId)) {
|
|
384
|
+
const page = parsePaginationValue(optionId);
|
|
385
|
+
if (page != null) {
|
|
386
|
+
return {
|
|
387
|
+
action: 'paginate',
|
|
388
|
+
result: buildChoicePrompt(state, { page }),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const dialogRef = parseDialogChoiceValue(optionId);
|
|
394
|
+
if (dialogRef) {
|
|
395
|
+
return {
|
|
396
|
+
action: 'dialog',
|
|
397
|
+
result: chooseDialog(state, dialogRef.dialogId, dialogRef.index),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (optionId.startsWith('talk_')) {
|
|
402
|
+
const npcName = optionId.slice('talk_'.length);
|
|
403
|
+
return { action: 'talk', result: talkTo(state, npcName) };
|
|
404
|
+
}
|
|
405
|
+
if (optionId.startsWith('move_')) {
|
|
406
|
+
const dest = optionId.slice('move_'.length);
|
|
407
|
+
return { action: 'move', result: moveTo(state, dest) };
|
|
408
|
+
}
|
|
409
|
+
if (optionId.startsWith('buy_')) {
|
|
410
|
+
const itemName = optionId.slice('buy_'.length);
|
|
411
|
+
return { action: 'buy', result: buyItem(state, itemName) };
|
|
412
|
+
}
|
|
413
|
+
if (optionId.startsWith('interact_')) {
|
|
414
|
+
const eventId = optionId.slice('interact_'.length);
|
|
415
|
+
const events = runTriggeredEvents(state, 'interact', { mapName: state.location, eventId });
|
|
416
|
+
autoSave(state);
|
|
417
|
+
const message = events
|
|
418
|
+
.map((e) => e.message)
|
|
419
|
+
.filter(Boolean)
|
|
420
|
+
.join('\n');
|
|
421
|
+
const battle = events.find((e) => e.type === 'battle');
|
|
422
|
+
return {
|
|
423
|
+
action: 'interact',
|
|
424
|
+
result: {
|
|
425
|
+
success: true,
|
|
426
|
+
message: message || '你探索了一番。',
|
|
427
|
+
events,
|
|
428
|
+
encounter: battle?.enemyName,
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (optionId === 'explore') {
|
|
433
|
+
return {
|
|
434
|
+
action: 'explore',
|
|
435
|
+
result: { success: true, message: getLocationInfo(state) },
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (optionId === 'rest') {
|
|
439
|
+
return { action: 'rest', result: rest(state) };
|
|
440
|
+
}
|
|
441
|
+
if (optionId === 'status') {
|
|
442
|
+
return { action: 'status', result: getStatus(state) };
|
|
443
|
+
}
|
|
444
|
+
return { action: 'unknown', result: { success: false, message: `未知选项:${optionId}` } };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function buildChoicePrompt(
|
|
448
|
+
state: GameState,
|
|
449
|
+
ctx: BuildChoicePromptContext = {},
|
|
450
|
+
): PlayerChoicePrompt {
|
|
451
|
+
return buildChoicePromptFromOptions(getOptions(state), ctx);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function buildChoicePromptFromTalk(
|
|
455
|
+
state: GameState,
|
|
456
|
+
talkResult: TalkResult,
|
|
457
|
+
message?: string,
|
|
458
|
+
): PlayerChoicePrompt {
|
|
459
|
+
if (talkResult.choices?.length && talkResult.dialogId) {
|
|
460
|
+
return buildChoicePromptFromOptions(getOptions(state), {
|
|
461
|
+
message: message ?? talkResult.message,
|
|
462
|
+
dialogChoices: talkResult.choices,
|
|
463
|
+
dialogId: talkResult.dialogId,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return buildChoicePrompt(state, { message: message ?? talkResult.message });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function loadOrCreateGameForUser(userId: string, name = '主角'): LoadGameResult {
|
|
470
|
+
return persistenceLoadOrCreateGameForUser(userId, createNewGame, name);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function getNpcContext(state: GameState, npcName: string): NpcContext | null {
|
|
474
|
+
const map = getMap(state.location);
|
|
475
|
+
if (!map || !map.npcs.includes(npcName)) return null;
|
|
476
|
+
|
|
477
|
+
const card = buildNpcCard(state.location, npcName);
|
|
478
|
+
if (!card) return null;
|
|
479
|
+
|
|
480
|
+
const char = state.character;
|
|
481
|
+
const availableActions: NpcContext['availableActions'] = ['talk'];
|
|
482
|
+
const constraints: string[] = [
|
|
483
|
+
`你是${card.name}${card.title ? `(${card.title})` : ''},必须保持角色性格一致。`,
|
|
484
|
+
'不可编造引擎未确认的物品给予、武功传授或任务完成。',
|
|
485
|
+
];
|
|
486
|
+
|
|
487
|
+
if (typeof card.persona === 'object') {
|
|
488
|
+
if (card.persona.tone) constraints.push(`说话风格:${card.persona.tone}`);
|
|
489
|
+
if (card.persona.dislikes?.length) {
|
|
490
|
+
constraints.push(`厌恶:${card.persona.dislikes.join('、')}`);
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
constraints.push(`性格:${card.persona}`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (card.canTeach?.length) {
|
|
497
|
+
for (const skill of card.canTeach) {
|
|
498
|
+
const cond = card.conditions?.teach;
|
|
499
|
+
const levelOk = !cond?.minLevel || char.level >= cond.minLevel;
|
|
500
|
+
const iqOk = !cond?.minIQ || char.attributes.iq >= cond.minIQ;
|
|
501
|
+
if (levelOk && iqOk && !char.skills.includes(skill)) {
|
|
502
|
+
availableActions.push('teach');
|
|
503
|
+
constraints.push(`可传授${skill}(需调用 learnSkill 落实,不可口头编造)。`);
|
|
504
|
+
} else if (!levelOk || !iqOk) {
|
|
505
|
+
constraints.push(
|
|
506
|
+
`传授${skill}需等级${cond?.minLevel ?? 1}${cond?.minIQ ? `、资质${cond.minIQ}` : ''},当前未达标,不得声称已传授。`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (card.canGive?.length) {
|
|
513
|
+
availableActions.push('give');
|
|
514
|
+
constraints.push(`可给予物品:${card.canGive.join('、')}(须引擎确认)。`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (card.canHelp?.length || card.knowledge.length > 0) {
|
|
518
|
+
availableActions.push('quest');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
card,
|
|
523
|
+
playerRelation: {
|
|
524
|
+
level: char.level,
|
|
525
|
+
iq: char.attributes.iq,
|
|
526
|
+
flags: { ...state.flags },
|
|
527
|
+
inventory: state.inventory.items.map((i) => i.name),
|
|
528
|
+
},
|
|
529
|
+
availableActions: [...new Set(availableActions)],
|
|
530
|
+
constraints,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
198
534
|
export function getInventory(state: GameState): string {
|
|
199
535
|
const items = state.inventory.items;
|
|
200
536
|
if (items.length === 0) return `💰 银两: ${state.inventory.silver}\n\n📦 背包空空如也`;
|
|
@@ -223,7 +559,13 @@ export function getSkills(state: GameState): string {
|
|
|
223
559
|
export function moveTo(
|
|
224
560
|
state: GameState,
|
|
225
561
|
destination: string,
|
|
226
|
-
): {
|
|
562
|
+
): {
|
|
563
|
+
success: boolean;
|
|
564
|
+
message: string;
|
|
565
|
+
encounter?: string;
|
|
566
|
+
events?: EventResult[];
|
|
567
|
+
locationDetail?: LocationDetail;
|
|
568
|
+
} {
|
|
227
569
|
const alive = assertAlive(state);
|
|
228
570
|
if (!alive.ok) return { success: false, message: alive.message };
|
|
229
571
|
|
|
@@ -269,20 +611,30 @@ export function moveTo(
|
|
|
269
611
|
encounter = pool[Math.floor(Math.random() * pool.length)];
|
|
270
612
|
}
|
|
271
613
|
|
|
614
|
+
const events = runTriggeredEvents(state, 'auto', { mapName: destination });
|
|
615
|
+
const locationDetail = getLocationDetail(state);
|
|
616
|
+
|
|
272
617
|
autoSave(state);
|
|
273
618
|
|
|
274
|
-
|
|
619
|
+
const meta = getLocationMeta(destination);
|
|
620
|
+
let message = meta.description
|
|
621
|
+
? `你来到了${destination}。${meta.description}`
|
|
622
|
+
: `你来到了${destination}`;
|
|
623
|
+
const eventMessages = events.map((e) => e.message).filter(Boolean);
|
|
624
|
+
if (eventMessages.length > 0) {
|
|
625
|
+
message += `\n${eventMessages.join('\n')}`;
|
|
626
|
+
}
|
|
275
627
|
if (encounter) {
|
|
276
628
|
message += `。暗处传来脚步声——似乎有${encounter}埋伏!`;
|
|
277
629
|
}
|
|
278
|
-
return { success: true, message, encounter };
|
|
630
|
+
return { success: true, message, encounter, events, locationDetail };
|
|
279
631
|
}
|
|
280
632
|
|
|
281
633
|
// ============================================================================
|
|
282
634
|
// NPC 交互
|
|
283
635
|
// ============================================================================
|
|
284
636
|
|
|
285
|
-
export function talkTo(state: GameState, npcName: string):
|
|
637
|
+
export function talkTo(state: GameState, npcName: string): TalkResult {
|
|
286
638
|
const map = getMap(state.location);
|
|
287
639
|
if (!map) return { success: false, message: '当前位置未知' };
|
|
288
640
|
|
|
@@ -295,15 +647,80 @@ export function talkTo(state: GameState, npcName: string): { success: boolean; m
|
|
|
295
647
|
return { success: false, message: `${state.location}没有${npcName}` };
|
|
296
648
|
}
|
|
297
649
|
|
|
650
|
+
const npc = buildNpcCard(state.location, npcName);
|
|
651
|
+
const context = getNpcContext(state, npcName) ?? undefined;
|
|
652
|
+
|
|
653
|
+
const talkEvents = runTriggeredEvents(state, 'talk', {
|
|
654
|
+
mapName: state.location,
|
|
655
|
+
npcName,
|
|
656
|
+
});
|
|
657
|
+
|
|
298
658
|
const dialogId = map.npcDialogs[npcName];
|
|
299
659
|
if (dialogId) {
|
|
300
660
|
const dialog = getDialog(dialogId);
|
|
301
661
|
if (dialog) {
|
|
302
|
-
|
|
662
|
+
autoSave(state);
|
|
663
|
+
return {
|
|
664
|
+
success: true,
|
|
665
|
+
message: `${dialog.speaker}:「${dialog.text}」`,
|
|
666
|
+
npc,
|
|
667
|
+
context,
|
|
668
|
+
dialogId,
|
|
669
|
+
choices: dialogToChoices(dialogId),
|
|
670
|
+
events: talkEvents.length > 0 ? talkEvents : undefined,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
success: true,
|
|
677
|
+
message: `你和${npcName}聊了起来`,
|
|
678
|
+
npc,
|
|
679
|
+
context,
|
|
680
|
+
events: talkEvents.length > 0 ? talkEvents : undefined,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export function chooseDialog(state: GameState, dialogId: string, choiceIndex: number): TalkResult {
|
|
685
|
+
const dialog = getDialog(dialogId);
|
|
686
|
+
if (!dialog) {
|
|
687
|
+
return { success: false, message: '对话不存在' };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const choice = dialog.choices?.[choiceIndex];
|
|
691
|
+
if (!choice) {
|
|
692
|
+
return { success: false, message: '无效的选项' };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const actionResults: EventResult[] = [];
|
|
696
|
+
if (choice.actions?.length) {
|
|
697
|
+
actionResults.push(...processDialogChoiceActions(state, choice.actions));
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (choice.nextId) {
|
|
701
|
+
const nextDialog = getDialog(choice.nextId);
|
|
702
|
+
if (nextDialog) {
|
|
703
|
+
autoSave(state);
|
|
704
|
+
return {
|
|
705
|
+
success: true,
|
|
706
|
+
message: `${nextDialog.speaker}:「${nextDialog.text}」`,
|
|
707
|
+
dialogId: choice.nextId,
|
|
708
|
+
choices: dialogToChoices(choice.nextId),
|
|
709
|
+
events: actionResults.length > 0 ? actionResults : undefined,
|
|
710
|
+
};
|
|
303
711
|
}
|
|
304
712
|
}
|
|
305
713
|
|
|
306
|
-
|
|
714
|
+
autoSave(state);
|
|
715
|
+
const actionMsg = actionResults
|
|
716
|
+
.map((r) => r.message)
|
|
717
|
+
.filter(Boolean)
|
|
718
|
+
.join('\n');
|
|
719
|
+
return {
|
|
720
|
+
success: true,
|
|
721
|
+
message: actionMsg || '你结束了对话。',
|
|
722
|
+
events: actionResults.length > 0 ? actionResults : undefined,
|
|
723
|
+
};
|
|
307
724
|
}
|
|
308
725
|
|
|
309
726
|
// ============================================================================
|
package/scripts/game-types.ts
CHANGED
|
@@ -51,3 +51,145 @@ export interface BattleEnemy {
|
|
|
51
51
|
attack: number;
|
|
52
52
|
defence: number;
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
export interface NpcPersonaDetail {
|
|
56
|
+
archetype?: string;
|
|
57
|
+
tone?: string;
|
|
58
|
+
likes?: string[];
|
|
59
|
+
dislikes?: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface NpcTeachCondition {
|
|
63
|
+
minLevel?: number;
|
|
64
|
+
minIQ?: number;
|
|
65
|
+
flag?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface NpcCard {
|
|
69
|
+
name: string;
|
|
70
|
+
title?: string;
|
|
71
|
+
persona: string | NpcPersonaDetail;
|
|
72
|
+
knowledge: string[];
|
|
73
|
+
canHelp?: string[];
|
|
74
|
+
isShop?: boolean;
|
|
75
|
+
canTeach?: string[];
|
|
76
|
+
canGive?: string[];
|
|
77
|
+
conditions?: {
|
|
78
|
+
teach?: NpcTeachCondition;
|
|
79
|
+
give?: NpcTeachCondition;
|
|
80
|
+
quest?: NpcTeachCondition;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface DialogChoice {
|
|
85
|
+
text: string;
|
|
86
|
+
nextId: string;
|
|
87
|
+
index: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface EventResult {
|
|
91
|
+
type: 'dialog' | 'setFlag' | 'addItem' | 'message' | 'battle' | 'heal';
|
|
92
|
+
message?: string;
|
|
93
|
+
dialogId?: string;
|
|
94
|
+
choices?: DialogChoice[];
|
|
95
|
+
flag?: string;
|
|
96
|
+
itemName?: string;
|
|
97
|
+
enemyName?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ActionOption {
|
|
101
|
+
id: string;
|
|
102
|
+
label: string;
|
|
103
|
+
category: 'talk' | 'move' | 'shop' | 'explore' | 'rest' | 'status' | 'interact';
|
|
104
|
+
hint?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface TalkResult {
|
|
108
|
+
success: boolean;
|
|
109
|
+
message: string;
|
|
110
|
+
npc?: NpcCard;
|
|
111
|
+
choices?: DialogChoice[];
|
|
112
|
+
events?: EventResult[];
|
|
113
|
+
dialogId?: string;
|
|
114
|
+
context?: NpcContext;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface LocationDetail {
|
|
118
|
+
name: string;
|
|
119
|
+
description: string;
|
|
120
|
+
atmosphere: string;
|
|
121
|
+
dangerLevel: 'safe' | 'cautious' | 'dangerous';
|
|
122
|
+
connections: string[];
|
|
123
|
+
npcs: Array<{ name: string; persona?: string }>;
|
|
124
|
+
shops: string[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface NpcContext {
|
|
128
|
+
card: NpcCard;
|
|
129
|
+
playerRelation: {
|
|
130
|
+
level: number;
|
|
131
|
+
iq: number;
|
|
132
|
+
flags: Record<string, boolean | number>;
|
|
133
|
+
inventory: string[];
|
|
134
|
+
};
|
|
135
|
+
availableActions: Array<'teach' | 'give' | 'quest' | 'talk'>;
|
|
136
|
+
constraints: string[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ResolveOptionResult {
|
|
140
|
+
action: string;
|
|
141
|
+
result:
|
|
142
|
+
| TalkResult
|
|
143
|
+
| {
|
|
144
|
+
success: boolean;
|
|
145
|
+
message: string;
|
|
146
|
+
encounter?: string;
|
|
147
|
+
events?: EventResult[];
|
|
148
|
+
locationDetail?: LocationDetail;
|
|
149
|
+
}
|
|
150
|
+
| { success: boolean; message: string }
|
|
151
|
+
| string
|
|
152
|
+
| { events: EventResult[] }
|
|
153
|
+
| PlayerChoicePrompt;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface PlayerChoiceItem {
|
|
157
|
+
value: string;
|
|
158
|
+
label: string;
|
|
159
|
+
description?: string;
|
|
160
|
+
category?: ActionOption['category'] | 'nav';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface PlayerChoicePrompt {
|
|
164
|
+
type: 'player_choice';
|
|
165
|
+
message: string;
|
|
166
|
+
choices: PlayerChoiceItem[];
|
|
167
|
+
dialogChoices?: DialogChoice[];
|
|
168
|
+
dialogId?: string;
|
|
169
|
+
page?: number;
|
|
170
|
+
hasMore?: boolean;
|
|
171
|
+
totalPages?: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface McpElicitationParams {
|
|
175
|
+
mode: 'form';
|
|
176
|
+
message: string;
|
|
177
|
+
requestedSchema: {
|
|
178
|
+
type: 'object';
|
|
179
|
+
properties: {
|
|
180
|
+
action: {
|
|
181
|
+
type: 'string';
|
|
182
|
+
title: string;
|
|
183
|
+
description?: string;
|
|
184
|
+
oneOf: Array<{ const: string; title: string }>;
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
required: ['action'];
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface FeishuInteractiveCard {
|
|
192
|
+
config: { wide_screen_mode: boolean };
|
|
193
|
+
header?: { title: { tag: 'plain_text'; content: string } };
|
|
194
|
+
elements: Array<Record<string, unknown>>;
|
|
195
|
+
}
|