mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 19:48:38 +00:00
Add ensure achievements unlock
- Verify all implemented achievements have unlock triggers - Add score-based triggers for Tetris, Pac-Man, Snake, Breakout - Integrate AI message count checks and music listener flag into achievements - Wire score checks into each game’s score updates - Prepare terminal help updates and user notifications for unlocks X-Lovable-Edit-ID: edt-9cb9ff95-cae4-4afb-8a78-64de6d64c7d7
This commit is contained in:
commit
d3bbb321f0
@ -25,6 +25,32 @@ export const getAiMessageCount = (): number => {
|
|||||||
return parseInt(localStorage.getItem(AI_MESSAGE_COUNT_KEY) || '0', 10);
|
return parseInt(localStorage.getItem(AI_MESSAGE_COUNT_KEY) || '0', 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Game score achievement thresholds
|
||||||
|
export const GAME_SCORE_ACHIEVEMENTS = {
|
||||||
|
tetris: [
|
||||||
|
{ score: 1000, id: 'tetris_1000' },
|
||||||
|
{ score: 10000, id: 'tetris_10000' },
|
||||||
|
{ score: 50000, id: 'tetris_50000' },
|
||||||
|
],
|
||||||
|
pacman: [
|
||||||
|
{ score: 1000, id: 'pacman_1000' },
|
||||||
|
{ score: 5000, id: 'pacman_5000' },
|
||||||
|
{ score: 10000, id: 'pacman_10000' },
|
||||||
|
],
|
||||||
|
snake: [
|
||||||
|
{ score: 500, id: 'snake_500' },
|
||||||
|
{ score: 2000, id: 'snake_2000' },
|
||||||
|
{ score: 5000, id: 'snake_5000' },
|
||||||
|
],
|
||||||
|
breakout: [
|
||||||
|
{ score: 1000, id: 'breakout_1000' },
|
||||||
|
{ score: 5000, id: 'breakout_5000' },
|
||||||
|
{ score: 10000, id: 'breakout_10000' },
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type GameType = keyof typeof GAME_SCORE_ACHIEVEMENTS;
|
||||||
|
|
||||||
interface AchievementsContextType {
|
interface AchievementsContextType {
|
||||||
achievements: Achievement[];
|
achievements: Achievement[];
|
||||||
unlockAchievement: (id: string) => void;
|
unlockAchievement: (id: string) => void;
|
||||||
@ -32,6 +58,9 @@ interface AchievementsContextType {
|
|||||||
getTotalCount: () => number;
|
getTotalCount: () => number;
|
||||||
timeOnSite: number;
|
timeOnSite: number;
|
||||||
checkAiAchievements: () => void;
|
checkAiAchievements: () => void;
|
||||||
|
checkGameScoreAchievements: (game: GameType, score: number) => void;
|
||||||
|
unlockMusicListener: () => void;
|
||||||
|
unlockMaxScore: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultAchievements: Achievement[] = [
|
const defaultAchievements: Achievement[] = [
|
||||||
@ -202,6 +231,19 @@ export const AchievementsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
unlockAchievement('first_visit');
|
unlockAchievement('first_visit');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Check for music listener achievement (set by MusicContext)
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMusicListener = () => {
|
||||||
|
if (localStorage.getItem('has-listened-to-music') === 'true') {
|
||||||
|
unlockAchievement('music_listener');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkMusicListener();
|
||||||
|
// Also check periodically in case user starts playing music
|
||||||
|
const interval = setInterval(checkMusicListener, 2000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [notification, setNotification] = useState<Achievement | null>(null);
|
const [notification, setNotification] = useState<Achievement | null>(null);
|
||||||
|
|
||||||
const unlockAchievement = useCallback((id: string) => {
|
const unlockAchievement = useCallback((id: string) => {
|
||||||
@ -236,6 +278,26 @@ export const AchievementsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
if (count >= 20) unlockAchievement('ai_long_chat');
|
if (count >= 20) unlockAchievement('ai_long_chat');
|
||||||
}, [unlockAchievement]);
|
}, [unlockAchievement]);
|
||||||
|
|
||||||
|
// Check game score achievements
|
||||||
|
const checkGameScoreAchievements = useCallback((game: GameType, score: number) => {
|
||||||
|
const thresholds = GAME_SCORE_ACHIEVEMENTS[game];
|
||||||
|
for (const threshold of thresholds) {
|
||||||
|
if (score >= threshold.score) {
|
||||||
|
unlockAchievement(threshold.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [unlockAchievement]);
|
||||||
|
|
||||||
|
// Unlock music listener achievement
|
||||||
|
const unlockMusicListener = useCallback(() => {
|
||||||
|
unlockAchievement('music_listener');
|
||||||
|
}, [unlockAchievement]);
|
||||||
|
|
||||||
|
// Unlock max score achievement
|
||||||
|
const unlockMaxScore = useCallback(() => {
|
||||||
|
unlockAchievement('max_score');
|
||||||
|
}, [unlockAchievement]);
|
||||||
|
|
||||||
// Save achievements
|
// Save achievements
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('achievements', JSON.stringify(achievements));
|
localStorage.setItem('achievements', JSON.stringify(achievements));
|
||||||
@ -250,6 +312,9 @@ export const AchievementsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
getTotalCount,
|
getTotalCount,
|
||||||
timeOnSite,
|
timeOnSite,
|
||||||
checkAiAchievements,
|
checkAiAchievements,
|
||||||
|
checkGameScoreAchievements,
|
||||||
|
unlockMusicListener,
|
||||||
|
unlockMaxScore,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -104,6 +104,8 @@ export const MusicProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
audio.onplaying = () => {
|
audio.onplaying = () => {
|
||||||
setIsBuffering(false);
|
setIsBuffering(false);
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
|
// Set flag for music listener achievement (checked in AchievementsContext)
|
||||||
|
localStorage.setItem('has-listened-to-music', 'true');
|
||||||
};
|
};
|
||||||
|
|
||||||
audio.onplay = () => {
|
audio.onplay = () => {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings } from '@/contexts/SettingsContext';
|
||||||
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import GlitchCrash from '@/components/GlitchCrash';
|
import GlitchCrash from '@/components/GlitchCrash';
|
||||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
import { Maximize2, Minimize2 } from 'lucide-react';
|
||||||
@ -26,6 +27,7 @@ interface Brick {
|
|||||||
|
|
||||||
const Breakout = () => {
|
const Breakout = () => {
|
||||||
const { playSound } = useSettings();
|
const { playSound } = useSettings();
|
||||||
|
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [score, setScore] = useState(0);
|
const [score, setScore] = useState(0);
|
||||||
const [highScore, setHighScore] = useState(0);
|
const [highScore, setHighScore] = useState(0);
|
||||||
@ -96,6 +98,14 @@ const Breakout = () => {
|
|||||||
if (saved) setHighScore(Math.min(parseInt(saved, 10), MAX_SCORE));
|
if (saved) setHighScore(Math.min(parseInt(saved, 10), MAX_SCORE));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Check score achievements
|
||||||
|
useEffect(() => {
|
||||||
|
if (score > 0) {
|
||||||
|
checkGameScoreAchievements('breakout', score);
|
||||||
|
if (score >= MAX_SCORE) unlockMaxScore();
|
||||||
|
}
|
||||||
|
}, [score, checkGameScoreAchievements, unlockMaxScore]);
|
||||||
|
|
||||||
const initBricks = useCallback(() => {
|
const initBricks = useCallback(() => {
|
||||||
const bricks: Brick[] = [];
|
const bricks: Brick[] = [];
|
||||||
const brickWidth = (canvasSize.width - (BRICK_COLS + 1) * BRICK_GAP) / BRICK_COLS;
|
const brickWidth = (canvasSize.width - (BRICK_COLS + 1) * BRICK_GAP) / BRICK_COLS;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings } from '@/contexts/SettingsContext';
|
||||||
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import GlitchCrash from '@/components/GlitchCrash';
|
import GlitchCrash from '@/components/GlitchCrash';
|
||||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
import { Maximize2, Minimize2 } from 'lucide-react';
|
||||||
@ -43,6 +44,7 @@ const MAZE_TEMPLATE = [
|
|||||||
|
|
||||||
const Pacman = () => {
|
const Pacman = () => {
|
||||||
const { playSound } = useSettings();
|
const { playSound } = useSettings();
|
||||||
|
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
||||||
const [pacman, setPacman] = useState<Position>({ x: 10, y: 15 });
|
const [pacman, setPacman] = useState<Position>({ x: 10, y: 15 });
|
||||||
const [direction, setDirection] = useState<Direction>('right');
|
const [direction, setDirection] = useState<Direction>('right');
|
||||||
const [nextDirection, setNextDirection] = useState<Direction>('right');
|
const [nextDirection, setNextDirection] = useState<Direction>('right');
|
||||||
@ -112,6 +114,14 @@ const Pacman = () => {
|
|||||||
if (saved) setHighScore(Math.min(parseInt(saved, 10), MAX_SCORE));
|
if (saved) setHighScore(Math.min(parseInt(saved, 10), MAX_SCORE));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Check score achievements
|
||||||
|
useEffect(() => {
|
||||||
|
if (score > 0) {
|
||||||
|
checkGameScoreAchievements('pacman', score);
|
||||||
|
if (score >= MAX_SCORE) unlockMaxScore();
|
||||||
|
}
|
||||||
|
}, [score, checkGameScoreAchievements, unlockMaxScore]);
|
||||||
|
|
||||||
const initDots = useCallback(() => {
|
const initDots = useCallback(() => {
|
||||||
const newDots = new Set<string>();
|
const newDots = new Set<string>();
|
||||||
const newPowerPellets = new Set<string>();
|
const newPowerPellets = new Set<string>();
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings } from '@/contexts/SettingsContext';
|
||||||
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import GlitchCrash from '@/components/GlitchCrash';
|
import GlitchCrash from '@/components/GlitchCrash';
|
||||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
import { Maximize2, Minimize2 } from 'lucide-react';
|
||||||
@ -17,6 +18,7 @@ type Position = { x: number; y: number };
|
|||||||
|
|
||||||
const Snake = () => {
|
const Snake = () => {
|
||||||
const { playSound } = useSettings();
|
const { playSound } = useSettings();
|
||||||
|
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
||||||
const [snake, setSnake] = useState<Position[]>([{ x: 10, y: 10 }]);
|
const [snake, setSnake] = useState<Position[]>([{ x: 10, y: 10 }]);
|
||||||
const [direction, setDirection] = useState<Direction>('right');
|
const [direction, setDirection] = useState<Direction>('right');
|
||||||
const [food, setFood] = useState<Position>({ x: 15, y: 10 });
|
const [food, setFood] = useState<Position>({ x: 15, y: 10 });
|
||||||
@ -94,6 +96,14 @@ const Snake = () => {
|
|||||||
if (saved) setHighScore(Math.min(parseInt(saved, 10), MAX_SCORE));
|
if (saved) setHighScore(Math.min(parseInt(saved, 10), MAX_SCORE));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Check score achievements
|
||||||
|
useEffect(() => {
|
||||||
|
if (score > 0) {
|
||||||
|
checkGameScoreAchievements('snake', score);
|
||||||
|
if (score >= MAX_SCORE) unlockMaxScore();
|
||||||
|
}
|
||||||
|
}, [score, checkGameScoreAchievements, unlockMaxScore]);
|
||||||
|
|
||||||
const spawnFood = useCallback((currentSnake: Position[]): Position | null => {
|
const spawnFood = useCallback((currentSnake: Position[]): Position | null => {
|
||||||
const occupied = new Set(currentSnake.map(s => `${s.x},${s.y}`));
|
const occupied = new Set(currentSnake.map(s => `${s.x},${s.y}`));
|
||||||
const available: Position[] = [];
|
const available: Position[] = [];
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings } from '@/contexts/SettingsContext';
|
||||||
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import GlitchCrash from '@/components/GlitchCrash';
|
import GlitchCrash from '@/components/GlitchCrash';
|
||||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
import { Maximize2, Minimize2 } from 'lucide-react';
|
||||||
@ -57,6 +58,7 @@ const rotate = (matrix: number[][]): number[][] => {
|
|||||||
|
|
||||||
const Tetris = () => {
|
const Tetris = () => {
|
||||||
const { playSound } = useSettings();
|
const { playSound } = useSettings();
|
||||||
|
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
||||||
const [board, setBoard] = useState<Board>(createBoard);
|
const [board, setBoard] = useState<Board>(createBoard);
|
||||||
const [piece, setPiece] = useState<Piece>(randomTetromino);
|
const [piece, setPiece] = useState<Piece>(randomTetromino);
|
||||||
const [nextPiece, setNextPiece] = useState<Piece>(randomTetromino);
|
const [nextPiece, setNextPiece] = useState<Piece>(randomTetromino);
|
||||||
@ -122,6 +124,14 @@ const Tetris = () => {
|
|||||||
if (saved) setHighScore(Math.min(parseInt(saved, 10), MAX_SCORE));
|
if (saved) setHighScore(Math.min(parseInt(saved, 10), MAX_SCORE));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Check score achievements
|
||||||
|
useEffect(() => {
|
||||||
|
if (score > 0) {
|
||||||
|
checkGameScoreAchievements('tetris', score);
|
||||||
|
if (score >= MAX_SCORE) unlockMaxScore();
|
||||||
|
}
|
||||||
|
}, [score, checkGameScoreAchievements, unlockMaxScore]);
|
||||||
|
|
||||||
const isValidMove = useCallback((newPiece: Piece, currentBoard: Board): boolean => {
|
const isValidMove = useCallback((newPiece: Piece, currentBoard: Board): boolean => {
|
||||||
for (let y = 0; y < newPiece.shape.length; y++) {
|
for (let y = 0; y < newPiece.shape.length; y++) {
|
||||||
for (let x = 0; x < newPiece.shape[y].length; x++) {
|
for (let x = 0; x < newPiece.shape[y].length; x++) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user