@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/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 {
|
|
@@ -64,10 +99,17 @@ export interface CharacterConfig {
|
|
|
64
99
|
source: string;
|
|
65
100
|
}
|
|
66
101
|
|
|
102
|
+
export interface DialogChoiceConfig {
|
|
103
|
+
text: string;
|
|
104
|
+
nextId: string;
|
|
105
|
+
actions?: MapEventAction[];
|
|
106
|
+
}
|
|
107
|
+
|
|
67
108
|
export interface DialogConfig {
|
|
68
109
|
id: string;
|
|
69
110
|
speaker: string;
|
|
70
111
|
text: string;
|
|
112
|
+
choices?: DialogChoiceConfig[];
|
|
71
113
|
}
|
|
72
114
|
|
|
73
115
|
export interface EnemyTemplate {
|
|
@@ -79,6 +121,17 @@ export interface EnemyTemplate {
|
|
|
79
121
|
onHitHurt?: number;
|
|
80
122
|
}
|
|
81
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>;
|
|
133
|
+
}
|
|
134
|
+
|
|
82
135
|
export interface GameTemplates {
|
|
83
136
|
defaultCharacter: {
|
|
84
137
|
name: string;
|
|
@@ -90,15 +143,7 @@ export interface GameTemplates {
|
|
|
90
143
|
};
|
|
91
144
|
startLocation: string;
|
|
92
145
|
enemies: Record<string, EnemyTemplate | { characterId: number }>;
|
|
93
|
-
maps?: Record<
|
|
94
|
-
string,
|
|
95
|
-
{
|
|
96
|
-
npcs: string[];
|
|
97
|
-
shops: string[];
|
|
98
|
-
connections: string[];
|
|
99
|
-
encounters?: number | { rate: number; enemies: string[] };
|
|
100
|
-
}
|
|
101
|
-
>;
|
|
146
|
+
maps?: Record<string, TemplateMapEntry>;
|
|
102
147
|
}
|
|
103
148
|
|
|
104
149
|
function parseEncounters(
|
|
@@ -111,6 +156,17 @@ function parseEncounters(
|
|
|
111
156
|
return { encounterRate: encounters.rate, encounterEnemies: [...encounters.enemies] };
|
|
112
157
|
}
|
|
113
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
|
+
|
|
114
170
|
// ============================================================================
|
|
115
171
|
// 加载
|
|
116
172
|
// ============================================================================
|
|
@@ -147,6 +203,7 @@ function buildMapsFromConfig(): void {
|
|
|
147
203
|
maps: Array<{
|
|
148
204
|
id: number;
|
|
149
205
|
name: string;
|
|
206
|
+
desc?: string;
|
|
150
207
|
npcs: Array<{
|
|
151
208
|
roleId: number;
|
|
152
209
|
dialogId: string;
|
|
@@ -154,13 +211,14 @@ function buildMapsFromConfig(): void {
|
|
|
154
211
|
shopItems: number[];
|
|
155
212
|
}>;
|
|
156
213
|
connections: Array<{ targetMapId: number }>;
|
|
214
|
+
events?: MapEvent[];
|
|
157
215
|
}>;
|
|
158
216
|
dialogs: Record<string, DialogConfig>;
|
|
159
217
|
}>('game-config.json');
|
|
160
218
|
|
|
161
219
|
const mapIdToName = new Map<number, string>();
|
|
162
220
|
for (const map of gameConfig.maps) {
|
|
163
|
-
mapIdToName.set(map.id, map.name);
|
|
221
|
+
mapIdToName.set(map.id, normalizeMapName(map.name));
|
|
164
222
|
}
|
|
165
223
|
|
|
166
224
|
for (const [id, dialog] of Object.entries(gameConfig.dialogs)) {
|
|
@@ -170,7 +228,8 @@ function buildMapsFromConfig(): void {
|
|
|
170
228
|
const templateMaps = templates.maps ?? {};
|
|
171
229
|
|
|
172
230
|
for (const map of gameConfig.maps) {
|
|
173
|
-
const
|
|
231
|
+
const mapName = normalizeMapName(map.name);
|
|
232
|
+
const templateMap = templateMaps[mapName as keyof typeof templateMaps];
|
|
174
233
|
const npcDialogs: Record<string, string> = {};
|
|
175
234
|
const npcNames: string[] = [];
|
|
176
235
|
const shopNames = new Set<string>();
|
|
@@ -193,18 +252,25 @@ function buildMapsFromConfig(): void {
|
|
|
193
252
|
? [...templateMap.connections]
|
|
194
253
|
: map.connections
|
|
195
254
|
.map((c) => mapIdToName.get(c.targetMapId))
|
|
196
|
-
.filter((name): name is string => Boolean(name))
|
|
255
|
+
.filter((name): name is string => Boolean(name))
|
|
256
|
+
.map(normalizeMapName);
|
|
197
257
|
|
|
198
258
|
const shops =
|
|
199
259
|
templateMap && templateMap.shops?.length > 0 ? [...templateMap.shops] : [...shopNames];
|
|
200
260
|
|
|
201
261
|
const encounterConfig = parseEncounters(templateMap?.encounters);
|
|
262
|
+
const events = map.events ?? [];
|
|
202
263
|
|
|
203
|
-
mapsByName.set(
|
|
264
|
+
mapsByName.set(mapName, {
|
|
204
265
|
npcs: templateMap?.npcs?.length ? [...templateMap.npcs] : npcNames,
|
|
205
266
|
shops,
|
|
206
267
|
connections,
|
|
207
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,
|
|
208
274
|
...encounterConfig,
|
|
209
275
|
});
|
|
210
276
|
}
|
|
@@ -218,6 +284,10 @@ function buildMapsFromConfig(): void {
|
|
|
218
284
|
shops: [...map.shops],
|
|
219
285
|
connections: [...map.connections],
|
|
220
286
|
npcDialogs: {},
|
|
287
|
+
description: map.description,
|
|
288
|
+
atmosphere: map.atmosphere,
|
|
289
|
+
dangerLevel: deriveDangerLevel(map, encounterConfig, []),
|
|
290
|
+
npcCards: map.npcCards ? { ...map.npcCards } : undefined,
|
|
221
291
|
...encounterConfig,
|
|
222
292
|
});
|
|
223
293
|
}
|
|
@@ -306,7 +376,7 @@ export function getTemplates(): GameTemplates {
|
|
|
306
376
|
|
|
307
377
|
export function getMap(name: string): MapInfo | undefined {
|
|
308
378
|
initConfigs();
|
|
309
|
-
return mapsByName.get(name);
|
|
379
|
+
return mapsByName.get(normalizeMapName(name));
|
|
310
380
|
}
|
|
311
381
|
|
|
312
382
|
export function getAllMapNames(): string[] {
|
|
@@ -319,6 +389,11 @@ export function getItem(name: string): ItemConfig | undefined {
|
|
|
319
389
|
return itemsByName.get(name);
|
|
320
390
|
}
|
|
321
391
|
|
|
392
|
+
export function getItemById(id: number): ItemConfig | undefined {
|
|
393
|
+
initConfigs();
|
|
394
|
+
return itemsById.get(id);
|
|
395
|
+
}
|
|
396
|
+
|
|
322
397
|
export function getSkill(name: string): SkillConfig | undefined {
|
|
323
398
|
initConfigs();
|
|
324
399
|
return skillsByName.get(name);
|
|
@@ -362,6 +437,42 @@ export function getDialog(dialogId: string): DialogConfig | undefined {
|
|
|
362
437
|
return dialogsById.get(dialogId);
|
|
363
438
|
}
|
|
364
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
|
+
|
|
365
476
|
export function getEnemyTemplate(enemyName: string): EnemyTemplate | undefined {
|
|
366
477
|
initConfigs();
|
|
367
478
|
const entry = templates.enemies[enemyName];
|
|
@@ -437,6 +548,13 @@ export function validateAssets(): string[] {
|
|
|
437
548
|
errors.push(`Map "${mapName}" connection target unknown: ${conn}`);
|
|
438
549
|
}
|
|
439
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
|
+
}
|
|
440
558
|
}
|
|
441
559
|
|
|
442
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
|
+
}
|