impr: show xp gain details as list (@fehmer, @miodec) (#5895)

![image](https://github.com/user-attachments/assets/b75f405e-5ce5-4f54-9cc8-f2153964008d)

![image](https://github.com/user-attachments/assets/bfa8f7ca-1335-489f-bc48-eebe9abc29d3)

---------

Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
Christian Fehmer 2024-09-23 14:56:58 +02:00 committed by GitHub
parent 8d6f2b4edc
commit d9788a15e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 201 additions and 100 deletions

View file

@ -132,8 +132,11 @@
<div class="level" data-balloon-pos="up">1</div>
<div class="xpBar">
<div class="bar"></div>
<div class="xpGain"></div>
<div class="xpBreakdown"></div>
<div class="xpBreakdown">
<div class="total"></div>
<!-- <div class="divider"></div> -->
<div class="list"></div>
</div>
</div>
</div>
</a>

View file

@ -74,15 +74,19 @@ nav {
}
.xpBar {
z-index: 5;
opacity: 0;
pointer-events: none;
position: absolute;
height: 0.25em;
bottom: -0.5em;
width: 100%;
left: 0;
min-width: 16ch;
right: 0;
background: var(--sub-alt-color);
border-radius: var(--roundness);
display: grid;
grid-template-columns: auto 2.5em;
.bar {
left: 0;
width: 0%;
@ -90,26 +94,61 @@ nav {
background: var(--main-color);
border-radius: var(--roundness);
}
.xpGain {
position: absolute;
left: 100%;
margin-left: 0.5em;
top: 0.2em;
transform: translateY(-50%);
font-size: 0.75em;
color: var(--main-color);
}
.xpBreakdown {
backdrop-filter: blur(1em);
/*background: linear-gradient(
90deg,
transparent 0%,
var(--bg-color) 10%
);*/
width: 100%;
position: absolute;
color: var(--text-color);
display: grid;
justify-items: center;
width: 100%;
margin-top: 0.75em;
.text {
gap: 0.5em;
//justify-items: center;
width: 32ch;
justify-self: end;
margin-top: 0.3em;
padding: 0.5em 1em;
margin-right: -0.5em;
border-radius: var(--roundness);
.total {
text-align: right;
font-size: 1em;
color: var(--main-color);
width: max-content;
font-size: 0.75em;
position: absolute;
justify-self: end;
}
.divider {
width: 100%;
height: 0.1rem;
background: var(--sub-alt-color);
border-radius: var(--roundness);
opacity: 0.5;
}
.list {
.line {
font-size: 0.8em;
display: grid;
grid-template-columns: auto 10ch;
gap: 0.5em;
& div {
text-align: right;
}
.positive {
color: var(--main-color);
}
.negative {
color: var(--error-color);
}
.total {
font-weight: bold;
}
}
}
}
}

View file

@ -192,7 +192,7 @@ export function update(snapshot: MonkeyTypes.Snapshot | undefined): void {
export async function updateXpBar(
currentXp: number,
addedXp: number,
breakdown?: Record<string, number>
breakdown?: XpBreakdown
): Promise<void> {
skipBreakdown = false;
const startingXp = Levels.getXpDetails(currentXp);
@ -207,119 +207,182 @@ export async function updateXpBar(
const xpBreakdownPromise = animateXpBreakdown(addedXp, breakdown);
await Promise.all([xpBarPromise, xpBreakdownPromise]);
await Misc.sleep(2000);
if (skipBreakdown) {
void flashTotalXp(addedXp);
$("nav .xpBar .xpBreakdown .list")
.stop(true, true)
.animate(
{
opacity: 0,
},
250,
() => {
$("nav .xpBar .xpBreakdown .list").empty();
}
);
await Misc.sleep(2000);
} else {
await Misc.sleep(5000);
}
}
$("nav .level").text(Levels.getLevelFromTotalXp(currentXp + addedXp));
$("nav .xpBar")
.stop(true, true)
.css("opacity", 1)
.animate({ opacity: 0 }, SlowTimer.get() ? 0 : 250, () => {
$("nav .xpBar .xpGain").text(``);
});
.animate(
{
opacity: 0,
},
SlowTimer.get() ? 0 : 250
);
}
async function flashTotalXp(totalXp: number): Promise<void> {
const xpTotalEl = $("nav .xpBar .xpBreakdown .total");
xpTotalEl.text(`+${totalXp}`);
const rand = (Math.random() * 2 - 1) / 4;
const rand2 = (Math.random() + 1) / 2;
/**
* `borderSpacing` has no visible effect on this element,
* and is used in the animation only to provide numerical
* values for the `step(step)` function.
*/
xpTotalEl
.stop(true, true)
.css({
transition: "initial",
borderSpacing: 100,
})
.animate(
{
borderSpacing: 0,
},
{
step(step) {
xpTotalEl.css(
"transform",
`scale(${1 + (step / 200) * rand2}) rotate(${
(step / 10) * rand
}deg)`
);
},
duration: 2000,
easing: "easeOutCubic",
complete: () => {
xpTotalEl.css({
backgroundColor: "",
transition: "",
});
},
}
);
}
async function animateXpBreakdown(
addedXp: number,
breakdown?: XpBreakdown
): Promise<void> {
const xpBreakdownTotal = $("nav .xpBar .xpBreakdown .total");
const xpBreakdownList = $("nav .xpBar .xpBreakdown .list");
xpBreakdownList.css("opacity", 1);
if (!breakdown) {
$("nav .xpBar .xpGain").text(`+${addedXp}`);
xpBreakdownTotal.text(`+${addedXp}`);
return;
}
const delay = 1000;
const delay = 250;
let total = 0;
const xpGain = $("nav .xpBar .xpGain");
const xpBreakdown = $("nav .xpBar .xpBreakdown");
xpBreakdown.empty();
xpBreakdownList.empty();
async function append(string: string): Promise<void> {
if (skipBreakdown) {
total = addedXp;
string = "";
xpBreakdownTotal.text("+0");
async function append(
string: string,
amount: number | string | undefined,
options?: { extraClass?: string }
): Promise<void> {
if (skipBreakdown) return;
if (amount === undefined) {
xpBreakdownList.append(
`<div class="line" data-string='${string}'><div>${string}</div><div></div></div>`
);
} else if (typeof amount === "string") {
xpBreakdownList.append(
`
<div class="line" data-string='${string}'>
<div class="${options?.extraClass}">${string}</div>
<div class="${options?.extraClass}">${amount}</div>
</div>`
);
} else {
const positive = amount == undefined ? undefined : amount >= 0;
xpBreakdownList.append(`
<div class="line" data-string='${string}'>
<div class="${options?.extraClass}">${string}</div>
<div class="${positive ? "positive" : "negative"} ${
options?.extraClass
}">${positive ? "+" : "-"}${Math.abs(amount)}</div>
</div>`);
}
xpBreakdown.find(".next").removeClass("next").addClass("previous");
xpBreakdown.append(
`<div class='text next' style="opacity: 0; margin-top: 1rem;">${string}</div>`
);
const previous = xpBreakdown.find(".previous");
previous.animate(
{
marginTop: "-1rem",
opacity: 0,
},
SlowTimer.get() ? 0 : 250,
() => {
previous.remove();
}
);
setTimeout(() => {
xpGain
.stop(true, true)
.text(`+${total}`)
.css({
borderSpacing: 100,
})
.animate(
{
borderSpacing: 0,
},
{
step(step) {
xpGain.css(
"transform",
`scale(${1 + step / 300}) translateY(-50%)`
);
},
duration: SlowTimer.get() ? 0 : 250,
easing: "swing",
}
);
}, 125);
const el = xpBreakdownList.find(`.line[data-string='${string}']`);
el.css("opacity", 0);
await Misc.promiseAnimation(
xpBreakdown.find(".next"),
el,
{
opacity: "1",
marginTop: "0",
},
SlowTimer.get() ? 0 : 250,
250,
"swing"
);
}
xpGain.text(`+0`);
xpBreakdown.append(
`<div class='text next'>time typing +${breakdown.base}</div>`
);
total += breakdown["base"] ?? 0;
// await Misc.sleep(delay / 2);
total += breakdown.base ?? 0;
// void flashTotalXp(total);
$("nav .xpBar .xpBreakdown .total").text(`+${total}`);
await append("time typing", breakdown.base);
if (breakdown.fullAccuracy) {
await Misc.sleep(delay);
await append(`perfect +${breakdown.fullAccuracy}`);
total += breakdown.fullAccuracy;
void flashTotalXp(total);
await append("perfect", breakdown.fullAccuracy);
} else if (breakdown.corrected) {
await Misc.sleep(delay);
await append(`clean +${breakdown.corrected}`);
total += breakdown.corrected;
void flashTotalXp(total);
await append("clean", breakdown.corrected);
}
if (skipBreakdown) return;
if (breakdown.quote) {
await Misc.sleep(delay);
await append(`quote +${breakdown.quote}`);
total += breakdown.quote;
void flashTotalXp(total);
await append("quote", breakdown.quote);
} else {
if (breakdown.punctuation) {
await Misc.sleep(delay);
await append(`punctuation +${breakdown.punctuation}`);
total += breakdown.punctuation;
void flashTotalXp(total);
await append("punctuation", breakdown.punctuation);
}
if (breakdown.numbers) {
await Misc.sleep(delay);
await append(`numbers +${breakdown.numbers}`);
total += breakdown.numbers;
void flashTotalXp(total);
await append("numbers", breakdown.numbers);
}
}
@ -327,56 +390,59 @@ async function animateXpBreakdown(
if (breakdown.funbox) {
await Misc.sleep(delay);
await append(`funbox +${breakdown.funbox}`);
total += breakdown.funbox;
void flashTotalXp(total);
await append("funbox", breakdown.funbox);
}
if (skipBreakdown) return;
if (breakdown.streak) {
await Misc.sleep(delay);
await append(`streak +${breakdown.streak}`);
total += breakdown.streak;
void flashTotalXp(total);
await append("streak", breakdown.streak);
}
if (skipBreakdown) return;
if (breakdown.accPenalty) {
await Misc.sleep(delay);
await append(`accuracy penalty -${breakdown.accPenalty}`);
total -= breakdown.accPenalty;
void flashTotalXp(total);
await append("accuracy penalty", breakdown.accPenalty * -1);
}
if (skipBreakdown) return;
if (breakdown.incomplete) {
await Misc.sleep(delay);
await append(`incomplete tests +${breakdown.incomplete}`);
total += breakdown.incomplete;
void flashTotalXp(total);
await append("incomplete tests", breakdown.incomplete);
}
if (skipBreakdown) return;
if (breakdown.configMultiplier) {
await Misc.sleep(delay);
await append(`global multiplier x${breakdown.configMultiplier}`);
total *= breakdown.configMultiplier;
void flashTotalXp(total);
await append("global multiplier", `x${breakdown.configMultiplier}`);
}
if (skipBreakdown) return;
if (breakdown.daily) {
await Misc.sleep(delay);
await append(`daily bonus +${breakdown.daily}`);
total += breakdown.daily;
void flashTotalXp(total);
await append("daily bonus", breakdown.daily);
}
if (skipBreakdown) return;
await Misc.sleep(delay);
await append("");
return;
//base (100% corrected) (quote punctuation numbers) accPenalty incomplete configMultiplier daily
}
async function animateXpBar(
@ -387,7 +453,7 @@ async function animateXpBar(
$("nav .xpBar").stop(true, true).css("opacity", 0);
await Misc.promiseAnimation(
void Misc.promiseAnimation(
$("nav .xpBar"),
{
opacity: "1",

View file

@ -19,7 +19,6 @@ import * as ConfigEvent from "../observables/config-event";
import * as Hangul from "hangul-js";
import { format } from "date-fns/format";
import { isAuthenticated } from "../firebase";
import { skipXpBreakdown } from "../elements/account-button";
import * as FunboxList from "./funbox/funbox-list";
import { debounce } from "throttle-debounce";
import * as ResultWordHighlight from "../elements/result-word-highlight";
@ -1619,12 +1618,6 @@ $("#wordsWrapper").on("click", () => {
focusWords();
});
$(document).on("keypress", () => {
if (resultVisible) {
skipXpBreakdown();
}
});
ConfigEvent.subscribe((key, value) => {
if (key === "quickRestart") {
if (value === "off") {