313
src/data/equipmentEffects.ts
Normal file
313
src/data/equipmentEffects.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { Character, EquipmentLoadout, EquipmentSlotId, GameState, InventoryItem, ItemRarity } from '../types';
|
||||
import { normalizeBuildRole, normalizeBuildTags } from './buildTags';
|
||||
import type { CharacterEquipmentItem } from './characterPresets';
|
||||
import { getCharacterEquipment, 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<ItemRarity, number> = {
|
||||
common: 0.06,
|
||||
uncommon: 0.1,
|
||||
rare: 0.14,
|
||||
epic: 0.2,
|
||||
legendary: 0.28,
|
||||
};
|
||||
|
||||
const ARMOR_HP_BONUS: Record<ItemRarity, number> = {
|
||||
common: 14,
|
||||
uncommon: 22,
|
||||
rare: 32,
|
||||
epic: 44,
|
||||
legendary: 58,
|
||||
};
|
||||
|
||||
const ARMOR_DAMAGE_MULTIPLIER: Record<ItemRarity, number> = {
|
||||
common: 0.97,
|
||||
uncommon: 0.94,
|
||||
rare: 0.9,
|
||||
epic: 0.86,
|
||||
legendary: 0.8,
|
||||
};
|
||||
|
||||
const RELIC_MANA_BONUS: Record<ItemRarity, number> = {
|
||||
common: 10,
|
||||
uncommon: 18,
|
||||
rare: 28,
|
||||
epic: 40,
|
||||
legendary: 54,
|
||||
};
|
||||
|
||||
const RELIC_DAMAGE_BONUS: Record<ItemRarity, number> = {
|
||||
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<string>([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) {
|
||||
const loadout = createEmptyEquipmentLoadout();
|
||||
const starterEquipment = getCharacterEquipment(character);
|
||||
|
||||
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 previousBonuses = getEquipmentBonuses(state.playerEquipment ?? createEmptyEquipmentLoadout());
|
||||
const nextBonuses = getEquipmentBonuses(nextEquipment);
|
||||
const baseMaxHp = Math.max(1, state.playerMaxHp - previousBonuses.maxHpBonus);
|
||||
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(',') : '暂无额外加成';
|
||||
}
|
||||
Reference in New Issue
Block a user