import { buildFallbackQuestIntent, compileQuestIntentToQuest, evaluateQuestOpportunity, } from '../../../src/data/questFlow.js'; import { parseJsonResponseText } from '../../../src/services/llmParsers.js'; import { buildQuestGenerationContextFromState } from '../../../src/services/questDirector.js'; import { buildQuestIntentPrompt, QUEST_INTENT_SYSTEM_PROMPT } from '../../../src/services/questPrompt.js'; import type { QuestIntent, QuestPreviewRequest } from '../../../src/services/questTypes.js'; import type { GameState } from '../../../src/types/game.js'; import type { Encounter } from '../../../src/types/scene.js'; import type { QuestLogEntry } from '../../../src/types/story.js'; import type { UpstreamLlmClient } from './llmClient.js'; function coerceString(value: unknown, fallback: string) { return typeof value === 'string' && value.trim() ? value.trim() : fallback; } function coerceStringArray(value: unknown, fallback: string[]) { if (!Array.isArray(value)) { return fallback; } const items = value .map((item) => (typeof item === 'string' ? item.trim() : '')) .filter(Boolean); return items.length > 0 ? items : fallback; } function sanitizeQuestIntent(rawIntent: unknown, fallback: QuestIntent): QuestIntent { if (!rawIntent || typeof rawIntent !== 'object') { return fallback; } const intent = rawIntent as Record; return { title: coerceString(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, 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) => [ '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), }; } export async function generateQuestForNpcEncounter( llmClient: UpstreamLlmClient, params: { state: GameState; encounter: Encounter; }, ): Promise { 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: QuestLogEntry) => ({ 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; } const fallbackIntent = buildFallbackQuestIntent(request); try { const content = await llmClient.requestMessageContent({ systemPrompt: QUEST_INTENT_SYSTEM_PROMPT, userPrompt: buildQuestIntentPrompt({ context: request.context!, scene: request.scene, opportunity, }), }); const parsed = parseJsonResponseText(content) as { intent?: unknown }; const intent = sanitizeQuestIntent(parsed.intent, fallbackIntent); return compileQuestIntentToQuest( { ...request, origin: 'ai_compiled', }, intent, ); } catch { return compileQuestIntentToQuest( { ...request, origin: 'fallback_builder', }, fallbackIntent, ); } }