View: Multi-Page Single Page

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;
}
Cancel for immunity. If the multiplier is 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);
    }
};
Replace, don't stack? If you want buffs to replace instead of stack, just overwrite: 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();
    }
}
Boss fights. Want to prevent fleeing? Just don't call 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");
    }
}
Loop vs overload. The multi-target overload applies the same damage to all targets and fires 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;
}
↓ ↑