276 lines
8.8 KiB
TypeScript
276 lines
8.8 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { Link, useSearchParams } from "react-router";
|
|
import {
|
|
classifyRarity,
|
|
resolvePhenotype,
|
|
exampleGenome,
|
|
type Genome,
|
|
} from "@botanica/genetics";
|
|
import { stages, type GrowthStage, type Background } from "@botanica/renderer";
|
|
import { PlantPreview } from "../components/PlantPreview";
|
|
import { GenomeEditor } from "../components/GenomeEditor";
|
|
import { ExportActions } from "../components/ExportActions";
|
|
import { QueryMessage } from "../components/QueryMessage";
|
|
import { api, plantQuery } from "../lib/api";
|
|
export function Lab() {
|
|
const [search] = useSearchParams(),
|
|
sourceId = search.get("plant") ?? "";
|
|
const catalog = useQuery({ queryKey: ["catalog"], queryFn: api.catalog }),
|
|
source = useQuery({ ...plantQuery(sourceId), enabled: !!sourceId });
|
|
const [genome, setGenome] = useState<Genome>(() =>
|
|
structuredClone(exampleGenome),
|
|
),
|
|
[stage, setStage] = useState<GrowthStage>("flowering"),
|
|
[seed, setSeed] = useState(8155),
|
|
[golden, setGolden] = useState(false),
|
|
[background, setBackground] = useState<Background>("warm"),
|
|
[name, setName] = useState("My velvetleaf");
|
|
const client = useQueryClient();
|
|
const save = useMutation({
|
|
mutationFn: api.create,
|
|
onSuccess: () => {
|
|
void client.invalidateQueries({ queryKey: ["plants"] });
|
|
void client.invalidateQueries({ queryKey: ["collection-stats"] });
|
|
void client.invalidateQueries({ queryKey: ["parents"] });
|
|
},
|
|
});
|
|
useEffect(() => {
|
|
if (source.data) {
|
|
setGenome(structuredClone(source.data.genome));
|
|
setSeed(source.data.visualSeed);
|
|
setGolden(false);
|
|
setName((source.data.name + " study").slice(0, 60));
|
|
save.reset();
|
|
}
|
|
}, [source.data]);
|
|
const phenotype = useMemo(
|
|
() =>
|
|
resolvePhenotype("velvetleaf", genome, golden ? ["golden_veins"] : []),
|
|
[genome, golden],
|
|
),
|
|
rarity = classifyRarity(phenotype);
|
|
return (
|
|
<>
|
|
<section className="lab-heading">
|
|
<div>
|
|
<p className="eyebrow">The plant laboratory</p>
|
|
<h1>Grow something unexpected.</h1>
|
|
<p>
|
|
Edit paired alleles and see their expression. Save a designed
|
|
founder, then cross saved plants in the breeding lab.
|
|
</p>
|
|
</div>
|
|
<Link className="button-link secondary" to="/breed">
|
|
Open breeding lab →
|
|
</Link>
|
|
</section>
|
|
{sourceId && source.isPending && <QueryMessage loading />}
|
|
{source.error && (
|
|
<QueryMessage
|
|
error={source.error}
|
|
retry={() => void source.refetch()}
|
|
/>
|
|
)}
|
|
<section className="render-workbench editor-workbench">
|
|
<div className="specimen">
|
|
<div className="specimen-top">
|
|
<span>VELVETLEAF / LIVE PREVIEW</span>
|
|
<span>Genome v1 · Renderer v1</span>
|
|
</div>
|
|
<PlantPreview
|
|
phenotype={phenotype}
|
|
visualSeed={seed}
|
|
stage={stage}
|
|
background={background}
|
|
/>
|
|
<div className="specimen-bottom">
|
|
<div>
|
|
<h2>{name || "Velvetleaf"}</h2>
|
|
<p>
|
|
{phenotype.pigment} · {phenotype.leafShape} ·{" "}
|
|
{phenotype.leafPattern}
|
|
</p>
|
|
</div>
|
|
<PlantPreview
|
|
phenotype={phenotype}
|
|
visualSeed={seed}
|
|
stage={stage}
|
|
size={64}
|
|
/>
|
|
</div>
|
|
<div className="phenotype-inspector">
|
|
<h3>Resolved phenotype</h3>
|
|
<dl>
|
|
{(
|
|
[
|
|
"pigment",
|
|
"leafShape",
|
|
"leafPattern",
|
|
"growthHabit",
|
|
"flowerColor",
|
|
"flowerDensity",
|
|
] as const
|
|
).map((key) => (
|
|
<div key={key}>
|
|
<dt>{key.replace(/([A-Z])/g, " $1")}</dt>
|
|
<dd>{phenotype[key]}</dd>
|
|
</div>
|
|
))}
|
|
<div>
|
|
<dt>Derived rarity</dt>
|
|
<dd>
|
|
{rarity.tier} · {rarity.score}/100
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
<p>
|
|
{rarity.reasons
|
|
.map((r) => r.label + " +" + r.points)
|
|
.join("; ") || "No unusual traits."}
|
|
</p>
|
|
<small>
|
|
Policy v1 · preview classification
|
|
{golden ? " includes a manual mutation" : ""}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
<aside className="study-panel">
|
|
<h2>The genome workbench</h2>
|
|
{catalog.error ? (
|
|
<QueryMessage
|
|
error={catalog.error}
|
|
retry={() => void catalog.refetch()}
|
|
/>
|
|
) : (
|
|
<label>
|
|
Species
|
|
<select
|
|
aria-label="Species"
|
|
value="velvetleaf"
|
|
disabled={catalog.isPending}
|
|
>
|
|
{(
|
|
catalog.data?.species ?? [
|
|
{ id: "velvetleaf", name: "Velvetleaf" },
|
|
]
|
|
).map((s) => (
|
|
<option key={s.id} value={s.id}>
|
|
{s.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
)}
|
|
<GenomeEditor
|
|
genome={genome}
|
|
onChange={(value) => {
|
|
setGenome(value);
|
|
save.reset();
|
|
}}
|
|
/>
|
|
<div className="control-pair">
|
|
<label>
|
|
Growth stage
|
|
<select
|
|
value={stage}
|
|
onChange={(e) => setStage(e.target.value as GrowthStage)}
|
|
>
|
|
{stages.map((s) => (
|
|
<option key={s}>{s}</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Visual seed
|
|
<input
|
|
type="number"
|
|
min={-2147483648}
|
|
max={2147483647}
|
|
value={seed}
|
|
onChange={(e) => {
|
|
const value = Number(e.target.value);
|
|
if (
|
|
Number.isInteger(value) &&
|
|
value >= -2147483648 &&
|
|
value <= 2147483647
|
|
)
|
|
setSeed(value);
|
|
}}
|
|
/>
|
|
</label>
|
|
</div>
|
|
<label className="checkbox">
|
|
<input
|
|
type="checkbox"
|
|
checked={golden}
|
|
onChange={(e) => setGolden(e.target.checked)}
|
|
/>
|
|
Golden veins · manual preview only
|
|
</label>
|
|
<p className="panel-note">
|
|
Manual mutation previews appear in image exports. They are not saved
|
|
or recorded as bred mutations.
|
|
</p>
|
|
<label>
|
|
Preview & export background
|
|
<select
|
|
value={background}
|
|
onChange={(e) => setBackground(e.target.value as Background)}
|
|
>
|
|
<option value="warm">Warm ivory</option>
|
|
<option value="transparent">Transparent</option>
|
|
</select>
|
|
</label>
|
|
<ExportActions
|
|
phenotype={phenotype}
|
|
visualSeed={seed}
|
|
stage={stage}
|
|
background={background}
|
|
/>
|
|
<form
|
|
className="save-plant"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
save.mutate({
|
|
speciesId: "velvetleaf",
|
|
genome,
|
|
visualSeed: seed,
|
|
name,
|
|
});
|
|
}}
|
|
>
|
|
<label>
|
|
Plant name
|
|
<input
|
|
required
|
|
maxLength={60}
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
/>
|
|
</label>
|
|
<button type="submit" disabled={save.isPending || !name.trim()}>
|
|
{save.isPending ? "Saving…" : "Save designed plant"}
|
|
</button>
|
|
<p className="panel-note">
|
|
Saves a new generation 0 founder. Earlier plants stay unchanged.
|
|
</p>
|
|
{save.error && (
|
|
<p role="alert" className="error-text">
|
|
{save.error.message}
|
|
</p>
|
|
)}
|
|
{save.data && (
|
|
<p role="status" className="success-text">
|
|
Saved to your collection.{" "}
|
|
<Link to={"/plants/" + save.data.id}>
|
|
Inspect {save.data.name} →
|
|
</Link>
|
|
</p>
|
|
)}
|
|
</form>
|
|
</aside>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|