Implements more abilities
All checks were successful
Build / Build (push) Successful in 47s

This commit is contained in:
2025-06-09 12:10:25 +02:00
parent af0126e413
commit 00005aa4bf
50 changed files with 80425 additions and 20485 deletions

View File

@@ -13,7 +13,7 @@ namespace PkmnLib.Dynamic.Models;
/// A battle is a representation of a battle in the Pokemon games. It contains all the information needed
/// to simulate a battle, and can be used to simulate a battle between two parties.
/// </summary>
public interface IBattle : IScriptSource, IDeepCloneable
public interface IBattle : IScriptSource, IDeepCloneable, IDisposable
{
/// <summary>
/// The library the battle uses for handling.
@@ -40,6 +40,12 @@ public interface IBattle : IScriptSource, IDeepCloneable
/// </summary>
byte PositionsPerSide { get; }
/// <summary>
/// Whether this battle is a wild battle. In a wild battle, the player can catch the opposing Pokemon,
/// and moves like roar will end the battle instead of switching out the Pokemon.
/// </summary>
bool IsWildBattle { get; }
/// <summary>
/// A list of all sides in the battle.
/// </summary>
@@ -93,6 +99,11 @@ public interface IBattle : IScriptSource, IDeepCloneable
/// </summary>
void ValidateBattleState();
/// <summary>
/// Forcefully ends the battle. This will set the result to inconclusive and set HasEnded to true.
/// </summary>
void ForceEndBattle();
/// <summary>
/// Checks whether a Pokemon has a forced turn choice. If it does, this returns true and the choice
/// is set in the out parameter. If it does not, this returns false and the out parameter is null.
@@ -119,7 +130,7 @@ public interface IBattle : IScriptSource, IDeepCloneable
/// A duration can be passed to set the duration of the weather in turns. This duration can be modified by
/// other scripts before the weather is set through the <see cref="Script.ChangeWeatherDuration"/> script hook.
/// </summary>
bool SetWeather(StringKey? weatherName, int duration);
bool SetWeather(StringKey? weatherName, int duration, EventBatchId batchId = default);
/// <summary>
/// Volatile scripts are scripts that are not permanent and can be removed by other scripts.
@@ -134,8 +145,7 @@ public interface IBattle : IScriptSource, IDeepCloneable
/// <summary>
/// Sets the current terrain for the battle. If null is passed, this clears the terrain.
/// </summary>
/// <param name="terrainName"></param>
void SetTerrain(StringKey? terrainName);
void SetTerrain(StringKey? terrainName, EventBatchId batchId = default);
/// <summary>
/// Gets the current terrain of the battle. If no terrain is present, this returns null.
@@ -165,13 +175,14 @@ public class BattleImpl : ScriptSource, IBattle
/// <param name="positionsPerSide">The number of spots there are on each side for Pokémon. 1 for singles, 2 for doubles, etc.</param>
/// <param name="randomSeed">The seed for the RNG. If null, this uses a time-dependent seed.</param>
public BattleImpl(IDynamicLibrary library, IReadOnlyList<IBattleParty> parties, bool canFlee, byte numberOfSides,
byte positionsPerSide, int? randomSeed = null)
byte positionsPerSide, bool isWildBattle, int? randomSeed = null)
{
Library = library;
Parties = parties;
CanFlee = canFlee;
NumberOfSides = numberOfSides;
PositionsPerSide = positionsPerSide;
IsWildBattle = isWildBattle;
Volatile = new ScriptSet(this);
var sides = new IBattleSide[numberOfSides];
for (byte i = 0; i < numberOfSides; i++)
@@ -196,6 +207,9 @@ public class BattleImpl : ScriptSource, IBattle
/// <inheritdoc />
public byte PositionsPerSide { get; }
/// <inheritdoc />
public bool IsWildBattle { get; }
/// <inheritdoc />
public IReadOnlyList<IBattleSide> Sides { get; }
@@ -263,6 +277,13 @@ public class BattleImpl : ScriptSource, IBattle
HasEnded = true;
}
/// <inheritdoc />
public void ForceEndBattle()
{
HasEnded = true;
Result = BattleResult.Inconclusive;
}
/// <inheritdoc />
public bool HasForcedTurn(IPokemon pokemon, [NotNullWhen(true)] out ITurnChoice? choice)
{
@@ -376,10 +397,28 @@ public class BattleImpl : ScriptSource, IBattle
public IReadOnlyScriptContainer WeatherScript => _weatherScript;
/// <inheritdoc />
public bool SetWeather(StringKey? weatherName, int duration)
public bool SetWeather(StringKey? weatherName, int duration, EventBatchId batchId = default)
{
var preventWeatherChange = false;
this.RunScriptHook(x => x.PreventWeatherChange(weatherName, ref preventWeatherChange));
if (preventWeatherChange)
return false;
var oldWeatherName = WeatherScript.Script?.Name;
if (weatherName.HasValue)
{
if (weatherName == oldWeatherName)
{
// Extend duration of existing weather
if (_weatherScript.Script is ILimitedTurnsScript existingWeatherScript)
{
this.RunScriptHook(x => x.ChangeWeatherDuration(weatherName.Value, ref duration));
if (duration < existingWeatherScript.TurnsRemaining)
return true;
existingWeatherScript.SetTurns(duration);
}
return true;
}
if (!Library.ScriptResolver.TryResolve(ScriptCategory.Weather, weatherName.Value, null, out var script))
throw new InvalidOperationException($"Weather script {weatherName} not found.");
@@ -396,8 +435,13 @@ public class BattleImpl : ScriptSource, IBattle
{
_weatherScript.Clear();
}
EventHook.Invoke(new WeatherChangeEvent(oldWeatherName, weatherName)
{
BatchId = batchId,
});
Sides.SelectMany(x => x.Pokemon).WhereNotNull()
.RunScriptHook(x => x.OnWeatherChange(this, weatherName, oldWeatherName));
return true;
// TODO: Trigger weather change script hooks
}
/// <inheritdoc />
@@ -409,12 +453,14 @@ public class BattleImpl : ScriptSource, IBattle
private readonly ScriptContainer _terrainScript = new();
/// <inheritdoc />
public void SetTerrain(StringKey? terrainName)
public void SetTerrain(StringKey? terrainName, EventBatchId batchId = default)
{
var oldTerrainName = TerrainName;
if (terrainName.HasValue)
{
if (!Library.ScriptResolver.TryResolve(ScriptCategory.Terrain, terrainName.Value, null, out var script))
throw new InvalidOperationException($"Terrain script {terrainName} not found.");
_terrainScript.Set(script);
script.OnAddedToParent(this);
}
@@ -422,6 +468,10 @@ public class BattleImpl : ScriptSource, IBattle
{
_terrainScript.Clear();
}
EventHook.Invoke(new TerrainChangeEvent(oldTerrainName, terrainName)
{
BatchId = batchId,
});
}
/// <inheritdoc />
@@ -463,4 +513,19 @@ public class BattleImpl : ScriptSource, IBattle
/// <inheritdoc />
public override void CollectScripts(List<IEnumerable<ScriptContainer>> scripts) => GetOwnScripts(scripts);
/// <inheritdoc />
public void Dispose()
{
foreach (var party in Parties)
{
foreach (var pokemon in party.Party.WhereNotNull())
{
pokemon.ClearBattleData();
}
}
_weatherScript.Clear();
_terrainScript.Clear();
Volatile.Clear();
}
}

View File

@@ -35,4 +35,9 @@ public enum DamageSource
/// The damage is done because of a status condition.
/// </summary>
Status = 5,
/// <summary>
/// The damage is done due to the Pokémon being confused and hitting itself.
/// </summary>
Confusion = 6,
}

View File

@@ -354,12 +354,12 @@ public interface IPokemon : IScriptSource, IDeepCloneable
/// <summary>
/// Adds a non-volatile status to the Pokemon.
/// </summary>
bool SetStatus(StringKey status);
bool SetStatus(StringKey status, EventBatchId batchId = default);
/// <summary>
/// Removes the current non-volatile status from the Pokemon.
/// </summary>
void ClearStatus();
void ClearStatus(EventBatchId batchId = default);
/// <summary>
/// Modifies the level by a certain amount
@@ -382,6 +382,11 @@ public interface IPokemon : IScriptSource, IDeepCloneable
/// <param name="position"></param>
void SetBattleSidePosition(byte position);
/// <summary>
/// Resets the battle data of the Pokémon. This is called when the battle ends.
/// </summary>
void ClearBattleData();
/// <summary>
/// Marks a Pokemon as seen in the battle.
/// </summary>
@@ -475,6 +480,16 @@ public interface IPokemonBattleData : IDeepCloneable
/// The side the Pokémon is on.
/// </summary>
IBattleSide BattleSide { get; }
/// <summary>
/// The species of the Pokémon at the time it was sent out.
/// </summary>
ISpecies OriginalSpecies { get; }
/// <summary>
/// The form of the Pokémon at the time it was sent out.
/// </summary>
IForm OriginalForm { get; }
}
/// <inheritdoc cref="IPokemon"/>
@@ -928,7 +943,7 @@ public class PokemonImpl : ScriptSource, IPokemon
}
/// <inheritdoc />
public void ChangeForm(IForm form, EventBatchId batchId)
public void ChangeForm(IForm form, EventBatchId batchId = default)
{
if (form == Form)
return;
@@ -1120,13 +1135,14 @@ public class PokemonImpl : ScriptSource, IPokemon
public bool HasStatus(StringKey status) => StatusScript.Script?.Name == status;
/// <inheritdoc />
public bool SetStatus(StringKey status)
public bool SetStatus(StringKey status, EventBatchId batchId = default)
{
if (!Library.ScriptResolver.TryResolve(ScriptCategory.Status, status, null, out var statusScript))
throw new KeyNotFoundException($"Status script {status} not found");
if (!StatusScript.IsEmpty)
return false;
var oldStatus = StatusScript.Script?.Name;
var preventStatus = false;
this.RunScriptHook(script => script.PreventStatusChange(this, status, ref preventStatus));
@@ -1135,11 +1151,22 @@ public class PokemonImpl : ScriptSource, IPokemon
StatusScript.Set(statusScript);
statusScript.OnAddedToParent(this);
BattleData?.Battle.EventHook.Invoke(new StatusChangeEvent(this, oldStatus, status)
{
BatchId = batchId,
});
return true;
}
/// <inheritdoc />
public void ClearStatus() => StatusScript.Clear();
public void ClearStatus(EventBatchId batchId = default)
{
StatusScript.Clear();
BattleData?.Battle.EventHook.Invoke(new StatusChangeEvent(this, StatusScript.Script?.Name, null)
{
BatchId = batchId,
});
}
/// <inheritdoc />
public void ChangeLevelBy(int change)
@@ -1160,7 +1187,17 @@ public class PokemonImpl : ScriptSource, IPokemon
}
else
{
BattleData = new PokemonBattleDataImpl(battle, sideIndex, battle.CurrentTurnNumber);
BattleData = new PokemonBattleDataImpl(battle, sideIndex, battle.CurrentTurnNumber, Species, Form);
}
if (ActiveAbility != null && Library.ScriptResolver.TryResolve(ScriptCategory.Ability, ActiveAbility.Name,
ActiveAbility.Parameters, out var abilityScript))
{
AbilityScript.Set(abilityScript);
abilityScript.OnAddedToParent(this);
}
else
{
AbilityScript.Clear();
}
}
@@ -1192,6 +1229,24 @@ public class PokemonImpl : ScriptSource, IPokemon
}
}
/// <inheritdoc />
public void ClearBattleData()
{
var battleData = BattleData;
BattleData = null;
Volatile.Clear();
WeightInKg = Form.Weight;
HeightInMeters = Form.Height;
Types = Form.Types;
OverrideAbility = null;
AbilitySuppressed = false;
StatBoost.Reset();
if (battleData != null && Form.IsBattleOnlyForm)
{
ChangeForm(battleData.OriginalSpecies == Species ? battleData.OriginalForm : Species.GetDefaultForm());
}
}
/// <inheritdoc />
public void MarkOpponentAsSeen(IPokemon pokemon) => BattleData?.MarkOpponentAsSeen(pokemon);
@@ -1295,11 +1350,14 @@ public class PokemonImpl : ScriptSource, IPokemon
public class PokemonBattleDataImpl : IPokemonBattleData
{
/// <inheritdoc cref="PokemonBattleDataImpl"/>
public PokemonBattleDataImpl(IBattle battle, byte sideIndex, uint switchInTurn)
public PokemonBattleDataImpl(IBattle battle, byte sideIndex, uint switchInTurn, ISpecies originalSpecies,
IForm originalForm)
{
Battle = battle;
SideIndex = sideIndex;
SwitchInTurn = switchInTurn;
OriginalSpecies = originalSpecies;
OriginalForm = originalForm;
}
/// <inheritdoc />
@@ -1342,4 +1400,10 @@ public class PokemonBattleDataImpl : IPokemonBattleData
/// <inheritdoc />
public IBattleSide BattleSide => Battle.Sides[SideIndex];
/// <inheritdoc />
public ISpecies OriginalSpecies { get; }
/// <inheritdoc />
public IForm OriginalForm { get; }
}