This commit is contained in:
108
src/data/attributeCombat.ts
Normal file
108
src/data/attributeCombat.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user