# 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.