Files
Genarrative/src/data/equipmentEffects.ts
2026-04-16 15:45:00 +08:00

322 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<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,
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('') : '暂无额外加成';
}