Refine NPC interactions and runtime item generation
This commit is contained in:
@@ -77,6 +77,14 @@ function commaText(value: string[]) {
|
||||
return value.join(', ');
|
||||
}
|
||||
|
||||
function clampInitialAffinity(value: string, fallback: number) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.max(-40, Math.min(90, Math.round(parsed)));
|
||||
}
|
||||
|
||||
function useDraft<T>(value: T) {
|
||||
const [draft, setDraft] = useState(value);
|
||||
useEffect(() => setDraft(value), [value]);
|
||||
@@ -726,8 +734,8 @@ function StoryNpcVisualEditorModal({
|
||||
/>
|
||||
{isAiGenerateOpen ? (
|
||||
<AiComingSoonModal
|
||||
title="AI生成场景角色形象"
|
||||
subtitle="场景角色形象AI生成功能仍在开发中。"
|
||||
title="智能生成场景角色形象"
|
||||
subtitle="场景角色形象智能生成功能仍在开发中。"
|
||||
onClose={() => setIsAiGenerateOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
@@ -900,6 +908,14 @@ function PlayableNpcEditor({
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="世界身份 / 职责">
|
||||
<TextInput
|
||||
value={draft.role}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({ ...current, role: value }))
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="简介">
|
||||
<TextArea
|
||||
value={draft.description}
|
||||
@@ -927,6 +943,15 @@ function PlayableNpcEditor({
|
||||
rows={3}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="当前动机">
|
||||
<TextArea
|
||||
value={draft.motivation}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({ ...current, motivation: value }))
|
||||
}
|
||||
rows={3}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="战斗风格">
|
||||
<TextArea
|
||||
value={draft.combatStyle}
|
||||
@@ -936,6 +961,33 @@ function PlayableNpcEditor({
|
||||
rows={3}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="初始好感">
|
||||
<TextInput
|
||||
type="number"
|
||||
value={draft.initialAffinity}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
initialAffinity: clampInitialAffinity(
|
||||
value,
|
||||
current.initialAffinity,
|
||||
),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="关系切入口">
|
||||
<TextArea
|
||||
value={commaText(draft.relationshipHooks)}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
relationshipHooks: parseCommaText(value),
|
||||
}))
|
||||
}
|
||||
rows={2}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="标签">
|
||||
<TextArea
|
||||
value={commaText(draft.tags)}
|
||||
@@ -975,21 +1027,7 @@ function StoryNpcEditor({
|
||||
onSave: (npc: CustomWorldNpc) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const initialDraft = useMemo(
|
||||
() => ({
|
||||
...npc,
|
||||
visual:
|
||||
npc.visual ??
|
||||
buildDefaultCustomWorldNpcVisual({
|
||||
id: npc.id,
|
||||
name: npc.name,
|
||||
role: npc.role,
|
||||
description: npc.description,
|
||||
}),
|
||||
}),
|
||||
[npc],
|
||||
);
|
||||
const [draft, setDraft] = useDraft(initialDraft);
|
||||
const [draft, setDraft] = useDraft(npc);
|
||||
const [isVisualEditorOpen, setIsVisualEditorOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -1003,12 +1041,7 @@ function StoryNpcEditor({
|
||||
<div className="grid gap-4 sm:grid-cols-[10rem_minmax(0,1fr)] sm:items-center">
|
||||
<div className="flex justify-center">
|
||||
<CustomWorldNpcPortrait
|
||||
npc={{
|
||||
id: draft.id,
|
||||
name: draft.name,
|
||||
role: draft.role,
|
||||
description: draft.description,
|
||||
}}
|
||||
npc={draft}
|
||||
visual={draft.visual}
|
||||
className="aspect-square w-full max-w-[9.5rem]"
|
||||
scale={2.05}
|
||||
@@ -1042,6 +1075,14 @@ function StoryNpcEditor({
|
||||
/>
|
||||
</Field>
|
||||
<Field label="头衔 / 职能">
|
||||
<TextInput
|
||||
value={draft.title}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({ ...current, title: value }))
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="世界身份 / 职能">
|
||||
<TextInput
|
||||
value={draft.role}
|
||||
onChange={(value) =>
|
||||
@@ -1058,6 +1099,24 @@ function StoryNpcEditor({
|
||||
rows={4}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="背景">
|
||||
<TextArea
|
||||
value={draft.backstory}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({ ...current, backstory: value }))
|
||||
}
|
||||
rows={4}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="性格">
|
||||
<TextArea
|
||||
value={draft.personality}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({ ...current, personality: value }))
|
||||
}
|
||||
rows={3}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="动机">
|
||||
<TextArea
|
||||
value={draft.motivation}
|
||||
@@ -1067,6 +1126,30 @@ function StoryNpcEditor({
|
||||
rows={4}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="战斗风格">
|
||||
<TextArea
|
||||
value={draft.combatStyle}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({ ...current, combatStyle: value }))
|
||||
}
|
||||
rows={3}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="初始好感">
|
||||
<TextInput
|
||||
type="number"
|
||||
value={draft.initialAffinity}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
initialAffinity: clampInitialAffinity(
|
||||
value,
|
||||
current.initialAffinity,
|
||||
),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="关系切入口">
|
||||
<TextArea
|
||||
value={commaText(draft.relationshipHooks)}
|
||||
@@ -1079,6 +1162,18 @@ function StoryNpcEditor({
|
||||
rows={3}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="标签">
|
||||
<TextArea
|
||||
value={commaText(draft.tags)}
|
||||
onChange={(value) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
tags: parseCommaText(value),
|
||||
}))
|
||||
}
|
||||
rows={2}
|
||||
/>
|
||||
</Field>
|
||||
<SaveBar
|
||||
onClose={onClose}
|
||||
onSave={() => {
|
||||
@@ -1089,7 +1184,15 @@ function StoryNpcEditor({
|
||||
{isVisualEditorOpen ? (
|
||||
<StoryNpcVisualEditorModal
|
||||
npc={draft}
|
||||
visual={draft.visual!}
|
||||
visual={
|
||||
draft.visual ??
|
||||
buildDefaultCustomWorldNpcVisual({
|
||||
id: draft.id,
|
||||
name: draft.name,
|
||||
role: draft.role,
|
||||
description: draft.description,
|
||||
})
|
||||
}
|
||||
onChange={(visual) =>
|
||||
setDraft((current) => ({ ...current, visual }))
|
||||
}
|
||||
@@ -1235,10 +1338,14 @@ function createPlayableNpc(
|
||||
),
|
||||
name: `自定义角色${profile.playableNpcs.length + 1}`,
|
||||
title: '自定义身份',
|
||||
role: '世界中的行动者',
|
||||
description: '',
|
||||
backstory: '',
|
||||
personality: '',
|
||||
motivation: '',
|
||||
combatStyle: '',
|
||||
initialAffinity: 18,
|
||||
relationshipHooks: ['首次接触', '合作空间'],
|
||||
tags: ['自定义'],
|
||||
templateCharacterId: template?.id,
|
||||
};
|
||||
@@ -1253,21 +1360,19 @@ function createStoryNpc(profile: CustomWorldProfile): CustomWorldNpc {
|
||||
seed,
|
||||
),
|
||||
name: `自定义场景角色${profile.storyNpcs.length + 1}`,
|
||||
title: '自定义头衔',
|
||||
role: '自定义身份',
|
||||
description: '',
|
||||
backstory: '',
|
||||
personality: '',
|
||||
motivation: '',
|
||||
combatStyle: '',
|
||||
initialAffinity: 6,
|
||||
relationshipHooks: ['合作', '互动'],
|
||||
tags: ['自定义'],
|
||||
} satisfies CustomWorldNpc;
|
||||
|
||||
return {
|
||||
...npc,
|
||||
visual: buildDefaultCustomWorldNpcVisual({
|
||||
id: npc.id,
|
||||
name: npc.name,
|
||||
role: npc.role,
|
||||
description: npc.description,
|
||||
}),
|
||||
};
|
||||
return npc;
|
||||
}
|
||||
|
||||
function createLandmark(profile: CustomWorldProfile): CustomWorldLandmark {
|
||||
|
||||
Reference in New Issue
Block a user