From 4468148acbe8592899198240f956c36bc69c4ac4 Mon Sep 17 00:00:00 2001 From: Pc Date: Tue, 6 Jan 2026 16:31:45 +0100 Subject: [PATCH] feat: games are no working on moblie devices --- .../src/components/FlappyCat.tsx | 224 +++++++++++++----- .../src/components/KittyGame.tsx | 218 ++++++++++------- 2 files changed, 294 insertions(+), 148 deletions(-) diff --git a/kittyurl-frontend/src/components/FlappyCat.tsx b/kittyurl-frontend/src/components/FlappyCat.tsx index 914a746..6f837c1 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 --- +// --- MODEL KOTA (Wizualizacja) --- interface DetailedKittyProps { isGameOver: boolean; } const DetailedKitty: React.FC = ({ isGameOver }) => { const mainColor = isGameOver ? '#cbd5e1' : '#f472b6'; @@ -32,16 +32,20 @@ const DetailedKitty: React.FC = ({ isGameOver }) => { ); }; -// --- KONFIGURACJA NIEZALEŻNA OD FPS (Wartości na sekundę) --- -const GAP_SIZE = 180; +// --- KONFIGURACJA GRY --- +const GAP_SIZE = 170; // Przerwa między rurami const PIPE_WIDTH = 70; -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; +const PIPE_SPEED = 220; // px/s +const PIPE_SPAWN_RATE = 1.6; // co ile sekund rura +const GRAVITY = 1400; // siła grawitacji +const FLAP_STRENGTH = -420; // siła skoku + +// Logiczne wymiary gry (fizyka działa na tych wartościach, a ekran je tylko skaluje) +const GAME_HEIGHT = 450; +const GAME_WIDTH = 600; export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { + // --- STANY --- const [isPlaying, setIsPlaying] = useState(false); const [gameOver, setGameOver] = useState(false); const [score, setScore] = useState(0); @@ -50,10 +54,13 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { return saved ? parseInt(saved, 10) : 0; }); + // Stany do renderowania const [displayKittyY, setDisplayKittyY] = useState(150); const [displayPipes, setDisplayPipes] = useState<{ x: number; topHeight: number; id: number }[]>([]); const [rotation, setRotation] = useState(0); + const [scale, setScale] = useState(1); // Skala RWD + // --- REFS (FIZYKA) --- const kittyYRef = useRef(150); const velocityRef = useRef(0); const pipesRef = useRef<{ x: number; topHeight: number; id: number; passed?: boolean }[]>([]); @@ -62,6 +69,26 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { const lastTimeRef = useRef(0); const spawnTimerRef = useRef(0); + // --- RWD: Obliczanie skali --- + useEffect(() => { + const handleResize = () => { + const availableWidth = window.innerWidth - 32; // margines boczny + const availableHeight = window.innerHeight - 100; // miejsce na nagłówek + + const scaleX = availableWidth / GAME_WIDTH; + const scaleY = availableHeight / GAME_HEIGHT; + + // Dopasuj do ekranu, ale nie powiększaj powyżej 100% (żeby nie tracić jakości) + const newScale = Math.min(scaleX, scaleY, 1); + setScale(newScale); + }; + + window.addEventListener('resize', handleResize); + handleResize(); // Init + return () => window.removeEventListener('resize', handleResize); + }, []); + + // --- LOGIKA GRY --- const endGame = useCallback(() => { setGameOver(true); setIsPlaying(false); @@ -81,7 +108,7 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { velocityRef.current = 0; pipesRef.current = []; spawnTimerRef.current = 0; - lastTimeRef.current = performance.now(); // Inicjalizacja czasu + lastTimeRef.current = performance.now(); setDisplayKittyY(150); setDisplayPipes([]); setRotation(0); @@ -91,64 +118,87 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { if (isPlaying && !gameOver) velocityRef.current = FLAP_STRENGTH; }, [isPlaying, gameOver]); + // --- OBSŁUGA INPUTU (NAPRAWIONA) --- + const handleAction = useCallback((e?: React.SyntheticEvent) => { + // WAŻNE: Nie używamy e.preventDefault() tutaj, bo to powoduje błąd w Chrome. + // Zamiast tego CSS 'touch-action: none' blokuje scrollowanie. + if (e) { + e.stopPropagation(); + } + + if (!isPlaying || gameOver) { + startGame(); + } else { + flap(); + } + }, [isPlaying, gameOver, startGame, flap]); + + // --- PĘTLA GRY --- useEffect(() => { const update = (currentTime: number) => { if (gameOver || !isPlaying) return; - // --- DYNAMICZNY DELTA TIME --- - // Obliczamy ile sekund upłynęło od ostatniej klatki (np. 0.0069s dla 144Hz) + // Delta time w sekundach const dt = (currentTime - lastTimeRef.current) / 1000; lastTimeRef.current = currentTime; + const frameTime = Math.min(dt, 0.1); // Limit laga - // Zabezpieczenie przed ogromnym skokiem fizyki przy lagu - const frameTime = Math.min(dt, 0.1); - - // Fizyka grawitacji + // 1. Fizyka grawitacji velocityRef.current += GRAVITY * frameTime; kittyYRef.current += velocityRef.current * frameTime; - // Płynna rotacja zależna od prędkości pionowej - setRotation(Math.min(Math.max(velocityRef.current * 0.12, -20), 80)); + // Rotacja + setRotation(Math.min(Math.max(velocityRef.current * 0.12, -25), 90)); - // Kolizja z sufitem/ziemią - if (kittyYRef.current > CANVAS_HEIGHT - 40 || kittyYRef.current < -50) { + // Kolizja z sufitem/podłogą + if (kittyYRef.current > GAME_HEIGHT - 40 || kittyYRef.current < -50) { endGame(); return; } - // Spawn rur + // 2. Generowanie rur 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() }); + const minPipe = 60; + const maxPipe = GAME_HEIGHT - GAP_SIZE - minPipe; + const topHeight = Math.random() * (maxPipe - minPipe) + minPipe; + + pipesRef.current.push({ x: GAME_WIDTH + 50, topHeight, id: Date.now() }); spawnTimerRef.current = 0; } - // Ruch rur i kolizje + // 3. Ruch rur i kolizje const updatedPipes = []; for (const p of pipesRef.current) { p.x -= PIPE_SPEED * frameTime; - // Hitbox (z małym marginesem dla kota) - if (p.x < 100 && p.x + PIPE_WIDTH > 55) { + // Hitbox rury (marginesy dla łatwiejszej gry) + const hitXLeft = p.x + 5; + const hitXRight = p.x + PIPE_WIDTH - 5; + + // Sprawdzenie czy kot jest w poziomie rury + if (hitXLeft < 100 && hitXRight > 55) { + // Sprawdzenie czy kot uderzył w górę lub dół + // (dodajemy marginesy bezpieczeństwa) if (kittyYRef.current < p.topHeight || kittyYRef.current > p.topHeight + GAP_SIZE - 45) { endGame(); return; } } - // Punktacja + // Naliczanie punktów if (p.x < 50 && !p.passed) { p.passed = true; scoreRef.current += 1; setScore(scoreRef.current); } - if (p.x > -PIPE_WIDTH) updatedPipes.push(p); + // Usuwanie starych rur + if (p.x > -PIPE_WIDTH - 50) updatedPipes.push(p); } pipesRef.current = updatedPipes; - // Update stanów do renderowania + // 4. Renderowanie setDisplayKittyY(kittyYRef.current); setDisplayPipes([...pipesRef.current]); @@ -161,64 +211,108 @@ export const FlappyCat: React.FC<{ onBack: () => void }> = ({ onBack }) => { return () => cancelAnimationFrame(requestRef.current); }, [isPlaying, gameOver, endGame]); + // Obsługa Spacji useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.code === 'Space') { - e.preventDefault(); - if (!isPlaying || gameOver) startGame(); else flap(); + e.preventDefault(); // Tu można bezpiecznie użyć preventDefault dla klawiatury + handleAction(); } }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); - }, [isPlaying, gameOver, startGame, flap]); + }, [handleAction]); return ( -
- + {/* WRAPPER SKALUJĄCY GRĘ */}
{ if (!isPlaying || gameOver) startGame(); else flap(); }} + style={{ + width: GAME_WIDTH, + height: GAME_HEIGHT, + transform: `scale(${scale})`, + transformOrigin: 'top center', + touchAction: 'none' // KLUCZOWE DLA MOBILE: blokuje gesty przeglądarki + }} + className="relative shrink-0" > -
-
+
+ {/* TŁO / DEKORACJE */} +
+
-
-
Score: {score}
-
Record: {highScore}
-
+ {/* CHMURY (Ozdoba) */} +
+
-
- -
+ {/* HUD */} +
+
Score: {score}
+
Record: {highScore}
+
- {displayPipes.map(p => ( - -
-
- - ))} + {/* GRACZ (KOT) */} +
+ +
- {!isPlaying && !gameOver && ( -
-
-

Flappy Cat 🎈

- + {/* RURY */} + {displayPipes.map(p => ( + + {/* Górna rura */} +
+ + {/* Dolna rura */} +
+ + ))} + + {/* EKRAN STARTOWY */} + {!isPlaying && !gameOver && ( +
+
+

Flappy Cat 🎈

+

Tap to Jump

+
START
+
-
- )} + )} - {gameOver && ( -
-

Oops! 😿

-

Score: {score}

- -
- )} + {/* GAME OVER */} + {gameOver && ( +
+

Oops! 😿

+

Score: {score}

+ +
+ )} +
+
+ +
+ Tap anywhere to jump
-
); }; \ No newline at end of file diff --git a/kittyurl-frontend/src/components/KittyGame.tsx b/kittyurl-frontend/src/components/KittyGame.tsx index b96f215..7a88f77 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'; -// --- MODEL KOTA (Animacje CSS zostają, bo są czasowe, nie klatkowe) --- +// --- MODEL KOTA (Bez zmian) --- interface DetailedKittyProps { isJumping: boolean; isNight: boolean; @@ -49,8 +49,13 @@ const DetailedKitty: React.FC = ({ isJumping, isNight, isGam ); }; -// --- GŁÓWNY KOMPONENT Z DYNAMICZNYM DELTA TIME --- +// --- STAŁE WYMIARY LOGICZNE --- +const GAME_WIDTH = 650; +const GAME_HEIGHT = 340; + +// --- GŁÓWNY KOMPONENT --- export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { + // Stany gry const [isPlaying, setIsPlaying] = useState(false); const [gameOver, setGameOver] = useState(false); const [score, setScore] = useState(0); @@ -60,25 +65,46 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { }); const [displayKittyY, setDisplayKittyY] = useState(0); - const [displayObstacleX, setDisplayObstacleX] = useState(650); + const [displayObstacleX, setDisplayObstacleX] = useState(GAME_WIDTH); + + // RWD State: Skala + const [scale, setScale] = useState(1); const isNight = Math.floor(score / 10) % 2 === 1; // Referencje fizyczne const kittyYRef = useRef(0); - const obstacleXRef = useRef(650); + const obstacleXRef = useRef(GAME_WIDTH); const velocityRef = useRef(0); const scoreRef = useRef(0); const requestRef = useRef(0); const lastTimeRef = useRef(0); - // 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 + // STAŁE KONFIGURACYJNE + const GRAVITY = 1800; + const JUMP_FORCE = -550; + const INITIAL_SPEED = 380; + const SPEED_INCREMENT = 12; const GROUND_Y = 0; + // --- RWD LOGIC --- + useEffect(() => { + const handleResize = () => { + const availableWidth = window.innerWidth - 32; // marginesy boczne + const availableHeight = window.innerHeight - 100; // miejsce na nagłówek + + const scaleX = availableWidth / GAME_WIDTH; + const scaleY = availableHeight / GAME_HEIGHT; + + // Skalujemy w dół jeśli ekran jest mały, ale max 1 (nie powiększamy na dużych ekranach) + setScale(Math.min(scaleX, scaleY, 1)); + }; + + window.addEventListener('resize', handleResize); + handleResize(); + return () => window.removeEventListener('resize', handleResize); + }, []); + const endGame = useCallback(() => { setGameOver(true); setIsPlaying(false); @@ -95,12 +121,11 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { setScore(0); scoreRef.current = 0; kittyYRef.current = 0; - obstacleXRef.current = 700; + obstacleXRef.current = GAME_WIDTH + 50; velocityRef.current = 0; - // Kluczowe: inicjalizacja czasu startu lastTimeRef.current = performance.now(); setDisplayKittyY(0); - setDisplayObstacleX(700); + setDisplayObstacleX(GAME_WIDTH + 50); }, []); const jump = useCallback(() => { @@ -109,19 +134,23 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { } }, [gameOver, isPlaying]); + // --- OBSŁUGA WEJŚCIA (Touch & Mouse) --- + const handleAction = useCallback((e?: React.SyntheticEvent) => { + // Nie używamy e.preventDefault() tutaj, bo CSS touch-action załatwia sprawę + if (e) e.stopPropagation(); + + if (!isPlaying || gameOver) startGame(); else jump(); + }, [isPlaying, gameOver, startGame, jump]); + useEffect(() => { const update = (currentTime: number) => { if (gameOver || !isPlaying) return; - // --- 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 (jednostki * czas) + // Fizyka velocityRef.current += GRAVITY * frameTime; kittyYRef.current -= velocityRef.current * frameTime; @@ -130,24 +159,22 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { velocityRef.current = 0; } - // Ruch przeszkody (prędkość * czas) + // Przeszkoda const currentSpeed = INITIAL_SPEED + (scoreRef.current * SPEED_INCREMENT); obstacleXRef.current -= currentSpeed * frameTime; - // Reset przeszkody if (obstacleXRef.current < -80) { - obstacleXRef.current = 750; + obstacleXRef.current = GAME_WIDTH + 50; scoreRef.current += 1; setScore(scoreRef.current); } - // Kolizja (strefa trafienia dopasowana do czasu) + // Kolizja if (obstacleXRef.current < 100 && obstacleXRef.current > 20 && kittyYRef.current < 45) { endGame(); return; } - // Renderowanie wizualne setDisplayKittyY(kittyYRef.current); setDisplayObstacleX(obstacleXRef.current); @@ -164,86 +191,111 @@ export const KittyGame: React.FC<{ onBack: () => void }> = ({ onBack }) => { const handleKey = (e: KeyboardEvent) => { if (e.code === 'Space') { e.preventDefault(); - if (!isPlaying || gameOver) startGame(); else jump(); + handleAction(); } }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); - }, [isPlaying, gameOver, startGame, jump]); + }, [handleAction]); return ( -
- + {/* KONTENER SKALOWANIA */}
{ if (!isPlaying || gameOver) startGame(); else jump(); }} + style={{ + width: GAME_WIDTH, + height: GAME_HEIGHT, + transform: `scale(${scale})`, + transformOrigin: 'top center', + touchAction: 'none' // Zapobiega scrollowaniu na mobile + }} + className="relative shrink-0" > - {/* Niebo */} -
- -
-
- -
- - {/* UI Score */} -
-
Score: {score}
-
- High: {highScore} -
-
- - {/* KOTEK */} -
- 2} isNight={isNight} isGameOver={gameOver} /> -
- - {/* PRZESZKODA */}
-
-
-
+ {/* Niebo */} +
+ +
+
+ +
- {/* Ziemia */} -
- {[...Array(10)].map((_, i) => 🐾)} -
- - {/* Ekrany start/stop */} - {!isPlaying && !gameOver && ( -
-
- - -

Click or Space

+ {/* UI Score */} +
+
Score: {score}
+
+ High: {highScore}
- )} - {gameOver && ( -
-

Oh No! 😿

-
Score: {score}
- + {/* KOTEK */} +
+ 2} isNight={isNight} isGameOver={gameOver} />
- )} + + {/* PRZESZKODA */} +
+
+
+
+ + {/* Ziemia */} +
+ {[...Array(10)].map((_, i) => 🐾)} +
+ + {/* EKRAN STARTOWY */} + {!isPlaying && !gameOver && ( +
+
+ + +

Tap or Space

+
+
+ )} + + {/* GAME OVER */} + {gameOver && ( +
+

Oh No! 😿

+
Score: {score}
+ +
+ )} +
+
+ + {/* Informacja dla mobile */} +
+ Tap anywhere to jump
-
); }; \ No newline at end of file