import { buildCustomWorldPlayableNpcAttributeProfile, buildCustomWorldStoryNpcAttributeProfile, buildItemAttributeResonance, } from '../data/attributeProfileGenerator'; import { mergeCustomWorldPlayableNpcTags } from '../data/customWorldBuildTags'; import { normalizeCustomWorldLandmarks } from '../data/customWorldSceneGraph'; import { CustomWorldProfile, WorldType } from '../types'; import { normalizeCustomWorldProfile } from './customWorld'; import { normalizeCustomWorldOwnedSettingLayers } from './customWorldOwnedSettingLayers'; import { resolveCustomWorldCompatibilityTemplateWorldType } from './customWorldTheme'; import { buildFallbackActorNarrativeProfile, normalizeActorNarrativeProfile, } from './storyEngine/actorNarrativeProfile'; import { compileCampaignFromWorldProfile } from './storyEngine/campaignPackCompiler'; import { buildKnowledgeGraph } from './storyEngine/knowledgeGraph'; import { registerScenarioPack } from './storyEngine/scenarioPackRegistry'; import { buildSceneNarrativeResidues } from './storyEngine/sceneResidueCompiler'; import { buildThemePackFromWorldProfile, normalizeThemePack, } from './storyEngine/themePack'; import { buildThreadContractsFromProfile } from './storyEngine/threadContract'; import { buildFallbackWorldStoryGraph, normalizeWorldStoryGraph, } from './storyEngine/worldStoryGraph'; const PLAYABLE_TEMPLATE_CHARACTER_IDS = [ 'sword-princess', 'archer-hero', 'girl-hero', 'punch-hero', 'fighter-4', ] as const; function pickCyclic(items: readonly T[], index: number, label: string): T { const item = items[index % items.length]; if (item === undefined) { throw new Error(`Missing ${label}`); } return item; } function getPlayableTemplateCharacterId(index: number) { return pickCyclic( PLAYABLE_TEMPLATE_CHARACTER_IDS, index, 'playable template character id', ); } function normalizeTags(tags: string[], fallbackTags: string[] = []) { return [ ...new Set( [...tags, ...fallbackTags].map((tag) => tag.trim()).filter(Boolean), ), ].slice(0, 5); } function normalizeHooks(hooks: string[]) { const normalized = [ ...new Set(hooks.map((hook) => hook.trim()).filter(Boolean)), ]; if (normalized.length > 0) { return normalized.slice(0, 3); } return ['掌握关键线索']; } function clampText(value: string, maxLength: number) { const normalized = value.trim().replace(/\s+/g, ' '); if (normalized.length <= maxLength) { return normalized; } return `${normalized.slice(0, Math.max(0, maxLength - 1)).trim()}…`; } function slugify(value: string) { const ascii = value .toLowerCase() .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-') .replace(/^-+|-+$/g, ''); if (ascii) { return ascii.slice(0, 24); } return 'entry'; } function createEntryId(prefix: string, label: string, index: number) { return `${prefix}-${slugify(label || `${prefix}-${index + 1}`)}-${index + 1}`; } function dedupeByName(items: T[]) { const seen = new Set(); return items.filter((item) => { const key = item.name.trim(); if (!key || seen.has(key)) { return false; } seen.add(key); return true; }); } export interface CustomWorldBuilderOptions {} export function buildExpandedCustomWorldProfile( raw: unknown, settingText: string, _options: CustomWorldBuilderOptions = {}, ): CustomWorldProfile { const profile = normalizeCustomWorldProfile(raw, settingText); const attributeSchema = profile.attributeSchema; const playableNpcs = dedupeByName(profile.playableNpcs) .slice(0, PLAYABLE_TEMPLATE_CHARACTER_IDS.length) .map((npc, index) => { const templateCharacterId = npc.templateCharacterId ?? getPlayableTemplateCharacterId(index); return { ...npc, id: npc.id || createEntryId('playable-npc', npc.name, index), templateCharacterId, tags: mergeCustomWorldPlayableNpcTags(profile, npc, { templateCharacterId, maxCount: 5, }), attributeProfile: npc.attributeProfile ?? buildCustomWorldPlayableNpcAttributeProfile(npc, attributeSchema), }; }); const storyNpcs = dedupeByName(profile.storyNpcs).map((npc, index) => ({ ...npc, id: npc.id || createEntryId('story-npc', npc.name, index), description: clampText(npc.description, 72), motivation: clampText(npc.motivation, 72), relationshipHooks: normalizeHooks(npc.relationshipHooks), attributeProfile: npc.attributeProfile ?? buildCustomWorldStoryNpcAttributeProfile(npc, attributeSchema), })); const storyNpcIdByReference = new Map(); storyNpcs.forEach((npc) => { storyNpcIdByReference.set(npc.id, npc.id); storyNpcIdByReference.set(npc.name, npc.id); }); profile.storyNpcs.forEach((npc) => { const nextNpc = storyNpcs.find((entry) => entry.name === npc.name); if (!nextNpc) { return; } storyNpcIdByReference.set(npc.id, nextNpc.id); storyNpcIdByReference.set(npc.name, nextNpc.id); }); const landmarkDrafts = dedupeByName(profile.landmarks).map((landmark, index) => ({ ...landmark, id: landmark.id || createEntryId('landmark', landmark.name, index), description: clampText(landmark.description, 96), dangerLevel: landmark.dangerLevel || (resolveCustomWorldCompatibilityTemplateWorldType(profile) === WorldType.XIANXIA ? 'high' : 'medium'), })); const landmarkIdByReference = new Map(); landmarkDrafts.forEach((landmark) => { landmarkIdByReference.set(landmark.id, landmark.id); landmarkIdByReference.set(landmark.name, landmark.id); }); profile.landmarks.forEach((landmark) => { const nextLandmark = landmarkDrafts.find( (entry) => entry.name === landmark.name, ); if (!nextLandmark) { return; } landmarkIdByReference.set(landmark.id, nextLandmark.id); landmarkIdByReference.set(landmark.name, nextLandmark.id); }); const landmarks = normalizeCustomWorldLandmarks({ landmarks: landmarkDrafts.map((landmark) => ({ ...landmark, sceneNpcIds: landmark.sceneNpcIds.map( (npcId) => storyNpcIdByReference.get(npcId) ?? npcId, ), connections: landmark.connections.map((connection) => ({ targetLandmarkId: landmarkIdByReference.get(connection.targetLandmarkId) ?? connection.targetLandmarkId, relativePosition: connection.relativePosition, summary: connection.summary, })), })), storyNpcs, }); const items = dedupeByName(profile.items).map((item, index) => ({ ...item, id: item.id || createEntryId('item', item.name, index), description: clampText(item.description, 72), tags: normalizeTags(item.tags), attributeResonance: item.attributeResonance ?? buildItemAttributeResonance(item), })); const baseExpandedProfile = { ...profile, playableNpcs, storyNpcs, items, landmarks, } satisfies CustomWorldProfile; const themePack = normalizeThemePack( profile.themePack, buildThemePackFromWorldProfile(baseExpandedProfile), ); const storyGraph = normalizeWorldStoryGraph( profile.storyGraph, buildFallbackWorldStoryGraph(baseExpandedProfile, themePack), ); const enrichedPlayableNpcs = playableNpcs.map((npc) => ({ ...npc, narrativeProfile: normalizeActorNarrativeProfile( npc.narrativeProfile, buildFallbackActorNarrativeProfile(npc, storyGraph, themePack), ), })); const enrichedStoryNpcs = storyNpcs.map((npc) => ({ ...npc, narrativeProfile: normalizeActorNarrativeProfile( npc.narrativeProfile, buildFallbackActorNarrativeProfile(npc, storyGraph, themePack), ), })); const landmarksWithResidues = landmarks.map((landmark) => ({ ...landmark, narrativeResidues: landmark.narrativeResidues && landmark.narrativeResidues.length > 0 ? landmark.narrativeResidues : buildSceneNarrativeResidues({ sceneId: landmark.id, sceneName: landmark.name, profile: { ...baseExpandedProfile, playableNpcs: enrichedPlayableNpcs, storyNpcs: enrichedStoryNpcs, storyGraph, themePack, }, }), })); const profileWithNarrative = { ...baseExpandedProfile, playableNpcs: enrichedPlayableNpcs, storyNpcs: enrichedStoryNpcs, themePack, storyGraph, landmarks: landmarksWithResidues, } satisfies CustomWorldProfile; const knowledgeFacts = profile.knowledgeFacts && profile.knowledgeFacts.length > 0 ? profile.knowledgeFacts : buildKnowledgeGraph(profileWithNarrative); const threadContracts = profile.threadContracts && profile.threadContracts.length > 0 ? profile.threadContracts : buildThreadContractsFromProfile(profileWithNarrative); const compiledPacks = compileCampaignFromWorldProfile({ profile: { ...profileWithNarrative, knowledgeFacts, threadContracts, }, }); registerScenarioPack(compiledPacks.scenarioPack); const finalizedProfile = { ...profileWithNarrative, knowledgeFacts, threadContracts, scenarioPackId: profile.scenarioPackId ?? compiledPacks.scenarioPack.id, campaignPackId: profile.campaignPackId ?? compiledPacks.campaignPack.id, } satisfies CustomWorldProfile; return { ...finalizedProfile, ownedSettingLayers: normalizeCustomWorldOwnedSettingLayers( finalizedProfile.ownedSettingLayers, finalizedProfile, ), }; }