import { type CustomWorldThemeMode, detectCustomWorldThemeMode, resolveCustomWorldCompatibilityTemplateWorldType, } from '../services/customWorldTheme'; import { CustomWorldItem, CustomWorldProfile, InventoryItem, WorldTemplateType, WorldType } from '../types'; let runtimeCustomWorldProfile: CustomWorldProfile | null = null; export function setRuntimeCustomWorldProfile(profile: CustomWorldProfile | null) { runtimeCustomWorldProfile = profile; } export function getRuntimeCustomWorldProfile() { return runtimeCustomWorldProfile; } export function resolveCompatibilityTemplateWorldType( worldType: WorldType | null | undefined, customWorldProfile: CustomWorldProfile | null | undefined = runtimeCustomWorldProfile, ): WorldTemplateType | null { if (!worldType) return null; if (worldType === WorldType.CUSTOM) { return customWorldProfile ? resolveCustomWorldCompatibilityTemplateWorldType(customWorldProfile) : WorldType.WUXIA; } return worldType; } export function isCustomWorldType(worldType: WorldType | null | undefined) { return worldType === WorldType.CUSTOM; } function hashText(value: string) { let hash = 0; for (let index = 0; index < value.length; index += 1) { hash = (hash * 31 + value.charCodeAt(index)) >>> 0; } return hash >>> 0; } function compactStrings(values: Array) { return [...new Set( values .map(value => typeof value === 'string' ? value.trim() : '') .filter(Boolean), )]; } function pickCyclic(items: readonly T[], index: number, fallback: T): T { return items[index % items.length] ?? fallback; } function normalizeInventoryItemId(item: CustomWorldItem, quantity: number, seedKey: string) { return `custom:${item.id}:${quantity}:${hashText(seedKey).toString(36)}`; } function toInventoryItem(item: CustomWorldItem, quantity: number, seedKey: string): InventoryItem { return { id: normalizeInventoryItemId(item, quantity, seedKey), category: item.category, name: item.name, quantity, rarity: item.rarity, tags: [...item.tags], iconSrc: item.iconSrc, description: item.description, equipmentSlotId: item.equipmentSlotId ?? null, statProfile: item.statProfile ?? null, useProfile: item.useProfile ?? null, value: item.value, runtimeMetadata: { origin: 'procedural', generationChannel: 'discovery', seedKey, sourceReason: `围绕自定义世界 ${runtimeCustomWorldProfile?.name ?? '未知世界'} 的主题即时生成。`, }, }; } export interface RuntimeCustomWorldItemQueryOptions { categories?: string[]; tags?: string[]; preferredTags?: string[]; keywords?: string[]; count?: number; quantity?: number; rarityFloor?: CustomWorldItem['rarity']; } const RARITY_ORDER: CustomWorldItem['rarity'][] = ['common', 'uncommon', 'rare', 'epic', 'legendary']; const DEFAULT_RUNTIME_CATEGORIES = ['武器', '护甲', '饰品', '消耗品', '材料', '稀有品', '专属物'] as const; const CATEGORY_DEFAULT_TAGS: Record = { 武器: ['weapon', '战斗'], 护甲: ['armor', '防护'], 饰品: ['relic', 'mana'], 消耗品: ['healing', '补给'], 材料: ['material', '采集'], 稀有品: ['rare', '线索'], 专属物: ['rare', '剧情关键'], }; const WORLD_ITEM_PREFIXES: Record = { mythic: ['远岸', '回声', '曙迹', '长旅', '微光', '新铭'], martial: ['风雨', '断桥', '青锋', '旧案', '夜行', '残影'], arcane: ['灵纹', '道痕', '云篆', '星芒', '界辉', '玉简'], machina: ['铁脊', '脉冲', '新星', '等离', '钢律', '核列'], tide: ['潮纹', '霜浪', '天澜', '海晕', '潮歌', '沧流'], rift: ['裂痕', '灰域', '界桥', '断层', '回响', '前哨'], }; const WORLD_ITEM_NOUNS: Record = { 武器: ['刃', '剑', '弓', '枪', '印', '锤'], 护甲: ['甲', '衣', '护符', '披风', '战铠', '护腕'], 饰品: ['坠', '环', '佩', '珠', '印记', '信物'], 消耗品: ['药', '露', '符', '瓶', '包', '散'], 材料: ['砂', '石', '铁', '木', '羽', '晶'], 稀有品: ['残页', '密卷', '古钥', '图录', '印匣', '秘函'], 专属物: ['遗物', '核心', '母印', '真符', '遗钥', '界核'], }; function normalizeLookupText(value: string) { return value.trim().toLowerCase(); } function getRarityFloorValue(rarityFloor?: CustomWorldItem['rarity']) { return rarityFloor ? RARITY_ORDER.indexOf(rarityFloor) : -1; } function sanitizeNameFragment(value: string) { return value.replace(/[^\u4e00-\u9fa5a-z0-9]/giu, '').slice(0, 4); } function getWorldSeedLabel(profile: CustomWorldProfile) { const fromName = sanitizeNameFragment(profile.name); if (fromName) return fromName; const fromSetting = sanitizeNameFragment(profile.settingText); if (fromSetting) return fromSetting; return '旅境'; } function buildRuntimeItemTags( category: string, options: RuntimeCustomWorldItemQueryOptions, seed: number, ) { const baseTags = [...(CATEGORY_DEFAULT_TAGS[category] ?? ['world-item'])]; const preferredTags = [...new Set((options.preferredTags ?? []).map(tag => tag.trim()).filter(Boolean))]; const keywordTags = [...new Set((options.keywords ?? []).map(tag => tag.trim()).filter(Boolean))]; const selectedPreferredTag = preferredTags.length > 0 ? preferredTags[seed % preferredTags.length] : undefined; const selectedKeywordTag = keywordTags.length > 0 ? keywordTags[(seed >>> 3) % keywordTags.length] : undefined; if (category === '消耗品' && preferredTags.some(tag => /mana|法力|灵气|内息/u.test(tag))) { baseTags.push('mana'); } if (category === '消耗品' && preferredTags.some(tag => /heal|疗|血|恢复/u.test(tag))) { baseTags.push('healing'); } return compactStrings([...baseTags, selectedPreferredTag, selectedKeywordTag]).slice(0, 5); } function inferRuntimeItemRarity(seed: number, rarityFloorValue: number): CustomWorldItem['rarity'] { const rolledRarity = [0, 1, 1, 2, 2, 2, 3, 3, 4][seed % 9] ?? 0; return RARITY_ORDER[Math.max(rarityFloorValue, rolledRarity)] ?? 'common'; } function inferRuntimeItemMechanics( category: string, rarity: CustomWorldItem['rarity'], tags: string[], seed: number, ) { const rarityTier = Math.max(1, RARITY_ORDER.indexOf(rarity) + 1); if (category === '武器') { return { equipmentSlotId: 'weapon' as const, statProfile: { outgoingDamageBonus: 2 * rarityTier + (seed % 3), }, useProfile: null, value: 28 * rarityTier, }; } if (category === '护甲') { return { equipmentSlotId: 'armor' as const, statProfile: { maxHpBonus: 10 * rarityTier + (seed % 8), incomingDamageMultiplier: Math.max(0.72, Number((1 - rarityTier * 0.04).toFixed(2))), }, useProfile: null, value: 26 * rarityTier, }; } if (category === '饰品' || category === '稀有品' || category === '专属物') { return { equipmentSlotId: 'relic' as const, statProfile: { maxManaBonus: 8 * rarityTier + (seed % 7), outgoingDamageBonus: rarityTier >= 3 ? rarityTier : undefined, }, useProfile: null, value: 32 * rarityTier, }; } if (category === '消耗品') { return { equipmentSlotId: null, statProfile: null, useProfile: tags.includes('mana') ? { manaRestore: 12 * rarityTier, cooldownReduction: rarityTier >= 3 ? 1 : 0 } : { hpRestore: 16 * rarityTier }, value: 18 * rarityTier, }; } return { equipmentSlotId: null, statProfile: null, useProfile: null, value: 10 * rarityTier, }; } function buildProceduralRuntimeItem( profile: CustomWorldProfile, seedKey: string, options: RuntimeCustomWorldItemQueryOptions, index: number, ) { const themeMode = detectCustomWorldThemeMode(profile); const seed = hashText(`${profile.id}:${seedKey}:${index}`); const defaultCategory = DEFAULT_RUNTIME_CATEGORIES[0] ?? 'world-item'; const categories = compactStrings(options.categories?.length ? options.categories : [...DEFAULT_RUNTIME_CATEGORIES]); const category = pickCyclic(categories, seed, defaultCategory); const rarityFloorValue = getRarityFloorValue(options.rarityFloor); const rarity = inferRuntimeItemRarity(seed, rarityFloorValue); const tags = buildRuntimeItemTags(category, options, seed); const prefixPool = WORLD_ITEM_PREFIXES[themeMode]; const nounPool = WORLD_ITEM_NOUNS[category] ?? WORLD_ITEM_NOUNS.稀有品; const fallbackNounPool = ['sigil', 'relic', 'token', 'seal', 'core', 'mark']; const resolvedNounPool = nounPool ?? fallbackNounPool; const worldSeed = getWorldSeedLabel(profile); const optionSeed = sanitizeNameFragment((options.preferredTags ?? [])[0] ?? '') || sanitizeNameFragment((options.keywords ?? [])[0] ?? ''); const prefix = pickCyclic(prefixPool, seed >>> 2, prefixPool[0] ?? 'world'); const noun = pickCyclic(resolvedNounPool, seed >>> 5, fallbackNounPool[0]); const name = `${prefix}${optionSeed || worldSeed}${noun}${index + 1}`; const mechanics = inferRuntimeItemMechanics(category, rarity, tags, seed); return { id: `runtime-item:${hashText(`${seedKey}:${index}`).toString(36)}`, name, category, rarity, description: `围绕“${profile.playerGoal}”即时生成的${category},适合在 ${profile.name} 中作为掉落、交易或补给资源。`, tags, origin: 'generated' as const, equipmentSlotId: mechanics.equipmentSlotId, statProfile: mechanics.statProfile, useProfile: mechanics.useProfile, value: mechanics.value, } satisfies CustomWorldItem; } function matchesRuntimeQuery( item: CustomWorldItem, options: RuntimeCustomWorldItemQueryOptions, rarityFloorValue: number, ) { if (options.categories?.length && !options.categories.includes(item.category)) { return false; } if (options.tags?.length && !options.tags.some(tag => item.tags.includes(tag))) { return false; } if (rarityFloorValue >= 0) { const itemRarityValue = RARITY_ORDER.indexOf(item.rarity); if (itemRarityValue < rarityFloorValue) { return false; } } return true; } function scoreItemRelevance(item: CustomWorldItem, options: RuntimeCustomWorldItemQueryOptions) { const haystack = normalizeLookupText([ item.name, item.category, item.description, ...(item.tags ?? []), ].join(' ')); const itemTags = new Set((item.tags ?? []).map(tag => normalizeLookupText(tag))); let score = 0; const preferredTags = [...new Set((options.preferredTags ?? []).map(normalizeLookupText).filter(Boolean))]; preferredTags.forEach(tag => { if (itemTags.has(tag)) { score += 10; return; } if (haystack.includes(tag)) { score += 4; } }); const keywords = [...new Set((options.keywords ?? []).map(normalizeLookupText).filter(keyword => keyword.length >= 2))]; keywords.forEach(keyword => { if (!haystack.includes(keyword)) { return; } score += keyword.length >= 4 ? 7 : keyword.length === 3 ? 5 : 3; }); if (options.categories?.includes(item.category)) { score += 2; } if (item.origin === 'generated') { score += 1; } return score; } function rankItems(items: CustomWorldItem[], seedKey: string, options: RuntimeCustomWorldItemQueryOptions = {}) { const seed = hashText(seedKey); return [...items].sort((left, right) => { const relevanceDelta = scoreItemRelevance(right, options) - scoreItemRelevance(left, options); if (relevanceDelta !== 0) { return relevanceDelta; } const leftScore = hashText(`${left.id}:${seed}`) % 997; const rightScore = hashText(`${right.id}:${seed}`) % 997; return leftScore - rightScore; }); } export function pickRuntimeCustomWorldItems( seedKey: string, options: RuntimeCustomWorldItemQueryOptions = {}, ) { const profile = runtimeCustomWorldProfile; if (!profile) return [] as CustomWorldItem[]; const rarityFloorValue = getRarityFloorValue(options.rarityFloor); const sourceItems = Array.from({ length: Math.max(16, (options.count ?? 1) * 10) }, (_, index) => buildProceduralRuntimeItem(profile, seedKey, options, index), ); const filtered = sourceItems.filter(item => matchesRuntimeQuery(item, options, rarityFloorValue)); return rankItems(filtered.length > 0 ? filtered : sourceItems, seedKey, options).slice(0, options.count ?? 1); } export function buildRuntimeCustomWorldInventoryItems( seedKey: string, options: RuntimeCustomWorldItemQueryOptions = {}, ) { const count = options.count ?? 1; return pickRuntimeCustomWorldItems(seedKey, options) .slice(0, count) .map((item, index) => toInventoryItem(item, options.quantity ?? 1, `${seedKey}:${index}`)); }