初始仓库迁移
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-04 23:57:06 +08:00
parent 80986b790d
commit c49c64896a
18446 changed files with 532435 additions and 2 deletions

View 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('') : '暂无额外加成';
}