Files
healer-man/src/game/arenaRules.ts
T
2026-09-05 20:42:38 -04:00

81 lines
4.4 KiB
TypeScript

export type ArenaBracket = "2v2" | "3v3";
export type ArenaMode = "practice" | "rated-ai" | "ranked-pvp";
export type ArenaRuleset = "wow" | "coa" | "rom";
export type ArenaRole = "healer" | "damage";
export interface ArenaAiProfile {
readonly rating: number;
readonly title: string;
readonly reactionMinSeconds: number;
readonly reactionMaxSeconds: number;
readonly healerInterruptChance: number;
readonly coordinatedCcChance: number;
readonly correctDefensiveChance: number;
readonly minimumErrorChance: number;
}
export const ARENA_BRACKETS: Readonly<Record<ArenaBracket, { readonly size: 2 | 3; readonly healers: 1; readonly damage: 1 | 2 }>> = Object.freeze({
"2v2": Object.freeze({ size: 2, healers: 1, damage: 1 }),
"3v3": Object.freeze({ size: 3, healers: 1, damage: 2 }),
});
export const ARENA_RATING_BANDS = Object.freeze([
{ min: 0, max: 499, title: "Recruit", k: 64, reaction: [1.4, 2.4], healerInterrupt: 0, coordinatedCc: 0.05, defensive: 0.35, error: 0.35 },
{ min: 500, max: 999, title: "Combatant", k: 48, reaction: [1, 1.8], healerInterrupt: 0.2, coordinatedCc: 0.2, defensive: 0.5, error: 0.28 },
{ min: 1000, max: 1399, title: "Challenger", k: 40, reaction: [0.75, 1.3], healerInterrupt: 0.4, coordinatedCc: 0.4, defensive: 0.65, error: 0.2 },
{ min: 1400, max: 1799, title: "Rival", k: 32, reaction: [0.55, 1], healerInterrupt: 0.6, coordinatedCc: 0.6, defensive: 0.75, error: 0.15 },
{ min: 1800, max: 2099, title: "Duelist", k: 28, reaction: [0.4, 0.8], healerInterrupt: 0.72, coordinatedCc: 0.72, defensive: 0.82, error: 0.12 },
{ min: 2100, max: 2399, title: "Elite", k: 24, reaction: [0.3, 0.65], healerInterrupt: 0.8, coordinatedCc: 0.8, defensive: 0.88, error: 0.1 },
{ min: 2400, max: 2500, title: "Gladiator", k: 20, reaction: [0.25, 0.55], healerInterrupt: 0.85, coordinatedCc: 0.85, defensive: 0.9, error: 0.08 },
] as const);
export function clampArenaRating(value: number): number {
return Math.max(0, Math.min(2500, Math.round(Number(value) || 0)));
}
export function arenaRatingBand(rating: number): typeof ARENA_RATING_BANDS[number] {
const normalized = clampArenaRating(rating);
return ARENA_RATING_BANDS.find((band) => normalized >= band.min && normalized <= band.max) ?? ARENA_RATING_BANDS[0];
}
function lerp(left: number, right: number, amount: number): number {
return left + (right - left) * Math.max(0, Math.min(1, amount));
}
export function arenaAiProfile(rating: number): ArenaAiProfile {
const normalized = clampArenaRating(rating);
const index = ARENA_RATING_BANDS.findIndex((band) => normalized >= band.min && normalized <= band.max);
const current = ARENA_RATING_BANDS[Math.max(0, index)];
const next = ARENA_RATING_BANDS[Math.min(ARENA_RATING_BANDS.length - 1, Math.max(0, index) + 1)];
const progress = current === next ? 0 : (normalized - current.min) / Math.max(1, current.max - current.min + 1);
return {
rating: normalized,
title: current.title,
reactionMinSeconds: lerp(current.reaction[0], next.reaction[0], progress),
reactionMaxSeconds: lerp(current.reaction[1], next.reaction[1], progress),
healerInterruptChance: lerp(current.healerInterrupt, next.healerInterrupt, progress),
coordinatedCcChance: lerp(current.coordinatedCc, next.coordinatedCc, progress),
correctDefensiveChance: lerp(current.defensive, next.defensive, progress),
minimumErrorChance: lerp(current.error, next.error, progress),
};
}
export function calculateArenaRating(rating: number, opponentRating: number, score: 0 | 0.5 | 1) {
const before = clampArenaRating(rating);
if (score === 0.5) return { before, expected: 0.5, delta: 0, after: before };
const expected = 1 / (1 + 10 ** ((clampArenaRating(opponentRating) - before) / 400));
const rawDelta = Math.round(arenaRatingBand(before).k * (score - expected));
const after = clampArenaRating(before + rawDelta);
return { before, expected, delta: after - before, after };
}
export function arenaDampening(elapsedMs: number): number {
return Math.min(1, 0.1 + Math.floor(Math.max(0, elapsedMs) / 30_000) * 0.05);
}
export function arenaDrDuration(baseDurationMs: number, priorApplications: number, elapsedSinceEndMs = Infinity) {
const applications = elapsedSinceEndMs >= 15_000 ? 0 : Math.max(0, Math.trunc(priorApplications));
const multiplier = [1, 0.5, 0.25, 0][Math.min(3, applications)];
return { durationMs: Math.round(Math.max(0, baseDurationMs) * multiplier), applications: Math.min(3, applications + 1), multiplier };
}