This commit is contained in:
Unknown 2025-02-17 23:25:00 -07:00 committed by End
parent 1f54c596b7
commit 1aed828b9e
No known key found for this signature in database
10 changed files with 720 additions and 40 deletions

View file

@ -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;

View file

@ -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;

View 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>
);
};

View 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>
);
};

View file

@ -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>
);
};
};

View file

@ -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);
};
}, []);
};
};

View file

@ -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 = () => {
}
};
}, []);
};
};

View 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;

View file

@ -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;
}
}

View file

@ -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: [],
}
}