Terrain Change Policy

Terrain brush paint and restore (demolish-to-previous-ground) can be restricted per cell. GridPlacement provides a generic, engine-agnostic validator; games choose occupancy behavior and may read TileSet custom data in the Godot adapter.

When this applies

ActionValidated by
Terrain palette paint (GridMode.Paint / tiles category)TerrainChangeValidator via PlacementControllerBase.CanChangeTerrainAt
Terrain restore on demolish (when no ECS occupant removed)Same check before TerrainPaintManager.RestoreCells
Structure / crop placementNot affected — still uses IsOccupied, IsBuildable, and placement rules

TileSet custom data (hard block)

Add a bool custom data layer on your TileSet:

KeyTypeEffect
terrain_change_blockedboolWhen true, paint and restore are denied for that atlas tile

This is defined in Core as TerrainChangeCustomData.ChangeBlockedKey. The Godot adapter reads it from the committed ground TileMapLayer via TileMapCustomData.

Designer-authored examples: bedrock, story tiles, water that must not be overwritten.

Occupancy policy (game choice)

TerrainChangeOccupancyPolicy controls whether occupied cells (per PlacementControllerBase.IsOccupied) may still be brushed:

PolicyBehavior
AllowWhenOccupied (default)Player may paint over crops/buildings; the game handles cleanup (farmability, crop drops, demolish rules, etc.).
BlockWhenOccupiedBrush is denied while the cell is occupied (preview shows invalid).

Set on your derived placement controller:

1
2
3
4
5
// Default — no code required for allow-over-occupants games (e.g. moonbark-idle)
TerrainChangeOccupancyPolicy = TerrainChangeOccupancyPolicy.AllowWhenOccupied;

// Stricter sim / city builder
TerrainChangeOccupancyPolicy = TerrainChangeOccupancyPolicy.BlockWhenOccupied;

Tile data terrain_change_blocked always wins: a blocked tile cannot be painted even when occupancy is allowed.

Core API (headless / tests)

1
2
3
4
5
6
7
8
9
var request = new TerrainChangeRequest(
    cell,
    hasGroundTile: true,
    isOccupied: true,
    tileDataChangeBlocked: false);

TerrainChangeResult result = TerrainChangeValidator.Validate(
    request,
    TerrainChangeOccupancyPolicy.AllowWhenOccupied);

Godot override points

On PlacementControllerBase:

MemberPurpose
TerrainChangeOccupancyPolicyOccupancy policy property
CanChangeTerrainAt(CoreVector2I)Uses validator; override for extra game rules
IsTerrainChangeBlockedByTileData(CoreVector2I)Reads terrain_change_blocked from GroundLayer; override for non-TileSet sources

After a successful paint, continue to sync simulation in OnTerrainPainted(TerrainPaintImpact) (farmability, crops, etc.) — that remains game-level.