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 ifHP <= 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
- Keep modules focused. Each module should handle one responsibility.
- Communicate through events, not direct references between modules.
- Test damage calculations with known inputs and expected outputs.
- Do not modify Battler state directly -- always go through the appropriate module.
- Use the state machine for turn sequencing. Do not add ad-hoc coroutines.