322 lines
9.4 KiB
TypeScript
322 lines
9.4 KiB
TypeScript
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(',') : '暂无额外加成';
|
||
}
|