Chris' Tutorials
Docs/Grid Placement

Grid Placement v6.0

Refund on Demolish

How object demolish refunds work through BuildCost, RefundService, and RefundCalculator.

StatusDraft
Versionv6.0
UpdatedDevelopment docs generated from GDScript source

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

Draft — Unreleased:This page is in active development. APIs, class names, and behavior may change before the release is finalized. Use it as a preview of what's coming — not as a stable integration target.

When a player demolishes a placed object, many games want to give back part of the original cost. Grid Placement does not know your inventory schema, so the refund path uses a small id-keyed contract.

Version note: This guide is for object demolish refunds in Grid Placement 6.0.0. Terrain refund/economy behavior is game-owned unless you build your own terrain-specific bridge through terrain hooks.


The Short Version

SpendMaterialsRuleById defines what the object cost
→ placed object can resolve that cost through BuildCost
→ demolish triggers ManipulationState.pre_demolish
→ RefundService / Refunder reads the BuildCost
→ RefundCalculator computes id → amount
→ your inventory bridge receives try_add_by_id(id, amount)

The plugin does not ship a production inventory. Your game provides the inventory bridge.


Core Types

Type Purpose
SpendMaterialsRuleById Canonical id-keyed build cost rule.
BuildCost Resolved id-keyed cost record for a placed/demolishable object.
RefundCalculator Pure math: BuildCost × refund_ratio → Dictionary[StringName, int].
RefundService Scene-free service that resolves a container and applies refunds.
Refunder Veto-capable hook called from ManipulationState.pre_demolish.
ManipulationState.refunders Registration list for refund/veto consumers.

Older notes or tests may use the phrase “refund system,” but the current public class to look for is RefundService.


Cost Source of Truth

Use SpendMaterialsRuleById for object build costs.

Example cost shape:

{
    &"Gold": 100,
    &"Wood": 20,
}

Save the rule as its own .tres file and reference it from the placeable's placement_rules. That keeps build cost and demolish refund value tied to the same source.


How BuildCost Resolves

BuildCost.resolve_for(manipulatable) uses this order:

  1. Explicit metadata stamp on the Manipulatable under BuildCost.META_KEY.
  2. Manipulatable.build_cost_rule.
  3. PlaceableInstance under the manipulatable root, using ScenePlacementEntry.get_build_costs().
  4. null if no cost source exists.

Use metadata when the runtime cost differs from the default placeable cost, such as discounts, upgrades, or scaling.

Use Manipulatable.build_cost_rule for scenes that can be demolished but do not have a ScenePlacementEntry/PlaceableInstance backing.


Inventory Bridge Contract

Your inventory or bridge node should implement the id interface used by both spending and refunding:

Method Used for
get_count_by_id(id: StringName) -> int Build-cost validation.
try_remove_by_id(id: StringName, amount: int) -> int Spending materials after successful placement.
try_add_by_id(id: StringName, amount: int) -> int Giving materials back on demolish.

Return the actual amount added or removed. For example, if the inventory is nearly full and can only accept 5 of 10 gold, try_add_by_id() should return 5.

Example bridge:

extends Node

func get_count_by_id(id: StringName) -> int:
    return GameInventory.get_item_count(id)

func try_remove_by_id(id: StringName, amount: int) -> int:
    return GameInventory.remove_items(id, amount)

func try_add_by_id(id: StringName, amount: int) -> int:
    return GameInventory.add_items(id, amount)

Put the bridge somewhere the locator can reach from PlacementOwner.owner_root, or set a container override from your bootstrap code.


Using RefundService

RefundService can be configured with:

  • refund_ratio, such as 0.5 for half refund.
  • rounding, using RefundCalculator.Rounding.
  • A NodeLocator, or a code-set container_override.
  • The active ManipulationState and GridTargetingState.

The service registers an internal Refunder adapter into ManipulationState.refunders. During demolish, that adapter calls the service, resolves BuildCost, and applies the calculated refunds through try_add_by_id.

Practical rule: configure one refund service for the placement setup, not one per placed object. Per-object refund services can stack duplicate refunds.


Using RefundCalculator Directly

Use RefundCalculator when you want to own the hook yourself.

var cost: BuildCost = BuildCost.resolve_for(demolished_manipulatable)
var calculator := RefundCalculator.new(0.5, RefundCalculator.Rounding.ROUND)
var applied: Dictionary[StringName, int] = calculator.apply_to(inventory_bridge, cost)

This is useful when your game already has a demolish pipeline or needs custom UI/telemetry around refund results.


Custom Refunder

Extend Refunder when you need to inspect or veto demolition.

class_name MyRefundVeto
extends Refunder

func on_pre_demolish(target: Manipulatable, cost: BuildCost) -> bool:
    if target == null:
        return false

    if _refund_would_overflow_inventory(cost):
        return true # veto demolish

    return false

Register it on the active manipulation state:

container.get_states().manipulation.refunders.append(MyRefundVeto.new())

Returning true vetoes demolition. Returning false lets demolition continue.


Common Pitfalls

  • Using old RefundSystem wording/API instead of RefundService.
  • Putting one refund service inside every building scene, causing duplicate refunds.
  • Forgetting that the inventory bridge must be reachable from the owner root or supplied as an override.
  • Using different ids for spend and refund. &"Gold" and &"gold" are different keys.
  • Double-refunding by using both RefundService and a manual success/finished signal handler.
  • Expecting terrain tiles to use object demolish refunds. Terrain is cell data, not a Manipulatable object.

Related Guides

Source

docs/v6-0/guides/refund-on-demolish.md

Plugin docs root:gdscript/plugins/grid_placement_dev/docs