import type { RoleAttributeProfile } from '../types'; const DEFAULT_ATTRIBUTE_SLOT_VALUE = 48; export const ATTRIBUTE_COMBAT_BONUS_LABELS = { axis_a: '攻击力', axis_b: '生命上限', axis_c: '生命恢复', axis_d: '攻击速度', axis_e: '暴击率', axis_f: '暴击伤害', } as const; export interface RoleCombatStats { attackPowerValue: number; maxHpValue: number; recoveryValue: number; attackSpeedValue: number; critChanceValue: number; critDamageValue: number; attackPowerMultiplier: number; maxHpBonus: number; storyRecovery: number; turnSpeed: number; critChance: number; critDamageMultiplier: number; } function roundNumber(value: number, digits = 4) { const factor = 10 ** digits; return Math.round(value * factor) / factor; } function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); } function getAttributeSlotValue( profile: RoleAttributeProfile | null | undefined, slotId: keyof typeof ATTRIBUTE_COMBAT_BONUS_LABELS, ) { const value = profile?.values?.[slotId]; if (typeof value === 'number' && Number.isFinite(value)) { return value; } return DEFAULT_ATTRIBUTE_SLOT_VALUE; } export function resolveRoleCombatStats( profile: RoleAttributeProfile | null | undefined, options: { baseSpeed?: number; } = {}, ): RoleCombatStats { const attackPowerValue = getAttributeSlotValue(profile, 'axis_a'); const maxHpValue = getAttributeSlotValue(profile, 'axis_b'); const recoveryValue = getAttributeSlotValue(profile, 'axis_c'); const attackSpeedValue = getAttributeSlotValue(profile, 'axis_d'); const critChanceValue = getAttributeSlotValue(profile, 'axis_e'); const critDamageValue = getAttributeSlotValue(profile, 'axis_f'); const baseSpeed = options.baseSpeed ?? 0; return { attackPowerValue, maxHpValue, recoveryValue, attackSpeedValue, critChanceValue, critDamageValue, attackPowerMultiplier: roundNumber(1 + attackPowerValue / 240), maxHpBonus: Math.max(1, Math.round(maxHpValue / 2)), storyRecovery: Math.max(3, Math.round(recoveryValue / 12)), turnSpeed: baseSpeed > 0 ? roundNumber(baseSpeed * (0.55 + attackSpeedValue / 100)) : roundNumber(Math.max(1, attackSpeedValue / 12)), critChance: roundNumber(clamp(critChanceValue / 500, 0.04, 0.24)), critDamageMultiplier: roundNumber( Math.max(1.45, 1.25 + critDamageValue / 120), ), }; } export function rollDeterministicCombatValue(seed: string) { let hash = 2166136261; for (let index = 0; index < seed.length; index += 1) { hash ^= seed.charCodeAt(index); hash = Math.imul(hash, 16777619); } return ((hash >>> 0) % 10000) / 10000; } export function resolveCriticalStrike( profile: RoleAttributeProfile | null | undefined, seed: string, ) { const stats = resolveRoleCombatStats(profile); const roll = rollDeterministicCombatValue(seed); return { isCritical: roll < stats.critChance, roll, critChance: stats.critChance, critDamageMultiplier: stats.critDamageMultiplier, }; }