U
tab 02Character & Combat9 lessons — tap to open
06 / 09 · 12 min
Character & Combat

Artifacts and stats

your project so far

Assets/

Scripts/

CharacterStats.csSkillCaster.csArtifactData.csEquipment.csfinal* stats

Data/

Artifact_ assets

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

Keep attack = 40 frozen forever. Every time gear changes, throw the total away and add it all up again from scratch.
animated diagram

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.

add to CharacterStats.cs
[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.

ArtifactData.cs
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.

Equipment.cs — put it on the Knight
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.

C# script
// flat first
// percent first

Try it

  1. 1

    Create two artifacts via Assets▸Create▸Game▸Artifact: Crown of Ember (+34 flat Attack) and Ring of Haste (+15% Attack, +12 flat MoveSpeed).

  2. 2

    Attach Equipment to the Knight and drag its CharacterStats into the slot.

  3. 3

    Bind keys 7 and 8 to Equip and Unequip so you can toggle them while the game runs.

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

a throwaway test you delete afterwards
[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}");
}