Cookbook
Copy-paste recipes for common battle patterns. Your formulas, your rules — these are starting points.
Mana Costs
SimpleJRPG doesn't manage MP — that's your domain. Check costs before calling combat verbs, deduct MP yourself.
void CastFireball(ICombatant caster, ICombatant target)
{
int mpCost = 8;
var myCaster = (MyCombatant)caster;
if (myCaster.MP < mpCost)
{
Debug.Log("Not enough MP!");
return;
}
myCaster.MP -= mpCost;
int damage = myCaster.MagicPower * 3;
battle.DealDamage(caster, target, damage, "magical", "fire");
}
Elemental Weakness / Resistance
Use OnBeforeDamage to modify damage based on element. The element
string is pass-through — you define what elements exist and how they interact.
battle.OnBeforeDamage += evt =>
{
var target = (MyCombatant)evt.Target;
float multiplier = target.GetElementMultiplier(evt.Element);
evt.Amount = (int)(evt.Amount * multiplier);
};
// In your MyCombatant class:
public float GetElementMultiplier(string element)
{
if (Weaknesses.Contains(element)) return 2.0f;
if (Resistances.Contains(element)) return 0.5f;
if (Immunities.Contains(element)) return 0f;
return 1.0f;
}
0, you can set evt.Cancel = true instead
and show an "Immune!" message in your UI.
Status Effect Timers
SimpleJRPG fires OnStatusApplied and OnStatusRemoved but doesn't
track duration. You manage timers and call RemoveStatus() when they expire.
// Track active statuses
Dictionary<ICombatant, List<(string id, int turnsLeft)>> statuses = new();
// When a status is applied
battle.OnStatusApplied += evt =>
{
if (!statuses.ContainsKey(evt.Target))
statuses[evt.Target] = new();
statuses[evt.Target].Add((evt.StatusId, evt.Duration));
};
// Tick statuses at end of each turn
battle.OnTurnEnd += evt =>
{
if (!statuses.ContainsKey(evt.Actor)) return;
var list = statuses[evt.Actor];
for (int i = list.Count - 1; i >= 0; i--)
{
// Apply per-turn effects (e.g. poison damage)
if (list[i].id == "poison")
battle.DealDamage(evt.Actor, evt.Actor, 5, "status", "poison");
var updated = (list[i].id, list[i].turnsLeft - 1);
if (updated.Item2 <= 0)
{
battle.RemoveStatus(evt.Actor, list[i].id);
list.RemoveAt(i);
}
else
{
list[i] = updated;
}
}
};
Buff Stacking
You decide how buffs stack. SimpleJRPG fires the events — you track amounts and apply
them in your damage formulas or in OnBeforeDamage / OnBeforeHeal.
// Track buffs: combatant -> buff id -> (multiplier, turnsLeft)
Dictionary<ICombatant, Dictionary<string, (float mult, int turns)>> buffs = new();
battle.OnBuffApplied += evt =>
{
if (!buffs.ContainsKey(evt.Target))
buffs[evt.Target] = new();
var dict = buffs[evt.Target];
if (dict.ContainsKey(evt.BuffId))
{
// Stack: multiply existing, refresh duration
var existing = dict[evt.BuffId];
dict[evt.BuffId] = (existing.mult * evt.Amount, evt.Duration);
}
else
{
dict[evt.BuffId] = (evt.Amount, evt.Duration);
}
};
// Apply attack buff in before-damage
battle.OnBeforeDamage += evt =>
{
if (buffs.TryGetValue(evt.Source, out var dict))
{
if (dict.TryGetValue("attack_up", out var buff))
evt.Amount = (int)(evt.Amount * buff.mult);
}
};
dict[evt.BuffId] = (evt.Amount, evt.Duration);
Revive Logic
Revive(target, hpAmount) restores a dead combatant with the specified HP
and re-adds them to the turn system. You control how much HP they come back with.
void CastRevive(ICombatant caster, ICombatant target, bool fullRevive)
{
if (target.IsAlive) return;
var myTarget = (MyCombatant)target;
int hp = fullRevive ? myTarget.MaxHP : myTarget.MaxHP / 2;
battle.Revive(target, hp);
}
// Revive items
void UsePhoenixFeather(ICombatant target)
{
if (target.IsAlive) return;
battle.Revive(target, 1); // Revive with 1 HP
}
Flee Chance
Flee(who) ends the battle with BattleState.Fled. You decide
whether fleeing succeeds and what the formula is.
void AttemptFlee(ICombatant actor)
{
// Classic formula: party avg speed vs enemy avg speed
float partySpeed = battle.GetAlive(0).Average(c => c.Speed);
float enemySpeed = battle.GetAlive(1).Average(c => c.Speed);
float chance = Mathf.Clamp01(partySpeed / (partySpeed + enemySpeed));
if (UnityEngine.Random.value < chance)
{
battle.Flee(actor);
}
else
{
Debug.Log("Couldn't escape!");
// Consume the turn
battle.EndTurn();
}
}
Flee(). Show a "Can't escape!" message instead.
Multi-Hit Attacks
For attacks that hit multiple times (e.g. Barrage, Double Slash), call DealDamage
in a loop. Each hit fires its own OnDamageDealt event.
void Barrage(ICombatant actor, ICombatant target, int hitCount)
{
for (int i = 0; i < hitCount; i++)
{
// Vary damage per hit if you want
int damage = CalculateDamage(actor, target);
bool isCrit = UnityEngine.Random.value < 0.15f;
battle.DealDamage(actor, target, damage, "physical", "", isCrit);
// Stop if target died
if (!target.IsAlive) break;
}
}
AoE / Group Attacks
Use the multi-target DealDamage overload. It fires individual
OnDamageDealt per target, then one OnGroupDamageDealt with totals.
void CastBlizzard(ICombatant caster)
{
int mpCost = 12;
var myCaster = (MyCombatant)caster;
if (myCaster.MP < mpCost) return;
myCaster.MP -= mpCost;
var targets = battle.GetEnemies(caster);
int damage = myCaster.MagicPower * 2;
battle.DealDamage(caster, targets, damage, "magical", "ice");
}
// Per-target damage variation
void CastMeteor(ICombatant caster)
{
var targets = battle.GetEnemies(caster);
foreach (var target in targets)
{
int damage = UnityEngine.Random.Range(80, 120);
battle.DealDamage(caster, target, damage, "magical", "fire");
}
}
OnGroupDamageDealt.
If you need per-target variation, use a loop with single-target DealDamage calls.
Damage Shields / Barriers
Use OnBeforeDamage to absorb or cancel damage. Track shield HP yourself.
Dictionary<ICombatant, int> shields = new();
void ApplyShield(ICombatant target, int amount)
{
shields[target] = amount;
battle.ApplyBuff(target, target, "shield", amount);
}
battle.OnBeforeDamage += evt =>
{
if (!shields.TryGetValue(evt.Target, out int shieldHP)) return;
if (shieldHP <= 0) return;
if (shieldHP >= evt.Amount)
{
shields[evt.Target] = shieldHP - evt.Amount;
evt.Cancel = true; // Fully absorbed
Debug.Log($"Shield absorbed {evt.Amount} damage!");
}
else
{
evt.Amount -= shieldHP;
shields[evt.Target] = 0;
battle.RemoveBuff(evt.Target, "shield");
Debug.Log($"Shield broke! {evt.Amount} damage gets through.");
}
};
Counter Attacks
React to incoming damage with OnDamageDealt. The target can strike back
immediately.
battle.OnDamageDealt += evt =>
{
if (!evt.Target.IsAlive) return;
var target = (MyCombatant)evt.Target;
if (!target.HasSkill("counter")) return;
float counterChance = 0.3f;
if (UnityEngine.Random.value < counterChance)
{
int damage = CalculateDamage(evt.Target, evt.Source);
battle.DealDamage(evt.Target, evt.Source, damage, "physical", "");
Debug.Log($"{evt.Target.Name} counters!");
}
};
Critical Hits
Pass isCrit: true to DealDamage. The WasCrit flag
carries through to events for your UI to react to.
void Attack(ICombatant attacker, ICombatant target)
{
int baseDamage = CalculateDamage(attacker, target);
float critChance = 0.1f; // 10%
bool isCrit = UnityEngine.Random.value < critChance;
int finalDamage = isCrit ? baseDamage * 2 : baseDamage;
battle.DealDamage(attacker, target, finalDamage, "physical", "", isCrit);
}
// React in UI
battle.OnDamageDealt += evt =>
{
if (evt.WasCrit)
ShowBigDamageNumber(evt.Amount); // Your UI method
else
ShowDamageNumber(evt.Amount);
};
Turn Order Display
Use GetTimeline() to show upcoming turn order in your UI.
Works with all turn systems.
void UpdateTurnOrderUI()
{
var timeline = battle.GetTimeline();
for (int i = 0; i < timeline.Count; i++)
{
// Update your UI elements
turnOrderSlots[i].SetCombatant(timeline[i]);
}
}
// For ATB, also show gauge fill:
void UpdateATBGauges(ATBTurnSystem atb)
{
foreach (var combatant in battle.GetAlive())
{
float gauge = atb.GetGauge(combatant);
gaugeBar[combatant].fillAmount = gauge / 100f;
}
}
Drain / Life Steal
Deal damage, then heal the attacker for a portion of the damage dealt.
void DrainAttack(ICombatant attacker, ICombatant target)
{
int damage = CalculateDamage(attacker, target);
battle.DealDamage(attacker, target, damage, "magical", "dark");
int healAmount = damage / 2;
battle.Heal(attacker, attacker, healAmount);
}
Instant Kill
Kill(target, killer) bypasses damage formulas entirely. Use for death spells
or execution mechanics.
void CastDeath(ICombatant caster, ICombatant target)
{
float hitChance = 0.3f; // 30% base
var myTarget = (MyCombatant)target;
// Undead or bosses might be immune
if (myTarget.IsImmuneToInstantDeath)
{
Debug.Log($"{target.Name} resisted!");
return;
}
if (UnityEngine.Random.value < hitChance)
battle.Kill(target, caster);
else
Debug.Log("It missed!");
}
Damage Formula Examples
SimpleJRPG never calculates damage. Here are classic formulas you might use:
// Simple: attack - defense
int SimpleDamage(MyCombatant atk, MyCombatant def)
{
return Math.Max(1, atk.Attack - def.Defense);
}
// DQ-style: attack/2 - defense/4, randomized
int DQDamage(MyCombatant atk, MyCombatant def)
{
int base_ = atk.Attack / 2 - def.Defense / 4;
base_ = Math.Max(1, base_);
float variance = UnityEngine.Random.Range(0.875f, 1.125f);
return (int)(base_ * variance);
}
// Level-scaled with crit
int ScaledDamage(MyCombatant atk, MyCombatant def, bool isCrit)
{
float levelBonus = 1f + (atk.Level - def.Level) * 0.05f;
int raw = (int)(atk.Attack * levelBonus) - def.Defense;
raw = Math.Max(1, raw);
return isCrit ? raw * 2 : raw;
}