338 lines
8.9 KiB
TypeScript
338 lines
8.9 KiB
TypeScript
import type {
|
|
InventoryItem,
|
|
ItemCatalogEntry,
|
|
ItemCatalogOverride,
|
|
ItemRarity,
|
|
WorldType,
|
|
} from '../types';
|
|
import {
|
|
EDITOR_ITEM_CATALOG_API_PATH,
|
|
} from '../editor/shared/editorApiClient';
|
|
import { buildDesignedItemMetadata } from './itemDesign';
|
|
|
|
export { EDITOR_ITEM_CATALOG_API_PATH as ITEM_CATALOG_API_PATH };
|
|
|
|
export const ITEM_CATEGORY_OPTIONS = [
|
|
'武器',
|
|
'护甲',
|
|
'饰品',
|
|
'消耗品',
|
|
'材料',
|
|
'稀有品',
|
|
'专属品',
|
|
] as const;
|
|
|
|
const CATEGORY_WEAPON = ITEM_CATEGORY_OPTIONS[0];
|
|
const CATEGORY_ARMOR = ITEM_CATEGORY_OPTIONS[1];
|
|
const CATEGORY_RELIC = ITEM_CATEGORY_OPTIONS[2];
|
|
const CATEGORY_CONSUMABLE = ITEM_CATEGORY_OPTIONS[3];
|
|
const CATEGORY_MATERIAL = ITEM_CATEGORY_OPTIONS[4];
|
|
const CATEGORY_RARE = ITEM_CATEGORY_OPTIONS[5];
|
|
const CATEGORY_EXCLUSIVE = ITEM_CATEGORY_OPTIONS[6];
|
|
|
|
const WEAPON_KEYWORDS = [
|
|
'weapon',
|
|
'sword',
|
|
'axe',
|
|
'bow',
|
|
'arrow',
|
|
'mace',
|
|
'wand',
|
|
'staff',
|
|
'pick',
|
|
'spade',
|
|
'blade',
|
|
'dagger',
|
|
'spear',
|
|
'hammer',
|
|
];
|
|
|
|
const ARMOR_KEYWORDS = [
|
|
'armor',
|
|
'armour',
|
|
'helm',
|
|
'helmet',
|
|
'chest',
|
|
'pants',
|
|
'boots',
|
|
'glove',
|
|
'glowes',
|
|
'shield',
|
|
'cloak',
|
|
'robe',
|
|
'cap',
|
|
];
|
|
|
|
const ACCESSORY_KEYWORDS = [
|
|
'ring',
|
|
'neck',
|
|
'amulet',
|
|
'jewel',
|
|
'jewelry',
|
|
'bracelet',
|
|
'relic',
|
|
'gem',
|
|
];
|
|
|
|
const CONSUMABLE_KEYWORDS = [
|
|
'potion',
|
|
'bottle',
|
|
'water',
|
|
'meat',
|
|
'apple',
|
|
'mushroom',
|
|
'bandage',
|
|
'torch',
|
|
'candle',
|
|
'food',
|
|
];
|
|
|
|
const MATERIAL_KEYWORDS = [
|
|
'wood',
|
|
'stone',
|
|
'leaf',
|
|
'flower',
|
|
'skin',
|
|
'rope',
|
|
'coin',
|
|
'silverbar',
|
|
'ore',
|
|
'bar',
|
|
'material',
|
|
];
|
|
|
|
const RARE_KEYWORDS = [
|
|
'scroll',
|
|
'book',
|
|
'bag',
|
|
'skull',
|
|
'cross',
|
|
'stairway',
|
|
'crystal',
|
|
'magic',
|
|
];
|
|
|
|
const EXCLUSIVE_KEYWORDS = [
|
|
'treasure',
|
|
'relic',
|
|
'artifact',
|
|
'legend',
|
|
'sacred',
|
|
];
|
|
|
|
function normalizeAssetPath(sourcePath: string) {
|
|
return sourcePath
|
|
.replace(/^public[\\/]/iu, '')
|
|
.replace(/\\/g, '/')
|
|
.replace(/^\/+/u, '');
|
|
}
|
|
|
|
function stripExtension(value: string) {
|
|
return value.replace(/\.[^.]+$/u, '');
|
|
}
|
|
|
|
function humanizeAssetPart(value: string) {
|
|
const cleaned = stripExtension(value)
|
|
.replace(/^\d+[_-]*/u, '')
|
|
.replace(/[_-]+/g, ' ')
|
|
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
|
|
if (!cleaned) return '';
|
|
|
|
return cleaned
|
|
.split(' ')
|
|
.map(part => {
|
|
const firstCharacter = part[0];
|
|
return part && firstCharacter ? firstCharacter.toUpperCase() + part.slice(1) : '';
|
|
})
|
|
.join(' ');
|
|
}
|
|
|
|
function includesAnyKeyword(text: string, keywords: string[]) {
|
|
return keywords.some(keyword => text.includes(keyword));
|
|
}
|
|
|
|
function dedupeTags(tags: string[]) {
|
|
return [...new Set(tags.filter(Boolean))];
|
|
}
|
|
|
|
export function buildItemCatalogId(sourcePath: string) {
|
|
return stripExtension(normalizeAssetPath(sourcePath))
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9/]+/g, '-')
|
|
.replace(/\/+/g, '__')
|
|
.replace(/-+/g, '-')
|
|
.replace(/^[-_]+|[-_]+$/g, '');
|
|
}
|
|
|
|
export function buildItemCatalogName(sourcePath: string) {
|
|
const normalized = normalizeAssetPath(sourcePath);
|
|
const parts = normalized.split('/');
|
|
const leafName = humanizeAssetPart(parts[parts.length - 1] ?? '');
|
|
const parentName = humanizeAssetPart(parts[parts.length - 2] ?? '');
|
|
|
|
if (!leafName && parentName) return parentName;
|
|
if (!leafName) return '未命名物品';
|
|
if (/^(Items|Icons|Singles|Variants|Text)$/u.test(leafName) && parentName) {
|
|
return `${parentName} ${leafName}`;
|
|
}
|
|
|
|
return leafName;
|
|
}
|
|
|
|
export function inferItemCatalogCategory(sourcePath: string) {
|
|
const normalized = normalizeAssetPath(sourcePath).toLowerCase();
|
|
|
|
if (includesAnyKeyword(normalized, WEAPON_KEYWORDS)) return '武器';
|
|
if (includesAnyKeyword(normalized, ARMOR_KEYWORDS)) return '护甲';
|
|
if (includesAnyKeyword(normalized, ACCESSORY_KEYWORDS)) return '饰品';
|
|
if (includesAnyKeyword(normalized, CONSUMABLE_KEYWORDS)) return '消耗品';
|
|
if (includesAnyKeyword(normalized, MATERIAL_KEYWORDS)) return '材料';
|
|
if (includesAnyKeyword(normalized, EXCLUSIVE_KEYWORDS)) return '专属品';
|
|
if (includesAnyKeyword(normalized, RARE_KEYWORDS)) return '稀有品';
|
|
|
|
return '稀有品';
|
|
}
|
|
|
|
export function inferItemCatalogRarity(sourcePath: string, category: string): ItemRarity {
|
|
const normalized = normalizeAssetPath(sourcePath).toLowerCase();
|
|
|
|
if (includesAnyKeyword(normalized, EXCLUSIVE_KEYWORDS)) return 'legendary';
|
|
if (includesAnyKeyword(normalized, ['magic', 'crystal', 'wand', 'gem', 'gold'])) return 'epic';
|
|
if (category === '武器' || category === '护甲' || category === '饰品' || category === '专属品') return 'rare';
|
|
if (category === '消耗品' || category === '材料') return 'uncommon';
|
|
|
|
return 'common';
|
|
}
|
|
|
|
export function inferItemCatalogTags(sourcePath: string, category: string) {
|
|
const normalized = normalizeAssetPath(sourcePath).toLowerCase();
|
|
const tags: string[] = [];
|
|
|
|
if (category === '武器') tags.push('weapon');
|
|
if (category === '护甲') tags.push('armor');
|
|
if (category === '饰品' || category === '专属品') tags.push('relic');
|
|
if (category === '材料') tags.push('material');
|
|
|
|
if (includesAnyKeyword(normalized, ['potion', 'bandage', 'water', 'meat', 'apple', 'mushroom'])) {
|
|
tags.push('healing');
|
|
}
|
|
|
|
if (includesAnyKeyword(normalized, ['mana', 'magic', 'crystal', 'gem', 'wand'])) {
|
|
tags.push('mana');
|
|
}
|
|
|
|
return dedupeTags(tags);
|
|
}
|
|
|
|
export function buildItemCatalogDescription(
|
|
sourcePath: string,
|
|
category: string,
|
|
name: string,
|
|
) {
|
|
return `由图标素材 ${normalizeAssetPath(sourcePath)} 自动生成的${category}物品“${name}”,可在编辑器中继续调整名称、稀有度、标签与描述。`;
|
|
}
|
|
|
|
export function buildBaseItemCatalogEntry(sourcePath: string): ItemCatalogEntry {
|
|
const normalizedSourcePath = normalizeAssetPath(sourcePath);
|
|
const name = buildItemCatalogName(normalizedSourcePath);
|
|
const category = inferItemCatalogCategory(normalizedSourcePath);
|
|
const rarity = inferItemCatalogRarity(normalizedSourcePath, category);
|
|
const tags = inferItemCatalogTags(normalizedSourcePath, category);
|
|
const designed = buildDesignedItemMetadata(
|
|
normalizedSourcePath,
|
|
name,
|
|
category,
|
|
rarity,
|
|
tags,
|
|
{
|
|
weapon: CATEGORY_WEAPON,
|
|
armor: CATEGORY_ARMOR,
|
|
relic: CATEGORY_RELIC,
|
|
consumable: CATEGORY_CONSUMABLE,
|
|
material: CATEGORY_MATERIAL,
|
|
rare: CATEGORY_RARE,
|
|
exclusive: CATEGORY_EXCLUSIVE,
|
|
},
|
|
);
|
|
|
|
return {
|
|
id: buildItemCatalogId(normalizedSourcePath),
|
|
sourcePath: normalizedSourcePath,
|
|
iconSrc: `/${normalizedSourcePath}`,
|
|
name: designed.name ?? name,
|
|
category: designed.category ?? category,
|
|
rarity: designed.rarity ?? rarity,
|
|
tags: dedupeTags(designed.tags ?? tags),
|
|
description: designed.description ?? buildItemCatalogDescription(normalizedSourcePath, category, name),
|
|
worldAffinity: designed.worldAffinity ?? 'neutral',
|
|
equipmentSlotId: designed.equipmentSlotId ?? null,
|
|
worldProfiles: designed.worldProfiles,
|
|
statProfile: designed.statProfile ?? null,
|
|
useProfile: designed.useProfile ?? null,
|
|
buildProfile: designed.buildProfile ?? null,
|
|
value: designed.value,
|
|
};
|
|
}
|
|
|
|
export function applyItemCatalogOverride(
|
|
baseItem: ItemCatalogEntry,
|
|
override?: ItemCatalogOverride | null,
|
|
): ItemCatalogEntry {
|
|
if (!override) return baseItem;
|
|
|
|
return {
|
|
...baseItem,
|
|
...override,
|
|
tags: override.tags ? dedupeTags(override.tags) : baseItem.tags,
|
|
worldProfiles: override.worldProfiles ?? baseItem.worldProfiles,
|
|
statProfile: override.statProfile ?? baseItem.statProfile,
|
|
useProfile: override.useProfile ?? baseItem.useProfile,
|
|
buildProfile: override.buildProfile ?? baseItem.buildProfile,
|
|
};
|
|
}
|
|
|
|
export function buildItemCatalogFromAssetPaths(
|
|
assetPaths: string[],
|
|
overrideMap: Record<string, ItemCatalogOverride> = {},
|
|
) {
|
|
return assetPaths
|
|
.map(sourcePath => buildBaseItemCatalogEntry(sourcePath))
|
|
.map(item => applyItemCatalogOverride(item, overrideMap[item.id]))
|
|
.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
|
|
}
|
|
|
|
export function createInventoryItemFromCatalogEntry(
|
|
item: ItemCatalogEntry,
|
|
quantity = 1,
|
|
worldType: WorldType | null = null,
|
|
): InventoryItem {
|
|
const worldProfile = worldType ? item.worldProfiles?.[worldType] : null;
|
|
return {
|
|
id: `catalog:${item.id}`,
|
|
catalogId: item.id,
|
|
category: item.category,
|
|
name: worldProfile?.name ?? item.name,
|
|
quantity,
|
|
rarity: item.rarity,
|
|
tags: [...item.tags],
|
|
iconSrc: item.iconSrc,
|
|
description: worldProfile?.description ?? item.description,
|
|
worldAffinity: item.worldAffinity,
|
|
equipmentSlotId: item.equipmentSlotId,
|
|
worldProfiles: item.worldProfiles,
|
|
statProfile: item.statProfile,
|
|
useProfile: item.useProfile,
|
|
buildProfile: item.buildProfile,
|
|
value: item.value,
|
|
runtimeMetadata: {
|
|
origin: 'catalog',
|
|
generationChannel: 'discovery',
|
|
seedKey: `catalog:${item.id}`,
|
|
sourceReason: '来自静态物品目录。',
|
|
},
|
|
};
|
|
}
|