071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
382 lines
11 KiB
TypeScript
382 lines
11 KiB
TypeScript
import type { QuestGenerationContext } from '../services/aiTypes';
|
|
import {
|
|
buildFallbackActorNarrativeProfile,
|
|
normalizeActorNarrativeProfile,
|
|
} from '../services/storyEngine/actorNarrativeProfile';
|
|
import { buildThemePackFromWorldProfile } from '../services/storyEngine/themePack';
|
|
import { buildFallbackWorldStoryGraph } from '../services/storyEngine/worldStoryGraph';
|
|
import type {
|
|
EquipmentLoadout,
|
|
GameState,
|
|
RuntimeItemGenerationChannel,
|
|
RuntimeItemGenerationContext,
|
|
ScenePresetInfo,
|
|
} from '../types';
|
|
import {
|
|
getCharacterCombatTags,
|
|
getTimedBuildBuffTags,
|
|
normalizeBuildTags,
|
|
} from './buildTags';
|
|
|
|
type GapDefinition = {
|
|
id: string;
|
|
tags: string[];
|
|
};
|
|
|
|
const BUILD_GAP_DEFINITIONS: GapDefinition[] = [
|
|
{ id: 'survival_gap', tags: ['守御', '护体', '回复', '续战'] },
|
|
{ id: 'mana_gap', tags: ['法力', '冷却', '护持', '过载'] },
|
|
{ id: 'finisher_gap', tags: ['爆发', '重击', '追击', '压制'] },
|
|
{ id: 'mobility_gap', tags: ['突进', '快袭', '风行', '游击'] },
|
|
{ id: 'control_gap', tags: ['控场', '符阵', '镇邪', '反击'] },
|
|
];
|
|
|
|
function dedupeStrings(values: Array<string | null | undefined | false>) {
|
|
return [
|
|
...new Set(
|
|
values
|
|
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
.filter(Boolean),
|
|
),
|
|
];
|
|
}
|
|
|
|
function collectLoadoutBuildTags(loadout: EquipmentLoadout | null | undefined) {
|
|
if (!loadout) return [] as string[];
|
|
|
|
return normalizeBuildTags(
|
|
[
|
|
...(loadout.weapon?.buildProfile?.tags ?? []),
|
|
loadout.weapon?.buildProfile?.role ?? '',
|
|
...(loadout.armor?.buildProfile?.tags ?? []),
|
|
loadout.armor?.buildProfile?.role ?? '',
|
|
...(loadout.relic?.buildProfile?.tags ?? []),
|
|
loadout.relic?.buildProfile?.role ?? '',
|
|
],
|
|
6,
|
|
);
|
|
}
|
|
|
|
function buildSceneTags(
|
|
scene: Pick<ScenePresetInfo, 'name' | 'description' | 'treasureHints'> | null,
|
|
) {
|
|
if (!scene) return [] as string[];
|
|
|
|
const seedParts = dedupeStrings([scene.name, ...(scene.treasureHints ?? [])]);
|
|
|
|
return seedParts
|
|
.flatMap((part) => part.split(/[、,。;:\s/]+/u))
|
|
.map((part) => part.trim())
|
|
.filter((part) => part.length >= 2)
|
|
.slice(0, 8);
|
|
}
|
|
|
|
function buildRecentStorySummary(lines: string[]) {
|
|
if (lines.length <= 0) return '最近没有形成稳定的事件线索。';
|
|
return lines.join(' / ');
|
|
}
|
|
|
|
function buildRecentStoryLines(storyHistory: GameState['storyHistory']) {
|
|
return storyHistory
|
|
.slice(-4)
|
|
.map((moment) => moment.text.trim())
|
|
.filter(Boolean)
|
|
.slice(-3);
|
|
}
|
|
|
|
function derivePlayerBuildGaps(playerBuildTags: string[]) {
|
|
const tagSet = new Set(playerBuildTags);
|
|
return BUILD_GAP_DEFINITIONS.filter(
|
|
(definition) =>
|
|
definition.tags.filter((tag) => tagSet.has(tag)).length <= 0,
|
|
)
|
|
.map((definition) => definition.id)
|
|
.slice(0, 3);
|
|
}
|
|
|
|
function resolveRelatedNpcNarrativeProfile(params: {
|
|
customWorldProfile: GameState['customWorldProfile'];
|
|
encounter: GameState['currentEncounter'];
|
|
}) {
|
|
const { customWorldProfile, encounter } = params;
|
|
if (!customWorldProfile || !encounter || encounter.kind !== 'npc') {
|
|
return null;
|
|
}
|
|
|
|
const role =
|
|
customWorldProfile.storyNpcs.find(
|
|
(npc) => npc.id === encounter.id || npc.name === encounter.npcName,
|
|
) ??
|
|
customWorldProfile.playableNpcs.find(
|
|
(npc) => npc.id === encounter.id || npc.name === encounter.npcName,
|
|
);
|
|
if (!role) {
|
|
return encounter.narrativeProfile ?? null;
|
|
}
|
|
|
|
const themePack =
|
|
customWorldProfile.themePack ??
|
|
buildThemePackFromWorldProfile(customWorldProfile);
|
|
const storyGraph =
|
|
customWorldProfile.storyGraph ??
|
|
buildFallbackWorldStoryGraph(customWorldProfile, themePack);
|
|
|
|
return normalizeActorNarrativeProfile(
|
|
role.narrativeProfile,
|
|
buildFallbackActorNarrativeProfile(role, storyGraph, themePack),
|
|
);
|
|
}
|
|
|
|
function resolveActiveThreadIds(params: {
|
|
customWorldProfile: GameState['customWorldProfile'];
|
|
relatedNpcNarrativeProfile: RuntimeItemGenerationContext['relatedNpcNarrativeProfile'];
|
|
storyEngineMemory?:
|
|
| GameState['storyEngineMemory']
|
|
| QuestGenerationContext['activeThreadIds'];
|
|
}) {
|
|
const threadSource = params.storyEngineMemory;
|
|
if (Array.isArray(threadSource) && threadSource.length > 0) {
|
|
return threadSource.slice(0, 4);
|
|
}
|
|
|
|
if (
|
|
threadSource &&
|
|
!Array.isArray(threadSource) &&
|
|
threadSource.activeThreadIds?.length
|
|
) {
|
|
return threadSource.activeThreadIds.slice(0, 4);
|
|
}
|
|
|
|
if (params.relatedNpcNarrativeProfile?.relatedThreadIds.length) {
|
|
return params.relatedNpcNarrativeProfile.relatedThreadIds.slice(0, 4);
|
|
}
|
|
|
|
if (!params.customWorldProfile) {
|
|
return [];
|
|
}
|
|
|
|
const themePack =
|
|
params.customWorldProfile.themePack ??
|
|
buildThemePackFromWorldProfile(params.customWorldProfile);
|
|
const storyGraph =
|
|
params.customWorldProfile.storyGraph ??
|
|
buildFallbackWorldStoryGraph(params.customWorldProfile, themePack);
|
|
|
|
return storyGraph.visibleThreads.slice(0, 3).map((thread) => thread.id);
|
|
}
|
|
|
|
function buildBaseRuntimeContext(params: {
|
|
worldType: GameState['worldType'];
|
|
customWorldProfile: GameState['customWorldProfile'];
|
|
scene: Pick<
|
|
ScenePresetInfo,
|
|
'id' | 'name' | 'description' | 'treasureHints'
|
|
> | null;
|
|
encounter: GameState['currentEncounter'];
|
|
relatedNpcState: GameState['npcStates'][string] | null;
|
|
storyEngineMemory?:
|
|
| GameState['storyEngineMemory']
|
|
| QuestGenerationContext['activeThreadIds'];
|
|
storyHistory: GameState['storyHistory'];
|
|
playerCharacterId: string;
|
|
playerBuildTags: string[];
|
|
playerEquipmentTags: string[];
|
|
generationChannel: RuntimeItemGenerationChannel;
|
|
}) {
|
|
const {
|
|
worldType,
|
|
customWorldProfile,
|
|
scene,
|
|
encounter,
|
|
relatedNpcState,
|
|
storyEngineMemory,
|
|
storyHistory,
|
|
playerCharacterId,
|
|
playerBuildTags,
|
|
playerEquipmentTags,
|
|
generationChannel,
|
|
} = params;
|
|
const recentStoryLines = buildRecentStoryLines(storyHistory);
|
|
const relatedNpcNarrativeProfile = resolveRelatedNpcNarrativeProfile({
|
|
customWorldProfile,
|
|
encounter,
|
|
});
|
|
const activeThreadIds = resolveActiveThreadIds({
|
|
customWorldProfile,
|
|
relatedNpcNarrativeProfile,
|
|
storyEngineMemory,
|
|
});
|
|
|
|
return {
|
|
worldType,
|
|
customWorldProfile,
|
|
sceneId: scene?.id ?? null,
|
|
sceneName: scene?.name ?? null,
|
|
sceneDescription: scene?.description ?? null,
|
|
sceneTags: buildSceneTags(scene),
|
|
treasureHints: [...(scene?.treasureHints ?? [])],
|
|
encounter: encounter ?? null,
|
|
encounterNpcId:
|
|
encounter?.id ??
|
|
encounter?.characterId ??
|
|
encounter?.monsterPresetId ??
|
|
encounter?.npcName ??
|
|
null,
|
|
encounterNpcName: encounter?.npcName ?? null,
|
|
encounterContextText: encounter?.context ?? null,
|
|
relatedNpcState,
|
|
relatedNpcNarrativeProfile,
|
|
relatedScene: scene,
|
|
recentStorySummary: buildRecentStorySummary(recentStoryLines),
|
|
recentActions: recentStoryLines,
|
|
activeThreadIds,
|
|
playerCharacterId,
|
|
playerBuildTags,
|
|
playerBuildGaps: derivePlayerBuildGaps(playerBuildTags),
|
|
playerEquipmentTags,
|
|
generationChannel,
|
|
} satisfies RuntimeItemGenerationContext;
|
|
}
|
|
|
|
export function buildLooseRuntimeItemGenerationContext(params: {
|
|
worldType: GameState['worldType'];
|
|
customWorldProfile?: GameState['customWorldProfile'];
|
|
scene?: Pick<
|
|
ScenePresetInfo,
|
|
'id' | 'name' | 'description' | 'treasureHints'
|
|
> | null;
|
|
encounter?: GameState['currentEncounter'];
|
|
relatedNpcState?: GameState['npcStates'][string] | null;
|
|
storyHistory?: GameState['storyHistory'];
|
|
playerCharacterId?: string;
|
|
playerBuildTags?: string[];
|
|
playerEquipmentTags?: string[];
|
|
generationChannel: RuntimeItemGenerationChannel;
|
|
}) {
|
|
return buildBaseRuntimeContext({
|
|
worldType: params.worldType,
|
|
customWorldProfile: params.customWorldProfile ?? null,
|
|
scene: params.scene ?? null,
|
|
encounter: params.encounter ?? null,
|
|
relatedNpcState: params.relatedNpcState ?? null,
|
|
storyEngineMemory:
|
|
params.customWorldProfile?.storyGraph?.visibleThreads.map(
|
|
(thread) => thread.id,
|
|
) ?? [],
|
|
storyHistory: params.storyHistory ?? [],
|
|
playerCharacterId: params.playerCharacterId ?? 'runtime-loose-player',
|
|
playerBuildTags: params.playerBuildTags ?? [],
|
|
playerEquipmentTags: params.playerEquipmentTags ?? [],
|
|
generationChannel: params.generationChannel,
|
|
});
|
|
}
|
|
|
|
export function buildRuntimeItemGenerationContext(params: {
|
|
state: GameState;
|
|
generationChannel: RuntimeItemGenerationChannel;
|
|
encounter?: GameState['currentEncounter'];
|
|
scene?: GameState['currentScenePreset'];
|
|
}) {
|
|
const { state, generationChannel } = params;
|
|
const encounter = params.encounter ?? state.currentEncounter;
|
|
const scene = params.scene ?? state.currentScenePreset;
|
|
const relatedNpcState = encounter
|
|
? (state.npcStates[encounter.id ?? encounter.npcName] ?? null)
|
|
: null;
|
|
const playerBuildTags = state.playerCharacter
|
|
? normalizeBuildTags(
|
|
[
|
|
...getCharacterCombatTags(state.playerCharacter),
|
|
...collectLoadoutBuildTags(state.playerEquipment),
|
|
...getTimedBuildBuffTags(state.activeBuildBuffs),
|
|
],
|
|
6,
|
|
)
|
|
: [];
|
|
|
|
return buildBaseRuntimeContext({
|
|
worldType: state.worldType,
|
|
customWorldProfile: state.customWorldProfile,
|
|
scene,
|
|
encounter,
|
|
relatedNpcState,
|
|
storyEngineMemory: state.storyEngineMemory,
|
|
storyHistory: state.storyHistory,
|
|
playerCharacterId: state.playerCharacter?.id ?? 'unknown-player',
|
|
playerBuildTags,
|
|
playerEquipmentTags: collectLoadoutBuildTags(state.playerEquipment),
|
|
generationChannel,
|
|
});
|
|
}
|
|
|
|
export function buildQuestRuntimeItemGenerationContext(params: {
|
|
context: QuestGenerationContext;
|
|
generationChannel?: RuntimeItemGenerationChannel;
|
|
issuerNpcId: string;
|
|
issuerNpcName: string;
|
|
roleText: string;
|
|
scene?: Pick<
|
|
ScenePresetInfo,
|
|
'id' | 'name' | 'description' | 'treasureHints'
|
|
> | null;
|
|
}) {
|
|
const {
|
|
context,
|
|
issuerNpcId,
|
|
issuerNpcName,
|
|
roleText,
|
|
scene,
|
|
generationChannel = 'quest_reward',
|
|
} = params;
|
|
const playerBuildTags = context.playerCharacter
|
|
? normalizeBuildTags(
|
|
[
|
|
...getCharacterCombatTags(context.playerCharacter),
|
|
...collectLoadoutBuildTags(context.playerEquipment),
|
|
],
|
|
6,
|
|
)
|
|
: [];
|
|
|
|
return buildBaseRuntimeContext({
|
|
worldType: context.worldType,
|
|
customWorldProfile: context.customWorldProfile ?? null,
|
|
scene:
|
|
scene ??
|
|
(context.currentSceneName
|
|
? {
|
|
id: context.currentSceneId ?? '',
|
|
name: context.currentSceneName,
|
|
description: context.currentSceneDescription ?? '',
|
|
treasureHints: [],
|
|
}
|
|
: null),
|
|
encounter: {
|
|
id: issuerNpcId,
|
|
kind: 'npc',
|
|
npcName: issuerNpcName,
|
|
npcDescription: roleText,
|
|
npcAvatar: '',
|
|
context: roleText,
|
|
},
|
|
relatedNpcState:
|
|
context.issuerAffinity == null
|
|
? null
|
|
: {
|
|
affinity: context.issuerAffinity,
|
|
helpUsed: false,
|
|
chattedCount: 0,
|
|
giftsGiven: 0,
|
|
inventory: [],
|
|
recruited: false,
|
|
revealedFacts: [],
|
|
},
|
|
storyEngineMemory: context.activeThreadIds,
|
|
storyHistory: context.recentStoryMoments ?? [],
|
|
playerCharacterId: context.playerCharacter?.id ?? 'quest-player',
|
|
playerBuildTags,
|
|
playerEquipmentTags: collectLoadoutBuildTags(context.playerEquipment),
|
|
generationChannel,
|
|
});
|
|
}
|