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

@@ -21,6 +21,10 @@ import {coerceWorldAttributeSchema} from './attributeValidation';
const CUSTOM_WORLD_LIBRARY_STORAGE_KEY = 'tavernrealms.custom-world-library.v1';
const CUSTOM_WORLD_LIBRARY_VERSION = 1;
const MAX_SAVED_CUSTOM_WORLDS = 12;
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;
const ITEM_RARITIES = new Set<ItemRarity>(['common', 'uncommon', 'rare', 'epic', 'legendary']);
const EQUIPMENT_SLOTS = new Set<EquipmentSlotId>(['weapon', 'armor', 'relic']);
const CUSTOM_WORLD_NPC_VISUAL_RACES = new Set<CustomWorldNpcVisualRace>(['human', 'elf', 'orc', 'goblin']);
@@ -52,6 +56,13 @@ function toOptionalInteger(value: unknown) {
return typeof value === 'number' && Number.isFinite(value) ? Math.round(value) : undefined;
}
function normalizeInitialAffinity(value: unknown, fallback: number) {
const resolved = typeof value === 'number' && Number.isFinite(value)
? Math.round(value)
: fallback;
return Math.max(MIN_CUSTOM_WORLD_AFFINITY, Math.min(MAX_CUSTOM_WORLD_AFFINITY, resolved));
}
function normalizeEquipmentSlot(value: unknown) {
return typeof value === 'string' && EQUIPMENT_SLOTS.has(value as EquipmentSlotId)
? value as EquipmentSlotId
@@ -129,16 +140,24 @@ function normalizePlayableNpc(value: unknown, index: number): CustomWorldPlayabl
const name = toText(value.name);
if (!name) return null;
const title = toText(value.title, toText(value.role, '未命名角色'));
const role = toText(value.role, title);
const relationshipHooks = toStringArray(value.relationshipHooks);
const tags = toStringArray(value.tags);
return {
id: toText(value.id, `saved-playable-${index + 1}`),
name,
title: toText(value.title, '未命名角色'),
title,
role,
description: toText(value.description),
backstory: toText(value.backstory),
personality: toText(value.personality),
motivation: toText(value.motivation, toText(value.description)),
combatStyle: toText(value.combatStyle),
tags: toStringArray(value.tags),
initialAffinity: normalizeInitialAffinity(value.initialAffinity, DEFAULT_PLAYABLE_INITIAL_AFFINITY),
relationshipHooks: relationshipHooks.length > 0 ? relationshipHooks : tags.slice(0, 3),
tags: tags.length > 0 ? tags : relationshipHooks.slice(0, 5),
templateCharacterId: toText(value.templateCharacterId) || undefined,
};
}
@@ -148,14 +167,24 @@ function normalizeStoryNpc(value: unknown, index: number): CustomWorldNpc | null
const name = toText(value.name);
if (!name) return null;
const title = toText(value.title, toText(value.role, '未命名场景角色'));
const role = toText(value.role, title);
const relationshipHooks = toStringArray(value.relationshipHooks);
const tags = toStringArray(value.tags);
return {
id: toText(value.id, `saved-story-${index + 1}`),
name,
role: toText(value.role, '未命名场景角色'),
title,
role,
description: toText(value.description),
backstory: toText(value.backstory),
personality: toText(value.personality),
motivation: toText(value.motivation),
relationshipHooks: toStringArray(value.relationshipHooks),
combatStyle: toText(value.combatStyle),
initialAffinity: normalizeInitialAffinity(value.initialAffinity, DEFAULT_STORY_NPC_INITIAL_AFFINITY),
relationshipHooks: relationshipHooks.length > 0 ? relationshipHooks : tags.slice(0, 3),
tags: tags.length > 0 ? tags : relationshipHooks.slice(0, 5),
imageSrc: toText(value.imageSrc) || undefined,
visual: normalizeCustomWorldNpcVisual(value.visual),
};