Refine NPC interactions and runtime item generation
This commit is contained in:
@@ -17,6 +17,10 @@ const CUSTOM_WORLD_RARITIES: ItemRarity[] = [
|
||||
'epic',
|
||||
'legendary',
|
||||
];
|
||||
const MIN_CUSTOM_WORLD_AFFINITY = -40;
|
||||
const MAX_CUSTOM_WORLD_AFFINITY = 90;
|
||||
const DEFAULT_PLAYABLE_INITIAL_AFFINITY = 18;
|
||||
const DEFAULT_STORY_NPC_INITIAL_AFFINITY = 6;
|
||||
|
||||
export const MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT = 5;
|
||||
export const MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT = 30;
|
||||
@@ -39,20 +43,30 @@ export const CUSTOM_WORLD_GENERATION_SYSTEM_PROMPT = `你正在为像素动作 R
|
||||
{
|
||||
"name": "角色名称",
|
||||
"title": "称号",
|
||||
"role": "在世界中的身份/职责",
|
||||
"description": "简短描述",
|
||||
"backstory": "背景经历",
|
||||
"personality": "性格特点",
|
||||
"motivation": "当前动机",
|
||||
"combatStyle": "战斗风格",
|
||||
"initialAffinity": 18,
|
||||
"relationshipHooks": ["关系切入口1", "关系切入口2"],
|
||||
"tags": ["标签1", "标签2"]
|
||||
}
|
||||
],
|
||||
"storyNpcs": [
|
||||
{
|
||||
"name": "场景角色名称",
|
||||
"title": "称号",
|
||||
"role": "身份",
|
||||
"description": "简短描述",
|
||||
"backstory": "背景经历",
|
||||
"personality": "性格特点",
|
||||
"motivation": "动机",
|
||||
"relationshipHooks": ["关系切入口1", "关系切入口2"]
|
||||
"combatStyle": "战斗风格",
|
||||
"initialAffinity": 6,
|
||||
"relationshipHooks": ["关系切入口1", "关系切入口2"],
|
||||
"tags": ["标签1", "标签2"]
|
||||
}
|
||||
],
|
||||
"landmarks": [
|
||||
@@ -72,8 +86,14 @@ export const CUSTOM_WORLD_GENERATION_SYSTEM_PROMPT = `你正在为像素动作 R
|
||||
- 必须生成足够多的 storyNpcs,使唯一角色总数至少达到 30。
|
||||
- 至少生成 10 个 landmarks。
|
||||
- 不要生成 items 字段。
|
||||
- playableNpcs 与 storyNpcs 中的每个角色都必须使用完全相同的字段结构,不要省略字段,也不要只给其中一类角色加私有字段。
|
||||
- initialAffinity 必须是整数,范围控制在 -40 到 90。
|
||||
- 可扮演角色通常从基础信任起步,initialAffinity 建议不低于 18;敌对角色、怪物型角色或开局明显 hostile 的 NPC,initialAffinity 应为负数。
|
||||
- 怪物也视为 NPC,可以直接出现在 storyNpcs 中;不要额外拆出 monster 字段。
|
||||
- 如果某个 NPC 更适合走怪物素材,请在 role、description、backstory、combatStyle、tags 中明确写出怪物特征、栖息环境、攻击方式或异形外观,方便后续形象解析同时引用 Medieval 和怪物素材。
|
||||
- 名称必须具体且有辨识度,不要使用 角色1、场景1 之类的占位名。
|
||||
- 名册中要覆盖多种社会身份,不能只有战斗角色。
|
||||
- storyNpcs 里既要有可交流、可合作的角色,也要允许出现敌对、怪物型或强压迫感的角色。
|
||||
- 地标必须像真实可游玩的场景,能够承载探索、战斗、旅行和剧情推进。
|
||||
- 不要引用现实品牌、受版权保护的 IP 或知名既有人物。`;
|
||||
|
||||
@@ -98,6 +118,19 @@ function normalizeTags(value: unknown, fallbackTags: string[] = []) {
|
||||
].slice(0, 5);
|
||||
}
|
||||
|
||||
function clampCustomWorldAffinity(value: number) {
|
||||
return Math.max(
|
||||
MIN_CUSTOM_WORLD_AFFINITY,
|
||||
Math.min(MAX_CUSTOM_WORLD_AFFINITY, Math.round(value)),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeInitialAffinity(value: unknown, fallback: number) {
|
||||
return typeof value === 'number' && Number.isFinite(value)
|
||||
? clampCustomWorldAffinity(value)
|
||||
: fallback;
|
||||
}
|
||||
|
||||
function normalizeWorldType(value: unknown, sourceText: string) {
|
||||
const worldType = toText(value).toUpperCase();
|
||||
if (worldType === WorldType.WUXIA || worldType === WorldType.XIANXIA) {
|
||||
@@ -207,15 +240,28 @@ function normalizePlayableNpcList(value: unknown) {
|
||||
return toRecordArray(value)
|
||||
.map((item, index) => {
|
||||
const name = toText(item.name);
|
||||
const title = toText(item.title) || toText(item.role) || '未定称号';
|
||||
const role = toText(item.role) || title;
|
||||
const relationshipHooks = normalizeTags(
|
||||
item.relationshipHooks,
|
||||
normalizeTags(item.tags),
|
||||
);
|
||||
return {
|
||||
id: createEntryId('playable-npc', name, index),
|
||||
name,
|
||||
title: toText(item.title),
|
||||
title,
|
||||
role,
|
||||
description: toText(item.description),
|
||||
backstory: toText(item.backstory),
|
||||
personality: toText(item.personality),
|
||||
motivation: toText(item.motivation) || toText(item.description),
|
||||
combatStyle: toText(item.combatStyle),
|
||||
tags: normalizeTags(item.tags),
|
||||
initialAffinity: normalizeInitialAffinity(
|
||||
item.initialAffinity,
|
||||
DEFAULT_PLAYABLE_INITIAL_AFFINITY,
|
||||
),
|
||||
relationshipHooks,
|
||||
tags: normalizeTags(item.tags, relationshipHooks),
|
||||
} satisfies CustomWorldPlayableNpc;
|
||||
})
|
||||
.filter((entry) => entry.name)
|
||||
@@ -226,13 +272,28 @@ function normalizeStoryNpcList(value: unknown) {
|
||||
return toRecordArray(value)
|
||||
.map((item, index) => {
|
||||
const name = toText(item.name);
|
||||
const title = toText(item.title) || toText(item.role) || '未定称号';
|
||||
const role = toText(item.role) || title;
|
||||
const relationshipHooks = normalizeTags(
|
||||
item.relationshipHooks,
|
||||
normalizeTags(item.tags),
|
||||
);
|
||||
return {
|
||||
id: createEntryId('story-npc', name, index),
|
||||
name,
|
||||
role: toText(item.role),
|
||||
title,
|
||||
role,
|
||||
description: toText(item.description),
|
||||
backstory: toText(item.backstory),
|
||||
personality: toText(item.personality),
|
||||
motivation: toText(item.motivation),
|
||||
relationshipHooks: normalizeTags(item.relationshipHooks),
|
||||
combatStyle: toText(item.combatStyle),
|
||||
initialAffinity: normalizeInitialAffinity(
|
||||
item.initialAffinity,
|
||||
DEFAULT_STORY_NPC_INITIAL_AFFINITY,
|
||||
),
|
||||
relationshipHooks,
|
||||
tags: normalizeTags(item.tags, relationshipHooks),
|
||||
} satisfies CustomWorldNpc;
|
||||
})
|
||||
.filter((entry) => entry.name);
|
||||
@@ -339,8 +400,11 @@ export function buildCustomWorldGenerationPrompt(settingText: string) {
|
||||
'- 必须生成足够多的 storyNpcs,使唯一角色总数至少达到 30。',
|
||||
'- 至少生成 10 个真正可游玩的 landmarks。',
|
||||
'- 不要生成任何 items,也不要包含 items 字段。',
|
||||
'- playableNpcs 与 storyNpcs 必须使用同一套字段结构:name、title、role、description、backstory、personality、motivation、combatStyle、initialAffinity、relationshipHooks、tags。',
|
||||
'- initialAffinity 必须是 -40 到 90 的整数;可扮演角色通常不低于 18,敌对或怪物型 NPC 应使用负数。',
|
||||
'- 每个场景角色和地标都必须直接源自玩家设定。',
|
||||
'- 要覆盖多种社会身份,不能只有战斗角色。',
|
||||
'- 怪物也视为 NPC,怪物型角色仍然放进 storyNpcs,并在文字里明确写出怪物特征、栖息环境或攻击方式,方便后续形象解析引用怪物素材。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -349,14 +413,14 @@ export function buildCustomWorldReferenceText(profile: CustomWorldProfile) {
|
||||
.slice(0, 3)
|
||||
.map(
|
||||
(npc) =>
|
||||
`- ${npc.name} / ${npc.title}:${npc.description};背景:${npc.backstory};风格:${npc.combatStyle}`,
|
||||
`- ${npc.name} / ${npc.title}:${npc.description};身份:${npc.role};背景:${npc.backstory};动机:${npc.motivation};风格:${npc.combatStyle};初始好感:${npc.initialAffinity}`,
|
||||
)
|
||||
.join('\n');
|
||||
const storyNpcText = profile.storyNpcs
|
||||
.slice(0, 8)
|
||||
.map(
|
||||
(npc) =>
|
||||
`- ${npc.name} / ${npc.role}:${npc.description};动机:${npc.motivation}`,
|
||||
`- ${npc.name} / ${npc.role}:${npc.description};称号:${npc.title};背景:${npc.backstory};动机:${npc.motivation};风格:${npc.combatStyle};初始好感:${npc.initialAffinity}`,
|
||||
)
|
||||
.join('\n');
|
||||
const landmarkText = profile.landmarks
|
||||
|
||||
Reference in New Issue
Block a user