RPG — Black Star Creatives
August 2026
Prototyping
Unity Engine 6
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:
- Emotion Level (1–5) is a state, not a resource. It climbs with momentum and gates passives, the Sin State, and Resolute — and nothing an enemy does can knock you down a level.
- Emotion Points (EP) are the fuel for emotion skills, spent in whole charges of 100. Spending EP never changes your level, so cashing in a skill can't knock you out of a buff or out of Resolute.
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:
- L3: crits and weakness hits pay an extra stack.
- L4 — Sin State: per-stack damage doubles, but every hit she takes strips the hoard. She's at her most dangerous and her most fragile at the same time — Greed's flaw in one rule.
- L5 — Resolute: the cap comes off and the hoard can no longer be stripped. In the full design, this is where she finally shares it: generosity was what her greed was hiding all along.
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:
- Described before built. The base
PassiveDataclass is concrete, not abstract. A plain asset with a name, description, and per-level notes is a valid "designed, not yet implemented" passive that can be slotted onto a character and read in the tools today; passives with real behaviour subclass it and override only the hooks they need. - Descriptions can't drift. Implemented passives generate their per-level description from their own serialized numbers, so retuning a value retunes the text. Hand-written notes only cover levels that aren't built yet.
- No stat hook, on purpose. A passive that modified stats would be called from the stat calculation, and a passive that reads a stat would call straight back into it. Stat changes go through the existing modifier layers instead, which can't recurse.
- Shared assets stay read-only. Passives are ScriptableObjects shared by every combatant, so per-battle state like Dragon's Eye stacks lives on each
CombatActor, keyed by the passive — never on the asset, where it would leak between characters and into the next play session.
// 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.
- Roster tabs with each character's portrait, underlined in their Sin's color.
- Stats, Skills, and Gear pages. Skills and Gear disable themselves for non-party characters such as enemies, rather than showing two pages of empty panels.
- Base vs. effective stats side by side, with any value moved by gear or bonuses shown in gold with its delta — STR 12 (+2), AGI 18 (−2).
- An Emotion Ladder readout: for each level, its charge ceiling, the character's Level 2 stat swing, what every equipped passive does there (innate, weapon, and accessories), and which skills unlock — the design above, checked against the live data.
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.