So I was playing Dislyte this evening on my phone, and was busy upgrading “Relics” (the game’s equivalent of equipment), and I decided to throw together a similar upgrading system for our RPG Course. This particular system has also been used in Summoner’s War and RAID: Shadow Legends.
I’ll start out by describing the concept behind the upgradable equipment.
Stats
Stats can be Primary Stats or Secondary stats, and can be absolute modifiers or percentage modifiers.Primary Stat
Every piece of equipment has a Primary stat modifier. The types of possible primary stat modifiers is determined by the location of the gear. For example: a Chest Plate might have a primary stat of Defense. Shoes might have a primary stat of Speed or Dodge. Gloves might have a primary stat of accuracy. Primary stats increase with each item level. The initial value of the primary stat is set by the Rarity.Secondary Stat
Each piece of equipment can have up to four secondary stats. The number of secondary stats is determined by a combination of level and quality. The higher the rarity, the more secondary stats the item starts with. As you gain stats by levelling, either a new (random) stat is added or if all four stats are enabled, then one of the random stats is boosted. Secondary stats initial value is set by Rarity, and only increases if the stat is boosted (by earning a secondary stat after the 4th stat is added).Quality
Items have a quality, usually represented by the number of stars. There is a direct relationship between the number of stars and the number of secondary stats that the item begins with. For this tutorial, I'll be calling the Quality ranksPoor (no stars) -- No starting secondary stats
Fair -- One secondary stat
Well Crafted -- Two Secondary stats
High Quality -- Three Secondary stats
Perfect -- Four Secondary stats
Level
Items used in the system start at Level 1, and can be upgraded to a max level. This level varies from game to game. Every level increase increases the value of the primary stat. The amount of the increase is determined by the Rarity. Every MaxLevel / 4 levels, a Secondary Stat is added or increased. Item levels are increased by spending materials, coins, or both. Some systems use coins and a material, other systems use just coins, but each attempt has a possibility of failure. The higher the level, the higher the chance of failure on each successive attempt. Dislyte uses this method, and caps the number of failures before victory is automatic.Rarity
Items have a rarity which determines the initial value of the Primary Stat and Secondary stats, as well as the amount of increase when those stats are boosted. For our scheme, we'll use the following rarities:Common -- A Very low stat value, it's also the value multiplied by the other rarities
Uncommon -- Twice as high a stat as Common
Rare -- Three times as high as Common
Mythic -- Four times as high as Common
Legendary -- Five times as high as Common
So all of these factors work together to determine the Stat Modifiers for a given item. We’re going to start by setting up some Enums and getting the initial framework for the Items.
ItemQuality.cs
namespace RPG.Inventories
{
public enum ItemQuality
{
Poor=0,
Fair=1,
WellCrafted=2,
HighQuality=3,
Perfect=4
}
}
You’ll note that I specified the integer value of each element in the series. By default, the Enum starts at 0 and increases by 1, but I’m being explicit here just for absolute clarity. We’ll be able to determine how many secondary stats the item starts with by simply casting the enum to an int.
ItemRarity.cs
namespace RPG.Inventories
{
public enum ItemRarity
{
Common=1,
Uncommon=2,
Rare=3,
Mythic=4,
Legendary=5
}
}
Note that in this case, I started at 1 instead of zero. This gives us a simple multiplier for the starting level for a stat and the increase in levels.
Next, we need a container struct for each modifier that will be on the item. This struct will hold all the information needed to determine the value of the modifier.
StatModifier.cs
using RPG.Stats;
using UnityEngine;
namespace RPG.Inventories
{
[System.Serializable]
public struct StatModifier
{
public Stat Stat;
public float InitialValue;
public float IncreasePerLevel;
public bool IsPercentage;
[HideInInspector] public int Level;
[HideInInspector] public ItemRarity Rarity;
public StatModifier(Stat stat, float initialValue, float increasePerLevel, bool isPercentage, int level, ItemRarity rarity)
{
Stat = stat;
InitialValue = initialValue;
IncreasePerLevel = increasePerLevel;
IsPercentage = isPercentage;
Level = level;
Rarity = rarity;
}
public float Evaluate()
{
float actualInitialValue = (int)Rarity * InitialValue;
float actualIncreasePerLevel = (int)Rarity * IncreasePerLevel;
return actualInitialValue + actualIncreasePerLevel * Level;
}
}
}
This struct covers all the needed details for one stat. It’s Evaluate method gets the initial value of the stat, based on it’s rarity, and then adds stats per level to get a final value. We’ll be using this in the next class, our new Item, the UpgradableEquipmentItem
Summary
using System.Collections.Generic;
using GameDevTV.Inventories;
using RPG.Stats;
using UnityEngine;
namespace RPG.Inventories
{
[CreateAssetMenu(fileName = "UpgradeableEquipment", menuName = "Inventory/UpgradableEquipment", order = 0)]
public class UpgradableEquipmentItem : EquipableItem, IModifierProvider
{
[SerializeField] private StatModifier[] possiblePrimaryStats;
[SerializeField] private StatModifier[] possibleSecondaryStats;
private ItemRarity rarity;
private ItemQuality quality;
private int itemLevel;
public ItemRarity Rarity() => rarity;
public ItemQuality Quality() => quality;
public int ItemLevel() => itemLevel;
private List<StatModifier> actualStatModifiers = new List<StatModifier>();
/// <summary>
/// Determine rarity and quality based on the level. Create stats based on setup values.
/// </summary>
/// <param name="level"></param>
public void SetupNewItem(int level)
{
if (possibleSecondaryStats.Length < 4)
{
Debug.Log($"{GetDisplayName()} needs more possible secondary items to function properly!");
}
if (possiblePrimaryStats.Length == 0)
{
Debug.Log($"{GetDisplayName()} needs at least one possible Primary Stat to function");
}
rarity = (ItemRarity)GenerateCharacteristic(level, 1, 5);
quality = (ItemQuality)GenerateCharacteristic(level, 0, 4);
StatModifier primaryModifier = possiblePrimaryStats[Random.Range(0, possiblePrimaryStats.Length)];
actualStatModifiers.Clear();
actualStatModifiers.Add(primaryModifier);
for (int i = 0; i < (int)quality; i++)
{
bool isValid = true;
do
{
StatModifier potentialModifier = possibleSecondaryStats[Random.Range(0, possibleSecondaryStats.Length)];
foreach (StatModifier actualStatModifier in actualStatModifiers)
{
if (potentialModifier.Stat == actualStatModifier.Stat &&
potentialModifier.IsPercentage == actualStatModifier.IsPercentage)
{
isValid = false;
break;
}
}
} while (!isValid);
}
}
private static int GenerateCharacteristic(int level, int startingValue, int maxValue=5)
{
int workingValue = startingValue;
int testLevel = level * 4;
// here, we're giving the item a chance to grow past Common.
// Each successful roll increases the Rarity, but the test for the next rarity level drops
while (workingValue < maxValue && Random.Range(0, 100) < testLevel)
{
workingValue++;
testLevel = level / workingValue;
}
return workingValue;
}
public IEnumerable<float> GetAdditiveModifiers(Stat stat)
{
foreach (var actualStatModifier in actualStatModifiers)
{
if (stat == actualStatModifier.Stat && !actualStatModifier.IsPercentage)
yield return actualStatModifier.Evaluate();
}
}
public IEnumerable<float> GetPercentageModifiers(Stat stat)
{
foreach (var actualStatModifier in actualStatModifiers)
{
if (stat == actualStatModifier.Stat && actualStatModifier.IsPercentage)
yield return actualStatModifier.Evaluate();
}
}
}
}
In the next post, we’ll go over creating the random item at runtime, as well as loading and saving the item.