初始仓库迁移
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-04 23:57:06 +08:00
parent 80986b790d
commit c49c64896a
18446 changed files with 532435 additions and 2 deletions

View 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'),
})),
};
}