mirror of
https://github.com/System-End/My-website.git
synced 2026-04-19 22:05:07 +00:00
.
This commit is contained in:
parent
1f54c596b7
commit
1aed828b9e
10 changed files with 720 additions and 40 deletions
39
src/App.tsx
39
src/App.tsx
|
|
@ -1,15 +1,44 @@
|
|||
// src/App.tsx
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import AboutPage from "@/pages/AboutPage";
|
||||
import ProjectsPage from "@/pages/ProjectsPage";
|
||||
import APCSPPage from "@/pages/APCSPPage";
|
||||
import ParallaxPage from "@/pages/ParallaxPage";
|
||||
import VNCViewer from '@/components/VNCViewer';
|
||||
import FoxGame from "@/games/fox-adventure/components/FoxGame";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const App = () => {
|
||||
const [isGameActive, setIsGameActive] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const konamiCode = [
|
||||
'ArrowUp', 'ArrowUp',
|
||||
'ArrowDown', 'ArrowDown',
|
||||
'ArrowLeft', 'ArrowRight',
|
||||
'ArrowLeft', 'ArrowRight',
|
||||
'b', 'a'
|
||||
];
|
||||
let index = 0;
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === konamiCode[index]) {
|
||||
index++;
|
||||
if (index === konamiCode.length) {
|
||||
setIsGameActive(true);
|
||||
}
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
return () => window.removeEventListener('keydown', handleKeydown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="min-h-screen bg-background-primary">
|
||||
<div className={`min-h-screen bg-background-primary ${isGameActive ? 'game-active' : ''}`}>
|
||||
{/* Background Logo */}
|
||||
<div className="fixed inset-0 z-behind pointer-events-none">
|
||||
<div className="absolute inset-0">
|
||||
|
|
@ -30,7 +59,6 @@ const App = () => {
|
|||
<Route path="/projects" element={<ProjectsPage />} />
|
||||
<Route path="/apcsp" element={<APCSPPage />} />
|
||||
{/* <Route path="/parallax" element={<ParallaxPage />} /> */}
|
||||
<Route path="/novnc" element={<VNCViewer />} />
|
||||
<Route path="*" element={
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] space-y-4">
|
||||
<h1 className="text-4xl font-bold text-glow">404: Page Not Found</h1>
|
||||
|
|
@ -40,9 +68,12 @@ const App = () => {
|
|||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Fox Game Overlay */}
|
||||
{isGameActive && <FoxGame />}
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
|
@ -3,20 +3,18 @@ import useGameStore from '../state/gameStore';
|
|||
import { useGameLoop } from '../hooks/useGameLoop';
|
||||
import { useGameControls } from '../hooks/useGameControls';
|
||||
import { Player } from './Player';
|
||||
import { Enemy } from './Enemy';
|
||||
import { Collectible } from './Collectible';
|
||||
import { PowerUp } from './PowerUp';
|
||||
import { GameHUD } from './GameHUD';
|
||||
import { GameOverlay } from './GameOverlay';
|
||||
|
||||
const FoxGame: React.FC = () => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const gameStore = useGameStore();
|
||||
|
||||
// Initialize game systems
|
||||
useGameLoop();
|
||||
useGameControls();
|
||||
|
||||
// Konami code for game activation
|
||||
// Konami code activation
|
||||
useEffect(() => {
|
||||
const konamiCode = [
|
||||
'ArrowUp', 'ArrowUp',
|
||||
|
|
@ -32,7 +30,7 @@ const FoxGame: React.FC = () => {
|
|||
index++;
|
||||
if (index === konamiCode.length) {
|
||||
setIsActive(true);
|
||||
useGameStore.getState().startNewGame();
|
||||
gameStore.startNewGame();
|
||||
}
|
||||
} else {
|
||||
index = 0;
|
||||
|
|
@ -46,13 +44,52 @@ const FoxGame: React.FC = () => {
|
|||
if (!isActive) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gradient-game z-50">
|
||||
<div className="fixed inset-0 bg-gradient-to-b from-background-primary to-background-secondary z-50">
|
||||
<div className="relative w-full h-full overflow-hidden game-viewport">
|
||||
{/* Game world */}
|
||||
<div className="absolute inset-0">
|
||||
<Player />
|
||||
{/* Other game elements render here */}
|
||||
|
||||
{/* Render collectibles */}
|
||||
{gameStore.collectibles.map(collectible => (
|
||||
<div
|
||||
key={collectible.id}
|
||||
className={`absolute w-4 h-4 transform -translate-x-1/2 -translate-y-1/2 rounded-full animate-pulse ${
|
||||
collectible.type === 'GEM' ? 'bg-purple-500' : 'bg-yellow-400'
|
||||
}`}
|
||||
style={{
|
||||
left: `${collectible.position.x}%`,
|
||||
top: `${collectible.position.y}%`
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Render enemies */}
|
||||
{gameStore.enemies.map(enemy => (
|
||||
<div
|
||||
key={enemy.id}
|
||||
className="absolute w-6 h-6 bg-red-500 rounded-full transform -translate-x-1/2 -translate-y-1/2"
|
||||
style={{
|
||||
left: `${enemy.position.x}%`,
|
||||
top: `${enemy.position.y}%`
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Render power-ups */}
|
||||
{gameStore.powerUps.map(powerUp => (
|
||||
<div
|
||||
key={powerUp.id}
|
||||
className="absolute w-8 h-8 bg-accent-neon rounded-full transform -translate-x-1/2 -translate-y-1/2 animate-float"
|
||||
style={{
|
||||
left: `${powerUp.position.x}%`,
|
||||
top: `${powerUp.position.y}%`
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* HUD and Overlay */}
|
||||
<GameHUD />
|
||||
<GameOverlay />
|
||||
</div>
|
||||
|
|
@ -60,4 +97,4 @@ const FoxGame: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default FoxGame;
|
||||
export default FoxGame;
|
||||
72
src/games/fox-adventure/components/GameHUD.tsx
Normal file
72
src/games/fox-adventure/components/GameHUD.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import React from 'react';
|
||||
import useGameStore from '../state/gameStore';
|
||||
import { Heart, Star, Timer, Trophy } from 'lucide-react';
|
||||
|
||||
export const GameHUD: React.FC = () => {
|
||||
const { player, score, level, timePlayed } = useGameStore();
|
||||
|
||||
const formatTime = (ms: number) => {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
return `${minutes}:${(seconds % 60).toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 left-0 w-full p-4 flex justify-between items-start pointer-events-none">
|
||||
{/* Left section - Health and PowerUps */}
|
||||
<div className="space-y-4">
|
||||
{/* Health bar */}
|
||||
<div className="flex items-center gap-2 bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm">
|
||||
<Heart
|
||||
className={`w-6 h-6 ${
|
||||
player.health > 20 ? 'text-red-500' : 'text-red-500 animate-pulse'
|
||||
}`}
|
||||
/>
|
||||
<div className="w-32 h-3 bg-background-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-red-500 transition-all duration-300"
|
||||
style={{ width: `${player.health}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active power-ups */}
|
||||
<div className="flex gap-2">
|
||||
{player.powerUps.map((powerUp) => (
|
||||
<div
|
||||
key={powerUp.id}
|
||||
className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm"
|
||||
>
|
||||
<div className="w-8 h-8 relative">
|
||||
<div className="absolute inset-0 bg-accent-primary/20 rounded-full animate-ping" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center - Score and Level */}
|
||||
<div className="absolute left-1/2 top-0 -translate-x-1/2 text-center space-y-2">
|
||||
<div className="bg-background-primary/50 px-4 py-2 rounded-lg backdrop-blur-sm">
|
||||
<div className="text-2xl font-bold text-accent-neon">Level {level}</div>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Star className="w-5 h-5 text-yellow-400" />
|
||||
<span className="text-xl">{score.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right section - Time and High Score */}
|
||||
<div className="space-y-4 text-right">
|
||||
<div className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm flex items-center gap-2">
|
||||
<Timer className="w-5 h-5 text-accent-primary" />
|
||||
<span>{formatTime(timePlayed)}</span>
|
||||
</div>
|
||||
<div className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm flex items-center gap-2">
|
||||
<Trophy className="w-5 h-5 text-yellow-400" />
|
||||
<span>Best: {Math.max(...useGameStore.getState().highScores).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
79
src/games/fox-adventure/components/GameOverlay.tsx
Normal file
79
src/games/fox-adventure/components/GameOverlay.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// src/games/fox-adventure/components/GameOverlay.tsx
|
||||
import React from 'react';
|
||||
import useGameStore from '../state/gameStore';
|
||||
import { Play, Pause, RotateCcw } from 'lucide-react';
|
||||
|
||||
export const GameOverlay: React.FC = () => {
|
||||
const { gameStatus, score, startNewGame, resumeGame } = useGameStore();
|
||||
|
||||
if (gameStatus === 'PLAYING') return null;
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 bg-background-primary/80 backdrop-blur-md flex items-center justify-center">
|
||||
<div className="max-w-md w-full p-8 bg-gradient-card rounded-xl border border-accent-primary/20">
|
||||
{gameStatus === 'MENU' && (
|
||||
<div className="text-center space-y-6">
|
||||
<h1 className="text-4xl font-bold text-glow">Fox Adventure</h1>
|
||||
<p className="text-lg text-text-primary/80">Help the fox collect treasures while avoiding enemies!</p>
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Controls:</h2>
|
||||
<ul className="text-left space-y-2">
|
||||
<li>Move: Arrow keys or WASD</li>
|
||||
<li>Pause: ESC</li>
|
||||
<li>Collect items to score points</li>
|
||||
<li>Find power-ups to gain advantages</li>
|
||||
<li>Avoid enemies or find shields</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
onClick={startNewGame}
|
||||
className="px-8 py-4 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center justify-center gap-2 mx-auto group"
|
||||
>
|
||||
<Play className="w-5 h-5 group-hover:animate-pulse" />
|
||||
Start Game
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{gameStatus === 'PAUSED' && (
|
||||
<div className="text-center space-y-6">
|
||||
<h2 className="text-3xl font-bold text-glow">Game Paused</h2>
|
||||
<div className="flex justify-center gap-4">
|
||||
<button
|
||||
onClick={resumeGame}
|
||||
className="px-6 py-3 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center gap-2 group"
|
||||
>
|
||||
<Play className="w-5 h-5 group-hover:animate-pulse" />
|
||||
Resume
|
||||
</button>
|
||||
<button
|
||||
onClick={startNewGame}
|
||||
className="px-6 py-3 bg-background-secondary hover:bg-background-secondary/80 transition-all rounded-lg flex items-center gap-2"
|
||||
>
|
||||
<RotateCcw className="w-5 h-5" />
|
||||
Restart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{gameStatus === 'GAME_OVER' && (
|
||||
<div className="text-center space-y-6">
|
||||
<h2 className="text-3xl font-bold text-red-500">Game Over</h2>
|
||||
<div className="space-y-2">
|
||||
<p className="text-xl">Final Score:</p>
|
||||
<p className="text-4xl font-bold text-accent-neon">{score.toLocaleString()}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={startNewGame}
|
||||
className="px-8 py-4 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center justify-center gap-2 mx-auto group"
|
||||
>
|
||||
<RotateCcw className="w-5 h-5 group-hover:animate-pulse" />
|
||||
Play Again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -7,7 +7,7 @@ export const Player: React.FC = () => {
|
|||
return (
|
||||
<div
|
||||
className={`absolute transition-all duration-100 ${
|
||||
player.isInvincible ? 'animate-pulse-fast' : ''
|
||||
player.isInvincible ? 'animate-pulse' : ''
|
||||
}`}
|
||||
style={{
|
||||
left: `${player.position.x}%`,
|
||||
|
|
@ -15,12 +15,50 @@ export const Player: React.FC = () => {
|
|||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
>
|
||||
{/* Fox body */}
|
||||
<div className="relative w-16 h-16">
|
||||
{/* Fox body */}
|
||||
{/* Main body */}
|
||||
<div className="absolute inset-0 bg-fox-orange rounded-full">
|
||||
{/* Face details here */}
|
||||
{/* Face */}
|
||||
<div className="absolute inset-0">
|
||||
{/* Eyes */}
|
||||
<div className="absolute top-1/3 left-1/4 w-2 h-2 bg-dark-accent rounded-full" />
|
||||
<div className="absolute top-1/3 right-1/4 w-2 h-2 bg-dark-accent rounded-full" />
|
||||
|
||||
{/* Nose */}
|
||||
<div className="absolute top-1/2 left-1/2 w-2 h-2 bg-dark-accent rounded-full transform -translate-x-1/2" />
|
||||
</div>
|
||||
|
||||
{/* Ears */}
|
||||
<div className="absolute -top-4 -left-2 w-4 h-4 bg-fox-orange transform rotate-45" />
|
||||
<div className="absolute -top-4 -right-2 w-4 h-4 bg-fox-orange transform -rotate-45" />
|
||||
|
||||
{/* Tail */}
|
||||
<div className="absolute -bottom-4 left-1/2 w-3 h-6 bg-fox-orange rounded-full transform -translate-x-1/2 origin-top animate-wag" />
|
||||
</div>
|
||||
|
||||
{/* Power-up effects */}
|
||||
{player.isInvincible && (
|
||||
<div className="absolute inset-0 rounded-full border-4 border-accent-neon animate-pulse" />
|
||||
)}
|
||||
|
||||
{/* Key indicator */}
|
||||
{player.hasKey && (
|
||||
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2">
|
||||
<div className="w-4 h-4 bg-accent-primary rounded-full animate-float" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Health indicator */}
|
||||
<div className="absolute -bottom-6 left-1/2 transform -translate-x-1/2 w-12">
|
||||
<div className="w-full h-1 bg-background-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-red-500 transition-all duration-300"
|
||||
style={{ width: `${player.health}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
@ -34,6 +34,11 @@ export const useGameControls = () => {
|
|||
if (keys.has('ArrowRight') || keys.has('d')) direction.x += 1;
|
||||
|
||||
if (direction.x !== 0 || direction.y !== 0) {
|
||||
// Normalize diagonal movement
|
||||
const magnitude = Math.sqrt(direction.x * direction.x + direction.y * direction.y);
|
||||
direction.x /= magnitude;
|
||||
direction.y /= magnitude;
|
||||
|
||||
gameStore.movePlayer(direction);
|
||||
}
|
||||
};
|
||||
|
|
@ -54,4 +59,4 @@ export const useGameControls = () => {
|
|||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
};
|
||||
|
|
@ -4,18 +4,63 @@ import useGameStore from '../state/gameStore';
|
|||
export const useGameLoop = () => {
|
||||
const frameRef = useRef<number>();
|
||||
const lastUpdateRef = useRef<number>(0);
|
||||
const lastSpawnRef = useRef<number>(0);
|
||||
const gameStore = useGameStore();
|
||||
|
||||
useEffect(() => {
|
||||
const spawnCollectible = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastSpawnRef.current < 2000) return; // Spawn every 2 seconds
|
||||
|
||||
lastSpawnRef.current = now;
|
||||
const collectible: any = {
|
||||
id: `collectible-${now}`,
|
||||
type: Math.random() > 0.8 ? 'GEM' : 'STAR',
|
||||
value: Math.random() > 0.8 ? 10 : 5,
|
||||
position: {
|
||||
x: Math.random() * 90 + 5,
|
||||
y: Math.random() * 90 + 5
|
||||
}
|
||||
};
|
||||
|
||||
gameStore.collectibles.push(collectible);
|
||||
};
|
||||
|
||||
const gameLoop = (timestamp: number) => {
|
||||
if (!lastUpdateRef.current) lastUpdateRef.current = timestamp;
|
||||
const deltaTime = timestamp - lastUpdateRef.current;
|
||||
|
||||
if (gameStore.gameStatus === 'PLAYING') {
|
||||
// Update game state
|
||||
// Update entities
|
||||
gameStore.updateEnemies();
|
||||
checkCollisions();
|
||||
updatePowerUps(deltaTime);
|
||||
|
||||
// Spawn collectibles
|
||||
spawnCollectible();
|
||||
|
||||
// Check collisions
|
||||
const { player, enemies, collectibles } = gameStore;
|
||||
|
||||
// Enemy collisions
|
||||
enemies.forEach(enemy => {
|
||||
const dx = player.position.x - enemy.position.x;
|
||||
const dy = player.position.y - enemy.position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 5 && !player.isInvincible) {
|
||||
gameStore.takeDamage(20);
|
||||
}
|
||||
});
|
||||
|
||||
// Collectible collisions
|
||||
collectibles.forEach(collectible => {
|
||||
const dx = player.position.x - collectible.position.x;
|
||||
const dy = player.position.y - collectible.position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 5) {
|
||||
gameStore.collectItem(collectible.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lastUpdateRef.current = timestamp;
|
||||
|
|
@ -30,4 +75,4 @@ export const useGameLoop = () => {
|
|||
}
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
};
|
||||
187
src/games/fox-adventure/state/gameStore.ts
Normal file
187
src/games/fox-adventure/state/gameStore.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import { create } from 'zustand';
|
||||
|
||||
interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface PowerUp {
|
||||
id: string;
|
||||
type: 'SPEED' | 'SHIELD' | 'MAGNET';
|
||||
duration: number;
|
||||
position: Position;
|
||||
}
|
||||
|
||||
interface Collectible {
|
||||
id: string;
|
||||
type: 'STAR' | 'GEM' | 'KEY';
|
||||
value: number;
|
||||
position: Position;
|
||||
}
|
||||
|
||||
interface Enemy {
|
||||
id: string;
|
||||
type: 'WOLF' | 'OWL' | 'HUNTER';
|
||||
position: Position;
|
||||
direction: Position;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
interface PlayerState {
|
||||
position: Position;
|
||||
health: number;
|
||||
speed: number;
|
||||
powerUps: PowerUp[];
|
||||
isInvincible: boolean;
|
||||
hasKey: boolean;
|
||||
}
|
||||
|
||||
interface GameState {
|
||||
player: PlayerState;
|
||||
enemies: Enemy[];
|
||||
collectibles: Collectible[];
|
||||
powerUps: PowerUp[];
|
||||
score: number;
|
||||
level: number;
|
||||
gameStatus: 'MENU' | 'PLAYING' | 'PAUSED' | 'GAME_OVER';
|
||||
highScores: number[];
|
||||
timePlayed: number;
|
||||
|
||||
// Actions
|
||||
movePlayer: (direction: Position) => void;
|
||||
updateEnemies: () => void;
|
||||
collectItem: (itemId: string) => void;
|
||||
takeDamage: (amount: number) => void;
|
||||
activatePowerUp: (powerUpId: string) => void;
|
||||
startNewGame: () => void;
|
||||
pauseGame: () => void;
|
||||
resumeGame: () => void;
|
||||
}
|
||||
|
||||
const useGameStore = create<GameState>((set, get) => ({
|
||||
player: {
|
||||
position: { x: 50, y: 50 },
|
||||
health: 100,
|
||||
speed: 5,
|
||||
powerUps: [],
|
||||
isInvincible: false,
|
||||
hasKey: false
|
||||
},
|
||||
enemies: [],
|
||||
collectibles: [],
|
||||
powerUps: [],
|
||||
score: 0,
|
||||
level: 1,
|
||||
gameStatus: 'MENU',
|
||||
highScores: [],
|
||||
timePlayed: 0,
|
||||
|
||||
movePlayer: (direction) => {
|
||||
const { player } = get();
|
||||
set({
|
||||
player: {
|
||||
...player,
|
||||
position: {
|
||||
x: Math.max(0, Math.min(100, player.position.x + direction.x * player.speed)),
|
||||
y: Math.max(0, Math.min(100, player.position.y + direction.y * player.speed))
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateEnemies: () => {
|
||||
const { enemies, player } = get();
|
||||
const updatedEnemies = enemies.map(enemy => {
|
||||
const dx = player.position.x - enemy.position.x;
|
||||
const dy = player.position.y - enemy.position.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
return {
|
||||
...enemy,
|
||||
direction: {
|
||||
x: dx / distance,
|
||||
y: dy / distance
|
||||
},
|
||||
position: {
|
||||
x: enemy.position.x + (enemy.direction.x * enemy.speed),
|
||||
y: enemy.position.y + (enemy.direction.y * enemy.speed)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
set({ enemies: updatedEnemies });
|
||||
},
|
||||
|
||||
collectItem: (itemId) => {
|
||||
const { collectibles, score, player } = get();
|
||||
const item = collectibles.find(c => c.id === itemId);
|
||||
if (!item) return;
|
||||
|
||||
set({
|
||||
collectibles: collectibles.filter(c => c.id !== itemId),
|
||||
score: score + item.value
|
||||
});
|
||||
},
|
||||
|
||||
takeDamage: (amount) => {
|
||||
const { player, gameStatus } = get();
|
||||
if (player.isInvincible) return;
|
||||
|
||||
const newHealth = player.health - amount;
|
||||
set({
|
||||
player: {
|
||||
...player,
|
||||
health: newHealth
|
||||
},
|
||||
gameStatus: newHealth <= 0 ? 'GAME_OVER' : gameStatus
|
||||
});
|
||||
},
|
||||
|
||||
activatePowerUp: (powerUpId) => {
|
||||
const { player, powerUps } = get();
|
||||
const powerUp = powerUps.find(p => p.id === powerUpId);
|
||||
if (!powerUp) return;
|
||||
|
||||
set({
|
||||
player: {
|
||||
...player,
|
||||
powerUps: [...player.powerUps, powerUp]
|
||||
},
|
||||
powerUps: powerUps.filter(p => p.id !== powerUpId)
|
||||
});
|
||||
|
||||
// Reset power-up after duration
|
||||
setTimeout(() => {
|
||||
const currentPlayer = get().player;
|
||||
set({
|
||||
player: {
|
||||
...currentPlayer,
|
||||
powerUps: currentPlayer.powerUps.filter(p => p.id !== powerUp.id)
|
||||
}
|
||||
});
|
||||
}, powerUp.duration);
|
||||
},
|
||||
|
||||
startNewGame: () => set({
|
||||
player: {
|
||||
position: { x: 50, y: 50 },
|
||||
health: 100,
|
||||
speed: 5,
|
||||
powerUps: [],
|
||||
isInvincible: false,
|
||||
hasKey: false
|
||||
},
|
||||
enemies: [],
|
||||
collectibles: [],
|
||||
powerUps: [],
|
||||
score: 0,
|
||||
level: 1,
|
||||
gameStatus: 'PLAYING',
|
||||
timePlayed: 0
|
||||
}),
|
||||
|
||||
pauseGame: () => set({ gameStatus: 'PAUSED' }),
|
||||
resumeGame: () => set({ gameStatus: 'PLAYING' })
|
||||
}));
|
||||
|
||||
export default useGameStore;
|
||||
|
|
@ -1,31 +1,205 @@
|
|||
@import 'base.css';
|
||||
@import 'animations.css';
|
||||
@import 'utilities.css';
|
||||
@import 'cursor.css';
|
||||
/* src/styles/game.css */
|
||||
|
||||
@import './game.css';
|
||||
|
||||
/* Game Styles */
|
||||
/* Base game styles */
|
||||
.game-viewport {
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--background-primary) 0%,
|
||||
var(--background-secondary) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.game-active * {
|
||||
cursor: none;
|
||||
/* Game UI elements */
|
||||
.game-hud {
|
||||
pointer-events: none;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Game Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
.game-card {
|
||||
background: rgba(47, 28, 84, 0.3);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(157, 78, 221, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
.game-card:hover {
|
||||
border-color: rgba(178, 73, 248, 0.4);
|
||||
box-shadow: 0 0 20px rgba(178, 73, 248, 0.2);
|
||||
}
|
||||
|
||||
/* Game-specific effects */
|
||||
.fox-shadow {
|
||||
filter: drop-shadow(0 0 8px var(--fox-pink-glow));
|
||||
/* Player animations */
|
||||
.player-idle {
|
||||
animation: playerIdle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.player-move {
|
||||
animation: playerMove 0.3s linear infinite;
|
||||
}
|
||||
|
||||
.player-hit {
|
||||
animation: playerHit 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes playerIdle {
|
||||
0%, 100% { transform: translate(-50%, -50%); }
|
||||
50% { transform: translate(-50%, calc(-50% - 4px)); }
|
||||
}
|
||||
|
||||
@keyframes playerMove {
|
||||
0% { transform: rotate(-2deg); }
|
||||
50% { transform: rotate(2deg); }
|
||||
100% { transform: rotate(-2deg); }
|
||||
}
|
||||
|
||||
@keyframes playerHit {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Enemy animations */
|
||||
.enemy-patrol {
|
||||
animation: enemyPatrol 3s linear infinite;
|
||||
}
|
||||
|
||||
.enemy-chase {
|
||||
animation: enemyChase 0.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes enemyPatrol {
|
||||
0% { transform: translateX(0); }
|
||||
50% { transform: translateX(50px); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes enemyChase {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* Collectible animations */
|
||||
.collectible {
|
||||
animation: collectibleFloat 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.collectible-gem {
|
||||
animation: collectibleGem 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes collectibleFloat {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
@keyframes collectibleGem {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Power-up effects */
|
||||
.powerup-active {
|
||||
animation: powerupPulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.powerup-shield {
|
||||
animation: shieldRotate 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes powerupPulse {
|
||||
0%, 100% { filter: brightness(1); }
|
||||
50% { filter: brightness(1.5); }
|
||||
}
|
||||
|
||||
@keyframes shieldRotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Game effects */
|
||||
.particle {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
animation: particleFade 1s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes particleFade {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
100% { transform: scale(0); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Game UI animations */
|
||||
.score-popup {
|
||||
animation: scorePopup 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.health-change {
|
||||
animation: healthChange 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes scorePopup {
|
||||
0% { transform: scale(0) translateY(0); opacity: 1; }
|
||||
50% { transform: scale(1.2) translateY(-20px); opacity: 1; }
|
||||
100% { transform: scale(1) translateY(-40px); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes healthChange {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.2); }
|
||||
}
|
||||
|
||||
/* Menu transitions */
|
||||
.menu-enter {
|
||||
animation: menuEnter 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.menu-exit {
|
||||
animation: menuExit 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes menuEnter {
|
||||
from { opacity: 0; transform: scale(0.9); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes menuExit {
|
||||
from { opacity: 1; transform: scale(1); }
|
||||
to { opacity: 0; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* Custom cursor */
|
||||
.game-cursor {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
mix-blend-mode: difference;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.game-hud {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.game-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.player-idle,
|
||||
.player-move,
|
||||
.enemy-patrol,
|
||||
.collectible,
|
||||
.powerup-active {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,12 @@ export default {
|
|||
'float': 'float 3s ease-in-out infinite',
|
||||
'spin-slow': 'spin 3s linear infinite',
|
||||
'wag': 'wag 1s ease-in-out infinite',
|
||||
'score-popup': 'scorePopup 0.5s ease-out forwards',
|
||||
'particle-fade': 'particleFade 1s ease-out forwards',
|
||||
'menu-enter': 'menuEnter 0.3s ease-out forwards',
|
||||
'menu-exit': 'menuExit 0.3s ease-in forwards',
|
||||
'player-idle': 'playerIdle 2s ease-in-out infinite',
|
||||
'player-hit': 'playerHit 0.5s ease-in-out',
|
||||
},
|
||||
keyframes: {
|
||||
glow: {
|
||||
|
|
@ -40,7 +46,13 @@ export default {
|
|||
'50%': { transform: 'rotate(10deg)' },
|
||||
},
|
||||
},
|
||||
backgroundImage: {
|
||||
'gradient-game': 'linear-gradient(135deg, var(--background-primary) 0%, var(--background-secondary) 100%)',
|
||||
},
|
||||
backdropFilter: {
|
||||
'game': 'blur(8px)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue