import { getRuntimeCustomWorldProfile } from '../data/customWorldRuntime'; import { ITEM_CATEGORY_OPTIONS } from '../data/itemCatalog'; import { Character, CharacterSkillDefinition, CustomWorldPlayableNpc, CustomWorldProfile, EquipmentSlotId, ItemRarity, ItemStatProfile, ItemUseProfile, WorldType, } from '../types'; import { resolveCustomWorldCampScene } from './customWorldCamp'; import { resolveCustomWorldRuleProfile } from './customWorldOwnedSettingLayers'; import { type CustomWorldThemeMode, detectCustomWorldThemeMode, } from './customWorldTheme'; type ThemeMode = CustomWorldThemeMode; type AttributeLabelMap = Record; const [ CATEGORY_WEAPON, CATEGORY_ARMOR, CATEGORY_RELIC, CATEGORY_CONSUMABLE, CATEGORY_MATERIAL, CATEGORY_RARE, CATEGORY_EXCLUSIVE, ] = ITEM_CATEGORY_OPTIONS; type WorldPresentation = { mode: ThemeMode; attributeLabels: AttributeLabelMap; hpLabel: string; mpLabel: string; maxHpLabel: string; maxMpLabel: string; damageLabel: string; guardLabel: string; rangeLabel: string; cooldownLabel: string; manaCostLabel: string; campSuffix: string; itemPrefixes: string[]; itemInfixes: string[]; skillPrefixes: string[]; skillSuffixByStyle: Record; }; const WORLD_PRESENTATIONS: Record = { mythic: { mode: 'mythic', attributeLabels: { strength: '体魄', agility: '身法', intelligence: '识见', spirit: '心魂' }, hpLabel: '生命', mpLabel: '心流', maxHpLabel: '生命上限', maxMpLabel: '心流上限', damageLabel: '势能', guardLabel: '防护', rangeLabel: '距离', cooldownLabel: '回整', manaCostLabel: '心流消耗', campSuffix: '归舍', itemPrefixes: ['远岸', '回声', '曙迹', '长旅', '微光', '新铭'], itemInfixes: ['印', '纹', '辉', '迹', '息', '铭'], skillPrefixes: ['映', '折', '回', '逐', '临', '流'], skillSuffixByStyle: { burst: ['震', '断', '破', '坠'], steady: ['守', '定', '护', '镇'], mobility: ['跃', '移', '转', '行'], finisher: ['终', '决', '落', '尽'], projectile: ['矢', '刃', '波', '纹'], }, }, martial: { mode: 'martial', attributeLabels: { strength: '力量', agility: '敏捷', intelligence: '智力', spirit: '精神' }, hpLabel: '气血', mpLabel: '内力', maxHpLabel: '气血上限', maxMpLabel: '内力上限', damageLabel: '招式', guardLabel: '防御', rangeLabel: '招距', cooldownLabel: '调息', manaCostLabel: '内力消耗', campSuffix: '归舍', itemPrefixes: ['风雨', '青锋', '断桥', '冷铁', '旧案', '残影'], itemInfixes: ['刃','锋','魂','诀','式','影'], skillPrefixes: ['破','斩','击','御','飞','隐'], skillSuffixByStyle: { burst: ['杀','灭','破','击'], steady: ['守','御','护','镇'], mobility: ['闪','移','跃','遁'], finisher: ['决','断','灭','终'], projectile: ['飞','射','投','掷'], }, }, arcane: { mode: 'arcane', attributeLabels: { strength: '道骨', agility: '身法', intelligence: '神识', spirit: '灵韵' }, hpLabel: '元命', mpLabel: '灵韵', maxHpLabel: '元命上限', maxMpLabel: '灵韵上限', damageLabel: '术法', guardLabel: '护盾', rangeLabel: '术距', cooldownLabel: '回息', manaCostLabel: '灵韵消耗', campSuffix: '栖居', itemPrefixes: ['灵韵', '道纹', '云篆', '星芒', '界辉', '道痕'], itemInfixes: ['灵','道','法','术','诀','印'], skillPrefixes: ['灵','道','法','界','星','印'], skillSuffixByStyle: { burst: ['破','灭','毁','绝'], steady: ['守','御','护','镇'], mobility: ['闪','移','跃','遁'], finisher: ['决','断','灭','终'], projectile: ['飞','射','投','掷'], }, }, machina: { mode: 'machina', attributeLabels: { strength: '动力', agility: '精度', intelligence: '逻辑', spirit: '核心' }, hpLabel: '耐久', mpLabel: '能量', maxHpLabel: '耐久上限', maxMpLabel: '能量上限', damageLabel: '火力', guardLabel: '护盾', rangeLabel: '射程', cooldownLabel: '充能', manaCostLabel: '能量消耗', campSuffix: '整备居', itemPrefixes: ['铁脊', '钢律', '脉冲', '核列', '新星', '等离'], itemInfixes: ['芯', '驱', '链', '阵', '节', '机'], skillPrefixes: ['超载', '脉冲', '聚核', '磁轨', '新星', '裂火'], skillSuffixByStyle: { burst: ['爆裂', '齐射', '连发', '倾泻'], steady: ['稳压', '固守', '护持', '锚定'], mobility: ['疾冲', '推进', '跃迁', '闪移'], finisher: ['终断', '歼灭', '过载', '坠落'], projectile: ['弹', '束', '矢', '炮'], }, }, tide: { mode: 'tide', attributeLabels: { strength: '潮力', agility: '浪步', intelligence: '潮识', spirit: '潮魄' }, hpLabel: '潮命', mpLabel: '潮息', maxHpLabel: '潮命上限', maxMpLabel: '潮息上限', damageLabel: '潮势', guardLabel: '潮护', rangeLabel: '潮距', cooldownLabel: '回潮', manaCostLabel: '潮息消耗', campSuffix: '潮居', itemPrefixes: ['潮纹', '海晕', '霜浪', '天澜', '潮歌', '沧流'], itemInfixes: ['潮', '浪', '汐', '海', '涛', '澜'], skillPrefixes: ['潮', '浪', '汐', '海', '澜', '涌'], skillSuffixByStyle: { burst: ['裂潮', '怒涌', '连浪', '奔潮'], steady: ['守潮', '潮护', '定澜', '镇流'], mobility: ['踏浪', '游潮', '跃汐', '逐流'], finisher: ['断潮', '覆海', '终汐', '沉落'], projectile: ['潮矢', '水矛', '浪刃', '飞涌'], }, }, rift: { mode: 'rift', attributeLabels: { strength: '界劲', agility: '裂步', intelligence: '界识', spirit: '界压' }, hpLabel: '界命', mpLabel: '裂能', maxHpLabel: '界命上限', maxMpLabel: '裂能上限', damageLabel: '界势', guardLabel: '稳界', rangeLabel: '界距', cooldownLabel: '复界', manaCostLabel: '裂能消耗', campSuffix: '界隙居所', itemPrefixes: ['裂界', '断层', '边潮', '灰域', '界桥', '前哨'], itemInfixes: ['锋', '隙', '锚', '印', '界', '核'], skillPrefixes: ['裂', '断', '界', '相', '折', '迁'], skillSuffixByStyle: { burst: ['崩断', '碎坠', '裂爆', '界崩'], steady: ['守界', '固相', '帷障', '界卫'], mobility: ['裂步', '转相', '闪迁', '漂移'], finisher: ['终坠', '断灭', '裂终', '界燃'], projectile: ['界刺', '裂矢', '碎片', '裂波'], }, }, }; const CATEGORY_NOUNS: Record = Object.fromEntries([ [CATEGORY_WEAPON, ['剑', '刃', '弓', '杖', '枪', '盾']], [CATEGORY_ARMOR, ['甲', '袍', '披风', '护具', '肩甲', '护腕']], [CATEGORY_RELIC, ['戒', '印', '徽', '玉', '符', '珠']], [CATEGORY_CONSUMABLE, ['药', '散', '剂', '露', '油', '卷']], [CATEGORY_MATERIAL, ['矿', '晶', '骨', '草', '核', '丝']], [CATEGORY_RARE, ['符', '遗物', '残页', '图', '钥', '像']], [CATEGORY_EXCLUSIVE, ['核心', '封印', '主钥', '源匣', '真印', '界核']], ]); const DEFAULT_CATEGORY_NOUNS = ['符', '印', '信物', '匣', '核', '铭片']; const ROLE_SKILL_ROOTS: Record = { 'sword-princess': ['王剑', '锋式', '裁锋', '裂锋'], 'archer-hero': ['弦诀', '远袭', '追风', '贯矢'], 'girl-hero': ['双刃', '影袭', '疾斩', '掠影'], 'punch-hero': ['拳势', '震击', '裂拳', '崩步'], 'fighter-4': ['重锋', '盾阵', '镇线', '压城'], }; const SKILL_ROOT_STOP_WORDS = new Set([ '世界', '设定', '基调', '目标', '角色', '战斗', '风格', '背景', '性格', '故事', 'custom-world', 'playable-role', ]); 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 pickCyclic(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 getWorldPresentation(profile: CustomWorldProfile) { return WORLD_PRESENTATIONS[detectCustomWorldThemeMode(profile)]; } function dedupeStrings(values: string[], max = 12) { return [...new Set(values.map(value => value.trim()).filter(Boolean))].slice(0, max); } function collectSkillRootFragments(value: string, max = 8) { if (!value.trim()) return [] as string[]; const directSegments = value .split(/[ \t\r\n,!?:"()|/\\[\]-]+/u) .map(segment => segment.trim()) .filter(segment => segment.length >= 2 && segment.length <= 6) .filter(segment => !SKILL_ROOT_STOP_WORDS.has(segment)); const chineseSource = value.replace(/[^\u4e00-\u9fa5]/gu, ''); const ngrams: string[] = []; for (let size = 2; size <= 4; size += 1) { for (let index = 0; index <= chineseSource.length - size; index += 1) { const fragment = chineseSource.slice(index, index + size); if (SKILL_ROOT_STOP_WORDS.has(fragment)) { continue; } ngrams.push(fragment); if (ngrams.length >= max) { return dedupeStrings([...directSegments, ...ngrams], max); } } } return dedupeStrings([...directSegments, ...ngrams], max); } function buildSkillThemeSeedSource( profile: CustomWorldProfile, character: Character, skill: CharacterSkillDefinition, index: number, role?: Pick | null, ) { return [ profile.name, profile.settingText, profile.summary, profile.tone, profile.playerGoal, role?.title ?? '', role?.combatStyle ?? '', role?.tags.join('|') ?? '', character.id, skill.id, skill.style, skill.delivery ?? '', index, ].join('::'); } function buildSkillRootOptions( profile: CustomWorldProfile, character: Character, role?: Pick | null, ) { const fallbackRoots = ROLE_SKILL_ROOTS[character.id] ?? ['界式', '行诀', '裂锋', '潮印']; const derivedRoots = dedupeStrings([ ...collectSkillRootFragments(role?.title ?? '', 4), ...collectSkillRootFragments(role?.combatStyle ?? '', 6), ...(role?.tags ?? []).flatMap(tag => collectSkillRootFragments(tag, 2)), ...collectSkillRootFragments(profile.name, 4), ...collectSkillRootFragments(profile.playerGoal, 6), ], 8); return derivedRoots.length > 0 ? dedupeStrings([...derivedRoots, ...fallbackRoots], 8) : fallbackRoots; } export function getCustomWorldProfileForDisplay(worldType: WorldType | null | undefined, explicitProfile?: CustomWorldProfile | null) { if (explicitProfile) return explicitProfile; if (worldType === WorldType.CUSTOM) { return getRuntimeCustomWorldProfile(); } return null; } export function getAttributeLabelsForWorld(worldType: WorldType | null | undefined, explicitProfile?: CustomWorldProfile | null): AttributeLabelMap { const profile = getCustomWorldProfileForDisplay(worldType, explicitProfile); if (!profile) { if (worldType === WorldType.XIANXIA) { return { strength: '道骨', agility: '身法', intelligence: '神识', spirit: '灵韵' }; } return { strength: '力量', agility: '敏捷', intelligence: '智力', spirit: '精神' }; } return getWorldPresentation(profile).attributeLabels; } export function getResourceLabelsForWorld(worldType: WorldType | null | undefined, explicitProfile?: CustomWorldProfile | null) { const profile = getCustomWorldProfileForDisplay(worldType, explicitProfile); if (!profile) { if (worldType === WorldType.XIANXIA) { return { hp: '命元', mp: '灵韵', maxHp: '命元上限', maxMp: '灵韵上限', damage: '术势', guard: '护元', range: '术距', cooldown: '回息', manaCost: '灵韵消耗', }; } return { hp: '生命', mp: '灵力', maxHp: '生命上限', maxMp: '灵力上限', damage: '伤害', guard: '防护', range: '距离', cooldown: '冷却', manaCost: '消耗', }; } const ruleProfile = resolveCustomWorldRuleProfile(profile); if (ruleProfile) { return { hp: ruleProfile.resourceLabels.hp, mp: ruleProfile.resourceLabels.mp, maxHp: ruleProfile.resourceLabels.maxHp, maxMp: ruleProfile.resourceLabels.maxMp, damage: ruleProfile.resourceLabels.damage, guard: ruleProfile.resourceLabels.guard, range: ruleProfile.resourceLabels.range, cooldown: ruleProfile.resourceLabels.cooldown, manaCost: ruleProfile.resourceLabels.manaCost, }; } const presentation = getWorldPresentation(profile); return { hp: presentation.hpLabel, mp: presentation.mpLabel, maxHp: presentation.maxHpLabel, maxMp: presentation.maxMpLabel, damage: presentation.damageLabel, guard: presentation.guardLabel, range: presentation.rangeLabel, cooldown: presentation.cooldownLabel, manaCost: presentation.manaCostLabel, }; } export function buildCustomCampSceneName(profile: CustomWorldProfile) { return resolveCustomWorldCampScene(profile).name; } export function buildThemedSkillName( profile: CustomWorldProfile, character: Character, skill: CharacterSkillDefinition, index: number, role?: Pick | null, ) { const presentation = getWorldPresentation(profile); const seed = hashText(buildSkillThemeSeedSource(profile, character, skill, index, role)); const rootOptions = buildSkillRootOptions(profile, character, role); const prefix = presentation.skillPrefixes[seed % presentation.skillPrefixes.length]; const root = rootOptions[(seed >>> 3) % rootOptions.length]; const suffix = presentation.skillSuffixByStyle[skill.style][(seed >>> 5) % presentation.skillSuffixByStyle[skill.style].length]; return `${prefix}${root}${suffix}`; } function getCategoryNouns(category: string) { return CATEGORY_NOUNS[category] ?? CATEGORY_NOUNS[CATEGORY_RARE]; } function getResolvedCategoryNouns(category: string): string[] { return getCategoryNouns(category) ?? DEFAULT_CATEGORY_NOUNS; } export function buildThemedItemName( profile: CustomWorldProfile, category: string, sourceKey: string, index: number, ) { const presentation = getWorldPresentation(profile); const seed = hashText(`${profile.id}:${sourceKey}:${category}:${index}`); const prefix = presentation.itemPrefixes[seed % presentation.itemPrefixes.length]; const infix = presentation.itemInfixes[(seed >>> 3) % presentation.itemInfixes.length]; const nouns = getResolvedCategoryNouns(category); const noun = pickCyclic(nouns, seed >>> 5, `item noun for category "${category}"`); return `${prefix}${infix}${noun}${index + 1}`; } export function buildThemedItemDescription( profile: CustomWorldProfile, category: string, rarity: ItemRarity, seedKey: string, ) { const seed = hashText(`${profile.id}:${category}:${rarity}:${seedKey}`); const hooks = [ `适合围绕“${profile.playerGoal}”继续推进。`, `它的气质和“${profile.tone}”这条世界基调很贴近。`, '很可能会出现在这个世界的关键冲突里。', '能明显牵出这个世界正在扩大的主要矛盾。', ]; const rarityText = { common: '常见', uncommon: '进阶', rare: '稀有', epic: '核心', legendary: '关键', }[rarity]; return `${rarityText}${category}。${hooks[seed % hooks.length]}`; } export function inferCustomItemMechanics( category: string, rarity: ItemRarity, tags: string[], seedKey: string, ): { equipmentSlotId?: EquipmentSlotId | null; statProfile?: ItemStatProfile | null; useProfile?: ItemUseProfile | null; value: number; } { const seed = hashText(`${category}:${rarity}:${seedKey}:${tags.join('|')}`); const rarityTier = { common: 1, uncommon: 2, rare: 3, epic: 4, legendary: 5, }[rarity]; if (category === CATEGORY_WEAPON) { return { equipmentSlotId: 'weapon', statProfile: { outgoingDamageBonus: 2 * rarityTier + (seed % 3), }, value: 28 * rarityTier, }; } if (category === CATEGORY_ARMOR) { return { equipmentSlotId: 'armor', statProfile: { maxHpBonus: 10 * rarityTier + (seed % 8), incomingDamageMultiplier: Math.max(0.72, Number((1 - rarityTier * 0.04).toFixed(2))), }, value: 26 * rarityTier, }; } if (category === CATEGORY_RELIC || category === CATEGORY_RARE || category === CATEGORY_EXCLUSIVE) { return { equipmentSlotId: 'relic', statProfile: { maxManaBonus: 8 * rarityTier + (seed % 7), outgoingDamageBonus: rarityTier >= 3 ? rarityTier : undefined, }, value: 32 * rarityTier, }; } if (category === CATEGORY_CONSUMABLE) { const heals = tags.includes('healing') || seed % 2 === 0; return { useProfile: heals ? { hpRestore: 16 * rarityTier } : { manaRestore: 12 * rarityTier, cooldownReduction: rarityTier >= 3 ? 1 : 0 }, value: 18 * rarityTier, }; } return { value: 10 * rarityTier, }; }