diff --git a/kittyurl-frontend/src/components/FlappyCat.tsx b/kittyurl-frontend/src/components/FlappyCat.tsx index 2989b54..bc1a457 100644 --- a/kittyurl-frontend/src/components/FlappyCat.tsx +++ b/kittyurl-frontend/src/components/FlappyCat.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { ArrowLeft, Trophy, Sparkles } from 'lucide-react'; -// --- MODEL KOTA (bez zmian) --- +// --- MODEL KOTA --- interface DetailedKittyProps { isGameOver: boolean; } const DetailedKitty: React.FC = ({ isGameOver }) => { const mainColor = isGameOver ? '#cbd5e1' : '#f472b6'; @@ -32,16 +32,13 @@ const DetailedKitty: React.FC = ({ isGameOver }) => { ); }; -// --- KONFIGURACJA STABILNA --- -const FPS_LIMIT = 60; -const FRAME_MIN_TIME = 1000 / FPS_LIMIT; // ok. 16.67ms - +// --- KONFIGURACJA NIEZALEŻNA OD FPS (Wartości na sekundę) --- const GAP_SIZE = 180; const PIPE_WIDTH = 70; -const PIPE_SPEED = 180; -const PIPE_SPAWN_RATE = 2.0; -const GRAVITY = 1200; -const FLAP_STRENGTH = -380; +const PIPE_SPEED = 200; // px/s +const PIPE_SPAWN_RATE = 1.8; // sekundy +const GRAVITY = 1400; // px/s^2 +const FLAP_STRENGTH = -420; // px/s const CANVAS_HEIGHT = 450; export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { @@ -63,17 +60,17 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { const scoreRef = useRef(0); const requestRef = useRef(0); const lastTimeRef = useRef(0); - const lastFrameTimestampRef = useRef(0); // Do pilnowania FPS const spawnTimerRef = useRef(0); const endGame = useCallback(() => { setGameOver(true); setIsPlaying(false); - if (scoreRef.current > highScore) { + const currentHS = parseInt(localStorage.getItem('flappyKittyHighScore') || '0', 10); + if (scoreRef.current > currentHS) { setHighScore(scoreRef.current); localStorage.setItem('flappyKittyHighScore', scoreRef.current.toString()); } - }, [highScore]); + }, []); const startGame = useCallback(() => { setIsPlaying(true); @@ -84,10 +81,10 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { velocityRef.current = 0; pipesRef.current = []; spawnTimerRef.current = 0; - lastTimeRef.current = performance.now(); - lastFrameTimestampRef.current = performance.now(); + lastTimeRef.current = performance.now(); // Inicjalizacja czasu setDisplayKittyY(150); setDisplayPipes([]); + setRotation(0); }, []); const flap = useCallback(() => { @@ -98,31 +95,28 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { const update = (currentTime: number) => { if (gameOver || !isPlaying) return; - // --- MECHANIZM FPS CAP --- - const elapsedSinceLastFrame = currentTime - lastFrameTimestampRef.current; - - // Jeśli klatka przyszła za szybko (np. na monitorze 144Hz), pomijamy update - if (elapsedSinceLastFrame < FRAME_MIN_TIME) { - requestRef.current = requestAnimationFrame(update); - return; - } - - // Obliczamy dt na podstawie rzeczywistego czasu, który upłynął + // --- DYNAMICZNY DELTA TIME --- + // Obliczamy ile sekund upłynęło od ostatniej klatki (np. 0.0069s dla 144Hz) const dt = (currentTime - lastTimeRef.current) / 1000; lastTimeRef.current = currentTime; - lastFrameTimestampRef.current = currentTime; // Aktualizujemy znacznik klatki + // Zabezpieczenie przed ogromnym skokiem fizyki przy lagu const frameTime = Math.min(dt, 0.1); + // Fizyka grawitacji velocityRef.current += GRAVITY * frameTime; kittyYRef.current += velocityRef.current * frameTime; - setRotation(Math.min(Math.max(velocityRef.current * 0.12, -20), 70)); - if (kittyYRef.current > CANVAS_HEIGHT - 40 || kittyYRef.current < -40) { + // Płynna rotacja zależna od prędkości pionowej + setRotation(Math.min(Math.max(velocityRef.current * 0.12, -20), 80)); + + // Kolizja z sufitem/ziemią + if (kittyYRef.current > CANVAS_HEIGHT - 40 || kittyYRef.current < -50) { endGame(); return; } + // Spawn rur spawnTimerRef.current += frameTime; if (spawnTimerRef.current >= PIPE_SPAWN_RATE) { const topHeight = Math.random() * (220 - 70) + 70; @@ -130,26 +124,34 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { spawnTimerRef.current = 0; } + // Ruch rur i kolizje const updatedPipes = []; for (const p of pipesRef.current) { p.x -= PIPE_SPEED * frameTime; - if (p.x < 100 && p.x + PIPE_WIDTH > 50) { + + // Hitbox (z małym marginesem dla kota) + if (p.x < 100 && p.x + PIPE_WIDTH > 55) { if (kittyYRef.current < p.topHeight || kittyYRef.current > p.topHeight + GAP_SIZE - 45) { endGame(); return; } } + + // Punktacja if (p.x < 50 && !p.passed) { p.passed = true; scoreRef.current += 1; setScore(scoreRef.current); } + if (p.x > -PIPE_WIDTH) updatedPipes.push(p); } pipesRef.current = updatedPipes; + // Update stanów do renderowania setDisplayKittyY(kittyYRef.current); setDisplayPipes([...pipesRef.current]); + requestRef.current = requestAnimationFrame(update); }; @@ -171,13 +173,13 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { }, [isPlaying, gameOver, startGame, flap]); return ( -
+
{ if (!isPlaying || gameOver) startGame(); else flap(); }} >
@@ -188,7 +190,7 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => {
Record: {highScore}
-
+
@@ -216,6 +218,7 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => {
)}
+

Physics: {lastTimeRef.current ? "Frame-Independent" : "Detecting..."}

); }; \ No newline at end of file diff --git a/kittyurl-frontend/src/components/KittyGame.tsx b/kittyurl-frontend/src/components/KittyGame.tsx index 6f0aa64..5ca323e 100644 --- a/kittyurl-frontend/src/components/KittyGame.tsx +++ b/kittyurl-frontend/src/components/KittyGame.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { ArrowLeft, Trophy, Sparkles, Moon, Sun } from 'lucide-react'; -// --- ZAAWANSOWANY MODEL KOTA (DetailedKitty) --- +// --- MODEL KOTA (Animacje CSS zostają, bo są czasowe, nie klatkowe) --- interface DetailedKittyProps { isJumping: boolean; isNight: boolean; @@ -15,13 +15,8 @@ const DetailedKitty: React.FC = ({ isJumping, isNight, isGam return (
-
+
@@ -41,27 +36,20 @@ const DetailedKitty: React.FC = ({ isJumping, isNight, isGam
+ {/* Łapki */}
+ style={{ backgroundColor: mainColor, animation: !isJumping && !isGameOver ? 'kitty-walk 0.2s infinite' : 'none', transform: isJumping ? 'rotate(45deg)' : 'none' }}>
+ style={{ backgroundColor: mainColor, animation: !isJumping && !isGameOver ? 'kitty-walk-alt 0.2s infinite' : 'none', transform: isJumping ? 'rotate(-45deg)' : 'none' }}>
); }; -// --- GŁÓWNY KOMPONENT GRY --- +// --- GŁÓWNY KOMPONENT Z DYNAMICZNYM DELTA TIME --- export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { const [isPlaying, setIsPlaying] = useState(false); const [gameOver, setGameOver] = useState(false); @@ -76,23 +64,19 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { const isNight = Math.floor(score / 10) % 2 === 1; - // --- REF DO FPS I FIZYKI --- + // Referencje fizyczne const kittyYRef = useRef(0); const obstacleXRef = useRef(650); const velocityRef = useRef(0); const scoreRef = useRef(0); const requestRef = useRef(0); const lastTimeRef = useRef(0); - const lastFrameTimestampRef = useRef(0); - // --- STAŁE --- - const TARGET_FPS = 60; - const FRAME_MIN_TIME = 1000 / TARGET_FPS; // ok. 16.67ms - - const GRAVITY = 1800; - const JUMP_FORCE = -550; - const INITIAL_SPEED = 350; - const SPEED_INCREMENT = 15; + // STAŁE KONFIGURACYJNE (Wartości na sekundę - niezależne od Hz) + const GRAVITY = 1800; // Kot spadnie o 1800px w ciągu sekundy (jeśli nie ma prędkości początkowej) + const JUMP_FORCE = -550; // Prędkość startowa skoku + const INITIAL_SPEED = 380; // Prędkość przeszkody w px/s + const SPEED_INCREMENT = 12; // Przyspieszenie px/s na każdy punkt const GROUND_Y = 0; const endGame = useCallback(() => { @@ -111,12 +95,12 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { setScore(0); scoreRef.current = 0; kittyYRef.current = 0; - obstacleXRef.current = 650; + obstacleXRef.current = 700; velocityRef.current = 0; + // Kluczowe: inicjalizacja czasu startu lastTimeRef.current = performance.now(); - lastFrameTimestampRef.current = performance.now(); setDisplayKittyY(0); - setDisplayObstacleX(650); + setDisplayObstacleX(700); }, []); const jump = useCallback(() => { @@ -126,24 +110,18 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { }, [gameOver, isPlaying]); useEffect(() => { - const update = (time: number) => { + const update = (currentTime: number) => { if (gameOver || !isPlaying) return; - // --- MECHANIZM FPS CAP --- - const elapsedSinceLastFrame = time - lastFrameTimestampRef.current; - if (elapsedSinceLastFrame < FRAME_MIN_TIME) { - requestRef.current = requestAnimationFrame(update); - return; - } - - // Delta time (dt) w sekundach - const dt = (time - lastTimeRef.current) / 1000; - lastTimeRef.current = time; - lastFrameTimestampRef.current = time; // Aktualizujemy czas klatki + // --- OBLICZANIE DELTA TIME (Dynamiczna szybkość) --- + // dt to czas w sekundach, jaki upłynął od ostatniej klatki (np. 0.016 dla 60Hz, 0.007 dla 144Hz) + const dt = (currentTime - lastTimeRef.current) / 1000; + lastTimeRef.current = currentTime; + // Zabezpieczenie przed "skokiem" (np. gdy użytkownik zmieni kartę w przeglądarce) const frameTime = Math.min(dt, 0.1); - // Fizyka kota + // Fizyka kota (jednostki * czas) velocityRef.current += GRAVITY * frameTime; kittyYRef.current -= velocityRef.current * frameTime; @@ -152,24 +130,27 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { velocityRef.current = 0; } - // Ruch przeszkody + // Ruch przeszkody (prędkość * czas) const currentSpeed = INITIAL_SPEED + (scoreRef.current * SPEED_INCREMENT); obstacleXRef.current -= currentSpeed * frameTime; - if (obstacleXRef.current < -60) { - obstacleXRef.current = 700; + // Reset przeszkody + if (obstacleXRef.current < -80) { + obstacleXRef.current = 750; scoreRef.current += 1; setScore(scoreRef.current); } - // Kolizja - if (obstacleXRef.current < 110 && obstacleXRef.current > 30 && kittyYRef.current < 45) { + // Kolizja (strefa trafienia dopasowana do czasu) + if (obstacleXRef.current < 100 && obstacleXRef.current > 20 && kittyYRef.current < 45) { endGame(); return; } + // Renderowanie wizualne setDisplayKittyY(kittyYRef.current); setDisplayObstacleX(obstacleXRef.current); + requestRef.current = requestAnimationFrame(update); }; @@ -191,16 +172,17 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { }, [isPlaying, gameOver, startGame, jump]); return ( -
+
{ if (!isPlaying || gameOver) startGame(); else jump(); }} > + {/* Niebo */}
@@ -208,6 +190,7 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => {
+ {/* UI Score */}
Score: {score}
@@ -215,46 +198,55 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => {
+ {/* KOTEK */}
2} isNight={isNight} isGameOver={gameOver} />
+ {/* PRZESZKODA */}
-
+
+
+ {/* Ziemia */}
- {[...Array(9)].map((_, i) => 🐾)} + ${isNight ? 'bg-slate-900 border-indigo-900 text-indigo-950' : 'bg-pink-50 border-pink-100 text-pink-100'}`}> + {[...Array(10)].map((_, i) => 🐾)}
+ {/* Ekrany start/stop */} {!isPlaying && !gameOver && ( -
-
- - +

Click or Space

)} {gameOver && ( -
-

Meow! 😿

-

Score: {score}

-
)}
-

Space / Click to Jump

+
+ Speed: {Math.round(INITIAL_SPEED + score * SPEED_INCREMENT)} px/s + Physics: Delta-Time Based +
); }; \ No newline at end of file