Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ related_to: []
- Execute in slice order as defined in the milestone index.

## Acceptance Criteria
- [ ] There is one plan op per vegetated feature key.
- [ ] Behavior matches baseline (parity diff is empty).
- [ ] RNG label strings and per-label call order are preserved.
- [x] There is one plan op per vegetated feature key.
- [x] Behavior matches baseline (parity diff is empty).
- [x] RNG label strings and per-label call order are preserved.

## Testing / Verification
- Gate G0.
Expand Down Expand Up @@ -57,9 +57,9 @@ Replace multi-feature vegetation planners with atomic per-feature plan ops:
- `FEATURE_FOREST`, `FEATURE_RAINFOREST`, `FEATURE_TAIGA`, `FEATURE_SAVANNA_WOODLAND`, `FEATURE_SAGEBRUSH_STEPPE`

**Acceptance Criteria**
- [ ] There is one plan op per vegetated feature key.
- [ ] Behavior matches baseline (parity diff is empty).
- [ ] RNG label strings and per-label call order are preserved.
- [x] There is one plan op per vegetated feature key.
- [x] Behavior matches baseline (parity diff is empty).
- [x] RNG label strings and per-label call order are preserved.

**Scope boundaries**
- In scope: new per-feature plan ops + rules factoring.
Expand Down
4 changes: 2 additions & 2 deletions mods/mod-swooper-maps/src/domain/ecology/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import BiomeClassificationContract from "./ops/classify-biomes/contract.js";
import PlanAquaticFeaturePlacementsContract from "./ops/plan-aquatic-feature-placements/contract.js";
import PlanIceFeaturePlacementsContract from "./ops/plan-ice-feature-placements/contract.js";
import PlanPlotEffectsContract from "./ops/plan-plot-effects/contract.js";
import PlanVegetatedFeaturePlacementsContract from "./ops/plan-vegetated-feature-placements/contract.js";
import PlanVegetatedPlacementForestContract from "./ops/plan-vegetated-placement-forest/contract.js";
import PlanWetFeaturePlacementsContract from "./ops/plan-wet-feature-placements/contract.js";
/**
* Biome classification config (Holdridge/Whittaker-inspired).
Expand All @@ -22,7 +22,7 @@ const BiomeBindingsSchema = BiomeEngineBindingsSchema;
*/
const FeaturesPlacementConfigSchema = Type.Object(
{
vegetated: PlanVegetatedFeaturePlacementsContract.config,
vegetated: PlanVegetatedPlacementForestContract.config,
wet: PlanWetFeaturePlacementsContract.config,
aquatic: PlanAquaticFeaturePlacementsContract.config,
ice: PlanIceFeaturePlacementsContract.config,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { clampInt } from "@swooper/mapgen-core/lib/math";

export function computeCoastalLandMask(args: {
width: number;
height: number;
landMask: Uint8Array;
radius: number;
}): Uint8Array {
const width = args.width | 0;
const height = args.height | 0;
const radius = clampInt(args.radius | 0, 0, Math.max(width, height));
const size = Math.max(0, width * height);

const mask = new Uint8Array(size);
if (radius <= 0) return mask;

for (let y = 0; y < height; y++) {
const y0 = Math.max(0, y - radius);
const y1 = Math.min(height - 1, y + radius);
for (let x = 0; x < width; x++) {
const i = y * width + x;
if (args.landMask[i] !== 1) continue;
const x0 = Math.max(0, x - radius);
const x1 = Math.min(width - 1, x + radius);
let adjacentWater = 0;
for (let ny = y0; ny <= y1 && !adjacentWater; ny++) {
const row = ny * width;
for (let nx = x0; nx <= x1; nx++) {
if (nx === x && ny === y) continue;
if (args.landMask[row + nx] === 0) {
adjacentWater = 1;
break;
}
}
}
mask[i] = adjacentWater;
}
}

return mask;
}

Original file line number Diff line number Diff line change
@@ -1,117 +1,5 @@
import { clampInt } from "@swooper/mapgen-core/lib/math";

type ComputeInputs = Readonly<{
width: number;
height: number;
riverClass: Uint8Array;
landMask: Uint8Array;
}>;

export function validateFeatureSubstrateInputs(input: ComputeInputs): number {
const width = input.width | 0;
const height = input.height | 0;
const size = Math.max(0, width * height);

if (!(input.riverClass instanceof Uint8Array) || input.riverClass.length !== size) {
throw new Error("[Ecology] Invalid riverClass for compute-feature-substrate.");
}
if (!(input.landMask instanceof Uint8Array) || input.landMask.length !== size) {
throw new Error("[Ecology] Invalid landMask for compute-feature-substrate.");
}

return size;
}

export function computeNavigableRiverMask(args: {
size: number;
riverClass: Uint8Array;
navigableRiverClass: number;
}): Uint8Array {
const mask = new Uint8Array(args.size);
const target = clampInt(args.navigableRiverClass | 0, 0, 255);
for (let i = 0; i < args.size; i++) {
mask[i] = (args.riverClass[i] ?? 0) === target ? 1 : 0;
}
return mask;
}

export function computeRiverAdjacencyMask(args: {
width: number;
height: number;
riverClass: Uint8Array;
radius: number;
}): Uint8Array {
const width = args.width | 0;
const height = args.height | 0;
const radius = clampInt(args.radius | 0, 0, Math.max(width, height));
const size = Math.max(0, width * height);

const mask = new Uint8Array(size);
if (radius <= 0) {
for (let i = 0; i < size; i++) mask[i] = args.riverClass[i] ? 1 : 0;
return mask;
}

for (let y = 0; y < height; y++) {
const y0 = Math.max(0, y - radius);
const y1 = Math.min(height - 1, y + radius);
for (let x = 0; x < width; x++) {
const x0 = Math.max(0, x - radius);
const x1 = Math.min(width - 1, x + radius);
let adjacent = 0;
for (let ny = y0; ny <= y1 && !adjacent; ny++) {
const row = ny * width;
for (let nx = x0; nx <= x1; nx++) {
if (args.riverClass[row + nx] !== 0) {
adjacent = 1;
break;
}
}
}
mask[y * width + x] = adjacent;
}
}

return mask;
}

export function computeCoastalLandMask(args: {
width: number;
height: number;
landMask: Uint8Array;
radius: number;
}): Uint8Array {
const width = args.width | 0;
const height = args.height | 0;
const radius = clampInt(args.radius | 0, 0, Math.max(width, height));
const size = Math.max(0, width * height);

const mask = new Uint8Array(size);
if (radius <= 0) return mask;

for (let y = 0; y < height; y++) {
const y0 = Math.max(0, y - radius);
const y1 = Math.min(height - 1, y + radius);
for (let x = 0; x < width; x++) {
const i = y * width + x;
if (args.landMask[i] !== 1) continue;
const x0 = Math.max(0, x - radius);
const x1 = Math.min(width - 1, x + radius);
let adjacentWater = 0;
for (let ny = y0; ny <= y1 && !adjacentWater; ny++) {
const row = ny * width;
for (let nx = x0; nx <= x1; nx++) {
if (nx === x && ny === y) continue;
if (args.landMask[row + nx] === 0) {
adjacentWater = 1;
break;
}
}
}
mask[i] = adjacentWater;
}
}

return mask;
}
export { validateFeatureSubstrateInputs } from "./validate.js";
export { computeNavigableRiverMask } from "./navigable-river-mask.js";
export { computeRiverAdjacencyMask } from "./river-adjacency-mask.js";
export { computeCoastalLandMask } from "./coastal-land-mask.js";

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { clampInt } from "@swooper/mapgen-core/lib/math";

export function computeNavigableRiverMask(args: {
size: number;
riverClass: Uint8Array;
navigableRiverClass: number;
}): Uint8Array {
const mask = new Uint8Array(args.size);
const target = clampInt(args.navigableRiverClass | 0, 0, 255);
for (let i = 0; i < args.size; i++) {
mask[i] = (args.riverClass[i] ?? 0) === target ? 1 : 0;
}
return mask;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { clampInt } from "@swooper/mapgen-core/lib/math";

export function computeRiverAdjacencyMask(args: {
width: number;
height: number;
riverClass: Uint8Array;
radius: number;
}): Uint8Array {
const width = args.width | 0;
const height = args.height | 0;
const radius = clampInt(args.radius | 0, 0, Math.max(width, height));
const size = Math.max(0, width * height);

const mask = new Uint8Array(size);
if (radius <= 0) {
for (let i = 0; i < size; i++) mask[i] = args.riverClass[i] ? 1 : 0;
return mask;
}

for (let y = 0; y < height; y++) {
const y0 = Math.max(0, y - radius);
const y1 = Math.min(height - 1, y + radius);
for (let x = 0; x < width; x++) {
const x0 = Math.max(0, x - radius);
const x1 = Math.min(width - 1, x + radius);
let adjacent = 0;
for (let ny = y0; ny <= y1 && !adjacent; ny++) {
const row = ny * width;
for (let nx = x0; nx <= x1; nx++) {
if (args.riverClass[row + nx] !== 0) {
adjacent = 1;
break;
}
}
}
mask[y * width + x] = adjacent;
}
}

return mask;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
type ComputeInputs = Readonly<{
width: number;
height: number;
riverClass: Uint8Array;
landMask: Uint8Array;
}>;

export function validateFeatureSubstrateInputs(input: ComputeInputs): number {
const width = input.width | 0;
const height = input.height | 0;
const size = Math.max(0, width * height);

if (!(input.riverClass instanceof Uint8Array) || input.riverClass.length !== size) {
throw new Error("[Ecology] Invalid riverClass for compute-feature-substrate.");
}
if (!(input.landMask instanceof Uint8Array) || input.landMask.length !== size) {
throw new Error("[Ecology] Invalid landMask for compute-feature-substrate.");
}

return size;
}

36 changes: 30 additions & 6 deletions mods/mod-swooper-maps/src/domain/ecology/ops/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@ import PlanIceFeaturePlacementsContract from "./plan-ice-feature-placements/cont
import PlanPlotEffectsContract from "./plan-plot-effects/contract.js";
import PlanReefEmbellishmentsContract from "./plan-reef-embellishments/contract.js";
import PlanReefsContract from "./features-plan-reefs/contract.js";
import PlanVegetatedFeaturePlacementsContract from "./plan-vegetated-feature-placements/contract.js";
import PlanVegetationContract from "./features-plan-vegetation/contract.js";
import PlanVegetatedPlacementForestContract from "./plan-vegetated-placement-forest/contract.js";
import PlanVegetatedPlacementRainforestContract from "./plan-vegetated-placement-rainforest/contract.js";
import PlanVegetatedPlacementSagebrushSteppeContract from "./plan-vegetated-placement-sagebrush-steppe/contract.js";
import PlanVegetatedPlacementSavannaWoodlandContract from "./plan-vegetated-placement-savanna-woodland/contract.js";
import PlanVegetatedPlacementTaigaContract from "./plan-vegetated-placement-taiga/contract.js";
import PlanVegetationForestContract from "./features-plan-vegetation-forest/contract.js";
import PlanVegetationRainforestContract from "./features-plan-vegetation-rainforest/contract.js";
import PlanVegetationSagebrushSteppeContract from "./features-plan-vegetation-sagebrush-steppe/contract.js";
import PlanVegetationSavannaWoodlandContract from "./features-plan-vegetation-savanna-woodland/contract.js";
import PlanVegetationTaigaContract from "./features-plan-vegetation-taiga/contract.js";
import PlanVegetationEmbellishmentsContract from "./plan-vegetation-embellishments/contract.js";
import PlanWetFeaturePlacementsContract from "./plan-wet-feature-placements/contract.js";
import PlanWetlandsContract from "./features-plan-wetlands/contract.js";
Expand All @@ -30,10 +38,18 @@ export const contracts = {
planIceFeaturePlacements: PlanIceFeaturePlacementsContract,
planPlotEffects: PlanPlotEffectsContract,
planReefEmbellishments: PlanReefEmbellishmentsContract,
planVegetatedFeaturePlacements: PlanVegetatedFeaturePlacementsContract,
planVegetatedPlacementForest: PlanVegetatedPlacementForestContract,
planVegetatedPlacementRainforest: PlanVegetatedPlacementRainforestContract,
planVegetatedPlacementTaiga: PlanVegetatedPlacementTaigaContract,
planVegetatedPlacementSavannaWoodland: PlanVegetatedPlacementSavannaWoodlandContract,
planVegetatedPlacementSagebrushSteppe: PlanVegetatedPlacementSagebrushSteppeContract,
planVegetationEmbellishments: PlanVegetationEmbellishmentsContract,
planWetFeaturePlacements: PlanWetFeaturePlacementsContract,
planVegetation: PlanVegetationContract,
planVegetationForest: PlanVegetationForestContract,
planVegetationRainforest: PlanVegetationRainforestContract,
planVegetationTaiga: PlanVegetationTaigaContract,
planVegetationSavannaWoodland: PlanVegetationSavannaWoodlandContract,
planVegetationSagebrushSteppe: PlanVegetationSagebrushSteppeContract,
planWetlands: PlanWetlandsContract,
planReefs: PlanReefsContract,
planIce: PlanIceContract,
Expand All @@ -54,8 +70,16 @@ export {
PlanPlotEffectsContract,
PlanReefEmbellishmentsContract,
PlanReefsContract,
PlanVegetatedFeaturePlacementsContract,
PlanVegetationContract,
PlanVegetatedPlacementForestContract,
PlanVegetatedPlacementRainforestContract,
PlanVegetatedPlacementSagebrushSteppeContract,
PlanVegetatedPlacementSavannaWoodlandContract,
PlanVegetatedPlacementTaigaContract,
PlanVegetationForestContract,
PlanVegetationRainforestContract,
PlanVegetationSagebrushSteppeContract,
PlanVegetationSavannaWoodlandContract,
PlanVegetationTaigaContract,
PlanVegetationEmbellishmentsContract,
PlanWetFeaturePlacementsContract,
PlanWetlandsContract,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Type, TypedArraySchemas, defineOp } from "@swooper/mapgen-core/authoring";

import { FeaturePlacementSchema } from "../../shared/placement-schema.js";

const PlanVegetationForestContract = defineOp({
kind: "plan",
id: "ecology/features/plan-vegetation/forest",
input: Type.Object({
width: Type.Integer({ minimum: 1 }),
height: Type.Integer({ minimum: 1 }),
biomeIndex: TypedArraySchemas.u8({ description: "Biome indices per tile." }),
vegetationDensity: TypedArraySchemas.f32({ description: "Vegetation density (0..1)." }),
effectiveMoisture: TypedArraySchemas.f32({ description: "Effective moisture per tile." }),
surfaceTemperature: TypedArraySchemas.f32({ description: "Surface temperature (C)." }),
fertility: TypedArraySchemas.f32({ description: "Fertility overlay (0..1)." }),
landMask: TypedArraySchemas.u8({ description: "Land mask (1 = land, 0 = water)." }),
}),
output: Type.Object({
placements: Type.Array(FeaturePlacementSchema),
}),
strategies: {
default: Type.Object({
baseDensity: Type.Number({ minimum: 0, maximum: 1, default: 0.35 }),
fertilityWeight: Type.Number({ minimum: 0, maximum: 2, default: 0.4 }),
moistureWeight: Type.Number({ minimum: 0, maximum: 2, default: 0.6 }),
moistureNormalization: Type.Number({ minimum: 1, default: 230 }),
coldCutoff: Type.Number({ default: -10 }),
}),
clustered: Type.Object({
baseDensity: Type.Number({ minimum: 0, maximum: 1, default: 0.35 }),
fertilityWeight: Type.Number({ minimum: 0, maximum: 2, default: 0.4 }),
moistureWeight: Type.Number({ minimum: 0, maximum: 2, default: 0.6 }),
moistureNormalization: Type.Number({ minimum: 1, default: 230 }),
coldCutoff: Type.Number({ default: -10 }),
}),
},
});

export default PlanVegetationForestContract;

Loading