This commit is contained in:
2026-04-21 19:18:26 +08:00
parent 4372ab5be1
commit 48957311bc
78 changed files with 643 additions and 3801 deletions

View File

@@ -1,139 +0,0 @@
import {
getNpcDisclosureStage,
getNpcWarmthStage,
} from '../data/npcInteractions';
import { evaluateQuestOpportunity } from '../data/questFlow';
import type { Encounter, GameState, QuestLogEntry } from '../types';
import type { QuestGenerationContext } from './aiTypes';
import { requestJson } from './apiClient';
import type { QuestPreviewRequest } from './questTypes';
import {
buildFallbackActorNarrativeProfile,
normalizeActorNarrativeProfile,
} from './storyEngine/actorNarrativeProfile';
import { buildThemePackFromWorldProfile } from './storyEngine/themePack';
import { buildFallbackWorldStoryGraph } from './storyEngine/worldStoryGraph';
function resolveIssuerNarrativeProfile(state: GameState, encounter: Encounter) {
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),
);
}
export function buildQuestGenerationContextFromState(params: {
state: GameState;
encounter: Encounter;
}): QuestGenerationContext {
const { state, encounter } = params;
const issuerNpcId = encounter.id ?? encounter.npcName;
const issuerState = state.npcStates[issuerNpcId];
const issuerNarrativeProfile = resolveIssuerNarrativeProfile(
state,
encounter,
);
return {
worldType: state.worldType,
customWorldProfile: state.customWorldProfile ?? null,
actState: state.storyEngineMemory?.actState ?? null,
currentSceneId: state.currentScenePreset?.id ?? null,
currentSceneName: state.currentScenePreset?.name ?? null,
currentSceneDescription: state.currentScenePreset?.description ?? null,
issuerNpcId,
issuerNpcName: encounter.npcName,
issuerNpcContext: encounter.context,
issuerAffinity: issuerState?.affinity ?? 0,
issuerNarrativeProfile,
issuerDisclosureStage: getNpcDisclosureStage(issuerState?.affinity ?? 0),
issuerWarmthStage: getNpcWarmthStage(issuerState?.affinity ?? 0),
activeThreadIds:
state.storyEngineMemory?.activeThreadIds?.slice(0, 4) ??
issuerNarrativeProfile?.relatedThreadIds?.slice(0, 4) ??
[],
encounterKind: encounter.kind ?? 'npc',
currentSceneTreasureHintCount:
state.currentScenePreset?.treasureHints?.length ?? 0,
currentSceneHostileNpcIds: (state.currentScenePreset?.npcs ?? [])
.filter((npc) => Boolean(npc.hostile || npc.monsterPresetId))
.map((npc) => npc.id),
recentStoryMoments: state.storyHistory.slice(-6),
playerCharacter: state.playerCharacter,
playerProgression: state.playerProgression ?? null,
playerHp: state.playerHp,
playerMaxHp: state.playerMaxHp,
playerMana: state.playerMana,
playerMaxMana: state.playerMaxMana,
playerInventory: state.playerInventory,
playerEquipment: state.playerEquipment,
activeCompanions: state.companions,
rosterCompanions: state.roster,
currentQuestSummary: state.quests.map((quest) => ({
id: quest.id,
title: quest.title,
status: quest.status,
issuerNpcId: quest.issuerNpcId,
})),
};
}
export async function generateQuestForNpcEncounter(params: {
state: GameState;
encounter: Encounter;
}): Promise<QuestLogEntry | null> {
const { state, encounter } = params;
const issuerNpcId = encounter.id ?? encounter.npcName;
const request: QuestPreviewRequest = {
issuerNpcId,
issuerNpcName: encounter.npcName,
roleText: encounter.context,
scene: state.currentScenePreset,
worldType: state.worldType,
currentQuests: state.quests.map((quest) => ({
id: quest.id,
issuerNpcId: quest.issuerNpcId,
status: quest.status,
})),
context: buildQuestGenerationContextFromState({ state, encounter }),
origin: 'ai_compiled',
};
const opportunity = evaluateQuestOpportunity(request);
if (!opportunity.shouldOffer) {
return null;
}
return requestJson<QuestLogEntry | null>(
'/api/runtime/quests/generate',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
},
'任务生成失败',
);
}

View File

@@ -1 +0,0 @@
export * from '../prompts/questPrompts';

View File

@@ -1 +0,0 @@
export * from '../prompts/runtimeItemPrompts';

View File

@@ -1,43 +0,0 @@
import { describe, expect, it } from 'vitest';
import { buildContentDependencyGraph } from './contentDependencyGraph';
describe('contentDependencyGraph', () => {
it('connects scenario, campaign, world, companions, and threads', () => {
const graph = buildContentDependencyGraph({
scenarioPack: {
id: 'scenario-1',
title: 'Scenario',
version: '0.1.0',
worldPackIds: ['world-1'],
campaignIds: ['campaign-1'],
sharedConstraintPackIds: [],
},
campaignPack: {
id: 'campaign-1',
scenarioPackId: 'scenario-1',
title: 'Campaign',
authoringStyle: 'classic',
campaignStateSeed: {
id: 'campaign-state',
title: 'Campaign',
currentActId: 'act-1',
currentActIndex: 0,
},
actTemplates: [],
requiredCompanionIds: [],
},
profile: {
id: 'world-1',
name: 'World',
playableNpcs: [{ id: 'npc-1', name: 'A' }],
storyGraph: {
visibleThreads: [{ id: 'thread-1', title: 'T1' }],
},
} as never,
});
expect(graph.nodes.length).toBeGreaterThan(2);
expect(graph.edges.some((edge) => edge.from === 'campaign-1' && edge.to === 'world-1')).toBe(true);
});
});

View File

@@ -1,76 +0,0 @@
import type {
CampaignPack,
CustomWorldProfile,
ScenarioPack,
} from '../../types';
export interface ContentDependencyNode {
id: string;
type: 'scenario' | 'campaign' | 'world' | 'thread' | 'companion' | 'constraint';
label: string;
}
export interface ContentDependencyEdge {
from: string;
to: string;
reason: string;
}
export function buildContentDependencyGraph(params: {
scenarioPack: ScenarioPack;
campaignPack: CampaignPack;
profile: CustomWorldProfile;
}) {
const nodes: ContentDependencyNode[] = [
{
id: params.scenarioPack.id,
type: 'scenario',
label: params.scenarioPack.title,
},
{
id: params.campaignPack.id,
type: 'campaign',
label: params.campaignPack.title,
},
{
id: params.profile.id,
type: 'world',
label: params.profile.name,
},
...params.profile.playableNpcs.map((npc) => ({
id: npc.id,
type: 'companion',
label: npc.name,
} as ContentDependencyNode)),
...(params.profile.storyGraph?.visibleThreads ?? []).map((thread) => ({
id: thread.id,
type: 'thread',
label: thread.title,
} as ContentDependencyNode)),
];
const edges: ContentDependencyEdge[] = [
{
from: params.scenarioPack.id,
to: params.campaignPack.id,
reason: 'scenario contains campaign',
},
{
from: params.campaignPack.id,
to: params.profile.id,
reason: 'campaign depends on world profile',
},
...params.profile.playableNpcs.map((npc) => ({
from: params.campaignPack.id,
to: npc.id,
reason: 'campaign references companion',
})),
...(params.profile.storyGraph?.visibleThreads ?? []).map((thread) => ({
from: params.campaignPack.id,
to: thread.id,
reason: 'campaign advances thread',
})),
];
return { nodes, edges };
}