mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-18 00:19:17 +00:00
Feat: Singleplayer Achievements (#2734)
Contributes towards the ongoing task of adding achievements: #2706 ## Description: Introduces a concept design and API implementation for singleplayer win achievements. New row of ~5~ 4 medals is added to the map select screen in the Singleplayer modal, one for each difficulty: <img width="3132" height="779" alt="image" src="https://github.com/user-attachments/assets/da8f0314-ccad-4f45-a03f-1beb46981301" /> In order to achieve a medal in a particular map, you must win the singleplayer game (multiplayer and private match games don't count) in the selected difficulty **without tampering with the options or settings**. If any setting is changed from the default, regardless of the difficulty, you will ~receive a fifth "Custom" medal~ not receive the medal for that difficulty. Team games **do not** count towards the medal achievement. Completion of a medal will fill in the full correct color, as defined in our `variables.css`: <img width="694" height="778" alt="image" src="https://github.com/user-attachments/assets/1b2d8370-aa86-4329-9402-adf43f3ef799" /> Completion medals can be toggled on or off (hidden by default) with the toggle button at the top of the section: https://github.com/user-attachments/assets/d08a58e0-b534-430e-9e8f-559134ad8852 [API implementation PR](https://github.com/openfrontio/infra/pull/234) ## 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 - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: bijx --------- Co-authored-by: iamlewis <lewismmmm@gmail.com> Co-authored-by: Evan <evanpelle@gmail.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import {
|
||||
Difficulty,
|
||||
Duos,
|
||||
@@ -49,6 +50,8 @@ export class SinglePlayerModal extends LitElement {
|
||||
@state() private useRandomMap: boolean = false;
|
||||
@state() private gameMode: GameMode = GameMode.FFA;
|
||||
@state() private teamCount: TeamCountConfig = 2;
|
||||
@state() private showAchievements: boolean = false;
|
||||
@state() private mapWins: Map<GameMapType, Set<Difficulty>> = new Map();
|
||||
|
||||
@state() private disabledUnits: UnitType[] = [];
|
||||
|
||||
@@ -57,9 +60,17 @@ export class SinglePlayerModal extends LitElement {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
document.addEventListener(
|
||||
"userMeResponse",
|
||||
this.handleUserMeResponse as EventListener,
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
document.removeEventListener(
|
||||
"userMeResponse",
|
||||
this.handleUserMeResponse as EventListener,
|
||||
);
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
@@ -71,13 +82,76 @@ export class SinglePlayerModal extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private toggleAchievements = () => {
|
||||
this.showAchievements = !this.showAchievements;
|
||||
};
|
||||
|
||||
private handleUserMeResponse = (
|
||||
event: CustomEvent<UserMeResponse | false>,
|
||||
) => {
|
||||
this.applyAchievements(event.detail);
|
||||
};
|
||||
|
||||
private applyAchievements(userMe: UserMeResponse | false) {
|
||||
if (!userMe) {
|
||||
this.mapWins = new Map();
|
||||
return;
|
||||
}
|
||||
|
||||
const achievements = Array.isArray(userMe.player.achievements)
|
||||
? userMe.player.achievements
|
||||
: [];
|
||||
|
||||
const completions =
|
||||
achievements.find(
|
||||
(achievement) => achievement?.type === "singleplayer-map",
|
||||
)?.data ?? [];
|
||||
|
||||
const winsMap = new Map<GameMapType, Set<Difficulty>>();
|
||||
for (const entry of completions) {
|
||||
const { mapName, difficulty } = entry ?? {};
|
||||
const isValidMap =
|
||||
typeof mapName === "string" &&
|
||||
Object.values(GameMapType).includes(mapName as GameMapType);
|
||||
const isValidDifficulty =
|
||||
typeof difficulty === "string" &&
|
||||
Object.values(Difficulty).includes(difficulty as Difficulty);
|
||||
if (!isValidMap || !isValidDifficulty) continue;
|
||||
|
||||
const map = mapName as GameMapType;
|
||||
const set = winsMap.get(map) ?? new Set<Difficulty>();
|
||||
set.add(difficulty as Difficulty);
|
||||
winsMap.set(map, set);
|
||||
}
|
||||
|
||||
this.mapWins = winsMap;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<o-modal title=${translateText("single_modal.title")}>
|
||||
<div class="options-layout">
|
||||
<!-- Map Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">${translateText("map.map")}</div>
|
||||
<div
|
||||
class="option-title"
|
||||
style="position:relative; display:flex; align-items:center; justify-content:center; width:100%;"
|
||||
>
|
||||
<span style="text-align:center; width:100%;">
|
||||
${translateText("map.map")}
|
||||
</span>
|
||||
<button
|
||||
@click=${this.toggleAchievements}
|
||||
title=${translateText("single_modal.toggle_achievements")}
|
||||
style="display:flex; align-items:center; justify-content:center; width:28px; height:28px; border:1px solid rgba(255,255,255,0.2); border-radius:6px; background:rgba(255,255,255,0.06); cursor:pointer; padding:4px; position:absolute; right:0; top:50%; transform:translateY(-50%);"
|
||||
>
|
||||
<img
|
||||
src="/images/MedalIconWhite.svg"
|
||||
alt="Toggle achievements"
|
||||
style=${`width:18px; height:18px; opacity:${this.showAchievements ? "1" : "0.5"};`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="option-cards flex-col">
|
||||
<!-- Use the imported mapCategories -->
|
||||
${Object.entries(mapCategories).map(
|
||||
@@ -103,6 +177,8 @@ export class SinglePlayerModal extends LitElement {
|
||||
.mapKey=${mapKey}
|
||||
.selected=${!this.useRandomMap &&
|
||||
this.selectedMap === mapValue}
|
||||
.showMedals=${this.showAchievements}
|
||||
.wins=${this.mapWins.get(mapValue) ?? new Set()}
|
||||
.translation=${translateText(
|
||||
`map.${mapKey?.toLowerCase()}`,
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user