mirror of
https://github.com/Tony0410/Mias-cloud-run.git
synced 2026-05-24 13:52:02 +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:
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;
|
||||
Reference in New Issue
Block a user