feat: recreate Achievements tab with category sections, progress tracking, and hidden achievement logic
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s

This commit is contained in:
2026-05-19 14:44:27 +02:00
parent 50a9a62060
commit 639d396f80
15 changed files with 1396 additions and 4 deletions
+129
View File
@@ -0,0 +1,129 @@
import { describe, it, expect } from 'vitest';
import {
ACHIEVEMENTS,
ACHIEVEMENT_CATEGORY_COLORS,
getAchievementsByCategory,
isAchievementRevealed,
} from '../data/achievements';
describe('Achievement Definitions', () => {
it('should have 24 achievements defined', () => {
expect(Object.keys(ACHIEVEMENTS).length).toBe(24);
});
it('should have achievements across 5 categories', () => {
const byCategory = getAchievementsByCategory();
const categories = Object.keys(byCategory).sort();
expect(categories).toEqual(['combat', 'crafting', 'magic', 'progression', 'special']);
});
it('should have valid structure for every achievement', () => {
Object.values(ACHIEVEMENTS).forEach((a) => {
expect(a.id).toBeTruthy();
expect(a.name).toBeTruthy();
expect(a.desc).toBeTruthy();
expect(a.category).toBeTruthy();
expect(a.requirement.type).toBeTruthy();
expect(a.requirement.value).toBeGreaterThan(0);
// At least one reward key must be present
const rewardKeys = Object.keys(a.reward);
expect(rewardKeys.length).toBeGreaterThan(0);
});
});
it('should have category colors for all used categories', () => {
const byCategory = getAchievementsByCategory();
Object.keys(byCategory).forEach((cat) => {
expect(ACHIEVEMENT_CATEGORY_COLORS[cat]).toBeDefined();
});
});
it('should have hidden flag only on special category achievements', () => {
Object.values(ACHIEVEMENTS).forEach((a) => {
if (a.hidden) {
expect(a.category).toBe('special');
}
});
});
it('should have exactly 2 hidden achievements', () => {
const hidden = Object.values(ACHIEVEMENTS).filter((a) => a.hidden);
expect(hidden.length).toBe(2); // speedRunner and perfectionist
});
});
describe('getAchievementsByCategory', () => {
it('should group achievements correctly', () => {
const byCategory = getAchievementsByCategory();
// Combat has 6 floor + 3 damage = 9
expect(byCategory['combat'].length).toBe(9);
// Progression has 3 pacts
expect(byCategory['progression'].length).toBe(3);
// Magic has 3 spells + 3 mana = 6
expect(byCategory['magic'].length).toBe(6);
// Crafting has 3
expect(byCategory['crafting'].length).toBe(3);
// Special has 3 (2 hidden + 1 survivor)
expect(byCategory['special'].length).toBe(3);
});
it('should return a new object each call (no mutation)', () => {
const result1 = getAchievementsByCategory();
const result2 = getAchievementsByCategory();
expect(result1).not.toBe(result2);
expect(result1).toEqual(result2);
});
});
describe('isAchievementRevealed', () => {
it('should always reveal non-hidden achievements', () => {
const nonHidden = Object.values(ACHIEVEMENTS).find((a) => !a.hidden);
expect(nonHidden).toBeDefined();
expect(isAchievementRevealed(nonHidden!, 0)).toBe(true);
expect(isAchievementRevealed(nonHidden!, 999)).toBe(true);
});
it('should hide hidden achievements below 50% progress', () => {
const hidden = Object.values(ACHIEVEMENTS).find((a) => a.hidden);
expect(hidden).toBeDefined();
// At 0% progress
expect(isAchievementRevealed(hidden!, 0)).toBe(false);
// At 25% progress
expect(isAchievementRevealed(hidden!, hidden!.requirement.value * 0.25)).toBe(false);
// At exactly 49% progress
expect(isAchievementRevealed(hidden!, hidden!.requirement.value * 0.49)).toBe(false);
});
it('should reveal hidden achievements at 50% progress', () => {
const hidden = Object.values(ACHIEVEMENTS).find((a) => a.hidden);
expect(hidden).toBeDefined();
// At exactly 50%
expect(isAchievementRevealed(hidden!, hidden!.requirement.value * 0.5)).toBe(true);
// At 75%
expect(isAchievementRevealed(hidden!, hidden!.requirement.value * 0.75)).toBe(true);
// At 100%
expect(isAchievementRevealed(hidden!, hidden!.requirement.value)).toBe(true);
});
});
describe('Achievement Rewards', () => {
it('should have insight rewards on all achievements', () => {
Object.values(ACHIEVEMENTS).forEach((a) => {
expect(a.reward.insight).toBeDefined();
expect(a.reward.insight).toBeGreaterThan(0);
});
});
it('should scale rewards with difficulty', () => {
const firstBlood = ACHIEVEMENTS['firstBlood'];
const apexReached = ACHIEVEMENTS['apexReached'];
expect(apexReached.reward.insight!).toBeGreaterThan(firstBlood.reward.insight!);
});
it('should have title rewards on top-tier achievements', () => {
const topTiers = ['apexReached', 'spireMaster', 'tenThousandDamage', 'spellStorm', 'manaOcean', 'pactMaster', 'legendaryEnchanter'];
topTiers.forEach((id) => {
expect(ACHIEVEMENTS[id].reward.title).toBeDefined();
});
});
});