Project Gold

In Prototyping

Project Type
RPG — Black Star Creatives
Date
August 2026
Status
Prototyping
Engine
Unity Engine 6
Role
Systems Programmer · Designer

Project Gold is a turn-based RPG in development at Black Star Creatives, the indie studio I co-founded. Every playable character is aligned to one of the Seven Deadly Sins, and climbing the emotion ladder in battle means giving in to that sin — gaining power along with its flaw — until it resolves into its opposing virtue. I designed that Emotion System, and I'm building the combat systems, data model, and editor tooling behind it.

Gameplay footage is coming once the project moves out of prototyping.

Introduction

Project Gold is where my design and engineering work meet most directly. I designed its central mechanic, the Emotion System, and I'm building everything needed to make it playable and tunable: the combat systems that run it, the data model underneath them, and a custom editor tool for balancing it all.

This page walks through the design first, then the systems and tooling behind it.

Designing the Emotion System

Every playable character is aligned to one of the Seven Deadly Sins. Climbing the emotion ladder in battle is giving in to that sin: power grows, but so does a characteristic flaw — until, at the top, the sin resolves into its opposing virtue. The whole arc in one line, from my design spec:

L1 restraint → L2–L3 the sin stirring → L4 the sin in full (power + its flaw) → L5 the virtue (mastery, flaw gone).

I split the system into two tracks so it can never punish players for engaging with it:

The charge ceiling per level is 1, 2, 3, 3, 4 — Level 4 deliberately matches Level 3. The Sin State is the struggle; the extra capacity waits for Resolute, as the payoff for pushing through it. Levels 4 and 5 are fixed landmarks for every character; what changes from character to character is what their sin does to them there.

Case Study: Logos and Dragon's Blood

Logos is Greed → Charity: she hoards because she's shy and afraid to share. Her innate passive, Dragon's Blood, turns that into a mechanic. Every landed hit adds a Dragon's Eye stack (more on crits and weakness hits), and each stack adds flat damage — the fuller the hoard, the harder she hits. Her Emotion Level changes the terms as she climbs:


public override void OnTookDamage(CombatActor self, CombatActor attacker, DamageBreakdown breakdown)
{
    // Plundered — but only in the Sin State. Below it the hoard is safe, and Resolute lifts the flaw
    // again, which is why IsInSinState is an exact level match rather than a threshold.
    if (!self.IsInSinState) return;
    if (breakdown.finalDamage <= 0) return;

    self.AddStacks(this, -stacksLostWhenHit);
}

public override int ModifyOutgoingDamage(CombatActor self, CombatActor target, int damage)
{
    int bonus = self.GetStacks(this) * damagePerStack;

    // From the Sin State on, the hoard hits twice as hard — which is exactly why losing it costs her twice.
    if (self.emotionLevel >= CombatActor.SinStateLevel)
    {
        bonus *= 2;
    }

    return damage + bonus;
}
    

One detail worth calling out: IsInSinState is true only at Level 4, not "Level 4 and up." Resolute is supposed to lift the flaw, so a threshold check would have quietly kept stripping her hoard at Level 5 — the opposite of what the design promises.

A Passive System Built for Iteration

Characters get designed faster than they get built, so the passive system needed to let design run ahead of code without the two drifting apart:


// Built from the serialized fields rather than written out, so retuning any of them retunes the
// Party Stat Viewer's ladder readout with it.
public override string DescribeEmotionLevel(int level)
{
    if (level == CombatActor.SinStateLevel)
    {
        string stacks = stacksLostWhenHit == 1 ? "1 stack" : $"{stacksLostWhenHit} stacks";
        return $"Sin State. Per-stack damage doubles ({damagePerStack} → {damagePerStack * 2} each), but every " +
               $"hit taken strips {stacks}. Augments cost no Dragon's Eye at all from here.";
    }

    // ...one branch per level this passive affects; null for levels where it does nothing
}
    

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.

Tooling: The Party Stat Viewer

All of this has to be tuned somewhere, so I built a custom editor window for it: the Party Stat Viewer, a UnityEditor.EditorWindow built with UI Toolkit (UXML/USS) rather than legacy IMGUI.

The Party Stat Viewer showing a character's identity, innate passive, and base versus effective stats

The panel binds directly to the selected character's SerializedObject, so editing any field writes straight back to the asset with full Undo support. The derived panels aren't serialized fields, so one subscription redraws all of them whenever anything changes:


currentSerializedObject = new SerializedObject(member);
detailContainer.Bind(currentSerializedObject);

// One subscription covering every field, so edits anywhere refresh the derived panels.
detailContainer.TrackSerializedObjectValue(currentSerializedObject, _ => RefreshAll());
    

One lesson from building it: in UI Toolkit, a mismatch between a UXML element's name and the C# query that looks it up doesn't throw — the field is just null, and the window half-works. After that bit the project once, the window checks every required element at startup and reports any missing ones by name, in the console and in the window itself, instead of failing silently.