Repository: Kermalis/PokemonBattleEngine
Branch: master
Commit: e733883abb04
Files: 242
Total size: 1.7 MB
Directory structure:
gitextract_erdgdp2y/
├── .gitattributes
├── .gitignore
├── LICENSE.md
├── PokemonBattleEngine/
│ ├── Battle/
│ │ ├── Battle.cs
│ │ ├── BattleActions.cs
│ │ ├── BattleDamage.cs
│ │ ├── BattleEffects.cs
│ │ ├── BattleEffects_HitLogic.cs
│ │ ├── BattleEnums.cs
│ │ ├── BattleEvents.cs
│ │ ├── BattleInventory.cs
│ │ ├── BattleMoveset.cs
│ │ ├── BattlePokemon.cs
│ │ ├── BattleReplay.cs
│ │ ├── BattleTargets.cs
│ │ ├── BattleTeam.cs
│ │ ├── BattleTrainer.cs
│ │ ├── BattleUtils.cs
│ │ ├── TrainerInfo.cs
│ │ └── TypeEffectiveness.cs
│ ├── Data/
│ │ ├── DataEnums.cs
│ │ ├── DataProvider.cs
│ │ ├── Interfaces/
│ │ │ ├── ItemData.cs
│ │ │ ├── LocalizedString.cs
│ │ │ ├── MoveData.cs
│ │ │ ├── MovesetInterfaces.cs
│ │ │ ├── PokemonData.cs
│ │ │ ├── PokemonInterfaces.cs
│ │ │ └── StatInterfaces.cs
│ │ ├── Legality/
│ │ │ ├── LegalEffortValues.cs
│ │ │ ├── LegalIndividualValues.cs
│ │ │ ├── LegalMoveset.cs
│ │ │ ├── LegalPokemon.cs
│ │ │ └── LegalPokemonCollection.cs
│ │ ├── PBEAlphabeticalList.cs
│ │ ├── PBEList.cs
│ │ ├── ReadOnlyLocalizedString.cs
│ │ ├── ReadOnlyMoveset.cs
│ │ ├── ReadOnlyPokemon.cs
│ │ ├── ReadOnlyPokemonCollection.cs
│ │ ├── Settings.cs
│ │ ├── StatCollection.cs
│ │ └── Utils/
│ │ ├── DataUtils_Effects.cs
│ │ ├── DataUtils_Forms.cs
│ │ ├── DataUtils_Items.cs
│ │ ├── DataUtils_Moves.cs
│ │ ├── DataUtils_Stats.cs
│ │ └── DataUtils_Validate.cs
│ ├── Network/
│ │ ├── Client.cs
│ │ ├── Encryption.cs
│ │ ├── NetworkUtils.cs
│ │ ├── Server.cs
│ │ └── ServerClient.cs
│ ├── Packets/
│ │ ├── ActionsRequestPacket.cs
│ │ ├── ActionsResponsePacket.cs
│ │ ├── AutoCenterPacket.cs
│ │ ├── BattlePacket.cs
│ │ ├── BattleResultPacket.cs
│ │ ├── FleeResponsePacket.cs
│ │ ├── HazePacket.cs
│ │ ├── MatchCancelledPacket.cs
│ │ ├── PartyRequestPacket.cs
│ │ ├── PartyResponsePacket.cs
│ │ ├── PlayerJoinedPacket.cs
│ │ ├── ResponsePacket.cs
│ │ ├── SwitchInRequestPacket.cs
│ │ ├── SwitchInResponsePacket.cs
│ │ ├── TurnBeganPacket.cs
│ │ ├── _AbilityPacket.cs
│ │ ├── _AbilityReplacedPacket.cs
│ │ ├── _BattleStatusPacket.cs
│ │ ├── _CapturePacket.cs
│ │ ├── _FleeFailedPacket.cs
│ │ ├── _IllusionPacket.cs
│ │ ├── _ItemPacket.cs
│ │ ├── _ItemTurnPacket.cs
│ │ ├── _MoveCritPacket.cs
│ │ ├── _MoveLockPacket.cs
│ │ ├── _MovePPChangedPacket.cs
│ │ ├── _MoveResultPacket.cs
│ │ ├── _MoveUsedPacket.cs
│ │ ├── _PkmnEXPChangedPacket.cs
│ │ ├── _PkmnEXPEarnedPacket.cs
│ │ ├── _PkmnFaintedPacket.cs
│ │ ├── _PkmnFormChangedPacket.cs
│ │ ├── _PkmnHPChangedPacket.cs
│ │ ├── _PkmnLevelChangedPacket.cs
│ │ ├── _PkmnStatChangedPacket.cs
│ │ ├── _PkmnSwitchInPacket.cs
│ │ ├── _PkmnSwitchOutPacket.cs
│ │ ├── _PsychUpPacket.cs
│ │ ├── _ReflectTypePacket.cs
│ │ ├── _SpecialMessagePacket.cs
│ │ ├── _Status1Packet.cs
│ │ ├── _Status2Packet.cs
│ │ ├── _TeamStatusDamagePacket.cs
│ │ ├── _TeamStatusPacket.cs
│ │ ├── _TransformPacket.cs
│ │ ├── _TypeChangedPacket.cs
│ │ ├── _WeatherDamagePacket.cs
│ │ ├── _WeatherPacket.cs
│ │ ├── _WildPkmnAppearedPacket.cs
│ │ ├── __Packet.cs
│ │ └── __PacketProcessor.cs
│ ├── PokemonBattleEngine.csproj
│ └── Utils/
│ ├── EmptyCollections.cs
│ ├── Random.cs
│ └── Utils.cs
├── PokemonBattleEngine.DefaultData/
│ ├── AI/
│ │ ├── AI.cs
│ │ ├── AIDecisions.cs
│ │ └── WildAI.cs
│ ├── Data/
│ │ ├── BerryData.cs
│ │ ├── BerryData_Data.cs
│ │ ├── EXPTables.cs
│ │ ├── EventPokemon.cs
│ │ ├── EventPokemon_Data.cs
│ │ ├── ItemData.cs
│ │ ├── ItemData_Data.cs
│ │ ├── LegalityChecker.cs
│ │ ├── MoveData.cs
│ │ ├── MoveData_Data.cs
│ │ └── PokemonData.cs
│ ├── DefaultDataProvider.cs
│ ├── Enums.cs
│ ├── IPokemonDataExtended.cs
│ ├── LocalizedString.cs
│ ├── PokemonBattleEngine.DefaultData.csproj
│ └── RandomTeamGenerator.cs
├── PokemonBattleEngine.sln
├── PokemonBattleEngineClient/
│ ├── App.xaml
│ ├── App.xaml.cs
│ ├── Clients/
│ │ ├── ActionsBuilder.cs
│ │ ├── BattleClient.cs
│ │ ├── NetworkClient.cs
│ │ ├── NonLocalClient.cs
│ │ ├── ReplayClient.cs
│ │ ├── SinglePlayerClient.cs
│ │ └── SwitchesBuilder.cs
│ ├── Infrastructure/
│ │ ├── BetterWrapPanel.cs
│ │ ├── Converters.cs
│ │ ├── StringRenderer.cs
│ │ ├── Utils.cs
│ │ └── WriteableBitmapSurface.cs
│ ├── MainWindow.xaml
│ ├── MainWindow.xaml.cs
│ ├── Models/
│ │ ├── MoveInfo.cs
│ │ ├── PokemonInfo.cs
│ │ ├── SwitchInfo.cs
│ │ ├── TargetInfo.cs
│ │ └── TeamInfo.cs
│ ├── PokemonBattleEngineClient.csproj
│ └── Views/
│ ├── ActionsView.xaml
│ ├── ActionsView.xaml.cs
│ ├── BattleView.xaml
│ ├── BattleView.xaml.cs
│ ├── FieldView.xaml
│ ├── FieldView.xaml.cs
│ ├── HPBarView.xaml
│ ├── HPBarView.xaml.cs
│ ├── MainView.xaml
│ ├── MainView.xaml.cs
│ ├── MessageView.xaml
│ ├── MessageView.xaml.cs
│ ├── PokemonView.xaml
│ ├── PokemonView.xaml.cs
│ ├── TeamBuilderView.xaml
│ └── TeamBuilderView.xaml.cs
├── PokemonBattleEngineClient.Android/
│ ├── Assets/
│ │ └── AboutAssets.txt
│ ├── MainActivity.cs
│ ├── PokemonBattleEngineClient.Android.csproj
│ ├── Properties/
│ │ ├── AndroidManifest.xml
│ │ └── AssemblyInfo.cs
│ └── Resources/
│ ├── AboutResources.txt
│ ├── Resource.Designer.cs
│ ├── layout/
│ │ └── Main.axml
│ └── values/
│ └── Strings.xml
├── PokemonBattleEngineClient.Desktop/
│ ├── PokemonBattleEngineClient.Desktop.csproj
│ └── Program.cs
├── PokemonBattleEngineClient.iOS/
│ ├── AppDelegate.cs
│ ├── Entitlements.plist
│ ├── Info.plist
│ ├── Main.cs
│ ├── PokemonBattleEngineClient.iOS.csproj
│ ├── Properties/
│ │ └── AssemblyInfo.cs
│ └── Resources/
│ └── LaunchScreen.xib
├── PokemonBattleEngineDiscord/
│ ├── BattleContext.cs
│ ├── BattleContext_Constants.cs
│ ├── BotCommands.cs
│ ├── ChannelHandler.cs
│ ├── Matchmaking.cs
│ ├── PokemonBattleEngineDiscord.csproj
│ ├── Program.cs
│ ├── ReactionHandler.cs
│ ├── ReplaySaver.cs
│ └── Utils.cs
├── PokemonBattleEngineExtras/
│ ├── AIBattleDemo.cs
│ ├── LocalizationDumper.cs
│ ├── NARCTextDumper.cs
│ ├── PokemonBattleEngineExtras.csproj
│ ├── PokemonDataDumper.cs
│ ├── PokemonDataDumper_Data.cs
│ ├── PokemonDataDumper_DreamWorld.cs
│ ├── Program.cs
│ └── Utils.cs
├── PokemonBattleEngineServer/
│ ├── BattleServer.cs
│ ├── Player.cs
│ ├── PokemonBattleEngineServer.csproj
│ └── Properties/
│ └── launchSettings.json
├── PokemonBattleEngineTests/
│ ├── Abilities/
│ │ ├── AntiStatusAbilityTests.cs
│ │ ├── IllusionTests.cs
│ │ ├── IntimidateTests.cs
│ │ ├── NaturalCureTests.cs
│ │ └── PoisonHealTests.cs
│ ├── ActionsTests.cs
│ ├── AutoCenterTests.cs
│ ├── BattleResultTests.cs
│ ├── BehaviorTests.cs
│ ├── Forms/
│ │ ├── CastformCherrimTests.cs
│ │ └── ShayminTests.cs
│ ├── Items/
│ │ └── GemTests.cs
│ ├── Moves/
│ │ ├── BellyDrumTests.cs
│ │ ├── CamouflageTests.cs
│ │ ├── HelpingHandTests.cs
│ │ ├── MultiStrikeTests.cs
│ │ ├── ProtectionTests.cs
│ │ ├── RoostTests.cs
│ │ ├── SecretPowerTests.cs
│ │ ├── TeleportTests.cs
│ │ └── WhirlwindTests.cs
│ ├── PokemonBattleEngineTests.csproj
│ ├── Statuses/
│ │ ├── ConfusionTests.cs
│ │ ├── PowerTrickTests.cs
│ │ └── SubstituteTests.cs
│ ├── TestUtils.cs
│ └── ThrowTests.cs
├── README.md
├── Shared Assets/
│ └── PKMN/
│ ├── FemaleMinispriteLookup.txt
│ └── FemaleSpriteLookup.txt
├── To Do Abilities.txt
├── To Do Items.txt
├── To Do Moves.txt
└── nuget.config
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text eol=lf
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
PokemonBattleEngineExtras/DumpedData
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
#*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) 2022 Kermalis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: PokemonBattleEngine/Battle/Battle.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using Kermalis.PokemonBattleEngine.Packets;
using Kermalis.PokemonBattleEngine.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Kermalis.PokemonBattleEngine.Battle;
/// Represents a specific Pokémon battle.
public sealed partial class PBEBattle
{
public delegate void BattleStateChangedEvent(PBEBattle battle);
public event BattleStateChangedEvent? OnStateChanged;
private PBEBattleState _battleState;
public PBEBattleState BattleState
{
get => _battleState;
private set
{
if (value != _battleState)
{
_battleState = value;
OnStateChanged?.Invoke(this);
}
}
}
public ushort TurnNumber { get; set; }
public PBEBattleResult? BattleResult { get; set; }
private readonly PBERandom _rand;
public bool IsLocallyHosted { get; }
public PBEBattleType BattleType { get; }
public PBEBattleTerrain BattleTerrain { get; }
public PBEBattleFormat BattleFormat { get; }
public PBESettings Settings { get; }
public PBETeams Teams { get; }
public PBETrainers Trainers { get; }
public List ActiveBattlers { get; } = new(6);
private readonly List _turnOrder;
public PBEWeather Weather { get; set; }
public byte WeatherCounter { get; set; }
public PBEBattleStatus BattleStatus { get; set; }
public byte TrickRoomCount { get; set; }
public List Events { get; } = new();
// Trainer battle
private PBEBattle(PBEBattleFormat battleFormat, PBESettings settings, IReadOnlyList ti0, IReadOnlyList ti1,
PBEBattleTerrain battleTerrain, PBEWeather weather, int? randomSeed)
{
if (battleFormat >= PBEBattleFormat.MAX)
{
throw new ArgumentOutOfRangeException(nameof(battleFormat));
}
if (battleTerrain >= PBEBattleTerrain.MAX)
{
throw new ArgumentOutOfRangeException(nameof(battleTerrain));
}
if (weather >= PBEWeather.MAX)
{
throw new ArgumentOutOfRangeException(nameof(weather));
}
settings.ShouldBeReadOnly(nameof(settings));
_rand = new PBERandom(randomSeed ?? PBEDataProvider.GlobalRandom.RandomInt());
IsLocallyHosted = true;
BattleType = PBEBattleType.Trainer;
BattleTerrain = battleTerrain;
BattleFormat = battleFormat;
Settings = settings;
Weather = weather;
Teams = new PBETeams(this, ti0, ti1, out PBETrainers trainers);
Trainers = trainers;
_turnOrder = new List(6);
QueueUpPokemon();
}
// Wild battle
private PBEBattle(PBEBattleFormat battleFormat, PBESettings settings, IReadOnlyList ti, PBEWildInfo wi,
PBEBattleTerrain battleTerrain, PBEWeather weather, int? randomSeed)
{
if (battleFormat >= PBEBattleFormat.MAX)
{
throw new ArgumentOutOfRangeException(nameof(battleFormat));
}
if (battleTerrain >= PBEBattleTerrain.MAX)
{
throw new ArgumentOutOfRangeException(nameof(battleTerrain));
}
if (weather >= PBEWeather.MAX)
{
throw new ArgumentOutOfRangeException(nameof(weather));
}
settings.ShouldBeReadOnly(nameof(settings));
_rand = new PBERandom(randomSeed ?? PBEDataProvider.GlobalRandom.RandomInt());
IsLocallyHosted = true;
BattleType = PBEBattleType.Wild;
BattleTerrain = battleTerrain;
BattleFormat = battleFormat;
Settings = settings;
Weather = weather;
Teams = new PBETeams(this, ti, wi, out PBETrainers trainers);
Trainers = trainers;
_turnOrder = new List(6);
QueueUpPokemon();
}
// Remote battle
private PBEBattle(PBEBattlePacket packet)
{
IsLocallyHosted = false;
BattleType = packet.BattleType;
BattleFormat = packet.BattleFormat;
BattleTerrain = packet.BattleTerrain;
Weather = packet.Weather;
Settings = packet.Settings;
Teams = new PBETeams(this, packet, out PBETrainers trainers);
Trainers = trainers;
// These two will never be used in a non-local battle
_rand = null!;
_turnOrder = null!;
}
public static PBEBattle CreateTrainerBattle(PBEBattleFormat battleFormat, PBESettings settings, PBETrainerInfo ti0, PBETrainerInfo ti1,
PBEBattleTerrain battleTerrain = PBEBattleTerrain.Plain, PBEWeather weather = PBEWeather.None, int? randomSeed = null)
{
return new PBEBattle(battleFormat, settings, new[] { ti0 }, new[] { ti1 }, battleTerrain, weather, randomSeed);
}
public static PBEBattle CreateTrainerBattle(PBEBattleFormat battleFormat, PBESettings settings, IReadOnlyList ti0, IReadOnlyList ti1,
PBEBattleTerrain battleTerrain = PBEBattleTerrain.Plain, PBEWeather weather = PBEWeather.None, int? randomSeed = null)
{
return new PBEBattle(battleFormat, settings, ti0, ti1, battleTerrain, weather, randomSeed);
}
public static PBEBattle CreateWildBattle(PBEBattleFormat battleFormat, PBESettings settings, PBETrainerInfo ti0, PBEWildInfo wi,
PBEBattleTerrain battleTerrain = PBEBattleTerrain.Plain, PBEWeather weather = PBEWeather.None, int? randomSeed = null)
{
return new PBEBattle(battleFormat, settings, new[] { ti0 }, wi, battleTerrain, weather, randomSeed);
}
public static PBEBattle CreateWildBattle(PBEBattleFormat battleFormat, PBESettings settings, IReadOnlyList ti0, PBEWildInfo wi,
PBEBattleTerrain battleTerrain = PBEBattleTerrain.Plain, PBEWeather weather = PBEWeather.None, int? randomSeed = null)
{
return new PBEBattle(battleFormat, settings, ti0, wi, battleTerrain, weather, randomSeed);
}
public static PBEBattle CreateRemoteBattle(PBEBattlePacket packet)
{
return new PBEBattle(packet);
}
private void QueueUp(PBETeam team, PBEFieldPosition pos, ref PBETrainer? tr, ref int i)
{
// See which trainer owns this spot
PBETrainer t = team.GetTrainer(pos);
// If it's not the previous trainer, we start at their first PKMN
if (tr != t)
{
i = 0;
tr = t;
}
PBEList party = t.Party;
// If the check index is valid, try to send out a non-fainted non-ignore PKMN
tryget:
if (i < party.Count)
{
PBEBattlePokemon p = party[i];
// If we should ignore this PKMN, try to get the one in the next index
if (!p.CanBattle)
{
i++;
goto tryget;
}
// Valid PKMN, send it out
p.Trainer.SwitchInQueue.Add((p, pos));
// Wild PKMN should be out already
if (team.IsWild)
{
p.FieldPosition = pos;
ActiveBattlers.Add(p);
}
// Next slot to check
i++;
}
}
internal void QueueUpPokemon()
{
switch (BattleFormat)
{
case PBEBattleFormat.Single:
{
foreach (PBETeam team in Teams)
{
PBETrainer? t = null;
int i = 0;
QueueUp(team, PBEFieldPosition.Center, ref t, ref i);
}
break;
}
case PBEBattleFormat.Double:
{
foreach (PBETeam team in Teams)
{
PBETrainer? t = null;
int i = 0;
QueueUp(team, PBEFieldPosition.Left, ref t, ref i);
QueueUp(team, PBEFieldPosition.Right, ref t, ref i);
}
break;
}
case PBEBattleFormat.Triple:
{
foreach (PBETeam team in Teams)
{
PBETrainer? t = null;
int i = 0;
QueueUp(team, PBEFieldPosition.Left, ref t, ref i);
QueueUp(team, PBEFieldPosition.Center, ref t, ref i);
QueueUp(team, PBEFieldPosition.Right, ref t, ref i);
}
break;
}
case PBEBattleFormat.Rotation:
{
foreach (PBETeam team in Teams)
{
PBETrainer? t = null;
int i = 0;
QueueUp(team, PBEFieldPosition.Center, ref t, ref i);
QueueUp(team, PBEFieldPosition.Left, ref t, ref i);
QueueUp(team, PBEFieldPosition.Right, ref t, ref i);
}
break;
}
default: throw new ArgumentOutOfRangeException(nameof(BattleFormat));
}
BattleState = PBEBattleState.ReadyToBegin;
}
private void CheckLocal()
{
if (!IsLocallyHosted)
{
throw new InvalidOperationException("This battle is not locally hosted");
}
}
/// Begins the battle.
/// Thrown when is not .
public void Begin()
{
CheckLocal();
if (_battleState != PBEBattleState.ReadyToBegin)
{
throw new InvalidOperationException($"{nameof(BattleState)} must be {PBEBattleState.ReadyToBegin} to begin the battle.");
}
BattleState = PBEBattleState.Processing;
BroadcastBattle(); // The first packet sent is PBEBattlePacket which replays rely on
// Wild Pokémon appearing
if (BattleType == PBEBattleType.Wild)
{
PBETeam team = Teams[1];
PBETrainer trainer = team.Trainers[0];
int count = trainer.SwitchInQueue.Count;
var appearances = new PBEPkmnAppearedInfo[count];
for (int i = 0; i < count; i++)
{
appearances[i] = new PBEPkmnAppearedInfo(trainer.SwitchInQueue[i].Pkmn);
}
trainer.SwitchInQueue.Clear();
BroadcastWildPkmnAppeared(appearances);
}
SwitchesOrActions();
}
/// Runs a turn.
/// Thrown when is not .
public void RunTurn()
{
CheckLocal();
if (_battleState != PBEBattleState.ReadyToRunTurn)
{
throw new InvalidOperationException($"{nameof(BattleState)} must be {PBEBattleState.ReadyToRunTurn} to run a turn.");
}
BattleState = PBEBattleState.Processing;
FleeCheck();
if (EndCheck())
{
return;
}
DetermineTurnOrder();
RunActionsInOrder();
TurnEnded();
}
public void RunSwitches()
{
CheckLocal();
if (_battleState != PBEBattleState.ReadyToRunSwitches)
{
throw new InvalidOperationException($"{nameof(BattleState)} must be {PBEBattleState.ReadyToRunSwitches} to run switches.");
}
BattleState = PBEBattleState.Processing;
FleeCheck();
if (EndCheck())
{
return;
}
SwitchesOrActions();
}
/// Sets to and clears and . Does not touch .
public void SetEnded()
{
if (_battleState != PBEBattleState.Ended)
{
BattleState = PBEBattleState.Ended;
OnNewEvent = null;
OnStateChanged = null;
}
}
private bool EndCheck()
{
if (BattleResult is not null)
{
BroadcastBattleResult(BattleResult.Value);
foreach (PBEBattlePokemon pkmn in ActiveBattlers)
{
pkmn.ApplyNaturalCure(); // Natural Cure happens at the end of the battle. Pokémon should be copied when BattleState is set to "Ended", not upon battle result.
}
SetEnded();
return true;
}
return false;
}
private void SwitchesOrActions()
{
// Checking SwitchInQueue count since SwitchInsRequired is set to 0 after submitting switches
PBETrainer[] trainersWithSwitchIns = Trainers.Where(t => t.SwitchInQueue.Count > 0).ToArray();
if (trainersWithSwitchIns.Length > 0)
{
var list = new List(6);
foreach (PBETrainer trainer in trainersWithSwitchIns)
{
int count = trainer.SwitchInQueue.Count;
var switches = new PBEPkmnAppearedInfo[count];
for (int i = 0; i < count; i++)
{
(PBEBattlePokemon pkmn, PBEFieldPosition pos) = trainer.SwitchInQueue[i];
pkmn.FieldPosition = pos;
switches[i] = CreateSwitchInInfo(pkmn);
PBETrainer.SwitchTwoPokemon(pkmn, pos); // Swap after Illusion
ActiveBattlers.Add(pkmn); // Add before broadcast
list.Add(pkmn);
}
BroadcastPkmnSwitchIn(trainer, switches);
}
DoSwitchInEffects(list);
}
foreach (PBETrainer trainer in Trainers)
{
int available = trainer.NumConsciousPkmn - trainer.NumPkmnOnField;
trainer.SwitchInsRequired = 0;
trainer.SwitchInQueue.Clear();
if (available > 0)
{
switch (BattleFormat)
{
case PBEBattleFormat.Single:
{
if (!trainer.IsSpotOccupied(PBEFieldPosition.Center))
{
trainer.SwitchInsRequired = 1;
}
break;
}
case PBEBattleFormat.Double:
{
if (trainer.OwnsSpot(PBEFieldPosition.Left) && !trainer.IsSpotOccupied(PBEFieldPosition.Left))
{
available--;
trainer.SwitchInsRequired++;
}
if (available > 0 && trainer.OwnsSpot(PBEFieldPosition.Right) && !trainer.IsSpotOccupied(PBEFieldPosition.Right))
{
trainer.SwitchInsRequired++;
}
break;
}
case PBEBattleFormat.Rotation:
case PBEBattleFormat.Triple:
{
if (trainer.OwnsSpot(PBEFieldPosition.Left) && !trainer.IsSpotOccupied(PBEFieldPosition.Left))
{
available--;
trainer.SwitchInsRequired++;
}
if (available > 0 && trainer.OwnsSpot(PBEFieldPosition.Center) && !trainer.IsSpotOccupied(PBEFieldPosition.Center))
{
available--;
trainer.SwitchInsRequired++;
}
if (available > 0 && trainer.OwnsSpot(PBEFieldPosition.Right) && !trainer.IsSpotOccupied(PBEFieldPosition.Right))
{
trainer.SwitchInsRequired++;
}
break;
}
default: throw new ArgumentOutOfRangeException(nameof(BattleFormat));
}
}
}
trainersWithSwitchIns = Trainers.Where(t => t.SwitchInsRequired > 0).ToArray();
if (trainersWithSwitchIns.Length > 0)
{
BattleState = PBEBattleState.WaitingForSwitchIns;
foreach (PBETrainer trainer in trainersWithSwitchIns)
{
BroadcastSwitchInRequest(trainer);
}
}
else
{
if (EndCheck())
{
return;
}
foreach (PBEBattlePokemon pkmn in ActiveBattlers)
{
pkmn.HasUsedMoveThisTurn = false;
pkmn.TurnAction = null;
pkmn.SpeedBoost_AbleToSpeedBoostThisTurn = pkmn.Ability == PBEAbility.SpeedBoost;
if (pkmn.Status2.HasFlag(PBEStatus2.Flinching))
{
BroadcastStatus2(pkmn, pkmn, PBEStatus2.Flinching, PBEStatusAction.Ended);
}
if (pkmn.Status2.HasFlag(PBEStatus2.HelpingHand))
{
BroadcastStatus2(pkmn, pkmn, PBEStatus2.HelpingHand, PBEStatusAction.Ended);
}
if (pkmn.Status2.HasFlag(PBEStatus2.LockOn))
{
if (--pkmn.LockOnTurns == 0)
{
pkmn.LockOnPokemon = null;
BroadcastStatus2(pkmn, pkmn, PBEStatus2.LockOn, PBEStatusAction.Ended);
}
}
if (pkmn.Protection_Used)
{
pkmn.Protection_Counter++;
pkmn.Protection_Used = false;
if (pkmn.Status2.HasFlag(PBEStatus2.Protected))
{
BroadcastStatus2(pkmn, pkmn, PBEStatus2.Protected, PBEStatusAction.Ended);
}
}
else
{
pkmn.Protection_Counter = 0;
}
if (pkmn.Status2.HasFlag(PBEStatus2.Roost))
{
pkmn.EndRoost();
BroadcastStatus2(pkmn, pkmn, PBEStatus2.Roost, PBEStatusAction.Ended);
}
}
foreach (PBETeam team in Teams)
{
if (team.TeamStatus.HasFlag(PBETeamStatus.QuickGuard))
{
BroadcastTeamStatus(team, PBETeamStatus.QuickGuard, PBETeamStatusAction.Ended);
}
if (team.TeamStatus.HasFlag(PBETeamStatus.WideGuard))
{
BroadcastTeamStatus(team, PBETeamStatus.WideGuard, PBETeamStatusAction.Ended);
}
}
foreach (PBETrainer trainer in Trainers)
{
trainer.ActionsRequired.Clear();
trainer.ActionsRequired.AddRange(trainer.ActiveBattlersOrdered);
}
// #318 - We check pkmn on the field instead of conscious pkmn because of multi-battles
// It still works if there's only one trainer on the team since we check for available switch-ins above
if (BattleFormat == PBEBattleFormat.Triple && Teams.All(t => t.NumPkmnOnField == 1))
{
PBEBattlePokemon pkmn0 = ActiveBattlers[0],
pkmn1 = ActiveBattlers[1];
if ((pkmn0.FieldPosition == PBEFieldPosition.Left && pkmn1.FieldPosition == PBEFieldPosition.Left) || (pkmn0.FieldPosition == PBEFieldPosition.Right && pkmn1.FieldPosition == PBEFieldPosition.Right))
{
PBEFieldPosition pkmn0OldPos = pkmn0.FieldPosition,
pkmn1OldPos = pkmn1.FieldPosition;
pkmn0.FieldPosition = PBEFieldPosition.Center;
pkmn1.FieldPosition = PBEFieldPosition.Center;
BroadcastAutoCenter(pkmn0, pkmn0OldPos, pkmn1, pkmn1OldPos);
}
}
TurnNumber++;
BroadcastTurnBegan();
foreach (PBETeam team in Teams)
{
bool old = team.MonFaintedThisTurn; // Fire events in a specific order
team.MonFaintedThisTurn = false;
team.MonFaintedLastTurn = old;
}
BattleState = PBEBattleState.WaitingForActions;
foreach (PBETrainer trainer in Trainers.Where(t => t.NumConsciousPkmn > 0))
{
BroadcastActionsRequest(trainer);
}
}
}
private IEnumerable GetActingOrder(IEnumerable pokemon, bool ignoreItemsThatActivate)
{
var evaluated = new List<(PBEBattlePokemon Pokemon, float Speed)>(); // TODO: Full Incense, Lagging Tail, Stall, Quick Claw
foreach (PBEBattlePokemon pkmn in pokemon)
{
float speed = pkmn.Speed * GetStatChangeModifier(pkmn.SpeedChange, false);
switch (pkmn.Item)
{
case PBEItem.ChoiceScarf:
{
speed *= 1.5f;
break;
}
case PBEItem.MachoBrace:
case PBEItem.PowerAnklet:
case PBEItem.PowerBand:
case PBEItem.PowerBelt:
case PBEItem.PowerBracer:
case PBEItem.PowerLens:
case PBEItem.PowerWeight:
{
speed *= 0.5f;
break;
}
case PBEItem.QuickPowder:
{
if (pkmn.OriginalSpecies == PBESpecies.Ditto && !pkmn.Status2.HasFlag(PBEStatus2.Transformed))
{
speed *= 2.0f;
}
break;
}
}
if (ShouldDoWeatherEffects())
{
if (Weather == PBEWeather.HarshSunlight && pkmn.Ability == PBEAbility.Chlorophyll)
{
speed *= 2.0f;
}
else if (Weather == PBEWeather.Rain && pkmn.Ability == PBEAbility.SwiftSwim)
{
speed *= 2.0f;
}
else if (Weather == PBEWeather.Sandstorm && pkmn.Ability == PBEAbility.SandRush)
{
speed *= 2.0f;
}
}
switch (pkmn.Ability)
{
case PBEAbility.QuickFeet:
{
if (pkmn.Status1 != PBEStatus1.None)
{
speed *= 1.5f;
}
break;
}
case PBEAbility.SlowStart:
{
if (pkmn.SlowStart_HinderTurnsLeft > 0)
{
speed *= 0.5f;
}
break;
}
}
if (pkmn.Ability != PBEAbility.QuickFeet && pkmn.Status1 == PBEStatus1.Paralyzed)
{
speed *= 0.25f;
}
if (pkmn.Team.TeamStatus.HasFlag(PBETeamStatus.Tailwind))
{
speed *= 2.0f;
}
(PBEBattlePokemon Pokemon, float Speed) tup = (pkmn, speed);
if (evaluated.Count == 0)
{
evaluated.Add(tup);
}
else
{
int pkmnTiedWith = evaluated.FindIndex(t => t.Speed == speed);
if (pkmnTiedWith != -1) // Speed tie - randomly go before or after the Pokémon it tied with
{
if (_rand.RandomBool())
{
if (pkmnTiedWith == evaluated.Count - 1)
{
evaluated.Add(tup);
}
else
{
evaluated.Insert(pkmnTiedWith + 1, tup);
}
}
else
{
evaluated.Insert(pkmnTiedWith, tup);
}
}
else
{
int pkmnToGoBefore = evaluated.FindIndex(t => BattleStatus.HasFlag(PBEBattleStatus.TrickRoom) ? t.Speed > speed : t.Speed < speed);
if (pkmnToGoBefore == -1)
{
evaluated.Add(tup);
}
else
{
evaluated.Insert(pkmnToGoBefore, tup);
}
}
}
}
return evaluated.Select(t => t.Pokemon);
}
private void DetermineTurnOrder()
{
static int GetMovePrio(PBEBattlePokemon p)
{
IPBEMoveData mData = PBEDataProvider.Instance.GetMoveData(p.TurnAction!.FightMove);
int priority = mData.Priority;
if (p.Ability == PBEAbility.Prankster && mData.Category == PBEMoveCategory.Status)
{
priority++;
}
return priority;
}
_turnOrder.Clear();
//const int PursuitPriority = +7;
const int SwitchRotatePriority = +6;
const int WildFleePriority = -7;
List pkmnUsingItem = ActiveBattlers.FindAll(p => p.TurnAction?.Decision == PBETurnDecision.Item);
List pkmnSwitchingOut = ActiveBattlers.FindAll(p => p.TurnAction?.Decision == PBETurnDecision.SwitchOut);
List pkmnFighting = ActiveBattlers.FindAll(p => p.TurnAction?.Decision == PBETurnDecision.Fight);
List wildFleeing = ActiveBattlers.FindAll(p => p.TurnAction?.Decision == PBETurnDecision.WildFlee);
// Item use happens first:
_turnOrder.AddRange(GetActingOrder(pkmnUsingItem, true));
// Get move/switch/rotate/wildflee priority sorted
IOrderedEnumerable> prios =
pkmnSwitchingOut.Select(p => (p, SwitchRotatePriority))
.Concat(pkmnFighting.Select(p => (p, GetMovePrio(p)))) // Get move priority
.Concat(wildFleeing.Select(p => (p, WildFleePriority)))
.GroupBy(t => t.Item2, t => t.p)
.OrderByDescending(t => t.Key);
foreach (IGrouping bracket in prios)
{
bool ignoreItemsThatActivate = bracket.Key == SwitchRotatePriority || bracket.Key == WildFleePriority;
_turnOrder.AddRange(GetActingOrder(bracket, ignoreItemsThatActivate));
}
}
private void RunActionsInOrder()
{
foreach (PBEBattlePokemon pkmn in _turnOrder.ToArray()) // Copy the list so a faint or ejection does not cause a collection modified exception
{
if (BattleResult is not null) // Do not broadcast battle result by calling EndCheck() in here; do it in TurnEnded()
{
return;
}
else if (ActiveBattlers.Contains(pkmn))
{
switch (pkmn.TurnAction!.Decision)
{
case PBETurnDecision.Fight:
{
UseMove(pkmn, pkmn.TurnAction.FightMove, pkmn.TurnAction.FightTargets);
break;
}
case PBETurnDecision.Item:
{
UseItem(pkmn, pkmn.TurnAction.UseItem);
break;
}
case PBETurnDecision.SwitchOut:
{
SwitchTwoPokemon(pkmn, pkmn.Trainer.GetPokemon(pkmn.TurnAction.SwitchPokemonId));
break;
}
case PBETurnDecision.WildFlee:
{
WildFleeCheck(pkmn);
break;
}
default: throw new ArgumentOutOfRangeException(nameof(pkmn.TurnAction.Decision));
}
}
}
}
private void TurnEnded()
{
if (EndCheck())
{
return;
}
// Verified: Effects before LightScreen/LuckyChant/Reflect/Safeguard/TrickRoom
DoTurnEndedEffects();
if (EndCheck())
{
return;
}
// Verified: LightScreen/LuckyChant/Reflect/Safeguard/TrickRoom are removed in the order they were added
foreach (PBETeam team in Teams)
{
if (team.TeamStatus.HasFlag(PBETeamStatus.LightScreen))
{
team.LightScreenCount--;
if (team.LightScreenCount == 0)
{
BroadcastTeamStatus(team, PBETeamStatus.LightScreen, PBETeamStatusAction.Ended);
}
}
if (team.TeamStatus.HasFlag(PBETeamStatus.LuckyChant))
{
team.LuckyChantCount--;
if (team.LuckyChantCount == 0)
{
BroadcastTeamStatus(team, PBETeamStatus.LuckyChant, PBETeamStatusAction.Ended);
}
}
if (team.TeamStatus.HasFlag(PBETeamStatus.Reflect))
{
team.ReflectCount--;
if (team.ReflectCount == 0)
{
BroadcastTeamStatus(team, PBETeamStatus.Reflect, PBETeamStatusAction.Ended);
}
}
if (team.TeamStatus.HasFlag(PBETeamStatus.Safeguard))
{
team.SafeguardCount--;
if (team.SafeguardCount == 0)
{
BroadcastTeamStatus(team, PBETeamStatus.Safeguard, PBETeamStatusAction.Ended);
}
}
if (team.TeamStatus.HasFlag(PBETeamStatus.Tailwind))
{
team.TailwindCount--;
if (team.TailwindCount == 0)
{
BroadcastTeamStatus(team, PBETeamStatus.Tailwind, PBETeamStatusAction.Ended);
}
}
}
// Trick Room
if (BattleStatus.HasFlag(PBEBattleStatus.TrickRoom))
{
TrickRoomCount--;
if (TrickRoomCount == 0)
{
BroadcastBattleStatus(PBEBattleStatus.TrickRoom, PBEBattleStatusAction.Ended);
}
}
SwitchesOrActions();
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattleActions.cs
================================================
using Kermalis.EndianBinaryIO;
using Kermalis.PokemonBattleEngine.Data;
using Kermalis.PokemonBattleEngine.Data.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
namespace Kermalis.PokemonBattleEngine.Battle;
public sealed class PBETurnAction
{
public byte PokemonId { get; }
public PBETurnDecision Decision { get; }
public PBEMove FightMove { get; }
public PBETurnTarget FightTargets { get; internal set; } // Internal set because of PBEMoveTarget.RandomFoeSurrounding (TODO: Shouldn't this happen at runtime?)
public PBEItem UseItem { get; }
public byte SwitchPokemonId { get; }
internal PBETurnAction(EndianBinaryReader r)
{
PokemonId = r.ReadByte();
Decision = r.ReadEnum();
switch (Decision)
{
case PBETurnDecision.Fight:
{
FightMove = r.ReadEnum();
FightTargets = r.ReadEnum();
break;
}
case PBETurnDecision.Item:
{
UseItem = r.ReadEnum();
break;
}
case PBETurnDecision.SwitchOut:
{
SwitchPokemonId = r.ReadByte();
break;
}
case PBETurnDecision.WildFlee: break;
default: throw new InvalidDataException(nameof(Decision));
}
}
// Fight
public PBETurnAction(PBEBattlePokemon pokemon, PBEMove fightMove, PBETurnTarget fightTargets)
: this(pokemon.Id, fightMove, fightTargets) { }
public PBETurnAction(byte pokemonId, PBEMove fightMove, PBETurnTarget fightTargets)
{
PokemonId = pokemonId;
Decision = PBETurnDecision.Fight;
FightMove = fightMove;
FightTargets = fightTargets;
}
// Item
public PBETurnAction(PBEBattlePokemon pokemon, PBEItem item)
: this(pokemon.Id, item) { }
public PBETurnAction(byte pokemonId, PBEItem item)
{
PokemonId = pokemonId;
Decision = PBETurnDecision.Item;
UseItem = item;
}
// Switch
public PBETurnAction(PBEBattlePokemon pokemon, PBEBattlePokemon switchPokemon)
: this(pokemon.Id, switchPokemon.Id) { }
public PBETurnAction(byte pokemonId, byte switchPokemonId)
{
PokemonId = pokemonId;
Decision = PBETurnDecision.SwitchOut;
SwitchPokemonId = switchPokemonId;
}
// Internal wild flee
internal PBETurnAction(PBEBattlePokemon pokemon)
: this(pokemon.Id) { }
internal PBETurnAction(byte pokemonId)
{
PokemonId = pokemonId;
Decision = PBETurnDecision.WildFlee;
}
internal void ToBytes(EndianBinaryWriter w)
{
w.WriteByte(PokemonId);
w.WriteEnum(Decision);
switch (Decision)
{
case PBETurnDecision.Fight:
{
w.WriteEnum(FightMove);
w.WriteEnum(FightTargets);
break;
}
case PBETurnDecision.Item:
{
w.WriteEnum(UseItem);
break;
}
case PBETurnDecision.SwitchOut:
{
w.WriteByte(SwitchPokemonId);
break;
}
case PBETurnDecision.WildFlee: break;
default: throw new InvalidDataException(nameof(Decision));
}
}
}
public sealed class PBESwitchIn
{
public byte PokemonId { get; }
public PBEFieldPosition Position { get; }
internal PBESwitchIn(EndianBinaryReader r)
{
PokemonId = r.ReadByte();
Position = r.ReadEnum();
}
public PBESwitchIn(PBEBattlePokemon pokemon, PBEFieldPosition position)
: this(pokemon.Id, position) { }
public PBESwitchIn(byte pokemonId, PBEFieldPosition position)
{
PokemonId = pokemonId;
Position = position;
}
internal void ToBytes(EndianBinaryWriter w)
{
w.WriteByte(PokemonId);
w.WriteEnum(Position);
}
}
public sealed partial class PBEBattle
{
internal static bool AreActionsValid(PBETrainer trainer, IReadOnlyCollection actions, [NotNullWhen(false)] out string? invalidReason)
{
if (trainer.Battle._battleState != PBEBattleState.WaitingForActions)
{
throw new InvalidOperationException($"{nameof(BattleState)} must be {PBEBattleState.WaitingForActions} to validate actions.");
}
if (trainer.ActionsRequired.Count == 0)
{
invalidReason = "Actions were already submitted";
return false;
}
if (actions.Count != trainer.ActionsRequired.Count)
{
invalidReason = $"Invalid amount of actions submitted; required amount is {trainer.ActionsRequired.Count}";
return false;
}
var verified = new List(trainer.ActionsRequired.Count);
var standBy = new List(trainer.ActionsRequired.Count);
var items = new Dictionary(trainer.ActionsRequired.Count);
foreach (PBETurnAction action in actions)
{
if (!trainer.TryGetPokemon(action.PokemonId, out PBEBattlePokemon? pkmn))
{
invalidReason = $"Invalid Pokémon ID ({action.PokemonId})";
return false;
}
if (!trainer.ActionsRequired.Contains(pkmn))
{
invalidReason = $"Pokémon {action.PokemonId} not looking for actions";
return false;
}
if (verified.Contains(pkmn))
{
invalidReason = $"Pokémon {action.PokemonId} was multiple actions";
return false;
}
switch (action.Decision)
{
case PBETurnDecision.Fight:
{
if (Array.IndexOf(pkmn.GetUsableMoves(), action.FightMove) == -1)
{
invalidReason = $"{action.FightMove} is not usable by Pokémon {action.PokemonId}";
return false;
}
if (action.FightMove == pkmn.TempLockedMove && action.FightTargets != pkmn.TempLockedTargets)
{
invalidReason = $"Pokémon {action.PokemonId} must target {pkmn.TempLockedTargets}";
return false;
}
if (!AreTargetsValid(pkmn, action.FightMove, action.FightTargets))
{
invalidReason = $"Invalid move targets for Pokémon {action.PokemonId}'s {action.FightMove}";
return false;
}
break;
}
case PBETurnDecision.Item:
{
if (pkmn.TempLockedMove != PBEMove.None)
{
invalidReason = $"Pokémon {action.PokemonId} must use {pkmn.TempLockedMove}";
return false;
}
if (!trainer.Inventory.TryGetValue(action.UseItem, out PBEBattleInventory.PBEBattleInventorySlot? slot))
{
invalidReason = $"Trainer \"{trainer.Name}\" does not have any {action.UseItem}"; // Handles wild Pokémon
return false;
}
bool used = items.TryGetValue(action.UseItem, out int amtUsed);
if (!used)
{
amtUsed = 0;
}
long newAmt = slot.Quantity - amtUsed;
if (newAmt <= 0)
{
invalidReason = $"Tried to use too many {action.UseItem}";
return false;
}
if (trainer.Battle.BattleType == PBEBattleType.Wild && trainer.Team.OpposingTeam.ActiveBattlers.Count > 1
&& PBEDataUtils.AllBalls.Contains(action.UseItem))
{
invalidReason = $"Cannot throw a ball at multiple wild Pokémon";
return false;
}
amtUsed++;
if (used)
{
items[action.UseItem] = amtUsed;
}
else
{
items.Add(action.UseItem, amtUsed);
}
break;
}
case PBETurnDecision.SwitchOut:
{
if (!pkmn.CanSwitchOut())
{
invalidReason = $"Pokémon {action.PokemonId} cannot switch out";
return false;
}
if (!trainer.TryGetPokemon(action.SwitchPokemonId, out PBEBattlePokemon? switchPkmn))
{
invalidReason = $"Invalid switch Pokémon ID ({action.PokemonId})";
return false;
}
if (switchPkmn.HP == 0)
{
invalidReason = $"Switch Pokémon {action.PokemonId} is fainted";
return false;
}
if (switchPkmn.PBEIgnore)
{
invalidReason = $"Switch Pokémon {action.PokemonId} cannot battle";
return false;
}
if (switchPkmn.FieldPosition != PBEFieldPosition.None)
{
invalidReason = $"Switch Pokémon {action.PokemonId} is already on the field";
return false;
}
if (standBy.Contains(switchPkmn))
{
invalidReason = $"Switch Pokémon {action.PokemonId} was asked to be switched in multiple times";
return false;
}
standBy.Add(switchPkmn);
break;
}
default:
{
invalidReason = $"Invalid turn decision ({action.Decision})";
return false;
}
}
verified.Add(pkmn);
}
invalidReason = null;
return true;
}
internal static bool SelectActionsIfValid(PBETrainer trainer, IReadOnlyCollection actions, [NotNullWhen(false)] out string? invalidReason)
{
if (!AreActionsValid(trainer, actions, out invalidReason))
{
return false;
}
trainer.ActionsRequired.Clear();
foreach (PBETurnAction action in actions)
{
PBEBattlePokemon pkmn = trainer.GetPokemon(action.PokemonId);
if (action.Decision == PBETurnDecision.Fight && pkmn.GetMoveTargets(action.FightMove) == PBEMoveTarget.RandomFoeSurrounding)
{
switch (trainer.Battle.BattleFormat)
{
case PBEBattleFormat.Single:
case PBEBattleFormat.Rotation:
{
action.FightTargets = PBETurnTarget.FoeCenter;
break;
}
case PBEBattleFormat.Double:
{
action.FightTargets = trainer.Battle._rand.RandomBool() ? PBETurnTarget.FoeLeft : PBETurnTarget.FoeRight;
break;
}
case PBEBattleFormat.Triple:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
action.FightTargets = trainer.Battle._rand.RandomBool() ? PBETurnTarget.FoeCenter : PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
PBETeam oppTeam = trainer.Team.OpposingTeam;
int r; // Keep randomly picking until a non-fainted foe is selected
roll:
r = trainer.Battle._rand.RandomInt(0, 2);
if (r == 0)
{
if (oppTeam.IsSpotOccupied(PBEFieldPosition.Left))
{
action.FightTargets = PBETurnTarget.FoeLeft;
}
else
{
goto roll;
}
}
else if (r == 1)
{
if (oppTeam.IsSpotOccupied(PBEFieldPosition.Center))
{
action.FightTargets = PBETurnTarget.FoeCenter;
}
else
{
goto roll;
}
}
else
{
if (oppTeam.IsSpotOccupied(PBEFieldPosition.Right))
{
action.FightTargets = PBETurnTarget.FoeRight;
}
else
{
goto roll;
}
}
}
else
{
action.FightTargets = trainer.Battle._rand.RandomBool() ? PBETurnTarget.FoeLeft : PBETurnTarget.FoeCenter;
}
break;
}
default: throw new InvalidDataException(nameof(trainer.Battle.BattleFormat));
}
}
pkmn.TurnAction = action;
}
if (trainer.Battle.Trainers.All(t => t.ActionsRequired.Count == 0))
{
trainer.Battle.BattleState = PBEBattleState.ReadyToRunTurn;
}
return true;
}
internal static bool AreSwitchesValid(PBETrainer trainer, IReadOnlyCollection switches, [NotNullWhen(false)] out string? invalidReason)
{
if (trainer.Battle._battleState != PBEBattleState.WaitingForSwitchIns)
{
throw new InvalidOperationException($"{nameof(BattleState)} must be {PBEBattleState.WaitingForSwitchIns} to validate switches.");
}
if (trainer.SwitchInsRequired == 0)
{
invalidReason = "Switches were already submitted";
return false;
}
if (switches.Count != trainer.SwitchInsRequired)
{
invalidReason = $"Invalid amount of switches submitted; required amount is {trainer.SwitchInsRequired}";
return false;
}
var verified = new List(trainer.SwitchInsRequired);
foreach (PBESwitchIn s in switches)
{
if (s.Position == PBEFieldPosition.None || s.Position >= PBEFieldPosition.MAX || !trainer.OwnsSpot(s.Position))
{
invalidReason = $"Invalid position ({s.PokemonId})";
return false;
}
if (!trainer.TryGetPokemon(s.PokemonId, out PBEBattlePokemon? pkmn))
{
invalidReason = $"Invalid Pokémon ID ({s.PokemonId})";
return false;
}
if (pkmn.HP == 0)
{
invalidReason = $"Pokémon {s.PokemonId} is fainted";
return false;
}
if (pkmn.PBEIgnore)
{
invalidReason = $"Pokémon {s.PokemonId} cannot battle";
return false;
}
if (pkmn.FieldPosition != PBEFieldPosition.None)
{
invalidReason = $"Pokémon {s.PokemonId} is already on the field";
return false;
}
if (verified.Contains(pkmn))
{
invalidReason = $"Pokémon {s.PokemonId} was asked to be switched in multiple times";
return false;
}
verified.Add(pkmn);
}
invalidReason = null;
return true;
}
internal static bool SelectSwitchesIfValid(PBETrainer trainer, IReadOnlyCollection switches, [NotNullWhen(false)] out string? invalidReason)
{
if (!AreSwitchesValid(trainer, switches, out invalidReason))
{
return false;
}
trainer.SwitchInsRequired = 0;
foreach (PBESwitchIn s in switches)
{
trainer.SwitchInQueue.Add((trainer.GetPokemon(s.PokemonId), s.Position));
}
if (trainer.Battle.Trainers.All(t => t.SwitchInsRequired == 0))
{
trainer.Battle.BattleState = PBEBattleState.ReadyToRunSwitches;
}
return true;
}
internal static bool IsFleeValid(PBETrainer trainer, [NotNullWhen(false)] out string? invalidReason)
{
if (trainer.Battle.BattleType != PBEBattleType.Wild)
{
throw new InvalidOperationException($"{nameof(BattleType)} must be {PBEBattleType.Wild} to flee.");
}
switch (trainer.Battle._battleState)
{
case PBEBattleState.WaitingForActions:
{
if (trainer.ActionsRequired.Count == 0)
{
invalidReason = "Actions were already submitted";
return false;
}
PBEBattlePokemon pkmn = trainer.ActiveBattlersOrdered.First();
if (pkmn.TempLockedMove != PBEMove.None)
{
invalidReason = $"Pokémon {pkmn.Id} must use {pkmn.TempLockedMove}";
return false;
}
break;
}
case PBEBattleState.WaitingForSwitchIns:
{
if (trainer.SwitchInsRequired == 0)
{
invalidReason = "Switches were already submitted";
return false;
}
break;
}
default: throw new InvalidOperationException($"{nameof(BattleState)} must be {PBEBattleState.WaitingForActions} or {PBEBattleState.WaitingForSwitchIns} to flee.");
}
invalidReason = null;
return true;
}
internal static bool SelectFleeIfValid(PBETrainer trainer, [NotNullWhen(false)] out string? invalidReason)
{
if (!IsFleeValid(trainer, out invalidReason))
{
return false;
}
trainer.RequestedFlee = true;
if (trainer.Battle._battleState == PBEBattleState.WaitingForActions)
{
trainer.ActionsRequired.Clear();
if (trainer.Battle.Trainers.All(t => t.ActionsRequired.Count == 0))
{
trainer.Battle.BattleState = PBEBattleState.ReadyToRunTurn;
}
}
else // WaitingForSwitches
{
trainer.SwitchInsRequired = 0;
if (trainer.Battle.Trainers.All(t => t.SwitchInsRequired == 0))
{
trainer.Battle.BattleState = PBEBattleState.ReadyToRunSwitches;
}
}
return true;
}
}
public sealed partial class PBETrainer
{
public bool AreActionsValid([NotNullWhen(false)] out string? invalidReason, params PBETurnAction[] actions)
{
return PBEBattle.AreActionsValid(this, actions, out invalidReason);
}
public bool AreActionsValid(IReadOnlyCollection actions, [NotNullWhen(false)] out string? invalidReason)
{
return PBEBattle.AreActionsValid(this, actions, out invalidReason);
}
public bool SelectActionsIfValid([NotNullWhen(false)] out string? invalidReason, params PBETurnAction[] actions)
{
return PBEBattle.SelectActionsIfValid(this, actions, out invalidReason);
}
public bool SelectActionsIfValid(IReadOnlyCollection actions, [NotNullWhen(false)] out string? invalidReason)
{
return PBEBattle.SelectActionsIfValid(this, actions, out invalidReason);
}
public bool AreSwitchesValid([NotNullWhen(false)] out string? invalidReason, params PBESwitchIn[] switches)
{
return PBEBattle.AreSwitchesValid(this, switches, out invalidReason);
}
public bool AreSwitchesValid(IReadOnlyCollection switches, [NotNullWhen(false)] out string? invalidReason)
{
return PBEBattle.AreSwitchesValid(this, switches, out invalidReason);
}
public bool SelectSwitchesIfValid([NotNullWhen(false)] out string? invalidReason, params PBESwitchIn[] switches)
{
return PBEBattle.SelectSwitchesIfValid(this, switches, out invalidReason);
}
public bool SelectSwitchesIfValid(IReadOnlyCollection switches, [NotNullWhen(false)] out string? invalidReason)
{
return PBEBattle.SelectSwitchesIfValid(this, switches, out invalidReason);
}
public bool IsFleeValid([NotNullWhen(false)] out string? invalidReason)
{
return PBEBattle.IsFleeValid(this, out invalidReason);
}
public bool SelectFleeIfValid([NotNullWhen(false)] out string? invalidReason)
{
return PBEBattle.SelectFleeIfValid(this, out invalidReason);
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattleDamage.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using System;
using System.Linq;
namespace Kermalis.PokemonBattleEngine.Battle;
public sealed partial class PBEBattle
{
/// Gets the influence a stat change has on a stat.
/// The stat change.
/// True if the stat is or .
public static float GetStatChangeModifier(sbyte change, bool forMissing)
{
float baseVal = forMissing ? 3 : 2;
float numerator = Math.Max(baseVal, baseVal + change);
float denominator = Math.Max(baseVal, baseVal - change);
return numerator / denominator;
}
// Verified: Sturdy and Substitute only activate on damaging attacks (so draining HP or liquid ooze etc can bypass sturdy)
private ushort DealDamage(PBEBattlePokemon culprit, PBEBattlePokemon victim, int hp, bool ignoreSubstitute = true, bool ignoreSturdy = true)
{
if (hp < 1)
{
hp = 1;
}
if (!ignoreSubstitute && victim.Status2.HasFlag(PBEStatus2.Substitute))
{
ushort oldSubHP = victim.SubstituteHP;
victim.SubstituteHP = (ushort)Math.Max(0, victim.SubstituteHP - hp);
ushort damageAmt = (ushort)(oldSubHP - victim.SubstituteHP);
BroadcastStatus2(victim, culprit, PBEStatus2.Substitute, PBEStatusAction.Damage);
return damageAmt;
}
ushort oldHP = victim.HP;
float oldPercentage = victim.HPPercentage;
victim.HP = (ushort)Math.Max(0, victim.HP - hp);
bool sturdyHappened = false, focusBandHappened = false, focusSashHappened = false;
if (!ignoreSturdy && victim.HP == 0)
{
// TODO: Endure
if (oldHP == victim.MaxHP && victim.Ability == PBEAbility.Sturdy && !culprit.HasCancellingAbility())
{
sturdyHappened = true;
victim.HP = 1;
}
else if (victim.Item == PBEItem.FocusBand && _rand.RandomBool(10, 100))
{
focusBandHappened = true;
victim.HP = 1;
}
else if (oldHP == victim.MaxHP && victim.Item == PBEItem.FocusSash)
{
focusSashHappened = true;
victim.HP = 1;
}
}
victim.UpdateHPPercentage();
BroadcastPkmnHPChanged(victim, oldHP, oldPercentage);
if (sturdyHappened)
{
BroadcastAbility(victim, culprit, PBEAbility.Sturdy, PBEAbilityAction.Damage);
BroadcastEndure(victim);
}
else if (focusBandHappened)
{
BroadcastItem(victim, culprit, PBEItem.FocusBand, PBEItemAction.Damage);
}
else if (focusSashHappened)
{
BroadcastItem(victim, culprit, PBEItem.FocusSash, PBEItemAction.Consumed);
}
return (ushort)(oldHP - victim.HP);
}
/// Restores HP to and broadcasts the HP changing if it changes.
/// The Pokémon receiving the HP.
/// The amount of HP will try to gain.
/// The amount of HP restored.
private ushort HealDamage(PBEBattlePokemon pkmn, int hp)
{
if (hp < 1)
{
hp = 1;
}
ushort oldHP = pkmn.HP;
float oldPercentage = pkmn.HPPercentage;
pkmn.HP = (ushort)Math.Min(pkmn.MaxHP, pkmn.HP + hp); // Always try to heal at least 1 HP
ushort healAmt = (ushort)(pkmn.HP - oldHP);
if (healAmt > 0)
{
pkmn.UpdateHPPercentage();
BroadcastPkmnHPChanged(pkmn, oldHP, oldPercentage);
}
return healAmt;
}
private float CalculateBasePower(PBEBattlePokemon user, PBEBattlePokemon[] targets, IPBEMoveData mData, PBEType moveType)
{
float basePower;
#region Get move's base power
switch (mData.Effect)
{
case PBEMoveEffect.CrushGrip:
{
basePower = Math.Max(1, targets.Select(t => (float)mData.Power * t.HP / t.MaxHP).Average());
break;
}
case PBEMoveEffect.Eruption:
{
basePower = Math.Max(1, mData.Power * user.HP / user.MaxHP);
break;
}
case PBEMoveEffect.Flail:
{
int val = 48 * user.HP / user.MaxHP;
if (val < 2)
{
basePower = 200;
}
else if (val < 4)
{
basePower = 150;
}
else if (val < 8)
{
basePower = 100;
}
else if (val < 16)
{
basePower = 80;
}
else if (val < 32)
{
basePower = 40;
}
else
{
basePower = 20;
}
break;
}
case PBEMoveEffect.Frustration:
{
basePower = Math.Max(1, (byte.MaxValue - user.Friendship) / 2.5f);
break;
}
case PBEMoveEffect.GrassKnot:
{
basePower = targets.Select(t =>
{
if (t.Weight >= 200.0f)
{
return 120f;
}
else if (t.Weight >= 100.0f)
{
return 100f;
}
else if (t.Weight >= 50.0f)
{
return 80f;
}
else if (t.Weight >= 25.0f)
{
return 60f;
}
else if (t.Weight >= 10.0f)
{
return 40f;
}
return 20f;
}).Average();
break;
}
case PBEMoveEffect.HeatCrash:
{
basePower = targets.Select(t =>
{
float relative = user.Weight / t.Weight;
if (relative < 2)
{
return 40f;
}
else if (relative < 3)
{
return 60f;
}
else if (relative < 4)
{
return 80f;
}
else if (relative < 5)
{
return 100f;
}
return 120f;
}).Average();
break;
}
case PBEMoveEffect.HiddenPower:
{
basePower = user.IndividualValues!.GetHiddenPowerBasePower(Settings);
break;
}
case PBEMoveEffect.Magnitude:
{
int val = _rand.RandomInt(0, 99);
byte magnitude;
if (val < 5) // Magnitude 4 - 5%
{
magnitude = 4;
basePower = 10;
}
else if (val < 15) // Magnitude 5 - 10%
{
magnitude = 5;
basePower = 30;
}
else if (val < 35) // Magnitude 6 - 20%
{
magnitude = 6;
basePower = 50;
}
else if (val < 65) // Magnitude 7 - 30%
{
magnitude = 7;
basePower = 70;
}
else if (val < 85) // Magnitude 8 - 20%
{
magnitude = 8;
basePower = 90;
}
else if (val < 95) // Magnitude 9 - 10%
{
magnitude = 9;
basePower = 110;
}
else // Magnitude 10 - 5%
{
magnitude = 10;
basePower = 150;
}
BroadcastMagnitude(magnitude);
break;
}
case PBEMoveEffect.Punishment:
{
basePower = Math.Max(1, Math.Min(200, targets.Select(t => mData.Power + (20f * t.GetPositiveStatTotal())).Average()));
break;
}
case PBEMoveEffect.Return:
{
basePower = Math.Max(1, user.Friendship / 2.5f);
break;
}
case PBEMoveEffect.StoredPower:
{
basePower = mData.Power + (20 * user.GetPositiveStatTotal());
break;
}
default:
{
basePower = Math.Max(1, (int)mData.Power);
break;
}
}
#endregion
// Technician goes before any other power boosts
if (user.Ability == PBEAbility.Technician && basePower <= 60)
{
basePower *= 1.5f;
}
#region Item-specific power boosts
switch (moveType)
{
case PBEType.Bug:
{
switch (user.Item)
{
case PBEItem.InsectPlate:
case PBEItem.SilverPowder:
{
basePower *= 1.2f;
break;
}
case PBEItem.BugGem:
{
BroadcastItem(user, user, PBEItem.BugGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Dark:
{
switch (user.Item)
{
case PBEItem.BlackGlasses:
case PBEItem.DreadPlate:
{
basePower *= 1.2f;
break;
}
case PBEItem.DarkGem:
{
BroadcastItem(user, user, PBEItem.DarkGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Dragon:
{
switch (user.Item)
{
case PBEItem.AdamantOrb:
{
if (user.OriginalSpecies == PBESpecies.Dialga)
{
basePower *= 1.2f;
}
break;
}
case PBEItem.DracoPlate:
case PBEItem.DragonFang:
{
basePower *= 1.2f;
break;
}
case PBEItem.GriseousOrb:
{
if (user.OriginalSpecies == PBESpecies.Giratina && user.RevertForm == PBEForm.Giratina_Origin)
{
basePower *= 1.2f;
}
break;
}
case PBEItem.LustrousOrb:
{
if (user.OriginalSpecies == PBESpecies.Palkia)
{
basePower *= 1.2f;
}
break;
}
case PBEItem.DragonGem:
{
BroadcastItem(user, user, PBEItem.DragonGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Electric:
{
switch (user.Item)
{
case PBEItem.Magnet:
case PBEItem.ZapPlate:
{
basePower *= 1.2f;
break;
}
case PBEItem.ElectricGem:
{
BroadcastItem(user, user, PBEItem.ElectricGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Fighting:
{
switch (user.Item)
{
case PBEItem.BlackBelt:
case PBEItem.FistPlate:
{
basePower *= 1.2f;
break;
}
case PBEItem.FightingGem:
{
BroadcastItem(user, user, PBEItem.FightingGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Fire:
{
switch (user.Item)
{
case PBEItem.Charcoal:
case PBEItem.FlamePlate:
{
basePower *= 1.2f;
break;
}
case PBEItem.FireGem:
{
BroadcastItem(user, user, PBEItem.FireGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Flying:
{
switch (user.Item)
{
case PBEItem.SharpBeak:
case PBEItem.SkyPlate:
{
basePower *= 1.2f;
break;
}
case PBEItem.FlyingGem:
{
BroadcastItem(user, user, PBEItem.FlyingGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Ghost:
{
switch (user.Item)
{
case PBEItem.GriseousOrb:
{
if (user.OriginalSpecies == PBESpecies.Giratina && user.RevertForm == PBEForm.Giratina_Origin)
{
basePower *= 1.2f;
}
break;
}
case PBEItem.SpellTag:
case PBEItem.SpookyPlate:
{
basePower *= 1.2f;
break;
}
case PBEItem.GhostGem:
{
BroadcastItem(user, user, PBEItem.GhostGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Grass:
{
switch (user.Item)
{
case PBEItem.MeadowPlate:
case PBEItem.MiracleSeed:
case PBEItem.RoseIncense:
{
basePower *= 1.2f;
break;
}
case PBEItem.GrassGem:
{
BroadcastItem(user, user, PBEItem.GrassGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Ground:
{
switch (user.Item)
{
case PBEItem.EarthPlate:
case PBEItem.SoftSand:
{
basePower *= 1.2f;
break;
}
case PBEItem.GroundGem:
{
BroadcastItem(user, user, PBEItem.GroundGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Ice:
{
switch (user.Item)
{
case PBEItem.IciclePlate:
case PBEItem.NeverMeltIce:
{
basePower *= 1.2f;
break;
}
case PBEItem.IceGem:
{
BroadcastItem(user, user, PBEItem.IceGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.None:
{
break;
}
case PBEType.Normal:
{
switch (user.Item)
{
case PBEItem.SilkScarf:
{
basePower *= 1.2f;
break;
}
case PBEItem.NormalGem:
{
BroadcastItem(user, user, PBEItem.NormalGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Poison:
{
switch (user.Item)
{
case PBEItem.PoisonBarb:
case PBEItem.ToxicPlate:
{
basePower *= 1.2f;
break;
}
case PBEItem.PoisonGem:
{
BroadcastItem(user, user, PBEItem.PoisonGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Psychic:
{
switch (user.Item)
{
case PBEItem.MindPlate:
case PBEItem.OddIncense:
case PBEItem.TwistedSpoon:
{
basePower *= 1.2f;
break;
}
case PBEItem.PsychicGem:
{
BroadcastItem(user, user, PBEItem.PsychicGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Rock:
{
switch (user.Item)
{
case PBEItem.HardStone:
case PBEItem.RockIncense:
case PBEItem.StonePlate:
{
basePower *= 1.2f;
break;
}
case PBEItem.RockGem:
{
BroadcastItem(user, user, PBEItem.RockGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Steel:
{
switch (user.Item)
{
case PBEItem.AdamantOrb:
{
if (user.OriginalSpecies == PBESpecies.Dialga)
{
basePower *= 1.2f;
}
break;
}
case PBEItem.IronPlate:
case PBEItem.MetalCoat:
{
basePower *= 1.2f;
break;
}
case PBEItem.SteelGem:
{
BroadcastItem(user, user, PBEItem.SteelGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
case PBEType.Water:
{
switch (user.Item)
{
case PBEItem.LustrousOrb:
{
if (user.OriginalSpecies == PBESpecies.Palkia)
{
basePower *= 1.2f;
}
break;
}
case PBEItem.MysticWater:
case PBEItem.SeaIncense:
case PBEItem.SplashPlate:
case PBEItem.WaveIncense:
{
basePower *= 1.2f;
break;
}
case PBEItem.WaterGem:
{
BroadcastItem(user, user, PBEItem.WaterGem, PBEItemAction.Consumed);
basePower *= 1.5f;
break;
}
}
break;
}
default: throw new ArgumentOutOfRangeException(nameof(moveType));
}
#endregion
#region Move-specific power boosts
switch (mData.Effect)
{
case PBEMoveEffect.Acrobatics:
{
if (user.Item == PBEItem.None)
{
basePower *= 2.0f;
}
break;
}
case PBEMoveEffect.Brine:
{
if (Array.FindIndex(targets, t => t.HP <= t.HP / 2) != -1)
{
basePower *= 2.0f;
}
break;
}
case PBEMoveEffect.Facade:
{
if (user.Status1 == PBEStatus1.Burned || user.Status1 == PBEStatus1.Paralyzed || user.Status1 == PBEStatus1.Poisoned || user.Status1 == PBEStatus1.BadlyPoisoned)
{
basePower *= 2.0f;
}
break;
}
case PBEMoveEffect.Hex:
{
if (Array.FindIndex(targets, t => t.Status1 != PBEStatus1.None) != -1)
{
basePower *= 2.0f;
}
break;
}
case PBEMoveEffect.Payback:
{
if (Array.FindIndex(targets, t => t.HasUsedMoveThisTurn) != -1)
{
basePower *= 2.0f;
}
break;
}
case PBEMoveEffect.Retaliate:
{
if (user.Team.MonFaintedLastTurn)
{
basePower *= 2.0f;
}
break;
}
case PBEMoveEffect.SmellingSalt:
{
if (Array.FindIndex(targets, t => t.Status1 == PBEStatus1.Paralyzed) != -1)
{
basePower *= 2.0f;
}
break;
}
case PBEMoveEffect.Venoshock:
{
if (Array.FindIndex(targets, t => t.Status1 == PBEStatus1.Poisoned || t.Status1 == PBEStatus1.BadlyPoisoned) != -1)
{
basePower *= 2.0f;
}
break;
}
case PBEMoveEffect.WakeUpSlap:
{
if (Array.FindIndex(targets, t => t.Status1 == PBEStatus1.Asleep) != -1)
{
basePower *= 2.0f;
}
break;
}
case PBEMoveEffect.WeatherBall:
{
if (ShouldDoWeatherEffects() && Weather != PBEWeather.None)
{
basePower *= 2.0f;
}
break;
}
}
#endregion
#region Weather-specific power boosts
if (ShouldDoWeatherEffects())
{
switch (Weather)
{
case PBEWeather.HarshSunlight:
{
if (moveType == PBEType.Fire)
{
basePower *= 1.5f;
}
else if (moveType == PBEType.Water)
{
basePower *= 0.5f;
}
break;
}
case PBEWeather.Rain:
{
if (moveType == PBEType.Water)
{
basePower *= 1.5f;
}
else if (moveType == PBEType.Fire)
{
basePower *= 0.5f;
}
break;
}
case PBEWeather.Sandstorm:
{
if (user.Ability == PBEAbility.SandForce && (moveType == PBEType.Rock || moveType == PBEType.Ground || moveType == PBEType.Steel))
{
basePower *= 1.3f;
}
break;
}
}
}
#endregion
#region Other power boosts
if (user.Status2.HasFlag(PBEStatus2.HelpingHand))
{
basePower *= 1.5f;
}
if (user.Ability == PBEAbility.FlareBoost && mData.Category == PBEMoveCategory.Special && user.Status1 == PBEStatus1.Burned)
{
basePower *= 1.5f;
}
if (user.Ability == PBEAbility.ToxicBoost && mData.Category == PBEMoveCategory.Physical && (user.Status1 == PBEStatus1.Poisoned || user.Status1 == PBEStatus1.BadlyPoisoned))
{
basePower *= 1.5f;
}
if (user.Item == PBEItem.LifeOrb)
{
basePower *= 1.3f;
}
if (user.Ability == PBEAbility.IronFist && mData.Flags.HasFlag(PBEMoveFlag.AffectedByIronFist))
{
basePower *= 1.2f;
}
if (user.Ability == PBEAbility.Reckless && mData.Flags.HasFlag(PBEMoveFlag.AffectedByReckless))
{
basePower *= 1.2f;
}
if (user.Item == PBEItem.MuscleBand && mData.Category == PBEMoveCategory.Physical)
{
basePower *= 1.1f;
}
if (user.Item == PBEItem.WiseGlasses && mData.Category == PBEMoveCategory.Special)
{
basePower *= 1.1f;
}
#endregion
return basePower;
}
private float CalculateDamageMultiplier(PBEBattlePokemon user, PBEBattlePokemon target, IPBEMoveData mData, PBEType moveType, PBEResult moveResult, bool criticalHit)
{
float damageMultiplier = 1;
if (target.Status2.HasFlag(PBEStatus2.Airborne) && mData.Flags.HasFlag(PBEMoveFlag.DoubleDamageAirborne))
{
damageMultiplier *= 2.0f;
}
if (target.Minimize_Used && mData.Flags.HasFlag(PBEMoveFlag.DoubleDamageMinimized))
{
damageMultiplier *= 2.0f;
}
if (target.Status2.HasFlag(PBEStatus2.Underground) && mData.Flags.HasFlag(PBEMoveFlag.DoubleDamageUnderground))
{
damageMultiplier *= 2.0f;
}
if (target.Status2.HasFlag(PBEStatus2.Underwater) && mData.Flags.HasFlag(PBEMoveFlag.DoubleDamageUnderwater))
{
damageMultiplier *= 2.0f;
}
if (criticalHit)
{
damageMultiplier *= Settings.CritMultiplier;
if (user.Ability == PBEAbility.Sniper)
{
damageMultiplier *= 1.5f;
}
}
else if (user.Ability != PBEAbility.Infiltrator)
{
if ((target.Team.TeamStatus.HasFlag(PBETeamStatus.Reflect) && mData.Category == PBEMoveCategory.Physical)
|| (target.Team.TeamStatus.HasFlag(PBETeamStatus.LightScreen) && mData.Category == PBEMoveCategory.Special))
{
if (target.Team.NumPkmnOnField == 1)
{
damageMultiplier *= 0.5f;
}
else
{
damageMultiplier *= 0.66f;
}
}
}
switch (moveResult)
{
case PBEResult.NotVeryEffective_Type:
{
if (user.Ability == PBEAbility.TintedLens)
{
damageMultiplier *= 2.0f;
}
break;
}
case PBEResult.SuperEffective_Type:
{
if ((target.Ability == PBEAbility.Filter || target.Ability == PBEAbility.SolidRock) && !user.HasCancellingAbility())
{
damageMultiplier *= 0.75f;
}
if (user.Item == PBEItem.ExpertBelt)
{
damageMultiplier *= 1.2f;
}
break;
}
}
if (user.ReceivesSTAB(moveType))
{
if (user.Ability == PBEAbility.Adaptability)
{
damageMultiplier *= 2.0f;
}
else
{
damageMultiplier *= 1.5f;
}
}
if (mData.Category == PBEMoveCategory.Physical && user.Status1 == PBEStatus1.Burned && user.Ability != PBEAbility.Guts)
{
damageMultiplier *= 0.5f;
}
if (moveType == PBEType.Fire && target.Ability == PBEAbility.Heatproof && !user.HasCancellingAbility())
{
damageMultiplier *= 0.5f;
}
return damageMultiplier;
}
private float CalculateAttack(PBEBattlePokemon user, PBEBattlePokemon target, PBEType moveType, float initialAttack)
{
float attack = initialAttack;
if (user.Ability == PBEAbility.HugePower || user.Ability == PBEAbility.PurePower)
{
attack *= 2.0f;
}
if (user.Item == PBEItem.ThickClub && (user.OriginalSpecies == PBESpecies.Cubone || user.OriginalSpecies == PBESpecies.Marowak))
{
attack *= 2.0f;
}
if (user.Item == PBEItem.LightBall && user.OriginalSpecies == PBESpecies.Pikachu)
{
attack *= 2.0f;
}
if (moveType == PBEType.Bug && user.Ability == PBEAbility.Swarm && user.HP <= user.MaxHP / 3)
{
attack *= 1.5f;
}
if (moveType == PBEType.Fire && user.Ability == PBEAbility.Blaze && user.HP <= user.MaxHP / 3)
{
attack *= 1.5f;
}
if (moveType == PBEType.Grass && user.Ability == PBEAbility.Overgrow && user.HP <= user.MaxHP / 3)
{
attack *= 1.5f;
}
if (moveType == PBEType.Water && user.Ability == PBEAbility.Torrent && user.HP <= user.MaxHP / 3)
{
attack *= 1.5f;
}
if (user.Ability == PBEAbility.Hustle)
{
attack *= 1.5f;
}
if (user.Ability == PBEAbility.Guts && user.Status1 != PBEStatus1.None)
{
attack *= 1.5f;
}
if (user.Item == PBEItem.ChoiceBand)
{
attack *= 1.5f;
}
if (!user.HasCancellingAbility() && ShouldDoWeatherEffects() && Weather == PBEWeather.HarshSunlight && user.Team.ActiveBattlers.FindIndex(p => p.Ability == PBEAbility.FlowerGift) != -1)
{
attack *= 1.5f;
}
if ((moveType == PBEType.Fire || moveType == PBEType.Ice) && target.Ability == PBEAbility.ThickFat && !user.HasCancellingAbility())
{
attack *= 0.5f;
}
if (user.Ability == PBEAbility.Defeatist && user.HP <= user.MaxHP / 2)
{
attack *= 0.5f;
}
if (user.Ability == PBEAbility.SlowStart && user.SlowStart_HinderTurnsLeft > 0)
{
attack *= 0.5f;
}
return attack;
}
private static float CalculateDefense(PBEBattlePokemon user, PBEBattlePokemon target, float initialDefense)
{
float defense = initialDefense;
if (target.Item == PBEItem.MetalPowder && target.OriginalSpecies == PBESpecies.Ditto && !target.Status2.HasFlag(PBEStatus2.Transformed))
{
defense *= 2.0f;
}
if (target.Ability == PBEAbility.MarvelScale && target.Status1 != PBEStatus1.None && !user.HasCancellingAbility())
{
defense *= 1.5f;
}
if (target.Item == PBEItem.Eviolite && PBEDataProvider.Instance.HasEvolutions(target.OriginalSpecies, target.RevertForm))
{
defense *= 1.5f;
}
return defense;
}
private float CalculateSpAttack(PBEBattlePokemon user, PBEBattlePokemon target, PBEType moveType, float initialSpAttack)
{
float spAttack = initialSpAttack;
if (user.Item == PBEItem.DeepSeaTooth && user.OriginalSpecies == PBESpecies.Clamperl)
{
spAttack *= 2.0f;
}
if (user.Item == PBEItem.LightBall && user.OriginalSpecies == PBESpecies.Pikachu)
{
spAttack *= 2.0f;
}
if (moveType == PBEType.Bug && user.Ability == PBEAbility.Swarm && user.HP <= user.MaxHP / 3)
{
spAttack *= 1.5f;
}
if (moveType == PBEType.Fire && user.Ability == PBEAbility.Blaze && user.HP <= user.MaxHP / 3)
{
spAttack *= 1.5f;
}
if (moveType == PBEType.Grass && user.Ability == PBEAbility.Overgrow && user.HP <= user.MaxHP / 3)
{
spAttack *= 1.5f;
}
if (moveType == PBEType.Water && user.Ability == PBEAbility.Torrent && user.HP <= user.MaxHP / 3)
{
spAttack *= 1.5f;
}
if (ShouldDoWeatherEffects() && Weather == PBEWeather.HarshSunlight && user.Ability == PBEAbility.SolarPower)
{
spAttack *= 1.5f;
}
if (user.Item == PBEItem.SoulDew && (user.OriginalSpecies == PBESpecies.Latias || user.OriginalSpecies == PBESpecies.Latios))
{
spAttack *= 1.5f;
}
if (user.Item == PBEItem.ChoiceSpecs)
{
spAttack *= 1.5f;
}
if ((user.Ability == PBEAbility.Minus || user.Ability == PBEAbility.Plus) && user.Team.ActiveBattlers.FindIndex(p => p != user && (p.Ability == PBEAbility.Minus || p.Ability == PBEAbility.Plus)) != -1)
{
spAttack *= 1.5f;
}
if ((moveType == PBEType.Fire || moveType == PBEType.Ice) && target.Ability == PBEAbility.ThickFat && !user.HasCancellingAbility())
{
spAttack *= 0.5f;
}
if (user.Ability == PBEAbility.Defeatist && user.HP <= user.MaxHP / 2)
{
spAttack *= 0.5f;
}
return spAttack;
}
private float CalculateSpDefense(PBEBattlePokemon user, PBEBattlePokemon target, float initialSpDefense)
{
float spDefense = initialSpDefense;
if (target.Item == PBEItem.DeepSeaScale && target.OriginalSpecies == PBESpecies.Clamperl)
{
spDefense *= 2.0f;
}
if (target.Item == PBEItem.SoulDew && (target.OriginalSpecies == PBESpecies.Latias || target.OriginalSpecies == PBESpecies.Latios))
{
spDefense *= 1.5f;
}
if (target.Item == PBEItem.Eviolite && PBEDataProvider.Instance.HasEvolutions(target.OriginalSpecies, target.RevertForm))
{
spDefense *= 1.5f;
}
if (ShouldDoWeatherEffects())
{
if (Weather == PBEWeather.Sandstorm && target.HasType(PBEType.Rock))
{
spDefense *= 1.5f;
}
if (!user.HasCancellingAbility() && Weather == PBEWeather.HarshSunlight && target.Team.ActiveBattlers.FindIndex(p => p.Ability == PBEAbility.FlowerGift) != -1)
{
spDefense *= 1.5f;
}
}
return spDefense;
}
private int CalculateDamage(PBEBattlePokemon user, float a, float d, float basePower)
{
float damage;
damage = (2 * user.Level / 5) + 2;
damage = damage * a * basePower / d;
damage /= 50;
damage += 2;
return (int)(damage * ((100f - _rand.RandomInt(0, 15)) / 100));
}
private int CalculateConfusionDamage(PBEBattlePokemon pkmn)
{
// Verified: Unaware has no effect on confusion damage
float m = GetStatChangeModifier(pkmn.AttackChange, false);
float a = CalculateAttack(pkmn, pkmn, PBEType.None, pkmn.Attack * m);
m = GetStatChangeModifier(pkmn.DefenseChange, false);
float d = CalculateDefense(pkmn, pkmn, pkmn.Defense * m);
return CalculateDamage(pkmn, a, d, 40);
}
private int CalculateDamage(PBEBattlePokemon user, PBEBattlePokemon target, IPBEMoveData mData, PBEType moveType, float basePower, bool criticalHit)
{
PBEBattlePokemon aPkmn;
PBEMoveCategory aCat = mData.Category, dCat;
switch (mData.Effect)
{
case PBEMoveEffect.FoulPlay:
{
aPkmn = target;
dCat = aCat;
break;
}
case PBEMoveEffect.Psyshock:
{
aPkmn = user;
dCat = PBEMoveCategory.Physical;
break;
}
default:
{
aPkmn = user;
dCat = aCat;
break;
}
}
bool ignoreA = user != target && target.Ability == PBEAbility.Unaware && !user.HasCancellingAbility();
bool ignoreD = user != target && (mData.Effect == PBEMoveEffect.ChipAway || user.Ability == PBEAbility.Unaware);
float a, d;
if (aCat == PBEMoveCategory.Physical)
{
float m = ignoreA ? 1 : GetStatChangeModifier(criticalHit ? Math.Max((sbyte)0, aPkmn.AttackChange) : aPkmn.AttackChange, false);
a = CalculateAttack(user, target, moveType, aPkmn.Attack * m);
}
else
{
float m = ignoreA ? 1 : GetStatChangeModifier(criticalHit ? Math.Max((sbyte)0, aPkmn.SpAttackChange) : aPkmn.SpAttackChange, false);
a = CalculateSpAttack(user, target, moveType, aPkmn.SpAttack * m);
}
if (dCat == PBEMoveCategory.Physical)
{
float m = ignoreD ? 1 : GetStatChangeModifier(criticalHit ? Math.Min((sbyte)0, target.DefenseChange) : target.DefenseChange, false);
d = CalculateDefense(user, target, target.Defense * m);
}
else
{
float m = ignoreD ? 1 : GetStatChangeModifier(criticalHit ? Math.Min((sbyte)0, target.SpDefenseChange) : target.SpDefenseChange, false);
d = CalculateSpDefense(user, target, target.SpDefense * m);
}
return CalculateDamage(user, a, d, basePower);
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattleEffects.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using Kermalis.PokemonBattleEngine.Data.Utils;
using Kermalis.PokemonBattleEngine.Packets;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Kermalis.PokemonBattleEngine.Battle;
public sealed partial class PBEBattle
{
private bool _calledFromOtherMove = false;
private void DoSwitchInEffects(IEnumerable battlers, PBEBattlePokemon? forcedInBy = null)
{
// Set EXP Seens first
foreach (PBEBattlePokemon pkmn in battlers)
{
PBETeam opTeam = pkmn.Team.OpposingTeam;
foreach (PBEBattlePokemon op in opTeam.ActiveBattlers)
{
op.AddEXPPokemon(pkmn);
pkmn.AddEXPPokemon(op);
}
}
IEnumerable order = GetActingOrder(battlers, true);
foreach (PBEBattlePokemon pkmn in order)
{
bool grounded = pkmn.IsGrounded(forcedInBy) == PBEResult.Success;
// Verified: (Spikes/StealthRock/ToxicSpikes in the order they were applied) before ability
if (grounded && pkmn.Team.TeamStatus.HasFlag(PBETeamStatus.Spikes))
{
BroadcastTeamStatusDamage(pkmn.Team, PBETeamStatus.Spikes, pkmn);
DealDamage(pkmn, pkmn, (int)(pkmn.MaxHP / (10.0 - (2 * pkmn.Team.SpikeCount))));
if (FaintCheck(pkmn))
{
continue;
}
LowHPBerryCheck(pkmn, forcedToEatBy: forcedInBy);
}
if (pkmn.Team.TeamStatus.HasFlag(PBETeamStatus.StealthRock))
{
BroadcastTeamStatusDamage(pkmn.Team, PBETeamStatus.StealthRock, pkmn);
DealDamage(pkmn, pkmn, (int)(pkmn.MaxHP * PBETypeEffectiveness.GetStealthRockMultiplier(pkmn.Type1, pkmn.Type2)));
if (FaintCheck(pkmn))
{
continue;
}
LowHPBerryCheck(pkmn, forcedToEatBy: forcedInBy);
}
if (grounded && pkmn.Team.TeamStatus.HasFlag(PBETeamStatus.ToxicSpikes))
{
if (pkmn.HasType(PBEType.Poison))
{
pkmn.Team.ToxicSpikeCount = 0;
BroadcastTeamStatus(pkmn.Team, PBETeamStatus.ToxicSpikes, PBETeamStatusAction.Cleared);
}
else if (pkmn.IsPoisonPossible(forcedInBy, ignoreSubstitute: true) == PBEResult.Success)
{
if (pkmn.Team.ToxicSpikeCount == 1)
{
pkmn.Status1 = PBEStatus1.Poisoned;
}
else
{
pkmn.Status1 = PBEStatus1.BadlyPoisoned;
pkmn.Status1Counter = 1;
}
BroadcastStatus1(pkmn, pkmn, pkmn.Status1, PBEStatusAction.Added);
// Immunity activates in ActivateAbility() below
}
}
ActivateAbility(pkmn, true);
}
// Verified: Castform/Cherrim transformation goes last. Even if multiple weather abilities activate, they will not change until every ability has been activated
CastformCherrimCheck(order);
}
private void DoPostHitEffects(PBEBattlePokemon user, PBEBattlePokemon victim, IPBEMoveData mData, PBEType moveType)
{
IllusionBreak(victim, user); // Verified: Illusion before Rocky Helmet
if (victim.HP > 0 && victim.Ability == PBEAbility.Justified && moveType == PBEType.Dark) // Verified: Justified before Rocky Helmet
{
BroadcastAbility(victim, user, PBEAbility.Justified, PBEAbilityAction.Damage);
ApplyStatChangeIfPossible(victim, victim, PBEStat.Attack, +1);
}
if (victim.HP > 0 && victim.Ability == PBEAbility.Rattled && (moveType == PBEType.Bug || moveType == PBEType.Dark || moveType == PBEType.Ghost)) // Verified: Rattled before Rocky Helmet
{
BroadcastAbility(victim, user, PBEAbility.Rattled, PBEAbilityAction.Damage);
ApplyStatChangeIfPossible(victim, victim, PBEStat.Speed, +1);
}
if (victim.HP > 0 && victim.Ability == PBEAbility.WeakArmor && mData.Category == PBEMoveCategory.Physical) // Verified: Weak Armor before Rocky Helmet
{
BroadcastAbility(victim, user, PBEAbility.WeakArmor, PBEAbilityAction.Damage);
ApplyStatChangeIfPossible(victim, victim, PBEStat.Defense, -1);
ApplyStatChangeIfPossible(victim, victim, PBEStat.Speed, +1);
}
if (mData.Flags.HasFlag(PBEMoveFlag.MakesContact))
{
if (user.HP > 0 && victim.Ability == PBEAbility.Mummy && user.Ability != PBEAbility.Multitype && user.Ability != PBEAbility.Mummy && user.Ability != PBEAbility.ZenMode)
{
BroadcastAbility(victim, user, PBEAbility.Mummy, PBEAbilityAction.Damage);
SetAbility(victim, user, PBEAbility.Mummy);
}
if (user.HP > 0 && (victim.Ability == PBEAbility.IronBarbs || victim.Ability == PBEAbility.RoughSkin))
{
BroadcastAbility(victim, user, victim.Ability, PBEAbilityAction.Damage);
DealDamage(victim, user, user.MaxHP / 8);
if (!FaintCheck(user))
{
LowHPBerryCheck(user);
}
}
// Verified: Cute Charm can activate when victim is about to faint
if (user.HP > 0 && victim.Ability == PBEAbility.CuteCharm && user.IsAttractionPossible(victim) == PBEResult.Success && GetManipulableChance(victim, 30))
{
BroadcastAbility(victim, user, PBEAbility.CuteCharm, PBEAbilityAction.ChangedStatus);
CauseInfatuation(user, victim);
}
if (user.HP > 0 && victim.Ability == PBEAbility.EffectSpore && user.Status1 == PBEStatus1.None)
{
// Commented in case the Rainbow affects Effect Spore
//int randomNum = PBERandom.RandomInt(0, 99);
if (GetManipulableChance(victim, 30))
{
// Spaghetti code taken from the assembly in generation 5 games
PBEStatus1 status = PBEStatus1.None;
int randomNum = _rand.RandomInt(0, 29);
if (randomNum <= 20)
{
if (randomNum > 10) // 11-20 (10%)
{
// TODO: Can it really not paralyze electric? I thought that's gen 6+
if (!user.HasType(PBEType.Electric) && user.IsParalysisPossible(victim) == PBEResult.Success)
{
status = PBEStatus1.Paralyzed;
}
}
else // 0-10 (11%)
{
if (user.IsSleepPossible(victim) == PBEResult.Success)
{
status = PBEStatus1.Asleep;
}
}
}
else // 21-29 (9%)
{
if (user.IsPoisonPossible(victim) == PBEResult.Success)
{
status = PBEStatus1.Poisoned;
}
}
if (status != PBEStatus1.None)
{
BroadcastAbility(victim, user, PBEAbility.EffectSpore, PBEAbilityAction.ChangedStatus);
user.Status1 = status;
if (status == PBEStatus1.Asleep)
{
SetSleepTurns(user, Settings.SleepMinTurns, Settings.SleepMaxTurns);
}
user.Status1Counter = 0;
BroadcastStatus1(user, victim, status, PBEStatusAction.Added);
AntiStatusAbilityCheck(user);
}
}
}
if (user.HP > 0 && victim.Ability == PBEAbility.FlameBody && user.IsBurnPossible(victim) == PBEResult.Success && GetManipulableChance(victim, 30))
{
BroadcastAbility(victim, user, PBEAbility.FlameBody, PBEAbilityAction.ChangedStatus);
user.Status1 = PBEStatus1.Burned;
BroadcastStatus1(user, victim, PBEStatus1.Burned, PBEStatusAction.Added);
AntiStatusAbilityCheck(user);
}
if (user.HP > 0 && victim.Ability == PBEAbility.PoisonPoint && user.IsPoisonPossible(victim) == PBEResult.Success && GetManipulableChance(victim, 30))
{
BroadcastAbility(victim, user, PBEAbility.PoisonPoint, PBEAbilityAction.ChangedStatus);
user.Status1 = PBEStatus1.Poisoned;
BroadcastStatus1(user, victim, PBEStatus1.Poisoned, PBEStatusAction.Added);
AntiStatusAbilityCheck(user);
}
if (user.HP > 0 && victim.Ability == PBEAbility.Static && user.IsParalysisPossible(victim) == PBEResult.Success && GetManipulableChance(victim, 30))
{
BroadcastAbility(victim, user, PBEAbility.Static, PBEAbilityAction.ChangedStatus);
user.Status1 = PBEStatus1.Paralyzed;
BroadcastStatus1(user, victim, PBEStatus1.Paralyzed, PBEStatusAction.Added);
AntiStatusAbilityCheck(user);
}
// Verified: Above abilities before Rocky Helmet
if (user.HP > 0 && victim.Item == PBEItem.RockyHelmet)
{
BroadcastItem(victim, user, PBEItem.RockyHelmet, PBEItemAction.Damage);
DealDamage(victim, user, user.MaxHP / 6);
if (!FaintCheck(user))
{
LowHPBerryCheck(user);
}
}
}
}
private void DoPostAttackedEffects(PBEBattlePokemon user, List allies, List foes, bool doLifeOrb,
int? recoilDamage = null,
PBEType colorChangeType = PBEType.None)
{
#region User
if (user.HP > 0)
{
// Verified: Recoil before LifeOrb
// Verified: Recoil calls berry check directly, and both can faint here
if (recoilDamage is not null)
{
BroadcastRecoil(user);
DealDamage(user, user, recoilDamage.Value);
if (!FaintCheck(user))
{
LowHPBerryCheck(user);
}
}
if (user.HP > 0 && doLifeOrb && user.Item == PBEItem.LifeOrb)
{
BroadcastItem(user, user, PBEItem.LifeOrb, PBEItemAction.Damage);
DealDamage(user, user, user.MaxHP / 10);
FaintCheck(user); // No berry check because we are holding Life Orb
}
}
#endregion
#region Victims
void DoColorChange(IEnumerable order)
{
foreach (PBEBattlePokemon pkmn in order)
{
if (pkmn.Ability == PBEAbility.ColorChange && !pkmn.HasType(colorChangeType))
{
BroadcastAbility(pkmn, pkmn, PBEAbility.ColorChange, PBEAbilityAction.ChangedAppearance);
BroadcastTypeChanged(pkmn, colorChangeType, PBEType.None);
}
}
}
IEnumerable a = allies.Select(v => v.Pkmn).Where(p => p.HP > 0);
IEnumerable f = foes.Select(v => v.Pkmn).Where(p => p.HP > 0);
// Verified: ColorChange (foes, allies) before Berry
if (colorChangeType != PBEType.None)
{
DoColorChange(f);
DoColorChange(a);
}
// Verified: Berry (allies, foes) before AntiStatusAbility
LowHPBerryCheck(a, forcedToEatBy: user);
LowHPBerryCheck(f, forcedToEatBy: user);
// Verified: AntiStatusAbility (allies, foes)
AntiStatusAbilityCheck(a); // Heal a status that was given with the user's Mold Breaker
AntiStatusAbilityCheck(f);
#endregion
}
private void DoTurnEndedEffects()
{
IEnumerable order = GetActingOrder(ActiveBattlers, true);
// Verified: Weather stops before doing damage
if (Weather != PBEWeather.None && WeatherCounter > 0)
{
WeatherCounter--;
if (WeatherCounter == 0)
{
PBEWeather w = Weather;
Weather = PBEWeather.None;
BroadcastWeather(w, PBEWeatherAction.Ended);
CastformCherrimCheck(order);
}
}
// Verified: Hailstorm/Sandstorm/IceBody/RainDish/SolarPower before all
if (Weather != PBEWeather.None && ShouldDoWeatherEffects())
{
foreach (PBEBattlePokemon pkmn in order)
{
if (pkmn.HP == 0)
{
continue;
}
switch (Weather)
{
case PBEWeather.Hailstorm:
{
if (pkmn.Ability == PBEAbility.IceBody)
{
if (pkmn.HP < pkmn.MaxHP)
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.RestoredHP);
HealDamage(pkmn, pkmn.MaxHP / Settings.IceBodyHealDenominator);
}
}
else if (!pkmn.HasType(PBEType.Ice)
&& pkmn.Ability != PBEAbility.Overcoat
&& pkmn.Ability != PBEAbility.SnowCloak)
{
BroadcastWeatherDamage(PBEWeather.Hailstorm, pkmn);
DealDamage(pkmn, pkmn, pkmn.MaxHP / Settings.HailDamageDenominator);
if (!FaintCheck(pkmn))
{
LowHPBerryCheck(pkmn);
}
}
break;
}
case PBEWeather.HarshSunlight:
{
if (pkmn.Ability == PBEAbility.SolarPower)
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.Damage);
DealDamage(pkmn, pkmn, pkmn.MaxHP / 8);
if (!FaintCheck(pkmn))
{
LowHPBerryCheck(pkmn);
}
}
break;
}
case PBEWeather.Rain:
{
if (pkmn.Ability == PBEAbility.RainDish && pkmn.HP < pkmn.MaxHP)
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.RestoredHP);
HealDamage(pkmn, pkmn.MaxHP / 16);
}
break;
}
case PBEWeather.Sandstorm:
{
if (!pkmn.HasType(PBEType.Rock)
&& !pkmn.HasType(PBEType.Ground)
&& !pkmn.HasType(PBEType.Steel)
&& pkmn.Ability != PBEAbility.Overcoat
&& pkmn.Ability != PBEAbility.SandForce
&& pkmn.Ability != PBEAbility.SandRush
&& pkmn.Ability != PBEAbility.SandVeil
&& !pkmn.Status2.HasFlag(PBEStatus2.Underground)
&& !pkmn.Status2.HasFlag(PBEStatus2.Underwater))
{
BroadcastWeatherDamage(PBEWeather.Sandstorm, pkmn);
DealDamage(pkmn, pkmn, pkmn.MaxHP / Settings.SandstormDamageDenominator);
if (!FaintCheck(pkmn))
{
LowHPBerryCheck(pkmn);
}
}
break;
}
}
}
}
// Verified: Healer/ShedSkin/BlackSludge/Leftovers before LeechSeed
foreach (PBEBattlePokemon pkmn in order)
{
if (pkmn.HP == 0)
{
continue;
}
switch (pkmn.Ability)
{
case PBEAbility.Healer:
{
foreach (PBEBattlePokemon ally in GetRuntimeSurrounding(pkmn, true, false))
{
// TODO: #265
if (ally.Status1 != PBEStatus1.None && GetManipulableChance(pkmn, 30))
{
BroadcastAbility(pkmn, ally, pkmn.Ability, PBEAbilityAction.ChangedStatus);
PBEStatus1 status1 = ally.Status1;
ally.Status1 = PBEStatus1.None;
ally.Status1Counter = 0;
ally.SleepTurns = 0;
BroadcastStatus1(ally, pkmn, status1, PBEStatusAction.Cleared);
if (status1 == PBEStatus1.Asleep)
{
CureNightmare(ally, pkmn);
}
}
}
break;
}
case PBEAbility.ShedSkin:
{
if (pkmn.Status1 != PBEStatus1.None && GetManipulableChance(pkmn, 30))
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.ChangedStatus);
PBEStatus1 status1 = pkmn.Status1;
pkmn.Status1 = PBEStatus1.None;
pkmn.Status1Counter = 0;
pkmn.SleepTurns = 0;
BroadcastStatus1(pkmn, pkmn, status1, PBEStatusAction.Cleared);
if (status1 == PBEStatus1.Asleep)
{
CureNightmare(pkmn, pkmn);
}
}
break;
}
}
switch (pkmn.Item)
{
case PBEItem.BlackSludge:
{
if (!pkmn.HasType(PBEType.Poison))
{
BroadcastItem(pkmn, pkmn, pkmn.Item, PBEItemAction.Damage);
DealDamage(pkmn, pkmn, pkmn.MaxHP / Settings.BlackSludgeDamageDenominator);
FaintCheck(pkmn); // No need to call HealingBerryCheck() because if you are holding BlackSludge you are not holding a healing berry
}
else if (pkmn.HP < pkmn.MaxHP)
{
BroadcastItem(pkmn, pkmn, pkmn.Item, PBEItemAction.RestoredHP);
HealDamage(pkmn, pkmn.MaxHP / Settings.BlackSludgeHealDenominator);
}
break;
}
case PBEItem.Leftovers:
{
if (pkmn.HP < pkmn.MaxHP)
{
BroadcastItem(pkmn, pkmn, pkmn.Item, PBEItemAction.RestoredHP);
HealDamage(pkmn, pkmn.MaxHP / Settings.LeftoversHealDenominator);
}
break;
}
}
}
// Verified: LeechSeed before Status1/PoisonHeal
foreach (PBEBattlePokemon pkmn in order)
{
if (pkmn.HP == 0 || !pkmn.Status2.HasFlag(PBEStatus2.LeechSeed))
{
continue;
}
if (!pkmn.SeededTeam!.TryGetPokemon(pkmn.SeededPosition, out PBEBattlePokemon? sucker))
{
continue;
}
BroadcastStatus2(pkmn, sucker, PBEStatus2.LeechSeed, PBEStatusAction.Damage);
int restoreAmt = DealDamage(sucker, pkmn, pkmn.MaxHP / Settings.LeechSeedDenominator);
// In the games, the pkmn faints after taking damage (before liquid ooze/heal)
// We cannot have it faint and then still broadcast ability like the games, similar to why we can't faint before Explosion
// The faint order should be maintained, though, so the correct winner can be chosen
ApplyBigRoot(pkmn, ref restoreAmt);
if (pkmn.Ability == PBEAbility.LiquidOoze)
{
BroadcastAbility(pkmn, sucker, PBEAbility.LiquidOoze, PBEAbilityAction.Damage);
DealDamage(pkmn, sucker, restoreAmt);
if (!FaintCheck(pkmn))
{
LowHPBerryCheck(pkmn);
}
if (!FaintCheck(sucker))
{
LowHPBerryCheck(sucker);
}
}
else
{
if (!FaintCheck(pkmn))
{
LowHPBerryCheck(pkmn);
}
HealDamage(sucker, restoreAmt);
}
}
// Verified: Status1/PoisonHeal before Curse
foreach (PBEBattlePokemon pkmn in order)
{
if (pkmn.HP == 0)
{
continue;
}
switch (pkmn.Status1)
{
case PBEStatus1.BadlyPoisoned:
case PBEStatus1.Poisoned:
{
if (pkmn.Ability != PBEAbility.PoisonHeal)
{
BroadcastStatus1(pkmn, pkmn, pkmn.Status1, PBEStatusAction.Damage);
int damage = pkmn.Status1 == PBEStatus1.BadlyPoisoned
? pkmn.MaxHP * pkmn.Status1Counter / Settings.ToxicDamageDenominator
: pkmn.MaxHP / Settings.PoisonDamageDenominator;
DealDamage(pkmn, pkmn, damage);
if (!FaintCheck(pkmn))
{
LowHPBerryCheck(pkmn);
}
}
else if (pkmn.HP < pkmn.MaxHP)
{
BroadcastAbility(pkmn, pkmn, PBEAbility.PoisonHeal, PBEAbilityAction.RestoredHP);
HealDamage(pkmn, pkmn.MaxHP / 8);
}
if (pkmn.Status1 == PBEStatus1.BadlyPoisoned)
{
pkmn.Status1Counter++; // Counter still increments if PoisonHeal exists
}
break;
}
case PBEStatus1.Burned:
{
BroadcastStatus1(pkmn, pkmn, pkmn.Status1, PBEStatusAction.Damage);
int damage = pkmn.MaxHP / Settings.BurnDamageDenominator;
if (pkmn.Ability == PBEAbility.Heatproof)
{
damage /= 2;
}
DealDamage(pkmn, pkmn, damage);
if (!FaintCheck(pkmn))
{
LowHPBerryCheck(pkmn);
}
break;
}
}
}
// Verified: Nightmare before Curse, not same loop
foreach (PBEBattlePokemon pkmn in order)
{
if (pkmn.HP == 0 || !pkmn.Status2.HasFlag(PBEStatus2.Nightmare))
{
continue;
}
BroadcastStatus2(pkmn, pkmn, PBEStatus2.Nightmare, PBEStatusAction.Damage);
DealDamage(pkmn, pkmn, pkmn.MaxHP / 4);
if (!FaintCheck(pkmn))
{
LowHPBerryCheck(pkmn);
}
}
// Verified: Curse before MagnetRise
foreach (PBEBattlePokemon pkmn in order)
{
if (pkmn.HP == 0 || !pkmn.Status2.HasFlag(PBEStatus2.Cursed))
{
continue;
}
BroadcastStatus2(pkmn, pkmn, PBEStatus2.Cursed, PBEStatusAction.Damage);
DealDamage(pkmn, pkmn, pkmn.MaxHP / Settings.CurseDenominator);
if (!FaintCheck(pkmn))
{
LowHPBerryCheck(pkmn);
}
}
// Verified: MagnetRise before Abilities/Orbs
foreach (PBEBattlePokemon pkmn in order)
{
if (pkmn.HP == 0 || !pkmn.Status2.HasFlag(PBEStatus2.MagnetRise) || pkmn.MagnetRiseTurns == 0)
{
continue;
}
pkmn.MagnetRiseTurns--;
if (pkmn.MagnetRiseTurns == 0)
{
BroadcastStatus2(pkmn, pkmn, PBEStatus2.MagnetRise, PBEStatusAction.Ended);
}
}
// Verified: BadDreams/Moody/SlowStart/SpeedBoost before Orbs, but activate together
foreach (PBEBattlePokemon pkmn in order)
{
if (pkmn.HP == 0)
{
continue;
}
// Ability before Orb
switch (pkmn.Ability)
{
case PBEAbility.BadDreams:
{
foreach (PBEBattlePokemon victim in GetRuntimeSurrounding(pkmn, false, true).Where(p => p.Status1 == PBEStatus1.Asleep))
{
BroadcastAbility(pkmn, victim, PBEAbility.BadDreams, PBEAbilityAction.Damage);
DealDamage(pkmn, victim, pkmn.MaxHP / 8);
if (!FaintCheck(victim))
{
LowHPBerryCheck(victim);
}
}
break;
}
case PBEAbility.Moody:
{
List statsThatCanGoUp = PBEDataUtils.MoodyStats.FindAll(s => pkmn.GetStatChange(s) < Settings.MaxStatChange);
PBEStat? upStat = statsThatCanGoUp.Count == 0 ? null : _rand.RandomElement(statsThatCanGoUp);
List statsThatCanGoDown = PBEDataUtils.MoodyStats.FindAll(s => pkmn.GetStatChange(s) > -Settings.MaxStatChange);
if (upStat is not null)
{
statsThatCanGoDown.Remove(upStat.Value);
}
PBEStat? downStat = statsThatCanGoDown.Count == 0 ? null : _rand.RandomElement(statsThatCanGoDown);
if (upStat is not null || downStat is not null)
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.Stats);
if (upStat is not null)
{
ApplyStatChangeIfPossible(pkmn, pkmn, upStat.Value, +2);
}
if (downStat is not null)
{
ApplyStatChangeIfPossible(pkmn, pkmn, downStat.Value, -1);
}
}
break;
}
case PBEAbility.SlowStart:
{
if (pkmn.SlowStart_HinderTurnsLeft > 0)
{
pkmn.SlowStart_HinderTurnsLeft--;
if (pkmn.SlowStart_HinderTurnsLeft == 0)
{
BroadcastAbility(pkmn, pkmn, PBEAbility.SlowStart, PBEAbilityAction.SlowStart_Ended);
}
}
break;
}
case PBEAbility.SpeedBoost:
{
if (pkmn.SpeedBoost_AbleToSpeedBoostThisTurn && pkmn.SpeedChange < Settings.MaxStatChange)
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.Stats);
ApplyStatChangeIfPossible(pkmn, pkmn, PBEStat.Speed, +1);
}
break;
}
}
// Orb
switch (pkmn.Item)
{
case PBEItem.FlameOrb:
{
if (pkmn.IsBurnPossible(null, ignoreSubstitute: true, ignoreSafeguard: true) == PBEResult.Success)
{
pkmn.Status1 = PBEStatus1.Burned;
BroadcastItem(pkmn, pkmn, pkmn.Item, PBEItemAction.Announced);
BroadcastStatus1(pkmn, pkmn, PBEStatus1.Burned, PBEStatusAction.Added);
}
break;
}
case PBEItem.ToxicOrb:
{
if (pkmn.IsPoisonPossible(null, ignoreSubstitute: true, ignoreSafeguard: true) == PBEResult.Success)
{
pkmn.Status1 = PBEStatus1.BadlyPoisoned;
pkmn.Status1Counter = 1;
BroadcastItem(pkmn, pkmn, pkmn.Item, PBEItemAction.Announced);
BroadcastStatus1(pkmn, pkmn, PBEStatus1.BadlyPoisoned, PBEStatusAction.Added);
}
break;
}
}
}
}
public bool ShouldDoWeatherEffects()
{
// If HP is needed to be above 0, use HPPercentage so clients can continue to use this
// However, I see no instance of this getting called where an ActiveBattler has 0 hp
return ActiveBattlers.FindIndex(p => p.Ability == PBEAbility.AirLock || p.Ability == PBEAbility.CloudNine) == -1;
}
public bool WillLeafGuardActivate()
{
return ShouldDoWeatherEffects() && Weather == PBEWeather.HarshSunlight;
}
private void FleeCheck()
{
foreach (PBETrainer trainer in Trainers)
{
if (!trainer.RequestedFlee)
{
continue;
}
if (trainer.IsWild)
{
PBEBattlePokemon wildPkmn = trainer.ActiveBattlersOrdered.First();
wildPkmn.TurnAction = new PBETurnAction(wildPkmn); // Convert into a WildFlee turn action for the first Pokémon
trainer.RequestedFlee = false;
continue;
}
PBEBattlePokemon? pkmn = trainer.ActiveBattlersOrdered.FirstOrDefault();
if (pkmn is not null)
{
// Verified: RunAway before SmokeBall
if (pkmn.Ability == PBEAbility.RunAway)
{
BroadcastAbility(pkmn, pkmn, PBEAbility.RunAway, PBEAbilityAction.Announced);
SetEscaped(pkmn);
return;
}
if (pkmn.Item == PBEItem.SmokeBall)
{
BroadcastItem(pkmn, pkmn, PBEItem.SmokeBall, PBEItemAction.Announced);
SetEscaped(pkmn);
return;
}
}
else
{
pkmn = trainer.Party[0]; // Use the first fainted Pokémon's speed if there's no active battler
}
// TODO: I'm using the gen 3/4 formula below.
// TODO: Figure out the gen 5 formula, as well as what to use in a double wild battle
int a = pkmn.Speed;
int b = (int)trainer.Team.OpposingTeam.ActiveBattlers.Average(p => p.Speed);
int c = ++trainer.Team.NumTimesTriedToFlee; // Increment even if guaranteed
bool success;
if (a > b)
{
success = true;
}
else
{
int f = ((a * 128 / b) + (30 * c)) % 256;
success = _rand.RandomInt(0, 255) < f;
}
if (success)
{
SetEscaped(pkmn);
return;
}
else
{
BroadcastFleeFailed(pkmn);
}
trainer.RequestedFlee = false;
}
}
private void WildFleeCheck(PBEBattlePokemon pkmn)
{
// TODO: Trapping effects
SetEscaped(pkmn);
}
private void CalcEXP(PBEBattlePokemon loser)
{
IPBEPokemonData loserPData = PBEDataProvider.Instance.GetPokemonData(loser);
float modTrainer = loser.IsWild ? 1 : 1.5f;
int expYield = loserPData.BaseEXPYield;
int levelLoser = loser.Level;
float modPassPower = PBEDataProvider.Instance.GetEXPModifier(this);
int amtParticipated = loser.EXPPokemon.Count(pk => pk.Trainer.GainsEXP && pk.HP > 0);
int amtEXPShare = loser.EXPPokemon.Count(pk => pk.Trainer.GainsEXP && pk.Item == PBEItem.ExpShare);
foreach (PBEBattlePokemon victor in loser.EXPPokemon)
{
if (!victor.Trainer.GainsEXP || victor.HP == 0 || victor.Level >= Settings.MaxLevel)
{
continue;
}
int levelVictor = victor.Level;
float modParticipators;
if (amtEXPShare == 0)
{
modParticipators = amtParticipated;
}
else if (victor.Item == PBEItem.ExpShare)
{
modParticipators = 2 * amtEXPShare;
}
else
{
modParticipators = 2 * amtParticipated;
}
float modTraded = PBEDataProvider.Instance.GetEXPTradeModifier(victor);
float modLuckyEgg = victor.Item == PBEItem.LuckyEgg ? 1.5f : 1;
float result1H = modTrainer * expYield * levelLoser;
float result1L = 5 * modParticipators;
float result1 = result1H / result1L;
float result2H = MathF.Pow(2 * levelLoser + 10, 2.5f);
float result2L = MathF.Pow(levelLoser + levelVictor + 10, 2.5f);
float result2 = result2H / result2L;
float combined = result1 * result2 + 1;
float final = combined * modTraded * modLuckyEgg * modPassPower;
GiveEXP(victor, (uint)final);
}
}
private void GiveEXP(PBEBattlePokemon victor, uint amount)
{
// TODO: Should we allow remote battles with learning moves? No packets right now
BroadcastPkmnEXPEarned(victor, amount);
PBEGrowthRate growthRate = PBEDataProvider.Instance.GetPokemonData(victor).GrowthRate;
top:
uint oldEXP = victor.EXP;
uint nextLevelAmt = PBEDataProvider.Instance.GetEXPRequired(growthRate, (byte)(victor.Level + 1));
if (oldEXP + amount >= nextLevelAmt)
{
victor.EXP = nextLevelAmt;
BroadcastPkmnEXPChanged(victor, oldEXP);
victor.Level++;
victor.SetStats(true, false);
BroadcastPkmnLevelChanged(victor);
// BUG: PBEStatus2.PowerTrick is not cleared when leveling up, even though the stats are replaced (meaning it can still be baton passed)
if (Settings.BugFix && victor.Status2.HasFlag(PBEStatus2.PowerTrick))
{
BroadcastStatus2(victor, victor, PBEStatus2.PowerTrick, PBEStatusAction.Ended);
}
if (victor.Level == Settings.MaxLevel)
{
return;
}
uint grewBy = nextLevelAmt - oldEXP;
amount -= grewBy;
if (amount > 0)
{
goto top; // Keep gaining and leveling
}
}
else
{
victor.EXP = oldEXP + amount;
BroadcastPkmnEXPChanged(victor, oldEXP);
}
}
private static float PokedexCountTable(int count, float g600, float g450, float g300, float g150, float g30, float ge0)
{
if (count > 600)
{
return g600;
}
if (count > 450)
{
return g450;
}
if (count > 300)
{
return g300;
}
if (count > 150)
{
return g150;
}
if (count > 30)
{
return g30;
}
return ge0;
}
private void GenerateCapture(PBEBattlePokemon user, PBEBattlePokemon wildPkmn, PBEItem ball, out byte shakes, out bool success, out bool isCriticalCapture)
{
if (PBEDataProvider.Instance.IsGuaranteedCapture(this, wildPkmn.OriginalSpecies, wildPkmn.RevertForm))
{
shakes = 3;
success = true;
isCriticalCapture = false;
return;
}
IPBEPokemonData pData = PBEDataProvider.Instance.GetPokemonData(wildPkmn.OriginalSpecies, wildPkmn.RevertForm);
float rate = pData.CatchRate * PBEDataProvider.Instance.GetCatchRateModifier(this);
float bonusBall = 1;
switch (ball)
{
case PBEItem.GreatBall:
case PBEItem.SafariBall:
case PBEItem.SportBall: bonusBall = 1.5f; break;
case PBEItem.UltraBall: bonusBall = 2; break;
case PBEItem.MasterBall:
case PBEItem.ParkBall: bonusBall = 255; break;
case PBEItem.FastBall:
{
if (wildPkmn.Speed >= 100)
{
rate *= 4;
}
break;
}
case PBEItem.LevelBall:
{
int wl = wildPkmn.Level;
int ul = user.Level;
if (ul > wl * 4)
{
rate *= 8;
}
else if (ul > wl * 2)
{
rate *= 4;
}
else if (ul > wl)
{
rate *= 2;
}
break;
}
case PBEItem.LureBall:
{
if (PBEDataProvider.Instance.IsFishing(this))
{
rate *= 3;
}
break;
}
case PBEItem.HeavyBall:
{
float weight = pData.Weight;
if (weight >= 409.6f)
{
rate += 40;
}
else if (weight >= 307.2f)
{
rate += 30;
}
else if (weight >= 204.8f)
{
rate += 20;
}
else
{
rate -= 20;
}
break;
}
case PBEItem.LoveBall:
{
if (user.Species == wildPkmn.Species && user.Gender.IsOppositeGender(wildPkmn.Gender))
{
rate *= 8;
}
break;
}
case PBEItem.MoonBall:
{
if (PBEDataProvider.Instance.IsMoonBallFamily(wildPkmn.OriginalSpecies, wildPkmn.RevertForm))
{
rate *= 4;
}
break;
}
case PBEItem.NetBall:
{
if (wildPkmn.HasType(PBEType.Bug) || wildPkmn.HasType(PBEType.Water))
{
bonusBall = 3;
}
break;
}
case PBEItem.NestBall:
{
bonusBall = Math.Max(1, (41 - wildPkmn.Level) / 10);
break;
}
case PBEItem.RepeatBall:
{
if (PBEDataProvider.Instance.IsRepeatBallSpecies(wildPkmn.OriginalSpecies))
{
bonusBall = 3;
}
break;
}
case PBEItem.TimerBall:
{
bonusBall = Math.Min(4, 1 + (TurnNumber * 0.3f));
break;
}
case PBEItem.DiveBall:
{
if (PBEDataProvider.Instance.IsFishing(this) || PBEDataProvider.Instance.IsSurfing(this) || PBEDataProvider.Instance.IsUnderwater(this))
{
bonusBall = 3.5f;
}
break;
}
case PBEItem.DuskBall:
{
if (PBEDataProvider.Instance.IsDuskBallSetting(this))
{
bonusBall = 3.5f;
}
break;
}
case PBEItem.QuickBall:
{
if (TurnNumber == 1)
{
bonusBall = 5;
}
break;
}
}
rate = Math.Clamp(rate, 1, 255);
float bonusStatus;
switch (wildPkmn.Status1)
{
case PBEStatus1.Asleep:
case PBEStatus1.Frozen: bonusStatus = 2.5f; break;
case PBEStatus1.None: bonusStatus = 1; break;
default: bonusStatus = 1.5f; break;
}
float pkmnFactor = (3 * wildPkmn.MaxHP) - (2 * wildPkmn.HP);
int pkmnCaught = PBEDataProvider.Instance.GetSpeciesCaught();
if (PBEDataProvider.Instance.IsDarkGrass(this))
{
pkmnFactor *= PokedexCountTable(pkmnCaught, 1, 0.9f, 0.8f, 0.7f, 0.5f, 0.3f);
}
float a = pkmnFactor * rate * bonusBall / (3 * wildPkmn.MaxHP) * bonusStatus;
float c = a * PokedexCountTable(pkmnCaught, 2.5f, 2, 1.5f, 1, 0.5f, 0); // Critical capture modifier
isCriticalCapture = _rand.RandomInt(0, 0xFF) < c / 6;
byte numShakes = isCriticalCapture ? (byte)1 : (byte)3;
if (a >= 0xFF)
{
shakes = numShakes; // Skip shake checks
success = true;
return;
}
float b = 0x10000 / MathF.Sqrt(MathF.Sqrt(0xFF / a));
for (shakes = 0; shakes < numShakes; shakes++)
{
if (_rand.RandomInt(0, 0xFFFF) >= b)
{
break; // Shake check fails
}
}
success = shakes == numShakes;
if (shakes == 2)
{
shakes = 3; // If there are only 2 shakes and a failure, shake three times and still fail
}
}
private void UseItem(PBEBattlePokemon user, PBEItem item)
{
BroadcastItemTurn(user, item, PBEItemTurnAction.Attempt);
if (PBEDataUtils.AllBalls.Contains(item))
{
user.Trainer.Inventory.Remove(item);
if (BattleType != PBEBattleType.Wild)
{
goto fail;
}
PBEBattlePokemon wildPkmn = user.Team.OpposingTeam.ActiveBattlers.Single();
GenerateCapture(user, wildPkmn, item, out byte numShakes, out bool success, out bool critical);
BroadcastCapture(wildPkmn, item, numShakes, success, critical);
if (success)
{
wildPkmn.CaughtBall = wildPkmn.KnownCaughtBall = item;
BattleResult = PBEBattleResult.WildCapture;
}
return;
}
else
{
switch (item)
{
case PBEItem.FluffyTail:
case PBEItem.PokeDoll:
case PBEItem.PokeToy:
{
user.Trainer.Inventory.Remove(item);
if (BattleType == PBEBattleType.Wild)
{
SetEscaped(user);
return;
}
goto fail;
}
}
}
fail:
BroadcastItemTurn(user, item, PBEItemTurnAction.NoEffect);
}
private void UseMove(PBEBattlePokemon user, PBEMove move, PBETurnTarget requestedTargets)
{
// Cancel the semi-invulnerable move if the user is affected by its status1
if (!_calledFromOtherMove && PreMoveStatusCheck(user, move))
{
if (user.Status2.HasFlag(PBEStatus2.Airborne))
{
BroadcastMoveLock_Temporary(user, PBEMove.None, PBETurnTarget.None);
BroadcastStatus2(user, user, PBEStatus2.Airborne, PBEStatusAction.Ended);
}
if (user.Status2.HasFlag(PBEStatus2.ShadowForce))
{
BroadcastMoveLock_Temporary(user, PBEMove.None, PBETurnTarget.None);
BroadcastStatus2(user, user, PBEStatus2.ShadowForce, PBEStatusAction.Ended);
}
if (user.Status2.HasFlag(PBEStatus2.Underground))
{
BroadcastMoveLock_Temporary(user, PBEMove.None, PBETurnTarget.None);
BroadcastStatus2(user, user, PBEStatus2.Underground, PBEStatusAction.Ended);
}
if (user.Status2.HasFlag(PBEStatus2.Underwater))
{
BroadcastMoveLock_Temporary(user, PBEMove.None, PBETurnTarget.None);
BroadcastStatus2(user, user, PBEStatus2.Underwater, PBEStatusAction.Ended);
}
return;
}
IPBEMoveData mData = PBEDataProvider.Instance.GetMoveData(move);
PBEBattlePokemon[] targets = GetRuntimeTargets(user, requestedTargets, user.GetMoveTargets(mData) == PBEMoveTarget.SingleNotSelf, _rand);
switch (mData.Effect)
{
case PBEMoveEffect.Acrobatics:
case PBEMoveEffect.Brine:
case PBEMoveEffect.ChipAway:
case PBEMoveEffect.CrushGrip:
case PBEMoveEffect.Eruption:
case PBEMoveEffect.Facade:
case PBEMoveEffect.Flail:
case PBEMoveEffect.FoulPlay:
case PBEMoveEffect.Frustration:
case PBEMoveEffect.GrassKnot:
case PBEMoveEffect.HeatCrash:
case PBEMoveEffect.Hex:
case PBEMoveEffect.HiddenPower:
case PBEMoveEffect.Hit:
case PBEMoveEffect.Judgment:
case PBEMoveEffect.Magnitude:
case PBEMoveEffect.Payback:
case PBEMoveEffect.Psyshock:
case PBEMoveEffect.Punishment:
case PBEMoveEffect.Retaliate:
case PBEMoveEffect.Return:
case PBEMoveEffect.StoredPower:
case PBEMoveEffect.TechnoBlast:
case PBEMoveEffect.Venoshock:
case PBEMoveEffect.WeatherBall: Ef_Hit(user, targets, move, mData); break;
case PBEMoveEffect.Attract: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.Infatuated); break;
case PBEMoveEffect.BellyDrum: Ef_BellyDrum(user, targets, move, mData); break;
case PBEMoveEffect.Bounce: Ef_Bounce(user, targets, move, mData, requestedTargets); break;
case PBEMoveEffect.BrickBreak: Ef_BrickBreak(user, targets, move, mData); break;
case PBEMoveEffect.Burn: Ef_TryForceStatus1(user, targets, move, mData, PBEStatus1.Burned); break;
case PBEMoveEffect.Camouflage: Ef_Camouflage(user, targets, move, mData); break;
case PBEMoveEffect.ChangeTarget_ACC: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Accuracy, mData.EffectParam) }); break;
case PBEMoveEffect.ChangeTarget_ATK: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Attack, mData.EffectParam) }); break;
case PBEMoveEffect.ChangeTarget_DEF: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Defense, mData.EffectParam) }); break;
case PBEMoveEffect.ChangeTarget_EVA:
case PBEMoveEffect.Minimize: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Evasion, mData.EffectParam) }); break;
case PBEMoveEffect.ChangeTarget_SPATK: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.SpAttack, mData.EffectParam) }); break;
case PBEMoveEffect.ChangeTarget_SPATK__IfAttractionPossible: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.SpAttack, mData.EffectParam) }, requireAttraction: true); break;
case PBEMoveEffect.ChangeTarget_SPDEF: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.SpDefense, mData.EffectParam) }); break;
case PBEMoveEffect.ChangeTarget_SPE: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Speed, mData.EffectParam) }); break;
case PBEMoveEffect.Confuse: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.Confused); break;
case PBEMoveEffect.Conversion: Ef_Conversion(user, targets, move, mData); break;
case PBEMoveEffect.Curse: Ef_Curse(user, targets, move, mData); break;
case PBEMoveEffect.Dig: SemiInvulnerableChargeMove(user, targets, move, mData, requestedTargets, PBEStatus2.Underground); break;
case PBEMoveEffect.Dive: SemiInvulnerableChargeMove(user, targets, move, mData, requestedTargets, PBEStatus2.Underwater); break;
case PBEMoveEffect.Endeavor: Ef_Endeavor(user, targets, move, mData); break;
case PBEMoveEffect.Entrainment: Ef_Entrainment(user, targets, move, mData); break;
case PBEMoveEffect.Feint: Ef_Feint(user, targets, move, mData); break;
case PBEMoveEffect.FinalGambit: Ef_FinalGambit(user, targets, move, mData); break;
case PBEMoveEffect.Flatter: Ef_Flatter(user, targets, move, mData); break;
case PBEMoveEffect.Fly: SemiInvulnerableChargeMove(user, targets, move, mData, requestedTargets, PBEStatus2.Airborne); break;
case PBEMoveEffect.FocusEnergy: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.Pumped); break;
case PBEMoveEffect.Foresight: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.Identified); break;
case PBEMoveEffect.GastroAcid: Ef_SetOtherAbility(user, targets, move, mData, PBEAbility.None, false); break;
case PBEMoveEffect.Growth: Ef_Growth(user, targets, move, mData); break;
case PBEMoveEffect.Hail: Ef_TryForceWeather(user, move, mData, PBEWeather.Hailstorm); break;
case PBEMoveEffect.Haze: Ef_Haze(user, targets, move, mData); break;
case PBEMoveEffect.HelpingHand: Ef_HelpingHand(user, targets, move, mData); break;
case PBEMoveEffect.Hit__2Times: Ef_MultiHit(user, targets, move, mData, 2); break;
case PBEMoveEffect.Hit__2Times__MaybePoison: Ef_MultiHit(user, targets, move, mData, 2, status1: PBEStatus1.Poisoned, chanceToInflictStatus1: mData.EffectParam); break;
case PBEMoveEffect.Hit__2To5Times: Ef_MultiHit_2To5(user, targets, move, mData); break;
case PBEMoveEffect.Hit__MaybeBurn: Ef_Hit(user, targets, move, mData, status1: PBEStatus1.Burned, chanceToInflictStatus1: mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeBurn__10PercentFlinch: Ef_Hit(user, targets, move, mData, status1: PBEStatus1.Burned, chanceToInflictStatus1: mData.EffectParam, status2: PBEStatus2.Flinching, chanceToInflictStatus2: 10); break;
case PBEMoveEffect.Hit__MaybeBurnFreezeParalyze: Ef_Hit__MaybeBurnFreezeParalyze(user, targets, move, mData); break;
case PBEMoveEffect.Hit__MaybeConfuse: Ef_Hit(user, targets, move, mData, status2: PBEStatus2.Confused, chanceToInflictStatus2: mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeFlinch: Ef_Hit(user, targets, move, mData, status2: PBEStatus2.Flinching, chanceToInflictStatus2: mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeFreeze: Ef_Hit(user, targets, move, mData, status1: PBEStatus1.Frozen, chanceToInflictStatus1: mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeFreeze__10PercentFlinch: Ef_Hit(user, targets, move, mData, status1: PBEStatus1.Frozen, chanceToInflictStatus1: mData.EffectParam, status2: PBEStatus2.Flinching, chanceToInflictStatus2: 10); break;
case PBEMoveEffect.Hit__MaybeLowerTarget_ACC_By1: Ef_Hit__MaybeChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Accuracy, -1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeLowerTarget_ATK_By1: Ef_Hit__MaybeChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Attack, -1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeLowerTarget_DEF_By1: Ef_Hit__MaybeChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Defense, -1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeLowerTarget_SPATK_By1: Ef_Hit__MaybeChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.SpAttack, -1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeLowerTarget_SPDEF_By1: Ef_Hit__MaybeChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.SpDefense, -1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeLowerTarget_SPDEF_By2: Ef_Hit__MaybeChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.SpDefense, -2) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeLowerTarget_SPE_By1: Ef_Hit__MaybeChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Speed, -1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeParalyze: Ef_Hit(user, targets, move, mData, status1: PBEStatus1.Paralyzed, chanceToInflictStatus1: mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeParalyze__10PercentFlinch: Ef_Hit(user, targets, move, mData, status1: PBEStatus1.Paralyzed, chanceToInflictStatus1: mData.EffectParam, status2: PBEStatus2.Flinching, chanceToInflictStatus2: 10); break;
case PBEMoveEffect.Hit__MaybePoison: Ef_Hit(user, targets, move, mData, status1: PBEStatus1.Poisoned, chanceToInflictStatus1: mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeLowerUser_ATK_DEF_By1: Ef_Hit__MaybeChangeUserStats(user, targets, move, mData, new[] { (PBEStat.Attack, -1), (PBEStat.Defense, -1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeLowerUser_DEF_SPDEF_By1: Ef_Hit__MaybeChangeUserStats(user, targets, move, mData, new[] { (PBEStat.Defense, -1), (PBEStat.SpDefense, -1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeLowerUser_SPATK_By2: Ef_Hit__MaybeChangeUserStats(user, targets, move, mData, new[] { (PBEStat.SpAttack, -2) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeLowerUser_SPE_By1: Ef_Hit__MaybeChangeUserStats(user, targets, move, mData, new[] { (PBEStat.Speed, -1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeLowerUser_SPE_DEF_SPDEF_By1: Ef_Hit__MaybeChangeUserStats(user, targets, move, mData, new[] { (PBEStat.Speed, -1), (PBEStat.Defense, -1), (PBEStat.SpDefense, -1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeRaiseUser_ATK_By1: Ef_Hit__MaybeChangeUserStats(user, targets, move, mData, new[] { (PBEStat.Attack, +1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeRaiseUser_ATK_DEF_SPATK_SPDEF_SPE_By1: Ef_Hit__MaybeChangeUserStats(user, targets, move, mData, new[] { (PBEStat.Attack, +1), (PBEStat.Defense, +1), (PBEStat.SpAttack, +1), (PBEStat.SpDefense, +1), (PBEStat.Speed, +1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeRaiseUser_DEF_By1: Ef_Hit__MaybeChangeUserStats(user, targets, move, mData, new[] { (PBEStat.Defense, +1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeRaiseUser_SPATK_By1: Ef_Hit__MaybeChangeUserStats(user, targets, move, mData, new[] { (PBEStat.SpAttack, +1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeRaiseUser_SPE_By1: Ef_Hit__MaybeChangeUserStats(user, targets, move, mData, new[] { (PBEStat.Speed, +1) }, mData.EffectParam); break;
case PBEMoveEffect.Hit__MaybeToxic: Ef_Hit(user, targets, move, mData, status1: PBEStatus1.BadlyPoisoned, chanceToInflictStatus1: mData.EffectParam); break;
case PBEMoveEffect.HPDrain: Ef_HPDrain(user, targets, move, mData); break;
case PBEMoveEffect.HPDrain__RequireSleep: Ef_HPDrain(user, targets, move, mData, requireSleep: true); break;
case PBEMoveEffect.LeechSeed: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.LeechSeed); break;
case PBEMoveEffect.LightScreen: Ef_TryForceTeamStatus(user, move, mData, PBETeamStatus.LightScreen); break;
case PBEMoveEffect.LockOn: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.LockOn); break;
case PBEMoveEffect.LowerTarget_ATK_DEF_By1: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Attack, -1), (PBEStat.Defense, -1) }); break;
case PBEMoveEffect.LowerTarget_DEF_SPDEF_By1_Raise_ATK_SPATK_SPE_By2: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Defense, -1), (PBEStat.SpDefense, -1), (PBEStat.Attack, +2), (PBEStat.SpAttack, +2), (PBEStat.Speed, +2) }); break;
case PBEMoveEffect.LuckyChant: Ef_TryForceTeamStatus(user, move, mData, PBETeamStatus.LuckyChant); break;
case PBEMoveEffect.MagnetRise: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.MagnetRise); break;
case PBEMoveEffect.Metronome: Ef_Metronome(user, move, mData); break;
case PBEMoveEffect.MiracleEye: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.MiracleEye); break;
case PBEMoveEffect.Moonlight: Ef_Moonlight(user, targets, move, mData); break;
case PBEMoveEffect.Nightmare: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.Nightmare); break;
case PBEMoveEffect.Nothing: Ef_Nothing(user, move, mData); break;
case PBEMoveEffect.OneHitKnockout: Ef_OneHitKnockout(user, targets, move, mData); break;
case PBEMoveEffect.PainSplit: Ef_PainSplit(user, targets, move, mData); break;
case PBEMoveEffect.Paralyze: Ef_TryForceStatus1(user, targets, move, mData, PBEStatus1.Paralyzed); break;
case PBEMoveEffect.PayDay: Ef_PayDay(user, targets, move, mData); break;
case PBEMoveEffect.Poison: Ef_TryForceStatus1(user, targets, move, mData, PBEStatus1.Poisoned); break;
case PBEMoveEffect.PowerTrick: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.PowerTrick); break;
case PBEMoveEffect.Protect: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.Protected); break;
case PBEMoveEffect.PsychUp: Ef_PsychUp(user, targets, move, mData); break;
case PBEMoveEffect.Psywave: Ef_Psywave(user, targets, move, mData); break;
case PBEMoveEffect.QuickGuard: Ef_TryForceTeamStatus(user, move, mData, PBETeamStatus.QuickGuard); break;
case PBEMoveEffect.RainDance: Ef_TryForceWeather(user, move, mData, PBEWeather.Rain); break;
case PBEMoveEffect.RaiseTarget_ATK_ACC_By1: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Attack, +1), (PBEStat.Accuracy, +1) }); break;
case PBEMoveEffect.RaiseTarget_ATK_DEF_By1: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Attack, +1), (PBEStat.Defense, +1) }); break;
case PBEMoveEffect.RaiseTarget_ATK_DEF_ACC_By1: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Attack, +1), (PBEStat.Defense, +1), (PBEStat.Accuracy, +1) }); break;
case PBEMoveEffect.RaiseTarget_ATK_SPATK_By1: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Attack, +1), (PBEStat.SpAttack, +1) }); break;
case PBEMoveEffect.RaiseTarget_ATK_SPE_By1: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Attack, +1), (PBEStat.Speed, +1) }); break;
case PBEMoveEffect.RaiseTarget_DEF_SPDEF_By1: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Defense, +1), (PBEStat.SpDefense, +1) }); break;
case PBEMoveEffect.RaiseTarget_SPATK_SPDEF_By1: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.SpAttack, +1), (PBEStat.SpDefense, +1) }); break;
case PBEMoveEffect.RaiseTarget_SPATK_SPDEF_SPE_By1: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.SpAttack, +1), (PBEStat.SpDefense, +1), (PBEStat.Speed, +1) }); break;
case PBEMoveEffect.RaiseTarget_SPE_By2_ATK_By1: Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Speed, +2), (PBEStat.Attack, +1) }); break;
case PBEMoveEffect.Recoil: Ef_Recoil(user, targets, move, mData); break;
case PBEMoveEffect.Recoil__10PercentBurn: Ef_Recoil(user, targets, move, mData, status1: PBEStatus1.Burned, chanceToInflictStatus1: 10); break;
case PBEMoveEffect.Recoil__10PercentParalyze: Ef_Recoil(user, targets, move, mData, status1: PBEStatus1.Paralyzed, chanceToInflictStatus1: 10); break;
case PBEMoveEffect.Reflect: Ef_TryForceTeamStatus(user, move, mData, PBETeamStatus.Reflect); break;
case PBEMoveEffect.ReflectType: Ef_ReflectType(user, targets, move, mData); break;
case PBEMoveEffect.Refresh: Ef_Refresh(user, targets, move, mData); break;
case PBEMoveEffect.Rest: Ef_Rest(user, move, mData); break;
case PBEMoveEffect.RestoreTargetHP: Ef_RestoreTargetHP(user, targets, move, mData); break;
case PBEMoveEffect.RolePlay: Ef_RolePlay(user, targets, move, mData); break;
case PBEMoveEffect.Roost: Ef_Roost(user, targets, move, mData); break;
case PBEMoveEffect.Safeguard: Ef_TryForceTeamStatus(user, move, mData, PBETeamStatus.Safeguard); break;
case PBEMoveEffect.Sandstorm: Ef_TryForceWeather(user, move, mData, PBEWeather.Sandstorm); break;
case PBEMoveEffect.SecretPower: Ef_SecretPower(user, targets, move, mData); break;
case PBEMoveEffect.SeismicToss: Ef_SeismicToss(user, targets, move, mData); break;
case PBEMoveEffect.Selfdestruct: Ef_Selfdestruct(user, targets, move, mData); break;
case PBEMoveEffect.SetDamage: Ef_SetDamage(user, targets, move, mData); break;
case PBEMoveEffect.ShadowForce: Ef_ShadowForce(user, targets, move, mData, requestedTargets); break;
case PBEMoveEffect.SimpleBeam: Ef_SetOtherAbility(user, targets, move, mData, PBEAbility.Simple, true); break;
case PBEMoveEffect.Sleep: Ef_TryForceStatus1(user, targets, move, mData, PBEStatus1.Asleep); break;
case PBEMoveEffect.SmellingSalt: Ef_SmellingSalt(user, targets, move, mData); break;
case PBEMoveEffect.Snore: Ef_Snore(user, targets, move, mData); break;
case PBEMoveEffect.Soak: Ef_Soak(user, targets, move, mData); break;
case PBEMoveEffect.Spikes: Ef_TryForceTeamStatus(user, move, mData, PBETeamStatus.Spikes); break;
case PBEMoveEffect.StealthRock: Ef_TryForceTeamStatus(user, move, mData, PBETeamStatus.StealthRock); break;
case PBEMoveEffect.Struggle: Ef_Struggle(user, targets, move, mData); break;
case PBEMoveEffect.Substitute: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.Substitute); break;
case PBEMoveEffect.SuckerPunch: Ef_SuckerPunch(user, targets, move, mData); break;
case PBEMoveEffect.SunnyDay: Ef_TryForceWeather(user, move, mData, PBEWeather.HarshSunlight); break;
case PBEMoveEffect.SuperFang: Ef_SuperFang(user, targets, move, mData); break;
case PBEMoveEffect.Swagger: Ef_Swagger(user, targets, move, mData); break;
case PBEMoveEffect.Tailwind: Ef_TryForceTeamStatus(user, move, mData, PBETeamStatus.Tailwind); break;
case PBEMoveEffect.Teleport: Ef_Teleport(user, move, mData); break;
case PBEMoveEffect.ThunderWave: Ef_TryForceStatus1(user, targets, move, mData, PBEStatus1.Paralyzed, thunderWave: true); break;
case PBEMoveEffect.Toxic: Ef_TryForceStatus1(user, targets, move, mData, PBEStatus1.BadlyPoisoned); break;
case PBEMoveEffect.ToxicSpikes: Ef_TryForceTeamStatus(user, move, mData, PBETeamStatus.ToxicSpikes); break;
case PBEMoveEffect.Transform: Ef_TryForceStatus2(user, targets, move, mData, PBEStatus2.Transformed); break;
case PBEMoveEffect.TrickRoom: Ef_TryForceBattleStatus(user, move, mData, PBEBattleStatus.TrickRoom); break;
case PBEMoveEffect.WakeUpSlap: Ef_WakeUpSlap(user, targets, move, mData); break;
case PBEMoveEffect.Whirlwind: Ef_Whirlwind(user, targets, move, mData); break;
case PBEMoveEffect.WideGuard: Ef_TryForceTeamStatus(user, move, mData, PBETeamStatus.WideGuard); break;
case PBEMoveEffect.WorrySeed: Ef_SetOtherAbility(user, targets, move, mData, PBEAbility.Insomnia, true); break;
default: throw new InvalidDataException(nameof(mData.Effect));
}
}
private bool PreMoveStatusCheck(PBEBattlePokemon user, PBEMove move)
{
IPBEMoveData mData = PBEDataProvider.Instance.GetMoveData(move);
// Verified: Sleep and Freeze don't interact with Flinch unless they come out of the status
// Sleep causes Confusion, Flinch, and Infatuation to activate if the user is trying to use Snore
if (user.Status1 == PBEStatus1.Asleep)
{
user.Status1Counter++;
if (user.Status1Counter > user.SleepTurns)
{
user.Status1 = PBEStatus1.None;
user.Status1Counter = 0;
user.SleepTurns = 0;
BroadcastStatus1(user, user, PBEStatus1.Asleep, PBEStatusAction.Ended);
CureNightmare(user, user);
}
else if (mData.Effect != PBEMoveEffect.Snore)
{
BroadcastStatus1(user, user, PBEStatus1.Asleep, PBEStatusAction.CausedImmobility);
return true;
}
}
else if (user.Status1 == PBEStatus1.Frozen)
{
if (mData.Flags.HasFlag(PBEMoveFlag.DefrostsUser) || _rand.RandomBool(20, 100))
{
user.Status1 = PBEStatus1.None;
BroadcastStatus1(user, user, PBEStatus1.Frozen, PBEStatusAction.Ended);
}
else
{
BroadcastStatus1(user, user, PBEStatus1.Frozen, PBEStatusAction.CausedImmobility);
return true;
}
}
// Verified: Flinch before Confusion, Infatuation, and Paralysis can do anything
if (user.Status2.HasFlag(PBEStatus2.Flinching))
{
BroadcastStatus2(user, user, PBEStatus2.Flinching, PBEStatusAction.CausedImmobility);
if (user.Ability == PBEAbility.Steadfast && user.SpeedChange < Settings.MaxStatChange)
{
BroadcastAbility(user, user, PBEAbility.Steadfast, PBEAbilityAction.Stats);
ApplyStatChangeIfPossible(user, user, PBEStat.Speed, +1);
}
return true;
}
// Verified: Confusion before Infatuation and Paralysis
if (user.Status2.HasFlag(PBEStatus2.Confused))
{
user.ConfusionCounter++;
if (user.ConfusionCounter > user.ConfusionTurns)
{
user.ConfusionCounter = 0;
user.ConfusionTurns = 0;
BroadcastStatus2(user, user, PBEStatus2.Confused, PBEStatusAction.Ended);
}
else
{
BroadcastStatus2(user, user, PBEStatus2.Confused, PBEStatusAction.Announced);
if (_rand.RandomBool(50, 100))
{
int damage = CalculateConfusionDamage(user);
DealDamage(user, user, damage, ignoreSturdy: false);
BroadcastStatus2(user, user, PBEStatus2.Confused, PBEStatusAction.Damage);
// BUG: Confusion damage does not activate these items
if (!FaintCheck(user) && Settings.BugFix)
{
LowHPBerryCheck(user);
}
return true;
}
}
}
// Verified: Paralysis before Infatuation
if (user.Status1 == PBEStatus1.Paralyzed && _rand.RandomBool(25, 100))
{
BroadcastStatus1(user, user, PBEStatus1.Paralyzed, PBEStatusAction.CausedImmobility);
return true;
}
// Infatuation
if (user.Status2.HasFlag(PBEStatus2.Infatuated))
{
BroadcastStatus2(user, user.InfatuatedWithPokemon!, PBEStatus2.Infatuated, PBEStatusAction.Announced);
if (_rand.RandomBool(50, 100))
{
BroadcastStatus2(user, user.InfatuatedWithPokemon!, PBEStatus2.Infatuated, PBEStatusAction.CausedImmobility);
return true;
}
}
return false;
}
private bool MissCheck(PBEBattlePokemon user, PBEBattlePokemon target, IPBEMoveData mData)
{
if (user == target)
{
return false;
}
// Verified: WideGuard happens before Protect
if (target.Team.TeamStatus.HasFlag(PBETeamStatus.WideGuard) && mData.Category != PBEMoveCategory.Status && PBEDataUtils.IsSpreadMove(user.GetMoveTargets(mData)))
{
BroadcastTeamStatusDamage(target.Team, PBETeamStatus.WideGuard, target);
return true;
}
// Feint ignores Quick Guard unless the target is an ally
if (target.Team.TeamStatus.HasFlag(PBETeamStatus.QuickGuard) && mData.Priority > 0 && (mData.Effect != PBEMoveEffect.Feint || user.Team == target.Team))
{
BroadcastTeamStatusDamage(target.Team, PBETeamStatus.QuickGuard, target);
return true;
}
if (target.Status2.HasFlag(PBEStatus2.Protected) && mData.Flags.HasFlag(PBEMoveFlag.AffectedByProtect))
{
BroadcastStatus2(target, user, PBEStatus2.Protected, PBEStatusAction.Damage);
return true;
}
if (user.Status2.HasFlag(PBEStatus2.LockOn) && user.LockOnPokemon == target)
{
return false;
}
if (user.Ability == PBEAbility.NoGuard || target.Ability == PBEAbility.NoGuard)
{
return false;
}
if (target.Status2.HasFlag(PBEStatus2.Airborne) && !(mData.Flags.HasFlag(PBEMoveFlag.DoubleDamageAirborne) || mData.Flags.HasFlag(PBEMoveFlag.HitsAirborne)))
{
goto miss;
}
if (target.Status2.HasFlag(PBEStatus2.ShadowForce))
{
goto miss;
}
if (target.Status2.HasFlag(PBEStatus2.Underground) && !(mData.Flags.HasFlag(PBEMoveFlag.DoubleDamageUnderground) || mData.Flags.HasFlag(PBEMoveFlag.HitsUnderground)))
{
goto miss;
}
if (target.Status2.HasFlag(PBEStatus2.Underwater) && !(mData.Flags.HasFlag(PBEMoveFlag.DoubleDamageUnderwater) || mData.Flags.HasFlag(PBEMoveFlag.HitsUnderwater)))
{
goto miss;
}
// These go after semi-invulnerable
float chance = mData.Accuracy;
if (chance == 0) // Moves that don't miss
{
return false;
}
if (ShouldDoWeatherEffects())
{
if (Weather == PBEWeather.Hailstorm && mData.Flags.HasFlag(PBEMoveFlag.NeverMissHail))
{
return false;
}
if (mData.Flags.HasFlag(PBEMoveFlag.NeverMissRain))
{
if (Weather == PBEWeather.Rain)
{
return false;
}
if (Weather == PBEWeather.HarshSunlight)
{
chance = Math.Min(50, chance);
}
}
}
if (mData.Effect == PBEMoveEffect.OneHitKnockout)
{
chance = user.Level - target.Level + chance;
if (chance < 1)
{
goto miss;
}
else
{
goto roll; // Skip all modifiers
}
}
if (target.Ability == PBEAbility.WonderSkin && mData.Category == PBEMoveCategory.Status && !user.HasCancellingAbility())
{
chance = Math.Min(50, chance);
}
bool ignoreA = mData.Category != PBEMoveCategory.Status && target.Ability == PBEAbility.Unaware && !user.HasCancellingAbility();
bool ignoreE = mData.Effect == PBEMoveEffect.ChipAway || (mData.Category != PBEMoveCategory.Status && user.Ability == PBEAbility.Unaware);
float accuracy = ignoreA ? 1 : GetStatChangeModifier(user.AccuracyChange, true);
float evasion;
if (ignoreE)
{
evasion = 1;
}
else
{
bool ignorePositive = target.Status2.HasFlag(PBEStatus2.Identified) || target.Status2.HasFlag(PBEStatus2.MiracleEye);
evasion = GetStatChangeModifier(ignorePositive ? Math.Min((sbyte)0, target.EvasionChange) : target.EvasionChange, true);
}
chance *= accuracy / evasion;
if (user.Ability == PBEAbility.Compoundeyes)
{
chance *= 1.3f;
}
if (user.Team.ActiveBattlers.FindIndex(p => p.Ability == PBEAbility.VictoryStar) != -1)
{
chance *= 1.1f;
}
if (user.Ability == PBEAbility.Hustle && mData.Category == PBEMoveCategory.Physical)
{
chance *= 0.8f;
}
if (!user.HasCancellingAbility() && ShouldDoWeatherEffects())
{
if (Weather == PBEWeather.Sandstorm && target.Ability == PBEAbility.SandVeil)
{
chance *= 0.8f;
}
if (Weather == PBEWeather.Hailstorm && target.Ability == PBEAbility.SnowCloak)
{
chance *= 0.8f;
}
}
if (target.Item == PBEItem.BrightPowder)
{
chance *= 0.9f;
}
if (target.Item == PBEItem.LaxIncense)
{
chance *= 0.9f;
}
if (user.Item == PBEItem.WideLens)
{
chance *= 1.1f;
}
if (target.Ability == PBEAbility.TangledFeet && target.Status2.HasFlag(PBEStatus2.Confused) && !user.HasCancellingAbility())
{
chance *= 0.5f;
}
roll:
if (_rand.RandomBool((int)chance, 100))
{
return false;
}
miss:
BroadcastMoveResult(user, target, PBEResult.Missed);
return true;
}
private bool AttackTypeCheck(PBEBattlePokemon user, PBEBattlePokemon target, PBEType moveType, out PBEResult result, out float damageMultiplier)
{
result = PBETypeEffectiveness.IsAffectedByAttack(user, target, moveType, out damageMultiplier);
if (result == PBEResult.Ineffective_Ability)
{
BroadcastAbility(target, user, target.Ability, PBEAbilityAction.Damage);
}
if (result != PBEResult.NotVeryEffective_Type && result != PBEResult.Success && result != PBEResult.SuperEffective_Type)
{
BroadcastMoveResult(user, target, result);
return false;
}
return true;
}
private bool CritCheck(PBEBattlePokemon user, PBEBattlePokemon target, IPBEMoveData mData)
{
if (((target.Ability == PBEAbility.BattleArmor || target.Ability == PBEAbility.ShellArmor) && !user.HasCancellingAbility())
|| target.Team.TeamStatus.HasFlag(PBETeamStatus.LuckyChant))
{
return false;
}
if (mData.Flags.HasFlag(PBEMoveFlag.AlwaysCrit))
{
return true;
}
byte stage = 0;
if (user.Status2.HasFlag(PBEStatus2.Pumped))
{
stage += 2;
}
if (user.OriginalSpecies == PBESpecies.Chansey && user.Item == PBEItem.LuckyPunch)
{
stage += 2;
}
if (user.OriginalSpecies == PBESpecies.Farfetchd && user.Item == PBEItem.Stick)
{
stage += 2;
}
if (mData.Flags.HasFlag(PBEMoveFlag.HighCritChance))
{
stage += 1;
}
if (user.Ability == PBEAbility.SuperLuck)
{
stage += 1;
}
if (user.Item == PBEItem.RazorClaw || user.Item == PBEItem.ScopeLens)
{
stage += 1;
}
float chance;
switch (stage)
{
case 0: chance = 6.25f; break;
case 1: chance = 12.5f; break;
case 2: chance = 25; break;
case 3: chance = 33.3f; break;
default: chance = 50; break;
}
return _rand.RandomBool((int)(chance * 100), 100 * 100);
}
private void TrySetLoser(PBEBattlePokemon pkmn)
{
if (BattleResult is null && pkmn.Team.NumConsciousPkmn == 0)
{
BattleResult = pkmn.Team.Id == 0 ? PBEBattleResult.Team1Win : PBEBattleResult.Team0Win;
}
}
private void SetEscaped(PBEBattlePokemon pkmn)
{
BattleResult = pkmn.IsWild ? PBEBattleResult.WildFlee : PBEBattleResult.WildEscape;
}
private bool FaintCheck(PBEBattlePokemon pkmn)
{
if (pkmn.HP == 0)
{
_turnOrder.Remove(pkmn);
ActiveBattlers.Remove(pkmn);
PBEFieldPosition oldPos = pkmn.FieldPosition;
pkmn.ClearForFaint();
BroadcastPkmnFainted(pkmn, oldPos);
RemoveInfatuationsAndLockOns(pkmn);
CalcEXP(pkmn);
pkmn.EXPPokemon.Clear();
pkmn.Team.MonFaintedThisTurn = true;
TrySetLoser(pkmn);
CastformCherrimCheckAll();
return true;
}
return false;
}
private bool GetManipulableChance(PBEBattlePokemon pkmn, int chance)
{
// TODO: Does the Rainbow affect abilities activating, such as CuteCharm/Static, Healer/ShedSkin, etc, and which side of the field would they activate from? Victim?
// TODO: If it does affect abilities, does it affect Effect Spore? It uses its own weird rng
if (pkmn.Ability == PBEAbility.SereneGrace)
{
chance *= 2;
}
return _rand.RandomBool(chance, 100);
}
private void ActivateAbility(PBEBattlePokemon pkmn, bool switchIn)
{
if (!switchIn)
{
CastformCherrimCheck(pkmn); // Switch-Ins check this after all Pokémon are sent out
}
AntiStatusAbilityCheck(pkmn);
switch (pkmn.Ability)
{
case PBEAbility.AirLock:
case PBEAbility.CloudNine:
{
if (switchIn)
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.Weather);
}
else
{
CastformCherrimCheckAll();
}
break;
}
case PBEAbility.Anticipation:
{
foreach (PBEBattlePokemon opponent in pkmn.Team.OpposingTeam.ActiveBattlers)
{
foreach (PBEBattleMoveset.PBEBattleMovesetSlot moveSlot in opponent.Moves)
{
PBEMove move = moveSlot.Move;
if (move != PBEMove.None)
{
IPBEMoveData mData = PBEDataProvider.Instance.GetMoveData(move);
if (mData.Category != PBEMoveCategory.Status)
{
float d = PBETypeEffectiveness.GetEffectiveness(mData.Type, pkmn);
if (d > 1)
{
BroadcastAbility(pkmn, pkmn, PBEAbility.Anticipation, PBEAbilityAction.Announced);
goto bottomAnticipation;
}
}
}
}
}
bottomAnticipation:
break;
}
case PBEAbility.Download:
{
List oppActive = pkmn.Team.OpposingTeam.ActiveBattlers;
if (oppActive.Count != 0)
{
PBEStat stat = oppActive.Average(p => p.Defense * GetStatChangeModifier(p.DefenseChange, false))
< oppActive.Average(p => p.SpDefense * GetStatChangeModifier(p.SpDefenseChange, false))
? PBEStat.Attack : PBEStat.SpAttack;
if (pkmn.GetStatChange(stat) < Settings.MaxStatChange)
{
BroadcastAbility(pkmn, pkmn, PBEAbility.Download, PBEAbilityAction.Stats);
ApplyStatChangeIfPossible(pkmn, pkmn, stat, +1);
}
}
break;
}
case PBEAbility.Drizzle:
{
if (Weather != PBEWeather.Rain || WeatherCounter != 0)
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.Weather);
SetWeather(PBEWeather.Rain, 0, switchIn);
}
break;
}
case PBEAbility.Drought:
{
if (Weather != PBEWeather.HarshSunlight || WeatherCounter != 0)
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.Weather);
SetWeather(PBEWeather.HarshSunlight, 0, switchIn);
}
break;
}
case PBEAbility.Imposter:
{
PBEFieldPosition targetPos = GetPositionAcross(BattleFormat, pkmn.FieldPosition);
if (pkmn.Team.OpposingTeam.TryGetPokemon(targetPos, out PBEBattlePokemon? target)
&& target.IsTransformPossible(pkmn) == PBEResult.Success)
{
BroadcastAbility(pkmn, target, pkmn.Ability, PBEAbilityAction.ChangedAppearance);
DoTransform(pkmn, target);
}
break;
}
case PBEAbility.Intimidate:
{
// Verified: Do not announce if the positions are empty
IReadOnlyList targets = GetRuntimeSurrounding(pkmn, false, true);
if (targets.Count > 0)
{
// Verified: Announce even if nobody is lowered (due to Substitute, Minimized Attack, or Ability)
BroadcastAbility(pkmn, pkmn, PBEAbility.Intimidate, PBEAbilityAction.Stats);
foreach (PBEBattlePokemon target in GetActingOrder(targets, true))
{
ApplyStatChangeIfPossible(pkmn, target, PBEStat.Attack, -1); // Verified: Substitute, Minimized Attack, and Ability are announced
}
}
break;
}
case PBEAbility.MoldBreaker:
case PBEAbility.Teravolt:
case PBEAbility.Turboblaze:
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.Announced);
break;
}
case PBEAbility.SandStream:
{
if (Weather != PBEWeather.Sandstorm || WeatherCounter != 0)
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.Weather);
SetWeather(PBEWeather.Sandstorm, 0, switchIn);
}
break;
}
case PBEAbility.SlowStart:
{
pkmn.SlowStart_HinderTurnsLeft = 5;
BroadcastAbility(pkmn, pkmn, PBEAbility.SlowStart, PBEAbilityAction.Announced);
break;
}
case PBEAbility.SnowWarning:
{
if (Weather != PBEWeather.Hailstorm || WeatherCounter != 0)
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.Weather);
SetWeather(PBEWeather.Hailstorm, 0, switchIn);
}
break;
}
}
}
private void CastformCherrimCheckAll()
{
CastformCherrimCheck(GetActingOrder(ActiveBattlers, true));
}
private void CastformCherrimCheck(IEnumerable order)
{
foreach (PBEBattlePokemon pkmn in order)
{
CastformCherrimCheck(pkmn);
}
}
private void CastformCherrimCheck(PBEBattlePokemon pkmn)
{
if (pkmn.HP == 0)
{
return; // #344 - Castform/Cherrim can change form while fainting from Explosion, if they kill someone with a weather-blocking ability
}
if (pkmn.Species == PBESpecies.Castform && pkmn.OriginalSpecies == PBESpecies.Castform)
{
PBEForm newForm = PBEForm.Castform;
if (pkmn.Ability == PBEAbility.Forecast && ShouldDoWeatherEffects())
{
switch (Weather)
{
case PBEWeather.Hailstorm: newForm = PBEForm.Castform_Snowy; break;
case PBEWeather.HarshSunlight: newForm = PBEForm.Castform_Sunny; break;
case PBEWeather.Rain: newForm = PBEForm.Castform_Rainy; break;
}
if (newForm != pkmn.Form)
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.ChangedAppearance);
}
}
if (newForm != pkmn.Form)
{
BroadcastPkmnFormChanged(pkmn, newForm, pkmn.Ability, pkmn.KnownAbility, false);
}
}
else if (pkmn.Species == PBESpecies.Cherrim && pkmn.OriginalSpecies == PBESpecies.Cherrim)
{
PBEForm newForm = PBEForm.Cherrim;
if (pkmn.Ability == PBEAbility.FlowerGift && ShouldDoWeatherEffects())
{
if (Weather == PBEWeather.HarshSunlight)
{
newForm = PBEForm.Cherrim_Sunshine;
}
if (newForm != pkmn.Form)
{
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.ChangedAppearance);
}
}
if (newForm != pkmn.Form)
{
BroadcastPkmnFormChanged(pkmn, newForm, pkmn.Ability, pkmn.KnownAbility, false);
}
}
}
private void ShayminCheck(PBEBattlePokemon pkmn)
{
// If a Shaymin_Sky is given MagmaArmor and then Frozen, it will change to Shaymin and obtain Shaymin's ability, therefore losing MagmaArmor and as a result will not be cured of its Frozen status.
if (pkmn.Species == PBESpecies.Shaymin && pkmn.OriginalSpecies == PBESpecies.Shaymin && pkmn.Form == PBEForm.Shaymin_Sky && pkmn.Status1 == PBEStatus1.Frozen)
{
const PBEForm newForm = PBEForm.Shaymin;
PBEAbility newAbility = PBEDataProvider.Instance.GetPokemonData(PBESpecies.Shaymin, newForm).Abilities[0];
BroadcastPkmnFormChanged(pkmn, newForm, newAbility, PBEAbility.MAX, true);
ActivateAbility(pkmn, false);
}
}
private void IllusionBreak(PBEBattlePokemon pkmn, PBEBattlePokemon breaker)
{
if (pkmn.Status2.HasFlag(PBEStatus2.Disguised))
{
pkmn.KnownGender = pkmn.Gender;
pkmn.KnownCaughtBall = pkmn.CaughtBall;
pkmn.KnownNickname = pkmn.Nickname;
pkmn.KnownShiny = pkmn.Shiny;
pkmn.KnownSpecies = pkmn.Species;
pkmn.KnownType1 = pkmn.Type1;
pkmn.KnownType2 = pkmn.Type2;
BroadcastIllusion(pkmn);
BroadcastAbility(pkmn, breaker, PBEAbility.Illusion, PBEAbilityAction.ChangedAppearance);
BroadcastStatus2(pkmn, breaker, PBEStatus2.Disguised, PBEStatusAction.Ended);
}
}
private void AntiStatusAbilityCheck(IEnumerable order)
{
foreach (PBEBattlePokemon pkmn in order)
{
AntiStatusAbilityCheck(pkmn);
}
}
private void AntiStatusAbilityCheck(PBEBattlePokemon pkmn)
{
switch (pkmn.Ability)
{
case PBEAbility.Immunity:
{
if (pkmn.Status1 == PBEStatus1.BadlyPoisoned || pkmn.Status1 == PBEStatus1.Poisoned)
{
PBEStatus1 oldStatus1 = pkmn.Status1;
pkmn.Status1 = PBEStatus1.None;
pkmn.Status1Counter = 0;
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.ChangedStatus);
BroadcastStatus1(pkmn, pkmn, oldStatus1, PBEStatusAction.Cleared);
}
break;
}
case PBEAbility.Insomnia:
case PBEAbility.VitalSpirit:
{
if (pkmn.Status1 == PBEStatus1.Asleep)
{
pkmn.Status1 = PBEStatus1.None;
pkmn.Status1Counter = 0;
pkmn.SleepTurns = 0;
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.ChangedStatus);
BroadcastStatus1(pkmn, pkmn, PBEStatus1.Asleep, PBEStatusAction.Cleared);
CureNightmare(pkmn, pkmn);
}
break;
}
case PBEAbility.Limber:
{
if (pkmn.Status1 == PBEStatus1.Paralyzed)
{
pkmn.Status1 = PBEStatus1.None;
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.ChangedStatus);
BroadcastStatus1(pkmn, pkmn, PBEStatus1.Paralyzed, PBEStatusAction.Cleared);
}
break;
}
case PBEAbility.MagmaArmor:
{
if (pkmn.Status1 == PBEStatus1.Frozen)
{
pkmn.Status1 = PBEStatus1.None;
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.ChangedStatus);
BroadcastStatus1(pkmn, pkmn, PBEStatus1.Frozen, PBEStatusAction.Cleared);
}
break;
}
case PBEAbility.Oblivious:
{
if (pkmn.Status2.HasFlag(PBEStatus2.Infatuated))
{
pkmn.InfatuatedWithPokemon = null;
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.ChangedStatus);
BroadcastStatus2(pkmn, pkmn, PBEStatus2.Infatuated, PBEStatusAction.Cleared);
}
break;
}
case PBEAbility.OwnTempo:
{
if (pkmn.Status2.HasFlag(PBEStatus2.Confused))
{
pkmn.ConfusionCounter = 0;
pkmn.ConfusionTurns = 0;
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.ChangedStatus);
BroadcastStatus2(pkmn, pkmn, PBEStatus2.Confused, PBEStatusAction.Cleared);
}
break;
}
case PBEAbility.WaterVeil:
{
if (pkmn.Status1 == PBEStatus1.Burned)
{
pkmn.Status1 = PBEStatus1.None;
BroadcastAbility(pkmn, pkmn, pkmn.Ability, PBEAbilityAction.ChangedStatus);
BroadcastStatus1(pkmn, pkmn, PBEStatus1.Burned, PBEStatusAction.Cleared);
}
break;
}
}
}
private void CauseConfusion(PBEBattlePokemon target, PBEBattlePokemon other)
{
target.ConfusionCounter = 0;
target.ConfusionTurns = (byte)_rand.RandomInt(Settings.ConfusionMinTurns, Settings.ConfusionMaxTurns);
BroadcastStatus2(target, other, PBEStatus2.Confused, PBEStatusAction.Added);
AntiStatusAbilityCheck(target);
}
private void CauseInfatuation(PBEBattlePokemon target, PBEBattlePokemon other)
{
target.InfatuatedWithPokemon = other;
BroadcastStatus2(target, other, PBEStatus2.Infatuated, PBEStatusAction.Added);
if (target.Item == PBEItem.DestinyKnot && other.IsAttractionPossible(target) == PBEResult.Success)
{
BroadcastItem(target, other, PBEItem.DestinyKnot, PBEItemAction.Announced);
other.InfatuatedWithPokemon = target;
BroadcastStatus2(other, target, PBEStatus2.Infatuated, PBEStatusAction.Added);
}
AntiStatusAbilityCheck(target);
}
// TODO: Use & add packet handlers
private void WhiteHerbCheck(PBEBattlePokemon pkmn)
{
if (pkmn.Item == PBEItem.WhiteHerb)
{
PBEStat[] negStats = pkmn.GetStatsLessThan(0);
if (negStats.Length > 0)
{
foreach (PBEStat s in negStats)
{
pkmn.SetStatChange(s, 0);
}
BroadcastItem(pkmn, pkmn, PBEItem.WhiteHerb, PBEItemAction.Consumed);
}
}
}
private bool PowerHerbCheck(PBEBattlePokemon pkmn)
{
if (pkmn.Item == PBEItem.PowerHerb)
{
BroadcastItem(pkmn, pkmn, PBEItem.PowerHerb, PBEItemAction.Consumed);
return true;
}
return false;
}
private void LowHPBerryCheck(IEnumerable order, PBEBattlePokemon? forcedToEatBy = null)
{
foreach (PBEBattlePokemon pkmn in order)
{
LowHPBerryCheck(pkmn, forcedToEatBy: forcedToEatBy);
}
}
private void LowHPBerryCheck(PBEBattlePokemon pkmn, PBEBattlePokemon? forcedToEatBy = null)
{
forcedToEatBy ??= pkmn;
void DoConfuseBerry(PBEFlavor flavor)
{
BroadcastItem(pkmn, forcedToEatBy, pkmn.Item, PBEItemAction.Consumed);
HealDamage(pkmn, pkmn.MaxHP / 8);
// Verified: Ignores Safeguard & Substitute, but not Own Tempo
// Mold Breaker etc actually affect whether Own Tempo is ignored, which is what forcedToEatBy is for
// I verified each of the times the Pokémon eats to check if Mold Breaker affected the outcome
if (pkmn.Nature.GetRelationshipToFlavor(flavor) < 0 && pkmn.IsConfusionPossible(forcedToEatBy, ignoreSubstitute: true, ignoreSafeguard: true) == PBEResult.Success)
{
CauseConfusion(pkmn, forcedToEatBy);
}
}
void DoHealItem(int hp)
{
BroadcastItem(pkmn, forcedToEatBy, pkmn.Item, PBEItemAction.Consumed);
HealDamage(pkmn, hp);
}
void DoStatItem(PBEStat stat, int change)
{
// Verified: Mold Breaker affects Contrary/Simple here, unlike with Belly Drum
if (pkmn.IsStatChangePossible(stat, forcedToEatBy, change, out sbyte oldValue, out sbyte newValue, ignoreSubstitute: true) == PBEResult.Success)
{
BroadcastItem(pkmn, forcedToEatBy, pkmn.Item, PBEItemAction.Consumed);
SetStatAndBroadcast(pkmn, stat, oldValue, newValue);
}
}
if (pkmn.HP <= pkmn.MaxHP / 4)
{
switch (pkmn.Item)
{
case PBEItem.ApicotBerry: DoStatItem(PBEStat.SpDefense, +1); break;
case PBEItem.GanlonBerry: DoStatItem(PBEStat.Defense, +1); break;
case PBEItem.LiechiBerry: DoStatItem(PBEStat.Attack, +1); break;
case PBEItem.PetayaBerry: DoStatItem(PBEStat.SpAttack, +1); break;
case PBEItem.SalacBerry: DoStatItem(PBEStat.Speed, +1); break;
case PBEItem.StarfBerry:
{
// Verified: Starf Berry does not activate for Accuracy or Evasion, or if all other stats are maximized
List statsThatCanGoUp = PBEDataUtils.StarfBerryStats.FindAll(s => pkmn.GetStatChange(s) < Settings.MaxStatChange);
if (statsThatCanGoUp.Count > 0)
{
DoStatItem(_rand.RandomElement(statsThatCanGoUp), +2);
}
break;
}
}
}
if (pkmn.HP <= pkmn.MaxHP / 2)
{
switch (pkmn.Item)
{
case PBEItem.AguavBerry: DoConfuseBerry(PBEFlavor.Bitter); break;
case PBEItem.BerryJuice: DoHealItem(20); break;
case PBEItem.FigyBerry: DoConfuseBerry(PBEFlavor.Spicy); break;
case PBEItem.IapapaBerry: DoConfuseBerry(PBEFlavor.Sour); break;
case PBEItem.MagoBerry: DoConfuseBerry(PBEFlavor.Sweet); break;
case PBEItem.OranBerry: DoHealItem(10); break;
case PBEItem.SitrusBerry: DoHealItem(pkmn.MaxHP / 4); break;
case PBEItem.WikiBerry: DoConfuseBerry(PBEFlavor.Dry); break;
}
}
}
private void SetAbility(PBEBattlePokemon user, PBEBattlePokemon target, PBEAbility ability)
{
// This func assumes new ability is different from current
PBEAbility oldAbility = target.Ability;
BroadcastAbilityReplaced(target, ability);
switch (oldAbility)
{
case PBEAbility.Illusion:
{
IllusionBreak(target, user);
break;
}
case PBEAbility.SlowStart:
{
target.SlowStart_HinderTurnsLeft = 0;
break;
}
case PBEAbility.SpeedBoost:
{
target.SpeedBoost_AbleToSpeedBoostThisTurn = false;
break;
}
}
ActivateAbility(target, false);
}
private void SetWeather(PBEWeather weather, byte weatherCounter, bool switchIn)
{
Weather = weather;
WeatherCounter = weatherCounter;
BroadcastWeather(Weather, PBEWeatherAction.Added);
if (!switchIn)
{
CastformCherrimCheckAll(); // Switch-Ins check this after all Pokémon are sent out
}
}
private void RecordExecutedMove(PBEBattlePokemon user, PBEMove move, IPBEMoveData mData)
{
user.HasUsedMoveThisTurn = true;
// Doesn't care if there is a Choice Locked move already. As long as the user knows it, it will become locked. (Metronome calling a move the user knows, Ditto transforming into someone else with transform)
if ((user.Item == PBEItem.ChoiceBand || user.Item == PBEItem.ChoiceScarf || user.Item == PBEItem.ChoiceSpecs) && user.Moves.Contains(move))
{
BroadcastMoveLock_ChoiceItem(user, move);
}
if (mData.Effect == PBEMoveEffect.Minimize)
{
user.Minimize_Used = true;
}
}
private void PPReduce(PBEBattlePokemon pkmn, PBEMove move)
{
if (!_calledFromOtherMove)
{
const int amountToReduce = 1;
// TODO: If target is not self and has pressure
PBEBattleMoveset.PBEBattleMovesetSlot slot = pkmn.Moves[move]!;
int oldPP = slot.PP;
int newPP = Math.Max(0, oldPP - amountToReduce);
int amountReduced = oldPP - newPP;
slot.PP = newPP;
pkmn.UpdateKnownPP(move, amountReduced);
BroadcastMovePPChanged(pkmn, move, amountReduced);
}
}
private static void ApplyBigRoot(PBEBattlePokemon pkmn, ref int restoreAmt)
{
if (pkmn.Item == PBEItem.BigRoot)
{
restoreAmt += (int)(restoreAmt * 0.3);
}
}
private void CureNightmare(PBEBattlePokemon wakingUp, PBEBattlePokemon pokemon2)
{
if (wakingUp.Status2.HasFlag(PBEStatus2.Nightmare))
{
BroadcastStatus2(wakingUp, pokemon2, PBEStatus2.Nightmare, PBEStatusAction.Ended);
}
}
private void SetSleepTurns(PBEBattlePokemon pkmn, int minTurns, int maxTurns)
{
pkmn.SleepTurns = (byte)(_rand.RandomInt(minTurns, maxTurns) / (pkmn.Ability == PBEAbility.EarlyBird ? 2 : 1));
}
private void DoTransform(PBEBattlePokemon user, PBEBattlePokemon target)
{
user.Transform(target);
BroadcastTransform(user, target);
BroadcastStatus2(user, target, PBEStatus2.Transformed, PBEStatusAction.Added);
// Remove power trick (so it cannot be baton passed)
if (user.Status2.HasFlag(PBEStatus2.PowerTrick))
{
BroadcastStatus2(user, user, PBEStatus2.PowerTrick, PBEStatusAction.Ended);
}
if (!user.Moves.Contains(user.ChoiceLockedMove))
{
BroadcastMoveLock_ChoiceItem(user, PBEMove.None);
}
}
private PBEResult ApplyStatus1IfPossible(PBEBattlePokemon user, PBEBattlePokemon target, PBEStatus1 status, bool broadcastUnsuccessful)
{
PBEResult result;
switch (status)
{
case PBEStatus1.Asleep: result = target.IsSleepPossible(user); break;
case PBEStatus1.BadlyPoisoned:
case PBEStatus1.Poisoned: result = target.IsPoisonPossible(user); break;
case PBEStatus1.Burned: result = target.IsBurnPossible(user); break;
case PBEStatus1.Frozen: result = target.IsFreezePossible(user); break;
case PBEStatus1.Paralyzed: result = target.IsParalysisPossible(user); break;
default: throw new ArgumentOutOfRangeException(nameof(status));
}
if (result == PBEResult.Success)
{
target.Status1 = status;
if (status == PBEStatus1.BadlyPoisoned)
{
target.Status1Counter = 1;
}
else if (status == PBEStatus1.Asleep)
{
SetSleepTurns(target, Settings.SleepMinTurns, Settings.SleepMaxTurns);
target.Status1Counter = 0;
}
BroadcastStatus1(target, user, status, PBEStatusAction.Added);
ShayminCheck(target);
}
else if (broadcastUnsuccessful)
{
if (result == PBEResult.Ineffective_Ability)
{
BroadcastAbility(target, user, target.Ability, PBEAbilityAction.PreventedStatus);
}
BroadcastMoveResult(user, target, result);
}
return result;
}
private PBEResult ApplyStatus2IfPossible(PBEBattlePokemon user, PBEBattlePokemon target, PBEStatus2 status, bool broadcastUnsuccessful)
{
PBEResult result;
switch (status)
{
case PBEStatus2.Confused:
{
result = target.IsConfusionPossible(user);
if (result == PBEResult.Success)
{
CauseConfusion(target, user);
}
break;
}
case PBEStatus2.Cursed:
{
if (!target.Status2.HasFlag(PBEStatus2.Cursed))
{
BroadcastStatus2(target, user, PBEStatus2.Cursed, PBEStatusAction.Added);
DealDamage(user, user, user.MaxHP / 2);
if (!FaintCheck(user))
{
LowHPBerryCheck(user);
}
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBEStatus2.Flinching:
{
result = target.IsFlinchPossible(user);
if (result == PBEResult.Success)
{
target.Status2 |= PBEStatus2.Flinching; // No broadcast, not known
}
break;
}
case PBEStatus2.HelpingHand:
{
if (!target.HasUsedMoveThisTurn)
{
BroadcastStatus2(target, user, PBEStatus2.HelpingHand, PBEStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.InvalidConditions;
}
break;
}
case PBEStatus2.Identified:
{
if (!target.Status2.HasFlag(PBEStatus2.Identified))
{
BroadcastStatus2(target, user, PBEStatus2.Identified, PBEStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBEStatus2.Infatuated:
{
result = target.IsAttractionPossible(user);
if (result == PBEResult.Success)
{
CauseInfatuation(target, user);
}
break;
}
case PBEStatus2.LeechSeed:
{
result = target.IsLeechSeedPossible();
if (result == PBEResult.Success)
{
target.SeededPosition = user.FieldPosition;
target.SeededTeam = user.Team;
BroadcastStatus2(target, user, PBEStatus2.LeechSeed, PBEStatusAction.Added);
}
break;
}
case PBEStatus2.LockOn:
{
if (!user.Status2.HasFlag(PBEStatus2.LockOn))
{
user.LockOnPokemon = target;
user.LockOnTurns = 2;
BroadcastStatus2(user, target, PBEStatus2.LockOn, PBEStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBEStatus2.MagnetRise:
{
result = target.IsMagnetRisePossible();
if (result == PBEResult.Success)
{
target.MagnetRiseTurns = 5;
BroadcastStatus2(target, user, PBEStatus2.MagnetRise, PBEStatusAction.Added);
}
break;
}
case PBEStatus2.MiracleEye:
{
if (!target.Status2.HasFlag(PBEStatus2.MiracleEye))
{
BroadcastStatus2(target, user, PBEStatus2.MiracleEye, PBEStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBEStatus2.Nightmare:
{
if (target.Status1 == PBEStatus1.Asleep && !target.Status2.HasFlag(PBEStatus2.Nightmare))
{
BroadcastStatus2(target, user, PBEStatus2.Nightmare, PBEStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBEStatus2.PowerTrick:
{
target.ApplyPowerTrickChange();
BroadcastStatus2(target, user, PBEStatus2.PowerTrick, PBEStatusAction.Added);
result = PBEResult.Success;
break;
}
case PBEStatus2.Protected:
{
// TODO: If the user goes last, fail
if (_rand.RandomBool(user.GetProtectionChance(), ushort.MaxValue))
{
user.Protection_Used = true;
BroadcastStatus2(user, user, PBEStatus2.Protected, PBEStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.InvalidConditions;
}
break;
}
case PBEStatus2.Pumped:
{
if (!target.Status2.HasFlag(PBEStatus2.Pumped))
{
BroadcastStatus2(target, user, PBEStatus2.Pumped, PBEStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBEStatus2.Substitute:
{
result = target.IsSubstitutePossible();
if (result == PBEResult.Success)
{
int hpRequired = target.MaxHP / 4;
DealDamage(user, target, hpRequired);
LowHPBerryCheck(target); // Verified: Berry is eaten between damage and substitute
target.SubstituteHP = (ushort)hpRequired;
BroadcastStatus2(target, user, PBEStatus2.Substitute, PBEStatusAction.Added);
}
break;
}
case PBEStatus2.Transformed:
{
result = target.IsTransformPossible(user);
if (result == PBEResult.Success)
{
DoTransform(user, target);
}
break;
}
default: throw new ArgumentOutOfRangeException(nameof(status));
}
if (broadcastUnsuccessful && result != PBEResult.Success)
{
if (result == PBEResult.Ineffective_Ability)
{
BroadcastAbility(target, user, target.Ability, PBEAbilityAction.PreventedStatus);
}
BroadcastMoveResult(user, target, result);
}
return result;
}
private void ApplyStatChangeIfPossible(PBEBattlePokemon user, PBEBattlePokemon target, PBEStat stat, int change, bool isSecondaryEffect = false)
{
PBEResult result = target.IsStatChangePossible(stat, user, change, out sbyte oldValue, out sbyte newValue);
bool broadcast;
if (result == PBEResult.Success)
{
target.SetStatChange(stat, newValue);
broadcast = true;
}
else
{
if (result == PBEResult.Ineffective_Ability)
{
if (!isSecondaryEffect)
{
BroadcastAbility(target, user, target.Ability, PBEAbilityAction.Stats);
}
return;
}
if (result == PBEResult.Ineffective_Substitute)
{
if (!isSecondaryEffect)
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Substitute);
}
return;
}
// Do not broadcast "could not be lowered!" for Mud-Slap, etc
broadcast = !isSecondaryEffect;
}
if (broadcast)
{
BroadcastPkmnStatChanged(target, stat, oldValue, newValue);
}
}
private void SetStatAndBroadcast(PBEBattlePokemon pkmn, PBEStat stat, sbyte oldValue, sbyte newValue)
{
pkmn.SetStatChange(stat, newValue);
BroadcastPkmnStatChanged(pkmn, stat, oldValue, newValue);
}
private static PBEPkmnAppearedInfo CreateSwitchInInfo(PBEBattlePokemon pkmn)
{
if (pkmn.Ability == PBEAbility.Illusion)
{
PBEBattlePokemon? p = pkmn.GetPkmnWouldDisguiseAs();
if (p is not null)
{
pkmn.Status2 |= PBEStatus2.Disguised; // No broadcast, not known
pkmn.KnownGender = p.Gender;
pkmn.KnownCaughtBall = p.CaughtBall;
pkmn.KnownNickname = p.Nickname;
pkmn.KnownShiny = p.Shiny;
pkmn.KnownSpecies = p.OriginalSpecies;
pkmn.KnownForm = p.Form;
IPBEPokemonData pData = PBEDataProvider.Instance.GetPokemonData(pkmn.KnownSpecies, pkmn.KnownForm);
pkmn.KnownType1 = pData.Type1;
pkmn.KnownType2 = pData.Type2;
}
}
return new PBEPkmnAppearedInfo(pkmn);
}
private void SwitchTwoPokemon(PBEBattlePokemon pkmnLeaving, PBEBattlePokemon pkmnComing, PBEBattlePokemon? forcedByPkmn = null)
{
_turnOrder.Remove(pkmnLeaving);
ActiveBattlers.Remove(pkmnLeaving);
PBEFieldPosition pos = pkmnLeaving.FieldPosition;
pkmnLeaving.ClearForSwitch();
BroadcastPkmnSwitchOut(pkmnLeaving, pos, forcedByPkmn);
RemoveInfatuationsAndLockOns(pkmnLeaving);
pkmnComing.FieldPosition = pos;
var switches = new PBEPkmnAppearedInfo[] { CreateSwitchInInfo(pkmnComing) };
PBETrainer.SwitchTwoPokemon(pkmnLeaving, pkmnComing); // Swap after Illusion
ActiveBattlers.Add(pkmnComing); // Add to active before broadcast
BroadcastPkmnSwitchIn(pkmnComing.Trainer, switches, forcedByPkmn);
if (forcedByPkmn is not null)
{
BroadcastDraggedOut(pkmnComing);
}
DoSwitchInEffects(new[] { pkmnComing }, forcedByPkmn);
CastformCherrimCheckAll();
}
private void RemoveInfatuationsAndLockOns(PBEBattlePokemon pkmnLeaving)
{
foreach (PBEBattlePokemon pkmn in ActiveBattlers)
{
if (pkmn.Status2.HasFlag(PBEStatus2.Infatuated) && pkmn.InfatuatedWithPokemon == pkmnLeaving)
{
pkmn.InfatuatedWithPokemon = null;
BroadcastStatus2(pkmn, pkmn, PBEStatus2.Infatuated, PBEStatusAction.Ended);
}
if (pkmn.Status2.HasFlag(PBEStatus2.LockOn) && pkmn.LockOnPokemon == pkmnLeaving)
{
pkmn.LockOnPokemon = null;
pkmn.LockOnTurns = 0;
BroadcastStatus2(pkmn, pkmn, PBEStatus2.LockOn, PBEStatusAction.Ended);
}
}
}
private void SemiInvulnerableChargeMove(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, PBETurnTarget requestedTargets, PBEStatus2 status2,
Action? beforePostHit = null,
Action? afterPostHit = null)
{
BroadcastMoveUsed(user, move);
top:
if (user.Status2.HasFlag(status2))
{
BroadcastMoveLock_Temporary(user, PBEMove.None, PBETurnTarget.None);
BroadcastStatus2(user, user, status2, PBEStatusAction.Ended);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
BasicHit(user, targets, mData, beforePostHit: beforePostHit, afterPostHit: afterPostHit);
}
RecordExecutedMove(user, move, mData); // Should only count as the last used move if it finishes charging
}
else
{
PPReduce(user, move);
BroadcastMoveLock_Temporary(user, move, requestedTargets);
BroadcastStatus2(user, user, status2, PBEStatusAction.Added);
if (PowerHerbCheck(user))
{
goto top;
}
}
}
private void Ef_TryForceStatus1(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, PBEStatus1 status, bool thunderWave = false)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
if (thunderWave)
{
PBEResult result = PBETypeEffectiveness.ThunderWaveTypeCheck(user, target, move);
if (result != PBEResult.Success)
{
BroadcastMoveResult(user, target, result);
continue;
}
}
ApplyStatus1IfPossible(user, target, status, true);
AntiStatusAbilityCheck(target);
}
}
}
RecordExecutedMove(user, move, mData);
return;
}
private void Ef_TryForceStatus2(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, PBEStatus2 status)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
ApplyStatus2IfPossible(user, target, status, true);
AntiStatusAbilityCheck(target);
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_TryForceBattleStatus(PBEBattlePokemon user, PBEMove move, IPBEMoveData mData, PBEBattleStatus status)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
switch (status)
{
case PBEBattleStatus.TrickRoom:
{
if (!BattleStatus.HasFlag(PBEBattleStatus.TrickRoom))
{
TrickRoomCount = 5;
BroadcastBattleStatus(PBEBattleStatus.TrickRoom, PBEBattleStatusAction.Added);
}
else
{
TrickRoomCount = 0;
BroadcastBattleStatus(PBEBattleStatus.TrickRoom, PBEBattleStatusAction.Cleared);
}
break;
}
default: throw new ArgumentOutOfRangeException(nameof(status));
}
RecordExecutedMove(user, move, mData);
}
private void Ef_TryForceTeamStatus(PBEBattlePokemon user, PBEMove move, IPBEMoveData mData, PBETeamStatus status)
{
PBEResult result;
BroadcastMoveUsed(user, move);
PPReduce(user, move);
switch (status)
{
case PBETeamStatus.LightScreen:
{
if (!user.Team.TeamStatus.HasFlag(PBETeamStatus.LightScreen))
{
user.Team.LightScreenCount = (byte)(Settings.LightScreenTurns + (user.Item == PBEItem.LightClay ? Settings.LightClayTurnExtension : 0));
BroadcastTeamStatus(user.Team, PBETeamStatus.LightScreen, PBETeamStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBETeamStatus.LuckyChant:
{
if (!user.Team.TeamStatus.HasFlag(PBETeamStatus.LuckyChant))
{
user.Team.LuckyChantCount = 5;
BroadcastTeamStatus(user.Team, PBETeamStatus.LuckyChant, PBETeamStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBETeamStatus.QuickGuard:
{
if (!user.Team.TeamStatus.HasFlag(PBETeamStatus.QuickGuard) && _rand.RandomBool(user.GetProtectionChance(), ushort.MaxValue))
{
user.Protection_Used = true;
BroadcastTeamStatus(user.Team, PBETeamStatus.QuickGuard, PBETeamStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBETeamStatus.Reflect:
{
if (!user.Team.TeamStatus.HasFlag(PBETeamStatus.Reflect))
{
user.Team.ReflectCount = (byte)(Settings.ReflectTurns + (user.Item == PBEItem.LightClay ? Settings.LightClayTurnExtension : 0));
BroadcastTeamStatus(user.Team, PBETeamStatus.Reflect, PBETeamStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBETeamStatus.Safeguard:
{
if (!user.Team.TeamStatus.HasFlag(PBETeamStatus.Safeguard))
{
user.Team.SafeguardCount = 5;
BroadcastTeamStatus(user.Team, PBETeamStatus.Safeguard, PBETeamStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBETeamStatus.Spikes:
{
if (user.Team.OpposingTeam.SpikeCount < 3)
{
user.Team.OpposingTeam.SpikeCount++;
BroadcastTeamStatus(user.Team.OpposingTeam, PBETeamStatus.Spikes, PBETeamStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBETeamStatus.StealthRock:
{
if (!user.Team.OpposingTeam.TeamStatus.HasFlag(PBETeamStatus.StealthRock))
{
BroadcastTeamStatus(user.Team.OpposingTeam, PBETeamStatus.StealthRock, PBETeamStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBETeamStatus.Tailwind:
{
if (!user.Team.TeamStatus.HasFlag(PBETeamStatus.Tailwind))
{
user.Team.TailwindCount = 4;
BroadcastTeamStatus(user.Team, PBETeamStatus.Tailwind, PBETeamStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBETeamStatus.ToxicSpikes:
{
if (user.Team.OpposingTeam.ToxicSpikeCount < 2)
{
user.Team.OpposingTeam.ToxicSpikeCount++;
BroadcastTeamStatus(user.Team.OpposingTeam, PBETeamStatus.ToxicSpikes, PBETeamStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
case PBETeamStatus.WideGuard:
{
if (!user.Team.TeamStatus.HasFlag(PBETeamStatus.WideGuard) && _rand.RandomBool(user.GetProtectionChance(), ushort.MaxValue))
{
user.Protection_Used = true;
BroadcastTeamStatus(user.Team, PBETeamStatus.WideGuard, PBETeamStatusAction.Added);
result = PBEResult.Success;
}
else
{
result = PBEResult.Ineffective_Status;
}
break;
}
default: throw new ArgumentOutOfRangeException(nameof(status));
}
if (result != PBEResult.Success)
{
BroadcastMoveResult(user, user, result);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_TryForceWeather(PBEBattlePokemon user, PBEMove move, IPBEMoveData mData, PBEWeather weather)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (Weather == weather)
{
BroadcastMoveResult(user, user, PBEResult.Ineffective_Status);
}
else
{
byte turns;
PBEItem extensionItem;
byte itemTurnExtension;
switch (weather)
{
case PBEWeather.Hailstorm:
{
turns = Settings.HailTurns;
extensionItem = PBEItem.IcyRock;
itemTurnExtension = Settings.IcyRockTurnExtension;
break;
}
case PBEWeather.HarshSunlight:
{
turns = Settings.SunTurns;
extensionItem = PBEItem.HeatRock;
itemTurnExtension = Settings.HeatRockTurnExtension;
break;
}
case PBEWeather.Rain:
{
turns = Settings.RainTurns;
extensionItem = PBEItem.DampRock;
itemTurnExtension = Settings.DampRockTurnExtension;
break;
}
case PBEWeather.Sandstorm:
{
turns = Settings.SandstormTurns;
extensionItem = PBEItem.SmoothRock;
itemTurnExtension = Settings.SmoothRockTurnExtension;
break;
}
default: throw new ArgumentOutOfRangeException(nameof(weather));
}
SetWeather(weather, (byte)(turns + (user.Item == extensionItem ? itemTurnExtension : 0)), false);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Growth(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
int change = WillLeafGuardActivate() ? +2 : +1;
Ef_ChangeTargetStats(user, targets, move, mData, new[] { (PBEStat.Attack, change), (PBEStat.SpAttack, change) });
}
private void Ef_ChangeTargetStats(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, (PBEStat, int)[] statChanges, bool requireAttraction = false)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
PBEResult result = requireAttraction ? target.IsAttractionPossible(user, ignoreCurrentStatus: true) : PBEResult.Success;
if (result != PBEResult.Success)
{
BroadcastMoveResult(user, target, result);
}
else if (user != target && target.Status2.HasFlag(PBEStatus2.Substitute))
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Substitute);
}
else
{
for (int i = 0; i < statChanges.Length; i++)
{
(PBEStat stat, int change) = statChanges[i];
ApplyStatChangeIfPossible(user, target, stat, change);
}
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Hit__MaybeChangeTargetStats(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, (PBEStat, int)[] statChanges, int chanceToChangeStats)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
void BeforePostHit(PBEBattlePokemon target, ushort damageDealt)
{
if (target.HP == 0 || !GetManipulableChance(user, chanceToChangeStats))
{
return;
}
for (int i = 0; i < statChanges.Length; i++)
{
(PBEStat stat, int change) = statChanges[i];
ApplyStatChangeIfPossible(user, target, stat, change, isSecondaryEffect: true);
}
}
BasicHit(user, targets, mData, beforePostHit: BeforePostHit);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Hit__MaybeChangeUserStats(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, (PBEStat, int)[] statChanges, int chanceToChangeStats)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
void BeforePostHit(PBEBattlePokemon target, ushort damageDealt)
{
if (user.HP > 0 && GetManipulableChance(user, chanceToChangeStats))
{
for (int i = 0; i < statChanges.Length; i++)
{
(PBEStat stat, int change) = statChanges[i];
ApplyStatChangeIfPossible(user, user, stat, change, isSecondaryEffect: true);
}
}
}
BasicHit(user, targets, mData, beforePostHit: BeforePostHit);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Entrainment(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
var blockedUserAbilities = new PBEAbility[] { PBEAbility.FlowerGift, PBEAbility.Forecast, PBEAbility.Illusion,
PBEAbility.Imposter, PBEAbility.Trace };
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
if (target.Ability == user.Ability || blockedUserAbilities.Contains(user.Ability))
{
BroadcastMoveResult(user, target, PBEResult.InvalidConditions);
}
else if (target.Ability == PBEAbility.Multitype || target.Ability == PBEAbility.Truant)
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Ability);
}
else
{
SetAbility(user, target, user.Ability);
// TODO: #234 - Reveal other Pokémon's Ability
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_RolePlay(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
var blockedUserAbilities = new PBEAbility[] { PBEAbility.Imposter, PBEAbility.Multitype, PBEAbility.ZenMode };
var blockedTargetAbilities = new PBEAbility[] { PBEAbility.FlowerGift, PBEAbility.Forecast, PBEAbility.Illusion,
PBEAbility.Imposter, PBEAbility.Multitype, PBEAbility.Trace, PBEAbility.WonderGuard, PBEAbility.ZenMode };
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
if (target.Ability == user.Ability || blockedUserAbilities.Contains(user.Ability) || blockedTargetAbilities.Contains(target.Ability))
{
BroadcastMoveResult(user, target, PBEResult.InvalidConditions);
}
else
{
SetAbility(user, user, target.Ability);
// TODO: #234 - Reveal other Pokémon's Ability
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_SetOtherAbility(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, PBEAbility ability, bool blockedByTruant)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
if (target.Ability == ability)
{
BroadcastMoveResult(user, target, PBEResult.InvalidConditions);
}
else if (target.Ability == PBEAbility.Multitype || (blockedByTruant && target.Ability == PBEAbility.Truant))
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Ability);
}
else
{
SetAbility(user, target, ability);
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Bounce(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, PBETurnTarget requestedTargets)
{
void BeforePostHit(PBEBattlePokemon target, ushort damageDealt)
{
if (target.HP > 0 && GetManipulableChance(user, mData.EffectParam))
{
ApplyStatus1IfPossible(user, target, PBEStatus1.Paralyzed, false);
}
}
SemiInvulnerableChargeMove(user, targets, move, mData, requestedTargets, PBEStatus2.Airborne, beforePostHit: BeforePostHit);
}
private void Ef_ShadowForce(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, PBETurnTarget requestedTargets)
{
void AfterPostHit(PBEBattlePokemon target)
{
if (target.HP > 0 && target.Status2.HasFlag(PBEStatus2.Protected))
{
BroadcastStatus2(target, user, PBEStatus2.Protected, PBEStatusAction.Cleared);
}
if (target.Team.TeamStatus.HasFlag(PBETeamStatus.QuickGuard))
{
BroadcastTeamStatus(target.Team, PBETeamStatus.QuickGuard, PBETeamStatusAction.Cleared);
}
if (target.Team.TeamStatus.HasFlag(PBETeamStatus.WideGuard))
{
BroadcastTeamStatus(target.Team, PBETeamStatus.WideGuard, PBETeamStatusAction.Cleared);
}
}
SemiInvulnerableChargeMove(user, targets, move, mData, requestedTargets, PBEStatus2.ShadowForce, afterPostHit: AfterPostHit);
}
private void Ef_BrickBreak(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
void BeforeDoingDamage(PBEBattlePokemon target)
{
// Verified: Reflect then Light Screen
if (target.Team.TeamStatus.HasFlag(PBETeamStatus.Reflect))
{
target.Team.ReflectCount = 0;
BroadcastTeamStatus(target.Team, PBETeamStatus.Reflect, PBETeamStatusAction.Cleared);
}
if (target.Team.TeamStatus.HasFlag(PBETeamStatus.LightScreen))
{
target.Team.LightScreenCount = 0;
BroadcastTeamStatus(target.Team, PBETeamStatus.LightScreen, PBETeamStatusAction.Cleared);
}
}
BasicHit(user, targets, mData, beforeDoingDamage: BeforeDoingDamage);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Feint(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
void BeforeDoingDamage(PBEBattlePokemon target)
{
if (target.HP > 0 && target.Status2.HasFlag(PBEStatus2.Protected))
{
BroadcastStatus2(target, user, PBEStatus2.Protected, PBEStatusAction.Cleared);
}
if (target.Team == user.Team.OpposingTeam)
{
if (target.Team.TeamStatus.HasFlag(PBETeamStatus.QuickGuard))
{
BroadcastTeamStatus(target.Team, PBETeamStatus.QuickGuard, PBETeamStatusAction.Cleared);
}
if (target.Team.TeamStatus.HasFlag(PBETeamStatus.WideGuard))
{
BroadcastTeamStatus(target.Team, PBETeamStatus.WideGuard, PBETeamStatusAction.Cleared);
}
}
}
BasicHit(user, targets, mData, beforeDoingDamage: BeforeDoingDamage);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Hit(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, PBEStatus1 status1 = PBEStatus1.None, int chanceToInflictStatus1 = 0, PBEStatus2 status2 = PBEStatus2.None, int chanceToInflictStatus2 = 0)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
void BeforePostHit(PBEBattlePokemon target, ushort damageDealt)
{
if (target.HP == 0)
{
return;
}
if (status1 != PBEStatus1.None && GetManipulableChance(user, chanceToInflictStatus1))
{
ApplyStatus1IfPossible(user, target, status1, false);
}
if (status2 != PBEStatus2.None && GetManipulableChance(user, chanceToInflictStatus2))
{
ApplyStatus2IfPossible(user, target, status2, false);
}
}
BasicHit(user, targets, mData, beforePostHit: status1 != PBEStatus1.None || status2 != PBEStatus2.None ? BeforePostHit : null);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Hit__MaybeBurnFreezeParalyze(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
void BeforePostHit(PBEBattlePokemon target, ushort damageDealt)
{
if (target.HP == 0 || !GetManipulableChance(user, mData.EffectParam))
{
return;
}
PBEStatus1 status1;
int val = _rand.RandomInt(0, 2);
if (val == 0)
{
status1 = PBEStatus1.Burned;
}
else if (val == 1)
{
status1 = PBEStatus1.Frozen;
}
else
{
status1 = PBEStatus1.Paralyzed;
}
ApplyStatus1IfPossible(user, target, status1, false);
}
BasicHit(user, targets, mData, beforePostHit: BeforePostHit);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_MultiHit(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, byte numHits, bool subsequentMissChecks = false, PBEStatus1 status1 = PBEStatus1.None, int chanceToInflictStatus1 = 0)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
void BeforePostHit(PBEBattlePokemon target)
{
if (target.HP > 0 && GetManipulableChance(user, chanceToInflictStatus1))
{
ApplyStatus1IfPossible(user, target, status1, false);
}
}
MultiHit(user, targets, mData, numHits, subsequentMissChecks: subsequentMissChecks, beforePostHit: status1 != PBEStatus1.None ? BeforePostHit : null); // Doesn't need to be its own func but neater
}
RecordExecutedMove(user, move, mData);
}
private void Ef_MultiHit_2To5(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
byte numHits;
if (user.Ability == PBEAbility.SkillLink)
{
numHits = 5;
}
else
{
int val = _rand.RandomInt(0, 5);
if (val < 2)
{
numHits = 2;
}
else if (val < 4)
{
numHits = 3;
}
else if (val < 5)
{
numHits = 4;
}
else
{
numHits = 5;
}
}
Ef_MultiHit(user, targets, move, mData, numHits);
}
private void Ef_PayDay(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
void BeforeDoingDamage(PBEBattlePokemon target)
{
BroadcastPayDay();
}
BasicHit(user, targets, mData, beforeDoingDamage: BeforeDoingDamage);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Recoil(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, PBEStatus1 status1 = PBEStatus1.None, int chanceToInflictStatus1 = 0, PBEStatus2 status2 = PBEStatus2.None, int chanceToInflictStatus2 = 0)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
int? RecoilFunc(int totalDamageDealt)
{
return user.Ability == PBEAbility.RockHead || totalDamageDealt == 0 ? null : totalDamageDealt / mData.EffectParam;
}
void BeforePostHit(PBEBattlePokemon target, ushort damageDealt)
{
if (target.HP == 0)
{
return;
}
if (status1 != PBEStatus1.None && GetManipulableChance(user, chanceToInflictStatus1))
{
ApplyStatus1IfPossible(user, target, status1, false);
}
if (status2 != PBEStatus2.None && GetManipulableChance(user, chanceToInflictStatus2))
{
ApplyStatus2IfPossible(user, target, status2, false);
}
}
BasicHit(user, targets, mData, recoilFunc: RecoilFunc, beforePostHit: status1 != PBEStatus1.None || status2 != PBEStatus2.None ? BeforePostHit : null);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_SecretPower(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
void BeforePostHit(PBEBattlePokemon target, ushort damageDealt)
{
// BUG: SecretPower is unaffected by SereneGrace and the Rainbow
if (target.HP > 0
&& (Settings.BugFix
? GetManipulableChance(user, mData.EffectParam)
: _rand.RandomBool(mData.EffectParam, 100))
)
{
switch (BattleTerrain)
{
case PBEBattleTerrain.Cave: ApplyStatus2IfPossible(user, target, PBEStatus2.Flinching, false); break;
case PBEBattleTerrain.Grass: ApplyStatus1IfPossible(user, target, PBEStatus1.Asleep, false); break;
case PBEBattleTerrain.Plain: ApplyStatus1IfPossible(user, target, PBEStatus1.Paralyzed, false); break;
case PBEBattleTerrain.Puddle: ApplyStatChangeIfPossible(user, target, PBEStat.Speed, -1, isSecondaryEffect: true); break;
case PBEBattleTerrain.Sand: ApplyStatChangeIfPossible(user, target, PBEStat.Accuracy, -1, isSecondaryEffect: true); break;
case PBEBattleTerrain.Snow: ApplyStatus1IfPossible(user, target, PBEStatus1.Frozen, false); break;
case PBEBattleTerrain.Water: ApplyStatChangeIfPossible(user, target, PBEStat.Attack, -1, isSecondaryEffect: true); break;
default: throw new InvalidDataException(nameof(BattleTerrain));
}
}
}
BasicHit(user, targets, mData, beforePostHit: BeforePostHit);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Selfdestruct(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
// TODO: Damp
BroadcastMoveUsed(user, move);
PPReduce(user, move);
// In gen 5, the user faints first (and loses if possible)
// Due to technical limitations, we cannot faint first, but we should still make the user lose so it is the same behavior
DealDamage(user, user, user.MaxHP);
TrySetLoser(user);
if (targets.Length == 0) // You still faint if there are no targets
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
BasicHit(user, targets, mData);
}
FaintCheck(user); // No berry check because we are always fainted
RecordExecutedMove(user, move, mData);
}
private void Ef_SmellingSalt(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
void AfterPostHit(PBEBattlePokemon target)
{
if (target.HP > 0 && target.Status1 == PBEStatus1.Paralyzed)
{
target.Status1 = PBEStatus1.None;
BroadcastStatus1(target, user, PBEStatus1.Paralyzed, PBEStatusAction.Cleared);
}
}
BasicHit(user, targets, mData, afterPostHit: AfterPostHit);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Snore(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
// TODO: Snore etc fail in BasicHit?
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else if (user.Status1 != PBEStatus1.Asleep)
{
BroadcastMoveResult(user, user, PBEResult.Ineffective_Status);
}
else
{
void BeforePostHit(PBEBattlePokemon target, ushort damageDealt)
{
if (target.HP > 0 && GetManipulableChance(user, mData.EffectParam))
{
ApplyStatus2IfPossible(user, target, PBEStatus2.Flinching, false);
}
}
BasicHit(user, targets, mData, beforePostHit: BeforePostHit);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Struggle(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastStruggle(user);
BroadcastMoveUsed(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
int? RecoilFunc(int totalDamageDealt)
{
return user.MaxHP / mData.EffectParam;
}
BasicHit(user, targets, mData, recoilFunc: RecoilFunc);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_SuckerPunch(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
PBEResult FailFunc(PBEBattlePokemon target)
{
if (target.TurnAction is null // Just switched in, used item, etc
|| target.HasUsedMoveThisTurn
|| target.TurnAction.Decision != PBETurnDecision.Fight
|| PBEDataProvider.Instance.GetMoveData(target.TurnAction.FightMove).Category == PBEMoveCategory.Status)
{
const PBEResult result = PBEResult.InvalidConditions;
BroadcastMoveResult(user, target, result);
return result;
}
return PBEResult.Success;
}
BasicHit(user, targets, mData, failFunc: FailFunc);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_WakeUpSlap(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
void AfterPostHit(PBEBattlePokemon target)
{
if (target.HP > 0 && target.Status1 == PBEStatus1.Asleep)
{
target.Status1 = PBEStatus1.None;
target.Status1Counter = 0;
target.SleepTurns = 0;
BroadcastStatus1(target, user, PBEStatus1.Asleep, PBEStatusAction.Cleared);
CureNightmare(target, user);
}
}
BasicHit(user, targets, mData, afterPostHit: AfterPostHit);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Endeavor(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
int DamageFunc(PBEBattlePokemon target)
{
return target.HP - user.HP;
}
PBEResult FailFunc(PBEBattlePokemon target)
{
if (target.HP <= user.HP)
{
const PBEResult result = PBEResult.InvalidConditions;
BroadcastMoveResult(user, target, result);
return result;
}
return PBEResult.Success;
}
FixedDamageHit(user, targets, mData, DamageFunc, failFunc: FailFunc);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_FinalGambit(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
int oldHP = user.HP;
int DamageFunc(PBEBattlePokemon target)
{
// Only faint/deal damage first time
if (user.HP > 0)
{
DealDamage(user, user, oldHP);
FaintCheck(user); // No berry check because we are always fainted
}
return oldHP;
}
FixedDamageHit(user, targets, mData, DamageFunc);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_OneHitKnockout(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
int DamageFunc(PBEBattlePokemon target)
{
return target.HP;
}
PBEResult FailFunc(PBEBattlePokemon target)
{
if (target.Level > user.Level)
{
const PBEResult result = PBEResult.Ineffective_Level;
BroadcastMoveResult(user, target, result);
return result;
}
else
{
return PBEResult.Success;
}
}
void BeforePostHit()
{
// This Any is for Sturdy survivors
if (Array.FindIndex(targets, p => p.HP == 0) != -1)
{
BroadcastOneHitKnockout();
}
}
FixedDamageHit(user, targets, mData, DamageFunc, failFunc: FailFunc, beforePostHit: BeforePostHit);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Psywave(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
int DamageFunc(PBEBattlePokemon target)
{
return user.Level * (_rand.RandomInt(0, 100) + 50) / 100;
}
FixedDamageHit(user, targets, mData, DamageFunc);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_SeismicToss(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
int DamageFunc(PBEBattlePokemon target)
{
return user.Level;
}
FixedDamageHit(user, targets, mData, DamageFunc);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_SetDamage(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
int DamageFunc(PBEBattlePokemon target)
{
return mData.EffectParam;
}
FixedDamageHit(user, targets, mData, DamageFunc);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_SuperFang(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
static int DamageFunc(PBEBattlePokemon target)
{
return target.HP / 2;
}
FixedDamageHit(user, targets, mData, DamageFunc);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_HPDrain(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData, bool requireSleep = false)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
PBEResult FailFunc(PBEBattlePokemon target)
{
if (target.Status1 != PBEStatus1.Asleep)
{
const PBEResult result = PBEResult.Ineffective_Status;
BroadcastMoveResult(user, target, result);
return result;
}
return PBEResult.Success;
}
void BeforePostHit(PBEBattlePokemon target, ushort damageDealt)
{
if (user.HP == 0)
{
return;
}
int restoreAmt = (int)(damageDealt * (mData.EffectParam / 100.0));
ApplyBigRoot(user, ref restoreAmt);
if (target.Ability == PBEAbility.LiquidOoze)
{
// Verified: User faints first here, target faints at normal spot afterwards
BroadcastAbility(target, user, PBEAbility.LiquidOoze, PBEAbilityAction.Damage);
DealDamage(target, user, restoreAmt);
if (!FaintCheck(user))
{
LowHPBerryCheck(user);
}
}
else if (HealDamage(user, restoreAmt) > 0)
{
BroadcastHPDrained(target);
}
}
BasicHit(user, targets, mData, failFunc: requireSleep ? FailFunc : null, beforePostHit: BeforePostHit);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Moonlight(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
int denominator;
if (ShouldDoWeatherEffects())
{
switch (Weather)
{
case PBEWeather.None: denominator = 2; break;
case PBEWeather.HarshSunlight: denominator = 3; break;
default: denominator = 4; break;
}
}
else
{
denominator = 2;
}
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
if (HealDamage(user, user.MaxHP / denominator) == 0)
{
BroadcastMoveResult(user, user, PBEResult.Ineffective_Stat);
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_PainSplit(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
if (target.Status2.HasFlag(PBEStatus2.Substitute))
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Substitute);
}
else
{
int total = user.HP + target.HP;
int hp = total / 2;
foreach (PBEBattlePokemon pkmn in new PBEBattlePokemon[] { user, target })
{
if (hp > pkmn.HP)
{
HealDamage(pkmn, hp - pkmn.HP);
}
else if (hp < pkmn.HP)
{
DealDamage(user, pkmn, pkmn.HP - hp);
}
}
BroadcastPainSplit(user, target);
LowHPBerryCheck(user);
LowHPBerryCheck(target, forcedToEatBy: user); // Verified: Berry is activated but no illusion breaking
// Verified: ColorChange/LifeOrb is ignored
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Rest(PBEBattlePokemon user, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (user.Status1 == PBEStatus1.Asleep)
{
BroadcastMoveResult(user, user, PBEResult.Ineffective_Status);
}
else if (user.HP == user.MaxHP)
{
BroadcastMoveResult(user, user, PBEResult.Ineffective_Stat);
}
else
{
PBEResult result = user.IsSleepPossible(null, ignoreSubstitute: true, ignoreCurrentStatus: true, ignoreSafeguard: true);
if (result == PBEResult.Ineffective_Ability)
{
BroadcastAbility(user, user, user.Ability, PBEAbilityAction.PreventedStatus);
}
if (result != PBEResult.Success)
{
BroadcastMoveResult(user, user, result);
}
else
{
user.Status1 = PBEStatus1.Asleep;
SetSleepTurns(user, Settings.SleepMaxTurns, Settings.SleepMaxTurns); // Not a typo; Rest always sleeps for max turns
user.Status1Counter = 0;
BroadcastStatus1(user, user, PBEStatus1.Asleep, PBEStatusAction.Added);
HealDamage(user, user.MaxHP);
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_RestoreTargetHP(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
if (user != target && target.Status2.HasFlag(PBEStatus2.Substitute))
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Substitute);
}
else
{
if (HealDamage(target, (int)(target.MaxHP * (mData.EffectParam / 100.0))) == 0)
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Stat);
}
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Roost(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
if (target.Status2.HasFlag(PBEStatus2.Roost))
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Status);
}
else
{
if (HealDamage(target, (int)(target.MaxHP * (mData.EffectParam / 100.0))) == 0)
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Stat);
}
else
{
target.StartRoost();
BroadcastStatus2(target, user, PBEStatus2.Roost, PBEStatusAction.Added);
}
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_BellyDrum(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
int requirement = target.MaxHP / 2;
// BUG: The games do not check if the target has Contrary
sbyte oldValue = 0, newValue = 0;
if (target.HP <= requirement
|| (Settings.BugFix
? target.IsStatChangePossible(PBEStat.Attack, user, byte.MaxValue, out oldValue, out newValue) != PBEResult.Success
: target.AttackChange == Settings.MaxStatChange)
)
{
BroadcastMoveResult(user, target, PBEResult.InvalidConditions);
}
else
{
DealDamage(user, target, requirement);
if (Settings.BugFix)
{
SetStatAndBroadcast(target, PBEStat.Attack, oldValue, newValue);
}
else
{
ApplyStatChangeIfPossible(user, target, PBEStat.Attack, byte.MaxValue); // byte.MaxValue will work for all PBESettings
}
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Camouflage(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
PBEType type;
switch (BattleTerrain)
{
case PBEBattleTerrain.Cave: type = PBEType.Rock; break;
case PBEBattleTerrain.Grass: type = PBEType.Grass; break;
case PBEBattleTerrain.Plain: type = PBEType.Normal; break;
case PBEBattleTerrain.Puddle: type = PBEType.Ground; break;
case PBEBattleTerrain.Sand: type = PBEType.Ground; break;
case PBEBattleTerrain.Snow: type = PBEType.Ice; break;
case PBEBattleTerrain.Water: type = PBEType.Water; break;
default: throw new InvalidDataException(nameof(BattleTerrain));
}
// Verified: Works on dual-type, fails on single-type
if (target.Type1 == type && target.Type2 == PBEType.None)
{
BroadcastMoveResult(user, target, PBEResult.InvalidConditions);
}
else
{
BroadcastTypeChanged(target, type, PBEType.None);
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Conversion(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
PBEBattleMoveset moves = target.Moves;
int count = moves.Count;
var available = new List(count);
for (int i = 0; i < count; i++)
{
PBEMove m = moves[i].Move;
if (m != PBEMove.None && m != move)
{
PBEType type = PBEDataProvider.Instance.GetMoveData(m).Type;
if (!target.HasType(type))
{
available.Add(type);
}
}
}
if (available.Count == 0)
{
BroadcastMoveResult(user, target, PBEResult.InvalidConditions);
}
else
{
BroadcastTypeChanged(target, _rand.RandomElement(available), PBEType.None);
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Curse(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
if (user.HasType(PBEType.Ghost))
{
if (targets.Length == 1 && targets[0] == user) // Just gained the Ghost type after selecting the move, so get a random target
{
PBEFieldPosition prioritizedPos = GetPositionAcross(BattleFormat, user.FieldPosition);
PBETurnTarget moveTarget = prioritizedPos == PBEFieldPosition.Left ? PBETurnTarget.FoeLeft : prioritizedPos == PBEFieldPosition.Center ? PBETurnTarget.FoeCenter : PBETurnTarget.FoeRight;
targets = GetRuntimeTargets(user, moveTarget, false, _rand);
}
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
ApplyStatus2IfPossible(user, target, PBEStatus2.Cursed, true);
}
}
}
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (target.SpeedChange == -Settings.MaxStatChange
&& target.AttackChange == Settings.MaxStatChange
&& target.DefenseChange == Settings.MaxStatChange)
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Stat);
}
else
{
ApplyStatChangeIfPossible(user, target, PBEStat.Speed, -1);
ApplyStatChangeIfPossible(user, target, PBEStat.Attack, +1);
ApplyStatChangeIfPossible(user, target, PBEStat.Defense, +1);
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Flatter(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
if (target.Status2.HasFlag(PBEStatus2.Substitute))
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Substitute);
}
else
{
ApplyStatChangeIfPossible(user, target, PBEStat.SpAttack, +1);
ApplyStatus2IfPossible(user, target, PBEStatus2.Confused, true);
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Haze(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
foreach (PBEBattlePokemon pkmn in targets)
{
pkmn.ClearStatChanges();
}
BroadcastHaze();
RecordExecutedMove(user, move, mData);
}
private void Ef_HelpingHand(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
// TODO: When triple battle shifting happens, all moves that can target allies but not the user will have to check if the user targetted itself due to shifting.
// For now, I'll put this check here, because this is the only move that will attempt to target the user when the move cannot normally do so (single/rotation battle).
if (target == user)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
ApplyStatus2IfPossible(user, target, PBEStatus2.HelpingHand, true); // No MissCheck because should be able to hit through protect, shadowforce, etc
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Metronome(PBEBattlePokemon user, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
// Record before the called move is recorded
RecordExecutedMove(user, move, mData);
PBEMove calledMove = _rand.RandomElement(PBEDataUtils.MetronomeMoves);
_calledFromOtherMove = true;
UseMove(user, calledMove, GetRandomTargetForMetronome(user, calledMove, _rand));
_calledFromOtherMove = false;
}
private void Ef_Nothing(PBEBattlePokemon user, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
BroadcastNothingHappened();
RecordExecutedMove(user, move, mData);
}
private void Ef_PsychUp(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
BroadcastPsychUp(user, target);
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_ReflectType(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
// Fail if pure flying-type roosts
if (target.Type1 == PBEType.None && target.Type2 == PBEType.None)
{
BroadcastMoveResult(user, target, PBEResult.InvalidConditions);
}
else
{
BroadcastReflectType(user, target);
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Refresh(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
PBEStatus1 status1 = target.Status1;
if (status1 == PBEStatus1.None || status1 == PBEStatus1.Asleep || status1 == PBEStatus1.Frozen)
{
BroadcastMoveResult(user, target, PBEResult.InvalidConditions);
}
else
{
target.Status1 = PBEStatus1.None;
target.Status1Counter = 0;
BroadcastStatus1(target, user, status1, PBEStatusAction.Cleared);
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Soak(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
BroadcastTypeChanged(target, PBEType.Water, PBEType.None);
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Swagger(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (!MissCheck(user, target, mData))
{
if (target.Status2.HasFlag(PBEStatus2.Substitute))
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Substitute);
}
else
{
ApplyStatChangeIfPossible(user, target, PBEStat.Attack, +2);
ApplyStatus2IfPossible(user, target, PBEStatus2.Confused, true);
}
}
}
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Teleport(PBEBattlePokemon user, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
// TODO: Trapping effects, SmokeBall
// In gen 5 there is a bug that prevents wild Pokémon holding a SmokeBall from escaping if they are affected by trapping effects
if (BattleType == PBEBattleType.Wild && BattleFormat == PBEBattleFormat.Single)
{
SetEscaped(user);
}
else
{
BroadcastMoveResult(user, user, PBEResult.InvalidConditions);
}
RecordExecutedMove(user, move, mData);
}
private void Ef_Whirlwind(PBEBattlePokemon user, PBEBattlePokemon[] targets, PBEMove move, IPBEMoveData mData)
{
BroadcastMoveUsed(user, move);
PPReduce(user, move);
if (targets.Length == 0)
{
BroadcastMoveResult(user, user, PBEResult.NoTarget);
}
else
{
foreach (PBEBattlePokemon target in targets)
{
if (MissCheck(user, target, mData))
{
continue;
}
// TODO: Trapping effects
if (BattleType == PBEBattleType.Wild)
{
if (BattleFormat == PBEBattleFormat.Single)
{
// Wild single battle requires user's level to be >= target's level, then it'll end the battle
if (user.Level < target.Level)
{
BroadcastMoveResult(user, target, PBEResult.Ineffective_Level);
continue;
}
SetEscaped(target);
break;
}
else
{
// Trainer using whirlwind in a wild double+ battle will cause it to fail (even if there's only one wild Pokémon left)
if (!user.IsWild)
{
BroadcastMoveResult(user, target, PBEResult.InvalidConditions);
continue;
}
// A wild Pokémon using it will cause it to switch the target out (as normal below)
}
}
List possibleSwitcheroonies = target.Trainer.Party.FindAll(p => p.FieldPosition == PBEFieldPosition.None && p.CanBattle);
if (possibleSwitcheroonies.Count == 0)
{
BroadcastMoveResult(user, target, PBEResult.InvalidConditions);
}
else
{
SwitchTwoPokemon(target, _rand.RandomElement(possibleSwitcheroonies), user);
}
}
}
RecordExecutedMove(user, move, mData);
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattleEffects_HitLogic.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using System;
using System.Collections.Generic;
namespace Kermalis.PokemonBattleEngine.Battle;
// This constant looping order that's present in hitting as well as turn ended effects is very weird and unnecessary, but I mimic it for accuracy
// That's why this file exists in favor of the order I had before
public sealed partial class PBEBattle
{
private class PBEAttackVictim
{
public PBEBattlePokemon Pkmn { get; }
public PBEResult Result { get; }
public float TypeEffectiveness { get; }
public bool Crit { get; set; }
public ushort Damage { get; set; }
public PBEAttackVictim(PBEBattlePokemon pkmn, PBEResult result, float typeEffectiveness)
{
Pkmn = pkmn; Result = result; TypeEffectiveness = typeEffectiveness;
}
}
// TODO: TripleKick miss logic
private void Hit_GetVictims(PBEBattlePokemon user, PBEBattlePokemon[] targets, IPBEMoveData mData, PBEType moveType, out List victims,
Func? failFunc = null)
{
victims = new List(targets.Length);
foreach (PBEBattlePokemon target in targets)
{
if (!AttackTypeCheck(user, target, moveType, out PBEResult result, out float typeEffectiveness))
{
continue;
}
// Verified: These fails are after type effectiveness (So SuckerPunch will not affect Ghost types due to Normalize before it fails due to invalid conditions)
if (failFunc is not null && failFunc.Invoke(target) != PBEResult.Success)
{
continue;
}
victims.Add(new PBEAttackVictim(target, result, typeEffectiveness));
}
if (victims.Count == 0)
{
return;
}
victims.RemoveAll(t => MissCheck(user, t.Pkmn, mData));
return;
}
// Outs are for hit targets that were not behind substitute
private static void Hit_HitTargets(PBETeam user, Action> doSub, Action> doNormal, List victims,
out List allies, out List foes)
{
List subAllies = victims.FindAll(v =>
{
PBEBattlePokemon pkmn = v.Pkmn;
return pkmn.Team == user && pkmn.Status2.HasFlag(PBEStatus2.Substitute);
});
allies = victims.FindAll(v =>
{
PBEBattlePokemon pkmn = v.Pkmn;
return pkmn.Team == user && !pkmn.Status2.HasFlag(PBEStatus2.Substitute);
});
List subFoes = victims.FindAll(v =>
{
PBEBattlePokemon pkmn = v.Pkmn;
return pkmn.Team != user && pkmn.Status2.HasFlag(PBEStatus2.Substitute);
});
foes = victims.FindAll(v =>
{
PBEBattlePokemon pkmn = v.Pkmn;
return pkmn.Team != user && !pkmn.Status2.HasFlag(PBEStatus2.Substitute);
});
doSub(subAllies);
doNormal(allies);
doSub(subFoes);
doNormal(foes);
}
private void Hit_DoCrit(List victims)
{
foreach (PBEAttackVictim victim in victims)
{
if (victim.Crit)
{
BroadcastMoveCrit(victim.Pkmn);
}
}
}
private void Hit_DoMoveResult(PBEBattlePokemon user, List victims)
{
foreach (PBEAttackVictim victim in victims)
{
PBEResult result = victim.Result;
if (result != PBEResult.Success)
{
BroadcastMoveResult(user, victim.Pkmn, result);
}
}
}
private void Hit_FaintCheck(List victims)
{
foreach (PBEAttackVictim victim in victims)
{
FaintCheck(victim.Pkmn);
}
}
private void BasicHit(PBEBattlePokemon user, PBEBattlePokemon[] targets, IPBEMoveData mData,
Func? failFunc = null,
Action? beforeDoingDamage = null,
Action? beforePostHit = null,
Action? afterPostHit = null,
Func? recoilFunc = null)
{
// Targets array is [FoeLeft, FoeCenter, FoeRight, AllyLeft, AllyCenter, AllyRight]
// User can faint or heal with a berry at LiquidOoze, IronBarbs/RockyHelmet, and also at Recoil/LifeOrb
// -------------Official order-------------
// Setup - [effectiveness/fail checks foes], [effectiveness/fail checks allies], [miss/protection checks foes] [miss/protection checks allies], gem,
// Allies - [sub damage allies, sub effectiveness allies, sub crit allies, sub break allies], [hit allies], [effectiveness allies], [crit allies], [posthit allies], [faint allies],
// Foes - [sub damage foes, sub effectiveness foes, sub crit foes, sub break foes], [hit foes], [effectiveness foes], [crit foes], [posthit foes], [faint foes],
// Cleanup - recoil, lifeorb, [colorchange foes], [colorchange allies], [berry allies], [berry foes], [antistatusability allies], [antistatusability foes], exp
PBEType moveType = user.GetMoveType(mData);
// DreamEater checks for sleep before gem activates
// SuckerPunch fails
Hit_GetVictims(user, targets, mData, moveType, out List victims, failFunc: failFunc);
if (victims.Count == 0)
{
return;
}
float basePower = CalculateBasePower(user, targets, mData, moveType); // Gem activates here
float initDamageMultiplier = victims.Count > 1 ? 0.75f : 1;
int totalDamageDealt = 0;
void CalcDamage(PBEAttackVictim victim)
{
PBEBattlePokemon target = victim.Pkmn;
PBEResult result = victim.Result;
float damageMultiplier = initDamageMultiplier * victim.TypeEffectiveness;
// Brick Break destroys Light Screen and Reflect before doing damage (after gem)
// Feint destroys protection
// Pay Day scatters coins
beforeDoingDamage?.Invoke(target);
bool crit = CritCheck(user, target, mData);
damageMultiplier *= CalculateDamageMultiplier(user, target, mData, moveType, result, crit);
int damage = (int)(damageMultiplier * CalculateDamage(user, target, mData, moveType, basePower, crit));
victim.Damage = DealDamage(user, target, damage, ignoreSubstitute: false, ignoreSturdy: false);
totalDamageDealt += victim.Damage;
victim.Crit = crit;
}
void DoSub(List subs)
{
foreach (PBEAttackVictim victim in subs)
{
CalcDamage(victim);
PBEBattlePokemon target = victim.Pkmn;
PBEResult result = victim.Result;
if (result != PBEResult.Success)
{
BroadcastMoveResult(user, target, result);
}
if (victim.Crit)
{
BroadcastMoveCrit(target);
}
if (target.SubstituteHP == 0)
{
BroadcastStatus2(target, user, PBEStatus2.Substitute, PBEStatusAction.Ended);
}
}
}
void DoNormal(List normals)
{
foreach (PBEAttackVictim victim in normals)
{
CalcDamage(victim);
}
Hit_DoMoveResult(user, normals);
Hit_DoCrit(normals);
foreach (PBEAttackVictim victim in normals)
{
PBEBattlePokemon target = victim.Pkmn;
// Stats/statuses are changed before post-hit effects
// HP-draining moves restore HP
beforePostHit?.Invoke(target, victim.Damage); // TODO: LiquidOoze fainting/healing
DoPostHitEffects(user, target, mData, moveType);
// ShadowForce destroys protection
// SmellingSalt cures paralysis
// WakeUpSlap cures sleep
afterPostHit?.Invoke(target); // Verified: These happen before Recoil/LifeOrb
}
Hit_FaintCheck(normals);
}
Hit_HitTargets(user.Team, DoSub, DoNormal, victims, out List allies, out List foes);
DoPostAttackedEffects(user, allies, foes, true, recoilDamage: recoilFunc?.Invoke(totalDamageDealt), colorChangeType: moveType);
}
// None of these moves are multi-target
private void FixedDamageHit(PBEBattlePokemon user, PBEBattlePokemon[] targets, IPBEMoveData mData, Func damageFunc,
Func? failFunc = null,
Action? beforePostHit = null)
{
PBEType moveType = user.GetMoveType(mData);
// Endeavor fails if the target's HP is <= the user's HP
// One hit knockout moves fail if the target's level is > the user's level
Hit_GetVictims(user, targets, mData, moveType, out List victims, failFunc: failFunc);
if (victims.Count == 0)
{
return;
}
// BUG: Gems activate for these moves despite base power not being involved
if (!Settings.BugFix)
{
_ = CalculateBasePower(user, targets, mData, moveType);
}
void CalcDamage(PBEAttackVictim victim)
{
PBEBattlePokemon target = victim.Pkmn;
// FinalGambit user faints here
victim.Damage = DealDamage(user, target, damageFunc.Invoke(target));
}
void DoSub(List subs)
{
foreach (PBEAttackVictim victim in subs)
{
CalcDamage(victim);
PBEBattlePokemon target = victim.Pkmn;
if (target.SubstituteHP == 0)
{
BroadcastStatus2(target, user, PBEStatus2.Substitute, PBEStatusAction.Ended);
}
}
}
void DoNormal(List normals)
{
foreach (PBEAttackVictim victim in normals)
{
CalcDamage(victim);
}
foreach (PBEAttackVictim victim in normals)
{
PBEBattlePokemon target = victim.Pkmn;
// "It's a one-hit KO!"
beforePostHit?.Invoke();
DoPostHitEffects(user, target, mData, moveType);
}
Hit_FaintCheck(normals);
}
Hit_HitTargets(user.Team, DoSub, DoNormal, victims, out List allies, out List foes);
DoPostAttackedEffects(user, allies, foes, false, colorChangeType: moveType);
}
// None of these moves are multi-target
private void MultiHit(PBEBattlePokemon user, PBEBattlePokemon[] targets, IPBEMoveData mData, byte numHits,
bool subsequentMissChecks = false,
Action? beforePostHit = null)
{
PBEType moveType = user.GetMoveType(mData);
Hit_GetVictims(user, targets, mData, moveType, out List victims);
if (victims.Count == 0)
{
return;
}
float basePower = CalculateBasePower(user, targets, mData, moveType); // Verified: Gem boost applies to all hits
float initDamageMultiplier = victims.Count > 1 ? 0.75f : 1;
void CalcDamage(PBEAttackVictim victim)
{
PBEBattlePokemon target = victim.Pkmn;
PBEResult result = victim.Result;
float damageMultiplier = initDamageMultiplier * victim.TypeEffectiveness;
bool crit = CritCheck(user, target, mData);
damageMultiplier *= CalculateDamageMultiplier(user, target, mData, moveType, result, crit);
int damage = (int)(damageMultiplier * CalculateDamage(user, target, mData, moveType, basePower, crit));
victim.Damage = DealDamage(user, target, damage, ignoreSubstitute: false, ignoreSturdy: false);
victim.Crit = crit;
}
void DoSub(List subs)
{
foreach (PBEAttackVictim victim in subs)
{
CalcDamage(victim);
PBEBattlePokemon target = victim.Pkmn;
if (victim.Crit)
{
BroadcastMoveCrit(target);
}
if (target.SubstituteHP == 0)
{
BroadcastStatus2(target, user, PBEStatus2.Substitute, PBEStatusAction.Ended);
}
}
}
void DoNormal(List normals)
{
normals.RemoveAll(v => v.Pkmn.HP == 0); // Remove ones that fainted from previous hits
foreach (PBEAttackVictim victim in normals)
{
CalcDamage(victim);
}
Hit_DoCrit(normals);
foreach (PBEAttackVictim victim in normals)
{
PBEBattlePokemon target = victim.Pkmn;
// Twineedle has a chance to poison on each strike
beforePostHit?.Invoke(target);
DoPostHitEffects(user, target, mData, moveType);
}
}
byte hit = 0;
List allies, foes;
do
{
Hit_HitTargets(user.Team, DoSub, DoNormal, victims, out allies, out foes);
hit++;
} while (hit < numHits && user.HP > 0 && user.Status1 != PBEStatus1.Asleep && victims.FindIndex(v => v.Pkmn.HP > 0) != -1);
Hit_DoMoveResult(user, allies);
Hit_DoMoveResult(user, foes);
BroadcastMultiHit(hit);
Hit_FaintCheck(allies);
Hit_FaintCheck(foes);
DoPostAttackedEffects(user, allies, foes, true, colorChangeType: moveType);
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattleEnums.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using System;
namespace Kermalis.PokemonBattleEngine.Battle;
/// Represents the battle's terrain.
public enum PBEBattleTerrain : byte
{
Cave, // Rock, RockSlide, RockThrow
Grass, // Grass, SeedBomb, NeedleArm
/// Used for bridges, buildings, and link battles.
Plain, // Normal, TriAttack, BodySlam
Puddle, // Ground, MudBomb, MudShot
Sand, // Ground, Earthquake, MudSlap
Snow, // Ice, Blizzard, Avalanche
Water, // Water, HydroPump, WaterPulse
MAX
}
/// Represents the format of a specific battle.
public enum PBEBattleFormat : byte
{
/// A 1v1 battle. Each Pokémon is able to use moves or switch out for another Pokémon.
Single,
/// A 2v2 battle where all Pokémon are able to use moves or switch out for another Pokémon.
Double,
/// A 3v3 battle where all Pokémon are able to use moves, shift positions with a teammate, or switch out for another Pokémon.
Triple,
/// A 3v3 battle where only the front Pokémon are able to force a team rotation, use a move, or switch out for another Pokémon.
/// Team rotation does not take up a turn and can be done once per turn.
Rotation,
/// Invalid battle format.
MAX
}
public enum PBEBattleType : byte
{
Trainer,
Wild
}
/// Represents the current state of a specific battle.
public enum PBEBattleState : byte
{
/// The battle is waiting for teams.
WaitingForPlayers,
/// The battle is ready to begin.
ReadyToBegin,
/// The battle is waiting for players to send actions.
WaitingForActions,
/// The battle is ready to run a turn.
ReadyToRunTurn,
/// The battle is processing.
Processing,
/// The battle is waiting for players to send switch-ins.
WaitingForSwitchIns,
/// The battle is ready to run switches.
ReadyToRunSwitches,
/// The battle ended.
Ended
}
/// Represents the result of an ended battle.
public enum PBEBattleResult : byte
{
/// Team 0 forfeited.
Team0Forfeit,
/// Team 0 defeated Team 1.
Team0Win,
/// Team 1 forfeited.
Team1Forfeit,
/// Team 1 defeated Team 0.
Team1Win,
/// A wild Pokémon was captured.
WildCapture,
/// The trainer(s) escaped from the wild Pokémon.
WildEscape,
/// A wild Pokémon escaped from the trainer(s).
WildFlee
}
/// Represents the weather in a specific battle.
public enum PBEWeather : byte
{
/// There is no weather.
None,
/// It is hailing.
Hailstorm,
/// The sunlight is harsh.
HarshSunlight,
/// It is raining.
Rain,
/// A sandstorm is brewing.
Sandstorm,
MAX
}
/// Represents a position on the battle field.
public enum PBEFieldPosition : byte
{
/// A Pokémon is not on the field.
None,
/// The Pokémon to a player's left in a Double, Triple, or Rotation battle.
Left,
/// The Pokémon in the center of the field in a Single, Triple, or Rotation battle.
Center,
/// The Pokémon to a player's right in a Double, Triple, or Rotation battle.
Right,
MAX
}
/// Represents a 's targets.
[Flags]
public enum PBETurnTarget : byte
{
/// The Pokémon has not chosen any targets.
None,
/// The move is targetting the player's left Pokémon.
AllyLeft = 1 << 0,
/// The move is targetting the player's center Pokémon.
AllyCenter = 1 << 1,
/// The move is targetting the player's right Pokémon.
AllyRight = 1 << 2,
/// The move is targetting the opponent's left Pokémon.
FoeLeft = 1 << 3,
/// The move is targetting the opponent's center Pokémon.
FoeCenter = 1 << 4,
/// The move is targetting the opponent's right Pokémon.
FoeRight = 1 << 5
}
/// Represents a Pokémon's decision for a turn.
public enum PBETurnDecision : byte
{
/// The Pokémon has not made a decision.
None,
/// The Pokémon has chosen to use a move.
Fight,
/// The Pokémon has chosen to use an item.
Item,
/// The Pokémon has chosen to switch out for another Pokémon.
SwitchOut,
/// The wild Pokémon has chosen to use flee.
WildFlee
}
/// Represents a specific Pokémon's non-volatile status.
public enum PBEStatus1 : byte
{
/// The Pokémon has no status.
None,
/// The Pokémon is asleep.
Asleep,
/// The Pokémon is badly poisoned.
BadlyPoisoned,
/// The Pokémon is burned.
Burned,
/// The Pokémon is frozen.
Frozen,
/// The Pokémon is paralyzed.
Paralyzed,
/// The Pokémon is poisoned.
Poisoned,
MAX
}
/// Represents a specific Pokémon's volatile status.
[Flags]
public enum PBEStatus2 : uint
{
/// The Pokémon has no status.
None,
/// The Pokémon is high up in the air. A move will miss against the Pokémon unless it has or either Pokémon has .
Airborne = 1 << 0,
/// The Pokémon is confused and may hurt itself instead of execute its chosen move.
Confused = 1 << 1,
/// The Pokémon is cursed and will take damage at the end of each turn.
Cursed = 1 << 2,
/// The Pokémon is disguised with .
Disguised = 1 << 3,
/// The Pokémon is flinching and will be unable to move this turn.
Flinching = 1 << 4,
/// The Pokémon will gain a power boost due to .
HelpingHand = 1 << 5,
Identified = 1 << 6,
/// The Pokémon is infatuated with and may be unable to move this turn.
Infatuated = 1 << 7,
/// The Pokémon is seeded and HP will be stolen at the end of each turn.
LeechSeed = 1 << 8,
LockOn = 1 << 9,
MagnetRise = 1 << 10,
MiracleEye = 1 << 11,
Nightmare = 1 << 12,
/// The Pokémon's and are switched.
PowerTrick = 1 << 13,
/// The Pokémon is protected from moves this turn.
Protected = 1 << 14,
/// The Pokémon is under the effect of or and has a higher chance of landing critical hits.
Pumped = 1 << 15,
Roost = 1 << 16,
ShadowForce = 1 << 17,
/// The Pokémon is behind a substitute that will take damage on behalf of the Pokémon and prevent most moves from affecting the Pokémon.
Substitute = 1 << 18,
/// The Pokémon is transformed into another Pokémon.
Transformed = 1 << 19,
/// The Pokémon is underground. A move will miss against the Pokémon unless it has or either Pokémon has .
Underground = 1 << 20,
/// The Pokémon is underwater. A move will miss against the Pokémon unless it has or either Pokémon has .
Underwater = 1 << 21
}
/// Keeps track of which types changes to/from .
[Flags]
public enum PBERoostTypes : byte
{
None,
KnownType1 = 1 << 0,
KnownType2 = 1 << 1,
Type1 = 1 << 2,
Type2 = 1 << 3
}
/// Represents a specific 's status.
[Flags]
public enum PBEBattleStatus : byte
{
/// The battle has no status.
None,
/// The acting order of Pokémon in this battle is reversed.
TrickRoom = 1 << 0 // TODO: Full Incense, Lagging Tail, Stall, Quick Claw
}
/// Represents a specific 's status.
[Flags]
public enum PBETeamStatus : ushort
{
/// The team has no status.
None,
/// The team will take less damage from moves.
LightScreen = 1 << 0,
/// The team is shielded from critical hits.
LuckyChant = 1 << 1,
QuickGuard = 1 << 2,
/// The team will take less damage from moves.
Reflect = 1 << 3,
Safeguard = 1 << 4,
/// Grounded Pokémon that switch in will take damage. The amount of damage is based on .
Spikes = 1 << 5, // TODO: Gravity, magic guard, iron ball, baton pass with ingrain, air balloon
/// Pokémon that switch in will take damage. The amount of damage is based on the effectiveness of on the Pokémon.
StealthRock = 1 << 6, // TODO: magic guard
Tailwind = 1 << 7,
/// Grounded Pokémon that switch in will be if is 1 or if it is 2.
/// Grounded Pokémon will remove toxic spikes.
ToxicSpikes = 1 << 8, // TODO: Gravity, magic guard, iron ball, baton pass with ingrain, air balloon, synchronize with roar/whirlwind
/// The team is protected from spread moves for a turn.
WideGuard = 1 << 9
}
/// Represents an action regarding a .
public enum PBEAbilityAction : byte
{
/// The ability is first announced.
Announced = 0,
/// The ability caused a Pokémon to change its appearance.
ChangedAppearance = 1,
/// The ability changed a Pokémon's or .
ChangedStatus = 2,
/// The ability was involved with damage.
Damage = 3,
/// The ability prevented a Pokémon from being inflicted with a or .
PreventedStatus = 4,
/// The ability restored a Pokémon's HP.
RestoredHP = 5,
/// ended.
SlowStart_Ended = 6,
/// The ability was involved with stats.
Stats = 7,
/// The ability was involved with weather.
Weather = 8
}
/// Represents an action regarding a .
public enum PBEItemAction : byte
{
/// The item initiated its main effect.
Announced = 0,
/// The item was consumed by a Pokémon.
Consumed = 1,
/// The item was involved with damage.
Damage = 2,
/// The item restored HP to a Pokémon.
RestoredHP = 3
}
public enum PBEItemTurnAction : byte
{
NoEffect = 0,
Attempt = 1,
Success = 2
}
/// Represents an action regarding a or .
public enum PBEStatusAction : byte
{
/// The status was added to a Pokémon.
/// The Pokémon became .
Added = 0,
/// The status activated its main effect.
/// states the Pokémon is in love.
Announced = 1,
/// The status caused a Pokémon to be immobile.
/// prevented movement.
CausedImmobility = 2,
/// The status was cured from a Pokémon.
/// cured a Pokémon of .
Cleared = 3,
/// The status was involved with damage.
/// A Pokémon's took damage.
Damage = 4,
/// The status has ended naturally.
/// A Pokémon with regained its senses.
Ended = 5
}
public enum PBEBattleStatusAction : byte
{
Added = 0,
Cleared = 1,
Ended = 2
}
/// Represents an action regarding a .
public enum PBETeamStatusAction : byte
{
/// The status was added to a team.
/// A team set up .
Added = 0,
/// The status was forcefully removed from a team.
/// A Pokémon used and destroyed .
Cleared = 1,
/// The status ended naturally.
/// wore off.
Ended = 2
}
/// Represents the result of an intention.
public enum PBEResult : byte
{
/// No failure.
Success = 0,
/// Failure due to a .
Ineffective_Ability = 1,
/// Failure due to a .
Ineffective_Gender = 2,
/// Failure due to a Pokémon's level.
Ineffective_Level = 3,
/// Failure due to .
Ineffective_MagnetRise = 4,
/// Failure due to .
Ineffective_Safeguard = 5,
/// Failure due to a .
Ineffective_Stat = 6,
/// Failure due to a , , , , or .
Ineffective_Status = 7,
/// Failure due to .
Ineffective_Substitute = 8,
/// Failure due to a .
Ineffective_Type = 9,
/// Failure due to the intention's unmet special conditions.
InvalidConditions = 10,
/// Failure due to accuracy and/or evasion.
Missed = 11,
/// Failure due to having no available targets.
NoTarget = 12,
/// Limited success due to a Pokémon's .
NotVeryEffective_Type = 13,
/// Great success due to a Pokémon's .
SuperEffective_Type = 14
}
/// Represents an action regarding a .
public enum PBEWeatherAction : byte
{
/// The weather was added to the battle.
Added = 0,
/// The weather was removed from the battle.
Ended = 1
}
public enum PBESpecialMessage : byte
{
DraggedOut = 0,
Endure = 1,
HPDrained = 2,
Magnitude = 3,
MultiHit = 4,
NothingHappened = 5,
OneHitKnockout = 6,
PainSplit = 7,
PayDay = 8,
Recoil = 9,
Struggle = 10
}
public enum PBEMoveLockType : byte
{
ChoiceItem = 0,
Temporary = 1
}
================================================
FILE: PokemonBattleEngine/Battle/BattleEvents.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using Kermalis.PokemonBattleEngine.Data.Utils;
using Kermalis.PokemonBattleEngine.Packets;
using Kermalis.PokemonBattleEngine.Utils;
using System;
using System.IO;
using System.Linq;
namespace Kermalis.PokemonBattleEngine.Battle;
public sealed partial class PBEBattle
{
public delegate void BattleEvent(PBEBattle battle, IPBEPacket packet);
public event BattleEvent? OnNewEvent;
private void Broadcast(IPBEPacket packet)
{
Events.Add(packet);
OnNewEvent?.Invoke(this, packet);
}
private void BroadcastAbility(PBEBattlePokemon abilityOwner, PBEBattlePokemon pokemon2, PBEAbility ability, PBEAbilityAction abilityAction)
{
abilityOwner.Ability = ability;
abilityOwner.KnownAbility = ability;
Broadcast(new PBEAbilityPacket(abilityOwner, pokemon2, ability, abilityAction));
}
private void BroadcastAbilityReplaced(PBEBattlePokemon abilityOwner, PBEAbility newAbility)
{
PBEAbility? oldAbility = newAbility == PBEAbility.None ? null : abilityOwner.Ability; // Gastro Acid does not reveal previous ability
abilityOwner.Ability = newAbility;
abilityOwner.KnownAbility = newAbility;
Broadcast(new PBEAbilityReplacedPacket(abilityOwner, oldAbility, newAbility));
}
private void BroadcastBattleStatus(PBEBattleStatus battleStatus, PBEBattleStatusAction battleStatusAction)
{
switch (battleStatusAction)
{
case PBEBattleStatusAction.Added: BattleStatus |= battleStatus; break;
case PBEBattleStatusAction.Cleared:
case PBEBattleStatusAction.Ended: BattleStatus &= ~battleStatus; break;
default: throw new ArgumentOutOfRangeException(nameof(battleStatusAction));
}
Broadcast(new PBEBattleStatusPacket(battleStatus, battleStatusAction));
}
private void BroadcastCapture(PBEBattlePokemon pokemon, PBEItem ball, byte numShakes, bool success, bool critical)
{
Broadcast(new PBECapturePacket(pokemon, ball, numShakes, success, critical));
}
private void BroadcastFleeFailed(PBEBattlePokemon pokemon)
{
Broadcast(new PBEFleeFailedPacket(pokemon));
}
private void BroadcastHaze()
{
Broadcast(new PBEHazePacket());
}
private void BroadcastIllusion(PBEBattlePokemon pokemon)
{
Broadcast(new PBEIllusionPacket(pokemon));
}
private void BroadcastItem(PBEBattlePokemon itemHolder, PBEBattlePokemon pokemon2, PBEItem item, PBEItemAction itemAction)
{
switch (itemAction)
{
case PBEItemAction.Consumed:
{
itemHolder.Item = PBEItem.None;
itemHolder.KnownItem = PBEItem.None;
break;
}
default:
{
itemHolder.Item = item;
itemHolder.KnownItem = item;
break;
}
}
Broadcast(new PBEItemPacket(itemHolder, pokemon2, item, itemAction));
}
private void BroadcastItemTurn(PBEBattlePokemon itemUser, PBEItem item, PBEItemTurnAction itemAction)
{
Broadcast(new PBEItemTurnPacket(itemUser, item, itemAction));
}
private void BroadcastMoveCrit(PBEBattlePokemon victim)
{
Broadcast(new PBEMoveCritPacket(victim));
}
private void BroadcastMoveLock_ChoiceItem(PBEBattlePokemon moveUser, PBEMove lockedMove)
{
moveUser.ChoiceLockedMove = lockedMove;
Broadcast(new PBEMoveLockPacket(moveUser, PBEMoveLockType.ChoiceItem, lockedMove));
}
private void BroadcastMoveLock_Temporary(PBEBattlePokemon moveUser, PBEMove lockedMove, PBETurnTarget lockedTargets)
{
moveUser.TempLockedMove = lockedMove;
moveUser.TempLockedTargets = lockedTargets;
Broadcast(new PBEMoveLockPacket(moveUser, PBEMoveLockType.Temporary, lockedMove, lockedTargets));
}
private void BroadcastMovePPChanged(PBEBattlePokemon moveUser, PBEMove move, int amountReduced)
{
Broadcast(new PBEMovePPChangedPacket(moveUser, move, amountReduced));
}
private void BroadcastMoveResult(PBEBattlePokemon moveUser, PBEBattlePokemon pokemon2, PBEResult result)
{
Broadcast(new PBEMoveResultPacket(moveUser, pokemon2, result));
}
private void BroadcastMoveUsed(PBEBattlePokemon moveUser, PBEMove move)
{
bool owned;
if (!_calledFromOtherMove && moveUser.Moves.Contains(move))
{
// Check if this move is known first. If you check for PBEMove.MAX then you will get multiple results
if (!moveUser.KnownMoves.Contains(move))
{
moveUser.KnownMoves[PBEMove.MAX]!.Move = move;
}
owned = true;
}
else
{
owned = false;
}
Broadcast(new PBEMoveUsedPacket(moveUser, move, owned));
}
private void BroadcastPkmnEXPChanged(PBEBattlePokemon pokemon, uint oldEXP)
{
Broadcast(new PBEPkmnEXPChangedPacket(pokemon, oldEXP));
}
private void BroadcastPkmnEXPEarned(PBEBattlePokemon pokemon, uint earned)
{
Broadcast(new PBEPkmnEXPEarnedPacket(pokemon, earned));
}
private void BroadcastPkmnFainted(PBEBattlePokemon pokemon, PBEFieldPosition oldPosition)
{
Broadcast(new PBEPkmnFaintedPacket(pokemon, oldPosition));
}
private void BroadcastPkmnFormChanged(PBEBattlePokemon pokemon, PBEForm newForm, PBEAbility newAbility, PBEAbility newKnownAbility, bool isRevertForm)
{
pokemon.Ability = newAbility;
pokemon.KnownAbility = newKnownAbility;
pokemon.Form = newForm;
pokemon.KnownForm = newForm;
if (isRevertForm)
{
pokemon.RevertForm = newForm;
pokemon.RevertAbility = newAbility;
}
// This calcs all stats and then adds/removes HP based on the new MaxHP. So if the new MaxHP was 5 more than old, the mon would gain 5 HP.
// This is the same logic a level-up and evolution would use when HP changes.
pokemon.SetStats(true, false);
IPBEPokemonData pData = PBEDataProvider.Instance.GetPokemonData(pokemon.Species, newForm);
PBEType type1 = pData.Type1;
pokemon.Type1 = type1;
pokemon.KnownType1 = type1;
PBEType type2 = pData.Type2;
pokemon.Type2 = type2;
pokemon.KnownType2 = type2;
float weight = pData.Weight; // TODO: Is weight updated here? Bulbapedia claims in Autotomize's page that it is not
pokemon.Weight = weight;
pokemon.KnownWeight = weight;
Broadcast(new PBEPkmnFormChangedPacket(pokemon, isRevertForm));
// BUG: PBEStatus2.PowerTrick is not cleared when changing form (meaning it can still be baton passed)
if (Settings.BugFix && pokemon.Status2.HasFlag(PBEStatus2.PowerTrick))
{
BroadcastStatus2(pokemon, pokemon, PBEStatus2.PowerTrick, PBEStatusAction.Ended);
}
}
private void BroadcastPkmnHPChanged(PBEBattlePokemon pokemon, ushort oldHP, float oldHPPercentage)
{
Broadcast(new PBEPkmnHPChangedPacket(pokemon, oldHP, oldHPPercentage));
}
private void BroadcastPkmnLevelChanged(PBEBattlePokemon pokemon)
{
Broadcast(new PBEPkmnLevelChangedPacket(pokemon));
}
private void BroadcastPkmnStatChanged(PBEBattlePokemon pokemon, PBEStat stat, sbyte oldValue, sbyte newValue)
{
Broadcast(new PBEPkmnStatChangedPacket(pokemon, stat, oldValue, newValue));
}
private void BroadcastPkmnSwitchIn(PBETrainer trainer, PBEPkmnAppearedInfo[] switchIns, PBEBattlePokemon? forcedByPokemon = null)
{
Broadcast(new PBEPkmnSwitchInPacket(trainer, switchIns, forcedByPokemon));
}
private void BroadcastPkmnSwitchOut(PBEBattlePokemon pokemon, PBEFieldPosition oldPosition, PBEBattlePokemon? forcedByPokemon = null)
{
Broadcast(new PBEPkmnSwitchOutPacket(pokemon, oldPosition, forcedByPokemon));
}
private void BroadcastPsychUp(PBEBattlePokemon user, PBEBattlePokemon target)
{
user.AttackChange = target.AttackChange;
user.DefenseChange = target.DefenseChange;
user.SpAttackChange = target.SpAttackChange;
user.SpDefenseChange = target.SpDefenseChange;
user.SpeedChange = target.SpeedChange;
user.AccuracyChange = target.AccuracyChange;
user.EvasionChange = target.EvasionChange;
Broadcast(new PBEPsychUpPacket(user, target));
}
private void BroadcastReflectType(PBEBattlePokemon user, PBEBattlePokemon target)
{
user.Type1 = user.KnownType1 = target.KnownType1 = target.Type1;
user.Type2 = user.KnownType2 = target.KnownType2 = target.Type2;
Broadcast(new PBEReflectTypePacket(user, target));
}
private void BroadcastDraggedOut(PBEBattlePokemon pokemon)
{
Broadcast(new PBESpecialMessagePacket(PBESpecialMessage.DraggedOut, pokemon));
}
private void BroadcastEndure(PBEBattlePokemon pokemon)
{
Broadcast(new PBESpecialMessagePacket(PBESpecialMessage.Endure, pokemon));
}
private void BroadcastHPDrained(PBEBattlePokemon pokemon)
{
Broadcast(new PBESpecialMessagePacket(PBESpecialMessage.HPDrained, pokemon));
}
private void BroadcastMagnitude(byte magnitude)
{
Broadcast(new PBESpecialMessagePacket(PBESpecialMessage.Magnitude, magnitude));
}
private void BroadcastMultiHit(byte numHits)
{
Broadcast(new PBESpecialMessagePacket(PBESpecialMessage.MultiHit, numHits));
}
private void BroadcastNothingHappened()
{
Broadcast(new PBESpecialMessagePacket(PBESpecialMessage.NothingHappened));
}
private void BroadcastOneHitKnockout()
{
Broadcast(new PBESpecialMessagePacket(PBESpecialMessage.OneHitKnockout));
}
private void BroadcastPainSplit(PBEBattlePokemon user, PBEBattlePokemon target)
{
Broadcast(new PBESpecialMessagePacket(PBESpecialMessage.PainSplit, user, target));
}
private void BroadcastPayDay()
{
Broadcast(new PBESpecialMessagePacket(PBESpecialMessage.PayDay));
}
private void BroadcastRecoil(PBEBattlePokemon pokemon)
{
Broadcast(new PBESpecialMessagePacket(PBESpecialMessage.Recoil, pokemon));
}
private void BroadcastStruggle(PBEBattlePokemon pokemon)
{
Broadcast(new PBESpecialMessagePacket(PBESpecialMessage.Struggle, pokemon));
}
private void BroadcastStatus1(PBEBattlePokemon status1Receiver, PBEBattlePokemon pokemon2, PBEStatus1 status1, PBEStatusAction statusAction)
{
Broadcast(new PBEStatus1Packet(status1Receiver, pokemon2, status1, statusAction));
}
private void BroadcastStatus2(PBEBattlePokemon status2Receiver, PBEBattlePokemon pokemon2, PBEStatus2 status2, PBEStatusAction statusAction)
{
switch (statusAction)
{
case PBEStatusAction.Added:
case PBEStatusAction.Announced:
case PBEStatusAction.CausedImmobility:
case PBEStatusAction.Damage: status2Receiver.Status2 |= status2; status2Receiver.KnownStatus2 |= status2; break;
case PBEStatusAction.Cleared:
case PBEStatusAction.Ended: status2Receiver.Status2 &= ~status2; status2Receiver.KnownStatus2 &= ~status2; break;
default: throw new ArgumentOutOfRangeException(nameof(statusAction));
}
Broadcast(new PBEStatus2Packet(status2Receiver, pokemon2, status2, statusAction));
}
private void BroadcastTeamStatus(PBETeam team, PBETeamStatus teamStatus, PBETeamStatusAction teamStatusAction)
{
switch (teamStatusAction)
{
case PBETeamStatusAction.Added: team.TeamStatus |= teamStatus; break;
case PBETeamStatusAction.Cleared:
case PBETeamStatusAction.Ended: team.TeamStatus &= ~teamStatus; break;
default: throw new ArgumentOutOfRangeException(nameof(teamStatusAction));
}
Broadcast(new PBETeamStatusPacket(team, teamStatus, teamStatusAction));
}
private void BroadcastTeamStatusDamage(PBETeam team, PBETeamStatus teamStatus, PBEBattlePokemon damageVictim)
{
team.TeamStatus |= teamStatus;
Broadcast(new PBETeamStatusDamagePacket(team, teamStatus, damageVictim));
}
private void BroadcastTransform(PBEBattlePokemon user, PBEBattlePokemon target)
{
Broadcast(new PBETransformPacket(user, target));
}
private void BroadcastTypeChanged(PBEBattlePokemon pokemon, PBEType type1, PBEType type2)
{
pokemon.Type1 = type1;
pokemon.KnownType1 = type1;
pokemon.Type2 = type2;
pokemon.KnownType2 = type2;
Broadcast(new PBETypeChangedPacket(pokemon, type1, type2));
}
private void BroadcastWeather(PBEWeather weather, PBEWeatherAction weatherAction)
{
Broadcast(new PBEWeatherPacket(weather, weatherAction));
}
private void BroadcastWeatherDamage(PBEWeather weather, PBEBattlePokemon damageVictim)
{
Broadcast(new PBEWeatherDamagePacket(weather, damageVictim));
}
private void BroadcastWildPkmnAppeared(PBEPkmnAppearedInfo[] appearances)
{
Broadcast(new PBEWildPkmnAppearedPacket(appearances));
}
private void BroadcastActionsRequest(PBETrainer trainer)
{
Broadcast(new PBEActionsRequestPacket(trainer));
}
private void BroadcastAutoCenter(PBEBattlePokemon pokemon0, PBEFieldPosition pokemon0OldPosition, PBEBattlePokemon pokemon1, PBEFieldPosition pokemon1OldPosition)
{
Broadcast(new PBEAutoCenterPacket(pokemon0, pokemon0OldPosition, pokemon1, pokemon1OldPosition));
}
private void BroadcastBattle()
{
Broadcast(new PBEBattlePacket(this));
}
private void BroadcastBattleResult(PBEBattleResult r)
{
Broadcast(new PBEBattleResultPacket(r));
}
private void BroadcastSwitchInRequest(PBETrainer trainer)
{
Broadcast(new PBESwitchInRequestPacket(trainer));
}
private void BroadcastTurnBegan()
{
Broadcast(new PBETurnBeganPacket(TurnNumber));
}
public static string? GetDefaultMessage(PBEBattle battle, IPBEPacket packet, bool showRawHP = false, PBETrainer? userTrainer = null,
Func? pkmnNameFunc = null, Func? trainerNameFunc = null, Func? teamNameFunc = null)
{
// This is not used by switching in or out or wild Pokémon appearing; those always use the known nickname
string GetPkmnName(PBEBattlePokemon pkmn, bool firstLetterCapitalized)
{
if (pkmnNameFunc is not null)
{
return pkmnNameFunc(pkmn, firstLetterCapitalized);
}
if (pkmn.IsWild)
{
string wildPrefix = firstLetterCapitalized ? "The wild " : "the wild ";
return wildPrefix + pkmn.KnownNickname;
}
// Replay/spectator always see prefix, but if you're battling a multi-battle, your Pokémon should still have no prefix
if (userTrainer is null || (pkmn.Trainer != userTrainer && pkmn.Team.Trainers.Count > 1))
{
return $"{GetTrainerName(pkmn.Trainer)}'s {pkmn.KnownNickname}";
}
string ownerPrefix = string.Empty;
string foePrefix = firstLetterCapitalized ? "The foe's " : "the foe's ";
string prefix = pkmn.Trainer == userTrainer ? ownerPrefix : foePrefix;
return prefix + pkmn.KnownNickname;
}
string GetTrainerName(PBETrainer trainer)
{
if (trainerNameFunc is not null)
{
return trainerNameFunc(trainer);
}
return trainer.Name;
}
string GetRawCombinedName(PBETeam team, bool firstLetterCapitalized)
{
if (team.IsWild)
{
string prefix = firstLetterCapitalized ? "The" : "the";
return prefix + " wild Pokémon";
}
return team.CombinedName;
}
// This is not used by PBEBattleResultPacket; those use GetRawCombinedName()
string GetTeamName(PBETeam team, bool firstLetterCapitalized)
{
if (teamNameFunc is not null)
{
return teamNameFunc(team, firstLetterCapitalized);
}
if (userTrainer is null)
{
return $"{GetRawCombinedName(team, firstLetterCapitalized)}'s";
}
string ownerPrefix = firstLetterCapitalized ? "Your" : "your";
string foePrefix = firstLetterCapitalized ? "The opposing" : "the opposing";
return team == userTrainer.Team ? ownerPrefix : foePrefix;
}
string DoHiddenHP(PBEBattlePokemon pokemon, float percentageChange, float absPercentageChange)
{
return string.Format("{0} {1} {2:P2} of its HP!", GetPkmnName(pokemon, true), percentageChange <= 0 ? "lost" : "restored", absPercentageChange);
}
switch (packet)
{
case PBEAbilityPacket ap:
{
PBEBattlePokemon abilityOwner = ap.AbilityOwnerTrainer.GetPokemon(ap.AbilityOwner);
PBEBattlePokemon pokemon2 = ap.AbilityOwnerTrainer.GetPokemon(ap.Pokemon2);
bool abilityOwnerCaps = true,
pokemon2Caps = true;
string message;
switch (ap.Ability)
{
case PBEAbility.AirLock:
case PBEAbility.CloudNine:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Weather: message = "{0}'s {2} causes the effects of weather to disappear!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.Anticipation:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Announced: message = "{0}'s {2} made it shudder!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.BadDreams:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Damage: message = "{1} is tormented by {0}'s {2}!"; abilityOwnerCaps = false; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.BigPecks:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Stats: message = $"{{0}}'s {PBEDataProvider.Instance.GetStatName(PBEStat.Defense).English} was not lowered!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.ClearBody:
case PBEAbility.WhiteSmoke:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Stats: message = "{0}'s {2} prevents stat reduction!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.ColorChange:
case PBEAbility.FlowerGift:
case PBEAbility.Forecast:
case PBEAbility.Imposter:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.ChangedAppearance: message = "{0}'s {2} activated!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.CuteCharm:
case PBEAbility.EffectSpore:
case PBEAbility.FlameBody:
case PBEAbility.Healer:
case PBEAbility.PoisonPoint:
case PBEAbility.ShedSkin:
case PBEAbility.Static:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.ChangedStatus: message = "{0}'s {2} activated!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.Download:
case PBEAbility.Intimidate:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Stats: message = "{0}'s {2} activated!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.Drizzle:
case PBEAbility.Drought:
case PBEAbility.SandStream:
case PBEAbility.SnowWarning:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Weather: message = "{0}'s {2} activated!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.HyperCutter:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Stats: message = $"{{0}}'s {PBEDataProvider.Instance.GetStatName(PBEStat.Attack).English} was not lowered!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.IceBody:
case PBEAbility.PoisonHeal:
case PBEAbility.RainDish:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.RestoredHP: message = "{0}'s {2} activated!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.Illusion:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.ChangedAppearance: goto bottom;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
}
case PBEAbility.Immunity:
case PBEAbility.Insomnia:
case PBEAbility.Limber:
case PBEAbility.MagmaArmor:
case PBEAbility.Oblivious:
case PBEAbility.OwnTempo:
case PBEAbility.VitalSpirit:
case PBEAbility.WaterVeil:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.ChangedStatus:
case PBEAbilityAction.PreventedStatus: message = "{0}'s {2} activated!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.IronBarbs:
case PBEAbility.Justified:
case PBEAbility.Levitate:
case PBEAbility.Mummy:
case PBEAbility.Rattled:
case PBEAbility.RoughSkin:
case PBEAbility.SolarPower:
case PBEAbility.Sturdy:
case PBEAbility.WeakArmor:
case PBEAbility.WonderGuard:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Damage: message = "{0}'s {2} activated!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.KeenEye:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Stats: message = $"{{0}}'s {PBEDataProvider.Instance.GetStatName(PBEStat.Accuracy).English} was not lowered!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.LeafGuard:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.PreventedStatus: message = "{0}'s {2} activated!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.LiquidOoze:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Damage: message = "{1} sucked up the liquid ooze!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.MoldBreaker:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Announced: message = "{0} breaks the mold!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.Moody:
case PBEAbility.SpeedBoost:
case PBEAbility.Steadfast:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Stats: message = "{0}'s {2} activated!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.RunAway:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Announced: message = "{0}'s {2} activated!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.SlowStart:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Announced: message = "{0} can't get it going!"; break;
case PBEAbilityAction.SlowStart_Ended: message = "{0} finally got its act together!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.Teravolt:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Announced: message = "{0} is radiating a bursting aura!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
case PBEAbility.Turboblaze:
{
switch (ap.AbilityAction)
{
case PBEAbilityAction.Announced: message = "{0} is radiating a blazing aura!"; break;
default: throw new InvalidDataException(nameof(ap.AbilityAction));
}
break;
}
default: throw new InvalidDataException(nameof(ap.Ability));
}
return string.Format(message, GetPkmnName(abilityOwner, abilityOwnerCaps), GetPkmnName(pokemon2, pokemon2Caps), PBEDataProvider.Instance.GetAbilityName(ap.Ability).English);
}
case PBEAbilityReplacedPacket arp:
{
PBEBattlePokemon abilityOwner = arp.AbilityOwnerTrainer.GetPokemon(arp.AbilityOwner);
string message;
switch (arp.NewAbility)
{
case PBEAbility.None: message = "{0}'s {1} was suppressed!"; break;
default: message = "{0}'s {1} was changed to {2}!"; break;
}
return string.Format(message,
GetPkmnName(abilityOwner, true),
arp.OldAbility is null ? "Ability" : PBEDataProvider.Instance.GetAbilityName(arp.OldAbility.Value).English,
PBEDataProvider.Instance.GetAbilityName(arp.NewAbility).English);
}
case PBEBattleStatusPacket bsp:
{
string message;
switch (bsp.BattleStatus)
{
case PBEBattleStatus.TrickRoom:
{
switch (bsp.BattleStatusAction)
{
case PBEBattleStatusAction.Added: message = "The dimensions were twisted!"; break;
case PBEBattleStatusAction.Cleared:
case PBEBattleStatusAction.Ended: message = "The twisted dimensions returned to normal!"; break;
default: throw new InvalidDataException(nameof(bsp.BattleStatusAction));
}
break;
}
default: throw new InvalidDataException(nameof(bsp.BattleStatus));
}
return message;
}
case PBECapturePacket cp:
{
PBEBattlePokemon pokemon = cp.PokemonTrainer.GetPokemon(cp.Pokemon);
string ballEnglish = PBEDataProvider.Instance.GetItemName(cp.Ball).English;
if (cp.Success)
{
return string.Format("Gotcha! {0} was caught with the {1} after {2} shake{3}!", pokemon.Nickname, ballEnglish, cp.NumShakes, cp.NumShakes == 1 ? string.Empty : "s");
}
if (cp.NumShakes == 0)
{
return "The Pokémon broke free without shaking!";
}
return string.Format("The Pokémon broke free after {0} shake{1}!", cp.NumShakes, cp.NumShakes == 1 ? string.Empty : "s");
}
case PBEFleeFailedPacket ffp:
{
string name;
if (ffp.Pokemon == PBEFieldPosition.None)
{
name = GetTrainerName(ffp.PokemonTrainer);
}
else
{
PBEBattlePokemon pokemon = ffp.PokemonTrainer.GetPokemon(ffp.Pokemon);
name = GetPkmnName(pokemon, true);
}
return string.Format("{0} could not get away!", name);
}
case PBEHazePacket _:
{
return "All stat changes were eliminated!";
}
case PBEItemPacket ip:
{
PBEBattlePokemon itemHolder = ip.ItemHolderTrainer.GetPokemon(ip.ItemHolder);
PBEBattlePokemon pokemon2 = ip.Pokemon2Trainer.GetPokemon(ip.Pokemon2);
bool itemHolderCaps = true,
pokemon2Caps = false;
string message;
switch (ip.Item)
{
case PBEItem.AguavBerry:
case PBEItem.BerryJuice:
case PBEItem.FigyBerry:
case PBEItem.IapapaBerry:
case PBEItem.MagoBerry:
case PBEItem.OranBerry:
case PBEItem.SitrusBerry:
case PBEItem.WikiBerry:
{
switch (ip.ItemAction)
{
case PBEItemAction.Consumed: message = "{0} restored its health using its {2}!"; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.ApicotBerry:
case PBEItem.GanlonBerry:
case PBEItem.LiechiBerry:
case PBEItem.PetayaBerry:
case PBEItem.SalacBerry:
case PBEItem.StarfBerry:
{
switch (ip.ItemAction)
{
case PBEItemAction.Consumed: message = "{0} used its {2}!"; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.BugGem:
case PBEItem.DarkGem:
case PBEItem.DragonGem:
case PBEItem.ElectricGem:
case PBEItem.FightingGem:
case PBEItem.FireGem:
case PBEItem.FlyingGem:
case PBEItem.GhostGem:
case PBEItem.GrassGem:
case PBEItem.GroundGem:
case PBEItem.IceGem:
case PBEItem.NormalGem:
case PBEItem.PoisonGem:
case PBEItem.PsychicGem:
case PBEItem.RockGem:
case PBEItem.SteelGem:
case PBEItem.WaterGem:
{
switch (ip.ItemAction)
{
case PBEItemAction.Consumed: message = "The {2} strengthened {0}'s power!"; itemHolderCaps = false; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.BlackSludge:
{
switch (ip.ItemAction)
{
case PBEItemAction.Damage: message = "{0} is hurt by its {2}!"; break;
case PBEItemAction.RestoredHP: message = "{0} restored a little HP using its {2}!"; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.DestinyKnot:
{
switch (ip.ItemAction)
{
case PBEItemAction.Announced: message = "{0}'s {2} activated!"; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.FlameOrb:
{
switch (ip.ItemAction)
{
case PBEItemAction.Announced: message = "{0} was burned by its {2}!"; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.FocusBand:
{
switch (ip.ItemAction)
{
case PBEItemAction.Damage: message = "{0} hung on using its {2}!"; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.FocusSash:
{
switch (ip.ItemAction)
{
case PBEItemAction.Consumed: message = "{0} hung on using its {2}!"; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.Leftovers:
{
switch (ip.ItemAction)
{
case PBEItemAction.RestoredHP: message = "{0} restored a little HP using its {2}!"; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.LifeOrb:
{
switch (ip.ItemAction)
{
case PBEItemAction.Damage: message = "{0} is hurt by its {2}!"; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.PowerHerb:
{
switch (ip.ItemAction)
{
case PBEItemAction.Consumed: message = "{0} became fully charged due to its {2}!"; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.RockyHelmet:
{
switch (ip.ItemAction)
{
case PBEItemAction.Damage: message = "{1} was hurt by the {2}!"; pokemon2Caps = true; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.SmokeBall:
{
switch (ip.ItemAction)
{
case PBEItemAction.Announced: message = "{0} used its {2}!"; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
case PBEItem.ToxicOrb:
{
switch (ip.ItemAction)
{
case PBEItemAction.Announced: message = "{0} was badly poisoned by its {2}!"; break;
default: throw new InvalidDataException(nameof(ip.ItemAction));
}
break;
}
default: throw new InvalidDataException(nameof(ip.Item));
}
return string.Format(message, GetPkmnName(itemHolder, itemHolderCaps), GetPkmnName(pokemon2, pokemon2Caps), PBEDataProvider.Instance.GetItemName(ip.Item).English);
}
case PBEItemTurnPacket itp:
{
PBEBattlePokemon itemUser = itp.ItemUserTrainer.GetPokemon(itp.ItemUser);
string itemEnglish = PBEDataProvider.Instance.GetItemName(itp.Item).English;
switch (itp.ItemAction)
{
case PBEItemTurnAction.Attempt:
{
string word;
if (PBEDataUtils.AllBalls.Contains(itp.Item))
{
word = "threw";
}
else
{
word = "used";
}
return string.Format("{0} {1} the {2}.", GetTrainerName(itemUser.Trainer), word, itemEnglish);
}
case PBEItemTurnAction.NoEffect:
{
if (PBEDataUtils.AllBalls.Contains(itp.Item))
{
return "The trainer blocked the ball! Don't be a thief!";
}
return string.Format("The {0} had no effect.", itemEnglish);
}
case PBEItemTurnAction.Success:
{
//string message;
switch (itp.Item)
{
// No "success" items yet
default: throw new InvalidDataException(nameof(itp.Item));
}
//return string.Format(message, GetPkmnName(itemUser, true), itemEnglish);
}
default: throw new InvalidDataException(nameof(itp.ItemAction));
}
}
case PBEMoveCritPacket mcp:
{
PBEBattlePokemon victim = mcp.VictimTrainer.GetPokemon(mcp.Victim);
return string.Format("A critical hit on {0}!", GetPkmnName(victim, false));
}
case PBEMovePPChangedPacket mpcp:
{
PBEBattlePokemon moveUser = mpcp.MoveUserTrainer.GetPokemon(mpcp.MoveUser);
return string.Format("{0}'s {1} {3} {2} PP!",
GetPkmnName(moveUser, true),
PBEDataProvider.Instance.GetMoveName(mpcp.Move).English,
Math.Abs(mpcp.AmountReduced),
mpcp.AmountReduced >= 0 ? "lost" : "gained");
}
case PBEMoveResultPacket mrp:
{
PBEBattlePokemon moveUser = mrp.MoveUserTrainer.GetPokemon(mrp.MoveUser);
PBEBattlePokemon pokemon2 = mrp.Pokemon2Trainer.GetPokemon(mrp.Pokemon2);
bool pokemon2Caps = true;
string message;
switch (mrp.Result)
{
case PBEResult.Ineffective_Ability: message = "{1} is protected by its Ability!"; break;
case PBEResult.Ineffective_Gender: message = "It doesn't affect {1}..."; pokemon2Caps = false; break;
case PBEResult.Ineffective_Level: message = "{1} is protected by its level!"; break;
case PBEResult.Ineffective_MagnetRise: message = $"{{1}} is protected by {PBEDataProvider.Instance.GetMoveName(PBEMove.MagnetRise).English}!"; break;
case PBEResult.Ineffective_Safeguard: message = $"{{1}} is protected by {PBEDataProvider.Instance.GetMoveName(PBEMove.Safeguard).English}!"; break;
case PBEResult.Ineffective_Stat:
case PBEResult.Ineffective_Status:
case PBEResult.InvalidConditions: message = "But it failed!"; break;
case PBEResult.Ineffective_Substitute: message = $"{{1}} is protected by {PBEDataProvider.Instance.GetMoveName(PBEMove.Substitute).English}!"; break;
case PBEResult.Ineffective_Type: message = "{1} is protected by its Type!"; break;
case PBEResult.Missed: message = "{0}'s attack missed {1}!"; pokemon2Caps = false; break;
case PBEResult.NoTarget: message = "But there was no target..."; break;
case PBEResult.NotVeryEffective_Type: message = "It's not very effective on {1}..."; pokemon2Caps = false; break;
case PBEResult.SuperEffective_Type: message = "It's super effective on {1}!"; pokemon2Caps = false; break;
default: throw new InvalidDataException(nameof(mrp.Result));
}
return string.Format(message, GetPkmnName(moveUser, true), GetPkmnName(pokemon2, pokemon2Caps));
}
case PBEMoveUsedPacket mup:
{
PBEBattlePokemon moveUser = mup.MoveUserTrainer.GetPokemon(mup.MoveUser);
return string.Format("{0} used {1}!", GetPkmnName(moveUser, true), PBEDataProvider.Instance.GetMoveName(mup.Move).English);
}
case PBEPkmnFaintedPacket pfp:
{
PBEBattlePokemon pokemon = pfp.PokemonTrainer.GetPokemon(pfp.Pokemon);
return string.Format("{0} fainted!", GetPkmnName(pokemon, true));
}
case PBEPkmnEXPEarnedPacket peep:
{
PBEBattlePokemon pokemon = peep.PokemonTrainer.GetPokemon(peep.Pokemon);
return string.Format("{0} earned {1} EXP point(s)!", GetPkmnName(pokemon, true), peep.Earned);
}
case PBEPkmnFaintedPacket_Hidden pfph:
{
PBEBattlePokemon pokemon = pfph.PokemonTrainer.GetPokemon(pfph.OldPosition);
return string.Format("{0} fainted!", GetPkmnName(pokemon, true));
}
case IPBEPkmnFormChangedPacket pfcp:
{
PBEBattlePokemon pokemon = pfcp.PokemonTrainer.GetPokemon(pfcp.Pokemon);
return string.Format("{0}'s new form is {1}!", GetPkmnName(pokemon, true), PBEDataProvider.Instance.GetFormName(pokemon.Species, pfcp.NewForm).English);
}
case PBEPkmnHPChangedPacket phcp:
{
PBEBattlePokemon pokemon = phcp.PokemonTrainer.GetPokemon(phcp.Pokemon);
float percentageChange = phcp.NewHPPercentage - phcp.OldHPPercentage;
float absPercentageChange = Math.Abs(percentageChange);
if (showRawHP || userTrainer == pokemon.Trainer) // Owner should see raw values
{
int change = phcp.NewHP - phcp.OldHP;
int absChange = Math.Abs(change);
return string.Format("{0} {1} {2} ({3:P2}) HP!", GetPkmnName(pokemon, true), change <= 0 ? "lost" : "restored", absChange, absPercentageChange);
}
return DoHiddenHP(pokemon, percentageChange, absPercentageChange);
}
case PBEPkmnHPChangedPacket_Hidden phcph:
{
PBEBattlePokemon pokemon = phcph.PokemonTrainer.GetPokemon(phcph.Pokemon);
float percentageChange = phcph.NewHPPercentage - phcph.OldHPPercentage;
float absPercentageChange = Math.Abs(percentageChange);
return DoHiddenHP(pokemon, percentageChange, absPercentageChange);
}
case PBEPkmnLevelChangedPacket plcp:
{
PBEBattlePokemon pokemon = plcp.PokemonTrainer.GetPokemon(plcp.Pokemon);
return string.Format("{0} grew to level {1}!", GetPkmnName(pokemon, true), plcp.NewLevel);
}
case PBEPkmnStatChangedPacket pscp:
{
PBEBattlePokemon pokemon = pscp.PokemonTrainer.GetPokemon(pscp.Pokemon);
string statName, message;
switch (pscp.Stat)
{
case PBEStat.Accuracy: statName = "Accuracy"; break;
case PBEStat.Attack: statName = "Attack"; break;
case PBEStat.Defense: statName = "Defense"; break;
case PBEStat.Evasion: statName = "Evasion"; break;
case PBEStat.SpAttack: statName = "Special Attack"; break;
case PBEStat.SpDefense: statName = "Special Defense"; break;
case PBEStat.Speed: statName = "Speed"; break;
default: throw new InvalidDataException(nameof(pscp.Stat));
}
int change = pscp.NewValue - pscp.OldValue;
switch (change)
{
case -2: message = "harshly fell"; break;
case -1: message = "fell"; break;
case +1: message = "rose"; break;
case +2: message = "rose sharply"; break;
default:
{
if (change == 0 && pscp.NewValue == -battle.Settings.MaxStatChange)
{
message = "won't go lower";
}
else if (change == 0 && pscp.NewValue == battle.Settings.MaxStatChange)
{
message = "won't go higher";
}
else if (change <= -3)
{
message = "severely fell";
}
else if (change >= +3)
{
message = "rose drastically";
}
else
{
throw new Exception();
}
break;
}
}
return string.Format("{0}'s {1} {2}!", GetPkmnName(pokemon, true), statName, message);
}
case IPBEPkmnSwitchInPacket psip:
{
if (!psip.Forced)
{
return string.Format("{1} sent out {0}!", psip.SwitchIns.Select(s => s.Nickname).ToArray().Andify(), GetTrainerName(psip.Trainer));
}
goto bottom;
}
case PBEPkmnSwitchOutPacket psop:
{
if (!psop.Forced)
{
PBEBattlePokemon pokemon = psop.PokemonTrainer.GetPokemon(psop.Pokemon);
return string.Format("{1} withdrew {0}!", pokemon.KnownNickname, GetTrainerName(psop.PokemonTrainer));
}
goto bottom;
}
case PBEPkmnSwitchOutPacket_Hidden psoph:
{
if (!psoph.Forced)
{
PBEBattlePokemon pokemon = psoph.PokemonTrainer.GetPokemon(psoph.OldPosition);
return string.Format("{1} withdrew {0}!", pokemon.KnownNickname, GetTrainerName(psoph.PokemonTrainer));
}
goto bottom;
}
case PBEPsychUpPacket pup:
{
PBEBattlePokemon user = pup.UserTrainer.GetPokemon(pup.User);
PBEBattlePokemon target = pup.TargetTrainer.GetPokemon(pup.Target);
return string.Format("{0} copied {1}'s stat changes!", GetPkmnName(user, true), GetPkmnName(target, false));
}
case PBEReflectTypePacket rtp:
{
PBEBattlePokemon user = rtp.UserTrainer.GetPokemon(rtp.User);
PBEBattlePokemon target = rtp.TargetTrainer.GetPokemon(rtp.Target);
string type1Str = PBEDataProvider.Instance.GetTypeName(rtp.Type1).English;
return string.Format("{0} copied {1}'s {2}",
GetPkmnName(user, true),
GetPkmnName(target, false),
rtp.Type2 == PBEType.None ? $"{type1Str} type!" : $"{type1Str} and {PBEDataProvider.Instance.GetTypeName(rtp.Type2).English} types!");
}
case PBEReflectTypePacket_Hidden rtph:
{
PBEBattlePokemon user = rtph.UserTrainer.GetPokemon(rtph.User);
PBEBattlePokemon target = rtph.TargetTrainer.GetPokemon(rtph.Target);
return string.Format("{0} copied {1}'s types!", GetPkmnName(user, true), GetPkmnName(target, false));
}
case PBESpecialMessagePacket smp: // TODO: Clean
{
string message;
switch (smp.Message)
{
case PBESpecialMessage.DraggedOut: message = string.Format("{0} was dragged out!", GetPkmnName(((PBETrainer)smp.Params[0]).GetPokemon((PBEFieldPosition)smp.Params[1]), true)); break;
case PBESpecialMessage.Endure: message = string.Format("{0} endured the hit!", GetPkmnName(((PBETrainer)smp.Params[0]).GetPokemon((PBEFieldPosition)smp.Params[1]), true)); break;
case PBESpecialMessage.HPDrained: message = string.Format("{0} had its energy drained!", GetPkmnName(((PBETrainer)smp.Params[0]).GetPokemon((PBEFieldPosition)smp.Params[1]), true)); break;
case PBESpecialMessage.Magnitude: message = string.Format("Magnitude {0}!", (byte)smp.Params[0]); break;
case PBESpecialMessage.MultiHit: message = string.Format("Hit {0} time(s)!", (byte)smp.Params[0]); break;
case PBESpecialMessage.NothingHappened: message = "But nothing happened!"; break;
case PBESpecialMessage.OneHitKnockout: message = "It's a one-hit KO!"; break;
case PBESpecialMessage.PainSplit: message = "The battlers shared their pain!"; break;
case PBESpecialMessage.PayDay: message = "Coins were scattered everywhere!"; break;
case PBESpecialMessage.Recoil: message = string.Format("{0} is damaged by recoil!", GetPkmnName(((PBETrainer)smp.Params[0]).GetPokemon((PBEFieldPosition)smp.Params[1]), true)); break;
case PBESpecialMessage.Struggle: message = string.Format("{0} has no moves left!", GetPkmnName(((PBETrainer)smp.Params[0]).GetPokemon((PBEFieldPosition)smp.Params[1]), true)); break;
default: throw new InvalidDataException(nameof(smp.Message));
}
return message;
}
case PBEStatus1Packet s1p:
{
PBEBattlePokemon status1Receiver = s1p.Status1ReceiverTrainer.GetPokemon(s1p.Status1Receiver);
string message;
switch (s1p.Status1)
{
case PBEStatus1.Asleep:
{
switch (s1p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} fell asleep!"; break;
case PBEStatusAction.CausedImmobility: message = "{0} is fast asleep."; break;
case PBEStatusAction.Cleared:
case PBEStatusAction.Ended: message = "{0} woke up!"; break;
default: throw new InvalidDataException(nameof(s1p.StatusAction));
}
break;
}
case PBEStatus1.BadlyPoisoned:
{
switch (s1p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} was badly poisoned!"; break;
case PBEStatusAction.Cleared: message = "{0} was cured of its poisoning."; break;
case PBEStatusAction.Damage: message = "{0} was hurt by poison!"; break;
default: throw new InvalidDataException(nameof(s1p.StatusAction));
}
break;
}
case PBEStatus1.Burned:
{
switch (s1p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} was burned!"; break;
case PBEStatusAction.Cleared: message = "{0}'s burn was healed."; break;
case PBEStatusAction.Damage: message = "{0} was hurt by its burn!"; break;
default: throw new InvalidDataException(nameof(s1p.StatusAction));
}
break;
}
case PBEStatus1.Frozen:
{
switch (s1p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} was frozen solid!"; break;
case PBEStatusAction.CausedImmobility: message = "{0} is frozen solid!"; break;
case PBEStatusAction.Cleared:
case PBEStatusAction.Ended: message = "{0} thawed out!"; break;
default: throw new InvalidDataException(nameof(s1p.StatusAction));
}
break;
}
case PBEStatus1.Paralyzed:
{
switch (s1p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} is paralyzed! It may be unable to move!"; break;
case PBEStatusAction.CausedImmobility: message = "{0} is paralyzed! It can't move!"; break;
case PBEStatusAction.Cleared: message = "{0} was cured of paralysis."; break;
default: throw new InvalidDataException(nameof(s1p.StatusAction));
}
break;
}
case PBEStatus1.Poisoned:
{
switch (s1p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} was poisoned!"; break;
case PBEStatusAction.Cleared: message = "{0} was cured of its poisoning."; break;
case PBEStatusAction.Damage: message = "{0} was hurt by poison!"; break;
default: throw new InvalidDataException(nameof(s1p.StatusAction));
}
break;
}
default: throw new InvalidDataException(nameof(s1p.Status1));
}
return string.Format(message, GetPkmnName(status1Receiver, true));
}
case PBEStatus2Packet s2p:
{
PBEBattlePokemon status2Receiver = s2p.Status2ReceiverTrainer.GetPokemon(s2p.Status2Receiver);
PBEBattlePokemon pokemon2 = s2p.Pokemon2Trainer.GetPokemon(s2p.Pokemon2);
string message;
bool status2ReceiverCaps = true,
pokemon2Caps = false;
switch (s2p.Status2)
{
case PBEStatus2.Airborne:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} flew up high!"; break;
case PBEStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Confused:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} became confused!"; break;
case PBEStatusAction.Announced: message = "{0} is confused!"; break;
case PBEStatusAction.Cleared:
case PBEStatusAction.Ended: message = "{0} snapped out of its confusion."; break;
case PBEStatusAction.Damage: message = "It hurt itself in its confusion!"; break;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Cursed:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{1} cut its own HP and laid a curse on {0}!"; status2ReceiverCaps = false; pokemon2Caps = true; break;
case PBEStatusAction.Damage: message = "{0} is afflicted by the curse!"; break;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Disguised:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Ended: message = "{0}'s illusion wore off!"; break;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Flinching:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.CausedImmobility: message = "{0} flinched and couldn't move!"; break;
case PBEStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Identified:
case PBEStatus2.MiracleEye:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} was identified!"; break;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.HelpingHand:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{1} is ready to help {0}!"; status2ReceiverCaps = false; pokemon2Caps = true; break;
case PBEStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Infatuated:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} fell in love with {1}!"; break;
case PBEStatusAction.Announced: message = "{0} is in love with {1}!"; break;
case PBEStatusAction.CausedImmobility: message = "{0} is immobilized by love!"; break;
case PBEStatusAction.Cleared:
case PBEStatusAction.Ended: message = "{0} got over its infatuation."; break;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.LeechSeed:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} was seeded!"; break;
case PBEStatusAction.Damage: message = "{0}'s health is sapped by Leech Seed!"; break;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.LockOn:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} took aim at {1}!"; break;
case PBEStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.MagnetRise:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} levitated with electromagnetism!"; break;
case PBEStatusAction.Ended: message = "{0}'s electromagnetism wore off!"; break;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Nightmare:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} began having a nightmare!"; break;
case PBEStatusAction.Damage: message = "{0} is locked in a nightmare!"; break;
case PBEStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.PowerTrick:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} switched its Attack and Defense!"; break;
case PBEStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Protected:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added:
case PBEStatusAction.Damage: message = "{0} protected itself!"; break;
case PBEStatusAction.Cleared: message = "{1} broke through {0}'s protection!"; status2ReceiverCaps = false; pokemon2Caps = true; break;
case PBEStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Pumped:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} is getting pumped!"; break;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Roost:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added:
case PBEStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
}
case PBEStatus2.ShadowForce:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} vanished instantly!"; break;
case PBEStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Substitute:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} put in a substitute!"; break;
case PBEStatusAction.Damage: message = "The substitute took damage for {0}!"; status2ReceiverCaps = false; break;
case PBEStatusAction.Ended: message = "{0}'s substitute faded!"; break;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Transformed:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} transformed into {1}!"; break;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Underground:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} burrowed its way under the ground!"; break;
case PBEStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
case PBEStatus2.Underwater:
{
switch (s2p.StatusAction)
{
case PBEStatusAction.Added: message = "{0} hid underwater!"; break;
case PBEStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(s2p.StatusAction));
}
break;
}
default: throw new InvalidDataException(nameof(s2p.Status2));
}
return string.Format(message, GetPkmnName(status2Receiver, status2ReceiverCaps), GetPkmnName(pokemon2, pokemon2Caps));
}
case PBETeamStatusPacket tsp:
{
string message;
bool teamCaps = true;
switch (tsp.TeamStatus)
{
case PBETeamStatus.LightScreen:
{
switch (tsp.TeamStatusAction)
{
case PBETeamStatusAction.Added: message = "Light Screen raised {0} team's Special Defense!"; teamCaps = false; break;
case PBETeamStatusAction.Cleared:
case PBETeamStatusAction.Ended: message = "{0} team's Light Screen wore off!"; break;
default: throw new InvalidDataException(nameof(tsp.TeamStatusAction));
}
break;
}
case PBETeamStatus.LuckyChant:
{
switch (tsp.TeamStatusAction)
{
case PBETeamStatusAction.Added: message = "The Lucky Chant shielded {0} team from critical hits!"; teamCaps = false; break;
case PBETeamStatusAction.Ended: message = "{0} team's Lucky Chant wore off!"; break;
default: throw new InvalidDataException(nameof(tsp.TeamStatusAction));
}
break;
}
case PBETeamStatus.QuickGuard:
{
switch (tsp.TeamStatusAction)
{
case PBETeamStatusAction.Added: message = "Quick Guard protected {0} team!"; teamCaps = false; break;
case PBETeamStatusAction.Cleared: message = "{0} team's Quick Guard was destroyed!"; break;
case PBETeamStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(tsp.TeamStatusAction));
}
break;
}
case PBETeamStatus.Reflect:
{
switch (tsp.TeamStatusAction)
{
case PBETeamStatusAction.Added: message = "Reflect raised {0} team's Defense!"; teamCaps = false; break;
case PBETeamStatusAction.Cleared:
case PBETeamStatusAction.Ended: message = "{0} team's Reflect wore off!"; break;
default: throw new InvalidDataException(nameof(tsp.TeamStatusAction));
}
break;
}
case PBETeamStatus.Safeguard:
{
switch (tsp.TeamStatusAction)
{
case PBETeamStatusAction.Added: message = "{0} team became cloaked in a mystical veil!"; break;
case PBETeamStatusAction.Ended: message = "{0} team is no longer protected by Safeguard!"; break;
default: throw new InvalidDataException(nameof(tsp.TeamStatusAction));
}
break;
}
case PBETeamStatus.Spikes:
{
switch (tsp.TeamStatusAction)
{
case PBETeamStatusAction.Added: message = "Spikes were scattered all around the feet of {0} team!"; teamCaps = false; break;
//case PBETeamStatusAction.Cleared: message = "The spikes disappeared from around {0} team's feet!"; teamCaps = false; break;
default: throw new InvalidDataException(nameof(tsp.TeamStatusAction));
}
break;
}
case PBETeamStatus.StealthRock:
{
switch (tsp.TeamStatusAction)
{
case PBETeamStatusAction.Added: message = "Pointed stones float in the air around {0} team!"; teamCaps = false; break;
//case PBETeamStatusAction.Cleared: message = "The pointed stones disappeared from around {0} team!"; teamCaps = false; break;
default: throw new InvalidDataException(nameof(tsp.TeamStatusAction));
}
break;
}
case PBETeamStatus.Tailwind:
{
switch (tsp.TeamStatusAction)
{
case PBETeamStatusAction.Added: message = "The tailwind blew from behind {0} team!"; teamCaps = false; break;
case PBETeamStatusAction.Ended: message = "{0} team's tailwind petered out!"; break;
default: throw new InvalidDataException(nameof(tsp.TeamStatusAction));
}
break;
}
case PBETeamStatus.ToxicSpikes:
{
switch (tsp.TeamStatusAction)
{
case PBETeamStatusAction.Added: message = "Poison spikes were scattered all around {0} team's feet!"; break;
case PBETeamStatusAction.Cleared: message = "The poison spikes disappeared from around {0} team's feet!"; break;
default: throw new InvalidDataException(nameof(tsp.TeamStatusAction));
}
break;
}
case PBETeamStatus.WideGuard:
{
switch (tsp.TeamStatusAction)
{
case PBETeamStatusAction.Added: message = "Wide Guard protected {0} team!"; break;
case PBETeamStatusAction.Cleared: message = "{0} team's Wide Guard was destroyed!"; break;
case PBETeamStatusAction.Ended: goto bottom;
default: throw new InvalidDataException(nameof(tsp.TeamStatusAction));
}
break;
}
default: throw new InvalidDataException(nameof(tsp.TeamStatus));
}
return string.Format(message, GetTeamName(tsp.Team, teamCaps));
}
case PBETeamStatusDamagePacket tsdp:
{
PBEBattlePokemon damageVictim = tsdp.DamageVictimTrainer.GetPokemon(tsdp.DamageVictim);
string message;
bool damageVictimCaps = false;
switch (tsdp.TeamStatus)
{
case PBETeamStatus.QuickGuard: message = "Quick Guard protected {0}!"; break;
case PBETeamStatus.Spikes: message = "{0} is hurt by the spikes!"; damageVictimCaps = true; break;
case PBETeamStatus.StealthRock: message = "Pointed stones dug into {0}!"; break;
case PBETeamStatus.WideGuard: message = "Wide Guard protected {0}!"; break;
default: throw new InvalidDataException(nameof(tsdp.TeamStatus));
}
return string.Format(message, GetPkmnName(damageVictim, damageVictimCaps));
}
case PBETypeChangedPacket tcp:
{
PBEBattlePokemon pokemon = tcp.PokemonTrainer.GetPokemon(tcp.Pokemon);
string type1Str = PBEDataProvider.Instance.GetTypeName(tcp.Type1).English;
return string.Format("{0} transformed into the {1}",
GetPkmnName(pokemon, true),
tcp.Type2 == PBEType.None ? $"{type1Str} type!" : $"{type1Str} and {PBEDataProvider.Instance.GetTypeName(tcp.Type2).English} types!");
}
case PBEWeatherPacket wp:
{
switch (wp.Weather)
{
case PBEWeather.Hailstorm:
{
switch (wp.WeatherAction)
{
case PBEWeatherAction.Added: return "It started to hail!";
case PBEWeatherAction.Ended: return "The hail stopped.";
default: throw new InvalidDataException(nameof(wp.WeatherAction));
}
}
case PBEWeather.HarshSunlight:
{
switch (wp.WeatherAction)
{
case PBEWeatherAction.Added: return "The sunlight turned harsh!";
case PBEWeatherAction.Ended: return "The sunlight faded.";
default: throw new InvalidDataException(nameof(wp.WeatherAction));
}
}
case PBEWeather.Rain:
{
switch (wp.WeatherAction)
{
case PBEWeatherAction.Added: return "It started to rain!";
case PBEWeatherAction.Ended: return "The rain stopped.";
default: throw new InvalidDataException(nameof(wp.WeatherAction));
}
}
case PBEWeather.Sandstorm:
{
switch (wp.WeatherAction)
{
case PBEWeatherAction.Added: return "A sandstorm kicked up!";
case PBEWeatherAction.Ended: return "The sandstorm subsided.";
default: throw new InvalidDataException(nameof(wp.WeatherAction));
}
}
default: throw new InvalidDataException(nameof(wp.Weather));
}
}
case PBEWeatherDamagePacket wdp:
{
PBEBattlePokemon damageVictim = wdp.DamageVictimTrainer.GetPokemon(wdp.DamageVictim);
string message;
switch (wdp.Weather)
{
case PBEWeather.Hailstorm: message = "{0} is buffeted by the hail!"; break;
case PBEWeather.Sandstorm: message = "{0} is buffeted by the sandstorm!"; break;
default: throw new InvalidDataException(nameof(wdp.Weather));
}
return string.Format(message, GetPkmnName(damageVictim, true));
}
case IPBEWildPkmnAppearedPacket wpap:
{
return string.Format("{0}{1} appeared!", wpap.Pokemon.Count == 1 ? "A wild " : "Oh! A wild ", wpap.Pokemon.Select(s => s.Nickname).ToArray().Andify());
}
case PBEActionsRequestPacket arp:
{
return string.Format("{0} must submit actions for {1} Pokémon.", GetTrainerName(arp.Trainer), arp.Pokemon.Count);
}
case IPBEAutoCenterPacket _:
{
return "The battlers shifted to the center!";
}
case PBEBattleResultPacket brp:
{
bool team0Caps = true;
bool team1Caps = false;
string message;
switch (brp.BattleResult)
{
case PBEBattleResult.Team0Forfeit: message = "{0} forfeited."; break;
case PBEBattleResult.Team0Win: message = "{0} defeated {1}!"; break;
case PBEBattleResult.Team1Forfeit: message = "{1} forfeited."; team1Caps = true; break;
case PBEBattleResult.Team1Win: message = "{1} defeated {0}!"; team0Caps = false; team1Caps = true; break;
case PBEBattleResult.WildCapture: goto bottom;
case PBEBattleResult.WildEscape: message = "{0} got away!"; break;
case PBEBattleResult.WildFlee: message = "{1} got away!"; team1Caps = true; break;
default: throw new InvalidDataException(nameof(brp.BattleResult));
}
return string.Format(message, GetRawCombinedName(battle.Teams[0], team0Caps), GetRawCombinedName(battle.Teams[1], team1Caps));
}
case PBESwitchInRequestPacket sirp:
{
return string.Format("{0} must send in {1} Pokémon.", GetTrainerName(sirp.Trainer), sirp.Amount);
}
case PBETurnBeganPacket tbp:
{
return string.Format("Turn {0} is starting.", tbp.TurnNumber);
}
}
bottom:
return null;
}
/// Writes battle events to in English.
/// The battle that belongs to.
/// The battle event packet.
/// Thrown when or are null.
public static void ConsoleBattleEventHandler(PBEBattle battle, IPBEPacket packet)
{
string? message = GetDefaultMessage(battle, packet, showRawHP: true);
if (string.IsNullOrEmpty(message))
{
return;
}
Console.WriteLine(message);
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattleInventory.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using Kermalis.PokemonBattleEngine.Packets;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Kermalis.PokemonBattleEngine.Battle;
public sealed class PBEBattleInventory : IReadOnlyDictionary
{
public sealed class PBEBattleInventorySlot
{
public PBEItem Item { get; }
public uint Quantity { get; internal set; }
internal PBEBattleInventorySlot(PBEItem item, uint quantity)
{
Item = item;
Quantity = quantity;
}
}
private readonly Dictionary _slots;
public PBEBattleInventorySlot this[PBEItem key] => _slots[key];
public IEnumerable Keys => _slots.Keys;
public IEnumerable Values => _slots.Values;
public int Count => _slots.Count;
internal PBEBattleInventory(IReadOnlyList<(PBEItem item, uint quantity)> items)
{
_slots = new Dictionary(items.Count);
foreach ((PBEItem item, uint quantity) in items)
{
if (item == PBEItem.None || !Enum.IsDefined(item))
{
throw new ArgumentOutOfRangeException(nameof(items), $"Invalid item returned: {item}");
}
if (!_slots.TryGetValue(item, out PBEBattleInventorySlot? slot))
{
slot = new PBEBattleInventorySlot(item, quantity);
_slots.Add(item, slot);
}
else
{
slot.Quantity += quantity;
}
}
}
internal PBEBattleInventory(IReadOnlyList items)
{
_slots = new Dictionary(items.Count);
foreach (PBEBattlePacket.PBETeamInfo.PBETrainerInfo.PBEInventorySlotInfo slot in items)
{
_slots.Add(slot.Item, new PBEBattleInventorySlot(slot.Item, slot.Quantity));
}
}
public bool ContainsKey(PBEItem key)
{
return _slots.ContainsKey(key);
}
public bool TryGetValue(PBEItem key, [NotNullWhen(true)] out PBEBattleInventorySlot? value)
{
return _slots.TryGetValue(key, out value);
}
public IEnumerator> GetEnumerator()
{
return _slots.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return _slots.GetEnumerator();
}
private static readonly PBEBattleInventory _empty = new(Array.Empty<(PBEItem, uint)>());
internal static PBEBattleInventory Empty()
{
return _empty;
}
internal void Remove(PBEItem item)
{
_slots[item].Quantity--;
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattleMoveset.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using Kermalis.PokemonBattleEngine.Data.Utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace Kermalis.PokemonBattleEngine.Battle;
public sealed class PBEBattleMoveset : IReadOnlyList
{
public sealed class PBEBattleMovesetSlot : INotifyPropertyChanged
{
private void OnPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler? PropertyChanged;
private PBEMove _move;
public PBEMove Move
{
get => _move;
set
{
if (_move != value)
{
_move = value;
OnPropertyChanged(nameof(Move));
}
}
}
private int _pp;
public int PP
{
get => _pp;
set
{
if (_pp != value)
{
_pp = value;
OnPropertyChanged(nameof(PP));
}
}
}
private int _maxPP;
public int MaxPP
{
get => _maxPP;
set
{
if (_maxPP != value)
{
_maxPP = value;
OnPropertyChanged(nameof(MaxPP));
}
}
}
internal PBEBattleMovesetSlot()
{
_move = PBEMove.MAX;
}
internal PBEBattleMovesetSlot(PBEMove move, int pp, int maxPP)
{
_move = move;
_pp = pp;
_maxPP = maxPP;
}
}
private readonly PBEBattleMovesetSlot[] _list;
public int Count => _list.Length;
public PBEBattleMovesetSlot this[int index]
{
get
{
if (index >= _list.Length)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
return _list[index];
}
}
public PBEBattleMovesetSlot? this[PBEMove move]
{
get
{
for (int i = 0; i < _list.Length; i++)
{
PBEBattleMovesetSlot slot = _list[i];
if (slot.Move == move)
{
return slot;
}
}
return null;
}
}
internal PBEBattleMoveset(PBESettings settings)
{
int count = settings.NumMoves;
_list = new PBEBattleMovesetSlot[count];
for (int i = 0; i < count; i++)
{
_list[i] = new PBEBattleMovesetSlot();
}
}
internal PBEBattleMoveset(PBESettings settings, PBEReadOnlyPartyMoveset moveset)
{
int count = moveset.Count;
if (count != settings.NumMoves)
{
throw new ArgumentOutOfRangeException(nameof(moveset), $"Moveset count must be equal to \"{nameof(settings.NumMoves)}\" ({settings.NumMoves}).");
}
_list = new PBEBattleMovesetSlot[count];
for (int i = 0; i < count; i++)
{
PBEReadOnlyPartyMoveset.PBEReadOnlyPartyMovesetSlot slot = moveset[i];
PBEMove move = slot.Move;
int maxPP = PBEDataUtils.CalcMaxPP(move, slot.PPUps, settings);
_list[i] = new PBEBattleMovesetSlot(move, slot.PP, maxPP);
}
}
internal PBEBattleMoveset(PBEBattleMoveset other)
{
int count = other._list.Length;
_list = new PBEBattleMovesetSlot[count];
for (int i = 0; i < count; i++)
{
PBEBattleMovesetSlot oSlot = other._list[i];
_list[i] = new PBEBattleMovesetSlot(oSlot.Move, oSlot.PP, oSlot.MaxPP);
}
}
public static int GetTransformPP(PBESettings settings, PBEMove move)
{
settings.ShouldBeReadOnly(nameof(settings));
if (move == PBEMove.None)
{
return 0;
}
if (move >= PBEMove.MAX)
{
throw new ArgumentOutOfRangeException(nameof(move));
}
IPBEMoveData mData = PBEDataProvider.Instance.GetMoveData(move);
if (!mData.IsMoveUsable())
{
throw new ArgumentOutOfRangeException(nameof(move));
}
return mData.PPTier == 0 ? 1 : settings.PPMultiplier;
}
public static int GetNonTransformPP(PBESettings settings, PBEMove move, byte ppUps)
{
settings.ShouldBeReadOnly(nameof(settings));
if (move == PBEMove.None)
{
return 0;
}
if (move >= PBEMove.MAX)
{
throw new ArgumentOutOfRangeException(nameof(move));
}
IPBEMoveData mData = PBEDataProvider.Instance.GetMoveData(move);
if (!mData.IsMoveUsable())
{
throw new ArgumentOutOfRangeException(nameof(move));
}
byte tier = mData.PPTier;
return Math.Max(1, (tier * settings.PPMultiplier) + (tier * ppUps));
}
internal static void DoTransform(PBEBattlePokemon user, PBEBattlePokemon target)
{
PBEBattleMoveset? targetKnownBackup = null;
if (user.Trainer != target.Trainer)
{
targetKnownBackup = new PBEBattleMoveset(target.KnownMoves);
}
PBESettings settings = user.Battle.Settings;
for (int i = 0; i < settings.NumMoves; i++)
{
PBEBattleMovesetSlot userMove = user.Moves._list[i];
PBEBattleMovesetSlot userKnownMove = user.KnownMoves._list[i];
PBEBattleMovesetSlot targetMove = target.Moves._list[i];
PBEBattleMovesetSlot targetKnownMove = target.KnownMoves._list[i];
PBEMove move;
int pp;
if (user.Trainer == target.Trainer)
{
move = targetMove.Move;
pp = move == PBEMove.MAX ? 0 : GetTransformPP(settings, move);
userMove.Move = move;
userMove.PP = pp;
userMove.MaxPP = pp;
move = targetKnownMove.Move;
pp = move == PBEMove.MAX ? 0 : GetTransformPP(settings, move);
userKnownMove.Move = move;
userKnownMove.PP = 0;
userKnownMove.MaxPP = pp;
}
else
{
move = targetMove.Move;
pp = move == PBEMove.MAX ? 0 : GetTransformPP(settings, move);
userMove.Move = move;
userMove.PP = pp;
userMove.MaxPP = pp;
targetKnownMove.Move = move;
// Try to copy known PP from previous known moves
PBEBattleMovesetSlot? bSlot = targetKnownBackup![move];
if (bSlot is null) // bSlot is null if the current move was not previously known
{
targetKnownMove.PP = 0;
targetKnownMove.MaxPP = 0;
}
else
{
targetKnownMove.PP = bSlot.PP;
targetKnownMove.MaxPP = bSlot.MaxPP;
}
userKnownMove.Move = move;
userKnownMove.PP = 0;
userKnownMove.MaxPP = pp;
}
}
}
internal ReadOnlyCollection ForTransformPacket()
{
var moves = new PBEMove[_list.Length];
for (int i = 0; i < _list.Length; i++)
{
moves[i] = _list[i].Move;
}
return new ReadOnlyCollection(moves);
}
// Reorders after one move is changed. It won't work if there are multiple culprit spots
internal void Organize()
{
for (int i = 0; i < _list.Length - 1; i++)
{
PBEBattleMovesetSlot slot = _list[i];
if (slot.Move != PBEMove.None && slot.Move != PBEMove.MAX)
{
continue; // Skip populated slots
}
PBEBattleMovesetSlot nextSlot = _list[i + 1];
if (nextSlot.Move != PBEMove.None && nextSlot.Move != PBEMove.MAX)
{
_list[i] = nextSlot;
_list[i + 1] = slot; // Swap slots since next slot has a move but current doesn't
}
}
}
internal void Reset(PBEBattleMoveset other)
{
for (int i = 0; i < _list.Length; i++)
{
PBEBattleMovesetSlot slot = _list[i];
PBEBattleMovesetSlot oSlot = other._list[i];
slot.Move = oSlot.Move;
slot.PP = oSlot.PP;
slot.MaxPP = oSlot.MaxPP;
}
}
internal void SetUnknown()
{
for (int i = 0; i < _list.Length; i++)
{
PBEBattleMovesetSlot slot = _list[i];
slot.Move = PBEMove.MAX;
slot.PP = 0;
slot.MaxPP = 0;
}
}
public bool Contains(PBEMove move)
{
return this[move] is not null;
}
public bool Contains(PBEMoveEffect effect)
{
for (int i = 0; i < _list.Length; i++)
{
PBEMove move = _list[i].Move;
if (move != PBEMove.None && move != PBEMove.MAX && PBEDataProvider.Instance.GetMoveData(move).Effect == effect)
{
return true;
}
}
return false;
}
// TODO: This is copied from PBEMovesetInterfaceExtensions
public int CountMoves()
{
int num = 0;
for (int i = 0; i < _list.Length; i++)
{
if (_list[i].Move != PBEMove.None)
{
num++;
}
}
return num;
}
public IEnumerator GetEnumerator()
{
for (int i = 0; i < _list.Length; i++)
{
yield return _list[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattlePokemon.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using Kermalis.PokemonBattleEngine.Data.Utils;
using Kermalis.PokemonBattleEngine.Packets;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Kermalis.PokemonBattleEngine.Battle;
/// Represents a specific Pokémon during a battle.
public sealed class PBEBattlePokemon : IPBEPokemonKnownTypes, IPBEPokemonTypes, IPBESpeciesForm
{
public PBEBattle Battle { get; }
public PBETeam Team { get; }
public PBETrainer Trainer { get; }
public byte Id { get; }
public bool IsWild => Team.IsWild;
public bool IsLocallyHosted => Battle.IsLocallyHosted;
public bool PBEIgnore { get; }
public bool CanBattle => HP > 0 && !PBEIgnore;
#region Basic Properties
/// The Pokémon's current HP.
public ushort HP { get; set; }
/// The Pokémon's maximum HP.
public ushort MaxHP { get; set; }
/// The Pokémon's current HP as a percentage.
public float HPPercentage { get; set; }
/// The Pokémon's attack stat.
public ushort Attack { get; set; }
public sbyte AttackChange { get; set; }
/// The Pokémon's defense stat.
public ushort Defense { get; set; }
public sbyte DefenseChange { get; set; }
/// The Pokémon's special attack stat.
public ushort SpAttack { get; set; }
public sbyte SpAttackChange { get; set; }
/// The Pokémon's special defense stat.
public ushort SpDefense { get; set; }
public sbyte SpDefenseChange { get; set; }
/// The Pokémon's speed stat.
public ushort Speed { get; set; }
public sbyte SpeedChange { get; set; }
public sbyte AccuracyChange { get; set; }
public sbyte EvasionChange { get; set; }
public PBEReadOnlyStatCollection? OriginalEffortValues { get; }
public PBEStatCollection? EffortValues { get; }
public PBEReadOnlyStatCollection? IndividualValues { get; }
public byte Friendship { get; set; }
public byte OriginalLevel { get; set; }
/// The Pokémon's level.
public byte Level { get; set; }
public uint OriginalEXP { get; set; }
public uint EXP { get; set; }
/// The Pokémon's nature.
public PBENature Nature { get; set; }
/// The moveset the Pokémon had upon entering battle.
public PBEReadOnlyPartyMoveset? OriginalMoveset { get; }
/// The Pokémon's field position.
public PBEFieldPosition FieldPosition { get; set; }
/// The Pokémon's current ability.
public PBEAbility Ability { get; set; }
/// The ability the Pokémon is known to have.
public PBEAbility KnownAbility { get; set; }
/// The ability the Pokémon had upon entering battle.
public PBEAbility OriginalAbility { get; set; }
/// The ability the Pokémon will regain upon switching out, fainting, or the battle ending.
public PBEAbility RevertAbility { get; set; }
/// The Pokémon's gender.
public PBEGender Gender { get; set; }
/// The gender the Pokémon looks like (affected by transforming and disguising).
public PBEGender KnownGender { get; set; }
/// The Pokémon's current item.
public PBEItem Item { get; set; }
/// The item the Pokémon is known to have.
public PBEItem KnownItem { get; set; }
/// The item the Pokémon had upon entering battle.
public PBEItem OriginalItem { get; set; }
/// The Pokémon's current ball (affected by catching).
public PBEItem CaughtBall { get; set; }
/// The ball the Pokémon is known to be in (affected by disguising).
public PBEItem KnownCaughtBall { get; set; }
/// The ball the Pokémon was in upon entering battle.
public PBEItem OriginalCaughtBall { get; set; }
/// The moves the Pokémon currently has.
public PBEBattleMoveset Moves { get; }
/// The moves the Pokémon is known to have.
public PBEBattleMoveset KnownMoves { get; }
/// The nickname the Pokémon normally has.
public string Nickname { get; set; }
/// The nickname the Pokémon is known to have.
public string KnownNickname { get; set; }
/// The shininess the Pokémon normally has.
public bool Shiny { get; set; }
/// The shininess everyone sees the Pokémon has.
public bool KnownShiny { get; set; }
public bool Pokerus { get; set; }
/// The current species of the Pokémon (affected by transforming and form changing).
public PBESpecies Species { get; set; }
/// The species everyone sees the Pokémon as (affected by transforming, disguising, and form changing).
public PBESpecies KnownSpecies { get; set; }
/// The species the Pokémon was upon entering battle.
public PBESpecies OriginalSpecies { get; set; }
public PBEForm Form { get; set; }
public PBEForm KnownForm { get; set; }
public PBEForm OriginalForm { get; set; }
public PBEForm RevertForm { get; set; }
public PBEStatus1 Status1 { get; set; }
public PBEStatus1 OriginalStatus1 { get; set; }
public PBEStatus2 Status2 { get; set; }
public PBEStatus2 KnownStatus2 { get; set; }
/// The Pokémon's first type.
public PBEType Type1 { get; set; }
/// The first type everyone believes the Pokémon has.
public PBEType KnownType1 { get; set; }
/// The Pokémon's second type.
public PBEType Type2 { get; set; }
/// The second type everyone believes the Pokémon has.
public PBEType KnownType2 { get; set; }
public float Weight { get; set; }
public float KnownWeight { get; set; }
#endregion
#region Statuses
/// The counter used for and .
public byte Status1Counter { get; set; }
/// The amount of turns the Pokémon will sleep for before waking.
public byte SleepTurns { get; set; }
/// The counter used for .
public byte ConfusionCounter { get; set; }
/// The amount of turns the Pokémon will be confused for before snapping out of it.
public byte ConfusionTurns { get; set; }
/// The Pokémon that is bound to.
public PBEBattlePokemon? InfatuatedWithPokemon { get; set; }
/// The amount of turns until ends.
public byte MagnetRiseTurns { get; set; }
/// The Pokémon that is bound to.
public PBEBattlePokemon? LockOnPokemon { get; set; }
public byte LockOnTurns { get; set; }
/// The amount of times the Pokémon has successfully used , , and/or consecutively.
public int Protection_Counter { get; set; }
public bool Protection_Used { get; set; }
public PBERoostTypes RoostTypes { get; set; }
/// The position to return HP to on .
public PBEFieldPosition SeededPosition { get; set; }
/// The team responsible for .
public PBETeam? SeededTeam { get; set; }
/// The amount of HP the Pokémon's has left.
public ushort SubstituteHP { get; set; }
public PBEBattleMoveset TransformBackupMoves { get; }
#endregion
#region Actions
/// if the Pokémon has successfully executed a move this turn.
public bool HasUsedMoveThisTurn { get; set; }
/// The action the Pokémon will try to perform when the turn is run. if the Pokémon just switched in, attempted to flee, etc.
public PBETurnAction? TurnAction { get; set; }
/// The move the Pokémon is forced to use by multi-turn moves.
public PBEMove TempLockedMove { get; set; } // TODO: Tests - Does a pkmn lose its temp locked move if it runs out of pp on the move, all moves, or ever? (Some move can lower its pp while it's being used?)
/// The targets the Pokémon is forced to target by multi-turn moves.
public PBETurnTarget TempLockedTargets { get; set; }
/// The move the Pokémon is forced to use by its choice item.
public PBEMove ChoiceLockedMove { get; set; } // TODO: Tests - Does a pkmn lose its choice locked move if it runs out of pp on the move, all moves, or ever?
#endregion
#region Special Flags
/// True if the Pokémon has successfully used which makes it succeptible to double damage from .
public bool Minimize_Used { get; set; }
/// The amount of turns left until a Pokémon with loses its hinderance.
public byte SlowStart_HinderTurnsLeft { get; set; }
/// True if the Pokémon was present at the start of the turn, which would allow to activate.
public bool SpeedBoost_AbleToSpeedBoostThisTurn { get; set; }
#endregion
public List EXPPokemon { get; } = new();
#region Constructors
private PBEBattlePokemon(PBETrainer trainer, byte id,
PBESpecies species, PBEForm form, string nickname, byte level, uint exp, byte friendship, bool shiny, bool pokerus,
PBEAbility ability, PBENature nature, PBEGender gender, PBEItem item, PBEItem caughtBall,
PBEReadOnlyStatCollection evs, PBEReadOnlyStatCollection ivs, PBEReadOnlyPartyMoveset moves)
{
Battle = trainer.Battle;
Team = trainer.Team;
Trainer = trainer;
Id = id;
Species = OriginalSpecies = KnownSpecies = species;
Form = OriginalForm = KnownForm = RevertForm = form;
IPBEPokemonData pData = PBEDataProvider.Instance.GetPokemonData(species, form);
KnownType1 = Type1 = pData.Type1;
KnownType2 = Type2 = pData.Type2;
KnownWeight = Weight = pData.Weight;
Nickname = KnownNickname = nickname;
Level = OriginalLevel = level;
EXP = OriginalEXP = exp;
Friendship = friendship;
Shiny = KnownShiny = shiny;
Pokerus = pokerus;
Ability = OriginalAbility = RevertAbility = ability;
KnownAbility = PBEAbility.MAX;
Nature = nature;
Gender = KnownGender = gender;
Item = OriginalItem = item;
KnownItem = (PBEItem)ushort.MaxValue;
KnownCaughtBall = CaughtBall = OriginalCaughtBall = caughtBall;
OriginalEffortValues = evs;
EffortValues = new PBEStatCollection(evs);
IndividualValues = new PBEReadOnlyStatCollection(ivs);
SetStats(true, true);
OriginalMoveset = moves;
PBESettings settings = Battle.Settings;
Moves = new PBEBattleMoveset(settings, moves);
KnownMoves = new PBEBattleMoveset(settings);
TransformBackupMoves = new PBEBattleMoveset(settings);
}
private PBEBattlePokemon(PBETrainer trainer, byte id, IPBEPokemon pkmn, PBEReadOnlyPartyMoveset moves)
: this(trainer, id,
pkmn.Species, pkmn.Form, pkmn.Nickname, pkmn.Level, pkmn.EXP, pkmn.Friendship, pkmn.Shiny, pkmn.Pokerus,
pkmn.Ability, pkmn.Nature, pkmn.Gender, pkmn.Item, pkmn.CaughtBall,
new PBEReadOnlyStatCollection(pkmn.EffortValues), new PBEReadOnlyStatCollection(pkmn.IndividualValues), moves)
{
PBEIgnore = pkmn.PBEIgnore;
}
internal PBEBattlePokemon(PBETrainer trainer, byte id, IPBEPokemon pkmn)
: this(trainer, id, pkmn, new PBEReadOnlyPartyMoveset(trainer.Battle.Settings, pkmn.Moveset))
{
trainer.Party.Add(this);
}
internal PBEBattlePokemon(PBETrainer trainer, byte id, IPBEPartyPokemon pkmn)
: this(trainer, id, pkmn, new PBEReadOnlyPartyMoveset(pkmn.Moveset))
{
ushort hp = pkmn.HP;
if (hp > MaxHP)
{
throw new InvalidDataException(nameof(pkmn.HP));
}
HP = hp;
UpdateHPPercentage();
PBEStatus1 status1 = pkmn.Status1;
if (status1 >= PBEStatus1.MAX)
{
throw new InvalidDataException(nameof(pkmn.Status1));
}
if (status1 == PBEStatus1.BadlyPoisoned)
{
Status1Counter = 1;
}
Status1 = OriginalStatus1 = status1;
byte sleepTurns = pkmn.SleepTurns;
if (status1 != PBEStatus1.Asleep && sleepTurns != 0)
{
throw new InvalidDataException(nameof(pkmn.SleepTurns));
}
SleepTurns = sleepTurns;
trainer.Party.Add(this);
}
internal PBEBattlePokemon(PBETrainer trainer, PBEBattlePacket.PBETeamInfo.PBETrainerInfo.PBEBattlePokemonInfo info)
: this(trainer, info.Id,
info.Species, info.Form, info.Nickname, info.Level, info.EXP, info.Friendship, info.Shiny, info.Pokerus,
info.Ability, info.Nature, info.Gender, info.Item, info.CaughtBall,
info.EffortValues, info.IndividualValues, info.Moveset)
{
Status1 = info.Status1;
if (Status1 == PBEStatus1.BadlyPoisoned)
{
Status1Counter = 1;
}
}
private PBEBattlePokemon(PBETrainer trainer, IPBEPkmnAppearedInfo_Hidden info)
{
Battle = trainer.Battle;
Team = trainer.Team;
Trainer = trainer;
FieldPosition = info.FieldPosition;
HPPercentage = info.HPPercentage;
Status1 = info.Status1;
if (Status1 == PBEStatus1.BadlyPoisoned)
{
Status1Counter = 1;
}
Level = info.Level;
KnownAbility = Ability = PBEAbility.MAX;
KnownGender = Gender = info.Gender;
KnownItem = Item = (PBEItem)ushort.MaxValue;
Moves = new PBEBattleMoveset(Battle.Settings); // For Transform
KnownMoves = new PBEBattleMoveset(Battle.Settings);
TransformBackupMoves = new PBEBattleMoveset(Battle.Settings); // For Transform
KnownNickname = Nickname = info.Nickname;
KnownShiny = Shiny = info.Shiny;
KnownSpecies = Species = info.Species;
KnownForm = Form = info.Form;
IPBEPokemonData pData = PBEDataProvider.Instance.GetPokemonData(KnownSpecies, KnownForm);
KnownType1 = Type1 = pData.Type1;
KnownType2 = Type2 = pData.Type2;
KnownWeight = Weight = pData.Weight;
trainer.Party.Add(this);
Battle.ActiveBattlers.Add(this);
}
private PBEBattlePokemon(PBETrainer trainer, IPBEPkmnSwitchInInfo_Hidden info)
: this(trainer, (IPBEPkmnAppearedInfo_Hidden)info)
{
KnownCaughtBall = CaughtBall = info.CaughtBall;
}
public PBEBattlePokemon(PBETrainer trainer, PBEPkmnSwitchInPacket_Hidden.PBEPkmnSwitchInInfo info)
: this(trainer, (IPBEPkmnSwitchInInfo_Hidden)info) { }
public PBEBattlePokemon(PBEBattle battle, PBEWildPkmnAppearedPacket_Hidden.PBEWildPkmnInfo info)
: this(battle.Teams[1].Trainers[0], info) { }
#endregion
public void AddEXPPokemon(PBEBattlePokemon pkmn)
{
if (!EXPPokemon.Contains(pkmn))
{
EXPPokemon.Add(pkmn);
}
}
/// For use with level ups. Does not send any packets.
public void LearnMove(PBEMove move, int index)
{
bool transformed = Status2.HasFlag(PBEStatus2.Transformed);
PBEBattleMoveset moves = transformed ? TransformBackupMoves : Moves;
PBEBattleMoveset.PBEBattleMovesetSlot slot = moves[index];
PBEMove oldMove = slot.Move;
slot.Move = move;
int pp = PBEDataUtils.CalcMaxPP(move, 0, Battle.Settings);
slot.PP = pp;
slot.MaxPP = pp;
// Update known moves below
if (!transformed && FieldPosition != PBEFieldPosition.None)
{
moves = KnownMoves;
PBEBattleMoveset.PBEBattleMovesetSlot? slot2 = moves[oldMove];
if (slot2 is not null) // Check if move is known first
{
slot2.Move = PBEMove.MAX; // Make the move unknown if the old move was known
slot2.PP = 0;
moves.Organize();
}
}
}
public void ApplyPowerTrickChange()
{
ushort a = Attack;
Attack = Defense;
Defense = a;
}
/// Applies effects that occur on switching out or escaping such as .
public void ApplyNaturalCure()
{
if (!PBEIgnore && Ability == PBEAbility.NaturalCure)
{
Status1 = PBEStatus1.None;
Status1Counter = 0;
SleepTurns = 0;
}
}
private void ResetSpecies()
{
Species = KnownSpecies = OriginalSpecies;
Form = KnownForm = RevertForm;
Ability = KnownAbility = RevertAbility;
IPBEPokemonData pData = PBEDataProvider.Instance.GetPokemonData(this);
KnownAbility = PBEAbility.MAX;
KnownGender = Gender;
KnownItem = (PBEItem)ushort.MaxValue;
KnownMoves.SetUnknown();
KnownNickname = Nickname;
KnownShiny = Shiny;
KnownType1 = Type1 = pData.Type1;
KnownType2 = Type2 = pData.Type2;
}
private void ResetVolatileStuff()
{
ConfusionCounter = 0;
ConfusionTurns = 0;
LockOnPokemon = null;
LockOnTurns = 0;
MagnetRiseTurns = 0;
Protection_Counter = 0;
Protection_Used = false;
SeededPosition = PBEFieldPosition.None;
SeededTeam = null;
SubstituteHP = 0;
if (Status2.HasFlag(PBEStatus2.Transformed))
{
Moves.Reset(TransformBackupMoves);
TransformBackupMoves.SetUnknown();
}
Status2 = PBEStatus2.None;
KnownStatus2 = PBEStatus2.None;
HasUsedMoveThisTurn = false;
TurnAction = null;
TempLockedMove = PBEMove.None;
TempLockedTargets = PBETurnTarget.None;
ChoiceLockedMove = PBEMove.None;
Minimize_Used = false;
SlowStart_HinderTurnsLeft = 0;
SpeedBoost_AbleToSpeedBoostThisTurn = false;
}
/// Sets and clears all information required for switching out.
public void ClearForSwitch()
{
EXPPokemon.Clear();
FieldPosition = PBEFieldPosition.None;
ApplyNaturalCure();
if (Ability == PBEAbility.Regenerator)
{
HP = Math.Clamp((ushort)(HP + (MaxHP / 3)), ushort.MinValue, MaxHP);
UpdateHPPercentage();
}
ResetSpecies();
ClearStatChanges();
if (Status1 == PBEStatus1.Asleep)
{
Status1Counter = 0;
}
else if (Status1 == PBEStatus1.BadlyPoisoned)
{
Status1Counter = 1;
}
ResetVolatileStuff();
SetStats(false, false);
}
/// Sets and clears all information required for fainting.
public void ClearForFaint()
{
FieldPosition = PBEFieldPosition.None;
ResetSpecies();
ClearStatChanges();
Status1 = PBEStatus1.None;
ResetVolatileStuff();
SetStats(false, false);
}
// GameFreak are very inconsistent with how they handle Power Trick when recalculating stats
// Form change, Transform, Level up
public void SetStats(bool calculateHP, bool setMaxHPIfCalcHP)
{
IPBEPokemonData pData = PBEDataProvider.Instance.GetPokemonData(this);
PBENature nature = Nature;
PBEStatCollection evs = EffortValues!;
PBEReadOnlyStatCollection ivs = IndividualValues!;
byte level = Level;
PBESettings settings = Battle.Settings;
if (calculateHP)
{
ushort oldMaxHP = MaxHP;
ushort hp = PBEDataUtils.CalculateStat(pData, PBEStat.HP, nature, evs.HP, ivs.HP, level, settings);
MaxHP = hp;
if (setMaxHPIfCalcHP)
{
HP = hp;
}
else if (HP != 0 && oldMaxHP != hp) // Don't change for a fainted mon
{
HP = (ushort)Math.Max(1, HP + (hp - oldMaxHP));
}
UpdateHPPercentage();
}
Attack = PBEDataUtils.CalculateStat(pData, PBEStat.Attack, nature, evs.Attack, ivs.Attack, level, settings);
Defense = PBEDataUtils.CalculateStat(pData, PBEStat.Defense, nature, evs.Defense, ivs.Defense, level, settings);
SpAttack = PBEDataUtils.CalculateStat(pData, PBEStat.SpAttack, nature, evs.SpAttack, ivs.SpAttack, level, settings);
SpDefense = PBEDataUtils.CalculateStat(pData, PBEStat.SpDefense, nature, evs.SpDefense, ivs.SpDefense, level, settings);
Speed = PBEDataUtils.CalculateStat(pData, PBEStat.Speed, nature, evs.Speed, ivs.Speed, level, settings);
}
/// Copies the , does not set .
/// The Pokémon to transform into.
public void Transform(PBEBattlePokemon target)
{
if (Trainer != target.Trainer)
{
KnownAbility = target.KnownAbility = Ability = target.Ability;
KnownType1 = target.KnownType1 = Type1 = target.Type1;
KnownType2 = target.KnownType2 = Type2 = target.Type2;
KnownWeight = target.KnownWeight = Weight = target.Weight;
}
else
{
Ability = target.Ability;
KnownAbility = target.KnownAbility;
Type1 = target.Type1;
KnownType1 = target.KnownType1;
Type2 = target.Type2;
KnownType2 = target.KnownType2;
Weight = target.Weight;
KnownWeight = target.KnownWeight;
}
KnownGender = target.KnownGender = target.Gender;
KnownShiny = target.KnownShiny = target.Shiny;
KnownSpecies = target.KnownSpecies = Species = target.Species;
KnownForm = target.KnownForm = Form = target.Form;
Attack = target.Attack;
Defense = target.Defense;
SpAttack = target.SpAttack;
SpDefense = target.SpDefense;
Speed = target.Speed;
AttackChange = target.AttackChange;
DefenseChange = target.DefenseChange;
SpAttackChange = target.SpAttackChange;
SpDefenseChange = target.SpDefenseChange;
SpeedChange = target.SpeedChange;
AccuracyChange = target.AccuracyChange;
EvasionChange = target.EvasionChange;
TransformBackupMoves.Reset(Moves);
PBEBattleMoveset.DoTransform(this, target);
}
public void UpdateKnownPP(PBEMove move, int amountReduced)
{
if (move == PBEMove.None || move >= PBEMove.MAX || !PBEDataUtils.IsMoveUsable(move))
{
throw new ArgumentOutOfRangeException(nameof(move));
}
PBEBattleMoveset.PBEBattleMovesetSlot knownSlot = KnownMoves[move]!;
knownSlot.PP += amountReduced;
if (knownSlot.MaxPP == 0)
{
if (Status2.HasFlag(PBEStatus2.Transformed))
{
knownSlot.MaxPP = PBEBattleMoveset.GetTransformPP(Battle.Settings, move);
}
else if (Battle.Settings.MaxPPUps == 0 || knownSlot.PP > PBEBattleMoveset.GetNonTransformPP(Battle.Settings, move, (byte)(Battle.Settings.MaxPPUps - 1)))
{
knownSlot.MaxPP = PBEBattleMoveset.GetNonTransformPP(Battle.Settings, move, Battle.Settings.MaxPPUps);
}
}
}
/// Divides by and places the result in .
public void UpdateHPPercentage()
{
HPPercentage = (float)HP / MaxHP;
}
public void StartRoost()
{
PBERoostTypes t = PBERoostTypes.None;
if (Type1 == PBEType.Flying)
{
if (Type2 == PBEType.None)
{
Type1 = PBEType.Normal; // Pure flying-type becomes Normal-type
t |= PBERoostTypes.Type1;
}
else
{
Type1 = Type2;
Type2 = PBEType.None;
t |= PBERoostTypes.Type2;
}
}
if (Type2 == PBEType.Flying)
{
Type2 = PBEType.None;
t |= PBERoostTypes.Type2;
}
if (KnownType1 == PBEType.Flying)
{
if (KnownType2 == PBEType.None)
{
KnownType1 = PBEType.Normal;
t |= PBERoostTypes.KnownType1;
}
else
{
KnownType1 = KnownType2;
KnownType2 = PBEType.None;
t |= PBERoostTypes.KnownType2;
}
}
if (KnownType2 == PBEType.Flying)
{
KnownType2 = PBEType.None;
t |= PBERoostTypes.KnownType2;
}
RoostTypes = t;
}
public void EndRoost()
{
PBERoostTypes t = RoostTypes;
if (t.HasFlag(PBERoostTypes.Type1))
{
Type1 = PBEType.Flying;
}
if (t.HasFlag(PBERoostTypes.Type2))
{
Type2 = PBEType.Flying;
}
if (t.HasFlag(PBERoostTypes.KnownType1))
{
KnownType1 = PBEType.Flying;
}
if (t.HasFlag(PBERoostTypes.KnownType2))
{
KnownType2 = PBEType.Flying;
}
RoostTypes = PBERoostTypes.None;
}
public bool HasCancellingAbility(bool useKnownInfo = false)
{
PBEAbility kAbility = useKnownInfo ? KnownAbility : Ability;
return kAbility == PBEAbility.MoldBreaker || kAbility == PBEAbility.Teravolt || kAbility == PBEAbility.Turboblaze;
}
public PBEStat[] GetChangedStats()
{
var list = new List(7);
if (AttackChange != 0)
{
list.Add(PBEStat.Attack);
}
if (DefenseChange != 0)
{
list.Add(PBEStat.Defense);
}
if (SpAttackChange != 0)
{
list.Add(PBEStat.SpAttack);
}
if (SpDefenseChange != 0)
{
list.Add(PBEStat.SpDefense);
}
if (SpeedChange != 0)
{
list.Add(PBEStat.Speed);
}
if (AccuracyChange != 0)
{
list.Add(PBEStat.Accuracy);
}
if (EvasionChange != 0)
{
list.Add(PBEStat.Evasion);
}
return list.ToArray();
}
public PBEStat[] GetStatsLessThan(int i)
{
var list = new List(7);
if (AttackChange < i)
{
list.Add(PBEStat.Attack);
}
if (DefenseChange < i)
{
list.Add(PBEStat.Defense);
}
if (SpAttackChange < i)
{
list.Add(PBEStat.SpAttack);
}
if (SpDefenseChange < i)
{
list.Add(PBEStat.SpDefense);
}
if (SpeedChange < i)
{
list.Add(PBEStat.Speed);
}
if (AccuracyChange < i)
{
list.Add(PBEStat.Accuracy);
}
if (EvasionChange < i)
{
list.Add(PBEStat.Evasion);
}
return list.ToArray();
}
public PBEStat[] GetStatsGreaterThan(int i)
{
var list = new List(7);
if (AttackChange > i)
{
list.Add(PBEStat.Attack);
}
if (DefenseChange > i)
{
list.Add(PBEStat.Defense);
}
if (SpAttackChange > i)
{
list.Add(PBEStat.SpAttack);
}
if (SpDefenseChange > i)
{
list.Add(PBEStat.SpDefense);
}
if (SpeedChange > i)
{
list.Add(PBEStat.Speed);
}
if (AccuracyChange > i)
{
list.Add(PBEStat.Accuracy);
}
if (EvasionChange > i)
{
list.Add(PBEStat.Evasion);
}
return list.ToArray();
}
public sbyte GetStatChange(PBEStat stat)
{
switch (stat)
{
case PBEStat.Attack: return AttackChange;
case PBEStat.Defense: return DefenseChange;
case PBEStat.SpAttack: return SpAttackChange;
case PBEStat.SpDefense: return SpDefenseChange;
case PBEStat.Speed: return SpeedChange;
case PBEStat.Accuracy: return AccuracyChange;
case PBEStat.Evasion: return EvasionChange;
default: throw new ArgumentOutOfRangeException(nameof(stat));
}
}
public sbyte SetStatChange(PBEStat stat, int value)
{
sbyte maxStatChange = Battle.Settings.MaxStatChange;
sbyte val = (sbyte)Math.Clamp(value, -maxStatChange, maxStatChange);
switch (stat)
{
case PBEStat.Accuracy: AccuracyChange = val; break;
case PBEStat.Attack: AttackChange = val; break;
case PBEStat.Defense: DefenseChange = val; break;
case PBEStat.Evasion: EvasionChange = val; break;
case PBEStat.SpAttack: SpAttackChange = val; break;
case PBEStat.SpDefense: SpDefenseChange = val; break;
case PBEStat.Speed: SpeedChange = val; break;
default: throw new ArgumentOutOfRangeException(nameof(stat));
}
return val;
}
public PBEResult IsStatChangePossible(PBEStat stat, PBEBattlePokemon causer, int change, out sbyte oldValue, out sbyte newValue, bool useKnownInfo = false, bool ignoreSubstitute = false)
{
oldValue = GetStatChange(stat);
if (causer != this && !ignoreSubstitute && (useKnownInfo ? KnownStatus2 : Status2).HasFlag(PBEStatus2.Substitute))
{
newValue = oldValue;
return PBEResult.Ineffective_Substitute;
}
// These abilities do not activate when the Pokémon changes its own stat
if (causer != this && !causer.HasCancellingAbility())
{
switch (useKnownInfo ? KnownAbility : Ability)
{
case PBEAbility.BigPecks:
{
if (change < 0 && stat == PBEStat.Defense)
{
newValue = oldValue;
return PBEResult.Ineffective_Ability;
}
break;
}
case PBEAbility.ClearBody:
case PBEAbility.WhiteSmoke:
{
if (change < 0)
{
newValue = oldValue;
return PBEResult.Ineffective_Ability;
}
break;
}
case PBEAbility.HyperCutter:
{
if (change < 0 && stat == PBEStat.Attack)
{
newValue = oldValue;
return PBEResult.Ineffective_Ability;
}
break;
}
case PBEAbility.KeenEye:
{
if (change < 0 && stat == PBEStat.Accuracy)
{
newValue = oldValue;
return PBEResult.Ineffective_Ability;
}
break;
}
}
}
// Verified: Contrary/Simple are silent
// These abilities activate when the Pokémon changes its own stat
if (causer == this || !causer.HasCancellingAbility())
{
switch (useKnownInfo ? KnownAbility : Ability)
{
case PBEAbility.Contrary: change *= -1; break;
case PBEAbility.Simple: change *= 2; break;
}
}
sbyte maxStatChange = Battle.Settings.MaxStatChange;
newValue = (sbyte)Math.Clamp(oldValue + change, -maxStatChange, maxStatChange);
if (oldValue == newValue)
{
return PBEResult.Ineffective_Stat;
}
return PBEResult.Success;
}
public void ClearStatChanges()
{
AttackChange = 0;
DefenseChange = 0;
SpAttackChange = 0;
SpDefenseChange = 0;
SpeedChange = 0;
AccuracyChange = 0;
EvasionChange = 0;
}
/// For use with and .
public int GetPositiveStatTotal()
{
return GetStatsGreaterThan(0).Sum(s => GetStatChange(s));
}
public PBEType GetMoveType(IPBEMoveData mData, bool useKnownInfo = false)
{
if (!mData.IsMoveUsable())
{
throw new ArgumentOutOfRangeException(nameof(mData));
}
switch (mData.Effect)
{
case PBEMoveEffect.HiddenPower:
{
if (!useKnownInfo)
{
return IndividualValues!.GetHiddenPowerType();
}
break;
}
case PBEMoveEffect.Judgment:
{
switch (useKnownInfo ? KnownItem : Item)
{
case PBEItem.DracoPlate: return PBEType.Dragon;
case PBEItem.DreadPlate: return PBEType.Dark;
case PBEItem.EarthPlate: return PBEType.Ground;
case PBEItem.FistPlate: return PBEType.Fighting;
case PBEItem.FlamePlate: return PBEType.Fire;
case PBEItem.IciclePlate: return PBEType.Ice;
case PBEItem.InsectPlate: return PBEType.Bug;
case PBEItem.IronPlate: return PBEType.Steel;
case PBEItem.MeadowPlate: return PBEType.Grass;
case PBEItem.MindPlate: return PBEType.Psychic;
case PBEItem.SkyPlate: return PBEType.Flying;
case PBEItem.SplashPlate: return PBEType.Water;
case PBEItem.SpookyPlate: return PBEType.Ghost;
case PBEItem.StonePlate: return PBEType.Rock;
case PBEItem.ToxicPlate: return PBEType.Poison;
case PBEItem.ZapPlate: return PBEType.Electric;
}
break;
}
case PBEMoveEffect.Struggle:
{
return PBEType.None;
}
case PBEMoveEffect.TechnoBlast:
{
switch (useKnownInfo ? KnownItem : Item)
{
case PBEItem.BurnDrive: return PBEType.Fire;
case PBEItem.ChillDrive: return PBEType.Ice;
case PBEItem.DouseDrive: return PBEType.Water;
case PBEItem.ShockDrive: return PBEType.Electric;
}
break;
}
case PBEMoveEffect.WeatherBall:
{
if (Battle.ShouldDoWeatherEffects())
{
switch (Battle.Weather)
{
case PBEWeather.Hailstorm: return PBEType.Ice;
case PBEWeather.HarshSunlight: return PBEType.Fire;
case PBEWeather.Rain: return PBEType.Water;
case PBEWeather.Sandstorm: return PBEType.Rock;
}
}
break;
}
}
if ((useKnownInfo ? KnownAbility : Ability) == PBEAbility.Normalize)
{
return PBEType.Normal;
}
return mData.Type;
}
public PBEType GetMoveType(PBEMove move, bool useKnownInfo = false)
{
if (move == PBEMove.None || move >= PBEMove.MAX)
{
throw new ArgumentOutOfRangeException(nameof(move));
}
return GetMoveType(PBEDataProvider.Instance.GetMoveData(move), useKnownInfo: useKnownInfo);
}
public PBEMoveTarget GetMoveTargets(IPBEMoveData mData)
{
if (!mData.IsMoveUsable())
{
throw new ArgumentOutOfRangeException(nameof(mData));
}
if (mData.Effect == PBEMoveEffect.Curse)
{
if (this.HasType(PBEType.Ghost))
{
return PBEMoveTarget.SingleSurrounding;
}
return PBEMoveTarget.Self;
}
return mData.Targets;
}
/// Gets the possible targets that a move can target when used by this Pokémon.
/// The move to check.
/// Thrown when is invalid.
public PBEMoveTarget GetMoveTargets(PBEMove move)
{
if (move == PBEMove.None || move >= PBEMove.MAX)
{
throw new ArgumentOutOfRangeException(nameof(move));
}
return GetMoveTargets(PBEDataProvider.Instance.GetMoveData(move));
}
/// Returns True if the Pokémon is only able to use .
public bool IsForcedToStruggle()
{
if (TempLockedMove != PBEMove.None) // Temp locked moves deduct PP on the first turn and don't on the second, so having a temp locked move means it is supposed to be used again for the second turn
{
return false;
}
else if ((ChoiceLockedMove != PBEMove.None && Moves[ChoiceLockedMove]!.PP == 0) // If the choice locked move has 0 pp, it is forced to struggle
|| Moves.All(s => s.PP == 0) // If all moves have 0 pp, then the user is forced to struggle
)
{
return true;
}
else
{
return false;
}
}
/// Returns True if the Pokémon can switch out. Does not check if the Pokémon is on the field or if there are available Pokémon to switch into.
public bool CanSwitchOut()
{
return TempLockedMove == PBEMove.None;
}
public bool CanHitThroughSafeguard()
{
return Ability == PBEAbility.Infiltrator;
}
public PBEResult IsAttractionPossible(PBEBattlePokemon causer, bool useKnownInfo = false, bool ignoreCurrentStatus = false)
{
if (causer == this)
{
return PBEResult.InvalidConditions;
}
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (!ignoreCurrentStatus && kStatus2.HasFlag(PBEStatus2.Infatuated))
{
return PBEResult.Ineffective_Status;
}
PBEGender kGender = useKnownInfo ? KnownGender : Gender;
if (!kGender.IsOppositeGender(causer.Gender))
{
return PBEResult.Ineffective_Gender;
}
PBEAbility kAbility = useKnownInfo ? KnownAbility : Ability;
if (!causer.HasCancellingAbility() && kAbility == PBEAbility.Oblivious)
{
return PBEResult.Ineffective_Ability;
}
return PBEResult.Success;
}
public PBEResult IsBurnPossible(PBEBattlePokemon? other, bool useKnownInfo = false, bool ignoreSubstitute = false, bool ignoreCurrentStatus = false, bool ignoreSafeguard = false)
{
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (!ignoreSubstitute && kStatus2.HasFlag(PBEStatus2.Substitute))
{
return PBEResult.Ineffective_Substitute;
}
if (!ignoreCurrentStatus && Status1 != PBEStatus1.None)
{
return PBEResult.Ineffective_Status;
}
if (!ignoreSafeguard && other?.CanHitThroughSafeguard() == true && Team.TeamStatus.HasFlag(PBETeamStatus.Safeguard))
{
return PBEResult.Ineffective_Safeguard;
}
if (this.HasType(PBEType.Fire, useKnownInfo))
{
return PBEResult.Ineffective_Type;
}
PBEAbility kAbility = useKnownInfo ? KnownAbility : Ability;
if (other?.HasCancellingAbility() != true && (kAbility == PBEAbility.WaterVeil || (kAbility == PBEAbility.LeafGuard && Battle.WillLeafGuardActivate())))
{
return PBEResult.Ineffective_Ability;
}
return PBEResult.Success;
}
public PBEResult IsConfusionPossible(PBEBattlePokemon? other, bool useKnownInfo = false, bool ignoreSubstitute = false, bool ignoreCurrentStatus = false, bool ignoreSafeguard = false)
{
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (!ignoreSubstitute && kStatus2.HasFlag(PBEStatus2.Substitute))
{
return PBEResult.Ineffective_Substitute;
}
if (!ignoreCurrentStatus && kStatus2.HasFlag(PBEStatus2.Confused))
{
return PBEResult.Ineffective_Status;
}
if (!ignoreSafeguard && other?.CanHitThroughSafeguard() == true && Team.TeamStatus.HasFlag(PBETeamStatus.Safeguard))
{
return PBEResult.Ineffective_Safeguard;
}
PBEAbility kAbility = useKnownInfo ? KnownAbility : Ability;
if (other?.HasCancellingAbility() != true && kAbility == PBEAbility.OwnTempo)
{
return PBEResult.Ineffective_Ability;
}
return PBEResult.Success;
}
public PBEResult IsFreezePossible(PBEBattlePokemon? other, bool useKnownInfo = false, bool ignoreSubstitute = false, bool ignoreCurrentStatus = false, bool ignoreSafeguard = false)
{
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (!ignoreSubstitute && kStatus2.HasFlag(PBEStatus2.Substitute))
{
return PBEResult.Ineffective_Substitute;
}
if (!ignoreCurrentStatus && Status1 != PBEStatus1.None)
{
return PBEResult.Ineffective_Status;
}
if (!ignoreSafeguard && other?.CanHitThroughSafeguard() == true && Team.TeamStatus.HasFlag(PBETeamStatus.Safeguard))
{
return PBEResult.Ineffective_Safeguard;
}
if (this.HasType(PBEType.Ice, useKnownInfo))
{
return PBEResult.Ineffective_Type;
}
PBEAbility kAbility = useKnownInfo ? KnownAbility : Ability;
if (other?.HasCancellingAbility() != true && (kAbility == PBEAbility.MagmaArmor || (kAbility == PBEAbility.LeafGuard && Battle.WillLeafGuardActivate())))
{
return PBEResult.Ineffective_Ability;
}
return PBEResult.Success;
}
public PBEResult IsFlinchPossible(PBEBattlePokemon? other, bool useKnownInfo = false)
{
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (kStatus2.HasFlag(PBEStatus2.Substitute))
{
return PBEResult.Ineffective_Substitute;
}
if (kStatus2.HasFlag(PBEStatus2.Flinching))
{
return PBEResult.Ineffective_Status;
}
PBEAbility kAbility = useKnownInfo ? KnownAbility : Ability;
if (other?.HasCancellingAbility() != true && kAbility == PBEAbility.InnerFocus)
{
return PBEResult.Ineffective_Ability;
}
return PBEResult.Success;
}
public PBEResult IsGrounded(PBEBattlePokemon? other, bool useKnownInfo = false)
{
if (this.HasType(PBEType.Flying, useKnownInfo))
{
return PBEResult.Ineffective_Type;
}
PBEAbility kAbility = useKnownInfo ? KnownAbility : Ability;
if (other?.HasCancellingAbility() != true && kAbility == PBEAbility.Levitate)
{
return PBEResult.Ineffective_Ability;
}
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (kStatus2.HasFlag(PBEStatus2.MagnetRise))
{
return PBEResult.Ineffective_MagnetRise;
}
return PBEResult.Success;
}
public PBEResult IsLeechSeedPossible(bool useKnownInfo = false)
{
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (kStatus2.HasFlag(PBEStatus2.Substitute))
{
return PBEResult.Ineffective_Substitute;
}
if (kStatus2.HasFlag(PBEStatus2.LeechSeed))
{
return PBEResult.Ineffective_Status;
}
if (this.HasType(PBEType.Grass, useKnownInfo))
{
return PBEResult.Ineffective_Type;
}
return PBEResult.Success;
}
public PBEResult IsMagnetRisePossible(bool useKnownInfo = false)
{
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (kStatus2.HasFlag(PBEStatus2.MagnetRise))
{
return PBEResult.Ineffective_Status;
}
return PBEResult.Success;
}
public PBEResult IsParalysisPossible(PBEBattlePokemon? other, bool useKnownInfo = false, bool ignoreSubstitute = false, bool ignoreCurrentStatus = false, bool ignoreSafeguard = false)
{
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (!ignoreSubstitute && kStatus2.HasFlag(PBEStatus2.Substitute))
{
return PBEResult.Ineffective_Substitute;
}
if (!ignoreCurrentStatus && Status1 != PBEStatus1.None)
{
return PBEResult.Ineffective_Status;
}
if (!ignoreSafeguard && other?.CanHitThroughSafeguard() == true && Team.TeamStatus.HasFlag(PBETeamStatus.Safeguard))
{
return PBEResult.Ineffective_Safeguard;
}
PBEAbility kAbility = useKnownInfo ? KnownAbility : Ability;
if (other?.HasCancellingAbility() != true && (kAbility == PBEAbility.Limber || (kAbility == PBEAbility.LeafGuard && Battle.WillLeafGuardActivate())))
{
return PBEResult.Ineffective_Ability;
}
return PBEResult.Success;
}
public PBEResult IsPoisonPossible(PBEBattlePokemon? other, bool useKnownInfo = false, bool ignoreSubstitute = false, bool ignoreCurrentStatus = false, bool ignoreSafeguard = false)
{
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (!ignoreSubstitute && kStatus2.HasFlag(PBEStatus2.Substitute))
{
return PBEResult.Ineffective_Substitute;
}
if (!ignoreCurrentStatus && Status1 != PBEStatus1.None)
{
return PBEResult.Ineffective_Status;
}
if (!ignoreSafeguard && other?.CanHitThroughSafeguard() == true && Team.TeamStatus.HasFlag(PBETeamStatus.Safeguard))
{
return PBEResult.Ineffective_Safeguard;
}
if (this.HasType(PBEType.Poison, useKnownInfo) || this.HasType(PBEType.Steel, useKnownInfo))
{
return PBEResult.Ineffective_Type;
}
PBEAbility kAbility = useKnownInfo ? KnownAbility : Ability;
if (other?.HasCancellingAbility() != true && (kAbility == PBEAbility.Immunity || (kAbility == PBEAbility.LeafGuard && Battle.WillLeafGuardActivate())))
{
return PBEResult.Ineffective_Ability;
}
return PBEResult.Success;
}
public PBEResult IsSleepPossible(PBEBattlePokemon? other, bool useKnownInfo = false, bool ignoreSubstitute = false, bool ignoreCurrentStatus = false, bool ignoreSafeguard = false)
{
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (!ignoreSubstitute && kStatus2.HasFlag(PBEStatus2.Substitute))
{
return PBEResult.Ineffective_Substitute;
}
if (!ignoreCurrentStatus && Status1 != PBEStatus1.None)
{
return PBEResult.Ineffective_Status;
}
if (!ignoreSafeguard && other?.CanHitThroughSafeguard() == true && Team.TeamStatus.HasFlag(PBETeamStatus.Safeguard))
{
return PBEResult.Ineffective_Safeguard;
}
PBEAbility kAbility = useKnownInfo ? KnownAbility : Ability;
if (other?.HasCancellingAbility() != true && (kAbility == PBEAbility.Insomnia || (kAbility == PBEAbility.LeafGuard && Battle.WillLeafGuardActivate()) || kAbility == PBEAbility.VitalSpirit))
{
return PBEResult.Ineffective_Ability;
}
return PBEResult.Success;
}
public PBEResult IsSubstitutePossible(bool useKnownInfo = false, bool ignoreCurrentStatus = false)
{
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (!ignoreCurrentStatus && kStatus2.HasFlag(PBEStatus2.Substitute))
{
return PBEResult.Ineffective_Status;
}
int hpRequired = MaxHP / 4;
if (hpRequired < 1 || HP <= hpRequired)
{
return PBEResult.Ineffective_Stat;
}
return PBEResult.Success;
}
public PBEResult IsTransformPossible(PBEBattlePokemon user, bool useKnownInfo = false)
{
if (user.Status2.HasFlag(PBEStatus2.Transformed))
{
return PBEResult.Ineffective_Status;
}
PBEStatus2 kStatus2 = useKnownInfo ? KnownStatus2 : Status2;
if (kStatus2.HasFlag(PBEStatus2.Substitute))
{
return PBEResult.Ineffective_Substitute;
}
if (kStatus2.HasFlag(PBEStatus2.Disguised) || kStatus2.HasFlag(PBEStatus2.Transformed))
{
return PBEResult.Ineffective_Status;
}
return PBEResult.Success;
}
/// Returns an array of moves the Pokémon can use.
public PBEMove[] GetUsableMoves()
{
if (IsForcedToStruggle())
{
return new PBEMove[1] { PBEMove.Struggle };
}
else if (TempLockedMove != PBEMove.None)
{
return new PBEMove[1] { TempLockedMove };
}
else if (ChoiceLockedMove != PBEMove.None)
{
return new PBEMove[1] { ChoiceLockedMove }; // IsForcedToStruggle() being false means the choice locked move still has PP
}
else
{
int numMoves = Moves.Count;
var usableMoves = new List(numMoves);
for (int i = 0; i < numMoves; i++)
{
PBEBattleMoveset.PBEBattleMovesetSlot slot = Moves[i];
if (slot.PP > 0)
{
usableMoves.Add(slot.Move);
}
}
return usableMoves.ToArray();
}
}
/// Gets the chance of a protection move succeeding (based on ) out of .
public ushort GetProtectionChance()
{
int count = Protection_Counter;
return count == 0 ? ushort.MaxValue : (ushort)(ushort.MaxValue / (count * 2));
}
public PBEBattlePokemon? GetPkmnWouldDisguiseAs()
{
PBEList party = Trainer.Party;
for (int i = party.Count - 1; i >= 0; i--)
{
PBEBattlePokemon p = party[i];
// Does not copy eggs
if (p.CanBattle)
{
// If this Pokémon is the "last" conscious one, it will go out as itself (loop breaks)
// The only way to disguise as a Pokémon that's on the battlefield is the first turn of a Double/Triple/Rotation battle
if (p.OriginalSpecies != OriginalSpecies)
{
return p;
}
break;
}
}
return null;
}
/// Will only be accurate for the host
public override string ToString()
{
var sb = new StringBuilder();
string formStr = PBEDataUtils.HasForms(Species, false) ? $" ({PBEDataProvider.Instance.GetFormName(this).English})" : string.Empty;
sb.AppendLine($"{Nickname}/{PBEDataProvider.Instance.GetSpeciesName(Species).English}{formStr} {Gender.ToSymbol()} Lv.{Level}");
sb.AppendLine($"HP: {HP}/{MaxHP} ({HPPercentage:P2})");
sb.Append($"Types: {PBEDataProvider.Instance.GetTypeName(Type1).English}");
if (Type2 != PBEType.None)
{
sb.Append($"/{PBEDataProvider.Instance.GetTypeName(Type2).English}");
}
sb.AppendLine();
sb.Append($"Known types: {PBEDataProvider.Instance.GetTypeName(KnownType1).English}");
if (KnownType2 != PBEType.None)
{
sb.Append($"/{PBEDataProvider.Instance.GetTypeName(KnownType2).English}");
}
sb.AppendLine();
sb.AppendLine($"Position: {Team.CombinedName}'s {FieldPosition}");
sb.AppendLine($"Status1: {Status1}");
if (Status1 == PBEStatus1.Asleep)
{
sb.AppendLine($"Asleep turns: {Status1Counter}/{SleepTurns}");
}
else if (Status1 == PBEStatus1.BadlyPoisoned)
{
sb.AppendLine($"Toxic counter: {Status1Counter}");
}
sb.AppendLine($"Status2: {Status2}");
if (Status2.HasFlag(PBEStatus2.Confused))
{
sb.AppendLine($"Confusion turns: {ConfusionCounter}/{ConfusionTurns}");
}
if (Status2.HasFlag(PBEStatus2.Disguised))
{
formStr = PBEDataUtils.HasForms(KnownSpecies, false) ? $" ({PBEDataProvider.Instance.GetFormName(KnownSpecies, KnownForm).English})" : string.Empty;
sb.AppendLine($"Disguised as: {KnownNickname}/{PBEDataProvider.Instance.GetSpeciesName(KnownSpecies).English}{formStr} {KnownGender.ToSymbol()}");
}
if (Battle.BattleFormat != PBEBattleFormat.Single)
{
if (Status2.HasFlag(PBEStatus2.Infatuated))
{
sb.AppendLine($"Infatuated with: {InfatuatedWithPokemon!.Trainer.Name}'s {InfatuatedWithPokemon.Nickname}");
}
if (Status2.HasFlag(PBEStatus2.LeechSeed))
{
sb.AppendLine($"Seeded position: {SeededTeam!.CombinedName}'s {SeededPosition}");
}
if (Status2.HasFlag(PBEStatus2.LockOn))
{
sb.AppendLine($"Taking aim at: {LockOnPokemon!.Trainer.Name}'s {LockOnPokemon.Nickname}");
}
}
if (Status2.HasFlag(PBEStatus2.Substitute))
{
sb.AppendLine($"Substitute HP: {SubstituteHP}");
}
sb.AppendLine($"Stats: [A] {Attack}, [D] {Defense}, [SA] {SpAttack}, [SD] {SpDefense}, [S] {Speed}, [W] {Weight:0.0}");
PBEStat[] statChanges = GetChangedStats();
if (statChanges.Length > 0)
{
var statStrs = new List(7);
if (Array.IndexOf(statChanges, PBEStat.Attack) != -1)
{
statStrs.Add($"[A] x{PBEBattle.GetStatChangeModifier(AttackChange, false):0.00}");
}
if (Array.IndexOf(statChanges, PBEStat.Defense) != -1)
{
statStrs.Add($"[D] x{PBEBattle.GetStatChangeModifier(DefenseChange, false):0.00}");
}
if (Array.IndexOf(statChanges, PBEStat.SpAttack) != -1)
{
statStrs.Add($"[SA] x{PBEBattle.GetStatChangeModifier(SpAttackChange, false):0.00}");
}
if (Array.IndexOf(statChanges, PBEStat.SpDefense) != -1)
{
statStrs.Add($"[SD] x{PBEBattle.GetStatChangeModifier(SpDefenseChange, false):0.00}");
}
if (Array.IndexOf(statChanges, PBEStat.Speed) != -1)
{
statStrs.Add($"[S] x{PBEBattle.GetStatChangeModifier(SpeedChange, false):0.00}");
}
if (Array.IndexOf(statChanges, PBEStat.Accuracy) != -1)
{
statStrs.Add($"[AC] x{PBEBattle.GetStatChangeModifier(AccuracyChange, true):0.00}");
}
if (Array.IndexOf(statChanges, PBEStat.Evasion) != -1)
{
statStrs.Add($"[E] x{PBEBattle.GetStatChangeModifier(EvasionChange, true):0.00}");
}
sb.AppendLine($"Stat changes: {string.Join(", ", statStrs)}");
}
sb.AppendLine($"Ability: {PBEDataProvider.Instance.GetAbilityName(Ability).English}");
sb.AppendLine($"Known ability: {(KnownAbility == PBEAbility.MAX ? "???" : PBEDataProvider.Instance.GetAbilityName(KnownAbility).English)}");
sb.AppendLine($"Item: {PBEDataProvider.Instance.GetItemName(Item).English}");
sb.AppendLine($"Known item: {(KnownItem == (PBEItem)ushort.MaxValue ? "???" : PBEDataProvider.Instance.GetItemName(KnownItem).English)}");
if (Moves.Contains(PBEMoveEffect.Frustration) || Moves.Contains(PBEMoveEffect.Return))
{
sb.AppendLine($"Friendship: {Friendship} ({Friendship / byte.MaxValue:P2})");
}
if (Moves.Contains(PBEMoveEffect.HiddenPower))
{
PBEReadOnlyStatCollection ivs = IndividualValues!;
sb.AppendLine($"{PBEDataProvider.Instance.GetMoveName(PBEMove.HiddenPower).English}: {PBEDataProvider.Instance.GetTypeName(ivs.GetHiddenPowerType()).English}|{ivs.GetHiddenPowerBasePower(Battle.Settings)}");
}
sb.Append("Moves: ");
for (int i = 0; i < Battle.Settings.NumMoves; i++)
{
PBEBattleMoveset.PBEBattleMovesetSlot slot = Moves[i];
PBEMove move = slot.Move;
if (i > 0)
{
sb.Append(", ");
}
sb.Append(PBEDataProvider.Instance.GetMoveName(slot.Move).English);
if (move != PBEMove.None)
{
sb.Append($" ({slot.PP}/{slot.MaxPP})");
}
}
sb.AppendLine();
sb.Append("Known moves: ");
for (int i = 0; i < Battle.Settings.NumMoves; i++)
{
PBEBattleMoveset.PBEBattleMovesetSlot slot = KnownMoves[i];
PBEMove move = slot.Move;
int pp = slot.PP;
int maxPP = slot.MaxPP;
if (i > 0)
{
sb.Append(", ");
}
sb.Append(move == PBEMove.MAX ? "???" : PBEDataProvider.Instance.GetMoveName(move).English);
if (move != PBEMove.None && move != PBEMove.MAX)
{
sb.Append($" ({pp}{(maxPP == 0 ? ")" : $"/{maxPP})")}");
}
}
sb.AppendLine();
sb.Append($"Usable moves: {string.Join(", ", GetUsableMoves().Select(m => PBEDataProvider.Instance.GetMoveName(m).English))}");
return sb.ToString();
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattleReplay.cs
================================================
using Kermalis.EndianBinaryIO;
using Kermalis.PokemonBattleEngine.Data;
using Kermalis.PokemonBattleEngine.Packets;
using Kermalis.PokemonBattleEngine.Utils;
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
namespace Kermalis.PokemonBattleEngine.Battle;
public sealed partial class PBEBattle
{
private const ushort CUR_REPLAY_VERSION = 0;
public string GetDefaultReplayFileName()
{
// "2020-12-30 23-59-59 - Team 1 vs Team 2.pbereplay"
return PBEUtils.ToSafeFileName(new string(string.Format("{0:yyyy-MM-dd HH-mm-ss} - {1} vs {2}", DateTime.Now, Teams[0].CombinedName, Teams[1].CombinedName).Take(200).ToArray())) + ".pbereplay";
}
private void CheckCanSaveReplay()
{
if (!IsLocallyHosted)
{
throw new InvalidOperationException("Can only save replays of locally hosted battles");
}
if (_battleState != PBEBattleState.Ended)
{
throw new InvalidOperationException($"{nameof(BattleState)} must be {PBEBattleState.Ended} to save a replay.");
}
}
public void SaveReplay()
{
CheckCanSaveReplay();
SaveReplay(GetDefaultReplayFileName());
}
public void SaveReplayToFolder(string path)
{
CheckCanSaveReplay();
SaveReplay(Path.Combine(path, GetDefaultReplayFileName()));
}
public void SaveReplay(string path)
{
CheckCanSaveReplay();
using (var ms = new MemoryStream())
{
var w = new EndianBinaryWriter(ms);
w.WriteUInt16(CUR_REPLAY_VERSION);
w.WriteInt32(_rand.Seed);
int numEvents = Events.Count;
w.WriteInt32(numEvents);
for (int i = 0; i < numEvents; i++)
{
byte[] data = Events[i].Data.ToArray();
w.WriteUInt16((ushort)data.Length);
w.WriteBytes(data);
}
ms.Position = 0;
w.WriteBytes(MD5.HashData(ms));
File.WriteAllBytes(path, ms.ToArray());
}
}
public static PBEBattle LoadReplay(string path, PBEPacketProcessor packetProcessor)
{
byte[] fileBytes = File.ReadAllBytes(path);
using (var s = new MemoryStream(fileBytes))
{
var r = new EndianBinaryReader(s);
byte[] hash;
hash = MD5.HashData(fileBytes.AsSpan(0, fileBytes.Length - 16));
for (int i = 0; i < 16; i++)
{
if (hash[i] != fileBytes[fileBytes.Length - 16 + i])
{
throw new InvalidDataException();
}
}
ushort version = r.ReadUInt16(); // Unused for now
int seed = r.ReadInt32(); // Unused for now
PBEBattle b = null!; // The first packet should be a PBEBattlePacket
int numEvents = r.ReadInt32();
if (numEvents < 1)
{
throw new InvalidDataException();
}
for (int i = 0; i < numEvents; i++)
{
byte[] data = new byte[r.ReadUInt16()];
r.ReadBytes(data);
IPBEPacket packet = packetProcessor.CreatePacket(data, b);
if (packet is PBEBattlePacket bp)
{
if (i != 0)
{
throw new InvalidDataException();
}
b = new PBEBattle(bp);
}
else
{
if (i == 0)
{
throw new InvalidDataException();
}
if (packet is PBEWildPkmnAppearedPacket wpap)
{
PBETrainer wildTrainer = b.Teams[1].Trainers[0];
foreach (PBEPkmnAppearedInfo info in wpap.Pokemon)
{
PBEBattlePokemon pkmn = wildTrainer.GetPokemon(info.Pokemon);
// Process disguise and position now
pkmn.FieldPosition = info.FieldPosition;
if (info.IsDisguised)
{
pkmn.Status2 |= PBEStatus2.Disguised;
pkmn.KnownCaughtBall = info.CaughtBall;
pkmn.KnownGender = info.Gender;
pkmn.KnownNickname = info.Nickname;
pkmn.KnownShiny = info.Shiny;
pkmn.KnownSpecies = info.Species;
pkmn.KnownForm = info.Form;
IPBEPokemonData pData = PBEDataProvider.Instance.GetPokemonData(info);
pkmn.KnownType1 = pData.Type1;
pkmn.KnownType2 = pData.Type2;
}
b.ActiveBattlers.Add(pkmn);
}
}
b.Events.Add(packet);
}
}
b.BattleState = PBEBattleState.Ended;
return b;
}
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattleTargets.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using Kermalis.PokemonBattleEngine.Data.Utils;
using Kermalis.PokemonBattleEngine.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Kermalis.PokemonBattleEngine.Battle;
public sealed partial class PBEBattle
{
/// Gets the position across from the inputted position for a specific battle format.
/// The battle format.
/// The position.
/// Thrown when is invalid or is invalid for .
public static PBEFieldPosition GetPositionAcross(PBEBattleFormat battleFormat, PBEFieldPosition position)
{
switch (battleFormat)
{
case PBEBattleFormat.Single:
case PBEBattleFormat.Rotation:
{
if (position == PBEFieldPosition.Center)
{
return PBEFieldPosition.Center;
}
else
{
throw new ArgumentOutOfRangeException(nameof(position));
}
}
case PBEBattleFormat.Double:
{
if (position == PBEFieldPosition.Left)
{
return PBEFieldPosition.Right;
}
else if (position == PBEFieldPosition.Right)
{
return PBEFieldPosition.Left;
}
else
{
throw new ArgumentOutOfRangeException(nameof(position));
}
}
case PBEBattleFormat.Triple:
{
if (position == PBEFieldPosition.Left)
{
return PBEFieldPosition.Right;
}
else if (position == PBEFieldPosition.Center)
{
return PBEFieldPosition.Center;
}
else if (position == PBEFieldPosition.Right)
{
return PBEFieldPosition.Left;
}
else
{
throw new ArgumentOutOfRangeException(nameof(position));
}
}
default: throw new ArgumentOutOfRangeException(nameof(battleFormat));
}
}
/// Gets the Pokémon surrounding .
/// The Pokémon to check.
/// True if allies should be included, False otherwise.
/// True if foes should be included, False otherwise.
/// Thrown when 's 's is invalid or 's is invalid for 's 's .
public static IReadOnlyList GetRuntimeSurrounding(PBEBattlePokemon pkmn, bool includeAllies, bool includeFoes)
{
if (!includeAllies && !includeFoes)
{
throw new ArgumentException($"\"{nameof(includeAllies)}\" and \"{nameof(includeFoes)}\" were false.");
}
List allies = pkmn.Team.ActiveBattlers.FindAll(p => p != pkmn);
List foes = pkmn.Team.OpposingTeam.ActiveBattlers;
switch (pkmn.Battle.BattleFormat)
{
case PBEBattleFormat.Single:
{
if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
if (includeFoes)
{
return foes.FindAll(p => p.FieldPosition == PBEFieldPosition.Center);
}
return Array.Empty();
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEBattleFormat.Double:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
List ret = null!;
if (includeAllies)
{
ret = allies.FindAll(p => p.FieldPosition == PBEFieldPosition.Right);
if (!includeFoes)
{
return ret;
}
}
if (includeFoes)
{
List f = foes.FindAll(p => p.FieldPosition == PBEFieldPosition.Left || p.FieldPosition == PBEFieldPosition.Right);
if (!includeAllies)
{
return f;
}
ret.AddRange(f);
return ret;
}
return Array.Empty();
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
List ret = null!;
if (includeAllies)
{
ret = allies.FindAll(p => p.FieldPosition == PBEFieldPosition.Left);
if (!includeFoes)
{
return ret;
}
}
if (includeFoes)
{
List f = foes.FindAll(p => p.FieldPosition == PBEFieldPosition.Left || p.FieldPosition == PBEFieldPosition.Right);
if (!includeAllies)
{
return f;
}
ret.AddRange(f);
return ret;
}
return Array.Empty();
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEBattleFormat.Triple:
case PBEBattleFormat.Rotation:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
List ret = null!;
if (includeAllies)
{
ret = allies.FindAll(p => p.FieldPosition == PBEFieldPosition.Center);
if (!includeFoes)
{
return ret;
}
}
if (includeFoes)
{
List f = foes.FindAll(p => p.FieldPosition == PBEFieldPosition.Center || p.FieldPosition == PBEFieldPosition.Right);
if (!includeAllies)
{
return f;
}
ret.AddRange(f);
return ret;
}
return Array.Empty();
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
List ret = null!;
if (includeAllies)
{
ret = allies.FindAll(p => p.FieldPosition == PBEFieldPosition.Left || p.FieldPosition == PBEFieldPosition.Right);
if (!includeFoes)
{
return ret;
}
}
if (includeFoes)
{
List f = foes.FindAll(p => p.FieldPosition == PBEFieldPosition.Left || p.FieldPosition == PBEFieldPosition.Center || p.FieldPosition == PBEFieldPosition.Right);
if (!includeAllies)
{
return f;
}
ret.AddRange(f);
return ret;
}
return Array.Empty();
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
List ret = null!;
if (includeAllies)
{
ret = allies.FindAll(p => p.FieldPosition == PBEFieldPosition.Center);
if (!includeFoes)
{
return ret;
}
}
if (includeFoes)
{
List f = foes.FindAll(p => p.FieldPosition == PBEFieldPosition.Center || p.FieldPosition == PBEFieldPosition.Left);
if (!includeAllies)
{
return f;
}
ret.AddRange(f);
return ret;
}
return Array.Empty();
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
default: throw new InvalidDataException(nameof(pkmn.Battle.BattleFormat));
}
}
private static void FindFoeLeftTarget(PBEBattlePokemon user, bool canHitFarCorners, List targets)
{
PBETeam ot = user.Team.OpposingTeam;
if (!ot.TryGetPokemon(PBEFieldPosition.Left, out PBEBattlePokemon? pkmn))
{
// Left not found; fallback to its teammate
switch (user.Battle.BattleFormat)
{
case PBEBattleFormat.Double:
{
if (!ot.TryGetPokemon(PBEFieldPosition.Right, out pkmn))
{
return; // Nobody left and nobody right; fail
}
break;
}
case PBEBattleFormat.Triple:
{
if (!ot.TryGetPokemon(PBEFieldPosition.Center, out pkmn))
{
if (user.FieldPosition != PBEFieldPosition.Right || canHitFarCorners)
{
// Center fainted as well but the user can reach far right
if (!ot.TryGetPokemon(PBEFieldPosition.Right, out pkmn))
{
return; // Nobody left, center, or right; fail
}
}
else
{
return; // Nobody left and nobody center; fail since we can't reach the right
}
}
break;
}
default: throw new InvalidOperationException();
}
}
targets.Add(pkmn);
}
private static void FindFoeCenterTarget(PBEBattlePokemon user, bool canHitFarCorners, PBERandom rand, List targets)
{
PBETeam ot = user.Team.OpposingTeam;
if (!ot.TryGetPokemon(PBEFieldPosition.Center, out PBEBattlePokemon? pkmn))
{
switch (user.Battle.BattleFormat)
{
case PBEBattleFormat.Single:
case PBEBattleFormat.Rotation: return;
default: throw new InvalidOperationException();
case PBEBattleFormat.Triple:
{
// Center not found; fallback to its teammate
switch (user.FieldPosition)
{
case PBEFieldPosition.Left:
{
if (!ot.TryGetPokemon(PBEFieldPosition.Right, out pkmn))
{
if (canHitFarCorners)
{
if (!ot.TryGetPokemon(PBEFieldPosition.Left, out pkmn))
{
return; // Nobody center, right, or left; fail
}
}
else
{
return; // Nobody center and nobody right; fail since we can't reach the left
}
}
break;
}
case PBEFieldPosition.Center:
{
if (!ot.TryGetPokemon(PBEFieldPosition.Left, out PBEBattlePokemon? left))
{
if (!ot.TryGetPokemon(PBEFieldPosition.Right, out PBEBattlePokemon? right))
{
return; // Nobody left or right; fail
}
pkmn = right; // Nobody left; pick right
}
else
{
if (!ot.TryGetPokemon(PBEFieldPosition.Right, out PBEBattlePokemon? right))
{
pkmn = left; // Nobody right; pick left
}
else
{
pkmn = rand.RandomBool() ? left : right; // Left and right present; randomly select left or right
}
}
break;
}
case PBEFieldPosition.Right:
{
if (!ot.TryGetPokemon(PBEFieldPosition.Left, out pkmn))
{
if (canHitFarCorners)
{
if (!ot.TryGetPokemon(PBEFieldPosition.Right, out pkmn))
{
return; // Nobody center, left, or right; fail
}
}
else
{
return; // Nobody center and nobody left; fail since we can't reach the right
}
}
break;
}
default: throw new InvalidDataException();
}
break;
}
}
}
targets.Add(pkmn);
}
private static void FindFoeRightTarget(PBEBattlePokemon user, bool canHitFarCorners, List targets)
{
PBETeam ot = user.Team.OpposingTeam;
if (!ot.TryGetPokemon(PBEFieldPosition.Right, out PBEBattlePokemon? pkmn))
{
// Right not found; fallback to its teammate
switch (user.Battle.BattleFormat)
{
case PBEBattleFormat.Double:
{
if (!ot.TryGetPokemon(PBEFieldPosition.Left, out pkmn))
{
return; // Nobody right and nobody left; fail
}
break;
}
case PBEBattleFormat.Triple:
{
if (!ot.TryGetPokemon(PBEFieldPosition.Center, out pkmn))
{
if (user.FieldPosition != PBEFieldPosition.Left || canHitFarCorners)
{
// Center fainted as well but the user can reach far left
if (!ot.TryGetPokemon(PBEFieldPosition.Left, out pkmn))
{
return; // Nobody right, center, or left; fail
}
}
else
{
return; // Nobody right and nobody center; fail since we can't reach the left
}
}
break;
}
default: throw new InvalidOperationException();
}
}
targets.Add(pkmn);
}
/// Gets all Pokémon that will be hit.
/// The Pokémon that will act.
/// The targets the Pokémon wishes to hit.
/// Whether the move can hit far Pokémon in a triple battle.
/// The random to use.
private static PBEBattlePokemon[] GetRuntimeTargets(PBEBattlePokemon user, PBETurnTarget requestedTargets, bool canHitFarCorners, PBERandom rand)
{
var targets = new List();
// Foes first, then allies (since initial attack effects run that way)
if (requestedTargets.HasFlag(PBETurnTarget.FoeLeft))
{
FindFoeLeftTarget(user, canHitFarCorners, targets);
}
if (requestedTargets.HasFlag(PBETurnTarget.FoeCenter))
{
FindFoeCenterTarget(user, canHitFarCorners, rand, targets);
}
if (requestedTargets.HasFlag(PBETurnTarget.FoeRight))
{
FindFoeRightTarget(user, canHitFarCorners, targets);
}
PBETeam t = user.Team;
if (requestedTargets.HasFlag(PBETurnTarget.AllyLeft))
{
t.TryAddPokemonToCollection(PBEFieldPosition.Left, targets);
}
if (requestedTargets.HasFlag(PBETurnTarget.AllyCenter))
{
t.TryAddPokemonToCollection(PBEFieldPosition.Center, targets);
}
if (requestedTargets.HasFlag(PBETurnTarget.AllyRight))
{
t.TryAddPokemonToCollection(PBEFieldPosition.Right, targets);
}
return targets.Distinct().ToArray(); // Remove duplicate targets
}
/// Determines whether chosen targets are valid for a given move.
/// The Pokémon that will act.
/// The move the Pokémon wishes to use.
/// The targets bitfield to validate.
/// Thrown when , , 's , or 's 's is invalid.
public static bool AreTargetsValid(PBEBattlePokemon pkmn, PBEMove move, PBETurnTarget targets)
{
if (move == PBEMove.None || move >= PBEMove.MAX)
{
throw new ArgumentOutOfRangeException(nameof(move));
}
IPBEMoveData mData = PBEDataProvider.Instance.GetMoveData(move);
if (!mData.IsMoveUsable())
{
throw new ArgumentOutOfRangeException(nameof(move));
}
return AreTargetsValid(pkmn, mData, targets);
}
public static bool AreTargetsValid(PBEBattlePokemon pkmn, IPBEMoveData mData, PBETurnTarget targets)
{
PBEMoveTarget possibleTargets = pkmn.GetMoveTargets(mData);
switch (pkmn.Battle.BattleFormat)
{
case PBEBattleFormat.Single:
{
switch (possibleTargets)
{
case PBEMoveTarget.All:
{
if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return targets == (PBETurnTarget.AllyCenter | PBETurnTarget.FoeCenter);
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoes:
case PBEMoveTarget.AllFoesSurrounding:
case PBEMoveTarget.AllSurrounding:
case PBEMoveTarget.SingleFoeSurrounding:
case PBEMoveTarget.SingleNotSelf:
case PBEMoveTarget.SingleSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return targets == PBETurnTarget.FoeCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllTeam:
case PBEMoveTarget.RandomFoeSurrounding:
case PBEMoveTarget.Self:
case PBEMoveTarget.SelfOrAllySurrounding:
case PBEMoveTarget.SingleAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return targets == PBETurnTarget.AllyCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
default: throw new InvalidDataException(nameof(possibleTargets));
}
}
case PBEBattleFormat.Double:
{
switch (possibleTargets)
{
case PBEMoveTarget.All:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == (PBETurnTarget.AllyLeft | PBETurnTarget.AllyRight | PBETurnTarget.FoeLeft | PBETurnTarget.FoeRight);
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoes:
case PBEMoveTarget.AllFoesSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == (PBETurnTarget.FoeLeft | PBETurnTarget.FoeRight);
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllTeam:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == (PBETurnTarget.AllyLeft | PBETurnTarget.AllyRight);
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return targets == (PBETurnTarget.AllyRight | PBETurnTarget.FoeLeft | PBETurnTarget.FoeRight);
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == (PBETurnTarget.AllyLeft | PBETurnTarget.FoeLeft | PBETurnTarget.FoeRight);
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.RandomFoeSurrounding:
case PBEMoveTarget.Self:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return targets == PBETurnTarget.AllyLeft;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.AllyRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SelfOrAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.AllyLeft || targets == PBETurnTarget.AllyRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return targets == PBETurnTarget.AllyRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.AllyLeft;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleFoeSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.FoeLeft || targets == PBETurnTarget.FoeRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleNotSelf:
case PBEMoveTarget.SingleSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return targets == PBETurnTarget.AllyRight || targets == PBETurnTarget.FoeLeft || targets == PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.AllyLeft || targets == PBETurnTarget.FoeLeft || targets == PBETurnTarget.FoeRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
default: throw new InvalidDataException(nameof(possibleTargets));
}
}
case PBEBattleFormat.Triple:
{
switch (possibleTargets)
{
case PBEMoveTarget.All:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == (PBETurnTarget.AllyLeft | PBETurnTarget.AllyCenter | PBETurnTarget.AllyRight | PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight);
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoes:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == (PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight);
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoesSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return targets == (PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight);
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return targets == (PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight);
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == (PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter);
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return targets == (PBETurnTarget.AllyCenter | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight);
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return targets == (PBETurnTarget.AllyLeft | PBETurnTarget.AllyRight | PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight);
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == (PBETurnTarget.AllyCenter | PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter);
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllTeam:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == (PBETurnTarget.AllyLeft | PBETurnTarget.AllyCenter | PBETurnTarget.AllyRight);
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.RandomFoeSurrounding:
case PBEMoveTarget.Self:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return targets == PBETurnTarget.AllyLeft;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return targets == PBETurnTarget.AllyCenter;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.AllyRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SelfOrAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return targets == PBETurnTarget.AllyLeft || targets == PBETurnTarget.AllyCenter;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return targets == PBETurnTarget.AllyLeft || targets == PBETurnTarget.AllyCenter || targets == PBETurnTarget.AllyRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.AllyCenter || targets == PBETurnTarget.AllyRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.AllyCenter;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return targets == PBETurnTarget.AllyLeft || targets == PBETurnTarget.AllyRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleFoeSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return targets == PBETurnTarget.FoeCenter || targets == PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return targets == PBETurnTarget.FoeLeft || targets == PBETurnTarget.FoeCenter || targets == PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.FoeLeft || targets == PBETurnTarget.FoeCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleNotSelf:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return targets == PBETurnTarget.AllyCenter || targets == PBETurnTarget.AllyRight || targets == PBETurnTarget.FoeLeft || targets == PBETurnTarget.FoeCenter || targets == PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return targets == PBETurnTarget.AllyLeft || targets == PBETurnTarget.AllyRight || targets == PBETurnTarget.FoeLeft || targets == PBETurnTarget.FoeCenter || targets == PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.AllyLeft || targets == PBETurnTarget.AllyCenter || targets == PBETurnTarget.FoeLeft || targets == PBETurnTarget.FoeCenter || targets == PBETurnTarget.FoeRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return targets == PBETurnTarget.AllyCenter || targets == PBETurnTarget.FoeCenter || targets == PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return targets == PBETurnTarget.AllyLeft || targets == PBETurnTarget.AllyRight || targets == PBETurnTarget.FoeLeft || targets == PBETurnTarget.FoeCenter || targets == PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.AllyCenter || targets == PBETurnTarget.FoeLeft || targets == PBETurnTarget.FoeCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
default: throw new InvalidDataException(nameof(possibleTargets));
}
}
case PBEBattleFormat.Rotation:
{
switch (possibleTargets)
{
case PBEMoveTarget.All:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == (PBETurnTarget.AllyCenter | PBETurnTarget.FoeCenter);
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoes:
case PBEMoveTarget.AllFoesSurrounding:
case PBEMoveTarget.AllSurrounding:
case PBEMoveTarget.SingleFoeSurrounding:
case PBEMoveTarget.SingleNotSelf:
case PBEMoveTarget.SingleSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.FoeCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllTeam:
case PBEMoveTarget.RandomFoeSurrounding:
case PBEMoveTarget.Self:
case PBEMoveTarget.SelfOrAllySurrounding:
case PBEMoveTarget.SingleAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return targets == PBETurnTarget.AllyCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
default: throw new InvalidDataException(nameof(possibleTargets));
}
}
default: throw new InvalidDataException(nameof(pkmn.Battle.BattleFormat));
}
}
/// Gets a random target a move can hit when called by .
/// The Pokémon using .
/// The move being called.
/// The random to use.
/// Thrown when , 's , or 's 's is invalid.
public static PBETurnTarget GetRandomTargetForMetronome(PBEBattlePokemon pkmn, PBEMove calledMove, PBERandom rand)
{
if (calledMove == PBEMove.None || calledMove >= PBEMove.MAX || !PBEDataUtils.IsMoveUsable(calledMove))
{
throw new ArgumentOutOfRangeException(nameof(calledMove));
}
IPBEMoveData mData = PBEDataProvider.Instance.GetMoveData(calledMove);
if (!mData.IsMoveUsable())
{
throw new ArgumentOutOfRangeException(nameof(calledMove));
}
PBEMoveTarget possibleTargets = pkmn.GetMoveTargets(mData);
switch (pkmn.Battle.BattleFormat)
{
case PBEBattleFormat.Single:
{
switch (possibleTargets)
{
case PBEMoveTarget.All:
{
if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return PBETurnTarget.AllyCenter | PBETurnTarget.FoeCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoes:
case PBEMoveTarget.AllFoesSurrounding:
case PBEMoveTarget.AllSurrounding:
case PBEMoveTarget.RandomFoeSurrounding:
case PBEMoveTarget.SingleFoeSurrounding:
case PBEMoveTarget.SingleNotSelf:
case PBEMoveTarget.SingleSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return PBETurnTarget.FoeCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllTeam:
case PBEMoveTarget.Self:
case PBEMoveTarget.SelfOrAllySurrounding:
case PBEMoveTarget.SingleAllySurrounding: // Helping Hand cannot be called by Metronome anyway
{
if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return PBETurnTarget.AllyCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
default: throw new InvalidDataException(nameof(possibleTargets));
}
}
case PBEBattleFormat.Double:
{
switch (possibleTargets)
{
case PBEMoveTarget.All:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyLeft | PBETurnTarget.AllyRight | PBETurnTarget.FoeLeft | PBETurnTarget.FoeRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoes:
case PBEMoveTarget.AllFoesSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.FoeLeft | PBETurnTarget.FoeRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllTeam:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyLeft | PBETurnTarget.AllyRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return PBETurnTarget.AllyRight | PBETurnTarget.FoeLeft | PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyLeft | PBETurnTarget.FoeLeft | PBETurnTarget.FoeRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.Self:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return PBETurnTarget.AllyLeft;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SelfOrAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
if (rand.RandomBool())
{
return PBETurnTarget.AllyLeft;
}
else
{
return PBETurnTarget.AllyRight;
}
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleAllySurrounding: // Helping Hand cannot be called by Metronome anyway
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return PBETurnTarget.AllyRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyLeft;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.RandomFoeSurrounding:
case PBEMoveTarget.SingleFoeSurrounding:
case PBEMoveTarget.SingleNotSelf:
case PBEMoveTarget.SingleSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
if (rand.RandomBool())
{
return PBETurnTarget.FoeLeft;
}
else
{
return PBETurnTarget.FoeRight;
}
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
default: throw new InvalidDataException(nameof(possibleTargets));
}
}
case PBEBattleFormat.Triple:
{
switch (possibleTargets)
{
case PBEMoveTarget.All:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyLeft | PBETurnTarget.AllyCenter | PBETurnTarget.AllyRight | PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoes:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoesSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return PBETurnTarget.AllyCenter | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return PBETurnTarget.AllyLeft | PBETurnTarget.AllyRight | PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyCenter | PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllTeam:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyLeft | PBETurnTarget.AllyCenter | PBETurnTarget.AllyRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.Self:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return PBETurnTarget.AllyLeft;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return PBETurnTarget.AllyCenter;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyRight;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SelfOrAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
if (rand.RandomBool())
{
return PBETurnTarget.AllyLeft;
}
else
{
return PBETurnTarget.AllyCenter;
}
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
int val = rand.RandomInt(0, 2);
if (val == 0)
{
return PBETurnTarget.AllyLeft;
}
else if (val == 1)
{
return PBETurnTarget.AllyCenter;
}
else
{
return PBETurnTarget.AllyRight;
}
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
if (rand.RandomBool())
{
return PBETurnTarget.AllyCenter;
}
else
{
return PBETurnTarget.AllyRight;
}
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleAllySurrounding: // Helping Hand cannot be called by Metronome anyway
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return PBETurnTarget.AllyCenter;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
if (rand.RandomBool())
{
return PBETurnTarget.AllyLeft;
}
else
{
return PBETurnTarget.AllyRight;
}
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.RandomFoeSurrounding:
case PBEMoveTarget.SingleFoeSurrounding:
case PBEMoveTarget.SingleSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
if (rand.RandomBool())
{
return PBETurnTarget.FoeCenter;
}
else
{
return PBETurnTarget.FoeRight;
}
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
int val = rand.RandomInt(0, 2);
if (val == 0)
{
return PBETurnTarget.FoeLeft;
}
else if (val == 1)
{
return PBETurnTarget.FoeCenter;
}
else
{
return PBETurnTarget.FoeRight;
}
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
if (rand.RandomBool())
{
return PBETurnTarget.FoeLeft;
}
else
{
return PBETurnTarget.FoeCenter;
}
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleNotSelf:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
int val = rand.RandomInt(0, 2);
if (val == 0)
{
return PBETurnTarget.FoeLeft;
}
else if (val == 1)
{
return PBETurnTarget.FoeCenter;
}
else
{
return PBETurnTarget.FoeRight;
}
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
default: throw new InvalidDataException(nameof(possibleTargets));
}
}
case PBEBattleFormat.Rotation:
{
switch (possibleTargets)
{
case PBEMoveTarget.All:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyCenter | PBETurnTarget.FoeCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoes:
case PBEMoveTarget.AllFoesSurrounding:
case PBEMoveTarget.AllSurrounding:
case PBEMoveTarget.RandomFoeSurrounding:
case PBEMoveTarget.SingleFoeSurrounding:
case PBEMoveTarget.SingleNotSelf:
case PBEMoveTarget.SingleSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.FoeCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllTeam:
case PBEMoveTarget.Self:
case PBEMoveTarget.SelfOrAllySurrounding:
case PBEMoveTarget.SingleAllySurrounding: // Helping Hand cannot be called by Metronome anyway
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyCenter;
}
else
{
throw new InvalidDataException(nameof(pkmn.FieldPosition));
}
}
default: throw new InvalidDataException(nameof(possibleTargets));
}
}
default: throw new InvalidDataException(nameof(pkmn.Battle.BattleFormat));
}
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattleTeam.cs
================================================
using Kermalis.PokemonBattleEngine.Packets;
using Kermalis.PokemonBattleEngine.Utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
namespace Kermalis.PokemonBattleEngine.Battle;
public sealed class PBETeams : IReadOnlyList
{
private readonly PBETeam _team0;
private readonly PBETeam _team1;
public int Count => 2;
public PBETeam this[int index]
{
get
{
switch (index)
{
case 0: return _team0;
case 1: return _team1;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
// Trainer battle
internal PBETeams(PBEBattle battle, IReadOnlyList ti0, IReadOnlyList ti1, out PBETrainers trainers)
{
var allTrainers = new List();
_team0 = new PBETeam(battle, 0, ti0, allTrainers);
_team1 = new PBETeam(battle, 1, ti1, allTrainers);
_team0.OpposingTeam = _team1;
_team1.OpposingTeam = _team0;
trainers = new PBETrainers(allTrainers);
}
// Wild battle
internal PBETeams(PBEBattle battle, IReadOnlyList ti, PBEWildInfo wi, out PBETrainers trainers)
{
var allTrainers = new List();
_team0 = new PBETeam(battle, 0, ti, allTrainers);
_team1 = new PBETeam(battle, 1, wi, allTrainers);
_team0.OpposingTeam = _team1;
_team1.OpposingTeam = _team0;
trainers = new PBETrainers(allTrainers);
}
// Remote battle
internal PBETeams(PBEBattle battle, PBEBattlePacket packet, out PBETrainers trainers)
{
var allTrainers = new List();
_team0 = new PBETeam(battle, packet.Teams[0], allTrainers);
_team1 = new PBETeam(battle, packet.Teams[1], allTrainers);
_team0.OpposingTeam = _team1;
_team1.OpposingTeam = _team0;
trainers = new PBETrainers(allTrainers);
}
public bool All(Predicate match)
{
return match(_team0) && match(_team1);
}
public IEnumerator GetEnumerator()
{
yield return _team0;
yield return _team1;
}
IEnumerator IEnumerable.GetEnumerator()
{
yield return _team0;
yield return _team1;
}
}
/// Represents a team in a specific .
public sealed class PBETeam
{
/// The battle this team and its party belongs to.
public PBEBattle Battle { get; }
public PBETeam OpposingTeam { get; internal set; }
public ReadOnlyCollection Trainers { get; }
public byte Id { get; }
public bool IsWild => Battle.BattleType == PBEBattleType.Wild && Id == 1;
public string CombinedName { get; }
public IEnumerable CombinedParty => Trainers.SelectMany(t => t.Party);
public List ActiveBattlers => Battle.ActiveBattlers.FindAll(p => p.Team == this);
public int NumConsciousPkmn => Trainers.Sum(t => t.NumConsciousPkmn);
public int NumPkmnOnField => Trainers.Sum(t => t.NumPkmnOnField);
public int NumTimesTriedToFlee { get; set; }
public PBETeamStatus TeamStatus { get; set; }
public byte LightScreenCount { get; set; }
public byte LuckyChantCount { get; set; }
public byte ReflectCount { get; set; }
public byte SafeguardCount { get; set; }
public byte SpikeCount { get; set; }
public byte TailwindCount { get; set; }
public byte ToxicSpikeCount { get; set; }
public bool MonFaintedLastTurn { get; set; }
public bool MonFaintedThisTurn { get; set; }
// Trainer battle
internal PBETeam(PBEBattle battle, byte id, IReadOnlyList ti, List allTrainers)
{
int count = ti.Count;
if (!VerifyTrainerCount(battle.BattleFormat, count))
{
throw new ArgumentException($"Illegal trainer count (Format: {battle.BattleFormat}, Team: {id}, Count: {count}");
}
foreach (PBETrainerInfo t in ti)
{
if (!t.IsOkayForSettings(battle.Settings))
{
throw new ArgumentOutOfRangeException(nameof(ti), "Team settings do not comply with battle settings.");
}
}
Battle = battle;
Id = id;
var trainers = new PBETrainer[ti.Count];
for (int i = 0; i < ti.Count; i++)
{
trainers[i] = new PBETrainer(this, ti[i], allTrainers);
}
Trainers = new ReadOnlyCollection(trainers);
CombinedName = GetCombinedName();
OpposingTeam = null!; // OpposingTeam is set in PBETeams after both are created
}
// Wild battle
internal PBETeam(PBEBattle battle, byte id, PBEWildInfo wi, List allTrainers)
{
int count = wi.Party.Count;
if (!VerifyWildCount(battle.BattleFormat, count))
{
throw new ArgumentException($"Illegal wild Pokémon count (Format: {battle.BattleFormat}, Count: {count}");
}
if (!wi.IsOkayForSettings(battle.Settings))
{
throw new ArgumentOutOfRangeException(nameof(wi), "Team settings do not comply with battle settings.");
}
Battle = battle;
Id = id;
Trainers = new ReadOnlyCollection(new[] { new PBETrainer(this, wi, allTrainers) });
CombinedName = GetCombinedName();
OpposingTeam = null!; // OpposingTeam is set in PBETeams after both are created
}
// Remote battle
internal PBETeam(PBEBattle battle, PBEBattlePacket.PBETeamInfo info, List allTrainers)
{
ReadOnlyCollection ti = info.Trainers;
int count = ti.Count;
if (!VerifyTrainerCount(battle.BattleFormat, count))
{
throw new InvalidDataException();
}
Battle = battle;
Id = info.Id;
var trainers = new PBETrainer[ti.Count];
for (int i = 0; i < trainers.Length; i++)
{
trainers[i] = new PBETrainer(this, ti[i], allTrainers);
}
Trainers = new ReadOnlyCollection(trainers);
CombinedName = GetCombinedName();
OpposingTeam = null!; // OpposingTeam is set in PBETeams after both are created
}
private static bool VerifyWildCount(PBEBattleFormat format, int count)
{
switch (format)
{
case PBEBattleFormat.Single: return count == 1;
case PBEBattleFormat.Double: return count >= 1 && count <= 2;
case PBEBattleFormat.Rotation:
case PBEBattleFormat.Triple: return count >= 1 && count <= 3;
default: throw new ArgumentOutOfRangeException(nameof(format));
}
}
private static bool VerifyTrainerCount(PBEBattleFormat format, int count)
{
switch (format)
{
case PBEBattleFormat.Single:
case PBEBattleFormat.Rotation: return count == 1;
case PBEBattleFormat.Double: return count == 1 || count == 2;
case PBEBattleFormat.Triple: return count == 1 || count == 3;
default: throw new ArgumentOutOfRangeException(nameof(format));
}
}
private string GetCombinedName()
{
string[] names = new string[Trainers.Count];
for (int i = 0; i < names.Length; i++)
{
names[i] = Trainers[i].Name;
}
return names.Andify();
}
public bool IsSpotOccupied(PBEFieldPosition pos)
{
foreach (PBEBattlePokemon p in ActiveBattlers)
{
if (p.FieldPosition == pos)
{
return true;
}
}
return false;
}
public bool TryGetPokemon(PBEFieldPosition pos, [NotNullWhen(true)] out PBEBattlePokemon? pkmn)
{
foreach (PBEBattlePokemon p in ActiveBattlers)
{
if (p.FieldPosition == pos)
{
pkmn = p;
return true;
}
}
pkmn = null;
return false;
}
public void TryAddPokemonToCollection(PBEFieldPosition pos, ICollection list)
{
if (TryGetPokemon(pos, out PBEBattlePokemon? pkmn))
{
list.Add(pkmn);
}
}
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"Team {Id}:");
sb.AppendLine($"TeamStatus: {TeamStatus}");
//sb.AppendLine($"NumPkmn: {Party.Length}");
sb.AppendLine($"NumConsciousPkmn: {NumConsciousPkmn}");
sb.AppendLine($"NumPkmnOnField: {NumPkmnOnField}");
return sb.ToString();
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattleTrainer.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using Kermalis.PokemonBattleEngine.Packets;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Kermalis.PokemonBattleEngine.Battle;
public sealed class PBETrainers : IReadOnlyList
{
private readonly List _trainers;
public int Count => _trainers.Count;
public PBETrainer this[int index] => _trainers[index];
internal PBETrainers(List trainers)
{
_trainers = trainers;
}
public IEnumerator GetEnumerator()
{
return _trainers.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return _trainers.GetEnumerator();
}
}
public sealed partial class PBETrainer
{
public PBEBattle Battle { get; }
public PBETeam Team { get; }
public PBEList Party { get; }
public string Name { get; }
public bool GainsEXP { get; }
public PBEBattleInventory Inventory { get; }
public byte Id { get; }
public bool IsWild => Team.IsWild;
public List ActiveBattlers => Battle.ActiveBattlers.FindAll(p => p.Trainer == this);
public IEnumerable ActiveBattlersOrdered => ActiveBattlers.OrderBy(p => p.FieldPosition);
public int NumConsciousPkmn => Party.Count(p => p.CanBattle);
public int NumPkmnOnField => Party.Count(p => p.FieldPosition != PBEFieldPosition.None);
public bool RequestedFlee { get; set; }
public List ActionsRequired { get; } = new(3); // PBEBattleState.WaitingForActions
public byte SwitchInsRequired { get; set; } // PBEBattleState.WaitingForSwitchIns
public List<(PBEBattlePokemon Pkmn, PBEFieldPosition Pos)> SwitchInQueue { get; } = new(3); // PBEBattleState.WaitingForSwitchIns
// Trainer battle / wild battle
private PBETrainer(PBETeam team, PBETrainerInfoBase ti, string name, ReadOnlyCollection<(PBEItem Item, uint Quantity)>? inventory, List trainers)
{
Battle = team.Battle;
Team = team;
Id = (byte)trainers.Count;
Name = name;
if (inventory is null || inventory.Count == 0) // Wild trainer
{
Inventory = PBEBattleInventory.Empty();
}
else
{
Inventory = new PBEBattleInventory(inventory);
}
ReadOnlyCollection tiParty = ti.Party;
Party = new PBEList(tiParty.Count);
for (byte i = 0; i < tiParty.Count; i++)
{
IPBEPokemon pkmn = tiParty[i];
if (pkmn is IPBEPartyPokemon partyPkmn)
{
_ = new PBEBattlePokemon(this, i, partyPkmn);
}
else
{
_ = new PBEBattlePokemon(this, i, pkmn);
}
}
trainers.Add(this);
}
// Trainer battle
internal PBETrainer(PBETeam team, PBETrainerInfo ti, List trainers)
: this(team, ti, ti.Name, ti.Inventory, trainers)
{
GainsEXP = ti.GainsEXP;
}
// Wild battle
internal PBETrainer(PBETeam team, PBEWildInfo wi, List trainers)
: this(team, wi, "The wild Pokémon", null, trainers) { }
// Remote battle
internal PBETrainer(PBETeam team, PBEBattlePacket.PBETeamInfo.PBETrainerInfo info, List trainers)
{
Battle = team.Battle;
Team = team;
Id = info.Id;
Name = team.IsWild ? "The wild Pokémon" : info.Name;
Inventory = info.Inventory.Count == 0 ? PBEBattleInventory.Empty() : new PBEBattleInventory(info.Inventory);
Party = new PBEList(info.Party.Select(p => new PBEBattlePokemon(this, p)));
trainers.Add(this);
}
public static void Remove(PBEBattlePokemon pokemon)
{
pokemon.Trainer.Party.Remove(pokemon);
}
public static void SwitchTwoPokemon(PBEBattlePokemon a, PBEFieldPosition pos)
{
if (pos == PBEFieldPosition.None || pos >= PBEFieldPosition.MAX)
{
throw new ArgumentOutOfRangeException(nameof(pos));
}
PBETrainer t = a.Trainer;
PBEBattlePokemon b = t.Party[t.GetFieldPositionIndex(pos)];
if (a != b)
{
t.Party.Swap(a, b);
}
}
public static void SwitchTwoPokemon(PBEBattlePokemon a, PBEBattlePokemon b)
{
if (a != b)
{
PBETrainer t = a.Trainer;
if (t != b.Trainer)
{
throw new ArgumentException(nameof(a.Trainer));
}
t.Party.Swap(a, b);
}
}
public bool IsSpotOccupied(PBEFieldPosition pos)
{
foreach (PBEBattlePokemon p in ActiveBattlers)
{
if (p.FieldPosition == pos)
{
return true;
}
}
return false;
}
public bool TryGetPokemon(PBEFieldPosition pos, [NotNullWhen(true)] out PBEBattlePokemon? pkmn)
{
foreach (PBEBattlePokemon p in ActiveBattlers)
{
if (p.FieldPosition == pos)
{
pkmn = p;
return true;
}
}
pkmn = null;
return false;
}
public bool TryGetPokemon(byte pkmnId, [NotNullWhen(true)] out PBEBattlePokemon? pkmn)
{
foreach (PBEBattlePokemon p in Party)
{
if (p.Id == pkmnId)
{
pkmn = p;
return true;
}
}
pkmn = null;
return false;
}
public PBEBattlePokemon GetPokemon(PBEFieldPosition pos)
{
foreach (PBEBattlePokemon p in ActiveBattlers)
{
if (p.FieldPosition == pos)
{
return p;
}
}
throw new InvalidOperationException();
}
public PBEBattlePokemon GetPokemon(byte pkmnId)
{
foreach (PBEBattlePokemon p in Party)
{
if (p.Id == pkmnId)
{
return p;
}
}
throw new InvalidOperationException();
}
}
================================================
FILE: PokemonBattleEngine/Battle/BattleUtils.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using System;
using System.IO;
namespace Kermalis.PokemonBattleEngine.Battle;
public static class PBEBattleUtils
{
public static PBETurnTarget GetSpreadMoveTargets(PBEBattlePokemon pkmn, PBEMoveTarget targets)
{
switch (pkmn.Battle.BattleFormat)
{
case PBEBattleFormat.Single:
{
switch (targets)
{
case PBEMoveTarget.All:
{
if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return PBETurnTarget.AllyCenter | PBETurnTarget.FoeCenter;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoes:
case PBEMoveTarget.AllFoesSurrounding:
case PBEMoveTarget.AllSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return PBETurnTarget.FoeCenter;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllTeam:
{
if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return PBETurnTarget.AllyCenter;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
default: throw new ArgumentOutOfRangeException(nameof(targets));
}
}
case PBEBattleFormat.Double:
{
switch (targets)
{
case PBEMoveTarget.All:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyLeft | PBETurnTarget.AllyRight | PBETurnTarget.FoeLeft | PBETurnTarget.FoeRight;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoes:
case PBEMoveTarget.AllFoesSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.FoeLeft | PBETurnTarget.FoeRight;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllTeam:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyLeft | PBETurnTarget.AllyRight;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return PBETurnTarget.AllyRight | PBETurnTarget.FoeLeft | PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyLeft | PBETurnTarget.FoeLeft | PBETurnTarget.FoeRight;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
default: throw new ArgumentOutOfRangeException(nameof(targets));
}
}
case PBEBattleFormat.Triple:
{
switch (targets)
{
case PBEMoveTarget.All:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyLeft | PBETurnTarget.AllyCenter | PBETurnTarget.AllyRight | PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoes:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoesSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return PBETurnTarget.AllyCenter | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return PBETurnTarget.AllyLeft | PBETurnTarget.AllyRight | PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter | PBETurnTarget.FoeRight;
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyCenter | PBETurnTarget.FoeLeft | PBETurnTarget.FoeCenter;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllTeam:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyLeft | PBETurnTarget.AllyCenter | PBETurnTarget.AllyRight;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
default: throw new ArgumentOutOfRangeException(nameof(targets));
}
}
case PBEBattleFormat.Rotation:
{
switch (targets)
{
case PBEMoveTarget.All:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyCenter | PBETurnTarget.FoeCenter;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllFoes:
case PBEMoveTarget.AllFoesSurrounding:
case PBEMoveTarget.AllSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.FoeCenter;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.AllTeam:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return PBETurnTarget.AllyCenter;
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
default: throw new ArgumentOutOfRangeException(nameof(targets));
}
}
default: throw new InvalidDataException(nameof(pkmn.Battle.BattleFormat));
}
}
public static PBETurnTarget[] GetPossibleTargets(PBEBattlePokemon pkmn, PBEMoveTarget targets)
{
switch (pkmn.Battle.BattleFormat)
{
case PBEBattleFormat.Single:
{
switch (targets)
{
case PBEMoveTarget.SingleFoeSurrounding:
case PBEMoveTarget.SingleNotSelf:
case PBEMoveTarget.SingleSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return new PBETurnTarget[] { PBETurnTarget.FoeCenter };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.RandomFoeSurrounding:
case PBEMoveTarget.Self:
case PBEMoveTarget.SelfOrAllySurrounding:
case PBEMoveTarget.SingleAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return new PBETurnTarget[] { PBETurnTarget.AllyCenter };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
default: throw new ArgumentOutOfRangeException(nameof(targets));
}
}
case PBEBattleFormat.Double:
{
switch (targets)
{
case PBEMoveTarget.RandomFoeSurrounding:
case PBEMoveTarget.Self:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return new PBETurnTarget[] { PBETurnTarget.AllyLeft };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.AllyRight };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SelfOrAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.AllyLeft, PBETurnTarget.AllyRight };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return new PBETurnTarget[] { PBETurnTarget.AllyRight };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.AllyLeft };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleFoeSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.FoeLeft, PBETurnTarget.FoeRight };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleNotSelf:
case PBEMoveTarget.SingleSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return new PBETurnTarget[] { PBETurnTarget.AllyRight, PBETurnTarget.FoeLeft, PBETurnTarget.FoeRight };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.AllyLeft, PBETurnTarget.FoeLeft, PBETurnTarget.FoeRight };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
default: throw new ArgumentOutOfRangeException(nameof(targets));
}
}
case PBEBattleFormat.Triple:
{
switch (targets)
{
case PBEMoveTarget.RandomFoeSurrounding:
case PBEMoveTarget.Self:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return new PBETurnTarget[] { PBETurnTarget.AllyLeft };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return new PBETurnTarget[] { PBETurnTarget.AllyCenter };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.AllyRight };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SelfOrAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return new PBETurnTarget[] { PBETurnTarget.AllyLeft, PBETurnTarget.AllyCenter };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return new PBETurnTarget[] { PBETurnTarget.AllyLeft, PBETurnTarget.AllyCenter, PBETurnTarget.AllyRight };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.AllyCenter, PBETurnTarget.AllyRight };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.AllyCenter };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return new PBETurnTarget[] { PBETurnTarget.AllyLeft, PBETurnTarget.AllyRight };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleFoeSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return new PBETurnTarget[] { PBETurnTarget.FoeCenter, PBETurnTarget.FoeRight };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return new PBETurnTarget[] { PBETurnTarget.FoeLeft, PBETurnTarget.FoeCenter, PBETurnTarget.FoeRight };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.FoeLeft, PBETurnTarget.FoeCenter };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleNotSelf:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return new PBETurnTarget[] { PBETurnTarget.AllyCenter, PBETurnTarget.AllyRight, PBETurnTarget.FoeLeft, PBETurnTarget.FoeCenter, PBETurnTarget.FoeRight };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return new PBETurnTarget[] { PBETurnTarget.AllyLeft, PBETurnTarget.AllyRight, PBETurnTarget.FoeLeft, PBETurnTarget.FoeCenter, PBETurnTarget.FoeRight };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.AllyLeft, PBETurnTarget.AllyCenter, PBETurnTarget.FoeLeft, PBETurnTarget.FoeCenter, PBETurnTarget.FoeRight };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.SingleSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left)
{
return new PBETurnTarget[] { PBETurnTarget.AllyCenter, PBETurnTarget.FoeCenter, PBETurnTarget.FoeRight };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Center)
{
return new PBETurnTarget[] { PBETurnTarget.AllyLeft, PBETurnTarget.AllyRight, PBETurnTarget.FoeLeft, PBETurnTarget.FoeCenter, PBETurnTarget.FoeRight };
}
else if (pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.AllyCenter, PBETurnTarget.FoeLeft, PBETurnTarget.FoeCenter };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
default: throw new ArgumentOutOfRangeException(nameof(targets));
}
}
case PBEBattleFormat.Rotation:
{
switch (targets)
{
case PBEMoveTarget.SingleFoeSurrounding:
case PBEMoveTarget.SingleNotSelf:
case PBEMoveTarget.SingleSurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.FoeCenter };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
case PBEMoveTarget.RandomFoeSurrounding:
case PBEMoveTarget.Self:
case PBEMoveTarget.SelfOrAllySurrounding:
case PBEMoveTarget.SingleAllySurrounding:
{
if (pkmn.FieldPosition == PBEFieldPosition.Left || pkmn.FieldPosition == PBEFieldPosition.Center || pkmn.FieldPosition == PBEFieldPosition.Right)
{
return new PBETurnTarget[] { PBETurnTarget.AllyCenter };
}
else
{
throw new ArgumentException(nameof(pkmn.FieldPosition));
}
}
default: throw new ArgumentOutOfRangeException(nameof(targets));
}
}
default: throw new InvalidDataException(nameof(pkmn.Battle.BattleFormat));
}
}
internal static void VerifyPosition(PBEBattleFormat format, PBEFieldPosition pos)
{
if (pos != PBEFieldPosition.None && pos < PBEFieldPosition.MAX)
{
switch (format)
{
case PBEBattleFormat.Single:
{
switch (pos)
{
case PBEFieldPosition.Center: return;
}
break;
}
case PBEBattleFormat.Double:
{
switch (pos)
{
case PBEFieldPosition.Left:
case PBEFieldPosition.Right: return;
}
break;
}
case PBEBattleFormat.Triple:
case PBEBattleFormat.Rotation:
{
return;
}
}
}
throw new ArgumentOutOfRangeException(nameof(pos));
}
public static int GetFieldPositionIndex(this PBETrainer trainer, PBEFieldPosition position)
{
if (!trainer.OwnsSpot(position))
{
throw new ArgumentOutOfRangeException(nameof(position));
}
PBEBattleFormat battleFormat = trainer.Battle.BattleFormat;
int index = trainer.Team.Trainers.IndexOf(trainer);
switch (battleFormat)
{
case PBEBattleFormat.Single:
{
switch (position)
{
case PBEFieldPosition.Center: return 0;
}
break;
}
case PBEBattleFormat.Double:
{
switch (position)
{
case PBEFieldPosition.Left: return 0;
case PBEFieldPosition.Right: return index == 1 ? 0 : 1;
}
break;
}
case PBEBattleFormat.Triple:
{
switch (position)
{
case PBEFieldPosition.Left: return 0;
case PBEFieldPosition.Center: return index == 1 ? 0 : 1;
case PBEFieldPosition.Right: return index == 2 ? 0 : 2;
}
break;
}
case PBEBattleFormat.Rotation:
{
switch (position)
{
case PBEFieldPosition.Center: return 0;
case PBEFieldPosition.Left: return 1;
case PBEFieldPosition.Right: return 2;
}
break;
}
}
throw new Exception();
}
public static bool OwnsSpot(this PBETrainer trainer, PBEFieldPosition pos)
{
return GetTrainer(trainer.Team, pos) == trainer;
}
public static PBETrainer GetTrainer(this PBETeam team, PBEFieldPosition pos)
{
PBEBattleFormat format = team.Battle.BattleFormat;
VerifyPosition(format, pos);
int i = 0;
if (team.Trainers.Count != 1)
{
switch (format)
{
case PBEBattleFormat.Double: i = pos == PBEFieldPosition.Left ? 0 : 1; break;
case PBEBattleFormat.Triple: i = pos == PBEFieldPosition.Left ? 0 : pos == PBEFieldPosition.Center ? 1 : 2; break;
}
}
return team.Trainers[i];
}
}
================================================
FILE: PokemonBattleEngine/Battle/TrainerInfo.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using Kermalis.PokemonBattleEngine.Data.Legality;
using Kermalis.PokemonBattleEngine.Utils;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace Kermalis.PokemonBattleEngine.Battle;
public abstract class PBETrainerInfoBase
{
public ReadOnlyCollection Party { get; }
private readonly PBESettings? _requiredSettings;
protected PBETrainerInfoBase(IPBEPokemonCollection party)
{
if (party is IPBEPartyPokemonCollection ppc)
{
if (!ppc.Any(p => p.HP > 0 && !p.PBEIgnore))
{
throw new ArgumentException("Party must have at least 1 conscious battler", nameof(party));
}
}
else
{
if (!party.Any(p => !p.PBEIgnore))
{
throw new ArgumentException("Party must have at least 1 conscious battler", nameof(party));
}
}
if (party is PBELegalPokemonCollection lp)
{
_requiredSettings = lp.Settings;
}
Party = new ReadOnlyCollection(party.ToArray());
}
public bool IsOkayForSettings(PBESettings settings)
{
settings.ShouldBeReadOnly(nameof(settings));
if (_requiredSettings is not null)
{
return settings.Equals(_requiredSettings);
}
if (this is PBETrainerInfo ti && ti.GainsEXP && settings.MaxLevel > 100)
{
throw new ArgumentException("Cannot start a battle with EXP enabled and a higher MaxLevel than 100. Not supported.");
}
return true;
}
}
public sealed class PBETrainerInfo : PBETrainerInfoBase
{
public string Name { get; }
public bool GainsEXP { get; }
public ReadOnlyCollection<(PBEItem Item, uint Quantity)> Inventory { get; }
public PBETrainerInfo(IPBEPokemonCollection party, string name, bool gainsEXP, IList<(PBEItem Item, uint Quantity)>? inventory = null)
: base(party)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentOutOfRangeException(nameof(name));
}
Name = name;
GainsEXP = gainsEXP;
if (inventory is null || inventory.Count == 0)
{
Inventory = PBEEmptyReadOnlyCollection<(PBEItem, uint)>.Value;
}
else
{
Inventory = new ReadOnlyCollection<(PBEItem, uint)>(inventory);
}
}
}
public sealed class PBEWildInfo : PBETrainerInfoBase
{
public PBEWildInfo(IPBEPokemonCollection party)
: base(party) { }
}
================================================
FILE: PokemonBattleEngine/Battle/TypeEffectiveness.cs
================================================
using Kermalis.PokemonBattleEngine.Data;
using System;
using System.Collections.Generic;
namespace Kermalis.PokemonBattleEngine.Battle;
public static class PBETypeEffectiveness
{
#region Static Collections
/// The type effectiveness table. The first key is the attacking type and the second key is the defending type.
private static readonly Dictionary> _table = new()
{
{
PBEType.None,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 1.0f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 1.0f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 1.0f },
{ PBEType.Flying, 1.0f },
{ PBEType.Ghost, 1.0f },
{ PBEType.Grass, 1.0f },
{ PBEType.Ground, 1.0f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 1.0f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 1.0f },
{ PBEType.Steel, 1.0f },
{ PBEType.Water, 1.0f },
}
},
{
PBEType.Bug,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 1.0f },
{ PBEType.Dark, 2.0f },
{ PBEType.Dragon, 1.0f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 0.5f },
{ PBEType.Fire, 0.5f },
{ PBEType.Flying, 0.5f },
{ PBEType.Ghost, 0.5f },
{ PBEType.Grass, 2.0f },
{ PBEType.Ground, 1.0f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 0.5f },
{ PBEType.Psychic, 2.0f },
{ PBEType.Rock, 1.0f },
{ PBEType.Steel, 0.5f },
{ PBEType.Water, 1.0f },
}
},
{
PBEType.Dark,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 1.0f },
{ PBEType.Dark, 0.5f },
{ PBEType.Dragon, 1.0f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 0.5f },
{ PBEType.Fire, 1.0f },
{ PBEType.Flying, 1.0f },
{ PBEType.Ghost, 2.0f },
{ PBEType.Grass, 1.0f },
{ PBEType.Ground, 1.0f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 1.0f },
{ PBEType.Psychic, 2.0f },
{ PBEType.Rock, 1.0f },
{ PBEType.Steel, 0.5f },
{ PBEType.Water, 1.0f },
}
},
{
PBEType.Dragon,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 1.0f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 2.0f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 1.0f },
{ PBEType.Flying, 1.0f },
{ PBEType.Ghost, 1.0f },
{ PBEType.Grass, 1.0f },
{ PBEType.Ground, 1.0f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 1.0f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 1.0f },
{ PBEType.Steel, 0.5f },
{ PBEType.Water, 1.0f },
}
},
{
PBEType.Electric,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 1.0f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 0.5f },
{ PBEType.Electric, 0.5f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 1.0f },
{ PBEType.Flying, 2.0f },
{ PBEType.Ghost, 1.0f },
{ PBEType.Grass, 0.5f },
{ PBEType.Ground, 0.0f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 1.0f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 1.0f },
{ PBEType.Steel, 1.0f },
{ PBEType.Water, 2.0f },
}
},
{
PBEType.Fighting,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 0.5f },
{ PBEType.Dark, 2.0f },
{ PBEType.Dragon, 1.0f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 1.0f },
{ PBEType.Flying, 0.5f },
{ PBEType.Ghost, 0.0f },
{ PBEType.Grass, 1.0f },
{ PBEType.Ground, 1.0f },
{ PBEType.Ice, 2.0f },
{ PBEType.Normal, 2.0f },
{ PBEType.Poison, 0.5f },
{ PBEType.Psychic, 0.5f },
{ PBEType.Rock, 2.0f },
{ PBEType.Steel, 2.0f },
{ PBEType.Water, 1.0f },
}
},
{
PBEType.Fire,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 2.0f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 0.5f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 0.5f },
{ PBEType.Flying, 1.0f },
{ PBEType.Ghost, 1.0f },
{ PBEType.Grass, 2.0f },
{ PBEType.Ground, 1.0f },
{ PBEType.Ice, 2.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 1.0f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 0.5f },
{ PBEType.Steel, 2.0f },
{ PBEType.Water, 0.5f },
}
},
{
PBEType.Flying,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 2.0f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 1.0f },
{ PBEType.Electric, 0.5f },
{ PBEType.Fighting, 2.0f },
{ PBEType.Fire, 1.0f },
{ PBEType.Flying, 1.0f },
{ PBEType.Ghost, 1.0f },
{ PBEType.Grass, 2.0f },
{ PBEType.Ground, 1.0f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 1.0f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 0.5f },
{ PBEType.Steel, 0.5f },
{ PBEType.Water, 1.0f },
}
},
{
PBEType.Ghost,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 1.0f },
{ PBEType.Dark, 0.5f },
{ PBEType.Dragon, 1.0f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 1.0f },
{ PBEType.Flying, 1.0f },
{ PBEType.Ghost, 2.0f },
{ PBEType.Grass, 1.0f },
{ PBEType.Ground, 1.0f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 0.0f },
{ PBEType.Poison, 1.0f },
{ PBEType.Psychic, 2.0f },
{ PBEType.Rock, 1.0f },
{ PBEType.Steel, 0.5f },
{ PBEType.Water, 1.0f },
}
},
{
PBEType.Grass,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 0.5f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 0.5f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 0.5f },
{ PBEType.Flying, 0.5f },
{ PBEType.Ghost, 1.0f },
{ PBEType.Grass, 0.5f },
{ PBEType.Ground, 2.0f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 0.5f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 2.0f },
{ PBEType.Steel, 0.5f },
{ PBEType.Water, 2.0f },
}
},
{
PBEType.Ground,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 0.5f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 1.0f },
{ PBEType.Electric, 2.0f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 2.0f },
{ PBEType.Flying, 0.0f },
{ PBEType.Ghost, 1.0f },
{ PBEType.Grass, 0.5f },
{ PBEType.Ground, 1.0f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 2.0f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 2.0f },
{ PBEType.Steel, 2.0f },
{ PBEType.Water, 1.0f },
}
},
{
PBEType.Ice,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 1.0f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 2.0f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 0.5f },
{ PBEType.Flying, 2.0f },
{ PBEType.Ghost, 1.0f },
{ PBEType.Grass, 2.0f },
{ PBEType.Ground, 2.0f },
{ PBEType.Ice, 0.5f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 1.0f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 1.0f },
{ PBEType.Steel, 0.5f },
{ PBEType.Water, 0.5f },
}
},
{
PBEType.Normal,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 1.0f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 1.0f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 1.0f },
{ PBEType.Flying, 1.0f },
{ PBEType.Ghost, 0.0f },
{ PBEType.Grass, 1.0f },
{ PBEType.Ground, 1.0f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 1.0f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 0.5f },
{ PBEType.Steel, 0.5f },
{ PBEType.Water, 1.0f },
}
},
{
PBEType.Poison,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 1.0f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 1.0f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 1.0f },
{ PBEType.Flying, 1.0f },
{ PBEType.Ghost, 0.5f },
{ PBEType.Grass, 2.0f },
{ PBEType.Ground, 0.5f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 0.5f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 0.5f },
{ PBEType.Steel, 0.0f },
{ PBEType.Water, 1.0f },
}
},
{
PBEType.Psychic,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 1.0f },
{ PBEType.Dark, 0.0f },
{ PBEType.Dragon, 1.0f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 2.0f },
{ PBEType.Fire, 1.0f },
{ PBEType.Flying, 1.0f },
{ PBEType.Ghost, 1.0f },
{ PBEType.Grass, 1.0f },
{ PBEType.Ground, 1.0f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 2.0f },
{ PBEType.Psychic, 0.5f },
{ PBEType.Rock, 1.0f },
{ PBEType.Steel, 0.5f },
{ PBEType.Water, 1.0f },
}
},
{
PBEType.Rock,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 2.0f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 1.0f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 0.5f },
{ PBEType.Fire, 2.0f },
{ PBEType.Flying, 2.0f },
{ PBEType.Ghost, 1.0f },
{ PBEType.Grass, 1.0f },
{ PBEType.Ground, 0.5f },
{ PBEType.Ice, 2.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 1.0f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 1.0f },
{ PBEType.Steel, 0.5f },
{ PBEType.Water, 1.0f },
}
},
{
PBEType.Steel,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 1.0f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 1.0f },
{ PBEType.Electric, 0.5f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 0.5f },
{ PBEType.Flying, 1.0f },
{ PBEType.Ghost, 1.0f },
{ PBEType.Grass, 1.0f },
{ PBEType.Ground, 1.0f },
{ PBEType.Ice, 2.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 1.0f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 2.0f },
{ PBEType.Steel, 0.5f },
{ PBEType.Water, 0.5f },
}
},
{
PBEType.Water,
new Dictionary
{
{ PBEType.None, 1.0f },
{ PBEType.Bug, 1.0f },
{ PBEType.Dark, 1.0f },
{ PBEType.Dragon, 0.5f },
{ PBEType.Electric, 1.0f },
{ PBEType.Fighting, 1.0f },
{ PBEType.Fire, 2.0f },
{ PBEType.Flying, 1.0f },
{ PBEType.Ghost, 1.0f },
{ PBEType.Grass, 0.5f },
{ PBEType.Ground, 2.0f },
{ PBEType.Ice, 1.0f },
{ PBEType.Normal, 1.0f },
{ PBEType.Poison, 1.0f },
{ PBEType.Psychic, 1.0f },
{ PBEType.Rock, 2.0f },
{ PBEType.Steel, 1.0f },
{ PBEType.Water, 0.5f },
}
}
};
#endregion
public static PBEResult IsAffectedByAttack(PBEBattlePokemon user, PBEBattlePokemon target, PBEType moveType, out float damageMultiplier, bool useKnownInfo = false)
{
if (moveType >= PBEType.MAX)
{
throw new ArgumentOutOfRangeException(nameof(moveType));
}
PBEResult result;
if (moveType == PBEType.Ground)
{
result = target.IsGrounded(user, useKnownInfo: useKnownInfo);
if (result != PBEResult.Success)
{
damageMultiplier = 0;
return result;
}
}
bool ignoreGhost = user.Ability == PBEAbility.Scrappy || target.Status2.HasFlag(PBEStatus2.Identified),
ignoreDark = target.Status2.HasFlag(PBEStatus2.MiracleEye);
damageMultiplier = GetEffectiveness(moveType, target, useKnownInfo, ignoreGhost: ignoreGhost, ignoreDark: ignoreDark);
if (damageMultiplier <= 0) // (-infinity, 0]
{
damageMultiplier = 0;
return PBEResult.Ineffective_Type;
}
else if (damageMultiplier < 1) // (0, 1)
{
result = PBEResult.NotVeryEffective_Type;
}
else if (damageMultiplier == 1) // [1, 1]
{
result = PBEResult.Success;
}
else // (1, infinity)
{
return PBEResult.SuperEffective_Type;
}
PBEAbility kAbility = useKnownInfo ? target.KnownAbility : target.Ability;
if (kAbility == PBEAbility.WonderGuard && !user.HasCancellingAbility())
{
damageMultiplier = 0;
result = PBEResult.Ineffective_Ability;
}
return result;
}
/// Checks if 's type affects the target, taking into account .
public static PBEResult ThunderWaveTypeCheck(PBEBattlePokemon user, PBEBattlePokemon target, PBEMove move, bool useKnownInfo = false)
{
PBEType moveType = user.GetMoveType(move);
float d = GetEffectiveness(moveType, target, useKnownInfo);
if (d <= 0)
{
return PBEResult.Ineffective_Type;
}
return PBEResult.Success;
}
public static float GetEffectiveness(PBEType attackingType, PBEType defendingType, bool ignoreGhost = false, bool ignoreDark = false)
{
if (attackingType >= PBEType.MAX)
{
throw new ArgumentOutOfRangeException(nameof(attackingType));
}
if (defendingType >= PBEType.MAX)
{
throw new ArgumentOutOfRangeException(nameof(defendingType));
}
float d = _table[attackingType][defendingType];
if (d <= 0 && ((ignoreGhost && defendingType == PBEType.Ghost) || (ignoreDark && defendingType == PBEType.Dark)))
{
return 1;
}
return d;
}
public static float GetEffectiveness(PBEType attackingType, PBEType defendingType1, PBEType defendingType2, bool ignoreGhost = false, bool ignoreDark = false)
{
float d = GetEffectiveness(attackingType, defendingType1, ignoreGhost: ignoreGhost, ignoreDark: ignoreDark);
d *= GetEffectiveness(attackingType, defendingType2, ignoreGhost: ignoreGhost, ignoreDark: ignoreDark);
return d;
}
public static float GetEffectiveness(PBEType attackingType, IPBEPokemonTypes defendingTypes, bool ignoreGhost = false, bool ignoreDark = false)
{
return GetEffectiveness(attackingType, defendingTypes.Type1, defendingTypes.Type2, ignoreGhost: ignoreGhost, ignoreDark: ignoreDark);
}
public static float GetEffectiveness_Known(PBEType attackingType, IPBEPokemonKnownTypes defendingTypes, bool ignoreGhost = false, bool ignoreDark = false)
{
return GetEffectiveness(attackingType, defendingTypes.KnownType1, defendingTypes.KnownType2, ignoreGhost: ignoreGhost, ignoreDark: ignoreDark);
}
public static float GetEffectiveness(PBEType attackingType, T defendingTypes, bool useKnownInfo, bool ignoreGhost = false, bool ignoreDark = false)
where T : IPBEPokemonTypes, IPBEPokemonKnownTypes
{
return GetEffectiveness(attackingType, useKnownInfo ? defendingTypes.KnownType1 : defendingTypes.Type1, useKnownInfo ? defendingTypes.KnownType2 : defendingTypes.Type2, ignoreGhost: ignoreGhost, ignoreDark: ignoreDark);
}
public static float GetStealthRockMultiplier(PBEType type1, PBEType type2)
{
if (type1 >= PBEType.MAX)
{
throw new ArgumentOutOfRangeException(nameof(type1));
}
if (type2 >= PBEType.MAX)
{
throw new ArgumentOutOfRangeException(nameof(type2));
}
float d = 0.125f;
d *= _table[PBEType.Rock][type1];
d *= _table[PBEType.Rock][type2];
return d;
}
}
================================================
FILE: PokemonBattleEngine/Data/DataEnums.cs
================================================
using Kermalis.PokemonBattleEngine.Battle;
using System;
namespace Kermalis.PokemonBattleEngine.Data;
/// Represents a language the engine supports.
public enum PBELanguage : byte
{
English,
French,
German,
Italian,
Japanese_Kana,
Japanese_Kanji,
Korean,
Spanish,
MAX
}
/// Represents a specific Pokémon's gender.
public enum PBEGender : byte
{
/// The Pokémon is female.
Female,
/// The Pokémon is genderless.
Genderless,
/// The Pokémon is male.
Male,
/// Invalid gender.
MAX
}
/// Represents a Pokémon species' ratio.
public enum PBEGenderRatio : byte
{
/// The species is 87.5% male, 12.5% female.
M7_F1 = 0x1F,
/// The species is 75% male, 25% female.
M3_F1 = 0x3F,
/// The species is 50% male, 50% female.
M1_F1 = 0x7F,
/// The species is 25% male, 75% female.
M1_F3 = 0xBF,
/// The species is 0% male, 100% female.
M0_F1 = 0xFE,
/// The species is genderless.
M0_F0 = 0xFF,
/// The species is 100% male, 0% female.
M1_F0 = 0x00
}
public enum PBEGrowthRate : byte
{
Erratic = 1,
Fast = 4,
Fluctuating = 2,
MediumFast = 0,
MediumSlow = 3,
Slow = 5,
MAX = 6 // 6 & 7 in-game are clones of MediumFast, but no Pokémon uses 6 or 7
}
/// Represents a Pokémon stat.
public enum PBEStat : byte
{
/// Hit points.
HP,
/// Attack.
Attack,
/// Defense.
Defense,
/// Special Attack.
SpAttack,
/// Special Defense.
SpDefense,
/// Speed.
Speed,
/// Accuracy.
Accuracy,
/// Evasion.
Evasion
}
/// Represents a specific 's category.
public enum PBEMoveCategory : byte
{
/// The move deals no damage.
Status,
/// The move deals physical damage using the Attack and Defense stats.
Physical,
/// The move deals special damage using the Special Attack and Special Defense stats.
Special,
/// Invalid category.
MAX
}
/// Represents the various methods in which a Pokémon can learn a .
public enum PBEType : byte
{
None,
Bug,
Dark,
Dragon,
Electric,
Fighting,
Fire,
Flying,
Ghost,
Grass,
Ground,
Ice,
Normal,
Poison,
Psychic,
Rock,
Steel,
Water,
MAX
}
/// Represents a specific Pokémon's nature.
public enum PBENature : byte
{
/// The Pokémon favors attack and hinders special attack.
Adamant = 3,
/// The Pokémon doesn't favor any stat.
Bashful = 18,
/// The Pokémon favors defense and hinders attack.
Bold = 5,
/// The Pokémon favors attack and hinders speed.
Brave = 2,
/// The Pokémon favors special defense and hinders attack.
Calm = 20,
/// The Pokémon favors special defense and hinders special attack.
Careful = 23,
/// The Pokémon doesn't favor any stat.
Docile = 6,
/// The Pokémon favors special defense and hinders defense.
Gentle = 21,
/// The Pokémon doesn't favor any stat.
Hardy = 0,
/// The Pokémon favors speed and hinders defense.
Hasty = 11,
/// The Pokémon favors defense and hinders special attack.
Impish = 8,
/// The Pokémon favors speed and hinders special attack.
Jolly = 13,
/// The Pokémon favors defense and hinders special defense.
Lax = 9,
/// The Pokémon favors attack and hinders defense.
Lonely = 1,
/// The Pokémon favors special attack and hinders defense.
Mild = 16,
/// The Pokémon favors special attack and hinders attack.
Modest = 15,
/// The Pokémon favors speed and hinders special defense.
Naive = 14,
/// The Pokémon favors attack and hinders special defense.
Naughty = 4,
/// The Pokémon favors special attack and hinders speed.
Quiet = 17,
/// The Pokémon doesn't favor any stat.
Quirky = 24,
/// The Pokémon favors special attack and hinders special defense.
Rash = 19,
/// The Pokémon favors defense and hinders speed.
Relaxed = 7,
/// The Pokémon favors special defense and hinders speed.
Sassy = 22,
/// The Pokémon doesn't favor any stat.
Serious = 12,
/// The Pokémon favors speed and hinders attack.
Timid = 10,
/// Invalid nature.
MAX = 25
}
public enum PBEFlavor : byte
{
Bitter = 3,
Dry = 2,
Sour = 1,
Spicy = 0,
Sweet = 4,
MAX = 5
}
/// Represents a specific Pokémon's held item.
public enum PBEItem : ushort
{
/// No item.
None = 0,
AbsorbBulb = 545, // TODO
AdamantOrb = 135,
AguavBerry = 162,
AirBalloon = 541, // TODO
/// No effect.
AmuletCoin = 223,
Antidote = 18, // TODO
ApicotBerry = 205,
/// No effect.
ArmorFossil = 104,
AspearBerry = 153, // TODO
Awakening = 21, // TODO
BabiriBerry = 199, // TODO
/// No effect.
BalmMushroom = 580,
BelueBerry = 183, // TODO
BerryJuice = 43,
/// No effect.
BigMushroom = 87,
/// No effect.
BigNugget = 581,
/// No effect.
BigPearl = 89,
BigRoot = 296,
BindingBand = 544, // TODO
BlackBelt = 241,
/// No effect.
BlackFlute = 68,
BlackGlasses = 240,
BlackSludge = 281,
/// No effect.
BlkApricorn = 491,
/// No effect.
BluApricorn = 486,
/// No effect.
BlueFlute = 65,
/// No effect.
BlueScarf = 261,
/// No effect.
BlueShard = 73,
BlukBerry = 165, // TODO
/// No effect.
BridgeMailD = 145,
/// No effect.
BridgeMailM = 148,
/// No effect.
BridgeMailS = 144,
/// No effect.
BridgeMailT = 146,
/// No effect.
BridgeMailV = 147,
BrightPowder = 213,
BugGem = 558,
BurnDrive = 118,
BurnHeal = 19, // TODO
/// No effect.
Calcium = 49,
/// No effect.
Carbos = 48,
Casteliacone = 591, // TODO
CellBattery = 546, // TODO
Charcoal = 249,
ChartiBerry = 195, // TODO
CheriBerry = 149, // TODO
CherishBall = 16,
ChestoBerry = 150, // TODO
ChilanBerry = 200, // TODO
ChillDrive = 119,
ChoiceBand = 220,
ChoiceScarf = 287,
ChoiceSpecs = 297,
ChopleBerry = 189, // TODO
/// No effect.
ClawFossil = 100,
/// No effect.
CleanseTag = 224,
/// No effect.
CleverWing = 569,
CobaBerry = 192, // TODO
ColburBerry = 198, // TODO
/// No effect.
CometShard = 583,
CornnBerry = 175, // TODO
/// No effect.
CoverFossil = 572,
CustapBerry = 210, // TODO
/// No effect.
DampMulch = 96,
DampRock = 285,
DarkGem = 562,
/// No effect.
DawnStone = 109,
DeepSeaScale = 227,
DeepSeaTooth = 226,
DestinyKnot = 280,
DireHit = 56, // TODO
DiveBall = 7,
/// No effect.
DomeFossil = 102,
DouseDrive = 116,
DracoPlate = 311,
DragonFang = 250,
DragonGem = 561,
/// No effect.
DragonScale = 235,
DreadPlate = 312,
DreamBall = 576,
/// No effect.
DubiousDisc = 324,
DurinBerry = 182, // TODO
DuskBall = 13,
/// No effect.
DuskStone = 108,
EarthPlate = 305,
EjectButton = 547, // TODO
/// No effect.
Electirizer = 322,
ElectricGem = 550,
Elixir = 40, // TODO
EnergyPowder = 34, // TODO
EnergyRoot = 35, // TODO
EnigmaBerry = 208, // TODO
/// No effect.
EscapeRope = 78,
Ether = 38, // TODO
/// No effect.
Everstone = 229,
Eviolite = 538,
ExpertBelt = 268,
ExpShare = 216,
FastBall = 492,
/// No effect.
FavoredMail = 138,
FightingGem = 553,
FigyBerry = 159,
FireGem = 548,
/// No effect.
FireStone = 82,
FistPlate = 303,
/// The Pokémon contracts at the end of each turn if it has no other and it does not have .
FlameOrb = 273,
FlamePlate = 298,
FloatStone = 539, // TODO
FluffyTail = 64,
FlyingGem = 556,
FocusBand = 230,
FocusSash = 275,
FreshWater = 30, // TODO
FriendBall = 497,
FullHeal = 27, // TODO
FullIncense = 316, // TODO
FullRestore = 23, // TODO
GanlonBerry = 202,
/// No effect.
GeniusWing = 568,
GhostGem = 560,
/// No effect.
GooeyMulch = 98,
GrassGem = 551,
GreatBall = 3,
/// No effect.
GreenScarf = 263,
/// No effect.
GreenShard = 75,
/// No effect.
GreetMail = 137,
GrepaBerry = 173, // TODO
GripClaw = 286, // TODO
GriseousOrb = 112,
/// No effect.
GrnApricorn = 488,
GroundGem = 555,
/// No effect.
GrowthMulch = 95,
GuardSpec = 55, // TODO
HabanBerry = 197, // TODO
HardStone = 238,
HealBall = 14,
HealPowder = 36, // TODO
/// No effect.
HealthWing = 565,
/// No effect.
HeartScale = 93,
HeatRock = 284,
HeavyBall = 495,
/// No effect.
HelixFossil = 101,
HondewBerry = 172, // TODO
/// No effect.
Honey = 94,
/// No effect.
HPUp = 45,
HyperPotion = 25, // TODO
IapapaBerry = 163,
IceGem = 552,
IceHeal = 20, // TODO
IciclePlate = 302,
IcyRock = 282,
/// No effect.
InquiryMail = 141,
InsectPlate = 308,
/// No effect.
Iron = 47,
IronBall = 278, // TODO
IronPlate = 313,
JabocaBerry = 211, // TODO
KasibBerry = 196, // TODO
KebiaBerry = 190, // TODO
KelpsyBerry = 170, // TODO
KingsRock = 221, // TODO
LaggingTail = 279, // TODO
LansatBerry = 206, // TODO
LavaCookie = 42, // TODO
LaxIncense = 255,
/// No effect.
LeafStone = 85,
Leftovers = 234,
Lemonade = 32, // TODO
LeppaBerry = 154, // TODO
LevelBall = 493,
LiechiBerry = 201,
LifeOrb = 270,
LightBall = 236,
LightClay = 269,
/// No effect.
LikeMail = 142,
LoveBall = 496,
/// No effect.
LuckIncense = 319,
LuckyEgg = 231,
LuckyPunch = 256,
LumBerry = 157, // TODO
LureBall = 494,
LustrousOrb = 136,
LuxuryBall = 11,
MachoBrace = 215,
/// No effect.
Magmarizer = 323,
Magnet = 242,
MagoBerry = 161,
MagostBerry = 176, // TODO
MasterBall = 1,
MaxElixir = 41, // TODO
MaxEther = 39, // TODO
MaxPotion = 24, // TODO
/// No effect.
MaxRepel = 77,
MaxRevive = 29, // TODO
MeadowPlate = 301,
MentalHerb = 219, // TODO
MetalCoat = 233,
MetalPowder = 257,
Metronome = 277, // TODO
MicleBerry = 209, // TODO
MindPlate = 307,
MiracleSeed = 239,
MoomooMilk = 33, // TODO
MoonBall = 498,
/// No effect.
MoonStone = 81,
MuscleBand = 266,
/// No effect.
MuscleWing = 566,
MysticWater = 243,
NanabBerry = 166, // TODO
NestBall = 8,
NetBall = 6,
NeverMeltIce = 246,
NomelBerry = 178, // TODO
NormalGem = 564,
/// No effect.
Nugget = 92,
OccaBerry = 184, // TODO
OddIncense = 314,
/// No effect.
OddKeystone = 111,
/// No effect.
OldAmber = 103,
OldGateau = 54, // TODO
OranBerry = 155,
/// No effect.
OvalStone = 110,
PamtreBerry = 180, // TODO
ParalyzHeal = 22, // TODO
ParkBall = 500,
PasshoBerry = 185, // TODO
/// No effect.
PassOrb = 575,
PayapaBerry = 193, // TODO
/// No effect.
Pearl = 88,
/// No effect.
PearlString = 582,
PechaBerry = 151, // TODO
PersimBerry = 156, // TODO
PetayaBerry = 204,
PinapBerry = 168, // TODO
/// No effect.
PinkScarf = 262,
/// No effect.
PlumeFossil = 573,
/// No effect.
PnkApricorn = 489,
PoisonBarb = 245,
PoisonGem = 554,
PokeBall = 4,
PokeDoll = 63,
PokeToy = 577,
PomegBerry = 169, // TODO
Potion = 17, // TODO
PowerAnklet = 293,
PowerBand = 292,
PowerBelt = 290,
PowerBracer = 289,
PowerHerb = 271,
PowerLens = 291,
PowerWeight = 294,
/// No effect.
PPMax = 53,
/// No effect.
PPUp = 51,
PremierBall = 12,
/// No effect.
PrettyWing = 571,
/// No effect.
PrismScale = 537,
/// No effect.
Protector = 321,
/// No effect.
Protein = 46,
PsychicGem = 557,
/// No effect.
PureIncense = 320,
QualotBerry = 171, // TODO
QuickBall = 15,
QuickClaw = 217, // TODO
QuickPowder = 274,
RabutaBerry = 177, // TODO
RageCandyBar = 504, // TODO
/// No effect.
RareBone = 106,
/// No effect.
RareCandy = 50,
RawstBerry = 152, // TODO
RazorClaw = 326,
RazorFang = 327, // TODO
RazzBerry = 164, // TODO
/// No effect.
ReaperCloth = 325,
/// No effect.
RedApricorn = 485,
RedCard = 542, // TODO
/// No effect.
RedFlute = 67,
/// No effect.
RedScarf = 260,
/// No effect.
RedShard = 72,
/// No effect.
RelicBand = 588,
/// No effect.
RelicCopper = 584,
/// No effect.
RelicCrown = 590,
/// No effect.
RelicGold = 586,
/// No effect.
RelicSilver = 585,
/// No effect.
RelicStatue = 589,
/// No effect.
RelicVase = 587,
RepeatBall = 9,
/// No effect.
Repel = 79,
/// No effect.
ReplyMail = 143,
/// No effect.
ResistWing = 567,
RevivalHerb = 37, // TODO
Revive = 28, // TODO
RindoBerry = 187, // TODO
RingTarget = 543, // TODO
RockGem = 559,
RockIncense = 315,
RockyHelmet = 540,
/// No effect.
RootFossil = 99,
RoseIncense = 318,
RowapBerry = 212, // TODO
/// No effect.
RSVPMail = 139,
SacredAsh = 44, // TODO
SafariBall = 5,
SalacBerry = 203,
ScopeLens = 232,
SeaIncense = 254,
SharpBeak = 244,
ShedShell = 295, // TODO
ShellBell = 253, // TODO
/// No effect.
ShinyStone = 107,
/// No effect.
ShoalSalt = 70,
/// No effect.
ShoalShell = 71,
ShockDrive = 117,
ShucaBerry = 191, // TODO
SilkScarf = 251,
SilverPowder = 222,
SitrusBerry = 158,
/// No effect.
SkullFossil = 105,
SkyPlate = 306,
SmokeBall = 228,
SmoothRock = 283,
SodaPop = 31, // TODO
SoftSand = 237,
/// No effect.
SootheBell = 218,
SoulDew = 225,
SpellTag = 247,
SpelonBerry = 179, // TODO
SplashPlate = 299,
SpookyPlate = 310,
SportBall = 499,
/// No effect.
StableMulch = 97,
/// No effect.
Stardust = 90,
StarfBerry = 207,
/// No effect.
StarPiece = 91,
SteelGem = 563,
Stick = 259,
StickyBarb = 288, // TODO
StonePlate = 309,
/// No effect.
SunStone = 80,
SuperPotion = 26, // TODO
/// No effect.
SuperRepel = 76,
SweetHeart = 134, // TODO
/// No effect.
SwiftWing = 570,
TamatoBerry = 174, // TODO
TangaBerry = 194, // TODO
/// No effect.
ThanksMail = 140,
ThickClub = 258,
/// No effect.
Thunderstone = 83,
TimerBall = 10,
/// No effect.
TinyMushroom = 86,
/// The Pokémon contracts at the end of each turn if it has no other and it does not have or .
ToxicOrb = 272,
ToxicPlate = 304,
TwistedSpoon = 248,
UltraBall = 2,
UpGrade = 252,
WacanBerry = 186, // TODO
WaterGem = 549,
/// No effect.
WaterStone = 84,
WatmelBerry = 181, // TODO
WaveIncense = 317,
WepearBerry = 167, // TODO
WhiteHerb = 214, // TODO
/// No effect.
WhiteFlute = 69,
WideLens = 265,
WikiBerry = 160,
WiseGlasses = 267,
/// No effect.
WhtApricorn = 490,
XAccuracy = 60, // TODO
XAttack = 57, // TODO
XDefend = 58, // TODO
XSpecial = 61, // TODO
XSpDef = 62, // TODO
XSpeed = 59, // TODO
YacheBerry = 188, // TODO
/// No effect.
YellowFlute = 66,
/// No effect.
YellowScarf = 264,
/// No effect.
YellowShard = 74,
/// No effect.
YlwApricorn = 487,
ZapPlate = 300,
/// No effect.
Zinc = 52,
ZoomLens = 276 // TODO
}
/// Represents a specific Pokémon's special ability.
public enum PBEAbility : byte
{
/// The Pokémon's ability was suppressed with .
None = 0,
/// The Pokémon has a stronger same-type-attack-bonus.
Adaptability = 91,
Aftermath = 106, // TODO
AirLock = 76,
Analytic = 148, // TODO
AngerPoint = 83, // TODO
Anticipation = 107,
ArenaTrap = 71, // TODO
BadDreams = 123,
/// The Pokémon takes no critical hits.
BattleArmor = 4,
BigPecks = 145,
/// When the Pokémon has low HP, its Fire-type moves get a power boost.
Blaze = 66,
/// The Pokémon gets a speed boost in harsh sunlight.
Chlorophyll = 34,
ClearBody = 29,
CloudNine = 13,
ColorChange = 16,
/// The Pokémon accuracy is boosted.
Compoundeyes = 14,
Contrary = 126,
CursedBody = 130, // TODO
CuteCharm = 56,
Damp = 6, // TODO
Defeatist = 129,
Defiant = 128, // TODO
Download = 88,
/// The Pokémon changes the weather to infinite rain.
Drizzle = 2,
/// The Pokémon changes the weather to infinite harsh sunlight.
Drought = 70,
DrySkin = 87, // TODO
EarlyBird = 48,
EffectSpore = 27,
/// The Pokémon takes less damage from incoming super-effective moves.
Filter = 111,
FlameBody = 49,
FlareBoost = 138,
FlashFire = 18, // TODO
FlowerGift = 122,
Forecast = 59,
Forewarn = 108, // TODO (Also, does this activate when given/taken?)
FriendGuard = 132, // TODO
Frisk = 119, // TODO (Also, does this activate when given/taken?)
Gluttony = 82, // TODO
/// The Pokémon's attack is boosted when it is afflicted with a and the damage reduction from is not applied.
Guts = 62,
Harvest = 139, // TODO
Healer = 131,
/// The Pokémon takes less damage from Fire-type moves and from a burn.
Heatproof = 85,
HeavyMetal = 134, // TODO
/// No effect in battle.
HoneyGather = 118,
/// The Pokémon's attack is boosted.
HugePower = 37,
/// The Pokémon's attack is boosted, but its accuracy is lower for physical moves.
Hustle = 55,
Hydration = 93, // TODO
HyperCutter = 52,
/// In a hailstorm, the Pokémon takes no damage from the hailstorm and restores HP at the end of each turn.
IceBody = 115,
/// No effect in battle.
Illuminate = 35,
Illusion = 149,
Immunity = 17,
/// The Pokémon transforms into the foe across from it when switching in.
Imposter = 150,
Infiltrator = 151, // TODO: Mist
InnerFocus = 39,
Insomnia = 15,
Intimidate = 22,
IronBarbs = 160,
/// The power of moves with is increased.
IronFist = 89,
Justified = 154,
KeenEye = 51,
Klutz = 103, // TODO
LeafGuard = 102,
/// The Pokémon is immune to Ground-type moves and most entry hazards.
Levitate = 26,
LightMetal = 135, // TODO
Lightningrod = 31, // TODO
/// The Pokémon cannot be paralyzed.
Limber = 7,
LiquidOoze = 64,
MagicBounce = 156, // TODO
MagicGuard = 98, // TODO
MagmaArmor = 40,
MagnetPull = 42, // TODO
/// The Pokémon's defense is boosted when it is afflicted with a .
MarvelScale = 63,
Minus = 58,
MoldBreaker = 104,
Moody = 141,
MotorDrive = 78, // TODO
Moxie = 153, // TODO
Multiscale = 136, // TODO
/// No effect in battle.
Multitype = 121,
Mummy = 152,
NaturalCure = 30,
/// The Pokémon will always hit and always get hit unless protection is used.
NoGuard = 99,
Normalize = 96,
Oblivious = 12,
/// The Pokémon takes no damage from a hailstorm or sandstorm.
Overcoat = 142,
/// When the Pokémon has low HP, its Grass-type moves get a power boost.
Overgrow = 65,
OwnTempo = 20,
Pickpocket = 124, // TODO
Pickup = 53, // TODO
Plus = 57,
PoisonHeal = 90,
PoisonPoint = 38,
PoisonTouch = 143, // TODO
Prankster = 158,
Pressure = 46, // TODO (Also, does this activate when given/taken?)
/// The Pokémon's attack is boosted.
PurePower = 74,
QuickFeet = 95,
/// In rain, the Pokémon restores HP at the end of each turn.
RainDish = 44,
Rattled = 155,
Reckless = 120,
Regenerator = 144,
Rivalry = 79, // TODO
RockHead = 69,
RoughSkin = 24,
RunAway = 50,
/// In a sandstorm, the Pokémon takes no damage from the sandstorm and its Rock-, Ground-, and Steel-type moves get a power boost.
SandForce = 159,
/// The Pokémon gets a speed boost in a sandstorm.
SandRush = 146,
/// The Pokémon changes the weather to an infinite sandstorm.
SandStream = 45,
/// In a sandstorm, the Pokémon takes no damage from the sandstorm and gets a 20% evasion boost.
SandVeil = 8,
SapSipper = 157, // TODO
Scrappy = 113,
SereneGrace = 32,
ShadowTag = 23, // TODO
ShedSkin = 61,
SheerForce = 125, // TODO
/// The Pokémon takes no critical hits.
ShellArmor = 75,
ShieldDust = 19, // TODO
/// The Pokémon's stat changes are doubled.
Simple = 86,
SkillLink = 92, // TODO: Effect on Triple Kick
SlowStart = 112,
/// The Pokémon deals more damage when landing critical hits.
Sniper = 97,
/// In a hailstorm, the Pokémon takes no damage from the hailstorm and gets a 20% evasion boost.
SnowCloak = 81,
/// The Pokémon changes the weather to an infinite hailstorm.
SnowWarning = 117,
/// In harsh sunlight, the Pokémon gets a special attack boost and takes damage at the end of each turn.
SolarPower = 94,
/// The Pokémon takes less damage from incoming super-effective moves.
SolidRock = 116,
Soundproof = 43, // TODO
SpeedBoost = 3,
Stall = 100, // TODO
Static = 9,
Steadfast = 80,
Stench = 1, // TODO
StickyHold = 60, // TODO
StormDrain = 114, // TODO
Sturdy = 5,
SuctionCups = 21, // TODO
/// The Pokémon is more likely to land critical hits.
SuperLuck = 105,
/// When the Pokémon has low HP, its Bug-type moves get a power boost.
Swarm = 68,
/// The Pokémon gets a speed boost in rain.
SwiftSwim = 33,
Synchronize = 28, // TODO
TangledFeet = 77,
Technician = 101,
Telepathy = 140, // TODO
Teravolt = 164,
/// The Pokémon takes less damage from Ice- and Fire-type moves.
ThickFat = 47,
/// The Pokémon deals double damage with outgoing not-very-effective moves.
TintedLens = 110,
/// When the Pokémon has low HP, its Water-type moves get a power boost.
Torrent = 67,
ToxicBoost = 137,
Trace = 36, // TODO (Also, does this activate when given/taken?)
Truant = 54, // TODO
Turboblaze = 163,
Unaware = 109,
Unburden = 84, // TODO
Unnerve = 127, // TODO
VictoryStar = 162,
VitalSpirit = 72,
VoltAbsorb = 10, // TODO
WaterAbsorb = 11, // TODO
WaterVeil = 41,
WeakArmor = 133,
WhiteSmoke = 73,
/// The Pokémon is immune to all damaging moves except for moves that would deal super-effective damage.
WonderGuard = 25,
WonderSkin = 147,
ZenMode = 161, // TODO (Also, does this activate when given/taken?)
/// Invalid ability.
MAX = 165,
}
// Official IDs for the forms
#pragma warning disable CA1069 // Enums values should not be duplicated
public enum PBEForm : byte
{
Arceus = 0,
Arceus_Bug = 6,
Arceus_Dark = 16,
Arceus_Dragon = 15,
Arceus_Electric = 12,
Arceus_Fighting = 1,
Arceus_Fire = 9,
Arceus_Flying = 2,
Arceus_Ghost = 7,
Arceus_Grass = 11,
Arceus_Ground = 4,
Arceus_Ice = 14,
Arceus_Poison = 3,
Arceus_Psychic = 13,
Arceus_Rock = 5,
Arceus_Steel = 8,
Arceus_Water = 10,
Basculin_Blue = 1,
Basculin_Red = 0,
Burmy_Plant = 0,
Burmy_Sandy = 1,
Burmy_Trash = 2,
Castform = 0,
Castform_Rainy = 2,
Castform_Snowy = 3,
Castform_Sunny = 1,
Cherrim = 0,
Cherrim_Sunshine = 1,
Darmanitan = 0,
Darmanitan_Zen = 1,
Deerling_Autumn = 2,
Deerling_Spring = 0,
Deerling_Summer = 1,
Deerling_Winter = 3,
Deoxys = 0,
Deoxys_Attack = 1,
Deoxys_Defense = 2,
Deoxys_Speed = 3,
Gastrodon_East = 1,
Gastrodon_West = 0,
Genesect = 0,
Genesect_Burn = 3,
Genesect_Chill = 4,
Genesect_Douse = 1,
Genesect_Shock = 2,
Giratina = 0,
Giratina_Origin = 1,
Keldeo = 0,
Keldeo_Resolute = 1,
Kyurem = 0,
Kyurem_Black = 2,
Kyurem_White = 1,
Landorus = 0,
Landorus_Therian = 1,
Meloetta = 0,
Meloetta_Pirouette = 1,
Rotom = 0,
Rotom_Fan = 4,
Rotom_Frost = 3,
Rotom_Heat = 1,
Rotom_Mow = 5,
Rotom_Wash = 2,
Sawsbuck_Autumn = 2,
Sawsbuck_Spring = 0,
Sawsbuck_Summer = 1,
Sawsbuck_Winter = 3,
Shaymin = 0,
Shaymin_Sky = 1,
Shellos_East = 1,
Shellos_West = 0,
Thundurus = 0,
Thundurus_Therian = 1,
Tornadus = 0,
Tornadus_Therian = 1,
Unown_A = 0,
Unown_B = 1,
Unown_C = 2,
Unown_D = 3,
Unown_E = 4,
Unown_Exclamation = 26,
Unown_F = 5,
Unown_G = 6,
Unown_H = 7,
Unown_I = 8,
Unown_J = 9,
Unown_K = 10,
Unown_L = 11,
Unown_M = 12,
Unown_N = 13,
Unown_O = 14,
Unown_P = 15,
Unown_Q = 16,
Unown_Question = 27,
Unown_R = 17,
Unown_S = 18,
Unown_T = 19,
Unown_U = 20,
Unown_V = 21,
Unown_W = 22,
Unown_X = 23,
Unown_Y = 24,
Unown_Z = 25,
Wormadam_Plant = 0,
Wormadam_Sandy = 1,
Wormadam_Trash = 2
#pragma warning restore CA1069 // Enums values should not be duplicated
}
/// Represents a specific Pokémon species.
public enum PBESpecies : ushort
{
Abomasnow = 460,
Abra = 63,
Absol = 359,
Accelgor = 617,
Aerodactyl = 142,
Aggron = 306,
Aipom = 190,
Alakazam = 65,
Alomomola = 594,
Altaria = 334,
Ambipom = 424,
Amoonguss = 591,
Ampharos = 181,
Anorith = 347,
Arbok = 24,
Arcanine = 59,
Arceus = 493,
Archen = 566,
Archeops = 567,
Ariados = 168,
Armaldo = 348,
Aron = 304,
Articuno = 144,
Audino = 531,
Axew = 610,
Azelf = 482,
Azumarill = 184,
Azurill = 298,
Bagon = 371,
Baltoy = 343,
Banette = 354,
Barboach = 339,
Basculin = 550,
Bastiodon = 411,
Bayleef = 153,
Beartic = 614,
Beautifly = 267,
Beedrill = 15,
Beheeyem = 606,
Beldum = 374,
Bellossom = 182,
Bellsprout = 69,
Bibarel = 400,
Bidoof = 399,
Bisharp = 625,
Blastoise = 9,
Blaziken = 257,
Blissey = 242,
Blitzle = 522,
Boldore = 525,
Bonsly = 438,
Bouffalant = 626,
Braviary = 628,
Breloom = 286,
Bronzong = 437,
Bronzor = 436,
Budew = 406,
Buizel = 418,
Bulbasaur = 1,
Buneary = 427,
Burmy = 412,
Butterfree = 12,
Cacnea = 331,
Cacturne = 332,
Camerupt = 323,
Carnivine = 455,
Carracosta = 565,
Carvanha = 318,
Cascoon = 268,
Castform = 351,
Caterpie = 10,
Celebi = 251,
Chandelure = 609,
Chansey = 113,
Charizard = 6,
Charmander = 4,
Charmeleon = 5,
Chatot = 441,
Cherrim = 421,
Cherubi = 420,
Chikorita = 152,
Chimchar = 390,
Chimecho = 358,
Chinchou = 170,
Chingling = 433,
Cinccino = 573,
Clamperl = 366,
Claydol = 344,
Clefable = 36,
Clefairy = 35,
Cleffa = 173,
Cloyster = 91,
Cobalion = 638,
Cofagrigus = 563,
Combee = 415,
Combusken = 256,
Conkeldurr = 534,
Corphish = 341,
Corsola = 222,
Cottonee = 546,
Cradily = 346,
Cranidos = 408,
Crawdaunt = 342,
Cresselia = 488,
Croagunk = 453,
Crobat = 169,
Croconaw = 159,
Crustle = 558,
Cryogonal = 615,
Cubchoo = 613,
Cubone = 104,
Cyndaquil = 155,
Darkrai = 491,
Darmanitan = 555,
Darumaka = 554,
Deerling = 585,
Deino = 633,
Delcatty = 301,
Delibird = 225,
Deoxys = 386,
Dewgong = 87,
Dewott = 502,
Dialga = 483,
Diglett = 50,
Ditto = 132,
Dodrio = 85,
Doduo = 84,
Donphan = 232,
Dragonair = 148,
Dragonite = 149,
Drapion = 452,
Dratini = 147,
Drifblim = 426,
Drifloon = 425,
Drilbur = 529,
Drowzee = 96,
Druddigon = 621,
Ducklett = 580,
Dugtrio = 51,
Dunsparce = 206,
Duosion = 578,
Durant = 632,
Dusclops = 356,
Dusknoir = 477,
Duskull = 355,
Dustox = 269,
Dwebble = 557,
Eelektrik = 603,
Eelektross = 604,
Eevee = 133,
Ekans = 23,
Electabuzz = 125,
Electivire = 466,
Electrike = 309,
Electrode = 101,
Elekid = 239,
Elgyem = 605,
Emboar = 500,
Emolga = 587,
Empoleon = 395,
Entei = 244,
Escavalier = 589,
Espeon = 196,
Excadrill = 530,
Exeggcute = 102,
Exeggutor = 103,
Exploud = 295,
Farfetchd = 83,
Fearow = 22,
Feebas = 349,
Feraligatr = 160,
Ferroseed = 597,
Ferrothorn = 598,
Finneon = 456,
Flaaffy = 180,
Flareon = 136,
Floatzel = 419,
Flygon = 330,
Foongus = 590,
Forretress = 205,
Fraxure = 611,
Frillish = 592,
Froslass = 478,
Furret = 162,
Gabite = 444,
Gallade = 475,
Galvantula = 596,
Garbodor = 569,
Garchomp = 445,
Gardevoir = 282,
Gastly = 92,
Gastrodon = 423,
Genesect = 649,
Gengar = 94,
Geodude = 74,
Gible = 443,
Gigalith = 526,
Girafarig = 203,
Giratina = 487,
Glaceon = 471,
Glalie = 362,
Glameow = 431,
Gligar = 207,
Gliscor = 472,
Gloom = 44,
Golbat = 42,
Goldeen = 118,
Golduck = 55,
Golem = 76,
Golett = 622,
Golurk = 623,
Gorebyss = 368,
Gothita = 574,
Gothitelle = 576,
Gothorita = 575,
Granbull = 210,
Graveler = 75,
Grimer = 88,
Grotle = 388,
Groudon = 383,
Grovyle = 253,
Growlithe = 58,
Grumpig = 326,
Gulpin = 316,
Gurdurr = 533,
Gyarados = 130,
Happiny = 440,
Hariyama = 297,
Haunter = 93,
Haxorus = 612,
Heatmor = 631,
Heatran = 485,
Heracross = 214,
Herdier = 507,
Hippopotas = 449,
Hippowdon = 450,
Hitmonchan = 107,
Hitmonlee = 106,
Hitmontop = 237,
HoOh = 250,
Honchkrow = 430,
Hoothoot = 163,
Hoppip = 187,
Horsea = 116,
Houndoom = 229,
Houndour = 228,
Huntail = 367,
Hydreigon = 635,
Hypno = 97,
Igglybuff = 174,
Illumise = 314,
Infernape = 392,
Ivysaur = 2,
Jellicent = 593,
Jigglypuff = 39,
Jirachi = 385,
Jolteon = 135,
Joltik = 595,
Jumpluff = 189,
Jynx = 124,
Kabuto = 140,
Kabutops = 141,
Kadabra = 64,
Kakuna = 14,
Kangaskhan = 115,
Karrablast = 588,
Kecleon = 352,
Keldeo = 647,
Kingdra = 230,
Kingler = 99,
Kirlia = 281,
Klang = 600,
Klink = 599,
Klinklang = 601,
Koffing = 109,
Krabby = 98,
Kricketot = 401,
Kricketune = 402,
Krokorok = 552,
Krookodile = 553,
Kyogre = 382,
Kyurem = 646,
Lairon = 305,
Lampent = 608,
Landorus = 645,
Lanturn = 171,
Lapras = 131,
Larvesta = 636,
Larvitar = 246,
Latias = 380,
Latios = 381,
Leafeon = 470,
Leavanny = 542,
Ledian = 166,
Ledyba = 165,
Lickilicky = 463,
Lickitung = 108,
Liepard = 510,
Lileep = 345,
Lilligant = 549,
Lillipup = 506,
Linoone = 264,
Litwick = 607,
Lombre = 271,
Lopunny = 428,
Lotad = 270,
Loudred = 294,
Lucario = 448,
Ludicolo = 272,
Lugia = 249,
Lumineon = 457,
Lunatone = 337,
Luvdisc = 370,
Luxio = 404,
Luxray = 405,
Machamp = 68,
Machoke = 67,
Machop = 66,
Magby = 240,
Magcargo = 219,
Magikarp = 129,
Magmar = 126,
Magmortar = 467,
Magnemite = 81,
Magneton = 82,
Magnezone = 462,
Makuhita = 296,
Mamoswine = 473,
Manaphy = 490,
Mandibuzz = 630,
Manectric = 310,
Mankey = 56,
Mantine = 226,
Mantyke = 458,
Maractus = 556,
Mareep = 179,
Marill = 183,
Marowak = 105,
Marshtomp = 259,
Masquerain = 284,
Mawile = 303,
Medicham = 308,
Meditite = 307,
Meganium = 154,
Meloetta = 648,
Meowth = 52,
Mesprit = 481,
Metagross = 376,
Metang = 375,
Metapod = 11,
Mew = 151,
Mewtwo = 150,
Mienfoo = 619,
Mienshao = 620,
Mightyena = 262,
Milotic = 350,
Miltank = 241,
MimeJr = 439,
Minccino = 572,
Minun = 312,
Misdreavus = 200,
Mismagius = 429,
Moltres = 146,
Monferno = 391,
Mothim = 414,
MrMime = 122,
Mudkip = 258,
Muk = 89,
Munchlax = 446,
Munna = 517,
Murkrow = 198,
Musharna = 518,
Natu = 177,
Nidoking = 34,
Nidoqueen = 31,
Nidoran_F = 29,
Nidoran_M = 32,
Nidorina = 30,
Nidorino = 33,
Nincada = 290,
Ninetales = 38,
Ninjask = 291,
Noctowl = 164,
Nosepass = 299,
Numel = 322,
Nuzleaf = 274,
Octillery = 224,
Oddish = 43,
Omanyte = 138,
Omastar = 139,
Onix = 95,
Oshawott = 501,
Pachirisu = 417,
Palkia = 484,
Palpitoad = 536,
Panpour = 515,
Pansage = 511,
Pansear = 513,
Paras = 46,
Parasect = 47,
Patrat = 504,
Pawniard = 624,
Pelipper = 279,
Persian = 53,
Petilil = 548,
Phanpy = 231,
Phione = 489,
Pichu = 172,
Pidgeot = 18,
Pidgeotto = 17,
Pidgey = 16,
Pidove = 519,
Pignite = 499,
Pikachu = 25,
Piloswine = 221,
Pineco = 204,
Pinsir = 127,
Piplup = 393,
Plusle = 311,
Politoed = 186,
Poliwag = 60,
Poliwhirl = 61,
Poliwrath = 62,
Ponyta = 77,
Poochyena = 261,
Porygon = 137,
Porygon2 = 233,
PorygonZ = 474,
Primeape = 57,
Prinplup = 394,
Probopass = 476,
Psyduck = 54,
Pupitar = 247,
Purrloin = 509,
Purugly = 432,
Quagsire = 195,
Quilava = 156,
Qwilfish = 211,
Raichu = 26,
Raikou = 243,
Ralts = 280,
Rampardos = 409,
Rapidash = 78,
Raticate = 20,
Rattata = 19,
Rayquaza = 384,
Regice = 378,
Regigigas = 486,
Regirock = 377,
Registeel = 379,
Relicanth = 369,
Remoraid = 223,
Reshiram = 643,
Reuniclus = 579,
Rhydon = 112,
Rhyhorn = 111,
Rhyperior = 464,
Riolu = 447,
Roggenrola = 524,
Roselia = 315,
Roserade = 407,
Rotom = 479,
Rufflet = 627,
Sableye = 302,
Salamence = 373,
Samurott = 503,
Sandile = 551,
Sandshrew = 27,
Sandslash = 28,
Sawk = 539,
Sawsbuck = 586,
Sceptile = 254,
Scizor = 212,
Scolipede = 545,
Scrafty = 560,
Scraggy = 559,
Scyther = 123,
Seadra = 117,
Seaking = 119,
Sealeo = 364,
Seedot = 273,
Seel = 86,
Seismitoad = 537,
Sentret = 161,
Serperior = 497,
Servine = 496,
Seviper = 336,
Sewaddle = 540,
Sharpedo = 319,
Shaymin = 492,
Shedinja = 292,
Shelgon = 372,
Shellder = 90,
Shellos = 422,
Shelmet = 616,
Shieldon = 410,
Shiftry = 275,
Shinx = 403,
Shroomish = 285,
Shuckle = 213,
Shuppet = 353,
Sigilyph = 561,
Silcoon = 266,
Simipour = 516,
Simisage = 512,
Simisear = 514,
Skarmory = 227,
Skiploom = 188,
Skitty = 300,
Skorupi = 451,
Skuntank = 435,
Slaking = 289,
Slakoth = 287,
Slowbro = 80,
Slowking = 199,
Slowpoke = 79,
Slugma = 218,
Smeargle = 235,
Smoochum = 238,
Sneasel = 215,
Snivy = 495,
Snorlax = 143,
Snorunt = 361,
Snover = 459,
Snubbull = 209,
Solosis = 577,
Solrock = 338,
Spearow = 21,
Spheal = 363,
Spinarak = 167,
Spinda = 327,
Spiritomb = 442,
Spoink = 325,
Squirtle = 7,
Stantler = 234,
Staraptor = 398,
Staravia = 397,
Starly = 396,
Starmie = 121,
Staryu = 120,
Steelix = 208,
Stoutland = 508,
Stunfisk = 618,
Stunky = 434,
Sudowoodo = 185,
Suicune = 245,
Sunflora = 192,
Sunkern = 191,
Surskit = 283,
Swablu = 333,
Swadloon = 541,
Swalot = 317,
Swampert = 260,
Swanna = 581,
Swellow = 277,
Swinub = 220,
Swoobat = 528,
Taillow = 276,
Tangela = 114,
Tangrowth = 465,
Tauros = 128,
Teddiursa = 216,
Tentacool = 72,
Tentacruel = 73,
Tepig = 498,
Terrakion = 639,
Throh = 538,
Thundurus = 642,
Timburr = 532,
Tirtouga = 564,
Togekiss = 468,
Togepi = 175,
Togetic = 176,
Torchic = 255,
Torkoal = 324,
Tornadus = 641,
Torterra = 389,
Totodile = 158,
Toxicroak = 454,
Tranquill = 520,
Trapinch = 328,
Treecko = 252,
Tropius = 357,
Trubbish = 568,
Turtwig = 387,
Tympole = 535,
Tynamo = 602,
Typhlosion = 157,
Tyranitar = 248,
Tyrogue = 236,
Umbreon = 197,
Unfezant = 521,
Unown = 201,
Ursaring = 217,
Uxie = 480,
Vanillish = 583,
Vanillite = 582,
Vanilluxe = 584,
Vaporeon = 134,
Venipede = 543,
Venomoth = 49,
Venonat = 48,
Venusaur = 3,
Vespiquen = 416,
Vibrava = 329,
Victini = 494,
Victreebel = 71,
Vigoroth = 288,
Vileplume = 45,
Virizion = 640,
Volbeat = 313,
Volcarona = 637,
Voltorb = 100,
Vullaby = 629,
Vulpix = 37,
Wailmer = 320,
Wailord = 321,
Walrein = 365,
Wartortle = 8,
Watchog = 505,
Weavile = 461,
Weedle = 13,
Weepinbell = 70,
Weezing = 110,
Whimsicott = 547,
Whirlipede = 544,
Whiscash = 340,
Whismur = 293,
Wigglytuff = 40,
Wingull = 278,
Wobbuffet = 202,
Woobat = 527,
Wooper = 194,
Wormadam = 413,
Wurmple = 265,
Wynaut = 360,
Xatu = 178,
Yamask = 562,
Yanma = 193,
Yanmega = 469,
Zangoose = 335,
Zapdos = 145,
Zebstrika = 523,
Zekrom = 644,
Zigzagoon = 263,
Zoroark = 571,
Zorua = 570,
Zubat = 41,
Zweilous = 634,
MAX = 650
}
public enum PBEMoveTarget : byte
{
All, // Every battler (Ex. Perish Song)
AllFoes, // Every foe (Ex. Stealth Rock)
AllFoesSurrounding, // All foes surrounding (Ex. Growl)
AllSurrounding, // All battlers surrounding (Ex. Earthquake)
AllTeam, // User's entire team (Ex. Light Screen)
RandomFoeSurrounding, // Randomly picks a surrounding foe (Ex. Outrage)
Self, // Self (Ex. Growth)
SelfOrAllySurrounding, // Self or adjacent ally (Ex. Acupressure)
SingleAllySurrounding, // Adjacent ally (Ex. Helping Hand)
SingleFoeSurrounding, // Single foe surrounding (Ex. Me First)
SingleNotSelf, // Single battler except itself (Ex. Dark Pulse)
SingleSurrounding, // Single battler surrounding (Ex. Tackle)
Varies // Possible targets vary (Ex. Curse)
}
/// Represents a specific 's flags.
[Flags]
public enum PBEMoveFlag : ulong
{
/// The move has no flags.
None,
/// The move's power is boosted by .
AffectedByIronFist = 1 << 0,
AffectedByMagicCoat = 1 << 1,
AffectedByMirrorMove = 1 << 2,
AffectedByProtect = 1 << 3,
/// The move's power is boosted by .
AffectedByReckless = 1 << 4,
AffectedBySnatch = 1 << 5,
/// The move is blocked by .
AffectedBySoundproof = 1 << 6,
/// The move always lands a critical hit.
AlwaysCrit = 1 << 7,
BlockedByGravity = 1 << 8,
BlockedFromAssist = 1 << 9,
BlockedFromCopycat = 1 << 10,
BlockedFromMeFirst = 1 << 12,
BlockedFromMetronome = 1 << 13,
BlockedFromMimic = 1 << 14,
BlockedFromSketch = 1 << 15,
BlockedFromSketchWhenSuccessful = 1 << 16,
BlockedFromSleepTalk = 1 << 17,
/// The move removes from the user.
DefrostsUser = 1 << 18,
DoubleDamageAirborne = 1 << 19,
DoubleDamageMinimized = 1 << 20,
DoubleDamageUnderground = 1 << 21,
DoubleDamageUnderwater = 1 << 22,
DoubleDamageUserDefenseCurl = 1 << 23,
/// The move has a higher chance of landing a critical hit.
HighCritChance = 1 << 24,
/// The move can hit targets.
HitsAirborne = 1 << 25,
/// The move can hit targets.
HitsUnderground = 1 << 26,
/// The move can hit targets.
HitsUnderwater = 1 << 27,
/// The user makes contact with the target, causing it to take damage from the target's , , and .
MakesContact = 1 << 28,
NeverMissHail = 1 << 29,
NeverMissRain = 1 << 30,
UnaffectedByGems = 1uL << 31 // TODO
}
public enum PBEMoveEffect : byte
{
Acrobatics,
Attract,
BellyDrum,
Bounce,
BrickBreak,
Brine,
Burn,
Camouflage,
ChangeTarget_ACC,
ChangeTarget_ATK,
ChangeTarget_DEF,
ChangeTarget_EVA,
ChangeTarget_SPATK,
ChangeTarget_SPATK__IfAttractionPossible,
ChangeTarget_SPDEF,
ChangeTarget_SPE,
ChipAway,
Confuse,
Conversion,
CrushGrip,
Curse,
Dig,
Dive,
Endeavor,
Entrainment,
Eruption,
Facade,
Feint,
FinalGambit,
Flatter,
Flail,
Fly,
FocusEnergy,
Foresight,
FoulPlay,
Frustration,
GastroAcid,
GrassKnot,
Growth,
Hail,
Haze,
HeatCrash,
HelpingHand,
Hex,
HiddenPower,
Hit,
Hit__2Times,
Hit__2Times__MaybePoison,
Hit__2To5Times,
Hit__MaybeBurn,
Hit__MaybeBurn__10PercentFlinch,
Hit__MaybeBurnFreezeParalyze,
Hit__MaybeConfuse,
Hit__MaybeFlinch,
Hit__MaybeFreeze,
Hit__MaybeFreeze__10PercentFlinch,
Hit__MaybeLowerTarget_ACC_By1,
Hit__MaybeLowerTarget_ATK_By1,
Hit__MaybeLowerTarget_DEF_By1,
Hit__MaybeLowerTarget_SPATK_By1,
Hit__MaybeLowerTarget_SPDEF_By1,
Hit__MaybeLowerTarget_SPDEF_By2,
Hit__MaybeLowerTarget_SPE_By1,
Hit__MaybeLowerUser_ATK_DEF_By1,
Hit__MaybeLowerUser_DEF_SPDEF_By1,
Hit__MaybeLowerUser_SPATK_By2,
Hit__MaybeLowerUser_SPE_By1,
Hit__MaybeLowerUser_SPE_DEF_SPDEF_By1,
Hit__MaybeParalyze,
Hit__MaybeParalyze__10PercentFlinch,
Hit__MaybePoison,
Hit__MaybeRaiseUser_ATK_By1,
Hit__MaybeRaiseUser_ATK_DEF_SPATK_SPDEF_SPE_By1,
Hit__MaybeRaiseUser_DEF_By1,
Hit__MaybeRaiseUser_SPATK_By1,
Hit__MaybeRaiseUser_SPE_By1,
Hit__MaybeToxic,
HPDrain,
HPDrain__RequireSleep,
Judgment,
LeechSeed,
LightScreen,
LockOn,
LowerTarget_ATK_DEF_By1,
LowerTarget_DEF_SPDEF_By1_Raise_ATK_SPATK_SPE_By2,
LuckyChant,
MagnetRise,
Magnitude,
Metronome,
Minimize,
MiracleEye,
Moonlight,
Nightmare,
Nothing,
OneHitKnockout,
PainSplit,
Paralyze,
Payback,
PayDay,
Poison,
PowerTrick,
Protect, // TODO: If the user goes last, fail
PsychUp,
Psyshock,
Psywave,
Punishment,
QuickGuard,
RainDance,
RaiseTarget_ATK_ACC_By1,
RaiseTarget_ATK_DEF_By1,
RaiseTarget_ATK_DEF_ACC_By1,
RaiseTarget_ATK_SPATK_By1,
RaiseTarget_ATK_SPE_By1,
RaiseTarget_DEF_SPDEF_By1,
RaiseTarget_SPATK_SPDEF_By1,
RaiseTarget_SPATK_SPDEF_SPE_By1,
RaiseTarget_SPE_By2_ATK_By1,
Recoil,
Recoil__10PercentBurn,
Recoil__10PercentParalyze,
Reflect,
ReflectType,
Refresh,
Rest,
RestoreTargetHP,
Retaliate,
Return,
RolePlay,
Roost,
Safeguard,
Sandstorm,
SecretPower,
SeismicToss,
Selfdestruct,
SetDamage,
ShadowForce,
SimpleBeam,
Sketch, // TODO
Sleep,
SmellingSalt,
Snore,
Soak,
Spikes,
StealthRock,
StoredPower,
Struggle,
Substitute,
SuckerPunch,
SunnyDay,
SuperFang,
Swagger,
Tailwind,
TechnoBlast,
Teleport, // TODO: Trapping effects & SmokeBall
ThunderWave,
Toxic,
ToxicSpikes,
Transform,
TrickRoom,
Venoshock,
WakeUpSlap,
WeatherBall,
Whirlwind, // TODO: Trapping effects
WideGuard,
WorrySeed,
TODOMOVE // Moves that are not added yet
}
public enum PBEMove : ushort
{
None = 0,
Absorb = 71,
Acid = 51,
AcidArmor = 151,
AcidSpray = 491,
Acrobatics = 512,
Acupressure = 367, // TODO
AerialAce = 332,
Aeroblast = 177,
AfterYou = 495, // TODO
Agility = 97,
AirCutter = 314,
AirSlash = 403,
AllySwitch = 502, // TODO
Amnesia = 133,
AncientPower = 246,
AquaJet = 453,
AquaRing = 392, // TODO
AquaTail = 401,
ArmThrust = 292,
Aromatherapy = 312, // TODO
Assist = 274, // TODO
Assurance = 372, // TODO
Astonish = 310,
AttackOrder = 454,
Attract = 213,
AuraSphere = 396,
AuroraBeam = 62,
Autotomize = 475, // TODO
Avalanche = 419, // TODO
Barrage = 140,
Barrier = 112,
BatonPass = 226, // TODO
BeatUp = 251, // TODO
BellyDrum = 187,
Bestow = 516, // TODO
Bide = 117, // TODO
Bind = 20, // TODO
Bite = 44,
BlastBurn = 307, // TODO
BlazeKick = 299,
Blizzard = 59,
Block = 335, // TODO
BlueFlare = 551,
BodySlam = 34,
BoltStrike = 550,
BoneClub = 125,
Bonemerang = 155,
BoneRush = 198,
Bounce = 340,
BraveBird = 413,
BrickBreak = 280,
Brine = 362,
Bubble = 145,
BubbleBeam = 61,
BugBite = 450, // TODO
BugBuzz = 405,
BulkUp = 339,
Bulldoze = 523,
BulletPunch = 418,
BulletSeed = 331,
CalmMind = 347,
Camouflage = 293,
Captivate = 445,
Charge = 268, // TODO
ChargeBeam = 451,
Charm = 204,
Chatter = 448,
ChipAway = 498,
CircleThrow = 509, // TODO
Clamp = 128, // TODO
ClearSmog = 499, // TODO
CloseCombat = 370,
Coil = 489,
CometPunch = 4,
ConfuseRay = 109,
Confusion = 93,
Constrict = 132,
Conversion = 160,
Conversion2 = 176, // TODO
Copycat = 383, // TODO
CosmicPower = 322,
CottonGuard = 538,
CottonSpore = 178,
Counter = 68, // TODO
Covet = 343, // TODO
Crabhammer = 152,
CrossChop = 238,
CrossPoison = 440,
Crunch = 242,
CrushClaw = 306,
CrushGrip = 462,
Curse = 174,
Cut = 15,
DarkPulse = 399,
DarkVoid = 464,
DefendOrder = 455,
DefenseCurl = 111,
Defog = 432, // TODO
DestinyBond = 194, // TODO
Detect = 197,
Dig = 91,
Disable = 50, // TODO
Discharge = 435,
Dive = 291,
DizzyPunch = 146,
DoomDesire = 353, // TODO
DoubleEdge = 38,
DoubleHit = 458,
DoubleKick = 24,
DoubleSlap = 3,
DoubleTeam = 104,
DracoMeteor = 434,
DragonBreath = 225,
DragonClaw = 337,
DragonDance = 349,
DragonPulse = 406,
DragonRage = 82,
DragonRush = 407,
DragonTail = 525, // TODO
DrainPunch = 409,
DreamEater = 138,
DrillPeck = 65,
DrillRun = 529,
DualChop = 530,
DynamicPunch = 223,
EarthPower = 414,
Earthquake = 89,
EchoedVoice = 497, // TODO
EggBomb = 121,
ElectroBall = 486, // TODO
Electroweb = 527,
Embargo = 373, // TODO
Ember = 52,
Encore = 227, // TODO
Endeavor = 283,
Endure = 203, // TODO
EnergyBall = 412,
Entrainment = 494,
Eruption = 284,
Explosion = 153,
Extrasensory = 326,
ExtremeSpeed = 245,
Facade = 263,
FaintAttack = 185,
FakeOut = 252, // TODO
FakeTears = 313,
FalseSwipe = 206, // TODO
FeatherDance = 297,
Feint = 364,
FieryDance = 552,
FinalGambit = 515,
FireBlast = 126,
FireFang = 424,
FirePledge = 519, // TODO
FirePunch = 7,
FireSpin = 83, // TODO
Fissure = 90,
Flail = 175,
FlameBurst = 481, // TODO
FlameCharge = 488,
Flamethrower = 53,
FlameWheel = 172,
FlareBlitz = 394,
Flash = 148,
FlashCannon = 430,
Flatter = 260,
Fling = 374, // TODO
Fly = 19,
FocusBlast = 411,
FocusEnergy = 116,
FocusPunch = 264, // TODO
FollowMe = 266, // TODO
ForcePalm = 395,
Foresight = 193,
FoulPlay = 492,
FreezeShock = 553, // TODO
FrenzyPlant = 338, // TODO
FrostBreath = 524,
Frustration = 218,
FuryAttack = 31,
FuryCutter = 210, // TODO
FurySwipes = 154,
FusionBolt = 559, // TODO
FusionFlare = 558, // TODO
FutureSight = 248, // TODO
GastroAcid = 380, // TODO: Magic Bounce, Magic Coat
GearGrind = 544,
GigaDrain = 202,
GigaImpact = 416, // TODO
Glaciate = 549,
Glare = 137,
GrassKnot = 447,
GrassPledge = 520, // TODO
GrassWhistle = 320,
Gravity = 356, // TODO
Growl = 45,
Growth = 74,
Grudge = 288, // TODO
GuardSplit = 470, // TODO
GuardSwap = 385, // TODO
Guillotine = 12,
GunkShot = 441,
Gust = 16,
GyroBall = 360, // TODO
Hail = 258,
HammerArm = 359,
Harden = 106,
Haze = 114,
Headbutt = 29,
HeadCharge = 543,
HeadSmash = 457,
HealBell = 215, // TODO
HealBlock = 377, // TODO
HealingWish = 361, // TODO
HealOrder = 456,
HealPulse = 505,
HeartStamp = 531,
HeartSwap = 391, // TODO
HeatCrash = 535,
HeatWave = 257,
HeavySlam = 484,
HelpingHand = 270,
Hex = 506,
HiddenPower = 237,
HiJumpKick = 136, // TODO
HoneClaws = 468,
HornAttack = 30,
HornDrill = 32,
HornLeech = 532,
Howl = 336,
Hurricane = 542,
HydroCannon = 308, // TODO
HydroPump = 56,
HyperBeam = 63, // TODO
HyperFang = 158,
HyperVoice = 304,
Hypnosis = 95,
IceBall = 301, // TODO
IceBeam = 58,
IceBurn = 554, // TODO
IceFang = 423,
IcePunch = 8,
IceShard = 420,
IcicleCrash = 556,
IcicleSpear = 333,
IcyWind = 196,
Imprison = 286, // TODO
Incinerate = 510, // TODO
Inferno = 517,
Ingrain = 275, // TODO
IronDefense = 334,
IronHead = 442,
IronTail = 231,
Judgment = 449,
JumpKick = 26, // TODO
KarateChop = 2,
Kinesis = 134,
KnockOff = 282, // TODO
LastResort = 387, // TODO
LavaPlume = 436,
LeafBlade = 348,
LeafStorm = 437,
LeafTornado = 536,
LeechLife = 141,
LeechSeed = 73,
Leer = 43,
Lick = 122,
LightScreen = 113,
LockOn = 199,
LovelyKiss = 142,
LowKick = 67,
LowSweep = 490,
LuckyChant = 381,
LunarDance = 461, // TODO
LusterPurge = 295,
MachPunch = 183,
MagicalLeaf = 345,
MagicCoat = 277, // TODO
MagicRoom = 478, // TODO
MagmaStorm = 463, // TODO
MagnetBomb = 443,
MagnetRise = 393,
Magnitude = 222,
MeanLook = 212, // TODO
Meditate = 96,
MeFirst = 382, // TODO & TODO: Sucker Punch
MegaDrain = 72,
Megahorn = 224,
MegaKick = 25,
MegaPunch = 5,
Memento = 262, // TODO
MetalBurst = 368, // TODO
MetalClaw = 232,
MetalSound = 319,
MeteorMash = 309,
Metronome = 118,
MilkDrink = 208,
Mimic = 102, // TODO
MindReader = 170,
Minimize = 107,
MiracleEye = 357,
MirrorCoat = 243, // TODO
MirrorMove = 119, // TODO
MirrorShot = 429,
Mist = 54, // TODO & TODO: Infiltrator
MistBall = 296,
Moonlight = 236,
MorningSun = 234,
MudBomb = 426,
MuddyWater = 330,
MudShot = 341,
MudSlap = 189,
MudSport = 300, // TODO
NastyPlot = 417,
NaturalGift = 363, // TODO
NaturePower = 267, // TODO
NeedleArm = 302,
NightDaze = 539,
Nightmare = 171,
NightShade = 101,
NightSlash = 400,
Octazooka = 190,
OdorSleuth = 316,
OminousWind = 466,
Outrage = 200, // TODO
Overheat = 315,
PainSplit = 220,
Payback = 371, // TODO: If the target used an item instead of a move
PayDay = 6,
Peck = 64,
PerishSong = 195, // TODO
PetalDance = 80, // TODO
PinMissile = 42,
Pluck = 365, // TODO
PoisonFang = 305,
PoisonGas = 139,
PoisonJab = 398,
PoisonPowder = 77,
PoisonSting = 40,
PoisonTail = 342,
Pound = 1,
PowderSnow = 181,
PowerGem = 408,
PowerSplit = 471, // TODO
PowerSwap = 384, // TODO
PowerTrick = 379,
PowerWhip = 438,
Present = 217, // TODO
Protect = 182,
Psybeam = 60,
Psychic = 94,
PsychoBoost = 354,
PsychoCut = 427,
PsychoShift = 375, // TODO
PsychUp = 244,
Psyshock = 473,
Psystrike = 540,
Psywave = 149,
Punishment = 386,
Pursuit = 228, // TODO
Quash = 511, // TODO
QuickAttack = 98,
QuickGuard = 501,
QuiverDance = 483,
Rage = 99, // TODO
RagePowder = 476, // TODO
RainDance = 240,
RapidSpin = 229, // TODO
RazorLeaf = 75,
RazorShell = 534,
RazorWind = 13, // TODO
Recover = 105,
Recycle = 278, // TODO
Reflect = 115,
ReflectType = 513,
Refresh = 287,
RelicSong = 547, // TODO
Rest = 156, // TODO: Uproar, Leaf Guard
Retaliate = 514,
Return = 216,
Revenge = 279, // TODO
Reversal = 179,
Roar = 46, // TODO: Suction Cups, Soundproof, Ingrain
RoarOfTime = 459, // TODO
RockBlast = 350,
RockClimb = 431,
RockPolish = 397,
RockSlide = 157,
RockSmash = 249,
RockThrow = 88,
RockTomb = 317,
RockWrecker = 439, // TODO
RolePlay = 272,
RollingKick = 27,
Rollout = 205, // TODO
Roost = 355,
Round = 496, // TODO
SacredFire = 221,
SacredSword = 533,
Safeguard = 219,
SandAttack = 28,
Sandstorm = 201,
SandTomb = 328, // TODO
Scald = 503,
ScaryFace = 184,
Scratch = 10,
Screech = 103,
SearingShot = 545,
SecretPower = 290,
SecretSword = 548,
SeedBomb = 402,
SeedFlare = 465,
SeismicToss = 69,
Selfdestruct = 120,
ShadowBall = 247,
ShadowClaw = 421,
ShadowForce = 467,
ShadowPunch = 325,
ShadowSneak = 425,
Sharpen = 159,
SheerCold = 329,
ShellSmash = 504,
ShiftGear = 508,
ShockWave = 351,
SignalBeam = 324,
SilverWind = 318,
SimpleBeam = 493,
Sing = 47,
Sketch = 166, // TODO
SkillSwap = 285, // TODO
SkullBash = 130, // TODO
SkyAttack = 143, // TODO
SkyDrop = 507, // TODO
SkyUppercut = 327,
SlackOff = 303,
Slam = 21,
Slash = 163,
SleepPowder = 79,
SleepTalk = 214, // TODO & TODO: Moves such as ice ball/outrage that last multiple turns are not locked, even upon waking up (is this the same for metronome etc?)
Sludge = 124,
SludgeBomb = 188,
SludgeWave = 482,
SmackDown = 479, // TODO
SmellingSalt = 265,
Smog = 123,
SmokeScreen = 108,
Snarl = 555,
Snatch = 289, // TODO
Snore = 173,
Soak = 487,
Softboiled = 135,
SolarBeam = 76, // TODO
SonicBoom = 49,
SpacialRend = 460,
Spark = 209,
SpiderWeb = 169, // TODO
SpikeCannon = 131,
Spikes = 191,
Spite = 180, // TODO
SpitUp = 255, // TODO
Splash = 150,
Spore = 147,
StealthRock = 446,
Steamroller = 537,
SteelWing = 211,
Stockpile = 254, // TODO
Stomp = 23,
StoneEdge = 444,
StoredPower = 500,
StormThrow = 480,
Strength = 70,
StringShot = 81,
Struggle = 165,
StruggleBug = 522,
StunSpore = 78,
Submission = 66,
Substitute = 164,
SuckerPunch = 389,
SunnyDay = 241,
SuperFang = 162,
Superpower = 276,
Supersonic = 48,
Surf = 57,
Swagger = 207,
Swallow = 256, // TODO
SweetKiss = 186,
SweetScent = 230,
Swift = 129,
Switcheroo = 415, // TODO
SwordsDance = 14,
Synchronoise = 485, // TODO
Synthesis = 235,
Tackle = 33,
TailGlow = 294,
TailSlap = 541,
TailWhip = 39,
Tailwind = 366,
TakeDown = 36,
Taunt = 269, // TODO
TechnoBlast = 546,
TeeterDance = 298,
Telekinesis = 477, // TODO
Teleport = 100,
Thief = 168, // TODO
Thrash = 37, // TODO
Thunder = 87,
Thunderbolt = 85,
ThunderFang = 422,
ThunderPunch = 9,
ThunderShock = 84,
ThunderWave = 86,
Tickle = 321,
Torment = 259, // TODO
Toxic = 92,
ToxicSpikes = 390,
Transform = 144,
TriAttack = 161,
Trick = 271, // TODO
TrickRoom = 433,
TripleKick = 167, // TODO
TrumpCard = 376, // TODO
Twineedle = 41,
Twister = 239,
Uproar = 253, // TODO
Uturn = 369, // TODO
VacuumWave = 410,
VCreate = 557,
Venoshock = 474,
ViceGrip = 11,
VineWhip = 22,
VitalThrow = 233,
VoltSwitch = 521, // TODO
VoltTackle = 344,
WakeUpSlap = 358,
Waterfall = 127,
WaterGun = 55,
WaterPledge = 518, // TODO
WaterPulse = 352,
WaterSport = 346, // TODO
WaterSpout = 323,
WeatherBall = 311,
Whirlpool = 250, // TODO
Whirlwind = 18, // TODO: Suction Cups, Ingrain
WideGuard = 469,
WildCharge = 528,
WillOWisp = 261,
WingAttack = 17,
Wish = 273, // TODO
Withdraw = 110,
WonderRoom = 472, // TODO
WoodHammer = 452,
WorkUp = 526,
WorrySeed = 388,
Wrap = 35, // TODO
WringOut = 378,
XScissor = 404,
Yawn = 281, // TODO
ZapCannon = 192,
ZenHeadbutt = 428,
/// Invalid move.
MAX = 560
}
================================================
FILE: PokemonBattleEngine/Data/DataProvider.cs
================================================
using Kermalis.PokemonBattleEngine.Battle;
using Kermalis.PokemonBattleEngine.Data.Legality;
using Kermalis.PokemonBattleEngine.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Kermalis.PokemonBattleEngine.Data;
public abstract class PBEDataProvider
{
public static PBEDataProvider Instance { get; private set; } = null!;
public static PBELanguage GlobalLanguage { get; private set; } = default;
public static PBERandom GlobalRandom { get; private set; } = null!;
protected PBEDataProvider(PBELanguage language, PBERandom rand)
{
if (language >= PBELanguage.MAX)
{
throw new ArgumentOutOfRangeException(nameof(language));
}
GlobalLanguage = language;
GlobalRandom = rand;
Instance = this;
}
#region Data
public abstract bool IsBerry(PBEItem item);
public abstract IPBEBerryData GetBerryData(PBEItem item, bool cache = true);
public abstract bool TryGetBerryData(PBEItem item, [NotNullWhen(true)] out IPBEBerryData? bData, bool cache = true);
public abstract IPBEItemData GetItemData(PBEItem item, bool cache = true);
public abstract IPBEMoveData GetMoveData(PBEMove move, bool cache = true);
public abstract bool HasEvolutions(PBESpecies species, PBEForm form, bool cache = true);
public virtual bool HasEvolutions(IPBESpeciesForm pkmn, bool cache = true)
{
return HasEvolutions(pkmn.Species, pkmn.Form, cache);
}
public abstract IPBEPokemonData GetPokemonData(PBESpecies species, PBEForm form, bool cache = true);
public abstract int GetSpeciesCaught();
/// Used by .
public abstract IReadOnlyCollection GetLegalMoves(PBESpecies species, PBEForm form, byte level);
public virtual IPBEPokemonData GetPokemonData(IPBESpeciesForm pkmn, bool cache = true)
{
return GetPokemonData(pkmn.Species, pkmn.Form, cache: cache);
}
#endregion
#region EXP
public abstract uint GetEXPRequired(PBEGrowthRate type, byte level);
public abstract byte GetEXPLevel(PBEGrowthRate type, uint exp);
/// This is the boost to the EXP rate. In generation 5, Pass Powers boost the EXP rate.
public abstract float GetEXPModifier(PBEBattle battle);
/// In generation 5, this is 1 for ot, 1.5 for domestic trade, and 1.7 for international trade.
public abstract float GetEXPTradeModifier(PBEBattlePokemon pkmn);
#endregion
#region Catching
public abstract bool IsDarkGrass(PBEBattle battle);
public abstract bool IsDuskBallSetting(PBEBattle battle);
public abstract bool IsFishing(PBEBattle battle);
public abstract bool IsGuaranteedCapture(PBEBattle battle, PBESpecies species, PBEForm form);
public abstract bool IsMoonBallFamily(PBESpecies species, PBEForm form);
public abstract bool IsRepeatBallSpecies(PBESpecies species);
public abstract bool IsSurfing(PBEBattle battle);
public abstract bool IsUnderwater(PBEBattle battle);
/// This is the boost to the catch rate. In generation 5, Capture Powers boost the catch rate.
public abstract float GetCatchRateModifier(PBEBattle battle);
public virtual bool IsGuaranteedCapture(PBEBattle battle, IPBESpeciesForm pkmn)
{
return IsGuaranteedCapture(battle, pkmn.Species, pkmn.Form);
}
public virtual bool IsMoonBallFamily(IPBESpeciesForm pkmn)
{
return IsMoonBallFamily(pkmn.Species, pkmn.Form);
}
#endregion
#region LocalizedString
public abstract bool GetAbilityByName(string abilityName, [NotNullWhen(true)] out PBEAbility? ability);
public abstract IPBEReadOnlyLocalizedString GetAbilityName(PBEAbility ability);
public abstract bool GetFormByName(PBESpecies species, string formName, [NotNullWhen(true)] out PBEForm? form);
public abstract IPBEReadOnlyLocalizedString GetFormName(PBESpecies species, PBEForm form);
public abstract bool GetGenderByName(string genderName, [NotNullWhen(true)] out PBEGender? gender);
public abstract IPBEReadOnlyLocalizedString GetGenderName(PBEGender gender);
public abstract bool GetItemByName(string itemName, [NotNullWhen(true)] out PBEItem? item);
public abstract IPBEReadOnlyLocalizedString GetItemName(PBEItem item);
public abstract bool GetMoveByName(string moveName, [NotNullWhen(true)] out PBEMove? move);
public abstract IPBEReadOnlyLocalizedString GetMoveName(PBEMove move);
public abstract bool GetNatureByName(string natureName, [NotNullWhen(true)] out PBENature? nature);
public abstract IPBEReadOnlyLocalizedString GetNatureName(PBENature nature);
public abstract bool GetSpeciesByName(string speciesName, [NotNullWhen(true)] out PBESpecies? species);
public abstract IPBEReadOnlyLocalizedString GetSpeciesName(PBESpecies species);
public abstract bool GetStatByName(string statName, [NotNullWhen(true)] out PBEStat? stat);
public abstract IPBEReadOnlyLocalizedString GetStatName(PBEStat stat);
public abstract bool GetTypeByName(string typeName, [NotNullWhen(true)] out PBEType? type);
public abstract IPBEReadOnlyLocalizedString GetTypeName(PBEType type);
public virtual IPBEReadOnlyLocalizedString GetFormName(IPBESpeciesForm pkmn)
{
return GetFormName(pkmn.Species, pkmn.Form);
}
#endregion
}
================================================
FILE: PokemonBattleEngine/Data/Interfaces/ItemData.cs
================================================
namespace Kermalis.PokemonBattleEngine.Data;
public interface IPBEItemData
{
/// The power has when the user is holding this item. 0 will cause the move to fail.
byte FlingPower { get; }
}
public interface IPBEBerryData
{
byte Bitterness { get; }
byte Dryness { get; }
byte Sourness { get; }
byte Spicyness { get; }
byte Sweetness { get; }
/// The power has when the user is holding this item.
byte NaturalGiftPower { get; }
/// The type becomes when the user is holding this item.
PBEType NaturalGiftType { get; }
}
================================================
FILE: PokemonBattleEngine/Data/Interfaces/LocalizedString.cs
================================================
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace Kermalis.PokemonBattleEngine.Data;
public interface IPBEReadOnlyLocalizedString
{
string English { get; }
string French { get; }
string German { get; }
string Italian { get; }
string Japanese_Kana { get; }
string Japanese_Kanji { get; }
string Korean { get; }
string Spanish { get; }
}
public interface IPBELocalizedString : IPBEReadOnlyLocalizedString
{
new string English { get; set; }
new string French { get; set; }
new string German { get; set; }
new string Italian { get; set; }
new string Japanese_Kana { get; set; }
new string Japanese_Kanji { get; set; }
new string Korean { get; set; }
new string Spanish { get; set; }
}
public static class PBELanguageExtensions
{
public static bool ToPBELanguage(this CultureInfo cultureInfo, [NotNullWhen(true)] out PBELanguage? lang)
{
string id = cultureInfo.TwoLetterISOLanguageName;
switch (id)
{
case "en": lang = PBELanguage.English; return true;
case "fr": lang = PBELanguage.French; return true;
case "de": lang = PBELanguage.German; return true;
case "it": lang = PBELanguage.Italian; return true;
case "ja": lang = PBELanguage.Japanese_Kana; return true;
case "ko": lang = PBELanguage.Korean; return true;
case "es": lang = PBELanguage.Spanish; return true;
}
lang = default;
return false;
}
public static CultureInfo ToCultureInfo(this PBELanguage language)
{
switch (language)
{
case PBELanguage.English: return CultureInfo.GetCultureInfo("en-US");
case PBELanguage.French: return CultureInfo.GetCultureInfo("fr-FR");
case PBELanguage.German: return CultureInfo.GetCultureInfo("de-DE");
case PBELanguage.Italian: return CultureInfo.GetCultureInfo("it-IT");
case PBELanguage.Japanese_Kana:
case PBELanguage.Japanese_Kanji: return CultureInfo.GetCultureInfo("ja-JP");
case PBELanguage.Korean: return CultureInfo.GetCultureInfo("ko-KR");
case PBELanguage.Spanish: return CultureInfo.GetCultureInfo("es-ES");
default: throw new ArgumentOutOfRangeException(nameof(language));
}
}
public static string FromGlobalLanguage(this IPBEReadOnlyLocalizedString str)
{
return str.Get(PBEDataProvider.GlobalLanguage);
}
public static string Get(this IPBEReadOnlyLocalizedString str, PBELanguage lang)
{
switch (lang)
{
case PBELanguage.English: return str.English;
case PBELanguage.French: return str.French;
case PBELanguage.German: return str.German;
case PBELanguage.Italian: return str.Italian;
case PBELanguage.Japanese_Kana: return str.Japanese_Kana;
case PBELanguage.Japanese_Kanji: return str.Japanese_Kanji;
case PBELanguage.Korean: return str.Korean;
case PBELanguage.Spanish: return str.Spanish;
default: throw new ArgumentOutOfRangeException(nameof(lang));
}
}
}
================================================
FILE: PokemonBattleEngine/Data/Interfaces/MoveData.cs
================================================
using Kermalis.PokemonBattleEngine.Data.Utils;
namespace Kermalis.PokemonBattleEngine.Data;
public interface IPBEMoveData
{
PBEType Type { get; }
PBEMoveCategory Category { get; }
sbyte Priority { get; }
/// 0 PPTier will become 1 PP (unaffected by pp ups)
byte PPTier { get; }
/// 0 power will show up as --
byte Power { get; }
/// 0 accuracy will show up as --
byte Accuracy { get; }
PBEMoveEffect Effect { get; }
int EffectParam { get; }
PBEMoveTarget Targets { get; }
PBEMoveFlag Flags { get; }
}
public static class PBEMoveDataExtensions
{
public static bool HasSecondaryEffects(this IPBEMoveData mData, PBESettings settings)
{
return PBEDataUtils.HasSecondaryEffects(mData.Effect, settings);
}
public static bool IsHPDrainMove(this IPBEMoveData mData)
{
return PBEDataUtils.IsHPDrainMove(mData.Effect);
}
public static bool IsHPRestoreMove(this IPBEMoveData mData)
{
return PBEDataUtils.IsHPRestoreMove(mData.Effect);
}
public static bool IsMultiHitMove(this IPBEMoveData mData)
{
return PBEDataUtils.IsMultiHitMove(mData.Effect);
}
public static bool IsRecoilMove(this IPBEMoveData mData)
{
return PBEDataUtils.IsRecoilMove(mData.Effect);
}
public static bool IsSetDamageMove(this IPBEMoveData mData)
{
return PBEDataUtils.IsSetDamageMove(mData.Effect);
}
public static bool IsSpreadMove(this IPBEMoveData mData)
{
return PBEDataUtils.IsSpreadMove(mData.Targets);
}
public static bool IsWeatherMove(this IPBEMoveData mData)
{
return PBEDataUtils.IsWeatherMove(mData.Effect);
}
/// Temporary check to see if a move is usable, can be removed once all moves are added
public static bool IsMoveUsable(this IPBEMoveData mData)
{
return PBEDataUtils.IsMoveUsable(mData.Effect);
}
}
================================================
FILE: PokemonBattleEngine/Data/Interfaces/MovesetInterfaces.cs
================================================
using Kermalis.EndianBinaryIO;
using System.Collections.Generic;
using System.Text.Json;
namespace Kermalis.PokemonBattleEngine.Data;
public interface IPBEMovesetSlot
{
PBEMove Move { get; }
byte PPUps { get; }
}
public interface IPBEPartyMovesetSlot : IPBEMovesetSlot
{
int PP { get; }
}
public interface IPBEMoveset : IReadOnlyList where T : IPBEMovesetSlot
{
//
}
public interface IPBEMoveset : IPBEMoveset
{
//
}
public interface IPBEPartyMoveset : IReadOnlyList where T : IPBEPartyMovesetSlot
{
//
}
public interface IPBEPartyMoveset : IPBEPartyMoveset
{
//
}
public static class PBEMovesetInterfaceExtensions
{
public static int CountMoves(this IPBEMoveset moves)
{
int num = 0;
for (int i = 0; i < moves.Count; i++)
{
if (moves[i].Move != PBEMove.None)
{
num++;
}
}
return num;
}
internal static void ToBytes(this IPBEMoveset moveset, EndianBinaryWriter w)
{
byte count = (byte)moveset.Count;
w.WriteByte(count);
for (int i = 0; i < count; i++)
{
IPBEMovesetSlot slot = moveset[i];
w.WriteEnum(slot.Move);
w.WriteByte(slot.PPUps);
}
}
internal static void ToBytes(this IPBEPartyMoveset moveset, EndianBinaryWriter w)
{
byte count = (byte)moveset.Count;
w.WriteByte(count);
for (int i = 0; i < count; i++)
{
IPBEPartyMovesetSlot slot = moveset[i];
w.WriteEnum(slot.Move);
w.WriteInt32(slot.PP);
w.WriteByte(slot.PPUps);
}
}
internal static void ToJson(this IPBEMoveset moveset, Utf8JsonWriter w)
{
w.WriteStartArray();
for (int i = 0; i < moveset.Count; i++)
{
IPBEMovesetSlot slot = moveset[i];
w.WriteStartObject();
w.WriteString(nameof(IPBEMovesetSlot.Move), slot.Move.ToString());
w.WriteNumber(nameof(IPBEMovesetSlot.PPUps), slot.PPUps);
w.WriteEndObject();
}
w.WriteEndArray();
}
internal static void ToJson(this IPBEPartyMoveset moveset, Utf8JsonWriter w)
{
w.WriteStartArray();
for (int i = 0; i < moveset.Count; i++)
{
IPBEPartyMovesetSlot slot = moveset[i];
w.WriteStartObject();
w.WriteString(nameof(IPBEPartyMovesetSlot.Move), slot.Move.ToString());
w.WriteNumber(nameof(IPBEPartyMovesetSlot.PP), slot.PP);
w.WriteNumber(nameof(IPBEPartyMovesetSlot.PPUps), slot.PPUps);
w.WriteEndObject();
}
w.WriteEndArray();
}
}
================================================
FILE: PokemonBattleEngine/Data/Interfaces/PokemonData.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
namespace Kermalis.PokemonBattleEngine.Data;
public interface IPBEPokemonData : IPBEPokemonTypes, IPBESpeciesForm
{
IPBEReadOnlyStatCollection BaseStats { get; }
PBEGenderRatio GenderRatio { get; }
PBEGrowthRate GrowthRate { get; }
ushort BaseEXPYield { get; }
byte CatchRate { get; }
byte FleeRate { get; }
/// Weight in Kilograms
float Weight { get; }
IReadOnlyList Abilities { get; }
}
public static class PBEPokemonDataExtensions
{
public static bool HasAbility(this IPBEPokemonData pData, PBEAbility ability)
{
if (ability == PBEAbility.None || ability >= PBEAbility.MAX)
{
throw new ArgumentOutOfRangeException(nameof(ability));
}
return pData.Abilities.Contains(ability);
}
}
================================================
FILE: PokemonBattleEngine/Data/Interfaces/PokemonInterfaces.cs
================================================
using Kermalis.EndianBinaryIO;
using Kermalis.PokemonBattleEngine.Battle;
using Kermalis.PokemonBattleEngine.Data.Utils;
using System;
using System.Collections.Generic;
using System.Text.Json;
namespace Kermalis.PokemonBattleEngine.Data;
// Not separating this into IPBEWildPokemon for these reasons:
// 1: A lot of work to do that
// 2: If someone wants to do pal park or catch released Pokémon etc, they'd need all these things
// 3: If they want just some things (like effort values pre-seeded) they'd also need this
public interface IPBEPokemon : IPBESpeciesForm
{
/// This marks the Pokémon to be ignored by the battle engine. The Pokémon will be treated like an egg or fainted Pokémon.
/// Therefore, it won't be sent out, copied with , or count as a battler if the rest of the team faints.
bool PBEIgnore { get; }
PBEGender Gender { get; }
string Nickname { get; }
bool Shiny { get; }
byte Level { get; }
uint EXP { get; }
bool Pokerus { get; }
PBEItem Item { get; }
byte Friendship { get; }
PBEAbility Ability { get; }
PBENature Nature { get; }
PBEItem CaughtBall { get; }
IPBEStatCollection EffortValues { get; }
IPBEReadOnlyStatCollection IndividualValues { get; }
IPBEMoveset Moveset { get; }
}
public interface IPBEPartyPokemon : IPBEPokemon
{
ushort HP { get; }
PBEStatus1 Status1 { get; }
byte SleepTurns { get; }
new IPBEPartyMoveset Moveset { get; }
}
public interface IPBEPokemonCollection : IReadOnlyList where T : IPBEPokemon
{
}
public interface IPBEPokemonCollection : IReadOnlyList
{
}
public interface IPBEPartyPokemonCollection : IReadOnlyList where T : IPBEPartyPokemon
{
}
public interface IPBEPartyPokemonCollection : IReadOnlyList
{
}
public interface IPBEPokemonTypes
{
/// The Pokémon's first type.
PBEType Type1 { get; }
/// The Pokémon's second type.
PBEType Type2 { get; }
}
public interface IPBEPokemonKnownTypes
{
/// The first type everyone believes the Pokémon has.
PBEType KnownType1 { get; }
/// The second type everyone believes the Pokémon has.
PBEType KnownType2 { get; }
}
public interface IPBESpeciesForm
{
PBESpecies Species { get; }
PBEForm Form { get; }
}
public static class PBEPokemonInterfaceExtensions
{
public static bool HasType(this IPBEPokemonTypes pkmn, PBEType type)
{
if (type >= PBEType.MAX)
{
throw new ArgumentOutOfRangeException(nameof(type));
}
return pkmn.Type1 == type || pkmn.Type2 == type;
}
public static bool HasType_Known(this IPBEPokemonKnownTypes pkmn, PBEType type)
{
if (type >= PBEType.MAX)
{
throw new ArgumentOutOfRangeException(nameof(type));
}
return pkmn.KnownType1 == type || pkmn.KnownType2 == type;
}
public static bool HasType(this T pkmn, PBEType type, bool useKnownInfo) where T : IPBEPokemonKnownTypes, IPBEPokemonTypes
{
return useKnownInfo ? HasType_Known(pkmn, type) : HasType(pkmn, type);
}
public static bool ReceivesSTAB(this IPBEPokemonTypes pkmn, PBEType type)
{
return type != PBEType.None && HasType(pkmn, type);
}
public static bool ReceivesSTAB_Known(this IPBEPokemonKnownTypes pkmn, PBEType type)
{
return type != PBEType.None && HasType_Known(pkmn, type);
}
public static bool ReceivesSTAB(this T pkmn, PBEType type, bool useKnownInfo) where T : IPBEPokemonKnownTypes, IPBEPokemonTypes
{
return useKnownInfo ? ReceivesSTAB_Known(pkmn, type) : ReceivesSTAB(pkmn, type);
}
internal static void ToBytes(this IPBEPokemon pkmn, EndianBinaryWriter w)
{
w.WriteEnum(pkmn.Species);
w.WriteEnum(pkmn.Form);
w.WriteChars_NullTerminated(pkmn.Nickname);
w.WriteByte(pkmn.Level);
w.WriteUInt32(pkmn.EXP);
w.WriteByte(pkmn.Friendship);
w.WriteBoolean(pkmn.Shiny);
w.WriteBoolean(pkmn.Pokerus);
w.WriteEnum(pkmn.Ability);
w.WriteEnum(pkmn.Nature);
w.WriteEnum(pkmn.CaughtBall);
w.WriteEnum(pkmn.Gender);
w.WriteEnum(pkmn.Item);
pkmn.EffortValues.ToBytes(w);
pkmn.IndividualValues.ToBytes(w);
pkmn.Moveset.ToBytes(w);
}
internal static void ToJson(this IPBEPokemon pkmn, Utf8JsonWriter w)
{
w.WriteStartObject();
PBESpecies species = pkmn.Species;
w.WriteString(nameof(IPBEPokemon.Species), species.ToString());
if (PBEDataUtils.HasForms(species, true))
{
w.WriteString(nameof(IPBEPokemon.Form), PBEDataUtils.GetNameOfForm(species, pkmn.Form));
}
w.WriteString(nameof(IPBEPokemon.Nickname), pkmn.Nickname);
w.WriteNumber(nameof(IPBEPokemon.Level), pkmn.Level);
w.WriteNumber(nameof(IPBEPokemon.EXP), pkmn.EXP);
w.WriteNumber(nameof(IPBEPokemon.Friendship), pkmn.Friendship);
w.WriteBoolean(nameof(IPBEPokemon.Shiny), pkmn.Shiny);
w.WriteBoolean(nameof(IPBEPokemon.Pokerus), pkmn.Pokerus);
w.WriteString(nameof(IPBEPokemon.Ability), pkmn.Ability.ToString());
w.WriteString(nameof(IPBEPokemon.Nature), pkmn.Nature.ToString());
w.WriteString(nameof(IPBEPokemon.CaughtBall), pkmn.CaughtBall.ToString());
w.WriteString(nameof(IPBEPokemon.Gender), pkmn.Gender.ToString());
w.WriteString(nameof(IPBEPokemon.Item), pkmn.Item.ToString());
w.WritePropertyName(nameof(IPBEPokemon.EffortValues));
pkmn.EffortValues.ToJson(w);
w.WritePropertyName(nameof(IPBEPokemon.IndividualValues));
pkmn.IndividualValues.ToJson(w);
w.WritePropertyName(nameof(IPBEPokemon.Moveset));
pkmn.Moveset.ToJson(w);
w.WriteEndObject();
}
internal static void ToBytes(this IPBEPokemonCollection party, EndianBinaryWriter w)
{
byte count = (byte)party.Count;
w.WriteByte(count);
for (int i = 0; i < count; i++)
{
party[i].ToBytes(w);
}
}
}
================================================
FILE: PokemonBattleEngine/Data/Interfaces/StatInterfaces.cs
================================================
using Kermalis.EndianBinaryIO;
using Kermalis.PokemonBattleEngine.Data.Utils;
using System;
using System.Text.Json;
namespace Kermalis.PokemonBattleEngine.Data;
public interface IPBEReadOnlyStatCollection
{
byte HP { get; }
byte Attack { get; }
byte Defense { get; }
byte SpAttack { get; }
byte SpDefense { get; }
byte Speed { get; }
}
public interface IPBEStatCollection : IPBEReadOnlyStatCollection
{
new byte HP { get; set; }
new byte Attack { get; set; }
new byte Defense { get; set; }
new byte SpAttack { get; set; }
new byte SpDefense { get; set; }
new byte Speed { get; set; }
}
public static class PBEStatInterfaceExtensions
{
public static byte GetStat(this IPBEReadOnlyStatCollection stats, PBEStat stat)
{
switch (stat)
{
case PBEStat.HP: return stats.HP;
case PBEStat.Attack: return stats.Attack;
case PBEStat.Defense: return stats.Defense;
case PBEStat.SpAttack: return stats.SpAttack;
case PBEStat.SpDefense: return stats.SpDefense;
case PBEStat.Speed: return stats.Speed;
default: throw new ArgumentOutOfRangeException(nameof(stat));
}
}
public static void SetStat(this IPBEStatCollection stats, PBEStat stat, byte value)
{
switch (stat)
{
case PBEStat.HP: stats.HP = value; break;
case PBEStat.Attack: stats.Attack = value; break;
case PBEStat.Defense: stats.Defense = value; break;
case PBEStat.SpAttack: stats.SpAttack = value; break;
case PBEStat.SpDefense: stats.SpDefense = value; break;
case PBEStat.Speed: stats.Speed = value; break;
default: throw new ArgumentOutOfRangeException(nameof(stat));
}
}
public static byte GetHiddenPowerBasePower(this IPBEReadOnlyStatCollection stats, PBESettings settings)
{
return PBEDataUtils.GetHiddenPowerBasePower(stats.HP, stats.Attack, stats.Defense, stats.SpAttack, stats.SpDefense, stats.Speed, settings);
}
public static PBEType GetHiddenPowerType(this IPBEReadOnlyStatCollection stats)
{
return PBEDataUtils.GetHiddenPowerType(stats.HP, stats.Attack, stats.Defense, stats.SpAttack, stats.SpDefense, stats.Speed);
}
internal static void ToBytes(this IPBEReadOnlyStatCollection stats, EndianBinaryWriter w)
{
w.WriteByte(stats.HP);
w.WriteByte(stats.Attack);
w.WriteByte(stats.Defense);
w.WriteByte(stats.SpAttack);
w.WriteByte(stats.SpDefense);
w.WriteByte(stats.Speed);
}
internal static void ToJson(this IPBEReadOnlyStatCollection stats, Utf8JsonWriter w)
{
w.WriteStartObject();
w.WriteNumber(nameof(IPBEReadOnlyStatCollection.HP), stats.HP);
w.WriteNumber(nameof(IPBEReadOnlyStatCollection.Attack), stats.Attack);
w.WriteNumber(nameof(IPBEReadOnlyStatCollection.Defense), stats.Defense);
w.WriteNumber(nameof(IPBEReadOnlyStatCollection.SpAttack), stats.SpAttack);
w.WriteNumber(nameof(IPBEReadOnlyStatCollection.SpDefense), stats.SpDefense);
w.WriteNumber(nameof(IPBEReadOnlyStatCollection.Speed), stats.Speed);
w.WriteEndObject();
}
}
================================================
FILE: PokemonBattleEngine/Data/Legality/LegalEffortValues.cs
================================================
using Kermalis.EndianBinaryIO;
using Kermalis.PokemonBattleEngine.Utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text.Json.Nodes;
namespace Kermalis.PokemonBattleEngine.Data.Legality;
public sealed class PBELegalEffortValues : IPBEStatCollection, IEnumerable, INotifyPropertyChanged
{
public sealed class PBELegalEffortValue : INotifyPropertyChanged
{
private void OnPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler? PropertyChanged;
private readonly PBELegalEffortValues _parent;
public PBEStat Stat { get; }
private byte _value;
public byte Value
{
get => _value;
set
{
if (_value != value)
{
ushort oldTotal = _parent.StatTotal;
int newTotal = oldTotal - _value + value;
if (newTotal > _parent.Settings.MaxTotalEVs)
{
byte newValue = (byte)(value - (newTotal - _parent.Settings.MaxTotalEVs));
if (_value != newValue)
{
Update(newValue);
}
}
else
{
Update(value);
}
}
}
}
internal PBELegalEffortValue(PBELegalEffortValues parent, PBEStat stat, byte value)
{
_parent = parent;
Stat = stat;
_value = value;
}
private void Update(byte newValue)
{
_value = newValue;
OnPropertyChanged(nameof(Value));
_parent.OnPropertyChanged(nameof(_parent.StatTotal));
}
}
private void OnPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler? PropertyChanged;
public PBESettings Settings { get; }
private readonly PBELegalEffortValue[] _evs;
public PBELegalEffortValue this[PBEStat stat]
{
get
{
int statIndex = (int)stat;
if (statIndex >= 6)
{
throw new ArgumentOutOfRangeException(nameof(stat));
}
return _evs[statIndex];
}
}
public ushort StatTotal
{
get
{
ushort total = 0;
for (int i = 0; i < 6; i++)
{
total += _evs[i].Value;
}
return total;
}
}
public byte HP
{
get => _evs[0].Value;
set => _evs[0].Value = value;
}
public byte Attack
{
get => _evs[1].Value;
set => _evs[1].Value = value;
}
public byte Defense
{
get => _evs[2].Value;
set => _evs[2].Value = value;
}
public byte SpAttack
{
get => _evs[3].Value;
set => _evs[3].Value = value;
}
public byte SpDefense
{
get => _evs[4].Value;
set => _evs[4].Value = value;
}
public byte Speed
{
get => _evs[5].Value;
set => _evs[5].Value = value;
}
internal PBELegalEffortValues(PBESettings settings, EndianBinaryReader r)
{
byte hp = r.ReadByte();
byte attack = r.ReadByte();
byte defense = r.ReadByte();
byte spAttack = r.ReadByte();
byte spDefense = r.ReadByte();
byte speed = r.ReadByte();
if (hp + attack + defense + spAttack + spDefense + speed > settings.MaxTotalEVs)
{
throw new InvalidDataException();
}
Settings = settings;
_evs = CreateEVs(hp, attack, defense, spAttack, spDefense, speed);
}
internal PBELegalEffortValues(PBESettings settings, JsonObject jObj)
{
byte hp = jObj.GetSafe(nameof(PBEStat.HP)).GetValue();
byte attack = jObj.GetSafe(nameof(PBEStat.Attack)).GetValue();
byte defense = jObj.GetSafe(nameof(PBEStat.Defense)).GetValue();
byte spAttack = jObj.GetSafe(nameof(PBEStat.SpAttack)).GetValue();
byte spDefense = jObj.GetSafe(nameof(PBEStat.SpDefense)).GetValue();
byte speed = jObj.GetSafe(nameof(PBEStat.Speed)).GetValue();
if (hp + attack + defense + spAttack + spDefense + speed > settings.MaxTotalEVs)
{
throw new InvalidDataException($"Effort values total must not exceed \"{nameof(settings.MaxTotalEVs)}\" ({settings.MaxTotalEVs})");
}
Settings = settings;
_evs = CreateEVs(hp, attack, defense, spAttack, spDefense, speed);
}
internal PBELegalEffortValues(PBELegalEffortValues other)
{
Settings = other.Settings;
_evs = CreateEVs(other.HP, other.Attack, other.Defense, other.SpAttack, other.SpDefense, other.Speed);
}
public PBELegalEffortValues(PBESettings settings, bool randomize)
{
settings.ShouldBeReadOnly(nameof(settings));
Settings = settings;
_evs = CreateEVs(0, 0, 0, 0, 0, 0);
if (randomize)
{
Randomize();
}
}
private PBELegalEffortValue[] CreateEVs(byte hp, byte attack, byte defense, byte spAttack, byte spDefense, byte speed)
{
return new PBELegalEffortValue[6]
{
new PBELegalEffortValue(this, PBEStat.HP, hp),
new PBELegalEffortValue(this, PBEStat.Attack, attack),
new PBELegalEffortValue(this, PBEStat.Defense, defense),
new PBELegalEffortValue(this, PBEStat.SpAttack, spAttack),
new PBELegalEffortValue(this, PBEStat.SpDefense, spDefense),
new PBELegalEffortValue(this, PBEStat.Speed, speed),
};
}
public void Clear()
{
for (int i = 0; i < 6; i++)
{
_evs[i].Value = 0;
}
}
public void Equalize()
{
Clear();
for (int i = 0; i < 6; i++)
{
_evs[i].Value = (byte)(Settings.MaxTotalEVs / 6);
}
}
public void Randomize()
{
if (Settings.MaxTotalEVs == 0)
{
return;
}
byte[] vals = new byte[6];
int[] a = Enumerable.Repeat(0, 6 - 1)
.Select(x => PBEDataProvider.GlobalRandom.RandomInt(1, Settings.MaxTotalEVs - 1))
.Concat(new int[] { Settings.MaxTotalEVs })
.OrderBy(x => x)
.ToArray();
ushort total = 0;
for (int i = 0; i < 6; i++)
{
byte b = (byte)Math.Min(byte.MaxValue, a[i] - total);
vals[i] = b;
total += b;
}
// This "while" will fix the issue where the speed stat was supposed to be above 255
var notMax = new List(5);
while (total != Settings.MaxTotalEVs)
{
notMax.Clear();
for (int i = 0; i < 6; i++)
{
if (vals[i] != byte.MaxValue)
{
notMax.Add(i);
}
}
int index = PBEDataProvider.GlobalRandom.RandomElement(notMax);
byte old = vals[index];
byte b = (byte)Math.Min(byte.MaxValue, old + (Settings.MaxTotalEVs - total));
vals[index] = b;
total += (byte)(b - old);
}
for (int i = 0; i < 6; i++)
{
_evs[i].Value = vals[i];
}
}
public IEnumerator GetEnumerator()
{
for (int i = 0; i < 6; i++)
{
yield return _evs[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
================================================
FILE: PokemonBattleEngine/Data/Legality/LegalIndividualValues.cs
================================================
using Kermalis.EndianBinaryIO;
using Kermalis.PokemonBattleEngine.Data.Utils;
using Kermalis.PokemonBattleEngine.Utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Text.Json.Nodes;
namespace Kermalis.PokemonBattleEngine.Data.Legality;
public sealed class PBELegalIndividualValues : IPBEStatCollection, IEnumerable, INotifyPropertyChanged
{
public sealed class PBELegalIndividualValue : INotifyPropertyChanged
{
private void OnPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler? PropertyChanged;
private readonly PBELegalIndividualValues _parent;
public PBEStat Stat { get; }
private byte _value;
public byte Value
{
get => _value;
set
{
if (_value != value)
{
if (value > _parent.Settings.MaxIVs)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_value = value;
OnPropertyChanged(nameof(Value));
_parent.UpdateHiddenPower();
}
}
}
internal PBELegalIndividualValue(PBELegalIndividualValues parent, PBEStat stat, byte value)
{
_parent = parent;
Stat = stat;
_value = value;
}
}
private void OnPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler? PropertyChanged;
public PBESettings Settings { get; }
private readonly PBELegalIndividualValue[] _ivs;
public PBELegalIndividualValue this[PBEStat stat]
{
get
{
int statIndex = (int)stat;
if (statIndex >= 6)
{
throw new ArgumentOutOfRangeException(nameof(stat));
}
return _ivs[statIndex];
}
}
private PBEType _hiddenPowerType;
public PBEType HiddenPowerType
{
get => _hiddenPowerType;
private set
{
if (_hiddenPowerType != value)
{
_hiddenPowerType = value;
OnPropertyChanged(nameof(HiddenPowerType));
}
}
}
private byte _hiddenPowerBasePower;
public byte HiddenPowerBasePower
{
get => _hiddenPowerBasePower;
private set
{
if (_hiddenPowerBasePower != value)
{
_hiddenPowerBasePower = value;
OnPropertyChanged(nameof(HiddenPowerBasePower));
}
}
}
public byte HP
{
get => _ivs[0].Value;
set => _ivs[0].Value = value;
}
public byte Attack
{
get => _ivs[1].Value;
set => _ivs[1].Value = value;
}
public byte Defense
{
get => _ivs[2].Value;
set => _ivs[2].Value = value;
}
public byte SpAttack
{
get => _ivs[3].Value;
set => _ivs[3].Value = value;
}
public byte SpDefense
{
get => _ivs[4].Value;
set => _ivs[4].Value = value;
}
public byte Speed
{
get => _ivs[5].Value;
set => _ivs[5].Value = value;
}
internal PBELegalIndividualValues(PBESettings settings, EndianBinaryReader r)
{
void Validate(byte val)
{
if (val > settings.MaxIVs)
{
throw new InvalidDataException();
}
}
byte hp = r.ReadByte();
Validate(hp);
byte attack = r.ReadByte();
Validate(attack);
byte defense = r.ReadByte();
Validate(defense);
byte spAttack = r.ReadByte();
Validate(spAttack);
byte spDefense = r.ReadByte();
Validate(spDefense);
byte speed = r.ReadByte();
Validate(speed);
Settings = settings;
_ivs = CreateIVs(hp, attack, defense, spAttack, spDefense, speed);
UpdateHiddenPower();
}
internal PBELegalIndividualValues(PBESettings settings, JsonObject jObj)
{
void Validate(byte val, string name)
{
if (val > settings.MaxIVs)
{
throw new InvalidDataException($"\"{name}\" individual value must not exceed \"{nameof(settings.MaxIVs)}\" ({settings.MaxIVs})");
}
}
byte hp = jObj.GetSafe(nameof(PBEStat.HP)).GetValue();
Validate(hp, nameof(PBEStat.HP));
byte attack = jObj.GetSafe(nameof(PBEStat.Attack)).GetValue();
Validate(attack, nameof(PBEStat.Attack));
byte defense = jObj.GetSafe(nameof(PBEStat.Defense)).GetValue();
Validate(defense, nameof(PBEStat.Defense));
byte spAttack = jObj.GetSafe(nameof(PBEStat.SpAttack)).GetValue();
Validate(spAttack, nameof(PBEStat.SpAttack));
byte spDefense = jObj.GetSafe(nameof(PBEStat.SpDefense)).GetValue();
Validate(spDefense, nameof(PBEStat.SpDefense));
byte speed = jObj.GetSafe(nameof(PBEStat.Speed)).GetValue();
Validate(speed, nameof(PBEStat.Speed));
Settings = settings;
_ivs = CreateIVs(hp, attack, defense, spAttack, spDefense, speed);
UpdateHiddenPower();
}
internal PBELegalIndividualValues(PBELegalIndividualValues other)
{
Settings = other.Settings;
_ivs = CreateIVs(other.HP, other.Attack, other.Defense, other.SpAttack, other.SpDefense, other.Speed);
UpdateHiddenPower();
}
public PBELegalIndividualValues(PBESettings settings, bool randomize)
{
settings.ShouldBeReadOnly(nameof(settings));
Settings = settings;
_ivs = CreateIVs(0, 0, 0, 0, 0, 0);
UpdateHiddenPower();
if (randomize)
{
Randomize();
}
}
private PBELegalIndividualValue[] CreateIVs(byte hp, byte attack, byte defense, byte spAttack, byte spDefense, byte speed)
{
return new PBELegalIndividualValue[6]
{
new PBELegalIndividualValue(this, PBEStat.HP, hp),
new PBELegalIndividualValue(this, PBEStat.Attack, attack),
new PBELegalIndividualValue(this, PBEStat.Defense, defense),
new PBELegalIndividualValue(this, PBEStat.SpAttack, spAttack),
new PBELegalIndividualValue(this, PBEStat.SpDefense, spDefense),
new PBELegalIndividualValue(this, PBEStat.Speed, speed)
};
}
private void UpdateHiddenPower()
{
HiddenPowerType = PBEDataUtils.GetHiddenPowerType(HP, Attack, Defense, SpAttack, SpDefense, Speed);
HiddenPowerBasePower = PBEDataUtils.GetHiddenPowerBasePower(HP, Attack, Defense, SpAttack, SpDefense, Speed, Settings);
}
public void Clear()
{
for (int i = 0; i < 6; i++)
{
_ivs[i].Value = 0;
}
}
public void Maximize()
{
for (int i = 0; i < 6; i++)
{
_ivs[i].Value = Settings.MaxIVs;
}
}
public void Randomize()
{
for (int i = 0; i < 6; i++)
{
_ivs[i].Value = (byte)PBEDataProvider.GlobalRandom.RandomInt(0, Settings.MaxIVs);
}
}
public IEnumerator GetEnumerator()
{
for (int i = 0; i < 6; i++)
{
yield return _ivs[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
================================================
FILE: PokemonBattleEngine/Data/Legality/LegalMoveset.cs
================================================
using Kermalis.PokemonBattleEngine.Data.Utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
namespace Kermalis.PokemonBattleEngine.Data.Legality;
public sealed class PBELegalMoveset : IPBEMoveset, IPBEMoveset, INotifyPropertyChanged
{
public sealed class PBELegalMovesetSlot : IPBEMovesetSlot, INotifyPropertyChanged
{
private void OnPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler? PropertyChanged;
private readonly PBELegalMoveset _parent;
private readonly int _index;
public PBEAlphabeticalList Allowed { get; }
private PBEMove _move;
public PBEMove Move
{
get => _move;
set
{
PBEMove old = _move;
if (old != value)
{
if (value >= PBEMove.MAX || (value != PBEMove.None && !PBEDataUtils.IsMoveUsable(value)))
{
throw new ArgumentOutOfRangeException(nameof(value));
}
if (!_isMoveEditable)
{
throw new InvalidOperationException($"Slot {_index}'s move cannot be changed because there is no move in slot {_index - 1}.");
}
if (!Allowed.Contains(value))
{
throw new ArgumentOutOfRangeException(nameof(value), $"Slot {_index} does not allow {value}.");
}
if (value != PBEMove.None)
{
// If "move" is in another slot, place "slotIndex"'s old move at the other slot
for (int i = 0; i < _parent.Settings.NumMoves; i++)
{
if (i != _index)
{
PBELegalMovesetSlot iSlot = _parent[i];
if (iSlot.Move == value)
{
// If slot 0 is Snore and slot 3 is None but is trying to become Snore, do nothing because the first Snore is in an earlier slot and swapping None to an earlier slot makes no sense
if (old == PBEMove.None && i < _index)
{
goto finish;
}
else
{
UpdateMove(value);
iSlot.UpdateMove(old);
goto editables;
}
}
}
}
}
else
{
// If "move" is None and a slot after "slotIndex" is not None, then place None at the other slot instead and place the other slot's move at "slotIndex"
for (int i = _parent.Settings.NumMoves - 1; i > _index; i--)
{
PBELegalMovesetSlot iSlot = _parent[i];
if (iSlot.Move != PBEMove.None)
{
UpdateMove(iSlot.Move);
iSlot.UpdateMove(PBEMove.None);
goto editables;
}
}
}
// This gets reached if:
// "move" is not None and there is no other slot with "move"
// "move" is None and there is no slot after "slotIndex" with a move
UpdateMove(value);
editables:
_parent.SetEditables();
finish:
;
}
}
}
private bool _isMoveEditable;
public bool IsMoveEditable
{
get => _isMoveEditable;
internal set
{
if (_isMoveEditable != value)
{
_isMoveEditable = value;
OnPropertyChanged(nameof(IsMoveEditable));
}
}
}
private byte _ppUps;
public byte PPUps
{
get => _ppUps;
set
{
if (_ppUps != value)
{
if (value > _parent.Settings.MaxPPUps)
{
throw new ArgumentOutOfRangeException(nameof(value), $"\"{nameof(value)}\" cannot exceed \"{nameof(_parent.Settings.MaxPPUps)}\" ({_parent.Settings.MaxPPUps}).");
}
if (!IsPPUpsEditable)
{
throw new InvalidOperationException($"Slot {_index}'s PP-Ups cannot be changed because it has no move.");
}
UpdatePPUps(value);
}
}
}
private bool _isPPUpsEditable;
public bool IsPPUpsEditable
{
get => _isPPUpsEditable;
private set
{
if (_isPPUpsEditable != value)
{
_isPPUpsEditable = value;
OnPropertyChanged(nameof(IsPPUpsEditable));
}
}
}
private static readonly PBEMove[] _none = new PBEMove[1] { PBEMove.None };
internal PBELegalMovesetSlot(PBELegalMoveset parent, int index)
{
_parent = parent;
_index = index;
_isMoveEditable = index < 2;
Allowed = new PBEAlphabeticalList(_none);
}
private void UpdateMove(PBEMove move)
{
if (_move != move)
{
_move = move;
OnPropertyChanged(nameof(Move));
if (_move == PBEMove.None)
{
UpdatePPUps(0);
}
IsPPUpsEditable = _move != PBEMove.None;
}
}
private void UpdatePPUps(byte ppUps)
{
if (_ppUps != ppUps)
{
_ppUps = ppUps;
OnPropertyChanged(nameof(PPUps));
}
}
}
private void OnPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler? PropertyChanged;
private PBESpecies _species;
public PBESpecies Species
{
get => _species;
set
{
if (_species != value)
{
PBEDataUtils.ValidateSpecies(value, 0, true);
_species = value;
_form = 0;
OnPropertyChanged(nameof(Species));
SetAlloweds();
}
}
}
private PBEForm _form;
public PBEForm Form
{
get => _form;
set
{
if (_form != value)
{
PBEDataUtils.ValidateSpecies(_species, value, true);
_form = value;
OnPropertyChanged(nameof(Form));
SetAlloweds();
}
}
}
private byte _level;
public byte Level
{
get => _level;
set
{
if (_level != value)
{
PBEDataUtils.ValidateLevel(value, Settings);
_level = value;
OnPropertyChanged(nameof(Level));
SetAlloweds();
}
}
}
public PBESettings Settings { get; }
private readonly PBELegalMovesetSlot[] _list;
public int Count => Settings.NumMoves;
public PBELegalMovesetSlot this[int index]
{
get
{
if (index >= Settings.NumMoves)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
return _list[index];
}
}
IPBEMovesetSlot IReadOnlyList.this[int index] => this[index];
public PBELegalMovesetSlot? this[PBEMove move]
{
get
{
for (int i = 0; i < Settings.NumMoves; i++)
{
PBELegalMovesetSlot slot = _list[i];
if (slot.Move == move)
{
return slot;
}
}
return null;
}
}
internal PBELegalMoveset(PBELegalMoveset other)
{
_species = other._species;
_form = other._form;
_level = other._level;
Settings = other.Settings;
int count = Settings.NumMoves;
_list = new PBELegalMovesetSlot[count];
for (int i = 0; i < count; i++)
{
_list[i] = new PBELegalMovesetSlot(this, i);
}
SetAlloweds();
for (int i = 0; i < count; i++)
{
PBELegalMovesetSlot slot = _list[i];
PBELegalMovesetSlot oSlot = other[i];
slot.Move = oSlot.Move;
slot.PPUps = oSlot.PPUps;
}
}
internal PBELegalMoveset(PBESpecies species, PBEForm form, byte level, PBESettings settings, IPBEMoveset other)
{
int count = other.Count;
if (count != settings.NumMoves)
{
throw new InvalidDataException($"Moveset count must be equal to \"{nameof(settings.NumMoves)}\" ({settings.NumMoves}).");
}
_species = species;
_form = form;
_level = level;
Settings = settings;
_list = new PBELegalMovesetSlot[count];
for (int i = 0; i < count; i++)
{
_list[i] = new PBELegalMovesetSlot(this, i);
}
SetAlloweds();
for (int i = 0; i < count; i++)
{
IPBEMovesetSlot oSlot = other[i];
PBELegalMovesetSlot slot = _list[i];
PBEMove move = oSlot.Move;
slot.Move = move;
if (slot.Move != move)
{
throw new ArgumentOutOfRangeException(nameof(IPBEPokemon.Moveset), "Invalid moves.");
}
byte ppUps = oSlot.PPUps;
slot.PPUps = ppUps;
if (slot.PPUps != ppUps)
{
throw new ArgumentOutOfRangeException(nameof(IPBEPokemon.Moveset), "Invalid PP-Ups.");
}
}
}
/// Creates a new object with the specified traits.
/// The species of the Pokémon that this moveset will be built for.
/// The form of the Pokémon that this moveset will be built for.
/// The level of the Pokémon that this moveset will be built for.
/// The settings that will be used to evaluate the .
/// True if should be called, False if the move slots use their default values.
public PBELegalMoveset(PBESpecies species, PBEForm form, byte level, PBESettings settings, bool randomize)
{
settings.ShouldBeReadOnly(nameof(settings));
PBEDataUtils.ValidateSpecies(species, form, true);
PBEDataUtils.ValidateLevel(level, settings);
_level = level;
_species = species;
_form = form;
Settings = settings;
int count = settings.NumMoves;
_list = new PBELegalMovesetSlot[count];
for (int i = 0; i < count; i++)
{
_list[i] = new PBELegalMovesetSlot(this, i);
}
SetAlloweds();
if (randomize)
{
Randomize();
}
}
private static readonly PBEAlphabeticalList secretSwordArray = new(new PBEMove[1] { PBEMove.SecretSword });
private void SetAlloweds()
{
// Set alloweds
int i;
IReadOnlyCollection legalMoves = PBEDataProvider.Instance.GetLegalMoves(_species, _form, _level);
var allowed = new List(legalMoves.Count + 1);
allowed.AddRange(legalMoves);
if (_species == PBESpecies.Keldeo && _form == PBEForm.Keldeo_Resolute)
{
_list[0].Allowed.Reset(secretSwordArray);
allowed.Remove(PBEMove.SecretSword);
i = 1;
}
else
{
i = 0;
}
for (; i < Settings.NumMoves; i++)
{
if (i == 1)
{
allowed.Insert(0, PBEMove.None);
}
_list[i].Allowed.Reset(allowed);
}
// Remove unalloweds (slot.Move setter will automatically sort PBEMove.None)
while (true)
{
int bad = Array.FindIndex(_list, s => !s.Allowed.Contains(s.Move));
if (bad == -1)
{
break;
}
else
{
PBELegalMovesetSlot slot = _list[bad];
slot.Move = slot.Allowed[0];
}
}
SetEditables();
}
private void SetEditables()
{
for (int i = 2; i < Settings.NumMoves; i++)
{
_list[i].IsMoveEditable = _list[i - 1].Move != PBEMove.None;
}
}
/// Sets every move slot excluding the first to with 0 PP-Ups.
public void Clear()
{
for (int i = 1; i < Settings.NumMoves; i++)
{
_list[i].Move = PBEMove.None;
}
SetEditables();
}
public bool Contains(PBEMove move)
{
return this[move] is not null;
}
/// Randomizes the move and PP-Ups in each slot without creating duplicate moves.
public void Randomize()
{
var blacklist = new List(Settings.NumMoves) { PBEMove.None };
for (int i = 0; i < Settings.NumMoves; i++)
{
PBELegalMovesetSlot slot = _list[i];
PBEMove[] allowed = slot.Allowed.Except(blacklist).ToArray();
if (allowed.Length == 0)
{
for (int j = i; j < Settings.NumMoves; j++)
{
_list[j].Move = PBEMove.None;
}
break;
}
else
{
PBEMove move = PBEDataProvider.GlobalRandom.RandomElement(allowed);
if (i < Settings.NumMoves - 1)
{
blacklist.Add(move);
}
slot.Move = move;
slot.PPUps = (byte)PBEDataProvider.GlobalRandom.RandomInt(0, Settings.MaxPPUps);
}
}
SetEditables();
}
public IEnumerator GetEnumerator()
{
for (int i = 0; i < Settings.NumMoves; i++)
{
yield return _list[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
================================================
FILE: PokemonBattleEngine/Data/Legality/LegalPokemon.cs
================================================
using Kermalis.EndianBinaryIO;
using Kermalis.PokemonBattleEngine.Data.Utils;
using Kermalis.PokemonBattleEngine.Utils;
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.Json.Nodes;
namespace Kermalis.PokemonBattleEngine.Data.Legality;
public sealed class PBELegalPokemon : IPBEPokemon, INotifyPropertyChanged
{
private void OnPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler? PropertyChanged;
public PBESettings Settings { get; }
private IPBEPokemonData _pData;
public PBEAlphabeticalList SelectableAbilities { get; } = new();
public PBEAlphabeticalList SelectableForms { get; } = new();
public PBEAlphabeticalList SelectableGenders { get; } = new();
public PBEAlphabeticalList SelectableItems { get; } = new();
public bool PBEIgnore => false;
private PBESpecies _species;
public PBESpecies Species
{
get => _species;
set
{
if (_species != value)
{
PBEDataUtils.ValidateSpecies(value, 0, true);
PBESpecies oldSpecies = _species;
_species = value;
_form = 0;
OnPropertyChanged(nameof(Species));
OnSpeciesChanged(oldSpecies);
OnPropertyChanged(nameof(Form));
}
}
}
private PBEForm _form;
public PBEForm Form
{
get => _form;
set
{
if (_form != value)
{
PBEDataUtils.ValidateSpecies(_species, value, true);
_form = value;
OnPropertyChanged(nameof(Form));
OnFormChanged();
}
}
}
private string _nickname;
public string Nickname
{
get => _nickname;
set
{
if (_nickname != value)
{
PBEDataUtils.ValidateNickname(value, Settings);
_nickname = value;
OnPropertyChanged(nameof(Nickname));
}
}
}
private byte _level;
public byte Level
{
get => _level;
set
{
if (_level != value)
{
PBEDataUtils.ValidateLevel(value, Settings);
_level = value;
OnPropertyChanged(nameof(Level));
Moveset.Level = value;
EXP = PBEDataProvider.Instance.GetEXPRequired(_pData.GrowthRate, value);
}
}
}
private uint _exp;
public uint EXP
{
get => _exp;
set
{
if (_exp != value)
{
_level = PBEDataProvider.Instance.GetEXPLevel(_pData.GrowthRate, value);
_exp = value;
OnPropertyChanged(nameof(EXP));
OnPropertyChanged(nameof(Level));
Moveset.Level = _level;
}
}
}
private byte _friendship;
public byte Friendship
{
get => _friendship;
set
{
if (value != _friendship)
{
_friendship = value;
OnPropertyChanged(nameof(Friendship));
}
}
}
private bool _shiny;
public bool Shiny
{
get => _shiny;
set
{
if (value != _shiny)
{
_shiny = value;
OnPropertyChanged(nameof(Shiny));
}
}
}
private bool _pokerus;
public bool Pokerus
{
get => _pokerus;
set
{
if (value != _pokerus)
{
_pokerus = value;
OnPropertyChanged(nameof(Pokerus));
}
}
}
private PBEAbility _ability;
public PBEAbility Ability
{
get => _ability;
set
{
if (value != _ability)
{
PBEDataUtils.ValidateAbility(SelectableAbilities, value);
_ability = value;
OnPropertyChanged(nameof(Ability));
}
}
}
private PBENature _nature;
public PBENature Nature
{
get => _nature;
set
{
if (value != _nature)
{
PBEDataUtils.ValidateNature(value);
_nature = value;
OnPropertyChanged(nameof(Nature));
}
}
}
private PBEItem _caughtBall;
public PBEItem CaughtBall
{
get => _caughtBall;
set
{
if (value != _caughtBall)
{
PBEDataUtils.ValidateCaughtBall(value);
_caughtBall = value;
OnPropertyChanged(nameof(CaughtBall));
}
}
}
private PBEGender _gender;
public PBEGender Gender
{
get => _gender;
set
{
if (value != _gender)
{
PBEDataUtils.ValidateGender(SelectableGenders, value);
_gender = value;
OnPropertyChanged(nameof(Gender));
}
}
}
private PBEItem _item;
public PBEItem Item
{
get => _item;
set
{
if (value != _item)
{
PBEDataUtils.ValidateItem(SelectableItems, value);
_item = value;
OnPropertyChanged(nameof(Item));
}
}
}
public PBELegalEffortValues EffortValues { get; }
IPBEStatCollection IPBEPokemon.EffortValues => EffortValues;
public PBELegalIndividualValues IndividualValues { get; }
IPBEReadOnlyStatCollection IPBEPokemon.IndividualValues => IndividualValues;
public PBELegalMoveset Moveset { get; }
IPBEMoveset IPBEPokemon.Moveset => Moveset;
internal PBELegalPokemon(PBESettings settings, EndianBinaryReader r)
{
Settings = settings;
PBESpecies species = r.ReadEnum();
PBEForm form = r.ReadEnum();
PBEDataUtils.ValidateSpecies(species, form, true);
_species = species;
_form = form;
SetSelectable();
string nickname = r.ReadString_NullTerminated();
PBEDataUtils.ValidateNickname(nickname, Settings);
_nickname = nickname;
byte level = r.ReadByte();
PBEDataUtils.ValidateLevel(level, Settings);
_level = level;
uint exp = r.ReadUInt32();
PBEDataUtils.ValidateEXP(_pData!.GrowthRate, exp, level);
_exp = exp;
_friendship = r.ReadByte();
_shiny = r.ReadBoolean();
_pokerus = r.ReadBoolean();
PBEAbility ability = r.ReadEnum();
PBEDataUtils.ValidateAbility(SelectableAbilities, ability);
_ability = ability;
PBENature nature = r.ReadEnum();
PBEDataUtils.ValidateNature(nature);
_nature = nature;
PBEItem caughtBall = r.ReadEnum();
PBEDataUtils.ValidateCaughtBall(caughtBall);
_caughtBall = caughtBall;
PBEGender gender = r.ReadEnum();
PBEDataUtils.ValidateGender(SelectableGenders, gender);
_gender = gender;
PBEItem item = r.ReadEnum();
PBEDataUtils.ValidateItem(SelectableItems, item);
_item = item;
EffortValues = new PBELegalEffortValues(Settings, r);
IndividualValues = new PBELegalIndividualValues(Settings, r);
Moveset = new PBELegalMoveset(species, form, level, Settings, new PBEReadOnlyMoveset(r));
}
internal PBELegalPokemon(PBESettings settings, JsonObject jObj)
{
Settings = settings;
_friendship = jObj.GetSafe(nameof(Friendship)).GetValue();
_shiny = jObj.GetSafe(nameof(Shiny)).GetValue();
_pokerus = jObj.GetSafe(nameof(Pokerus)).GetValue();
byte level = jObj.GetSafe(nameof(Level)).GetValue