import React, { useState, useEffect, useRef, useCallback } from "react"; /* --------------------------------------------------------- DESIGN TOKENS cream #FBF3EC · blush #E3A9B8 · sage #93AC7B · butter #EEC183 mauve (text) #634252 · lace #FFFCF9 · plum accent #7D4F63 --------------------------------------------------------- */ const SEEDS = { lavender: { name: "Lavender", seed: "🪻", cost: 3, growMs: 16000 }, strawberry: { name: "Strawberry", seed: "🍓", cost: 4, growMs: 20000 }, mint: { name: "Mint", seed: "🌿", cost: 2, growMs: 12000 }, sunflower: { name: "Sunflower", seed: "🌻", cost: 3, growMs: 18000 }, }; const RECIPES = [ { id: "latte", name: "Lavender Latte", needs: { lavender: 2 }, pay: 16, icon: "☕" }, { id: "tart", name: "Strawberry Tart", needs: { strawberry: 2 }, pay: 17, icon: "🥧" }, { id: "tea", name: "Mint Tea", needs: { mint: 2 }, pay: 12, icon: "🍵" }, { id: "cookie", name: "Sunflower Cookie", needs: { sunflower: 2 }, pay: 13, icon: "🍪" }, { id: "mix", name: "Garden Mix Bowl", needs: { mint: 1, strawberry: 1 }, pay: 14, icon: "🥣" }, ]; const PET_TYPES = [ { id: "bunny", name: "Bunny", emoji: "🐰", cost: 25 }, { id: "cat", name: "Cat", emoji: "🐱", cost: 30 }, { id: "duck", name: "Duckling", emoji: "🦆", cost: 20 }, { id: "hedgehog", name: "Hedgehog", emoji: "🦔", cost: 35 }, { id: "fox", name: "Fox", emoji: "🦊", cost: 45 }, ]; const DECOR = [ { id: "rug", name: "Woven Rug", emoji: "🧶", cost: 10, spot: { left: "38%", top: "78%" } }, { id: "teapot", name: "Teapot", emoji: "🫖", cost: 10, spot: { left: "70%", top: "58%" } }, { id: "vase", name: "Flower Vase", emoji: "💐", cost: 12, spot: { left: "16%", top: "50%" } }, { id: "candles", name: "Candles", emoji: "🕯️", cost: 8, spot: { left: "58%", top: "48%" } }, { id: "books", name: "Bookshelf", emoji: "📚", cost: 20, spot: { left: "12%", top: "24%" } }, { id: "chair", name: "Rocking Chair", emoji: "🪑", cost: 15, spot: { left: "78%", top: "72%" } }, { id: "fire", name: "Fireplace", emoji: "🔥", cost: 18, spot: { left: "45%", top: "30%" } }, { id: "window", name: "Window Seat", emoji: "🪟", cost: 16, spot: { left: "84%", top: "28%" } }, ]; const STORAGE_KEY = "cozy-cottage-state-v1"; function freshState() { return { petals: 40, plots: Array(6).fill(null), inventory: { lavender: 0, strawberry: 0, mint: 0, sunflower: 0 }, customers: [], ownedPets: [], ownedDecor: [], cottageName: "Kulfi's Cottage", }; } function uid() { return Math.random().toString(36).slice(2, 9); } /* --------------------------------------------------------- MAIN APP --------------------------------------------------------- */ export default function CozyCottageSim() { const [state, setState] = useState(null); const [tab, setTab] = useState("cottage"); const [toast, setToast] = useState(null); const [renaming, setRenaming] = useState(false); const [nameDraft, setNameDraft] = useState(""); const [adoptDraft, setAdoptDraft] = useState({ open: null, name: "" }); const petCooldowns = useRef({}); const saveTimer = useRef(null); const spawnTimer = useRef(0); // load useEffect(() => { let cancelled = false; (async () => { try { const res = await window.storage.get(STORAGE_KEY, false); if (!cancelled && res && res.value) { setState(JSON.parse(res.value)); return; } } catch (e) { /* no save yet */ } if (!cancelled) setState(freshState()); })(); return () => { cancelled = true; }; }, []); // save (debounced) useEffect(() => { if (!state) return; if (saveTimer.current) clearTimeout(saveTimer.current); saveTimer.current = setTimeout(async () => { try { await window.storage.set(STORAGE_KEY, JSON.stringify(state), false); } catch (e) { /* ignore */ } }, 600); return () => clearTimeout(saveTimer.current); }, [state]); const showToast = useCallback((msg) => { setToast(msg); setTimeout(() => setToast((t) => (t === msg ? null : t)), 1800); }, []); // game tick: pet decay + customer spawn useEffect(() => { if (!state) return; const tick = setInterval(() => { setState((s) => { if (!s) return s; let next = { ...s }; // pet decay if (next.ownedPets.length) { next.ownedPets = next.ownedPets.map((p) => ({ ...p, hunger: Math.max(0, p.hunger - 1), happiness: Math.max(0, p.happiness - 1), })); } // spawn customer spawnTimer.current += 2; if (spawnTimer.current >= 14 && next.customers.length < 3) { spawnTimer.current = 0; const recipe = RECIPES[Math.floor(Math.random() * RECIPES.length)]; next.customers = [...next.customers, { id: uid(), recipeId: recipe.id }]; } return next; }); }, 2000); return () => clearInterval(tick); }, [state]); // force re-render for plant growth animation const [, forceTick] = useState(0); useEffect(() => { const t = setInterval(() => forceTick((n) => n + 1), 1000); return () => clearInterval(t); }, []); if (!state) { return (
🌸
Waking up the cottage…
); } /* ---------------- garden actions ---------------- */ const plantSeed = (idx, type) => { const s = SEEDS[type]; if (state.petals < s.cost) return showToast("Not enough petals 🌸"); setState((prev) => { const plots = [...prev.plots]; plots[idx] = { type, plantedAt: Date.now() }; return { ...prev, petals: prev.petals - s.cost, plots }; }); }; const harvestPlot = (idx) => { setState((prev) => { const plot = prev.plots[idx]; if (!plot) return prev; const s = SEEDS[plot.type]; const ready = Date.now() - plot.plantedAt >= s.growMs; if (!ready) return prev; const plots = [...prev.plots]; plots[idx] = null; const inventory = { ...prev.inventory, [plot.type]: prev.inventory[plot.type] + 1 }; return { ...prev, plots, inventory }; }); showToast("Harvested 🧺"); }; /* ---------------- cafe actions ---------------- */ const serveCustomer = (customerId, recipeId) => { const recipe = RECIPES.find((r) => r.id === recipeId); const canServe = Object.entries(recipe.needs).every( ([k, v]) => state.inventory[k] >= v ); if (!canServe) return showToast("Missing ingredients 🌿"); setState((prev) => { const inventory = { ...prev.inventory }; Object.entries(recipe.needs).forEach(([k, v]) => (inventory[k] -= v)); const customers = prev.customers.filter((c) => c.id !== customerId); return { ...prev, inventory, customers, petals: prev.petals + recipe.pay }; }); showToast(`+${recipe.pay} 🌸`); }; /* ---------------- pets actions ---------------- */ const adoptPet = (typeId) => { const t = PET_TYPES.find((p) => p.id === typeId); if (state.petals < t.cost) return showToast("Not enough petals 🌸"); setAdoptDraft({ open: typeId, name: "" }); }; const confirmAdopt = () => { const t = PET_TYPES.find((p) => p.id === adoptDraft.open); const name = adoptDraft.name.trim() || t.name; setState((prev) => ({ ...prev, petals: prev.petals - t.cost, ownedPets: [ ...prev.ownedPets, { id: uid(), typeId: t.id, name, hunger: 100, happiness: 100 }, ], })); setAdoptDraft({ open: null, name: "" }); showToast(`Welcome home, ${name}! 💕`); }; const feedPet = (petId, ingredient) => { if (state.inventory[ingredient] <= 0) return showToast("Out of that ingredient 🌾"); setState((prev) => ({ ...prev, inventory: { ...prev.inventory, [ingredient]: prev.inventory[ingredient] - 1 }, ownedPets: prev.ownedPets.map((p) => p.id === petId ? { ...p, hunger: Math.min(100, p.hunger + 30) } : p ), })); }; const pettingCooldownActive = (petId) => { const last = petCooldowns.current[petId] || 0; return Date.now() - last < 2500; }; const petThePet = (petId) => { if (pettingCooldownActive(petId)) return; petCooldowns.current[petId] = Date.now(); setState((prev) => ({ ...prev, ownedPets: prev.ownedPets.map((p) => p.id === petId ? { ...p, happiness: Math.min(100, p.happiness + 12) } : p ), })); }; /* ---------------- cottage actions ---------------- */ const buyDecor = (id) => { const d = DECOR.find((x) => x.id === id); if (state.petals < d.cost) return showToast("Not enough petals 🌸"); if (state.ownedDecor.includes(id)) return; setState((prev) => ({ ...prev, petals: prev.petals - d.cost, ownedDecor: [...prev.ownedDecor, id], })); showToast(`${d.name} placed ✨`); }; const saveName = () => { if (nameDraft.trim()) { setState((prev) => ({ ...prev, cottageName: nameDraft.trim() })); } setRenaming(false); }; return (
{/* header */}
{renaming ? ( setNameDraft(e.target.value)} onBlur={saveName} onKeyDown={(e) => e.key === "Enter" && saveName()} style={nameInput} /> ) : (

{ setNameDraft(state.cottageName); setRenaming(true); }} title="Click to rename" > {state.cottageName}

)}
a little world of your own 🌷
🌸 {state.petals}
{/* content */}
{tab === "cottage" && ( )} {tab === "garden" && ( )} {tab === "cafe" && ( )} {tab === "pets" && ( )}
{/* nav */}
{[ { id: "cottage", label: "Cottage", icon: "🏡" }, { id: "garden", label: "Garden", icon: "🌷" }, { id: "cafe", label: "Café", icon: "☕" }, { id: "pets", label: "Pets", icon: "🐾" }, ].map((t) => ( ))}
{toast &&
{toast}
} {adoptDraft.open && (
setAdoptDraft({ open: null, name: "" })}>
e.stopPropagation()}>
{PET_TYPES.find((p) => p.id === adoptDraft.open).emoji}
Name your new friend
setAdoptDraft((d) => ({ ...d, name: e.target.value }))} onKeyDown={(e) => e.key === "Enter" && confirmAdopt()} placeholder={PET_TYPES.find((p) => p.id === adoptDraft.open).name} style={modalInput} />
)}
); } /* --------------------------------------------------------- COTTAGE VIEW --------------------------------------------------------- */ function CottageView({ state, buyDecor }) { return (
{DECOR.filter((d) => state.ownedDecor.includes(d.id)).map((d) => (
{d.emoji}
))} {state.ownedPets.map((p, i) => { const t = PET_TYPES.find((x) => x.id === p.typeId); return (
{t.emoji}
); })} {state.ownedDecor.length === 0 && state.ownedPets.length === 0 && (
an empty little room, waiting to be filled
)}
{DECOR.map((d) => { const owned = state.ownedDecor.includes(d.id); return (
{d.emoji}
{d.name}
); })}
); } /* --------------------------------------------------------- GARDEN VIEW --------------------------------------------------------- */ function GardenView({ state, plantSeed, harvestPlot }) { const [pickerIdx, setPickerIdx] = useState(null); return (
{state.plots.map((plot, idx) => { if (!plot) { return ( ); } const s = SEEDS[plot.type]; const elapsed = Date.now() - plot.plantedAt; const ready = elapsed >= s.growMs; const pct = Math.min(100, Math.floor((elapsed / s.growMs) * 100)); return ( ); })}
{Object.entries(state.inventory).map(([k, v]) => (
{SEEDS[k].seed} {v}
))}
{pickerIdx !== null && (
setPickerIdx(null)}>
e.stopPropagation()}>
Choose a seed to plant
{Object.entries(SEEDS).map(([key, s]) => ( ))}
)}
); } /* --------------------------------------------------------- CAFE VIEW --------------------------------------------------------- */ function CafeView({ state, serveCustomer }) { return (
{state.customers.length === 0 && (
the café is quiet — guests will wander in soon 🕊️
)}
{state.customers.map((c) => { const recipe = RECIPES.find((r) => r.id === c.recipeId); const canServe = Object.entries(recipe.needs).every( ([k, v]) => state.inventory[k] >= v ); return (
{recipe.icon}
{recipe.name}
{Object.entries(recipe.needs) .map(([k, v]) => `${SEEDS[k].seed}×${v}`) .join(" ")}
); })}
{Object.entries(state.inventory).map(([k, v]) => (
{SEEDS[k].seed} {v}
))}
); } /* --------------------------------------------------------- PETS VIEW --------------------------------------------------------- */ function PetsView({ state, adoptPet, feedPet, petThePet }) { const [feedFor, setFeedFor] = useState(null); return (
{state.ownedPets.length === 0 && (
no companions yet — visit the shelter below 🐾
)}
{state.ownedPets.map((p) => { const t = PET_TYPES.find((x) => x.id === p.typeId); return (
{t.emoji}
{p.name}
); })}
{PET_TYPES.map((t) => (
{t.emoji}
{t.name}
))}
{feedFor && (
setFeedFor(null)}>
e.stopPropagation()}>
Feed with…
{Object.entries(state.inventory).map(([k, v]) => ( ))}
)}
); } function MiniBar({ label, value, color }) { return (
{label}
); } function SectionLabel({ text }) { return
{text}
; } /* --------------------------------------------------------- STYLES --------------------------------------------------------- */ const fontImport = ` @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,500;0,9..144,600;1,9..144,500&family=Quicksand:wght@400;500;600;700&display=swap'); @keyframes bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } } `; const outer = { minHeight: "100vh", width: "100%", background: "linear-gradient(180deg,#FBF3EC 0%,#F6E4E9 100%)", display: "flex", justifyContent: "center", alignItems: "flex-start", padding: "16px", fontFamily: "'Quicksand', sans-serif", boxSizing: "border-box", }; const frame = { width: "100%", maxWidth: 420, background: "#FFFCF9", borderRadius: 22, boxShadow: "0 12px 40px rgba(125,79,99,0.15)", overflow: "hidden", position: "relative", border: "1px solid #F1D9DE", }; const header = { display: "flex", justifyContent: "space-between", alignItems: "flex-start", padding: "18px 20px 14px", background: "linear-gradient(180deg,#F6DEE4 0%,#FFFCF9 100%)", }; const titleStyle = { fontFamily: "'Fraunces', serif", fontStyle: "italic", fontWeight: 600, fontSize: 21, color: "#634252", margin: 0, cursor: "pointer", }; const nameInput = { fontFamily: "'Fraunces', serif", fontStyle: "italic", fontSize: 19, color: "#634252", border: "none", borderBottom: "1.5px solid #E3A9B8", background: "transparent", outline: "none", padding: "0 0 2px", }; const petalPill = { background: "#fff", border: "1px solid #F1D9DE", borderRadius: 999, padding: "6px 12px", fontWeight: 700, color: "#7D4F63", fontSize: 14, boxShadow: "0 2px 6px rgba(125,79,99,0.08)", whiteSpace: "nowrap", }; const content = { padding: "16px 18px 10px", minHeight: 380, maxHeight: 520, overflowY: "auto", }; const sectionLabel = { fontFamily: "'Fraunces', serif", fontStyle: "italic", fontSize: 14, color: "#7D4F63", margin: "16px 0 8px", borderBottom: "1px dashed #EFD3D9", paddingBottom: 4, }; const grid3 = { display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 8, }; const shopCard = { background: "#FFF9F6", border: "1px solid #F1D9DE", borderRadius: 14, padding: "10px 6px", textAlign: "center", }; const itemName = { fontSize: 11, color: "#634252", margin: "4px 0 6px", fontWeight: 600 }; const buyBtn = { background: "#E3A9B8", color: "#fff", border: "none", borderRadius: 999, padding: "5px 8px", fontSize: 11, fontWeight: 700, cursor: "pointer", width: "100%", }; const ownedBtn = { ...buyBtn, background: "#E7E1D8", color: "#8a7f70", cursor: "default" }; const room = { position: "relative", height: 230, borderRadius: 16, background: "linear-gradient(180deg,#F3D9C9 0%,#F3D9C9 55%, #E9C9A9 55%, #E9C9A9 100%)", overflow: "hidden", border: "1px solid #F1D9DE", }; const roomFloor = { position: "absolute", bottom: 0, left: 0, right: 0, height: "45%", background: "repeating-linear-gradient(90deg,#D8AE86,#D8AE86 18px,#CFA179 18px,#CFA179 20px)", }; const roomEmptyLabel = { position: "absolute", top: "42%", width: "100%", textAlign: "center", fontSize: 12, color: "#8a6b78", fontStyle: "italic", }; const decorSprite = { position: "absolute", fontSize: 30, transform: "translate(-50%,-50%)" }; const petSprite = { position: "absolute", fontSize: 24, transform: "translate(-50%,-50%)", animation: "bob 1.6s ease-in-out infinite", }; const gardenGrid = { display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10, }; const plotBase = { height: 84, borderRadius: 14, border: "1.5px dashed #D8B7C2", background: "#FDF3F0", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", cursor: "pointer", }; const plotEmpty = { ...plotBase }; const plotFilled = { ...plotBase, border: "1.5px solid #E3C9A9", background: "#FBF0DF" }; const plotReady = { background: "#F3EFD4", border: "1.5px solid #C9D8A9", cursor: "pointer" }; const progressTrack = { width: "70%", height: 4, background: "#EFE0E6", borderRadius: 4, marginTop: 6, overflow: "hidden", }; const progressFill = { height: "100%", background: "#C9A9BB" }; const readyLabel = { fontSize: 9, color: "#7D8A54", marginTop: 4, fontWeight: 700 }; const inventoryRow = { display: "flex", flexWrap: "wrap", gap: 8 }; const invPill = { background: "#FFF9F6", border: "1px solid #F1D9DE", borderRadius: 999, padding: "5px 10px", fontSize: 13, color: "#634252", }; const emptyNote = { fontSize: 12, color: "#8a6b78", fontStyle: "italic", margin: "6px 0 14px" }; const customerCard = { display: "flex", alignItems: "center", gap: 10, background: "#FFF9F6", border: "1px solid #F1D9DE", borderRadius: 14, padding: "10px 12px", }; const primaryBtnSm = { background: "#93AC7B", color: "#fff", border: "none", borderRadius: 999, padding: "7px 10px", fontSize: 11, fontWeight: 700, cursor: "pointer", whiteSpace: "nowrap", }; const disabledBtnSm = { ...primaryBtnSm, background: "#E7E1D8", color: "#a89d8e", cursor: "not-allowed" }; const petCard = { display: "flex", alignItems: "center", gap: 10, background: "#FFF9F6", border: "1px solid #F1D9DE", borderRadius: 14, padding: "10px 12px", }; const smallBtn = { background: "#fff", border: "1px solid #E3A9B8", color: "#7D4F63", borderRadius: 999, padding: "4px 10px", fontSize: 10, fontWeight: 700, cursor: "pointer", }; const miniBarTrack = { flex: 1, height: 5, background: "#EFE0E6", borderRadius: 4, overflow: "hidden" }; const miniBarFill = { height: "100%" }; const navWrap = { position: "relative", marginTop: 4 }; const scallopTop = { height: 10, backgroundImage: "radial-gradient(circle 9px at 9px 0px, transparent 8px, #FFFCF9 9px)", backgroundSize: "18px 10px", backgroundRepeat: "repeat-x", background: "radial-gradient(circle 9px at 9px 9px, #FFFCF9 8px, transparent 9px) top / 18px 18px repeat-x, #F6DEE4", }; const nav = { display: "flex", background: "#FFFCF9", borderTop: "1px solid #F1D9DE", padding: "6px 6px 10px", }; const navBtn = { flex: 1, background: "transparent", border: "none", color: "#a5828f", padding: "6px 2px", borderRadius: 12, cursor: "pointer", }; const navBtnActive = { color: "#7D4F63", background: "#F6DEE4" }; const toastStyle = { position: "absolute", bottom: 78, left: "50%", transform: "translateX(-50%)", background: "#634252", color: "#fff", padding: "8px 16px", borderRadius: 999, fontSize: 12, boxShadow: "0 6px 16px rgba(0,0,0,0.2)", }; const modalOverlay = { position: "absolute", inset: 0, background: "rgba(99,66,82,0.35)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24, zIndex: 5, }; const modalCard = { background: "#FFFCF9", borderRadius: 18, padding: "22px 20px", width: "100%", maxWidth: 300, textAlign: "center", boxShadow: "0 10px 30px rgba(0,0,0,0.2)", }; const modalInput = { width: "100%", border: "1px solid #E3A9B8", borderRadius: 10, padding: "8px 10px", fontSize: 14, outline: "none", marginBottom: 12, boxSizing: "border-box", fontFamily: "'Quicksand', sans-serif", }; const primaryBtn = { background: "#E3A9B8", color: "#fff", border: "none", borderRadius: 999, padding: "9px 18px", fontWeight: 700, cursor: "pointer", fontSize: 13, }; const seedOption = { display: "flex", alignItems: "center", background: "#FFF9F6", border: "1px solid #F1D9DE", borderRadius: 12, padding: "8px 12px", cursor: "pointer", fontSize: 13, color: "#634252", fontFamily: "'Quicksand', sans-serif", }; const card = { background: "#FFFCF9", borderRadius: 18, boxShadow: "0 8px 24px rgba(0,0,0,0.1)" };