109 lines
3.0 KiB
TypeScript
109 lines
3.0 KiB
TypeScript
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,
|
|
};
|
|
}
|