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 = {}, ) { 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: '来自静态物品目录。', }, }; }