mirror of
https://github.com/Tony0410/Mias-cloud-run.git
synced 2026-05-24 22:01:40 +08:00
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:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
383
App.tsx
Normal 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;
|
||||||
25
README.md
25
README.md
@@ -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
1551
components/GameCanvas.tsx
Normal file
File diff suppressed because it is too large
Load Diff
120
constants.ts
Normal file
120
constants.ts
Normal 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
30
index.html
Normal 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
15
index.tsx
Normal 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
5
metadata.json
Normal 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
22
package.json
Normal 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
29
tsconfig.json
Normal 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
123
types.ts
Normal 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
23
vite.config.ts
Normal 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, '.'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user