From fcd8d727b0ffb897113097b7a3453ee9638d2174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=89=A9?= <253518756@qq.com> Date: Sun, 5 Apr 2026 22:20:30 +0800 Subject: [PATCH] Split custom world generation into staged lightweight batches --- .env.example | 2 +- AGENTS.md | 1 + src/components/AdventureEntityModal.tsx | 753 ++++---- src/components/AdventurePanel.tsx | 8 +- src/components/AffinityStatusCard.tsx | 86 +- src/components/BackstoryArchive.tsx | 96 ++ src/components/CharacterDetailModal.tsx | 10 +- src/components/CharacterPanel.tsx | 717 +++++--- src/components/CustomWorldEntityCatalog.tsx | 229 ++- .../CustomWorldEntityEditorModal.tsx | 522 +++++- src/components/GameShell.tsx | 3 +- src/components/MapModal.tsx | 177 +- src/components/NpcModals.tsx | 15 + src/components/SkillEffectPreview.tsx | 3 + src/components/StateFunctionEditor.tsx | 6 +- .../game-canvas/GameCanvasRuntime.tsx | 5 +- .../game-shell/GameShellCanvasStage.tsx | 3 +- .../game-shell/PreGameSelectionFlow.tsx | 2 +- src/data/affinityLevels.ts | 130 ++ src/data/attributeCombat.ts | 108 ++ src/data/attributeResolver.ts | 7 +- src/data/buildDamage.test.ts | 277 ++- src/data/buildDamage.ts | 567 ++++-- src/data/characterPresets.ts | 142 +- src/data/customWorldCharacterLoadout.ts | 118 +- src/data/customWorldLibrary.ts | 357 +++- src/data/customWorldSceneGraph.ts | 422 +++++ src/data/equipmentEffects.ts | 11 +- src/data/functionCatalog/npc/npcGift.ts | 8 + src/data/functionCatalog/npc/npcRecruit.ts | 8 + src/data/functionCatalog/npc/npcTrade.ts | 8 + src/data/hostileNpcs.ts | 11 +- src/data/npcInteractions.ts | 87 +- src/data/questFlow.ts | 10 +- src/data/sceneEncounterPreviews.ts | 7 +- src/data/scenePresets.ts | 200 ++- src/data/storyRecovery.ts | 59 + src/hooks/combat/battlePlan.test.ts | 48 + src/hooks/combat/battlePlan.ts | 121 +- src/hooks/story/choiceActions.test.ts | 322 ++++ src/hooks/story/choiceActions.ts | 87 +- src/hooks/story/npcEncounterActions.ts | 181 +- src/hooks/story/npcInteraction.ts | 241 ++- src/hooks/story/progressionActions.ts | 5 + src/hooks/story/uiTypes.ts | 3 + src/hooks/useGameFlow.ts | 16 +- src/hooks/useGamePersistence.ts | 13 +- src/hooks/useStoryGeneration.ts | 8 +- src/services/ai.test.ts | 355 +++- src/services/ai.ts | 561 +++++- src/services/customWorld.test.ts | 227 +++ src/services/customWorld.ts | 1519 +++++++++++++++-- src/services/customWorldBuilder.ts | 120 +- src/services/llmClient.ts | 15 +- src/services/runtimeItemAiDirector.ts | 2 +- src/types/customWorld.ts | 44 + src/types/scene.ts | 8 + 57 files changed, 7646 insertions(+), 1425 deletions(-) create mode 100644 src/components/BackstoryArchive.tsx create mode 100644 src/data/affinityLevels.ts create mode 100644 src/data/attributeCombat.ts create mode 100644 src/data/customWorldSceneGraph.ts create mode 100644 src/data/storyRecovery.ts create mode 100644 src/hooks/story/choiceActions.test.ts create mode 100644 src/services/customWorld.test.ts diff --git a/.env.example b/.env.example index 8f6f4755..7a3d0d2e 100644 --- a/.env.example +++ b/.env.example @@ -30,7 +30,7 @@ DASHSCOPE_IMAGE_REQUEST_TIMEOUT_MS="150000" VITE_LLM_REQUEST_TIMEOUT_MS="15000" # Optional: longer timeout for custom world generation, in milliseconds. -VITE_LLM_CUSTOM_WORLD_TIMEOUT_MS="45000" +VITE_LLM_CUSTOM_WORLD_TIMEOUT_MS="120000" # Optional: timeout for custom-world scene image generation, in milliseconds. VITE_SCENE_IMAGE_REQUEST_TIMEOUT_MS="150000" diff --git a/AGENTS.md b/AGENTS.md index 14dc768c..4374269f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,3 +8,4 @@ - 在 PowerShell 5.1 中读取或写入文本时,必须显式使用 UTF-8;如果终端输出疑似乱码,要用 `Get-Content -Encoding UTF8`、Python 或 Node 再次核对原文。 - 非必要不要整文件重写,尤其是包含中文的文件;优先做局部补丁,避免把未改动的中文内容重新编码。 - 修改包含中文的文件后,优先运行仓库里的编码检查,确保没有把文本写坏。 +- UI面板中不要默认写一些规则描述文案,清爽一些,按照游戏UI设计规范设计即可。 diff --git a/src/components/AdventureEntityModal.tsx b/src/components/AdventureEntityModal.tsx index e0f3f361..05c7fa47 100644 --- a/src/components/AdventureEntityModal.tsx +++ b/src/components/AdventureEntityModal.tsx @@ -3,6 +3,10 @@ import { AnimatePresence, motion } from 'motion/react'; import type { CSSProperties, ReactNode } from 'react'; import { useEffect, useMemo, useState } from 'react'; +import { + AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS, + DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY, +} from '../data/affinityLevels'; import { buildRelationState, formatAttributeList, @@ -11,28 +15,39 @@ import { } from '../data/attributeResolver'; import { type BuildDamageBreakdown, - describeBuildContribution, + formatBuildContributionPercent, getBuildContributionAttributeRows, - getBuildSourceLabel, + getBuildContributionQualityLabel, + getBuildContributionQualityRatio, getCompanionBuildDamageBreakdown, getPlayerBuildDamageBreakdown, + resolveMonsterOutgoingDamage, } from '../data/buildDamage'; import { getCharacterById, + getCharacterMaxHp, getCharacterMaxMana, getCharacterPrivateChatUnlockAffinity, + getCharacterPublicBackstorySummary, getInventoryItems, + getLockedCharacterBackstoryChapters, + getUnlockedCharacterBackstoryChapters, } from '../data/characterPresets'; -import { getHostileNpcPresetById } from '../data/hostileNpcPresets'; +import { + getHostileNpcPresetById, + getMonsterPresetsByWorld, +} from '../data/hostileNpcPresets'; import { buildEncounterAttributeRumors, resolveEncounterAttributeProfile, } from '../data/npcAttributeInsights'; import { buildInitialNpcState, + createNpcBattleMonster, normalizeNpcPersistentState, } from '../data/npcInteractions'; import type { CharacterChatTarget } from '../hooks/useStoryGeneration'; +import { getResourceLabelsForWorld } from '../services/customWorldPresentation'; import { AnimationState, type Character, @@ -40,11 +55,16 @@ import { type GameState, type InventoryItem, type NpcPersistentState, - type WorldAttributeSchema, } from '../types'; import { getNineSliceStyle, UI_CHROME } from '../uiAssets'; import { AffinityStatusCard } from './AffinityStatusCard'; +import { + BackstoryArchive, + type BackstoryLockedChapter, + type BackstoryUnlockedChapter, +} from './BackstoryArchive'; import { CharacterAnimator } from './CharacterAnimator'; +import { GENERIC_NPC_SCENE_SCALE } from './game-canvas/GameCanvasShared'; import type { GameCanvasEntitySelection } from './GameCanvas'; import { HostileNpcAnimator } from './HostileNpcAnimator'; import { @@ -52,6 +72,7 @@ import { InventoryItemGrid, } from './InventoryItemViews'; import { MedievalNpcAnimator } from './MedievalNpcAnimator'; +import { SkillEffectPreview } from './SkillEffectPreview'; interface AdventureEntityModalProps { selection: GameCanvasEntitySelection | null; @@ -60,15 +81,29 @@ interface AdventureEntityModalProps { onOpenCharacterChat?: (target: CharacterChatTarget) => void; } -function estimateCharacterMaxHp(character: Character) { - return Math.max( - 120, - 90 + character.attributes.strength * 10 + character.attributes.spirit * 4, - ); +const [ + BACKSTORY_UNLOCK_AFFINITY_EASED = 15, + BACKSTORY_UNLOCK_AFFINITY_FRIENDLY = 30, + BACKSTORY_UNLOCK_AFFINITY_TRUSTED = 60, + BACKSTORY_UNLOCK_AFFINITY_CLOSE = 90, +] = AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS; + +function estimateCharacterMaxHp( + character: Character, + worldType: GameState['worldType'], + customWorldProfile: GameState['customWorldProfile'], +) { + return getCharacterMaxHp(character, worldType, customWorldProfile); } -function estimateNpcMaxHp(character: Character | null) { - return character ? estimateCharacterMaxHp(character) : 120; +function estimateNpcMaxHp( + character: Character | null, + worldType: GameState['worldType'], + customWorldProfile: GameState['customWorldProfile'], +) { + return character + ? estimateCharacterMaxHp(character, worldType, customWorldProfile) + : 120; } function estimateNpcMaxMana(character: Character | null) { @@ -121,10 +156,6 @@ function Section({ title, children }: { title: string; children: ReactNode }) { ); } -function clamp(value: number, min: number, max: number) { - return Math.min(max, Math.max(min, value)); -} - const SKILL_STYLE_LABELS = { burst: '爆发', steady: '稳态', @@ -145,24 +176,25 @@ function getSkillStyleLabel(skill: Character['skills'][number]) { return SKILL_STYLE_LABELS[skill.style]; } -function getContributionHeatRatio(value: number, minValue = 0, maxValue = 1) { - const normalizedMin = Number.isFinite(minValue) ? minValue : 0; - const normalizedMax = Number.isFinite(maxValue) ? maxValue : 1; - const range = normalizedMax - normalizedMin; - - if (range <= 0.0001) { - return normalizedMax > 0 ? 1 : 0; +function resolveSkillPreviewMonsterId(gameState: GameState) { + if (!gameState.worldType) { + return null; } - return clamp((value - normalizedMin) / range, 0, 1); + const sceneMonsterId = gameState.currentScenePreset?.monsterIds?.[0] ?? null; + if (sceneMonsterId) { + return sceneMonsterId; + } + + return getMonsterPresetsByWorld(gameState.worldType)[0]?.id ?? null; } -function getContributionVisualStyle( - value: number, - minValue = 0, - maxValue = 1, -): CSSProperties { - const ratio = getContributionHeatRatio(value, minValue, maxValue); +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; @@ -180,28 +212,11 @@ function getContributionVisualStyle( }; } -function getContributionTrackStyle( - value: number, - minValue = 0, - maxValue = 1, -): CSSProperties { - const ratio = getContributionHeatRatio(value, minValue, maxValue); - const widthRatio = 0.18 + ratio * 0.82; - const hue = 210 - ratio * 178; - - return { - width: `${widthRatio * 100}%`, - background: `linear-gradient(90deg, hsla(${hue}, ${70 + ratio * 14}%, ${56 + ratio * 10}%, 0.94) 0%, rgba(255, 229, 214, 0.98) 100%)`, - }; -} - function MultiplierContributionList({ breakdown, - schema, onSelectContribution, }: { breakdown: BuildDamageBreakdown; - schema: WorldAttributeSchema; onSelectContribution: (row: ContributionRow) => void; }) { const sortedRows = [...breakdown.rows].sort( @@ -209,30 +224,12 @@ function MultiplierContributionList({ right.bonusDelta - left.bonusDelta || left.label.localeCompare(right.label, 'zh-CN'), ); - const contributionProducts = sortedRows.map((row) => row.bonusDelta); - const weakestProduct = - contributionProducts.length > 0 ? Math.min(...contributionProducts) : 0; - const strongestProduct = - contributionProducts.length > 0 ? Math.max(...contributionProducts) : 1; return (