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.