146
src/services/customWorldBuilder.ts
Normal file
146
src/services/customWorldBuilder.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
buildCustomWorldPlayableNpcAttributeProfile,
|
||||
buildCustomWorldStoryNpcAttributeProfile,
|
||||
buildItemAttributeResonance,
|
||||
} from '../data/attributeProfileGenerator';
|
||||
import { mergeCustomWorldPlayableNpcTags } from '../data/customWorldBuildTags';
|
||||
import { CustomWorldProfile, WorldType } from '../types';
|
||||
import { normalizeCustomWorldProfile } from './customWorld';
|
||||
|
||||
const PLAYABLE_TEMPLATE_CHARACTER_IDS = [
|
||||
'sword-princess',
|
||||
'archer-hero',
|
||||
'girl-hero',
|
||||
'punch-hero',
|
||||
'fighter-4',
|
||||
] as const;
|
||||
|
||||
function pickCyclic<T>(items: readonly T[], index: number, label: string): T {
|
||||
const item = items[index % items.length];
|
||||
if (item === undefined) {
|
||||
throw new Error(`Missing ${label}`);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
function getPlayableTemplateCharacterId(index: number) {
|
||||
return pickCyclic(
|
||||
PLAYABLE_TEMPLATE_CHARACTER_IDS,
|
||||
index,
|
||||
'playable template character id',
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeTags(tags: string[], fallbackTags: string[] = []) {
|
||||
return [
|
||||
...new Set(
|
||||
[...tags, ...fallbackTags].map((tag) => tag.trim()).filter(Boolean),
|
||||
),
|
||||
].slice(0, 5);
|
||||
}
|
||||
|
||||
function normalizeHooks(hooks: string[]) {
|
||||
const normalized = [
|
||||
...new Set(hooks.map((hook) => hook.trim()).filter(Boolean)),
|
||||
];
|
||||
if (normalized.length > 0) {
|
||||
return normalized.slice(0, 3);
|
||||
}
|
||||
return ['掌握关键线索'];
|
||||
}
|
||||
|
||||
function clampText(value: string, maxLength: number) {
|
||||
const normalized = value.trim().replace(/\s+/g, ' ');
|
||||
if (normalized.length <= maxLength) {
|
||||
return normalized;
|
||||
}
|
||||
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trim()}…`;
|
||||
}
|
||||
|
||||
function slugify(value: string) {
|
||||
const ascii = value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
|
||||
if (ascii) {
|
||||
return ascii.slice(0, 24);
|
||||
}
|
||||
|
||||
return 'entry';
|
||||
}
|
||||
|
||||
function createEntryId(prefix: string, label: string, index: number) {
|
||||
return `${prefix}-${slugify(label || `${prefix}-${index + 1}`)}-${index + 1}`;
|
||||
}
|
||||
|
||||
function dedupeByName<T extends { name: string }>(items: T[]) {
|
||||
const seen = new Set<string>();
|
||||
return items.filter((item) => {
|
||||
const key = item.name.trim();
|
||||
if (!key || seen.has(key)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export interface CustomWorldBuilderOptions {}
|
||||
|
||||
export function buildExpandedCustomWorldProfile(
|
||||
raw: unknown,
|
||||
settingText: string,
|
||||
_options: CustomWorldBuilderOptions = {},
|
||||
): CustomWorldProfile {
|
||||
const profile = normalizeCustomWorldProfile(raw, settingText);
|
||||
const attributeSchema = profile.attributeSchema;
|
||||
|
||||
return {
|
||||
...profile,
|
||||
playableNpcs: dedupeByName(profile.playableNpcs)
|
||||
.slice(0, PLAYABLE_TEMPLATE_CHARACTER_IDS.length)
|
||||
.map((npc, index) => {
|
||||
const templateCharacterId =
|
||||
npc.templateCharacterId ?? getPlayableTemplateCharacterId(index);
|
||||
return {
|
||||
...npc,
|
||||
id: createEntryId('playable-npc', npc.name, index),
|
||||
templateCharacterId,
|
||||
tags: mergeCustomWorldPlayableNpcTags(profile, npc, {
|
||||
templateCharacterId,
|
||||
maxCount: 5,
|
||||
}),
|
||||
attributeProfile:
|
||||
npc.attributeProfile ??
|
||||
buildCustomWorldPlayableNpcAttributeProfile(npc, attributeSchema),
|
||||
};
|
||||
}),
|
||||
storyNpcs: dedupeByName(profile.storyNpcs).map((npc, index) => ({
|
||||
...npc,
|
||||
id: createEntryId('story-npc', npc.name, index),
|
||||
description: clampText(npc.description, 72),
|
||||
motivation: clampText(npc.motivation, 72),
|
||||
relationshipHooks: normalizeHooks(npc.relationshipHooks),
|
||||
attributeProfile:
|
||||
npc.attributeProfile ??
|
||||
buildCustomWorldStoryNpcAttributeProfile(npc, attributeSchema),
|
||||
})),
|
||||
items: dedupeByName(profile.items).map((item, index) => ({
|
||||
...item,
|
||||
id: createEntryId('item', item.name, index),
|
||||
description: clampText(item.description, 72),
|
||||
tags: normalizeTags(item.tags),
|
||||
attributeResonance:
|
||||
item.attributeResonance ?? buildItemAttributeResonance(item),
|
||||
})),
|
||||
landmarks: dedupeByName(profile.landmarks).map((landmark, index) => ({
|
||||
...landmark,
|
||||
id: createEntryId('landmark', landmark.name, index),
|
||||
description: clampText(landmark.description, 96),
|
||||
dangerLevel:
|
||||
landmark.dangerLevel ||
|
||||
(profile.templateWorldType === WorldType.XIANXIA ? 'high' : 'medium'),
|
||||
})),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user