import { useState, useRef, useEffect } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Link, useSearchParams } from "react-router"; import { canBreed } from "@botanica/genetics"; import { PlantPreview } from "../components/PlantPreview"; import { QueryMessage } from "../components/QueryMessage"; import { api, loadAllParents } from "../lib/api"; import type { PlantDto } from "@botanica/shared"; import { useGame } from "../lib/game"; import { newRequestId } from "../lib/requestId"; function Parent({ label, id, onChange, plants, }: { label: string; id: string; onChange: (id: string) => void; plants: PlantDto[]; }) { const plant = plants.find((p) => p.id === id); return (
{plant ? ( <>

{plant.name}

{plant.phenotype.pigment} · {plant.phenotype.leafShape} ·{" "} {plant.phenotype.leafPattern}

Generation {plant.generation} · {plant.rarity.tier} Inspect parent → ) : (
Select a parent to see its traits.
)}
); } export function BreedingLab() { const game = useGame(); const [search] = useSearchParams(); const parents = useQuery({ queryKey: ["parents"], queryFn: loadAllParents }), [a, setA] = useState(search.get("parent") ?? ""), [b, setB] = useState(""), [cursor, setCursor] = useState(), history = useQuery({ queryKey: ["breeding", cursor], queryFn: () => api.events(cursor), }), client = useQueryClient(); const key = useRef<{ pair: string; id: string } | null>(null); const cross = useMutation({ mutationFn: api.cross, onSuccess: () => { void client.invalidateQueries({ queryKey: ["game"] }); void client.invalidateQueries({ queryKey: ["parents"] }); void client.invalidateQueries({ queryKey: ["plants"] }); void client.invalidateQueries({ queryKey: ["collection-stats"] }); void client.invalidateQueries({ queryKey: ["breeding"] }); }, }); const resultHeading = useRef(null); useEffect(() => { if (cross.data) resultHeading.current?.focus(); }, [cross.data]); const list = parents.data ?? [], left = list.find((p) => p.id === a), right = list.find((p) => p.id === b); let message = "Choose two saved parents to check compatibility.", compatible = false; if (left && right) { if (a === b) message = "Select two different parent plants."; else { try { compatible = canBreed(left.speciesId, right.speciesId).compatible; } catch { compatible = false; } message = compatible ? "Compatible velvetleaf pair. The server will choose and save the child." : "These species have no approved compatibility or hybrid rule."; } } function choose(side: "A" | "B", id: string) { if (side === "A") setA(id); else setB(id); key.current = null; cross.reset(); } function breed() { const pair = a + ":" + b; if (!key.current || key.current.pair !== pair) key.current = { pair, id: newRequestId() }; cross.mutate({ parentAId: a, parentBId: b, idempotencyKey: key.current.id, }); } return ( <>

The next generation

Let their traits meet.

Choose two saved plants. Inheritance, mutations and the child's identity are decided once by the server.

Advanced genetics →
{parents.isPending ? ( ) : parents.error ? ( void parents.refetch()} /> ) : !list.length ? (

No saved parents yet

Grow and harvest two plants in your greenhouse. Breeding unlocks at level 3 and costs {game.breedCost} coins; both parents are kept.

Open greenhouse →
) : ( <>
choose("A", id)} plants={list} /> choose("B", id)} plants={list} />

{message}

{game.player.level < 3 ? "Reach level 3 to unlock breeding." : game.player.coins < game.breedCost ? "Not enough coins for this cross." : `${game.breedCost} coins per cross · instant offspring · parents kept.`}

{cross.error && (

{cross.error.message} Retry uses the same request key.

)} {cross.data && ( )}
)} {cross.data && (

A new discovery

Cross saved to your collection.

{cross.data.plant.name}

{cross.data.plant.phenotype.pigment} ·{" "} {cross.data.plant.phenotype.leafPattern} ·{" "} {cross.data.plant.rarity.tier}

{cross.data.event.mutationEvents.length ? cross.data.event.mutationEvents.length + " mutation events recorded." : "No mutations in this cross."}

Inspect child & lineage →
)}

The breeding journal

{cursor && ( )}
{history.isPending ? ( ) : history.error ? ( void history.refetch()} /> ) : history.data?.items.length ? ( <>
    {history.data.items.map((event) => (
  1. Parent {event.parentAId.slice(0, 8)} × Parent {event.parentBId.slice(0, 8)}
    View child →
  2. ))}
{history.data.nextCursor && ( )} ) : (

The first cross is still to come

Every saved offspring will leave a trace here.

)}
); }