1
This commit is contained in:
@@ -32,6 +32,14 @@ import {
|
||||
buildDefaultCustomWorldCoverProfile,
|
||||
resolveCustomWorldCoverPresentation,
|
||||
} from '../../services/customWorldCover';
|
||||
import {
|
||||
getCustomWorldFoundationAnchorContent,
|
||||
parseFoundationTagText,
|
||||
type CustomWorldFoundationEntryId,
|
||||
} from '../../services/customWorldFoundationEntries';
|
||||
import {
|
||||
createEmptyCustomWorldCreatorIntent,
|
||||
} from '../../services/customWorldCreatorIntent';
|
||||
import {
|
||||
type CustomWorldCoverAssetResult,
|
||||
generateCustomWorldCoverImage,
|
||||
@@ -94,6 +102,7 @@ import {
|
||||
|
||||
export type RpgCreationEditorTarget =
|
||||
| { kind: 'world' }
|
||||
| { kind: 'foundation' }
|
||||
| { kind: 'cover' }
|
||||
| { kind: 'camp' }
|
||||
| { kind: 'playable'; mode: 'create' }
|
||||
@@ -4522,6 +4531,270 @@ export function WorldEditor({
|
||||
);
|
||||
}
|
||||
|
||||
type FoundationDraft = Record<CustomWorldFoundationEntryId, string>;
|
||||
|
||||
const FOUNDATION_EDITOR_FIELDS: Array<{
|
||||
id: CustomWorldFoundationEntryId;
|
||||
label: string;
|
||||
rows: number;
|
||||
}> = [
|
||||
{ id: 'world-promise', label: '世界承诺', rows: 4 },
|
||||
{ id: 'player-fantasy', label: '玩家幻想', rows: 4 },
|
||||
{ id: 'theme-boundary', label: '主题边界', rows: 4 },
|
||||
{ id: 'player-entry-point', label: '玩家切入口', rows: 4 },
|
||||
{ id: 'core-conflict', label: '核心冲突', rows: 4 },
|
||||
{ id: 'key-relationships', label: '关键关系', rows: 4 },
|
||||
{ id: 'hidden-lines', label: '暗线与揭示', rows: 4 },
|
||||
{ id: 'iconic-elements', label: '标志元素', rows: 4 },
|
||||
];
|
||||
|
||||
function buildFoundationDraft(profile: CustomWorldProfile): FoundationDraft {
|
||||
const anchorContent = getCustomWorldFoundationAnchorContent(profile);
|
||||
|
||||
return {
|
||||
'world-promise': [
|
||||
anchorContent.worldPromise?.hook || '',
|
||||
anchorContent.worldPromise?.differentiator || '',
|
||||
anchorContent.worldPromise?.desiredExperience || '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';'),
|
||||
'player-fantasy': [
|
||||
anchorContent.playerFantasy?.playerRole || '',
|
||||
anchorContent.playerFantasy?.corePursuit || '',
|
||||
anchorContent.playerFantasy?.fearOfLoss || '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';'),
|
||||
'theme-boundary': [
|
||||
anchorContent.themeBoundary?.toneKeywords.join('、') || '',
|
||||
anchorContent.themeBoundary?.aestheticDirectives.join('、') || '',
|
||||
anchorContent.themeBoundary?.forbiddenDirectives.length
|
||||
? `避免:${anchorContent.themeBoundary.forbiddenDirectives.join('、')}`
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';'),
|
||||
'player-entry-point': [
|
||||
anchorContent.playerEntryPoint?.openingIdentity || '',
|
||||
anchorContent.playerEntryPoint?.openingProblem || '',
|
||||
anchorContent.playerEntryPoint?.entryMotivation || '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';'),
|
||||
'core-conflict': [
|
||||
anchorContent.coreConflict?.surfaceConflicts.join('、') || '',
|
||||
anchorContent.coreConflict?.hiddenCrisis || '',
|
||||
anchorContent.coreConflict?.firstTouchedConflict || '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';'),
|
||||
'key-relationships': anchorContent.keyRelationships
|
||||
.map((entry) =>
|
||||
[entry.pairs, entry.relationshipType, entry.secretOrCost]
|
||||
.filter(Boolean)
|
||||
.join(';'),
|
||||
)
|
||||
.join('\n'),
|
||||
'hidden-lines': [
|
||||
anchorContent.hiddenLines?.hiddenTruths.join('、') || '',
|
||||
anchorContent.hiddenLines?.misdirectionHints.join('、') || '',
|
||||
anchorContent.hiddenLines?.revealPacing || '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';'),
|
||||
'iconic-elements': [
|
||||
anchorContent.iconicElements?.iconicMotifs.join('、') || '',
|
||||
anchorContent.iconicElements?.institutionsOrArtifacts.join('、') || '',
|
||||
anchorContent.iconicElements?.hardRules.join('、') || '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';'),
|
||||
};
|
||||
}
|
||||
|
||||
function splitCommaTags(value: string) {
|
||||
return value
|
||||
.split(/[、,,]/u)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function stripAvoidPrefix(value: string) {
|
||||
return value.replace(/^避免[::]\s*/u, '').trim();
|
||||
}
|
||||
|
||||
function applyFoundationDraftToProfile(
|
||||
profile: CustomWorldProfile,
|
||||
draft: FoundationDraft,
|
||||
): CustomWorldProfile {
|
||||
const worldPromiseTags = parseFoundationTagText(draft['world-promise']);
|
||||
const playerFantasyTags = parseFoundationTagText(draft['player-fantasy']);
|
||||
const themeBoundaryTags = parseFoundationTagText(draft['theme-boundary']);
|
||||
const playerEntryTags = parseFoundationTagText(draft['player-entry-point']);
|
||||
const coreConflictTags = parseFoundationTagText(draft['core-conflict']);
|
||||
const hiddenLineTags = parseFoundationTagText(draft['hidden-lines']);
|
||||
const iconicElementTags = parseFoundationTagText(draft['iconic-elements']);
|
||||
const relationshipLines = draft['key-relationships']
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const creatorIntent =
|
||||
profile.creatorIntent ?? createEmptyCustomWorldCreatorIntent('freeform');
|
||||
|
||||
return {
|
||||
...profile,
|
||||
summary: worldPromiseTags[0] || profile.summary,
|
||||
subtitle: worldPromiseTags[1] || profile.subtitle,
|
||||
tone: themeBoundaryTags[0] || profile.tone,
|
||||
playerGoal: playerFantasyTags[1] || profile.playerGoal,
|
||||
worldHook: worldPromiseTags[0] || profile.worldHook || null,
|
||||
playerPremise: playerFantasyTags[0] || profile.playerPremise || null,
|
||||
coreConflicts: coreConflictTags[0]
|
||||
? splitCommaTags(coreConflictTags[0])
|
||||
: profile.coreConflicts,
|
||||
creatorIntent: {
|
||||
...creatorIntent,
|
||||
worldHook: worldPromiseTags[0] || creatorIntent.worldHook,
|
||||
playerPremise: playerFantasyTags[0] || creatorIntent.playerPremise,
|
||||
openingSituation: playerEntryTags[1] || creatorIntent.openingSituation,
|
||||
themeKeywords: themeBoundaryTags[0]
|
||||
? splitCommaTags(themeBoundaryTags[0])
|
||||
: creatorIntent.themeKeywords,
|
||||
toneDirectives: themeBoundaryTags[1]
|
||||
? splitCommaTags(themeBoundaryTags[1])
|
||||
: creatorIntent.toneDirectives,
|
||||
coreConflicts: coreConflictTags[0]
|
||||
? splitCommaTags(coreConflictTags[0])
|
||||
: creatorIntent.coreConflicts,
|
||||
iconicElements: iconicElementTags[0]
|
||||
? splitCommaTags(iconicElementTags[0])
|
||||
: creatorIntent.iconicElements,
|
||||
forbiddenDirectives: themeBoundaryTags[2]
|
||||
? splitCommaTags(stripAvoidPrefix(themeBoundaryTags[2]))
|
||||
: creatorIntent.forbiddenDirectives,
|
||||
},
|
||||
anchorContent: {
|
||||
worldPromise: {
|
||||
hook: worldPromiseTags[0] || '',
|
||||
differentiator: worldPromiseTags[1] || '',
|
||||
desiredExperience: worldPromiseTags[2] || '',
|
||||
},
|
||||
playerFantasy: {
|
||||
playerRole: playerFantasyTags[0] || '',
|
||||
corePursuit: playerFantasyTags[1] || '',
|
||||
fearOfLoss: playerFantasyTags[2] || '',
|
||||
},
|
||||
themeBoundary: {
|
||||
toneKeywords: themeBoundaryTags[0]
|
||||
? splitCommaTags(themeBoundaryTags[0])
|
||||
: [],
|
||||
aestheticDirectives: themeBoundaryTags[1]
|
||||
? splitCommaTags(themeBoundaryTags[1])
|
||||
: [],
|
||||
forbiddenDirectives: themeBoundaryTags[2]
|
||||
? splitCommaTags(stripAvoidPrefix(themeBoundaryTags[2]))
|
||||
: [],
|
||||
},
|
||||
playerEntryPoint: {
|
||||
openingIdentity: playerEntryTags[0] || '',
|
||||
openingProblem: playerEntryTags[1] || '',
|
||||
entryMotivation: playerEntryTags[2] || '',
|
||||
},
|
||||
coreConflict: {
|
||||
surfaceConflicts: coreConflictTags[0]
|
||||
? splitCommaTags(coreConflictTags[0])
|
||||
: [],
|
||||
hiddenCrisis: coreConflictTags[1] || '',
|
||||
firstTouchedConflict: coreConflictTags[2] || '',
|
||||
},
|
||||
keyRelationships: relationshipLines.map((line) => {
|
||||
const tags = parseFoundationTagText(line);
|
||||
return {
|
||||
pairs: tags[0] || '',
|
||||
relationshipType: tags[1] || '',
|
||||
secretOrCost: tags[2] || '',
|
||||
};
|
||||
}),
|
||||
hiddenLines: {
|
||||
hiddenTruths: hiddenLineTags[0] ? splitCommaTags(hiddenLineTags[0]) : [],
|
||||
misdirectionHints: hiddenLineTags[1]
|
||||
? splitCommaTags(hiddenLineTags[1])
|
||||
: [],
|
||||
revealPacing: hiddenLineTags[2] || '',
|
||||
},
|
||||
iconicElements: {
|
||||
iconicMotifs: iconicElementTags[0]
|
||||
? splitCommaTags(iconicElementTags[0])
|
||||
: [],
|
||||
institutionsOrArtifacts: iconicElementTags[1]
|
||||
? splitCommaTags(iconicElementTags[1])
|
||||
: [],
|
||||
hardRules: iconicElementTags[2] ? splitCommaTags(iconicElementTags[2]) : [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function WorldFoundationEditor({
|
||||
profile,
|
||||
onSave,
|
||||
onClose,
|
||||
}: {
|
||||
profile: CustomWorldProfile;
|
||||
onSave: (profile: CustomWorldProfile) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const initialDraft = useMemo(() => buildFoundationDraft(profile), [profile]);
|
||||
const [draft, setDraft] = useDraft(initialDraft);
|
||||
|
||||
return (
|
||||
<ModalShell
|
||||
title="编辑基本设定"
|
||||
onClose={onClose}
|
||||
panelClassName="sm:max-w-4xl"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{FOUNDATION_EDITOR_FIELDS.map((field) => (
|
||||
<Field key={field.id} label={field.label}>
|
||||
<div className="space-y-3">
|
||||
<TextArea
|
||||
value={draft[field.id]}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
[field.id]: value,
|
||||
}))
|
||||
}
|
||||
rows={field.rows}
|
||||
/>
|
||||
{draft[field.id].trim() ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{parseFoundationTagText(draft[field.id]).map((tag, index) => (
|
||||
<span
|
||||
key={`${field.id}-${index}-${tag}`}
|
||||
className="rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-xs leading-5 text-zinc-100"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Field>
|
||||
))}
|
||||
<SaveBar
|
||||
onClose={onClose}
|
||||
onSave={() => {
|
||||
onSave(applyFoundationDraftToProfile(profile, draft));
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ModalShell>
|
||||
);
|
||||
}
|
||||
|
||||
export function CampSceneEditor({
|
||||
profile,
|
||||
onSaveProfile,
|
||||
@@ -5921,6 +6194,16 @@ function RpgCreationEntityEditorModal({
|
||||
);
|
||||
}
|
||||
|
||||
if (target.kind === 'foundation') {
|
||||
return (
|
||||
<WorldFoundationEditor
|
||||
profile={profile}
|
||||
onSave={onProfileChange}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (target.kind === 'cover') {
|
||||
return (
|
||||
<WorldCoverEditor
|
||||
|
||||
Reference in New Issue
Block a user