feat(achievements): implement unlocking, saving and notification

This commit is contained in:
2026-01-18 22:50:35 +01:00
parent ca9d0a2b55
commit 41eea74cc0
13 changed files with 269 additions and 2 deletions

View File

@@ -0,0 +1,83 @@
import { useLocalStorage } from "@vueuse/core";
const STORAGE_ID = "achievements";
export const ACHIEVEMENTS = [
"boot",
// projects
"projects_visit",
"projects_view_5",
"projects_open_link",
// gallery
"gallery_visit",
// contact
"contact_visit",
"contact_git_visit",
// settings
"settings_color_change",
// snake
"snake_play",
"snake_score_40",
// secrets
"settings_color_try_all",
"settings_language_try_all",
"settings_visit_all",
"contact_36_notifications",
] as const;
export type Achievement = (typeof ACHIEVEMENTS)[number];
export const useAchievementsStore = defineStore("achievements", () => {
const app = useAppStore();
const { locale } = useI18n();
const storage = useLocalStorage(
STORAGE_ID,
{
unlocked: [] as Achievement[],
advancement: {
colors: [app.color.hex],
languages: [locale.value],
visitedSettings: [] as string[],
},
},
{ mergeDefaults: true },
);
if (!storage.value.advancement.colors.includes(app.color.hex)) {
storage.value.advancement.colors.push(app.color.hex);
}
if (!storage.value.advancement.languages.includes(locale.value)) {
storage.value.advancement.languages.push(locale.value);
}
const unlock = (name: Achievement) => {
if (storage.value.unlocked.includes(name)) {
return false;
}
storage.value.unlocked.push(name);
return true;
};
const reset = () => {
storage.value = {
unlocked: [],
advancement: {
colors: [app.color.hex],
languages: [locale.value],
visitedSettings: [],
},
};
};
return {
achievements: computed(() => storage.value.unlocked),
advancement: computed(() => storage.value.advancement),
unlock,
reset,
isUnlocked: computed(
() => (name: Achievement) => storage.value.unlocked.includes(name),
),
};
});

View File

@@ -41,6 +41,22 @@ export const useAppStore = defineStore("app", {
navigateTo(screen: AppScreen) {
this.screen = screen;
const achievements = useAchievementsStore();
switch (screen) {
case "projects":
achievements.unlock("projects_visit");
break;
case "gallery":
achievements.unlock("gallery_visit");
break;
case "contact":
achievements.unlock("contact_visit");
break;
}
},
setCamera(camera: THREE.Camera) {

View File

@@ -82,6 +82,11 @@ export const useContactStore = defineStore("contact", {
{ notificationsYOffset: 20 },
{ notificationsYOffset: 0, duration: 0.075 },
);
if (this.notifications.length === 36) {
const achievements = useAchievementsStore();
achievements.unlock("contact_36_notifications");
}
},
animateOutro() {

View File

@@ -68,7 +68,9 @@ export const useIntroStore = defineStore("intro", {
})
.call(() => {
const app = useAppStore();
const achievements = useAchievementsStore();
app.booted = true;
achievements.unlock("boot");
});
},
},

View File

@@ -61,7 +61,11 @@ export const useProjectsStore = defineStore("projects", {
visitProject() {
const link = this.projects[this.currentProject]?.link;
if (link) navigateTo(link, { open: { target: "_blank" } });
if (link) {
navigateTo(link, { open: { target: "_blank" } });
const achievements = useAchievementsStore();
achievements.unlock("projects_open_link");
}
},
scrollProjects(direction: "left" | "right") {
@@ -89,6 +93,13 @@ export const useProjectsStore = defineStore("projects", {
},
);
}
if (this.currentProject >= 4) {
setTimeout(() => {
const achievements = useAchievementsStore();
achievements.unlock("projects_view_5");
}, 500);
}
},
// TODO: not used anymore

View File

@@ -14,6 +14,17 @@ export const useSettingsStore = defineStore("settings", {
openSubMenu(submenu: SettingsSubMenu) {
this.currentSubMenu = submenu;
const achievements = useAchievementsStore();
if (!achievements.advancement.visitedSettings.includes(submenu)) {
achievements.advancement.visitedSettings.push(submenu);
}
if (
achievements.advancement.visitedSettings.length ===
SETTINGS_SUB_MENUS.length
) {
achievements.unlock("settings_visit_all");
}
},
closeSubMenu() {