305 lines
7.8 KiB
TypeScript
305 lines
7.8 KiB
TypeScript
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: ['自定义'],
|
|
},
|
|
],
|
|
templateCharacterId: profile.playableNpcs[0]?.templateCharacterId,
|
|
};
|
|
}
|
|
|
|
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: '',
|
|
dangerLevel: '中',
|
|
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;
|
|
}
|