View: Multi-Page Single Page

SimpleJRPG

A lean runtime battle API for JRPGs

Your rules, your formulas.

Philosophy

SimpleJRPG orchestrates battles. You bring your own stats, formulas, UI, and data storage.

There are no opinionated damage formulas, no built-in status effect logic, no UI layer. Instead, SimpleJRPG exposes clean combat verbs and fires events on every action. Your code decides what "Fire" means, how much damage a critical hit deals, and what happens when a character is knocked out. We just make sure the battle flows.

Turn Systems

Five pluggable turn systems ship out of the box. Implement ITurnSystem to build your own.

Classic

DQ-style speed-sorted round-robin. Each round, combatants act in speed order. Simple, predictable, timeless.

ATB

Active Time Battle. Gauges fill over ticks based on speed. When a gauge is full, that combatant acts. No waiting for the whole team.

Timeline / CTB

Charge Time Battle. Actions have cast times that place actors on a timeline. Heavier moves push your next turn further out.

Press Turn

SMT-style team phases. Each team gets a pool of turn points. Exploit weaknesses to gain extra points; miss to lose them.

Action Point

Spend/Save AP system. Bank AP by defending, then spend it for multiple actions in a single turn. Risk and reward.

Features

Instance-Based

No singletons, no statics, no MonoBehaviours. Create a Battle instance, pass in combatants, and go.

Pass-Through Design

Strings, not enums, for all game-specific values. We don't interpret "Fire" or "Poison" — we pass them through. Your system decides what they mean.

Combat Verbs

DealDamage(), Heal(), Kill(), Revive(), ApplyStatus(), ApplyBuff(), Flee() — simple imperative methods.

Events Over Callbacks

Every verb fires an event. Subscribe to OnDamageDealt, OnHealed, OnKO, and more to drive UI, sound, VFX, or logging.

Pluggable Turn Systems

Five built-in implementations of ITurnSystem. Swap between Classic, ATB, Timeline, Press Turn, or Action Point — or write your own.

No Unity Dependencies

Pure C# with noEngineReferences: true. No ScriptableObjects, no MonoBehaviours, no UnityEngine imports. Testable anywhere.

Getting Started

  1. Import SimpleJRPG from the Unity Asset Store into your project.
  2. Implement ICombatant on your character class — provide Name, IsAlive, Team, Speed, TakeDamage(), and Heal().
  3. Create a Battle instance and pass in your list of combatants along with a turn system.
  4. Choose a turn system: ClassicTurnSystem, ATBTurnSystem, TimelineTurnSystem, PressTurnSystem, or ActionPointTurnSystem.
  5. Subscribe to events like OnDamageDealt, OnHealed, and OnKO to wire up your UI, animations, and game logic.

Installation

Get SimpleJRPG into your Unity project in under a minute.

Import from Unity Asset Store

SimpleJRPG is available on the Unity Asset Store. Follow these steps to add it to your project:

  1. Open the Unity Editor and navigate to Window > Package Manager.
  2. In the Package Manager, select My Assets from the dropdown in the top-left.
  3. Search for SimpleJRPG in your asset list.
  4. Click Download if you haven't already, then click Import.
  5. In the Import dialog, ensure all files are selected and click Import.
  6. SimpleJRPG will appear under Assets/SimpleJRPG/ in your project.

Folder Structure

After importing, you'll find the following structure in your project:

Assets/SimpleJRPG/
├── Runtime/          — Core battle API
├── Demo/             — Interactive demo scenes
├── Documentation/    — This documentation
└── Scenes/           — Generated demo scenes

The Runtime/ folder contains everything you need. The other folders are optional and can be safely deleted if you only want the core API.

Assembly Definition

SimpleJRPG ships with an assembly definition file (SimpleJRPG.asmdef) located in the Runtime/ folder. This assembly definition is configured with noEngineReferences: true, which means:

  • Pure C# — No dependency on UnityEngine or any Unity API.
  • Testable — You can reference this assembly from a test project and run unit tests with any .NET test framework, no Unity test runner required.
  • Portable — The code can be compiled and used outside of Unity entirely, making it easy to share logic between a Unity client and a server, for example.

To reference SimpleJRPG from your own assembly definitions, simply add SimpleJRPG to your asmdef's reference list.

Requirements

  • Unity 2021.3+ — Any recent Unity version is supported (2021 LTS or newer).
  • No third-party dependencies — SimpleJRPG does not rely on any external packages or plugins.
Zero Dependencies SimpleJRPG has zero external dependencies. It's pure C# — no MonoBehaviours, no ScriptableObjects, no Unity API calls.

Quick Start

Get a battle running in four steps.

Step 1: Implement ICombatant

ICombatant is the only interface your characters need to implement. You own the stats, the properties, the logic. SimpleJRPG just needs a name, alive status, team index, speed, and methods to take damage and heal.

public class MyCombatant : ICombatant
{
    public string Name { get; }
    public bool IsAlive => HP > 0;
    public int Team { get; }
    public float Speed { get; }

    public int HP { get; private set; }
    public int MaxHP { get; }

    public MyCombatant(string name, int hp, float speed, int team)
    {
        Name = name;
        HP = hp;
        MaxHP = hp;
        Speed = speed;
        Team = team;
    }

    public void TakeDamage(int amount) => HP = Math.Max(0, HP - amount);
    public void Heal(int amount) => HP = Math.Min(MaxHP, HP + amount);
}
Your class, your rules. Add whatever stats you need — MP, defense, elemental resistances, equipment slots. SimpleJRPG never inspects those. It only touches what ICombatant defines.

Step 2: Create a Battle

Create a Battle instance, build your party and enemy lists, then call Start() with all combatants and a turn system.

var battle = new Battle();
var party = new List<ICombatant>
{
    new MyCombatant("Hero", 100, 10f, 0),
    new MyCombatant("Mage", 70, 12f, 0),
};
var enemies = new List<ICombatant>
{
    new MyCombatant("Goblin", 50, 8f, 1),
    new MyCombatant("Orc", 80, 6f, 1),
};
var all = new List<ICombatant>();
all.AddRange(party);
all.AddRange(enemies);
battle.Start(all, new ClassicTurnSystem());
Team convention. Team 0 is the player party. All other team values are enemies. CheckBattleEnd() uses this to determine Victory vs. Defeat.

Step 3: Subscribe to Events

Every combat verb fires an event. Subscribe to drive your UI, play sound effects, trigger animations, or log to console — whatever your game needs.

battle.OnDamageDealt += e => Debug.Log($"{e.Source.Name} hits {e.Target.Name} for {e.Amount}!");
battle.OnKO += e => Debug.Log($"{e.Target.Name} was knocked out!");
battle.OnBattleEnd += (b, state) => Debug.Log($"Battle ended: {state}");

Events are the primary way SimpleJRPG communicates what happened. There are no built-in UI components, no logging systems, no animation controllers. You subscribe to exactly the events you care about and handle them however you want.

Step 4: Run the Battle Loop

Ask the battle for the next actor, execute your game logic (pick targets, calculate damage with your own formulas), call combat verbs, end the turn, and check if the battle is over.

while (battle.State == BattleState.WaitingForCommands)
{
    ICombatant actor = battle.BeginNextTurn();
    // Your game logic: pick targets, calculate damage, etc.
    var target = battle.GetEnemies(actor)[0];
    int damage = CalculateDamage(actor, target); // Your formula
    battle.DealDamage(actor, target, damage, "physical", "none");
    battle.EndTurn();
    battle.CheckBattleEnd();
}
You do the math. CalculateDamage() is your function, not ours. SimpleJRPG never calculates damage, hit rates, or any other formula. You pass in the final number, we apply it and fire the event.

What's Next

You have a working battle. Here is where to go from here:

  • Battle Core — All combat verbs: DealDamage, Heal, Kill, Revive, ApplyStatus, ApplyBuff, Flee, and the events they fire.
  • Turn Systems — Alternatives to ClassicTurnSystem: ATB, Timeline/CTB, Press Turn, and Action Point.
  • API Reference — Full details on every class, interface, method, and event.

Battle Core

The Battle class is the heart of SimpleJRPG — a plain C# instance that orchestrates combat through lifecycle methods, combat verbs, and events.

Battle Lifecycle

Start()

public void Start(List<ICombatant> combatants, ITurnSystem turnSystem)

Initializes the battle. Pass in your list of combatants and a turn system implementation. Internally, this method:

  1. Stores the combatants and turn system references.
  2. Resets the turn counter and battle history.
  3. Calls turnSystem.Init(combatants) so the turn system can set up its internal state.
  4. Sets State to BattleState.WaitingForCommands.
  5. Fires the OnBattleStart event.

Start() will silently return if the battle state is anything other than NotStarted. You cannot restart a battle instance — create a new one.

EndBattle()

public void EndBattle(BattleState result)

Ends the battle with the given outcome. Pass BattleState.Victory, BattleState.Defeat, or BattleState.Fled. This method:

  1. Sets State to the given result.
  2. Clears CurrentActor.
  3. Creates a BattleResult containing the outcome, total turns taken, surviving combatants, and the full battle history.
  4. Fires the OnBattleEnd event with the battle instance and the resulting state.

If the battle has already ended (state is Victory, Defeat, or Fled), calling EndBattle() again does nothing.

Turn Flow

BeginNextTurn()

public ICombatant BeginNextTurn()

Advances to the next turn. Asks the turn system for the next actor via GetNextActor(), increments the turn counter, sets state to Executing, and fires OnTurnStart. Returns the actor whose turn it is, or null if no actor is available.

Can only be called when state is WaitingForCommands or Executing.

EndTurn()

public void EndTurn()

Ends the current turn. Fires OnTurnEnd, notifies the turn system via OnTurnEnd(), clears CurrentActor, then automatically calls CheckBattleEnd(). If the battle hasn't ended, state returns to WaitingForCommands.

Can only be called when state is Executing.

State Machine

The battle progresses through a simple state machine. All transitions are driven by the methods described above.

NotStarted ──Start()──> WaitingForCommands │ BeginNextTurn() │ ▼ Executing │ EndTurn() │ ▼ WaitingForCommands │ CheckBattleEnd() / \ Victory Defeat Fled
Note Fled is reached via Flee(), which calls EndBattle(BattleState.Fled) directly — it can happen from any active state, not just after CheckBattleEnd().

Combat Verbs

Combat verbs are the imperative methods you call to make things happen in battle. You do the math — we orchestrate the result.

DealDamage

public void DealDamage(ICombatant source, ICombatant target, int amount,
    string damageType = "", string element = "", bool isCrit = false)

Deals damage to a single target. The flow is:

  1. Fires OnBeforeDamage with a BeforeDamageEvent. Subscribers can modify Amount, DamageType, Element, IsCrit, or set Cancel = true to prevent the damage entirely.
  2. If not cancelled, calls target.TakeDamage(amount) with the (potentially modified) amount.
  3. Fires OnDamageDealt with the final DamageEvent.
  4. If the target is no longer alive, automatically fires OnKO and notifies the turn system.

There is also a group overload that accepts IList<ICombatant> targets and fires OnGroupDamageDealt after processing all individual hits.

Heal

public void Heal(ICombatant source, ICombatant target, int amount)

Heals a single target. The flow mirrors damage:

  1. Fires OnBeforeHeal with a BeforeHealEvent. Subscribers can modify Amount or set Cancel = true.
  2. If not cancelled, calls target.Heal(amount).
  3. Fires OnHealed with the final HealEvent.

Group overload available — fires OnGroupHealed after all individual heals.

Kill

public void Kill(ICombatant target, ICombatant killer = null)

Forces a KO regardless of the target's current HP. Calls target.TakeDamage(int.MaxValue), then fires OnKO and notifies the turn system. Does nothing if the target is already dead. This bypasses the before-event system — there is no OnBeforeDamage for Kill.

Revive

public void Revive(ICombatant target, int hpAmount)

Revives a dead combatant by calling target.Heal(hpAmount), then notifies the turn system via OnActorAdded() so the revived combatant is re-inserted into the turn order. Fires OnRevived. Does nothing if the target is already alive.

Status & Buff

Event-only — no internal tracking Battle does NOT maintain a list of active statuses or buffs. These methods exist solely to fire events. Your game's status system subscribes to these events and handles all tracking, stacking, duration, and removal logic.

ApplyStatus / RemoveStatus

public void ApplyStatus(ICombatant source, ICombatant target, string statusId, int duration = -1)
public void RemoveStatus(ICombatant target, string statusId)

ApplyStatus fires OnStatusApplied with a StatusEvent containing the source, target, status ID string, and optional duration. RemoveStatus fires OnStatusRemoved with the target and status ID. Both have group overloads that fire OnGroupStatusApplied.

The statusId is a pass-through string — Battle doesn't know or care what "Poison" or "Blind" means. Your system decides.

ApplyBuff / RemoveBuff

public void ApplyBuff(ICombatant source, ICombatant target, string buffId, float amount, int duration = -1)
public void RemoveBuff(ICombatant target, string buffId)

ApplyBuff fires OnBuffApplied with a BuffEvent containing source, target, buff ID, numeric amount, and optional duration. RemoveBuff fires OnBuffRemoved. Group overloads fire OnGroupBuffApplied.

Same pass-through philosophy — buffId and amount are yours to interpret. "ATK_UP" with amount 1.5 could mean +50% attack, or 1.5 flat — your call.

Flee

public void Flee(ICombatant who)

Fires OnFled with the fleeing combatant, then immediately calls EndBattle(BattleState.Fled). The battle ends — no further actions are possible.

Whether the flee attempt succeeds is your game's decision. Only call Flee() after you've already determined the attempt was successful. If you want a chance-based flee, do your roll first, and only call this if it passes.

CheckBattleEnd

public bool CheckBattleEnd()

Checks whether the battle should end based on which teams still have living combatants. Returns true if the battle ended, false otherwise.

The logic:

  • Team 0 = player team. All other team numbers are considered enemies.
  • If no enemies are alive (only team 0 remains) — EndBattle(BattleState.Victory).
  • If no team 0 members are alive — EndBattle(BattleState.Defeat).
Automatic calls CheckBattleEnd() is called automatically at two points: inside EndTurn(), and inside HandleKO() (which fires whenever a combatant dies from DealDamage or Kill). You typically don't need to call it yourself, but you can if you want to check mid-turn.

Before-Events

Intercept, modify, or cancel actions before they execute.

Overview

Before-events fire before the action executes, giving subscribers a chance to modify or cancel it. Two before-events exist: BeforeDamageEvent and BeforeHealEvent.

These are classes (not structs) so modifications persist. When a subscriber changes Amount or sets Cancel = true, those changes are visible to the Battle method that fired the event and to all subsequent subscribers.

BeforeDamageEvent

Fired by Battle.DealDamage() before any damage is applied.

Property Type Settable Description
Source ICombatant No The combatant dealing damage.
Target ICombatant No The combatant receiving damage.
Amount int Yes The damage amount. Modify this to increase or reduce damage.
DamageType string No Pass-through string for the type of damage (e.g. "Physical", "Magic").
Element string No Pass-through string for the element (e.g. "Fire", "Ice").
IsCrit bool No Whether this hit is a critical.
Cancel bool Yes Set to true to cancel the damage entirely. OnDamageDealt will not fire.

When Cancel is set to true, the damage is not applied and OnDamageDealt does not fire.

BeforeHealEvent

Fired by Battle.Heal() before any healing is applied.

Property Type Settable Description
Source ICombatant No The combatant performing the heal.
Target ICombatant No The combatant being healed.
Amount int Yes The heal amount. Modify this to boost or reduce healing.
Cancel bool Yes Set to true to cancel the heal entirely. OnHealed will not fire.

Same cancellation behavior as BeforeDamageEvent — when Cancel is true, the heal is not applied and OnHealed does not fire.

Example: Dodge System

Subscribe to OnBeforeDamage and cancel the event to implement a dodge mechanic:

battle.OnBeforeDamage += e =>
{
    // 20% dodge chance
    if (Random.value < 0.2f)
    {
        e.Cancel = true;
        Debug.Log($"{e.Target.Name} dodged the attack!");
    }
};

Example: Damage Reduction

Modify Amount to reduce incoming damage when a combatant is defending:

battle.OnBeforeDamage += e =>
{
    // Halve damage if target is defending
    if (IsDefending(e.Target))
        e.Amount /= 2;
};

Example: Healing Boost

Modify Amount on OnBeforeHeal to amplify healing:

battle.OnBeforeHeal += e =>
{
    // Double healing if healer has "Amplify" buff
    if (HasBuff(e.Source, "Amplify"))
        e.Amount *= 2;
};
Before-events are classes, not structs. This is intentional — modifications to Amount and Cancel must persist to the Battle method that fired the event. Multiple subscribers can all modify the same event.

Helpers

Multi-target overloads, query methods, battle history, and utility features.

Multi-Target Overloads

DealDamage, Heal, ApplyStatus, and ApplyBuff all have overloads that accept IList<ICombatant> instead of a single target. Each individual action still fires its own event. Additionally, a group event fires with aggregated data.

  • OnGroupDamageDealt(GroupDamageEvent) — contains Source, list of Hits (IReadOnlyList<DamageEvent>), TotalDamage
  • OnGroupHealed(GroupHealEvent) — contains Source, list of Heals (IReadOnlyList<HealEvent>), TotalHealed
  • OnGroupStatusApplied(GroupStatusEvent) — contains Source, list of Applications
  • OnGroupBuffApplied(GroupBuffEvent) — contains Source, list of Applications
var enemies = battle.GetEnemies(actor);
battle.DealDamage(actor, enemies, 50, "magic", "Fire");
// Fires OnDamageDealt for EACH enemy, then OnGroupDamageDealt once

Query Helpers

GetEnemies(combatant) returns all alive combatants NOT on the same team. GetAllies(combatant, includeSelf) returns alive combatants on the same team. GetAlive(team) and GetDead(team) filter by team (-1 for all).

var enemies = battle.GetEnemies(hero);     // All alive enemies
var allies = battle.GetAllies(hero, true);  // All alive allies including hero
var dead = battle.GetDead(0);               // Dead party members (team 0)

Battle History

Battle records every event as a BattleHistoryEntry with Sequence number, TurnNumber, EventType string, and the Event object. Access via battle.History (IReadOnlyList<BattleHistoryEntry>).

foreach (var entry in battle.History)
    Debug.Log($"[Turn {entry.TurnNumber}] {entry.EventType}: {entry.Event}");

RemoveCombatant

Removes a combatant from battle entirely (not just KO). Fires OnCombatantRemoved. Useful for flee-per-combatant or banish mechanics.

battle.RemoveCombatant(summon); // Completely removes from battle

BattleResult

Created when battle ends via EndBattle() or CheckBattleEnd(). Contains Outcome (BattleState), TotalTurns, Survivors (list of alive combatants), and full History. Access via battle.Result.

battle.OnBattleEnd += (b, state) =>
{
    var result = b.Result;
    Debug.Log($"Outcome: {result.Value.Outcome}");
    Debug.Log($"Turns: {result.Value.TotalTurns}");
    Debug.Log($"Survivors: {result.Value.Survivors.Count}");
};

Turn Systems

Five pluggable turn systems — from classic round-robin to press turns and action points.

ITurnSystem Interface

All turn systems implement ITurnSystem. This is the shared contract that Battle uses to manage turn order. You can also implement your own.

public interface ITurnSystem
{
    void Init(List<ICombatant> combatants);
    ICombatant GetNextActor();
    IReadOnlyList<ICombatant> GetTimeline();
    void OnActorDied(ICombatant combatant);
    void OnActorAdded(ICombatant combatant);
    void OnTurnEnd();
}

Methods

  • Init(combatants) — called once when the battle starts. Sets up internal state from the combatant list.
  • GetNextActor() — returns the next combatant who should act, or null if no one is ready.
  • GetTimeline() — returns a read-only list representing upcoming turn order. Useful for UI previews.
  • OnActorDied(combatant) — notifies the turn system that a combatant died so it can clean up.
  • OnActorAdded(combatant) — notifies the turn system that a combatant was added mid-battle.
  • OnTurnEnd() — called after an actor finishes their turn. Advances internal state.

ClassicTurnSystem

DQ-style speed-sorted round-robin. Simple and predictable.

Constructor

new ClassicTurnSystem()  // No parameters

Behavior

  • On init (and at the start of each new round), all alive combatants are sorted by Speed descending.
  • Dead combatants are skipped automatically in GetNextActor().
  • Mid-round additions via OnActorAdded() are appended to the end of the current round.
  • When all combatants in the round have acted, a new round begins with a fresh speed sort.

Additional Methods

None. ClassicTurnSystem only exposes the ITurnSystem interface.

Usage

var battle = new Battle();
battle.Start(combatants, new ClassicTurnSystem());

var actor = battle.BeginNextTurn(); // Fastest combatant
// ... do action ...
battle.EndTurn();

ATBTurnSystem

Active Time Battle (gauge-based, FF-style). Gauges fill in real time based on speed.

Constructor

new ATBTurnSystem(float gaugeMax = 100f)
  • gaugeMax — the gauge value a combatant must reach before they can act. Default: 100f.

Behavior

  • Each combatant has a gauge starting at 0.
  • Tick(deltaTime) fills each alive combatant's gauge by Speed * deltaTime. Already-full gauges do not overflow.
  • GetNextActor() returns the ready combatant (gauge >= max) with the highest gauge. Ties broken by speed. Returns null if no one is ready.
  • After acting, OnTurnEnd() resets the actor's gauge to 0.
  • Dead combatants have their gauge removed entirely.

Additional Methods

MethodDescription
Tick(float deltaTime)Advances all alive combatants' gauges by Speed * deltaTime.
IsReady(ICombatant)Returns true if the combatant's gauge >= max.
GetGauge(ICombatant)Returns current gauge value (or 0 if not tracked).
SetGauge(ICombatant, float)Sets gauge value directly (e.g. for slow/haste effects).
HasReadyActor()Returns true if any alive combatant's gauge >= max.

Usage

var atb = new ATBTurnSystem(100f);
battle.Start(combatants, atb);

// Game loop
atb.Tick(1f); // Advance time
if (atb.HasReadyActor())
{
    var actor = battle.BeginNextTurn();
    // ... do action ...
    battle.EndTurn();
}
Real-time system. You drive the clock by calling Tick() yourself. This works with Unity's Time.deltaTime, fixed timesteps, or any custom tick rate.

TimelineTurnSystem

Charge Time Battle (timeline-based, FFT-style). Combatants sit on a tick timeline; lower tick = sooner turn.

Constructor

new TimelineTurnSystem(int defaultActionCost = 100, int baseTick = 100)
  • defaultActionCost — the default cost added after an action. Default: 100.
  • baseTick — base tick value used for initial placement and new combatants. Default: 100.

Behavior

  • On init, each combatant is placed on the timeline at tick baseTick / Speed. Faster combatants start closer to 0.
  • GetNextActor() returns the alive combatant with the lowest tick. Ties broken by highest speed, then init order.
  • After acting, the actor's tick advances by actionCost (default or overridden via SetActionCost()).
  • Heavy actions with higher costs push the actor further down the timeline. Light actions let them act again sooner.
  • New combatants added mid-battle are placed at currentTick + baseTick.

Additional Methods

MethodDescription
SetActionCost(int cost)Override the cost for the current action. Call between BeginNextTurn() and EndTurn(). Resets to default each turn.
GetTick(ICombatant)Returns the combatant's current tick value on the timeline.

Usage

var timeline = new TimelineTurnSystem(100, 100);
battle.Start(combatants, timeline);

var actor = battle.BeginNextTurn(); // Lowest tick actor

// Light attack — default cost
battle.EndTurn();

var next = battle.BeginNextTurn();
timeline.SetActionCost(200); // Heavy attack — delays next turn more
battle.EndTurn();
Action cost strategy. Use SetActionCost() to make different abilities cost different amounts of time. A quick jab might cost 50, a powerful spell 300. This creates meaningful tactical decisions about speed vs. power.

PressTurnSystem

SMT-style press turn system. Teams take turns in phases, spending turn points to act.

Constructor

new PressTurnSystem(int maxPointsPerTeam = 0)
  • maxPointsPerTeam — cap on points per phase. 0 = auto (one point per alive team member).

Behavior

  • Teams are determined from combatant Team values, sorted ascending. Team 0 goes first.
  • Each team phase starts with full points = number of alive members (capped by maxPointsPerTeam if set).
  • You choose who acts via SelectActor(), then call GetNextActor() to retrieve them.
  • Actions consume or convert points. When points are exhausted, the next team's phase begins.
  • A KO mid-phase does not retroactively shrink points — dead actors simply cannot be selected.

Properties

PropertyTypeDescription
ActiveTeamintThe team ID currently taking their phase.
FullPointsintRemaining full turn points.
HalfPointsintRemaining half turn points.
TotalPointsintFull + half points combined.
HasActionsRemainingbooltrue if TotalPoints > 0.

Additional Methods

MethodDescription
SelectActor(ICombatant)Choose who acts next. Must be alive and on the active team. Throws if not.
ConsumeAction()Spends one point. Consumes a half point first; if none, consumes a full point.
ConvertAction()Converts one full point into a half point. If no full points, consumes a half point instead.
ConsumeActions(int count)Consumes multiple points (half first, then full).
ConsumeAllActions()Ends the team's phase immediately by zeroing all points.

Usage

var press = new PressTurnSystem();
battle.Start(combatants, press);

// Team 0's phase — 1 point per alive member
press.SelectActor(myChosenActor);
var actor = battle.BeginNextTurn();
// ... do action ...
press.ConsumeAction();
battle.EndTurn();

// If points remain, select another actor
if (press.HasActionsRemaining)
{
    press.SelectActor(anotherActor);
    var next = battle.BeginNextTurn();
    // ... hit a weakness? Convert instead of consume ...
    press.ConvertAction();
    battle.EndTurn();
}
You manage the points. SimpleJRPG does not decide when to consume vs. convert — that is your game logic. Hitting a weakness, getting a critical, or missing are all your system's responsibility. Call the appropriate point method based on your rules.

ActionPointTurnSystem

Action Point system. Combatants bank or spend Action Points for extra actions.

Constructor

new ActionPointTurnSystem(int defaultAP = 0, int minAP = -4, int maxAP = 4)
  • defaultAP — starting AP for each combatant. Default: 0.
  • minAP — lowest AP can go (going negative means skipping future turns). Default: -4.
  • maxAP — highest AP can be banked. Default: 4.

Behavior

  • Uses speed-sorted round-robin like ClassicTurnSystem for base turn order.
  • At the start of each round, all alive combatants gain +1 AP passively (capped at maxAP).
  • SpendAP() spends 1 AP to queue an extra action this turn. Can be called multiple times. AP can go negative down to minAP.
  • SaveAP() gains 1 AP (capped at maxAP) and the combatant skips their action.
  • Combatants with negative AP are skipped when their turn comes — they must wait for passive AP to bring them back to 0.
  • If a combatant dies mid-spend-chain, remaining extra actions are cleared.

Properties

PropertyTypeDescription
MinAPintThe minimum AP value (constructor param).
MaxAPintThe maximum AP value (constructor param).

Additional Methods

MethodDescription
GetAP(ICombatant)Returns the combatant's current Action Points.
SpendAP()Spend 1 AP for an extra action this turn. Throws if already at minAP.
SaveAP()Gain 1 AP (capped at maxAP). The combatant should skip their action.

Usage

var ap = new ActionPointTurnSystem(0, -4, 4);
battle.Start(combatants, ap);

var actor = battle.BeginNextTurn();

// Option A: SpendAP — take multiple actions now, risk negative AP
ap.SpendAP(); // AP: 0 -> -1, queues 1 extra action
// ... do first action ...
battle.EndTurn();
// Extra action fires — same actor again
var same = battle.BeginNextTurn();
// ... do second action ...
battle.EndTurn();

// Option B: SaveAP — skip turn, bank AP for later
var next = battle.BeginNextTurn();
ap.SaveAP(); // AP +1
battle.EndTurn(); // Skips action
Risk vs. reward. Spending AP lets you burst multiple actions in one turn but leaves you vulnerable if AP goes negative. Saving AP is safe but costs a turn. The tension between the two is the core of this system.

Comparison Table

Feature Classic ATB Timeline Press Turn Action Point
Turn order Speed sort Gauge fill Tick timeline Team phase Round-robin
Real-time No Yes (Tick) No No No
Player choice in order No No Via action cost SelectActor Spend/Save AP
Team-based No No No Yes No
Resource management None None Action cost Turn points AP

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.

battle.OnBeforeDamage += evt =>
{
    var target = (MyCombatant)evt.Target;
    float multiplier = target.GetElementMultiplier(evt.Element);
    evt.Amount = (int)(evt.Amount * multiplier);
    if (multiplier == 0f) evt.Cancel = true; // Immune
};

Status Effect Timers

Track durations yourself and call RemoveStatus() when they expire. Tick in OnTurnEnd.

Dictionary<ICombatant, List<(string id, int turnsLeft)>> statuses = new();

battle.OnStatusApplied += evt =>
{
    if (!statuses.ContainsKey(evt.Target)) statuses[evt.Target] = new();
    statuses[evt.Target].Add((evt.StatusId, evt.Duration));
};

battle.OnTurnEnd += evt =>
{
    if (!statuses.ContainsKey(evt.Actor)) return;
    var list = statuses[evt.Actor];
    for (int i = list.Count - 1; i >= 0; i--)
    {
        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

Track buff multipliers yourself. Apply them in OnBeforeDamage / OnBeforeHeal.

Dictionary<ICombatant, Dictionary<string, (float mult, int turns)>> buffs = new();

battle.OnBuffApplied += evt =>
{
    if (!buffs.ContainsKey(evt.Target)) buffs[evt.Target] = new();
    buffs[evt.Target][evt.BuffId] = (evt.Amount, evt.Duration); // Replace
};

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);
};

Revive Logic

void CastRevive(ICombatant caster, ICombatant target, bool full)
{
    if (target.IsAlive) return;
    var myTarget = (MyCombatant)target;
    int hp = full ? myTarget.MaxHP : myTarget.MaxHP / 2;
    battle.Revive(target, hp);
}

Flee Chance

void AttemptFlee(ICombatant actor)
{
    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!"); battle.EndTurn(); }
}

Multi-Hit Attacks

void Barrage(ICombatant actor, ICombatant target, int hitCount)
{
    for (int i = 0; i < hitCount; i++)
    {
        int damage = CalculateDamage(actor, target);
        bool isCrit = UnityEngine.Random.value < 0.15f;
        battle.DealDamage(actor, target, damage, "physical", "", isCrit);
        if (!target.IsAlive) break;
    }
}

Damage Shields / Barriers

Use OnBeforeDamage to absorb or cancel damage. Track shield HP yourself.

Dictionary<ICombatant, int> shields = new();

battle.OnBeforeDamage += evt =>
{
    if (!shields.TryGetValue(evt.Target, out int hp) || hp <= 0) return;
    if (hp >= evt.Amount) { shields[evt.Target] = hp - evt.Amount; evt.Cancel = true; }
    else { evt.Amount -= hp; shields[evt.Target] = 0; battle.RemoveBuff(evt.Target, "shield"); }
};

Counter Attacks

battle.OnDamageDealt += evt =>
{
    if (!evt.Target.IsAlive) return;
    var target = (MyCombatant)evt.Target;
    if (!target.HasSkill("counter")) return;
    if (UnityEngine.Random.value < 0.3f)
        battle.DealDamage(evt.Target, evt.Source, CalculateDamage(evt.Target, evt.Source), "physical", "");
};

Critical Hits

void Attack(ICombatant attacker, ICombatant target)
{
    int baseDamage = CalculateDamage(attacker, target);
    bool isCrit = UnityEngine.Random.value < 0.1f;
    int finalDamage = isCrit ? baseDamage * 2 : baseDamage;
    battle.DealDamage(attacker, target, finalDamage, "physical", "", isCrit);
}

battle.OnDamageDealt += evt =>
{
    if (evt.WasCrit) ShowBigDamageNumber(evt.Amount);
    else ShowDamageNumber(evt.Amount);
};

Drain / Life Steal

void DrainAttack(ICombatant attacker, ICombatant target)
{
    int damage = CalculateDamage(attacker, target);
    battle.DealDamage(attacker, target, damage, "magical", "dark");
    battle.Heal(attacker, attacker, damage / 2);
}

Instant Kill

void CastDeath(ICombatant caster, ICombatant target)
{
    var myTarget = (MyCombatant)target;
    if (myTarget.IsImmuneToInstantDeath) { Debug.Log($"{target.Name} resisted!"); return; }
    if (UnityEngine.Random.value < 0.3f) battle.Kill(target, caster);
    else Debug.Log("It missed!");
}

Damage Formula Examples

// Simple: attack - defense
int SimpleDamage(MyCombatant atk, MyCombatant def)
    => Math.Max(1, atk.Attack - def.Defense);

// DQ-style: attack/2 - defense/4, randomized
int DQDamage(MyCombatant atk, MyCombatant def)
{
    int base_ = Math.Max(1, atk.Attack / 2 - def.Defense / 4);
    return (int)(base_ * UnityEngine.Random.Range(0.875f, 1.125f));
}

// Level-scaled with crit
int ScaledDamage(MyCombatant atk, MyCombatant def, bool isCrit)
{
    float levelBonus = 1f + (atk.Level - def.Level) * 0.05f;
    int raw = Math.Max(1, (int)(atk.Attack * levelBonus) - def.Defense);
    return isCrit ? raw * 2 : raw;
}

API Reference

Complete reference for every type, member, method, event, and struct in SimpleJRPG.

ICombatant Interface

The interface users implement for their characters and enemies. Battle never modifies combatants directly — it calls TakeDamage() and Heal(), and your implementation decides what that means.

Member Type Description
Name string (get) Display name
IsAlive bool (get) Whether combatant is alive
Team int (get) Team identifier (0 = player)
Speed float (get) Speed stat for turn order
TakeDamage(int amount) void Reduce HP by amount
Heal(int amount) void Restore HP by amount

ITurnSystem Interface

The interface for turn order logic. Implement this to create custom turn systems, or use one of the built-in implementations.

Member Type Description
Init(List<ICombatant> combatants) void Initialize with combatant list
GetNextActor() ICombatant Get next combatant to act
GetTimeline() IReadOnlyList<ICombatant> Predicted turn order
OnActorDied(ICombatant) void Notify of combatant death
OnActorAdded(ICombatant) void Notify of combatant addition
OnTurnEnd() void Notify turn completed

Battle — Properties

Property Type Description
State BattleState Current battle state
TurnNumber int Current turn count
Combatants List<ICombatant> All combatants
TurnSystem ITurnSystem Active turn system
CurrentActor ICombatant Current acting combatant
Result BattleResult? Result after battle ends
History IReadOnlyList<BattleHistoryEntry> All recorded events

Battle — Methods

Method Returns Description
Start(List<ICombatant>, ITurnSystem) void Start battle
EndBattle(BattleState) void End battle with result
BeginNextTurn() ICombatant Start next turn, returns actor
EndTurn() void End current turn
DealDamage(ICombatant source, ICombatant target, int amount, string damageType = "", string element = "", bool isCrit = false) void Deal damage to single target
DealDamage(ICombatant source, IList<ICombatant> targets, int amount, string damageType = "", string element = "", bool isCrit = false) void Deal damage to multiple targets
Heal(ICombatant source, ICombatant target, int amount) void Heal single target
Heal(ICombatant source, IList<ICombatant> targets, int amount) void Heal multiple targets
Kill(ICombatant target, ICombatant killer = null) void Force KO
Revive(ICombatant target, int hpAmount) void Revive dead target
ApplyStatus(ICombatant source, ICombatant target, string statusId, int duration = -1) void Apply status effect
ApplyStatus(ICombatant source, IList<ICombatant> targets, string statusId, int duration = -1) void Apply status to multiple
RemoveStatus(ICombatant target, string statusId) void Remove status
ApplyBuff(ICombatant source, ICombatant target, string buffId, float amount, int duration = -1) void Apply buff
ApplyBuff(ICombatant source, IList<ICombatant> targets, string buffId, float amount, int duration = -1) void Apply buff to multiple
RemoveBuff(ICombatant target, string buffId) void Remove buff
Flee(ICombatant who) void Flee from battle
RemoveCombatant(ICombatant) void Remove from battle entirely
CheckBattleEnd() bool Check if battle should end
GetAlive(int team = -1) List<ICombatant> Get alive combatants
GetDead(int team = -1) List<ICombatant> Get dead combatants
GetEnemies(ICombatant) List<ICombatant> Get alive enemies
GetAllies(ICombatant, bool includeSelf = false) List<ICombatant> Get alive allies
GetTimeline() IReadOnlyList<ICombatant> Get turn order from turn system

Battle — Events

Event Signature When
OnBattleStart Action<Battle> Battle.Start() called
OnBattleEnd Action<Battle, BattleState> Battle ends
OnTurnStart Action<TurnEvent> BeginNextTurn() called
OnTurnEnd Action<TurnEvent> EndTurn() called
OnBeforeDamage Action<BeforeDamageEvent> Before damage applied (cancellable)
OnDamageDealt Action<DamageEvent> After damage applied
OnBeforeHeal Action<BeforeHealEvent> Before heal applied (cancellable)
OnHealed Action<HealEvent> After heal applied
OnKO Action<KOEvent> Combatant knocked out
OnRevived Action<ICombatant> Combatant revived
OnStatusApplied Action<StatusEvent> Status applied
OnStatusRemoved Action<ICombatant, string> Status removed
OnBuffApplied Action<BuffEvent> Buff applied
OnBuffRemoved Action<ICombatant, string> Buff removed
OnFled Action<ICombatant> Combatant fled
OnCombatantRemoved Action<ICombatant> Combatant removed
OnGroupDamageDealt Action<GroupDamageEvent> Multi-target damage
OnGroupHealed Action<GroupHealEvent> Multi-target heal
OnGroupStatusApplied Action<GroupStatusEvent> Multi-target status
OnGroupBuffApplied Action<GroupBuffEvent> Multi-target buff

BattleEvents — Structs

DamageEvent

Field Type Description
Source ICombatant Who dealt the damage
Target ICombatant Who received the damage
Amount int Damage amount
DamageType string Pass-through damage type
Element string Pass-through element
WasCrit bool Whether the hit was critical

HealEvent

Field Type Description
Source ICombatant Who performed the heal
Target ICombatant Who was healed
Amount int Heal amount

StatusEvent

Field Type Description
Source ICombatant Who applied the status
Target ICombatant Who received the status
StatusId string Pass-through status identifier
Duration int Duration in turns (-1 = permanent)

BuffEvent

Field Type Description
Source ICombatant Who applied the buff
Target ICombatant Who received the buff
BuffId string Pass-through buff identifier
Amount float Numeric value (user-interpreted)
Duration int Duration in turns (-1 = permanent)

KOEvent

Field Type Description
Target ICombatant Who was knocked out
Killer ICombatant (nullable) Who caused the KO (null if none)

TurnEvent

Field Type Description
Actor ICombatant Whose turn it is
TurnNumber int Current turn number

GroupDamageEvent

Field Type Description
Source ICombatant Who dealt the damage
Hits IReadOnlyList<DamageEvent> Individual damage events
TotalDamage int Sum of all damage dealt

GroupHealEvent

Field Type Description
Source ICombatant Who performed the heals
Heals IReadOnlyList<HealEvent> Individual heal events
TotalHealed int Sum of all HP restored

GroupStatusEvent

Field Type Description
Source ICombatant Who applied the statuses
Applications IReadOnlyList<StatusEvent> Individual status events

GroupBuffEvent

Field Type Description
Source ICombatant Who applied the buffs
Applications IReadOnlyList<BuffEvent> Individual buff events

BattleHistoryEntry

Field Type Description
Sequence int Global sequence number
TurnNumber int Turn when event occurred
EventType string Type of event recorded
Event object The event data (cast to specific type)

BattleResult

Field Type Description
Outcome BattleState Victory, Defeat, or Fled
TotalTurns int Total turns taken
Survivors List<ICombatant> Combatants still alive at end
History IReadOnlyList<BattleHistoryEntry> Complete battle event log

BattleEvents — Before-Events (Classes)

Before-events are mutable classes (not structs) that allow subscribers to modify or cancel actions before they execute.

BeforeDamageEvent

Field Type Description
Source ICombatant Who is dealing the damage
Target ICombatant Who will receive the damage
Amount int (settable) Damage amount — modify to change final damage
DamageType string Pass-through damage type
Element string Pass-through element
IsCrit bool Whether the hit is critical
Cancel bool (settable) Set to true to prevent damage entirely

BeforeHealEvent

Field Type Description
Source ICombatant Who is performing the heal
Target ICombatant Who will be healed
Amount int (settable) Heal amount — modify to change final heal
Cancel bool (settable) Set to true to prevent heal entirely

BattleState Enum

Value Description
NotStarted Battle has not begun yet
WaitingForCommands Waiting for the next turn to begin
Executing A turn is in progress
Victory Player team won
Defeat Player team lost
Fled A combatant fled the battle

Demos

6 interactive demo scenes that showcase every feature of SimpleJRPG.

Overview

SimpleJRPG ships with 6 interactive demo scenes, each generated via a menu item in the Unity editor. Every scene is fully self-contained with its own UI and demonstrates a specific feature of the API. No setup required — just generate and hit Play.

Generating Demos

Open the Unity menu bar and go to Window > Simple JRPG. Each menu item generates a fully-wired scene with combatants, UI, and event subscriptions already configured.

Battle Core Demo

Menu: Window > Simple JRPG > Generate Battle Core Demo

The kitchen-sink demo. Demonstrates ALL core features of the Battle class: full lifecycle (StartBattle through EndBattle), DealDamage (single-target and multi-target), Heal, Kill, Revive, before-events (toggle cancel and toggle halve), ApplyStatus, ApplyBuff, Flee, battle history, RemoveCombatant, BattleResult, and the GetEnemies/GetAllies query helpers.

UI layout: Party HP panel on the left, action buttons in the center, and a scrolling event log on the right.

Classic Turn Demo

Menu: Window > Simple JRPG > Generate Classic Turn Demo

A 3v3 party battle using ClassicTurnSystem. Step through turns one at a time, see the turn order displayed, watch auto-attacks resolve, observe round cycling as all combatants take their turns, and see dead combatants get skipped. Includes revive to demonstrate mid-battle turn order updates.

ATB Turn Demo

Menu: Window > Simple JRPG > Generate ATB Turn Demo

A 3v3 party battle using ATBTurnSystem. A Tick button advances time, filling each combatant's gauge based on their speed. Gauge bars are displayed per combatant. When a combatant's gauge is full, they become ready and you can select an action for them.

Timeline Turn Demo

Menu: Window > Simple JRPG > Generate Timeline Turn Demo

A 3v3 party battle using TimelineTurnSystem. Displays the full timeline showing when each combatant will act next. Actions have different cast-time costs — Quick (50), Normal (100), and Heavy (200) — that push the actor further down the timeline. Tick progression advances the timeline and resolves turns as they come up.

Press Turn Demo

Menu: Window > Simple JRPG > Generate Press Turn Demo

A two-team battle using PressTurnSystem. Displays the current phase and the team's turn point pool as Full and Half points. Demonstrates SelectActor to pick who acts, ConsumeAction for normal actions, ConvertAction to convert a Full point to Half, and ConsumeAllActions to end the phase early.

Action Point Turn Demo

Menu: Window > Simple JRPG > Generate Action Point Turn Demo

A 3v3 party battle using ActionPointTurnSystem. Displays each combatant's current AP (Action Points). Spend AP queues extra actions in a single turn. Save AP banks points by skipping a turn. Demonstrates the risk/reward of going negative on AP for burst damage versus saving up for future turns.

Scene Location Demo scenes are saved to Assets/SimpleJRPG/Scenes/. You can regenerate them at any time — existing scenes will be overwritten.

About

The design philosophy behind SimpleJRPG.

Design Philosophy

SimpleJRPG orchestrates battles — turn order, events, state management. You implement damage formulas, design status effects, and build your UI.

SimpleJRPG has no opinions on your game design. It uses pass-through strings instead of enums for all game-specific values like damage types, elements, status IDs, and buff IDs. We never interpret what "Fire" or "Poison" means — your system decides.

Everything is instance-based. No singletons, no statics, no global state. Create a Battle instance, pass in your combatants and a turn system, subscribe to events, and call combat verbs. That's it.

What SimpleJRPG Is NOT

  • Not a complete battle system — it's the skeleton, you add the flesh. We provide orchestration; you provide the game.
  • Not opinionated about damage formulas, stat systems, or game balance — you do the math, pass us the result, and we route it through.
  • Not a UI framework — events let you drive any UI you want. We fire OnDamageDealt, you decide what that looks like on screen.
  • Not dependent on Unity — pure C#, no MonoBehaviours, no ScriptableObjects, no UnityEngine imports. The asmdef uses noEngineReferences: true.

Version History

  • v2.0 (February 2026) — Complete rewrite. Lean runtime API with an instance-based Battle class, pluggable turn systems (Classic, ATB, Timeline, Press Turn, Action Point), before-events for cancellation and modification, and battle helpers (multi-target, queries, history, results).
  • v1.0 (scrapped) — ScriptableObject-based composable effects system. Over-engineered abstraction layers. Lesson learned.

Support

For questions, bug reports, and feature requests:

↓ ↑