Skip to content

M7.2 — Equipment Slots

What you'll learn

  • How to model equipment as a map from slot to the equipped ItemData, separate from the bag.
  • The equip/unequip operations, including swapping (equipping into an occupied slot returns the old item to the bag).
  • How equipping feeds the item's modifiers into the M5.4 recompute, so stats change the instant you equip — and revert losslessly when you unequip.
  • Why this is the payoff of every earlier data decision: items are modifier lists, stats recompute from base, and the bag is data.

How it applies

  • Equipping is where loot becomes power. Finding a great item is half the reward; the other half is putting it on and watching your numbers jump. The equip step is the hinge between the loot loop (M6) and the stat system (M5), and it must be instant and correct or the reward falls flat.
  • Slots enforce the rules of a build. One weapon, one helm, two rings — the slot map is what stops the player from wearing five swords and what makes choosing which ring a decision. The constraints are the build game.
  • Swap must be lossless. Equipping a new helm over an old one has to return the old one to the bag, not delete it, and recompute stats exactly. A swap that loses the old item or leaves its bonus behind is a data-integrity bug — the kind a tester catches by equipping, unequipping, and asserting the bag and stats returned to their prior state.
  • It validates the architecture. If equipping is "append the item's modifiers and recompute, move the item between bag and slot," then every prior decision (items as modifier lists, recompute from base, inventory as data) was correct. If it requires special cases, something upstream was modeled wrong.

Concepts

Equipment is a slot map

Where the bag (M7.1) is a flat list, equipment is a map from slot to item: at most one item per slot. The slots match ItemData.Slot (weapon, helm, chest, boots, ring…). Equipment is data, like the inventory — an autoload (the player has one equipment set) or part of the player. This book keeps it as a small autoload (or a node on the player); the operations are what matter.

Example

An equipment model: a slot→item dictionary with equip/unequip.

# res://scripts/equipment.gd  (autoload "Equipment", or a node on the player)
extends Node

signal changed                          # a slot changed; UI redraws

var slots: Dictionary = {}              # ItemData.Slot -> ItemData

func equipped_in(slot: int) -> ItemData:
    return slots.get(slot, null)

func equip(item: ItemData) -> void:
    var slot := item.slot
    var previous: ItemData = slots.get(slot, null)
    if previous != null:
        _apply_item(previous, false)    # remove old item's modifiers
        Inventory.add_item(previous)    # return it to the bag (swap, don't delete)
    slots[slot] = item
    Inventory.remove_item(item)         # it leaves the bag while worn
    _apply_item(item, true)             # add new item's modifiers
    changed.emit()

func unequip(slot: int) -> void:
    var item: ItemData = slots.get(slot, null)
    if item == null:
        return
    _apply_item(item, false)
    slots.erase(slot)
    Inventory.add_item(item)            # back to the bag
    changed.emit()

equip handles the swap: if the slot is occupied, the old item's modifiers are removed and it returns to the bag before the new item goes on — no loss. The new item leaves the bag (you can't have it in two places). Every change emits changed for the UI.

Equipping feeds the recompute

_apply_item is the bridge to M5.4: it appends or removes the item's modifiers on the player and triggers a recompute. This is the entire mechanical effect of equipment — there is no equipment-specific stat math.

Example

func _apply_item(item: ItemData, add: bool) -> void:
    var player := get_tree().get_first_node_in_group("player")
    if add:
        player.modifiers.append_array(item.all_modifiers())
    else:
        for m in item.all_modifiers():
            player.modifiers.erase(m)
    player.recompute_stats()            # M5.4 rebuilds derived stats from base + modifiers

On equip, the item's all_modifiers() (implicit + rolled affixes, M6.1) are appended to the player's modifier list and stats recompute — the next hit is stronger immediately. On unequip, those exact modifier objects are erased and stats recompute, returning to the prior values with no residue. The losslessness comes from M5.4's rebuild-from-base design: removing modifiers and recomputing is exact, because the derived stats are rebuilt, not un-nudged.

(A subtlety: erase removes by object identity, so the same StatModifier instances added on equip must be the ones erased on unequip — which they are, because all_modifiers() returns the item's own modifier objects both times. If you instead recreated modifiers each call, you'd need to match by value; using the item's stored modifier instances avoids that.)

Swapping is the interesting case

The non-trivial operation is equipping into an occupied slot. The correct order:

  1. Remove the old item's modifiers (recompute).
  2. Return the old item to the bag.
  3. Put the new item in the slot, take it out of the bag.
  4. Add the new item's modifiers (recompute).

Get the order wrong and you can lose an item or leave a bonus behind. The model above does it in one equip call, so callers (the UI in M7.3) just say "equip this" and the swap is handled.

Why this is the architecture's exam

Notice what equip did not require: no item-specific stat code, no special handling per slot, no duplicate stat pathway. It moved an item between two data containers (bag and slot map) and fed the item's modifier list into the one recompute. That simplicity is the proof that the upstream models were right: items as modifier lists (M6.1), stats recomputed from base (M5.4), inventory as data (M7.1). If equipping had needed to special-case "swords add damage, helms add armor," the item model would have been wrong.

Walkthrough

  1. Create res://scripts/equipment.gd (the model above). Register as autoload Equipment (after Inventory, which it calls), or attach it to the player — either works; the book uses the autoload.
  2. Confirm the player exposes modifiers and recompute_stats() (M5.4) and is in the player group (M4.2) so _apply_item can find it.
  3. (Temporary, until M7.3) Drive equip from code to test: pick up an item, then call Equipment.equip(Inventory.items[0]) from a debug key. Print the player's derived.damage_min/max before and after.
  4. Press F5. Pick up a weapon with a damage affix, equip it, and confirm the player's derived damage rises (and the next hit is stronger). Unequip and confirm damage returns exactly to its prior value.
  5. Test the swap: equip a second weapon while one is worn; confirm the first returns to the bag, the second is worn, and stats reflect only the second. No item is lost; no bonus lingers.

Optional sanity check

Record the player's derived damage with nothing equipped. Equip a weapon (damage rises), then unequip it (damage returns to the recorded value — exactly, not off by a rounding step). Now equip A, equip B over it (A returns to bag), unequip B (B to bag): the bag should contain both A and B and the player's damage should equal the no-equipment baseline. Any drift in the number or a missing item in the bag is a swap/recompute bug — exactly the lossless-round-trip property the rebuild-from-base design guarantees, and a state-coverage check a tester would run.

Self-check quiz

Q1 — When equipping a new item into an occupied slot, what must happen to the previously equipped item, and why?

A. It is deleted to make room. B. Its modifiers are removed (recompute) and it is returned to the bag before the new item is equipped, so no item is lost and no stale bonus remains. C. It stays equipped alongside the new one. D. It is moved to a random slot.

Reveal answer

B. A swap must be lossless: the old item's bonuses come off (recompute) and it goes back to the bag, then the new item goes on. Deleting it (A) loses loot; keeping both (C) violates one-per-slot; moving it elsewhere (D) is wrong. The model's equip does this in order.

Q2 — Why does equipping require no equipment-specific stat code?

A. Because equipment doesn't affect stats. B. Because an item is a list of StatModifiers (M6.1) and the player recomputes from base + modifiers (M5.4); equipping just appends the item's modifiers and recomputes, so the existing stat pipeline does all the work. C. Because Godot equips items automatically. D. Because slots compute stats themselves.

Reveal answer

B. The item is its modifier list, and the stat system already knows how to apply modifiers, so equip is "append modifiers + recompute" and unequip is "erase + recompute." No per-slot or per-item stat math exists. A is false (it very much affects stats). C and D are fabricated.

Q3 — Why is unequipping guaranteed to return stats to their exact prior values?

A. Because the engine caches the old stats. B. Because M5.4 rebuilds the derived stats from the immutable base plus the current modifier list; removing the item's modifiers and recomputing reconstructs the prior result with no rounding residue. C. Because items round their bonuses to integers. D. It isn't guaranteed; small drift is expected.

Reveal answer

B. The rebuild-from-base design means stats are never nudged in place; removing modifiers and recomputing reproduces exactly the value you'd get without them. A is false (no such cache). C is irrelevant to losslessness. D is the bug the design prevents — drift is exactly what in-place mutation would cause and recompute avoids.

Integration question

Q4 — open

Equipping moves an item between two data containers (bag and slot map) and feeds its modifier list into one recompute — and needs no item-specific stat code. Explain why that simplicity is the proof that M6.1 (items as modifier lists) and M5.4 (recompute from base) were modeled correctly, and describe the full state change, across inventory, equipment, and player stats, when the player equips a rare sword over a common one.

Reveal expected answer

The simplicity is the proof because a correct upstream model makes the downstream operation trivial: since an item is a base type plus a StatModifier list (M6.1) and the player's stats are rebuilt from an immutable base plus the current modifier list (M5.4), equipping reduces to two container moves (item leaves the bag, enters the slot map; any displaced item makes the reverse trip) plus appending the item's modifiers and recomputing. No branch on "is this a sword or a helm," no per-stat special case — the item declares its own effect as data the stat system already consumes. If either model were wrong (say items stored "damage += 5" as bespoke code, or stats were nudged in place), equip would need special cases and unequip would leak residue; the absence of those is the evidence the models were right. The full state change equipping a rare sword over a common one: in equipment, the common sword is removed from the weapon slot, its modifiers are erased from the player and stats recompute (back to baseline for that slot), and it is returned to the inventory via add_item; then the rare sword is placed in the weapon slot, removed from the inventory via remove_item, its all_modifiers() appended to the player's list, and recompute_stats() (M5.4) rebuilds the derived StatBlock with the rare's flat/increased/more damage, updating Health's max and emitting stats_recomputed so the HUD reflects the jump. Both Equipment.changed and Inventory.changed fire, so the equipment panel and the bag grid redraw. Net: the bag now holds the common sword instead of the rare, the weapon slot holds the rare, and the player's damage reflects exactly the rare's modifiers — all from container moves plus one recompute.