# Project export: Plantasia

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Learn to take care of your plant in a fun game inspired by Plants Vs. Zombies.
- Devpost: https://devpost.com/software/plantasia
- GitHub: https://github.com/suchithh/plantasia
- Video: https://www.youtube.com/embed/0E57ahmUZ9E?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — suchi (4 commits)

## Devpost submission (written by the team)

### Inspiration

I've killed every plant I've ever owned. Either overwatered it, underwatered it, kept it away from sunlight or simply forgot it existed for two weeks. But I also spent my childhood playing Plants vs Zombies, a tower defense game where you defeat zombies using plants and powerups for your plants, obsessed with defending those little plants from zombies. The idea hit me: what if the zombies were the diseases, and you defeat them by actually learning to care for your plant? That's Plantasia.

### What it does

Plantasia turns real plant care into a tower defense game. You scan your actual houseplant, AI identifies it and gives it a personality, and it lives in your virtual garden as a character. A physical soil moisture sensor monitors your plant via Bluetooth. When something's wrong such as the soil being too dry, too wet, or you haven't checked in; a zombie spawns and heads toward your plant. Each zombie is a real disease: Drownface is root rot, Thirster is dehydration. To defeat them, you complete quests (generated based on the plant using Gemini) that teach you what's actually wrong through "chatting" with your plant and how to fix it. Water your plant in real life, the sensor detects it, quest completes, zombie dies. Your plant thanks you virtually and in real life. You earn coins for streaks, daily check-ins, and defeating zombies, which you spend on cosmetics and garden expansion. Miss your check-ins? More zombies. Let a zombie reach your plant? It's not gonna make it.

### How we built it

React and three.js for the game environment. ESP32 microcontroller with a capacitive soil moisture sensor handles the hardware side, communicating via Bluetooth LE. Plant identification and health analysis run through GPT5. Chat, personality generation and quests use Gemini and makes sure to serve you the most accurate information. The pixel art style was inspired by the original PvZ aesthetic: colorful, not dark, because plant care should feel fun.

### Challenges we ran into

BLE is pain. Getting the ESP32 to reliably stream data to React Native took longer than building the actual game logic. Sensor calibration was another problem. Balancing the game, specifically the zombies was challenging. If they're too aggressive, it's stressful. Too passive, nobody cares. 3D sprites were made from scratch (not downloaded from the internet) with the help of Claude, so there was lots of trial and error

### Accomplishments we're proud of

The live sensor integration actually works. When I water my fern during the demo, you see the moisture percentage climb in real-time and the quest auto-completes. I'm also proud of the educational loop: you literally can't defeat a zombie without learning something about plant care first. It was my first time making a game with Javascript frameworks so it was technically pretty challenging getting up to speed with everything.

### What we learned

Plants are harder to keep alive than code. Capacitive sensors are better than resistive ones (less corrosion), BLE has tons of different ways to fail silently. Creating 3D models in three.js is a pain even with Claude. Creative game design takes a lot of work.

### What's next

Creating better harness for the AI to actually understand your plant along with the context about how you take care of your plant to cater better to your interests. Adding more sensors to the hardware kit to enable more quests.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 38 recognized source files, 364 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (41 of 41)

```
.gitignore
idea.md
index.html
keys.md
package.json
src/App.tsx
src/components/checkin/CheckInResult.tsx
src/components/checkin/DailyCheckIn.tsx
src/components/garden/GardenHUD.tsx
src/components/garden/GardenScene.tsx
src/components/garden/PlantModel.tsx
src/components/garden/SensorOverlay.tsx
src/components/garden/ZombieModel.tsx
src/components/plant/PlantChat.tsx
src/components/plant/PlantDetailPanel.tsx
src/components/quest/QuestPanel.tsx
src/components/scan/ScanView.tsx
src/components/sensor/SensorPairScreen.tsx
src/components/shop/ShopPanel.tsx
src/components/ui/BottomDock.tsx
src/components/zombie/ZombieEncounter.tsx
src/components/zombie/ZombieInfoPanel.tsx
src/constants/colors.ts
src/constants/quests.ts
src/constants/typography.ts
src/constants/zombies.ts
src/index.css
src/main.tsx
src/services/bluetooth.ts
src/services/gemini.ts
src/services/hardwareSimulator.ts
src/services/mockData.ts
src/services/sensorBridge.ts
src/services/shopData.ts
src/services/turso.ts
src/stores/gameStore.ts
src/types/index.ts
src/utils/animationUtils.ts
src/vite-env.d.ts
tsconfig.json
vite.config.ts
```

### Dependencies

- package.json: @libsql/client@^0.14.0, @react-three/drei@^10.7.7, @react-three/fiber@^9.5.0, @types/react@^19.0.0, @types/react-dom@^19.0.0, @types/three@^0.182.0, @vitejs/plugin-react@^4.3.4, canvas-confetti@^1.9.4, framer-motion@^12.34.0, lucide-react@^0.564.0, react@^19.0.0, react-dom@^19.0.0, sonner@^2.0.7, three@^0.182.0, typescript@~5.6.2, vite@^6.0.0, zustand@^5.0.3

### Recent commits (newest first)

- bug fixes
- Improve plant location and appearance
- improve fidelity of plant and zombie
- add quest logic
- add controls to board
- hud polish
- gameplay loop
- Make ui better
- intial interface
- intial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### keys.md

```markdown
Turso token and databse url
eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3NzA5NjIyNjAsImlkIjoiOTk4OGYzMGItMGNhYi00ODlmLThlYTUtOWRhODQ0ZGY5MDEyIiwicmlkIjoiMmE3NWQyZTMtMTRkMy00NmUzLTg2NzEtOTYxNTllYjI5ZmMyIn0.JQTI05mxPa7o18IXhOIroBStYasvrn_nXD-g5tzemN_KwxpBnJvOd9eK3QEynLhCkDWEh6W2Xl2X0zZyBjYUBw
libsql://treehacks-plantasia-suchithh.aws-us-east-1.turso.io
Gemini free tier: AIzaSyAuIHq21bLV5i29n4R3O8iaC3fhWMo6qqU

```

### idea.md

```markdown
# Plantasia: Guardians — Vision & Spec for AI Tools

> **For:** Antigravity AI / Stitch AI / Claude Code / Any AI coding tool
> **Context:** TreeHacks 2026 hackathon (36 hours, solo hacker)
> **Inspiration:** Plants vs Zombies (the original — colorful, goofy, educational)

---

## One-Sentence Pitch

A mobile game where you scan your real houseplants, they become characters in an isometric garden, and you protect them from disease-zombies by learning about plant care and actually doing it.

---

## The Vibe

**This is NOT a dark/grim game.** Think original Plants vs Zombies:
- Bright, colorful, but still stylized
- Zombies are goofy and educational, not scary
- It's an adventure, not a horror game
- You're learning while playing
- Winning feels good, losing is funny (not punishing)

**Visual Direction:**
- Isometric garden view (like Stardew Valley, Hay Day)
- Stylized plant characters (not realistic, not chibi-cute)
- Zombies are cartoony disease mascots
- Daytime garden with nice lighting (not dark/nighttime)
- Game world you want to hang out in

**What it's NOT:**
- A traditional app with cards and lists
- Dark/moody/grim aesthetic
- Scary or stressful
- Cluttered with UI elements

---

## Core Concept

```
┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│   YOUR REAL PLANTS       →      GAME CHARACTERS                    │
│   (photos you take)             (AI-generated avatars)              │
│                                                                     │
│   PLANT DISEASES         →      ZOMBIE ENEMIES                     │
│   (root rot, pests, etc)        (educational mascots)               │
│                                                                     │
│   LEARNING + DOING       →      DEFEATING ZOMBIES                  │
│   (care for your plant)         (earn rewards)                      │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
```

---

## Where AI Slots In

There are **4 key AI integration points**. For the hackathon prototype, these can be simulated with mock data, but the UI should be designed to accommodate real AI responses.

### 1. 🌱 Plant Avatar Generation

**Trigger:** User scans a plant with the camera

**What AI Does:**
- Identifies the plant species from the photo
- Generates a stylized character avatar for that plant
- Assigns a personality type

**User Experience:**
```
[User takes photo of their monstera]
     ↓
[Loading: "Creating your plant's character..."]
     ↓
[Avatar appears: A stylized monstera character with personality]
[Name suggestion: "Gerald" | Personality: "Dramatic Diva"]
     ↓
[User confirms or edits name]
[Plant joins the garden]
```

**For Prototype:** 
- Use a set of 5-6 pre-made plant avatars
- Randomly assign based on detected plant type
- Species detection can use GPT-4V or be sim
[truncated — 13351 more characters]
```

### package.json

```
{
  "name": "plantasia-guardians",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@libsql/client": "^0.14.0",
    "@react-three/drei": "^10.7.7",
    "@react-three/fiber": "^9.5.0",
    "canvas-confetti": "^1.9.4",
    "framer-motion": "^12.34.0",
    "lucide-react": "^0.564.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "sonner": "^2.0.7",
    "three": "^0.182.0",
    "zustand": "^5.0.3"
  },
  "devDependencies": {
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0",
    "@types/three": "^0.182.0",
    "@vitejs/plugin-react": "^4.3.4",
    "typescript": "~5.6.2",
    "vite": "^6.0.0"
  }
}

```

### src/main.tsx

```typescript
import { createRoot } from 'react-dom/client';
import { App } from './App';
import './index.css';

createRoot(document.getElementById('root')!).render(<App />);

```

### src/App.tsx

```typescript
// Plantasia: Guardians — App Shell
import { useEffect, useState } from 'react';
import { GardenScene } from './components/garden/GardenScene';
import { GardenHUD } from './components/garden/GardenHUD';
import { SensorOverlay } from './components/garden/SensorOverlay';
import { PlantDetailPanel } from './components/plant/PlantDetailPanel';
import { PlantChat } from './components/plant/PlantChat';
import { ZombieInfoPanel } from './components/zombie/ZombieInfoPanel';
import { QuestPanel } from './components/quest/QuestPanel';
import { ZombieEncounter } from './components/zombie/ZombieEncounter';
import { Toaster } from 'sonner';
import { ScanView } from './components/scan/ScanView';
import { SensorPairScreen } from './components/sensor/SensorPairScreen';
import { DailyCheckIn } from './components/checkin/DailyCheckIn';
import { CheckInResult } from './components/checkin/CheckInResult';
import { ShopPanel } from './components/shop/ShopPanel';
import { BottomDock } from './components/ui/BottomDock';
import { useGameStore } from './stores/gameStore';
import { getSensorBridge } from './services/sensorBridge';
import type { GameEvent } from './types';
import {
    Skull,
    Swords,
    PartyPopper,
    Heart,
    Shield,
    Sprout,
    Coins,
    Gamepad2,
    Radio,
    Hand,
    Droplets,
    CloudSun,
    CircleCheck,
    CircleStop,
    Plus,
    X,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';

// Toast messages with Lucide icons
const toastConfig: Record<string, { message: string, Icon: LucideIcon, color: string }> = {
    zombie_spawned: { message: 'A zombie has appeared!', Icon: Skull, color: '#EF4444' },
    zombie_defeated: { message: 'Zombie defeated! +50 coins', Icon: Swords, color: '#22C55E' },
    quest_completed: { message: 'Quest completed!', Icon: PartyPopper, color: '#FBBF24' },
    plant_touched: { message: 'Plant says hello! +15 coins', Icon: Heart, color: '#F472B6' },
    shield_built: { message: 'Shield strength increasing!', Icon: Shield, color: '#38BDF8' },
    plant_added: { message: 'New plant added to garden!', Icon: Sprout, color: '#22C55E' },
    coins_earned: { message: 'Coins earned!', Icon: Coins, color: '#FBBF24' },
    daily_checkin: { message: 'Daily check-in complete!', Icon: Heart, color: '#22C55E' },
    shop_purchase: { message: 'Item purchased!', Icon: Coins, color: '#FBBF24' },
};

function GameToast({ event }: { event: GameEvent }) {
    const [visible, setVisible] = useState(true);
    const config = toastConfig[event.type] || { message: 'Something happened!', Icon: Sprout, color: '#3B82F6' };

    useEffect(() => {
        const timer = setTimeout(() => setVisible(false), 3500);
        return () => clearTimeout(timer);
    }, []);

    if (!visible) return null;
    if (event.type === 'challenger_approaching') return null;

    const { Icon } = config;

    return (
        <div className="game-toast" style={{ borderLeftColor: config.color }}>
            <span className="toast-icon"><Icon size={18} color={config.color} /></span>
            <span>{config.message}</span>
        </div>
    );
}

// Demo control panel
function DemoControls({ sensorConnection, onConnectSim, onConnectBLE, onDisconnect, onTouch, onMoisture, onSetMoisture }: {
    sensorConnection: string;
    onConnectSim: () => void;
    onConnectBLE: () => void;
    onDisconnect: () => void;
    onTouch: () => void;
    onMoisture: (trend: 'stable' | 'rising' | 'falling') => void;
    onSetMoisture: (value: number) => void;
}) {
    const [expanded, setExpanded] = useState(false);

    return (
        <div className={`demo-controls ${expanded ? 'expanded' : ''}`}>
            <button className="demo-toggle" onClick={() => setExpanded(!expanded)}>
                {expanded ? <X size={18} /> : <Gamepad2 size={18} />}
            </button>
            {expanded && (
                <div className="demo-panel">
                    <div className="demo-title">Demo Controls</div>
                    {sensorConnection === 'disconnected' ? (
                        <div className="demo-group">
                            <button className="demo-btn primary" onClick={onConnectSim}>
                                <Gamepad2 size={16} /> Start Demo
                            </button>
                            <button className="demo-btn" onClick={onConnectBLE}>
                                <Radio size={16} /> Real Sensor
                            </button>
                        </div>
                    ) : (
                        <>
                            <div className="demo-group">
                                <button className="demo-btn success" onClick={onTouch}>
                                    <Hand size={16} /> Touch Plant
                                </button>
                            </div>
                            <div className="demo-group">
                                <div className="demo-label">Moisture Presets</div>
                                <button className="demo-btn danger" onClick={() => onSetMoisture(25)}>
                                    <CloudSun size={16} /> Set 25%
                                </button>
                                <button className="demo-btn success" onClick={() => onSetMoisture(55)}>
                                    <CircleCheck size={16} /> Set 55%
                                </button>
                            </div>
                            <div className="demo-group">
                                <div className="demo-label">Moisture Trends</div>
                                <button className="demo-btn danger" onClick={() => onMoisture('rising')}>
                                    <Droplets size={16} /> Overwater
                                </button>
                                <button className="demo-btn warning" onClick={() => onMoisture('falling')}>
                                    <CloudSun size={16} /> Dry Out
                         
[truncated — 4102 more characters]
```

### src/types/index.ts

```typescript
// Plantasia: Guardians — All TypeScript Types

// ─── Personality System ───

export type PersonalityType = 'dramatic' | 'chill' | 'anxious' | 'wise' | 'cheerful';

export interface Personality {
    type: PersonalityType;
    quirks: string[];
    speakingStyle: string;
}

export type Mood = 'happy' | 'neutral' | 'worried' | 'excited' | 'dramatic' | 'sleepy';
export type PlantReaction = 'bounce' | 'shake' | 'wiggle' | 'spin' | 'heart' | 'sad';


// ─── Plant Characters ───

export type PlantHealthStatus = 'healthy' | 'threatened' | 'in_battle' | 'damaged' | 'dead';

export interface PlantCharacter {
    id: string;
    species: string;
    commonName: string;
    nickname: string;
    personality: Personality;
    mood: Mood;
    activeReaction?: PlantReaction;
    avatarColor: string; // for procedural avatar
    healthStatus: PlantHealthStatus;
    position: { x: number; y: number; z: number }; // garden grid position
    xp: number;
    level: number;
    happiness: number; // 0-100
    streak: number;
    shieldStrength: number; // 0-100, builds when soil is healthy
    lastCheckIn?: string; // ISO date of last daily check-in
    createdAt: string;
}

// ─── Zombie Enemies ───

export type ZombieType = 'drownface' | 'thirster' | 'sunscorch' | 'fungus_phil' | 'the_swarm' | 'neglecto';
export type ZombieState = 'approaching' | 'spawn_animation' | 'fighting' | 'dying' | 'defeated';

export interface ZombieLore {
    backstory: string;
    whatIsIt: string;
    whyItHappens: string;
    earlyWarnings: string[];
    science: string;
    weakness: string;
    plantsAffected: string;
}

export interface ZombieEnemy {
    id: string;
    type: ZombieType;
    name: string;
    disease: string;
    subtitle: string;
    state: ZombieState;
    targetPlantId: string;
    threatLevel: 1 | 2 | 3 | 4 | 5;
    lore: ZombieLore;
    spawnPhrase?: string; // "A NEW CHALLENGER APPROACHING!"
    color: string;
    emoji: string;
    defeatSteps: string[];
    position: { x: number; y: number; z: number };
    progress: number; // 0-1, how close to plant
}

// ─── Quest System ───

export type QuestType = 'care' | 'battle' | 'daily' | 'photo' | 'touch';

export type QuestActionType = 'navigate' | 'interaction' | 'observation' | 'care';

export interface QuestAction {
    type: QuestActionType;
    target?: string;
    value?: number;
}

export interface QuestStep {
    id: string;
    description: string;
    subtitle?: string;
    action?: QuestAction;
    completed: boolean;
}

export interface QuestReward {
    coins: number;
    xp: number;
    trophy?: string;
}

export interface Quest {
    id: string;
    title: string;
    description: string;
    type: QuestType;
    steps: QuestStep[];
    reward: QuestReward;
    zombieId?: string; // linked zombie-specific quest
    plantId?: string;
    timeLimit?: number; // hours
    active: boolean;
    completed: boolean;
    educational: string; // tie-in to learning
}

// ─── Sensor / Hardware ───

export interface SensorData {
    soilMoisture: number; // 0-100 percentage
    gsrValue: number;
    gsrVariance: number;
    touchDetected: boolean;
    timestamp: number;
}

export type SensorConnectionState = 'disconnected' | 'connecting' | 'connected_real' | 'connected_simulated';
export type EmotionalState = 'sleepy' | 'content' | 'curious' | 'excited' | 'startled';

// ─── UI State ───

export type ActivePanel = 'none' | 'plant_detail' | 'plant_chat' | 'zombie_info' | 'quest_list' | 'scan' | 'sensor_pair' | 'daily_checkin' | 'checkin_result' | 'shop' | 'victory_scan';

export interface ChatMessage {
    id: string;
    sender: 'plant' | 'user';
    text: string;
    timestamp: number;
}

// ─── Shop & Garden ───

export type GardenSize = 'small' | 'medium' | 'large';

export interface ShopItem {
    id: string;
    name: string;
    description: string;
    price: number;
    category: 'garden_upgrade' | 'cosmetic';
    icon: string;        // lucide icon name
    owned: boolean;
    gardenSize?: GardenSize; // for garden upgrades
}

// ─── Daily Check-In ───

export interface CheckInRecord {
    id: string;
    plantId: string;
    date: string; // ISO date
    healthy: boolean;
    coins: number;
    zombieType?: ZombieType;
    tips?: string[];
}

export interface CheckInAnalysis {
    healthy: boolean;
    confidence: number;
    tips: string[];
    coins: number;
    // unhealthy path
    zombieType?: ZombieType;
    disease?: string;
    severity?: 'minor' | 'moderate' | 'severe' | 'critical';
    explanation?: string;
    defeatSteps?: string[];
}

// ─── Care Tasks ───

export interface CareTask {
    id: string;
    plantId: string;
    title: string;
    description: string;
    coins: number;
    completed: boolean;
    zombieType: ZombieType; // zombie that spawns if neglected
    sensorDriven: boolean;  // true = from sensor data
}

// ─── Gemini AI Responses ───

export interface IdentificationResult {
    species: string;
    commonName: string;
    confidence: number;
    careLevel: 'easy' | 'moderate' | 'difficult';
}

export interface PersonalityResult {
    suggestedName: string;
    personality: Personality;
}

export interface DiagnosisResult {
    zombieType: ZombieType;
    disease: string;
    confidence: number;
    severity: 'minor' | 'moderate' | 'severe' | 'critical';
    explanation: string;
    defeatSteps: string[];
}

// ─── Game Events ───

export type GameEvent =
    | { type: 'zombie_spawned'; zombieId: string; zombieType: ZombieType; targetPlantId: string }
    | { type: 'zombie_defeated'; zombieId: string }
    | { type: 'plant_reaction'; plantId: string; reaction: PlantReaction }
    | { type: 'challenger_approaching'; zombieId: string; zombieType: ZombieType } // For the Smash Bros overlay
    | { type: 'quest_completed'; questId: string; reward: QuestReward }
    | { type: 'plant_touched'; plantId: string; coins: number }
    | { type: 'shield_built'; plantId: string; strength: number }
    | { type: 'plant_
[truncated — 234 more characters]
```

### vite.config.ts

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    open: true,
  },
})

```

### index.html

```html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta name="description"
    content="Plantasia: Guardians — Protect your plants from disease-zombies by learning about plant care!" />
  <title>Plantasia: Guardians</title>
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <link
    href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;500;600;700;800&display=swap"
    rel="stylesheet" />
</head>

<body>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>

</html>
```

### src/vite-env.d.ts

```typescript
/// <reference types="vite/client" />

```

### src/utils/animationUtils.ts

```typescript
import * as THREE from 'three';

/**
 * Smoothly damps a value towards a target using frame-rate independent lerp.
 * @param current The current value
 * @param target The target value
 * @param lambda Smoothing factor (higher is faster, e.g., 5-20)
 * @param delta Time delta in seconds
 * @returns The new damped value
 */
export function damp(current: number, target: number, lambda: number, delta: number): number {
    return THREE.MathUtils.damp(current, target, lambda, delta);
}

/**
 * Smoothly damps a Vector3 towards a target.
 */
export function dampv3(current: THREE.Vector3, target: THREE.Vector3, lambda: number, delta: number) {
    current.x = THREE.MathUtils.damp(current.x, target.x, lambda, delta);
    current.y = THREE.MathUtils.damp(current.y, target.y, lambda, delta);
    current.z = THREE.MathUtils.damp(current.z, target.z, lambda, delta);
}

/**
 * Smoothly damps a Euler rotation towards a target.
 */
export function dampe(current: THREE.Euler, target: THREE.Euler, lambda: number, delta: number) {
    current.x = THREE.MathUtils.damp(current.x, target.x, lambda, delta);
    current.y = THREE.MathUtils.damp(current.y, target.y, lambda, delta);
    current.z = THREE.MathUtils.damp(current.z, target.z, lambda, delta);
}

```

[28 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]