feat: Initialize Mia's Clean Run project

Sets up the basic project structure, dependencies, and configuration for the game. Includes initial HTML, TypeScript, and Vite configurations. Adds initial types and constants for game mechanics and assets.
This commit is contained in:
Anthony
2025-11-23 19:32:47 +08:00
parent 995cff776a
commit 77f803f26e
12 changed files with 2342 additions and 8 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

383
App.tsx Normal file
View File

@@ -0,0 +1,383 @@
import React, { useState, useRef, useEffect } from 'react';
import GameCanvas from './components/GameCanvas';
import { GameStatus, MiaCustomization } from './types';
import { Play, RotateCcw, Trophy, Heart, ArrowRight, Sparkles, Palette, Pause, Home } from 'lucide-react';
import { MAX_LIVES, FUR_COLORS, COLLAR_COLORS } from './constants';
const App: React.FC = () => {
const [gameStatus, setGameStatus] = useState<GameStatus>(GameStatus.START_SCREEN);
const [score, setScore] = useState(0);
const [lives, setLives] = useState(MAX_LIVES);
const [highScore, setHighScore] = useState(0);
const [showDamageOverlay, setShowDamageOverlay] = useState(false);
const [customizationTab, setCustomizationTab] = useState<'body' | 'head' | 'style'>('body');
const [customization, setCustomization] = useState<MiaCustomization>({
furColor: FUR_COLORS.RUSTY,
collarColor: COLLAR_COLORS.RED,
hat: 'none',
glasses: 'none',
shirt: 'none'
});
const prevLivesRef = useRef(MAX_LIVES);
const canvasRef = useRef<HTMLCanvasElement>(null);
// Trigger damage overlay when lives decrease
useEffect(() => {
if (lives < prevLivesRef.current) {
setShowDamageOverlay(true);
const t = setTimeout(() => setShowDamageOverlay(false), 300);
return () => clearTimeout(t);
}
prevLivesRef.current = lives;
}, [lives]);
const handleStart = () => {
setGameStatus(GameStatus.PLAYING);
setLives(MAX_LIVES);
prevLivesRef.current = MAX_LIVES;
};
const handleRestart = () => {
if (score > highScore) {
setHighScore(score);
}
// Fully reset logic
setGameStatus(GameStatus.START_SCREEN);
setScore(0);
setLives(MAX_LIVES);
prevLivesRef.current = MAX_LIVES;
};
const handleNextLevel = () => {
setGameStatus(GameStatus.PLAYING);
};
const updateStatus = (status: GameStatus) => {
if (status === GameStatus.GAME_OVER) {
if (score > highScore) setHighScore(score);
}
setGameStatus(status);
};
return (
<div className="relative w-full h-screen overflow-hidden bg-amber-50">
{/* Game Layer */}
<div className="absolute inset-0 z-0">
<GameCanvas
gameStatus={gameStatus}
setGameStatus={updateStatus}
setScore={setScore}
setLives={setLives}
canvasRef={canvasRef}
customization={customization}
/>
</div>
{/* Damage Overlay */}
<div className={`absolute inset-0 z-10 bg-red-600 pointer-events-none transition-opacity duration-200 ${showDamageOverlay ? 'opacity-40' : 'opacity-0'}`} />
{/* UI Layer - HUD */}
{(gameStatus === GameStatus.PLAYING || gameStatus === GameStatus.LEVEL_COMPLETE || gameStatus === GameStatus.PAUSED) && (
<div className="absolute top-0 left-0 w-full p-4 flex justify-between items-start z-10">
<div className="flex gap-4 pointer-events-none">
{/* Score */}
<div className="bg-white/90 backdrop-blur-sm p-3 rounded-xl border-2 border-amber-800/20 shadow-lg flex flex-col">
<span className="text-xs text-amber-800 font-bold uppercase tracking-wider">Score</span>
<span className="text-2xl text-amber-900 font-black font-mono">{score.toString().padStart(5, '0')}</span>
</div>
{/* Lives */}
<div className="bg-white/90 backdrop-blur-sm p-3 rounded-xl border-2 border-red-800/20 shadow-lg flex items-center gap-1">
{Array.from({length: MAX_LIVES}).map((_, i) => (
<Heart
key={i}
className={`w-6 h-6 ${i < lives ? 'fill-red-500 text-red-600' : 'fill-gray-300 text-gray-400'}`}
/>
))}
</div>
</div>
<div className="flex gap-4">
{/* Best Score */}
<div className="bg-white/90 backdrop-blur-sm p-3 rounded-xl border-2 border-amber-800/20 shadow-lg flex flex-col items-end pointer-events-none">
<span className="text-xs text-amber-800 font-bold uppercase tracking-wider flex items-center gap-1">
<Trophy size={12} /> Best
</span>
<span className="text-2xl text-amber-900 font-black font-mono">{highScore.toString().padStart(5, '0')}</span>
</div>
{/* Pause Button */}
<button
onClick={() => setGameStatus(gameStatus === GameStatus.PLAYING ? GameStatus.PAUSED : GameStatus.PLAYING)}
className="bg-amber-500 text-white p-4 rounded-xl shadow-lg hover:bg-amber-600 active:scale-95 transition-all"
>
<Pause fill="white" />
</button>
</div>
</div>
)}
{/* UI Layer - Pause Menu */}
{gameStatus === GameStatus.PAUSED && (
<div className="absolute inset-0 z-30 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="bg-white p-8 rounded-3xl shadow-2xl max-w-sm w-full text-center border-4 border-amber-700 animate-in fade-in zoom-in duration-200">
<h2 className="text-3xl font-black text-amber-900 mb-6">PAUSED</h2>
<div className="flex flex-col gap-3">
<button
onClick={() => setGameStatus(GameStatus.PLAYING)}
className="w-full py-3 bg-amber-500 text-white rounded-xl font-bold text-lg hover:bg-amber-600 shadow-md"
>
Resume Game
</button>
<button
onClick={handleRestart}
className="w-full py-3 bg-white text-amber-800 border-2 border-amber-200 rounded-xl font-bold text-lg hover:bg-amber-50"
>
Quit to Menu
</button>
</div>
</div>
</div>
)}
{/* UI Layer - Start Screen */}
{gameStatus === GameStatus.START_SCREEN && (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="bg-white p-8 rounded-3xl shadow-2xl max-w-md w-full text-center border-4 border-amber-700 mx-4 transform transition-all hover:scale-105 duration-300">
<div className="mb-6 flex justify-center">
<div className="w-24 h-24 bg-amber-100 rounded-full flex items-center justify-center border-4 border-amber-700 animate-bounce">
<span className="text-5xl">🐶</span>
</div>
</div>
<h1 className="text-4xl font-black text-amber-900 mb-2 tracking-tight">Mia's Clean Run</h1>
<p className="text-amber-800 mb-6 text-lg">Clean the house and avoid the cats!</p>
<div className="flex flex-col gap-4">
<button
onClick={handleStart}
className="group w-full py-4 bg-gradient-to-r from-amber-600 to-orange-600 text-white rounded-2xl font-bold text-xl shadow-lg hover:shadow-xl transition-all active:scale-95 flex items-center justify-center gap-2"
>
<Play className="fill-white" />
Play Now
</button>
<button
onClick={() => setGameStatus(GameStatus.CUSTOMIZE)}
className="w-full py-3 bg-white text-amber-800 border-2 border-amber-200 rounded-2xl font-bold text-lg hover:bg-amber-50 transition-all flex items-center justify-center gap-2"
>
<Palette size={20} />
Customize Mia
</button>
</div>
</div>
</div>
)}
{/* UI Layer - Customization Screen */}
{gameStatus === GameStatus.CUSTOMIZE && (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/60 backdrop-blur-md">
<div className="bg-white rounded-3xl shadow-2xl max-w-lg w-full border-4 border-amber-700 mx-4 h-[80vh] flex flex-col overflow-hidden">
<div className="p-6 pb-2 bg-amber-50 border-b border-amber-100">
<h2 className="text-3xl font-black text-amber-900 text-center">Style Your Pup!</h2>
</div>
{/* Tabs */}
<div className="flex border-b border-amber-100">
<button
onClick={() => setCustomizationTab('body')}
className={`flex-1 py-3 font-bold text-sm uppercase ${customizationTab === 'body' ? 'bg-white text-amber-600 border-b-2 border-amber-600' : 'bg-gray-50 text-gray-500'}`}
>
Body
</button>
<button
onClick={() => setCustomizationTab('head')}
className={`flex-1 py-3 font-bold text-sm uppercase ${customizationTab === 'head' ? 'bg-white text-amber-600 border-b-2 border-amber-600' : 'bg-gray-50 text-gray-500'}`}
>
Head
</button>
<button
onClick={() => setCustomizationTab('style')}
className={`flex-1 py-3 font-bold text-sm uppercase ${customizationTab === 'style' ? 'bg-white text-amber-600 border-b-2 border-amber-600' : 'bg-gray-50 text-gray-500'}`}
>
Clothes
</button>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-8 bg-white">
{customizationTab === 'body' && (
<>
<div>
<h3 className="text-sm font-bold text-amber-800 uppercase mb-3">Fur Color</h3>
<div className="grid grid-cols-4 gap-3">
{Object.entries(FUR_COLORS).map(([name, color]) => (
<button
key={name}
onClick={() => setCustomization(p => ({...p, furColor: color}))}
className={`h-12 rounded-full border-4 shadow-sm transition-transform active:scale-95 ${customization.furColor === color ? 'border-blue-500 scale-110' : 'border-gray-200'}`}
style={{ backgroundColor: color }}
/>
))}
</div>
</div>
<div>
<h3 className="text-sm font-bold text-amber-800 uppercase mb-3">Collar Color</h3>
<div className="grid grid-cols-5 gap-3">
{Object.entries(COLLAR_COLORS).map(([name, color]) => (
<button
key={name}
onClick={() => setCustomization(p => ({...p, collarColor: color}))}
className={`h-10 rounded-full border-4 shadow-sm transition-transform active:scale-95 ${customization.collarColor === color ? 'border-blue-500 scale-110' : 'border-gray-200'}`}
style={{ backgroundColor: color }}
/>
))}
</div>
</div>
</>
)}
{customizationTab === 'head' && (
<>
<div>
<h3 className="text-sm font-bold text-amber-800 uppercase mb-3">Hats</h3>
<div className="grid grid-cols-3 gap-3">
{[
{id: 'none', label: 'None', icon: '🚫'},
{id: 'party', label: 'Party', icon: '🎉'},
{id: 'tophat', label: 'Fancy', icon: '🎩'},
{id: 'bow', label: 'Bow', icon: '🎀'},
{id: 'cowboy', label: 'Cowboy', icon: '🤠'},
{id: 'crown', label: 'Royal', icon: '👑'},
].map((item) => (
<button
key={item.id}
onClick={() => setCustomization(p => ({...p, hat: item.id as any}))}
className={`py-3 rounded-xl border-2 font-medium text-sm transition-colors ${customization.hat === item.id ? 'bg-amber-100 border-amber-600 text-amber-900' : 'bg-gray-50 border-gray-200 text-gray-600'}`}
>
<span className="text-2xl block mb-1">{item.icon}</span>
{item.label}
</button>
))}
</div>
</div>
<div>
<h3 className="text-sm font-bold text-amber-800 uppercase mb-3">Glasses</h3>
<div className="grid grid-cols-3 gap-3">
{[
{id: 'none', label: 'None', icon: '🚫'},
{id: 'sunglasses', label: 'Cool', icon: '😎'},
{id: 'nerd', label: 'Smart', icon: '🤓'},
{id: '3d', label: 'Movie', icon: '🍿'},
].map((item) => (
<button
key={item.id}
onClick={() => setCustomization(p => ({...p, glasses: item.id as any}))}
className={`py-3 rounded-xl border-2 font-medium text-sm transition-colors ${customization.glasses === item.id ? 'bg-amber-100 border-amber-600 text-amber-900' : 'bg-gray-50 border-gray-200 text-gray-600'}`}
>
<span className="text-2xl block mb-1">{item.icon}</span>
{item.label}
</button>
))}
</div>
</div>
</>
)}
{customizationTab === 'style' && (
<div>
<h3 className="text-sm font-bold text-amber-800 uppercase mb-3">Outfits</h3>
<div className="grid grid-cols-2 gap-3">
{[
{id: 'none', label: 'Au Naturel', icon: '🐕'},
{id: 'bandana', label: 'Bandana', icon: '🧣'},
{id: 'vest', label: 'Service Vest', icon: '🦺'},
{id: 'superhero', label: 'Hero Cape', icon: '🦸'},
].map((item) => (
<button
key={item.id}
onClick={() => setCustomization(p => ({...p, shirt: item.id as any}))}
className={`py-4 px-3 rounded-xl border-2 font-medium text-sm transition-colors flex items-center gap-3 ${customization.shirt === item.id ? 'bg-amber-100 border-amber-600 text-amber-900' : 'bg-gray-50 border-gray-200 text-gray-600'}`}
>
<span className="text-2xl">{item.icon}</span>
{item.label}
</button>
))}
</div>
</div>
)}
</div>
<div className="p-6 border-t border-gray-100 bg-white">
<button
onClick={() => setGameStatus(GameStatus.START_SCREEN)}
className="w-full py-4 bg-amber-600 text-white rounded-2xl font-bold text-xl shadow-lg active:scale-95 hover:bg-amber-700 transition-colors"
>
Save & Close
</button>
</div>
</div>
</div>
)}
{/* UI Layer - Level Complete */}
{gameStatus === GameStatus.LEVEL_COMPLETE && (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/50 backdrop-blur-md">
<div className="bg-white p-8 rounded-3xl shadow-2xl max-w-md w-full text-center border-4 border-yellow-400 mx-4 animate-in fade-in slide-in-from-bottom-10 duration-500">
<Sparkles className="w-16 h-16 text-yellow-400 mx-auto mb-4 animate-spin-slow" />
<h2 className="text-4xl font-black text-amber-900 mb-2">Level Complete!</h2>
<p className="text-amber-700 mb-6 text-lg">Mia is a good girl!</p>
<div className="bg-yellow-50 rounded-xl p-4 mb-8 border-2 border-yellow-200">
<div className="text-sm text-yellow-800 uppercase font-bold mb-1">Current Score</div>
<div className="text-4xl font-black text-amber-900">{score}</div>
</div>
<button
onClick={handleNextLevel}
className="w-full py-4 bg-gradient-to-r from-yellow-400 to-amber-500 text-white rounded-2xl font-bold text-xl shadow-lg hover:shadow-xl transition-all active:scale-95 flex items-center justify-center gap-2"
>
Next Room <ArrowRight />
</button>
</div>
</div>
)}
{/* UI Layer - Game Over */}
{gameStatus === GameStatus.GAME_OVER && (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-red-900/40 backdrop-blur-md">
<div className="bg-white p-8 rounded-3xl shadow-2xl max-w-md w-full text-center border-4 border-red-500 mx-4 animate-in fade-in zoom-in duration-300">
<h2 className="text-4xl font-black text-red-600 mb-2">Oops!</h2>
<p className="text-gray-600 mb-6 text-lg">Mia ran out of lives!</p>
<div className="bg-amber-50 rounded-xl p-6 mb-8 border-2 border-amber-100">
<div className="text-sm text-amber-800 uppercase font-bold mb-1">Final Score</div>
<div className="text-5xl font-black text-amber-900">{score}</div>
</div>
<div className="flex gap-3">
<button
onClick={handleRestart}
className="flex-1 py-4 bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-2xl font-bold text-xl shadow-lg hover:shadow-xl transition-all active:scale-95 flex items-center justify-center gap-2"
>
<RotateCcw />
Retry
</button>
<button
onClick={() => setGameStatus(GameStatus.START_SCREEN)}
className="px-6 py-4 bg-gray-100 text-gray-700 rounded-2xl font-bold text-xl hover:bg-gray-200 transition-all active:scale-95 flex items-center justify-center"
>
<Home />
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default App;

View File

@@ -1,11 +1,20 @@
<div align="center"> <div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" /> <img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
<h1>Built with AI Studio</h2>
<p>The fastest path from prompt to production with Gemini.</p>
<a href="https://aistudio.google.com/apps">Start building</a>
</div> </div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/drive/1hILMyAxAYnMmNvWuSd6JAz1fmey0teWE
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

1551
components/GameCanvas.tsx Normal file

File diff suppressed because it is too large Load Diff

120
constants.ts Normal file
View File

@@ -0,0 +1,120 @@
// Physics & World
export const GRAVITY = 0.6;
export const JUMP_FORCE = -15;
export const DOUBLE_JUMP_FORCE = -12;
export const GROUND_HEIGHT = 80;
export const ACCELERATION = 0.5;
export const FRICTION = 0.85;
export const MAX_SPEED_BASE = 9; // Renamed from MAX_SPEED
export const ZOOMIES_SPEED_MULTIPLIER = 1.6;
// Progression
export const LEVEL_SCORE_THRESHOLD = 4000; // Increased significantly for longer levels
export const SPEED_INCREASE_PER_LEVEL = 0.5;
// Visual Feedback
export const SHAKE_INTENSITY = 15;
export const SHAKE_DURATION = 20;
// World Generation
export const CHUNK_SIZE = 1200; // How much world creates at once
// Player Dimensions & Stats
export const MIA_WIDTH = 64;
export const MIA_HEIGHT = 44;
export const MAX_LIVES = 3;
export const INVINCIBILITY_FRAMES = 120; // 2 seconds at 60fps
export const ZOOMIES_DURATION = 300; // 5 seconds at 60fps
// Bark Ability
export const BARK_RANGE = 250;
export const BARK_DURATION = 15; // Frames the visual lasts
export const BARK_COOLDOWN = 60; // Frames until can bark again
// Obstacle Dimensions
export const WEE_WIDTH_MIN = 50;
export const WEE_WIDTH_MAX = 90;
export const WEE_HEIGHT = 20;
export const POO_WIDTH_MIN = 30;
export const POO_WIDTH_MAX = 50;
export const CAT_WIDTH = 70; // Slightly wider for legs
export const CAT_HEIGHT = 50;
export const CAT_POUNCE_RANGE = 200;
export const CAT_POUNCE_VY = -13;
export const CAT_POUNCE_VX = 6;
export const BIRD_WIDTH = 45;
export const BIRD_HEIGHT = 30;
export const BIRD_SPEED = 3.5;
export const LEAF_WIDTH = 30;
export const LEAF_HEIGHT = 30;
export const LEAF_FALL_SPEED = 2.5;
export const ROOMBA_WIDTH = 50;
export const ROOMBA_HEIGHT = 20;
export const ROOMBA_SPEED = 2;
export const FLY_WIDTH = 16;
export const FLY_HEIGHT = 12;
export const FLY_SPEED = 3.5; // Chases you slightly slower than running speed
// Platform Dimensions
export const PLATFORM_HEIGHT = 20;
export const OTTOMAN_HEIGHT = 50;
export const COUNTER_HEIGHT = 120; // For kitchen
export const FRIDGE_HEIGHT = 160;
export const SOFA_HEIGHT = 70;
export const BED_HEIGHT = 80;
export const BATHTUB_HEIGHT = 70;
export const TOILET_HEIGHT = 60;
export const PLATFORM_WIDTH_MIN = 80;
export const PLATFORM_WIDTH_MAX = 150;
// Collectibles
export const CHICKEN_WIDTH = 55;
export const CHICKEN_HEIGHT = 35;
export const STEAK_WIDTH = 45;
export const STEAK_HEIGHT = 35;
export const BISCUIT_WIDTH = 30;
export const BISCUIT_HEIGHT = 30;
export const BONE_WIDTH = 30;
export const BONE_HEIGHT = 15;
// Colors
export const COLOR_WHITE = '#FFFFFF';
export const COLOR_WEE = '#FFD700'; // Gold
export const COLOR_POO = '#4B3621'; // Warm Black/Brown
export const COLOR_CAT = '#1a1a1a'; // Almost Black
export const COLOR_CHICKEN = '#FFA500'; // Orange/Gold
export const COLOR_BONE = '#F5F5DC'; // Beige
// Theme Colors
export const THEME_LIVING_ROOM_WALL = '#FFF5E1';
export const THEME_LIVING_ROOM_FLOOR = '#DEB887';
export const THEME_KITCHEN_WALL = '#E0FFFF'; // Light Cyan
export const THEME_KITCHEN_FLOOR = '#D3D3D3'; // Light Grey Tiles
export const THEME_GARDEN_WALL = '#87CEEB'; // Sky Blue
export const THEME_GARDEN_FLOOR = '#90EE90'; // Light Green Grass
export const THEME_BEDROOM_WALL = '#E6E6FA'; // Lavender
export const THEME_BEDROOM_FLOOR = '#F5DEB3'; // Wheat/Carpet
export const THEME_BATHROOM_WALL = '#F0F8FF'; // Alice Blue
export const THEME_BATHROOM_FLOOR = '#708090'; // Slate Grey tiles
// Customization Options
export const FUR_COLORS = {
RUSTY: '#A0522D',
GOLDEN: '#DAA520',
CHOCOLATE: '#5D4037',
OREO: '#333333',
PINK: '#FFB6C1',
WHITE: '#F0F0F0'
};
export const COLLAR_COLORS = {
RED: '#FF6347',
BLUE: '#4169E1',
PINK: '#FF69B4',
GREEN: '#32CD32',
BLACK: '#000000',
PURPLE: '#9370DB'
};

30
index.html Normal file
View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Mia's Clean Run</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Fredoka', sans-serif;
overflow: hidden;
touch-action: none;
}
</style>
<script type="importmap">
{
"imports": {
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
"react": "https://aistudiocdn.com/react@^19.2.0",
"react/": "https://aistudiocdn.com/react@^19.2.0/",
"lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0"
}
}
</script>
</head>
<body>
<div id="root"></div>
</body>
</html>

15
index.tsx Normal file
View File

@@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

5
metadata.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "Mia's Clean Run",
"description": "Jump over puddles, piles, and cats while collecting treats in this cute domestic adventure.",
"requestFramePermissions": []
}

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "mia's-clean-run",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react-dom": "^19.2.0",
"react": "^19.2.0",
"lucide-react": "^0.554.0"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

123
types.ts Normal file
View File

@@ -0,0 +1,123 @@
export interface Player {
x: number;
y: number;
vx: number; // Horizontal velocity
vy: number; // Vertical velocity
width: number;
height: number;
isGrounded: boolean;
jumpCount: number; // For double jump
runFrame: number;
facingRight: boolean; // For sprite flipping
isBarking: boolean;
barkTimer: number; // How long the bark visual lasts
barkCooldown: number; // Time until next bark
isZoomies: boolean; // Invincibility mode
zoomiesTimer: number;
lives: number;
maxLives: number;
invincibilityTimer: number;
}
export interface MiaCustomization {
furColor: string;
collarColor: string;
hat: 'none' | 'party' | 'tophat' | 'bow' | 'cowboy' | 'crown';
glasses: 'none' | 'sunglasses' | 'nerd' | '3d';
shirt: 'none' | 'bandana' | 'vest' | 'superhero';
}
export type ObstacleType = 'wee' | 'poo' | 'cat_walking' | 'cat_sleeping' | 'cat_pouncing' | 'bird' | 'leaf' | 'roomba' | 'fly';
export interface Obstacle {
id: number;
x: number;
y: number;
width: number;
height: number;
type: ObstacleType;
vx?: number; // Velocity for moving obstacles
vy?: number; // Vertical velocity for falling items
initialY?: number; // Reference for flying patterns
initialX?: number; // Reference for patrolling
patrolRange?: number; // How far to move back and forth
swayOffset?: number; // Random offset for leaf sway/animations
hasSpawnedFly?: boolean; // Track if a fly has emerged from this poo
pounceTriggered?: boolean; // For pouncing cats
isAsleep?: boolean; // For sleeping cats
}
export type PlatformType = 'ottoman' | 'books' | 'shelf' | 'counter' | 'sofa' | 'fridge' | 'bed' | 'bathtub' | 'toilet' | 'vanity';
export interface Platform {
id: number;
x: number;
y: number;
width: number;
height: number;
color: string;
type: PlatformType;
}
export type CollectibleType = 'chicken' | 'bone' | 'steak' | 'biscuit';
export interface Collectible {
id: number;
x: number;
y: number;
width: number;
height: number;
type: CollectibleType;
collected: boolean;
rotation: number; // For animation
}
export interface Particle {
id: number;
x: number;
y: number;
vx: number;
vy: number;
life: number;
color: string;
size: number;
}
export interface FloatingText {
id: number;
x: number;
y: number;
text: string;
life: number;
color: string;
}
export interface ParallaxObject {
id: number;
x: number;
y: number;
type: 'tree_bg' | 'cloud' | 'hill' | 'cityscape';
width: number;
height: number;
depth: number; // 0.1 (far) to 0.9 (near), affects scroll speed
}
export interface BackgroundObject {
id: number;
x: number;
y: number;
type: 'tree' | 'bush' | 'plant_pot' | 'window' | 'painting' | 'cabinet' | 'lamp' | 'towel_rack';
variant: number;
width?: number; // Added for windows
height?: number; // Added for windows
}
export enum GameStatus {
START_SCREEN = 'START_SCREEN',
CUSTOMIZE = 'CUSTOMIZE',
PLAYING = 'PLAYING',
PAUSED = 'PAUSED',
LEVEL_COMPLETE = 'LEVEL_COMPLETE',
GAME_OVER = 'GAME_OVER'
}

23
vite.config.ts Normal file
View File

@@ -0,0 +1,23 @@
import path from 'path';
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
server: {
port: 3000,
host: '0.0.0.0',
},
plugins: [react()],
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
}
}
};
});