Skip to content

Commit 111ccd1

Browse files
committed
feat: add achievements (Konami Code, Night Owl, Combo Breaker, Human Metronome)
1 parent 0b1acdd commit 111ccd1

File tree

5 files changed

+121
-2
lines changed

5 files changed

+121
-2
lines changed

src/App.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { AnimationProvider } from './context/AnimationContext'; // Import Animat
1111
import { CommandPaletteProvider } from './context/CommandPaletteContext';
1212
import { VisualSettingsProvider } from './context/VisualSettingsContext';
1313
import { AchievementProvider } from './context/AchievementContext';
14+
import AchievementListeners from './components/AchievementListeners';
1415

1516
function App() {
1617
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -47,6 +48,7 @@ function App() {
4748
<Router>
4849
<ToastProvider>
4950
<AchievementProvider>
51+
<AchievementListeners />
5052
<VisualSettingsProvider>
5153
<DigitalRain isActive={isRainActive} />
5254
<ScrollToTop />
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useEffect, useState } from 'react';
2+
import { useAchievements } from '../context/AchievementContext';
3+
4+
const AchievementListeners = () => {
5+
const { unlockAchievement } = useAchievements();
6+
const [konamiIndex, setKonamiIndex] = useState(0);
7+
8+
// Night Owl Check
9+
useEffect(() => {
10+
const checkNightOwl = () => {
11+
const now = new Date();
12+
const hour = now.getHours();
13+
// Between 3 AM (03:00) and 5 AM (05:00)
14+
if (hour >= 3 && hour < 5) {
15+
unlockAchievement('night_owl');
16+
}
17+
};
18+
checkNightOwl();
19+
}, [unlockAchievement]);
20+
// Konami Code Listener
21+
useEffect(() => { // Konami Code Sequence: Up, Up, Down, Down, Left, Right, Left, Right, B, A
22+
const konamiCode = [
23+
'ArrowUp',
24+
'ArrowUp',
25+
'ArrowDown',
26+
'ArrowDown',
27+
'ArrowLeft',
28+
'ArrowRight',
29+
'ArrowLeft',
30+
'ArrowRight',
31+
'b',
32+
'a',
33+
];
34+
const handleKeyDown = (e) => {
35+
// Check if the key matches the current step in the sequence
36+
if (e.key === konamiCode[konamiIndex]) {
37+
const nextIndex = konamiIndex + 1;
38+
// If the sequence is complete
39+
if (nextIndex === konamiCode.length) {
40+
unlockAchievement('konami_code');
41+
setKonamiIndex(0); // Reset
42+
} else {
43+
setKonamiIndex(nextIndex); // Advance
44+
}
45+
} else {
46+
setKonamiIndex(0); // Mistake, reset
47+
}
48+
};
49+
window.addEventListener('keydown', handleKeyDown);
50+
return () => window.removeEventListener('keydown', handleKeyDown);
51+
}, [konamiIndex, unlockAchievement]);
52+
return null; // This component renders nothing
53+
};
54+
55+
export default AchievementListeners;

src/config/achievements.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,4 +687,32 @@ export const ACHIEVEMENTS = [
687687
icon: <TrashIcon size={32} weight="duotone" />,
688688
category: 'Secret',
689689
},
690+
{
691+
id: 'konami_code',
692+
title: 'Konami Code',
693+
description: 'Up, Up, Down, Down, Left, Right, Left, Right, B, A.',
694+
icon: <GameControllerIcon size={32} weight="duotone" />,
695+
category: 'Secret',
696+
},
697+
{
698+
id: 'night_owl',
699+
title: 'Night Owl',
700+
description: 'Visited Fezcodex between 3 AM and 5 AM.',
701+
icon: <EyeIcon size={32} weight="duotone" />,
702+
category: 'Secret',
703+
},
704+
{
705+
id: 'combo_breaker',
706+
title: 'Combo Breaker',
707+
description: 'Completed Memory Game in under 45 seconds.',
708+
icon: <LightningIcon size={32} weight="duotone" />,
709+
category: 'Secret',
710+
},
711+
{
712+
id: 'human_metronome',
713+
title: 'Human Metronome',
714+
description: 'Maintained the exact same BPM for 15 taps in a row.',
715+
icon: <MetronomeIcon size={32} weight="duotone" />,
716+
category: 'Secret',
717+
},
690718
];

src/pages/apps/BpmGuesserPage.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ const BpmGuesserPage = () => {
3333
const lastTapTime = useRef(0);
3434
const { unlockAchievement } = useAchievements();
3535

36+
// For Human Metronome achievement
37+
const prevBpmRef = useRef(0);
38+
const streakRef = useRef(0);
39+
3640
useEffect(() => {
3741
if (bpm === 90) {
3842
unlockAchievement('on_the_beat');
@@ -46,6 +50,8 @@ const BpmGuesserPage = () => {
4650
lastTapTime.current = now;
4751
setTaps([]);
4852
setBpm(0);
53+
streakRef.current = 0;
54+
prevBpmRef.current = 0;
4955
return;
5056
}
5157

@@ -56,6 +62,8 @@ const BpmGuesserPage = () => {
5662
if (diff > 2000) {
5763
setTaps([]);
5864
setBpm(0);
65+
streakRef.current = 0;
66+
prevBpmRef.current = 0;
5967
return;
6068
}
6169

@@ -68,13 +76,30 @@ const BpmGuesserPage = () => {
6876

6977
const averageDiff = newTaps.reduce((a, b) => a + b, 0) / newTaps.length;
7078
const calculatedBpm = Math.round(60000 / averageDiff);
79+
80+
// Check consistency for Human Metronome
81+
// Only check if we have enough samples to be stable (e.g., > 3 taps)
82+
if (newTaps.length > 3) {
83+
if (calculatedBpm === prevBpmRef.current) {
84+
streakRef.current += 1;
85+
if (streakRef.current >= 15) {
86+
unlockAchievement('human_metronome');
87+
}
88+
} else {
89+
streakRef.current = 0;
90+
}
91+
}
92+
prevBpmRef.current = calculatedBpm;
93+
7194
setBpm(calculatedBpm);
7295
};
7396

7497
const reset = () => {
7598
setTaps([]);
7699
setBpm(0);
77100
lastTapTime.current = 0;
101+
streakRef.current = 0;
102+
prevBpmRef.current = 0;
78103
};
79104

80105
const cardStyle = {

src/pages/apps/MemoryGamePage.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const MemoryGamePage = () => {
3636
const [moves, setMoves] = useState(0);
3737
const [gameOver, setGameOver] = useState(false);
3838
const [gameStarted, setGameStarted] = useState(false);
39+
const [startTime, setStartTime] = useState(null);
3940

4041
const initializeGame = useCallback(() => {
4142
const shuffledCards = shuffleCards(cardValues);
@@ -45,12 +46,18 @@ const MemoryGamePage = () => {
4546
setMoves(0);
4647
setGameOver(false);
4748
setGameStarted(false); // Game starts when "Play Game" is clicked
49+
setStartTime(null);
4850
}, []);
4951

5052
useEffect(() => {
5153
initializeGame();
5254
}, [initializeGame]);
5355

56+
const startGame = () => {
57+
setGameStarted(true);
58+
setStartTime(Date.now());
59+
};
60+
5461
const shuffleCards = (values) => {
5562
let id = 0;
5663
const initialCards = [...values, ...values].map((value) => ({
@@ -120,11 +127,13 @@ const MemoryGamePage = () => {
120127
useEffect(() => {
121128
if (matchesFound === cardValues.length) {
122129
setGameOver(true);
130+
const duration = (Date.now() - startTime) / 1000;
131+
if (duration < 45) unlockAchievement('combo_breaker');
123132
if (moves <= 24) unlockAchievement('sharp_eye');
124133
if (moves <= 18) unlockAchievement('eidetic_memory');
125134
if (moves <= 14) unlockAchievement('mind_palace');
126135
}
127-
}, [matchesFound, moves, unlockAchievement]);
136+
}, [matchesFound, moves, unlockAchievement, startTime]);
128137

129138
const cardStyle = {
130139
backgroundColor: colors['app-alpha-10'],
@@ -190,7 +199,7 @@ const MemoryGamePage = () => {
190199
<div className="start-game-message text-center text-3xl font-bold mb-4">
191200
Click "Play Game" to Start!
192201
<button
193-
onClick={() => setGameStarted(true)}
202+
onClick={startGame}
194203
className="block mx-auto mt-4 px-6 py-2 rounded-md text-lg font-arvo font-normal transition-colors duration-300 ease-in-out"
195204
style={{
196205
backgroundColor: 'rgba(0, 0, 0, 0.2)',

0 commit comments

Comments
 (0)