diff --git a/packages/demo/package.json b/packages/demo/package.json index b139672..ab5f637 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -10,6 +10,7 @@ "clean": "rm -rf dist" }, "dependencies": { + "@minigames-react/dino": "workspace:*", "@minigames-react/minesweeper": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/packages/demo/src/App.tsx b/packages/demo/src/App.tsx index ddc946b..97f6ef4 100644 --- a/packages/demo/src/App.tsx +++ b/packages/demo/src/App.tsx @@ -1,6 +1,8 @@ import React, { useState } from 'react'; import { Minesweeper } from '@minigames-react/minesweeper'; import '@minigames-react/minesweeper/dist/index.css'; +import { Dino } from '@minigames-react/dino'; +import '@minigames-react/dino/dist/index.css'; import './App.css'; interface GameResult { @@ -26,6 +28,7 @@ const difficulties: Record = { function App() { const [difficulty, setDifficulty] = useState('easy'); const [gameResult, setGameResult] = useState(null); + const [dinoResult, setDinoResult] = useState<{ score: number; time: number } | null>(null); const [key, setKey] = useState(0); const config = difficulties[difficulty]; @@ -34,6 +37,10 @@ function App() { setGameResult(result); }; + const handleDinoFinish = (result: { score: number; time: number }) => { + setDinoResult(result); + }; + const handleDifficultyChange = (newDifficulty: Difficulty) => { setDifficulty(newDifficulty); setGameResult(null); @@ -102,6 +109,26 @@ function App() { )} + +
+

🦕 Dino Game

+ +
+

+ How to play: +

+

Press SPACE or ↑ to jump, ↓ to duck

+

Avoid obstacles and survive as long as you can!

+
+ + + + {dinoResult && ( +
+ 🦕 Game Over! Score: {Math.floor(dinoResult.score)} | Time: {dinoResult.time.toFixed(1)}s +
+ )} +
); diff --git a/packages/dino/README.md b/packages/dino/README.md new file mode 100644 index 0000000..08d1f7a --- /dev/null +++ b/packages/dino/README.md @@ -0,0 +1,50 @@ +# @minigames-react/dino + +A Chrome Dino-style game component for React. + +## Installation + +```bash +npm install @minigames-react/dino +# or +pnpm add @minigames-react/dino +``` + +## Usage + +```tsx +import { Dino } from '@minigames-react/dino'; +import '@minigames-react/dino/dist/index.css'; + +function App() { + const handleFinish = (result) => { + console.log(`Game Over! Score: ${result.score}, Time: ${result.time}s`); + }; + + return ( +
+

Dino Game

+ +
+ ); +} +``` + +## Props + +- `onFinish?: (result: { score: number; time: number }) => void` - Callback function called when the game ends +- `speed?: number` - Game speed multiplier (default: 1) + +## Controls + +- **Space** or **↑** - Jump +- **↓** - Duck +- **R** - Restart (when game is over) + +## Features + +- Classic Chrome Dino game mechanics +- Jump and duck to avoid obstacles +- Progressive difficulty +- Score tracking +- Keyboard controls diff --git a/packages/dino/package.json b/packages/dino/package.json new file mode 100644 index 0000000..7fdd24e --- /dev/null +++ b/packages/dino/package.json @@ -0,0 +1,35 @@ +{ + "name": "@minigames-react/dino", + "version": "0.1.0", + "description": "Dino game component for React", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "rollup -c", + "dev": "rollup -c -w", + "clean": "rm -rf dist" + }, + "keywords": ["react", "dino", "game", "chrome", "t-rex"], + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-typescript": "^11.1.6", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rollup": "^4.9.6", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-postcss": "^4.0.2", + "tslib": "^2.6.2", + "typescript": "^5.3.3" + }, + "files": [ + "dist" + ] +} diff --git a/packages/dino/rollup.config.js b/packages/dino/rollup.config.js new file mode 100644 index 0000000..5f5ceb7 --- /dev/null +++ b/packages/dino/rollup.config.js @@ -0,0 +1,31 @@ +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import typescript from '@rollup/plugin-typescript'; +import peerDepsExternal from 'rollup-plugin-peer-deps-external'; +import postcss from 'rollup-plugin-postcss'; + +export default { + input: 'src/index.ts', + output: [ + { + file: 'dist/index.js', + format: 'cjs', + sourcemap: true, + }, + { + file: 'dist/index.esm.js', + format: 'esm', + sourcemap: true, + }, + ], + plugins: [ + peerDepsExternal(), + resolve(), + commonjs(), + typescript({ tsconfig: './tsconfig.json' }), + postcss({ + extract: true, + minimize: true, + }), + ], +}; diff --git a/packages/dino/src/Dino.css b/packages/dino/src/Dino.css new file mode 100644 index 0000000..ffb5dc9 --- /dev/null +++ b/packages/dino/src/Dino.css @@ -0,0 +1,66 @@ +.dino-game { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + padding: 20px; + background: #fff; +} + +.dino-controls { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + width: 100%; +} + +.control-hint { + display: flex; + gap: 20px; + font-size: 13px; + color: #757575; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.control-hint span { + padding: 4px 8px; + background: #f5f5f5; + border-radius: 3px; + border: 1px solid #ddd; +} + +.restart-button { + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + color: #fff; + background: #535353; + border: none; + border-radius: 3px; + cursor: pointer; + transition: background 0.2s; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.restart-button:hover { + background: #3a3a3a; +} + +.restart-button:active { + transform: scale(0.98); +} + +.dino-canvas { + border: 2px solid #535353; + background: #fff; + cursor: pointer; + display: block; + image-rendering: crisp-edges; + image-rendering: pixelated; +} + +.dino-canvas:focus { + outline: none; + border-color: #535353; +} diff --git a/packages/dino/src/Dino.tsx b/packages/dino/src/Dino.tsx new file mode 100644 index 0000000..ddb6766 --- /dev/null +++ b/packages/dino/src/Dino.tsx @@ -0,0 +1,388 @@ +import React, { useEffect, useRef, useState, useCallback } from 'react'; +import { DinoProps } from './types'; +import { + createInitialState, + updateGameState, + CANVAS_WIDTH, + CANVAS_HEIGHT, + GROUND_HEIGHT, + JUMP_STRENGTH, +} from './gameLogic'; +import './Dino.css'; + +export const Dino: React.FC = ({ onFinish, speed = 1 }) => { + const canvasRef = useRef(null); + const [gameState, setGameState] = useState(createInitialState()); + const [startTime, setStartTime] = useState(0); + const velocityYRef = useRef(0); + const animationFrameRef = useRef(); + const keysPressed = useRef>(new Set()); + const gameStateRef = useRef(gameState); + const onFinishRef = useRef(onFinish); + const startTimeRef = useRef(startTime); + const onFinishCalledRef = useRef(false); + + // Update refs when state changes + useEffect(() => { + gameStateRef.current = gameState; + onFinishRef.current = onFinish; + startTimeRef.current = startTime; + }, [gameState, onFinish, startTime]); + + // Reset game + const resetGame = useCallback(() => { + setGameState(createInitialState()); + velocityYRef.current = 0; + setStartTime(0); + keysPressed.current.clear(); + onFinishCalledRef.current = false; + }, []); + + // Start game + const startGame = useCallback(() => { + if (!gameState.gameStarted && !gameState.gameOver) { + setGameState(prev => ({ ...prev, gameStarted: true })); + setStartTime(Date.now()); + } + }, [gameState.gameStarted, gameState.gameOver]); + + // Jump + const jump = useCallback(() => { + if (!gameState.gameOver && !gameState.isJumping) { + if (!gameState.gameStarted) { + startGame(); + } + setGameState(prev => ({ ...prev, isJumping: true })); + velocityYRef.current = JUMP_STRENGTH; + } + }, [gameState.gameOver, gameState.isJumping, gameState.gameStarted, startGame]); + + // Duck + const setDucking = useCallback((ducking: boolean) => { + if (!gameState.gameOver) { + if (!gameState.gameStarted && ducking) { + startGame(); + } + setGameState(prev => ({ ...prev, isDucking: ducking })); + } + }, [gameState.gameOver, gameState.gameStarted, startGame]); + + // Handle keyboard input + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.code === 'Space' || e.code === 'ArrowUp') { + e.preventDefault(); + if (!keysPressed.current.has(e.code)) { + keysPressed.current.add(e.code); + jump(); + } + } else if (e.code === 'ArrowDown') { + e.preventDefault(); + keysPressed.current.add(e.code); + setDucking(true); + } + }; + + const handleKeyUp = (e: KeyboardEvent) => { + if (e.code === 'ArrowDown') { + e.preventDefault(); + keysPressed.current.delete(e.code); + setDucking(false); + } else if (e.code === 'Space' || e.code === 'ArrowUp') { + keysPressed.current.delete(e.code); + } + }; + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyUp); + }; + }, [jump, setDucking]); + + // Game loop + useEffect(() => { + const gameLoop = () => { + const currentState = gameStateRef.current; + + if (currentState.gameOver) { + const currentOnFinish = onFinishRef.current; + const currentStartTime = startTimeRef.current; + if (currentOnFinish && currentStartTime > 0 && !onFinishCalledRef.current) { + onFinishCalledRef.current = true; + const elapsedTime = (Date.now() - currentStartTime) / 1000; + currentOnFinish({ + score: Math.floor(currentState.score), + time: elapsedTime, + }); + } + return; + } + + if (!currentState.gameStarted) { + animationFrameRef.current = requestAnimationFrame(gameLoop); + return; + } + + const { newState, newVelocityY } = updateGameState( + currentState, + velocityYRef.current, + speed + ); + velocityYRef.current = newVelocityY; + setGameState(newState); + animationFrameRef.current = requestAnimationFrame(gameLoop); + }; + + animationFrameRef.current = requestAnimationFrame(gameLoop); + + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + }; + }, [speed]); // Only depend on speed which is a prop + + // Helper function to draw T-Rex + const drawDino = (ctx: CanvasRenderingContext2D, x: number, y: number, isDucking: boolean, frame: number) => { + ctx.fillStyle = '#535353'; + + if (isDucking) { + // Ducking T-Rex - lower profile + // Body + ctx.fillRect(x + 6, y + 18, 32, 14); + // Tail + ctx.fillRect(x, y + 20, 6, 4); + // Head + ctx.fillRect(x + 32, y + 14, 10, 8); + ctx.fillRect(x + 38, y + 14, 4, 4); + // Eye (white background then black pupil for contrast) + ctx.fillStyle = '#fff'; + ctx.fillRect(x + 38, y + 15, 2, 2); + ctx.fillStyle = '#535353'; + ctx.fillRect(x + 38, y + 15, 1, 1); + // Front leg + ctx.fillRect(x + 30, y + 28, 4, 4); + // Back leg + ctx.fillRect(x + 18, y + 28, 4, 4); + } else { + // Standing/Jumping T-Rex + // Tail + ctx.fillRect(x, y + 16, 4, 4); + ctx.fillRect(x + 4, y + 12, 4, 8); + + // Body + ctx.fillRect(x + 8, y + 8, 16, 16); + + // Neck + ctx.fillRect(x + 22, y + 4, 6, 12); + + // Head + ctx.fillRect(x + 28, y, 12, 10); + ctx.fillRect(x + 32, y + 10, 8, 4); + + // Eye + ctx.fillRect(x + 34, y + 3, 2, 2); + + // Mouth detail + ctx.fillRect(x + 38, y + 6, 2, 2); + + // Legs - alternate for running animation + const legFrame = Math.floor(frame / 5) % 2; + if (legFrame === 0) { + // Front leg forward + ctx.fillRect(x + 20, y + 24, 4, 6); + ctx.fillRect(x + 20, y + 30, 6, 2); + // Back leg back + ctx.fillRect(x + 10, y + 24, 4, 4); + ctx.fillRect(x + 8, y + 28, 6, 2); + } else { + // Front leg back + ctx.fillRect(x + 20, y + 24, 4, 4); + ctx.fillRect(x + 18, y + 28, 6, 2); + // Back leg forward + ctx.fillRect(x + 10, y + 24, 4, 6); + ctx.fillRect(x + 10, y + 30, 6, 2); + } + + // Arm + ctx.fillRect(x + 22, y + 12, 4, 4); + } + }; + + // Helper function to draw cactus + const drawCactus = (ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) => { + ctx.fillStyle = '#535353'; + + // Main stem + const stemWidth = 8; + const stemX = x + (width - stemWidth) / 2; + ctx.fillRect(stemX, y, stemWidth, height); + + // Add arms if cactus is tall enough + if (height > 30) { + // Left arm + ctx.fillRect(stemX - 6, y + height * 0.3, 6, 3); + ctx.fillRect(stemX - 6, y + height * 0.3, 3, height * 0.3); + + // Right arm (if wide enough) + if (width > 16) { + ctx.fillRect(stemX + stemWidth, y + height * 0.4, 6, 3); + ctx.fillRect(stemX + stemWidth + 3, y + height * 0.4, 3, height * 0.25); + } + } + }; + + // Helper function to draw pterodactyl + const drawPterodactyl = (ctx: CanvasRenderingContext2D, x: number, y: number, frame: number) => { + ctx.fillStyle = '#535353'; + + // Body + ctx.fillRect(x + 6, y + 4, 16, 8); + + // Head + ctx.fillRect(x + 20, y + 2, 8, 6); + ctx.fillRect(x + 26, y + 4, 4, 4); + + // Beak + ctx.fillRect(x + 28, y + 6, 4, 2); + + // Eye + ctx.fillRect(x + 24, y + 4, 2, 2); + + // Tail + ctx.fillRect(x, y + 6, 6, 4); + ctx.fillRect(x - 4, y + 8, 4, 2); + + // Wings - flapping animation + const wingFrame = Math.floor(frame / 8) % 2; + if (wingFrame === 0) { + // Wings up + ctx.fillRect(x + 6, y, 12, 4); + ctx.fillRect(x + 6, y + 12, 12, 4); + } else { + // Wings down + ctx.fillRect(x + 6, y - 2, 12, 4); + ctx.fillRect(x + 6, y + 14, 12, 4); + } + }; + + // Animation frame counter + const frameCountRef = useRef(0); + + // Render game + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + frameCountRef.current++; + + // Clear canvas with white background (Chrome style) + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + // Draw ground with dashed line + ctx.strokeStyle = '#535353'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(0, GROUND_HEIGHT); + ctx.lineTo(CANVAS_WIDTH, GROUND_HEIGHT); + ctx.stroke(); + + // Draw ground dots/texture + ctx.fillStyle = '#535353'; + for (let i = 0; i < CANVAS_WIDTH; i += 20) { + ctx.fillRect(i, GROUND_HEIGHT + 2, 2, 2); + } + + // Draw dino + drawDino( + ctx, + gameState.dino.x, + gameState.dino.y, + gameState.isDucking && !gameState.isJumping, + gameState.gameStarted ? frameCountRef.current : 0 + ); + + // Draw obstacles + gameState.obstacles.forEach(obstacle => { + if (obstacle.type === 'cactus') { + drawCactus(ctx, obstacle.x, obstacle.y, obstacle.width, obstacle.height); + } else { + drawPterodactyl(ctx, obstacle.x, obstacle.y, frameCountRef.current); + } + }); + + // Draw score (Chrome style) + ctx.fillStyle = '#535353'; + ctx.font = 'bold 16px monospace'; + ctx.textAlign = 'right'; + const score = Math.floor(gameState.score); + const scoreText = ('00000' + Math.min(score, 99999)).slice(-5); + ctx.fillText(scoreText, CANVAS_WIDTH - 20, 30); + + // Draw game over message (Chrome style) + if (gameState.gameOver) { + ctx.fillStyle = '#535353'; + ctx.font = 'bold 24px monospace'; + ctx.textAlign = 'center'; + ctx.fillText('G A M E O V E R', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 - 10); + + // Draw restart icon (simple arrow) + const iconX = CANVAS_WIDTH / 2 - 20; + const iconY = CANVAS_HEIGHT / 2 + 15; + ctx.fillRect(iconX, iconY, 16, 16); + ctx.fillStyle = '#fff'; + ctx.fillRect(iconX + 4, iconY + 4, 8, 8); + ctx.fillStyle = '#535353'; + ctx.font = '12px monospace'; + ctx.fillText('Press R to restart', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2 + 50); + } else if (!gameState.gameStarted) { + // Draw start message more subtly + ctx.fillStyle = '#535353'; + ctx.font = '14px monospace'; + ctx.textAlign = 'center'; + ctx.fillText('Press SPACE or ↑ to start', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2); + } + }, [gameState]); + + // Handle restart + useEffect(() => { + const handleRestart = (e: KeyboardEvent) => { + if (e.code === 'KeyR' && gameState.gameOver) { + e.preventDefault(); + resetGame(); + } + }; + + window.addEventListener('keydown', handleRestart); + return () => window.removeEventListener('keydown', handleRestart); + }, [gameState.gameOver, resetGame]); + + return ( +
+
+
+ ⬆️ Jump (Space/↑) + ⬇️ Duck (↓) +
+ {gameState.gameOver && ( + + )} +
+ +
+ ); +}; diff --git a/packages/dino/src/gameLogic.ts b/packages/dino/src/gameLogic.ts new file mode 100644 index 0000000..fb2bb01 --- /dev/null +++ b/packages/dino/src/gameLogic.ts @@ -0,0 +1,164 @@ +import { GameObject, Obstacle, GameState } from './types'; + +// Game constants +export const CANVAS_WIDTH = 800; +export const CANVAS_HEIGHT = 200; +export const GROUND_HEIGHT = 150; +export const DINO_WIDTH = 40; +export const DINO_HEIGHT = 45; +export const DINO_X = 50; +export const GRAVITY = 0.6; +export const JUMP_STRENGTH = -12; +export const DUCK_HEIGHT = 30; +export const OBSTACLE_SPEED = 5; +export const OBSTACLE_SPAWN_DISTANCE = 300; + +/** + * Creates initial game state + */ +export function createInitialState(): GameState { + return { + dino: { + x: DINO_X, + y: GROUND_HEIGHT - DINO_HEIGHT, + width: DINO_WIDTH, + height: DINO_HEIGHT, + }, + obstacles: [], + score: 0, + isJumping: false, + isDucking: false, + gameOver: false, + gameStarted: false, + }; +} + +/** + * Checks collision between dino and obstacle + */ +export function checkCollision(dino: GameObject, obstacle: Obstacle): boolean { + // Add some padding for more forgiving collision + const padding = 5; + + return ( + dino.x + padding < obstacle.x + obstacle.width && + dino.x + dino.width - padding > obstacle.x && + dino.y + padding < obstacle.y + obstacle.height && + dino.y + dino.height - padding > obstacle.y + ); +} + +/** + * Creates a random obstacle + */ +export function createObstacle(lastObstacleX: number): Obstacle { + const types: ('cactus' | 'bird')[] = ['cactus', 'bird']; + const type = types[Math.floor(Math.random() * types.length)]; + + const gap = OBSTACLE_SPAWN_DISTANCE + Math.random() * 200; + const x = Math.max(CANVAS_WIDTH, lastObstacleX + gap); + + if (type === 'cactus') { + const heights = [35, 45, 50]; + const height = heights[Math.floor(Math.random() * heights.length)]; + return { + type: 'cactus', + x, + y: GROUND_HEIGHT - height, + width: 20, + height, + }; + } else { + // Bird at different heights + const birdHeights = [GROUND_HEIGHT - 80, GROUND_HEIGHT - 60, GROUND_HEIGHT - 100]; + const y = birdHeights[Math.floor(Math.random() * birdHeights.length)]; + return { + type: 'bird', + x, + y, + width: 35, + height: 25, + }; + } +} + +/** + * Updates game state + */ +export function updateGameState( + state: GameState, + velocityY: number, + speed: number +): { newState: GameState; newVelocityY: number } { + if (state.gameOver || !state.gameStarted) { + return { newState: state, newVelocityY: velocityY }; + } + + const newState = { ...state }; + let newVelocityY = velocityY; + + // Update dino position + if (state.isJumping) { + newVelocityY += GRAVITY; + newState.dino = { + ...state.dino, + y: state.dino.y + newVelocityY, + }; + + // Check if landed + const groundY = GROUND_HEIGHT - DINO_HEIGHT; + if (newState.dino.y >= groundY) { + newState.dino.y = groundY; + newState.isJumping = false; + newVelocityY = 0; + } + } + + // Update dino height when ducking + if (state.isDucking && !state.isJumping) { + newState.dino = { + ...state.dino, + height: DUCK_HEIGHT, + y: GROUND_HEIGHT - DUCK_HEIGHT, + }; + } else if (!state.isDucking && state.dino.height !== DINO_HEIGHT) { + newState.dino = { + ...state.dino, + height: DINO_HEIGHT, + y: state.isJumping ? state.dino.y : GROUND_HEIGHT - DINO_HEIGHT, + }; + } + + // Update obstacles + const obstacleSpeed = OBSTACLE_SPEED * speed; + newState.obstacles = state.obstacles + .map(obstacle => ({ + ...obstacle, + x: obstacle.x - obstacleSpeed, + })) + .filter(obstacle => obstacle.x + obstacle.width > 0); + + // Add new obstacles + const lastObstacleX = newState.obstacles.length > 0 + ? newState.obstacles[newState.obstacles.length - 1].x + : -OBSTACLE_SPAWN_DISTANCE; + + if (lastObstacleX < CANVAS_WIDTH) { + newState.obstacles.push(createObstacle(lastObstacleX)); + } + + // Check collisions + for (const obstacle of newState.obstacles) { + if (checkCollision(newState.dino, obstacle)) { + newState.gameOver = true; + break; + } + } + + // Update score (increment by a small amount each frame) + // Note: This assumes consistent frame rates (~60fps). For variable frame rate support, + // consider using delta time: score += (pointsPerSecond * deltaTime * speed) + newState.score = state.score + (0.1 * speed); + + return { newState, newVelocityY }; +} diff --git a/packages/dino/src/index.ts b/packages/dino/src/index.ts new file mode 100644 index 0000000..a013201 --- /dev/null +++ b/packages/dino/src/index.ts @@ -0,0 +1,2 @@ +export { Dino } from './Dino'; +export type { DinoProps } from './types'; diff --git a/packages/dino/src/types.ts b/packages/dino/src/types.ts new file mode 100644 index 0000000..db63a96 --- /dev/null +++ b/packages/dino/src/types.ts @@ -0,0 +1,35 @@ +export interface DinoProps { + /** + * Callback function called when the game ends + * @param result - Object containing game result information + * @param result.score - Final score + * @param result.time - Time survived in seconds + */ + onFinish?: (result: { score: number; time: number }) => void; + + /** + * Game speed multiplier (default: 1) + */ + speed?: number; +} + +export interface GameObject { + x: number; + y: number; + width: number; + height: number; +} + +export interface Obstacle extends GameObject { + type: 'cactus' | 'bird'; +} + +export interface GameState { + dino: GameObject; + obstacles: Obstacle[]; + score: number; + isJumping: boolean; + isDucking: boolean; + gameOver: boolean; + gameStarted: boolean; +} diff --git a/packages/dino/tsconfig.json b/packages/dino/tsconfig.json new file mode 100644 index 0000000..ea02317 --- /dev/null +++ b/packages/dino/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2015", + "module": "ESNext", + "lib": ["ES2015", "DOM"], + "jsx": "react", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1475875..82c0eb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: packages/demo: dependencies: + '@minigames-react/dino': + specifier: workspace:* + version: link:../dino '@minigames-react/minesweeper': specifier: workspace:* version: link:../minesweeper @@ -40,6 +43,45 @@ importers: specifier: ^5.0.11 version: 5.4.21 + packages/dino: + devDependencies: + '@rollup/plugin-commonjs': + specifier: ^25.0.7 + version: 25.0.8(rollup@4.57.1) + '@rollup/plugin-node-resolve': + specifier: ^15.2.3 + version: 15.3.1(rollup@4.57.1) + '@rollup/plugin-typescript': + specifier: ^11.1.6 + version: 11.1.6(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3) + '@types/react': + specifier: ^18.2.48 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.3.7(@types/react@18.3.28) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + rollup: + specifier: ^4.9.6 + version: 4.57.1 + rollup-plugin-peer-deps-external: + specifier: ^2.2.4 + version: 2.2.4(rollup@4.57.1) + rollup-plugin-postcss: + specifier: ^4.0.2 + version: 4.0.2(postcss@8.5.6) + tslib: + specifier: ^2.6.2 + version: 2.8.1 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + packages/minesweeper: devDependencies: '@rollup/plugin-commonjs':