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 (