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:
- Explicit metadata stamp on the
ManipulatableunderBuildCost.META_KEY. Manipulatable.build_cost_rule.PlaceableInstanceunder the manipulatable root, usingScenePlacementEntry.get_build_costs().nullif 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 as0.5for half refund.rounding, usingRefundCalculator.Rounding.- A
NodeLocator, or a code-setcontainer_override. - The active
ManipulationStateandGridTargetingState.
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
RefundSystemwording/API instead ofRefundService. - 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
RefundServiceand a manual success/finished signal handler. - Expecting terrain tiles to use object demolish refunds. Terrain is cell data, not a
Manipulatableobject.
Related Guides
Source
docs/v6-0/guides/refund-on-demolish.md
Plugin docs root:gdscript/plugins/grid_placement_dev/docs