Placement Rules (5.0.3)

This guide covers runtime placement validation. For configuration and runtime-readiness checks, see Validation: Configuration.

What placement rules are

Placement rules are resources that validate whether an object can be placed at the current target. Each rule:

  • receives the active targeting state via setup(p_gts: GridTargetingState)
  • returns a RuleResult from validate_placement()
  • may run post-success side effects in apply()

Rule sources

There are two practical rule layers in the 5.0.3 runtime:

  • Base rules
    • configured on GBSettings.placement_rules
    • apply broadly to placement workflows
  • Placeable-specific rules
    • configured on each Placeable
    • apply only to that placeable’s preview/placement flow

Core Rule Classes

ClassPurpose
PlacementRuleBase class for all placement rules
TileCheckRuleBase class for rules that evaluate tile/indicator state
RuleResultContains validation outcome and issues

Built-in rules

GridBuilding 5.0.3 includes these built-in placement rules:

Rule ClassBase ClassPurpose
WithinTilemapBoundsRuleTileCheckRuleRestricts placement to valid tilemap cells
CollisionsCheckRuleTileCheckRuleChecks for overlapping physics
ValidPlacementTileRuleTileCheckRuleBasic validity check
SpendMaterialsRuleGenericPlacementRuleConsumes inventory/materials after successful placement

Rule lifecycle

1. setup(p_gts: GridTargetingState) -> Array[String]

Called before the rule is used. PlacementRule.setup(...) stores the targeting state and marks the rule ready.

2. validate_placement() -> RuleResult

Called during validation. This is where the rule decides pass/fail.

3. apply() -> Array[String]

Called after successful placement if the workflow uses the apply phase for side effects.

4. tear_down() -> void

Called when the preview/rule evaluation cycle is reset or completed.

Example: custom grid bounds rule

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class_name MyGridBoundsRule
extends TileCheckRule

@export var min_x: int = -5
@export var max_x: int = 5
@export var min_y: int = -5
@export var max_y: int = 5

func validate_placement() -> RuleResult:
    var target_map: TileMapLayer = _grid_targeting_state.target_map
    var positioner: Node2D = _grid_targeting_state.positioner
    
    if target_map == null or positioner == null:
        return RuleResult.build(self, ["Targeting state incomplete"])
    
    var local_pos: Vector2 = target_map.to_local(positioner.global_position)
    var target_cell: Vector2i = target_map.local_to_map(local_pos)
    
    if target_cell.x < min_x or target_cell.x > max_x or target_cell.y < min_y or target_cell.y > max_y:
        return RuleResult.build(self, ["Placement is outside the allowed bounds"])
    
    return RuleResult.build(self, [])

Example: non-tile gameplay rule

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class_name MyEconomyRule
extends PlacementRule

@export var required_credits: int = 100

func validate_placement() -> RuleResult:
    var owner_root: Node = _grid_targeting_state.get_owner()
    var economy: Node = owner_root.get_node_or_null("Economy")
    
    if economy == null:
        return RuleResult.build(self, ["Economy service not available"])
    
    if economy.credits < required_credits:
        return RuleResult.build(self, ["Insufficient credits"])
    
    return RuleResult.build(self, [])

func apply() -> Array[String]:
    var owner_root: Node = _grid_targeting_state.get_owner()
    var economy: Node = owner_root.get_node_or_null("Economy")
    if economy:
        economy.credits -= required_credits
    return []

Built-in rule behavior plugin users should know

WithinTilemapBoundsRule

  • extends TileCheckRule
  • checks each indicator against the active TileMapLayer
  • fails when indicator cells resolve to no TileData

CollisionsCheckRule

  • extends TileCheckRule
  • checks each indicator with a shapecast collision mask
  • excludes preview bodies and configured collision exclusions
  • supports both clear-space and required-overlap flows through pass_on_collision

Practical guidance

  • always call super.setup(...) if you override setup
  • use TileCheckRule when your rule depends on indicator positions or tilemap cells
  • use PlacementRule directly when the rule depends on owner/inventory/game-state logic
  • keep side effects in apply() if they should only happen after successful placement
  • put rules that should affect every placement into GBSettings.placement_rules

Common mistakes

  • forgetting to call setup(...) before validation
  • treating rules as editor-only resources instead of runtime logic
  • duplicating grid-position logic in UI instead of reading targeting state
  • putting placeable-specific rules into global settings when they should live on the Placeable