Saltar al contenido principal

Analisis profundo de los internos de batalla

Esta guia es para usuarios del nivel Source que tienen acceso al codigo fuente completo de MCE en C#.

Este documento proporciona una mirada detallada a la arquitectura del BattleManager, sus mas de 15 modulos, el BattleStateMachine y como agregar nuevas funcionalidades de batalla.

Arquitectura del BattleManager

El BattleManager sigue un patron de composicion modular. En lugar de ser una clase monolitica con miles de lineas, la logica de batalla se divide en modulos enfocados que se comunican a traves del hub BattleManager.

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

Clase base BattleManagerModule

Todos los modulos heredan de BattleManagerModule:

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

public virtual void Initialize(BattleManager manager)
{
BattleManager = manager;
}

protected T GetModule<T>() where T : BattleManagerModule
{
return BattleManager.GetModule<T>();
}
}

Este patron permite a cada modulo:

  • Acceder a otros modulos a traves del manager (acoplamiento debil).
  • Inicializarse independientemente.
  • Ser probado en aislamiento mockeando el manager.

Modulos en detalle

BattleManagerBattlersModule

Archivo: BattleManagerBattlersModule.cs

Gestiona las instancias de Battler, la representacion en batalla de los monstruos:

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
}

Metodos clave:

  • CreateBattler(MonsterInstance) -- Crea un Battler a partir de un MonsterInstance.
  • GetActiveBattler(side) -- Devuelve el combatiente activo actual para un lado.
  • GetAllBattlers() -- Devuelve todos los combatientes (ambos lados).

BattleManagerMovesModule

Archivo: BattleManagerMovesModule.cs

El motor central de dano. Este es el modulo mas grande con ~180KB en el archivo Battler.cs.

Pipeline de calculo de dano:

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

Metodos clave:

  • ExecuteMove(Battler, Move, Battler) -- Ejecucion completa del movimiento con efectos.
  • CanUseMove(Battler, MoveSlot) -- Verificar PP, deshabilitado, congelado, etc.
  • ApplySecondaryEffect(Move, Battler, Battler) -- Cambios de estadisticas, infligir estado.
  • GetTypeEffectiveness(Move, Battler) -- Consulta de tabla de tipos con tipos duales.

BattleManagerHealthModule

Archivo: BattleManagerHealthModule.cs

Todos los cambios de HP fluyen a traves de este modulo:

  • DealDamage(Battler, int) -- Reducir HP, verificar debilitamiento.
  • Heal(Battler, int) -- Restaurar HP, limitado al maximo.
  • CheckFainted(Battler) -- Devuelve true si HP <= 0.
  • ProcessFaint(Battler) -- Otorgar XP, EVs, verificar evolucion.

BattleManagerStatusesModule

Archivo: BattleManagerStatusesModule.cs

Gestiona condiciones de estado y sus efectos por turno:

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

Archivo: BattleManagerCaptureModule.cs

Implementa la formula de tasa de captura:

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

Archivo: BattleStateMachine.cs

La maquina de estados usa un enfoque basado en enum con 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);
}
}

Anadir nuevos modulos de batalla

Para anadir un nuevo modulo al sistema de batalla:

Paso 1: Crear la clase del modulo

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

Paso 2: Registrar el modulo

Anade el modulo a la lista de modulos del BattleManager:

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

Paso 3: Integrar con modulos existentes

En el MovesModule, llame al modulo de clima durante el calculo de dano:

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

Paso 4: Conectar con la maquina de estados

En la fase de efectos de turno, procese el clima:

GetModule<BattleManagerWeatherModule>().ProcessTurnEnd();

Sistema de eventos de batalla

Los modulos se comunican a traves de eventos en el 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;

Este sistema de eventos permite que los modulos reaccionen a las acciones de otros sin acoplamiento directo.

Probar logica de batalla

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

Mejores practicas

  1. Mantenga los modulos enfocados. Cada modulo deberia manejar una sola responsabilidad.
  2. Comuniquese a traves de eventos, no de referencias directas entre modulos.
  3. Pruebe los calculos de dano con entradas conocidas y salidas esperadas.
  4. No modifique el estado de Battler directamente; siempre pase por el modulo apropiado.
  5. Use la maquina de estados para la secuenciacion de turnos. No anade coroutines ad-hoc.