import type { CustomWorldLibraryEntry, CustomWorldProfileRecord, } from './runtime'; import type { RpgAgentSupportedAction } from './rpgAgentActions'; import type { RpgCreationAnchorContent } from './rpgAgentAnchors'; import type { RpgAgentAssetCoverageSummary, RpgAgentDraftCardSummary, RpgAgentFoundationDraftProfile, } from './rpgAgentDraft'; import type { RpgAgentSessionSnapshot } from './rpgAgentSession'; import type { RpgCreationPreviewEnvelope } from './rpgCreationPreview'; import type { ListRpgCreationWorksResponse, RpgCreationWorkSummary, } from './rpgCreationWorkSummary'; const RPG_CREATION_FIXTURE_SESSION_ID = 'rpg-session-fixture'; const RPG_CREATION_FIXTURE_PROFILE_ID = 'rpg-profile-fixture'; const RPG_CREATION_FIXTURE_USER_ID = 'fixture-user'; const RPG_CREATION_FIXTURE_UPDATED_AT = '2026-04-21T09:30:00.000Z'; const RPG_CREATION_FIXTURE_PUBLISHED_AT = '2026-04-21T10:00:00.000Z'; function cloneFixture(value: T): T { return JSON.parse(JSON.stringify(value)) as T; } /** * 共享八锚点 fixture。 * 用于 contract test、session fixture 和 works 集成测试复用同一份创作意图样本。 */ export function createRpgCreationAnchorContentFixture(): RpgCreationAnchorContent { return cloneFixture({ worldPromise: { hook: '被海雾吞没的旧航路群岛', differentiator: '灯塔与禁航令共同决定谁能活着穿过去。', desiredExperience: '压抑、悬疑、潮湿', }, playerFantasy: { playerRole: '玩家回到群岛调查沉船真相。', corePursuit: '找出失控航路背后的真相。', fearOfLoss: '失去最后一个还能对上旧案的人。', }, themeBoundary: { toneKeywords: ['压抑', '潮湿', '悬疑'], aestheticDirectives: ['旧灯塔', '潮雾', '断裂航路'], forbiddenDirectives: ['不要出现现代枪械'], }, playerEntryPoint: { openingIdentity: '被迫返乡的失职守灯人', openingProblem: '首夜就有陌生船只闯入禁航区。', entryMotivation: '查清沉船夜里被谁改动了灯册。', }, coreConflict: { surfaceConflicts: ['守灯会与航运公会争夺旧航路控制权'], hiddenCrisis: '沉船夜的航灯与灯册被人动过手脚。', firstTouchedConflict: '玩家开局就会撞上新的封航命令。', }, keyRelationships: [ { pairs: '玩家 / 沈砺', relationshipType: '旧友兼潜在背叛者', secretOrCost: '沈砺暗地里在替沉船商盟引路。', }, ], hiddenLines: { hiddenTruths: ['沉船夜的真实失误并不是单纯天灾。'], misdirectionHints: ['所有人都会先把问题推给潮雾本身。'], revealPacing: '第一章露出痕迹,第二章才让玩家摸到灯册线。', }, iconicElements: { iconicMotifs: ['会移动的海雾'], institutionsOrArtifacts: ['回潮旧灯塔', '封灯令', '旧潮图'], hardRules: ['禁航信号一旦点亮,任何船都必须退航。'], }, } satisfies RpgCreationAnchorContent); } /** * 共享 foundation draft fixture。 * 这份样本同时服务 session 草稿、preview 适配回归测试和 works 聚合测试。 */ export function createRpgAgentFoundationDraftProfileFixture(): RpgAgentFoundationDraftProfile { return cloneFixture({ name: '潮雾列岛', subtitle: '旧灯塔与失控航路', summary: '第一版世界底稿已经整理完成,玩家将从回潮旧灯塔切入沉船旧案。', tone: '压抑、潮湿、悬疑', playerGoal: '查清沉船与禁航区异动的真相。', majorFactions: ['守灯会', '航运公会'], coreConflicts: ['守灯会与航运公会争夺旧航路控制权'], playableNpcs: [ { id: 'playable-1', name: '沈砺', title: '旧航路引路人', role: '关键同行者', publicIdentity: '最熟悉旧航路的人。', publicMask: '看上去像可靠旧友。', currentPressure: '他必须在两股势力间站队。', hiddenHook: '暗中替沉船商盟引路。', relationToPlayer: '旧友兼潜在背叛者', threadIds: ['thread-1'], summary: '他像旧友,但也像一把始终没收回鞘的刀。', skills: [ { id: 'skill-playable-1', name: '潮行引路', actionPreviewConfig: { basePath: '/generated-characters/playable-1/animations/skills/skill-playable-1', }, }, ], imageSrc: '/generated-characters/playable-1/visual/asset-runtime/master.png', generatedVisualAssetId: 'asset-runtime-playable', generatedAnimationSetId: 'animation-set-playable-1', animationMap: { idle: { basePath: '/generated-characters/playable-1/animations/idle', }, run: { basePath: '/generated-characters/playable-1/animations/run', }, attack: { basePath: '/generated-characters/playable-1/animations/attack', }, }, }, ], storyNpcs: [ { id: 'story-1', name: '顾潮音', title: '守灯会值夜人', role: '场景关键角色', publicIdentity: '负责夜间巡灯与封锁。', publicMask: '对外一直冷静克制。', currentPressure: '她知道更多禁航区真相。', hiddenHook: '曾亲眼见过失控海雾吞船。', relationToPlayer: '最早愿意交换线索的人', threadIds: ['thread-1'], summary: '她总像比所有人更早知道海雾会往哪一侧压下来。', skills: [ { id: 'skill-story-1', name: '夜潮灯语', actionPreviewConfig: { basePath: '/generated-characters/story-1/animations/skills/skill-story-1', }, }, ], imageSrc: '/generated-characters/story-1/visual/asset-runtime/master.png', generatedVisualAssetId: 'asset-runtime-story', generatedAnimationSetId: 'animation-set-story-1', animationMap: { run: { basePath: '/generated-characters/story-1/animations/run', }, attack: { basePath: '/generated-characters/story-1/animations/attack', }, }, }, ], landmarks: [ { id: 'landmark-1', name: '回潮旧灯塔', description: '被海雾啃旧的石塔仍在夜里维持着微弱灯火。', purpose: '观察雾潮与往来船只', mood: '潮湿、压抑、风声不止', importance: '开局核心场景', secret: '高处潮痕说明海面异常抬升过。', imageSrc: '/generated-custom-world-scenes/landmark-1/latest-scene.png', generatedSceneAssetId: 'scene-asset-landmark-1', characterIds: ['story-1'], threadIds: ['thread-1'], summary: '旧灯塔是整片群岛最先看见异动的地方。', }, ], camp: { id: 'camp-1', name: '回潮暂栖所', description: '能暂时收拢队伍、整理灯册与潮路线索的落脚点。', mood: '克制、紧绷,但还能暂时收拢局势', imageSrc: '/custom/camp/huichao.png', generatedSceneAssetId: 'scene-asset-camp-1', summary: '玩家能在这里整理情报、回看旧灯册和沉船名单。', }, themePack: { id: 'theme-pack:tide', displayName: '潮雾悬疑', }, storyGraph: { visibleThreads: [ { id: 'thread-visible-1', title: '封航争夺', }, ], }, factions: [ { id: 'faction-1', name: '守灯会', title: '守灯会', subtitle: '把控禁航灯令的人', publicGoal: '维持封航秩序并压住灯册流出。', relatedConflict: '想把旧案继续压在禁航记录之下。', tension: '他们越强调规矩,越像在遮掩灯册。', playerRelation: '玩家迟早要与他们正面冲突。', summary: '掌握灯塔与封航令的势力,也是最怕旧案被翻出来的一方。', }, ], threads: [ { id: 'thread-1', title: '沉船旧案', type: 'main', conflictType: '真相遮蔽', conflict: '沉船夜的航灯与灯册被人动过手脚。', stakes: '真相一旦坐实,群岛秩序会先崩。', characterIds: ['playable-1', 'story-1'], landmarkIds: ['landmark-1'], summary: '玩家会从灯塔高处潮痕一路追到沉船夜的真相。', }, ], chapters: [ { id: 'chapter-1', title: '灯塔回潮', openingEvent: '禁航区闯入了一艘不该出现的陌生船。', playerGoal: '先稳住局势,再拿到第一份灯册线索。', characterIds: ['playable-1', 'story-1'], landmarkIds: ['landmark-1'], understandingShift: '玩家会意识到沉船旧案至今仍在操控群岛秩序。', summary: '第一章聚焦灯塔与封航令,给玩家一条可追的旧案线索。', }, ], sceneChapters: [ { id: 'scene-chapter-1', sceneId: 'landmark-1', sceneName: '回潮旧灯塔', title: '灯塔初章', summary: '围绕灯塔推进的首个场景章节。', linkedThreadIds: ['thread-1'], linkedLandmarkIds: ['landmark-1'], acts: [ { id: 'scene-act-1', title: '第一幕', summary: '先接住回潮灯塔的入口压力。', stageCoverage: ['opening'], backgroundImageSrc: '/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png', backgroundAssetId: 'scene-asset-runtime', encounterNpcIds: ['story-1'], primaryNpcId: 'story-1', linkedThreadIds: ['thread-1'], actGoal: '接住首幕入口', transitionHook: '向第二幕推进。', advanceRule: 'after_primary_contact', }, ], }, ], worldHook: '被海雾吞没的旧航路群岛', playerPremise: '玩家回到群岛调查沉船真相。', openingSituation: '首夜就有陌生船只闯入禁航区。', iconicElements: ['会移动的海雾', '回潮旧灯塔'], sourceAnchorSummary: '海雾、旧灯塔、失控航路。', } satisfies RpgAgentFoundationDraftProfile); } function createRpgAgentDraftCardsFixture(): RpgAgentDraftCardSummary[] { return cloneFixture([ { id: 'world-foundation', kind: 'world', title: '潮雾列岛', subtitle: '旧灯塔与失控航路', summary: '第一版世界底稿已经整理完成。', status: 'suggested', linkedIds: ['playable-1', 'story-1', 'landmark-1'], warningCount: 0, }, { id: 'playable-1', kind: 'character', title: '沈砺', subtitle: '旧航路引路人 / 动作已齐', summary: '最熟悉旧航路的人,也可能是最危险的旧友。', status: 'suggested', linkedIds: ['thread-1', 'landmark-1'], warningCount: 0, assetStatus: 'complete', assetStatusLabel: '动作已齐', }, { id: 'landmark-1', kind: 'landmark', title: '回潮旧灯塔', subtitle: '观察雾潮与往来船只', summary: '旧灯塔是整片群岛最先看见异动的地方。', status: 'suggested', linkedIds: ['story-1', 'thread-1'], warningCount: 0, }, ] satisfies RpgAgentDraftCardSummary[]); } function createRpgAgentAssetCoverageFixture(): RpgAgentAssetCoverageSummary { return cloneFixture({ roleAssets: [ { roleId: 'playable-1', roleName: '沈砺', roleKind: 'playable', priorityTier: 'hero', portraitPath: '/generated-characters/playable-1/visual/asset-runtime/master.png', generatedVisualAssetId: 'asset-runtime-playable', generatedAnimationSetId: 'animation-set-playable-1', status: 'complete', missingAnimations: [], nextPointCost: 0, }, { roleId: 'story-1', roleName: '顾潮音', roleKind: 'story', priorityTier: 'featured', portraitPath: '/generated-characters/story-1/visual/asset-runtime/master.png', generatedVisualAssetId: 'asset-runtime-story', generatedAnimationSetId: 'animation-set-story-1', status: 'complete', missingAnimations: [], nextPointCost: 0, }, ], sceneAssets: [ { sceneId: 'landmark-1', sceneName: '回潮旧灯塔', actId: 'scene-act-1', actTitle: '第一幕', imageSrc: '/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png', assetId: 'scene-asset-runtime', status: 'ready', nextPointCost: 0, }, ], allRoleAssetsReady: true, allSceneAssetsReady: true, } satisfies RpgAgentAssetCoverageSummary); } /** * 已发布 profile fixture。 * 用于 preview compiler、works 聚合和 library 元数据解析测试。 */ export function createRpgCreationPublishedProfileFixture(): CustomWorldProfileRecord { const draft = createRpgAgentFoundationDraftProfileFixture(); return cloneFixture({ id: RPG_CREATION_FIXTURE_PROFILE_ID, settingText: draft.worldHook, name: draft.name, subtitle: draft.subtitle, summary: draft.summary, tone: draft.tone, playerGoal: draft.playerGoal, templateWorldType: 'WUXIA', compatibilityTemplateWorldType: 'WUXIA', majorFactions: draft.majorFactions, coreConflicts: draft.coreConflicts, playableNpcs: draft.playableNpcs.map((role) => ({ id: role.id, name: role.name, title: role.title, role: role.role, description: role.publicIdentity, backstory: role.hiddenHook || role.summary, personality: role.publicMask || role.summary, motivation: role.currentPressure, combatStyle: '借地形和潮路换位,先拉扯再压近。', initialAffinity: 18, relationshipHooks: [role.relationToPlayer], tags: ['潮路', '旧案'], imageSrc: role.imageSrc, generatedVisualAssetId: role.generatedVisualAssetId, generatedAnimationSetId: role.generatedAnimationSetId, animationMap: role.animationMap, skills: [ { id: 'skill-playable-1', name: '潮行引路', summary: '踩着旧潮阶切线前压,替队伍打开角度。', style: '机动周旋', }, ], })), storyNpcs: draft.storyNpcs.map((role) => ({ id: role.id, name: role.name, title: role.title, role: role.role, description: role.publicIdentity, backstory: role.hiddenHook || role.summary, personality: role.publicMask || role.summary, motivation: role.currentPressure, combatStyle: '借塔顶视角和风向压制,再用灯火错位扰乱。', initialAffinity: 8, relationshipHooks: [role.relationToPlayer], tags: ['守灯会', '灯塔'], imageSrc: role.imageSrc, generatedVisualAssetId: role.generatedVisualAssetId, skills: [ { id: 'skill-story-1', name: '夜潮灯语', summary: '借灯语与潮声干扰对方判断。', style: '起手压制', }, ], })), camp: { name: draft.camp?.name, description: draft.camp?.description, imageSrc: draft.camp?.imageSrc, }, landmarks: draft.landmarks.map((landmark) => ({ id: landmark.id, name: landmark.name, description: landmark.description, imageSrc: landmark.imageSrc, sceneNpcIds: landmark.characterIds, connections: [ { targetLandmarkId: 'landmark-1', relativePosition: 'forward', summary: '沿着旧潮阶继续前压到雾栈尽头。', }, ], })), cover: { sourceType: 'default', characterRoleIds: ['playable-1'], }, sceneChapterBlueprints: draft.sceneChapters.map((chapter) => ({ id: chapter.id, sceneId: chapter.sceneId, sceneName: chapter.sceneName, title: chapter.title, summary: chapter.summary, acts: chapter.acts.map((act) => ({ id: act.id, title: act.title, summary: act.summary, backgroundImageSrc: act.backgroundImageSrc, backgroundAssetId: act.backgroundAssetId, encounterNpcIds: act.encounterNpcIds, primaryNpcId: act.primaryNpcId, actGoal: act.actGoal, transitionHook: act.transitionHook, })), })), themePack: draft.themePack, storyGraph: draft.storyGraph, scenarioPackId: 'scenario-pack:tide', campaignPackId: 'campaign-pack:tide', generationMode: 'fast', generationStatus: 'key_only', updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT, publishedAt: RPG_CREATION_FIXTURE_PUBLISHED_AT, } satisfies CustomWorldProfileRecord); } export function createRpgCreationPreviewEnvelopeFixture(): RpgCreationPreviewEnvelope { return cloneFixture({ preview: { ...createRpgCreationPublishedProfileFixture(), previewId: 'preview-fixture-1', sessionId: RPG_CREATION_FIXTURE_SESSION_ID, profileId: RPG_CREATION_FIXTURE_PROFILE_ID, }, source: 'session_preview', generatedAt: RPG_CREATION_FIXTURE_UPDATED_AT, qualityFindings: [ { id: 'finding-scene-asset-ready', severity: 'info', code: 'scene_asset_ready', targetId: 'scene-act-1', message: '首幕背景图已经就绪,可直接用于结果页预览。', }, ], blockers: [], publishReady: true, canEnterWorld: false, } satisfies RpgCreationPreviewEnvelope); } export function createRpgAgentSupportedActionsFixture(): RpgAgentSupportedAction[] { return cloneFixture([ { action: 'draft_foundation', enabled: true, }, { action: 'generate_role_assets', enabled: true, }, { action: 'publish_world', enabled: true, }, ] satisfies RpgAgentSupportedAction[]); } /** * 共享 session snapshot fixture。 * 默认模拟“底稿、预览、资产都已准备好”的 ready_to_publish 状态。 */ export function createRpgAgentSessionFixture(): RpgAgentSessionSnapshot { const draftProfile = createRpgAgentFoundationDraftProfileFixture(); return cloneFixture({ sessionId: RPG_CREATION_FIXTURE_SESSION_ID, currentTurn: 6, anchorContent: createRpgCreationAnchorContentFixture(), progressPercent: 100, lastAssistantReply: '八锚点与底稿都已经齐备,可以进入结果页收口。', stage: 'ready_to_publish', focusCardId: null, creatorIntent: { sourceMode: 'card', rawSettingText: draftProfile.worldHook, worldHook: draftProfile.worldHook, playerPremise: draftProfile.playerPremise, themeKeywords: ['海雾', '旧航路'], toneDirectives: ['压抑', '悬疑'], openingSituation: draftProfile.openingSituation, coreConflicts: draftProfile.coreConflicts, keyFactions: ['守灯会'], keyCharacters: ['沈砺', '顾潮音'], keyLandmarks: ['回潮旧灯塔'], iconicElements: draftProfile.iconicElements, forbiddenDirectives: ['不要出现现代枪械'], }, creatorIntentReadiness: { isReady: true, completedKeys: [ 'world_hook', 'player_premise', 'theme_and_tone', 'core_conflict', 'relationship_seed', 'iconic_element', ], missingKeys: [], }, anchorPack: { summary: draftProfile.sourceAnchorSummary, }, lockState: { lockedCardIds: ['world-foundation'], }, draftProfile: draftProfile as unknown as Record, messages: [ { id: 'message-1', role: 'assistant', kind: 'summary', text: '世界底稿已整理完成,建议进入结果页确认资产与发布门槛。', createdAt: RPG_CREATION_FIXTURE_UPDATED_AT, relatedOperationId: null, }, ], draftCards: createRpgAgentDraftCardsFixture(), pendingClarifications: [], suggestedActions: [ { id: 'action-publish', type: 'publish_world', label: '发布世界', }, ], recommendedReplies: ['先看结果页', '继续精修角色关系'], qualityFindings: [ { id: 'finding-scene-asset-ready', severity: 'info', code: 'scene_asset_ready', targetId: 'scene-act-1', message: '首幕背景图已经就绪,可直接用于结果页预览。', }, ], assetCoverage: createRpgAgentAssetCoverageFixture(), checkpoints: [ { checkpointId: 'checkpoint-foundation-v1', createdAt: RPG_CREATION_FIXTURE_UPDATED_AT, label: '世界底稿 V1', }, ], supportedActions: createRpgAgentSupportedActionsFixture(), resultPreview: createRpgCreationPreviewEnvelopeFixture(), updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT, } satisfies RpgAgentSessionSnapshot); } export function createRpgWorldLibraryEntryFixture(): CustomWorldLibraryEntry { const profile = createRpgCreationPublishedProfileFixture(); return cloneFixture({ ownerUserId: RPG_CREATION_FIXTURE_USER_ID, profileId: RPG_CREATION_FIXTURE_PROFILE_ID, publicWorkCode: 'cw-fixture-001', authorPublicUserCode: 'sy-fixture-user', profile, visibility: 'published', publishedAt: RPG_CREATION_FIXTURE_PUBLISHED_AT, updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT, authorDisplayName: '测试玩家', worldName: String(profile.name ?? '潮雾列岛'), subtitle: String(profile.subtitle ?? '旧灯塔与失控航路'), summaryText: String(profile.summary ?? '第一版世界底稿已经整理完成。'), coverImageSrc: '/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png', themeMode: 'tide', playableNpcCount: Array.isArray(profile.playableNpcs) ? profile.playableNpcs.length : 0, landmarkCount: Array.isArray(profile.landmarks) ? profile.landmarks.length : 0, } satisfies CustomWorldLibraryEntry); } export function createRpgCreationWorksResponseFixture(): ListRpgCreationWorksResponse { return cloneFixture({ items: [ { workId: `draft:${RPG_CREATION_FIXTURE_SESSION_ID}`, sourceType: 'agent_session', status: 'draft', title: '潮雾列岛', subtitle: '旧灯塔与失控航路', summary: '第一版世界底稿已经整理完成,玩家将从回潮旧灯塔切入沉船旧案。', coverImageSrc: '/custom/camp/huichao.png', coverRenderMode: 'scene_with_roles', coverCharacterImageSrcs: [ '/generated-characters/playable-1/visual/asset-runtime/master.png', ], updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT, publishedAt: null, stage: 'ready_to_publish', stageLabel: '准备发布', playableNpcCount: 2, landmarkCount: 1, roleVisualReadyCount: 2, roleAnimationReadyCount: 2, roleAssetSummaryLabel: '沈砺 · 动作已就绪', sessionId: RPG_CREATION_FIXTURE_SESSION_ID, profileId: null, canResume: true, canEnterWorld: false, blockerCount: 0, publishReady: true, }, { workId: `published:${RPG_CREATION_FIXTURE_PROFILE_ID}`, sourceType: 'published_profile', status: 'published', title: '潮雾列岛', subtitle: '旧灯塔与失控航路', summary: '第一版世界底稿已经整理完成,玩家将从回潮旧灯塔切入沉船旧案。', coverImageSrc: '/generated-custom-world-scenes/landmark-1/scene-act-1/scene.png', coverRenderMode: 'scene_with_roles', coverCharacterImageSrcs: [ '/generated-characters/playable-1/visual/asset-runtime/master.png', ], updatedAt: RPG_CREATION_FIXTURE_UPDATED_AT, publishedAt: RPG_CREATION_FIXTURE_PUBLISHED_AT, stage: 'published', stageLabel: '已发布', playableNpcCount: 1, landmarkCount: 1, roleVisualReadyCount: 1, roleAnimationReadyCount: 1, roleAssetSummaryLabel: '动作已就绪 1', sessionId: null, profileId: RPG_CREATION_FIXTURE_PROFILE_ID, canResume: false, canEnterWorld: true, blockerCount: 0, publishReady: true, }, ] satisfies RpgCreationWorkSummary[], } satisfies ListRpgCreationWorksResponse); }