Skip to main content

Battle Internals Deep Dive

This guide is for Source tier users who have access to the full MCE C# source code.

This document provides a detailed look at the BattleManager architecture, its 15+ modules, the BattleStateMachine, and how to add new battle features.

BattleManager Architecture

The BattleManager follows a modular composition pattern. Rather than being a monolithic class with thousands of lines, battle logic is split across focused modules that communicate through the BattleManager hub.

public class BattleManager : MonoBehaviour, IBattleManager
{
// Module references (assigned in Inspector or via Zenject)
[SerializeField] private BattleManagerBattlersModule battlersModule;
[SerializeField] private BattleManagerHealthModule healthModule;
[SerializeField] private BattleManagerMovesModule movesModule;
[SerializeField] private BattleManagerItemsModule itemsModule;
[SerializeField] private BattleManagerCaptureModule captureModule;
[SerializeField] private BattleManagerAIModule aiModule;
[SerializeField] private BattleManagerAnimationModule animationModule;
[SerializeField] private BattleManagerAudioModule audioModule;
[SerializeField] private BattleManagerStatusesModule statusesModule;
[SerializeField] private BattleManagerBattlerStatsModule statsModule;
[SerializeField] private BattleManagerBattlerSwitchModule switchModule;
[SerializeField] private BattleManagerRostersModule rostersModule;
[SerializeField] private BattleManagerScenariosModule scenariosModule;
[SerializeField] private BattleManagerMegaModule megaModule;
[SerializeField] private BattleManagerCharactersModule charactersModule;
}

BattleManagerModule Base Class

All modules inherit from BattleManagerModule:

public abstract class BattleManagerModule : MonoBehaviour
{
protected BattleManager BattleManager { get; private set; }

// Called by BattleManager during initialization
public virtual void Initialize(BattleManager manager)
{
BattleManager = manager;
}

// Access other modules through the manager
protected T GetModule<T>() where T : BattleManagerModule
{
return BattleManager.GetModule<T>();
}
}

This pattern allows each module to:

  • Access other modules through the manager (loose coupling).
  • Initialize independently.
  • Be tested in isolation by mocking the manager.

Module Deep Dive

BattleManagerBattlersModule

File: BattleManagerBattlersModule.cs

Manages Battler instances -- the in-battle representation of monsters:

public class Battler
{
public MonsterInstance Monster; // Underlying data
public MonsterEntry Species; // Species reference
public int CurrentHP; // Battle HP
public MoveSlot[] Moves; // 4 move slots with PP
public int[] StatStages; // -6 to +6 per stat
public StatusCondition Status; // Non-volatile status
public List<VolatileStatus> Volatiles; // Volatile statuses
public bool IsFainted => CurrentHP <= 0;

// Calculated effective stats (base + IV + EV + nature + stage)
public int EffectiveAttack { get; }
public int EffectiveDefense { get; }
// ... etc
}

Key Methods:

  • CreateBattler(MonsterInstance) -- Creates a Battler from a MonsterInstance.
  • GetActiveBattler(side) -- Returns the currently active battler for a side.
  • GetAllBattlers() -- Returns all battlers (both sides).

BattleManagerMovesModule

File: BattleManagerMovesModule.cs

The core damage engine. This is the largest module at ~180KB in the Battler.cs file.

Damage Calculation Pipeline:

public int CalculateDamage(Battler attacker, Battler defender, Move move)
{
// 1. Base damage
int level = attacker.Monster.Level;
int power = GetEffectivePower(move, attacker, defender);
int attack = GetAttackStat(move, attacker);
int defense = GetDefenseStat(move, defender);

float baseDamage = ((2f * level / 5f + 2f) * power * attack / defense) / 50f + 2f;

// 2. Modifier chain
float modifier = 1f;
modifier *= GetCriticalMultiplier(attacker, move);
modifier *= GetRandomFactor(); // 0.85 - 1.0
modifier *= GetSTAB(attacker, move); // 1.0 or 1.5
modifier *= GetTypeEffectiveness(move, defender); // 0, 0.25, 0.5, 1, 2, or 4
modifier *= GetWeatherModifier(move);
modifier *= GetAbilityModifier(attacker, defender, move);
modifier *= GetItemModifier(attacker, move);

return Mathf.Max(1, Mathf.FloorToInt(baseDamage * modifier));
}

Key Methods:

  • ExecuteMove(Battler, Move, Battler) -- Full move execution with effects.
  • CanUseMove(Battler, MoveSlot) -- Check PP, disabled, frozen, etc.
  • ApplySecondaryEffect(Move, Battler, Battler) -- Stat changes, status infliction.
  • GetTypeEffectiveness(Move, Battler) -- Type chart lookup with dual types.

BattleManagerHealthModule

File: BattleManagerHealthModule.cs

All HP changes flow through this module:

  • DealDamage(Battler, int) -- Reduce HP, check for faint.
  • Heal(Battler, int) -- Restore HP, capped at max.
  • CheckFainted(Battler) -- Returns true if HP <= 0.
  • ProcessFaint(Battler) -- Award XP, EVs, check for evolution.

BattleManagerStatusesModule

File: BattleManagerStatusesModule.cs

Manages status conditions and their per-turn effects:

public void ApplyStatus(Battler target, StatusCondition status)
{
// Check immunities (Fire can't burn, Electric can't paralyze, etc.)
if (IsImmune(target, status)) return;

// Check if already has a non-volatile status
if (target.Status != StatusCondition.None) return;

target.Status = status;
// Trigger status application animation
}

public void ProcessTurnEndStatuses(Battler battler)
{
switch (battler.Status)
{
case StatusCondition.Burn:
DealStatusDamage(battler, battler.MaxHP / 16);
break;
case StatusCondition.Poison:
DealStatusDamage(battler, battler.MaxHP / 8);
break;
case StatusCondition.BadPoison:
badPoisonCounter++;
DealStatusDamage(battler, battler.MaxHP * badPoisonCounter / 16);
break;
}
}

BattleManagerCaptureModule

File: BattleManagerCaptureModule.cs

Implements the catch rate formula:

public CaptureResult AttemptCapture(Battler wildBattler, CaptureItem ball)
{
float maxHP = wildBattler.MaxHP;
float currentHP = wildBattler.CurrentHP;
float catchRate = wildBattler.Species.CatchRate;
float ballBonus = ball.CatchRateModifier;
float statusBonus = GetStatusBonus(wildBattler.Status);

// Modified catch rate
float modifiedRate = (3 * maxHP - 2 * currentHP) / (3 * maxHP);
modifiedRate *= catchRate * ballBonus * statusBonus;

// Shake check (0-3 shakes before capture or escape)
int shakeThreshold = Mathf.FloorToInt(65536f / Mathf.Pow(255f / modifiedRate, 0.1875f));

int shakes = 0;
for (int i = 0; i < 4; i++)
{
if (Random.Range(0, 65536) < shakeThreshold)
shakes++;
else
break;
}

return new CaptureResult
{
Shakes = shakes,
Captured = shakes >= 4,
CriticalCapture = CheckCriticalCapture(modifiedRate)
};
}

BattleStateMachine

File: BattleStateMachine.cs

The state machine uses an enum-based approach with coroutines:

public class BattleStateMachine : MonoBehaviour
{
public enum BattleState
{
Intro,
TurnStart,
ActionSelection,
ActionExecution,
TurnEffects,
FaintCheck,
ReplaceFainted,
TurnEnd,
BattleEnd,
Results,
Cleanup
}

private BattleState currentState;

public IEnumerator RunBattle(BattleParameters parameters)
{
yield return RunState(BattleState.Intro);

while (currentState != BattleState.BattleEnd)
{
yield return RunState(BattleState.TurnStart);
yield return RunState(BattleState.ActionSelection);
yield return RunState(BattleState.ActionExecution);
yield return RunState(BattleState.TurnEffects);
yield return RunState(BattleState.FaintCheck);

if (NeedReplacement())
yield return RunState(BattleState.ReplaceFainted);

if (IsBattleOver())
break;

yield return RunState(BattleState.TurnEnd);
}

yield return RunState(BattleState.BattleEnd);
yield return RunState(BattleState.Results);
yield return RunState(BattleState.Cleanup);
}
}

Adding New Battle Modules

To add a new module to the battle system:

Step 1: Create the Module Class

public class BattleManagerWeatherModule : BattleManagerModule
{
private WeatherType currentWeather = WeatherType.None;
private int weatherTurnsRemaining;

public override void Initialize(BattleManager manager)
{
base.Initialize(manager);
currentWeather = WeatherType.None;
weatherTurnsRemaining = 0;
}

public void SetWeather(WeatherType weather, int turns)
{
currentWeather = weather;
weatherTurnsRemaining = turns;
}

public float GetDamageMultiplier(Move move)
{
return currentWeather switch
{
WeatherType.Rain when move.Type == MonsterType.Water => 1.5f,
WeatherType.Rain when move.Type == MonsterType.Fire => 0.5f,
WeatherType.Sun when move.Type == MonsterType.Fire => 1.5f,
WeatherType.Sun when move.Type == MonsterType.Water => 0.5f,
_ => 1f
};
}

public void ProcessTurnEnd()
{
weatherTurnsRemaining--;
if (weatherTurnsRemaining <= 0)
{
currentWeather = WeatherType.None;
// Show "The weather returned to normal" message
}
}
}

Step 2: Register the Module

Add the module to the BattleManager's module list:

// In BattleManager.cs
[SerializeField] private BattleManagerWeatherModule weatherModule;

Step 3: Integrate with Existing Modules

In the MovesModule, call into the weather module during damage calculation:

modifier *= GetModule<BattleManagerWeatherModule>().GetDamageMultiplier(move);

Step 4: Hook into the State Machine

In the turn effects phase, process weather:

GetModule<BattleManagerWeatherModule>().ProcessTurnEnd();

Battle Event System

Modules communicate through events on the BattleManager:

// Define events
public event Action<Battler, Move, int> OnDamageDealt;
public event Action<Battler, StatusCondition> OnStatusApplied;
public event Action<Battler> OnBattlerFainted;
public event Action<Battler, Battler> OnBattlerSwitched;

// Fire events from modules
BattleManager.OnDamageDealt?.Invoke(target, move, damage);

// Subscribe from other modules
BattleManager.OnDamageDealt += HandleDamageDealt;

This event system allows modules to react to each other's actions without direct coupling.

Testing Battle Logic

[Test]
public void DamageCalculation_SuperEffective_DoublesDamage()
{
var attacker = CreateTestBattler(level: 50, attack: 100);
var defender = CreateTestBattler(defense: 100, types: MonsterType.Grass);
var move = CreateTestMove(power: 80, type: MonsterType.Fire);

int damage = movesModule.CalculateDamage(attacker, defender, move);

// Super effective should roughly double the damage
int neutralDamage = CalculateNeutralDamage(attacker, defender, move);
Assert.AreEqual(neutralDamage * 2, damage, neutralDamage * 0.15f);
}

Best Practices

  1. Keep modules focused. Each module should handle one responsibility.
  2. Communicate through events, not direct references between modules.
  3. Test damage calculations with known inputs and expected outputs.
  4. Do not modify Battler state directly -- always go through the appropriate module.
  5. Use the state machine for turn sequencing. Do not add ad-hoc coroutines.