This commit is contained in:
2026-04-21 19:18:26 +08:00
parent 4372ab5be1
commit 48957311bc
78 changed files with 643 additions and 3801 deletions

View File

@@ -1,254 +0,0 @@
import { Character, ItemCatalogOverride, WorldType } from '../types';
import { CharacterPresetOverride } from './characterPresets';
import { MonsterPreset, MonsterPresetOverride } from './hostileNpcPresets';
import { SceneNpcPresetOverride, ScenePreset, ScenePresetOverride } from './scenePresets';
function pushError(errors: string[], message: string) {
errors.push(message);
}
function isPositiveNumber(value: number | undefined) {
return typeof value === 'number' && Number.isFinite(value) && value > 0;
}
function isKnownGender(value: unknown): value is 'male' | 'female' {
return value === 'male' || value === 'female';
}
function isNonEmptyStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === 'string' && item.trim().length > 0);
}
function validateBuildBuffs(errors: string[], ownerId: string, label: string, buffs: unknown) {
if (!Array.isArray(buffs)) {
pushError(errors, `${ownerId} ${label} must be an array.`);
return;
}
buffs.forEach((buff, index) => {
if (!buff || typeof buff !== 'object') {
pushError(errors, `${ownerId} ${label}[${index}] must be an object.`);
return;
}
const typedBuff = buff as {
name?: unknown;
tags?: unknown;
durationTurns?: unknown;
};
if (typeof typedBuff.name !== 'string' || !typedBuff.name.trim()) {
pushError(errors, `${ownerId} ${label}[${index}] is missing a valid name.`);
}
if (!isNonEmptyStringArray(typedBuff.tags)) {
pushError(errors, `${ownerId} ${label}[${index}].tags must be a non-empty string array.`);
}
if (typeof typedBuff.durationTurns !== 'number' || !Number.isFinite(typedBuff.durationTurns) || typedBuff.durationTurns <= 0) {
pushError(errors, `${ownerId} ${label}[${index}].durationTurns must be > 0.`);
}
});
}
export function validateCharacterOverrides(
overrideMap: Record<string, CharacterPresetOverride>,
characters: Character[],
scenesByWorld: Partial<Record<WorldType, Array<Pick<ScenePreset, 'id'>>>>,
) {
const errors: string[] = [];
const validCharacterIds = new Set(characters.map(character => character.id));
const validSceneIdsByWorld = {
[WorldType.WUXIA]: new Set((scenesByWorld[WorldType.WUXIA] ?? []).map(scene => scene.id)),
[WorldType.XIANXIA]: new Set((scenesByWorld[WorldType.XIANXIA] ?? []).map(scene => scene.id)),
[WorldType.CUSTOM]: new Set((scenesByWorld[WorldType.CUSTOM] ?? []).map(scene => scene.id)),
};
Object.entries(overrideMap).forEach(([characterId, override]) => {
if (!validCharacterIds.has(characterId)) {
pushError(errors, `未知角色覆盖:${characterId}`);
return;
}
if (override.gender !== undefined && !isKnownGender(override.gender)) {
pushError(errors, `${characterId} gender must be "male" or "female".`);
}
if (override.combatTags !== undefined && !isNonEmptyStringArray(override.combatTags)) {
pushError(errors, `${characterId} combatTags must be a non-empty string array.`);
}
if (override.skills) {
override.skills.forEach((skill, index) => {
const skillLabel = `${characterId} skill ${skill.id || index + 1}`;
if (!skill.id?.trim()) pushError(errors, `${skillLabel} is missing id.`);
if (!skill.name?.trim()) pushError(errors, `${skillLabel} is missing name.`);
if (!isPositiveNumber(skill.range)) pushError(errors, `${skillLabel} range must be > 0.`);
if (typeof skill.damage !== 'number' || skill.damage < 0) pushError(errors, `${skillLabel} damage must be >= 0.`);
if (typeof skill.manaCost !== 'number' || skill.manaCost < 0) pushError(errors, `${skillLabel} manaCost must be >= 0.`);
if (typeof skill.cooldownTurns !== 'number' || skill.cooldownTurns < 0) pushError(errors, `${skillLabel} cooldownTurns must be >= 0.`);
if (skill.buildBuffs !== undefined) {
validateBuildBuffs(errors, characterId, `skill ${skill.id || index + 1} buildBuffs`, skill.buildBuffs);
}
});
}
if (override.sceneBindings) {
Object.entries(override.sceneBindings).forEach(([world, binding]) => {
if (!binding) return;
const worldType = world as WorldType;
const validSceneIds = validSceneIdsByWorld[worldType];
if (binding.homeSceneId && !validSceneIds.has(binding.homeSceneId)) {
pushError(errors, `${characterId} has invalid homeSceneId for ${worldType}: ${binding.homeSceneId}`);
}
(binding.npcSceneIds ?? []).forEach(sceneId => {
if (!validSceneIds.has(sceneId)) {
pushError(errors, `${characterId} has invalid npcSceneId for ${worldType}: ${sceneId}`);
}
});
});
}
});
return errors;
}
export function validateMonsterOverrides(
overrideMap: Record<string, MonsterPresetOverride>,
monsters: MonsterPreset[],
) {
const errors: string[] = [];
const validMonsterIds = new Set(monsters.map(monster => monster.id));
Object.entries(overrideMap).forEach(([monsterId, override]) => {
if (!validMonsterIds.has(monsterId)) {
pushError(errors, `未知怪物覆盖:${monsterId}`);
return;
}
Object.entries(override.baseStats ?? {}).forEach(([key, value]) => {
const numericValue = typeof value === 'number' ? value : undefined;
if (!isPositiveNumber(numericValue)) {
pushError(errors, `${monsterId} baseStats.${key} must be > 0.`);
}
});
if (override.combatTags !== undefined && !isNonEmptyStringArray(override.combatTags)) {
pushError(errors, `${monsterId} combatTags must be a non-empty string array.`);
}
Object.entries(override.animations ?? {}).forEach(([animation, rawConfig]) => {
const config = rawConfig as { frames?: number; fps?: number } | undefined;
if (!config) return;
if (!isPositiveNumber(config.frames)) pushError(errors, `${monsterId} ${animation}.frames must be > 0.`);
if (!isPositiveNumber(config.fps)) pushError(errors, `${monsterId} ${animation}.fps must be > 0.`);
});
});
return errors;
}
export function validateSceneOverrides(
overrideMap: Record<string, ScenePresetOverride>,
scenes: ScenePreset[],
_monstersByWorld: Partial<Record<WorldType, MonsterPreset[]>>,
) {
const errors: string[] = [];
const sceneById = new Map(scenes.map(scene => [scene.id, scene]));
const validSceneIds = new Set(scenes.map(scene => scene.id));
Object.entries(overrideMap).forEach(([sceneId, override]) => {
const scene = sceneById.get(sceneId);
if (!scene) {
pushError(errors, `未知场景覆盖:${sceneId}`);
return;
}
if (override.forwardSceneId && !validSceneIds.has(override.forwardSceneId)) {
pushError(errors, `${sceneId} has invalid forwardSceneId: ${override.forwardSceneId}`);
}
(override.connectedSceneIds ?? []).forEach(targetSceneId => {
if (!validSceneIds.has(targetSceneId)) {
pushError(errors, `${sceneId} has invalid connectedSceneId: ${targetSceneId}`);
}
});
});
return errors;
}
export function validateSceneNpcOverrides(
overrideMap: Record<string, SceneNpcPresetOverride>,
validNpcIds: string[],
characters: Character[],
) {
const errors: string[] = [];
const npcIdSet = new Set(validNpcIds);
const characterIdSet = new Set(characters.map(character => character.id));
Object.entries(overrideMap).forEach(([npcId, override]) => {
if (!npcIdSet.has(npcId)) {
pushError(errors, `未知场景角色覆盖:${npcId}`);
return;
}
if (override.gender !== undefined && !isKnownGender(override.gender)) {
pushError(errors, `${npcId} gender must be "male" or "female".`);
}
if (override.characterId && !characterIdSet.has(override.characterId)) {
pushError(errors, `${npcId} has invalid characterId: ${override.characterId}`);
}
});
return errors;
}
export function validateItemOverrides(
overrideMap: Record<string, ItemCatalogOverride>,
validItemIds: string[],
) {
const errors: string[] = [];
const itemIdSet = new Set(validItemIds);
Object.entries(overrideMap).forEach(([itemId, override]) => {
if (!itemIdSet.has(itemId)) {
pushError(errors, `未知物品覆盖:${itemId}`);
return;
}
if (override.name !== undefined && !override.name.trim()) {
pushError(errors, `${itemId} name cannot be empty.`);
}
if (override.category !== undefined && !override.category.trim()) {
pushError(errors, `${itemId} category cannot be empty.`);
}
if (override.description !== undefined && !override.description.trim()) {
pushError(errors, `${itemId} description cannot be empty.`);
}
if (override.tags !== undefined && !isNonEmptyStringArray(override.tags)) {
pushError(errors, `${itemId} tags must be a non-empty string array.`);
}
if (override.buildProfile?.tags !== undefined && !isNonEmptyStringArray(override.buildProfile.tags)) {
pushError(errors, `${itemId} buildProfile.tags must be a non-empty string array.`);
}
if (override.buildProfile?.craftTags !== undefined && !isNonEmptyStringArray(override.buildProfile.craftTags)) {
pushError(errors, `${itemId} buildProfile.craftTags must be a non-empty string array.`);
}
if (override.useProfile?.buildBuffs !== undefined) {
validateBuildBuffs(errors, itemId, 'useProfile.buildBuffs', override.useProfile.buildBuffs);
}
});
return errors;
}

View File

@@ -30,7 +30,7 @@ export const STORY_OPENING_CAMP_DIALOGUE_FUNCTION: FunctionDocumentationEntry =
storyMode: 'special_sequence',
uiMode: 'none',
executor:
'src/hooks/rpg-runtime-story/openingAdventure.ts + src/services/prompt.ts',
'server-node/src/modules/rpg-runtime-story/RpgRuntimeStoryActionDomain.ts + src/hooks/rpg-runtime-story/storyContextBuilder.ts',
animationNote: '重点在对白本身,不额外驱动独立战斗/位移动画。',
storyNote: '会把 prompt 切到营地开场对白模式,并要求输出结构化对话行。',
uiNote: '不弹 modal直接进入对白流。',

View File

@@ -14,6 +14,7 @@ export * from './flow/campTravelHomeScene';
export * from './flow/storyContinueAdventure';
export * from './flow/storyOpeningCampDialogue';
export * from './npc/npcChat';
export * from './npc/npcChatQuestOffer';
export * from './npc/npcFight';
export * from './npc/npcGift';
export * from './npc/npcHelp';

View File

@@ -1,5 +1,10 @@
import type { FunctionDocumentationEntry } from '../types';
import { NPC_CHAT_FUNCTION } from './npcChat';
import {
NPC_CHAT_QUEST_OFFER_ABANDON_FUNCTION,
NPC_CHAT_QUEST_OFFER_REPLACE_FUNCTION,
NPC_CHAT_QUEST_OFFER_VIEW_FUNCTION,
} from './npcChatQuestOffer';
import { NPC_FIGHT_FUNCTION } from './npcFight';
import { NPC_GIFT_FUNCTION } from './npcGift';
import { NPC_HELP_FUNCTION } from './npcHelp';
@@ -18,6 +23,9 @@ export const NPC_FUNCTION_DOCUMENTATION: FunctionDocumentationEntry[] = [
NPC_SPAR_FUNCTION,
NPC_HELP_FUNCTION,
NPC_CHAT_FUNCTION,
NPC_CHAT_QUEST_OFFER_VIEW_FUNCTION,
NPC_CHAT_QUEST_OFFER_REPLACE_FUNCTION,
NPC_CHAT_QUEST_OFFER_ABANDON_FUNCTION,
NPC_GIFT_FUNCTION,
NPC_RECRUIT_FUNCTION,
NPC_QUEST_ACCEPT_FUNCTION,

View File

@@ -24,7 +24,7 @@ export const NPC_CHAT_FUNCTION: FunctionDocumentationEntry = {
storyMode: 'stream_then_defer',
uiMode: 'none',
executor:
'src/hooks/rpg-runtime-story/npcEncounterActions.ts -> commitNpcChatState',
'src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts -> commitNpcChatState',
animationNote: '重点在流式对白和轻量站场,不额外打开窗口。',
storyNote:
'先生成聊天正文,再把真正的新选项放入 deferredOptions等待 continue adventure。',

View File

@@ -0,0 +1,86 @@
import type { FunctionDocumentationEntry } from '../types';
/**
* npc_chat_quest_offer_*
*
* NPC 聊天态里的临时委托处理 function。它们不是新的任务系统
* 而是高好感聊天中 pending quest offer 的查看、更换和放弃入口。
*/
const QUEST_OFFER_SOURCE = 'src/data/functionCatalog/npc/npcChatQuestOffer.ts';
const QUEST_OFFER_EXECUTOR =
'server-node/src/modules/quest/questStoryActionService.ts -> resolveQuestStoryAction';
export const NPC_CHAT_QUEST_OFFER_VIEW_FUNCTION: FunctionDocumentationEntry = {
id: 'npc_chat_quest_offer_view',
domain: 'npc',
title: '查看委托',
source: QUEST_OFFER_SOURCE,
summary: '查看当前聊天中 NPC 刚提出但尚未领取的委托。',
detailedDescription:
'它用于 pending quest offer 阶段,只打开或返回当前待领取任务详情,不把任务写入正式 quest log。',
trigger: 'NPC 聊天触发待领取委托后,任务处理态选项中出现。',
execution:
'后端读取当前 pending quest offer并返回可展示的任务详情与领取入口。',
result: '玩家可以查看任务目标和奖励,确认领取前不会改变正式任务日志。',
active: true,
runtime: {
storyMode: 'local_only',
uiMode: 'none',
executor: QUEST_OFFER_EXECUTOR,
animationNote: '不触发角色位移动画,重点是切换任务详情展示。',
storyNote: '只保留当前委托上下文,不生成新的聊天剧情。',
uiNote: '展示待领取任务详情,等待玩家领取、替换或返回聊天。',
compactDetailText: '查看这份委托',
},
};
export const NPC_CHAT_QUEST_OFFER_REPLACE_FUNCTION: FunctionDocumentationEntry =
{
id: 'npc_chat_quest_offer_replace',
domain: 'npc',
title: '更换委托',
source: QUEST_OFFER_SOURCE,
summary: '让 NPC 重新生成一份聊天内待领取委托。',
detailedDescription:
'它不会本地改写现有任务文案,而是重新走任务生成链,替换当前 pending quest offer。',
trigger: 'NPC 聊天任务处理态中,玩家不满意当前委托时出现。',
execution:
'后端调用任务生成链生成新 quest offer并覆盖当前聊天态 pending offer。',
result:
'当前待领取委托被替换,聊天仍停留在任务处理态,正式 quest log 不变。',
active: true,
runtime: {
storyMode: 'local_effect_then_generate',
uiMode: 'none',
executor: QUEST_OFFER_EXECUTOR,
animationNote: '不触发战斗或移动演出,只追加轻量聊天反馈。',
storyNote: '重新生成 pending quest offer并说明 NPC 换了一个委托。',
uiNote: '继续显示查看、更换、放弃这组任务处理选项。',
compactDetailText: '换一个委托',
},
};
export const NPC_CHAT_QUEST_OFFER_ABANDON_FUNCTION: FunctionDocumentationEntry =
{
id: 'npc_chat_quest_offer_abandon',
domain: 'npc',
title: '放弃委托',
source: QUEST_OFFER_SOURCE,
summary: '丢弃当前聊天中尚未领取的委托。',
detailedDescription:
'它只清理 pending quest offer不影响已经写入 quest log 的正式任务,也不会扣除奖励或结算任务失败。',
trigger: 'NPC 聊天任务处理态中,玩家暂时不想接这份委托时出现。',
execution:
'后端清空当前聊天态 pending quest offer并恢复普通 NPC 聊天选项。',
result: '待领取委托消失,玩家回到自由聊天或离开 NPC 的正常流程。',
active: true,
runtime: {
storyMode: 'local_only',
uiMode: 'none',
executor: QUEST_OFFER_EXECUTOR,
animationNote: '不触发额外演出,只回到普通聊天态。',
storyNote: '追加玩家暂时不接委托的轻量反馈。',
uiNote: '恢复普通 npc_chat 建议和自定义输入。',
compactDetailText: '暂时不接',
},
};

View File

@@ -23,7 +23,7 @@ export const NPC_FIGHT_FUNCTION: FunctionDocumentationEntry = {
storyMode: 'special_sequence',
uiMode: 'none',
executor:
'src/hooks/rpg-runtime-story/npcEncounterActions.ts -> handleNpcInteraction',
'src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts -> handleNpcInteraction',
animationNote: '切到 NPC 战斗模式后,由战斗播放链路驱动后续动画。',
storyNote: '不会先弹窗,直接把当前 encounter 切成战斗态并进入后续结算。',
uiNote: '不弹 modal直接进入战斗。',

View File

@@ -22,7 +22,7 @@ export const NPC_HELP_FUNCTION: FunctionDocumentationEntry = {
storyMode: 'local_effect_then_generate',
uiMode: 'none',
executor:
'src/hooks/rpg-runtime-story/npcEncounterActions.ts -> handleNpcInteraction',
'src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts -> handleNpcInteraction',
animationNote: '不单独开窗口,直接在当前交互里结算帮助结果。',
storyNote: '点击后立即按本地奖励规则结算,并继续生成新的故事状态。',
uiNote: '不弹 modal直接获得帮助反馈。',

View File

@@ -21,7 +21,7 @@ export const NPC_LEAVE_FUNCTION: FunctionDocumentationEntry = {
storyMode: 'local_effect_then_generate',
uiMode: 'none',
executor:
'src/hooks/rpg-runtime-story/npcEncounterActions.ts -> handleNpcInteraction',
'src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts -> handleNpcInteraction',
animationNote: '通常只做轻量离场,不单独打开窗口。',
storyNote: '点击后结束当前 NPC 交互,并回到新的探索剧情。',
uiNote: '不弹 modal直接退出互动。',

View File

@@ -60,7 +60,7 @@ export const NPC_PREVIEW_TALK_FUNCTION: FunctionDocumentationEntry = {
uiMode: 'npc_interaction_entry',
visuals: NPC_PREVIEW_TALK_OPTION_VISUALS,
executor:
'src/hooks/rpg-runtime-story/choiceActions.ts + src/hooks/rpg-runtime-story/npcEncounterActions.ts',
'src/hooks/rpg-runtime-story/choiceActions.ts + src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts',
animationNote:
'保持轻量 idle 站场,不做额外位移,重点是把交互焦点切到 NPC 身上。',
storyNote:

View File

@@ -21,7 +21,7 @@ export const NPC_SPAR_FUNCTION: FunctionDocumentationEntry = {
storyMode: 'special_sequence',
uiMode: 'none',
executor:
'src/hooks/rpg-runtime-story/npcEncounterActions.ts -> handleNpcInteraction',
'src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts -> handleNpcInteraction',
animationNote: '切到 spar 战斗模式后,由战斗播放链路驱动切磋演出。',
storyNote: '不会先弹窗,直接进入点到为止的切磋流程。',
uiNote: '不弹 modal直接切磋。',

View File

@@ -0,0 +1,35 @@
import type { FunctionDocumentationEntry } from '../types';
/**
* battle_attack_basic
*
* 后端单行为战斗模型的普通攻击入口。该 function 只登记文档和契约,
* 不进入前端本地 state function 候选池。
*/
export const BATTLE_ATTACK_BASIC_FUNCTION: FunctionDocumentationEntry = {
id: 'battle_attack_basic',
domain: 'state',
title: '普通攻击',
source: 'src/data/functionCatalog/state/battleAttackBasic.ts',
summary: '后端单行为战斗模型中的基础攻击 function。',
detailedDescription:
'这个 function 代表一次明确的普通攻击点击,后端直接结算伤害、敌方反击和下一轮战斗选项,不再请求 AI 续写整段战斗剧情。',
trigger: '仅在 battle 状态且场上仍有存活敌人时,由后端战斗 option 池下发。',
execution:
'前端透传 functionId后端 combatResolutionService 直接按普通攻击规则结算本回合。',
result: '刷新 HP、战斗日志和下一轮战斗 options若敌人被击败再进入脱战剧情推理。',
state: 'battle',
category: 'battle',
active: true,
runtime: {
storyMode: 'local_only',
uiMode: 'none',
executor:
'server-node/src/modules/combat/combatResolutionService.ts -> resolveCombatAction',
animationNote: '播放一次基础攻击和受击反馈,不扩展成连续多段连击。',
storyNote:
'战斗未结束时只展示本次结算文本;战斗结束后才请求脱战剧情。',
uiNote: '由后端战斗 option 池生成,不进入前端本地 state function 候选。',
compactDetailText: '直接攻击眼前敌人',
},
};

View File

@@ -0,0 +1,36 @@
import type { FunctionDocumentationEntry } from '../types';
/**
* battle_use_skill
*
* 后端单行为战斗模型的技能释放入口。每个技能 option 复用同一个
* functionId具体技能必须由 runtimePayload.skillId 指定。
*/
export const BATTLE_USE_SKILL_FUNCTION: FunctionDocumentationEntry = {
id: 'battle_use_skill',
domain: 'state',
title: '释放技能',
source: 'src/data/functionCatalog/state/battleUseSkill.ts',
summary: '后端单行为战斗模型中的指定技能释放 function。',
detailedDescription:
'这个 functionId 可以对应多个技能 option 实例。前端只展示技能名和不可用原因,后端根据 runtimePayload.skillId 校验蓝量、冷却并结算本次技能效果。',
trigger: '仅在 battle 状态下由后端按角色技能列表生成,可能携带 disabled 状态。',
execution:
'前端透传 runtimePayload.skillId后端 combatResolutionService 校验技能并完成一次技能动作结算。',
result:
'更新 MP、技能冷却、敌我 HP 和下一轮战斗 options若战斗结束再触发脱战剧情推理。',
state: 'battle',
category: 'battle',
active: true,
runtime: {
storyMode: 'local_only',
uiMode: 'none',
executor:
'server-node/src/modules/combat/combatResolutionService.ts -> resolveCombatAction',
animationNote: '根据技能 option 播放一次技能演出,不在本 function 内追加多回合动作。',
storyNote:
'战斗未结束时使用本次技能结算文本;只有战斗结束才请求新剧情。',
uiNote: '每个技能是一个后端下发的独立 option必须携带 skillId。',
compactDetailText: '释放一个指定技能',
},
};

View File

@@ -1,11 +1,13 @@
import type { StateFunctionSource } from '../types';
import { BATTLE_ALL_IN_CRUSH_FUNCTION_SOURCE } from './battleAllInCrush';
import { BATTLE_ATTACK_BASIC_FUNCTION } from './battleAttackBasic';
import { BATTLE_ESCAPE_BREAKOUT_FUNCTION_SOURCE } from './battleEscapeBreakout';
import { BATTLE_FEINT_STEP_FUNCTION_SOURCE } from './battleFeintStep';
import { BATTLE_FINISHER_WINDOW_FUNCTION_SOURCE } from './battleFinisherWindow';
import { BATTLE_GUARD_BREAK_FUNCTION_SOURCE } from './battleGuardBreak';
import { BATTLE_PROBE_PRESSURE_FUNCTION_SOURCE } from './battleProbePressure';
import { BATTLE_RECOVER_BREATH_FUNCTION_SOURCE } from './battleRecoverBreath';
import { BATTLE_USE_SKILL_FUNCTION } from './battleUseSkill';
import { IDLE_CALL_OUT_FUNCTION_SOURCE } from './idleCallOut';
import { IDLE_EXPLORE_FORWARD_FUNCTION_SOURCE } from './idleExploreForward';
import { IDLE_FOLLOW_CLUE_FUNCTION_SOURCE } from './idleFollowClue';
@@ -40,7 +42,9 @@ export const STATE_FUNCTION_PROMPT_DESCRIPTIONS = Object.fromEntries(
]),
) as Record<string, string>;
export const STATE_FUNCTION_DOCUMENTATION = STATE_FUNCTION_SOURCES.map(
(source) => source.documentation,
);
export const STATE_FUNCTION_DOCUMENTATION = [
BATTLE_ATTACK_BASIC_FUNCTION,
BATTLE_USE_SKILL_FUNCTION,
...STATE_FUNCTION_SOURCES.map((source) => source.documentation),
];

View File

@@ -1,37 +0,0 @@
import { WorldType } from '../types';
import { getSceneFriendlyNpcs, getSceneHostileNpcs,getScenePresetById } from './scenePresets';
export function buildSceneObserveSignsStoryText(
worldType: WorldType | null,
sceneId: string | null | undefined,
) {
if (!worldType) {
return '你停下来倾听,但目前场景上下文不足,无法判断附近有什么。';
}
const scene = getScenePresetById(worldType, sceneId);
if (!scene) {
return '你停下来倾听,但这个区域还没有露出任何可靠的痕迹。';
}
const friendlyNpcs = getSceneFriendlyNpcs(scene);
const hostileNpcs = getSceneHostileNpcs(scene);
const npcSummary = friendlyNpcs.length > 0
? `可能的角色:${friendlyNpcs.map(npc => `${npc.name}${npc.role}`).join('')}`
: '可能的角色:暂无明确识别';
const hostileSummary = hostileNpcs.length > 0
? `可能的敌对角色:${hostileNpcs.map(npc => npc.name).join('')}`
: '可能的敌对角色:无明确威胁特征';
const treasureSummary = scene.treasureHints.length > 0
? `可能的宝藏线索:${scene.treasureHints.slice(0, 2).join('')}`
: '可能的宝藏线索:暂无发现';
const bossCandidate = hostileNpcs[0] ?? null;
const bossSummary = bossCandidate
? hostileNpcs.length >= 3
? `Boss线索${bossCandidate.name} 感觉是这里最强的敌对存在。${bossCandidate.description}`
: `Boss线索暂无明显首领${bossCandidate.name} 仍然是最需要警惕的危险威胁。`
: 'Boss线索暂无迹象指向该区域有明确首领。';
return `你稳住队伍,梳理${scene.name}周围隐藏的迹象。${npcSummary}${hostileSummary}${treasureSummary}${bossSummary}`;
}