Refine NPC interactions and runtime item generation

This commit is contained in:
2026-04-05 17:13:07 +08:00
parent c49c64896a
commit 89cecda7da
58 changed files with 4199 additions and 1562 deletions

View File

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