Files
Genarrative/src/data/attributeCombat.ts
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

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,
};
}