From 502543d6aceec9328a1bebf2439613d84f8c3ce7 Mon Sep 17 00:00:00 2001 From: Pc Date: Tue, 30 Dec 2025 19:33:28 +0100 Subject: [PATCH] fix: games physics --- .../src/components/FlappyCat.tsx | 160 +++++++++--------- .../src/components/KittyGame.tsx | 80 ++++----- 2 files changed, 124 insertions(+), 116 deletions(-) diff --git a/kittyurl-frontend/src/components/FlappyCat.tsx b/kittyurl-frontend/src/components/FlappyCat.tsx index e53af9a..6114196 100644 --- a/kittyurl-frontend/src/components/FlappyCat.tsx +++ b/kittyurl-frontend/src/components/FlappyCat.tsx @@ -2,9 +2,14 @@ import { ArrowLeft, Trophy, Sparkles } from 'lucide-react'; import { DetailedKitty } from './DetailedKitty'; -const GAP_SIZE = 170; // Przerwa między drapakami +// --- STAŁE KONFIGURACYJNE (Wartości na sekundę) --- +const GAP_SIZE = 170; const PIPE_WIDTH = 70; -const PIPE_SPAWN_RATE = 1500; // Nowy drapak co 1.5 sekundy +const PIPE_SPEED = 250; // px/s +const PIPE_SPAWN_RATE = 1.5; // Sekundy +const GRAVITY = 1600; // px/s^2 +const FLAP_STRENGTH = -450; // px/s +const CANVAS_HEIGHT = 450; export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { const [isPlaying, setIsPlaying] = useState(false); @@ -15,20 +20,28 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { return saved ? parseInt(saved, 10) : 0; }); - // Stany pozycji do renderowania const [displayKittyY, setDisplayKittyY] = useState(150); const [displayPipes, setDisplayPipes] = useState<{ x: number; topHeight: number; id: number }[]>([]); const [rotation, setRotation] = useState(0); - // Referencje do fizyki (obliczenia poza Reactem dla płynności) + // Referencje do fizyki i czasu const kittyYRef = useRef(150); const velocityRef = useRef(0); - const pipesRef = useRef<{ x: number; topHeight: number; id: number }[]>([]); + const pipesRef = useRef<{ x: number; topHeight: number; id: number; passed?: boolean }[]>([]); const scoreRef = useRef(0); const requestRef = useRef(0); + const lastTimeRef = useRef(0); + const spawnTimerRef = useRef(0); - const GRAVITY = 0.35; - const JUMP_STRENGTH = -7; + const endGame = useCallback(() => { + setGameOver(true); + setIsPlaying(false); + const currentHS = parseInt(localStorage.getItem('flappyKittyHighScore') || '0', 10); + if (scoreRef.current > currentHS) { + setHighScore(scoreRef.current); + localStorage.setItem('flappyKittyHighScore', scoreRef.current.toString()); + } + }, []); const startGame = useCallback(() => { setIsPlaying(true); @@ -38,90 +51,92 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { kittyYRef.current = 150; velocityRef.current = 0; pipesRef.current = []; + spawnTimerRef.current = 0; + lastTimeRef.current = performance.now(); setDisplayKittyY(150); setDisplayPipes([]); }, []); const flap = useCallback(() => { if (isPlaying && !gameOver) { - velocityRef.current = JUMP_STRENGTH; + velocityRef.current = FLAP_STRENGTH; } }, [isPlaying, gameOver]); useEffect(() => { - const update = () => { + const update = (currentTime: number) => { if (gameOver || !isPlaying) return; - // 1. Fizyka lotu - velocityRef.current += GRAVITY; - kittyYRef.current += velocityRef.current; + // 1. Obliczanie Delta Time + const dt = (currentTime - lastTimeRef.current) / 1000; + lastTimeRef.current = currentTime; - // Obrót kota zależnie od prędkości - setRotation(Math.min(Math.max(velocityRef.current * 4, -20), 90)); + // Zabezpieczenie przed "skokami" przy lagach (max 100ms) + const frameTime = Math.min(dt, 0.1); - // 2. Kolizja z sufitem i podłogą - if (kittyYRef.current > 380 || kittyYRef.current < -50) { + // 2. Fizyka Kota + velocityRef.current += GRAVITY * frameTime; + kittyYRef.current += velocityRef.current * frameTime; + + // Wizualny obrót (od -20 stopni przy locie w górę do 90 przy spadaniu) + const targetRotation = Math.min(Math.max(velocityRef.current * 0.15, -20), 90); + setRotation(targetRotation); + + // 3. Kolizja z granicami ekranu + if (kittyYRef.current > CANVAS_HEIGHT - 40 || kittyYRef.current < -20) { endGame(); return; } - // 3. Ruch drapaków (rur) - pipesRef.current = pipesRef.current - .map(p => ({ ...p, x: p.x - 4 })) // Prędkość rur - .filter(p => p.x > -100); + // 4. Zarządzanie Rurami (Spawning) + spawnTimerRef.current += frameTime; + if (spawnTimerRef.current >= PIPE_SPAWN_RATE) { + const topHeight = Math.random() * (220 - 70) + 70; + pipesRef.current.push({ x: 650, topHeight, id: Date.now() }); + spawnTimerRef.current = 0; + } - // 4. Wykrywanie kolizji z drapakami - pipesRef.current.forEach(p => { - // Kot jest na stałej pozycji X (ok. 50-100px) - if (p.x < 110 && p.x + PIPE_WIDTH > 40) { - // Sprawdzanie czy kot uderzył w górny lub dolny drapak - if (kittyYRef.current < p.topHeight || kittyYRef.current > p.topHeight + GAP_SIZE - 40) { + // 5. Ruch rur i Kolizje + const updatedPipes = []; + for (let p of pipesRef.current) { + p.x -= PIPE_SPEED * frameTime; + + // Sprawdzanie kolizji + // Kitty X jest stałe na ok. 50-90px. Kot ma szerokość ok. 40px w uproszczeniu kolizyjnym. + if (p.x < 100 && p.x + PIPE_WIDTH > 50) { + if (kittyYRef.current < p.topHeight || kittyYRef.current > p.topHeight + GAP_SIZE - 45) { endGame(); + return; } } - // Dodawanie punktów - if (p.x < 40 && !p.hasOwnProperty('passed')) { - (p as any).passed = true; + // Punktacja + if (p.x < 50 && !p.passed) { + p.passed = true; scoreRef.current += 1; setScore(scoreRef.current); } - }); - // 5. Synchronizacja z widokiem + // Usuwanie rur poza ekranem + if (p.x > -PIPE_WIDTH) { + updatedPipes.push(p); + } + } + pipesRef.current = updatedPipes; + + // 6. Sync UI setDisplayKittyY(kittyYRef.current); setDisplayPipes([...pipesRef.current]); requestRef.current = requestAnimationFrame(update); }; - const endGame = () => { - setGameOver(true); - setIsPlaying(false); - const currentHS = parseInt(localStorage.getItem('flappyKittyHighScore') || '0', 10); - if (scoreRef.current > currentHS) { - setHighScore(scoreRef.current); - localStorage.setItem('flappyKittyHighScore', scoreRef.current.toString()); - } - }; - if (isPlaying && !gameOver) { requestRef.current = requestAnimationFrame(update); } return () => cancelAnimationFrame(requestRef.current); - }, [isPlaying, gameOver]); + }, [isPlaying, gameOver, endGame]); - // Generator nowych drapaków - useEffect(() => { - if (!isPlaying || gameOver) return; - const interval = setInterval(() => { - const topHeight = Math.random() * (220 - 50) + 50; - pipesRef.current.push({ x: 650, topHeight, id: Date.now() }); - }, PIPE_SPAWN_RATE); - return () => clearInterval(interval); - }, [isPlaying, gameOver]); - - // Obsługa klawiszy useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.code === 'Space') { @@ -134,65 +149,55 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { }, [isPlaying, gameOver, startGame, flap]); return ( -
+
- {/* OKNO GRY */}
{ if (!isPlaying || gameOver) startGame(); else flap(); }} > - {/* Dekoracje tła */} -
-
- {/* Punkty */}
Score: {score}
-
Record: {highScore}
+
+ Record: {highScore} +
{/* KOTEK */}
- {/* DRAPAKI (Rury) */} + {/* DRAPAKI */} {displayPipes.map(p => ( - {/* Górny Drapak */}
-
-
-
- {/* Dolny Drapak */} + />
-
-
-
+ style={{ left: p.x, top: p.topHeight + GAP_SIZE, width: PIPE_WIDTH, height: CANVAS_HEIGHT - (p.topHeight + GAP_SIZE) }} + /> ))} - {/* Ekran Startowy */} + {/* Ekrany informacyjne */} {!isPlaying && !gameOver && (

Flappy Cat 🎈

-

Click or Space

@@ -200,12 +205,11 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => {
)} - {/* Ekran Koniec Gry */} {gameOver && (

Oops! 😿

Score: {score}

-
diff --git a/kittyurl-frontend/src/components/KittyGame.tsx b/kittyurl-frontend/src/components/KittyGame.tsx index 1998d44..cb3dcd3 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) --- +// --- ZAAWANSOWANY MODEL KOTA (Bez zmian w UI) --- interface DetailedKittyProps { isJumping: boolean; isNight: boolean; @@ -15,8 +15,6 @@ const DetailedKitty: React.FC = ({ isJumping, isNight, isGam return (
- - {/* OGON */}
= ({ isJumping, isNight, isGam animation: !isGameOver ? 'tail-wag 0.8s ease-in-out infinite' : 'none' }} /> - - {/* TUŁÓW */}
- {/* Paski dekoracyjne */}
- - {/* GŁOWA */}
- {/* Uszy */}
- - {/* Oczy */}
{isGameOver ? 'x' :
}
{isGameOver ? 'x' :
}
- - {/* Wąsy */}
- - {/* ŁAPKI (Animowane) */}
= ({ isJumping, isNight, isGam ); }; -// --- GŁÓWNY KOMPONENT GRY --- +// --- GŁÓWNY KOMPONENT GRY (Zoptymalizowany pod Delta Time) --- export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { const [isPlaying, setIsPlaying] = useState(false); const [gameOver, setGameOver] = useState(false); @@ -92,14 +78,19 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { const isNight = Math.floor(score / 10) % 2 === 1; + // Referencje do fizyki const kittyYRef = useRef(0); const obstacleXRef = useRef(600); const velocityRef = useRef(0); const scoreRef = useRef(0); const requestRef = useRef(0); + const lastTimeRef = useRef(0); - const GRAVITY = 0.6; - const JUMP_STRENGTH = -11; + // STAŁE FIZYKI (Wartości na sekundę) + const GRAVITY = 1800; // px/s^2 + const JUMP_FORCE = -550; // Moc skoku + const INITIAL_SPEED = 350; // px/s + const SPEED_INCREMENT = 15; // Przyspieszenie z każdym punktem const GROUND_Y = 0; const endGame = useCallback(() => { @@ -118,38 +109,51 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { setScore(0); scoreRef.current = 0; kittyYRef.current = 0; - obstacleXRef.current = 600; + obstacleXRef.current = 650; velocityRef.current = 0; + lastTimeRef.current = performance.now(); // Reset czasu startu setDisplayKittyY(0); - setDisplayObstacleX(600); + setDisplayObstacleX(650); }, []); const jump = useCallback(() => { if (kittyYRef.current <= GROUND_Y && !gameOver && isPlaying) { - velocityRef.current = JUMP_STRENGTH; + velocityRef.current = JUMP_FORCE; } }, [gameOver, isPlaying]); useEffect(() => { - const update = () => { + const update = (time: number) => { if (gameOver || !isPlaying) return; - velocityRef.current += GRAVITY; - kittyYRef.current -= velocityRef.current; + // Oblicz delta time (czas w sekundach) + const deltaTime = (time - lastTimeRef.current) / 1000; + lastTimeRef.current = time; + + // Ogranicz deltaTime, aby uniknąć gigantycznych skoków przy lagach + const dt = Math.min(deltaTime, 0.1); + + // Fizyka Skoku + velocityRef.current += GRAVITY * dt; + kittyYRef.current -= velocityRef.current * dt; + if (kittyYRef.current <= GROUND_Y) { kittyYRef.current = GROUND_Y; velocityRef.current = 0; } - obstacleXRef.current -= 5 + (scoreRef.current * 0.2); - if (obstacleXRef.current < -50) { - obstacleXRef.current = 650; + // Fizyka Przeszkody + const currentSpeed = INITIAL_SPEED + (scoreRef.current * SPEED_INCREMENT); + obstacleXRef.current -= currentSpeed * dt; + + if (obstacleXRef.current < -60) { + obstacleXRef.current = 700; scoreRef.current += 1; setScore(scoreRef.current); } - // DOSTOSOWANA KOLIZJA: Uwzględnia większy model kota - if (obstacleXRef.current < 110 && obstacleXRef.current > 30 && kittyYRef.current < 45) { + // Kolizja + if (obstacleXRef.current < 90 && obstacleXRef.current > 20 && kittyYRef.current < 45) { endGame(); return; } @@ -177,25 +181,25 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { }, [isPlaying, gameOver, startGame, jump]); return ( -
+
{ if (!isPlaying || gameOver) startGame(); else jump(); }} > - {/* Niebo */} + {/* Niebo i Słońce/Księżyc */}
- +
- {/* Wyniki */} + {/* Score UI */}
Score: {score}
@@ -208,10 +212,10 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { 2} isNight={isNight} isGameOver={gameOver} />
- {/* PRZESZKODA (Kłębek wełny) */} + {/* PRZESZKODA */}
@@ -219,7 +223,7 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { {/* Ziemia */}
+ ${isNight ? 'bg-slate-900 border-indigo-900 text-indigo-900' : 'bg-pink-50 border-pink-100 text-pink-200'}`}> {[...Array(9)].map((_, i) => 🐾)}
@@ -237,7 +241,7 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { {gameOver && (
+ ${isNight ? 'bg-slate-950/90' : 'bg-pink-50/90'}`}>

Meow! 😿

Score: {score}