This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { CampSceneEditor as default } from './RpgCreationEntityEditorShared';
|
||||
@@ -0,0 +1 @@
|
||||
export { WorldCoverEditor as default } from './RpgCreationEntityEditorShared';
|
||||
@@ -0,0 +1 @@
|
||||
export { WorldFoundationEditor as default } from './RpgCreationEntityEditorShared';
|
||||
@@ -0,0 +1 @@
|
||||
export { LandmarkEditor as default } from './RpgCreationEntityEditorShared';
|
||||
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
PlayableNpcEditor as default,
|
||||
StoryNpcEditor,
|
||||
} from './RpgCreationEntityEditorShared';
|
||||
@@ -0,0 +1 @@
|
||||
export { WorldEditor as default } from './RpgCreationEntityEditorShared';
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
import RpgCreationEntityEditorModalImpl from './RpgCreationEntityEditorModalImpl';
|
||||
import type {
|
||||
RpgCreationEditorTarget,
|
||||
} from './RpgCreationEntityEditorModalImpl';
|
||||
|
||||
/**
|
||||
* 工作包 C 完成后,编辑器 façade 已直接桥接 RPG 创作目录下的目标分发壳层。
|
||||
* 旧 `CustomWorldEntityEditorModal.tsx` 兼容入口已经删除,当前继续保留 shared 实现承载复杂表单细节,
|
||||
* 后续可在不改入口的前提下继续物理拆分 section。
|
||||
*/
|
||||
export type RpgCreationEntityEditorModalProps = ComponentProps<
|
||||
typeof RpgCreationEntityEditorModalImpl
|
||||
>;
|
||||
|
||||
export function RpgCreationEntityEditorModal(
|
||||
props: RpgCreationEntityEditorModalProps,
|
||||
) {
|
||||
return <RpgCreationEntityEditorModalImpl {...props} />;
|
||||
}
|
||||
export type { RpgCreationEditorTarget };
|
||||
|
||||
export default RpgCreationEntityEditorModal;
|
||||
@@ -0,0 +1,148 @@
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
|
||||
import CampSceneEditor from './CustomWorldCampEditorSection';
|
||||
import WorldCoverEditor from './CustomWorldCoverEditorSection';
|
||||
import WorldFoundationEditor from './CustomWorldFoundationEditorSection';
|
||||
import LandmarkEditor from './CustomWorldLandmarkEditorSection';
|
||||
import PlayableNpcEditor, {
|
||||
StoryNpcEditor,
|
||||
} from './CustomWorldRoleEditorSection';
|
||||
import WorldEditor from './CustomWorldWorldEditorSection';
|
||||
import {
|
||||
resolveEditableLandmark,
|
||||
resolveEditablePlayableNpc,
|
||||
resolveEditableStoryNpc,
|
||||
} from './rpgCreationResultFormMapper';
|
||||
|
||||
export type RpgCreationEditorTarget =
|
||||
| { kind: 'world' }
|
||||
| { kind: 'foundation' }
|
||||
| { kind: 'cover' }
|
||||
| { kind: 'camp' }
|
||||
| { kind: 'playable'; mode: 'create' }
|
||||
| { kind: 'playable'; mode: 'edit'; id: string }
|
||||
| { kind: 'story'; mode: 'create' }
|
||||
| { kind: 'story'; mode: 'edit'; id: string }
|
||||
| { kind: 'landmark'; mode: 'create' }
|
||||
| { kind: 'landmark'; mode: 'edit'; id: string };
|
||||
|
||||
export interface RpgCreationEntityEditorModalProps {
|
||||
profile: CustomWorldProfile;
|
||||
target: RpgCreationEditorTarget | null;
|
||||
onClose: () => void;
|
||||
onProfileChange: (profile: CustomWorldProfile) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作包 C 收口后的编辑器主入口只负责目标分发。
|
||||
* 具体 section 实现已经下沉到 shared/section 文件,后续可以继续物理拆分而不再膨胀主壳层。
|
||||
*/
|
||||
export function RpgCreationEntityEditorModal({
|
||||
profile,
|
||||
target,
|
||||
onClose,
|
||||
onProfileChange,
|
||||
}: RpgCreationEntityEditorModalProps) {
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (target.kind === 'world') {
|
||||
return (
|
||||
<WorldEditor
|
||||
profile={profile}
|
||||
onSave={onProfileChange}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (target.kind === 'foundation') {
|
||||
return (
|
||||
<WorldFoundationEditor
|
||||
profile={profile}
|
||||
onSave={onProfileChange}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (target.kind === 'cover') {
|
||||
return (
|
||||
<WorldCoverEditor
|
||||
profile={profile}
|
||||
onSaveProfile={onProfileChange}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (target.kind === 'camp') {
|
||||
return (
|
||||
<CampSceneEditor
|
||||
profile={profile}
|
||||
onSaveProfile={onProfileChange}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (target.kind === 'playable') {
|
||||
const npc = resolveEditablePlayableNpc(profile, target);
|
||||
return npc ? (
|
||||
<PlayableNpcEditor
|
||||
profile={profile}
|
||||
npc={npc}
|
||||
mode={target.mode}
|
||||
onSave={(nextNpc) =>
|
||||
onProfileChange({
|
||||
...profile,
|
||||
playableNpcs:
|
||||
target.mode === 'create'
|
||||
? [...profile.playableNpcs, nextNpc]
|
||||
: profile.playableNpcs.map((item) =>
|
||||
item.id === nextNpc.id ? nextNpc : item,
|
||||
),
|
||||
})
|
||||
}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
if (target.kind === 'story') {
|
||||
const npc = resolveEditableStoryNpc(profile, target);
|
||||
return npc ? (
|
||||
<StoryNpcEditor
|
||||
profile={profile}
|
||||
npc={npc}
|
||||
mode={target.mode}
|
||||
onSave={(nextNpc) =>
|
||||
onProfileChange({
|
||||
...profile,
|
||||
storyNpcs:
|
||||
target.mode === 'create'
|
||||
? [...profile.storyNpcs, nextNpc]
|
||||
: profile.storyNpcs.map((item) =>
|
||||
item.id === nextNpc.id ? nextNpc : item,
|
||||
),
|
||||
})
|
||||
}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
const landmark = resolveEditableLandmark(profile, target);
|
||||
return landmark ? (
|
||||
<LandmarkEditor
|
||||
profile={profile}
|
||||
landmark={landmark}
|
||||
mode={target.mode}
|
||||
onSaveProfile={onProfileChange}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default RpgCreationEntityEditorModal;
|
||||
6527
src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx
Normal file
6527
src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,302 @@
|
||||
import type {
|
||||
RpgCreationEditorTarget,
|
||||
RpgCreationEntityEditorModalProps,
|
||||
} from './RpgCreationEntityEditorModalImpl';
|
||||
import type {
|
||||
CustomWorldLandmark,
|
||||
CustomWorldNpc,
|
||||
CustomWorldPlayableNpc,
|
||||
CustomWorldProfile,
|
||||
} from '../../types';
|
||||
|
||||
/**
|
||||
* 工作包 C 第一轮先把编辑器目标分发需要的 profile 变更收口到 mapper。
|
||||
* 后续继续拆 section 表单时,提交 patch 和字段清洗会逐步下沉到这里。
|
||||
*/
|
||||
|
||||
function slugify(value: string) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
|
||||
return normalized || 'entry';
|
||||
}
|
||||
|
||||
function createEntryId(prefix: string, label: string, seed: number) {
|
||||
return `${prefix}-${slugify(label || `${prefix}-${seed}`)}-${seed.toString(36)}`;
|
||||
}
|
||||
|
||||
const BACKSTORY_UNLOCK_AFFINITY_EASED = 6;
|
||||
const BACKSTORY_UNLOCK_AFFINITY_FRIENDLY = 12;
|
||||
const BACKSTORY_UNLOCK_AFFINITY_TRUSTED = 18;
|
||||
const BACKSTORY_UNLOCK_AFFINITY_CLOSE = 24;
|
||||
|
||||
export function createPlayableNpcDraft(
|
||||
profile: CustomWorldProfile,
|
||||
): CustomWorldPlayableNpc {
|
||||
const seed = Date.now() + profile.playableNpcs.length;
|
||||
|
||||
return {
|
||||
id: createEntryId(
|
||||
'playable-npc',
|
||||
`角色-${profile.playableNpcs.length + 1}`,
|
||||
seed,
|
||||
),
|
||||
name: `自定义角色${profile.playableNpcs.length + 1}`,
|
||||
title: '自定义身份',
|
||||
role: '世界中的行动者',
|
||||
description: '',
|
||||
backstory: '',
|
||||
personality: '',
|
||||
motivation: '',
|
||||
combatStyle: '',
|
||||
initialAffinity: 18,
|
||||
relationshipHooks: ['首次接触', '合作空间'],
|
||||
relations: [],
|
||||
tags: ['自定义'],
|
||||
backstoryReveal: {
|
||||
publicSummary: '',
|
||||
chapters: [
|
||||
{
|
||||
id: 'surface',
|
||||
title: '表层来意',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_EASED,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
{
|
||||
id: 'scar',
|
||||
title: '旧事裂痕',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_FRIENDLY,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
{
|
||||
id: 'hidden',
|
||||
title: '隐藏执念',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_TRUSTED,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
{
|
||||
id: 'final',
|
||||
title: '最终底牌',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_CLOSE,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
skills: [
|
||||
{ id: 'skill-1', name: '基础起手', summary: '', style: '起手压制' },
|
||||
{ id: 'skill-2', name: '常用变招', summary: '', style: '机动周旋' },
|
||||
{ id: 'skill-3', name: '压箱底牌', summary: '', style: '爆发终结' },
|
||||
],
|
||||
initialItems: [
|
||||
{
|
||||
id: 'item-1',
|
||||
name: '随身武具',
|
||||
category: '武器',
|
||||
quantity: 1,
|
||||
rarity: 'rare',
|
||||
description: '',
|
||||
tags: ['自定义'],
|
||||
},
|
||||
{
|
||||
id: 'item-2',
|
||||
name: '补给包',
|
||||
category: '消耗品',
|
||||
quantity: 2,
|
||||
rarity: 'uncommon',
|
||||
description: '',
|
||||
tags: ['自定义'],
|
||||
},
|
||||
{
|
||||
id: 'item-3',
|
||||
name: '私人物件',
|
||||
category: '专属物品',
|
||||
quantity: 1,
|
||||
rarity: 'rare',
|
||||
description: '',
|
||||
tags: ['自定义'],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function createStoryNpcDraft(
|
||||
profile: Pick<CustomWorldProfile, 'storyNpcs'>,
|
||||
): CustomWorldNpc {
|
||||
const seed = Date.now() + profile.storyNpcs.length;
|
||||
|
||||
return {
|
||||
id: createEntryId(
|
||||
'story-npc',
|
||||
`场景角色-${profile.storyNpcs.length + 1}`,
|
||||
seed,
|
||||
),
|
||||
name: `自定义场景角色${profile.storyNpcs.length + 1}`,
|
||||
title: '自定义头衔',
|
||||
role: '自定义身份',
|
||||
description: '',
|
||||
backstory: '',
|
||||
personality: '',
|
||||
motivation: '',
|
||||
combatStyle: '',
|
||||
initialAffinity: 6,
|
||||
relationshipHooks: ['合作', '互动'],
|
||||
relations: [],
|
||||
tags: ['自定义'],
|
||||
backstoryReveal: {
|
||||
publicSummary: '',
|
||||
chapters: [
|
||||
{
|
||||
id: 'surface',
|
||||
title: '表层来意',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_EASED,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
{
|
||||
id: 'scar',
|
||||
title: '旧事裂痕',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_FRIENDLY,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
{
|
||||
id: 'hidden',
|
||||
title: '隐藏执念',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_TRUSTED,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
{
|
||||
id: 'final',
|
||||
title: '最终底牌',
|
||||
affinityRequired: BACKSTORY_UNLOCK_AFFINITY_CLOSE,
|
||||
teaser: '',
|
||||
content: '',
|
||||
contextSnippet: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
skills: [
|
||||
{ id: 'skill-1', name: '基础起手', summary: '', style: '起手压制' },
|
||||
{ id: 'skill-2', name: '常用变招', summary: '', style: '机动周旋' },
|
||||
{ id: 'skill-3', name: '压箱底牌', summary: '', style: '爆发终结' },
|
||||
],
|
||||
initialItems: [
|
||||
{
|
||||
id: 'item-1',
|
||||
name: '随身武具',
|
||||
category: '武器',
|
||||
quantity: 1,
|
||||
rarity: 'rare',
|
||||
description: '',
|
||||
tags: ['自定义'],
|
||||
},
|
||||
{
|
||||
id: 'item-2',
|
||||
name: '补给包',
|
||||
category: '消耗品',
|
||||
quantity: 2,
|
||||
rarity: 'uncommon',
|
||||
description: '',
|
||||
tags: ['自定义'],
|
||||
},
|
||||
{
|
||||
id: 'item-3',
|
||||
name: '私人物件',
|
||||
category: '专属物品',
|
||||
quantity: 1,
|
||||
rarity: 'rare',
|
||||
description: '',
|
||||
tags: ['自定义'],
|
||||
},
|
||||
],
|
||||
} satisfies CustomWorldNpc;
|
||||
}
|
||||
|
||||
export function createLandmarkDraft(
|
||||
profile: CustomWorldProfile,
|
||||
): CustomWorldLandmark {
|
||||
const seed = Date.now() + profile.landmarks.length;
|
||||
const previousLandmark = profile.landmarks[profile.landmarks.length - 1];
|
||||
|
||||
return {
|
||||
id: createEntryId(
|
||||
'landmark',
|
||||
`scene-${profile.landmarks.length + 1}`,
|
||||
seed,
|
||||
),
|
||||
name: `自定义场景${profile.landmarks.length + 1}`,
|
||||
description: '',
|
||||
imageSrc: undefined,
|
||||
sceneNpcIds: profile.storyNpcs.slice(0, 3).map((npc) => npc.id),
|
||||
connections: previousLandmark
|
||||
? [
|
||||
{
|
||||
targetLandmarkId: previousLandmark.id,
|
||||
relativePosition: 'south',
|
||||
summary: `南侧可回到${previousLandmark.name}`,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildEditorTargetRendererParams(
|
||||
props: RpgCreationEntityEditorModalProps,
|
||||
) {
|
||||
const { profile, target, onClose, onProfileChange } = props;
|
||||
|
||||
return {
|
||||
onClose,
|
||||
onProfileChange,
|
||||
profile,
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveEditablePlayableNpc(
|
||||
profile: CustomWorldProfile,
|
||||
target: Extract<RpgCreationEditorTarget, { kind: 'playable' }>,
|
||||
) {
|
||||
if (target.mode === 'create') {
|
||||
return createPlayableNpcDraft(profile);
|
||||
}
|
||||
|
||||
return profile.playableNpcs.find((item) => item.id === target.id) ?? null;
|
||||
}
|
||||
|
||||
export function resolveEditableStoryNpc(
|
||||
profile: CustomWorldProfile,
|
||||
target: Extract<RpgCreationEditorTarget, { kind: 'story' }>,
|
||||
) {
|
||||
if (target.mode === 'create') {
|
||||
return createStoryNpcDraft(profile);
|
||||
}
|
||||
|
||||
return profile.storyNpcs.find((item) => item.id === target.id) ?? null;
|
||||
}
|
||||
|
||||
export function resolveEditableLandmark(
|
||||
profile: CustomWorldProfile,
|
||||
target: Extract<RpgCreationEditorTarget, { kind: 'landmark' }>,
|
||||
) {
|
||||
if (target.mode === 'create') {
|
||||
return createLandmarkDraft(profile);
|
||||
}
|
||||
|
||||
return profile.landmarks.find((item) => item.id === target.id) ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user