mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-11-17 22:29:52 +08:00
fix(xp): displayed level-xp not calculated correctly, animations sometimes not playing correctly (NadAlaba) (#5379)
* new xp details calculation based on same level-xp-formula * fix displayed xp on profile page * fix xp-bar width on account-button * change ordering to minimize code duplication * animate xp bar between different levels when (endingLevel-startingLevel<1) * refactoring and commenting on levels functions * fix flash level animation the animation of flashLevel() was not visible because the css property 'transition', which was set to 0.125s in order to highlight the level on hover, was conflicting with the parameters of jquery animate() * annotate a clever trick and give more suitable variable names * update function and variable names * rename function * naming * reeename * reeeeEeeeEeeeename * rererereeeeeeeeeeeeee name * fix function names in comments --------- Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
parent
10d491c2c1
commit
5cd8b0e6e4
3 changed files with 99 additions and 64 deletions
|
|
@ -103,9 +103,12 @@ export async function update(
|
|||
): Promise<void> {
|
||||
if (isAuthenticated()) {
|
||||
if (xp !== undefined) {
|
||||
$("header nav .level").text(Math.floor(Levels.getLevel(xp)));
|
||||
const xpDetails = Levels.getXpDetails(xp);
|
||||
const levelCompletionRatio =
|
||||
xpDetails.levelCurrentXp / xpDetails.levelMaxXp;
|
||||
$("header nav .level").text(xpDetails.level);
|
||||
$("header nav .bar").css({
|
||||
width: (Levels.getLevel(xp) % 1) * 100 + "%",
|
||||
width: levelCompletionRatio * 100 + "%",
|
||||
});
|
||||
}
|
||||
if ((discordAvatar ?? "") && (discordId ?? "")) {
|
||||
|
|
@ -157,29 +160,25 @@ export async function updateXpBar(
|
|||
breakdown?: Record<string, number>
|
||||
): Promise<void> {
|
||||
skipBreakdown = false;
|
||||
const startingLevel = Levels.getLevel(currentXp);
|
||||
const endingLevel = Levels.getLevel(currentXp + addedXp);
|
||||
const startingXp = Levels.getXpDetails(currentXp);
|
||||
const endingXp = Levels.getXpDetails(currentXp + addedXp);
|
||||
const startingLevel =
|
||||
startingXp.level + startingXp.levelCurrentXp / startingXp.levelMaxXp;
|
||||
const endingLevel =
|
||||
endingXp.level + endingXp.levelCurrentXp / endingXp.levelMaxXp;
|
||||
|
||||
const snapshot = getSnapshot();
|
||||
if (!snapshot) return;
|
||||
|
||||
if (skipBreakdown) {
|
||||
$("nav .level").text(Math.floor(Levels.getLevel(snapshot.xp)));
|
||||
$("nav .xpBar")
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
.animate({ opacity: 0 }, SlowTimer.get() ? 0 : 250, () => {
|
||||
$("nav .xpBar .xpGain").text(``);
|
||||
});
|
||||
return;
|
||||
if (!skipBreakdown) {
|
||||
const xpBarPromise = animateXpBar(startingLevel, endingLevel);
|
||||
const xpBreakdownPromise = animateXpBreakdown(addedXp, breakdown);
|
||||
|
||||
await Promise.all([xpBarPromise, xpBreakdownPromise]);
|
||||
await Misc.sleep(2000);
|
||||
}
|
||||
|
||||
const xpBarPromise = animateXpBar(startingLevel, endingLevel);
|
||||
const xpBreakdownPromise = animateXpBreakdown(addedXp, breakdown);
|
||||
|
||||
await Promise.all([xpBarPromise, xpBreakdownPromise]);
|
||||
await Misc.sleep(2000);
|
||||
$("nav .level").text(Math.floor(Levels.getLevel(snapshot.xp)));
|
||||
$("nav .level").text(Levels.getLevelFromTotalXp(snapshot.xp));
|
||||
$("nav .xpBar")
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
|
|
@ -393,39 +392,31 @@ async function animateXpBar(
|
|||
let toAnimate = difference;
|
||||
|
||||
let firstOneDone = false;
|
||||
let animationDuration = quickSpeed;
|
||||
let animationEasing = "linear";
|
||||
let decrement = 1 - (startingLevel % 1);
|
||||
|
||||
while (toAnimate > 1) {
|
||||
do {
|
||||
if (toAnimate - 1 < 1) {
|
||||
if (firstOneDone) {
|
||||
void flashLevel();
|
||||
barEl.css("width", "0%");
|
||||
}
|
||||
await Misc.promiseAnimation(
|
||||
barEl,
|
||||
{
|
||||
width: "100%",
|
||||
},
|
||||
SlowTimer.get() ? 0 : Misc.mapRange(toAnimate - 1, 0, 0.5, 1000, 200),
|
||||
"easeOutQuad"
|
||||
);
|
||||
toAnimate--;
|
||||
} else {
|
||||
if (firstOneDone) {
|
||||
void flashLevel();
|
||||
barEl.css("width", "0%");
|
||||
}
|
||||
await Misc.promiseAnimation(
|
||||
barEl,
|
||||
{
|
||||
width: "100%",
|
||||
},
|
||||
SlowTimer.get() ? 0 : quickSpeed,
|
||||
"linear"
|
||||
);
|
||||
toAnimate--;
|
||||
animationDuration = Misc.mapRange(toAnimate - 1, 0, 0.5, 1000, 200);
|
||||
animationEasing = "easeOutQuad";
|
||||
}
|
||||
if (firstOneDone) {
|
||||
void flashLevel();
|
||||
barEl.css("width", "0%");
|
||||
decrement = 1;
|
||||
}
|
||||
await Misc.promiseAnimation(
|
||||
barEl,
|
||||
{
|
||||
width: "100%",
|
||||
},
|
||||
SlowTimer.get() ? 0 : animationDuration,
|
||||
animationEasing
|
||||
);
|
||||
toAnimate -= decrement;
|
||||
firstOneDone = true;
|
||||
}
|
||||
} while (toAnimate > 1);
|
||||
|
||||
void flashLevel();
|
||||
barEl.css("width", "0%");
|
||||
|
|
@ -443,19 +434,25 @@ async function animateXpBar(
|
|||
|
||||
async function flashLevel(): Promise<void> {
|
||||
const themecolors = await getAll();
|
||||
const barEl = $("nav .level");
|
||||
const levelEl = $("nav .level");
|
||||
|
||||
barEl.text(parseInt(barEl.text()) + 1);
|
||||
levelEl.text(parseInt(levelEl.text()) + 1);
|
||||
|
||||
const rand = Math.random() * 2 - 1;
|
||||
const rand2 = Math.random() + 1;
|
||||
|
||||
barEl
|
||||
/**
|
||||
* `borderSpacing` has no visible effect on this element,
|
||||
* and is used in the animation only to provide numerical
|
||||
* values for the `step(step)` function.
|
||||
*/
|
||||
levelEl
|
||||
.stop(true, true)
|
||||
.css({
|
||||
backgroundColor: themecolors.main,
|
||||
// transform: "scale(1.5) rotate(10deg)",
|
||||
borderSpacing: 100,
|
||||
transition: "initial",
|
||||
})
|
||||
.animate(
|
||||
{
|
||||
|
|
@ -464,7 +461,7 @@ async function flashLevel(): Promise<void> {
|
|||
},
|
||||
{
|
||||
step(step) {
|
||||
barEl.css(
|
||||
levelEl.css(
|
||||
"transform",
|
||||
`scale(${1 + (step / 200) * rand2}) rotate(${
|
||||
(step / 10) * rand
|
||||
|
|
@ -474,7 +471,10 @@ async function flashLevel(): Promise<void> {
|
|||
duration: 2000,
|
||||
easing: "easeOutCubic",
|
||||
complete: () => {
|
||||
barEl.css("background-color", "");
|
||||
levelEl.css({
|
||||
backgroundColor: "",
|
||||
transition: "",
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -294,13 +294,12 @@ export async function update(
|
|||
}
|
||||
|
||||
const xp = profile.xp ?? 0;
|
||||
const levelFraction = Levels.getLevel(xp);
|
||||
const level = Math.floor(levelFraction);
|
||||
const xpForLevel = Levels.getXpForLevel(level);
|
||||
const xpToDisplay = Math.round(xpForLevel * (levelFraction % 1));
|
||||
const xpDetails = Levels.getXpDetails(xp);
|
||||
const xpForLevel = xpDetails.levelMaxXp;
|
||||
const xpToDisplay = xpDetails.levelCurrentXp;
|
||||
details
|
||||
.find(".level")
|
||||
.text(level)
|
||||
.text(xpDetails.level)
|
||||
.attr("aria-label", `${Numbers.abbreviateNumber(xp)} total xp`);
|
||||
details
|
||||
.find(".xp")
|
||||
|
|
|
|||
|
|
@ -1,17 +1,53 @@
|
|||
/**
|
||||
* Calculates the level based on the XP.
|
||||
* @param xp The experience points.
|
||||
* Calculates the level based on the total XP.
|
||||
* This is the inverse of the function getTotalXpToReachLevel()
|
||||
* @param totalXp The total experience points.
|
||||
* @returns The calculated level.
|
||||
*/
|
||||
export function getLevel(xp: number): number {
|
||||
return (1 / 98) * (-151 + Math.sqrt(392 * xp + 22801)) + 1;
|
||||
export function getLevelFromTotalXp(totalXp: number): number {
|
||||
return Math.floor((Math.sqrt(392 * totalXp + 22801) - 53) / 98);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the experience points required for a given level.
|
||||
* Calculates the experience points required to complete a given level.
|
||||
* It does not take into consideration the experience points already gained in that level.
|
||||
* @param level The level.
|
||||
* @returns The experience points required for the level.
|
||||
*/
|
||||
export function getXpForLevel(level: number): number {
|
||||
function getLevelMaxXp(level: number): number {
|
||||
return 49 * (level - 1) + 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total experience points to reach the given level.
|
||||
* This is the inverse of the function getLevelFromTotalXp().
|
||||
* Calculated as the sum of xp required for all levels up to `level`,
|
||||
* where xp required for `level` is calculated using getLevelMaxXp(),
|
||||
* and the first level is 1 and it requires 100xp to reach level 2.
|
||||
* @param level The level.
|
||||
* @returns The total experience points required to reach the level.
|
||||
*/
|
||||
function getTotalXpToReachLevel(level: number): number {
|
||||
return (49 * Math.pow(level, 2) + 53 * level - 102) / 2;
|
||||
}
|
||||
|
||||
type XPDetails = {
|
||||
level: number;
|
||||
levelCurrentXp: number;
|
||||
levelMaxXp: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the details of experience points based on given total xp.
|
||||
* @param totalXp The total experience points.
|
||||
* @returns An object with the following properties: calculated level (floored),
|
||||
* partial xp of the level, and required xp for level completion.
|
||||
*/
|
||||
export function getXpDetails(totalXp: number): XPDetails {
|
||||
const level = getLevelFromTotalXp(totalXp);
|
||||
return {
|
||||
level,
|
||||
levelCurrentXp: totalXp - getTotalXpToReachLevel(level),
|
||||
levelMaxXp: getLevelMaxXp(level),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue