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 (
);
}
/* ---------------- 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}
);
})}
{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 (
);
}
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)" };