@@ -23,6 +23,7 @@ import type {
|
||||
CharacterChatSuggestionsRequest,
|
||||
CharacterChatSummaryRequest,
|
||||
NpcChatDialogueRequest,
|
||||
NpcChatTurnDirective,
|
||||
NpcChatTurnRequest,
|
||||
NpcChatTurnResult,
|
||||
NpcRecruitDialogueRequest,
|
||||
@@ -977,6 +978,7 @@ export async function streamNpcChatTurn(
|
||||
state: GameState;
|
||||
turnCount: number;
|
||||
} | null;
|
||||
chatDirective?: NpcChatTurnDirective | null;
|
||||
} = {},
|
||||
) {
|
||||
const payload = {
|
||||
@@ -998,6 +1000,7 @@ export async function streamNpcChatTurn(
|
||||
turnCount: options.questOfferContext.turnCount,
|
||||
}
|
||||
: null,
|
||||
chatDirective: options.chatDirective ?? null,
|
||||
} satisfies NpcChatTurnRequest;
|
||||
|
||||
const response = await fetchWithApiAuth(
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
NpcDisclosureStage,
|
||||
NpcWarmthStage,
|
||||
PlayerStyleProfile,
|
||||
PlayerProgressionState,
|
||||
QuestStatus,
|
||||
ReleaseGateReport,
|
||||
ScenarioPack,
|
||||
@@ -212,6 +213,7 @@ export interface QuestGenerationContext {
|
||||
currentSceneTreasureHintCount?: number;
|
||||
recentStoryMoments: StoryMoment[];
|
||||
playerCharacter?: Character | null;
|
||||
playerProgression?: PlayerProgressionState | null;
|
||||
playerHp?: number;
|
||||
playerMaxHp?: number;
|
||||
playerMana?: number;
|
||||
|
||||
@@ -3,8 +3,8 @@ import { normalizeCustomWorldProfileRecord } from '../data/customWorldLibrary';
|
||||
import { type CustomWorldProfile, WorldType } from '../types';
|
||||
import { buildAgentDraftFoundationSettingText } from './customWorldAgentGenerationProgress';
|
||||
|
||||
function toText(value: unknown) {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
function toText(value: unknown, fallback = '') {
|
||||
return typeof value === 'string' ? value.trim() : fallback;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
@@ -178,6 +178,88 @@ function adaptDraftLandmarks(value: unknown, storyNpcIdSet: Set<string>) {
|
||||
.filter(Boolean) as AdaptedDraftLandmark[];
|
||||
}
|
||||
|
||||
function toStageCoverage(value: unknown) {
|
||||
const stageCoverage = Array.isArray(value)
|
||||
? value
|
||||
.filter((entry): entry is string => typeof entry === 'string')
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
return [...new Set(stageCoverage)];
|
||||
}
|
||||
|
||||
function adaptDraftSceneChapters(
|
||||
value: unknown,
|
||||
storyNpcIdSet: Set<string>,
|
||||
landmarkIdSet: Set<string>,
|
||||
) {
|
||||
return toRecordArray(value)
|
||||
.map((record, index) => {
|
||||
const sceneId = toText(record.sceneId);
|
||||
if (!sceneId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const acts = toRecordArray(record.acts)
|
||||
.map((actRecord, actIndex) => {
|
||||
const encounterNpcIds = toStringArray(
|
||||
actRecord.encounterNpcIds,
|
||||
).filter((entry) => storyNpcIdSet.has(entry));
|
||||
const primaryNpcId = toText(
|
||||
actRecord.primaryNpcId,
|
||||
encounterNpcIds[0] ?? '',
|
||||
);
|
||||
|
||||
return {
|
||||
id: toText(actRecord.id) || `scene-act-${sceneId}-${actIndex + 1}`,
|
||||
sceneId,
|
||||
title: toText(actRecord.title) || `第 ${actIndex + 1} 幕`,
|
||||
summary:
|
||||
toText(actRecord.summary) ||
|
||||
toText(actRecord.actGoal) ||
|
||||
`围绕${toText(record.sceneName, sceneId)}继续推进`,
|
||||
stageCoverage:
|
||||
toStageCoverage(actRecord.stageCoverage).length > 0
|
||||
? toStageCoverage(actRecord.stageCoverage)
|
||||
: actIndex === 0
|
||||
? ['opening']
|
||||
: ['climax', 'aftermath'],
|
||||
backgroundImageSrc:
|
||||
toText(actRecord.backgroundImageSrc) || undefined,
|
||||
encounterNpcIds,
|
||||
primaryNpcId,
|
||||
linkedThreadIds: toStringArray(actRecord.linkedThreadIds),
|
||||
advanceRule:
|
||||
toText(actRecord.advanceRule) || 'after_active_step_complete',
|
||||
actGoal: toText(actRecord.actGoal),
|
||||
transitionHook: toText(actRecord.transitionHook),
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.encounterNpcIds.length > 0 || entry.backgroundImageSrc,
|
||||
);
|
||||
|
||||
return {
|
||||
id: toText(record.id) || `scene-chapter-${sceneId}-${index + 1}`,
|
||||
sceneId,
|
||||
title: toText(record.title) || toText(record.sceneName) || sceneId,
|
||||
summary:
|
||||
toText(record.summary) ||
|
||||
toText(record.title) ||
|
||||
toText(record.sceneName) ||
|
||||
sceneId,
|
||||
linkedThreadIds: toStringArray(record.linkedThreadIds, 8),
|
||||
linkedLandmarkIds: toStringArray(record.linkedLandmarkIds, 8).filter(
|
||||
(entry) => landmarkIdSet.has(entry),
|
||||
),
|
||||
acts,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function buildCustomWorldProfileFromAgentDraft(
|
||||
session: CustomWorldAgentSessionSnapshot | null | undefined,
|
||||
): CustomWorldProfile | null {
|
||||
@@ -203,6 +285,13 @@ export function buildCustomWorldProfileFromAgentDraft(
|
||||
const storyNpcIdSet = new Set(
|
||||
storyNpcs.map((entry) => toText(entry.id)).filter(Boolean),
|
||||
);
|
||||
const adaptedLandmarks = adaptDraftLandmarks(
|
||||
draftProfile.landmarks,
|
||||
storyNpcIdSet,
|
||||
);
|
||||
const landmarkIdSet = new Set(
|
||||
adaptedLandmarks.map((entry) => toText(entry.id)).filter(Boolean),
|
||||
);
|
||||
const normalized = normalizeCustomWorldProfileRecord({
|
||||
id: `agent-draft-${session.sessionId}`,
|
||||
settingText,
|
||||
@@ -220,7 +309,7 @@ export function buildCustomWorldProfileFromAgentDraft(
|
||||
coreConflicts: toStringArray(draftProfile.coreConflicts, 6),
|
||||
playableNpcs,
|
||||
storyNpcs,
|
||||
landmarks: adaptDraftLandmarks(draftProfile.landmarks, storyNpcIdSet),
|
||||
landmarks: adaptedLandmarks,
|
||||
camp: isRecord(draftProfile.camp)
|
||||
? {
|
||||
name: toText(draftProfile.camp.name),
|
||||
@@ -231,6 +320,11 @@ export function buildCustomWorldProfileFromAgentDraft(
|
||||
imageSrc: toText(draftProfile.camp.imageSrc) || undefined,
|
||||
}
|
||||
: undefined,
|
||||
sceneChapterBlueprints: adaptDraftSceneChapters(
|
||||
draftProfile.sceneChapters,
|
||||
storyNpcIdSet,
|
||||
landmarkIdSet,
|
||||
),
|
||||
anchorContent: session.anchorContent,
|
||||
creatorIntent: session.creatorIntent,
|
||||
anchorPack: session.anchorPack,
|
||||
|
||||
175
src/services/customWorldSceneActRuntime.ts
Normal file
175
src/services/customWorldSceneActRuntime.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import type { NpcChatTurnDirective } from '../../packages/shared/src/contracts/story';
|
||||
import type {
|
||||
CustomWorldProfile,
|
||||
GameState,
|
||||
SceneActBlueprint,
|
||||
SceneChapterBlueprint,
|
||||
SceneActRuntimeState,
|
||||
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 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,
|
||||
});
|
||||
const turnLimit = 5;
|
||||
const remainingTurns = Math.max(0, turnLimit - params.nextTurnCount);
|
||||
|
||||
return {
|
||||
sceneActId: activeAct?.id ?? null,
|
||||
turnLimit,
|
||||
remainingTurns,
|
||||
limitReason: 'negative_affinity' as const,
|
||||
closingMode:
|
||||
params.nextTurnCount >= turnLimit
|
||||
? ('foreshadow_close' as const)
|
||||
: ('free' as const),
|
||||
forceExitAfterTurn: params.nextTurnCount >= turnLimit,
|
||||
};
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
import {getNpcDisclosureStage, getNpcWarmthStage} from '../data/npcInteractions';
|
||||
import {
|
||||
getNpcDisclosureStage,
|
||||
getNpcWarmthStage,
|
||||
} from '../data/npcInteractions';
|
||||
import {
|
||||
buildFallbackQuestIntent,
|
||||
compileQuestIntentToQuest,
|
||||
evaluateQuestOpportunity,
|
||||
} from '../data/questFlow';
|
||||
import type {
|
||||
Encounter,
|
||||
GameState,
|
||||
QuestLogEntry,
|
||||
} from '../types';
|
||||
import type {QuestGenerationContext} from './aiTypes';
|
||||
import type { Encounter, GameState, QuestLogEntry } from '../types';
|
||||
import type { QuestGenerationContext } from './aiTypes';
|
||||
import { requestJson } from './apiClient';
|
||||
import {requestChatMessageContent} from './llmClient';
|
||||
import {parseJsonResponseText} from './llmParsers';
|
||||
import {buildQuestIntentPrompt, QUEST_INTENT_SYSTEM_PROMPT} from './questPrompt';
|
||||
import type {QuestIntent, QuestPreviewRequest} from './questTypes';
|
||||
import { requestChatMessageContent } from './llmClient';
|
||||
import { parseJsonResponseText } from './llmParsers';
|
||||
import {
|
||||
buildQuestIntentPrompt,
|
||||
QUEST_INTENT_SYSTEM_PROMPT,
|
||||
} from './questPrompt';
|
||||
import type { QuestIntent, QuestPreviewRequest } from './questTypes';
|
||||
import {
|
||||
buildFallbackActorNarrativeProfile,
|
||||
normalizeActorNarrativeProfile,
|
||||
@@ -47,16 +49,13 @@ function coerceStringArray(value: unknown, fallback: string[]) {
|
||||
}
|
||||
|
||||
const items = value
|
||||
.map(item => (typeof item === 'string' ? item.trim() : ''))
|
||||
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
||||
.filter(Boolean);
|
||||
|
||||
return items.length > 0 ? items : fallback;
|
||||
}
|
||||
|
||||
function resolveIssuerNarrativeProfile(
|
||||
state: GameState,
|
||||
encounter: Encounter,
|
||||
) {
|
||||
function resolveIssuerNarrativeProfile(state: GameState, encounter: Encounter) {
|
||||
if (encounter.narrativeProfile) {
|
||||
return encounter.narrativeProfile;
|
||||
}
|
||||
@@ -65,22 +64,22 @@ function resolveIssuerNarrativeProfile(
|
||||
}
|
||||
|
||||
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,
|
||||
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);
|
||||
state.customWorldProfile.themePack ??
|
||||
buildThemePackFromWorldProfile(state.customWorldProfile);
|
||||
const storyGraph =
|
||||
state.customWorldProfile.storyGraph
|
||||
?? buildFallbackWorldStoryGraph(state.customWorldProfile, themePack);
|
||||
state.customWorldProfile.storyGraph ??
|
||||
buildFallbackWorldStoryGraph(state.customWorldProfile, themePack);
|
||||
|
||||
return normalizeActorNarrativeProfile(
|
||||
role.narrativeProfile,
|
||||
@@ -88,7 +87,10 @@ function resolveIssuerNarrativeProfile(
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeQuestIntent(rawIntent: unknown, fallback: QuestIntent): QuestIntent {
|
||||
function sanitizeQuestIntent(
|
||||
rawIntent: unknown,
|
||||
fallback: QuestIntent,
|
||||
): QuestIntent {
|
||||
if (!rawIntent || typeof rawIntent !== 'object') {
|
||||
return fallback;
|
||||
}
|
||||
@@ -99,44 +101,56 @@ function sanitizeQuestIntent(rawIntent: unknown, fallback: QuestIntent): QuestIn
|
||||
title: coerceQuestTitle(intent.title, fallback.title),
|
||||
description: coerceString(intent.description, fallback.description),
|
||||
summary: coerceString(intent.summary, fallback.summary),
|
||||
narrativeType: (
|
||||
typeof intent.narrativeType === 'string'
|
||||
&& ['bounty', 'escort', 'investigation', 'retrieval', 'relationship', 'trial'].includes(intent.narrativeType)
|
||||
)
|
||||
? intent.narrativeType as QuestIntent['narrativeType']
|
||||
: fallback.narrativeType,
|
||||
narrativeType:
|
||||
typeof intent.narrativeType === 'string' &&
|
||||
[
|
||||
'bounty',
|
||||
'escort',
|
||||
'investigation',
|
||||
'retrieval',
|
||||
'relationship',
|
||||
'trial',
|
||||
].includes(intent.narrativeType)
|
||||
? (intent.narrativeType as QuestIntent['narrativeType'])
|
||||
: fallback.narrativeType,
|
||||
dramaticNeed: coerceString(intent.dramaticNeed, fallback.dramaticNeed),
|
||||
issuerGoal: coerceString(intent.issuerGoal, fallback.issuerGoal),
|
||||
playerHook: coerceString(intent.playerHook, fallback.playerHook),
|
||||
worldReason: coerceString(intent.worldReason, fallback.worldReason),
|
||||
recommendedObjectiveKinds: coerceStringArray(intent.recommendedObjectiveKinds, fallback.recommendedObjectiveKinds)
|
||||
.filter(kind => [
|
||||
recommendedObjectiveKinds: coerceStringArray(
|
||||
intent.recommendedObjectiveKinds,
|
||||
fallback.recommendedObjectiveKinds,
|
||||
).filter((kind) =>
|
||||
[
|
||||
'defeat_hostile_npc',
|
||||
'inspect_treasure',
|
||||
'spar_with_npc',
|
||||
'talk_to_npc',
|
||||
'reach_scene',
|
||||
'deliver_item',
|
||||
].includes(kind)) as QuestIntent['recommendedObjectiveKinds'],
|
||||
urgency: (
|
||||
typeof intent.urgency === 'string'
|
||||
&& ['low', 'medium', 'high'].includes(intent.urgency)
|
||||
)
|
||||
? intent.urgency as QuestIntent['urgency']
|
||||
: fallback.urgency,
|
||||
intimacy: (
|
||||
typeof intent.intimacy === 'string'
|
||||
&& ['transactional', 'cooperative', 'trust_based'].includes(intent.intimacy)
|
||||
)
|
||||
? intent.intimacy as QuestIntent['intimacy']
|
||||
: fallback.intimacy,
|
||||
rewardTheme: (
|
||||
typeof intent.rewardTheme === 'string'
|
||||
&& ['currency', 'resource', 'relationship', 'intel', 'rare_item'].includes(intent.rewardTheme)
|
||||
)
|
||||
? intent.rewardTheme as QuestIntent['rewardTheme']
|
||||
: fallback.rewardTheme,
|
||||
followupHooks: coerceStringArray(intent.followupHooks, fallback.followupHooks),
|
||||
].includes(kind),
|
||||
) as QuestIntent['recommendedObjectiveKinds'],
|
||||
urgency:
|
||||
typeof intent.urgency === 'string' &&
|
||||
['low', 'medium', 'high'].includes(intent.urgency)
|
||||
? (intent.urgency as QuestIntent['urgency'])
|
||||
: fallback.urgency,
|
||||
intimacy:
|
||||
typeof intent.intimacy === 'string' &&
|
||||
['transactional', 'cooperative', 'trust_based'].includes(intent.intimacy)
|
||||
? (intent.intimacy as QuestIntent['intimacy'])
|
||||
: fallback.intimacy,
|
||||
rewardTheme:
|
||||
typeof intent.rewardTheme === 'string' &&
|
||||
['currency', 'resource', 'relationship', 'intel', 'rare_item'].includes(
|
||||
intent.rewardTheme,
|
||||
)
|
||||
? (intent.rewardTheme as QuestIntent['rewardTheme'])
|
||||
: fallback.rewardTheme,
|
||||
followupHooks: coerceStringArray(
|
||||
intent.followupHooks,
|
||||
fallback.followupHooks,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,10 +158,13 @@ export function buildQuestGenerationContextFromState(params: {
|
||||
state: GameState;
|
||||
encounter: Encounter;
|
||||
}): QuestGenerationContext {
|
||||
const {state, encounter} = params;
|
||||
const { state, encounter } = params;
|
||||
const issuerNpcId = encounter.id ?? encounter.npcName;
|
||||
const issuerState = state.npcStates[issuerNpcId];
|
||||
const issuerNarrativeProfile = resolveIssuerNarrativeProfile(state, encounter);
|
||||
const issuerNarrativeProfile = resolveIssuerNarrativeProfile(
|
||||
state,
|
||||
encounter,
|
||||
);
|
||||
|
||||
return {
|
||||
worldType: state.worldType,
|
||||
@@ -164,16 +181,18 @@ export function buildQuestGenerationContextFromState(params: {
|
||||
issuerDisclosureStage: getNpcDisclosureStage(issuerState?.affinity ?? 0),
|
||||
issuerWarmthStage: getNpcWarmthStage(issuerState?.affinity ?? 0),
|
||||
activeThreadIds:
|
||||
state.storyEngineMemory?.activeThreadIds?.slice(0, 4)
|
||||
?? issuerNarrativeProfile?.relatedThreadIds?.slice(0, 4)
|
||||
?? [],
|
||||
state.storyEngineMemory?.activeThreadIds?.slice(0, 4) ??
|
||||
issuerNarrativeProfile?.relatedThreadIds?.slice(0, 4) ??
|
||||
[],
|
||||
encounterKind: encounter.kind ?? 'npc',
|
||||
currentSceneTreasureHintCount: state.currentScenePreset?.treasureHints?.length ?? 0,
|
||||
currentSceneTreasureHintCount:
|
||||
state.currentScenePreset?.treasureHints?.length ?? 0,
|
||||
currentSceneHostileNpcIds: (state.currentScenePreset?.npcs ?? [])
|
||||
.filter(npc => Boolean(npc.hostile || npc.monsterPresetId))
|
||||
.map(npc => npc.id),
|
||||
.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,
|
||||
@@ -182,7 +201,7 @@ export function buildQuestGenerationContextFromState(params: {
|
||||
playerEquipment: state.playerEquipment,
|
||||
activeCompanions: state.companions,
|
||||
rosterCompanions: state.roster,
|
||||
currentQuestSummary: state.quests.map(quest => ({
|
||||
currentQuestSummary: state.quests.map((quest) => ({
|
||||
id: quest.id,
|
||||
title: quest.title,
|
||||
status: quest.status,
|
||||
@@ -195,7 +214,7 @@ export async function generateQuestForNpcEncounter(params: {
|
||||
state: GameState;
|
||||
encounter: Encounter;
|
||||
}): Promise<QuestLogEntry | null> {
|
||||
const {state, encounter} = params;
|
||||
const { state, encounter } = params;
|
||||
const issuerNpcId = encounter.id ?? encounter.npcName;
|
||||
const request: QuestPreviewRequest = {
|
||||
issuerNpcId,
|
||||
@@ -203,12 +222,12 @@ export async function generateQuestForNpcEncounter(params: {
|
||||
roleText: encounter.context,
|
||||
scene: state.currentScenePreset,
|
||||
worldType: state.worldType,
|
||||
currentQuests: state.quests.map(quest => ({
|
||||
currentQuests: state.quests.map((quest) => ({
|
||||
id: quest.id,
|
||||
issuerNpcId: quest.issuerNpcId,
|
||||
status: quest.status,
|
||||
})),
|
||||
context: buildQuestGenerationContextFromState({state, encounter}),
|
||||
context: buildQuestGenerationContextFromState({ state, encounter }),
|
||||
origin: 'ai_compiled',
|
||||
};
|
||||
const opportunity = evaluateQuestOpportunity(request);
|
||||
@@ -257,7 +276,7 @@ export async function generateQuestForNpcEncounter(params: {
|
||||
debugLabel: 'quest-intent',
|
||||
},
|
||||
);
|
||||
const parsed = parseJsonResponseText(content) as {intent?: unknown};
|
||||
const parsed = parseJsonResponseText(content) as { intent?: unknown };
|
||||
const intent = sanitizeQuestIntent(parsed.intent, fallbackIntent);
|
||||
return compileQuestIntentToQuest(
|
||||
{
|
||||
@@ -267,7 +286,10 @@ export async function generateQuestForNpcEncounter(params: {
|
||||
intent,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[QuestDirector] falling back to deterministic quest intent', error);
|
||||
console.warn(
|
||||
'[QuestDirector] falling back to deterministic quest intent',
|
||||
error,
|
||||
);
|
||||
return compileQuestIntentToQuest(
|
||||
{
|
||||
...request,
|
||||
|
||||
@@ -46,6 +46,7 @@ export function createEmptyStoryEngineMemoryState(): StoryEngineMemoryState {
|
||||
resolvedScarIds: [],
|
||||
recentCarrierIds: [],
|
||||
openedSceneChapterIds: [],
|
||||
currentSceneActState: null,
|
||||
recentSignalIds: [],
|
||||
recentCompanionReactions: [],
|
||||
currentChapter: null,
|
||||
|
||||
Reference in New Issue
Block a user