# 4. Equipment, Items and Classes
Part of [[Stat-Driven RPG Systems]]. Requires Unity 6000.5+. Previous: [[3.Leveling and XP Curves]]. Next: [[5.Damage Formulas]].
## 4.1 Character classes
```csharp
// CharacterClass.cs
using System;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "RPG/Character Class")]
public class CharacterClass : ScriptableObject
{
[Serializable] public struct StatValue { public StatDefinition stat; public float value; }
public string displayName = "Warrior";
public Sprite portrait;
public List<StatValue> baseStats = new(); // level 1 values
public List<StatValue> growthPerLevel = new(); // added each level
public List<EquipSlot> allowedSlots = new() { EquipSlot.Head, EquipSlot.Chest, EquipSlot.MainHand, EquipSlot.OffHand, EquipSlot.Ring };
public List<ItemCategory> allowedWeapons = new();
public float GrowthFor(StatDefinition stat)
{
foreach (var g in growthPerLevel) if (g.stat == stat) return g.value;
return 0f;
}
}
```
Make `Warrior` (high STR/VIT growth), `Rogue` (DEX), and `Mage` (INT). Make sure every primary stat appears in `baseStats`, even at 0, so it exists on the character.
## 4.2 Items
The **definition** is the asset. The **instance** is what the player owns. It holds per-copy data such as a rolled affix or durability, and it's what we use as the modifier source.
```csharp
// ItemDefinition.cs
using System;
using System.Collections.Generic;
using UnityEngine;
public enum EquipSlot { None, Head, Chest, Legs, MainHand, OffHand, Ring, Amulet }
public enum ItemCategory { Misc, Sword, Axe, Bow, Staff, Dagger, Shield, Armor, Jewelry }
[CreateAssetMenu(menuName = "RPG/Item")]
public class ItemDefinition : ScriptableObject
{
[Serializable] public struct Mod { public StatDefinition stat; public float value; public ModType type; }
public string id; // stable id for saving, e.g. "sword_iron"
public string displayName = "Iron Sword";
public Sprite icon;
public ItemCategory category;
public EquipSlot slot = EquipSlot.MainHand;
public int requiredLevel = 1;
public List<Mod> modifiers = new();
[Header("Weapon")]
public float minDamage = 3, maxDamage = 7;
void OnValidate() { if (string.IsNullOrEmpty(id)) id = name.ToLowerInvariant().Replace(' ', '_'); }
}
[Serializable]
public class ItemInstance
{
public ItemDefinition def;
public List<ItemDefinition.Mod> rolledAffixes = new(); // random "of the Bear" etc.
public ItemInstance(ItemDefinition d) { def = d; }
}
```
## 4.3 Equipment
```csharp
// Equipment.cs
using System;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(CharacterStats), typeof(CharacterLevel))]
public class Equipment : MonoBehaviour
{
readonly Dictionary<EquipSlot, ItemInstance> _equipped = new();
CharacterStats _stats;
CharacterLevel _level;
public event Action<EquipSlot, ItemInstance> Changed;
public IReadOnlyDictionary<EquipSlot, ItemInstance> Equipped => _equipped;
void Awake() { _stats = GetComponent<CharacterStats>(); _level = GetComponent<CharacterLevel>(); }
public bool CanEquip(ItemInstance item, out string reason)
{
reason = null;
var cls = _stats.Class;
if (item?.def == null || item.def.slot == EquipSlot.None) { reason = "Not equippable"; return false; }
if (_level.Level < item.def.requiredLevel) { reason =
quot;Requires level {item.def.requiredLevel}"; return false; }
if (cls && !cls.allowedSlots.Contains(item.def.slot)) { reason = "Slot not allowed for class"; return false; }
if (cls && item.def.slot == EquipSlot.MainHand && cls.allowedWeapons.Count > 0 && !cls.allowedWeapons.Contains(item.def.category))
{ reason = quot;{cls.displayName} cannot use {item.def.category}"; return false; }
return true;
}
/// <returns>The item that was previously in the slot (put it back in the inventory).</returns>
public ItemInstance Equip(ItemInstance item)
{
if (!CanEquip(item, out var reason)) { Debug.Log(reason); return null; }
var previous = Unequip(item.def.slot);
_equipped[item.def.slot] = item;
foreach (var m in item.def.modifiers) _stats.Get(m.stat)?.AddModifier(new StatModifier(m.value, m.type, item));
foreach (var m in item.rolledAffixes) _stats.Get(m.stat)?.AddModifier(new StatModifier(m.value, m.type, item));
Changed?.Invoke(item.def.slot, item);
return previous;
}
public ItemInstance Unequip(EquipSlot slot)
{
if (!_equipped.TryGetValue(slot, out var item)) return null;
foreach (var s in _stats.All) s.RemoveAllFromSource(item);
_equipped.Remove(slot);
Changed?.Invoke(slot, null);
return item;
}
public ItemInstance Get(EquipSlot slot) => _equipped.TryGetValue(slot, out var i) ? i : null;
}
```
## 4.4 Comparing items in a tooltip
To show "+4 Attack Power" when hovering a new sword, don't do the math yourself. Equip it temporarily, read the values, and put the old item back. Because `Stat` uses dirty caching, this is cheap, and it automatically accounts for derived stats.
```csharp
// ItemComparer.cs
using System.Collections.Generic;
public static class ItemComparer
{
public static Dictionary<StatDefinition, float> Diff(CharacterStats stats, Equipment eq, ItemInstance candidate)
{
var before = new Dictionary<StatDefinition, float>();
foreach (var s in stats.All) before[s.Definition] = s.Value;
var old = eq.Equip(candidate);
var diff = new Dictionary<StatDefinition, float>();
foreach (var s in stats.All) diff[s.Definition] = s.Value - before[s.Definition];
eq.Unequip(candidate.def.slot);
if (old != null) eq.Equip(old);
return diff;
}
}
```
> [!note] Side effects
> `Equipment.Changed` fires during the preview. If listeners play sounds or save, add a `bool silent` parameter, or set a `Previewing` flag that listeners check.