This commit is contained in:
342
src/data/runtimeItemContext.ts
Normal file
342
src/data/runtimeItemContext.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user