1
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
import type {
|
||||
CustomWorldCoverProfile,
|
||||
CustomWorldCoverSourceType,
|
||||
CustomWorldItem,
|
||||
CustomWorldPlayableNpc,
|
||||
} from '../runtimeTypes.js';
|
||||
|
||||
/**
|
||||
* 工作包 G:
|
||||
* 把 runtime profile 编译流程里的通用常量、基础文本归一和通用小型归一逻辑收口到共享模块,
|
||||
* 让角色、地点、场景章节和主编译入口都不再重复维护这些基础能力。
|
||||
*/
|
||||
|
||||
const MIN_CUSTOM_WORLD_AFFINITY = -40;
|
||||
const MAX_CUSTOM_WORLD_AFFINITY = 90;
|
||||
const CUSTOM_WORLD_RARITIES = [
|
||||
'common',
|
||||
'uncommon',
|
||||
'rare',
|
||||
'epic',
|
||||
'legendary',
|
||||
] as const;
|
||||
const CUSTOM_WORLD_ROLE_ITEM_CATEGORIES = [
|
||||
'武器',
|
||||
'护甲',
|
||||
'饰品',
|
||||
'消耗品',
|
||||
'材料',
|
||||
'稀有品',
|
||||
'专属物品',
|
||||
'专属物',
|
||||
] as const;
|
||||
|
||||
export const MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT = 5;
|
||||
export const MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT = 30;
|
||||
export const MIN_CUSTOM_WORLD_LANDMARK_COUNT = 10;
|
||||
export const MIN_CUSTOM_WORLD_STORY_NPC_COUNT = Math.max(
|
||||
0,
|
||||
MIN_CUSTOM_WORLD_TOTAL_NPC_COUNT - MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT,
|
||||
);
|
||||
|
||||
export const PLAYABLE_TEMPLATE_CHARACTER_IDS = [
|
||||
'sword-princess',
|
||||
'archer-hero',
|
||||
'girl-hero',
|
||||
'punch-hero',
|
||||
'fighter-4',
|
||||
] as const;
|
||||
|
||||
export function toText(value: unknown) {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
export function toFiniteInteger(value: unknown) {
|
||||
return typeof value === 'number' && Number.isFinite(value)
|
||||
? Math.round(value)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function toRecord(value: unknown) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function toRecordArray(value: unknown) {
|
||||
return Array.isArray(value)
|
||||
? (value.filter((item) => item && typeof item === 'object') as Array<
|
||||
Record<string, unknown>
|
||||
>)
|
||||
: [];
|
||||
}
|
||||
|
||||
export function toStringArray(value: unknown, nestedKey?: string) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return value
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return item.trim();
|
||||
}
|
||||
if (nestedKey && item && typeof item === 'object') {
|
||||
return toText((item as Record<string, unknown>)[nestedKey]);
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function normalizeTags(value: unknown, fallbackTags: string[] = []) {
|
||||
const tags = Array.isArray(value)
|
||||
? value.map((item) => toText(item)).filter(Boolean)
|
||||
: [];
|
||||
return [
|
||||
...new Set((tags.length > 0 ? tags : fallbackTags).filter(Boolean)),
|
||||
].slice(0, 5);
|
||||
}
|
||||
|
||||
export function clampText(value: string, maxLength: number) {
|
||||
const normalized = value.trim().replace(/\s+/g, ' ');
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
if (normalized.length <= maxLength) {
|
||||
return normalized;
|
||||
}
|
||||
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trim()}…`;
|
||||
}
|
||||
|
||||
export function slugify(value: string) {
|
||||
const ascii = value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
|
||||
return ascii ? ascii.slice(0, 24) : 'entry';
|
||||
}
|
||||
|
||||
export function createEntryId(prefix: string, label: string, index: number) {
|
||||
return `${prefix}-${slugify(label || `${prefix}-${index + 1}`)}-${index + 1}`;
|
||||
}
|
||||
|
||||
export function clampCustomWorldAffinity(value: number) {
|
||||
return Math.max(
|
||||
MIN_CUSTOM_WORLD_AFFINITY,
|
||||
Math.min(MAX_CUSTOM_WORLD_AFFINITY, Math.round(value)),
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeInitialAffinity(value: unknown, fallback: number) {
|
||||
return typeof value === 'number' && Number.isFinite(value)
|
||||
? clampCustomWorldAffinity(value)
|
||||
: fallback;
|
||||
}
|
||||
|
||||
export function normalizeRarity(
|
||||
value: unknown,
|
||||
fallback: (typeof CUSTOM_WORLD_RARITIES)[number] = 'rare',
|
||||
) {
|
||||
const rarity = toText(value).toLowerCase();
|
||||
return CUSTOM_WORLD_RARITIES.includes(
|
||||
rarity as (typeof CUSTOM_WORLD_RARITIES)[number],
|
||||
)
|
||||
? (rarity as (typeof CUSTOM_WORLD_RARITIES)[number])
|
||||
: fallback;
|
||||
}
|
||||
|
||||
export function normalizeRoleItemCategory(value: unknown, fallback = '材料') {
|
||||
const category = toText(value);
|
||||
if (
|
||||
(CUSTOM_WORLD_ROLE_ITEM_CATEGORIES as readonly string[]).includes(category)
|
||||
) {
|
||||
return category === '专属物' ? '专属物品' : category;
|
||||
}
|
||||
if (/武器|刀|剑|弓|枪|炮|锤/u.test(category)) return '武器';
|
||||
if (/甲|护|盾|衣|袍/u.test(category)) return '护甲';
|
||||
if (/饰|符|佩|坠|珠|印/u.test(category)) return '饰品';
|
||||
if (/药|食|补给|瓶|卷轴/u.test(category)) return '消耗品';
|
||||
if (/材|矿|木|石|鳞|骨/u.test(category)) return '材料';
|
||||
if (/秘|钥|图|册|录|卷/u.test(category)) return '稀有品';
|
||||
if (/信物|遗物|核心|母印|真符/u.test(category)) return '专属物品';
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function normalizeCustomWorldCoverCharacterRoleIds(
|
||||
value: unknown,
|
||||
playableNpcs: Array<Pick<CustomWorldPlayableNpc, 'id'>>,
|
||||
) {
|
||||
const availableIds = new Set(
|
||||
playableNpcs.map((entry) => entry.id.trim()).filter(Boolean),
|
||||
);
|
||||
const selectedIds = Array.isArray(value)
|
||||
? [
|
||||
...new Set(
|
||||
value
|
||||
.map((entry) => toText(entry))
|
||||
.filter((entry) => entry && availableIds.has(entry)),
|
||||
),
|
||||
].slice(0, 3)
|
||||
: [];
|
||||
|
||||
if (selectedIds.length > 0) {
|
||||
return selectedIds;
|
||||
}
|
||||
|
||||
return playableNpcs
|
||||
.map((entry) => entry.id.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 3);
|
||||
}
|
||||
|
||||
export function buildDefaultCustomWorldCover(
|
||||
playableNpcs: Array<Pick<CustomWorldPlayableNpc, 'id'>>,
|
||||
): CustomWorldCoverProfile {
|
||||
return {
|
||||
sourceType: 'default' as const,
|
||||
imageSrc: null,
|
||||
characterRoleIds: normalizeCustomWorldCoverCharacterRoleIds(
|
||||
undefined,
|
||||
playableNpcs,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeCustomWorldCover(
|
||||
value: unknown,
|
||||
playableNpcs: Array<Pick<CustomWorldPlayableNpc, 'id'>>,
|
||||
): CustomWorldCoverProfile {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return buildDefaultCustomWorldCover(playableNpcs);
|
||||
}
|
||||
|
||||
const item = value as Record<string, unknown>;
|
||||
const sourceType: CustomWorldCoverSourceType =
|
||||
item.sourceType === 'uploaded' || item.sourceType === 'generated'
|
||||
? item.sourceType
|
||||
: 'default';
|
||||
const imageSrc = toText(item.imageSrc) || null;
|
||||
|
||||
if (sourceType !== 'default' && imageSrc) {
|
||||
return {
|
||||
sourceType,
|
||||
imageSrc,
|
||||
characterRoleIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
return buildDefaultCustomWorldCover(playableNpcs);
|
||||
}
|
||||
|
||||
export function normalizeItemList(value: unknown) {
|
||||
return toRecordArray(value)
|
||||
.map((item, index) => {
|
||||
const name = toText(item.name);
|
||||
const category = toText(item.category);
|
||||
return {
|
||||
id: toText(item.id) || createEntryId('item', name, index),
|
||||
name,
|
||||
category,
|
||||
rarity: normalizeRarity(item.rarity, 'rare'),
|
||||
description: toText(item.description),
|
||||
tags: normalizeTags(item.tags),
|
||||
} satisfies CustomWorldItem;
|
||||
})
|
||||
.filter((entry) => entry.name && entry.category);
|
||||
}
|
||||
Reference in New Issue
Block a user