Grid Placement

Placement Rules (5.0.2)

How to create and use runtime placement rules for validating object placement in GridBuilding 5.0.2

This guide covers runtime placement validation — rules that run when players attempt to place objects. For configuration validation at startup, see Validation Tools.

What Are Placement Rules?

Placement rules are components that validate whether an object can be placed at a specific location. Each rule:

  • Receives the current targeting state via setup(p_gts: GridTargetingState)
  • Returns a RuleResult with success/failure messages
  • Optionally performs actions in apply() after successful placement

Core Rule Classes

ClassPurpose
PlacementRuleBase class for all placement rules (v5.0)
TileCheckRuleBase class for rules that evaluate against TileMapGrid
RuleResultContains validation outcome and messages

Built-in Rules (v5.0)

GridBuilding 5.0.2 includes these built-in placement rules:

Rule ClassBase ClassPurposeKey Properties
WithinTilemapBoundsRuleTileCheckRuleRestricts placement to tilemap boundsUses tilemap’s used_rect
CollisionsCheckRuleTileCheckRuleChecks for overlapping physicsapply_to_objects_mask, collision_mask
ValidPlacementTileRuleTileCheckRuleBasic validity checkUses ValidPlacementTileRuleSettings
SpendMaterialsRuleGenericPlacementRuleConsumes inventory itemscost_map, inventory_component_path

Example: Custom Grid Bounds Rule (TileCheckRule)

For rules that evaluate against the TileMap, extend TileCheckRule. In 5.0.2, you calculate the current cell using the target_map and the positioner’s global position.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class_name MyGridBoundsRule
extends TileCheckRule

## Custom rule that limits placement to specific grid coordinates.
## This example restricts placement to the center 10x10 area of the grid.

@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"])
    
    # Calculate current map cell from global position
    var local_pos := target_map.to_local(positioner.global_position)
    var target_cell := 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:
        var message := "Can only place within bounds (x: %d to %d, y: %d to %d)" % [min_x, max_x, min_y, max_y]
        return RuleResult.build(self, [message])
    
    return RuleResult.build(self, [])

func apply() -> Array[String]:
    # Optional post-placement logic
    return []

Using the Custom Rule

1
2
3
4
5
6
7
# Add to a placeable's placement_rules
var bounds_rule := MyGridBoundsRule.new()
bounds_rule.min_x = -10
bounds_rule.max_x = 10

var placeable: Placeable = load("**res://items/my_item.tres**")
placeable.placement_rules.append(bounds_rule)

Example: Rule with Dependencies (PlacementRule)

For rules that don’t need direct tile access, extend PlacementRule directly. Dependencies are typically accessed through the GBCompositionContainer which you can capture during resolve_gb_dependencies or via the _grid_targeting_state’s owner context.

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

## Rule that checks player currency before allowing placement.

@export var required_credits: int = 100

func validate_placement() -> RuleResult:
    # Example: Accessing a global economy service
    var economy = _grid_targeting_state.get_owner().get_node("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 economy = _grid_targeting_state.get_owner().get_node("Economy")
    if economy:
        economy.credits -= required_credits
    return []

Rule Lifecycle

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

Called to initialize the rule with targeting state. In 5.0.2, this is handled by the IndicatorManager.

1
2
3
func setup(p_gts: GridTargetingState) -> Array[String]:
    # Always call super.setup to initialize _grid_targeting_state and _ready
    return super.setup(p_gts)

2. validate_placement() -> RuleResult

Called during validation to check if placement is allowed. Returns a RuleResult.

1
2
3
4
5
func validate_placement() -> RuleResult:
    # Use _grid_targeting_state to perform checks
    if _is_valid():
        return RuleResult.build(self, [])
    return RuleResult.build(self, ["Invalid placement"])

3. apply() -> Array[String]

Called after successful placement to perform actions (spend resources, trigger events, etc.). Returns an array of error messages (empty if successful).

1
2
3
func apply() -> Array[String]:
    # Logic to run when placement is confirmed
    return []

4. tear_down() -> void

Called when the rule evaluation cycle is finished or changed.

1
2
3
func tear_down() -> void:
    # super.tear_down cleans up _ready state
    super.tear_down()

Validated By

  • Rule Logic & Inheritance: res://addons/grid_building/placement/placement_rules/placement_rule.gd — Defines the base contract and lifecycle for 5.0.2 rules.
  • Tile-Based Rules: res://addons/grid_building/placement/placement_rules/tile_check_rule.gd and res://addons/grid_building/test/rules/validation/tilemap_bounds_rule_unit_test.gd — Verify coordinate-based validation.
  • Rule Orchestration: res://addons/grid_building/test/rules/validation/rule_system_integration_tests.gd — Validates the full sequence of setup -> validate_placement -> apply.
  • Built-in Implementation: res://addons/grid_building/placement/placement_rules/template_rules/spend_materials_rule_generic.gd — Example of a production-stable rule using the apply() hook.