diff --git a/.project/designs/app/package.json b/.project/designs/app/package.json index 45035c21..1b874441 100644 --- a/.project/designs/app/package.json +++ b/.project/designs/app/package.json @@ -23,6 +23,7 @@ "@types/styled-components": "^5.1.34", "@vitejs/plugin-react": "^4.4.1", "typescript": "^5.8.3", - "vite": "^6.3.3" + "vite": "^6.3.3", + "vite-plugin-wasm": "^3.6.0" } } diff --git a/.project/designs/app/src/utils/wasm/useWasmGrid.ts b/.project/designs/app/src/utils/wasm/useWasmGrid.ts new file mode 100644 index 00000000..8b59f462 --- /dev/null +++ b/.project/designs/app/src/utils/wasm/useWasmGrid.ts @@ -0,0 +1,31 @@ +import { useState, useEffect, useRef } from "react"; +import type { WasmGrid } from "../../../../../../.local/build/wasm/magic_civ_physics"; + +type GridState = + | { status: "idle" } + | { status: "loading" } + | { status: "ready"; grid: WasmGrid } + | { status: "error"; message: string }; + +export function useWasmGrid(seed: number, mapSize: string = "tiny"): GridState { + const [state, setState] = useState({ status: "idle" }); + const genRef = useRef(0); + + useEffect(() => { + const gen = ++genRef.current; + setState({ status: "loading" }); + + import("../../../../../../.local/build/wasm/magic_civ_physics") + .then(({ WasmGrid }) => { + if (gen !== genRef.current) return; + const grid = WasmGrid.generateForLab(BigInt(seed), mapSize); + setState({ status: "ready", grid }); + }) + .catch((err: unknown) => { + if (gen !== genRef.current) return; + setState({ status: "error", message: String(err) }); + }); + }, [seed, mapSize]); + + return state; +} diff --git a/.project/designs/app/vite.config.ts b/.project/designs/app/vite.config.ts index 1bfd1256..27929144 100644 --- a/.project/designs/app/vite.config.ts +++ b/.project/designs/app/vite.config.ts @@ -1,9 +1,11 @@ import path from "path"; import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +import wasm from "vite-plugin-wasm"; export default defineConfig({ - plugins: [react()], + plugins: [wasm(), react()], + build: { target: "esnext" }, resolve: { alias: { "@game-data": path.resolve(__dirname, "../../../public/games/age-of-dwarves/data"), @@ -26,6 +28,7 @@ export default defineConfig({ path.resolve(__dirname, "../../../public"), path.resolve(__dirname, "../../../.local/audio-alternatives"), path.resolve(__dirname, "../../../.local/audio-staging"), + path.resolve(__dirname, "../../../.local/build/wasm"), path.resolve(__dirname, "../../reports"), ], }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50991792..97639e0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: vite: specifier: ^6.3.3 version: 6.4.2(@types/node@25.6.0)(tsx@4.21.0) + vite-plugin-wasm: + specifier: ^3.6.0 + version: 3.6.0(vite@6.4.2(@types/node@25.6.0)(tsx@4.21.0)) public/games/age-of-dwarves/guide: dependencies: diff --git a/public/games/age-of-dwarves/data/world_shapes/manifest.json b/public/games/age-of-dwarves/data/world_shapes/manifest.json new file mode 100644 index 00000000..e74cb5ea --- /dev/null +++ b/public/games/age-of-dwarves/data/world_shapes/manifest.json @@ -0,0 +1,62 @@ +{ + "schema_version": "1", + "default": "earthlike", + "earthlike": { + "id": "earthlike", + "name": "Earthlike", + "description": "Familiar continents, moderate climate, balanced rainfall.", + "axes": { + "landmass": "continents", + "climate": "temperate", + "moisture": "balanced", + "age": "mature", + "sea_level": "standard" + }, + "param_overrides": { + "plate_count": 10, + "convergent_bias": 0.40, + "divergent_bias": -0.25, + "sea_level": 0.28, + "ocean_percentage": { "target": 0.40 }, + "temp_offset": 0.0, + "latitude_gradient": 0.7, + "base_precip_offset": 0.0, + "windward_boost": 1.5, + "leeward_factor": 0.4, + "erosion_iterations": 5, + "seasonality_scale": 0.8, + "rain_shadow_factor": 1.0 + }, + "thumbnail": "world_shapes/previews/earthlike.png", + "is_default": true + }, + "axes": { + "landmass": ["pangaea", "continents", "archipelago", "islands", "shattered"], + "climate": ["cold", "temperate", "hot", "extreme"], + "moisture": ["arid", "dry", "balanced", "wet", "lush"], + "age": ["young", "mature", "ancient"], + "sea_level": ["low", "standard", "high"] + }, + "presets": [ + "landmass/pangaea.json", + "landmass/continents.json", + "landmass/archipelago.json", + "landmass/islands.json", + "landmass/shattered.json", + "climate/cold.json", + "climate/temperate.json", + "climate/hot.json", + "climate/extreme.json", + "moisture/arid.json", + "moisture/dry.json", + "moisture/balanced.json", + "moisture/wet.json", + "moisture/lush.json", + "age/young.json", + "age/mature.json", + "age/ancient.json", + "sea_level/low.json", + "sea_level/standard.json", + "sea_level/high.json" + ] +} diff --git a/src/simulator/api-wasm/src/lib.rs b/src/simulator/api-wasm/src/lib.rs index 6b50453b..b9dc3b9e 100644 --- a/src/simulator/api-wasm/src/lib.rs +++ b/src/simulator/api-wasm/src/lib.rs @@ -1,11 +1,12 @@ //! WASM API surface — exposes Rust simulation to the web guide via wasm-bindgen. //! All public functions match the signatures specified in Task #2. +//! See: public/games/age-of-dwarves/docs/terrain/WORLDGEN_PIPELINE.md use wasm_bindgen::prelude::*; use mc_core::grid::{GridState, biome_registry::{has_tag, BiomeTag}}; use mc_climate::{ClimatePhysics, EcologyPhysics, step_atmospheric_chemistry}; -use mc_mapgen::MapGenerator; +use mc_mapgen::{MapGenerator, seed::{derive as derive_seed, SeedDomain}}; /// WASM-exposed grid handle wrapping GridState. #[wasm_bindgen] @@ -36,6 +37,19 @@ impl WasmGrid { Ok(Self { inner }) } + /// Run the full worldgen pipeline (tectonics → climate → erosion → hydrology) + /// and return a populated WasmGrid ready for tile_*_json queries. + /// `map_size` accepts "duel" | "tiny" | "small" | "standard" | "large" | "huge". + /// Used by the design lab's per-layer playground pages (p1-53). + /// See: public/games/age-of-dwarves/docs/terrain/WORLDGEN_PIPELINE.md + #[wasm_bindgen(js_name = "generateForLab")] + pub fn generate_for_lab(seed: u64, map_size: &str) -> WasmGrid { + let gen = MapGenerator::new("{}"); + WasmGrid { + inner: gen.generate(seed, map_size), + } + } + #[wasm_bindgen(getter)] pub fn width(&self) -> i32 { self.inner.width @@ -329,6 +343,25 @@ impl WasmEcologyPhysics { } } +/// Derive a deterministic sub-seed for the given domain from a map seed. +/// `domain` matches SeedDomain discriminants: 0=Tectonics, 1=Erosion, +/// 2=Hydrology, 3=Climate, 4=FloraSelect, 5=FaunaSelect. +/// Used by the RNG playground page (p1-53). +/// See: public/games/age-of-dwarves/docs/terrain/WORLDGEN_PIPELINE.md +#[wasm_bindgen(js_name = "seedDerive")] +pub fn wasm_seed_derive(map_seed: u64, domain: u8) -> u64 { + let d = match domain { + 0 => SeedDomain::Tectonics, + 1 => SeedDomain::Erosion, + 2 => SeedDomain::Hydrology, + 3 => SeedDomain::Climate, + 4 => SeedDomain::FloraSelect, + 5 => SeedDomain::FaunaSelect, + _ => SeedDomain::Tectonics, + }; + derive_seed(map_seed, d) +} + /// WASM-exposed atmospheric chemistry step. #[wasm_bindgen(js_name = "stepAtmosphericChemistry")] pub fn wasm_step_atmospheric_chemistry(grid: &mut WasmGrid, spec_json: &str) {