feat(@projects/@magic-civilization): ✨ add dynamic tab routing for world generation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
3b0bdb0ba1
commit
1e431d389f
4 changed files with 694 additions and 339 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue