init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -0,0 +1 @@
export { CampSceneEditor as default } from './RpgCreationEntityEditorShared';

View File

@@ -0,0 +1 @@
export { WorldCoverEditor as default } from './RpgCreationEntityEditorShared';

View File

@@ -0,0 +1 @@
export { WorldFoundationEditor as default } from './RpgCreationEntityEditorShared';

View File

@@ -0,0 +1 @@
export { LandmarkEditor as default } from './RpgCreationEntityEditorShared';

View File

@@ -0,0 +1,4 @@
export {
PlayableNpcEditor as default,
StoryNpcEditor,
} from './RpgCreationEntityEditorShared';

View File

@@ -0,0 +1 @@
export { WorldEditor as default } from './RpgCreationEntityEditorShared';

View File

@@ -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;

View File

@@ -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;

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}