import { Character, CustomWorldProfile, EquipmentLoadout, EquipmentSlotId, GameState, InventoryItem, ItemRarity } from '../types'; import { normalizeBuildRole, normalizeBuildTags } from './buildTags'; import type { CharacterEquipmentItem } from './characterPresets'; import { getCharacterEquipment, getCharacterMaxHp, getCharacterMaxMana } from './characterPresets'; export type EquipmentBonuses = { maxHpBonus: number; maxManaBonus: number; outgoingDamageMultiplier: number; incomingDamageMultiplier: number; }; export const EQUIPMENT_SLOTS: EquipmentSlotId[] = ['weapon', 'armor', 'relic']; const WEAPON_DAMAGE_BONUS: Record = { common: 0.06, uncommon: 0.1, rare: 0.14, epic: 0.2, legendary: 0.28, }; const ARMOR_HP_BONUS: Record = { common: 14, uncommon: 22, rare: 32, epic: 44, legendary: 58, }; const ARMOR_DAMAGE_MULTIPLIER: Record = { common: 0.97, uncommon: 0.94, rare: 0.9, epic: 0.86, legendary: 0.8, }; const RELIC_MANA_BONUS: Record = { common: 10, uncommon: 18, rare: 28, epic: 40, legendary: 54, }; const RELIC_DAMAGE_BONUS: Record = { common: 0.02, uncommon: 0.04, rare: 0.06, epic: 0.09, legendary: 0.12, }; export function createEmptyEquipmentLoadout(): EquipmentLoadout { return { weapon: null, armor: null, relic: null, }; } export function getEquipmentSlotLabel(slot: EquipmentSlotId) { return { weapon: '武器', armor: '护甲', relic: '饰品', }[slot]; } export function getEquipmentRarityLabel(rarity: ItemRarity) { return { common: '普通', uncommon: '优秀', rare: '稀有', epic: '史诗', legendary: '传说', }[rarity]; } function normalizePresetRarity(rarityText: string | undefined): ItemRarity { if (!rarityText) return 'common'; if (/传说|legendary/i.test(rarityText)) return 'legendary'; if (/史诗|epic/i.test(rarityText)) return 'epic'; if (/稀有|rare/i.test(rarityText)) return 'rare'; if (/优秀|uncommon/i.test(rarityText)) return 'uncommon'; return 'common'; } function inferSlotFromText(value: string) { if (/武器|剑|弓|刀|拳套|战刃|佩刀|枪|刃/u.test(value)) return 'weapon' as const; if (/护甲|甲|护臂|衣|袍|铠/u.test(value)) return 'armor' as const; if (/饰品|护符|徽章|玉|珠|坠|铃|盘|令|匣/u.test(value)) return 'relic' as const; return null; } function inferEquipmentTags(slot: EquipmentSlotId, name: string) { const tags = new Set([slot]); if (/灵|气|符|珠|盘|玉/u.test(name)) tags.add('mana'); if (/护|守|甲|铠/u.test(name)) tags.add('armor'); if (/刃|剑|弓|刀|拳/u.test(name)) tags.add('weapon'); if (/徽章|护符|坠|铃|盘|令/u.test(name)) tags.add('relic'); if (/疗|愈|血/u.test(name)) tags.add('healing'); return [...tags]; } function buildStarterEquipmentItem( characterId: string, equipmentItem: CharacterEquipmentItem, slot: EquipmentSlotId, ): InventoryItem { return { id: `starter:${characterId}:${slot}`, category: getEquipmentSlotLabel(slot), name: equipmentItem.item, quantity: 1, rarity: normalizePresetRarity(equipmentItem.rarity), tags: inferEquipmentTags(slot, equipmentItem.item), equipmentSlotId: slot, buildProfile: inferStarterBuildProfile(slot, equipmentItem.item), }; } function inferStarterBuildProfile(slot: EquipmentSlotId, name: string): InventoryItem['buildProfile'] { const source = `${slot} ${name}`; if (/弓|箭|矢/u.test(source)) { return { role: normalizeBuildRole('游击'), tags: normalizeBuildTags(['远射', '游击', '风行']), synergy: ['拉扯', '先手试探', '远程压制'], forgeRank: 0, }; } if (/盾|甲|铠|护/u.test(source)) { return { role: normalizeBuildRole('先锋'), tags: normalizeBuildTags(['守御', '护体', '先锋']), synergy: ['正面承压', '稳定推进', '反手压场'], forgeRank: 0, }; } if (/拳|锤|斧/u.test(source)) { return { role: normalizeBuildRole('狂战'), tags: normalizeBuildTags(['重击', '爆发', '压血']), synergy: ['近身爆发', '压低血线', '强攻破面'], forgeRank: 0, }; } if (/符|印|珠|戒|坠/u.test(source)) { return { role: normalizeBuildRole('法修'), tags: normalizeBuildTags(['法力', '护体', '镇邪']), synergy: ['法力支撑', '续战调息', '偏功能补位'], forgeRank: 0, }; } if (slot === 'weapon') { return { role: normalizeBuildRole('快剑'), tags: normalizeBuildTags(['快剑', '突进', '压制']), synergy: ['贴身连击', '起手压制', '追身进攻'], forgeRank: 0, }; } if (slot === 'armor') { return { role: normalizeBuildRole('守御'), tags: normalizeBuildTags(['守御', '护体']), synergy: ['过渡承伤', '基础防护'], forgeRank: 0, }; } return { role: normalizeBuildRole('均衡'), tags: normalizeBuildTags(['均衡', '续战']), synergy: ['过渡补强', '基础续航'], forgeRank: 0, }; } export function getEquipmentSlotFromItem(item: InventoryItem): EquipmentSlotId | null { if (item.equipmentSlotId) return item.equipmentSlotId; if (item.tags.includes('weapon')) return 'weapon'; if (item.tags.includes('armor')) return 'armor'; if (item.tags.includes('relic')) return 'relic'; return inferSlotFromText(`${item.category} ${item.name}`); } export function isInventoryItemEquippable(item: InventoryItem) { return getEquipmentSlotFromItem(item) !== null; } export function buildInitialEquipmentLoadout( character: Character, customWorldProfile: CustomWorldProfile | null = null, ) { const loadout = createEmptyEquipmentLoadout(); const starterEquipment = getCharacterEquipment(character, customWorldProfile); starterEquipment.forEach((equipmentItem, index) => { const inferredSlot = inferSlotFromText(`${equipmentItem.slot} ${equipmentItem.item}`) ?? EQUIPMENT_SLOTS[index] ?? null; if (!inferredSlot || loadout[inferredSlot]) return; loadout[inferredSlot] = buildStarterEquipmentItem(character.id, equipmentItem, inferredSlot); }); return loadout; } function getFallbackBonusesForItem(slot: EquipmentSlotId, rarity: ItemRarity) { if (slot === 'weapon') { return { maxHpBonus: 0, maxManaBonus: 0, outgoingDamageBonus: WEAPON_DAMAGE_BONUS[rarity], incomingDamageMultiplier: 1, }; } if (slot === 'armor') { return { maxHpBonus: ARMOR_HP_BONUS[rarity], maxManaBonus: 0, outgoingDamageBonus: 0, incomingDamageMultiplier: ARMOR_DAMAGE_MULTIPLIER[rarity], }; } return { maxHpBonus: 0, maxManaBonus: RELIC_MANA_BONUS[rarity], outgoingDamageBonus: RELIC_DAMAGE_BONUS[rarity], incomingDamageMultiplier: 1, }; } function getItemEquipmentBonuses(item: InventoryItem, slot: EquipmentSlotId) { const fallback = getFallbackBonusesForItem(slot, item.rarity); const statProfile = item.statProfile; return { maxHpBonus: statProfile?.maxHpBonus ?? fallback.maxHpBonus, maxManaBonus: statProfile?.maxManaBonus ?? fallback.maxManaBonus, outgoingDamageBonus: statProfile?.outgoingDamageBonus ?? fallback.outgoingDamageBonus, incomingDamageMultiplier: statProfile?.incomingDamageMultiplier ?? fallback.incomingDamageMultiplier, }; } export function getEquipmentBonuses(loadout: EquipmentLoadout): EquipmentBonuses { let maxHpBonus = 0; let maxManaBonus = 0; let outgoingDamageBonus = 0; let incomingDamageMultiplier = 1; EQUIPMENT_SLOTS.forEach(slot => { const item = loadout[slot]; if (!item) return; const itemBonuses = getItemEquipmentBonuses(item, slot); maxHpBonus += itemBonuses.maxHpBonus; maxManaBonus += itemBonuses.maxManaBonus; outgoingDamageBonus += itemBonuses.outgoingDamageBonus; incomingDamageMultiplier *= itemBonuses.incomingDamageMultiplier; }); return { maxHpBonus, maxManaBonus, outgoingDamageMultiplier: Number((1 + outgoingDamageBonus).toFixed(4)), incomingDamageMultiplier: Number(incomingDamageMultiplier.toFixed(4)), }; } export function applyEquipmentLoadoutToState( state: GameState, nextEquipment: EquipmentLoadout, ): GameState { const nextBonuses = getEquipmentBonuses(nextEquipment); const baseMaxHp = state.playerCharacter ? getCharacterMaxHp( state.playerCharacter, state.worldType, state.customWorldProfile, ) : Math.max(1, state.playerMaxHp); const nextMaxHp = baseMaxHp + nextBonuses.maxHpBonus; const nextMaxMana = state.playerCharacter ? getCharacterMaxMana(state.playerCharacter) : state.playerMaxMana; return { ...state, playerMaxHp: nextMaxHp, playerHp: Math.min(nextMaxHp, state.playerHp), playerMaxMana: nextMaxMana, playerMana: nextMaxMana, playerEquipment: nextEquipment, }; } export function describeEquipmentBonuses(bonuses: EquipmentBonuses) { const parts = [ bonuses.maxHpBonus > 0 ? `气血上限 +${bonuses.maxHpBonus}` : null, bonuses.maxManaBonus > 0 ? `灵力上限 +${bonuses.maxManaBonus}` : null, bonuses.outgoingDamageMultiplier > 1 ? `伤害 x${bonuses.outgoingDamageMultiplier}` : null, bonuses.incomingDamageMultiplier < 1 ? `承伤 x${bonuses.incomingDamageMultiplier}` : null, ].filter(Boolean); return parts.length > 0 ? parts.join(',') : '暂无额外加成'; }