mirror of
https://github.com/System-End/My-website.git
synced 2026-04-19 22:05:07 +00:00
feat: add fox adventure game
This commit is contained in:
parent
80a86c36e1
commit
40e518b818
16 changed files with 479 additions and 95 deletions
36
package-lock.json
generated
36
package-lock.json
generated
|
|
@ -15,7 +15,8 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"tailwind-merge": "^2.2.1"
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"zustand": "5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "4.20250129.0",
|
||||
|
|
@ -25,8 +26,8 @@
|
|||
"@testing-library/react": "^14.2.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "20.17.16",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
|
|
@ -10701,6 +10702,35 @@
|
|||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
|
||||
"integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.22.0",
|
||||
"tailwind-merge": "^2.2.1"
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"zustand": "5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "4.20250129.0",
|
||||
|
|
@ -39,8 +40,8 @@
|
|||
"@testing-library/react": "^14.2.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "20.17.16",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
|
|
@ -28,7 +29,8 @@ const App = () => {
|
|||
<Route path="/" element={<AboutPage />} />
|
||||
<Route path="/projects" element={<ProjectsPage />} />
|
||||
<Route path="/apcsp" element={<APCSPPage />} />
|
||||
<Route path="/parallax" element={<ParallaxPage />} />
|
||||
{/* <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>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,14 @@ const Navbar = () => {
|
|||
<span>APCSP</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/novnc"
|
||||
className={`nav-link ${location.pathname === '/novnc' ? 'active' : ''}`}
|
||||
>
|
||||
<Code size={20} />
|
||||
<span>noVNC</span>
|
||||
</Link>
|
||||
|
||||
<a
|
||||
href="https://twitch.tv/EndofTimee"
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -1,80 +1,75 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Monitor } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
import { Monitor, Power, Lock } from 'lucide-react';
|
||||
|
||||
const VNCViewer = () => {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
// Matrix rain effect characters
|
||||
const chars = '01';
|
||||
const [drops, setDrops] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize matrix rain
|
||||
const canvas = document.getElementById('matrix-bg');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
|
||||
const fontSize = 10;
|
||||
const columns = canvas.width / fontSize;
|
||||
|
||||
const initialDrops = Array(Math.floor(columns)).fill(1);
|
||||
setDrops(initialDrops);
|
||||
|
||||
const draw = () => {
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.fillStyle = '#0F0';
|
||||
ctx.font = fontSize + 'px monospace';
|
||||
|
||||
for (let i = 0; i < drops.length; i++) {
|
||||
const text = chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
ctx.fillText(text, i * fontSize, drops[i] * fontSize);
|
||||
|
||||
if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
|
||||
drops[i] = 0;
|
||||
}
|
||||
drops[i]++;
|
||||
const toggleFullscreen = () => {
|
||||
const iframe = document.getElementById('vnc-iframe');
|
||||
if (iframe) {
|
||||
if (!document.fullscreenElement) {
|
||||
iframe.requestFullscreen();
|
||||
setIsFullscreen(true);
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
setIsFullscreen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(draw, 33);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full flex items-center justify-center p-4 relative overflow-hidden">
|
||||
<canvas id="matrix-bg" className="absolute inset-0 z-0" />
|
||||
|
||||
<div className="w-full max-w-2xl bg-background-primary/80 backdrop-blur-sm rounded-xl shadow-xl border border-accent-primary/20 p-6 space-y-4 transition-all duration-300 hover:border-accent-neon/40 hover:shadow-accent-primary/20 z-10">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Monitor className="text-accent-primary" size={24} />
|
||||
<h2 className="text-xl font-semibold text-text-primary">Raspberry Pi VNC Viewer</h2>
|
||||
<div className="min-h-screen w-full flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-6xl bg-background-primary/80 backdrop-blur-sm rounded-xl shadow-xl border border-accent-primary/20 overflow-hidden transition-all duration-300 hover:border-accent-neon/40 hover:shadow-accent-primary/20">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-accent-primary/20">
|
||||
<div className="flex items-center gap-3">
|
||||
<Monitor className="text-accent-primary" size={24} />
|
||||
<h2 className="text-xl font-semibold text-text-primary">Raspberry Pi Remote Access</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
className="p-2 rounded-lg hover:bg-accent-primary/20 transition-colors"
|
||||
title="Toggle Fullscreen"
|
||||
>
|
||||
<Monitor size={20} className="text-accent-primary" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsConnected(!isConnected)}
|
||||
className="p-2 rounded-lg hover:bg-accent-primary/20 transition-colors"
|
||||
title="Toggle Connection"
|
||||
>
|
||||
<Power
|
||||
size={20}
|
||||
className={isConnected ? "text-green-500" : "text-accent-primary"}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="aspect-video w-full bg-black/50 rounded-lg overflow-hidden">
|
||||
{/* VNC Viewer */}
|
||||
<div className="aspect-video w-full bg-black/50 relative">
|
||||
{isConnected ? (
|
||||
<iframe
|
||||
src="http://your-raspberry-pi-ip:6080/vnc.html?host=your-raspberry-pi-ip&port=5900"
|
||||
<iframe
|
||||
id="vnc-iframe"
|
||||
src="http://68.104.222.58:6080/vnc.html?host=68.104.222.58&port=5901&autoconnect=true&resize=scale"
|
||||
className="w-full h-full border-0"
|
||||
title="VNC Viewer"
|
||||
allow="fullscreen"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-text-primary/60">
|
||||
Not connected
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-text-primary/60">
|
||||
<Lock size={48} className="mb-4 text-accent-primary/40" />
|
||||
<p>Click the power button to connect</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() => setIsConnected(!isConnected)}
|
||||
className="px-4 py-2 bg-accent-primary/20 rounded-lg text-text-primary hover:bg-accent-primary/30 transition-colors"
|
||||
>
|
||||
{isConnected ? 'Disconnect' : 'Connect'}
|
||||
</button>
|
||||
{/* Status Bar */}
|
||||
<div className="p-2 border-t border-accent-primary/20 flex justify-between items-center text-sm text-text-primary/60">
|
||||
<span>Status: {isConnected ? 'Connected' : 'Disconnected'}</span>
|
||||
<span>Press ESC to exit fullscreen</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
63
src/games/fox-adventure/components/FoxGame.tsx
Normal file
63
src/games/fox-adventure/components/FoxGame.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
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);
|
||||
|
||||
// Initialize game systems
|
||||
useGameLoop();
|
||||
useGameControls();
|
||||
|
||||
// Konami code for game activation
|
||||
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) {
|
||||
setIsActive(true);
|
||||
useGameStore.getState().startNewGame();
|
||||
}
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
return () => window.removeEventListener('keydown', handleKeydown);
|
||||
}, []);
|
||||
|
||||
if (!isActive) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gradient-game 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 */}
|
||||
</div>
|
||||
<GameHUD />
|
||||
<GameOverlay />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FoxGame;
|
||||
26
src/games/fox-adventure/components/Player.tsx
Normal file
26
src/games/fox-adventure/components/Player.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import useGameStore from '../state/gameStore';
|
||||
|
||||
export const Player: React.FC = () => {
|
||||
const player = useGameStore(state => state.player);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute transition-all duration-100 ${
|
||||
player.isInvincible ? 'animate-pulse-fast' : ''
|
||||
}`}
|
||||
style={{
|
||||
left: `${player.position.x}%`,
|
||||
top: `${player.position.y}%`,
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
>
|
||||
<div className="relative w-16 h-16">
|
||||
{/* Fox body */}
|
||||
<div className="absolute inset-0 bg-fox-orange rounded-full">
|
||||
{/* Face details here */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
57
src/games/fox-adventure/hooks/useGameControls.ts
Normal file
57
src/games/fox-adventure/hooks/useGameControls.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { useEffect } from 'react';
|
||||
import useGameStore from '../state/gameStore';
|
||||
|
||||
export const useGameControls = () => {
|
||||
const gameStore = useGameStore();
|
||||
|
||||
useEffect(() => {
|
||||
const keys = new Set<string>();
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
keys.add(e.key);
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
if (gameStore.gameStatus === 'PLAYING') {
|
||||
gameStore.pauseGame();
|
||||
} else if (gameStore.gameStatus === 'PAUSED') {
|
||||
gameStore.resumeGame();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
keys.delete(e.key);
|
||||
};
|
||||
|
||||
const updatePlayerMovement = () => {
|
||||
if (gameStore.gameStatus !== 'PLAYING') return;
|
||||
|
||||
const direction = { x: 0, y: 0 };
|
||||
|
||||
if (keys.has('ArrowUp') || keys.has('w')) direction.y -= 1;
|
||||
if (keys.has('ArrowDown') || keys.has('s')) direction.y += 1;
|
||||
if (keys.has('ArrowLeft') || keys.has('a')) direction.x -= 1;
|
||||
if (keys.has('ArrowRight') || keys.has('d')) direction.x += 1;
|
||||
|
||||
if (direction.x !== 0 || direction.y !== 0) {
|
||||
gameStore.movePlayer(direction);
|
||||
}
|
||||
};
|
||||
|
||||
let animationFrameId: number;
|
||||
const gameLoop = () => {
|
||||
updatePlayerMovement();
|
||||
animationFrameId = requestAnimationFrame(gameLoop);
|
||||
};
|
||||
animationFrameId = requestAnimationFrame(gameLoop);
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
33
src/games/fox-adventure/hooks/useGameLoop.ts
Normal file
33
src/games/fox-adventure/hooks/useGameLoop.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
import useGameStore from '../state/gameStore';
|
||||
|
||||
export const useGameLoop = () => {
|
||||
const frameRef = useRef<number>();
|
||||
const lastUpdateRef = useRef<number>(0);
|
||||
const gameStore = useGameStore();
|
||||
|
||||
useEffect(() => {
|
||||
const gameLoop = (timestamp: number) => {
|
||||
if (!lastUpdateRef.current) lastUpdateRef.current = timestamp;
|
||||
const deltaTime = timestamp - lastUpdateRef.current;
|
||||
|
||||
if (gameStore.gameStatus === 'PLAYING') {
|
||||
// Update game state
|
||||
gameStore.updateEnemies();
|
||||
checkCollisions();
|
||||
updatePowerUps(deltaTime);
|
||||
}
|
||||
|
||||
lastUpdateRef.current = timestamp;
|
||||
frameRef.current = requestAnimationFrame(gameLoop);
|
||||
};
|
||||
|
||||
frameRef.current = requestAnimationFrame(gameLoop);
|
||||
|
||||
return () => {
|
||||
if (frameRef.current) {
|
||||
cancelAnimationFrame(frameRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
|
@ -1,13 +1,53 @@
|
|||
/* Default cursor for all elements */
|
||||
* {
|
||||
cursor: url('/cursors/default.svg') 16 16, auto;
|
||||
}
|
||||
|
||||
a, button, [role="button"],
|
||||
input[type="submit"], input[type="button"],
|
||||
select {
|
||||
/* Interactive elements cursor */
|
||||
a,
|
||||
button,
|
||||
[role="button"],
|
||||
input[type="submit"],
|
||||
input[type="button"],
|
||||
select,
|
||||
.interactive,
|
||||
.nav-link {
|
||||
cursor: url('/cursors/paw.svg') 16 16, pointer;
|
||||
}
|
||||
|
||||
.loading {
|
||||
/* Loading state cursor */
|
||||
.loading,
|
||||
:disabled,
|
||||
[aria-busy="true"] {
|
||||
cursor: url('/cursors/tail-loading.svg') 16 16, progress;
|
||||
}
|
||||
|
||||
/* Hover effects for interactive elements */
|
||||
a:hover,
|
||||
button:hover,
|
||||
[role="button"]:hover,
|
||||
.nav-link:hover {
|
||||
/* Add a subtle glow effect on hover */
|
||||
filter: drop-shadow(0 0 4px var(--fox-pink-glow));
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
/* Custom cursor regions */
|
||||
.text-select {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.resize {
|
||||
cursor: nw-resize;
|
||||
}
|
||||
|
||||
/* Ensure cursors work with transform effects */
|
||||
* {
|
||||
cursor-position: fixed;
|
||||
}
|
||||
|
||||
/* Prevent cursor inheritance in certain cases */
|
||||
iframe,
|
||||
canvas {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
|
@ -1,3 +1,21 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<path fill="#ffffff" stroke="#000000" stroke-width="2" d="M6,2 L26,22 L16,22 L12,30 L8,22 L6,22 Z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<!-- Fox head shape -->
|
||||
<path d="M16 2 L24 10 L24 24 L8 24 L8 10 Z"
|
||||
fill="#ff9466"
|
||||
stroke="#240046"
|
||||
stroke-width="1.5"/>
|
||||
<!-- Ears -->
|
||||
<path d="M8 10 L2 2 L8 8"
|
||||
fill="#ff9466"
|
||||
stroke="#240046"
|
||||
stroke-width="1.5"/>
|
||||
<path d="M24 10 L30 2 L24 8"
|
||||
fill="#ff9466"
|
||||
stroke="#240046"
|
||||
stroke-width="1.5"/>
|
||||
<!-- Eyes -->
|
||||
<circle cx="12" cy="14" r="2" fill="#240046"/>
|
||||
<circle cx="20" cy="14" r="2" fill="#240046"/>
|
||||
<!-- Nose -->
|
||||
<circle cx="16" cy="18" r="1.5" fill="#240046"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 196 B After Width: | Height: | Size: 658 B |
|
|
@ -1,12 +1,26 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<path fill="none" stroke="#ffc6e5" stroke-width="2">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 16 16"
|
||||
to="360 16 16"
|
||||
dur="1s"
|
||||
repeatCount="indefinite"/>
|
||||
<path d="M16,4 C22,4 26,8 26,14 C26,20 22,24 16,24 C10,24 6,20 6,14 C6,8 10,4 16,4 Z"/>
|
||||
</path>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<!-- Tail base -->
|
||||
<path d="M4 16 Q8 8 16 8 Q24 8 28 16 Q24 24 16 24 Q8 24 4 16"
|
||||
fill="#ff9466"
|
||||
stroke="#240046"
|
||||
stroke-width="1.5">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 16 16"
|
||||
to="360 16 16"
|
||||
dur="1s"
|
||||
repeatCount="indefinite"/>
|
||||
</path>
|
||||
<!-- Tail tip -->
|
||||
<circle cx="16" cy="16" r="4"
|
||||
fill="#ffc6e5"
|
||||
stroke="#240046"
|
||||
stroke-width="1.5">
|
||||
<animate
|
||||
attributeName="r"
|
||||
values="4;5;4"
|
||||
dur="1s"
|
||||
repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 460 B After Width: | Height: | Size: 683 B |
31
src/styles/game.css
Normal file
31
src/styles/game.css
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* Game-specific styles */
|
||||
.game-active * {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.game-viewport {
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/* Animation utilities */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Game-specific shadows */
|
||||
.fox-shadow {
|
||||
filter: drop-shadow(0 0 8px var(--fox-pink-glow));
|
||||
}
|
||||
|
||||
.enemy-shadow {
|
||||
filter: drop-shadow(0 0 8px rgba(255, 0, 0, 0.3));
|
||||
}
|
||||
|
|
@ -2,3 +2,30 @@
|
|||
@import 'animations.css';
|
||||
@import 'utilities.css';
|
||||
@import 'cursor.css';
|
||||
|
||||
@import './game.css';
|
||||
|
||||
/* Game Styles */
|
||||
.game-viewport {
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.game-active * {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
/* Game Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Game-specific effects */
|
||||
.fox-shadow {
|
||||
filter: drop-shadow(0 0 8px var(--fox-pink-glow));
|
||||
}
|
||||
|
|
|
|||
42
src/types/game.ts
Normal file
42
src/types/game.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
export interface PowerUp {
|
||||
id: string;
|
||||
type: 'SPEED' | 'SHIELD' | 'MAGNET';
|
||||
duration: number;
|
||||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
export interface Collectible {
|
||||
id: string;
|
||||
type: 'STAR' | 'GEM' | 'KEY';
|
||||
value: number;
|
||||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
export interface Enemy {
|
||||
id: string;
|
||||
type: 'WOLF' | 'OWL' | 'HUNTER';
|
||||
position: { x: number; y: number };
|
||||
direction: { x: number; y: number };
|
||||
speed: number;
|
||||
}
|
||||
|
||||
export interface PlayerState {
|
||||
position: { x: number; y: number };
|
||||
health: number;
|
||||
speed: number;
|
||||
powerUps: PowerUp[];
|
||||
isInvincible: boolean;
|
||||
hasKey: boolean;
|
||||
}
|
||||
|
||||
export interface GameState {
|
||||
player: PlayerState;
|
||||
enemies: Enemy[];
|
||||
collectibles: Collectible[];
|
||||
powerUps: PowerUp[];
|
||||
score: number;
|
||||
level: number;
|
||||
gameStatus: 'MENU' | 'PLAYING' | 'PAUSED' | 'GAME_OVER';
|
||||
highScores: number[];
|
||||
timePlayed: number;
|
||||
}
|
||||
|
|
@ -23,6 +23,8 @@ export default {
|
|||
'bounce-slow': 'bounce 3s linear infinite',
|
||||
'glow': 'glow 2s ease-in-out infinite',
|
||||
'float': 'float 3s ease-in-out infinite',
|
||||
'spin-slow': 'spin 3s linear infinite',
|
||||
'wag': 'wag 1s ease-in-out infinite',
|
||||
},
|
||||
keyframes: {
|
||||
glow: {
|
||||
|
|
@ -33,16 +35,11 @@ export default {
|
|||
'0%, 100%': { transform: 'translateY(0)' },
|
||||
'50%': { transform: 'translateY(-10px)' },
|
||||
},
|
||||
wag: {
|
||||
'0%, 100%': { transform: 'rotate(-10deg)' },
|
||||
'50%': { transform: 'rotate(10deg)' },
|
||||
},
|
||||
},
|
||||
backgroundImage: {
|
||||
'gradient-primary': 'linear-gradient(135deg, var(--background-primary) 0%, var(--background-secondary) 100%)',
|
||||
'gradient-card': 'linear-gradient(135deg, rgba(47, 28, 84, 0.3) 0%, rgba(157, 78, 221, 0.1) 100%)',
|
||||
'gradient-hover': 'linear-gradient(135deg, rgba(157, 78, 221, 0.2) 0%, rgba(178, 73, 248, 0.1) 100%)',
|
||||
},
|
||||
zIndex: {
|
||||
'behind': '-1',
|
||||
'deep': '-10',
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue