feat: Achievement medal overview (#4487)

## Description:

In the spirit of achievement hunting, seeing how many medals a player
has achieved helps show overall progress. Overview only toggles on when
user clicks toggle achievements button. Works on mobile too.


https://github.com/user-attachments/assets/ea77075b-5e91-4e62-8ac9-52bcdf95cc64

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory

## Please put your Discord username so you can be contacted if a bug or
regression is found:

bijx
This commit is contained in:
bijx
2026-07-02 21:31:19 -04:00
committed by GitHub
parent 64a6111fd4
commit ad760a0f3d
4 changed files with 184 additions and 42 deletions
+4 -26
View File
@@ -1,12 +1,10 @@
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import medalIconRaw from "../../../../resources/images/MedalIconWhite.svg?raw";
import { Difficulty, GameMapType } from "../../../core/game/Game";
import { terrainMapFileLoader } from "../../TerrainMapFileLoader";
import { translateText } from "../../Utils";
import { starIcon } from "./MapFavorites";
const medalMaskUrl = `url('data:image/svg+xml;utf8,${encodeURIComponent(medalIconRaw)}') no-repeat center / contain`;
import { MEDAL_ORDER, medalIcon } from "./Medals";
@customElement("map-display")
export class MapDisplay extends LitElement {
@@ -177,30 +175,10 @@ export class MapDisplay extends LitElement {
}
private renderMedals() {
const medalOrder: Difficulty[] = [
Difficulty.Easy,
Difficulty.Medium,
Difficulty.Hard,
Difficulty.Impossible,
];
const colors: Record<Difficulty, string> = {
[Difficulty.Easy]: "var(--medal-easy)",
[Difficulty.Medium]: "var(--medal-medium)",
[Difficulty.Hard]: "var(--medal-hard)",
[Difficulty.Impossible]: "var(--medal-impossible)",
};
const wins = this.readWins();
return medalOrder.map((medal) => {
const earned = wins.has(medal);
const mask = medalMaskUrl;
return html`<div
class="w-5 h-5 ${earned ? "opacity-100" : "opacity-25"}"
style="background-color:${colors[
medal
]}; mask: ${mask}; -webkit-mask: ${mask};"
title=${translateText(`difficulty.${medal.toLowerCase()}`)}
></div>`;
});
return MEDAL_ORDER.map((medal) =>
medalIcon(medal, "w-5 h-5", wins.has(medal)),
);
}
private readWins(): Set<Difficulty> {
+40
View File
@@ -0,0 +1,40 @@
import { html, TemplateResult } from "lit";
import medalIconRaw from "../../../../resources/images/MedalIconWhite.svg?raw";
import { Difficulty } from "../../../core/game/Game";
import { translateText } from "../../Utils";
// CSS mask that renders the medal glyph; tint it via `background-color`.
export const MEDAL_MASK = `url('data:image/svg+xml;utf8,${encodeURIComponent(medalIconRaw)}') no-repeat center / contain`;
// Difficulty medals, easiest to hardest — the canonical display order.
export const MEDAL_ORDER: readonly Difficulty[] = [
Difficulty.Easy,
Difficulty.Medium,
Difficulty.Hard,
Difficulty.Impossible,
];
export const MEDAL_COLORS: Record<Difficulty, string> = {
[Difficulty.Easy]: "var(--medal-easy)",
[Difficulty.Medium]: "var(--medal-medium)",
[Difficulty.Hard]: "var(--medal-hard)",
[Difficulty.Impossible]: "var(--medal-impossible)",
};
/**
* A single colored medal glyph. Pass `earned=false` to dim it so it reads as
* "not yet won" (used on map cards); the overview keeps them full-color.
*/
export function medalIcon(
difficulty: Difficulty,
sizeClass = "w-5 h-5",
earned = true,
): TemplateResult {
return html`<div
class="${sizeClass} ${earned ? "opacity-100" : "opacity-25"}"
style="background-color:${MEDAL_COLORS[
difficulty
]}; mask:${MEDAL_MASK}; -webkit-mask:${MEDAL_MASK};"
title=${translateText(`difficulty.${difficulty.toLowerCase()}`)}
></div>`;
}