Skip to main content

PvP Battles

MCE Online provides a full server-authoritative battle system covering PvP matchmaking, wild/trainer battle validation, spectating, raid co-op, and capture verification. All battle commands are routed through Nakama RPCs so the server is the source of truth.

Architecture Overview

ClassFileResponsibility
NetworkedBattleManagerBattle/NetworkedBattleManager.csMonoBehaviour bridge between MCE's local BattleManager and the online layer
ServerBattleAuthorityBattle/BattleServices.csSubmits all battle actions (move, switch, item, flee) via Nakama RPCs
BattleMatchmakerBattle/BattleServices.csELO-based PvP matchmaking using Nakama's matchmaker
SpectatorServiceBattle/BattleServices.csWatch live battles in read-only mode
RaidBattleServiceBattle/BattleServices.cs5v1 co-op boss battles via realtime matches

Server-Authoritative Battle Flow

NetworkedBattleManager decides whether to use online validation based on NakamaManager.IsConnected. If offline or forceOffline is set, battles run entirely through the local BattleManager.

Starting a Battle

Wild encounter (server generates the wild monster to prevent IV/shiny manipulation):

string battleId = await networkedBattleManager.StartWildBattleAsync(
mapId, encounterTableId, playerLevel);

This calls the battle_start_wild RPC. The server returns a BattleStartResponse with battleId and matchId. The client joins the battle match for real-time state updates.

Trainer/NPC battle:

string battleId = await networkedBattleManager.StartTrainerBattleAsync(
trainerId, mapId);

Calls the battle_start_trainer RPC.

Submitting Actions

All actions go through ServerBattleAuthority via NetworkedBattleManager.SubmitActionAsync():

// Use a move
await networkedBattleManager.SubmitActionAsync(
BattleActionType.Move, moveIndex: 0, target: 0);

// Switch monster
await networkedBattleManager.SubmitActionAsync(
BattleActionType.Switch, index: 2);

// Use an item (index is treated as an item ID string internally;
// it is passed as index.ToString() to ServerBattleAuthority.SubmitItemAsync(battleId, itemId, targetSlot))
await networkedBattleManager.SubmitActionAsync(
BattleActionType.Item, index: itemSlot, target: targetSlot);

// Attempt to flee
await networkedBattleManager.SubmitActionAsync(
BattleActionType.Flee, index: 0);

Each action type sends a different payload to the battle_submit_action RPC:

ActionPayloadServer Validates
MovebattleId, moveIndex, targetSlot, actionType: "move"Move exists, PP remaining, valid target
SwitchbattleId, switchTo (slot), actionType: "switch"Monster alive, not trapped
ItembattleId, itemId, targetSlot, actionType: "item"Item in inventory, valid use
FleebattleId, actionType: "flee"Flee check (cannot flee from trainer PvP)

Turn Results

After both players submit actions, the server resolves the turn and returns a TurnResult:

networkedBattleManager.OnTurnProcessed += (TurnResult result) =>
{
// result.turnNumber, result.playerMoveName, result.enemyMoveName
// result.playerDamageDealt, result.enemyDamageDealt
// result.effectiveness, result.statusApplied, result.weatherChange
// result.playerFainted, result.enemyFainted
};

Battle End

When the battle concludes, OnBattleEnded fires with a BattleResult:

networkedBattleManager.OnBattleEnded += (BattleResult result) =>
{
// result.playerWon, result.expGained, result.moneyGained
// result.itemsGained, result.evGained, result.enemyCaught
// result.eloChange (PvP only)
};

The match is automatically left and state cleaned up via EndBattleAsync().

Online Capture System

Capture attempts are server-validated to prevent catch rate manipulation:

CaptureResult result = await networkedBattleManager.AttemptCaptureAsync("pokeball");
// result.Success -- whether the capture succeeded
// result.ShakeCount -- number of shakes (0-3)
// result.IsCriticalCapture -- whether it was a critical capture

The battle_attempt_capture RPC receives the battleId and ballId, and the server computes the catch rate, shake count, and critical capture chance.

ELO-Based Matchmaking

BattleMatchmaker uses Nakama's built-in matchmaker with ELO-based filtering.

await NakamaManager.Instance.Matchmaker.StartSearchAsync(
eloRating: 1200, tier: "any");

The matchmaker query finds opponents within +/- 200 ELO:

+properties.elo:>=1000 +properties.elo:<=1400

Numeric properties include the player's elo, and string properties include the tier.

The matchmaker creates a 2-player match (minCount: 2, maxCount: 2).

Match Found

When a match is found, ReceivedMatchmakerMatched fires on the socket, which triggers:

NakamaManager.Instance.Matchmaker.OnMatchFound += (matchId, opponent) =>
{
// opponent.UserId, opponent.DisplayName
// matchId -- the battle match to join
};

Search Status and Cancellation

bool searching = NakamaManager.Instance.Matchmaker.IsSearching;
float duration = NakamaManager.Instance.Matchmaker.SearchDuration;

await NakamaManager.Instance.Matchmaker.CancelSearchAsync();

OnMatchCancelled fires after successful cancellation. The matchmaking timeout is configured via OnlineConfig.MatchmakingTimeout (default: 60 seconds).

Spectator Mode

SpectatorService lets players watch live battles in read-only mode.

Listing Live Battles

List<SpectableBattle> battles =
await NakamaManager.Instance.Spectator.GetLiveBattlesAsync();

foreach (var battle in battles)
{
// battle.BattleId, battle.Player1Name, battle.Player2Name
// battle.SpectatorCount, battle.Duration
}

This calls the get_live_battles RPC.

Watching a Battle

bool joined = await NakamaManager.Instance.Spectator.SpectateAsync(battleMatchId);
// Receive battle state updates via OnBattleStateUpdate

await NakamaManager.Instance.Spectator.StopSpectatingAsync();

Spectator mode is controlled by OnlineConfig.EnableSpectator (default: true). Maximum spectators per battle: MaxSpectatorsPerBattle (default: 20).

Raid Battles (5v1 Co-op)

RaidBattleService provides cooperative boss battles where up to 5 players fight a single powerful monster.

Available Raids

List<RaidBossInfo> raids =
await NakamaManager.Instance.Raids.GetAvailableRaidsAsync();

foreach (var raid in raids)
{
// raid.BossId, raid.BossName, raid.Difficulty (Normal/Hard/Legendary/Mythic)
// raid.CurrentParticipants, raid.MaxParticipants, raid.RecommendedLevel
}

Creating a Raid Lobby

string raidMatchId = await NakamaManager.Instance.Raids.CreateRaidLobbyAsync(
bossId: "raid_boss_legendary_dragon",
difficulty: RaidDifficulty.Legendary);

This creates a Nakama match and registers it on the server via the raid_create RPC with the matchId, bossId, difficulty, and maxParticipants (from OnlineConfig.RaidMaxParticipants, default: 5).

Joining and Playing

bool joined = await NakamaManager.Instance.Raids.JoinRaidAsync(raidMatchId);

// Submit actions during the raid
await NakamaManager.Instance.Raids.SubmitRaidActionAsync(raidId, moveIndex);

Raid actions use match state op code 200.

Raid Events

Raids.OnRaidFound += (raidId) => { };
Raids.OnPlayerJoinedRaid += (userId, count) => { };
Raids.OnRaidStarted += (raidId) => { };
Raids.OnRaidCompleted += (raidId, success) => { };

Raid battles are controlled by OnlineConfig.EnableRaidBattles (default: true).

Difficulty Settings

Config FieldDefaultDescription
MatchmakingTimeout60sPvP search timeout
PvPTurnTimeout45sTime per turn in PvP
MaxSpectatorsPerBattle20Spectator cap
RaidMaxParticipants5Max players in a raid
EnableSpectatortrueToggle spectator mode
EnableRaidBattlestrueToggle raid battles

Anti-Cheat in Battle Context

When OnlineConfig.EnableServerAuthority is true, ALL battle outcomes are determined server-side:

  • Wild encounters: The server generates the wild monster (species, level, IVs, shininess) to prevent client-side manipulation.
  • Capture attempts: Catch rate is computed server-side.
  • EXP/money/items: Rewards in BattleResult come from the server and are applied only after validation.
  • ELO changes: Ranking adjustments are computed and applied by the server.

The OnlineCommandBridge ensures that any items or money gained from battles are validated via the mce_give_item and mce_give_money RPCs.