This commit is contained in:
2026-04-18 13:05:29 +08:00
parent 09d4c0c31b
commit 5032701c38
77 changed files with 8538 additions and 2413 deletions

View File

@@ -1,3 +1,7 @@
import type {
EightAnchorContent,
KeyRelationshipValue,
} from '../../packages/shared/src/contracts/customWorldAgent';
import {
type ReactNode,
useDeferredValue,
@@ -13,10 +17,7 @@ import {
resolveCustomWorldLandmarkImageMap,
} from '../data/customWorldVisuals';
import { resolveCustomWorldCampScene } from '../services/customWorldCamp';
import {
buildCustomWorldCreatorIntentFoundationText,
normalizeCustomWorldCreatorIntent,
} from '../services/customWorldCreatorIntent';
import { normalizeCustomWorldCreatorIntent } from '../services/customWorldCreatorIntent';
import { AnimationState, Character, CustomWorldProfile } from '../types';
import { getNineSliceStyle, UI_CHROME } from '../uiAssets';
import { CharacterAnimator } from './CharacterAnimator';
@@ -348,6 +349,226 @@ function compactTextList(values: Array<string | null | undefined>) {
return values.map((value) => value?.trim() ?? '').filter(Boolean);
}
function toText(value: unknown) {
return typeof value === 'string' ? value.trim() : '';
}
function toTextArray(value: unknown) {
return Array.isArray(value)
? value.map((item) => toText(item)).filter(Boolean)
: [];
}
function toRecord(value: unknown) {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function buildRelationshipSeedText(value: unknown) {
const record = toRecord(value);
if (!record) {
return '';
}
return compactTextList([
toText(record.name),
toText(record.role),
toText(record.relationToPlayer)
? `与玩家:${toText(record.relationToPlayer)}`
: '',
toText(record.hiddenHook) ? `代价/暗线:${toText(record.hiddenHook)}` : '',
]).join('');
}
function buildKeyRelationshipText(value: KeyRelationshipValue) {
return compactTextList([
value.pairs,
value.relationshipType,
value.secretOrCost ? `代价/秘密:${value.secretOrCost}` : '',
]).join('');
}
function buildAnchorContentFromProfileFallback(
profile: CustomWorldProfile,
): EightAnchorContent {
const creatorIntent = normalizeCustomWorldCreatorIntent(profile.creatorIntent);
const relationshipSeed = creatorIntent?.keyCharacters[0] ?? null;
return {
worldPromise: {
hook:
creatorIntent?.worldHook ||
profile.anchorPack?.worldSummary ||
profile.summary,
differentiator: profile.subtitle || profile.settingText,
desiredExperience:
compactTextList([
creatorIntent?.toneDirectives.join('、') || '',
profile.tone,
]).join('') || profile.tone,
},
playerFantasy: {
playerRole: creatorIntent?.playerPremise || profile.playerGoal,
corePursuit: profile.playerGoal,
fearOfLoss:
relationshipSeed?.hiddenHook ||
creatorIntent?.coreConflicts[0] ||
profile.coreConflicts[0] ||
'',
},
themeBoundary: {
toneKeywords: compactTextList([
creatorIntent?.themeKeywords.join('、') || '',
creatorIntent?.toneDirectives.join('、') || '',
]),
aestheticDirectives: compactTextList([profile.tone, profile.subtitle]),
forbiddenDirectives: creatorIntent?.forbiddenDirectives ?? [],
},
playerEntryPoint: {
openingIdentity: creatorIntent?.playerPremise || '',
openingProblem:
creatorIntent?.openingSituation || profile.coreConflicts[0] || '',
entryMotivation: profile.playerGoal,
},
coreConflict: {
surfaceConflicts:
creatorIntent?.coreConflicts.length
? creatorIntent.coreConflicts
: profile.coreConflicts,
hiddenCrisis:
relationshipSeed?.hiddenHook ||
profile.summary ||
profile.settingText,
firstTouchedConflict:
creatorIntent?.openingSituation ||
profile.coreConflicts[0] ||
profile.playerGoal,
},
keyRelationships: relationshipSeed
? [
{
pairs: compactTextList([
relationshipSeed.name,
relationshipSeed.role,
]).join(' · '),
relationshipType: relationshipSeed.relationToPlayer || '',
secretOrCost: relationshipSeed.hiddenHook || '',
},
]
: [],
hiddenLines: {
hiddenTruths: compactTextList([
relationshipSeed?.hiddenHook || '',
profile.summary,
]),
misdirectionHints: compactTextList([
profile.subtitle,
profile.majorFactions[0] || '',
]),
revealPacing:
creatorIntent?.openingSituation ||
profile.coreConflicts[0] ||
profile.playerGoal,
},
iconicElements: {
iconicMotifs:
creatorIntent?.iconicElements.length
? creatorIntent.iconicElements
: compactTextList([
profile.anchorPack?.motifDirectives.join('、') || '',
profile.landmarks[0]?.name || '',
]),
institutionsOrArtifacts: compactTextList([
profile.camp?.name || '',
profile.majorFactions[0] || '',
]),
hardRules: compactTextList([profile.playerGoal, profile.coreConflicts[0] || '']),
},
};
}
function getProfileAnchorContent(profile: CustomWorldProfile) {
const anchorContentRecord = profile.anchorContent;
if (!anchorContentRecord) {
return buildAnchorContentFromProfileFallback(profile);
}
const worldPromiseRecord = toRecord(anchorContentRecord.worldPromise);
const playerFantasyRecord = toRecord(anchorContentRecord.playerFantasy);
const themeBoundaryRecord = toRecord(anchorContentRecord.themeBoundary);
const playerEntryPointRecord = toRecord(anchorContentRecord.playerEntryPoint);
const coreConflictRecord = toRecord(anchorContentRecord.coreConflict);
const hiddenLinesRecord = toRecord(anchorContentRecord.hiddenLines);
const iconicElementsRecord = toRecord(anchorContentRecord.iconicElements);
return {
worldPromise: worldPromiseRecord
? {
hook: toText(worldPromiseRecord.hook),
differentiator: toText(worldPromiseRecord.differentiator),
desiredExperience: toText(worldPromiseRecord.desiredExperience),
}
: null,
playerFantasy: playerFantasyRecord
? {
playerRole: toText(playerFantasyRecord.playerRole),
corePursuit: toText(playerFantasyRecord.corePursuit),
fearOfLoss: toText(playerFantasyRecord.fearOfLoss),
}
: null,
themeBoundary: themeBoundaryRecord
? {
toneKeywords: toTextArray(themeBoundaryRecord.toneKeywords),
aestheticDirectives: toTextArray(
themeBoundaryRecord.aestheticDirectives,
),
forbiddenDirectives: toTextArray(themeBoundaryRecord.forbiddenDirectives),
}
: null,
playerEntryPoint: playerEntryPointRecord
? {
openingIdentity: toText(playerEntryPointRecord.openingIdentity),
openingProblem: toText(playerEntryPointRecord.openingProblem),
entryMotivation: toText(playerEntryPointRecord.entryMotivation),
}
: null,
coreConflict: coreConflictRecord
? {
surfaceConflicts: toTextArray(coreConflictRecord.surfaceConflicts),
hiddenCrisis: toText(coreConflictRecord.hiddenCrisis),
firstTouchedConflict: toText(coreConflictRecord.firstTouchedConflict),
}
: null,
keyRelationships: Array.isArray(anchorContentRecord.keyRelationships)
? anchorContentRecord.keyRelationships
.map((entry) => toRecord(entry))
.filter(Boolean)
.map((entry) => ({
pairs: toText(entry?.pairs),
relationshipType: toText(entry?.relationshipType),
secretOrCost: toText(entry?.secretOrCost),
}))
: [],
hiddenLines: hiddenLinesRecord
? {
hiddenTruths: toTextArray(hiddenLinesRecord.hiddenTruths),
misdirectionHints: toTextArray(hiddenLinesRecord.misdirectionHints),
revealPacing: toText(hiddenLinesRecord.revealPacing),
}
: null,
iconicElements: iconicElementsRecord
? {
iconicMotifs: toTextArray(iconicElementsRecord.iconicMotifs),
institutionsOrArtifacts: toTextArray(
iconicElementsRecord.institutionsOrArtifacts,
),
hardRules: toTextArray(iconicElementsRecord.hardRules),
}
: null,
} satisfies EightAnchorContent;
}
function buildOpeningSceneSearchText(
profile: CustomWorldProfile,
campScene: ReturnType<typeof resolveCustomWorldCampScene>,
@@ -365,71 +586,85 @@ function buildOpeningSceneSearchText(
function buildStructuredFoundationEntries(profile: CustomWorldProfile) {
const creatorIntent = normalizeCustomWorldCreatorIntent(profile.creatorIntent);
const relationshipSeed = creatorIntent?.keyCharacters[0];
const relationshipText = relationshipSeed
? compactTextList([
relationshipSeed.name,
relationshipSeed.role,
relationshipSeed.relationToPlayer
? `与玩家:${relationshipSeed.relationToPlayer}`
: '',
relationshipSeed.hiddenHook
? `暗线:${relationshipSeed.hiddenHook}`
: '',
]).join(' · ')
: '';
const themeToneText = compactTextList([
creatorIntent?.themeKeywords.join('、') || '',
creatorIntent?.toneDirectives.join('、') || '',
]).join(' / ');
const playerOpeningText = compactTextList([
creatorIntent?.playerPremise || '',
creatorIntent?.openingSituation || '',
]).join('');
const anchorContent = getProfileAnchorContent(profile);
const fallbackRelationshipText =
buildRelationshipSeedText(creatorIntent?.keyCharacters[0]) ||
profile.playableNpcs[0]?.relationshipHooks.join('') ||
profile.storyNpcs[0]?.relationshipHooks.join('') ||
'';
return [
{
id: 'world-hook',
label: '世界一句话',
value:
creatorIntent?.worldHook ||
profile.anchorPack?.worldSummary ||
profile.summary,
id: 'world-promise',
label: '世界承诺',
value: compactTextList([
anchorContent.worldPromise?.hook || '',
anchorContent.worldPromise?.differentiator || '',
anchorContent.worldPromise?.desiredExperience || '',
]).join(''),
},
{
id: 'player-opening',
label: '玩家开局',
value: playerOpeningText || profile.playerGoal,
id: 'player-fantasy',
label: '玩家幻想',
value: compactTextList([
anchorContent.playerFantasy?.playerRole || '',
anchorContent.playerFantasy?.corePursuit || '',
anchorContent.playerFantasy?.fearOfLoss || '',
]).join(''),
},
{
id: 'theme-tone',
label: '主题气质',
value: themeToneText || profile.tone,
id: 'theme-boundary',
label: '主题边界',
value: compactTextList([
anchorContent.themeBoundary?.toneKeywords.join('、') || '',
anchorContent.themeBoundary?.aestheticDirectives.join('、') || '',
anchorContent.themeBoundary?.forbiddenDirectives.length
? `避免:${anchorContent.themeBoundary.forbiddenDirectives.join('、')}`
: '',
]).join(''),
},
{
id: 'player-entry-point',
label: '玩家切入口',
value: compactTextList([
anchorContent.playerEntryPoint?.openingIdentity || '',
anchorContent.playerEntryPoint?.openingProblem || '',
anchorContent.playerEntryPoint?.entryMotivation || '',
]).join(''),
},
{
id: 'core-conflict',
label: '核心冲突',
value:
creatorIntent?.coreConflicts.join('') ||
profile.coreConflicts.join('') ||
profile.summary,
value: compactTextList([
anchorContent.coreConflict?.surfaceConflicts.join('') || '',
anchorContent.coreConflict?.hiddenCrisis || '',
anchorContent.coreConflict?.firstTouchedConflict || '',
]).join(''),
},
{
id: 'relationship-seed',
id: 'key-relationships',
label: '关键关系',
value:
relationshipText ||
profile.playableNpcs[0]?.relationshipHooks.join('') ||
profile.storyNpcs[0]?.relationshipHooks.join('') ||
'待补充',
anchorContent.keyRelationships.map(buildKeyRelationshipText).join('\n') ||
fallbackRelationshipText,
},
{
id: 'hidden-lines',
label: '暗线与揭示',
value: compactTextList([
anchorContent.hiddenLines?.hiddenTruths.join('、') || '',
anchorContent.hiddenLines?.misdirectionHints.join('、') || '',
anchorContent.hiddenLines?.revealPacing || '',
]).join(''),
},
{
id: 'iconic-elements',
label: '标志元素',
value:
creatorIntent?.iconicElements.join('、') ||
profile.anchorPack?.motifDirectives.join('、') ||
'待补充',
value: compactTextList([
anchorContent.iconicElements?.iconicMotifs.join('、') || '',
anchorContent.iconicElements?.institutionsOrArtifacts.join('、') || '',
anchorContent.iconicElements?.hardRules.join('、') || '',
]).join(''),
},
];
}
@@ -594,12 +829,6 @@ export function CustomWorldEntityCatalog({
() => buildStructuredFoundationEntries(profile),
[profile],
);
const structuredFoundationSourceText = useMemo(
() =>
buildCustomWorldCreatorIntentFoundationText(profile.creatorIntent).trim() ||
profile.settingText.trim(),
[profile.creatorIntent, profile.settingText],
);
const normalizedCreatorIntent = useMemo(
() => normalizeCustomWorldCreatorIntent(profile.creatorIntent),
[profile.creatorIntent],
@@ -907,9 +1136,6 @@ export function CustomWorldEntityCatalog({
}
>
<div className="space-y-3">
<div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{structuredFoundationEntries.map((entry) => (
<div
@@ -919,22 +1145,12 @@ export function CustomWorldEntityCatalog({
<div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
{entry.label}
</div>
<div className="mt-2 text-sm leading-7 text-zinc-100">
<div className="mt-2 whitespace-pre-line text-sm leading-7 text-zinc-100">
{entry.value || '待补充'}
</div>
</div>
))}
</div>
{structuredFoundationSourceText ? (
<div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-4">
<div className="text-[11px] font-bold tracking-[0.18em] text-zinc-500">
</div>
<div className="mt-2 whitespace-pre-line text-sm leading-7 text-zinc-200">
{structuredFoundationSourceText}
</div>
</div>
) : null}
</div>
</Section>
</>