Generation

Themes, matchers & rules

The theme and biome system, pattern matchers for grid cells and markers, predicate combinators, and weighted random placement.

Once markers are in the grid, a ThemeEngine drives the final placement pass. It organises Theme objects into AreaTheme regions with binary masks, applies terrain layer rules, and processes each MarkerNode against a rule set. The matching and predicate layer is a small algebra of composable types, all verified in source.

Source: @woosh/meep-engine/src/generation/

Theme structure

A Theme holds three independent rule sets:

FieldTypePurpose
terrainTerrainThemeList of TerrainLayerRule objects that paint terrain layer weights
nodesMarkerNodeProcessingRuleSetRules that consume MarkerNode objects and produce ECS entities
cellsCellProcessingRuleSetPer-cell rules applied during ThemeEngine.applyCellRules

theme.initialize(seed, ecd, grid) initialises all three sub-sets in one call.

Terrain layer rules

TerrainLayerRule binds a CellFilter (the weight function) to a TerrainLayerDescription (which texture/material to use). The ThemeEngine evaluates every rule’s filter at each grid cell, normalises the weights across all active themes at that cell (accounting for distance-field blending at biome boundaries), and writes the result into the terrain’s splat map.

import { TerrainLayerRule }        from "@woosh/meep-engine/src/generation/theme/TerrainLayerRule.js";
import { TerrainLayerDescription } from "@woosh/meep-engine/src/generation/theme/TerrainLayerDescription.js";
import { CellFilterSimplexNoise }  from "@woosh/meep-engine/src/generation/filtering/numeric/complex/CellFilterSimplexNoise.js";
import { CellFilterSaturate }      from "@woosh/meep-engine/src/generation/filtering/numeric/math/CellFilterSaturate.js";

const rule = TerrainLayerRule.from(
    CellFilterSaturate.from(CellFilterSimplexNoise.fractal(3, 40)),
    new TerrainLayerDescription()   // fill in texture asset refs
);

theme.terrain.rules.push(rule);

ThemeEngine and area masks

ThemeEngine stores AreaTheme objects in a quad-tree. Each AreaTheme pairs a Theme with an AreaMask:

import { ThemeEngine } from "@woosh/meep-engine/src/generation/theme/ThemeEngine.js";
import { AreaTheme }   from "@woosh/meep-engine/src/generation/theme/AreaTheme.js";
import { AreaMask }    from "@woosh/meep-engine/src/generation/theme/AreaMask.js";

const engine = new ThemeEngine();
engine.seed = 42;
engine.assetManager = assetManager;

const area = new AreaTheme();
area.theme = myTheme;
area.mask.resize(128, 128);
// paint area.mask.mask.data with 0/1 …
engine.add(area);   // auto-computes bounds + distance field

const taskGroup = engine.apply(grid, ecd);

engine.apply(grid, ecd) returns a TaskGroup that in order: initialises themes, applies terrain, ensures terrain tiles are built, processes markers into nodes, applies per-cell rules, and builds the lightmap. When multiple areas overlap a cell, each area’s distance-field value is used to proportionally blend the terrain weights.

engine.getThemesByPosition(result, x, y) returns all AreaTheme objects whose mask contains the point — used internally during both terrain and node passes, and accessible for custom logic.

CellMatcher — pattern predicates on grid cells

CellMatcher is the base class for predicates over GridData. Its interface is match(grid, x, y, rotation) → boolean. It is used as the pattern field of GridCellPlacementRule.

Primitive matchers

ClassImport pathMatches when
CellMatcherAnyrules/CellMatcherAny.jsAlways — use as a wildcard
CellMatcherFromFilterrules/CellMatcherFromFilter.jsThe underlying CellFilter.execute returns > 0
CellMatcherLayerBitMaskTestrules/CellMatcherLayerBitMaskTest.jsA bit-mask test on an integer GridDataLayer passes at the cell
CellMatcherContainsMarkerWithinRadiusrules/cell/CellMatcherContainsMarkerWithinRadius.jsA marker matching a given MarkerNodeMatcher exists within a radius of the cell

Pattern matcher

CellMatcherGridPattern matches a multi-cell neighbourhood. Each cell in the pattern is described by a GridPatternMatcherCell — a (x, y) local offset plus a child CellMatcher to evaluate at that offset. The whole pattern is tested after rotating each offset by rotation, so a single rule fires regardless of orientation when allowRotation is true.

import { CellMatcherGridPattern }  from "@woosh/meep-engine/src/generation/rules/cell/CellMatcherGridPattern.js";
import { GridPatternMatcherCell }  from "@woosh/meep-engine/src/generation/rules/cell/GridPatternMatcherCell.js";
import { CellMatcherFromFilter }   from "@woosh/meep-engine/src/generation/rules/CellMatcherFromFilter.js";

const pattern = CellMatcherGridPattern.from([
    GridPatternMatcherCell.from(someFilter, 0, 0),   // origin
    GridPatternMatcherCell.from(edgeFilter,  1, 0),  // one cell to the right
    GridPatternMatcherCell.from(edgeFilter, -1, 0),  // one cell to the left
]);

Logic combinators for CellMatcher

ClassImport pathBehaviour
CellMatcherAndrules/logic/CellMatcherAnd.jsBoth children must match
CellMatcherOrrules/logic/CellMatcherOr.jsEither child must match
CellMatcherNotrules/logic/CellMatcherNot.jsInverts a single child

All three have a static from(left, right) / static from(source) factory that asserts isCellMatcher.

MarkerNodeMatcher — predicates over marker nodes

MarkerNodeMatcher evaluates a MarkerNode — not a grid position — and is used in MarkerProcessingRule and as the filter argument to spatial queries like countMarkerInCircle.

Primitive marker matchers

ClassImport pathMatches when
MarkerNodeMatcherAnymarkers/matcher/MarkerNodeMatcherAny.jsAlways
MarkerNodeMatcherNonemarkers/matcher/MarkerNodeMatcherNone.jsNever
MarkerNodeMatcherByTypemarkers/matcher/MarkerNodeMatcherByType.jsnode.type === type
MarkerNodeMatcherContainsTagmarkers/matcher/MarkerNodeMatcherContainsTag.jsnode.tags contains the given tag string

Logic combinators for MarkerNodeMatcher

ClassImport pathBehaviour
MarkerNodeMatcherAndmarkers/matcher/MarkerNodeMatcherAnd.jsBoth children match
MarkerNodeMatcherOrmarkers/matcher/MarkerNodeMatcherOr.jsEither child matches
MarkerNodeMatcherNotmarkers/matcher/MarkerNodeMatcherNot.jsInverts child

GridDataNodePredicate — spatial node predicates

GridDataNodePredicate evaluates both a GridData and a MarkerNode together — useful for decisions that depend on spatial context. Its evaluate(grid, node) → boolean method receives the full grid.

ClassImport pathBehaviour
GridDataNodePredicateAnymarkers/predicate/GridDataNodePredicateAny.jsAlways true; .INSTANCE singleton
GridDataNodePredicateNotmarkers/predicate/GridDataNodePredicateNot.jsInverts a child predicate
GridDataNodePredicateAndmarkers/predicate/GridDataNodePredicateAnd.jsBoth children must be true
GridDataNodePredicateOverlapsmarkers/predicate/GridDataNodePredicateOverlaps.jsTrue if any marker matching a MarkerNodeMatcher overlaps the node’s circle

Marker processing rules

MarkerProcessingRule is the rule that binds a matcher, optional transformers, and an action for the marker processing pass:

import { MarkerProcessingRule }          from "@woosh/meep-engine/src/generation/markers/actions/MarkerProcessingRule.js";
import { MarkerNodeMatcherByType }       from "@woosh/meep-engine/src/generation/markers/matcher/MarkerNodeMatcherByType.js";
import { MarkerNodeActionEntityPlacement } from "@woosh/meep-engine/src/generation/markers/actions/MarkerNodeActionEntityPlacement.js";

const rule = MarkerProcessingRule.from({
    matcher: MarkerNodeMatcherByType.from("tree"),
    transformers: [],
    action: MarkerNodeActionEntityPlacement.from({ blueprint: treeBlueprint }),
    consume: true,   // stop processing this node after this rule fires
});

theme.nodes.add(rule);

MarkerNodeActionEntityPlacement reads the marker’s transform and properties to position the entity blueprint. It copies the marker’s GridPosition if the entity has one, then multiplies the node’s world transform with the rule’s base transform.

Marker transformers

Transformers mutate a MarkerNode clone before the action runs:

TransformerImport pathEffect
MarkerNodeTransformRotateRandommarkers/transform/Random Y rotation
MarkerNodeTransformerYRotateByFiltermarkers/transform/Y rotation driven by a CellFilter value
MarkerNodeTransformerYRotateByFilterGradientmarkers/transform/Y rotation aligned to the gradient direction of a CellFilter
MarkerNodeTransformerAddPositionYFromFiltermarkers/transform/Adds filter value to the marker’s world Y position (terrain snap)
MarkerNodeTransformerOffsetPositionmarkers/transform/Translates position by a fixed offset
MarkerNodeTransformerRecordPropertymarkers/transform/Writes a filter value into node.properties
MarkerNodeTransformerRecordUniqueRandomEnummarkers/transform/Picks a unique random value from a set and stores it in properties
MarkerNodeTransformerRemoveTagmarkers/transform/Removes a tag from the node

Weighted random placement

MarkerNodeActionSelectWeighted picks one action from a list based on per-position weights. Each option is a MarkerNodeActionWeightedElement — an action paired with a CellFilter that returns the weight at the marker’s position. The selection is deterministic given the seed.

import { MarkerNodeActionSelectWeighted }  from "@woosh/meep-engine/src/generation/markers/actions/probability/MarkerNodeActionSelectWeighted.js";
import { MarkerNodeActionWeightedElement } from "@woosh/meep-engine/src/generation/markers/actions/probability/MarkerNodeActionWeightedElement.js";

const selector = MarkerNodeActionSelectWeighted.from([
    MarkerNodeActionWeightedElement.from(placeOak,   highElevationFilter),
    MarkerNodeActionWeightedElement.from(placePine,  lowElevationFilter),
    MarkerNodeActionWeightedElement.from(placeShrub, CellFilterLiteralFloat.from(0.3)),
]);

The weight filters are evaluated at the marker’s grid position and accumulated into a cumulative distribution; a seeded random roll selects the bucket. A weight of 0 or less is skipped. Weights do not need to sum to 1 — the selector normalises by total weight automatically.

Putting it together

import { GridCellPlacementRule }        from "@woosh/meep-engine/src/generation/placement/GridCellPlacementRule.js";
import { GridCellActionPlaceMarker }    from "@woosh/meep-engine/src/generation/markers/GridCellActionPlaceMarker.js";
import { CellMatcherFromFilter }        from "@woosh/meep-engine/src/generation/rules/CellMatcherFromFilter.js";
import { CellFilterSimplexNoise }       from "@woosh/meep-engine/src/generation/filtering/numeric/complex/CellFilterSimplexNoise.js";
import { CellFilterSaturate }           from "@woosh/meep-engine/src/generation/filtering/numeric/math/CellFilterSaturate.js";
import { CellFilterLiteralFloat }       from "@woosh/meep-engine/src/generation/filtering/numeric/CellFilterLiteralFloat.js";
import { GridActionRuleSet }            from "@woosh/meep-engine/src/generation/markers/GridActionRuleSet.js";

// 1. A filter that is high in noisy forest areas
const forestMask = CellFilterSaturate.from(CellFilterSimplexNoise.fractal(3, 40));

// 2. A rule that emits a "tree" marker when the forest mask is > 0
const treeRule = GridCellPlacementRule.from({
    matcher: CellMatcherFromFilter.from(forestMask),
    action: GridCellActionPlaceMarker.from({ type: "tree", size: 1.5 }),
    probability: forestMask,   // denser near high forest values
    allowRotation: false,
});

// 3. Run the rule set over the grid
const ruleSet = GridActionRuleSet.from({ rules: [treeRule] });
const emitTask = ruleSet.process(grid, seed);

// 4. After emission, theme engine converts "tree" markers to entities
theme.nodes.add(MarkerProcessingRule.from({
    matcher: MarkerNodeMatcherByType.from("tree"),
    action: MarkerNodeActionEntityPlacement.from({ blueprint: treeBlueprint }),
    consume: true,
}));

See Procedural generation overview for how to run the full pipeline and Cell filters & automata for the complete filter reference.