feat(@projects/@magic-civilization): add dynamic tab routing for world generation

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-30 16:52:02 -04:00
parent 3b0bdb0ba1
commit 1e431d389f
4 changed files with 694 additions and 339 deletions

View file

@ -49,7 +49,8 @@ export function App(): React.ReactElement {
<Route path="/replay" element={<ReplayPage />} />
<Route path="/gd-rust" element={<GdRustBridgePage />} />
<Route path="/gd-rust/map" element={<GdRustMapPage />} />
<Route path="/world-gen" element={<WorldGenPage />} />
<Route path="/world-gen" element={<WorldGenPage />} />
<Route path="/world-gen/:tab" element={<WorldGenPage />} />
</Routes>
</BrowserRouter>
);

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import styled from "styled-components";
import { t } from "../theme";
import { MapPanel } from "./WorldGen/MapPanel";
@ -8,7 +8,9 @@ import { BiomeTransitions } from "./WorldGen/BiomeTransitions";
import { NoiseAnatomy } from "./WorldGen/NoiseAnatomy";
import { ForestLab } from "./WorldGen/ForestLab";
type Tab = "map" | "pipeline" | "catalog" | "transitions" | "noise" | "forest";
type Tab = "map" | "pipeline" | "catalog" | "transitions" | "noise" | "forest-lab";
const VALID_TABS: readonly Tab[] = ["map", "pipeline", "catalog", "transitions", "noise", "forest-lab"];
const TABS: { id: Tab; label: string }[] = [
{ id: "map", label: "⬡ World Map" },
@ -16,7 +18,7 @@ const TABS: { id: Tab; label: string }[] = [
{ id: "catalog", label: "▦ Terrain Catalog" },
{ id: "transitions", label: "⊕ Biome Transitions" },
{ id: "noise", label: "∿ Noise Anatomy" },
{ id: "forest", label: "🌲 Forest Lab" },
{ id: "forest-lab", label: "🌲 Forest Lab" },
];
const Page = styled.div`
@ -69,7 +71,11 @@ const TabBtn = styled.button<{ $active: boolean }>`
`;
export function WorldGenPage(): React.ReactElement {
const [tab, setTab] = useState<Tab>("map");
const { tab: tabParam } = useParams<{ tab?: string }>();
const navigate = useNavigate();
const tab: Tab = VALID_TABS.includes(tabParam as Tab) ? (tabParam as Tab) : "map";
const switchTab = (t: Tab) => navigate(`/world-gen/${t}`);
return (
<Page>
@ -82,7 +88,7 @@ export function WorldGenPage(): React.ReactElement {
<TabBar>
{TABS.map(({ id, label }) => (
<TabBtn key={id} $active={tab === id} onClick={() => setTab(id)}>
<TabBtn key={id} $active={tab === id} onClick={() => switchTab(id)}>
{label}
</TabBtn>
))}
@ -93,7 +99,7 @@ export function WorldGenPage(): React.ReactElement {
{tab === "catalog" && <TerrainCatalog />}
{tab === "transitions" && <BiomeTransitions />}
{tab === "noise" && <NoiseAnatomy />}
{tab === "forest" && <ForestLab />}
{tab === "forest-lab" && <ForestLab />}
</Page>
);
}

File diff suppressed because it is too large Load diff

View file

@ -109,37 +109,73 @@ function stipple(
}
}
// ── Decorations per terrain ───────────────────────────────────────────────────
// ── 3-layer tree primitives (exported for lab use) ────────────────────────────
function drawTree(
export type TreeVariety = "temperate" | "tropical";
export function drawTree3Layer(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
r: number,
color: string
variety: TreeVariety = "temperate"
): void {
const colors: Record<TreeVariety, { dark: string; mid: string; light: string }> = {
temperate: { dark: "#2a5a18", mid: "#3d7a25", light: "#5a9a3a" },
tropical: { dark: "#1a4a10", mid: "#2d6a1c", light: "#448a28" },
};
const c = colors[variety];
ctx.beginPath();
ctx.ellipse(x + r * 0.3, y + r * 0.5, r * 1.05, r * 0.45, 0, 0, Math.PI * 2);
ctx.fillStyle = "rgba(0,0,0,0.20)";
ctx.fill();
const grad = ctx.createRadialGradient(x - r * 0.28, y - r * 0.28, 0, x, y, r);
grad.addColorStop(0.0, c.light);
grad.addColorStop(0.45, c.mid);
grad.addColorStop(1.0, c.dark);
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fillStyle = grad;
ctx.fill();
ctx.beginPath();
ctx.arc(x - r * 0.32, y - r * 0.32, r * 0.22, 0, Math.PI * 2);
ctx.fillStyle = "rgba(200,240,140,0.32)";
ctx.fill();
}
function drawConifer(
export function drawConifer3Layer(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
h: number,
color: string
h: number
): void {
ctx.beginPath();
ctx.ellipse(x + 2, y + h * 0.22, h * 0.34, h * 0.14, 0, 0, Math.PI * 2);
ctx.fillStyle = "rgba(0,0,0,0.18)";
ctx.fill();
ctx.beginPath();
ctx.moveTo(x, y - h);
ctx.lineTo(x - h * 0.4, y);
ctx.lineTo(x + h * 0.4, y);
ctx.lineTo(x - h * 0.6, y + h * 0.5);
ctx.lineTo(x + h * 0.6, y + h * 0.5);
ctx.closePath();
ctx.fillStyle = color;
ctx.fillStyle = "#2a5c46";
ctx.fill();
ctx.beginPath();
ctx.moveTo(x, y - h * 1.4);
ctx.lineTo(x - h * 0.4, y - h * 0.4);
ctx.lineTo(x + h * 0.4, y - h * 0.4);
ctx.closePath();
ctx.fillStyle = "#357558";
ctx.fill();
}
// ── Decorations per terrain ───────────────────────────────────────────────────
function drawPeak(
ctx: CanvasRenderingContext2D,
x: number,
@ -212,20 +248,20 @@ export function drawDecorations(
switch (terrainId) {
case "forest": {
const pts = poissonDisc(cx, cy, size, 6, 5, rng);
const pts = poissonDisc(cx, cy, size * 0.85, 7, 5, rng);
for (const p of pts)
drawTree(ctx, p.x, p.y, 3 + rng.next() * 2.5, "rgba(45,100,30,0.85)");
drawTree3Layer(ctx, p.x, p.y, 3 + rng.next() * 3, "temperate");
break;
}
case "jungle": {
const pts = poissonDisc(cx, cy, size, 5, 7, rng);
const pts = poissonDisc(cx, cy, size * 0.85, 6, 7, rng);
for (const p of pts)
drawTree(ctx, p.x, p.y, 4 + rng.next() * 3.5, "rgba(25,85,15,0.9)");
drawTree3Layer(ctx, p.x, p.y, 4 + rng.next() * 4, "tropical");
break;
}
case "boreal_forest": {
const pts = poissonDisc(cx, cy, size, 7, 4, rng);
for (const p of pts) drawConifer(ctx, p.x, p.y, 10, "rgba(30,75,55,0.9)");
const pts = poissonDisc(cx, cy, size * 0.85, 8, 4, rng);
for (const p of pts) drawConifer3Layer(ctx, p.x, p.y, 9 + rng.next() * 5);
break;
}
case "mountains": {