init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View 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 '向前走';
}
}