Перейти к основному содержимому

Глубокое погружение во внутренние системы сражений

Это руководство для пользователей уровня Source, имеющих доступ к полному исходному коду MCE на C#.

Этот документ предоставляет детальное рассмотрение архитектуры BattleManager, его 15+ модулей, BattleStateMachine и способов добавления новых боевых функций.

Архитектура BattleManager

BattleManager следует паттерну модульной композиции. Вместо монолитного класса с тысячами строк, логика боя разделена между фокусированными модулями, которые взаимодействуют через хаб 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;
}

Базовый класс BattleManagerModule

Все модули наследуют от 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>();
}
}

Этот паттерн позволяет каждому модулю:

  • Обращаться к другим модулям через менеджер (слабое связывание).
  • Инициализироваться независимо.
  • Тестироваться изолированно с помощью мока менеджера.

Подробный обзор модулей

BattleManagerBattlersModule

Файл: BattleManagerBattlersModule.cs

Управляет экземплярами Battler -- боевым представлением монстров:

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
}

Ключевые методы:

  • CreateBattler(MonsterInstance) -- Создаёт Battler из MonsterInstance.
  • GetActiveBattler(side) -- Возвращает активного бойца стороны.
  • GetAllBattlers() -- Возвращает всех бойцов (обе стороны).

BattleManagerMovesModule

Файл: BattleManagerMovesModule.cs

Основной механизм урона. Крупнейший модуль (~180КБ в файле Battler.cs).

Конвейер расчёта урона:

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

Ключевые методы:

  • ExecuteMove(Battler, Move, Battler) -- Полное выполнение приёма с эффектами.
  • CanUseMove(Battler, MoveSlot) -- Проверка PP, отключения, заморозки и т.д.
  • ApplySecondaryEffect(Move, Battler, Battler) -- Изменения характеристик, наложение статусов.
  • GetTypeEffectiveness(Move, Battler) -- Поиск в таблице типов с двойными типами.

BattleManagerHealthModule

Файл: BattleManagerHealthModule.cs

Все изменения HP проходят через этот модуль:

  • DealDamage(Battler, int) -- Уменьшить HP, проверить нокаут.
  • Heal(Battler, int) -- Восстановить HP, ограничено максимумом.
  • CheckFainted(Battler) -- Возвращает true если HP <= 0.
  • ProcessFaint(Battler) -- Начислить опыт, EVs, проверить эволюцию.

BattleManagerStatusesModule

Файл: BattleManagerStatusesModule.cs

Управляет состояниями статуса и их эффектами за ход:

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

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

Файл: BattleManagerCaptureModule.cs

Реализует формулу шанса поимки:

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

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

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

Файл: BattleStateMachine.cs

Конечный автомат использует подход на основе перечислений с корутинами:

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

Добавление новых модулей боя

Для добавления нового модуля в систему сражений:

Шаг 1: Создание класса модуля

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

Шаг 2: Регистрация модуля

Добавьте модуль в список модулей BattleManager:

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

Шаг 3: Интеграция с существующими модулями

В MovesModule вызовите модуль погоды при расчёте урона:

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

Шаг 4: Подключение к конечному автомату

В фазе эффектов хода обработайте погоду:

GetModule<BattleManagerWeatherModule>().ProcessTurnEnd();

Система событий боя

Модули взаимодействуют через события на BattleManager:

// Определение событий
public event Action<Battler, Move, int> OnDamageDealt;
public event Action<Battler, StatusCondition> OnStatusApplied;
public event Action<Battler> OnBattlerFainted;
public event Action<Battler, Battler> OnBattlerSwitched;

// Генерация событий из модулей
BattleManager.OnDamageDealt?.Invoke(target, move, damage);

// Подписка из других модулей
BattleManager.OnDamageDealt += HandleDamageDealt;

Эта система событий позволяет модулям реагировать на действия друг друга без прямого связывания.

Тестирование логики боя

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

int neutralDamage = CalculateNeutralDamage(attacker, defender, move);
Assert.AreEqual(neutralDamage * 2, damage, neutralDamage * 0.15f);
}

Лучшие практики

  1. Сохраняйте фокусировку модулей. Каждый модуль должен обрабатывать одну ответственность.
  2. Взаимодействуйте через события, а не прямые ссылки между модулями.
  3. Тестируйте расчёты урона с известными входами и ожидаемыми выходами.
  4. Не изменяйте состояние Battler напрямую -- всегда через соответствующий модуль.
  5. Используйте конечный автомат для последовательности ходов. Не добавляйте специальные корутины.