This commit is contained in:
@@ -8,16 +8,10 @@ namespace PkmnLib.Dynamic.Events;
|
||||
/// For example, when a Pokemon gets hurt by poison, we want to show the purple poison animation and the damage at the
|
||||
/// same time. This is done by batching the events together.
|
||||
/// </remarks>
|
||||
public readonly record struct EventBatchId
|
||||
public readonly record struct EventBatchId()
|
||||
{
|
||||
/// <inheritdoc cref="EventBatchId"/>
|
||||
public EventBatchId()
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The unique identifier for this batch of events.
|
||||
/// </summary>
|
||||
public Guid Id { get; init; }
|
||||
public Guid Id { get; init; } = Guid.NewGuid();
|
||||
}
|
||||
36
PkmnLib.Dynamic/Events/StatusChangeEvent.cs
Normal file
36
PkmnLib.Dynamic/Events/StatusChangeEvent.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using PkmnLib.Dynamic.Models;
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an event that occurs when a Pokémon's status changes.
|
||||
/// </summary>
|
||||
public record StatusChangeEvent : IEventData
|
||||
{
|
||||
/// <inheritdoc cref="StatusChangeEvent"/>
|
||||
public StatusChangeEvent(IPokemon pokemon, StringKey? previousStatus, StringKey? newStatus)
|
||||
{
|
||||
Pokemon = pokemon;
|
||||
PreviousStatus = previousStatus;
|
||||
NewStatus = newStatus;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Pokémon whose status has changed.
|
||||
/// </summary>
|
||||
public IPokemon Pokemon { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The new status of the Pokémon after the change.
|
||||
/// </summary>
|
||||
public StringKey? NewStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The previous status of the Pokémon before the change.
|
||||
/// </summary>
|
||||
public StringKey? PreviousStatus { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public EventBatchId BatchId { get; init; }
|
||||
}
|
||||
18
PkmnLib.Dynamic/Events/TerrainChangeEvent.cs
Normal file
18
PkmnLib.Dynamic/Events/TerrainChangeEvent.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.Events;
|
||||
|
||||
public class TerrainChangeEvent : IEventData
|
||||
{
|
||||
public StringKey? OldTerrain { get; }
|
||||
public StringKey? NewTerrain { get; }
|
||||
|
||||
public TerrainChangeEvent(StringKey? oldTerrain, StringKey? newTerrain)
|
||||
{
|
||||
OldTerrain = oldTerrain;
|
||||
NewTerrain = newTerrain;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public EventBatchId BatchId { get; init; }
|
||||
}
|
||||
18
PkmnLib.Dynamic/Events/WeatherChangeEvent.cs
Normal file
18
PkmnLib.Dynamic/Events/WeatherChangeEvent.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using PkmnLib.Static.Utils;
|
||||
|
||||
namespace PkmnLib.Dynamic.Events;
|
||||
|
||||
public class WeatherChangeEvent : IEventData
|
||||
{
|
||||
public StringKey? OldWeather { get; }
|
||||
public StringKey? NewWeather { get; }
|
||||
|
||||
public WeatherChangeEvent(StringKey? oldWeather, StringKey? newWeather)
|
||||
{
|
||||
OldWeather = oldWeather;
|
||||
NewWeather = newWeather;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public EventBatchId BatchId { get; init; }
|
||||
}
|
||||
@@ -106,6 +106,11 @@ public class SerializedForm
|
||||
/// <inheritdoc cref="PkmnLib.Static.Species.IForm.Flags"/>
|
||||
public string[] Flags { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Check if the form is a battle-only form, meaning it should return to its original form after the battle ends.
|
||||
/// </summary>
|
||||
public bool IsBattleOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional data that is not part of the standard form data.
|
||||
/// </summary>
|
||||
|
||||
@@ -108,7 +108,7 @@ public static class SpeciesDataLoader
|
||||
return new FormImpl(name, form.Height, form.Weight, form.BaseExp, types, DeserializeStats(form.BaseStats),
|
||||
form.Abilities.Select(x => new StringKey(x)).ToList(),
|
||||
form.HiddenAbilities.Select(x => new StringKey(x)).ToList(), DeserializeMoves(form.Moves),
|
||||
form.Flags.Select(x => new StringKey(x)).ToImmutableHashSet());
|
||||
form.Flags.Select(x => new StringKey(x)).ToImmutableHashSet(), form.IsBattleOnly || form.IsMega);
|
||||
}
|
||||
|
||||
private static ILearnableMoves DeserializeMoves(SerializedMoves moves)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -5,6 +5,11 @@ namespace PkmnLib.Dynamic.ScriptHandling;
|
||||
/// </summary>
|
||||
public interface ILimitedTurnsScript
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of turns remaining for the script to last.
|
||||
/// </summary>
|
||||
public int TurnsRemaining { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the number of turns the script will last.
|
||||
/// </summary>
|
||||
|
||||
@@ -371,6 +371,14 @@ public abstract class Script : IDeepCloneable
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function allows a script to change the damage modifier of an incoming move.
|
||||
/// </summary>
|
||||
public virtual void ChangeIncomingMoveDamageModifier(IExecutingMove executingMove, IPokemon target, byte hitNumber,
|
||||
ref float modifier)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function allows a script to modify the outgoing damage done by a move.
|
||||
/// </summary>
|
||||
@@ -740,4 +748,16 @@ public abstract class Script : IDeepCloneable
|
||||
public virtual void IsFloating(IPokemon pokemon, ref bool isFloating)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function allows a script to prevent the weather from changing. This is used for abilities such as
|
||||
/// Delta Stream, which prevent the weather from changing to anything other than strong winds.
|
||||
/// </summary>
|
||||
public virtual void PreventWeatherChange(StringKey? weatherName, ref bool preventWeatherChange)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void OnWeatherChange(IBattle battle, StringKey? weatherName, StringKey? oldWeatherName)
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user