feat: show final character stats
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { calculateFinalStats, formatFinalStat } from "./final-stats";
|
||||
import type { Build, CatalogBaseStat } from "./types";
|
||||
|
||||
const build: Build = {
|
||||
id: "test", characterId: "10000032", characterKey: "bennett", characterName: "Bennett", characterImageUrl: "", level: 90, maxLevel: 90, constellation: 0, talents: [9, 9, 9], weaponId: "11401", weaponKey: "favoniussword", weaponName: "Favonius Sword", weaponLevel: 90, weaponMaxLevel: 90, refinement: 1, artifactMode: "4pc", artifactOneId: "15007", artifactOneKey: "noblesseoblige", artifactOneName: "Noblesse Oblige", artifactTwoId: "", artifactTwoKey: "", artifactTwoName: "", sands: "er", goblet: "pyro%", circlet: "cr", rolls: { hp: 2, atk: 1, def: 0, em: 2, er: 3, "hp%": 0, "atk%": 4, "def%": 1, cr: 5, cd: 6 },
|
||||
};
|
||||
const character: CatalogBaseStat[] = [{ key: "hp", value: 12397 }, { key: "atk", value: 191 }, { key: "def", value: 771 }, { key: "ER", value: "26.7" }];
|
||||
const weapon: CatalogBaseStat[] = [{ key: "atk", value: 454 }, { key: "Energy Recharge%", value: 61.3 }];
|
||||
|
||||
describe("final character stats", () => {
|
||||
it("combines raw level 90 character, weapon, and artifact stats", () => {
|
||||
const stats = calculateFinalStats(build, character, weapon);
|
||||
expect(stats.find((stat) => stat.key === "HP")?.value).toBeCloseTo(17684.88);
|
||||
expect(stats.find((stat) => stat.key === "ATK")?.value).toBeCloseTo(1100.508);
|
||||
expect(stats.find((stat) => stat.key === "DEF")?.value).toBeCloseTo(818.802);
|
||||
expect(stats.find((stat) => stat.key === "ER")?.value).toBeCloseTo(2.5633);
|
||||
expect(stats.find((stat) => stat.key === "CR")?.value).toBeCloseTo(0.5265);
|
||||
expect(stats.find((stat) => stat.key === "CD")?.value).toBeCloseTo(0.8972);
|
||||
expect(stats.find((stat) => stat.key === "pyro")?.value).toBeCloseTo(0.466);
|
||||
});
|
||||
|
||||
it("formats final flat and percentage values", () => {
|
||||
expect(formatFinalStat({ key: "HP", value: 17684.88 })).toBe("HP 17,685");
|
||||
expect(formatFinalStat({ key: "ER", value: 2.6503, percent: true })).toBe("ER 265.03%");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { calculateSubstats, MAIN_STATS } from "./generate";
|
||||
import type { Build, CatalogBaseStat } from "./types";
|
||||
|
||||
export interface FinalStat {
|
||||
key: string;
|
||||
value: number;
|
||||
percent?: boolean;
|
||||
}
|
||||
|
||||
type Bonuses = Record<string, number>;
|
||||
|
||||
function addCatalogStats(stats: CatalogBaseStat[], bonuses: Bonuses) {
|
||||
for (const stat of stats) {
|
||||
const value = Number(stat.value);
|
||||
if (!Number.isFinite(value)) continue;
|
||||
const key = stat.key.toLowerCase().replaceAll(" ", "");
|
||||
if (key === "em") bonuses.em += value;
|
||||
else if (key === "er" || key === "energyrecharge%") bonuses.er += value / 100;
|
||||
else if (key === "cr" || key === "critrate%") bonuses.cr += value / 100;
|
||||
else if (key === "cd" || key === "critdmg%") bonuses.cd += value / 100;
|
||||
else if (key === "hp%") bonuses["hp%"] += value / 100;
|
||||
else if (key === "atk%") bonuses["atk%"] += value / 100;
|
||||
else if (key === "def%") bonuses["def%"] += value / 100;
|
||||
else if (key.endsWith("%")) {
|
||||
const bonusKey = key.slice(0, -1);
|
||||
bonuses[bonusKey] = (bonuses[bonusKey] ?? 0) + value / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function baseValue(stats: CatalogBaseStat[], key: string): number {
|
||||
return Number(stats.find((stat) => stat.key.toLowerCase() === key)?.value ?? 0);
|
||||
}
|
||||
|
||||
export function calculateFinalStats(build: Build, characterStats: CatalogBaseStat[], weaponStats: CatalogBaseStat[]): FinalStat[] {
|
||||
const bonuses: Bonuses = { em: 0, er: 1, cr: 0.05, cd: 0.5, "hp%": 0, "atk%": 0, "def%": 0 };
|
||||
addCatalogStats(characterStats.filter((stat) => !["hp", "atk", "def"].includes(stat.key.toLowerCase())), bonuses);
|
||||
addCatalogStats(weaponStats.filter((stat) => stat.key.toLowerCase() !== "atk"), bonuses);
|
||||
|
||||
const artifactStats: Bonuses = { hp: 4780, atk: 311, def: 0, em: 0, er: 0, cr: 0, cd: 0, "hp%": 0, "atk%": 0, "def%": 0 };
|
||||
for (const key of [build.sands, build.goblet, build.circlet]) {
|
||||
const bonusKey = key.endsWith("%") && !["hp%", "atk%", "def%"].includes(key) ? key.slice(0, -1) : key;
|
||||
artifactStats[bonusKey] = (artifactStats[bonusKey] ?? 0) + MAIN_STATS[key];
|
||||
}
|
||||
const substats = calculateSubstats(build);
|
||||
for (const [key, value] of Object.entries(substats)) artifactStats[key] = (artifactStats[key] ?? 0) + value;
|
||||
for (const [key, value] of Object.entries(artifactStats)) bonuses[key] = (bonuses[key] ?? 0) + value;
|
||||
|
||||
const characterAtk = baseValue(characterStats, "atk");
|
||||
const weaponAtk = baseValue(weaponStats, "atk");
|
||||
const core: FinalStat[] = [
|
||||
{ key: "HP", value: baseValue(characterStats, "hp") * (1 + bonuses["hp%"]) + bonuses.hp },
|
||||
{ key: "ATK", value: (characterAtk + weaponAtk) * (1 + bonuses["atk%"]) + bonuses.atk },
|
||||
{ key: "DEF", value: baseValue(characterStats, "def") * (1 + bonuses["def%"]) + bonuses.def },
|
||||
{ key: "EM", value: bonuses.em },
|
||||
{ key: "ER", value: bonuses.er, percent: true },
|
||||
{ key: "CR", value: bonuses.cr, percent: true },
|
||||
{ key: "CD", value: bonuses.cd, percent: true },
|
||||
];
|
||||
const known = new Set(["hp", "atk", "def", "em", "er", "cr", "cd", "hp%", "atk%", "def%"]);
|
||||
const extras = Object.entries(bonuses)
|
||||
.filter(([key, value]) => !known.has(key) && value !== 0)
|
||||
.map(([key, value]) => ({ key, value, percent: true }));
|
||||
return [...core, ...extras];
|
||||
}
|
||||
|
||||
export function formatFinalStat(stat: FinalStat): string {
|
||||
const value = stat.percent
|
||||
? `${(stat.value * 100).toLocaleString("en-US", { maximumFractionDigits: 2 })}%`
|
||||
: Math.round(stat.value).toLocaleString("en-US");
|
||||
return `${stat.key.toUpperCase()} ${value}`;
|
||||
}
|
||||
Reference in New Issue
Block a user