1
This commit is contained in:
@@ -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),
|
||||
},
|
||||
'任务生成失败',
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from '../prompts/questPrompts';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '../prompts/runtimeItemPrompts';
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user