Artifacts and stats
Assets/
Scripts/
Data/
you already havethis lesson adds
The Knight has three skills but the same numbers it started with. An artifact is a piece of gear you put on to make those numbers bigger.
Crown of Ember: +34 attack. Put it on, attack goes from 40 to 74. Take it off, attack goes back to 40. That is the entire feature.
The trap, before any code
The obvious way to write this is to add straight into the character's attack. It works the first time and breaks quietly forever after.
✖ Add and subtract
attack += 34 on equip, attack -= 34 on unequip. Miss one subtraction — a swap, a death, a scene reload — and the Knight is permanently stronger.✓ Never touch the base
attack = 40 frozen forever. Every time gear changes, throw the total away and add it all up again from scratch.Watch the dashed line: the base part of the bar never moves. Only the amber part appears and disappears.
Two numbers per stat
So CharacterStats needs a second set of fields. The base ones you type in the Inspector and never change again; the final ones the game recalculates and actually uses.
[Header("Base — you set these, nothing else may")]
public float attack = 40f;
public float defense = 30f;
public float moveSpeed = 5f;
public float maxHealth = 200f;
[Header("Final — recalculated, use these in gameplay")]
public float finalAttack;
public float finalDefense;
public float finalSpeed;
public float finalMaxHp;An artifact is just a list of bonuses
One asset, one list. Crown of Ember holds three lines: +34 attack, +18 defense, +12 speed. Nothing in the file knows who will wear it.
using UnityEngine;
public enum StatType { Attack, Defense, MoveSpeed, MaxHealth }
public enum ArtifactSlot { Crown, Chest, Boots, Ring }
[System.Serializable]
public struct StatBonus
{
public StatType stat;
public float flat; // +34
public float percent;
}
[CreateAssetMenu(fileName = "Artifact_", menuName = "Game/Artifact")]
public class ArtifactData : ScriptableObject
{
public string displayName = "Crown of Ember";
public Sprite icon;
[Range(1, 5)] public int rarity = 4;
public ArtifactSlot slot = ArtifactSlot.Crown; // one item per slot
public StatBonus[] bonuses;
}[System.Serializable] on the struct is what makes the bonus lines editable in the Inspector. Without it the array shows up empty.
The slot is why you cannot wear two crowns. One artifact per slot, four slots, that is the whole inventory rule.
flat and percent are separate on purpose — the next section explains why the order matters.
Recalculate: throw it away, add it up again
This is the only method that writes to the final fields, and it always starts from the base. Nothing accumulates, so nothing can drift.
using System.Collections.Generic;
using UnityEngine;
public class Equipment : MonoBehaviour
{
[SerializeField] private CharacterStats stats;
private readonly Dictionary<ArtifactSlot, ArtifactData> worn = new();
public System.Action OnChanged; // the UI listens
public void Equip(ArtifactData a)
{
worn[a.slot] = a; // replaces whatever was in that slot
Recalculate();
}
public void Unequip(ArtifactSlot slot)
{
worn.Remove(slot);
Recalculate();
}
void Recalculate()
{
// one bucket per stat
int n = System.Enum.GetValues(typeof(StatType)).Length;
float[] flat = new float[n];
float[] pct = new float[n];
foreach (ArtifactData a in worn.Values)
foreach (StatBonus b in a.bonuses)
{
flat[(int)b.stat] += b.flat;
pct[(int)b.stat] += b.percent;
}
// base + all the flat, THEN multiply by the percent
stats.finalAttack = Apply(stats.attack, flat, pct, StatType.Attack);
stats.finalDefense = Apply(stats.defense, flat, pct, StatType.Defense);
stats.finalSpeed = Apply(stats.moveSpeed, flat, pct, StatType.MoveSpeed);
stats.finalMaxHp = Apply(stats.maxHealth, flat, pct, StatType.MaxHealth);
OnChanged?.Invoke();
}
static float Apply(float baseValue, float[] flat, float[] pct, StatType s)
=> (baseValue + flat[(int)s]) * (1f + pct[(int)s] / 100f);
}Why flat first, then percent
With base 40, a +34 flat and a +15% bonus, the two orders give different answers — and only one of them matches what the tooltip promised.
// flat first
// percent firstTry it
- 1
Create two artifacts via : Crown of Ember (+34 flat Attack) and Ring of Haste (+15% Attack, +12 flat MoveSpeed).
- 2
Attach Equipment to the Knight and drag its CharacterStats into the slot.
- 3
Bind keys 7 and 8 to Equip and Unequip so you can toggle them while the game runs.
- 4
Press Play, watch finalAttack in the Inspector: 40 → 74 → 85.1, and back down to exactly 40.
The test that actually proves it works
Equip and unequip both artifacts twenty times in a row. If finalAttack does not land back on exactly 40, something is writing into the base — go find it.
[ContextMenu("Stress test equip")]
void StressTest()
{
float before = stats.finalAttack;
for (int i = 0; i < 20; i++)
{
Equip(crown); Equip(ring);
Unequip(ArtifactSlot.Crown); Unequip(ArtifactSlot.Ring);
}
Debug.Log(before == stats.finalAttack
? "OK — no drift"
: $"DRIFT! {before} → {stats.finalAttack}");
}