This commit is contained in:
2026-04-28 19:36:39 +08:00
parent a9febe7678
commit f0471a4f8d
206 changed files with 18456 additions and 10133 deletions

View File

@@ -89,6 +89,51 @@ describe('echoMemory', () => {
expect(synced.seenBackstoryChapterIds).toContain('scar');
});
it('accepts projected story engine memory snapshots with missing arrays', () => {
const npcState: NpcPersistentState = {
affinity: 18,
helpUsed: false,
chattedCount: 0,
giftsGiven: 0,
inventory: [],
recruited: false,
revealedFacts: [],
knownAttributeRumors: [],
firstMeaningfulContactResolved: false,
seenBackstoryChapterIds: [],
stanceProfile: {
trust: 50,
warmth: 46,
ideologicalFit: 50,
fearOrGuard: 38,
loyalty: 28,
currentConflictTag: null,
recentApprovals: [],
recentDisapprovals: [],
},
};
const synced = syncNpcNarrativeState({
encounter: createEncounter(),
npcState,
storyEngineMemory: {
currentChapter: {
id: 'chapter-1',
title: '断桥再燃',
summary: '桥口旧案重新压到眼前。',
stage: 'rising',
relatedQuestIds: [],
relatedSceneIds: [],
relatedThreadIds: ['thread-1'],
pressureTags: [],
},
} as never,
});
expect(synced.revealedFacts).toContain('publicMask');
expect(synced.revealedFacts).toContain('thread:thread-1');
});
it('writes recent carriers and scar echoes into story engine memory', () => {
const item: InventoryItem = {
id: 'runtime:quest:evidence',

View File

@@ -12,7 +12,7 @@ import {
import { buildThemePackFromWorldProfile } from './themePack';
import {
buildEncounterVisibilitySlice,
createEmptyStoryEngineMemoryState,
normalizeStoryEngineMemoryState,
} from './visibilityEngine';
import { buildFallbackWorldStoryGraph } from './worldStoryGraph';
@@ -81,8 +81,7 @@ export function syncNpcNarrativeState(params: {
return npcState;
}
const storyEngineMemory =
params.storyEngineMemory ?? createEmptyStoryEngineMemoryState();
const storyEngineMemory = normalizeStoryEngineMemoryState(params.storyEngineMemory);
const activeThreadIds =
storyEngineMemory.activeThreadIds.length > 0
? storyEngineMemory.activeThreadIds
@@ -122,8 +121,7 @@ export function appendStoryEngineCarrierMemory(
state: GameState,
items: InventoryItem[],
) {
const storyEngineMemory =
state.storyEngineMemory ?? createEmptyStoryEngineMemoryState();
const storyEngineMemory = normalizeStoryEngineMemoryState(state.storyEngineMemory);
const carriers = items.filter((item) => item.runtimeMetadata?.storyFingerprint);
if (carriers.length <= 0) {
return {

View File

@@ -71,6 +71,67 @@ export function createEmptyStoryEngineMemoryState(): StoryEngineMemoryState {
};
}
export function normalizeStoryEngineMemoryState(
memory?: Partial<StoryEngineMemoryState> | null,
): StoryEngineMemoryState {
const empty = createEmptyStoryEngineMemoryState();
if (!memory) return empty;
// 后端投影或旧存档可能只带增量字段,前端消费前统一补齐数组字段。
return {
...empty,
...memory,
discoveredFactIds: Array.isArray(memory.discoveredFactIds)
? memory.discoveredFactIds
: empty.discoveredFactIds,
inferredFactIds: Array.isArray(memory.inferredFactIds)
? memory.inferredFactIds
: empty.inferredFactIds,
activeThreadIds: Array.isArray(memory.activeThreadIds)
? memory.activeThreadIds
: empty.activeThreadIds,
resolvedScarIds: Array.isArray(memory.resolvedScarIds)
? memory.resolvedScarIds
: empty.resolvedScarIds,
recentCarrierIds: Array.isArray(memory.recentCarrierIds)
? memory.recentCarrierIds
: empty.recentCarrierIds,
openedSceneChapterIds: Array.isArray(memory.openedSceneChapterIds)
? memory.openedSceneChapterIds
: empty.openedSceneChapterIds,
recentSignalIds: Array.isArray(memory.recentSignalIds)
? memory.recentSignalIds
: empty.recentSignalIds,
recentCompanionReactions: Array.isArray(memory.recentCompanionReactions)
? memory.recentCompanionReactions
: empty.recentCompanionReactions,
companionArcStates: Array.isArray(memory.companionArcStates)
? memory.companionArcStates
: empty.companionArcStates,
worldMutations: Array.isArray(memory.worldMutations)
? memory.worldMutations
: empty.worldMutations,
chronicle: Array.isArray(memory.chronicle)
? memory.chronicle
: empty.chronicle,
factionTensionStates: Array.isArray(memory.factionTensionStates)
? memory.factionTensionStates
: empty.factionTensionStates,
consequenceLedger: Array.isArray(memory.consequenceLedger)
? memory.consequenceLedger
: empty.consequenceLedger,
companionResolutions: Array.isArray(memory.companionResolutions)
? memory.companionResolutions
: empty.companionResolutions,
narrativeCodex: Array.isArray(memory.narrativeCodex)
? memory.narrativeCodex
: empty.narrativeCodex,
simulationRunResults: Array.isArray(memory.simulationRunResults)
? memory.simulationRunResults
: empty.simulationRunResults,
};
}
function buildBaseFactIds(
narrativeProfile?: ActorNarrativeProfile | null,
backstoryReveal?: CharacterBackstoryRevealConfig | null,
@@ -113,7 +174,7 @@ function resolveUnlockedChapterIds(
export function buildEncounterVisibilitySlice(
params: EncounterVisibilityParams,
) {
const memory = params.storyEngineMemory ?? createEmptyStoryEngineMemoryState();
const memory = normalizeStoryEngineMemoryState(params.storyEngineMemory);
const factIds = buildBaseFactIds(params.narrativeProfile, params.backstoryReveal);
const unlockedChapterIds = resolveUnlockedChapterIds(
params.backstoryReveal,
@@ -193,7 +254,7 @@ export function buildEncounterVisibilitySlice(
export function buildQuestVisibilitySlice(
params: QuestVisibilityParams,
) {
const memory = params.storyEngineMemory ?? createEmptyStoryEngineMemoryState();
const memory = normalizeStoryEngineMemoryState(params.storyEngineMemory);
const narrativeProfile = params.issuerNarrativeProfile;
const factIds = dedupeStrings([
narrativeProfile ? 'publicMask' : null,