M6.1 — The ItemData Resource¶
What you'll learn
- How to model an item as a
Resource: a base type plus the data that makes one sword differ from another. - The split between an item's base type (shared template: "Iron Sword", icon, slot, implicit stats) and a specific rolled instance (this drop's rarity and affixes).
- Why item modifiers are exactly the
StatModifierlist from M5.4, so equipping an item is just feeding those modifiers into the recompute. - How
duplicate()produces a unique instance from a shared base template.
How it applies
- Loot is the genre's reason to keep playing. "What did it drop?" is the question that pulls the player through the dungeon. The item model is the structure behind that question; if it's clean, every later loot feature (rarity, affixes, tooltips, comparison) attaches to it.
- Base type vs instance is the model that makes variety cheap. A thousand distinct rare swords are not a thousand assets — they are one "Sword" base type plus a thousand different rolled affix lists. Separating the template from the roll is what lets a handful of base types produce effectively infinite loot.
- Items reuse the stat system, not a parallel one. An item's bonuses are
StatModifiers (M5.4), so equipping (M7) is appending them to the player's modifier list and recomputing. No second stat pathway means no divergence between "what the item says" and "what it does" — a class of bug a tester would otherwise hunt forever. - Serializable by construction. Because items are Resources made of typed fields and a modifier list, they save and load (M8) without bespoke code — the same serialization the StatBlock gets.
Concepts¶
An item is data¶
An item is not a node — it has no behavior of its own while in your bag; it is data describing a thing
you can equip. So it is a Resource, like StatBlock. The minimum an ARPG item needs:
display_name— what the tooltip shows.icon— the inventory sprite.base_type— what kind of item it is (which equipment slot, what it fundamentally is).rarity— its tier (M6.2 defines the enum and what it gates).modifiers— the list ofStatModifiers (M5.4) it grants when equipped.- optional
implicitmodifiers — bonuses inherent to the base type, present at every rarity.
Example
A first ItemData:
# res://scripts/item_data.gd
class_name ItemData
extends Resource
enum Slot { WEAPON, HELM, CHEST, BOOTS, RING }
@export var display_name: String = "Item"
@export var icon: Texture2D
@export var slot: Slot = Slot.WEAPON
@export var rarity: int = 0 # index into the rarity tiers (M6.2)
@export var implicit: Array[StatModifier] = [] # inherent to the base type
@export var affixes: Array[StatModifier] = [] # rolled on this specific instance (M6.2)
func all_modifiers() -> Array[StatModifier]:
return implicit + affixes
all_modifiers() concatenates the base type's inherent bonuses with the rolled affixes — the single
list M7's equip step appends to the player's modifiers. The item's effect is entirely those
StatModifiers; nothing else about the item touches combat.
Base type vs rolled instance¶
There are two layers:
- A base type is a template authored once as a
.tres: "Iron Sword" — name, icon, slotWEAPON, and any implicit modifiers (e.g., a weapon's inherent base damage). It carries no rolled affixes; it is the blank from which drops are stamped. - A rolled instance is a specific drop: a
duplicate()of a base type withrarityset andaffixesfilled by the roll (M6.2). Two drops of "Iron Sword" share the base template but have different affix lists — they are different items.
This is the M5.1 shared-base / duplicate()-to-diverge rule applied to loot. The base type is shared,
immutable design data; each drop is an independent copy that diverges via its rolled affixes.
Example
Producing a drop from a base type (the roll itself is M6.2; here is the duplication shape):
static func roll_from_base(base: ItemData, rarity: int, rolled: Array[StatModifier]) -> ItemData:
var item := base.duplicate(true) as ItemData # deep copy so affixes is independent
item.rarity = rarity
item.affixes = rolled
return item
duplicate(true) deep-copies so the new item's affixes array is its own, not shared with the base
(a shallow copy would have every drop appending into the same array — a classic aliasing bug). The
base .tres is never mutated; each drop is a private instance.
Why items reuse StatModifier¶
An item could have invented its own bonus format. It doesn't, on purpose: its bonuses are the exact
StatModifier type from M5.4. The payoff is that the whole stat pipeline already knows how to apply them.
Equipping (M7) is player.modifiers.append_array(item.all_modifiers()); recompute_stats(); unequipping is
erase-ing them and recomputing. There is no item-specific stat math — items plug into the one stat
system, so an item's tooltip text and its mechanical effect are the same data, never two representations
that can disagree.
Base type as .tres, drop as runtime copy¶
You author base types in the editor (one .tres per base item) and store them where the drop system can
reach them (a folder, or a registry). At runtime, a drop is a duplicate() of a base with a rolled
rarity and affix list. The authored base types are content a designer maintains; the drops are ephemeral
instances the game generates, picks up, equips, and saves.
Walkthrough¶
- Create
res://scripts/item_data.gd(theItemDataResource above, with theSlotenum,all_modifiers(), and theroll_from_basestatic or a separate factory). - Author a base type: FileSystem dock →
Create New → Resource → ItemData, save asres://resources/items/iron_sword.tres. Setdisplay_name"Iron Sword",slotWEAPON, assign anicon. Add animplicitStatModifier(e.g., flat+3 damage) so even a plain Iron Sword does something. - Author one or two more base types (a helm, a ring) so later systems have variety to draw from.
- (Sanity, temporary) In a throwaway script, load
iron_sword.tres,duplicate(true)it, setrarity = 1, append aStatModifierto itsaffixes, andprint(item.all_modifiers().size())— confirm the copy has implicit + affixes and the original.tresstill has only the implicit. This proves the base/instance split before M6.2 automates the roll. - There is nothing to see in-game yet — items are data until M6.4 spawns a pickup and M7 shows an
inventory. M6.2 fills
affixeswith real rolls; M6.3 decides which base type drops.
Optional sanity check
After the duplicate-and-modify experiment, load iron_sword.tres fresh in a second variable and
confirm its affixes array is still empty and its implicit unchanged — proof duplicate(true)
isolated the drop from the base. If the base shows the rolled affix, you used a shallow copy
(duplicate() without true) and the arrays are aliased — the exact bug the deep copy prevents.
Self-check quiz¶
Q1 — Why is an item modeled as a Resource rather than a Node?
A. Resources render in the inventory automatically. B. An item in your bag is inert data describing an equippable, not a live scene-tree object with behavior; Resources are inspector-editable, shareable as base types, and serializable for saves. C. Nodes can't hold a Texture2D. D. Resources are required for arrays.
Reveal answer
B. An item is data — name, icon, slot, modifiers — with no runtime behavior while held, which
is exactly a Resource's role; it's authored as a base .tres, copied per drop, and saved with the
game. (Only the pickup in the world, M6.4, is a node.) A is false. C is false. D is false.
Q2 — Why use duplicate(true) (deep copy) when creating a drop from a base type?
A. To make the drop render faster.
B. So the drop's affixes array is independent; a shallow copy would share the base's array, so every
drop would append into the same list (aliasing) and corrupt the base and each other.
C. Because shallow copies aren't allowed for ItemData.
D. To reset the modifiers to empty.
Reveal answer
B. A shallow duplicate() copies the reference to the affixes array, so all drops (and the
base) would point at one shared array; deep duplicate(true) gives each drop its own array to roll
into. A is irrelevant. C is false (shallow is allowed, just wrong here). D is false — duplicate
copies values, it doesn't empty them.
Q3 — Why are an item's bonuses the same StatModifier type from M5.4 rather than a new item-specific format?
A. To save memory. B. So items plug straight into the existing recompute pipeline: equipping appends the item's modifiers and recomputes, unequipping erases them — no item-specific stat math, so tooltip text and actual effect are one source of truth. C. Because items can't define their own types. D. Because StatModifier is the only Resource allowed in arrays.
Reveal answer
B. Reusing StatModifier means the stat system already knows how to apply item bonuses, so
equip/unequip is just modifying the modifier list and recomputing; there's no parallel item math
to keep in sync with the display. A is negligible. C and D are false.
Integration question¶
Q4 — open
ItemData reuses three M5 ideas: it's a Resource (M5.1), its bonuses are StatModifiers (M5.4), and
drops are duplicate()d from a shared base (M5.1's divergence rule). Explain how the base-type /
rolled-instance split makes a large, varied loot pool cheap to produce, and trace what M7's equip step
will do with all_modifiers() to change the player's stats — showing why no item-specific combat code
is ever needed.
Reveal expected answer
The base-type / rolled-instance split decouples what kind of thing an item is from what this
particular drop rolled. Base types are a small set of authored .tres templates (Iron Sword,
helm, ring) carrying name, icon, slot, and implicit bonuses; a drop is a deep duplicate() of one
base with a rarity and a rolled affixes list (M6.2). Because the variety lives in the rolled
affix lists, a handful of base types times the affix combinations yields effectively unlimited
distinct items without authoring each one — the same shared-base/diverge-by-copy pattern M5.1
established, now the engine of loot variety. At equip time (M7), the player calls
item.all_modifiers() (implicit + rolled affixes), appends that list to its own modifiers array,
and runs recompute_stats() from M5.4 — which rebuilds the derived StatBlock via the
(base + flat) * (1 + Σincreased) * Π(1 + more) formula and updates Health and the HUD via
stats_recomputed. Unequipping erases exactly those modifiers and recomputes. No item-specific
combat code exists anywhere because an item is a base type plus a list of the same modifiers the
stat system already consumes — the item's described bonus and its mechanical effect are literally
the same data, so they cannot disagree.