From ddcb5d5c8cdc10365024e1e4a055465bb1b83578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=89=A9?= <253518756@qq.com> Date: Mon, 6 Apr 2026 23:19:00 +0800 Subject: [PATCH] Rework story engine flow and reorganize project docs --- .encoding-check-ignore | 11 +- .eslintrc.cjs | 2 + .gitignore | 1 + AGENTS.md | 1 + README.md | 51 +- UI_CODING_STANDARD.md | 2 +- docs/CHINESE_MOJIBAKE_INVENTORY.md | 91 -- docs/README.md | 30 + .../FUNCTION_DESIGN_AUDIT_2026-04-03.md | 8 +- .../ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md | 2 +- docs/audits/README.md | 19 + ...INEERING_OPTIMIZATION_REVIEW_2026-03-29.md | 0 ...INEERING_OPTIMIZATION_REVIEW_2026-03-30.md | 2 +- ...INEERING_OPTIMIZATION_REVIEW_2026-04-01.md | 0 ...ONSTER_NPC_UNIFICATION_AUDIT_2026-04-06.md | 173 ++++ docs/audits/engineering/README.md | 19 + .../audits/text/CHINESE_MOJIBAKE_INVENTORY.md | 91 ++ ...DITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md | 0 ...AME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md | 0 ...AME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md | 0 ..._PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.md | 0 ..._EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md | 0 ..._UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md | 0 ..._UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md | 0 ..._UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md | 0 docs/audits/text/README.md | 27 + ...RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md | 0 ...SHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md | 0 ...INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md | 492 ++++++++++ ...MENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md | 0 docs/design/README.md | 18 + .../npc-conversation-situation-draft.md | 0 .../ADVENTURE_RUNTIME_DEV_EXPERIENCE.md | 0 docs/{ => experience}/AGENT_UI_CHANGELOG.md | 0 ...EX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md | 0 .../CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md | 20 +- .../MOBILE_UI_DEV_EXPERIENCE.md | 0 .../PROJECT_DEVELOPMENT_EXPERIENCE.md | 0 .../PROJECT_WORK_EXPERIENCE_PLAYBOOK.md | 8 +- docs/experience/README.md | 25 + ...NT_GAME_ITERATION_PRIORITIES_2026-04-03.md | 24 +- docs/planning/README.md | 10 + ...EATION_FLOW_OPTIMIZATION_PRD_2026-04-06.md | 699 ++++++++++++++ ...E_PHASE1_IMPLEMENTATION_PLAN_2026-04-06.md | 869 ++++++++++++++++++ ...E_PHASE2_IMPLEMENTATION_PLAN_2026-04-06.md | 644 +++++++++++++ ...E_PHASE3_IMPLEMENTATION_PLAN_2026-04-06.md | 690 ++++++++++++++ ...E_PHASE4_IMPLEMENTATION_PLAN_2026-04-06.md | 725 +++++++++++++++ ...E_PHASE5_IMPLEMENTATION_PLAN_2026-04-06.md | 676 ++++++++++++++ ...E_PHASE6_IMPLEMENTATION_PLAN_2026-04-06.md | 559 +++++++++++ .../FUNCTION_SCRIPT_CATALOG_2026-04-04.md | 0 docs/reference/README.md | 10 + ...ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md | 0 ...ELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md | 0 docs/technical/README.md | 14 + ..._AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md | 4 +- scripts/check-encoding.mjs | 2 + scripts/smoke-content.ts | 44 +- scripts/validate-content.ts | 4 +- scripts/validate-overrides.ts | 15 +- src/components/AdventureEntityModal.tsx | 339 +++---- src/components/AdventurePanel.tsx | 39 +- src/components/CharacterDetailModal.tsx | 314 ++++--- src/components/CharacterInfoHelpers.ts | 123 +++ src/components/CharacterInfoShared.tsx | 291 ++++++ src/components/CharacterPanel.tsx | 422 ++------- src/components/CustomWorldEntityCatalog.tsx | 198 +++- src/components/CustomWorldGenerationView.tsx | 437 ++++----- src/components/CustomWorldResultView.tsx | 27 + src/components/GameShell.tsx | 29 +- src/components/InventoryItemViews.tsx | 243 ++--- src/components/InventoryPanel.tsx | 264 ++---- .../SelectionCustomizationModals.tsx | 406 ++++---- src/components/SkillEffectPreview.tsx | 14 +- src/components/StateFunctionEditor.tsx | 39 +- .../AdventurePanelOverlays.tsx | 211 ++++- .../game-canvas/GameCanvasEffectLayer.tsx | 8 +- .../game-canvas/GameCanvasEntityLayer.tsx | 27 +- .../game-canvas/GameCanvasRuntime.tsx | 27 +- .../game-canvas/GameCanvasShared.tsx | 11 +- .../game-shell/GameShellCanvasStage.tsx | 1 - .../game-shell/GameShellStoryPanels.tsx | 28 + .../game-shell/PreGameSelectionFlow.tsx | 533 +++++++++-- .../game-shell/useSceneTransitionModel.ts | 2 +- .../preset-editor/ScenePresetPanel.tsx | 25 +- src/data/buildDamage.test.ts | 4 +- src/data/buildDamage.ts | 8 +- src/data/buildTags.ts | 4 +- src/data/customWorldLibrary.ts | 38 +- src/data/editorValidation.ts | 12 +- src/data/encounterTransition.ts | 32 +- src/data/hostileNpcPresets.test.ts | 2 - src/data/hostileNpcPresets.ts | 2 +- src/data/hostileNpcs.ts | 15 +- src/data/monsters.ts | 1 - src/data/npcInteractions.test.ts | 6 +- src/data/npcInteractions.ts | 196 +++- src/data/questFlow.test.ts | 16 +- src/data/questFlow.ts | 79 +- src/data/runtimeItemContext.ts | 90 ++ src/data/runtimeItemDirector.test.ts | 3 + src/data/runtimeItemDirector.ts | 2 +- src/data/runtimeItemNarrative.ts | 155 ++-- src/data/sceneEncounterPreviews.test.ts | 11 +- src/data/sceneEncounterPreviews.ts | 56 +- src/data/scenePresets.ts | 150 +-- src/data/stateFunctions.ts | 6 +- src/hooks/combat/battlePlan.test.ts | 7 +- src/hooks/combat/battlePlan.ts | 63 +- src/hooks/combat/escapeFlow.test.ts | 6 +- src/hooks/combat/escapeFlow.ts | 18 +- src/hooks/combat/playback.ts | 40 +- src/hooks/combat/resolvedChoice.test.ts | 4 +- src/hooks/combatStoryUtils.ts | 4 +- src/hooks/idleAdventureFlow.ts | 12 +- src/hooks/story/choiceActions.test.ts | 8 +- src/hooks/story/choiceActions.ts | 19 +- src/hooks/story/npcEncounterActions.ts | 98 +- src/hooks/story/npcInteraction.ts | 41 +- src/hooks/story/openingAdventure.ts | 2 +- src/hooks/story/progressionActions.ts | 505 +++++++++- src/hooks/story/sessionActions.test.ts | 2 +- src/hooks/story/sessionActions.ts | 5 +- src/hooks/story/storyGenerationState.test.ts | 2 +- src/hooks/story/storyGenerationState.ts | 2 +- src/hooks/useGameFlow.ts | 28 +- src/hooks/useGamePersistence.ts | 39 +- src/hooks/useStoryGeneration.ts | 293 +++++- src/hooks/useTreasureFlow.ts | 7 +- src/index.css | 4 + src/services/ai.test.ts | 52 +- src/services/ai.ts | 438 ++++++++- src/services/aiTypes.ts | 57 ++ src/services/customWorld.ts | 401 +++++++- src/services/customWorldBuilder.test.ts | 93 ++ src/services/customWorldBuilder.ts | 114 ++- src/services/customWorldCreatorIntent.test.ts | 93 ++ src/services/customWorldCreatorIntent.ts | 536 +++++++++++ src/services/prompt.test.ts | 200 ++++ src/services/prompt.ts | 511 ++++++++-- src/services/questDirector.ts | 48 + src/services/questPrompt.ts | 49 + src/services/questTypes.ts | 2 +- src/services/runtimeItemAiDirector.ts | 15 +- src/services/runtimeItemAiPrompt.ts | 38 +- src/services/storyEngine/actPlanner.test.ts | 26 + src/services/storyEngine/actPlanner.ts | 69 ++ .../storyEngine/actorNarrativeProfile.ts | 206 +++++ .../adaptiveNarrativeTuner.test.ts | 49 + .../storyEngine/adaptiveNarrativeTuner.ts | 62 ++ .../authorialConstraintPack.test.ts | 23 + .../storyEngine/authorialConstraintPack.ts | 23 + .../storyEngine/branchBudgetPlanner.test.ts | 30 + .../storyEngine/branchBudgetPlanner.ts | 57 ++ .../storyEngine/campEventDirector.test.ts | 52 ++ src/services/storyEngine/campEventDirector.ts | 51 + .../storyEngine/campaignDirector.test.ts | 29 + src/services/storyEngine/campaignDirector.ts | 37 + .../storyEngine/campaignPackCompiler.test.ts | 24 + .../storyEngine/campaignPackCompiler.ts | 79 ++ .../carrierNarrativeCompiler.test.ts | 131 +++ .../storyEngine/carrierNarrativeCompiler.ts | 217 +++++ .../storyEngine/chapterDirector.test.ts | 92 ++ src/services/storyEngine/chapterDirector.ts | 88 ++ .../storyEngine/companionArcDirector.test.ts | 102 ++ .../storyEngine/companionArcDirector.ts | 106 +++ .../companionReactionDirector.test.ts | 131 +++ .../storyEngine/companionReactionDirector.ts | 123 +++ .../companionResolutionDirector.test.ts | 42 + .../companionResolutionDirector.ts | 92 ++ .../storyEngine/consequenceLedger.test.ts | 31 + src/services/storyEngine/consequenceLedger.ts | 90 ++ .../contentDependencyGraph.test.ts | 43 + .../storyEngine/contentDependencyGraph.ts | 76 ++ .../storyEngine/contentDiffReport.test.ts | 30 + src/services/storyEngine/contentDiffReport.ts | 35 + .../documentCarrierCompiler.test.ts | 33 + .../storyEngine/documentCarrierCompiler.ts | 57 ++ src/services/storyEngine/echoMemory.test.ts | 183 ++++ src/services/storyEngine/echoMemory.ts | 169 ++++ .../storyEngine/endingResolver.test.ts | 34 + src/services/storyEngine/endingResolver.ts | 81 ++ .../storyEngine/epilogueComposer.test.ts | 30 + src/services/storyEngine/epilogueComposer.ts | 19 + .../storyEngine/factionTensionState.test.ts | 70 ++ .../storyEngine/factionTensionState.ts | 63 ++ .../storyEngine/journeyBeatPlanner.test.ts | 90 ++ .../storyEngine/journeyBeatPlanner.ts | 74 ++ .../storyEngine/knowledgeContract.test.ts | 56 ++ src/services/storyEngine/knowledgeContract.ts | 98 ++ .../storyEngine/knowledgeGraph.test.ts | 118 +++ src/services/storyEngine/knowledgeGraph.ts | 278 ++++++ .../storyEngine/narrativeCarrierCatalog.ts | 54 ++ .../storyEngine/narrativeCodex.test.ts | 62 ++ src/services/storyEngine/narrativeCodex.ts | 115 +++ .../narrativeConsistencyChecks.test.ts | 41 + .../storyEngine/narrativeConsistencyChecks.ts | 42 + .../storyEngine/narrativeQaReport.test.ts | 22 + src/services/storyEngine/narrativeQaReport.ts | 20 + .../narrativeRegressionReplay.test.ts | 28 + .../storyEngine/narrativeRegressionReplay.ts | 29 + .../storyEngine/narrativeTelemetry.test.ts | 34 + .../storyEngine/narrativeTelemetry.ts | 35 + .../storyEngine/playerStyleProfiler.test.ts | 16 + .../storyEngine/playerStyleProfiler.ts | 85 ++ .../storyEngine/playthroughMatrixLab.test.ts | 34 + .../storyEngine/playthroughMatrixLab.ts | 32 + src/services/storyEngine/recapDigest.test.ts | 86 ++ src/services/storyEngine/recapDigest.ts | 32 + .../storyEngine/releaseGateReport.test.ts | 19 + src/services/storyEngine/releaseGateReport.ts | 36 + .../storyEngine/saveMigrationManifest.test.ts | 19 + .../storyEngine/saveMigrationManifest.ts | 36 + .../storyEngine/scenarioPackRegistry.test.ts | 23 + .../storyEngine/scenarioPackRegistry.ts | 16 + .../storyEngine/sceneNarrativeDirector.ts | 82 ++ .../storyEngine/sceneResidueCompiler.test.ts | 86 ++ .../storyEngine/sceneResidueCompiler.ts | 66 ++ .../storyEngine/setpieceDirector.test.ts | 36 + src/services/storyEngine/setpieceDirector.ts | 50 + .../storyEngine/storyChronicle.test.ts | 98 ++ src/services/storyEngine/storyChronicle.ts | 92 ++ .../storyEngine/storySimulationRunner.test.ts | 54 ++ .../storyEngine/storySimulationRunner.ts | 32 + src/services/storyEngine/themePack.ts | 198 ++++ .../storyEngine/threadContract.test.ts | 26 + src/services/storyEngine/threadContract.ts | 91 ++ .../storyEngine/threadSignalRouter.test.ts | 158 ++++ .../storyEngine/threadSignalRouter.ts | 169 ++++ .../storyEngine/visibilityEngine.test.ts | 55 ++ src/services/storyEngine/visibilityEngine.ts | 252 +++++ .../storyEngine/worldMutationRouter.test.ts | 105 +++ .../storyEngine/worldMutationRouter.ts | 127 +++ src/services/storyEngine/worldStoryGraph.ts | 378 ++++++++ src/types.ts | 1 + src/types/customWorld.ts | 103 +++ src/types/game.ts | 9 +- src/types/runtimeItem.ts | 14 + src/types/scene.ts | 17 +- src/types/story.ts | 6 + src/types/storyEngine.ts | 476 ++++++++++ vite.config.ts | 3 + 241 files changed, 19805 insertions(+), 2478 deletions(-) delete mode 100644 docs/CHINESE_MOJIBAKE_INVENTORY.md create mode 100644 docs/README.md rename docs/{ => audits}/FUNCTION_DESIGN_AUDIT_2026-04-03.md (98%) rename docs/{ => audits}/ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md (99%) create mode 100644 docs/audits/README.md rename docs/{ => audits/engineering}/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md (100%) rename docs/{ => audits/engineering}/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md (98%) rename docs/{ => audits/engineering}/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md (100%) create mode 100644 docs/audits/engineering/MONSTER_NPC_UNIFICATION_AUDIT_2026-04-06.md create mode 100644 docs/audits/engineering/README.md create mode 100644 docs/audits/text/CHINESE_MOJIBAKE_INVENTORY.md rename docs/{ => audits/text}/EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md (100%) rename docs/{ => audits/text}/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md (100%) rename docs/{ => audits/text}/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md (100%) rename docs/{ => audits/text}/GAME_UI_PRESET_EDITOR_NPC_PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.md (100%) rename docs/{ => audits/text}/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md (100%) rename docs/{ => audits/text}/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md (100%) rename docs/{ => audits/text}/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md (100%) rename docs/{ => audits/text}/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md (100%) create mode 100644 docs/audits/text/README.md rename docs/{ => design}/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md (100%) rename docs/{ => design}/COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md (100%) create mode 100644 docs/design/CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md rename docs/{ => design}/EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md (100%) create mode 100644 docs/design/README.md rename docs/{ => design}/npc-conversation-situation-draft.md (100%) rename docs/{ => experience}/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md (100%) rename docs/{ => experience}/AGENT_UI_CHANGELOG.md (100%) rename docs/{ => experience}/CODEX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md (100%) rename docs/{ => experience}/CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md (85%) rename docs/{ => experience}/MOBILE_UI_DEV_EXPERIENCE.md (100%) rename docs/{ => experience}/PROJECT_DEVELOPMENT_EXPERIENCE.md (100%) rename docs/{ => experience}/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md (97%) create mode 100644 docs/experience/README.md rename docs/{ => planning}/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md (86%) create mode 100644 docs/planning/README.md create mode 100644 docs/prd/AI_NATIVE_CUSTOM_WORLD_CREATION_FLOW_OPTIMIZATION_PRD_2026-04-06.md create mode 100644 docs/prd/AI_NATIVE_STORY_ENGINE_PHASE1_IMPLEMENTATION_PLAN_2026-04-06.md create mode 100644 docs/prd/AI_NATIVE_STORY_ENGINE_PHASE2_IMPLEMENTATION_PLAN_2026-04-06.md create mode 100644 docs/prd/AI_NATIVE_STORY_ENGINE_PHASE3_IMPLEMENTATION_PLAN_2026-04-06.md create mode 100644 docs/prd/AI_NATIVE_STORY_ENGINE_PHASE4_IMPLEMENTATION_PLAN_2026-04-06.md create mode 100644 docs/prd/AI_NATIVE_STORY_ENGINE_PHASE5_IMPLEMENTATION_PLAN_2026-04-06.md create mode 100644 docs/prd/AI_NATIVE_STORY_ENGINE_PHASE6_IMPLEMENTATION_PLAN_2026-04-06.md rename docs/{ => reference}/FUNCTION_SCRIPT_CATALOG_2026-04-04.md (100%) create mode 100644 docs/reference/README.md rename docs/{ => technical}/AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md (100%) rename docs/{ => technical}/PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md (100%) create mode 100644 docs/technical/README.md rename docs/{ => technical}/SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md (98%) create mode 100644 src/components/CharacterInfoHelpers.ts create mode 100644 src/components/CharacterInfoShared.tsx delete mode 100644 src/data/monsters.ts create mode 100644 src/services/customWorldBuilder.test.ts create mode 100644 src/services/customWorldCreatorIntent.test.ts create mode 100644 src/services/customWorldCreatorIntent.ts create mode 100644 src/services/prompt.test.ts create mode 100644 src/services/storyEngine/actPlanner.test.ts create mode 100644 src/services/storyEngine/actPlanner.ts create mode 100644 src/services/storyEngine/actorNarrativeProfile.ts create mode 100644 src/services/storyEngine/adaptiveNarrativeTuner.test.ts create mode 100644 src/services/storyEngine/adaptiveNarrativeTuner.ts create mode 100644 src/services/storyEngine/authorialConstraintPack.test.ts create mode 100644 src/services/storyEngine/authorialConstraintPack.ts create mode 100644 src/services/storyEngine/branchBudgetPlanner.test.ts create mode 100644 src/services/storyEngine/branchBudgetPlanner.ts create mode 100644 src/services/storyEngine/campEventDirector.test.ts create mode 100644 src/services/storyEngine/campEventDirector.ts create mode 100644 src/services/storyEngine/campaignDirector.test.ts create mode 100644 src/services/storyEngine/campaignDirector.ts create mode 100644 src/services/storyEngine/campaignPackCompiler.test.ts create mode 100644 src/services/storyEngine/campaignPackCompiler.ts create mode 100644 src/services/storyEngine/carrierNarrativeCompiler.test.ts create mode 100644 src/services/storyEngine/carrierNarrativeCompiler.ts create mode 100644 src/services/storyEngine/chapterDirector.test.ts create mode 100644 src/services/storyEngine/chapterDirector.ts create mode 100644 src/services/storyEngine/companionArcDirector.test.ts create mode 100644 src/services/storyEngine/companionArcDirector.ts create mode 100644 src/services/storyEngine/companionReactionDirector.test.ts create mode 100644 src/services/storyEngine/companionReactionDirector.ts create mode 100644 src/services/storyEngine/companionResolutionDirector.test.ts create mode 100644 src/services/storyEngine/companionResolutionDirector.ts create mode 100644 src/services/storyEngine/consequenceLedger.test.ts create mode 100644 src/services/storyEngine/consequenceLedger.ts create mode 100644 src/services/storyEngine/contentDependencyGraph.test.ts create mode 100644 src/services/storyEngine/contentDependencyGraph.ts create mode 100644 src/services/storyEngine/contentDiffReport.test.ts create mode 100644 src/services/storyEngine/contentDiffReport.ts create mode 100644 src/services/storyEngine/documentCarrierCompiler.test.ts create mode 100644 src/services/storyEngine/documentCarrierCompiler.ts create mode 100644 src/services/storyEngine/echoMemory.test.ts create mode 100644 src/services/storyEngine/echoMemory.ts create mode 100644 src/services/storyEngine/endingResolver.test.ts create mode 100644 src/services/storyEngine/endingResolver.ts create mode 100644 src/services/storyEngine/epilogueComposer.test.ts create mode 100644 src/services/storyEngine/epilogueComposer.ts create mode 100644 src/services/storyEngine/factionTensionState.test.ts create mode 100644 src/services/storyEngine/factionTensionState.ts create mode 100644 src/services/storyEngine/journeyBeatPlanner.test.ts create mode 100644 src/services/storyEngine/journeyBeatPlanner.ts create mode 100644 src/services/storyEngine/knowledgeContract.test.ts create mode 100644 src/services/storyEngine/knowledgeContract.ts create mode 100644 src/services/storyEngine/knowledgeGraph.test.ts create mode 100644 src/services/storyEngine/knowledgeGraph.ts create mode 100644 src/services/storyEngine/narrativeCarrierCatalog.ts create mode 100644 src/services/storyEngine/narrativeCodex.test.ts create mode 100644 src/services/storyEngine/narrativeCodex.ts create mode 100644 src/services/storyEngine/narrativeConsistencyChecks.test.ts create mode 100644 src/services/storyEngine/narrativeConsistencyChecks.ts create mode 100644 src/services/storyEngine/narrativeQaReport.test.ts create mode 100644 src/services/storyEngine/narrativeQaReport.ts create mode 100644 src/services/storyEngine/narrativeRegressionReplay.test.ts create mode 100644 src/services/storyEngine/narrativeRegressionReplay.ts create mode 100644 src/services/storyEngine/narrativeTelemetry.test.ts create mode 100644 src/services/storyEngine/narrativeTelemetry.ts create mode 100644 src/services/storyEngine/playerStyleProfiler.test.ts create mode 100644 src/services/storyEngine/playerStyleProfiler.ts create mode 100644 src/services/storyEngine/playthroughMatrixLab.test.ts create mode 100644 src/services/storyEngine/playthroughMatrixLab.ts create mode 100644 src/services/storyEngine/recapDigest.test.ts create mode 100644 src/services/storyEngine/recapDigest.ts create mode 100644 src/services/storyEngine/releaseGateReport.test.ts create mode 100644 src/services/storyEngine/releaseGateReport.ts create mode 100644 src/services/storyEngine/saveMigrationManifest.test.ts create mode 100644 src/services/storyEngine/saveMigrationManifest.ts create mode 100644 src/services/storyEngine/scenarioPackRegistry.test.ts create mode 100644 src/services/storyEngine/scenarioPackRegistry.ts create mode 100644 src/services/storyEngine/sceneNarrativeDirector.ts create mode 100644 src/services/storyEngine/sceneResidueCompiler.test.ts create mode 100644 src/services/storyEngine/sceneResidueCompiler.ts create mode 100644 src/services/storyEngine/setpieceDirector.test.ts create mode 100644 src/services/storyEngine/setpieceDirector.ts create mode 100644 src/services/storyEngine/storyChronicle.test.ts create mode 100644 src/services/storyEngine/storyChronicle.ts create mode 100644 src/services/storyEngine/storySimulationRunner.test.ts create mode 100644 src/services/storyEngine/storySimulationRunner.ts create mode 100644 src/services/storyEngine/themePack.ts create mode 100644 src/services/storyEngine/threadContract.test.ts create mode 100644 src/services/storyEngine/threadContract.ts create mode 100644 src/services/storyEngine/threadSignalRouter.test.ts create mode 100644 src/services/storyEngine/threadSignalRouter.ts create mode 100644 src/services/storyEngine/visibilityEngine.test.ts create mode 100644 src/services/storyEngine/visibilityEngine.ts create mode 100644 src/services/storyEngine/worldMutationRouter.test.ts create mode 100644 src/services/storyEngine/worldMutationRouter.ts create mode 100644 src/services/storyEngine/worldStoryGraph.ts create mode 100644 src/types/storyEngine.ts diff --git a/.encoding-check-ignore b/.encoding-check-ignore index b43cbcc9..aad2f9a7 100644 --- a/.encoding-check-ignore +++ b/.encoding-check-ignore @@ -1,10 +1,11 @@ # Temporary baseline for legacy files that already contain broken text. # Remove a path from this list as soon as the file is repaired. -docs/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md -docs/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md -docs/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md -docs/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md -docs/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md +docs/audits/text/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md +docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md +docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md +docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md +docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md src/components/AdventurePanel.tsx src/data/customWorldCharacterLoadout.ts +dist_check_monster_position/** diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 75b8616a..0da9c2d2 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -53,6 +53,8 @@ module.exports = { ], ignorePatterns: [ 'dist', + 'dist_check', + 'dist_check_monster_position', 'node_modules', 'public/Icons', 'media', diff --git a/.gitignore b/.gitignore index 110a3757..406944e9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ coverage/ .DS_Store *.log .env.local +/public/generated-custom-world-scenes diff --git a/AGENTS.md b/AGENTS.md index 4374269f..8188ef9e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,3 +9,4 @@ - 非必要不要整文件重写,尤其是包含中文的文件;优先做局部补丁,避免把未改动的中文内容重新编码。 - 修改包含中文的文件后,优先运行仓库里的编码检查,确保没有把文本写坏。 - UI面板中不要默认写一些规则描述文案,清爽一些,按照游戏UI设计规范设计即可。 +- UI设计需要兼顾网页端、移动端双端的使用体验,确保在不同设备上都能正常显示和操作,移动端优先考虑。 diff --git a/README.md b/README.md index ac2eb547..8dadca1e 100644 --- a/README.md +++ b/README.md @@ -86,42 +86,33 @@ npm run check:content 主运行时: -- [src/App.tsx](/E:/Repos/ai-native-visual-rpg/src/App.tsx) -- [src/components/GameShell.tsx](/E:/Repos/ai-native-visual-rpg/src/components/GameShell.tsx) -- [src/hooks/useCombatFlow.ts](/E:/Repos/ai-native-visual-rpg/src/hooks/useCombatFlow.ts) -- [src/hooks/useStoryGeneration.ts](/E:/Repos/ai-native-visual-rpg/src/hooks/useStoryGeneration.ts) +- [src/App.tsx](/E:/Repos/Genarrative/src/App.tsx) +- [src/components/GameShell.tsx](/E:/Repos/Genarrative/src/components/GameShell.tsx) +- [src/hooks/useCombatFlow.ts](/E:/Repos/Genarrative/src/hooks/useCombatFlow.ts) +- [src/hooks/useStoryGeneration.ts](/E:/Repos/Genarrative/src/hooks/useStoryGeneration.ts) 编辑器: -- [src/components/PresetEditor.tsx](/E:/Repos/ai-native-visual-rpg/src/components/PresetEditor.tsx) -- [src/components/NpcVisualEditor.tsx](/E:/Repos/ai-native-visual-rpg/src/components/NpcVisualEditor.tsx) -- [src/components/StateFunctionEditor.tsx](/E:/Repos/ai-native-visual-rpg/src/components/StateFunctionEditor.tsx) +- [src/components/PresetEditor.tsx](/E:/Repos/Genarrative/src/components/PresetEditor.tsx) +- [src/components/NpcVisualEditor.tsx](/E:/Repos/Genarrative/src/components/NpcVisualEditor.tsx) +- [src/components/StateFunctionEditor.tsx](/E:/Repos/Genarrative/src/components/StateFunctionEditor.tsx) 核心数据: -- [src/data/scenePresets.ts](/E:/Repos/ai-native-visual-rpg/src/data/scenePresets.ts) -- [src/data/characterPresets.ts](/E:/Repos/ai-native-visual-rpg/src/data/characterPresets.ts) -- [src/data/monsterPresets.ts](/E:/Repos/ai-native-visual-rpg/src/data/monsterPresets.ts) -- [src/data/npcInteractions.ts](/E:/Repos/ai-native-visual-rpg/src/data/npcInteractions.ts) -- [src/data/treasureInteractions.ts](/E:/Repos/ai-native-visual-rpg/src/data/treasureInteractions.ts) +- [src/data/scenePresets.ts](/E:/Repos/Genarrative/src/data/scenePresets.ts) +- [src/data/characterPresets.ts](/E:/Repos/Genarrative/src/data/characterPresets.ts) +- [src/data/monsterPresets.ts](/E:/Repos/Genarrative/src/data/monsterPresets.ts) +- [src/data/npcInteractions.ts](/E:/Repos/Genarrative/src/data/npcInteractions.ts) +- [src/data/treasureInteractions.ts](/E:/Repos/Genarrative/src/data/treasureInteractions.ts) ## 文档入口 -开发经验沉淀: - -- [docs/PROJECT_DEVELOPMENT_EXPERIENCE.md](/E:/Repos/ai-native-visual-rpg/docs/PROJECT_DEVELOPMENT_EXPERIENCE.md) -- [docs/FUNCTION_SCRIPT_CATALOG_2026-04-04.md](/E:/Repos/ai-native-visual-rpg/docs/FUNCTION_SCRIPT_CATALOG_2026-04-04.md) - -移动端与 UI 经验: - -- [docs/MOBILE_UI_DEV_EXPERIENCE.md](/E:/Repos/ai-native-visual-rpg/docs/MOBILE_UI_DEV_EXPERIENCE.md) -- [docs/AGENT_UI_CHANGELOG.md](/E:/Repos/ai-native-visual-rpg/docs/AGENT_UI_CHANGELOG.md) -- [UI_CODING_STANDARD.md](/E:/Repos/ai-native-visual-rpg/UI_CODING_STANDARD.md) - -优化路线图: - -- [docs/PROJECT_OPTIMIZATION_AND_FEATURE_ROADMAP.md](/E:/Repos/ai-native-visual-rpg/docs/PROJECT_OPTIMIZATION_AND_FEATURE_ROADMAP.md) - -切磋状态机说明: - -- [AGENT_SPAR_STATE_MACHINE_GUIDE.md](C:/Users/windows/Downloads/AGENT_SPAR_STATE_MACHINE_GUIDE.md) +- [docs/README.md](/E:/Repos/Genarrative/docs/README.md):文档总入口,按主题分类后的导航页 +- [docs/experience/README.md](/E:/Repos/Genarrative/docs/experience/README.md):项目开发经验、UI 交接、历史实现经验 +- [docs/audits/README.md](/E:/Repos/Genarrative/docs/audits/README.md):工程审查、文本审计、专项审计 +- [docs/planning/README.md](/E:/Repos/Genarrative/docs/planning/README.md):当前阶段优先级与推进顺序 +- [docs/design/README.md](/E:/Repos/Genarrative/docs/design/README.md):玩法、关系、物品与对话设计 +- [docs/technical/README.md](/E:/Repos/Genarrative/docs/technical/README.md):技术路线、服务端方案、外部产品拆解 +- [docs/reference/README.md](/E:/Repos/Genarrative/docs/reference/README.md):Function 与脚本速查 +- [docs/prd/](/E:/Repos/Genarrative/docs/prd/):PRD 与阶段计划,原样保留 +- [UI_CODING_STANDARD.md](/E:/Repos/Genarrative/UI_CODING_STANDARD.md):UI 资产与编码规范 diff --git a/UI_CODING_STANDARD.md b/UI_CODING_STANDARD.md index 2e491f7d..8fce4c39 100644 --- a/UI_CODING_STANDARD.md +++ b/UI_CODING_STANDARD.md @@ -1,6 +1,6 @@ # UI Coding Standard -> **会话交接 / 改动总览**:见 `docs/AGENT_UI_CHANGELOG.md`(文件映射、9-slice 架构、已知坑、未收尾项)。 +> **会话交接 / 改动总览**:见 `docs/experience/AGENT_UI_CHANGELOG.md`(文件映射、9-slice 架构、已知坑、未收尾项)。 ## Goal diff --git a/docs/CHINESE_MOJIBAKE_INVENTORY.md b/docs/CHINESE_MOJIBAKE_INVENTORY.md deleted file mode 100644 index 15b0dc4a..00000000 --- a/docs/CHINESE_MOJIBAKE_INVENTORY.md +++ /dev/null @@ -1,91 +0,0 @@ -# 中文乱码位置清单 - -更新时间:`2026-03-24` - -## 说明 - -- 本文档用于记录仓库内已确认或高置信度疑似存在中文乱码的位置。 -- 当前这份文档是重建版本;原有的 [`docs/CHINESE_MOJIBAKE_INVENTORY.md`](/E:/Repos/ai-native-visual-rpg/docs/CHINESE_MOJIBAKE_INVENTORY.md) 本身也已经乱码,因此已整体替换。 -- 本次整理依据: - - 仓库内旧清单中的完整文件/行号信息 - - 本轮人工复核时再次直接看到的明显乱码位置 -- 由于仓库内同时存在“文件内容已写坏”和“终端/工具显示失真”两类情况,下面清单优先保留高置信位置,便于后续逐项修复。 - -## 扫描范围 - -- 已纳入:`src/`、`docs/`、根目录文档与元数据文件 -- 已排除:`.git/`、`node_modules/`、`dist/`、纯图片资源目录 - -## 高置信位置 - -### 文档与元数据 - -- [`docs/AGENT_UI_CHANGELOG.md`](/E:/Repos/ai-native-visual-rpg/docs/AGENT_UI_CHANGELOG.md):1, 3, 7, 9, 11-18, 24, 26-28, 32-33, 37, 39, 41-43, 47, 49, 51, 53-66, 68, 72, 74, 77-79, 83, 87, 89-90, 94, 96, 98-100, 104, 106-112, 116 -- [`UI_CODING_STANDARD.md`](/E:/Repos/ai-native-visual-rpg/UI_CODING_STANDARD.md):3, 91, 104, 108, 112, 156, 158, 160-166 -- [`metadata.json`](/E:/Repos/ai-native-visual-rpg/metadata.json):2-3 - -### 组件层 - -- [`src/components/AdventurePanel.tsx`](/E:/Repos/ai-native-visual-rpg/src/components/AdventurePanel.tsx):57, 65 -- [`src/components/CharacterPanel.tsx`](/E:/Repos/ai-native-visual-rpg/src/components/CharacterPanel.tsx):37, 65-66, 91-95, 102-103 -- [`src/components/GameCanvas.tsx`](/E:/Repos/ai-native-visual-rpg/src/components/GameCanvas.tsx):240, 462 -- [`src/components/GameShell.tsx`](/E:/Repos/ai-native-visual-rpg/src/components/GameShell.tsx):108, 116, 124, 138, 171, 181 -- [`src/components/InventoryPanel.tsx`](/E:/Repos/ai-native-visual-rpg/src/components/InventoryPanel.tsx):55, 58, 82-83, 181-184, 189, 191 -- [`src/components/MapModal.tsx`](/E:/Repos/ai-native-visual-rpg/src/components/MapModal.tsx):105, 108, 136 -- [`src/components/MedievalNpcAnimator.tsx`](/E:/Repos/ai-native-visual-rpg/src/components/MedievalNpcAnimator.tsx):124 -- [`src/components/NpcVisualEditor.tsx`](/E:/Repos/ai-native-visual-rpg/src/components/NpcVisualEditor.tsx):65, 69-71, 403, 440, 444, 446, 464, 467, 470, 482, 569, 571, 585, 610, 628, 662, 690, 694-695, 697, 722, 751, 759, 775, 777, 781, 824 -- [`src/components/PresetEditor.tsx`](/E:/Repos/ai-native-visual-rpg/src/components/PresetEditor.tsx):34-37, 43-44, 94, 96, 349, 470, 472, 480, 482, 512, 516, 519, 525, 568, 612, 618, 637, 639, 643, 645, 652, 661, 677, 740, 769, 771, 779, 781, 806, 809, 820, 831, 835, 837, 840, 848, 871, 894, 916, 918, 930, 932, 950, 953, 956, 960, 962, 990, 1004, 1006, 1012, 1018, 1024, 1030, 1036, 1064, 1120, 1122, 1130, 1132, 1150, 1153, 1156, 1172-1175, 1180, 1182, 1186, 1188, 1199, 1203-1204, 1208, 1240, 1242 - -### 数据层 - -- [`src/data/characterPresets.ts`](/E:/Repos/ai-native-visual-rpg/src/data/characterPresets.ts):97, 102, 104, 107, 129, 132-133, 142, 144, 170, 276, 302, 470, 496, 531, 540, 566, 699-700, 729, 972 -- [`src/data/medievalNpcVisuals.ts`](/E:/Repos/ai-native-visual-rpg/src/data/medievalNpcVisuals.ts):103, 115, 117, 119, 136, 154, 156, 161, 167, 174, 177, 189, 226, 235-236, 241, 244-245, 249-254, 256-257, 260, 262, 274, 278, 288, 451-453, 565, 568, 577, 592 -- [`src/data/monsterPresets.ts`](/E:/Repos/ai-native-visual-rpg/src/data/monsterPresets.ts):41-42, 54, 60-61, 79-80, 92, 98-99, 117-118, 136-137, 155-156, 171-173, 185, 191-192, 204, 210-211, 229-230, 242, 248-249, 261, 267-268, 280, 286-287, 304-305, 323-324, 335 -- [`src/data/monsters.ts`](/E:/Repos/ai-native-visual-rpg/src/data/monsters.ts):112 -- [`src/data/npcInteractions.ts`](/E:/Repos/ai-native-visual-rpg/src/data/npcInteractions.ts):68-71, 80, 82-83, 161, 165, 173, 182, 188-190, 196, 198, 205, 231, 241, 245, 255-260, 272, 296, 319-320, 372, 444-445, 449, 451, 453, 507, 569-570, 578-579, 587-588, 597, 605-606, 615, 617-618, 626-627, 634, 641-643, 652, 661, 665, 670, 672, 676 -- [`src/data/scenePresets.ts`](/E:/Repos/ai-native-visual-rpg/src/data/scenePresets.ts):115, 120, 122, 128, 133, 135, 141, 146, 148, 154, 159, 161, 167, 172, 174, 180, 185, 187, 192-193, 198, 200, 205-206, 211, 213, 219, 224, 226, 232, 237, 239, 245, 250, 252, 258, 263, 265, 274, 279, 281, 287, 292, 294, 299-300, 305, 307, 313, 318, 320, 326, 331, 333, 339, 344, 346, 352, 357, 359, 364-365, 370, 372, 377-378, 383, 385, 390-391, 396, 398, 404, 409, 411, 417, 422, 424, 509, 523, 525 -- [`src/data/stateFunctions.ts`](/E:/Repos/ai-native-visual-rpg/src/data/stateFunctions.ts):72-73, 80, 95-96, 103, 117-118, 125, 139-140, 147, 161-162, 169, 186-187, 194, 209-210, 217, 237-238, 255-256, 273-274, 294, 311-312, 329-330, 420, 430-431, 433-435, 437-438, 440-442, 444-445, 447, 449, 451-452, 454-456, 458, 460-461, 464, 466, 468, 484-485, 487, 489, 491, 493, 601, 618 - -### Hooks 与服务层 - -- [`src/hooks/useCombatFlow.ts`](/E:/Repos/ai-native-visual-rpg/src/hooks/useCombatFlow.ts):54, 56-58, 566 -- [`src/services/ai.ts`](/E:/Repos/ai-native-visual-rpg/src/services/ai.ts):200-201, 209-210, 234-235, 269-270, 309, 311, 317, 338, 341, 358, 382 -- [`src/services/prompt.ts`](/E:/Repos/ai-native-visual-rpg/src/services/prompt.ts):7-8, 10, 13-15, 19-20, 25-40, 43, 55, 61-62, 64, 66, 74-76, 78-79, 83-84, 87-90, 96, 103-104, 112, 115, 157, 159, 161-162, 164-165, 167-168, 170, 172-173 - -### 其他源码 - -- [`src/uiAssets.ts`](/E:/Repos/ai-native-visual-rpg/src/uiAssets.ts):54, 115, 122, 129, 142, 173, 180 - -## 本轮人工复核补充 - -以下位置是在本轮实现过程中直接再次看到的明显乱码文本,建议优先复查: - -- [`src/hooks/useCombatFlow.ts`](/E:/Repos/ai-native-visual-rpg/src/hooks/useCombatFlow.ts):1094, 1554, 1556-1557 - -## 处理优先级建议 - -### 第一批 - -- `src/components/GameShell.tsx` -- `src/components/InventoryPanel.tsx` -- `src/components/CharacterPanel.tsx` -- `src/data/characterPresets.ts` -- `src/data/npcInteractions.ts` -- `src/data/scenePresets.ts` -- `src/services/prompt.ts` - -### 第二批 - -- `src/components/PresetEditor.tsx` -- `src/components/NpcVisualEditor.tsx` -- `src/data/monsterPresets.ts` -- `src/data/stateFunctions.ts` -- `docs/AGENT_UI_CHANGELOG.md` -- `UI_CODING_STANDARD.md` - -## 备注 - -- 当前文档的目标是“先把位置收拢清楚”,不是直接修复乱码。 -- 如果你下一步要我继续,我可以基于这份清单继续做两件事之一: - - 逐文件修复中文乱码 - - 先做一个“乱码修复优先级 + 替换建议”文档 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..84bb39a1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,30 @@ +# 文档总览 + +`docs/` 现在按主题拆成了 6 类,`docs/prd/` 保持独立,不参与本次整理改写。 + +## 快速入口 + +- [经验沉淀](./experience/README.md):项目开发经验、UI 交接、历史实现经验。 +- [审计与复盘](./audits/README.md):工程审查、文本/乱码审计、专项落地审计。 +- [系统设计](./design/README.md):玩法、关系、物品与对话设计。 +- [技术方案](./technical/README.md):动画、服务端、外部产品形态拆解。 +- [规划与优先级](./planning/README.md):当前阶段的迭代排序与落地优先级。 +- [参考目录](./reference/README.md):脚本/Function 速查入口。 +- [PRD](./prd/):产品需求与阶段计划,原样保留。 + +## 推荐阅读顺序 + +1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。 +2. 再看 [工程审查总览](./audits/engineering/README.md) 和 [文本审计总览](./audits/text/README.md),了解当前风险。 +3. 需要排期时看 [规划与优先级](./planning/README.md)。 +4. 需要补方案时进入 [系统设计](./design/README.md) / [技术方案](./technical/README.md)。 +5. 需要对齐目标边界时再进入 [PRD](./prd/)。 + +## 分类规则 + +- `experience/`:偏方法论、交接经验、长期有效的开发结论。 +- `audits/`:偏“现状扫描 / 问题定位 / 是否达标”的审查类文档。 +- `design/`:偏玩法机制、叙事关系、系统结构设计。 +- `technical/`:偏技术选型、实现路线、竞品/产品形态拆解。 +- `planning/`:偏阶段优先级与推进顺序。 +- `reference/`:偏目录、速查、检索辅助。 diff --git a/docs/FUNCTION_DESIGN_AUDIT_2026-04-03.md b/docs/audits/FUNCTION_DESIGN_AUDIT_2026-04-03.md similarity index 98% rename from docs/FUNCTION_DESIGN_AUDIT_2026-04-03.md rename to docs/audits/FUNCTION_DESIGN_AUDIT_2026-04-03.md index 6ac95539..85f46480 100644 --- a/docs/FUNCTION_DESIGN_AUDIT_2026-04-03.md +++ b/docs/audits/FUNCTION_DESIGN_AUDIT_2026-04-03.md @@ -4,10 +4,10 @@ 本次审计重点阅读并对照了这些位置: -- `docs/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md` -- `docs/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md` -- `docs/PROJECT_DEVELOPMENT_EXPERIENCE.md` -- `docs/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md` +- `docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md` +- `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md` +- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md` +- `docs/planning/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md` - `src/data/stateFunctions.ts` - `src/data/npcInteractions.ts` - `src/data/treasureInteractions.ts` diff --git a/docs/ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md b/docs/audits/ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md similarity index 99% rename from docs/ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md rename to docs/audits/ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md index 1482623c..28b39c5d 100644 --- a/docs/ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md +++ b/docs/audits/ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md @@ -7,7 +7,7 @@ - `docs/prd/AI_NATIVE_RUNTIME_ITEM_GENERATION_DESIGN.md` - `docs/prd/RUNTIME_ITEM_GENERATION_CURRENT_SYSTEM_DESIGN.md` - `docs/prd/BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md` -- `docs/EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md` +- `docs/design/EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md` ## 结论速览 diff --git a/docs/audits/README.md b/docs/audits/README.md new file mode 100644 index 00000000..430a4e89 --- /dev/null +++ b/docs/audits/README.md @@ -0,0 +1,19 @@ +# 审计与复盘 + +这一组文档聚焦“当前状态是否健康、问题在哪里、和目标设计差多少”。 + +## 系列总览 + +- [engineering/README.md](./engineering/README.md):工程优化审查三轮记录的融合入口。 +- [text/README.md](./text/README.md):文本、英文残留、乱码审计系列的融合入口。 + +## 专项审计 + +- [FUNCTION_DESIGN_AUDIT_2026-04-03.md](./FUNCTION_DESIGN_AUDIT_2026-04-03.md):Function 体系分层、职责边界和当前结构问题。 +- [ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md](./ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md):物品生成与 Build 标签系统对 PRD 的落地情况。 + +## 推荐使用方式 + +1. 先读系列总览,确认“最新结论在哪一份”。 +2. 再按需要进入具体日期文档,查看当时的证据和上下文。 +3. 做方案设计前,优先把对应审计文档看完,避免重复踩已知问题。 diff --git a/docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md b/docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md similarity index 100% rename from docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md rename to docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md diff --git a/docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md b/docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md similarity index 98% rename from docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md rename to docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md index 2276a4a6..e91e65e6 100644 --- a/docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md +++ b/docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md @@ -8,7 +8,7 @@ ## 先说结论 -这轮代码库相较 `docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md` 已经有明显进展,项目不再是“所有能力都糊在一个入口文件里”的状态了,但整体仍然处于“重构过渡期”。 +这轮代码库相较 `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md` 已经有明显进展,项目不再是“所有能力都糊在一个入口文件里”的状态了,但整体仍然处于“重构过渡期”。 已经落地的积极变化: diff --git a/docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md b/docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md similarity index 100% rename from docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md rename to docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md diff --git a/docs/audits/engineering/MONSTER_NPC_UNIFICATION_AUDIT_2026-04-06.md b/docs/audits/engineering/MONSTER_NPC_UNIFICATION_AUDIT_2026-04-06.md new file mode 100644 index 00000000..b3d4a412 --- /dev/null +++ b/docs/audits/engineering/MONSTER_NPC_UNIFICATION_AUDIT_2026-04-06.md @@ -0,0 +1,173 @@ +# 怪物-NPC 脚本统一整改审计 + +日期:2026-04-06 + +## 核心结论 + +当前工程仍然没有真正落实“怪物就是初始好感度为负数的 NPC”这一原则。 + +现状不是“NPC 脚本里支持 hostile 状态”,而是同时存在两条并行链路: + +1. `npc / encounter / npcStates / npcInteraction` +2. `monster / hostileNpc / sceneMonsters / sceneHostileNpcs / hostileNpcPresets` + +这会直接导致: + +- 同一个敌对实体同时拥有 NPC 身份和 monster 身份。 +- 场景、战斗、渲染、提示词都在维护两套入口。 +- 后续修 bug 时,任何位置、死亡、血条、入场、掉落问题都要同时查两条链路。 + +本次文档的目标不是立刻改代码,而是先把应该删除的分叉脚本、应该降级成素材层的文件、以及必须合并的字段全部列清楚,作为后续统一改造的依据。 + +## 当前违背原则的根因 + +### 1. 场景数据仍然把“怪物”和“NPC”当成两类实体 + +当前场景层同时维护: + +- `ScenePreset.monsterIds` +- `SceneNpc[]` +- `ScenePresetInfo.hostileNpcIds` +- `SceneNpc.monsterPresetId / hostileNpcPresetId` + +这意味着场景里一个敌对单位既可以来自 `monsterIds`,也可以来自 `npcs`,甚至会被脚本再生成为 hostile scene npc。 + +### 2. 运行时仍然存在“怪物专用实体状态” + +当前运行时仍然同时维护: + +- `GameState.sceneMonsters` +- `GameState.sceneHostileNpcs` +- `SceneHostileNpc / SceneMonster` + +这和“怪物本质上只是 hostile NPC”是冲突的。真正统一后,运行时只应该保留一套“场景 NPC / 战斗 NPC”状态。 + +### 3. 战斗脚本仍然把怪物当独立 actor + +战斗层目前不是“NPC 战斗,只是 hostile 的那部分会出手”,而是显式写了: + +- `TurnActor = 'player' | 'companion' | 'monster'` +- `getClosestMonster` +- `resetCombatPresentation(monsters, ...)` +- `sceneMonsters` 全链路结算 + +这会强制后面所有视觉、掉落、提示词、AI 上下文都跟着叫 monster。 + +### 4. 渲染层仍然有 monster 专属显示入口 + +画布层当前仍然依赖: + +- `sceneMonsters` +- `sceneHostileNpcs` +- `monsterPresetId` +- `HostileNpcAnimator` + +也就是“敌对 NPC 是否按 NPC 脚本渲染”这件事,到最终显示层仍然没有统一。 + +## 一级删除清单 + +下面这些文件属于“业务流程分叉脚本”,不是单纯资源适配层。后续统一时应优先删除或并入 NPC 主链路。 + +| 文件 | 当前分叉角色 | 处理建议 | +| --- | --- | --- | +| `src/data/monsters.ts` | 对 `hostileNpcs.ts` 的别名出口 | 直接删除,禁止继续保留 monster 专属入口名。 | +| `src/data/hostileNpcs.ts` | 负责 monster 创建、编队、距离、朝向、变化、落位 | 按 hostile NPC 运行时工具重写并并入 NPC 体系;原文件名不应继续保留。 | +| `src/data/sceneEncounterPreviews.ts` | 单独构造 hostile encounter group、auto battle、hostile preview | 删除 monster 专线逻辑,改为“负好感 NPC 预览/入场/转战斗”。 | +| `src/hooks/combat/battlePlan.ts` | 使用 `monster` actor、`sceneMonsters`、`getClosestMonster` | 改成统一的 hostile NPC combatant 规划脚本;monster actor 概念应移除。 | +| `src/hooks/combat/playback.ts` | 使用 `sceneMonsters` 播放怪物战斗演出 | 改成统一 NPC 战斗回放;不再区分 monster 播放器。 | +| `src/components/game-canvas/GameCanvasRuntime.tsx` | 运行时按 `sceneMonsters / sceneHostileNpcs` 双数据源选敌方实体 | 删除双源兜底,统一成单一 hostile NPC 列表。 | +| `src/components/game-canvas/GameCanvasEntityLayer.tsx` | 敌方实体按 monsterPreset 分支渲染 | 改成同一套 NPC 实体渲染,视觉差异仅由 visual preset 决定。 | +| `src/components/game-canvas/GameCanvasShared.tsx` | 定义 `sceneMonsters / sceneHostileNpcs` props 与 monster 专属计算 | 删除这些字段与 helper,改成统一 NPC 画布协议。 | +| `src/components/preset-editor/MonsterPresetPanel.tsx` | 独立怪物预设编辑面板 | 并回 NPC hostile visual preset 面板,或删除该独立编辑器入口。 | +| `src/components/preset-editor/MonsterPresetTab.tsx` | 独立怪物预设页签 | 与上面一并删除或并入 NPC preset 编辑器。 | +| `src/components/preset-editor/ScenePresetPanel.tsx` | 仍然单独编辑 `monsterIds` | 删除 `monsterIds` 编辑项,只保留场景 NPC 列表。 | + +## 二级归并清单 + +下面这些文件不一定需要物理删除,但它们当前仍然在放大 monster / NPC 分轨,必须在统一改造时一起收口。 + +| 文件 | 当前问题 | 处理建议 | +| --- | --- | --- | +| `src/data/scenePresets.ts` | 通过 `monsterIds` 再生 hostile scene npc,并区分 `getSceneHostileNpcs / getSceneFriendlyNpcs` | 保留场景数据文件本身,但删除 `monsterIds` 体系,让敌对角色直接存在于 `npcs` 中。 | +| `src/data/customWorldNpcMonsters.ts` | 用单独脚本推导“怪物型 NPC”预设 | 可保留为 hostile visual preset 选择器,但不能再生成第二套实体语义。 | +| `src/data/hostileNpcPresets.ts` | 目前既是视觉预设库,也是独立 hostile 流程的数据源 | 降级为 hostile visual/combat preset 库;不再拥有独立实体生命周期。 | +| `src/components/HostileNpcAnimator.tsx` | 当前名字和调用语义都在暗示“独立怪物实体” | 可以保留为贴图播放器,但应改为 hostile NPC 的视觉适配组件,而不是独立物种脚本。 | +| `src/components/AdventureEntityModal.tsx` | 详情弹窗仍会优先查 monster preset / hostileNpcPreset | 统一读取 NPC 档案;视觉差异只通过 hostile preset 补充。 | +| `src/components/SkillEffectPreview.tsx` | 预览器直接使用 `sceneMonsters` 和 `createSceneMonstersFromIds` | 改成统一 hostile NPC 预览态。 | +| `src/components/StateFunctionEditor.tsx` | 编辑器里仍然直接造 monster battle preview | 改成 hostile NPC preview。 | +| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | 仍然暴露 `monsterPresetId` 字段 | 改成更明确的 hostile visual preset 字段,避免“怪物类型”和“NPC 类型”双语义。 | +| `src/hooks/story/npcEncounterActions.ts` | 虽然入口叫 npc,但内部仍然写 `sceneMonsters / sceneHostileNpcs` | 改成统一 hostile NPC 战斗状态字段。 | +| `src/hooks/story/choiceActions.ts` | 仍然有 `buildHostileNpcBattleReward` 和 `getResolvedSceneHostileNpcs` 这一层额外概念 | 统一到 hostile NPC 结算工具,不再把“敌对 NPC”和“monster”混称。 | +| `src/hooks/useStoryGeneration.ts` | 给 AI/剧情层传入 `sceneMonsters` 或 `sceneHostileNpcs` | 改成统一的 hostile NPC 上下文切片。 | +| `src/services/prompt.ts` | 仍然从 `monsterIds` 和 `createSceneMonstersFromIds` 组 prompt | 改成从场景 NPC 列表中筛出 hostile NPC。 | +| `src/services/questDirector.ts` | 仍然依赖 `monsterPresetId` 推导当前敌对目标 | 统一改为基于负好感或 hostile 标记的 NPC。 | +| `src/services/ai.ts` | 仍然混用 `monsterIds`、sceneNpc 的 hostile 判定 | 与场景统一后改成只读 NPC 列表。 | +| `src/services/questTypes.ts` | 仍然把 `hostileNpcIds / monsterIds` 当作 scene 快照字段 | 删除 `monsterIds`,保留 hostile NPC 语义。 | + +## 可保留但必须降级为“素材/配置层”的内容 + +下面这些内容不一定要消失,但不能继续作为独立业务链路存在: + +| 文件/内容 | 可以保留的原因 | 必须收口的边界 | +| --- | --- | --- | +| `src/components/HostileNpcAnimator.tsx` | 怪物贴图是特殊资源,需要专门 sprite sheet 播放器 | 只负责画图,不再决定实体类型、战斗身份、交互入口。 | +| `src/data/hostileNpcPresets.ts` | hostile visual/combat preset 仍然有价值 | 只能作为 hostile NPC 的 visual/combat preset 库,不再驱动另一套“monster 实体”。 | +| `src/data/hostileNpcOverrides.json` | 资源级 override 仍可继续用 | 不能再配套出独立 hostile 流程。 | +| `src/data/monsterOverrides.json` | 如果只是素材映射,可迁移到 hostile visual preset override | 不应继续以 monster 专属命名长期存在。 | +| `src/data/customWorldNpcMonsters.ts` | 自定义世界里确实需要从文本匹配 hostile visual preset | 只能产出“NPC 使用哪个 hostile visual preset”,不能产出独立 monster 身份。 | + +## 字段级必须合并的内容 + +后续改代码时,至少要把下面这些字段和类型一起收口: + +| 当前字段/类型 | 问题 | 合并方向 | +| --- | --- | --- | +| `ScenePreset.monsterIds` | 场景里额外保存一份怪物池 | 删除,只保留 `npcs`。 | +| `ScenePresetInfo.hostileNpcIds` | 历史遗留双字段 | 直接由 `npcs.filter(initialAffinity < 0 或 hostile)` 推导。 | +| `Encounter.monsterPresetId` | 把 hostile NPC 再次物种化 | 改成 hostile visual preset 字段,或并入统一 visualRef。 | +| `Encounter.hostileNpcPresetId` | 与 `monsterPresetId` 语义重叠 | 与上面合并为一个字段。 | +| `GameState.sceneMonsters` | 把敌对 NPC 单独塞进 monster 容器 | 改成统一 `sceneNpcCombatants` 或等价单一列表。 | +| `GameState.sceneHostileNpcs` | 历史兼容层,导致双数据源 | 删除。 | +| `SceneHostileNpc / SceneMonster` | 类型名直接固化了分轨 | 改成统一的 hostile NPC / scene combat NPC 类型。 | +| `SceneHostileNpcChange / SceneMonsterChange` | 继续复制同一套变更结构 | 合并成统一 NPC scene change。 | +| `SceneNpc.monsterPresetId / hostileNpcPresetId` | 同一实体上挂两套 preset 入口 | 收敛为一个 hostile visual/combat preset 字段。 | + +## 本轮最优先的删除顺序 + +建议后续真正改代码时,按下面顺序删并,风险最低: + +1. 先删字段入口:`monsterIds / sceneHostileNpcs / hostileNpcPresetId` +2. 再删运行时双轨:`src/data/monsters.ts`、`src/data/hostileNpcs.ts`、`src/data/sceneEncounterPreviews.ts` +3. 再删战斗双轨:`battlePlan.ts`、`playback.ts` 里的 `monster` actor 与 `sceneMonsters` +4. 再删画布双轨:`GameCanvasRuntime.tsx`、`GameCanvasEntityLayer.tsx`、`GameCanvasShared.tsx` +5. 最后清编辑器和提示词:`MonsterPresetPanel.tsx`、`MonsterPresetTab.tsx`、`prompt.ts`、`questDirector.ts` + +## 改造后的目标形态 + +统一后应只剩下这一套语义: + +- 场景中所有可见角色都放在 `npcs` +- 怪物 = `initialAffinity < 0` 或 `hostile = true` 的 NPC +- hostile 的视觉差异只来自 hostile visual preset +- 战斗中所有敌方单位都属于 hostile NPC combatant +- AI、任务、渲染、详情、掉落都只读同一套 NPC 数据 + +如果后面代码里还出现下面这些关键词,基本都说明分轨没有删干净: + +- `sceneMonsters` +- `sceneHostileNpcs` +- `monsterIds` +- `hostileNpcPresetId` +- `createSceneMonstersFromIds` +- `getClosestMonster` +- `TurnActor = 'monster'` + +## 这份文档的使用方式 + +后续正式开始改造时,建议把文件分成三批执行: + +1. “直接删掉”的入口脚本 +2. “改名并并回 NPC 主链路”的桥接脚本 +3. “仅保留素材职责”的 renderer / preset 文件 + +不要继续接受“名字叫 NPC,但内部仍然先转成 monster 再跑”的中间态。 diff --git a/docs/audits/engineering/README.md b/docs/audits/engineering/README.md new file mode 100644 index 00000000..93c44c5f --- /dev/null +++ b/docs/audits/engineering/README.md @@ -0,0 +1,19 @@ +# 工程优化审查总览 + +这一组是同主题的连续审查记录,建议不要把它们当作三份彼此独立的文档来看。 + +## 当前推荐入口 + +1. [ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md) + 这一版最适合作为当前工程基线,重点从“是否真正绿色”“门禁有没有覆盖真实风险”来判断仓库状态。 +2. [ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md) + 适合回看运行时主链路、Story/Combat 边界、分层过渡期问题。 +3. [ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md) + 适合看第一轮系统性工程扫描,了解最早的问题基线。 + +## 融合结论 + +- 三轮结论是一致收敛的:问题不在“有没有开始工程化”,而在“工程化是否真正覆盖了最危险的主链路”。 +- 最新一轮已经把关注点集中到质量门禁、真实绿色基线、关键模块豁免和 build warning 上。 +- 如果只是为了判断现在先做什么,直接从 `2026-04-01` 开始即可。 +- 如果是要做长期重构方案,再按 `2026-03-29 -> 2026-03-30 -> 2026-04-01` 的顺序回看演进。 diff --git a/docs/audits/text/CHINESE_MOJIBAKE_INVENTORY.md b/docs/audits/text/CHINESE_MOJIBAKE_INVENTORY.md new file mode 100644 index 00000000..1ce9c131 --- /dev/null +++ b/docs/audits/text/CHINESE_MOJIBAKE_INVENTORY.md @@ -0,0 +1,91 @@ +# 中文乱码位置清单 + +更新时间:`2026-03-24` + +## 说明 + +- 本文档用于记录仓库内已确认或高置信度疑似存在中文乱码的位置。 +- 当前这份文档是重建版本;原有的 [`docs/audits/text/CHINESE_MOJIBAKE_INVENTORY.md`](/E:/Repos/Genarrative/docs/audits/text/CHINESE_MOJIBAKE_INVENTORY.md) 本身也已经乱码,因此已整体替换。 +- 本次整理依据: + - 仓库内旧清单中的完整文件/行号信息 + - 本轮人工复核时再次直接看到的明显乱码位置 +- 由于仓库内同时存在“文件内容已写坏”和“终端/工具显示失真”两类情况,下面清单优先保留高置信位置,便于后续逐项修复。 + +## 扫描范围 + +- 已纳入:`src/`、`docs/`、根目录文档与元数据文件 +- 已排除:`.git/`、`node_modules/`、`dist/`、纯图片资源目录 + +## 高置信位置 + +### 文档与元数据 + +- [`docs/experience/AGENT_UI_CHANGELOG.md`](/E:/Repos/Genarrative/docs/experience/AGENT_UI_CHANGELOG.md):1, 3, 7, 9, 11-18, 24, 26-28, 32-33, 37, 39, 41-43, 47, 49, 51, 53-66, 68, 72, 74, 77-79, 83, 87, 89-90, 94, 96, 98-100, 104, 106-112, 116 +- [`UI_CODING_STANDARD.md`](/E:/Repos/Genarrative/UI_CODING_STANDARD.md):3, 91, 104, 108, 112, 156, 158, 160-166 +- [`metadata.json`](/E:/Repos/Genarrative/metadata.json):2-3 + +### 组件层 + +- [`src/components/AdventurePanel.tsx`](/E:/Repos/Genarrative/src/components/AdventurePanel.tsx):57, 65 +- [`src/components/CharacterPanel.tsx`](/E:/Repos/Genarrative/src/components/CharacterPanel.tsx):37, 65-66, 91-95, 102-103 +- [`src/components/GameCanvas.tsx`](/E:/Repos/Genarrative/src/components/GameCanvas.tsx):240, 462 +- [`src/components/GameShell.tsx`](/E:/Repos/Genarrative/src/components/GameShell.tsx):108, 116, 124, 138, 171, 181 +- [`src/components/InventoryPanel.tsx`](/E:/Repos/Genarrative/src/components/InventoryPanel.tsx):55, 58, 82-83, 181-184, 189, 191 +- [`src/components/MapModal.tsx`](/E:/Repos/Genarrative/src/components/MapModal.tsx):105, 108, 136 +- [`src/components/MedievalNpcAnimator.tsx`](/E:/Repos/Genarrative/src/components/MedievalNpcAnimator.tsx):124 +- [`src/components/NpcVisualEditor.tsx`](/E:/Repos/Genarrative/src/components/NpcVisualEditor.tsx):65, 69-71, 403, 440, 444, 446, 464, 467, 470, 482, 569, 571, 585, 610, 628, 662, 690, 694-695, 697, 722, 751, 759, 775, 777, 781, 824 +- [`src/components/PresetEditor.tsx`](/E:/Repos/Genarrative/src/components/PresetEditor.tsx):34-37, 43-44, 94, 96, 349, 470, 472, 480, 482, 512, 516, 519, 525, 568, 612, 618, 637, 639, 643, 645, 652, 661, 677, 740, 769, 771, 779, 781, 806, 809, 820, 831, 835, 837, 840, 848, 871, 894, 916, 918, 930, 932, 950, 953, 956, 960, 962, 990, 1004, 1006, 1012, 1018, 1024, 1030, 1036, 1064, 1120, 1122, 1130, 1132, 1150, 1153, 1156, 1172-1175, 1180, 1182, 1186, 1188, 1199, 1203-1204, 1208, 1240, 1242 + +### 数据层 + +- [`src/data/characterPresets.ts`](/E:/Repos/Genarrative/src/data/characterPresets.ts):97, 102, 104, 107, 129, 132-133, 142, 144, 170, 276, 302, 470, 496, 531, 540, 566, 699-700, 729, 972 +- [`src/data/medievalNpcVisuals.ts`](/E:/Repos/Genarrative/src/data/medievalNpcVisuals.ts):103, 115, 117, 119, 136, 154, 156, 161, 167, 174, 177, 189, 226, 235-236, 241, 244-245, 249-254, 256-257, 260, 262, 274, 278, 288, 451-453, 565, 568, 577, 592 +- [`src/data/monsterPresets.ts`](/E:/Repos/Genarrative/src/data/monsterPresets.ts):41-42, 54, 60-61, 79-80, 92, 98-99, 117-118, 136-137, 155-156, 171-173, 185, 191-192, 204, 210-211, 229-230, 242, 248-249, 261, 267-268, 280, 286-287, 304-305, 323-324, 335 +- [`src/data/monsters.ts`](/E:/Repos/Genarrative/src/data/monsters.ts):112 +- [`src/data/npcInteractions.ts`](/E:/Repos/Genarrative/src/data/npcInteractions.ts):68-71, 80, 82-83, 161, 165, 173, 182, 188-190, 196, 198, 205, 231, 241, 245, 255-260, 272, 296, 319-320, 372, 444-445, 449, 451, 453, 507, 569-570, 578-579, 587-588, 597, 605-606, 615, 617-618, 626-627, 634, 641-643, 652, 661, 665, 670, 672, 676 +- [`src/data/scenePresets.ts`](/E:/Repos/Genarrative/src/data/scenePresets.ts):115, 120, 122, 128, 133, 135, 141, 146, 148, 154, 159, 161, 167, 172, 174, 180, 185, 187, 192-193, 198, 200, 205-206, 211, 213, 219, 224, 226, 232, 237, 239, 245, 250, 252, 258, 263, 265, 274, 279, 281, 287, 292, 294, 299-300, 305, 307, 313, 318, 320, 326, 331, 333, 339, 344, 346, 352, 357, 359, 364-365, 370, 372, 377-378, 383, 385, 390-391, 396, 398, 404, 409, 411, 417, 422, 424, 509, 523, 525 +- [`src/data/stateFunctions.ts`](/E:/Repos/Genarrative/src/data/stateFunctions.ts):72-73, 80, 95-96, 103, 117-118, 125, 139-140, 147, 161-162, 169, 186-187, 194, 209-210, 217, 237-238, 255-256, 273-274, 294, 311-312, 329-330, 420, 430-431, 433-435, 437-438, 440-442, 444-445, 447, 449, 451-452, 454-456, 458, 460-461, 464, 466, 468, 484-485, 487, 489, 491, 493, 601, 618 + +### Hooks 与服务层 + +- [`src/hooks/useCombatFlow.ts`](/E:/Repos/Genarrative/src/hooks/useCombatFlow.ts):54, 56-58, 566 +- [`src/services/ai.ts`](/E:/Repos/Genarrative/src/services/ai.ts):200-201, 209-210, 234-235, 269-270, 309, 311, 317, 338, 341, 358, 382 +- [`src/services/prompt.ts`](/E:/Repos/Genarrative/src/services/prompt.ts):7-8, 10, 13-15, 19-20, 25-40, 43, 55, 61-62, 64, 66, 74-76, 78-79, 83-84, 87-90, 96, 103-104, 112, 115, 157, 159, 161-162, 164-165, 167-168, 170, 172-173 + +### 其他源码 + +- [`src/uiAssets.ts`](/E:/Repos/Genarrative/src/uiAssets.ts):54, 115, 122, 129, 142, 173, 180 + +## 本轮人工复核补充 + +以下位置是在本轮实现过程中直接再次看到的明显乱码文本,建议优先复查: + +- [`src/hooks/useCombatFlow.ts`](/E:/Repos/Genarrative/src/hooks/useCombatFlow.ts):1094, 1554, 1556-1557 + +## 处理优先级建议 + +### 第一批 + +- `src/components/GameShell.tsx` +- `src/components/InventoryPanel.tsx` +- `src/components/CharacterPanel.tsx` +- `src/data/characterPresets.ts` +- `src/data/npcInteractions.ts` +- `src/data/scenePresets.ts` +- `src/services/prompt.ts` + +### 第二批 + +- `src/components/PresetEditor.tsx` +- `src/components/NpcVisualEditor.tsx` +- `src/data/monsterPresets.ts` +- `src/data/stateFunctions.ts` +- `docs/experience/AGENT_UI_CHANGELOG.md` +- `UI_CODING_STANDARD.md` + +## 备注 + +- 当前文档的目标是“先把位置收拢清楚”,不是直接修复乱码。 +- 如果你下一步要我继续,我可以基于这份清单继续做两件事之一: + - 逐文件修复中文乱码 + - 先做一个“乱码修复优先级 + 替换建议”文档 diff --git a/docs/EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md b/docs/audits/text/EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md similarity index 100% rename from docs/EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md rename to docs/audits/text/EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md diff --git a/docs/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md b/docs/audits/text/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md similarity index 100% rename from docs/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md rename to docs/audits/text/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md diff --git a/docs/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md b/docs/audits/text/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md similarity index 100% rename from docs/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md rename to docs/audits/text/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md diff --git a/docs/GAME_UI_PRESET_EDITOR_NPC_PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.md b/docs/audits/text/GAME_UI_PRESET_EDITOR_NPC_PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.md similarity index 100% rename from docs/GAME_UI_PRESET_EDITOR_NPC_PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.md rename to docs/audits/text/GAME_UI_PRESET_EDITOR_NPC_PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.md diff --git a/docs/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md b/docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md similarity index 100% rename from docs/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md rename to docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md diff --git a/docs/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md b/docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md similarity index 100% rename from docs/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md rename to docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md diff --git a/docs/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md b/docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md similarity index 100% rename from docs/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md rename to docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md diff --git a/docs/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md b/docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md similarity index 100% rename from docs/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md rename to docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md diff --git a/docs/audits/text/README.md b/docs/audits/text/README.md new file mode 100644 index 00000000..0abe2d0a --- /dev/null +++ b/docs/audits/text/README.md @@ -0,0 +1,27 @@ +# 文本与乱码审计总览 + +这一组文档记录的是同一条清理链路的不同阶段:从“发现哪里有英文/乱码”到“扩展到 prompt、npcInteraction、编辑器深层文本”。 + +## 当前推荐入口 + +1. [CHINESE_MOJIBAKE_INVENTORY.md](./CHINESE_MOJIBAKE_INVENTORY.md) + 偏全局库存清单,适合先确认问题分布范围。 +2. [GAME_UI_PRESET_EDITOR_NPC_PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.md](./GAME_UI_PRESET_EDITOR_NPC_PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.md) + 这是当前最完整的深度审计版本,已经扩到 `prompt`、`npcInteractions`、运行时弹窗与编辑器深层文本。 +3. [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md) + 适合看“扩展重查版”的 UI / 预设 / 编辑器问题面。 + +## 历史时间线 + +- [EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md](./EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md):较早期的整体首轮盘点。 +- [GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md](./GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md):复查阶段,开始收紧范围和口径。 +- [GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md](./GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md):继续复核真实乱码与英文残留。 +- [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md):对上一轮的续扫补充。 +- [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md):继续收敛 UI、预设与编辑器问题。 +- [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md):进入更明确的审计范围与方法阶段。 + +## 融合结论 + +- 早期几份文档主要负责“摸清哪里有问题”。 +- `2026-04-02` 两份文档开始把重点收敛到真正会影响玩家体验和 AI 生成质量的链路。 +- 现在做文本修复时,不必从最早一份开始逐篇读;优先看 `CHINESE_MOJIBAKE_INVENTORY` 和 `2026-04-02` 两份即可。 diff --git a/docs/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md b/docs/design/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md similarity index 100% rename from docs/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md rename to docs/design/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md diff --git a/docs/COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md b/docs/design/COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md similarity index 100% rename from docs/COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md rename to docs/design/COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md diff --git a/docs/design/CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md b/docs/design/CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md new file mode 100644 index 00000000..0dd5da38 --- /dev/null +++ b/docs/design/CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md @@ -0,0 +1,492 @@ +# 自定义世界创作者输入与 AI 分工边界设计 + +更新时间:`2026-04-06` + +## 0. 目标 + +这份文档回答一个非常关键的问题: + +**在“低创作门槛、高创作自由度”的前提下,自定义世界里哪些内容应该交给创作者直接定义,哪些内容应该交给 AI 和系统完成。** + +这里默认我们的创作者: + +- 不需要有专业作家背景 +- 不需要有专业游戏设计背景 +- 但希望作品有明显个人风格,而不是只是在用一个会自动补全设定的模板工具 + +一句话目标: + +**让创作者把精力放在“决定这个世界为什么值得被创作”,把 AI 用在“把这个世界展开、编译、铺开、校验、补足”。** + +## 1. 总体结论 + +自定义世界的分工边界应该遵守 3 条硬原则: + +1. 灵魂归创作者,杂活归 AI。 + - 凡是决定作品气质、主题、冲突、人物关系、审美方向的内容,都应由创作者掌握。 + +2. 重点对象归创作者,长尾铺量归 AI。 + - 创作者应重点塑造少量关键角色、关键地点、关键冲突、关键意象,而不是被迫手填几十个 NPC、几十个场景、几百条描述。 + +3. 决策归创作者,编译归 AI / 系统。 + - 创作者负责说“这个世界要成为什么样”,AI / 系统负责把它编译成可运行的数据、规则、文本、关系钩子和运行时结构。 + +这意味着: + +- 创作者应该主要编辑“高杠杆创作锚点” +- AI 应该主要承担“批量展开 + 结构编译 + 一致性维护 + 专业执行” + +## 2. 什么内容应该交给创作者 + +真正应该交给创作者的,不是大量表格字段,而是下面这些会显著决定作品质量、且 AI 不擅长替代的内容。 + +## 2.1 世界核心命题 + +创作者应该直接定义: + +- 这个世界的一句话设定 +- 这个世界最吸引人的核心幻想 +- 玩家来到这个世界,最想体验的感觉是什么 +- 这个世界和常规同题材作品相比,最不同的地方是什么 + +原因: + +- 这是作品的创作方向盘 +- 一旦这一层是空的,后面所有 AI 扩写都会变成“像一个世界”,而不是“这个世界” + +## 2.2 主题、气质与边界 + +创作者应该直接定义: + +- 主题关键词 +- 情绪基调 +- 审美偏好 +- 禁忌内容 / 不希望出现的表达 +- 可以接受的黑暗度、浪漫度、残酷度、神秘度 + +原因: + +- 这决定了 AI 后续生成时的“味道” +- 这类判断很难靠 AI 替代,因为它本质上不是信息补全,而是审美取舍 + +## 2.3 玩家身份与开局处境 + +创作者应该直接定义: + +- 玩家扮演的是什么人 +- 玩家一开始最缺什么、最想要什么 +- 开局时玩家被卷入什么局面 +- 玩家在这个世界里天然站在哪个位置上 + +原因: + +- 这决定了整个世界的观看视角 +- 同一个世界,玩家视角不同,最终体验会完全不同 + +## 2.4 核心冲突与关键势力 + +创作者应该直接定义少量高价值内容: + +- 世界当前最重要的 `2~4` 条明面冲突 +- 世界背后最关键的 `1~3` 条暗面问题 +- `2~6` 个关键势力 +- 这些势力各自想要什么、害怕什么、互相卡住了什么 + +原因: + +- 冲突结构决定世界是否“有戏” +- 势力关系是 AI 最容易写散、写平、写成百科介绍的部分 +- 这一层由创作者把握,才能真正提高作品的辨识度 + +## 2.5 关键角色与关系张力 + +创作者应该直接定义少量关键角色,而不是所有 NPC。 + +建议重点交给创作者的,是: + +- `3~8` 个关键角色 +- 玩家与这些人的潜在关系 +- 这些角色彼此之间的债、仇、秘密、误解、利益绑定 +- 每个关键角色“表面上像什么、实际上压着什么” + +原因: + +- 角色关系是最能显著提升作品质量的部分之一 +- 这也是 AI 最容易写得“完整但无味”的部分 +- 创作者不需要写长篇背景,但应掌握这些角色真正的关系骨架 + +## 2.6 关键地点与空间记忆点 + +创作者应该直接定义: + +- `4~12` 个关键地点 / 区域 / 地标 +- 这些地方为什么重要 +- 这些地方承载什么冲突、危险、秘密或情绪记忆 +- 玩家第一次来到这里时应该感到什么 + +原因: + +- “地方感”是世界质量的重要来源 +- 关键地点一旦成立,AI 后续才能稳定地生成周边事件、物件、NPC 和线索 + +## 2.7 标志性意象、物件、怪物、制度与规则 + +创作者应该优先控制世界里最能代表它的东西: + +- 标志性物件 +- 标志性怪物 / 生物 +- 标志性能力体系 / 修炼体系 / 技术体系 +- 标志性社会制度 / 宗教 / 仪式 / 禁忌 +- 世界的硬规则 + +原因: + +- 这些内容决定世界的“手感” +- 它们不是普通细节,而是会反复影响命名、剧情、视觉、对话与玩法解释的母题 + +## 2.8 创作者应直接控制的“禁止事项” + +创作者必须能明确锁定: + +- 什么绝对不能改 +- 什么不能被 AI 自动扩写到别的方向 +- 哪些角色、地点、关系、设定是核心锚点 +- 哪些内容允许 AI 自由发挥,哪些只能在锚点附近变体 + +原因: + +- 高自由度不等于所有内容都开放漂移 +- 如果没有“锁定机制”,AI 会把创作者真正关心的内容稀释掉 + +## 3. 什么内容应该交给 AI 和系统 + +应该交给 AI 的,不是“重要内容”,而是“重要内容之外的大量展开、编译、补缝、校验与专业执行”。 + +## 3.1 批量生成的长尾内容 + +应该主要交给 AI: + +- 普通 NPC +- 路人、商贩、巡逻者、村民、杂兵 +- 次级场景 +- 场景支线事件 +- 大量普通物品 +- 世界的长尾命名与描述 + +原因: + +- 这些内容数量大、重复度高 +- 它们需要“贴合世界”,但不需要都由创作者逐个手写 +- AI 很适合做“围绕锚点的批量铺量” + +## 3.2 从创作锚点到系统结构的编译 + +应该交给 AI / 系统: + +- 从自然语言世界设定中提取题材词汇 +- 从关键冲突中编译出世界叙事图谱 +- 从关键角色卡编译出角色叙事档案 +- 从创作者输入里自动生成标签、钩子、隐藏线索、章节摘要 +- 从地点和关系中编译出场景连接、事件触发和叙事回响 + +对应当前仓库,下面这些结构更适合由 AI / 系统生成,而不是让玩家直接编辑: + +- `ThemePack` +- `WorldStoryGraph` +- `ActorNarrativeProfile` +- `KnowledgeFact` +- `VisibilitySlice` +- `SceneNarrativeDirective` +- `CarrierStoryFingerprint` +- `ThreadContract` +- `StorySignal` + +原因: + +- 这些是运行时结构,不是创作者真正想表达的作品内容 +- 直接暴露给玩家,会把创作过程变成专业数据填表 + +## 3.3 专业化、规则化的任务 + +应该交给 AI / 系统: + +- 数值平衡 +- 标签归纳 +- 稀有度预算 +- 初始技能与初始物品的批量配置 +- build 方向匹配 +- 地图连接补全 +- 触发条件与推进信号编译 +- 背景章节拆分与 teaser 生成 +- 运行时物件命名与叙事描述的变体生成 + +原因: + +- 这些工作要么重复、要么专业、要么容易做脏活累活 +- 让非专业创作者处理,会显著提高门槛,却不一定显著提高质量 + +## 3.4 一致性、纠错与查漏补缺 + +应该交给 AI / 系统持续处理: + +- 世界设定冲突检查 +- 角色关系矛盾检查 +- 同名 / 重复 / 设定撞车检查 +- 信息越权泄露检查 +- prompt 裁剪 +- 风格一致性检查 +- “这个角色/地点/物件是否真的和世界主线有关”的弱关联检查 + +原因: + +- 这是 AI 比人更适合做的“维护型工作” +- 它属于创作支持,不属于创作者必须亲手完成的创作 + +## 4. 最合理的边界不是二分法,而是三层分工 + +自定义世界最合理的结构,不是“玩家写”与“AI 写”的简单二选一,而是三层。 + +## 4.1 第一层:创作者必控层 + +这一层必须给创作者高自由度,且能被锁定: + +- 世界核心命题 +- 主题与气质 +- 玩家身份与开局 +- 核心冲突 +- 关键势力 +- 关键角色 +- 关键地点 +- 标志性物件 / 怪物 / 规则 +- 禁止事项 + +这层的原则是: + +**少而重。** + +## 4.2 第二层:创作者可选强化层 + +这一层不应强制填写,但应该允许创作者继续深挖: + +- 明线 / 暗线种子 +- 角色之间的旧事 +- 地点背后的旧伤 +- 标志性物件的来历 +- 关键角色的口头习惯、禁忌、执念 +- 关键地点的视觉母题与情绪目标 + +这层的原则是: + +**愿意细写的人可以拉高作品上限,不愿细写的人也不会被门槛卡住。** + +## 4.3 第三层:AI 自动展开层 + +这一层默认交给 AI / 系统: + +- 长尾 NPC +- 次级地点 +- 章节拆分 +- 初始技能 +- 初始物品 +- 标签与属性映射 +- 任务 contract +- 物件叙事指纹 +- 可见性裁剪 +- 运行时导演指令 +- 批量命名与文案变体 + +这层的原则是: + +**AI 可以做多、做快、做杂,但不能越过第一层锁定内容。** + +## 5. 具体模块的建议归属 + +| 模块 | 建议归属 | 创作者应控制什么 | AI / 系统应负责什么 | +| --- | --- | --- | --- | +| 世界一句话设定、核心幻想、核心卖点 | 创作者直接控制 | 直接写、直接改、可锁定 | 给出备选表述和扩展方向 | +| 主题、基调、审美、禁忌 | 创作者直接控制 | 选择 / 改写 / 锁定 | 生成风格词、避雷词、提示词约束 | +| 玩家身份、开局处境、玩家目标 | 创作者直接控制 | 直接定义 | 补足开局钩子和初始叙事包装 | +| 关键势力与核心冲突 | 创作者主控,AI 辅助 | 定义核心关系和立场 | 扩展冲突支路、生成世界线程 | +| 关键角色 | 创作者主控,AI 辅助 | 定义角色骨架、关系张力、秘密方向 | 生成长背景、章节拆分、技能、物品、叙事档案 | +| 关键地点 | 创作者主控,AI 辅助 | 定义地点意义、气氛、秘密 | 扩展场景细节、连接关系、遭遇分布 | +| 标志性物件 / 怪物 / 制度 / 规则 | 创作者主控,AI 辅助 | 定义代表性要素与硬边界 | 扩展变体、命名、说明、运行时挂钩 | +| 普通 NPC / 路人 / 杂兵 / 次级地点 | 主要交给 AI | 仅在需要时抽查或替换 | 批量生成与风格保持 | +| 角色长背景、章节 teaser、context snippet | 主要交给 AI | 创作者只改关键角色即可 | 自动拆章、压缩、解锁节奏整理 | +| 技能、初始物品、标签、构筑倾向 | 主要交给 AI / 系统 | 提供偏好或少量 override | 按角色和世界规则自动编译 | +| 世界图谱、知识事实、可见性、导演指令 | AI / 系统内部层 | 不应默认暴露给玩家 | 运行时编译与维护 | +| 一致性检查、冲突检查、越权检查 | AI / 系统内部层 | 查看报告、决定是否采纳修改 | 自动扫描并提出修正建议 | + +## 6. 不应该要求玩家直接填写的字段 + +为了真正做到低门槛,下面这些内容不应直接以“专业字段”形式强迫玩家填写。 + +## 6.1 不应该要求玩家手填原始数值 + +例如: + +- `initialAffinity` +- `dangerLevel` +- 精确数值型 build 倾向 +- 复杂掉落预算 + +更合理的做法是让创作者填写直觉表达,例如: + +- `初见就戒备` +- `容易合作` +- `这里非常危险` +- `偏爆发型` + +再由系统编译成运行时数值。 + +## 6.2 不应该要求玩家手填技术型结构 + +例如: + +- `tags` +- `attributeSchema` +- `ThemePack` +- `WorldStoryGraph` +- `VisibilitySlice` +- `SceneNarrativeDirective` +- `ThreadContract` + +原因: + +- 这些字段属于系统运行结构,不属于创作者自然的创作语言 +- 直接让玩家填,会把工具变成只有懂系统的人才能用 + +## 6.3 不应该要求玩家逐个补完所有人物设定字段 + +当前 `CustomWorldRoleProfile` 里这些字段: + +- `backstory` +- `personality` +- `motivation` +- `combatStyle` +- `backstoryReveal` +- `skills` +- `initialItems` + +更适合的做法不是全部让玩家手写,而是先让玩家填写更自然的“角色卡”: + +- 这个人表面上是什么样 +- 这个人真正想要什么 +- 这个人最不想被提到什么 +- 这个人和玩家之间最可能形成什么关系 +- 这个人和哪个地点 / 物件 / 旧事绑得最紧 + +再由 AI / 系统编译成当前结构。 + +## 7. 推荐的创作输入形态 + +要让非专业创作者也能高自由度创作,输入形态必须改成“自然语言创作卡”,而不是“系统字段表单”。 + +## 7.1 世界层卡片 + +建议至少有这些卡片: + +1. 世界一句话 +2. 主题与气质 +3. 玩家是谁 +4. 核心冲突 +5. 关键势力 +6. 关键角色 +7. 关键地点 +8. 标志性要素 +9. 禁止事项 + +## 7.2 每张卡片都允许 3 种输入方式 + +1. 一句话自由输入 + - 适合低门槛创作者 + +2. 标签 / 选项 / 语气滑条 + - 适合不想写太多字的创作者 + +3. 高级补充 + - 适合愿意继续深挖的人 + +这样才能做到: + +- 不会逼着用户写长文 +- 但也不会限制愿意创作的人继续把世界做深 + +## 7.3 必须支持“锁定”与“局部重生成” + +这是高创作自由度里非常关键的一点。 + +创作者应当能: + +- 锁定一个角色 +- 锁定一个地点 +- 锁定一条冲突 +- 只重生成未锁定部分 +- 围绕锁定内容重写其余世界 + +否则创作者每次调用 AI,都会有“好不容易想好的东西被洗掉”的感受。 + +## 8. 面向当前仓库的结构映射建议 + +为了便于后续落实现有系统,这份边界建议可以直接映射到当前结构: + +## 8.1 创作者输入层 + +建议主要映射到: + +- `CustomWorldProfile.settingText` +- `CustomWorldProfile.name` +- `CustomWorldProfile.subtitle` +- `CustomWorldProfile.summary` +- `CustomWorldProfile.tone` +- `CustomWorldProfile.playerGoal` +- `CustomWorldProfile.majorFactions` +- `CustomWorldProfile.coreConflicts` + +以及关键角色、关键地点的创作卡输入。 + +## 8.2 AI 编译层 + +由 AI / 系统从创作者输入自动补出: + +- `themePack` +- `storyGraph` +- `knowledgeFacts` +- `threadContracts` +- 每个关键角色的 `narrativeProfile` +- 每个角色的 `backstoryReveal` +- 每个角色的 `skills` +- 每个角色的 `initialItems` + +## 8.3 运行时支持层 + +运行时继续由 AI / 系统维护: + +- `VisibilitySlice` +- `SceneNarrativeDirective` +- `CarrierStoryFingerprint` +- `StorySignal` + +这些内容应该是“系统如何把世界跑起来”,不是“创作者必须亲手写完的创作内容”。 + +## 9. 产品层面的最终结论 + +如果我们的目标真的是“低创作门槛、高创作自由度”,那么自定义世界不应该做成一个要求用户: + +- 填很多字段 +- 写很多长文 +- 理解很多系统结构 +- 自己负责平衡、命名、拆章节、补标签、补长尾内容 + +的专业编辑器。 + +它应该做成这样: + +1. 创作者决定世界的灵魂锚点。 +2. 创作者重点塑造少量关键人、关键地、关键冲突、关键物。 +3. AI 围绕这些锚点批量展开长尾内容。 +4. 系统把这些内容编译成可运行的图谱、可见性、任务、物件和关系结构。 +5. 创作者随时可以锁定核心创意,并局部重生成其余部分。 + +一句话收束: + +**创作者应该写“这个世界为什么动人”,AI 应该负责“让这个世界长出来并跑起来”。** diff --git a/docs/EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md b/docs/design/EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md similarity index 100% rename from docs/EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md rename to docs/design/EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md diff --git a/docs/design/README.md b/docs/design/README.md new file mode 100644 index 00000000..4f25a9b2 --- /dev/null +++ b/docs/design/README.md @@ -0,0 +1,18 @@ +# 系统设计 + +这一组是玩法、关系、物品、对话等系统设计文档,偏“应该怎么设计”而不是“现在哪里出问题”。 + +## 文档列表 + +- [CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md](./CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md):自定义世界里创作者输入与 AI 分工边界设计。 +- [AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md](./AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md):运行时物品生成系统重设计。 +- [EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md](./EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md):配装构筑与合成/锻造闭环设计。 +- [COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md](./COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md):角色首遇感、关系分层解锁、私聊系统设计。 +- [npc-conversation-situation-draft.md](./npc-conversation-situation-draft.md):NPC 对话阶段和情景注入草案。 + +## 推荐阅读 + +- 做物品、Build、锻造相关需求时,先看前两份。 +- 做自定义世界创作工作台、创作者输入边界、AI 分工设计时,先看第一份。 +- 做角色关系、同伴互动、对话表现时,先看后两份。 +- 如果要判断是否符合目标,再和 `docs/prd/` 中对应 PRD 对照阅读。 diff --git a/docs/npc-conversation-situation-draft.md b/docs/design/npc-conversation-situation-draft.md similarity index 100% rename from docs/npc-conversation-situation-draft.md rename to docs/design/npc-conversation-situation-draft.md diff --git a/docs/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md b/docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md similarity index 100% rename from docs/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md rename to docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md diff --git a/docs/AGENT_UI_CHANGELOG.md b/docs/experience/AGENT_UI_CHANGELOG.md similarity index 100% rename from docs/AGENT_UI_CHANGELOG.md rename to docs/experience/AGENT_UI_CHANGELOG.md diff --git a/docs/CODEX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md b/docs/experience/CODEX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md similarity index 100% rename from docs/CODEX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md rename to docs/experience/CODEX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md diff --git a/docs/CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md b/docs/experience/CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md similarity index 85% rename from docs/CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md rename to docs/experience/CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md index 1f7426f7..99166c90 100644 --- a/docs/CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md +++ b/docs/experience/CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md @@ -35,7 +35,7 @@ 完成内容: -- 将 [NpcVisualEditor](/E:/Repos/ai-native-visual-rpg/src/components/NpcVisualEditor.tsx) 嵌入 [PresetEditor](/E:/Repos/ai-native-visual-rpg/src/components/PresetEditor.tsx) 的 NPC 编辑页 +- 将 [NpcVisualEditor](/E:/Repos/Genarrative/src/components/NpcVisualEditor.tsx) 嵌入 [PresetEditor](/E:/Repos/Genarrative/src/components/PresetEditor.tsx) 的 NPC 编辑页 - 让 NPC 文本字段与视觉字段围绕同一个当前选中 NPC 联动 - 保留视觉覆盖保存与全局布局保存能力 @@ -48,7 +48,7 @@ 完成内容: -- 在 [medievalNpcVisuals.ts](/E:/Repos/ai-native-visual-rpg/src/data/medievalNpcVisuals.ts) 中重建了 Medieval NPC 的资产定义 +- 在 [medievalNpcVisuals.ts](/E:/Repos/Genarrative/src/data/medievalNpcVisuals.ts) 中重建了 Medieval NPC 的资产定义 - 补齐了 cloth / leather / metal / melee / magic / ranged 六大类真实素材 - 为素材增加了: - 语义化名称 @@ -68,7 +68,7 @@ 完成内容: -- 在 [MedievalNpcAnimator.tsx](/E:/Repos/ai-native-visual-rpg/src/components/MedievalNpcAnimator.tsx) 中为 `AtlasSprite` 增加: +- 在 [MedievalNpcAnimator.tsx](/E:/Repos/Genarrative/src/components/MedievalNpcAnimator.tsx) 中为 `AtlasSprite` 增加: - `tileWidth` - `tileHeight` - 对齐偏移支持 @@ -90,7 +90,7 @@ 完成内容: -- 在 [characterPresets.ts](/E:/Repos/ai-native-visual-rpg/src/data/characterPresets.ts) 中重新核对 5 个玩家角色的 Hero 动画目录 +- 在 [characterPresets.ts](/E:/Repos/Genarrative/src/data/characterPresets.ts) 中重新核对 5 个玩家角色的 Hero 动画目录 - 修正了错误帧数、错误前缀、遗漏动作 - 补齐了真实存在但之前未接入的动作: - `acquire` @@ -106,9 +106,9 @@ 相关文件: -- [types.ts](/E:/Repos/ai-native-visual-rpg/src/types.ts) -- [CharacterAnimator.tsx](/E:/Repos/ai-native-visual-rpg/src/components/CharacterAnimator.tsx) -- [characterCombat.ts](/E:/Repos/ai-native-visual-rpg/src/data/characterCombat.ts) +- [types.ts](/E:/Repos/Genarrative/src/types.ts) +- [CharacterAnimator.tsx](/E:/Repos/Genarrative/src/components/CharacterAnimator.tsx) +- [characterCombat.ts](/E:/Repos/Genarrative/src/data/characterCombat.ts) 经验: @@ -121,7 +121,7 @@ 完成内容: -- 在 [monsterPresets.ts](/E:/Repos/ai-native-visual-rpg/src/data/monsterPresets.ts) 中把怪物动画从“连续帧猜测”改成“按图集行起点取帧” +- 在 [monsterPresets.ts](/E:/Repos/Genarrative/src/data/monsterPresets.ts) 中把怪物动画从“连续帧猜测”改成“按图集行起点取帧” - 补上缺失的 `die` 配置 - 清除了所有落进空白格的动画段 @@ -151,11 +151,11 @@ 完成内容: -- 在 [StateFunctionEditor.tsx](/E:/Repos/ai-native-visual-rpg/src/components/StateFunctionEditor.tsx) 中新增 `BehaviorExecutionPreview` +- 在 [StateFunctionEditor.tsx](/E:/Repos/Genarrative/src/components/StateFunctionEditor.tsx) 中新增 `BehaviorExecutionPreview` - 预览不再是静态推测,而是: 1. 构造本地 `GameState` 2. 调用真实 `resolveFunctionOption` - 3. 再调用 [useCombatFlow.ts](/E:/Repos/ai-native-visual-rpg/src/hooks/useCombatFlow.ts) 的 + 3. 再调用 [useCombatFlow.ts](/E:/Repos/Genarrative/src/hooks/useCombatFlow.ts) 的 - `buildResolvedChoiceState` - `playResolvedChoice` - 从而直接复用游戏真实逻辑 diff --git a/docs/MOBILE_UI_DEV_EXPERIENCE.md b/docs/experience/MOBILE_UI_DEV_EXPERIENCE.md similarity index 100% rename from docs/MOBILE_UI_DEV_EXPERIENCE.md rename to docs/experience/MOBILE_UI_DEV_EXPERIENCE.md diff --git a/docs/PROJECT_DEVELOPMENT_EXPERIENCE.md b/docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md similarity index 100% rename from docs/PROJECT_DEVELOPMENT_EXPERIENCE.md rename to docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md diff --git a/docs/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md b/docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md similarity index 97% rename from docs/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md rename to docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md index 120159b8..e6ee7a4a 100644 --- a/docs/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md +++ b/docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md @@ -260,7 +260,7 @@ node scripts/vite-cli.mjs --port=3000 --host=0.0.0.0 如需继续细看已有沉淀,可结合以下文档一起阅读: -- `docs/PROJECT_DEVELOPMENT_EXPERIENCE.md` -- `docs/MOBILE_UI_DEV_EXPERIENCE.md` -- `docs/CODEX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md` -- `docs/AGENT_UI_CHANGELOG.md` +- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md` +- `docs/experience/MOBILE_UI_DEV_EXPERIENCE.md` +- `docs/experience/CODEX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md` +- `docs/experience/AGENT_UI_CHANGELOG.md` diff --git a/docs/experience/README.md b/docs/experience/README.md new file mode 100644 index 00000000..878fa35e --- /dev/null +++ b/docs/experience/README.md @@ -0,0 +1,25 @@ +# 经验沉淀 + +这一组文档主要回答两个问题: + +- 这个项目开发时有哪些稳定有效的方法论。 +- 遇到 UI、运行时、编辑器、AI 边界问题时,优先应该怎么判断。 + +## 推荐入口 + +1. [PROJECT_WORK_EXPERIENCE_PLAYBOOK.md](./PROJECT_WORK_EXPERIENCE_PLAYBOOK.md):最完整的项目开发手册,适合先建立全局认识。 +2. [PROJECT_DEVELOPMENT_EXPERIENCE.md](./PROJECT_DEVELOPMENT_EXPERIENCE.md):项目级经验浓缩版,适合快速回顾。 +3. [ADVENTURE_RUNTIME_DEV_EXPERIENCE.md](./ADVENTURE_RUNTIME_DEV_EXPERIENCE.md):专门看运行时、战斗、演出、NPC 流程时优先读。 +4. [MOBILE_UI_DEV_EXPERIENCE.md](./MOBILE_UI_DEV_EXPERIENCE.md):做移动端/游戏 UI 时的布局和交互经验。 +5. [AGENT_UI_CHANGELOG.md](./AGENT_UI_CHANGELOG.md):当前 UI 改动脉络、资产约束和已知坑。 + +## 历史实现经验 + +- [CODEX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md](./CODEX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md):偏“这类需求怎么拆链路”的实战经验。 +- [CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md](./CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md):偏“之前做过什么、怎么做的”的历史记录。 + +## 使用建议 + +- 只需要读一份时,优先看 `PROJECT_WORK_EXPERIENCE_PLAYBOOK`。 +- 做 UI 改动时,把本目录和根目录的 `UI_CODING_STANDARD.md` 对照着看。 +- 做运行时流程改动时,把本目录和 `docs/audits/engineering/README.md` 一起看,能更快发现风险边界。 diff --git a/docs/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md b/docs/planning/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md similarity index 86% rename from docs/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md rename to docs/planning/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md index 13214f5f..8efeba2f 100644 --- a/docs/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md +++ b/docs/planning/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md @@ -23,7 +23,7 @@ ### 为什么必须排第一 -- `docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md` 已明确指出:当前最值得优先优化的不是继续加功能,而是把“半完成的工程化”补齐。 +- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md` 已明确指出:当前最值得优先优化的不是继续加功能,而是把“半完成的工程化”补齐。 - 文档中提到过 `lint` 失败、`build` warning、核心热区文件被 ESLint ignore、部分测试未进入默认套件,这意味着当前代码库还不在真正稳定的绿色基线。 - 在这种状态下继续叠加新玩法,只会把问题扩散到更多运行时链路和编辑器链路。 @@ -51,8 +51,8 @@ ### 为什么必须紧跟在 P0-1 后面 -- `docs/PROJECT_DEVELOPMENT_EXPERIENCE.md`、`docs/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`、`docs/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md` 都反复强调:这个项目是叙事、状态、演出、界面四条链路耦合的复合项目,不能靠大文件硬扛。 -- `docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md` 和 `docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md` 一致指出,`useStoryGeneration`、`useCombatFlow`、`GameShell` 仍然是当前最大的复杂度集中点。 +- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md`、`docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`、`docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md` 都反复强调:这个项目是叙事、状态、演出、界面四条链路耦合的复合项目,不能靠大文件硬扛。 +- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md` 和 `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md` 一致指出,`useStoryGeneration`、`useCombatFlow`、`GameShell` 仍然是当前最大的复杂度集中点。 - 如果不先拆主链,后面的统一属性系统、任务系统、物品导演层都会继续堆进现有巨型流程控制器,技术债只会翻倍。 ### 本阶段要做什么 @@ -133,7 +133,7 @@ ### 为什么它值得排在任务系统前面 -- `docs/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md` 明确指出:当前宝藏、NPC、任务、锻造等入口都有物品,但缺少统一导演层,奖励与场景/NPC/事件的贴合度不够高。 +- `docs/design/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md` 明确指出:当前宝藏、NPC、任务、锻造等入口都有物品,但缺少统一导演层,奖励与场景/NPC/事件的贴合度不够高。 - 相比任务系统,运行时物品奖励能更快提升“世界贴脸感”和“当下反馈质量”,且可以先从宝藏入口低风险落地。 ### 本阶段要做什么 @@ -206,7 +206,7 @@ ### 为什么它放在 P2 -- `docs/PROJECT_DEVELOPMENT_EXPERIENCE.md` 和 `docs/MOBILE_UI_DEV_EXPERIENCE.md` 都强调:冒险页必须优先保证上方演出、一屏选项和文本区自适应。 +- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md` 和 `docs/experience/MOBILE_UI_DEV_EXPERIENCE.md` 都强调:冒险页必须优先保证上方演出、一屏选项和文本区自适应。 - 但从当前文档判断,移动端体验和包体问题更像“持续治理项”,不是当前阶段最核心的系统阻塞点。 ### 本阶段要做什么 @@ -265,13 +265,13 @@ ## 本清单的主要依据 -- `docs/PROJECT_DEVELOPMENT_EXPERIENCE.md` -- `docs/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md` -- `docs/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md` -- `docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md` -- `docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md` -- `docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md` -- `docs/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md` +- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md` +- `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md` +- `docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md` +- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md` +- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md` +- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md` +- `docs/design/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md` - `docs/prd/AI_NATIVE_UNIFIED_ROLE_ATTRIBUTE_SYSTEM_PRD_2026-04-02.md` - `docs/prd/BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md` - `docs/prd/AI_NATIVE_QUEST_SYSTEM_PRD_2026-04-02.md` diff --git a/docs/planning/README.md b/docs/planning/README.md new file mode 100644 index 00000000..22c7c861 --- /dev/null +++ b/docs/planning/README.md @@ -0,0 +1,10 @@ +# 规划与优先级 + +## 当前入口 + +- [CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md](./CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md):当前阶段最值得优先做什么、为什么,以及它和审计/PRD 的对应关系。 + +## 使用建议 + +- 需要排期、拆阶段、判断先修基线还是先加功能时,先看这份。 +- 这份文档大量引用了经验文档、工程审查和 PRD,适合作为跨文档导航页使用。 diff --git a/docs/prd/AI_NATIVE_CUSTOM_WORLD_CREATION_FLOW_OPTIMIZATION_PRD_2026-04-06.md b/docs/prd/AI_NATIVE_CUSTOM_WORLD_CREATION_FLOW_OPTIMIZATION_PRD_2026-04-06.md new file mode 100644 index 00000000..8e82c4a8 --- /dev/null +++ b/docs/prd/AI_NATIVE_CUSTOM_WORLD_CREATION_FLOW_OPTIMIZATION_PRD_2026-04-06.md @@ -0,0 +1,699 @@ +# AI 原生自定义世界生成流程优化 PRD + +更新时间:`2026-04-06` + +## 0. 文档目的 + +这份 PRD 用于基于 [CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md](../design/CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md) 的分工结论,系统优化当前自定义世界生成流程。 + +目标不是推翻当前已经存在的多阶段生成链,而是解决下面这个核心错位: + +**当前仓库已经开始把世界生成拆成 `framework -> themePack -> storyGraph -> role outline -> dossier -> narrativeProfile` 的分阶段 AI 编译流程,但创作者入口仍然是“一段大文本”,结果页又把大量低杠杆字段重新扔回给创作者人工兜底。** + +一句话定义本次优化: + +**让创作者先定义世界灵魂锚点,再让 AI / 系统围绕锚点分层生成、分层展开、分层可控地完成长尾内容。** + +## 1. 当前流程现状 + +## 1.1 当前用户流程 + +当前自定义世界的实际产品链路大致是: + +1. 世界选择页点击“创建自定义世界” +2. 弹出创建弹窗,只输入一段 `世界设定文本` +3. 调用 `generateCustomWorldProfile(...)` +4. 进入分阶段生成页,依次跑: + - 世界框架 + - 题材适配层 + - 世界线程图谱 + - 可扮演角色骨架 + - 场景角色骨架 + - 场景骨架 + - 场景连接 + - 角色叙事补全 + - 角色档案补全 + - 角色叙事档案补全 + - 最终归档 +5. 生成完成后进入结果页 +6. 在结果页里按 `世界 / 可扮演角色 / 场景角色 / 场景` 四个页签逐项编辑 +7. 保存并进入世界 + +对应当前仓库的主要模块: + +- 输入入口:`src/components/SelectionCustomizationModals.tsx` +- 预开局流程:`src/components/game-shell/PreGameSelectionFlow.tsx` +- 分阶段生成:`src/services/ai.ts` +- prompt 与结构归一:`src/services/customWorld.ts` +- 扩展与编译:`src/services/customWorldBuilder.ts` +- 结果页:`src/components/CustomWorldResultView.tsx` +- 结果目录:`src/components/CustomWorldEntityCatalog.tsx` +- 实体编辑器:`src/components/CustomWorldEntityEditorModal.tsx` + +## 1.2 当前流程已经做对了什么 + +当前流程并不是“完全错误”,它已经有几个很值得保留的基础: + +1. 生成链已经不是单次大 JSON 直出,而是分阶段生成。 +2. 已经存在 `themePack / storyGraph / narrativeProfile / knowledgeFacts / threadContracts` 这类更接近 AI 原生剧情引擎的结构。 +3. 已经有阶段进度反馈,而不是黑盒等待。 +4. 已经有结果页与人工编辑能力,可以作为后续工作台的基础。 +5. 已经有 normalize / fallback / repair prompt 机制,说明链路具备继续工程化的基础。 + +本次优化应尽量复用这些已有能力,而不是回到“单次 prompt 直接生世界”的旧思路。 + +## 1.3 当前流程的核心问题 + +## 1.3.1 创作者入口过于粗糙 + +当前创建入口只有一块大文本输入框。 + +这会直接导致: + +1. 不会写长描述的用户很难开局。 +2. 愿意精细创作的用户没有结构化落点。 +3. 系统无法明确分辨“哪些是创作者真正想锁定的锚点,哪些只是随口补充的描述”。 + +结果就是: + +**输入端自由,但信息信号不稳定;AI 虽然能生成很多内容,却不一定生成的是创作者真正关心的内容。** + +## 1.3.2 创作者与 AI 的职责发生倒置 + +当前流程实际上是: + +- 创作者先写一段泛化设定 +- AI 再把整个世界铺满 +- 创作者最后回到结果页,人工修改大量角色、章节、技能、初始物品、场景连接等细节 + +这与“低创作门槛、高创作自由度”的目标相反。 + +因为真正应该由创作者控制的,是: + +- 世界核心命题 +- 主题与气质 +- 玩家视角 +- 核心冲突 +- 关键角色 +- 关键地点 +- 标志性物件 / 怪物 / 禁忌 + +而不是让创作者在结果页里逐个补: + +- `backstoryReveal.chapters` +- `skills` +- `initialItems` +- `sceneNpcIds` +- `connections` +- 各类长尾角色与场景的细枝末节 + +## 1.3.3 关键内容与长尾内容没有分层 + +当前流程会稳定生成: + +- `5` 个可扮演角色 +- `25+` 个场景角色 +- `10+` 个场景 + +问题不在数量本身,而在于系统并没有明确区分: + +1. 哪些是创作者应重点塑造的关键对象 +2. 哪些只是 AI 应自动展开的长尾铺量 + +这会导致两个问题: + +1. AI 在早期就花大量成本生成长尾内容,等待时间长。 +2. 创作者在结果页里面对的是一整套“全部都生成了”的世界,而不是“先抓关键锚点,再决定是否继续铺开”。 + +## 1.3.4 当前结果页暴露了过多低杠杆字段 + +当前结果页和实体编辑器允许编辑的字段过多,而且很多是低创作价值、系统结构化字段: + +- 背景章节的标题、阈值、teaser、content、contextSnippet +- 角色技能与初始物品 +- 场景 NPC 分配 +- 场景连接网络 + +这对“专业创作者”当然有帮助,但对目标用户来说,容易把工具变成: + +**看起来自由度很高,实际上需要承担很多系统编辑工作。** + +## 1.3.5 当前重新生成是“整世界覆盖式”的 + +当前结果页的“重新生成”会清空并重做当前世界的所有信息。 + +这意味着: + +1. 创作者一旦修改过内容,就会担心被覆盖。 +2. 没有“锁定关键内容,只重生成长尾部分”的机制。 +3. AI 无法真正成为创作搭档,只像一次性大批量生成器。 + +## 1.3.6 当前生成阶段是“模型视角”,不是“创作者视角” + +当前生成页展示的是系统批次和阶段进度,这很好,但它主要回答的是: + +- 现在模型在跑哪一步 + +没有回答的是: + +- 创作者最关心的关键角色是否已经成型 +- 世界冲突是否已经稳定 +- 当前这轮已经锁定了哪些核心创意 +- 接下来生成的是关键锚点,还是长尾内容 + +也就是说: + +**当前链路的进度可见了,但创作过程仍然没有真正可见。** + +## 2. 本次优化的设计目标 + +这次优化要同时满足 6 个目标: + +1. 降低输入门槛 + - 不要求创作者一上来写长文,不要求理解系统字段。 + +2. 提高高杠杆创作自由度 + - 让创作者直接控制世界灵魂锚点,而不是低价值细节。 + +3. 明确创作者与 AI 的职责边界 + - 创作者负责“决定什么值得创作”,AI 负责“把它展开并跑起来”。 + +4. 保留现有分阶段生成骨架 + - 不推翻 `framework -> themePack -> storyGraph -> role/landmark` 的已有结构。 + +5. 引入锁定与局部重生成 + - 让创作者能保住自己在乎的内容,只重做其余部分。 + +6. 把结果页从“数据总表”升级成“创作工作台” + - 让编辑界面按创作价值组织,而不是按底层对象堆字段。 + +## 3. 核心产品结论 + +优化后的自定义世界流程应该改为: + +```text +创作者输入世界锚点 +-> AI 编译创作者意图摘要 +-> 创作者确认 / 锁定关键锚点 +-> AI 先生成关键角色与关键地点 +-> 创作者可局部修改 / 局部重生成 +-> AI 再展开长尾 NPC、长尾场景与运行时编译结构 +-> 结果页以“锚点 / 关键对象 / 扩展内容 / 运行时摘要”方式组织 +-> 保存并进入世界 +``` + +一句话: + +**先做创作决策,再做内容展开;先做关键对象,再做长尾铺量;先让创作者锁定灵魂,再让 AI 扩散世界。** + +## 4. 输入层优化方案 + +## 4.1 把“单一大文本”升级为“创作卡片” + +新的输入层不应只有一块大 textarea,而应拆成卡片式输入。 + +建议至少包含 9 张卡: + +1. 世界一句话 +2. 主题与气质 +3. 玩家是谁 +4. 核心冲突 +5. 关键势力 +6. 关键角色 +7. 关键地点 +8. 标志性要素 +9. 禁止事项 + +每张卡都支持: + +1. 一句话输入 +2. 标签 / 选项辅助 +3. 高级补充展开 + +这样做的目的不是让用户填更多,而是让: + +- 不会写长文的人,也能靠卡片完成创作输入 +- 愿意深挖的人,也有明确位置继续提升质量 + +## 4.2 保留“自由输入模式”,但不再只靠它 + +当前的 `settingText` 不应被废弃,而应保留为: + +1. 快速模式 +2. 导入模式 +3. 兜底模式 + +系统应支持两种输入入口: + +1. 快速文本模式 + - 用户只写一段设定,系统自动拆分成创作卡片建议 + +2. 卡片模式 + - 用户直接按结构化方式输入世界锚点 + +两种模式最终都编译成统一的创作者意图对象。 + +## 4.3 必填与选填要分开 + +不应要求用户每一张卡都填完。 + +建议: + +- 必填: + - 世界一句话 + - 主题与气质 + - 玩家是谁 + - 核心冲突 + +- 选填: + - 关键势力 + - 关键角色 + - 关键地点 + - 标志性要素 + - 禁止事项 + +这样既能保证世界最小成型,又不会把创作者门槛抬高。 + +## 4.4 明确支持“锁定” + +每张卡片、每个关键角色、每个关键地点都应支持锁定。 + +锁定后: + +1. AI 不得在重生成时覆盖该内容 +2. 长尾内容只能围绕它展开 +3. 结果页里应明确显示其为“创作者锚点” + +## 5. 生成链路优化方案 + +## 5.1 新增“创作者意图编译层” + +在真正开始世界生成前,先新增一个轻量阶段: + +**Creator Intent Compile** + +输入: + +- 文本模式输入 +- 或卡片模式输入 + +输出: + +- 创作者意图摘要 +- 世界锚点摘要 +- 系统识别出的关键角色 / 冲突 / 地点 / 禁忌 + +这一步的作用不是生成世界,而是先回答: + +1. 系统理解到的世界核心是什么 +2. 哪些内容将被视为创作者强锚点 +3. 哪些内容将交给 AI 扩展 + +## 5.2 把当前生成链改成“关键先行、长尾后补” + +当前 `generateCustomWorldProfile(...)` 的分阶段结构可以保留,但生成顺序需要更创作者化。 + +建议改成 5 层: + +### 第一层:世界锚点层 + +先生成: + +- 世界框架 +- ThemePack +- StoryGraph 的基础版 +- 创作者锚点摘要 + +这一层完成后,系统应能让创作者看到: + +- 世界现在到底被理解成了什么 +- 哪些冲突 / 势力 / 意象被识别出来了 + +### 第二层:关键对象层 + +优先生成: + +- 关键可扮演角色 +- 关键场景角色 +- 关键地点 + +这一层优先围绕创作者明确输入的角色和地点,而不是先铺满全部数量。 + +### 第三层:创作者校对层 + +在继续展开长尾内容前,应允许创作者做一次轻量校对: + +- 确认关键角色是否对 +- 确认关键地点是否对 +- 锁定已经满意的内容 +- 对不满意的关键对象做局部重生成 + +### 第四层:长尾展开层 + +在关键层稳定后,再生成: + +- 普通场景角色 +- 路人 / 杂兵 / 补位 NPC +- 次级地点 +- 扩展连接关系 +- 长尾命名和描述 + +### 第五层:运行时编译层 + +最后再编译: + +- `themePack` +- `storyGraph` +- `knowledgeFacts` +- `threadContracts` +- `narrativeProfile` +- 各类运行时支持结构 + +## 5.3 支持“快速开局生成”和“完整展开生成” + +建议提供两种生成策略: + +1. 快速开局 + - 先完成世界锚点 + 关键对象 + 最小可玩档案 + - 用户可以更快看到世界雏形 + +2. 完整展开 + - 一次性继续补齐长尾场景角色、地标和完整世界网络 + +这样做的价值很高: + +1. 降低首次等待焦虑 +2. 让创作者更早介入关键对象校正 +3. 避免系统在创作方向还没稳定前,先铺满大量长尾内容 + +## 5.4 角色与场景生成要改成“锚点优先 + 长尾补位” + +当前角色与场景的生成方式更像“按配额批量产出”。 + +优化后应改为: + +1. 先生成创作者明确指定的关键角色 / 地点 +2. 再根据世界冲突自动补位缺失的角色原型和场景功能位 +3. 最后再铺长尾 + +这样生成出来的世界会更像“围绕创作者意图长出来”,而不是“先生成了一个完整世界,再让创作者去认领” + +## 6. 结果页与编辑工作台优化方案 + +## 6.1 结果页不应再按“底层对象目录”组织为主 + +当前结果页主要按: + +- 世界 +- 可扮演角色 +- 场景角色 +- 场景 + +来组织。 + +优化后建议改成 4 层工作台: + +1. 创作锚点 + - 展示创作者输入和锁定内容 + +2. 关键对象 + - 关键角色、关键地点、关键冲突对象 + +3. 扩展内容 + - AI 自动展开的长尾角色、长尾地点、补位内容 + +4. 世界编译摘要 + - 展示世界线程、题材包、运行时摘要,但默认不要求创作者编辑 + +## 6.2 编辑界面应遵守“高价值字段前置,低价值字段折叠” + +对创作者默认暴露的应是: + +- 角色一句话定位 +- 角色表面面貌 +- 角色真正想要什么 +- 角色与谁有关 +- 地点为什么重要 +- 地点承载什么冲突 +- 世界哪些规则不能动 + +不应默认把下面这些字段摆在第一屏: + +- `backstoryReveal.chapters` +- `skills` +- `initialItems` +- `contextSnippet` +- `sceneNpcIds` +- `connections` + +这些应被系统下沉到: + +- 自动生成 +- 高级模式 +- 或局部展开区域 + +## 6.3 新增局部重生成 + +结果页必须支持至少 4 种局部重生成: + +1. 仅重生成某个关键角色 +2. 仅重生成某个关键地点 +3. 仅重生成长尾场景角色 +4. 仅重生成场景网络 / 长尾连接 + +并且所有局部重生成都必须: + +- 尊重锁定内容 +- 显示影响范围 +- 避免全局覆盖 + +## 6.4 新增“AI 建议修订”而非“只允许手改” + +结果页不应只提供人工编辑,还应提供: + +- 让 AI 重写一句话定位 +- 让 AI 补强冲突张力 +- 让 AI 改得更克制 / 更黑暗 / 更传奇 / 更现实 +- 让 AI 只围绕已锁定内容变体 + +这样才能真正体现“AI 是创作辅助,而不是一次性生成器”。 + +## 7. 数据结构建议 + +## 7.1 新增 `CustomWorldCreatorIntent` + +建议新增创作者输入的统一结构: + +```ts +interface CustomWorldCreatorIntent { + sourceMode: 'freeform' | 'card'; + rawSettingText: string; + worldHook: string; + themeKeywords: string[]; + toneDirectives: string[]; + playerPremise: string; + openingSituation: string; + coreConflicts: string[]; + keyFactions: CreatorFactionSeed[]; + keyCharacters: CreatorCharacterSeed[]; + keyLandmarks: CreatorLandmarkSeed[]; + iconicElements: string[]; + forbiddenDirectives: string[]; +} +``` + +作用: + +- 把“创作者真正输入了什么”从最终 `CustomWorldProfile` 中分离出来 + +## 7.2 新增 `CustomWorldAnchorPack` + +建议新增系统编译后的锚点包: + +```ts +interface CustomWorldAnchorPack { + worldSummary: string; + creatorIntentSummary: string; + lockedAnchorIds: string[]; + keyConflictSummaries: string[]; + keyFactionSummaries: string[]; + keyCharacterAnchors: ActorAnchor[]; + keyLandmarkAnchors: LandmarkAnchor[]; + motifDirectives: string[]; +} +``` + +作用: + +- 让后续长尾扩展、局部重生成、结果页工作台都围绕同一套“创作锚点”工作 + +## 7.3 新增 `CustomWorldLockState` + +```ts +interface CustomWorldLockState { + worldLockedFields: string[]; + lockedCharacterIds: string[]; + lockedLandmarkIds: string[]; + lockedConflictIds: string[]; + lockedFactionIds: string[]; +} +``` + +作用: + +- 明确哪些内容 AI 不能重写 + +## 7.4 新增 `CustomWorldGenerationDraft` + +```ts +interface CustomWorldGenerationDraft { + creatorIntent: CustomWorldCreatorIntent; + anchorPack: CustomWorldAnchorPack | null; + profile: CustomWorldProfile | null; + lockState: CustomWorldLockState; + generationMode: 'fast' | 'full'; + generationStatus: 'idle' | 'compiling' | 'generating_key' | 'generating_long_tail' | 'ready'; +} +``` + +作用: + +- 让“创作者输入、AI 编译、结果编辑”成为连续工作流,而不是只有最终成品对象 + +## 8. 与当前仓库的接入建议 + +## 8.1 输入层 + +优先改: + +- `src/components/SelectionCustomizationModals.tsx` +- `src/components/game-shell/PreGameSelectionFlow.tsx` + +目标: + +- 把单 textarea 升级为“快速模式 + 卡片模式” +- 新增创作者意图状态 +- 新增锁定和局部重生成入口 + +## 8.2 prompt 与生成服务层 + +优先改: + +- `src/services/customWorld.ts` +- `src/services/ai.ts` + +目标: + +- 新增 Creator Intent Compile prompt +- 新增 Anchor Pack compile prompt +- 新增局部重生成 prompt +- 保留现有 `framework / themePack / storyGraph / role / landmark` 生成骨架 + +## 8.3 Builder 与结构层 + +优先改: + +- `src/services/customWorldBuilder.ts` +- `src/types/customWorld.ts` + +目标: + +- 为 `CustomWorldProfile` 增加创作者意图与锚点相关扩展字段 +- 保持旧档兼容 +- 让现有 builder 能同时消费 `creatorIntent + anchorPack + profile seed` + +## 8.4 结果页与编辑器层 + +优先改: + +- `src/components/CustomWorldResultView.tsx` +- `src/components/CustomWorldEntityCatalog.tsx` +- `src/components/CustomWorldEntityEditorModal.tsx` + +目标: + +- 结果页由“数据目录”转为“创作工作台” +- 支持局部重生成 +- 高价值字段前置,低价值字段折叠 +- 允许 AI 辅助改写,而不只是手工编辑 + +## 9. 明确不做什么 + +本次优化不做以下事情: + +1. 不推翻当前自定义世界最终输出仍是 `CustomWorldProfile` 的兼容目标 +2. 不把所有运行时结构都暴露给创作者直接编辑 +3. 不要求创作者理解 `themePack / storyGraph / knowledgeFacts / threadContracts` 等系统结构 +4. 不把复杂数值平衡、掉落预算、build 预算转移给创作者 +5. 不把“高自由度”理解成“所有字段都手工可改” + +## 10. 验收标准 + +做到以下几点,才算这次优化真正成立: + +1. 创作者可以不用写长文,只靠卡片输入也能完成自定义世界创建。 +2. 系统会明确区分“创作者锚点”和“AI 自动展开内容”。 +3. 创作者不再需要默认手改大量 `skills / initialItems / backstoryReveal / scene connections` 才能得到可用世界。 +4. 结果页支持锁定关键角色、关键地点、关键冲突,并支持局部重生成。 +5. 重新生成不再默认覆盖整个世界。 +6. 当前 `framework -> themePack -> storyGraph -> role/landmark` 生成主链可以继续复用,而不是被废弃。 +7. 结果页默认展示的是高创作价值对象,而不是系统级低层字段。 +8. 长尾内容生成明显后置于关键对象生成,创作者能更早看到并修正关键对象。 +9. 旧的自由文本输入模式仍然可用,但不再是唯一入口。 + +## 11. 推荐落地顺序 + +## 阶段 A:先加创作者意图层 + +先做: + +- `CustomWorldCreatorIntent` +- 输入卡片 UI +- 快速文本 -> 卡片建议编译 + +目标: + +- 先把创作者输入从“单一大文本”升级成“可识别的创作锚点” + +## 阶段 B:再加锚点包与锁定能力 + +先做: + +- `CustomWorldAnchorPack` +- `CustomWorldLockState` +- 锁定 UI + +目标: + +- 让后续生成真正知道“哪些不能动” + +## 阶段 C:再改生成顺序 + +先做: + +- 关键对象优先 +- 长尾内容后置 +- 快速开局模式 + +目标: + +- 缩短“看到关键结果”的等待时间 + +## 阶段 D:最后重做结果页工作台 + +先做: + +- 锚点页 +- 关键对象页 +- 扩展内容页 +- 局部重生成 + +目标: + +- 让结果页真正成为创作工具,而不是人工补表页面 + +## 12. 一句话结论 + +当前自定义世界流程最需要优化的,不是“让 AI 再多生成一点内容”,而是: + +**把创作者从低价值字段编辑里解放出来,让创作者负责世界灵魂锚点,让 AI 负责围绕这些锚点分层生成、分层展开、分层可控地把世界长出来。** diff --git a/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE1_IMPLEMENTATION_PLAN_2026-04-06.md b/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE1_IMPLEMENTATION_PLAN_2026-04-06.md new file mode 100644 index 00000000..c36fa5ef --- /dev/null +++ b/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE1_IMPLEMENTATION_PLAN_2026-04-06.md @@ -0,0 +1,869 @@ +# AI 原生剧情引擎第一阶段技术落地方案 + +更新时间:`2026-04-06` + +## 0. 文档目的 + +这份方案用于把以下三份 PRD 收束成当前仓库可直接开工的第一阶段技术实现方案: + +- `docs/prd/AI_NATIVE_CROSS_GENRE_STORY_ENGINE_PRD_2026-04-06.md` +- `docs/prd/AI_NATIVE_NARRATIVE_THREAD_ITEM_AND_WORLD_NPC_PRD_2026-04-06.md` +- `docs/prd/AI_NATIVE_CLASSIC_RPG_EXPERIENCE_BENCHMARK_PRD_2026-04-06.md` + +目标不是一次把“经典 RPG 体验”全部做完,而是在当前项目框架内先做出一个最小但完整的剧情引擎闭环,让后续可以继续往《仙剑》《轩辕剑》《古剑》《黑神话》《博德之门》所代表的体验能力上叠。 + +这份方案必须满足两个约束: + +1. 不推翻当前仓库已有的 `customWorld / prompt / npcInteractions / questDirector / runtimeItem` 主链。 +2. 第一阶段做完后,玩家能立刻感知到内容质量提升,而不是只有内部结构更“先进”。 + +## 1. 第一阶段目标 + +第一阶段只做 4 件必须一起成立的事: + +1. 给自定义世界补 `题材适配层 + 世界线程图谱 + 角色叙事档案` +2. 给运行时 prompt 补 `信息可见性裁剪 + 情境导演最小指令` +3. 给重点 NPC 补 `低好感也有戏` 的首遇表达与最小关系立场矩阵 +4. 给重点运行时物件补 `叙事指纹`,让名称、描述、来源真正开始承载故事 + +一句话定义第一阶段: + +**先把“世界会说话、角色会藏话、物件会带话、prompt 不再全知”这四个基础能力接起来。** + +## 2. 第一阶段完成定义 + +第一阶段完成后,必须同时满足下面这些结果: + +1. 新生成的自定义世界,结构里会稳定带上 `ThemePack`、`WorldStoryGraph` 和 `ActorNarrativeProfile`。 +2. 自定义世界 NPC 在首遇或低披露阶段,不再把完整 `backstory` 与所有未解锁章节直接注入 prompt。 +3. 低初始好感 NPC 的首轮文本,会出现“当前压力 + 错位说辞 + 暗线钩子”,而不只是更冷淡。 +4. 稀有以上或重点奖励物件,会带 `storyFingerprint`,名称与描述不再只是 build 方向模板句。 +5. 旧存档和旧自定义世界数据仍能平滑读取,缺失字段有 fallback。 +6. 至少一条主链已经把“世界线程 -> 角色档案 -> 可见信息 -> 文本生成 -> 记忆回写”跑通。 + +## 3. 范围控制 + +## 3.1 第一阶段纳入范围 + +纳入范围的模块: + +- `src/services/customWorld.ts` +- `src/services/prompt.ts` +- `src/data/npcInteractions.ts` +- `src/hooks/useStoryGeneration.ts` +- `src/services/questDirector.ts` +- `src/services/runtimeItemAiPrompt.ts` +- `src/data/runtimeItemNarrative.ts` +- `src/types/customWorld.ts` +- `src/types/game.ts` +- `src/types/scene.ts` +- `src/types/runtimeItem.ts` +- `src/services/aiTypes.ts` + +新增模块: + +- `src/types/storyEngine.ts` +- `src/services/storyEngine/themePack.ts` +- `src/services/storyEngine/worldStoryGraph.ts` +- `src/services/storyEngine/actorNarrativeProfile.ts` +- `src/services/storyEngine/visibilityEngine.ts` +- `src/services/storyEngine/sceneNarrativeDirector.ts` +- `src/services/storyEngine/carrierNarrativeCompiler.ts` + +## 3.2 第一阶段明确不做 + +以下内容留到第二阶段及以后,不放进本阶段: + +1. 全量营地事件系统 +2. 队友 approval / disapproval 的完整 UI 面板 +3. 复杂多结局主线编排器 +4. 全题材内容包与人工词典大规模沉淀 +5. 所有叙事载体类型一次性接入 +6. 全支线线程化改造 +7. 复杂 scene residue 地图化编辑器 + +原因很简单: + +**第一阶段的目标是把底层语法和最小闭环接通,而不是把所有经典 RPG 能力一次铺满。** + +## 4. 第一阶段最小闭环 + +建议把第一阶段的最小闭环定义为: + +```text +自定义世界生成 +-> 题材适配层 +-> 世界线程图谱 +-> 角色叙事档案 +-> 遭遇时构造可见性切片 +-> 构造场景导演指令 +-> 生成首遇 / 对话 / 任务 / 物件叙事 +-> 回写最小剧情记忆 +``` + +这个闭环里,先只强接两个高价值落点: + +1. 大世界 NPC 遭遇 +2. 运行时重点物件 + +原因: + +- 这两条链最容易让玩家立刻感到“内容变得像经典 RPG 了” +- 也最符合当前仓库已经有的主链结构 + +## 5. 数据结构落地方案 + +## 5.1 新增 `src/types/storyEngine.ts` + +建议把第一阶段的引擎基础类型集中到一个新文件,避免继续把新语义散进已有 `story.ts`。 + +建议包含这些结构: + +```ts +export interface ThemePack { + id: string; + displayName: string; + toneRange: string[]; + institutionLexicon: string[]; + tabooLexicon: string[]; + artifactClasses: string[]; + actorArchetypes: string[]; + conflictForms: string[]; + clueForms: string[]; + namingPatterns: string[]; + revealStyles: string[]; +} + +export interface StoryThread { + id: string; + title: string; + visibility: 'visible' | 'hidden'; + summary: string; + conflictType: string; + stakes: string; + involvedFactionIds: string[]; + involvedActorIds: string[]; + relatedLocationIds: string[]; +} + +export interface StoryScar { + id: string; + title: string; + pastEvent: string; + publicResidue: string; + hiddenTruth: string; + relatedActorIds: string[]; + relatedLocationIds: string[]; +} + +export interface StoryMotif { + id: string; + label: string; + semanticRole: string; + lexicalHints: string[]; +} + +export interface WorldStoryGraph { + visibleThreads: StoryThread[]; + hiddenThreads: StoryThread[]; + scars: StoryScar[]; + motifs: StoryMotif[]; +} + +export interface ActorNarrativeProfile { + publicMask: string; + firstContactMask: string; + visibleLine: string; + hiddenLine: string; + contradiction: string; + debtOrBurden: string; + taboo: string; + immediatePressure: string; + relatedThreadIds: string[]; + relatedScarIds: string[]; + reactionHooks: string[]; +} + +export interface VisibilitySlice { + factIds: string[]; + sayableFactIds: string[]; + inferredFactIds: string[]; + forbiddenFactIds: string[]; + misdirectionHints: string[]; +} + +export interface SceneNarrativeDirective { + primaryPressure: string; + activeThreadIds: string[]; + foregroundActorIds: string[]; + foregroundCarrierIds: string[]; + revealBudget: 'low' | 'medium' | 'high'; + emotionalCadence: 'tense' | 'curious' | 'hostile' | 'intimate' | 'tragic' | 'mysterious'; +} + +export interface CarrierStoryFingerprint { + visibleClue: string; + witnessMark: string; + unresolvedQuestion: string; + currentAppearanceReason: string; + relatedThreadIds: string[]; + relatedScarIds: string[]; + reactionHooks: string[]; +} + +export interface CompanionStanceProfile { + trust: number; + warmth: number; + ideologicalFit: number; + fearOrGuard: number; + loyalty: number; + currentConflictTag?: string | null; + recentApprovals: string[]; + recentDisapprovals: string[]; +} + +export interface StoryEngineMemoryState { + discoveredFactIds: string[]; + activeThreadIds: string[]; + resolvedScarIds: string[]; + recentCarrierIds: string[]; +} +``` + +同时更新: + +- `src/types.ts` + +把 `storyEngine.ts` 导出。 + +## 5.2 扩展 `src/types/customWorld.ts` + +扩展: + +```ts +interface CustomWorldRoleProfile { + narrativeProfile?: ActorNarrativeProfile; +} + +interface CustomWorldProfile { + themePack?: ThemePack | null; + storyGraph?: WorldStoryGraph | null; +} +``` + +注意: + +1. 这两个字段都要允许 `null` / 缺失 +2. `normalizeCustomWorldProfile(...)` 必须能从旧档案平滑补 fallback + +## 5.3 扩展 `src/types/runtimeItem.ts` + +扩展: + +```ts +interface RuntimeItemMetadata { + storyFingerprint?: CarrierStoryFingerprint; +} +``` + +同时扩展 AI 意图: + +```ts +interface RuntimeItemAiIntent { + visibleClue?: string; + witnessMark?: string; + unfinishedBusiness?: string; + hiddenHook?: string; + reactionHooks?: string[]; + namingPattern?: string; +} +``` + +## 5.4 扩展 `src/types/game.ts` + +扩展: + +```ts +interface GameState { + storyEngineMemory?: StoryEngineMemoryState; +} +``` + +用途: + +1. 记录已发现事实 +2. 记录当前激活线程 +3. 记录已经回响过的旧伤 +4. 记录最近拿到的重点叙事载体 + +## 5.5 扩展 `src/types/scene.ts` + +扩展: + +```ts +interface NpcPersistentState { + stanceProfile?: CompanionStanceProfile; +} + +interface Encounter { + narrativeProfile?: ActorNarrativeProfile; +} + +interface SceneNpc { + narrativeProfile?: ActorNarrativeProfile; +} +``` + +注意: + +- 这里的 `stanceProfile` 不只给已入队同伴使用,重点 NPC 也可以先共享这套结构 + +## 5.6 扩展 `src/services/aiTypes.ts` + +给 `StoryGenerationContext` 增加: + +```ts +visibilitySlice?: VisibilitySlice | null; +sceneNarrativeDirective?: SceneNarrativeDirective | null; +encounterNarrativeProfile?: ActorNarrativeProfile | null; +activeThreadIds?: string[] | null; +``` + +给 `QuestGenerationContext` 增加: + +```ts +activeThreadIds?: string[] | null; +issuerNarrativeProfile?: ActorNarrativeProfile | null; +``` + +## 6. 模块实现方案 + +## 6.1 `src/services/storyEngine/themePack.ts` + +职责: + +1. 根据 `customWorldProfile.templateWorldType`、`summary`、`tone` 生成题材适配层 +2. 如果是预设世界,也能提供内置默认 `ThemePack` +3. 给后续命名、术语、禁忌词、物件类别提供统一词汇入口 + +建议导出: + +```ts +buildThemePackFromWorldProfile(profile) +resolveFallbackThemePack(worldType) +``` + +本阶段要求: + +- 先做 deterministic builder,不强依赖额外 LLM 调用 + +## 6.2 `src/services/storyEngine/worldStoryGraph.ts` + +职责: + +1. 根据 `CustomWorldProfile` 现有字段生成或补全 `WorldStoryGraph` +2. 先从 `majorFactions / coreConflicts / landmarks / storyNpcs` 抽图谱 +3. 缺失时允许走 LLM 辅助生成,但要有 deterministic fallback + +建议导出: + +```ts +buildFallbackWorldStoryGraph(profile, themePack) +generateWorldStoryGraphWithAi(profile, themePack) +normalizeWorldStoryGraph(value, fallback) +``` + +本阶段要求: + +- 默认必须可离线 fallback +- 图谱生成失败不能阻塞整个自定义世界生成 + +## 6.3 `src/services/storyEngine/actorNarrativeProfile.ts` + +职责: + +1. 从角色的 `description / backstory / motivation / relationshipHooks / tags / backstoryReveal` + 编译 `ActorNarrativeProfile` +2. 缺失时给出 fallback +3. 为 prompt 提供稳定的“首遇面具、当前压力、暗线钩子” + +建议导出: + +```ts +buildFallbackActorNarrativeProfile(role, graph) +generateActorNarrativeProfileWithAi(role, graph, themePack) +normalizeActorNarrativeProfile(value, fallback) +``` + +## 6.4 `src/services/storyEngine/visibilityEngine.ts` + +职责: + +1. 根据当前遭遇、好感、首遇状态、已解锁章节、故事记忆构造 `VisibilitySlice` +2. 明确哪些事实: + - 可以进入 prompt + - 只能作为推测 + - 绝对不能进入本轮上下文 + +建议导出: + +```ts +buildEncounterVisibilitySlice(params) +buildQuestVisibilitySlice(params) +buildCarrierVisibilitySlice(params) +``` + +本阶段关键约束: + +1. 自定义世界 NPC 首遇时禁止注入完整 `backstory` +2. 未解锁章节禁止注入 `content` +3. 低披露阶段允许注入: + - `publicMask` + - `firstContactMask` + - `visibleLine` + - `immediatePressure` + - 已解锁章节的 `contextSnippet` + +## 6.5 `src/services/storyEngine/sceneNarrativeDirector.ts` + +职责: + +1. 用当前场景、遭遇、最近行动、激活线程构造 `SceneNarrativeDirective` +2. 告诉 prompt 本轮更应该强调: + - 紧张 + - 试探 + - 情感深化 + - 揭示 + - 悬念 + +建议导出: + +```ts +buildSceneNarrativeDirective(params) +``` + +本阶段先做 local director,不新增额外 LLM 调用。 + +## 6.6 `src/services/storyEngine/carrierNarrativeCompiler.ts` + +职责: + +1. 根据 `WorldStoryGraph + relationAnchor + scene + actorNarrativeProfile` + 编译 `CarrierStoryFingerprint` +2. 给运行时重点物件提供: + - 可见线索 + - 见证痕 + - 未完成问题 + - 当前出现理由 + - 后续反应钩子 + +建议导出: + +```ts +buildRuntimeItemStoryFingerprint(params) +buildCarrierNarrativeName(params) +buildCarrierNarrativeDescription(params) +``` + +## 7. 现有文件改造方案 + +## 7.1 `src/services/customWorld.ts` + +当前问题: + +1. 角色生成偏设定卡 +2. 没有先产题材适配层与世界线程图谱 +3. 角色背景和章节没有统一挂到世界线程上 + +改造方案: + +1. 保留现有多阶段生成结构 +2. 插入两个新阶段: + - `theme_pack_and_story_graph` + - `actor_narrative_profile` +3. 最终输出时把: + - `themePack` + - `storyGraph` + - 每个 NPC 的 `narrativeProfile` + 写回 `CustomWorldProfile` + +建议新的顺序: + +1. 世界基础框架 +2. ThemePack +3. WorldStoryGraph +4. 角色基础字段 +5. ActorNarrativeProfile +6. backstoryReveal / skills / initialItems + +## 7.2 `src/services/prompt.ts` + +这是第一阶段必须重点改的文件。 + +当前问题已经确认: + +1. 自定义世界 NPC 遭遇会直接注入完整 `backstory` +2. 会直接注入所有章节摘要 +3. `describeCustomWorldSection(...)` 会整体塞入太多 NPC 全量信息 + +改造方案: + +1. 所有自定义世界 NPC prompt 统一先经过 `buildEncounterVisibilitySlice(...)` +2. 只允许读取 `visibilitySlice` 输出的事实 +3. `describeCustomWorldSection(...)` 改成只注入: + - 世界摘要 + - ThemePack 摘要 + - StoryGraph 激活线程摘要 + - 当前相关 NPC 的公开面与线程索引 + +禁止继续注入: + +1. 完整 `backstory` +2. 未解锁 `backstoryReveal.content` +3. 多角色全量技能、初始物品、完整背景 + +## 7.3 `src/data/npcInteractions.ts` + +改造目标: + +1. 低好感角色从“更冷”改成“更有压力和错位” +2. 接入最小 `stanceProfile` + +建议新增: + +```ts +buildInitialStanceProfile(...) +applyStoryChoiceToStanceProfile(...) +describeNpcNarrativePressure(...) +``` + +本阶段只接这些行为的 stance 更新: + +1. `npc_chat` +2. `npc_help` +3. `npc_gift` +4. `npc_recruit` +5. `npc_quest_accept` + +先不做完整 approval UI,只做: + +- 内部状态更新 +- prompt 注入差异 +- 文本反应差异 + +## 7.4 `src/hooks/useStoryGeneration.ts` + +当前不要再往里塞更多内容逻辑,而是让它接新引擎组件。 + +改造方式: + +1. 构造 `visibilitySlice` +2. 构造 `sceneNarrativeDirective` +3. 把这两者透传给 `generateInitialStory / generateNextStep` +4. 在回合结束时回写: + - `storyEngineMemory` + - `revealedFactIds` + - 最近激活线程 + +它在第一阶段里只做 orchestration,不承担新的叙事规则实现。 + +## 7.5 `src/services/questDirector.ts` + +当前它已经有: + +- AI intent +- fallback builder +- compile to quest + +第一阶段只补最小 thread awareness,不把整个任务系统完全重做。 + +改造方式: + +1. `QuestGenerationContext` 增加: + - `activeThreadIds` + - `issuerNarrativeProfile` +2. `buildQuestIntentPrompt(...)` 增加: + - 当前激活线程 + - NPC 当前压力 + - 可披露线索 + +本阶段不改: + +- `QuestLogEntry` 主结构 +- `questFlow.ts` 主编译方式 + +## 7.6 `src/services/runtimeItemAiPrompt.ts` + +扩展当前物品意图 contract,但不让它直接生成成品。 + +新增字段: + +1. `visibleClue` +2. `witnessMark` +3. `unfinishedBusiness` +4. `hiddenHook` +5. `reactionHooks` +6. `namingPattern` + +要求模型回答: + +1. 这件物到底见证过什么 +2. 它为什么现在出现 +3. 谁以后会对它起反应 + +## 7.7 `src/data/runtimeItemNarrative.ts` + +这是第一阶段另一个必须重点改的文件。 + +当前问题: + +1. 名称偏词块拼接 +2. 描述偏单模板句 + +改造方案: + +1. 先从 `CarrierStoryFingerprint` 编译名称 +2. 描述固定升级成三层式: + - 表面痕迹 + - 旧事牵连 + - 当前局势意义 +3. `sourceReason` 继续保留,但不再是描述的全部中心 + +## 8. Prompt Contract 方案 + +## 8.1 自定义世界生成新增 contract + +### 1. ThemePack Contract + +输入: + +- 世界 summary +- tone +- templateWorldType +- factions +- coreConflicts + +输出: + +- 题材词汇 +- 冲突形式 +- 载体种类 +- 命名范式 + +### 2. WorldStoryGraph Contract + +输入: + +- 世界摘要 +- major factions +- core conflicts +- landmarks +- story NPC 简要清单 + +输出: + +- `visibleThreads` +- `hiddenThreads` +- `scars` +- `motifs` + +### 3. ActorNarrativeProfile Contract + +输入: + +- 角色基础字段 +- 角色 `backstoryReveal` +- 所属世界线程摘要 + +输出: + +- `publicMask` +- `firstContactMask` +- `visibleLine` +- `hiddenLine` +- `contradiction` +- `debtOrBurden` +- `taboo` +- `immediatePressure` +- `reactionHooks` + +## 8.2 运行时剧情生成 contract + +运行时主 prompt 不再直接吃全量世界设定,而是吃: + +1. `SceneNarrativeDirective` +2. `VisibilitySlice` +3. 当前遭遇公开信息 +4. 已解锁章节摘要 +5. 当前激活线程 + +## 8.3 运行时物件 contract + +输入: + +- 生成渠道 +- 关系锚点 +- 当前场景 +- 当前激活线程 +- 相关角色公开面与当前压力 + +输出: + +- 物件叙事指纹字段 +- 命名范式建议 +- build 倾向字段 + +## 9. 迁移与兼容方案 + +## 9.1 旧自定义世界兼容 + +旧数据可能没有: + +- `themePack` +- `storyGraph` +- `narrativeProfile` + +策略: + +1. 读取时用 `normalizeCustomWorldProfile(...)` 自动补 fallback +2. fallback 不阻塞运行时 +3. 下次保存时按新结构写回 + +## 9.2 旧存档兼容 + +旧存档可能没有: + +- `storyEngineMemory` +- `stanceProfile` +- `storyFingerprint` + +策略: + +1. `GameState` 初始化时补空结构 +2. `NpcPersistentState` 初始化时补最小 stance +3. 旧物品没有 `storyFingerprint` 时,不报错,走旧描述逻辑 fallback + +## 10. 测试方案 + +## 10.1 单元测试 + +建议新增: + +1. `visibilityEngine.test.ts` + - 验证首遇和低披露阶段不会泄露完整背景 + +2. `worldStoryGraph.test.ts` + - 验证 fallback 图谱最少字段齐全 + +3. `actorNarrativeProfile.test.ts` + - 验证 fallback profile 至少包含压力、错位、钩子 + +4. `carrierNarrativeCompiler.test.ts` + - 验证重点物件能稳定产出指纹、名称和三层描述 + +## 10.2 集成测试 + +建议补这些集成检查: + +1. 自定义世界生成后,`CustomWorldProfile.storyGraph` 与 `narrativeProfile` 存在 +2. `prompt.ts` 在首遇自定义世界 NPC 时不含完整 `backstory` +3. `runtimeItemNarrative.ts` 重点物件输出包含故事指纹与升级描述 +4. `questDirector.ts` 任务上下文已经能读取线程与角色叙事档案 + +## 10.3 手工验证场景 + +至少做这 4 条人工回归: + +1. 初见低好感 NPC +2. 初见中高好感 NPC +3. 拿到 rare 级重点物件 +4. 从某 NPC 处接到调查或关系类任务 + +## 11. 交付顺序 + +建议按下面顺序做,避免返工: + +1. 类型与 fallback + - `storyEngine.ts` + - 现有类型扩展 + - 旧档兼容 + +2. 自定义世界生成链 + - `themePack` + - `worldStoryGraph` + - `actorNarrativeProfile` + +3. prompt 可见性层 + - `visibilityEngine` + - `prompt.ts` 裁剪 + - `useStoryGeneration.ts` 接线 + +4. NPC 首遇和 stance 最小链 + - `npcInteractions.ts` + - `StoryGenerationContext` 注入 + +5. 重点物件叙事链 + - `runtimeItemAiPrompt.ts` + - `carrierNarrativeCompiler.ts` + - `runtimeItemNarrative.ts` + +6. 任务线程感知最小接入 + - `questDirector.ts` + +7. 测试与内容回归 + +## 12. 风险与处理 + +## 风险 1:阶段过大 + +处理: + +- 第一阶段只强接 NPC 与重点物件两条链 +- 营地事件、全支线线程化后置 + +## 风险 2:prompt 体积继续膨胀 + +处理: + +- 先做 `VisibilitySlice` +- 严禁继续直接注入全量背景 + +## 风险 3:旧档与旧内容不兼容 + +处理: + +- 所有新增字段必须有 normalize fallback +- 第一阶段不删除旧字段 + +## 风险 4:AI 输出不稳定 + +处理: + +- 所有新 contract 都必须配 deterministic fallback builder +- AI 只产意图,不直接产运行时成品 + +## 13. 第一阶段验收清单 + +做到以下几点,才算第一阶段可收口: + +1. 新生成自定义世界可稳定带出 `ThemePack + WorldStoryGraph + ActorNarrativeProfile` +2. 首遇自定义世界 NPC 时,prompt 中不再包含完整 `backstory` +3. 低好感 NPC 首轮文本能显著表现“压力、错位、钩子” +4. 稀有以上重点物件会稳定带出 `storyFingerprint` +5. 重点物件名称与描述不再只依赖固定模板句 +6. 任务生成至少能读取激活线程和 NPC 当前叙事压力 +7. 旧档读取正常 +8. `lint / 相关测试 / check:encoding` 通过 + +## 14. 一句话结论 + +第一阶段不要追求“立刻做出所有经典 RPG 体验”,而要先把: + +- 世界线程 +- 角色叙事档案 +- 信息可见性 +- 重点物件叙事指纹 + +这四个底座接进当前仓库主链。 + +只要这一步做稳,后面无论是《仙剑》式角色关系、《轩辕剑》式历史神话、《古剑》式世界厚度、《黑神话》式空间残痕,还是《博德之门》式队友反应,都会开始有一个真正可持续生长的引擎底盘。 diff --git a/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE2_IMPLEMENTATION_PLAN_2026-04-06.md b/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE2_IMPLEMENTATION_PLAN_2026-04-06.md new file mode 100644 index 00000000..196ce07f --- /dev/null +++ b/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE2_IMPLEMENTATION_PLAN_2026-04-06.md @@ -0,0 +1,644 @@ +# AI 原生剧情引擎第二阶段技术落地方案 + +更新时间:`2026-04-06` + +## 0. 文档目的 + +这份方案建立在当前仓库已经基本完成的第一阶段能力之上,面向第二阶段的真实技术推进。 + +第一阶段已经把这些底座接进主链: + +1. `ThemePack` +2. `WorldStoryGraph` +3. `ActorNarrativeProfile` +4. `VisibilitySlice` +5. `SceneNarrativeDirective` +6. `CarrierStoryFingerprint` +7. `storyEngineMemory` + +同时也已经把这些链打通了: + +1. 自定义世界扩展构建链 +2. 自定义世界 NPC 首遇 prompt 裁剪 +3. 运行时重点物件叙事编译 +4. 任务 prompt 的最小线程感知 +5. NPC 最小 stance 更新 + +第二阶段不再解决“底座有没有”,而是解决: + +**这些底座如何从“静态骨架 + deterministic fallback”升级成“真正能持续驱动经典 RPG 体验的动态剧情引擎”。** + +## 1. 第一阶段审计结论 + +先明确当前真实状态,再定义第二阶段范围。 + +## 1.1 已经落地的部分 + +按代码审计结果,以下内容已经可以判定为“已落地”: + +1. 类型层已接入 + - `src/types/storyEngine.ts` + - `src/types/customWorld.ts` + - `src/types/game.ts` + - `src/types/runtimeItem.ts` + - `src/types/scene.ts` + - `src/services/aiTypes.ts` + +2. 自定义世界扩展构建已接入 + - `src/services/customWorldBuilder.ts` + - `src/services/customWorld.ts` + +3. Prompt 可见性裁剪已接入主链 + - `src/services/prompt.ts` + - `src/hooks/useStoryGeneration.ts` + +4. 重点物件叙事指纹已接入主链 + - `src/services/storyEngine/carrierNarrativeCompiler.ts` + - `src/data/runtimeItemNarrative.ts` + +5. 最小记忆回写已接入 + - `src/hooks/story/progressionActions.ts` + - `src/services/storyEngine/echoMemory.ts` + +6. 第一阶段关键测试已通过 + - `prompt.test.ts` + - `customWorldBuilder.test.ts` + - `visibilityEngine.test.ts` + - `carrierNarrativeCompiler.test.ts` + - `runtimeItemDirector.test.ts` + - `npcInteractions.test.ts` + +7. 工程门禁本轮检查通过 + - `lint` + - `typecheck` + - `check:encoding` + +## 1.2 仍然属于“部分落地”的部分 + +以下能力虽然已经有骨架,但还没达到第二阶段想要的经典 RPG 体验强度: + +1. `WorldStoryGraph` + - 当前主要仍是 fallback 构建,AI 版本还是 stub。 + +2. `ActorNarrativeProfile` + - 当前主要仍是 fallback 编译,AI 版本还是 stub。 + +3. `CompanionStanceProfile` + - 当前只覆盖局部 NPC 行为,不覆盖全局选择反馈、队友插话、营地事件。 + +4. `storyEngineMemory` + - 当前已经回写,但消费端还很弱,回响还没有真正反过来改变世界表达。 + +5. `QuestDirector` + - 当前已经有线程感知,但任务本体还没有成为“线程合约”的实体。 + +6. `CarrierStoryFingerprint` + - 当前只强接运行时物件,还没扩到地点残痕、文书、证物、怪物掉落情报、场景调查结果。 + +## 1.3 当前进入第二阶段前,最值得注意的两个真实缺口 + +### 缺口 A:AI 版世界线程 / 角色档案生成仍未真正接管主链 + +当前代码里虽然已经有: + +- `generateWorldStoryGraphWithAi(...)` +- `generateActorNarrativeProfileWithAi(...)` + +但它们还只是 fallback 包装,本质上没有真的调用独立 AI contract。 + +这意味着: + +- 当前世界线程和角色叙事档案仍偏“本地启发式编译” +- 第一阶段解决了“结构没有”的问题 +- 但还没有真正进入“AI 原生世界叙事图谱”阶段 + +### 缺口 B:可见性虽然裁掉了完整背景,但隐式泄露仍未完全收紧 + +当前 `prompt.ts` 里,对自定义世界 NPC 的: + +- `relationshipHooks` +- `tags` + +仍是无条件注入。 + +如果这些字段本身带有暗线名词、组织名、旧案对象名,那么: + +- 首遇阶段仍有隐式越权泄露的风险 + +这说明第二阶段必须把“知识图谱”和“可见性切片”的颗粒度继续做细,不能只停留在章节级裁剪。 + +## 2. 第二阶段目标 + +第二阶段只做 5 件事: + +1. 把世界线程和角色叙事档案从 fallback 编译升级成真正的 AI contract 驱动 +2. 把 `VisibilitySlice` 升级成可追踪的 `KnowledgeFact` 图谱 +3. 把 `CompanionStanceProfile` 从局部 NPC 状态升级成队友反应系统 +4. 把 `QuestDirector` 从“知道线程”升级成“线程合约推进器” +5. 把叙事载体从“重点物件”扩展成“物件 + 地点 + 文书 + 调查残痕” + +一句话定义第二阶段: + +**从“会组织故事骨架”升级到“会持续推进、反馈、回响和分化故事”。** + +## 3. 第二阶段完成定义 + +第二阶段完成后,必须同时满足: + +1. 自定义世界的 `ThemePack / WorldStoryGraph / ActorNarrativeProfile` 至少核心部分由独立 AI contract 生成,而不再只是 fallback。 +2. 世界中的“事实”被显式建模成可发现、可说、可误判、可禁止的 `KnowledgeFact`。 +3. 队友会对关键选择给出稳定的认可 / 反对 / 沉默 / 紧张回避反馈。 +4. 至少一类任务已经真正挂在 `StoryThread` 上推进,并能根据 `signal` 更新阶段。 +5. 玩家获得重点物件、调查地点或读取文书后,后续 NPC 和剧情会产生回响。 +6. “经典 RPG 感”开始明显来自系统联动,而不只是单次文本质量提升。 + +## 4. 第二阶段范围 + +## 4.1 纳入范围 + +- `src/services/customWorld.ts` +- `src/services/customWorldBuilder.ts` +- `src/services/prompt.ts` +- `src/services/questDirector.ts` +- `src/services/questPrompt.ts` +- `src/data/questFlow.ts` +- `src/data/npcInteractions.ts` +- `src/hooks/useStoryGeneration.ts` +- `src/hooks/story/npcEncounterActions.ts` +- `src/hooks/story/npcInteraction.ts` +- `src/services/storyEngine/*` +- `src/types/storyEngine.ts` +- `src/types/story.ts` + +## 4.2 新增模块 + +- `src/services/storyEngine/knowledgeGraph.ts` +- `src/services/storyEngine/knowledgeContract.ts` +- `src/services/storyEngine/companionReactionDirector.ts` +- `src/services/storyEngine/threadContract.ts` +- `src/services/storyEngine/threadSignalRouter.ts` +- `src/services/storyEngine/sceneResidueCompiler.ts` +- `src/services/storyEngine/narrativeCarrierCatalog.ts` + +## 4.3 明确不做 + +第二阶段仍不做: + +1. 全量结局树编辑器 +2. 全世界所有对象统一入图谱编辑器 +3. 多周目专用 meta-story 系统 +4. 完整 romance / betrayal / leave-party 大型队友事件网 +5. 全 UI 层 approval feed 面板 + +原因: + +第二阶段的核心仍是“让引擎开始动态驱动经典 RPG 体验”,而不是做内容工具大平台。 + +## 5. 第二阶段最小闭环 + +建议第二阶段最小闭环定义为: + +```text +世界图谱 AI 生成 +-> 角色叙事档案 AI 生成 +-> KnowledgeFact 图谱 +-> VisibilitySlice 2.0 +-> 队友反应导演 +-> 线程合约生成 +-> signal 推进 +-> 地点/文书/物件回响 +-> 记忆回写 +``` + +本阶段最关键的用户可感知落点有 3 条: + +1. 队友 / 同伴反应链 +2. 调查 / 任务推进链 +3. 地点 / 物件 / 文书残痕链 + +## 6. 数据结构升级方案 + +## 6.1 `src/types/storyEngine.ts` + +第二阶段新增: + +```ts +export interface KnowledgeFact { + id: string; + title: string; + content: string; + ownerActorIds: string[]; + relatedThreadIds: string[]; + relatedScarIds: string[]; + sourceType: 'actor' | 'item' | 'document' | 'scene' | 'monster' | 'quest'; + visibility: 'public' | 'discoverable' | 'private' | 'forbidden'; + sayability: 'direct' | 'indirect' | 'reactive_only'; + aliases?: string[]; +} + +export interface ThreadContractStep { + id: string; + title: string; + revealText: string; + completionSignalIds: string[]; + optionalFactIds: string[]; +} + +export interface ThreadContract { + id: string; + threadId: string; + issuerActorId?: string | null; + narrativeType: 'investigation' | 'escort' | 'hunt' | 'relationship' | 'trial' | 'recovery'; + currentStepId: string | null; + visibleStage: number; + steps: ThreadContractStep[]; + followupThreadIds: string[]; +} + +export interface StorySignal { + id: string; + signalType: + | 'enter_scene' + | 'leave_scene' + | 'talk_to_actor' + | 'obtain_carrier' + | 'inspect_scene' + | 'win_battle' + | 'give_item' + | 'accept_contract' + | 'resolve_contract_step'; + actorId?: string | null; + sceneId?: string | null; + carrierId?: string | null; + threadIds?: string[]; +} + +export interface CompanionReactionRecord { + id: string; + characterId: string; + reactionType: 'approve' | 'disapprove' | 'concern' | 'silence' | 'curious'; + reason: string; + relatedThreadIds: string[]; + createdAt: string; +} +``` + +并扩展: + +```ts +export interface StoryEngineMemoryState { + discoveredFactIds: string[]; + inferredFactIds?: string[]; + activeThreadIds: string[]; + resolvedScarIds: string[]; + recentCarrierIds: string[]; + recentSignalIds?: string[]; + recentCompanionReactions?: CompanionReactionRecord[]; +} +``` + +## 6.2 `src/types/story.ts` + +第二阶段建议扩展任务结构,而不是直接新起平行系统。 + +扩展: + +```ts +interface QuestLogEntry { + threadId?: string | null; + contractId?: string | null; + discoveredFactIds?: string[]; + relatedCarrierIds?: string[]; +} +``` + +注意: + +- 第二阶段仍继续复用 `QuestLogEntry` +- 不推翻当前任务系统,只增强其线程感知和推进能力 + +## 6.3 `src/types/scene.ts` + +扩展: + +```ts +interface ScenePresetInfo { + narrativeResidues?: SceneNarrativeResidue[]; +} + +interface SceneNarrativeResidue { + id: string; + title: string; + visibleClue: string; + linkedFactIds: string[]; + linkedThreadIds: string[]; +} +``` + +## 7. 模块实现方案 + +## 7.1 `knowledgeGraph.ts` + +职责: + +1. 从 `WorldStoryGraph + ActorNarrativeProfile + backstoryReveal + carriers` + 编译出全局 `KnowledgeFact[]` +2. 统一管理: + - 事实 ID + - 所属角色 + - 所属线程 + - 是否可直说 + - 是否只能反应式表达 + +建议导出: + +```ts +buildKnowledgeGraph(profile) +buildActorKnowledgeFacts(role, graph) +buildCarrierKnowledgeFacts(carrier, graph) +``` + +第二阶段关键要求: + +- `VisibilitySlice` 不再只靠字符串标签推断 +- 必须能落到真实事实节点 + +## 7.2 `knowledgeContract.ts` + +职责: + +1. 用 `KnowledgeFact` 构造 `VisibilitySlice 2.0` +2. 支持: + - 可直接说的事实 + - 只能暗示的事实 + - 只会在反应中暴露的事实 + - 玩家已经误判但系统不盖章的事实 + +建议导出: + +```ts +buildVisibilitySliceFromFacts(params) +buildMisdirectionFacts(params) +``` + +## 7.3 `companionReactionDirector.ts` + +职责: + +1. 根据玩家选择、活跃线程、事实图谱、队友 stance 生成队友反应 +2. 把当前只存在于 `stanceProfile` 内的数值,变成: + - 明确的认可 / 反对 / 担忧 / 沉默 + - 可写入聊天、插话、任务后反馈的短反应 + +建议导出: + +```ts +buildCompanionReactionBatch(params) +applyCompanionReactionToStance(params) +``` + +第二阶段要求: + +1. 至少支持关键选择后队友短反应 +2. 至少支持营地或旅途中一次补充评价 + +## 7.4 `threadContract.ts` + +职责: + +1. 把 `StoryThread` 编译成可推进的 `ThreadContract` +2. 为调查、关系、试炼、追索等线程提供稳定步骤结构 + +建议导出: + +```ts +buildThreadContract(params) +compileQuestFromThreadContract(params) +``` + +第二阶段目标: + +- 不是重做任务系统 +- 而是让任务成为线程推进器的一种表现 + +## 7.5 `threadSignalRouter.ts` + +职责: + +1. 接收运行时 signal +2. 判断哪些线程、哪些合约步骤应该推进 +3. 触发: + - 任务步骤完成 + - 新事实解锁 + - 队友反应 + - 新残痕 / 新物件回响 + +建议导出: + +```ts +collectStorySignals(params) +resolveSignalsToThreadUpdates(params) +``` + +## 7.6 `sceneResidueCompiler.ts` + +职责: + +1. 给场景和调查结果补 `narrativeResidues` +2. 让场景本身开始承担《黑神话》《古剑》式残痕叙事 + +建议导出: + +```ts +buildSceneNarrativeResidues(params) +buildResidueInspectResult(params) +``` + +第二阶段要求: + +1. 至少在自定义世界 landmark 上生成残痕 +2. 至少能在“观察痕迹 / inspect / observe_signs”时读出来 + +## 7.7 `narrativeCarrierCatalog.ts` + +职责: + +把当前只覆盖 `InventoryItem` 的载体,扩成统一的叙事载体目录: + +1. 物件 +2. 文书 +3. 场景残痕 +4. 怪物特殊掉落情报 +5. 任务证据 + +建议导出: + +```ts +buildNarrativeCarrierCatalog(state) +resolveCarrierById(id) +``` + +## 8. 现有文件改造方案 + +## 8.1 `src/services/customWorld.ts` + +第二阶段要做的,不是继续堆 fallback,而是把 AI contract 真正接进生成链。 + +改造目标: + +1. `ThemePack` 允许 AI 细化词汇与命名模式 +2. `WorldStoryGraph` 允许 AI 生成更像世界主线 / 暗线的线程图谱 +3. `ActorNarrativeProfile` 允许 AI 生成更有作者性的角色命题与错位感 + +要求: + +- 仍保留 fallback +- AI 失败时不阻塞生成 +- AI 成功时优先写入结构化结果 + +## 8.2 `src/services/customWorldBuilder.ts` + +第二阶段不再只是 normalize,而要支持: + +1. 合并 AI 输出与 fallback +2. 校验线程、角色、场景之间的 ID 关联 +3. 把 `KnowledgeFact` 预编译结果一起挂回 profile + +## 8.3 `src/services/prompt.ts` + +这是第二阶段最重要的运行时改造点。 + +改造目标: + +1. 彻底移除低披露阶段对 `relationshipHooks` / `tags` 的无条件直出 +2. `VisibilitySlice` 改为基于 `KnowledgeFact` 组装 +3. 引入 `recentCompanionReactions` +4. 引入 `recentCarrierEchoes` + +第二阶段后,prompt 不应再直接拼太多原始字段,而应该优先拼: + +1. 可见事实 +2. 可推测事实 +3. 反应提示 +4. 线程压力 + +## 8.4 `src/data/npcInteractions.ts` + +第二阶段重点: + +1. 把 `stanceProfile` 从局部数值状态,升级成真正会改变文本与反应方向的关系模型 +2. 增加: + - 审慎沉默 + - 价值观反对 + - 被触发的 taboo 反应 + - 队友间互评 + +仍不做大 UI,但要开始做: + +- 运行时差异反应文本 +- 旅途中短插话 + +## 8.5 `src/hooks/useStoryGeneration.ts` + +第二阶段继续保持 orchestrator 职责,不吞业务细节。 + +新增职责: + +1. 收集 `StorySignal` +2. 路由到 `threadSignalRouter` +3. 收集队友反应批次 +4. 把反应和回响写回 story history / memory + +## 8.6 `src/services/questDirector.ts` + `src/data/questFlow.ts` + +第二阶段目标: + +1. 任务不再只是“任务意图 -> 任务” +2. 而是: + - `StoryThread -> ThreadContract -> Quest manifestation` + +最低要求: + +1. 调查类任务 +2. 关系类任务 + +先完成线程化。 + +## 8.7 `src/data/runtimeItemNarrative.ts` + +第二阶段这里不再是主攻点,但仍要扩: + +1. 和 `narrativeCarrierCatalog` 对齐 +2. 支持非物件类叙事载体复用同一套描述语法 +3. 支持 NPC 对重点物件产生反应 + +## 9. 第二阶段开发顺序 + +建议顺序如下: + +1. `KnowledgeFact` 图谱 +2. `VisibilitySlice 2.0` +3. Prompt 泄露收口 +4. Companion reaction director +5. Thread contract / signal router +6. Scene residue / narrative carriers +7. 自定义世界 AI contract 真接入 +8. 测试与内容回归 + +原因: + +- 不先做知识图谱和可见性,后面所有反应系统都会继续飘 +- 不先做 signal router,线程化任务和回响都无法稳定推进 + +## 10. 测试方案 + +## 10.1 新增单元测试 + +建议新增: + +1. `knowledgeGraph.test.ts` +2. `knowledgeContract.test.ts` +3. `companionReactionDirector.test.ts` +4. `threadContract.test.ts` +5. `threadSignalRouter.test.ts` +6. `sceneResidueCompiler.test.ts` + +## 10.2 新增集成测试 + +建议补以下集成路径: + +1. 关键选择 -> 队友反应 -> stance 更新 +2. 获取重点物件 -> memory 回写 -> 后续 NPC 反应 +3. 调查线索 -> signal -> thread contract step 推进 +4. 自定义世界 AI 线程图谱 -> prompt 可见性 -> 首遇文本 + +## 10.3 第二阶段验收测试场景 + +至少人工回归这 5 条: + +1. 队友对玩家价值观选择的分化反应 +2. 调查类任务拿到线索后任务推进 +3. 场景残痕被观察后触发新对话变化 +4. 重点物件被某 NPC 识别并改变口风 +5. 同一条暗线在人物、任务、地点、物件上多次回响 + +## 11. 第二阶段验收标准 + +做到以下几点,才算第二阶段收口: + +1. 自定义世界线程图谱和角色叙事档案已真正支持 AI contract 生成 +2. 首遇和低披露阶段不再通过 `relationshipHooks / tags` 等隐式字段泄露暗线信息 +3. 队友会对关键选择给出差异化反应,且能写回关系状态 +4. 至少一类任务已经通过 `ThreadContract + StorySignal` 推进 +5. 地点调查、物件获取、对话推进之间开始形成回响闭环 +6. `storyEngineMemory` 中的 `recentCarrierIds / resolvedScarIds / recentCompanionReactions` + 已被运行时消费,而不只是存着 +7. 相关测试通过,`lint` / `typecheck` / `check:encoding` 通过 + +## 12. 一句话结论 + +第一阶段解决的是“剧情引擎的骨架有没有”,第二阶段要解决的是: + +**这些骨架能不能开始真正驱动选择后果、队友反应、调查推进和世界回响。** + +只要第二阶段做稳,当前项目就会从“结构上像 AI 原生剧情引擎”,真正走到“体验上开始像经典单机 RPG”。 diff --git a/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE3_IMPLEMENTATION_PLAN_2026-04-06.md b/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE3_IMPLEMENTATION_PLAN_2026-04-06.md new file mode 100644 index 00000000..bfe62b67 --- /dev/null +++ b/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE3_IMPLEMENTATION_PLAN_2026-04-06.md @@ -0,0 +1,690 @@ +# AI 原生剧情引擎第三阶段技术落地方案 + +更新时间:`2026-04-06` + +## 0. 文档目的 + +这份方案建立在当前仓库第二阶段已经基本落地的前提上,面向第三阶段的技术推进。 + +按当前代码审计结果,第二阶段这些关键能力已经基本接入: + +1. `KnowledgeFact` +2. `VisibilitySlice 2.0` +3. `CompanionReactionRecord` +4. `ThreadContract` +5. `StorySignal` +6. `SceneNarrativeResidue` +7. `narrativeCarrierCatalog` +8. `recentCompanionReactions / recentCarrierEchoes` +9. 自定义世界生成链中的 `ThemePack / StoryGraph / ActorNarrativeProfile` AI 生成阶段 + +同时,这些链已经形成了真实接线: + +1. `customWorldBuilder` 会生成 `knowledgeFacts / threadContracts / narrativeResidues` +2. `useStoryGeneration` 会读取 `knowledgeFacts` 并构造新的可见性切片 +3. `progressionActions` 会收集 `signals`、更新 `thread`、生成 `companion reactions` +4. `questFlow` 已开始挂接 `threadContract` +5. `prompt.ts` 已消费最近队友反应与载体回响 + +因此第三阶段不再解决: + +- 引擎底层语法有没有 +- 线程和事实能不能进主链 + +第三阶段要解决的是: + +**如何让这些系统进一步长成“章节化、队友线、世界状态变化、旅程高光、长期回顾”这些真正构成经典单机 RPG 体验的中高层叙事能力。** + +## 1. 第二阶段审计结论 + +## 1.1 可以判定为“基本落地”的部分 + +按代码和测试情况,第二阶段已经基本落地: + +1. 第二阶段新增模块都已存在并有测试: + - `knowledgeGraph` + - `knowledgeContract` + - `companionReactionDirector` + - `threadContract` + - `threadSignalRouter` + - `sceneResidueCompiler` + - `echoMemory` + +2. 关键主链已接入: + - `customWorldBuilder.ts` + - `useStoryGeneration.ts` + - `progressionActions.ts` + - `questFlow.ts` + - `prompt.ts` + +3. 第二阶段相关测试通过: + - `knowledgeGraph.test.ts` + - `knowledgeContract.test.ts` + - `companionReactionDirector.test.ts` + - `threadContract.test.ts` + - `threadSignalRouter.test.ts` + - `sceneResidueCompiler.test.ts` + - `echoMemory.test.ts` + +4. 工程门禁通过: + - `lint` + - `typecheck` + - `check:encoding` + +所以,“第二阶段技术落地方案已经基本落地”这个判断是成立的。 + +## 1.2 第二阶段仍然留下的真实缺口 + +虽然第二阶段已经基本落地,但它目前更像“动态剧情中层骨架”,还没完全进入“经典 RPG 体验高层”。 + +当前最关键的 6 个缺口是: + +1. `ThreadContract` 仍偏浅 + - 目前主要还是两步式 contract,更多是“线程感知任务”,还不是多阶段主线/支线推进器。 + +2. `CompanionReactionDirector` 仍偏通用 + - 现在队友反应能发生,但还主要是通用 approve/disapprove 级别,还没有真正进入角色个人命题、价值观冲突、营地事件、队友互评。 + +3. `KnowledgeFact` 虽然存在,但还缺“长期总结层” + - 现在事实能进可见性切片,但还缺章节摘要、世界回顾、角色关系归档、游玩 recap。 + +4. `SceneNarrativeResidue` 仍偏静态 + - 场景开始能讲故事了,但场景还不会因为线程推进、事件完成、阵营变化而发生状态变异。 + +5. 世界状态变化还很弱 + - 当前 `signal` 会推进记忆和 quest,可是还不会稳定驱动: + - 场景变更 + - NPC 立场改口 + - 商店库存风格变化 + - 场景敌人/残痕刷新 + +6. 缺少章节 / 旅程 / 高光导演层 + - 现在有线程、有事实、有反应,但还没有一个更高层的“章节推进器”来控制: + - 主线节奏 + - 旅程段落 + - 大事件前后 + - 营地休整与情感释放 + - 章节收束与下一章开启 + +## 1.3 结论 + +第一阶段解决的是“骨架有没有”,第二阶段解决的是“中层动态有没有”,第三阶段要解决的是: + +**这些系统能不能组合出真正像经典单机 RPG 的章节感、队友线、旅程高光和世界变化。** + +## 2. 第三阶段目标 + +第三阶段只做 5 件事: + +1. 建立 `章节 / 旅程 / 高光导演层` +2. 建立 `队友个人线与营地事件层` +3. 建立 `世界状态变化与阵营温度层` +4. 建立 `叙事文书 / 档案 / 回顾摘要层` +5. 建立 `大事件 / 章节高潮 / Boss 前后叙事编排层` + +一句话定义第三阶段: + +**从“动态剧情系统”升级到“能持续制造章节记忆点和旅程余味的 RPG 叙事框架”。** + +## 3. 第三阶段完成定义 + +第三阶段完成后,至少要同时满足: + +1. 游戏运行中能形成 `章节感` + - 玩家明确感到自己经历了“第一章、第二章、转折、大事件、余波”。 + +2. 队友不再只是对选择做短反应 + - 而是会有: + - 个人线推进 + - 营地事件 + - 队友互评 + - 价值观冲突 + - 忠诚变化 + +3. 世界会因为线程推进发生状态变化 + - 某些场景描述、残痕、NPC 态度、敌对压力、商店风格、任务机会会改变。 + +4. 玩家会得到结构化回顾 + - 包括: + - 当前章节摘要 + - 已发现关键真相 + - 队友关系变化 + - 最近大事件 + +5. 至少一条主线或关键线程,已经能从“起线 -> 扩张 -> 转折 -> 高潮 -> 余波”完整走完。 + +## 4. 第三阶段范围 + +## 4.1 纳入范围 + +- `src/services/storyEngine/*` +- `src/hooks/useStoryGeneration.ts` +- `src/hooks/story/progressionActions.ts` +- `src/data/npcInteractions.ts` +- `src/data/questFlow.ts` +- `src/services/prompt.ts` +- `src/components/AdventureEntityModal.tsx` +- `src/components/CharacterPanel.tsx` +- `src/components/InventoryPanel.tsx` +- `src/components/AdventurePanel.tsx` +- `src/types/storyEngine.ts` +- `src/types/story.ts` +- `src/types/game.ts` +- `src/types/scene.ts` + +## 4.2 新增模块 + +- `src/services/storyEngine/chapterDirector.ts` +- `src/services/storyEngine/journeyBeatPlanner.ts` +- `src/services/storyEngine/companionArcDirector.ts` +- `src/services/storyEngine/campEventDirector.ts` +- `src/services/storyEngine/worldMutationRouter.ts` +- `src/services/storyEngine/factionTensionState.ts` +- `src/services/storyEngine/documentCarrierCompiler.ts` +- `src/services/storyEngine/storyChronicle.ts` +- `src/services/storyEngine/recapDigest.ts` +- `src/services/storyEngine/setpieceDirector.ts` + +## 4.3 第三阶段明确不做 + +第三阶段仍不做: + +1. 全流程可视化剧情编辑器 +2. 通用 mod 工具链 +3. 全量配音/镜头脚本系统 +4. 无限复杂分支树编辑器 +5. 所有支线都拥有章节级演出 + +原因: + +第三阶段目标是把当前系统推到“经典 RPG 体验层”,不是同时把制作工具也做到工业级。 + +## 5. 第三阶段最小闭环 + +建议第三阶段最小闭环为: + +```text +ChapterDirector +-> JourneyBeatPlanner +-> ThreadContract / StorySignal +-> CompanionArcDirector +-> CampEventDirector +-> WorldMutationRouter +-> SetpieceDirector +-> StoryChronicle / RecapDigest +``` + +这条闭环对应玩家可感知的体验是: + +1. 章节推进 +2. 旅程段落变化 +3. 队友关系与个人线变化 +4. 场景 / 世界状态变化 +5. 大事件 / 高潮回合 +6. 章节后的回顾与余波 + +## 6. 数据结构升级方案 + +## 6.1 `src/types/storyEngine.ts` + +第三阶段建议新增: + +```ts +export interface ChapterState { + id: string; + title: string; + theme: string; + primaryThreadIds: string[]; + stage: 'opening' | 'expansion' | 'turning_point' | 'climax' | 'aftermath'; + chapterSummary: string; +} + +export interface JourneyBeat { + id: string; + beatType: 'approach' | 'investigation' | 'camp' | 'conflict' | 'boss_prelude' | 'climax' | 'recovery'; + title: string; + triggerThreadIds: string[]; + recommendedSceneIds: string[]; + emotionalGoal: string; +} + +export interface CompanionArcState { + characterId: string; + arcTheme: string; + currentStage: 'closed' | 'guarded' | 'opening' | 'conflicted' | 'bonded' | 'resolved'; + activeConflictTags: string[]; + pendingEventIds: string[]; + resolvedEventIds: string[]; +} + +export interface CampEvent { + id: string; + eventType: 'private_talk' | 'party_banter' | 'conflict' | 'comfort' | 'reveal' | 'decision'; + title: string; + participantCharacterIds: string[]; + triggerReason: string; + relatedThreadIds: string[]; +} + +export interface WorldMutation { + id: string; + mutationType: 'scene_text' | 'npc_attitude' | 'shop_style' | 'enemy_pressure' | 'route_lock' | 'route_unlock'; + targetId: string; + reason: string; + relatedThreadIds: string[]; +} + +export interface FactionTensionState { + factionId: string; + temperature: number; + pressureSummary: string; + activeConflictThreadIds: string[]; +} + +export interface ChronicleEntry { + id: string; + category: 'chapter' | 'thread' | 'companion' | 'carrier' | 'scene' | 'world_event'; + title: string; + summary: string; + relatedIds: string[]; + createdAt: string; +} +``` + +并扩展: + +```ts +export interface StoryEngineMemoryState { + currentChapter?: ChapterState | null; + currentJourneyBeatId?: string | null; + companionArcStates?: CompanionArcState[]; + worldMutations?: WorldMutation[]; + chronicle?: ChronicleEntry[]; +} +``` + +## 6.2 `src/types/game.ts` + +第三阶段建议新增: + +```ts +interface GameState { + chapterState?: ChapterState | null; +} +``` + +说明: + +- `chapterState` 放在 `GameState` 顶层,便于 UI 和主流程快速读取 +- 详细历史仍留在 `storyEngineMemory` + +## 6.3 `src/types/scene.ts` + +第三阶段建议扩展: + +```ts +interface ScenePresetInfo { + mutationStateText?: string | null; + currentPressureLevel?: 'low' | 'medium' | 'high' | 'extreme'; +} +``` + +用于场景在章节推进后发生显式变化。 + +## 7. 模块实现方案 + +## 7.1 `chapterDirector.ts` + +职责: + +1. 根据主活跃线程、最近 signal、世界冲突温度,决定当前章节处于哪个阶段 +2. 生成: + - 章节标题 + - 当前主题 + - 当前主线线程 + - 当前章节摘要 + +建议导出: + +```ts +resolveCurrentChapterState(params) +advanceChapterState(params) +``` + +第三阶段目标: + +- 让玩家的体验从“连续事件流”变成“章节化旅程” + +## 7.2 `journeyBeatPlanner.ts` + +职责: + +1. 在章节内部规划短周期旅程段落 +2. 给当前阶段分配: + - 接近 + - 调查 + - 休整 + - 冲突 + - 高潮前奏 + - 余波 + +建议导出: + +```ts +buildJourneyBeatQueue(params) +resolveCurrentJourneyBeat(params) +``` + +它是第三阶段对标《黑神话》旅程感和《仙剑》节奏起伏的关键模块。 + +## 7.3 `companionArcDirector.ts` + +职责: + +1. 把当前队友的 stance、thread、reaction、facts 组合成真正的个人线状态 +2. 决定某个队友此刻更接近: + - 打开 + - 回避 + - 冲突 + - 信任 + - 和解 + +建议导出: + +```ts +buildCompanionArcState(params) +advanceCompanionArc(params) +``` + +目标: + +- 从“通用反应”进化到“每个队友都有正在发生的个人线” + +## 7.4 `campEventDirector.ts` + +职责: + +1. 根据 `CompanionArcState + recentCompanionReactions + chapterState` + 决定是否生成营地/旅途中事件 +2. 生成: + - 私聊事件 + - 队友互评 + - 意见冲突 + - 情绪缓和 + - 个人秘密推进 + +建议导出: + +```ts +evaluateCampEventOpportunity(params) +buildCampEvent(params) +``` + +这是第三阶段对标《仙剑》角色羁绊、《博德之门》营地戏的核心模块。 + +## 7.5 `worldMutationRouter.ts` + +职责: + +1. 把 `StorySignal + ThreadContract + ChapterState` 转成世界状态变化 +2. 更新: + - 场景描述 + - NPC 口风 + - 商店风格 + - 敌人压力 + - 路线开关 + +建议导出: + +```ts +resolveWorldMutations(params) +applyWorldMutationsToGameState(params) +``` + +第三阶段目标: + +- 世界开始“因为你做过的事而变” + +## 7.6 `factionTensionState.ts` + +职责: + +1. 给 major factions 维护温度和压力摘要 +2. 决定某些章节里哪条势力线正在升温 + +建议导出: + +```ts +buildFactionTensionState(profile, memory) +applySignalToFactionTension(params) +``` + +它主要对标《轩辕剑》的大时代张力和《古剑》的世界推进感。 + +## 7.7 `documentCarrierCompiler.ts` + +职责: + +1. 把“文书、口供、记录、日志、残页、信件”等正式纳入叙事载体体系 +2. 和 `narrativeCarrierCatalog` 对齐 + +建议导出: + +```ts +buildNarrativeDocument(params) +compileDocumentKnowledgeFacts(params) +``` + +目标: + +- 第三阶段让叙事载体不再只限于物件和场景残痕 + +## 7.8 `storyChronicle.ts` + +职责: + +1. 把当前发生过的大事件、关键事实、角色转折写入 chronicle +2. 让玩家和系统都有一个更高层的“已发生过什么”的长期摘要 + +建议导出: + +```ts +appendChronicleEntry(params) +buildChronicleSummary(params) +``` + +## 7.9 `recapDigest.ts` + +职责: + +1. 生成章节回顾、阶段摘要、下回合简报 +2. 给 UI、存档恢复、继续游戏提供更强的 recap 文本 + +建议导出: + +```ts +buildChapterRecap(params) +buildContinueGameDigest(params) +``` + +## 7.10 `setpieceDirector.ts` + +职责: + +1. 决定何时进入大事件、Boss 前奏、对峙、章节高潮 +2. 给主链输出高光导演指令 + +建议导出: + +```ts +evaluateSetpieceOpportunity(params) +buildSetpieceDirective(params) +``` + +第三阶段目标: + +- 让剧情不只是“持续往前”,而是有真正高光和收束 + +## 8. 现有文件改造方案 + +## 8.1 `src/hooks/useStoryGeneration.ts` + +第三阶段这里要继续保持 orchestrator 职责,但增加“高层导演”的接线: + +新增接线: + +1. `chapterDirector` +2. `journeyBeatPlanner` +3. `setpieceDirector` +4. `campEventDirector` + +它要做的事情: + +1. 读取当前章节状态 +2. 读取当前旅程 beat +3. 判断本回合是否应该转入营地 / 高潮 / 余波 +4. 把这些高层指令压入 prompt + +## 8.2 `src/hooks/story/progressionActions.ts` + +这里是第三阶段最重要的状态推进点。 + +新增职责: + +1. 调用 `worldMutationRouter` +2. 调用 `companionArcDirector` +3. 调用 `storyChronicle` +4. 回写: + - `chapterState` + - `companionArcStates` + - `worldMutations` + - `chronicle` + +## 8.3 `src/services/prompt.ts` + +第三阶段改造目标: + +1. 加入 `chapterState` +2. 加入 `journeyBeat` +3. 加入 `recentWorldMutations` +4. 加入 `recentChronicleSummary` + +提示词不再只围绕“当前场景 + 当前遭遇 + facts”,还要围绕: + +1. 当前章节主题 +2. 当前旅程段落 +3. 最近发生的世界变化 +4. 队友当前情绪与线索包袱 + +## 8.4 `src/data/npcInteractions.ts` + +第三阶段重点: + +1. 支持由 `CompanionArcState` 决定对话气质 +2. 支持队友互评与冲突事件 +3. 支持同一选择在不同 arc stage 下触发不同态度 + +## 8.5 `src/data/questFlow.ts` + +第三阶段目标: + +1. 任务从“线程 manifest”继续升级为“章节与旅程服务” +2. 支持: + - 章节中期任务 + - 高潮前置任务 + - 余波任务 + +## 8.6 `src/components/*` + +第三阶段不主打大 UI 重做,但建议最小补这些表现: + +1. `AdventurePanel` + - 当前章节标题 / 当前旅程 beat 轻量显示 + +2. `CharacterPanel` + - 队友当前个人线阶段 + +3. `AdventureEntityModal` + - 最近与该角色相关的 chronicle / residue / carrier 片段 + +4. `InventoryPanel` + - 文书 / 证据 / 特殊载体的独立入口 + +## 9. 第三阶段开发顺序 + +建议顺序如下: + +1. `chapterDirector` +2. `journeyBeatPlanner` +3. `companionArcDirector` +4. `campEventDirector` +5. `worldMutationRouter` +6. `documentCarrierCompiler` +7. `storyChronicle` +8. `recapDigest` +9. `setpieceDirector` +10. UI 最小接线 + +原因: + +- 没有章节和旅程层,后面的营地事件和高潮都无从安放 +- 没有 world mutation,很多后续内容仍会像“文本在变,世界没变” + +## 10. 测试方案 + +## 10.1 新增单元测试 + +建议新增: + +1. `chapterDirector.test.ts` +2. `journeyBeatPlanner.test.ts` +3. `companionArcDirector.test.ts` +4. `campEventDirector.test.ts` +5. `worldMutationRouter.test.ts` +6. `factionTensionState.test.ts` +7. `documentCarrierCompiler.test.ts` +8. `storyChronicle.test.ts` +9. `recapDigest.test.ts` +10. `setpieceDirector.test.ts` + +## 10.2 新增集成测试 + +建议补这 5 类: + +1. 线程推进到转折点后,章节状态发生变化 +2. 队友在关键选择后推进个人线阶段 +3. 获得关键文书后,营地事件或下一轮剧情出现新回响 +4. 某条主线推进后,场景文本和 NPC 态度发生世界状态变化 +5. 高潮前后,prompt 中的章节、旅程、世界变化信息都能被正确注入 + +## 10.3 人工回归场景 + +至少做这 6 条: + +1. 一条调查线程从起线推进到转折 +2. 一个队友从 guarded 推到 conflicted / bonded +3. 一个营地事件被触发并影响后续关系 +4. 一个重点物件 + 文书 + 场景残痕组成同一条暗线回响 +5. 一个章节进入高潮前后,场景压力显著变化 +6. 继续游戏时能看到结构化 recap,而不是只靠最后几条 story history + +## 11. 第三阶段验收标准 + +做到以下几点,才算第三阶段可收口: + +1. 系统能稳定给出 `ChapterState` +2. 至少一条主线线程可以经历 `opening -> expansion -> turning_point -> climax -> aftermath` +3. 队友个人线能有明确阶段推进,并能触发营地或旅途事件 +4. 世界状态会因为线程推进产生可见变化 +5. 文书/证据类叙事载体正式进入主链 +6. 玩家可以在运行时获得章节回顾和近期大事件摘要 +7. 高潮节点会由 `setpieceDirector` 显式导演,而不是随机撞出来 +8. 相关测试通过,`lint` / `typecheck` / `check:encoding` 通过 + +## 12. 一句话结论 + +第一阶段解决“骨架”,第二阶段解决“动态系统”,第三阶段要解决的是: + +**这些动态系统能不能真正长出章节感、队友线、世界变化和章节高光。** + +只有第三阶段做稳,当前项目才会从“已经很像 AI 原生剧情引擎”,真正走向“能稳定制造经典单机 RPG 旅程体验”的层级。 diff --git a/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE4_IMPLEMENTATION_PLAN_2026-04-06.md b/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE4_IMPLEMENTATION_PLAN_2026-04-06.md new file mode 100644 index 00000000..41de1d93 --- /dev/null +++ b/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE4_IMPLEMENTATION_PLAN_2026-04-06.md @@ -0,0 +1,725 @@ +# AI 原生剧情引擎第四阶段技术落地方案 + +更新时间:`2026-04-06` + +## 0. 文档目的 + +这份方案建立在当前仓库第三阶段已经基本落地的前提上,面向第四阶段的技术推进。 + +按当前代码审计结果,第三阶段已经基本把这些中高层能力接进了主链: + +1. `ChapterState` +2. `JourneyBeat` +3. `CompanionArcState` +4. `CampEvent` +5. `WorldMutation` +6. `FactionTensionState` +7. `ChronicleEntry` +8. `ContinueGameDigest` +9. `SetpieceDirective` + +并且这些能力已经开始被: + +- `useStoryGeneration` +- `progressionActions` +- `prompt.ts` +- `AdventurePanel` +- `CharacterPanel` +- `AdventureEntityModal` +- `InventoryPanel` + +实际消费。 + +因此第四阶段不再解决: + +- 底层图谱有没有 +- 动态系统有没有 +- 章节、旅程和高光导演有没有 + +第四阶段要解决的是: + +**如何让当前已经具备的动态叙事系统,真正进入“可持续量产内容、可收束长线剧情、可形成多幕主线与结局体验、可进行作者性控制和质量校验”的阶段。** + +一句话说: + +**前三阶段把引擎“做起来”,第四阶段要把它“做完整、做可控、做可持续量产”。** + +## 1. 第三阶段审计结论 + +## 1.1 可以判定为“基本落地”的部分 + +按代码和测试情况,第三阶段已经基本落地: + +1. 第三阶段新增模块都已存在并有测试: + - `chapterDirector` + - `journeyBeatPlanner` + - `companionArcDirector` + - `campEventDirector` + - `worldMutationRouter` + - `factionTensionState` + - `documentCarrierCompiler` + - `storyChronicle` + - `recapDigest` + - `setpieceDirector` + +2. 关键主链已接入: + - `useStoryGeneration.ts` + - `progressionActions.ts` + - `prompt.ts` + - `AdventurePanel.tsx` + - `CharacterPanel.tsx` + - `AdventureEntityModal.tsx` + - `InventoryPanel.tsx` + - `GameShell.tsx` + +3. 第三阶段相关测试通过: + - `chapterDirector.test.ts` + - `journeyBeatPlanner.test.ts` + - `companionArcDirector.test.ts` + - `campEventDirector.test.ts` + - `worldMutationRouter.test.ts` + - `factionTensionState.test.ts` + - `documentCarrierCompiler.test.ts` + - `storyChronicle.test.ts` + - `recapDigest.test.ts` + - `setpieceDirector.test.ts` + +4. 工程门禁通过: + - `lint` + - `typecheck` + - `check:encoding` + +所以,“第三阶段技术落地方案已经基本落地”这个判断成立。 + +## 1.2 第三阶段留下的真实缺口 + +虽然第三阶段已经把“章节、旅程、队友线、世界变化、高光导演”这些高层能力接起来了,但它们还更像“运行时中高层系统”,还没有进入真正的“长线 campaign 完整度”和“内容生产可控性”阶段。 + +当前最关键的 7 个缺口是: + +1. `ChapterState` 已有,但还缺 `Act / Campaign` 级结构 + - 目前更像章节段落推进器,还不是完整的多幕主线架构。 + +2. 队友个人线已有,但还缺“收束态” + - 当前能推进 arc stage,但还没有: + - 个人线结局 + - 决裂 / 离队 / 归队 + - 忠诚锁定 + - 决战前站队 + +3. 世界变化已有,但还缺“全局后果账本” + - 当前能改场景和口风,但还没有统一的: + - 关键选择后果台账 + - 阵营态势累积 + - 长线不可逆变化 + +4. 文书与 chronicle 已有,但还缺“玩家可管理的故事档案” + - 现在有内容生成和少量展示,但还缺真正的 codex / archive 层。 + +5. `SetpieceDirector` 已有,但还偏事件触发器 + - 当前能识别高潮和余波,但还没有: + - 多阶段高潮编排 + - 决战前准备段 + - 决战后结算段 + +6. 缺少结局 / 尾声系统 + - 当前可以推进 journey,但没有明确的: + - 线程结局 + - 角色结局 + - 世界结局 + - 尾声摘要 + +7. 缺少作者性约束与质量门禁 + - 当前系统很强,但如果要稳定产出“经典 RPG 级长线内容”,必须补: + - branch budget + - ending coverage + - contradiction checks + - pacing checks + - narrative QA 报告 + +## 1.3 结论 + +第一阶段解决“骨架”,第二阶段解决“动态系统”,第三阶段解决“章节和旅程感”,第四阶段要解决的是: + +**这套引擎能不能稳定做出完整 campaign,并且能被持续生产、校验、维护和扩张。** + +## 2. 第四阶段目标 + +第四阶段只做 5 件事: + +1. 建立 `Campaign / Act / Ending` 级主线结构 +2. 建立 `Companion Resolution` 队友线收束系统 +3. 建立 `Consequence Ledger` 全局后果账本 +4. 建立 `Narrative Codex / Archive` 故事档案系统 +5. 建立 `Authorial Constraint + Narrative QA` 内容生产与质量门禁体系 + +一句话定义第四阶段: + +**从“能跑出经典 RPG 旅程感”升级到“能稳定完成一整部经典 RPG campaign”。** + +## 3. 第四阶段完成定义 + +第四阶段完成后,至少要同时满足: + +1. 系统能够维护多幕结构 + - `Act I / II / III` 或等价 campaign 分段真正成立。 + +2. 至少一条完整主线能走到结局 + - 并且结局由: + - 主线程结果 + - 队友个人线结果 + - 阵营态势 + - 关键后果账本 + 共同决定。 + +3. 队友个人线能有明确收束 + - 包括: + - 和解 + - 坚定追随 + - 决裂 + - 离队 + - 牺牲 / 缺席 / 旁观 + +4. 世界后果被系统记录并能影响结局和尾声 + - 不只是局部场景文字变化。 + +5. 玩家可以查看已发生的主线、角色线、文书、关键真相和结局线索。 + +6. 生成链具备明确的作者性约束和 QA 报告,不再只靠人工抽样验证。 + +## 4. 第四阶段范围 + +## 4.1 纳入范围 + +- `src/services/storyEngine/*` +- `src/hooks/story/progressionActions.ts` +- `src/hooks/useStoryGeneration.ts` +- `src/services/prompt.ts` +- `src/data/questFlow.ts` +- `src/data/npcInteractions.ts` +- `src/components/AdventurePanel.tsx` +- `src/components/CharacterPanel.tsx` +- `src/components/InventoryPanel.tsx` +- `src/components/AdventureEntityModal.tsx` +- `src/components/GameShell.tsx` +- `src/types/storyEngine.ts` +- `src/types/game.ts` +- `src/types/story.ts` + +## 4.2 新增模块 + +- `src/services/storyEngine/campaignDirector.ts` +- `src/services/storyEngine/actPlanner.ts` +- `src/services/storyEngine/endingResolver.ts` +- `src/services/storyEngine/epilogueComposer.ts` +- `src/services/storyEngine/companionResolutionDirector.ts` +- `src/services/storyEngine/consequenceLedger.ts` +- `src/services/storyEngine/narrativeCodex.ts` +- `src/services/storyEngine/authorialConstraintPack.ts` +- `src/services/storyEngine/branchBudgetPlanner.ts` +- `src/services/storyEngine/narrativeQaReport.ts` +- `src/services/storyEngine/narrativeConsistencyChecks.ts` + +## 4.3 第四阶段明确不做 + +第四阶段仍不做: + +1. 完整可视化剧情节点编辑器 +2. 商业级本地化 pipeline +3. 全自动配音 / 镜头脚本工具链 +4. 全量开放给 mod 作者的内容 DSL +5. 无上限多周目 meta-progression + +原因: + +第四阶段的任务是把 campaign 层做完整和可控,不是同时做全生产平台。 + +## 5. 第四阶段最小闭环 + +建议第四阶段最小闭环为: + +```text +CampaignDirector +-> ActPlanner +-> Chapter / Journey / Thread systems +-> ConsequenceLedger +-> CompanionResolutionDirector +-> EndingResolver +-> EpilogueComposer +-> NarrativeCodex +-> NarrativeQaReport +``` + +这条闭环对应玩家可感知的体验是: + +1. 这不是无限流局部冒险,而是一段完整 campaign +2. 你的选择有长期代价 +3. 队友线会真正收束 +4. 结局和尾声会回收之前埋下的线 +5. 整个过程具有更强作者性和完成度 + +## 6. 数据结构升级方案 + +## 6.1 `src/types/storyEngine.ts` + +第四阶段建议新增: + +```ts +export interface CampaignState { + id: string; + title: string; + currentActId: string | null; + currentActIndex: number; + resolvedEndingId?: string | null; +} + +export interface ActState { + id: string; + title: string; + actIndex: number; + theme: string; + primaryThreadIds: string[]; + status: 'opening' | 'midgame' | 'late_game' | 'finale' | 'resolved'; +} + +export interface ConsequenceRecord { + id: string; + category: 'thread' | 'companion' | 'faction' | 'world' | 'ending_flag'; + title: string; + summary: string; + weight: number; + relatedIds: string[]; + irreversible: boolean; +} + +export interface CompanionResolution { + characterId: string; + resolutionType: 'bonded' | 'reconciled' | 'estranged' | 'departed' | 'sacrificed' | 'neutral'; + summary: string; + relatedThreadIds: string[]; +} + +export interface EndingState { + id: string; + title: string; + endingType: 'heroic' | 'tragic' | 'bitter_sweet' | 'fractured' | 'ascendant'; + summary: string; + contributingThreadIds: string[]; + companionResolutions: CompanionResolution[]; + worldOutcomeSummary: string; +} + +export interface AuthorialConstraintPack { + toneRules: string[]; + noGoPatterns: string[]; + branchBudget: { + maxMajorDivergences: number; + maxEndingFamilies: number; + }; + mandatoryThemes: string[]; + requiredPayoffs: string[]; +} + +export interface NarrativeQaIssue { + id: string; + severity: 'low' | 'medium' | 'high'; + category: 'consistency' | 'pacing' | 'payoff' | 'branch_budget' | 'reveal_leak'; + summary: string; + relatedIds: string[]; +} + +export interface NarrativeQaReport { + generatedAt: string; + issues: NarrativeQaIssue[]; + summary: string; +} +``` + +并扩展: + +```ts +export interface StoryEngineMemoryState { + campaignState?: CampaignState | null; + actState?: ActState | null; + consequenceLedger?: ConsequenceRecord[]; + companionResolutions?: CompanionResolution[]; + endingState?: EndingState | null; + authorialConstraintPack?: AuthorialConstraintPack | null; + narrativeQaReport?: NarrativeQaReport | null; +} +``` + +## 6.2 `src/types/game.ts` + +扩展: + +```ts +interface GameState { + campaignState?: CampaignState | null; +} +``` + +## 6.3 `src/types/story.ts` + +扩展: + +```ts +interface QuestLogEntry { + actId?: string | null; + consequenceIds?: string[]; +} +``` + +目的: + +- 让任务正式进入 campaign / act 结构 + +## 7. 模块实现方案 + +## 7.1 `campaignDirector.ts` + +职责: + +1. 建立整个 campaign 的总状态 +2. 管理当前 act 与 campaign 收束进度 + +建议导出: + +```ts +resolveCampaignState(params) +advanceCampaignState(params) +``` + +目标: + +- 从章节推进升级到“整部故事”的推进 + +## 7.2 `actPlanner.ts` + +职责: + +1. 根据主线程、世界 tension、队友线推进情况划分 act +2. 决定何时进入: + - 第一幕铺陈 + - 第二幕扩张 + - 第三幕决战 + +建议导出: + +```ts +buildActPlan(params) +resolveCurrentActState(params) +``` + +## 7.3 `consequenceLedger.ts` + +职责: + +1. 记录关键选择和线程推进的长期后果 +2. 形成一个统一可回收的“后果账本” + +建议导出: + +```ts +appendConsequenceRecord(params) +buildConsequenceLedgerSummary(params) +``` + +这是第四阶段最重要的全局语义层之一。 + +## 7.4 `companionResolutionDirector.ts` + +职责: + +1. 根据 `CompanionArcState + reactions + key consequences` + 决定队友线的最终收束状态 +2. 输出: + - bonded + - reconciled + - estranged + - departed + - sacrificed + +建议导出: + +```ts +resolveCompanionResolution(params) +resolveAllCompanionResolutions(params) +``` + +## 7.5 `endingResolver.ts` + +职责: + +1. 综合: + - 主线程结局 + - 队友收束 + - faction tension + - consequence ledger +2. 生成最终 `EndingState` + +建议导出: + +```ts +resolveEndingState(params) +``` + +目标: + +- 真正形成“经典 RPG 结局体验” + +## 7.6 `epilogueComposer.ts` + +职责: + +1. 为结局后生成尾声摘要 +2. 回收: + - 关键线程 + - 队友去向 + - 世界变化 + - 玩家留下的长期后果 + +建议导出: + +```ts +buildEpilogueSummary(params) +``` + +## 7.7 `narrativeCodex.ts` + +职责: + +1. 把当前已有的: + - facts + - residues + - carriers + - chronicle + - documents + 组织成玩家可浏览的故事档案系统 + +建议导出: + +```ts +buildNarrativeCodex(state) +buildCodexSections(state) +``` + +## 7.8 `authorialConstraintPack.ts` + +职责: + +1. 把当前题材和 campaign 级作者性要求结构化 +2. 给 AI generation、ending、setpiece、epilogue 全部提供一致约束 + +建议导出: + +```ts +buildAuthorialConstraintPack(params) +``` + +## 7.9 `branchBudgetPlanner.ts` + +职责: + +1. 控制 major divergence 数量 +2. 控制 ending family 数量 +3. 防止 campaign 后期分支爆炸 + +建议导出: + +```ts +evaluateBranchBudget(params) +``` + +## 7.10 `narrativeQaReport.ts` + +职责: + +1. 汇总 consistency / pacing / payoff / leak / branch budget 等问题 +2. 输出结构化 QA report + +建议导出: + +```ts +buildNarrativeQaReport(params) +``` + +## 7.11 `narrativeConsistencyChecks.ts` + +职责: + +1. 做规则化检查 +2. 捕捉: + - 事实泄露 + - payoff 缺失 + - unresolved thread 过多 + - 角色线断裂 + - ending 收束不足 + +建议导出: + +```ts +runNarrativeConsistencyChecks(params) +``` + +## 8. 现有文件改造方案 + +## 8.1 `useStoryGeneration.ts` + +第四阶段这里新增 campaign 层接线: + +1. `campaignDirector` +2. `actPlanner` +3. `authorialConstraintPack` + +并把结果注入 prompt: + +1. 当前 act +2. 当前 campaign 目标 +3. 当前分支预算压力 +4. 当前必须回收的 payoff + +## 8.2 `progressionActions.ts` + +这是第四阶段最重要的状态推进点。 + +新增职责: + +1. 维护 `CampaignState / ActState` +2. 回写 `ConsequenceLedger` +3. 解析 `CompanionResolution` +4. 在满足条件时触发 `EndingResolver` +5. 生成 `NarrativeQaReport` + +## 8.3 `prompt.ts` + +第四阶段改造目标: + +1. prompt 不只是围绕当前章节和旅程 +2. 还要围绕: + - 当前 act + - campaign 主命题 + - 已积累的关键后果 + - 还必须回收的 payoff + - 作者性 constraint pack + +## 8.4 `questFlow.ts` + +第四阶段目标: + +1. 任务不只是 thread / chapter 服务 +2. 还要进入 act/campaign 层的安排 + +至少新增: + +1. act 关键任务 +2. ending 前置任务 +3. 队友个人线收束任务 + +## 8.5 `npcInteractions.ts` + +第四阶段目标: + +1. 队友/NPC 关系可以进入 resolution 判定 +2. 支持队友去留和最终站队 +3. 支持更高权重的不可逆关系变化 + +## 8.6 `components/*` + +第四阶段不主打大规模 UI 重做,但建议最小补这些: + +1. `AdventurePanel` + - 当前 act / chapter / ending pressure + +2. `CharacterPanel` + - companion resolution progress + +3. `InventoryPanel` + - narrative codex / document archive 入口 + +4. `AdventureEntityModal` + - 当前角色的关键 consequence / resolution 片段 + +## 9. 第四阶段开发顺序 + +建议顺序如下: + +1. `campaignDirector` +2. `actPlanner` +3. `consequenceLedger` +4. `companionResolutionDirector` +5. `endingResolver` +6. `epilogueComposer` +7. `narrativeCodex` +8. `authorialConstraintPack` +9. `branchBudgetPlanner` +10. `narrativeConsistencyChecks` +11. `narrativeQaReport` +12. UI 最小接线 + +原因: + +- 没有 campaign / act,结局和 QA 都无从谈起 +- 没有 consequence ledger,ending 会继续偏表层组合 +- 没有 authorial constraints 和 QA,长线内容会越来越漂 + +## 10. 测试方案 + +## 10.1 新增单元测试 + +建议新增: + +1. `campaignDirector.test.ts` +2. `actPlanner.test.ts` +3. `consequenceLedger.test.ts` +4. `companionResolutionDirector.test.ts` +5. `endingResolver.test.ts` +6. `epilogueComposer.test.ts` +7. `narrativeCodex.test.ts` +8. `authorialConstraintPack.test.ts` +9. `branchBudgetPlanner.test.ts` +10. `narrativeConsistencyChecks.test.ts` +11. `narrativeQaReport.test.ts` + +## 10.2 新增集成测试 + +建议补这 6 类: + +1. 主线程推进到 act 切换 +2. 关键后果进入 ledger 并影响后续 companion resolution +3. 队友个人线收束进入 ending +4. ending state 正确聚合主线、队友线、世界状态 +5. epilogue 能回收关键 threads / companions / world mutations +6. QA report 能检测未回收 payoff 或分支超预算 + +## 10.3 人工回归场景 + +至少做这 6 条: + +1. 一条完整主线从开局跑到结局 +2. 两名队友在同一 campaign 中走向不同 resolution +3. 一个重大选择改变结局基调 +4. 一条暗线在结局中被正式回收 +5. 玩家可以查看完整 codex / chronicle / documents +6. continue game / ending / epilogue 三个摘要层彼此一致 + +## 11. 第四阶段验收标准 + +做到以下几点,才算第四阶段可收口: + +1. 系统能维护 `CampaignState + ActState` +2. 至少一条完整主线可稳定走到 `EndingState` +3. 队友线能稳定产出 `CompanionResolution` +4. `ConsequenceLedger` 能真实影响 ending 和 epilogue +5. `NarrativeCodex` 可供玩家查看关键故事档案 +6. 系统能输出结构化 `NarrativeQaReport` +7. 分支预算和作者性约束已开始进入 generation 主链 +8. 相关测试通过,`lint` / `typecheck` / `check:encoding` 通过 + +## 12. 一句话结论 + +前三阶段让当前项目拥有了“会生成、会推进、会分层、会回响、会制造章节感”的 AI 原生剧情引擎。 + +第四阶段要做的,是让它进一步成为: + +**一套能稳定完成整部 RPG campaign、能回收长线伏笔、能产出多幕主线和结局、还能被持续生产和校验的完整叙事系统。** diff --git a/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE5_IMPLEMENTATION_PLAN_2026-04-06.md b/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE5_IMPLEMENTATION_PLAN_2026-04-06.md new file mode 100644 index 00000000..2cd6f8db --- /dev/null +++ b/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE5_IMPLEMENTATION_PLAN_2026-04-06.md @@ -0,0 +1,676 @@ +# AI 原生剧情引擎第五阶段技术落地方案 + +更新时间:`2026-04-06` + +## 0. 文档目的 + +这份方案建立在当前仓库第四阶段已经基本落地的前提上,面向第五阶段的技术推进。 + +按当前代码审计结果,第四阶段已经基本把这些 campaign 级能力接进了主链: + +1. `CampaignState` +2. `ActState` +3. `ConsequenceRecord` +4. `CompanionResolution` +5. `EndingState` +6. `NarrativeCodex` +7. `AuthorialConstraintPack` +8. `NarrativeQaReport` + +并且这些能力已经开始被: + +- `progressionActions` +- `useStoryGeneration` +- `prompt.ts` +- `AdventurePanel` +- `CharacterPanel` +- `InventoryPanel` +- `AdventureEntityModal` + +实际消费。 + +因此第五阶段不再解决: + +- campaign / act 有没有 +- ending / epilogue 有没有 +- codex / qa / constraint 有没有 + +第五阶段要解决的是: + +**如何让这套已经能完成完整 campaign 的引擎,进一步成为一套可批量生产、可多 campaign 复用、可仿真评估、可玩家画像自适应、可发布运营的叙事平台。** + +一句话说: + +**前四阶段把引擎做成“一部完整 RPG 的叙事系统”,第五阶段要把它做成“可以持续生产很多完整 RPG 体验的叙事平台”。** + +## 1. 第四阶段审计结论 + +## 1.1 可以判定为“基本落地”的部分 + +按代码和测试情况,第四阶段已经基本落地: + +1. 第四阶段新增模块都已存在并有测试: + - `campaignDirector` + - `actPlanner` + - `consequenceLedger` + - `companionResolutionDirector` + - `endingResolver` + - `epilogueComposer` + - `narrativeCodex` + - `authorialConstraintPack` + - `branchBudgetPlanner` + - `narrativeConsistencyChecks` + - `narrativeQaReport` + +2. 关键主链已接入: + - `useStoryGeneration.ts` + - `progressionActions.ts` + - `prompt.ts` + - `AdventurePanel.tsx` + - `CharacterPanel.tsx` + - `InventoryPanel.tsx` + - `AdventureEntityModal.tsx` + - `GameShell.tsx` + +3. 第四阶段相关测试通过: + - `campaignDirector.test.ts` + - `actPlanner.test.ts` + - `consequenceLedger.test.ts` + - `companionResolutionDirector.test.ts` + - `endingResolver.test.ts` + - `epilogueComposer.test.ts` + - `narrativeCodex.test.ts` + - `authorialConstraintPack.test.ts` + - `branchBudgetPlanner.test.ts` + - `narrativeConsistencyChecks.test.ts` + - `narrativeQaReport.test.ts` + +4. 工程门禁通过: + - `lint` + - `typecheck` + - `check:encoding` + +所以,“第四阶段技术落地方案已经基本落地”这个判断成立。 + +## 1.2 第四阶段留下的真实缺口 + +虽然第四阶段已经把 campaign 层做出来了,但它还更像“单 campaign 可运行系统”,还没有进入真正的“生产平台”和“高重玩度生态”阶段。 + +当前最关键的 8 个缺口是: + +1. `CampaignState / EndingState` 已有,但仍偏单线单实例 + - 当前更像运行时主战役状态,还没有正式进入“多 campaign 包”和“多 ending family 策略”。 + +2. `AuthorialConstraintPack` 已有,但仍偏静态 + - 目前更像运行时约束快照,还不是可以为不同 campaign / world pack / authorship style 配置的资产层。 + +3. `NarrativeQaReport` 已有,但仍偏单次运行内检查 + - 还没有真正的批量仿真、回归矩阵、发布门禁。 + +4. `BranchBudgetPlanner` 已有,但仍偏轻量 + - 当前更像局部预算检查,不是完整的 campaign 分支运营工具。 + +5. 缺少玩家画像与自适应导演 + - 当前系统很强,但还没有回答: + - 这位玩家偏剧情、偏探索、偏队友、偏战斗、偏收集哪一种? + - 节奏和推荐内容是否应该因玩家风格而微调? + +6. 缺少多 campaign / 多 scenario 资产化管理 + - 现在能做出一部完整故事,但还没有正式的: + - scenario pack + - campaign registry + - world pack dependency + - reusable story modules + +7. 缺少批量仿真和回放能力 + - 当前以测试和局部运行验证为主,还没有: + - playthrough simulation + - branch matrix runner + - narrative regression playback + +8. 缺少发布与迭代运营链路 + - 当前系统适合开发,但还没有: + - telemetry + - release gate report + - save migration manifest + - content diff report + +## 1.3 结论 + +第一阶段解决“骨架”,第二阶段解决“动态系统”,第三阶段解决“章节与旅程”,第四阶段解决“campaign 完整度”,第五阶段要解决的是: + +**这套引擎能不能从“一次做成一部作品”,升级到“可以持续生产、评估、复用、发布和运营很多部作品”。** + +## 2. 第五阶段目标 + +第五阶段只做 5 件事: + +1. 建立 `Scenario Pack / Campaign Registry` 多 campaign 资产体系 +2. 建立 `Simulation Lab / Regression Matrix` 批量仿真与回放评估体系 +3. 建立 `Player Style Model / Adaptive Narrative Tuner` 玩家画像与自适应导演体系 +4. 建立 `Release Gate / Telemetry / Save Migration` 发布运营体系 +5. 建立 `Narrative Production Platform` 内容复用、依赖管理和版本演化体系 + +一句话定义第五阶段: + +**从“完整 campaign 引擎”升级到“可持续生产和运营 campaign 的叙事平台”。** + +## 3. 第五阶段完成定义 + +第五阶段完成后,至少要同时满足: + +1. 系统可以同时承载多个 `Scenario Pack / Campaign Pack` +2. 不同 campaign 能共享部分叙事模块、队友模板、载体模板、约束包 +3. 发布前可以跑批量 narrative simulation,而不只靠单点测试 +4. 系统能识别不同玩家风格,并对节奏、事件权重、推荐内容做有限自适应 +5. 系统有正式的 `Release Gate Report` +6. 版本更新时有 `Save Migration Manifest` +7. campaign 之间可以被复用、比较、回归和持续优化 + +## 4. 第五阶段范围 + +## 4.1 纳入范围 + +- `src/services/storyEngine/*` +- `src/hooks/story/progressionActions.ts` +- `src/hooks/useStoryGeneration.ts` +- `src/services/prompt.ts` +- `src/services/customWorldBuilder.ts` +- `src/services/customWorld.ts` +- `src/types/storyEngine.ts` +- `src/types/game.ts` +- `src/types/customWorld.ts` + +## 4.2 新增模块 + +- `src/services/storyEngine/scenarioPackRegistry.ts` +- `src/services/storyEngine/campaignPackCompiler.ts` +- `src/services/storyEngine/contentDependencyGraph.ts` +- `src/services/storyEngine/storySimulationRunner.ts` +- `src/services/storyEngine/playthroughMatrixLab.ts` +- `src/services/storyEngine/narrativeRegressionReplay.ts` +- `src/services/storyEngine/playerStyleProfiler.ts` +- `src/services/storyEngine/adaptiveNarrativeTuner.ts` +- `src/services/storyEngine/narrativeTelemetry.ts` +- `src/services/storyEngine/releaseGateReport.ts` +- `src/services/storyEngine/saveMigrationManifest.ts` +- `src/services/storyEngine/contentDiffReport.ts` + +## 4.3 第五阶段明确不做 + +第五阶段仍不做: + +1. 大规模联网运营后台 +2. 多人协作编辑器平台 +3. 商业级可视化内容管理系统 +4. 实时云端剧情编排服务 +5. 全自动商业数据分析平台 + +原因: + +第五阶段目标是把引擎推进到“可工业化生产和可持续运营”的层级,而不是直接变成完整 SaaS。 + +## 5. 第五阶段最小闭环 + +建议第五阶段最小闭环为: + +```text +ScenarioPackRegistry +-> CampaignPackCompiler +-> StorySimulationRunner +-> PlaythroughMatrixLab +-> PlayerStyleProfiler +-> AdaptiveNarrativeTuner +-> NarrativeTelemetry +-> ReleaseGateReport +-> SaveMigrationManifest +``` + +对应到实际价值,就是: + +1. campaign 可以被资产化管理 +2. 发布前可以批量仿真 +3. 上线后可以持续调优 +4. 更新时不会轻易打坏旧存档 + +## 6. 数据结构升级方案 + +## 6.1 `src/types/storyEngine.ts` + +第五阶段建议新增: + +```ts +export interface ScenarioPack { + id: string; + title: string; + version: string; + worldPackIds: string[]; + campaignIds: string[]; + sharedConstraintPackIds: string[]; +} + +export interface CampaignPack { + id: string; + scenarioPackId: string; + title: string; + authoringStyle: string; + campaignStateSeed: CampaignState; + actTemplates: ActState[]; + requiredCompanionIds: string[]; +} + +export interface PlayerStyleProfile { + id: string; + preferenceWeights: { + story: number; + exploration: number; + combat: number; + companion: number; + collection: number; + }; + dominantStyle: + | 'story_first' + | 'explorer' + | 'combat_driver' + | 'companion_bond' + | 'collector'; +} + +export interface SimulationRunResult { + id: string; + scenarioPackId: string; + campaignPackId: string; + seed: string; + endingId?: string | null; + activeThreadCountPeak: number; + fracturedCompanionCount: number; + issueCount: number; + summary: string; +} + +export interface ReleaseGateReport { + generatedAt: string; + status: 'pass' | 'warn' | 'block'; + summary: string; + blockingIssues: string[]; + simulationCoverage: number; +} + +export interface SaveMigrationManifest { + version: string; + requiredTransforms: string[]; + backwardCompatible: boolean; +} +``` + +并扩展: + +```ts +export interface StoryEngineMemoryState { + playerStyleProfile?: PlayerStyleProfile | null; + simulationRunResults?: SimulationRunResult[]; + releaseGateReport?: ReleaseGateReport | null; + saveMigrationManifest?: SaveMigrationManifest | null; +} +``` + +## 6.2 `src/types/customWorld.ts` + +第五阶段建议扩展: + +```ts +interface CustomWorldProfile { + scenarioPackId?: string | null; + campaignPackId?: string | null; +} +``` + +目的: + +- 让世界档案正式进入 pack 体系 + +## 6.3 `src/types/game.ts` + +第五阶段建议扩展: + +```ts +interface GameState { + activeScenarioPackId?: string | null; + activeCampaignPackId?: string | null; +} +``` + +## 7. 模块实现方案 + +## 7.1 `scenarioPackRegistry.ts` + +职责: + +1. 维护多个 `ScenarioPack` +2. 让不同 world / campaign / constraint pack 成为正式资产 + +建议导出: + +```ts +registerScenarioPack(pack) +resolveScenarioPack(id) +listScenarioPacks() +``` + +## 7.2 `campaignPackCompiler.ts` + +职责: + +1. 把现有 campaign 结构编译成标准 `CampaignPack` +2. 支持: + - 不同 authorial style + - 不同 required companions + - 不同 ending families + +建议导出: + +```ts +buildCampaignPack(params) +compileCampaignFromWorldProfile(params) +``` + +## 7.3 `contentDependencyGraph.ts` + +职责: + +1. 维护: + - campaign -> world + - campaign -> companion + - campaign -> thread + - campaign -> constraint pack + 之间的依赖关系 + +建议导出: + +```ts +buildContentDependencyGraph(params) +``` + +## 7.4 `storySimulationRunner.ts` + +职责: + +1. 跑单条 playthrough 仿真 +2. 输出: + - 走到哪类 ending + - 多少线程未回收 + - 队友收束情况 + - QA 问题数量 + +建议导出: + +```ts +runStorySimulation(params) +``` + +## 7.5 `playthroughMatrixLab.ts` + +职责: + +1. 用多组 seed、多种选择倾向跑批量仿真 +2. 汇总: + - ending 分布 + - unresolved thread 峰值 + - fractured companion 分布 + - branch budget 压力 + +建议导出: + +```ts +runPlaythroughMatrix(params) +buildMatrixSummary(params) +``` + +## 7.6 `narrativeRegressionReplay.ts` + +职责: + +1. 记录关键 playthrough 样本 +2. 在版本升级后回放,检查 narrative regressions + +建议导出: + +```ts +recordReplaySeed(params) +replayNarrativeRun(params) +``` + +## 7.7 `playerStyleProfiler.ts` + +职责: + +1. 根据玩家行为识别风格画像 +2. 依据: + - 选项偏好 + - 队友互动密度 + - 探索行为 + - 战斗投入 + - 收集倾向 + +建议导出: + +```ts +buildPlayerStyleProfile(state) +updatePlayerStyleProfileFromAction(params) +``` + +## 7.8 `adaptiveNarrativeTuner.ts` + +职责: + +1. 基于 `PlayerStyleProfile` 微调运行时导演参数 +2. 允许有限自适应: + - 更偏队友戏 + - 更偏探索残痕 + - 更偏高压冲突 + - 更偏调查展开 + +建议导出: + +```ts +resolveAdaptiveNarrativeBias(params) +applyAdaptiveTuningToPromptContext(params) +``` + +注意: + +- 这里必须是“有限微调”,不能破坏作者性和主命题 + +## 7.9 `narrativeTelemetry.ts` + +职责: + +1. 汇总关键 narrative runtime 指标 +2. 至少包括: + - 平均活跃线程数 + - 队友反应密度 + - ending family 分布 + - unresolved payoff 计数 + +建议导出: + +```ts +captureNarrativeTelemetry(params) +buildTelemetrySnapshot(params) +``` + +## 7.10 `releaseGateReport.ts` + +职责: + +1. 把: + - QA report + - simulation results + - branch budget + - unresolved threads + 汇总成正式发布门禁报告 + +建议导出: + +```ts +buildReleaseGateReport(params) +``` + +## 7.11 `saveMigrationManifest.ts` + +职责: + +1. 为不同版本 story engine state 定义迁移要求 +2. 确保多 campaign、多版本更新后旧存档还能继续用 + +建议导出: + +```ts +buildSaveMigrationManifest(params) +applyStoryEngineMigration(params) +``` + +## 7.12 `contentDiffReport.ts` + +职责: + +1. 比较两个 campaign/world/constraint pack 版本的差异 +2. 用于发布前评估改动面 + +建议导出: + +```ts +buildContentDiffReport(params) +``` + +## 8. 现有文件改造方案 + +## 8.1 `useStoryGeneration.ts` + +第五阶段新增: + +1. 读取 `PlayerStyleProfile` +2. 应用 `AdaptiveNarrativeTuner` +3. 把 adaptive bias 注入 prompt + +作用: + +- 让同一套 campaign 在不破坏作者性前提下,对不同玩家风格有更好的适配 + +## 8.2 `progressionActions.ts` + +第五阶段重点: + +1. 回写 `PlayerStyleProfile` +2. 回写 `SimulationRunResult`(仅调试/实验模式) +3. 回写 `ReleaseGateReport`(构建或工具态) +4. 维护 `SaveMigrationManifest` + +## 8.3 `prompt.ts` + +第五阶段改造目标: + +1. 增加 adaptive bias 提示 +2. 增加 authoring style / scenario pack 提示 +3. 继续控制不要让自适应破坏核心主题和 mandatory payoffs + +## 8.4 `customWorldBuilder.ts` / `customWorld.ts` + +第五阶段目标: + +1. 正式支持 `ScenarioPack / CampaignPack` 资产化 +2. 支持不同 pack 的约束包和版本号 +3. 支持 build diff / migration 检查 + +## 8.5 `useGamePersistence.ts` + +第五阶段重点: + +1. 引入 `SaveMigrationManifest` +2. 对 story engine state 做版本迁移 +3. 确保多 pack、多版本情况下 continue game 仍安全 + +## 9. 第五阶段开发顺序 + +建议顺序如下: + +1. `scenarioPackRegistry` +2. `campaignPackCompiler` +3. `contentDependencyGraph` +4. `playerStyleProfiler` +5. `adaptiveNarrativeTuner` +6. `storySimulationRunner` +7. `playthroughMatrixLab` +8. `narrativeRegressionReplay` +9. `narrativeTelemetry` +10. `releaseGateReport` +11. `saveMigrationManifest` +12. `contentDiffReport` + +原因: + +- 先没有 pack 和依赖图,就无法进入平台化 +- 先没有 player style profile,就无法做自适应 +- 先没有 simulation/regression,就无法真正做 release gate + +## 10. 测试方案 + +## 10.1 新增单元测试 + +建议新增: + +1. `scenarioPackRegistry.test.ts` +2. `campaignPackCompiler.test.ts` +3. `contentDependencyGraph.test.ts` +4. `playerStyleProfiler.test.ts` +5. `adaptiveNarrativeTuner.test.ts` +6. `storySimulationRunner.test.ts` +7. `playthroughMatrixLab.test.ts` +8. `narrativeRegressionReplay.test.ts` +9. `narrativeTelemetry.test.ts` +10. `releaseGateReport.test.ts` +11. `saveMigrationManifest.test.ts` +12. `contentDiffReport.test.ts` + +## 10.2 新增集成测试 + +建议补这 6 类: + +1. 同一 scenario pack 下多 campaign 装载 +2. 版本更新后旧存档迁移 +3. 同一 campaign 不同 player style 下的有限自适应 +4. playthrough matrix 跑出多种 ending family +5. release gate 正确拦截高风险版本 +6. regression replay 能检测 narrative drift + +## 10.3 人工回归场景 + +至少做这 6 条: + +1. 同一套世界切两条不同 campaign 运行 +2. 同一 campaign 用两种玩家风格跑出明显不同的节奏偏重 +3. 更新版本后旧存档继续游戏 +4. 运行一批 simulation 并生成 release gate report +5. narrative codex 在不同 campaign 间内容不串线 +6. ending 和 epilogue 在 regression replay 中保持稳定 + +## 11. 第五阶段验收标准 + +做到以下几点,才算第五阶段可收口: + +1. 系统支持 `ScenarioPack + CampaignPack` +2. 系统支持 `PlayerStyleProfile + AdaptiveNarrativeTuner` +3. 系统支持批量 `playthrough matrix simulation` +4. 系统支持 `ReleaseGateReport` +5. 系统支持 `SaveMigrationManifest` +6. 系统支持 `NarrativeTelemetry` +7. campaign 可以被复用、对比、回归和版本迁移 +8. 相关测试通过,`lint` / `typecheck` / `check:encoding` 通过 + +## 12. 一句话结论 + +前四阶段让当前项目拥有了一套从世界图谱到 campaign 收束都能运转的 AI 原生剧情引擎。 + +第五阶段要做的,是让它进一步成为: + +**一套能够持续生产多个 campaign、进行批量仿真评估、适配不同玩家风格、支持版本演化和发布门禁的叙事平台。** diff --git a/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE6_IMPLEMENTATION_PLAN_2026-04-06.md b/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE6_IMPLEMENTATION_PLAN_2026-04-06.md new file mode 100644 index 00000000..fa33bf63 --- /dev/null +++ b/docs/prd/AI_NATIVE_STORY_ENGINE_PHASE6_IMPLEMENTATION_PLAN_2026-04-06.md @@ -0,0 +1,559 @@ +# AI 原生剧情引擎第六阶段技术落地方案 + +更新时间:`2026-04-06` + +## 0. 是否需要第六阶段 + +结论先说: + +**需要,但第六阶段不应该继续主要围绕“再加一层 runtime 叙事引擎能力”展开,而应该转向“内容生产、策划协同、仿真可视化、版本治理和持续调优”的平台层。** + +原因很明确: + +按当前代码审计结果,前五阶段已经基本把这些核心能力做出来了: + +1. 世界图谱 +2. 角色叙事档案 +3. 信息可见性 +4. 线程与信号推进 +5. 队友反应与个人线 +6. 章节 / 旅程 / 高光导演 +7. campaign / act / ending / epilogue +8. codex / chronicle / QA report +9. scenario pack / campaign pack +10. simulation / regression / release gate / migration + +这说明: + +- “引擎能力不够”已经不是最核心的问题了 +- 接下来最大的瓶颈会变成: + - 内容生产效率 + - 策划可控性 + - 多版本治理 + - 调优闭环 + - 让设计师和内容作者能真正驾驭这套系统 + +所以第六阶段不是 “story engine phase 6 = 再造一个新 runtime 层”,而是: + +**让这套引擎从“强大的技术系统”,进化成“可被持续使用的剧情生产系统”。** + +## 1. 第五阶段审计结论 + +## 1.1 可以判定为“基本落地”的部分 + +按代码和测试情况,第五阶段已经基本落地: + +1. 第五阶段模块已存在并有测试: + - `scenarioPackRegistry` + - `campaignPackCompiler` + - `contentDependencyGraph` + - `storySimulationRunner` + - `playthroughMatrixLab` + - `narrativeRegressionReplay` + - `playerStyleProfiler` + - `adaptiveNarrativeTuner` + - `narrativeTelemetry` + - `releaseGateReport` + - `saveMigrationManifest` + - `contentDiffReport` + +2. 关键主链已接入: + - `progressionActions.ts` + - `useStoryGeneration.ts` + - `prompt.ts` + - `customWorldBuilder.ts` + - `useGamePersistence.ts` + +3. 第五阶段相关测试通过: + - `scenarioPackRegistry.test.ts` + - `campaignPackCompiler.test.ts` + - `contentDependencyGraph.test.ts` + - `storySimulationRunner.test.ts` + - `playthroughMatrixLab.test.ts` + - `narrativeRegressionReplay.test.ts` + - `playerStyleProfiler.test.ts` + - `adaptiveNarrativeTuner.test.ts` + - `narrativeTelemetry.test.ts` + - `releaseGateReport.test.ts` + - `saveMigrationManifest.test.ts` + - `contentDiffReport.test.ts` + +## 1.2 当前真实缺口 + +虽然第五阶段已经把“平台化骨架”做出来了,但它还明显偏: + +1. 后台能力 +2. 内部结构 +3. 代码级接口 + +还没有真正形成下面这些“生产工作台”能力: + +1. 设计师 / 内容作者可视化地理解: + - 当前 world pack + - 当前 campaign pack + - 当前 branch budget + - 当前 unresolved payoff + +2. 仿真结果可视化与比对 + - 现在能跑 simulation,但还缺: + - matrix dashboard + - ending 分布图 + - unresolved thread 热区 + - companion fracture 热图 + +3. 人机协同编辑闭环 + - 当前可以生成、回归、校验,但还缺: + - 人工批注 + - 审核通过 / 驳回 + - 局部重生成 + - 受控覆盖 + +4. 内容治理体系 + - 当前 pack 可以存在,但还缺: + - 版本状态 + - 草稿 / 候选 / 已发布 + - 回滚 + - 依赖冲突提示 + +5. 面向策划的 narrative ops + - 当前 QA report 存在,但还缺: + - 每日 / 每版本比较 + - 关键指标趋势 + - 哪条线程最常断 + - 哪个 ending 最少触发 + +## 1.3 额外需要注意的现实问题 + +本轮审计里还有一个工程事实需要单独记下: + +- 第五阶段相关测试通过 +- 但当前全仓 `lint` 仍不是完全绿色,存在 import 排序和未使用导入问题 + +这说明: + +**从第六阶段开始,除了继续扩功能,更应该把“生产平台自己的门禁和治理”一起正式纳入。** + +## 2. 第六阶段目标 + +第六阶段只做 5 件事: + +1. 建立 `Narrative Studio Workspace` 剧情工作台 +2. 建立 `Simulation Dashboard / Regression Review` 仿真与回归可视化 +3. 建立 `Human-in-the-loop Authoring Flow` 人机协同创作流 +4. 建立 `Pack Governance / Publish Workflow` 内容治理与发布流 +5. 建立 `Narrative Ops` 指标、比较、告警与持续调优流 + +一句话定义第六阶段: + +**从“叙事平台”升级到“叙事生产系统”。** + +## 3. 第六阶段完成定义 + +第六阶段完成后,至少要同时满足: + +1. 设计师不看代码,也能看懂当前 campaign 的结构状态。 +2. 可以用可视化方式查看 simulation / regression / release gate 的结果。 +3. 可以对 AI 产物做人工批注、重生成、局部覆盖和审批。 +4. `ScenarioPack / CampaignPack` 有正式的版本状态与发布流。 +5. narrative QA 和 telemetry 不只是生成结果,而是成为可比较、可追踪、可告警的运营资产。 + +## 4. 第六阶段范围 + +## 4.1 纳入范围 + +- `src/services/storyEngine/*` +- `src/components/*` +- `src/components/preset-editor/*` +- `src/components/CustomWorldGenerationView.tsx` +- `src/components/StateFunctionEditor.tsx` +- `src/components/SelectionCustomizationModals.tsx` +- `src/services/customWorld.ts` +- `src/services/customWorldBuilder.ts` +- `src/hooks/useGamePersistence.ts` +- `src/types/storyEngine.ts` + +## 4.2 新增模块 + +- `src/services/storyEngine/narrativeStudioState.ts` +- `src/services/storyEngine/simulationDashboardModel.ts` +- `src/services/storyEngine/reviewQueue.ts` +- `src/services/storyEngine/regenerationPatchPlan.ts` +- `src/services/storyEngine/packPublishWorkflow.ts` +- `src/services/storyEngine/packRollbackPlan.ts` +- `src/services/storyEngine/narrativeOpsMetrics.ts` +- `src/services/storyEngine/narrativeAlerting.ts` +- `src/services/storyEngine/narrativeReviewNotes.ts` +- `src/services/storyEngine/qaTrendReport.ts` + +## 4.3 第六阶段明确不做 + +第六阶段仍不做: + +1. 全在线多人协同编辑器 +2. 完整云端数据平台 +3. 商业 BI 大盘系统 +4. 全自动策划代理替代人工审核 +5. 对外开放市场级内容生态 + +原因: + +第六阶段的目标是让内部生产真正运转起来,而不是直接做成对外产品平台。 + +## 5. 第六阶段最小闭环 + +建议第六阶段最小闭环为: + +```text +NarrativeStudioState +-> SimulationDashboardModel +-> ReviewQueue +-> RegenerationPatchPlan +-> PackPublishWorkflow +-> NarrativeOpsMetrics +-> QaTrendReport +-> NarrativeAlerting +``` + +对应到实际价值,就是: + +1. 可以看到当前内容状态 +2. 可以看 simulation 结果 +3. 可以对问题内容提出修复 +4. 可以受控发布和回滚 +5. 可以持续观察质量变化 + +## 6. 数据结构升级方案 + +## 6.1 `src/types/storyEngine.ts` + +第六阶段建议新增: + +```ts +export interface NarrativeReviewNote { + id: string; + targetId: string; + targetType: 'thread' | 'chapter' | 'companion' | 'carrier' | 'ending' | 'pack'; + note: string; + status: 'open' | 'addressed' | 'ignored'; +} + +export interface ReviewQueueItem { + id: string; + source: 'qa_report' | 'simulation' | 'manual'; + priority: 'low' | 'medium' | 'high'; + summary: string; + relatedIds: string[]; +} + +export interface PackPublishState { + packId: string; + version: string; + status: 'draft' | 'candidate' | 'released' | 'rolled_back'; + lastPublishedAt?: string | null; +} + +export interface NarrativeOpsSnapshot { + generatedAt: string; + activePackCount: number; + releaseGatePassRate: number; + unresolvedIssueCount: number; + topRiskAreas: string[]; +} + +export interface QaTrendPoint { + label: string; + issueCount: number; + blockingCount: number; +} +``` + +并扩展: + +```ts +export interface StoryEngineMemoryState { + reviewNotes?: NarrativeReviewNote[]; + reviewQueue?: ReviewQueueItem[]; + publishStates?: PackPublishState[]; + narrativeOpsSnapshot?: NarrativeOpsSnapshot | null; + qaTrend?: QaTrendPoint[]; +} +``` + +## 7. 模块实现方案 + +## 7.1 `narrativeStudioState.ts` + +职责: + +1. 聚合当前: + - campaign 状态 + - branch budget + - QA report + - ending 状态 + - simulation 结果 + +建议导出: + +```ts +buildNarrativeStudioState(params) +``` + +## 7.2 `simulationDashboardModel.ts` + +职责: + +1. 把 matrix / replay / telemetry 结果整理成可展示模型 +2. 输出: + - ending 分布 + - unresolved threads 排行 + - fracture companion 排行 + - regression failure 列表 + +建议导出: + +```ts +buildSimulationDashboardModel(params) +``` + +## 7.3 `reviewQueue.ts` + +职责: + +1. 把 QA、simulation、手工批注统一收敛成 review queue +2. 支持优先级排序与状态流转 + +建议导出: + +```ts +buildReviewQueue(params) +advanceReviewQueueItem(params) +``` + +## 7.4 `regenerationPatchPlan.ts` + +职责: + +1. 把 review item 转成局部重生成 / 局部 override 计划 +2. 支持: + - 角色档案重生成 + - 线程补偿 + - ending 修复 + - 文书 / 载体文本修复 + +建议导出: + +```ts +buildRegenerationPatchPlan(params) +``` + +## 7.5 `packPublishWorkflow.ts` + +职责: + +1. 管理 pack 的发布状态 +2. 要求: + - 发布前通过 release gate + - 生成 publish record + +建议导出: + +```ts +advancePackPublishState(params) +canPublishPack(params) +``` + +## 7.6 `packRollbackPlan.ts` + +职责: + +1. 对已发布 pack 生成回滚方案 +2. 和 `SaveMigrationManifest` 协作 + +建议导出: + +```ts +buildPackRollbackPlan(params) +``` + +## 7.7 `narrativeOpsMetrics.ts` + +职责: + +1. 聚合 narrative ops 指标 +2. 输出: + - pass rate + - issue density + - regression stability + - ending diversity + +建议导出: + +```ts +buildNarrativeOpsSnapshot(params) +``` + +## 7.8 `narrativeAlerting.ts` + +职责: + +1. 对: + - release gate blocking + - regression drift + - unresolved issue spike + 发出告警 + +建议导出: + +```ts +evaluateNarrativeAlerts(params) +``` + +## 7.9 `narrativeReviewNotes.ts` + +职责: + +1. 让人工可以对: + - chapter + - ending + - thread + - companion + 记录审核笔记 + +建议导出: + +```ts +appendNarrativeReviewNote(params) +resolveNarrativeReviewNote(params) +``` + +## 7.10 `qaTrendReport.ts` + +职责: + +1. 把多次 QA report 做趋势比较 +2. 输出: + - issue 趋势 + - block 趋势 + - 最常出问题区域 + +建议导出: + +```ts +buildQaTrendReport(params) +``` + +## 8. 现有文件改造方案 + +## 8.1 `progressionActions.ts` + +第六阶段重点: + +1. 回写 `reviewQueue` +2. 回写 `narrativeOpsSnapshot` +3. 回写 `qaTrend` + +## 8.2 `useStoryGeneration.ts` + +第六阶段重点: + +1. 在 studio/debug 模式下暴露更多状态 +2. 不污染正式运行时主链 + +## 8.3 `CustomWorldGenerationView.tsx` + +建议成为第六阶段的第一个“工作台入口”,用于展示: + +1. 当前 pack +2. 当前 release gate +3. 当前 QA trend +4. 当前 simulation 摘要 + +## 8.4 `StateFunctionEditor.tsx` / `preset-editor/*` + +建议逐步增加 narrative review / patch plan 的入口,而不是只做底层数据编辑。 + +## 8.5 `useGamePersistence.ts` + +继续强化: + +1. pack version +2. migration status +3. rollback safety + +## 9. 第六阶段开发顺序 + +建议顺序如下: + +1. `reviewQueue` +2. `narrativeReviewNotes` +3. `simulationDashboardModel` +4. `narrativeOpsMetrics` +5. `qaTrendReport` +6. `packPublishWorkflow` +7. `packRollbackPlan` +8. `regenerationPatchPlan` +9. `narrativeAlerting` +10. `narrativeStudioState` +11. UI 工作台最小接线 + +原因: + +- 第六阶段的重点是让“平台能力真正可操作” +- 所以先做 review / dashboard / publish flow,比继续堆 runtime 更有价值 + +## 10. 测试方案 + +## 10.1 新增单元测试 + +建议新增: + +1. `reviewQueue.test.ts` +2. `narrativeReviewNotes.test.ts` +3. `simulationDashboardModel.test.ts` +4. `narrativeOpsMetrics.test.ts` +5. `qaTrendReport.test.ts` +6. `packPublishWorkflow.test.ts` +7. `packRollbackPlan.test.ts` +8. `regenerationPatchPlan.test.ts` +9. `narrativeAlerting.test.ts` +10. `narrativeStudioState.test.ts` + +## 10.2 新增集成测试 + +建议补这 5 类: + +1. QA report -> review queue -> patch plan +2. simulation matrix -> dashboard -> release gate +3. pack candidate -> publish -> rollback +4. old save -> migration -> continue game +5. 多版本 QA trend 比较 + +## 10.3 人工回归场景 + +至少做这 5 条: + +1. 打开 narrative 工作台能看懂当前 campaign 状态 +2. 一次 regression drift 能正确进入 review queue +3. 一个 pack 通过 release gate 成功发布 +4. 一个 pack 发布后可以生成 rollback plan +5. 一组 QA trend 能看出问题是在变好还是变坏 + +## 11. 第六阶段验收标准 + +做到以下几点,才算第六阶段可收口: + +1. 系统具备可视化 narrative studio 状态模型 +2. 系统具备 review queue 和 review note 流程 +3. 系统具备 simulation dashboard 模型 +4. 系统具备 publish / rollback workflow +5. 系统具备 narrative ops snapshot 和 QA trend +6. 人工审核与 AI 重生成之间形成闭环 +7. 相关测试通过,`lint` / `typecheck` / `check:encoding` 通过 + +## 12. 一句话结论 + +如果前五阶段解决的是“把 AI 原生剧情引擎做成一套完整可运行、可发布、可复用的平台”, + +那么第六阶段要解决的,就是: + +**让这套平台真正变成团队可以长期驾驭、持续生产、持续审核、持续迭代的叙事生产系统。** diff --git a/docs/FUNCTION_SCRIPT_CATALOG_2026-04-04.md b/docs/reference/FUNCTION_SCRIPT_CATALOG_2026-04-04.md similarity index 100% rename from docs/FUNCTION_SCRIPT_CATALOG_2026-04-04.md rename to docs/reference/FUNCTION_SCRIPT_CATALOG_2026-04-04.md diff --git a/docs/reference/README.md b/docs/reference/README.md new file mode 100644 index 00000000..c1d1b9e4 --- /dev/null +++ b/docs/reference/README.md @@ -0,0 +1,10 @@ +# 参考目录 + +## 当前入口 + +- [FUNCTION_SCRIPT_CATALOG_2026-04-04.md](./FUNCTION_SCRIPT_CATALOG_2026-04-04.md):Function 独立脚本目录与分类速查。 + +## 使用建议 + +- 需要快速定位 Function 脚本,而不是阅读长篇方案时,优先看这里。 +- 如果要评估 Function 分层是否合理,再配合 `docs/audits/FUNCTION_DESIGN_AUDIT_2026-04-03.md` 一起看。 diff --git a/docs/AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md b/docs/technical/AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md similarity index 100% rename from docs/AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md rename to docs/technical/AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md diff --git a/docs/PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md b/docs/technical/PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md similarity index 100% rename from docs/PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md rename to docs/technical/PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md diff --git a/docs/technical/README.md b/docs/technical/README.md new file mode 100644 index 00000000..89da250e --- /dev/null +++ b/docs/technical/README.md @@ -0,0 +1,14 @@ +# 技术方案 + +这一组文档偏技术选型、实现路线和外部产品形态拆解。 + +## 文档列表 + +- [AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md](./AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md):AI 生成角色形象与角色动画的技术路线。 +- [PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md](./PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md):PixelMotion 产品形态与能力拆解。 +- [SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md](./SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md):服务端部署、代理层与 CORS 方案。 + +## 使用建议 + +- 做实现选型时,优先看这一组。 +- 做阶段排期时,把这一组和 `docs/planning/`、`docs/prd/` 一起看,更容易判断先后顺序。 diff --git a/docs/SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md b/docs/technical/SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md similarity index 98% rename from docs/SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md rename to docs/technical/SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md index c6e34062..4703b585 100644 --- a/docs/SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md +++ b/docs/technical/SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md @@ -53,12 +53,12 @@ ### 2.3 工程审查文档也已经指出同样风险 -[docs/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md](/E:/Repos/Genarrative/docs/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md) 明确沉淀过一条经验: +[docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md](/E:/Repos/Genarrative/docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md) 明确沉淀过一条经验: - 浏览器直连会遇到 CORS - 更稳的方案是开发服务器代理,再由前端请求 `/api/llm/...` -[docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md](/E:/Repos/Genarrative/docs/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md) 也明确指出: +[docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md](/E:/Repos/Genarrative/docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md) 也明确指出: - 编辑器、运行时、类后端能力全部耦合在 Vite 配置里 - 未来如果做独立部署、多人协作、远程编辑、权限控制,会非常难迁移 diff --git a/scripts/check-encoding.mjs b/scripts/check-encoding.mjs index ef454821..4c0c7d00 100644 --- a/scripts/check-encoding.mjs +++ b/scripts/check-encoding.mjs @@ -41,6 +41,8 @@ const EXCLUDED_PREFIXES = [ '.codex-logs/', '.git/', 'dist/', + 'dist_check/', + 'dist_check_monster_position/', 'media/', 'node_modules/', 'public/Icons/', diff --git a/scripts/smoke-content.ts b/scripts/smoke-content.ts index 349d55ac..586eac7c 100644 --- a/scripts/smoke-content.ts +++ b/scripts/smoke-content.ts @@ -12,8 +12,8 @@ import { createEmptyEquipmentLoadout, getEquipmentBonuses, } from '../src/data/equipmentEffects.ts'; +import { createSceneHostileNpcsFromIds } from '../src/data/hostileNpcs.ts'; import { isInventoryItemUsable, resolveInventoryItemUseEffect } from '../src/data/inventoryEffects.ts'; -import { createSceneMonstersFromIds } from '../src/data/monsters.ts'; import { buildInitialNpcState, buildInitialPlayerInventory, buildNpcEncounterStoryMoment, checkTradeItem, createNpcBattleMonster } from '../src/data/npcInteractions.ts'; import { acceptQuest, @@ -26,7 +26,7 @@ import { import { createInitialGameRuntimeStats } from '../src/data/runtimeStats.ts'; import { createSceneCallOutEncounter, createSceneEncounterPreview, ensureSceneEncounterPreview } from '../src/data/sceneEncounterPreviews.ts'; import { buildSceneObserveSignsStoryText } from '../src/data/sceneObservation.ts'; -import { getScenePresetsByWorld } from '../src/data/scenePresets.ts'; +import { getSceneHostileNpcPresetIds, getScenePresetsByWorld } from '../src/data/scenePresets.ts'; import { buildTreasureEncounterStoryMoment, buildTreasureResultText, resolveTreasureReward } from '../src/data/treasureInteractions.ts'; import { AnimationState, GameState, WorldType } from '../src/types.ts'; @@ -55,7 +55,7 @@ function createBaseState(worldType: WorldType, sceneId?: string): GameState { currentEncounter: null, npcInteractionActive: false, currentScenePreset, - sceneMonsters: [], + sceneHostileNpcs: [], playerX: 0, playerOffsetY: 0, playerFacing: 'right', @@ -92,10 +92,10 @@ function smokeScenePreviews() { const preview = createSceneEncounterPreview(createBaseState(worldType, scene.id)); assert(preview.currentEncounter?.kind !== 'treasure', `[preview] treasure encounter should be disabled for ${worldType}`); - assert(preview.currentEncounter || preview.sceneMonsters.length > 0 || scene.treasureHints.length === 0, `[preview] ${scene.id} produced no preview entity`); + assert(preview.currentEncounter || preview.sceneHostileNpcs.length > 0 || scene.treasureHints.length === 0, `[preview] ${scene.id} produced no preview entity`); const ensured = ensureSceneEncounterPreview(createBaseState(worldType, scene.id)); - assert(ensured.currentEncounter || ensured.sceneMonsters.length > 0 || scene.treasureHints.length === 0, `[preview] ${scene.id} failed ensureSceneEncounterPreview`); + assert(ensured.currentEncounter || ensured.sceneHostileNpcs.length > 0 || scene.treasureHints.length === 0, `[preview] ${scene.id} failed ensureSceneEncounterPreview`); } } @@ -163,20 +163,21 @@ function smokeTreasureStories() { function smokeMonsterCreation() { for (const worldType of [WorldType.WUXIA, WorldType.XIANXIA]) { - const sceneWithMonster = getScenePresetsByWorld(worldType).find(scene => scene.monsterIds.length > 0); + const sceneWithMonster = getScenePresetsByWorld(worldType).find(scene => getSceneHostileNpcPresetIds(scene).length > 0); assert(sceneWithMonster, `[monster] missing monster scene for ${worldType}`); - const monsters = createSceneMonstersFromIds(worldType, sceneWithMonster.monsterIds, 0); + const hostileNpcPresetIds = getSceneHostileNpcPresetIds(sceneWithMonster); + const monsters = createSceneHostileNpcsFromIds(worldType, hostileNpcPresetIds, 0); assert(monsters.length > 0, `[monster] ${sceneWithMonster.id} failed to create scene monsters`); assert( - monsters.length === Math.min(sceneWithMonster.monsterIds.length, 3), + monsters.length === Math.min(hostileNpcPresetIds.length, 3), `[monster] ${sceneWithMonster.id} should keep the full configured encounter group`, ); const resolvedState = createBaseState(worldType, sceneWithMonster.id); - resolvedState.sceneMonsters = monsters; + resolvedState.sceneHostileNpcs = monsters; resolvedState.inBattle = true; assert( - resolvedState.sceneMonsters.length === monsters.length, + resolvedState.sceneHostileNpcs.length === monsters.length, `[monster] ${sceneWithMonster.id} multi-enemy battle state lost monsters`, ); } @@ -206,7 +207,7 @@ function smokeObserveAndCallOut() { const baseState = createBaseState(worldType, scene.id); const callOutResult = createSceneCallOutEncounter(baseState); assert(callOutResult.currentEncounter?.kind !== 'treasure', `[idle] treasure call_out should be disabled for ${worldType}`); - assert(callOutResult.currentEncounter || callOutResult.sceneMonsters.length > 0 || scene.monsterIds.length === 0, `[idle] call_out failed for ${scene.id}`); + assert(callOutResult.currentEncounter || callOutResult.sceneHostileNpcs.length > 0 || getSceneHostileNpcPresetIds(scene).length === 0, `[idle] call_out failed for ${scene.id}`); const observeText = buildSceneObserveSignsStoryText(worldType, scene.id); assert(observeText.length > 12, `[idle] observe_signs text too short for ${scene.id}`); @@ -281,19 +282,20 @@ function smokeTradeEconomyLoop() { function smokeEncounterTransitionLoop() { for (const worldType of [WorldType.WUXIA, WorldType.XIANXIA]) { - const sceneWithMonster = getScenePresetsByWorld(worldType).find(scene => scene.monsterIds.length >= 2); + const sceneWithMonster = getScenePresetsByWorld(worldType).find(scene => getSceneHostileNpcPresetIds(scene).length >= 2); assert(sceneWithMonster, `[transition] missing multi-monster scene for ${worldType}`); - const finalMonsters = createSceneMonstersFromIds(worldType, sceneWithMonster.monsterIds, 0); + const hostileNpcPresetIds = getSceneHostileNpcPresetIds(sceneWithMonster); + const finalMonsters = createSceneHostileNpcsFromIds(worldType, hostileNpcPresetIds, 0); const finalState = { ...createBaseState(worldType, sceneWithMonster.id), inBattle: true, - sceneMonsters: finalMonsters, + sceneHostileNpcs: finalMonsters, }; const previewState = { ...finalState, inBattle: false, - sceneMonsters: finalMonsters.map((monster, index) => ({ + sceneHostileNpcs: finalMonsters.map((monster, index) => ({ ...monster, xMeters: 12 + (index * 1.8), })), @@ -301,15 +303,15 @@ function smokeEncounterTransitionLoop() { const transitionState = buildEncounterTransitionState(finalState, previewState); assert( - transitionState.sceneMonsters[1]?.xMeters === previewState.sceneMonsters[1]?.xMeters, + transitionState.sceneHostileNpcs[1]?.xMeters === previewState.sceneHostileNpcs[1]?.xMeters, `[transition] second monster should keep its preview x during transition for ${worldType}`, ); const halfwayState = interpolateEncounterTransitionState(transitionState, finalState, 0.5); assert( - halfwayState.sceneMonsters.every((monster, index) => { - const startX = transitionState.sceneMonsters[index]?.xMeters ?? monster.xMeters; - const endX = finalState.sceneMonsters[index]?.xMeters ?? monster.xMeters; + halfwayState.sceneHostileNpcs.every((monster, index) => { + const startX = transitionState.sceneHostileNpcs[index]?.xMeters ?? monster.xMeters; + const endX = finalState.sceneHostileNpcs[index]?.xMeters ?? monster.xMeters; return monster.xMeters !== startX && monster.xMeters !== endX; }), `[transition] all monsters should interpolate instead of only the first one for ${worldType}`, @@ -317,7 +319,7 @@ function smokeEncounterTransitionLoop() { const offscreenState = buildEncounterEntryState(finalState, 18); assert( - offscreenState.sceneMonsters.every(monster => monster.xMeters >= 18), + offscreenState.sceneHostileNpcs.every(monster => monster.xMeters >= 18), `[transition] offscreen entry should place the entire encounter group offscreen for ${worldType}`, ); } @@ -361,7 +363,7 @@ function smokeRosterLoop() { function smokeQuestLoop() { for (const worldType of [WorldType.WUXIA, WorldType.XIANXIA]) { const sceneWithNpcAndMonster = getScenePresetsByWorld(worldType).find( - scene => scene.npcs.length > 0 && scene.monsterIds.length > 0, + scene => scene.npcs.length > 0 && getSceneHostileNpcPresetIds(scene).length > 0, ); assert(sceneWithNpcAndMonster, `[quest] missing npc+monster scene for ${worldType}`); diff --git a/scripts/validate-content.ts b/scripts/validate-content.ts index 67530076..a79cfd6e 100644 --- a/scripts/validate-content.ts +++ b/scripts/validate-content.ts @@ -1,6 +1,6 @@ import { getCharacterHomeSceneId, getCharacterNpcSceneIds, PRESET_CHARACTERS } from '../src/data/characterPresets.ts'; import { MONSTER_PRESETS_BY_WORLD } from '../src/data/hostileNpcPresets.ts'; -import { getScenePresetsByWorld } from '../src/data/scenePresets.ts'; +import { getSceneHostileNpcPresetIds, getScenePresetsByWorld } from '../src/data/scenePresets.ts'; import { buildStateFunctionDefinitions } from '../src/data/stateFunctions.ts'; import { WorldType } from '../src/types.ts'; @@ -32,7 +32,7 @@ function validateScenes(errors: string[]) { } }); - scene.monsterIds.forEach(monsterId => { + getSceneHostileNpcPresetIds(scene).forEach(monsterId => { if (!monsterIdSet.has(monsterId)) { addError(errors, `[scene] ${scene.id} references unknown monster "${monsterId}" in ${worldType}`); } diff --git a/scripts/validate-overrides.ts b/scripts/validate-overrides.ts index 226148f9..709b7c5a 100644 --- a/scripts/validate-overrides.ts +++ b/scripts/validate-overrides.ts @@ -87,12 +87,12 @@ function validateMonsterOverrides(errors: string[]) { const overrides = readJsonFile>('src/data/monsterOverrides.json'); if (!expectPlainObject(errors, 'monsterOverrides', overrides)) return; - const monsterIds = new Set( + const hostilePresetIds = new Set( [...MONSTER_PRESETS_BY_WORLD[WorldType.WUXIA], ...MONSTER_PRESETS_BY_WORLD[WorldType.XIANXIA]].map(monster => monster.id), ); Object.entries(overrides).forEach(([monsterId, override]) => { - if (!monsterIds.has(monsterId)) { + if (!hostilePresetIds.has(monsterId)) { errors.push(`[override] monsterOverrides contains unknown monster id "${monsterId}"`); return; } @@ -109,10 +109,6 @@ function validateSceneOverrides(errors: string[]) { const sceneIds = new Set( [WorldType.WUXIA, WorldType.XIANXIA].flatMap(worldType => getScenePresetsByWorld(worldType).map(scene => scene.id)), ); - const monsterIds = new Set( - [...MONSTER_PRESETS_BY_WORLD[WorldType.WUXIA], ...MONSTER_PRESETS_BY_WORLD[WorldType.XIANXIA]].map(monster => monster.id), - ); - Object.entries(overrides).forEach(([sceneId, override]) => { if (!sceneIds.has(sceneId)) { errors.push(`[override] sceneOverrides contains unknown scene id "${sceneId}"`); @@ -134,13 +130,6 @@ function validateSceneOverrides(errors: string[]) { errors.push(`[override] sceneOverrides["${sceneId}"] has invalid connectedSceneIds`); } } - - const overrideMonsterIds = override.monsterIds; - if (overrideMonsterIds !== undefined) { - if (!Array.isArray(overrideMonsterIds) || overrideMonsterIds.some(id => typeof id !== 'string' || !monsterIds.has(id))) { - errors.push(`[override] sceneOverrides["${sceneId}"] has invalid monsterIds`); - } - } }); } diff --git a/src/components/AdventureEntityModal.tsx b/src/components/AdventureEntityModal.tsx index 05c7fa47..cb169ae7 100644 --- a/src/components/AdventureEntityModal.tsx +++ b/src/components/AdventureEntityModal.tsx @@ -1,6 +1,6 @@ import { X } from 'lucide-react'; import { AnimatePresence, motion } from 'motion/react'; -import type { CSSProperties, ReactNode } from 'react'; +import type { ReactNode } from 'react'; import { useEffect, useMemo, useState } from 'react'; import { @@ -9,16 +9,13 @@ import { } from '../data/affinityLevels'; import { buildRelationState, - formatAttributeList, resolveAttributeSchema, resolveCharacterAttributeProfile, } from '../data/attributeResolver'; import { - type BuildDamageBreakdown, formatBuildContributionPercent, getBuildContributionAttributeRows, getBuildContributionQualityLabel, - getBuildContributionQualityRatio, getCompanionBuildDamageBreakdown, getPlayerBuildDamageBreakdown, resolveMonsterOutgoingDamage, @@ -46,6 +43,7 @@ import { createNpcBattleMonster, normalizeNpcPersistentState, } from '../data/npcInteractions'; +import { getSceneHostileNpcPresetIds } from '../data/scenePresets'; import type { CharacterChatTarget } from '../hooks/useStoryGeneration'; import { getResourceLabelsForWorld } from '../services/customWorldPresentation'; import { @@ -64,6 +62,18 @@ import { type BackstoryUnlockedChapter, } from './BackstoryArchive'; import { CharacterAnimator } from './CharacterAnimator'; +import { + getCharacterDetailSpriteStyle, + getContributionVisualStyle, + getSkillDeliveryLabel, + getSkillStyleLabel, +} from './CharacterInfoHelpers'; +import { + CharacterAttributeGrid, + CharacterSkillsList, + MultiplierContributionList, + StatusRow, +} from './CharacterInfoShared'; import { GENERIC_NPC_SCENE_SCALE } from './game-canvas/GameCanvasShared'; import type { GameCanvasEntitySelection } from './GameCanvas'; import { HostileNpcAnimator } from './HostileNpcAnimator'; @@ -110,41 +120,6 @@ function estimateNpcMaxMana(character: Character | null) { return character ? getCharacterMaxMana(character) : 0; } -function StatBar({ - label, - current, - max, - tone, -}: { - label: string; - current: number; - max: number; - tone: 'hp' | 'mp'; -}) { - const ratio = Math.max(0, Math.min(1, max > 0 ? current / max : 0)); - const fillClass = - tone === 'hp' - ? 'from-emerald-400 via-lime-300 to-emerald-200' - : 'from-sky-500 via-cyan-300 to-sky-100'; - - return ( -
-
- {label} - - {current} / {max} - -
-
-
-
-
- ); -} - function Section({ title, children }: { title: string; children: ReactNode }) { return (
@@ -156,32 +131,12 @@ function Section({ title, children }: { title: string; children: ReactNode }) { ); } -const SKILL_STYLE_LABELS = { - burst: '爆发', - steady: '稳态', - mobility: '机动', - finisher: '终结', - projectile: '投射', -} satisfies Record; - -type ContributionRow = BuildDamageBreakdown['rows'][number]; - -function getSkillDeliveryLabel(skill: Character['skills'][number]) { - return skill.delivery === 'ranged' || skill.style === 'projectile' - ? '远程' - : '近战'; -} - -function getSkillStyleLabel(skill: Character['skills'][number]) { - return SKILL_STYLE_LABELS[skill.style]; -} - function resolveSkillPreviewMonsterId(gameState: GameState) { if (!gameState.worldType) { return null; } - const sceneMonsterId = gameState.currentScenePreset?.monsterIds?.[0] ?? null; + const sceneMonsterId = getSceneHostileNpcPresetIds(gameState.currentScenePreset)[0] ?? null; if (sceneMonsterId) { return sceneMonsterId; } @@ -189,121 +144,6 @@ function resolveSkillPreviewMonsterId(gameState: GameState) { return getMonsterPresetsByWorld(gameState.worldType)[0]?.id ?? null; } -function getContributionHeatRatio(value: number) { - return getBuildContributionQualityRatio(value); -} - -function getContributionVisualStyle(value: number): CSSProperties { - const ratio = getContributionHeatRatio(value); - const hue = 210 - ratio * 178; - const saturation = 62 + ratio * 16; - const lightness = 56 + ratio * 6; - - return { - borderColor: `hsla(${hue}, ${saturation + 6}%, ${lightness}%, ${0.22 + ratio * 0.28})`, - background: `linear-gradient(135deg, hsla(${hue}, ${saturation + 10}%, ${36 + ratio * 12}%, ${0.18 + ratio * 0.22}) 0%, rgba(12, 16, 24, 0.94) 72%)`, - boxShadow: `inset 0 1px 0 rgba(255,255,255,0.04), 0 0 ${10 + ratio * 20}px hsla(${hue}, ${saturation + 14}%, ${58 + ratio * 8}%, ${0.12 + ratio * 0.18})`, - color: - ratio > 0.76 - ? 'rgb(255 244 235)' - : ratio > 0.32 - ? 'rgb(236 242 248)' - : 'rgb(203 213 225)', - }; -} - -function MultiplierContributionList({ - breakdown, - onSelectContribution, -}: { - breakdown: BuildDamageBreakdown; - onSelectContribution: (row: ContributionRow) => void; -}) { - const sortedRows = [...breakdown.rows].sort( - (left, right) => - right.bonusDelta - left.bonusDelta || - left.label.localeCompare(right.label, 'zh-CN'), - ); - - return ( -
-
- 状态标签 - 点击标签查看具体属性加成 -
- {sortedRows.length > 0 ? ( -
- {sortedRows.map((row) => ( - - ))} -
- ) : ( - - 当前还没有形成有效标签 - - )} -
- ); -} - -function CharacterSkills({ - skills, - onSelectSkill, -}: { - skills: Character['skills']; - onSelectSkill: (skillId: string) => void; -}) { - if (skills.length === 0) { - return
暂无技能信息
; - } - - return ( -
- {skills.map((skill) => ( - - ))} -
- ); -} - function buildPreviewInventoryDescription( characterName: string, item: { category: string; name: string; quantity: number }, @@ -563,11 +403,10 @@ export function AdventureEntityModal({ buildInitialNpcState(npcEncounter, gameState.worldType, gameState), ) : null; - const hostileNpcPresetId = - npcEncounter?.hostileNpcPresetId ?? npcEncounter?.hostileNpcPresetId; + const monsterPresetId = npcEncounter?.monsterPresetId ?? null; const hostileNpcPreset = - hostileNpcPresetId && gameState.worldType - ? getHostileNpcPresetById(gameState.worldType, hostileNpcPresetId) + monsterPresetId && gameState.worldType + ? getHostileNpcPresetById(gameState.worldType, monsterPresetId) : null; const npcBattleState = selection?.kind === 'npc' ? (selection.battleState ?? null) : null; @@ -768,9 +607,6 @@ export function AdventureEntityModal({ customWorldProfile: gameState.customWorldProfile, }) : null; - const attributeRows = selectedAttributeProfile - ? formatAttributeList(selectedAttributeProfile, attributeSchema) - : []; const resourceLabels = getResourceLabelsForWorld( gameState.worldType, gameState.customWorldProfile, @@ -849,6 +685,31 @@ export function AdventureEntityModal({ inventory.find((item) => item.id === selectedItemId) ?? null; const selectedSkillOwnerName = detailCharacter?.name ?? npcEncounter?.npcName ?? title; + const recentChronicleEntries = gameState.storyEngineMemory?.chronicle?.slice(-3) ?? []; + const recentCarrierEchoes = (gameState.storyEngineMemory?.recentCarrierIds ?? []) + .map((carrierId) => + gameState.playerInventory.find((item) => item.id === carrierId)?.runtimeMetadata?.storyFingerprint?.visibleClue + ?? gameState.playerInventory.find((item) => item.id === carrierId)?.name + ?? '', + ) + .filter(Boolean) + .slice(0, 3); + const sceneResidues = gameState.currentScenePreset?.narrativeResidues?.slice(0, 3) ?? []; + const selectedCompanionResolution = + detailCharacter + ? gameState.storyEngineMemory?.companionResolutions?.find( + (resolution) => resolution.characterId === detailCharacter.id, + ) ?? null + : null; + const relatedConsequences = (gameState.storyEngineMemory?.consequenceLedger ?? []) + .filter((record) => + detailCharacter + ? record.relatedIds.includes(detailCharacter.id) + : npcEncounter + ? record.relatedIds.includes(npcEncounter.id ?? npcEncounter.npcName) + : false, + ) + .slice(-3); useEffect(() => { setSelectedSkillId(null); @@ -921,6 +782,9 @@ export function AdventureEntityModal({ character={playerCharacter} className="h-full w-full" imageClassName="object-bottom" + style={getCharacterDetailSpriteStyle( + playerCharacter, + )} /> ) : selection.kind === 'companion' && companionCharacter ? ( @@ -929,6 +793,9 @@ export function AdventureEntityModal({ character={companionCharacter} className="h-full w-full" imageClassName="object-bottom" + style={getCharacterDetailSpriteStyle( + companionCharacter, + )} /> ) : npcCharacter ? ( ) : hostileNpcPreset ? ( ) : null} + {(recentChronicleEntries.length > 0 || + recentCarrierEchoes.length > 0 || + sceneResidues.length > 0 || + relatedConsequences.length > 0 || + Boolean(selectedCompanionResolution)) && ( +
+
+ {selectedCompanionResolution && ( +
+ 队友收束:{selectedCompanionResolution.resolutionType} · {selectedCompanionResolution.summary} +
+ )} + {relatedConsequences.length > 0 && ( +
+ {relatedConsequences.map((record) => ( +
+ {record.title} + {':'} + {record.summary} +
+ ))} +
+ )} + {recentChronicleEntries.length > 0 && ( +
+ {recentChronicleEntries.map((entry) => ( +
+
+ {entry.title} +
+
+ {entry.summary} +
+
+ ))} +
+ )} + {recentCarrierEchoes.length > 0 && ( +
+ 载体回响:{recentCarrierEchoes.join(';')} +
+ )} + {sceneResidues.length > 0 && ( +
+ {sceneResidues.map((residue) => ( +
+ {residue.title} + {':'} + {residue.visibleClue} +
+ ))} +
+ )} +
+
+ )} +
- {maxMana > 0 ? ( - ) : null} - {attributeRows.length > 0 ? ( -
- {attributeRows.map(({ slot, value }) => ( -
-
- {slot.name} -
-
- {value} -
-
- {slot.definition} -
-
- ))} -
- ) : ( -
- 暂无属性信息 -
- )} +
{detailCharacter ? (
-
) : displayedSkills.length > 0 ? (
- @@ -1202,7 +1120,6 @@ export function AdventureEntityModal({
-
diff --git a/src/components/AdventurePanel.tsx b/src/components/AdventurePanel.tsx index 9df86d14..7513f7fe 100644 --- a/src/components/AdventurePanel.tsx +++ b/src/components/AdventurePanel.tsx @@ -30,10 +30,14 @@ import { getScenePresetById } from '../data/scenePresets'; import { getOptionImpactSummary } from '../hooks/combatStoryUtils'; import type { BattleRewardUi, QuestFlowUi } from '../hooks/useStoryGeneration'; import type { + CampEvent, + ChapterState, Character, InventoryItem, + JourneyBeat, NpcBattleMode, QuestLogEntry, + SetpieceDirective, StoryMoment, StoryOption, WorldType, @@ -89,6 +93,11 @@ interface AdventurePanelProps { musicVolume: number; onMusicVolumeChange: (value: number) => void; onSaveAndExit: () => void; + chapterState?: ChapterState | null; + journeyBeat?: JourneyBeat | null; + recentChronicleSummary?: string | null; + currentCampEvent?: CampEvent | null; + setpieceDirective?: SetpieceDirective | null; } const AdventurePanelOverlays = lazy(async () => { @@ -591,6 +600,11 @@ export function AdventurePanel({ musicVolume, onMusicVolumeChange, onSaveAndExit, + chapterState = null, + journeyBeat = null, + recentChronicleSummary = null, + currentCampEvent = null, + setpieceDirective = null, }: AdventurePanelProps) { const isDialogueStory = currentStory.displayMode === 'dialogue'; const dialogueTurns = currentStory.dialogue ?? []; @@ -601,6 +615,7 @@ export function AdventurePanel({ currentStory.deferredOptions?.length, ); const saveAndExitDisabled = isLoading || isStoryStreaming; + const [isChapterPanelOpen, setIsChapterPanelOpen] = useState(false); const [isQuestPanelOpen, setIsQuestPanelOpen] = useState(false); const [isSettingsPanelOpen, setIsSettingsPanelOpen] = useState(false); const [isStatsPanelOpen, setIsStatsPanelOpen] = useState(false); @@ -798,6 +813,7 @@ export function AdventurePanel({ [statistics], ); const shouldMountAdventureOverlays = + isChapterPanelOpen || isSettingsPanelOpen || isStatsPanelOpen || isQuestPanelOpen || @@ -838,9 +854,23 @@ export function AdventurePanel({ +
); diff --git a/src/components/CharacterInfoHelpers.ts b/src/components/CharacterInfoHelpers.ts new file mode 100644 index 00000000..09741965 --- /dev/null +++ b/src/components/CharacterInfoHelpers.ts @@ -0,0 +1,123 @@ +import type { CSSProperties } from 'react'; + +import { type RoleCombatStats } from '../data/attributeCombat'; +import { + type BuildDamageBreakdown, + getBuildContributionQuality, + getBuildContributionQualityRatio, +} from '../data/buildDamage'; +import { getResourceLabelsForWorld } from '../services/customWorldPresentation'; +import type { Character } from '../types'; + +export function getGenderLabel(gender: Character['gender']) { + if (gender === 'female') return '女'; + if (gender === 'male') return '男'; + return '未明'; +} + +export function getCharacterDetailSpriteStyle(character: Character) { + const groundOffset = character.groundOffsetY ?? 22; + const translateY = Math.max(10, Math.round((groundOffset - 22) * 0.34)); + + return { + transform: `translateY(${translateY}px) scale(1.34)`, + transformOrigin: 'center bottom', + } satisfies CSSProperties; +} + +const SKILL_STYLE_LABELS = { + burst: '爆发', + steady: '稳态', + mobility: '机动', + finisher: '终结', + projectile: '投射', +} satisfies Record; + +export function getSkillDeliveryLabel(skill: Character['skills'][number]) { + return skill.delivery === 'ranged' || skill.style === 'projectile' + ? '远程' + : '近战'; +} + +export function getSkillStyleLabel(skill: Character['skills'][number]) { + return SKILL_STYLE_LABELS[skill.style]; +} + +function getContributionHeatRatio(value: number) { + return getBuildContributionQualityRatio(value); +} + +export function getContributionVisualStyle(value: number): CSSProperties { + const quality = getBuildContributionQuality(value); + if (quality.tier === 'epic') { + return { + borderColor: 'hsla(286, 68%, 66%, 0.46)', + background: + 'linear-gradient(135deg, hsla(284, 72%, 44%, 0.34) 0%, hsla(265, 64%, 28%, 0.26) 42%, rgba(12, 16, 24, 0.94) 78%)', + boxShadow: + 'inset 0 1px 0 rgba(255,255,255,0.05), 0 0 24px hsla(284, 78%, 62%, 0.22)', + color: 'rgb(247 236 255)', + }; + } + + const ratio = getContributionHeatRatio(value); + const hue = 210 - ratio * 178; + const saturation = 62 + ratio * 16; + const lightness = 56 + ratio * 6; + + return { + borderColor: `hsla(${hue}, ${saturation + 6}%, ${lightness}%, ${0.22 + ratio * 0.28})`, + background: `linear-gradient(135deg, hsla(${hue}, ${saturation + 10}%, ${36 + ratio * 12}%, ${0.18 + ratio * 0.22}) 0%, rgba(12, 16, 24, 0.94) 72%)`, + boxShadow: `inset 0 1px 0 rgba(255,255,255,0.04), 0 0 ${10 + ratio * 20}px hsla(${hue}, ${saturation + 14}%, ${58 + ratio * 8}%, ${0.12 + ratio * 0.18})`, + color: + ratio > 0.76 + ? 'rgb(255 244 235)' + : ratio > 0.32 + ? 'rgb(236 242 248)' + : 'rgb(203 213 225)', + }; +} + +export function formatAttributeMetricValue(value: number) { + const rounded = Math.round(value * 10) / 10; + return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1); +} + +function formatAttributePercentValue(value: number) { + return `${formatAttributeMetricValue(value * 100)}%`; +} + +export function getAttributeBonusPillClassName(bonus: number) { + if (bonus >= 0.05) { + return 'border-amber-400/25 bg-amber-500/12 text-amber-100'; + } + if (bonus > 0) { + return 'border-emerald-400/20 bg-emerald-500/10 text-emerald-100'; + } + return 'border-white/10 bg-black/20 text-zinc-500'; +} + +export function getAttributeEffectText( + slotId: string, + combatStats: RoleCombatStats, + resourceLabels: ReturnType, +) { + switch (slotId) { + case 'axis_a': + return `攻击倍率 x${formatAttributeMetricValue(combatStats.attackPowerMultiplier)}`; + case 'axis_b': + return `${resourceLabels.maxHp} +${combatStats.maxHpBonus}`; + case 'axis_c': + return `${resourceLabels.hp}恢复 +${combatStats.storyRecovery}`; + case 'axis_d': + return `攻击速度 ${formatAttributeMetricValue(combatStats.turnSpeed)}`; + case 'axis_e': + return `暴击率 ${formatAttributePercentValue(combatStats.critChance)}`; + case 'axis_f': + return `暴击伤害 x${formatAttributeMetricValue(combatStats.critDamageMultiplier)}`; + default: + return '提升战斗表现'; + } +} + +export type ContributionRow = BuildDamageBreakdown['rows'][number]; diff --git a/src/components/CharacterInfoShared.tsx b/src/components/CharacterInfoShared.tsx new file mode 100644 index 00000000..ebc6a424 --- /dev/null +++ b/src/components/CharacterInfoShared.tsx @@ -0,0 +1,291 @@ +import { resolveRoleCombatStats } from '../data/attributeCombat'; +import { getAttributeSlotValue } from '../data/attributeResolver'; +import { + type BuildDamageBreakdown, + formatBuildContributionPercent, + getBuildContributionQualityLabel, +} from '../data/buildDamage'; +import { getResourceLabelsForWorld } from '../services/customWorldPresentation'; +import type { + Character, + RoleAttributeProfile, + WorldAttributeSchema, +} from '../types'; +import { + type ContributionRow, + formatAttributeMetricValue, + getAttributeBonusPillClassName, + getAttributeEffectText, + getContributionVisualStyle, + getSkillDeliveryLabel, + getSkillStyleLabel, +} from './CharacterInfoHelpers'; + +export function StatusRow({ + label, + current, + max, + tone, +}: { + label: string; + current: number; + max: number; + tone: 'hp' | 'mp'; +}) { + const ratio = Math.max(0, Math.min(1, max > 0 ? current / max : 0)); + const fillClass = + tone === 'hp' + ? 'from-emerald-400 via-lime-300 to-emerald-200' + : 'from-sky-500 via-cyan-300 to-sky-100'; + + return ( +
+
+ {label} + + {current} / {max} + +
+
+
+
+
+ ); +} + +export function CharacterSkillsList({ + skills, + onSelectSkill, + emptyText = '暂无技能信息', +}: { + skills: Character['skills']; + onSelectSkill?: ((skillId: string) => void) | null; + emptyText?: string; +}) { + if (skills.length === 0) { + return ( +
+ {emptyText} +
+ ); + } + + return ( +
+ {skills.map((skill) => { + const content = ( + <> +
+
{skill.name}
+ + {getSkillDeliveryLabel(skill)} + +
+
+
伤害:{skill.damage}
+
法力:{skill.manaCost}
+
冷却:{skill.cooldownTurns}
+
距离:{skill.range}
+
+
+ {getSkillStyleLabel(skill)} +
+ + ); + + if (onSelectSkill) { + return ( + + ); + } + + return ( +
+ {content} +
+ ); + })} +
+ ); +} + +export function MultiplierContributionList({ + breakdown, + onSelectContribution, +}: { + breakdown: BuildDamageBreakdown; + onSelectContribution: (row: ContributionRow) => void; +}) { + const sortedRows = [...breakdown.rows].sort( + (left, right) => + right.bonusDelta - left.bonusDelta || + left.label.localeCompare(right.label, 'zh-CN'), + ); + + return ( +
+
+ 状态标签 + + 点击标签查看具体属性加成 + +
+ {sortedRows.length > 0 ? ( +
+ {sortedRows.map((row) => ( + + ))} +
+ ) : ( + + 当前还没有形成有效标签 + + )} +
+ ); +} + +export function CharacterAttributeGrid({ + attributeProfile, + attributeSchema, + buildBreakdown = null, + resourceLabels, + emptyText = '暂无属性信息', + gridClassName = 'grid grid-cols-1 gap-2 text-sm text-zinc-300 sm:grid-cols-2', + cardClassName = 'rounded-xl border border-white/8 bg-black/25 px-3 py-2', +}: { + attributeProfile: RoleAttributeProfile | null | undefined; + attributeSchema: WorldAttributeSchema; + buildBreakdown?: BuildDamageBreakdown | null; + resourceLabels: ReturnType; + emptyText?: string; + gridClassName?: string; + cardClassName?: string; +}) { + const attributeRows = attributeSchema.slots.map((slot) => ({ + slot, + value: getAttributeSlotValue(attributeProfile, slot.slotId), + })); + const attributeBonusBySlot = Object.fromEntries( + attributeSchema.slots.map((slot) => [ + slot.slotId, + Number( + ( + buildBreakdown?.rows.reduce( + (sum, row) => + sum + (row.attributeModifierDeltas?.[slot.slotId] ?? 0), + 0, + ) ?? 0 + ).toFixed(4), + ), + ]), + ) as Record; + const boostedAttributeProfile = attributeProfile + ? { + ...attributeProfile, + values: { + ...(attributeProfile.values ?? {}), + ...Object.fromEntries( + attributeSchema.slots.map((slot) => { + const baseValue = attributeProfile.values?.[slot.slotId] ?? 0; + const totalBonus = attributeBonusBySlot[slot.slotId] ?? 0; + + return [ + slot.slotId, + Number((baseValue * (1 + totalBonus)).toFixed(4)), + ]; + }), + ), + }, + } + : null; + const boostedCombatStats = boostedAttributeProfile + ? resolveRoleCombatStats(boostedAttributeProfile) + : null; + const displayRows = attributeRows.map(({ slot, value }) => { + const totalBonus = attributeBonusBySlot[slot.slotId] ?? 0; + const boostedValue = Number((value * (1 + totalBonus)).toFixed(4)); + + return { + slot, + baseValue: value, + boostedValue, + totalBonus, + effectText: boostedCombatStats + ? getAttributeEffectText( + slot.slotId, + boostedCombatStats, + resourceLabels, + ) + : slot.combatUseText, + }; + }); + + if (displayRows.length === 0) { + return
{emptyText}
; + } + + return ( +
+ {displayRows.map( + ({ slot, baseValue, boostedValue, totalBonus, effectText }) => ( +
+
+ {slot.name} +
+
+
+
+ {formatAttributeMetricValue(boostedValue)} +
+
+
+ + 标签加成 {formatBuildContributionPercent(totalBonus)} + +
+ 原始 {formatAttributeMetricValue(baseValue)} +
+
+
+
+ {effectText} +
+
+ ), + )} +
+ ); +} diff --git a/src/components/CharacterPanel.tsx b/src/components/CharacterPanel.tsx index 18712cc7..7d209d03 100644 --- a/src/components/CharacterPanel.tsx +++ b/src/components/CharacterPanel.tsx @@ -1,12 +1,7 @@ import { AnimatePresence, motion } from 'motion/react'; -import { type CSSProperties, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { - resolveRoleCombatStats, - type RoleCombatStats, -} from '../data/attributeCombat'; -import { - formatAttributeList, resolveAttributeSchema, resolveCharacterAttributeProfile, } from '../data/attributeResolver'; @@ -15,7 +10,6 @@ import { formatBuildContributionPercent, getBuildContributionAttributeRows, getBuildContributionQualityLabel, - getBuildContributionQualityRatio, getCompanionBuildDamageBreakdown, getPlayerBuildDamageBreakdown, } from '../data/buildDamage'; @@ -36,7 +30,9 @@ import { getResourceLabelsForWorld } from '../services/customWorldPresentation'; import { AnimationState, Character, + CompanionArcState, CompanionRenderState, + CompanionResolution, CustomWorldProfile, EquipmentLoadout, GameState, @@ -53,6 +49,17 @@ import { import { AffinityStatusCard } from './AffinityStatusCard'; import { BackstoryArchive } from './BackstoryArchive'; import { CharacterAnimator } from './CharacterAnimator'; +import { + getCharacterDetailSpriteStyle, + getContributionVisualStyle, + getGenderLabel, +} from './CharacterInfoHelpers'; +import { + CharacterAttributeGrid, + CharacterSkillsList, + MultiplierContributionList, + StatusRow, +} from './CharacterInfoShared'; import type { GameCanvasEntitySelection } from './GameCanvas'; import { PixelIcon } from './PixelIcon'; @@ -73,6 +80,8 @@ interface CharacterPanelProps { onOpenCharacterChat?: (target: CharacterChatTarget) => void; chatSummaries?: Record; onInspectMember?: (selection: GameCanvasEntitySelection) => void; + companionArcStates?: CompanionArcState[]; + companionResolutions?: CompanionResolution[]; } type PartyMember = { @@ -95,212 +104,6 @@ type EquipmentRow = { rarityLabel: string; }; -type ContributionRow = BuildDamageBreakdown['rows'][number]; - -function StatusRow({ - label, - current, - max, - tone, -}: { - label: string; - current: number; - max: number; - tone: 'hp' | 'mp'; -}) { - const ratio = Math.max(0, Math.min(1, max > 0 ? current / max : 0)); - const fillClass = - tone === 'hp' - ? 'from-emerald-400 via-lime-300 to-emerald-200' - : 'from-sky-500 via-cyan-300 to-sky-100'; - - return ( -
-
- {label} - - {current} / {max} - -
-
-
-
-
- ); -} - -function getGenderLabel(gender: Character['gender']) { - if (gender === 'female') return '女'; - if (gender === 'male') return '男'; - return '未明'; -} - -const SKILL_STYLE_LABELS = { - burst: '爆发', - steady: '稳态', - mobility: '机动', - finisher: '终结', - projectile: '投射', -} satisfies Record; - -function getSkillDeliveryLabel(skill: Character['skills'][number]) { - return skill.delivery === 'ranged' || skill.style === 'projectile' - ? '远程' - : '近战'; -} - -function CharacterSkillsList({ character }: { character: Character }) { - if (character.skills.length === 0) { - return ( -
- 暂无技能信息 -
- ); - } - - return ( -
- {character.skills.map((skill) => ( -
-
-
{skill.name}
- - {getSkillDeliveryLabel(skill)} - -
-
-
伤害:{skill.damage}
-
法力:{skill.manaCost}
-
冷却:{skill.cooldownTurns}
-
距离:{skill.range}
-
-
- {SKILL_STYLE_LABELS[skill.style]} -
-
- ))} -
- ); -} - -function getContributionHeatRatio(value: number) { - return getBuildContributionQualityRatio(value); -} - -function getContributionVisualStyle(value: number): CSSProperties { - const ratio = getContributionHeatRatio(value); - const hue = 210 - ratio * 178; - const saturation = 62 + ratio * 16; - const lightness = 56 + ratio * 6; - - return { - borderColor: `hsla(${hue}, ${saturation + 6}%, ${lightness}%, ${0.22 + ratio * 0.28})`, - background: `linear-gradient(135deg, hsla(${hue}, ${saturation + 10}%, ${36 + ratio * 12}%, ${0.18 + ratio * 0.22}) 0%, rgba(12, 16, 24, 0.94) 72%)`, - boxShadow: `inset 0 1px 0 rgba(255,255,255,0.04), 0 0 ${10 + ratio * 20}px hsla(${hue}, ${saturation + 14}%, ${58 + ratio * 8}%, ${0.12 + ratio * 0.18})`, - color: - ratio > 0.76 - ? 'rgb(255 244 235)' - : ratio > 0.32 - ? 'rgb(236 242 248)' - : 'rgb(203 213 225)', - }; -} - -function MultiplierContributionList({ - breakdown, - onSelectContribution, -}: { - breakdown: BuildDamageBreakdown; - onSelectContribution: (row: ContributionRow) => void; -}) { - const sortedRows = [...breakdown.rows].sort( - (left, right) => - right.bonusDelta - left.bonusDelta || - left.label.localeCompare(right.label, 'zh-CN'), - ); - - return ( -
-
- {'\u72b6\u6001\u6807\u7b7e'} - - { - '\u70b9\u51fb\u6807\u7b7e\u67e5\u770b\u5177\u4f53\u5c5e\u6027\u52a0\u6210' - } - -
- {sortedRows.length > 0 ? ( -
- {sortedRows.map((row) => ( - - ))} -
- ) : ( - - {'\u5f53\u524d\u8fd8\u6ca1\u6709\u5f62\u6210\u6709\u6548\u6807\u7b7e'} - - )} -
- ); -} - -function formatAttributeMetricValue(value: number) { - const rounded = Math.round(value * 10) / 10; - return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1); -} - -function formatAttributePercentValue(value: number) { - return `${formatAttributeMetricValue(value * 100)}%`; -} - -function getAttributeBonusPillClassName(bonus: number) { - if (bonus >= 0.05) { - return 'border-amber-400/25 bg-amber-500/12 text-amber-100'; - } - if (bonus > 0) { - return 'border-emerald-400/20 bg-emerald-500/10 text-emerald-100'; - } - return 'border-white/10 bg-black/20 text-zinc-500'; -} - -function getAttributeEffectText( - slotId: string, - combatStats: RoleCombatStats, - resourceLabels: ReturnType, -) { - switch (slotId) { - case 'axis_a': - return `攻击倍率 x${formatAttributeMetricValue(combatStats.attackPowerMultiplier)}`; - case 'axis_b': - return `${resourceLabels.maxHp} +${combatStats.maxHpBonus}`; - case 'axis_c': - return `${resourceLabels.hp}恢复 +${combatStats.storyRecovery}`; - case 'axis_d': - return `攻击速度 ${formatAttributeMetricValue(combatStats.turnSpeed)}`; - case 'axis_e': - return `暴击率 ${formatAttributePercentValue(combatStats.critChance)}`; - case 'axis_f': - return `暴击伤害 x${formatAttributeMetricValue(combatStats.critDamageMultiplier)}`; - default: - return '提升战斗表现'; - } -} - function buildLeaderEquipmentRows( playerCharacter: Character, playerEquipment: EquipmentLoadout, @@ -331,16 +134,6 @@ function buildCompanionEquipmentRows( })); } -function getCharacterDetailSpriteStyle(character: Character) { - const groundOffset = character.groundOffsetY ?? 22; - const translateY = Math.max(10, Math.round((groundOffset - 22) * 0.34)); - - return { - transform: `translateY(${translateY}px) scale(1.34)`, - transformOrigin: 'center bottom', - } satisfies CSSProperties; -} - export function CharacterPanel({ worldType, customWorldProfile = null, @@ -355,6 +148,8 @@ export function CharacterPanel({ npcStates = {}, quests, onInspectMember, + companionArcStates = [], + companionResolutions = [], }: CharacterPanelProps) { const [selectedMemberId, setSelectedMemberId] = useState(null); const [selectedContributionLabel, setSelectedContributionLabel] = useState< @@ -458,6 +253,18 @@ export function CharacterPanel({ const selectedMemberAffinity = selectedMember?.npcId ? (npcStates[selectedMember.npcId]?.affinity ?? 0) : null; + const selectedMemberArcState = + selectedMember && !selectedMember.isLeader + ? companionArcStates.find( + (arcState) => arcState.characterId === selectedMember.character.id, + ) ?? null + : null; + const selectedMemberResolution = + selectedMember && !selectedMember.isLeader + ? companionResolutions.find( + (resolution) => resolution.characterId === selectedMember.character.id, + ) ?? null + : null; const selectedMemberPublicBackstory = selectedMember && !selectedMember.isLeader && selectedMemberAffinity != null ? getCharacterPublicBackstorySummary(selectedMember.character, worldType) @@ -503,96 +310,6 @@ export function CharacterPanel({ : null, [customWorldProfile, selectedMember, worldType], ); - const selectedAttributeRows = useMemo( - () => - selectedMemberAttributeProfile - ? formatAttributeList( - selectedMemberAttributeProfile, - selectedAttributeSchema, - ) - : [], - [selectedAttributeSchema, selectedMemberAttributeProfile], - ); - const selectedAttributeBonusBySlot = useMemo( - () => - Object.fromEntries( - selectedAttributeSchema.slots.map((slot) => [ - slot.slotId, - Number( - ( - selectedBuildBreakdown?.rows.reduce( - (sum, row) => - sum + (row.attributeModifierDeltas?.[slot.slotId] ?? 0), - 0, - ) ?? 0 - ).toFixed(4), - ), - ]), - ) as Record, - [selectedAttributeSchema, selectedBuildBreakdown], - ); - const selectedBoostedAttributeProfile = useMemo(() => { - if (!selectedMemberAttributeProfile) { - return null; - } - - return { - ...selectedMemberAttributeProfile, - values: { - ...(selectedMemberAttributeProfile.values ?? {}), - ...Object.fromEntries( - selectedAttributeSchema.slots.map((slot) => { - const baseValue = - selectedMemberAttributeProfile.values?.[slot.slotId] ?? 0; - const totalBonus = selectedAttributeBonusBySlot[slot.slotId] ?? 0; - - return [ - slot.slotId, - Number((baseValue * (1 + totalBonus)).toFixed(4)), - ]; - }), - ), - }, - }; - }, [ - selectedAttributeBonusBySlot, - selectedAttributeSchema, - selectedMemberAttributeProfile, - ]); - const selectedBoostedCombatStats = useMemo( - () => - selectedMember - ? resolveRoleCombatStats(selectedBoostedAttributeProfile) - : null, - [selectedBoostedAttributeProfile, selectedMember], - ); - const selectedDisplayAttributeRows = useMemo( - () => - selectedAttributeRows.map(({ slot, value }) => { - const totalBonus = selectedAttributeBonusBySlot[slot.slotId] ?? 0; - const boostedValue = Number((value * (1 + totalBonus)).toFixed(4)); - - return { - slot, - baseValue: value, - boostedValue, - totalBonus, - effectText: selectedBoostedCombatStats - ? getAttributeEffectText( - slot.slotId, - selectedBoostedCombatStats, - resourceLabels, - ) - : slot.combatUseText, - }; - }), - [ - resourceLabels, - selectedAttributeBonusBySlot, - selectedAttributeRows, - selectedBoostedCombatStats, - ], - ); const selectedContributionAttributes = selectedContributionRow ? getBuildContributionAttributeRows( selectedContributionRow, @@ -941,6 +658,32 @@ export function CharacterPanel({ {selectedMemberAffinity != null && ( )} + {selectedMemberArcState && ( +
+
+ 个人线阶段 +
+
+ {selectedMemberArcState.currentStage} +
+
+ {selectedMemberArcState.arcTheme} +
+
+ )} + {selectedMemberResolution && ( +
+
+ 收束状态 +
+
+ {selectedMemberResolution.resolutionType} +
+
+ {selectedMemberResolution.summary} +
+
+ )} {selectedMemberAffinity != null && ( )}
-
- {selectedDisplayAttributeRows.map( - ({ - slot, - baseValue, - boostedValue, - totalBonus, - effectText, - }) => ( -
-
- {slot.name} -
-
-
-
- {formatAttributeMetricValue(boostedValue)} -
-
-
- - 标签加成{' '} - {formatBuildContributionPercent(totalBonus)} - -
- 原始 {formatAttributeMetricValue(baseValue)} -
-
-
-
- {effectText} -
-
- ), - )} +
+
@@ -1037,7 +749,9 @@ export function CharacterPanel({
{'\u6280\u80fd'}
- +
void; onEditTarget: (target: CustomWorldEditorTarget) => void; onProfileChange: (profile: CustomWorldProfile) => void; + onRegeneratePlayableNpc?: (id: string) => void; + onRegenerateStoryNpc?: (id: string) => void; + onRegenerateLandmark?: (id: string) => void; + onRegenerateStoryExpansion?: () => void; + onRegenerateLandmarkNetwork?: () => void; createActionLabel?: string; onCreateAction?: () => void; } const RESULT_TABS: Array<{ id: ResultTab; label: string }> = [ { id: 'world', label: '世界' }, + { id: 'anchors', label: '锚点' }, { id: 'playable', label: '可扮演角色' }, { id: 'story', label: '场景角色' }, { id: 'landmarks', label: '场景' }, @@ -203,6 +210,11 @@ export function CustomWorldEntityCatalog({ onActiveTabChange, onEditTarget, onProfileChange, + onRegeneratePlayableNpc, + onRegenerateStoryNpc, + onRegenerateLandmark, + onRegenerateStoryExpansion, + onRegenerateLandmarkNetwork, createActionLabel, onCreateAction, }: CustomWorldEntityCatalogProps) { @@ -249,8 +261,34 @@ export function CustomWorldEntityCatalog({ [deferredSearch, landmarkById, profile.landmarks, storyNpcById], ); + const creatorIntentSummary = useMemo( + () => buildCustomWorldCreatorIntentDisplayText(profile.creatorIntent).trim(), + [profile.creatorIntent], + ); + const lockedCharacterNames = useMemo( + () => + new Set( + profile.creatorIntent?.keyCharacters + .filter((entry) => entry.locked) + .map((entry) => entry.name.trim()) + .filter(Boolean) ?? [], + ), + [profile.creatorIntent], + ); + const lockedLandmarkNames = useMemo( + () => + new Set( + profile.creatorIntent?.keyLandmarks + .filter((entry) => entry.locked) + .map((entry) => entry.name.trim()) + .filter(Boolean) ?? [], + ), + [profile.creatorIntent], + ); + const counts = { world: 1, + anchors: 1, playable: profile.playableNpcs.length, story: profile.storyNpcs.length, landmarks: profile.landmarks.length, @@ -325,7 +363,7 @@ export function CustomWorldEntityCatalog({ ))}
- {activeTab !== 'world' ? ( + {activeTab !== 'world' && activeTab !== 'anchors' ? (
@@ -348,6 +386,14 @@ export function CustomWorldEntityCatalog({
+ {creatorIntentSummary ? ( +
+
+ {creatorIntentSummary} +
+
+ ) : null} +
@@ -370,6 +416,101 @@ export function CustomWorldEntityCatalog({ ) : null} + {activeTab === 'anchors' ? ( +
+
+
+ {creatorIntentSummary || '当前还没有记录创作锚点。'} +
+
+ +
+
+ {profile.creatorIntent?.keyFactions.length ? ( + profile.creatorIntent.keyFactions.map((entry) => ( +
+
+
{entry.name || '未命名势力'}
+ {entry.locked ? ( + + 已锁定 + + ) : null} +
+
{entry.publicGoal || '暂无目标说明'}
+ {entry.tension ?
冲突:{entry.tension}
: null} + {entry.notes ?
补充:{entry.notes}
: null} +
+ )) + ) : ( + + )} +
+
+ +
+
+ {profile.creatorIntent?.keyCharacters.length ? ( + profile.creatorIntent.keyCharacters.map((entry) => ( +
+
+
{entry.name || '未命名角色'}
+ {entry.locked ? ( + + 已锁定 + + ) : null} +
+
{entry.role || '未填写身份'}
+ {entry.publicMask ?
表面:{entry.publicMask}
: null} + {entry.hiddenHook ?
暗线:{entry.hiddenHook}
: null} + {entry.relationToPlayer ?
与玩家:{entry.relationToPlayer}
: null} +
+ )) + ) : ( + + )} +
+
+ +
+
+ {profile.creatorIntent?.keyLandmarks.length ? ( + profile.creatorIntent.keyLandmarks.map((entry) => ( +
+
+
{entry.name || '未命名地点'}
+ {entry.locked ? ( + + 已锁定 + + ) : null} +
+
{entry.purpose || '未填写作用'}
+ {entry.mood ?
氛围:{entry.mood}
: null} + {entry.secret ?
秘密:{entry.secret}
: null} +
+ )) + ) : ( + + )} +
+
+
+ ) : null} + {activeTab === 'playable' ? (
@@ -388,6 +529,14 @@ export function CustomWorldEntityCatalog({ subtitle={role.title} actions={(
+ {onRegeneratePlayableNpc && !lockedCharacterNames.has(role.name.trim()) ? ( + onRegeneratePlayableNpc(role.id)} + tone="sky" + > + AI重生成 + + ) : null} onEditTarget({ kind: 'playable', mode: 'edit', id: role.id })} tone="sky">编辑 removePlayable(role.id, role.name)} tone="rose">删除
@@ -400,6 +549,11 @@ export function CustomWorldEntityCatalog({ ) : null}
+ {lockedCharacterNames.has(role.name.trim()) ? ( +
+ 创作者锁定角色 +
+ ) : null}
{role.description}
{role.backstory}
@@ -463,6 +617,13 @@ export function CustomWorldEntityCatalog({
场景角色默认可组合中世纪奇幻角色形象;当角色文本明显指向怪物型 NPC 且初始好感偏敌对时,预览也会自动尝试引用怪物素材。 + {onRegenerateStoryExpansion ? ( +
+ + 重生成长尾场景角色 + +
+ ) : null}
{filteredStory.length === 0 ? ( @@ -474,6 +635,14 @@ export function CustomWorldEntityCatalog({ subtitle={npc.role} actions={(
+ {onRegenerateStoryNpc && !lockedCharacterNames.has(npc.name.trim()) ? ( + onRegenerateStoryNpc(npc.id)} + tone="sky" + > + AI重生成 + + ) : null} onEditTarget({ kind: 'story', mode: 'edit', id: npc.id })} tone="sky">编辑 removeStoryNpc(npc.id, npc.name)} tone="rose">删除
@@ -487,6 +656,11 @@ export function CustomWorldEntityCatalog({ scale={2.18} />
+ {lockedCharacterNames.has(npc.name.trim()) ? ( +
+ 创作者锁定角色 +
+ ) : null}
{npc.description}
公开背景:{npc.backstoryReveal.publicSummary || '未填写'} @@ -556,6 +730,13 @@ export function CustomWorldEntityCatalog({
场景图会同步用于结果页和正式世界中的背景展示;这里还能看到每个场景承载的 NPC 和连接关系。 + {onRegenerateLandmarkNetwork ? ( +
+ + 重生成场景网络 + +
+ ) : null}
{filteredLandmarks.length === 0 ? ( @@ -566,12 +747,25 @@ export function CustomWorldEntityCatalog({ title={landmark.name} actions={(
+ {onRegenerateLandmark && !lockedLandmarkNames.has(landmark.name.trim()) ? ( + onRegenerateLandmark(landmark.id)} + tone="sky" + > + AI重生成 + + ) : null} onEditTarget({ kind: 'landmark', mode: 'edit', id: landmark.id })} tone="sky">编辑 removeLandmark(landmark.id, landmark.name)} tone="rose">删除
)} >
+ {lockedLandmarkNames.has(landmark.name.trim()) ? ( +
+ 创作者锁定场景 +
+ ) : null}
{landmark.description}
diff --git a/src/components/CustomWorldGenerationView.tsx b/src/components/CustomWorldGenerationView.tsx index da194581..79e1f073 100644 --- a/src/components/CustomWorldGenerationView.tsx +++ b/src/components/CustomWorldGenerationView.tsx @@ -3,13 +3,10 @@ import { motion } from 'motion/react'; import type { CustomWorldGenerationProgress, } from '../services/ai'; -import { AnimationState, type Character } from '../types'; import { getNineSliceStyle, UI_CHROME } from '../uiAssets'; -import { CharacterAnimator } from './CharacterAnimator'; interface CustomWorldGenerationViewProps { settingText: string; - actionPreviewCharacters: Character[]; progress: CustomWorldGenerationProgress | null; isGenerating: boolean; error: string | null; @@ -19,28 +16,6 @@ interface CustomWorldGenerationViewProps { onInterrupt: () => void; } -const ACTION_SHOWCASE: Array<{ - label: string; - description: string; - state: AnimationState; -}> = [ - { - label: '冲阵测试', - description: '检查角色前探、推进与开场压迫感。', - state: AnimationState.RUN, - }, - { - label: '交战演示', - description: '预热战斗站姿与交锋节奏。', - state: AnimationState.ATTACK, - }, - { - label: '驻场待命', - description: '确认角色在剧情停驻时的氛围姿态。', - state: AnimationState.IDLE, - }, -] as const; - function formatDuration(ms: number) { const safeMs = Math.max(0, Math.round(ms)); const totalSeconds = Math.ceil(safeMs / 1000); @@ -64,7 +39,6 @@ function getProgressPercentage(progress: CustomWorldGenerationProgress | null) { export function CustomWorldGenerationView({ settingText, - actionPreviewCharacters, progress, isGenerating, error, @@ -101,275 +75,172 @@ export function CustomWorldGenerationView({
-
-
-
-
-
-
- 玩家设定 -
-
- 这段文本会直接驱动本轮世界框架、角色与场景生成。 -
+
+
+
+
+
+ 玩家设定
-
+
+ +
+
+ {settingText} +
+
+ +
+
+
+
+ 生成进度 +
+
+ {progress?.phaseLabel ?? '正在启动世界生成'} +
+
+ {progress?.phaseDetail ?? '正在初始化世界生成链路与阶段监控。'} +
+
+
+
+ 总进度 +
+
+ {progressValue}% +
+
+
+ +
+ +
+ +
+
+
+ 当前批次 +
+
+ {progress?.batchLabel ?? '准备中'} +
+
+
+
+ 预计等待 +
+
+ {estimatedWaitText} +
+
+
+
+ 计时 +
+
+ {elapsedText} +
+
+
+ +
+ {steps.map((step) => ( +
- 修改设定 - -
-
- {settingText} -
-
- -
-
-
-
- 生成进度 -
-
- {progress?.phaseLabel ?? '正在启动世界生成'} -
-
- {progress?.phaseDetail ?? '正在初始化世界生成链路与阶段监控。'} -
-
-
-
- 总进度 -
-
- {progressValue}% -
-
-
- -
- -
- -
-
-
- 当前批次 -
-
- {progress?.batchLabel ?? '准备中'} -
-
-
-
- 预计等待 -
-
- {estimatedWaitText} -
-
-
-
- 计时 -
-
- {elapsedText} -
-
-
- -
- {steps.map((step) => ( -
-
-
- {step.label} -
-
- {step.completed}/{step.total} -
+
+
+ {step.label}
-
- {step.detail} +
+ {step.completed}/{step.total}
- ))} -
- - {error ? ( -
- {error} +
+ {step.detail} +
- ) : null} + ))} +
-
- {!isGenerating ? ( - <> - - - - ) : ( + {error ? ( +
+ {error} +
+ ) : null} + +
+ {!isGenerating ? ( + <> - )} -
-
-
- -
-
- - -
-
- 世界建造氛围 -
-
- 世界正在搭建地标、势力与角色关系 -
-
- 生成页不再只是一根等待条。这里会持续展示本轮设定的建造状态,让等待过程也像在看一场世界开局演出。 -
-
-
- 世界气候 -
-
- 势力碰撞 -
-
- 场景拓扑 -
-
-
-
- -
-
-
- 可扮演角色动作素材 -
-
- 先加载一组动作素材,让世界创建阶段也保持角色演出感。 -
-
- -
- {ACTION_SHOWCASE.map((showcase, index) => { - const character = - actionPreviewCharacters[ - index % Math.max(1, actionPreviewCharacters.length) - ]; - - return ( -
-
- {character ? ( - - ) : null} -
-
- {showcase.label} -
-
- {showcase.description} -
- {character ? ( -
- {character.name} -
- ) : null} +
-
-
+ + + ) : ( + + )} +
+
); diff --git a/src/components/CustomWorldResultView.tsx b/src/components/CustomWorldResultView.tsx index acd48f8a..91fb4a2a 100644 --- a/src/components/CustomWorldResultView.tsx +++ b/src/components/CustomWorldResultView.tsx @@ -15,6 +15,12 @@ interface CustomWorldResultViewProps { onBack: () => void; onEditSetting: () => void; onRegenerate: () => void; + onContinueExpand?: () => void; + onRegeneratePlayableNpc?: (id: string) => void; + onRegenerateStoryNpc?: (id: string) => void; + onRegenerateLandmark?: (id: string) => void; + onRegenerateStoryExpansion?: () => void; + onRegenerateLandmarkNetwork?: () => void; onSave: () => void; onProfileChange: (profile: CustomWorldProfile) => void; } @@ -70,6 +76,12 @@ export function CustomWorldResultView({ onBack, onEditSetting, onRegenerate: triggerRegenerate, + onContinueExpand, + onRegeneratePlayableNpc, + onRegenerateStoryNpc, + onRegenerateLandmark, + onRegenerateStoryExpansion, + onRegenerateLandmarkNetwork, onSave, onProfileChange, }: CustomWorldResultViewProps) { @@ -110,6 +122,11 @@ export function CustomWorldResultView({ onActiveTabChange={setActiveTab} onEditTarget={setEditorTarget} onProfileChange={onProfileChange} + onRegeneratePlayableNpc={onRegeneratePlayableNpc} + onRegenerateStoryNpc={onRegenerateStoryNpc} + onRegenerateLandmark={onRegenerateLandmark} + onRegenerateStoryExpansion={onRegenerateStoryExpansion} + onRegenerateLandmarkNetwork={onRegenerateLandmarkNetwork} createActionLabel={createLabel} onCreateAction={createTarget ? () => setEditorTarget(createTarget) : undefined} /> @@ -137,9 +154,19 @@ export function CustomWorldResultView({ ) : null}
+ {profile.generationStatus === 'key_only' ? ( +
+ 当前世界处于快速预览模式,只生成了关键对象。继续补全后,系统会生成长尾场景角色与完整场景网络。 +
+ ) : null}
修改设定 重新生成 + {profile.generationStatus === 'key_only' && onContinueExpand ? ( + + 继续补全世界 + + ) : null} -
-
-
+
+ className={`pointer-events-none absolute inset-0 bg-gradient-to-br ${rarityTheme.auraClass}`} + /> +
+
-
-
- {getInventoryRarityLabel(item.rarity)} +
+
+ {item.name}
-
- 数量:{item.quantity} -
-
- 持有者:{ownerLabel ?? playerCharacter.name} -
-
- 可使用:{isInventoryItemUsable(item) ? '是' : '否'} -
-
- 可装备: - {selectedItemEquipSlot - ? getEquipmentSlotLabel(selectedItemEquipSlot) - : '否'} -
-
- 装备类型: - {isInventoryItemEquippable(item) - ? '可装备物品' - : '非装备物品'} -
-
- 价值: - {formatCurrency(getInventoryItemValue(item), worldType)} +
+ 数量 x{item.quantity}
-
-
- 类型:{item.category} +
+
+
+

+ {itemSummary} +

-
- 标签:{item.tags.length} -
-
-
- {buildInventoryItemSummary(item, selectedItemUseEffect)} -
- {selectedItemUseEffect?.buildBuffs.length ? ( -
- {selectedItemUseEffect.buildBuffs.map((buff) => ( - - {buff.name} / {buff.tags.join('、')} /{' '} - {buff.durationTurns} 回合 - - ))} -
- ) : null} -
- {item.tags.length > 0 ? ( - item.tags.map((tag) => ( - - {tag} - - )) - ) : ( - - 无标签 - - )}
- - {footer ?? ( -
- -
- )}
+ + {footer != null ? ( +
+ {footer} +
+ ) : null} )} diff --git a/src/components/InventoryPanel.tsx b/src/components/InventoryPanel.tsx index 3baf2e97..f331cb54 100644 --- a/src/components/InventoryPanel.tsx +++ b/src/components/InventoryPanel.tsx @@ -1,16 +1,15 @@ import { useMemo, useState } from 'react'; import { formatCurrency } from '../data/economy'; -import { - getEquipmentSlotFromItem, - getEquipmentSlotLabel, - isInventoryItemEquippable, -} from '../data/equipmentEffects'; -import { type ForgeRecipeView, getReforgeCostView } from '../data/forgeSystem'; -import { resolveInventoryItemUseEffect } from '../data/inventoryEffects'; +import { type ForgeRecipeView } from '../data/forgeSystem'; import { buildInitialPlayerInventory } from '../data/npcInteractions'; -import { Character, InventoryItem, WorldType } from '../types'; -import { getNineSliceStyle, UI_CHROME } from '../uiAssets'; +import { + Character, + InventoryItem, + NarrativeCodexSection, + NarrativeQaReport, + WorldType, +} from '../types'; import { InventoryItemDetailModal, InventoryItemGrid, @@ -32,6 +31,9 @@ interface InventoryPanelProps { onCraftRecipe: (recipeId: string) => Promise; onDismantleItem: (itemId: string) => Promise; onReforgeItem: (itemId: string) => Promise; + continueGameDigest?: string | null; + narrativeCodex?: NarrativeCodexSection[]; + narrativeQaReport?: NarrativeQaReport | null; } export function InventoryPanel({ @@ -39,23 +41,14 @@ export function InventoryPanel({ worldType, playerInventory, playerCurrency, - playerHp, - playerMaxHp, - playerMana, - playerMaxMana, inBattle, - onUseItem, - onEquipItem, forgeRecipes, onCraftRecipe, - onDismantleItem, - onReforgeItem, + continueGameDigest = null, + narrativeCodex = [], + narrativeQaReport = null, }: InventoryPanelProps) { const [selectedItem, setSelectedItem] = useState(null); - const [isUsingItem, setIsUsingItem] = useState(false); - const [equipmentActionKey, setEquipmentActionKey] = useState( - null, - ); const [forgeActionKey, setForgeActionKey] = useState(null); const inventoryItems = useMemo( @@ -65,57 +58,85 @@ export function InventoryPanel({ : buildInitialPlayerInventory(playerCharacter, worldType), [playerCharacter, playerInventory, worldType], ); - - const selectedItemUseEffect = selectedItem - ? resolveInventoryItemUseEffect(selectedItem, playerCharacter) - : null; - const selectedItemEquipSlot = selectedItem - ? getEquipmentSlotFromItem(selectedItem) - : null; - const selectedItemReforgeCost = selectedItem - ? getReforgeCostView(selectedItem, worldType) - : null; - - const canUseSelectedItem = Boolean( - selectedItem && - selectedItemUseEffect && - ((selectedItemUseEffect.hpRestore > 0 && playerHp < playerMaxHp) || - (selectedItemUseEffect.manaRestore > 0 && playerMana < playerMaxMana) || - selectedItemUseEffect.cooldownReduction > 0 || - selectedItemUseEffect.buildBuffs.length > 0), - ); - - const canEquipSelectedItem = Boolean( - selectedItem && - selectedItemEquipSlot && - isInventoryItemEquippable(selectedItem) && - !inBattle, - ); - - const canDismantleSelectedItem = Boolean( - selectedItem && - !inBattle && - (isInventoryItemEquippable(selectedItem) || selectedItem.buildProfile), - ); - - const canReforgeSelectedItem = Boolean( - selectedItem && - !inBattle && - isInventoryItemEquippable(selectedItem) && - selectedItem.buildProfile && - selectedItemReforgeCost && - selectedItemReforgeCost.currencyCost <= playerCurrency, + const documentItems = useMemo( + () => inventoryItems.filter((item) => item.category === '文书' || item.tags.includes('document')), + [inventoryItems], ); return (
+ {continueGameDigest && ( +
+
+ 旅程回顾 +
+ {continueGameDigest} +
+ )} + {documentItems.length > 0 && ( +
+
+ 文书与证据 +
+
+ {documentItems.map((item) => ( + + ))} +
+
+ )} + + {(narrativeCodex.length > 0 || narrativeQaReport) && ( +
+
+ 故事档案 +
+ {narrativeQaReport && ( +
+ QA:{narrativeQaReport.summary} +
+ )} +
+ {narrativeCodex.slice(0, 3).map((section) => ( +
+
+ {section.title} +
+
+ {section.entries.slice(0, 3).map((entry) => ( +
+ {entry.title} + {':'} + {entry.summary} +
+ ))} +
+
+ ))} +
+
+ )} +
工坊 @@ -198,127 +219,6 @@ export function InventoryPanel({ playerCharacter={playerCharacter} worldType={worldType} onClose={() => setSelectedItem(null)} - footer={ - selectedItem ? ( -
- - - - - -
- ) : undefined - } />
); diff --git a/src/components/SelectionCustomizationModals.tsx b/src/components/SelectionCustomizationModals.tsx index 351ea9b4..2ba19311 100644 --- a/src/components/SelectionCustomizationModals.tsx +++ b/src/components/SelectionCustomizationModals.tsx @@ -1,22 +1,55 @@ -import { AnimatePresence, motion } from 'motion/react'; +import { X } from 'lucide-react'; import type { ReactNode } from 'react'; -import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../uiAssets'; -import { PixelIcon } from './PixelIcon'; +import type { + CustomWorldCreatorIntent, + CustomWorldGenerationMode, +} from '../types'; -interface CustomWorldCreatorModalProps { +type BaseModalProps = { isOpen: boolean; - draft: string; - onDraftChange: (value: string) => void; + title: string; onClose: () => void; - onSubmit: () => void; - isGenerating: boolean; - progress: number; - progressLabel: string; - error: string | null; + children: ReactNode; + footer?: ReactNode; +}; + +function SelectionModal({ + isOpen, + title, + onClose, + children, + footer = null, +}: BaseModalProps) { + if (!isOpen) return null; + + return ( +
+
+
+
{title}
+ +
+
+ {children} +
+ {footer ? ( +
+ {footer} +
+ ) : null} +
+
+ ); } -interface CharacterDraftModalProps { +export function CharacterDraftModal(props: { isOpen: boolean; characterLabel: string; draftName: string; @@ -25,124 +58,152 @@ interface CharacterDraftModalProps { onBackstoryChange: (value: string) => void; onClose: () => void; onConfirm: () => void; - error: string | null; -} - -function ModalShell({ - isOpen, - title, - subtitle, - onClose, - disableClose = false, - children, -}: { - isOpen: boolean; - title: string; - subtitle?: string; - onClose: () => void; - disableClose?: boolean; - children: ReactNode; + error?: string | null; }) { - return ( - - {isOpen && ( - - event.stopPropagation()} - > -
-
-
{title}
- {subtitle ? ( -
{subtitle}
- ) : null} -
- -
-
{children}
-
-
- )} -
- ); -} + const { + isOpen, + characterLabel, + draftName, + draftBackstory, + onNameChange, + onBackstoryChange, + onClose, + onConfirm, + error = null, + } = props; -export function CustomWorldCreatorModal({ - isOpen, - draft, - onDraftChange, - onClose, - onSubmit, - isGenerating, - progress, - progressLabel, - error, -}: CustomWorldCreatorModalProps) { return ( - + + + + )} >
+
+ 当前角色:{characterLabel} +