Skip to content

Grid Placement v6.0

Custom Placement Rules

Write your own placement rules for game-specific validation logic.

Status
Draft
Version
v6.0
Updated
Development docs generated from GDScript source

This is unreleased documentation in active development. APIs, class names, and behavior may change before the final release.

Use a custom rule for game policy that Grid Placement cannot know on its own: costs, progression, ownership, reserved zones, or other project-specific facts.

Do not use custom rules to replace core occupancy, GRID mount, SMOOTH footprint, or 3D slope/support validation.

Pick the base class

Base Use when
PlacementRule The rule checks game/session state and does not need per-cell 2D indicators.
TileCheckRule The rule needs 2D tile/indicator positions.

Start with PlacementRule unless you specifically need 2D indicator cells.

Minimal rule

class_name MyRule
extends PlacementRule

func validate_placement()-> RuleResult:
    if _should_block_placement():
        return RuleResult.build(self, ["Placement is blocked"])
    return RuleResult.build(self, [])

An empty issue list means the rule passed.

Side effects belong after success

Validation can run repeatedly while the preview moves. Keep it read-only.

Use the supported apply/post-success lifecycle for irreversible effects such as:

  • spending materials;
  • updating progression;
  • recording committed placement analytics;
  • applying game state that should exist only after commit.

Do not mutate those systems from validate_placement().

Optional lifecycle methods

Use additional rule lifecycle methods only when the rule needs them:

  • setup(...) — resolve required context before validation;
  • apply() — successful-placement side effects;
  • tear_down() — clear temporary rule state;
  • setup/runtime/editor issue methods — report configuration/diagnostic problems.

When extending base behavior, preserve the base method contract unless you intentionally replace it.

2D indicator-specific rules

TileCheckRule is useful when different covered 2D cells need different feedback.

Override get_failing_indicators(...) only when you need per-indicator results. If the whole placement has one answer, the default validation result is simpler and less error-prone.

When threaded 2D physics is enabled, prefer the plugin's cached indicator collision data instead of performing ad-hoc physics queries from rule code outside the physics tick.

Example: game-owned economy

class_name CreditPlacementRule
extends PlacementRule

@export var required_credits:= 100

func validate_placement()-> RuleResult:
    var economy:= _resolve_economy()
    if economy== null:
        return RuleResult.build(self, ["Economy service is unavailable"])
    if economy.credits< required_credits:
        return RuleResult.build(self, ["Not enough credits"])
    return RuleResult.build(self, [])

func apply()-> Array[String]:
    var economy:= _resolve_economy()
    if economy!= null:
        economy.credits-= required_credits
    return []

The important part is the lifecycle: check during validation, spend only after success.

For common material costs, prefer the built-in id-keyed cost rule instead of writing a custom economy rule from scratch.

Example: 2D cell restriction

A TileCheckRule can reject cells outside a game-defined 2D area or inspect TileMap cell data. Keep the rule focused on the project-specific restriction; bounds/collision behavior already provided by Grid Placement should stay in the core/built-in path.

For game-owned occupancy/reservation data, prefer Placement World Facts Provider over hard-coding a game registry directly into the addon.

Where to attach a rule

Location Best use
PlacementSettings.placement_rules Shared/default project rule.
PlacementProfile.placement_rules Category/profile behavior.
ScenePlacementEntry.placement_rules One entry's unique requirement.

While developing a rule, attach it to one entry first. Promote it only after its intended scope is clear.

Failure messages

Return a short reason that helps the developer/player act:

Good: "Requires 10 wood", "Reserved construction zone", "Must be near water".

Avoid: "failed", raw internal object dumps, or messages that expose implementation details without a useful correction.

Test every rule

At minimum verify:

  • allowed case;
  • blocked case and issue text;
  • missing required context;
  • repeated preview validation causes no side effects;
  • successful commit applies side effects exactly once.

For TileCheckRule, also verify the expected per-cell/indicator behavior.

Related guides