RPG — Black Star Creatives
August 2026
In Development
Unity Engine 6
Systems Programmer · Designer
Project Gold is a turn-based RPG in development at Black Star Creatives, built around an Emotion System as its central gimmick — every playable character is aligned to one of the Seven Deadly Sins, and climbing an emotion ladder in battle means giving in to that sin before it resolves into its opposing virtue. This page focuses on the foundational systems work I built that has to exist before any of that can be played: a data-driven character, skill, and gear model, plus a custom Unity Editor tool built with UI Toolkit for authoring and live-tuning it.
Introduction
Before any combat logic can exist, something has to represent a character, a skill, and a weapon — and answer the boring-but-critical questions underneath: how does a weapon's stat bonus differ from an accessory's, how do skills unlock as a character or their gear levels up, and how do you tell a party member apart from an enemy in code without duplicating half the data model. That's the layer this work covers.
Rather than build this blind, I paired it with a custom UI Toolkit editor window — a Party Stat Viewer that tabs through the active party, shows every stat live, and recomputes derived values (effective stats, unlocked skills) in real time as the underlying data changes. It doubled as the way I pressure-tested the data model itself: several real design flaws surfaced specifically because I could see and edit the numbers live instead of reasoning about them in the abstract.
A Data-Driven Character, Skill, and Gear Model
Characters, skills, and gear are all ScriptableObject-based, which keeps the whole system designer-editable without touching code. The core split is between CharacterData — shared identity and base stats (STR/MAG/ACT/DEF/AGI/MP/HP/EP), usable by enemies too — and PartyMemberData, which subclasses it to add party-only concerns: an equipped weapon, equipped accessories, and self-taught skill unlocks. A PartyRoster holding a plain List<CharacterData> accepts PartyMemberData instances polymorphically, so the party/enemy boundary is enforced by the type system rather than by convention.
Weapons level from 1 to 5, unlocking one of four skills per level and applying hand-authored, non-linear stat modifiers at each level — some stats go up, others deliberately go down, since a real weapon-upgrade curve isn't just "+1 to everything." Accessories layer on top with both flat and percentage modifiers to stats and elemental resistances. All of it feeds into a single computed method:
public int GetEffectiveStat(StatType stat)
{
int value = GetBaseStat(stat);
if (equippedWeapon.definition != null)
{
int levelCount = Mathf.Clamp(equippedWeapon.currentLevel, 1, equippedWeapon.definition.levels.Length);
for (int i = 0; i < levelCount; i++)
{
WeaponLevelData weaponLevel = equippedWeapon.definition.levels[i];
if (weaponLevel == null) continue;
foreach (StatModifier modifier in weaponLevel.statModifiers)
{
if (modifier.stat == stat)
{
value += modifier.amount;
}
}
}
}
float percentBonus = 0f;
foreach (AccessoryData accessory in equippedAccessories)
{
if (accessory == null) continue;
foreach (AccessoryStatModifier modifier in accessory.statModifiers)
{
if (modifier.stat != stat) continue;
if (modifier.mode == ModifierMode.Flat)
value += Mathf.RoundToInt(modifier.amount);
else
percentBonus += modifier.amount;
}
}
return Mathf.RoundToInt(value * (1f + percentBonus));
}
Weapon levels are summed cumulatively (each level stores a delta, not a running total), and accessory percentage bonuses stack additively before being applied once at the end — three +10% accessories add up to +30%, not a compounded ×1.1³. Compounding percentages get disproportionately strong the more gear you stack, which isn't the behavior I wanted for this system.
Catching a Real Design Flaw Before It Shipped
The first pass tracked a weapon's current level directly on the character — a single currentWeaponLevel int alongside equippedWeapon. It looked reasonable until I actually thought through weapon-swapping: since weapons are meant to level up independently through use, that design would tie a weapon's progress to whichever character's equip slot it happened to be sitting in. Swap weapons, and a fresh Level 1 sword would silently inherit whatever level the previous weapon had reached — or worse, a heavily-upgraded weapon would lose its progress the moment it left a character's hands.
The fix was to stop treating WeaponData as anything other than a shared definition — a template describing what each of its five levels does — and introduce a proper instance/definition split:
[System.Serializable]
public class WeaponInstance
{
public WeaponData definition;
[Range(1, 5)]
public int currentLevel = 1;
}
A character's equippedWeapon is now a WeaponInstance, pairing a definition reference with its own progress — the exact same pattern already used for self-taught skill unlocks ({ SkillData skill; int requiredLevel; }), just applied consistently once the gap became obvious. It's a small fix in isolation, but it's the kind of thing that's cheap to correct before other systems start depending on the wrong assumption, and expensive to unwind after.
Custom Editor Tooling with UI Toolkit
The Party Stat Viewer is a UnityEditor.EditorWindow built with UXML/USS rather than legacy IMGUI, so it works identically in and out of Play mode and can be authored declaratively. Its detail panel binds directly to each selected character's SerializedObject — editing any field writes straight back to the underlying asset with full Undo support, the same mechanism the built-in Inspector itself relies on:
currentSerializedObject = new SerializedObject(member);
detailContainer.Bind(currentSerializedObject);
detailContainer.TrackSerializedObjectValue(currentSerializedObject, _ =>
{
RefreshEffectiveStats();
RefreshWeaponSkills();
RefreshSelfSkills();
});
Not everything in the panel is a stored field, though — effective stats and unlocked skills are computed, not serialized, so they can't use the same declarative PropertyField binding as the raw stats. TrackSerializedObjectValue solves that by firing a callback whenever anything on the bound object changes, which recomputes and redraws those derived sections live as you tune weapon levels, swap accessories, or level a character up.
Party tabs are generated dynamically — one per roster member — and tinted per the character's Sin affiliation (Pride, Greed, Wrath, Envy, Gluttony, Lust, Sloth), using an inline style override that deliberately wins over the stylesheet's selection-state styling, so a tab's sin color stays visible whether or not it's the active tab.