Files
Genarrative/src/data/runtimeItemContext.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

343 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
});
}