This commit is contained in:
280
src/services/customWorldSceneActRuntime.ts
Normal file
280
src/services/customWorldSceneActRuntime.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import type { NpcChatTurnDirective } from '../../packages/shared/src/contracts/rpgRuntimeChat';
|
||||
import type {
|
||||
CustomWorldProfile,
|
||||
GameState,
|
||||
SceneActBlueprint,
|
||||
SceneActRuntimeState,
|
||||
SceneChapterBlueprint,
|
||||
SceneConnectionInfo,
|
||||
StoryEngineMemoryState,
|
||||
} from '../types';
|
||||
|
||||
function toSet(values: string[]) {
|
||||
return new Set(values.map((value) => value.trim()).filter(Boolean));
|
||||
}
|
||||
|
||||
export function resolveSceneChapterBlueprint(
|
||||
profile: CustomWorldProfile | null | undefined,
|
||||
sceneId: string | null | undefined,
|
||||
): SceneChapterBlueprint | null {
|
||||
if (!profile || !sceneId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
profile.sceneChapterBlueprints?.find(
|
||||
(entry) => entry.sceneId === sceneId || entry.linkedLandmarkIds.includes(sceneId),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveActiveSceneActBlueprint(params: {
|
||||
profile: CustomWorldProfile | null | undefined;
|
||||
sceneId: string | null | undefined;
|
||||
storyEngineMemory?: StoryEngineMemoryState | null;
|
||||
}): SceneActBlueprint | null {
|
||||
const chapter = resolveSceneChapterBlueprint(params.profile, params.sceneId);
|
||||
if (!chapter || chapter.acts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const runtimeState = params.storyEngineMemory?.currentSceneActState;
|
||||
if (
|
||||
runtimeState &&
|
||||
runtimeState.sceneId === chapter.sceneId &&
|
||||
runtimeState.chapterId === chapter.id
|
||||
) {
|
||||
const matchedAct = chapter.acts.find((entry) => entry.id === runtimeState.currentActId);
|
||||
if (matchedAct) {
|
||||
return matchedAct;
|
||||
}
|
||||
}
|
||||
|
||||
return chapter.acts[0] ?? null;
|
||||
}
|
||||
|
||||
export function resolveSceneActProgression(params: {
|
||||
profile: CustomWorldProfile | null | undefined;
|
||||
sceneId: string | null | undefined;
|
||||
storyEngineMemory?: StoryEngineMemoryState | null;
|
||||
}): {
|
||||
chapter: SceneChapterBlueprint;
|
||||
runtimeState: SceneActRuntimeState;
|
||||
activeAct: SceneActBlueprint;
|
||||
nextAct: SceneActBlueprint | null;
|
||||
isLastAct: boolean;
|
||||
} | null {
|
||||
const chapter = resolveSceneChapterBlueprint(params.profile, params.sceneId);
|
||||
if (!chapter || chapter.acts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const runtimeState = buildInitialSceneActRuntimeState(params);
|
||||
if (!runtimeState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeActIndex = chapter.acts.findIndex(
|
||||
(entry) => entry.id === runtimeState.currentActId,
|
||||
);
|
||||
const resolvedActIndex =
|
||||
activeActIndex >= 0
|
||||
? activeActIndex
|
||||
: Math.min(
|
||||
Math.max(runtimeState.currentActIndex, 0),
|
||||
chapter.acts.length - 1,
|
||||
);
|
||||
const activeAct = chapter.acts[resolvedActIndex] ?? chapter.acts[0]!;
|
||||
const nextAct = chapter.acts[resolvedActIndex + 1] ?? null;
|
||||
|
||||
return {
|
||||
chapter,
|
||||
runtimeState: {
|
||||
...runtimeState,
|
||||
currentActId: activeAct.id,
|
||||
currentActIndex: resolvedActIndex,
|
||||
},
|
||||
activeAct,
|
||||
nextAct,
|
||||
isLastAct: !nextAct,
|
||||
};
|
||||
}
|
||||
|
||||
export function advanceSceneActRuntimeState(params: {
|
||||
progress: NonNullable<ReturnType<typeof resolveSceneActProgression>>;
|
||||
}): SceneActRuntimeState | null {
|
||||
const { progress } = params;
|
||||
if (!progress.nextAct) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const completedActIds = toSet([
|
||||
...(progress.runtimeState.completedActIds ?? []),
|
||||
progress.activeAct.id,
|
||||
]);
|
||||
const visitedActIds = toSet([
|
||||
...(progress.runtimeState.visitedActIds ?? []),
|
||||
progress.nextAct.id,
|
||||
]);
|
||||
|
||||
return {
|
||||
sceneId: progress.chapter.sceneId,
|
||||
chapterId: progress.chapter.id,
|
||||
currentActId: progress.nextAct.id,
|
||||
currentActIndex: progress.runtimeState.currentActIndex + 1,
|
||||
completedActIds: [...completedActIds],
|
||||
visitedActIds: [...visitedActIds],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildInitialSceneActRuntimeState(params: {
|
||||
profile: CustomWorldProfile | null | undefined;
|
||||
sceneId: string | null | undefined;
|
||||
storyEngineMemory?: StoryEngineMemoryState | null;
|
||||
}): SceneActRuntimeState | null {
|
||||
const chapter = resolveSceneChapterBlueprint(params.profile, params.sceneId);
|
||||
if (!chapter || chapter.acts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const runtimeState = params.storyEngineMemory?.currentSceneActState;
|
||||
if (
|
||||
runtimeState &&
|
||||
runtimeState.sceneId === chapter.sceneId &&
|
||||
runtimeState.chapterId === chapter.id &&
|
||||
chapter.acts.some((entry) => entry.id === runtimeState.currentActId)
|
||||
) {
|
||||
return {
|
||||
...runtimeState,
|
||||
completedActIds: [...toSet(runtimeState.completedActIds ?? [])],
|
||||
visitedActIds: [...toSet(runtimeState.visitedActIds ?? [])],
|
||||
};
|
||||
}
|
||||
|
||||
const firstAct = chapter.acts[0]!;
|
||||
return {
|
||||
sceneId: chapter.sceneId,
|
||||
chapterId: chapter.id,
|
||||
currentActId: firstAct.id,
|
||||
currentActIndex: 0,
|
||||
completedActIds: [],
|
||||
visitedActIds: [firstAct.id],
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveActiveSceneActEncounterNpcIds(params: {
|
||||
profile: CustomWorldProfile | null | undefined;
|
||||
sceneId: string | null | undefined;
|
||||
storyEngineMemory?: StoryEngineMemoryState | null;
|
||||
}) {
|
||||
return (
|
||||
resolveActiveSceneActBlueprint(params)?.encounterNpcIds
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveActiveSceneActPrimaryNpcId(params: {
|
||||
profile: CustomWorldProfile | null | undefined;
|
||||
sceneId: string | null | undefined;
|
||||
storyEngineMemory?: StoryEngineMemoryState | null;
|
||||
}) {
|
||||
return resolveActiveSceneActBlueprint(params)?.primaryNpcId?.trim() || null;
|
||||
}
|
||||
|
||||
export function resolveActiveSceneActBackgroundImage(params: {
|
||||
profile: CustomWorldProfile | null | undefined;
|
||||
sceneId: string | null | undefined;
|
||||
storyEngineMemory?: StoryEngineMemoryState | null;
|
||||
}) {
|
||||
return resolveActiveSceneActBlueprint(params)?.backgroundImageSrc?.trim() || null;
|
||||
}
|
||||
|
||||
export function canUseLimitedPrimaryNpcChat(params: {
|
||||
profile: CustomWorldProfile | null | undefined;
|
||||
sceneId: string | null | undefined;
|
||||
storyEngineMemory?: StoryEngineMemoryState | null;
|
||||
npcId: string | null | undefined;
|
||||
affinity: number;
|
||||
}) {
|
||||
if (params.affinity >= 0 || !params.npcId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
resolveActiveSceneActPrimaryNpcId({
|
||||
profile: params.profile,
|
||||
sceneId: params.sceneId,
|
||||
storyEngineMemory: params.storyEngineMemory,
|
||||
}) === params.npcId
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveLimitedPrimaryNpcChatState(params: {
|
||||
state: Pick<GameState, 'customWorldProfile' | 'currentScenePreset' | 'storyEngineMemory'>;
|
||||
npcId: string | null | undefined;
|
||||
affinity: number;
|
||||
nextTurnCount: number;
|
||||
}): NpcChatTurnDirective | null {
|
||||
if (
|
||||
!canUseLimitedPrimaryNpcChat({
|
||||
profile: params.state.customWorldProfile,
|
||||
sceneId: params.state.currentScenePreset?.id ?? null,
|
||||
storyEngineMemory: params.state.storyEngineMemory,
|
||||
npcId: params.npcId,
|
||||
affinity: params.affinity,
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeAct = resolveActiveSceneActBlueprint({
|
||||
profile: params.state.customWorldProfile,
|
||||
sceneId: params.state.currentScenePreset?.id ?? null,
|
||||
storyEngineMemory: params.state.storyEngineMemory,
|
||||
});
|
||||
return {
|
||||
sceneActId: activeAct?.id ?? null,
|
||||
turnLimit: null,
|
||||
remainingTurns: null,
|
||||
limitReason: 'negative_affinity' as const,
|
||||
closingMode: 'free' as const,
|
||||
forceExitAfterTurn: false,
|
||||
terminationMode: 'hostile_model' as const,
|
||||
isHostileChat: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function getSceneConnectionDirectionText(
|
||||
relativePosition: SceneConnectionInfo['relativePosition'],
|
||||
) {
|
||||
switch (relativePosition) {
|
||||
case 'north':
|
||||
return '向北走';
|
||||
case 'south':
|
||||
return '向南走';
|
||||
case 'east':
|
||||
return '向东走';
|
||||
case 'west':
|
||||
return '向西走';
|
||||
case 'left':
|
||||
return '向左走';
|
||||
case 'right':
|
||||
return '向右走';
|
||||
case 'back':
|
||||
return '往回走';
|
||||
case 'up':
|
||||
return '向上走';
|
||||
case 'down':
|
||||
return '向下走';
|
||||
case 'inside':
|
||||
return '向内走';
|
||||
case 'outside':
|
||||
return '向外走';
|
||||
case 'portal':
|
||||
return '穿过通路';
|
||||
case 'forward':
|
||||
default:
|
||||
return '向前走';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user