Rework story engine flow and reorganize project docs
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-06 23:19:00 +08:00
parent d678929064
commit ddcb5d5c8c
241 changed files with 19805 additions and 2478 deletions

View File

@@ -1,4 +1,4 @@
import type { Dispatch, SetStateAction } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import { useCallback, useEffect, useState } from 'react';
import {
@@ -43,6 +43,43 @@ import {
} from '../data/stateFunctions';
import { applyStoryReasoningRecovery } from '../data/storyRecovery';
import { generateInitialStory, generateNextStep } from '../services/ai';
import {
buildFallbackActorNarrativeProfile,
normalizeActorNarrativeProfile,
} from '../services/storyEngine/actorNarrativeProfile';
import { applyAdaptiveTuningToPromptContext } from '../services/storyEngine/adaptiveNarrativeTuner';
import { compileCampaignFromWorldProfile } from '../services/storyEngine/campaignPackCompiler';
import {
buildCampEvent,
evaluateCampEventOpportunity,
} from '../services/storyEngine/campEventDirector';
import {
advanceChapterState,
resolveCurrentChapterState,
} from '../services/storyEngine/chapterDirector';
import {
advanceCompanionArc,
buildCompanionArcStates,
} from '../services/storyEngine/companionArcDirector';
import { syncNpcNarrativeState } from '../services/storyEngine/echoMemory';
import { resolveCurrentJourneyBeat } from '../services/storyEngine/journeyBeatPlanner';
import { buildVisibilitySliceFromFacts } from '../services/storyEngine/knowledgeContract';
import { buildKnowledgeGraph } from '../services/storyEngine/knowledgeGraph';
import { buildRecentCarrierEchoes } from '../services/storyEngine/narrativeCarrierCatalog';
import { buildChapterRecap } from '../services/storyEngine/recapDigest';
import { resolveScenarioPack } from '../services/storyEngine/scenarioPackRegistry';
import { buildSceneNarrativeDirective } from '../services/storyEngine/sceneNarrativeDirector';
import {
buildSetpieceDirective,
evaluateSetpieceOpportunity,
} from '../services/storyEngine/setpieceDirector';
import { buildChronicleSummary } from '../services/storyEngine/storyChronicle';
import { buildThemePackFromWorldProfile } from '../services/storyEngine/themePack';
import {
buildEncounterVisibilitySlice,
createEmptyStoryEngineMemoryState,
} from '../services/storyEngine/visibilityEngine';
import { buildFallbackWorldStoryGraph } from '../services/storyEngine/worldStoryGraph';
import {
Character,
Encounter,
@@ -285,6 +322,66 @@ function describeConversationTalkPriority(
}
}
function resolveEncounterNarrativeProfile(state: GameState) {
const encounter = state.currentEncounter;
if (!encounter || encounter.kind !== 'npc') {
return null;
}
if (encounter.narrativeProfile) {
return encounter.narrativeProfile;
}
if (!state.customWorldProfile) {
return null;
}
const role =
state.customWorldProfile.storyNpcs.find((npc) =>
npc.id === encounter.id || npc.name === encounter.npcName,
)
?? state.customWorldProfile.playableNpcs.find((npc) =>
npc.id === encounter.id || npc.name === encounter.npcName,
);
if (!role) {
return null;
}
const themePack =
state.customWorldProfile.themePack
?? buildThemePackFromWorldProfile(state.customWorldProfile);
const storyGraph =
state.customWorldProfile.storyGraph
?? buildFallbackWorldStoryGraph(state.customWorldProfile, themePack);
return normalizeActorNarrativeProfile(
role.narrativeProfile,
buildFallbackActorNarrativeProfile(role, storyGraph, themePack),
);
}
function resolveActiveThreadIds(
state: GameState,
encounterNarrativeProfile: ReturnType<typeof resolveEncounterNarrativeProfile>,
) {
if (state.storyEngineMemory?.activeThreadIds?.length) {
return state.storyEngineMemory.activeThreadIds.slice(0, 4);
}
if (encounterNarrativeProfile?.relatedThreadIds.length) {
return encounterNarrativeProfile.relatedThreadIds.slice(0, 4);
}
if (!state.customWorldProfile) {
return [];
}
const themePack =
state.customWorldProfile.themePack
?? buildThemePackFromWorldProfile(state.customWorldProfile);
const storyGraph =
state.customWorldProfile.storyGraph
?? buildFallbackWorldStoryGraph(state.customWorldProfile, themePack);
return storyGraph.visibleThreads.slice(0, 3).map((thread) => thread.id);
}
function buildStoryContextFromState(
state: GameState,
extras: {
@@ -361,10 +458,21 @@ function buildStoryContextFromState(
})()
: null;
const baseSceneDescription = state.currentScenePreset?.description ?? null;
const sceneMutationDescription = [
state.currentScenePreset?.mutationStateText
? `最新世界变化:${state.currentScenePreset.mutationStateText}`
: null,
state.currentScenePreset?.currentPressureLevel
? `当前区域压力等级:${state.currentScenePreset.currentPressureLevel}`
: null,
]
.filter(Boolean)
.join('\n');
const observeSignsSceneDescription =
extras.observeSignsRequested && state.worldType
? [
baseSceneDescription,
sceneMutationDescription,
'Observed entity pool:',
buildSceneEntityCatalogText(
state.worldType,
@@ -373,9 +481,141 @@ function buildStoryContextFromState(
]
.filter(Boolean)
.join('\n')
: baseSceneDescription;
: [baseSceneDescription, sceneMutationDescription].filter(Boolean).join('\n');
const storyEngineMemory =
state.storyEngineMemory ?? createEmptyStoryEngineMemoryState();
const knowledgeFacts =
state.customWorldProfile?.knowledgeFacts
?? (state.customWorldProfile ? buildKnowledgeGraph(state.customWorldProfile) : []);
const encounterNarrativeProfile = resolveEncounterNarrativeProfile(state);
const activeThreadIds = resolveActiveThreadIds(
{
...state,
storyEngineMemory,
} as GameState,
encounterNarrativeProfile,
);
const visibilitySlice =
state.currentEncounter?.kind === 'npc'
? (() => {
const relevantFacts = knowledgeFacts.filter((fact) =>
fact.ownerActorIds.includes(state.currentEncounter?.id ?? '')
|| fact.ownerActorIds.includes(state.currentEncounter?.npcName ?? '')
|| fact.relatedThreadIds.some((threadId) => activeThreadIds.includes(threadId)),
);
return relevantFacts.length > 0
? buildVisibilitySliceFromFacts({
facts: relevantFacts,
discoveredFactIds: [
...storyEngineMemory.discoveredFactIds,
...(encounterNpcState?.revealedFacts ?? []),
...(encounterNpcState?.seenBackstoryChapterIds ?? []).map(
(chapterId) =>
relevantFacts.find((fact) =>
fact.aliases?.includes(chapterId) || fact.id.includes(chapterId),
)?.id ?? '',
),
],
activeThreadIds,
disclosureStage: encounterDirective?.disclosureStage ?? null,
isFirstMeaningfulContact,
})
: buildEncounterVisibilitySlice({
narrativeProfile: encounterNarrativeProfile,
backstoryReveal: state.currentEncounter.backstoryReveal ?? null,
disclosureStage: encounterDirective?.disclosureStage ?? null,
isFirstMeaningfulContact,
seenBackstoryChapterIds: encounterNpcState?.seenBackstoryChapterIds ?? [],
storyEngineMemory,
activeThreadIds,
});
})()
: null;
const sceneNarrativeDirective = buildSceneNarrativeDirective({
sceneId: state.currentScenePreset?.id ?? null,
sceneName: state.currentScenePreset?.name ?? null,
encounterId: state.currentEncounter?.id ?? null,
encounterName: state.currentEncounter?.npcName ?? null,
recentActions: state.storyHistory.slice(-3).map((moment) => moment.text),
activeThreadIds,
visibilitySlice,
encounterNarrativeProfile,
disclosureStage: encounterDirective?.disclosureStage ?? null,
isFirstMeaningfulContact,
affinity: encounterNpcState?.affinity ?? null,
});
const chapterState = advanceChapterState({
previousChapter: state.chapterState ?? storyEngineMemory.currentChapter ?? null,
nextChapter: resolveCurrentChapterState({
state: {
...state,
storyEngineMemory,
},
}),
});
const journeyBeat = resolveCurrentJourneyBeat({
state: {
...state,
chapterState,
storyEngineMemory: {
...storyEngineMemory,
currentChapter: chapterState,
},
} as GameState,
chapterState,
});
const companionArcStates = advanceCompanionArc({
previous: storyEngineMemory.companionArcStates,
next: buildCompanionArcStates({
state,
reactions: storyEngineMemory.recentCompanionReactions,
}),
});
const currentCampEvent = evaluateCampEventOpportunity({
state,
chapterState,
journeyBeat,
companionArcStates,
})
? buildCampEvent({
state,
chapterState,
journeyBeat,
companionArcStates,
})
: null;
const setpieceDirective = evaluateSetpieceOpportunity({
state,
chapterState,
journeyBeat,
})
? buildSetpieceDirective({
state,
chapterState,
journeyBeat,
})
: null;
const recentWorldMutations = storyEngineMemory.worldMutations ?? [];
const recentChronicleSummary = buildChronicleSummary({
...state,
chapterState,
storyEngineMemory: {
...storyEngineMemory,
currentChapter: chapterState,
companionArcStates,
},
} as GameState);
const compiledPacks = state.customWorldProfile
? compileCampaignFromWorldProfile({ profile: state.customWorldProfile })
: null;
const activeScenarioPack =
resolveScenarioPack(state.activeScenarioPackId)
?? compiledPacks?.scenarioPack
?? null;
const activeCampaignPack = compiledPacks?.campaignPack ?? null;
return {
return applyAdaptiveTuningToPromptContext({
context: {
playerHp: state.playerHp,
playerMaxHp: state.playerMaxHp,
playerMana: state.playerMana,
@@ -399,6 +639,7 @@ function buildStoryContextFromState(
encounterName: state.currentEncounter?.npcName ?? null,
encounterDescription: state.currentEncounter?.npcDescription ?? null,
encounterContext: state.currentEncounter?.context ?? null,
encounterId: state.currentEncounter?.id ?? null,
encounterCharacterId: state.currentEncounter?.characterId ?? null,
encounterGender: state.currentEncounter?.gender ?? null,
encounterCustomProfile: state.currentEncounter
@@ -416,10 +657,12 @@ function buildStoryContextFromState(
initialItems: [...(state.currentEncounter.initialItems ?? [])],
imageSrc: state.currentEncounter.imageSrc,
visual: state.currentEncounter.visual,
narrativeProfile: state.currentEncounter.narrativeProfile,
}
: null,
encounterAffinity: encounterDirective?.affinity ?? null,
encounterAffinityText,
encounterStanceProfile: encounterNpcState?.stanceProfile ?? null,
encounterConversationStyle: encounterDirective?.style ?? null,
encounterDisclosureStage: encounterDirective?.disclosureStage ?? null,
encounterWarmthStage: encounterDirective?.warmthStage ?? null,
@@ -433,6 +676,34 @@ function buildStoryContextFromState(
recentSharedEvent:
recentSharedEvent ?? describeConversationSituation(conversationSituation),
talkPriority: describeConversationTalkPriority(conversationSituation),
visibilitySlice,
sceneNarrativeDirective,
campaignState: state.campaignState ?? storyEngineMemory.campaignState ?? null,
actState: storyEngineMemory.actState ?? null,
chapterState,
journeyBeat,
currentCampEvent,
setpieceDirective,
activeScenarioPack,
activeCampaignPack,
encounterNarrativeProfile,
knowledgeFacts,
activeThreadIds,
companionArcStates,
companionResolutions: storyEngineMemory.companionResolutions ?? [],
consequenceLedger: storyEngineMemory.consequenceLedger ?? [],
authorialConstraintPack: storyEngineMemory.authorialConstraintPack ?? null,
playerStyleProfile: storyEngineMemory.playerStyleProfile ?? null,
recentCompanionReactions: storyEngineMemory.recentCompanionReactions ?? [],
recentCarrierEchoes: buildRecentCarrierEchoes(state),
recentWorldMutations,
recentFactionTensionStates: storyEngineMemory.factionTensionStates ?? [],
recentChronicleSummary:
recentChronicleSummary || buildChapterRecap({ state: { ...state, chapterState } as GameState }),
narrativeQaReport: storyEngineMemory.narrativeQaReport ?? null,
releaseGateReport: storyEngineMemory.releaseGateReport ?? null,
simulationRunResults: storyEngineMemory.simulationRunResults ?? [],
branchBudgetPressure: storyEngineMemory.branchBudgetStatus?.pressure ?? null,
encounterRelationshipSummary: state.currentEncounter?.characterId
? getCharacterChatRecord(state, state.currentEncounter.characterId)
.summary || null
@@ -441,7 +712,9 @@ function buildStoryContextFromState(
customWorldProfile: state.customWorldProfile ?? null,
openingCampBackground: extras.openingCampBackground ?? null,
openingCampDialogue: extras.openingCampDialogue ?? null,
};
},
profile: storyEngineMemory.playerStyleProfile ?? null,
});
}
function buildNpcPreviewStory(
@@ -493,9 +766,7 @@ function getStoryGenerationHostileNpcs(state: GameState) {
}
function getResolvedSceneHostileNpcs(state: GameState) {
return state.sceneMonsters.length > 0
? state.sceneMonsters
: (state.sceneHostileNpcs ?? []);
return state.sceneHostileNpcs;
}
function sanitizeOptions(
@@ -1289,7 +1560,12 @@ export function useStoryGeneration({
npcStates: {
...state.npcStates,
[getNpcEncounterKey(encounter)]: normalizeNpcPersistentState(
updater(getResolvedNpcState(state, encounter)),
syncNpcNarrativeState({
encounter,
npcState: updater(getResolvedNpcState(state, encounter)),
customWorldProfile: state.customWorldProfile,
storyEngineMemory: state.storyEngineMemory,
}),
),
},
});
@@ -1527,7 +1803,6 @@ export function useStoryGeneration({
gameState.inBattle,
gameState.playerCharacter,
gameState.playerX,
gameState.sceneHostileNpcs,
gameState.worldType,
isLoading,
setGameState,