Table of Contents

Version: 6.0.0 (additive — no existing API changes)

Note

Design Philosophy & Scope: The refund mechanics are kept canonically inside the plugin as high-value convenience helpers. They are completely inventory-agnostic. While the plugin provides a generic drop-in RefundSystem (perfect for rapid prototyping), it deliberately does not predefine a tight bridge to any specific grid-inventory layout. For production games, developers are expected to implement a custom Refunder subclass to connect the demolition pipeline with their specific game database or network inventory framework.

When a player demolishes a building, most games want to give something back: gold, materials, achievement counters. The addon does not know what your inventory looks like, so 6.0.0 ships a small, canonical contract instead of a built-in inventory. Costs are addressed by StringName ids end-to-end — the same ids used by SpendMaterialsRuleById on the spend side:

  1. SpendMaterialsRuleById — spends Dictionary[StringName, int] costs at placement time, transactionally (partial failures roll back). Save it as a standalone .tres (e.g. smithy_costs_rule.tres): that one resource is the single source of truth for the cost, referenced by the placeable's placement_rules (spend side) and resolvable on the refund side.
  2. Scene-side labels — how a demolished instance finds its cost. Only objects with a [Manipulatable] can be demolished, so refund identity lives with demolishability:
    • PlaceableInstance (placeable-backed scenes): placeable_path links the scene to its placeable; Placeable.get_build_costs() sums the rule costs. The instantiator attaches one at runtime and the save system restores it, so runtime-built, editor-placed, and save-restored copies all resolve.
    • Manipulatable.build_cost_rule (scenes with no placeable backing): the demolishable object's own export, referencing the saved spend rule .tres directly. Per-object, on the node — not on the shared ManipulatableSettings resource.
  3. BuildCost.resolve_for(manipulatable) — the canonical resolution order: META_KEY metadata stamp (dynamic-cost override: discounts, scaling) → Manipulatable.build_cost_rulePlaceableInstancenull.
  4. Refund execution — pick your shape:
    • RefundSystem — drop-in system-level component (one per game, beside your other placement systems): exports refund_ratio, rounding, and a container locator (owner-root resolution, same pattern as the spend rules; bootstrap code can set container_override directly). Registers itself on injection. Do not put a RefundSystem inside a building scene: every registered refunder processes every demolition, so one per placed instance stacks the refunds — the node warns when it detects this.
    • RefundCalculator — the pure math (BuildCost × ratio → {id: amount}, apply_to(container) via the id interface) for consumers who'd rather call it from their own Refunder on an existing script.
  5. Refunder / ManipulationState.refunders — the veto-capable hook and registration point both shapes plug into. Return true from on_pre_demolish to cancel the demolition.

The demo wiring (smithy: 100 Gold to build, 50 Gold back)

The top-down demo is the reference integration, and it is configuration only — zero refund code:

  • demos/top_down/rules/smithy_costs_rule.tres — the saved SpendMaterialsRuleById ({&"Gold": 100} + locator). Cost SSOT.
  • placeable_smithy.tres references that rule in placement_rules (spend).
  • smithy.tscn carries a PlaceableInstance with placeable_path = ".../placeable_smithy.tres" (refund resolution).
  • demo_top_down.tscn has a RefundSystem under Systems with refund_ratio = 0.5 and the same MaterialsContainer locator the spend rule uses.

Demolishing any smithy — including the one editor-placed in top_down_level_1.tscn, which was never "built" at runtime — refunds 50 Gold into the player's ItemContainer. There is no stamping step; stamping META_KEY metadata remains available as a per-instance override when a building's cost diverges from its placeable (sale prices, upgrades, scaling).

A refunder returning true cancels the demolition: the node is preserved and the demolish data reports CANCELED (the 6.0.0-C pre_demolish veto path). demos/shared/refund/demo_inventory_refunder.gd shows a vetoing example (refund would overflow a capacity cap).

Wiring into your inventory (The "Id Interface")

The refund system does not need to know about your Item classes or Inventory architecture. It speaks in Ids (StringName). This is the "glue" that keeps the plugin lightweight and decoupled.

Your inventory (or a thin bridge node) just needs to implement these three methods to work with both the build-cost spend rules and the demolish-refund system:

Method Purpose Implementation Example
get_count_by_id(id) -> int Check if player can afford a build return _my_inv.get_quantity(id)
try_remove_by_id(id, amount) -> int Spend materials on placement return _my_inv.take(id, amount)
try_add_by_id(id, amount) -> int Give materials back on demolish return _my_inv.give(id, amount)

Note on Return Values: try_remove_by_id and try_add_by_id should return the actual amount successfully added or removed. If your inventory is full and can only take 5 out of 10 gold, return 5. The refund system uses this for transactional safety.

Example: ItemContainer (demo inventory)

If you use the demo inventory at demos/shared/inventory/, ItemContainer implements this interface automatically, using the item's display_name (or id if set) as its ID. Note this is demo-level prototyping code, not a production inventory system.

Example: Custom Bridge Node

If you have an existing inventory autoload GameInventory, add a small bridge script to a node under your player:

# inventory_bridge.gd
extends Node

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

func try_add_by_id(p_id: StringName, p_amount: int) -> int:
    # return the amount actually added
    return GameInventory.add_items(p_id, p_amount)

func try_remove_by_id(p_id: StringName, p_amount: int) -> int:
    return GameInventory.remove_items(p_id, p_amount)

Point the RefundSystem's locator at this node (using its name or group), and the wiring is complete.

Legacy / migration

  • The generic ResourceStack-keyed spend rule (spend_materials_rule_generic.gd) is deprecated: its global class_name was removed in 5.1 so it no longer appears in the editor create dialog, and it logs a one-time runtime warning. Existing .tres files keep loading (they bind by script path/uid); code that referenced the class name must preload the script during migration. It remains functional and transactional on the 5.0.x line unchanged.
  • Convert legacy stacks with BuildCost.from_resource_stacks(stacks); flat pairs with BuildCost.from_pairs([[&"coin", 10]]).
  • Adoption is opt-in: when no cost source resolves, refunders receive null and decide for themselves.

Tests / further reading

  • test/grid_building/demos/top_down_demo_e2e_test.gdtest_smithy_costs_gold_and_demolish_refunds_half (runtime build) and test_preplaced_smithy_refunds_half_without_ever_being_built (editor-placed, scene-as-source-of-truth) — the demo wiring end-to-end against the real scene.
  • test/grid_building/integration/system_interactions/refund_node_test.gd — RefundSystem registration, container_path refunding, unregistration.
  • test/grid_building/unit/refund_calculator_test.gd — the pure math (ratio, rounding policies, id-interface application).
  • test/grid_building/unit/spend_materials_rule_by_id_test.gd — canonical rule (validation, transactional rollback, smithy resource shape).
  • test/grid_building/demos/demo_refund_example_test.gd — generic worked example (stamp → demolish → refund, ratio, veto-on-overflow, unstamped).
  • test/grid_building/integration/system_interactions/refunder_contract_test.gd — the contract itself.
  • ROADMAP section 6.0.0-D — Demolish Refund Contract for the design notes.