Implementation of Pokeballs

This commit is contained in:
Deukhoofd 2025-01-10 11:58:23 +01:00
parent 0518499a4c
commit 42e3273483
Signed by: Deukhoofd
GPG Key ID: F63E044490819F6F
15 changed files with 254 additions and 12 deletions

View File

@ -0,0 +1,19 @@
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
namespace PkmnLib.Dynamic.Events;
public class CaptureAttemptEvent : IEventData
{
public CaptureAttemptEvent(IPokemon target, CaptureResult result)
{
Target = target;
Result = result;
}
public IPokemon Target { get; init; }
public CaptureResult Result { get; init; }
/// <inheritdoc />
public EventBatchId BatchId { get; init; }
}

View File

@ -0,0 +1,25 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Static;
namespace PkmnLib.Dynamic.Libraries;
public record struct CaptureResult
{
public CaptureResult(bool IsCaught, int Shakes, bool CriticalCapture)
{
this.IsCaught = IsCaught;
this.Shakes = Shakes;
this.CriticalCapture = CriticalCapture;
}
public bool IsCaught { get; init; }
public int Shakes { get; init; }
public bool CriticalCapture { get; init; }
public static CaptureResult Failed => new CaptureResult(false, 0, false);
}
public interface ICaptureLibrary
{
CaptureResult TryCapture(IPokemon target, IItem captureItem, IBattleRandom random);
}

View File

@ -32,6 +32,11 @@ public interface IDynamicLibrary
/// </summary>
IMiscLibrary MiscLibrary { get; }
/// <summary>
/// The capture library deals with the calculation of the capture rate of a Pokémon.
/// </summary>
ICaptureLibrary CaptureLibrary { get; }
/// <summary>
/// A holder of the script types that can be resolved by this library.
/// </summary>
@ -58,19 +63,22 @@ public class DynamicLibraryImpl : IDynamicLibrary
throw new InvalidOperationException("Stat calculator not found in plugins.");
if (registry.MiscLibrary is null)
throw new InvalidOperationException("Misc library not found in plugins.");
if (registry.CaptureLibrary is null)
throw new InvalidOperationException("Capture library not found in plugins.");
var scriptResolver = new ScriptResolver(registry.ScriptTypes, registry.ItemScriptTypes);
return new DynamicLibraryImpl(staticLibrary, registry.BattleStatCalculator,
registry.DamageCalculator, registry.MiscLibrary, scriptResolver);
registry.DamageCalculator, registry.MiscLibrary, registry.CaptureLibrary, scriptResolver);
}
private DynamicLibraryImpl(IStaticLibrary staticLibrary, IBattleStatCalculator statCalculator,
IDamageCalculator damageCalculator, IMiscLibrary miscLibrary, ScriptResolver scriptResolver)
IDamageCalculator damageCalculator, IMiscLibrary miscLibrary, ICaptureLibrary captureLibrary, ScriptResolver scriptResolver)
{
StaticLibrary = staticLibrary;
StatCalculator = statCalculator;
DamageCalculator = damageCalculator;
MiscLibrary = miscLibrary;
ScriptResolver = scriptResolver;
CaptureLibrary = captureLibrary;
}
/// <inheritdoc />
@ -85,6 +93,9 @@ public class DynamicLibraryImpl : IDynamicLibrary
/// <inheritdoc />
public IMiscLibrary MiscLibrary { get; }
/// <inheritdoc />
public ICaptureLibrary CaptureLibrary { get; }
/// <inheritdoc />
public ScriptResolver ScriptResolver { get; }
}

View File

@ -3,6 +3,7 @@ using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models.BattleFlow;
using PkmnLib.Dynamic.Models.Choices;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Static;
using PkmnLib.Static.Utils;
namespace PkmnLib.Dynamic.Models;
@ -116,6 +117,8 @@ public interface IBattle : IScriptSource, IDeepCloneable
/// for a single turn. The outer list is ordered from oldest to newest turn.
/// </summary>
IReadOnlyList<IReadOnlyList<ITurnChoice>> PreviousTurnChoices { get; }
CaptureResult AttempCapture(byte sideIndex, byte position, IItem item);
}
/// <inheritdoc cref="IBattle"/>
@ -335,6 +338,24 @@ public class BattleImpl : ScriptSource, IBattle
/// <inheritdoc />
public IReadOnlyList<IReadOnlyList<ITurnChoice>> PreviousTurnChoices => _previousTurnChoices;
/// <inheritdoc />
public CaptureResult AttempCapture(byte sideIndex, byte position, IItem item)
{
var target = GetPokemon(sideIndex, position);
if (target is not { IsUsable: true })
return CaptureResult.Failed;
var attemptCapture = Library.CaptureLibrary.TryCapture(target, item, Random);
if (attemptCapture.IsCaught)
{
target.MarkAsCaught();
var side = Sides[target.BattleData!.SideIndex];
side.ForceClearPokemonFromField(target.BattleData.Position);
}
EventHook.Invoke(new CaptureAttemptEvent(target, attemptCapture));
return attemptCapture;
}
/// <inheritdoc />
public override int ScriptCount => 2;

View File

@ -217,6 +217,13 @@ public class BattleSideImpl : ScriptSource, IBattleSide
/// <inheritdoc />
public void ForceClearPokemonFromField(byte index)
{
var pokemon = _pokemon[index];
if (pokemon is not null)
{
pokemon.RunScriptHook(script => script.OnRemove());
pokemon.SetOnBattlefield(false);
}
_pokemon[index] = null;
}

View File

@ -202,6 +202,8 @@ public interface IPokemon : IScriptSource, IDeepCloneable
/// </summary>
bool IsCaught { get; }
public void MarkAsCaught();
/// <summary>
/// The script for the held item.
/// </summary>
@ -632,6 +634,12 @@ public class PokemonImpl : ScriptSource, IPokemon
/// <inheritdoc />
public bool IsCaught { get; private set; }
/// <inheritdoc />
public void MarkAsCaught()
{
IsCaught = true;
}
/// <inheritdoc />
public ScriptContainer HeldItemTriggerScript { get; } = new();

View File

@ -1,10 +1,19 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Static;
using PkmnLib.Static.Utils;
namespace PkmnLib.Dynamic.ScriptHandling;
public abstract class ItemScript : IDeepCloneable
{
protected ItemScript(IItem item)
{
Item = item;
}
protected IItem Item { get; private set; }
/// <summary>
/// Initializes the script with the given parameters for a specific item
/// </summary>

View File

@ -0,0 +1,24 @@
using PkmnLib.Dynamic.Models;
using PkmnLib.Static;
namespace PkmnLib.Dynamic.ScriptHandling;
public abstract class PokeballScript : ItemScript
{
/// <inheritdoc />
protected PokeballScript(IItem item) : base(item)
{
}
public abstract byte GetCatchRate(IPokemon target);
/// <inheritdoc />
public override void OnUseWithTarget(IPokemon target)
{
var battleData = target.BattleData;
if (battleData == null)
return;
battleData.Battle.AttempCapture(battleData.SideIndex, battleData.Position, Item);
}
}

View File

@ -2,6 +2,7 @@ using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Static;
using PkmnLib.Static.Utils;
namespace PkmnLib.Dynamic.ScriptHandling.Registry;
@ -13,10 +14,11 @@ namespace PkmnLib.Dynamic.ScriptHandling.Registry;
public class ScriptRegistry
{
private readonly Dictionary<(ScriptCategory category, StringKey name), Func<Script>> _scriptTypes = new();
private readonly Dictionary<StringKey, Func<ItemScript>> _itemScriptTypes = new();
private readonly Dictionary<StringKey, Func<IItem, ItemScript>> _itemScriptTypes = new();
private IBattleStatCalculator? _battleStatCalculator;
private IDamageCalculator? _damageCalculator;
private IMiscLibrary? _miscLibrary;
private ICaptureLibrary? _captureLibrary;
/// <summary>
/// Automatically register all scripts in the given assembly that have the <see cref="ScriptAttribute"/>, and
@ -73,13 +75,15 @@ public class ScriptRegistry
throw new ArgumentNullException(nameof(type));
var constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
null, Type.EmptyTypes, null);
null, [typeof(IItem)], null);
if (constructor == null)
throw new ArgumentException($"Type {type} does not have a parameterless constructor.");
throw new ArgumentException($"Type {type} does not have a constructor that takes an IItem.");
// We create a lambda that creates a new instance of the script type.
// This is more performant than using Activator.CreateInstance.
_itemScriptTypes[name] = Expression.Lambda<Func<ItemScript>>(Expression.New(constructor)).Compile();
var parameterExpression = Expression.Parameter(typeof(IItem), "item");
var newExpression = Expression.New(constructor, parameterExpression);
_itemScriptTypes[name] = Expression.Lambda<Func<IItem, ItemScript>>(newExpression, parameterExpression).Compile();
}
/// <summary>
@ -100,9 +104,16 @@ public class ScriptRegistry
public void RegisterMiscLibrary<T>(T miscLibrary) where T : IMiscLibrary
=> _miscLibrary = miscLibrary;
/// <summary>
/// Register a capture library.
/// </summary>
public void RegisterCaptureLibrary<T>(T captureLibrary) where T : ICaptureLibrary
=> _captureLibrary = captureLibrary;
internal IReadOnlyDictionary<(ScriptCategory category, StringKey name), Func<Script>> ScriptTypes => _scriptTypes;
internal IReadOnlyDictionary<StringKey, Func<ItemScript>> ItemScriptTypes => _itemScriptTypes;
internal IReadOnlyDictionary<StringKey, Func<IItem, ItemScript>> ItemScriptTypes => _itemScriptTypes;
internal IBattleStatCalculator? BattleStatCalculator => _battleStatCalculator;
internal IDamageCalculator? DamageCalculator => _damageCalculator;
internal IMiscLibrary? MiscLibrary => _miscLibrary;
internal ICaptureLibrary? CaptureLibrary => _captureLibrary;
}

View File

@ -493,7 +493,7 @@ public abstract class Script : IDeepCloneable
/// rate of this attempt. Pokeball modifier effects should be implemented here, as well as for
/// example status effects that change capture rates.
/// </summary>
public virtual void ChangeCaptureRateBonus(IPokemon pokemon, IItem pokeball, ref byte modifier)
public virtual void ChangeCatchRateBonus(IPokemon pokemon, IItem pokeball, ref byte modifier)
{
}
}

View File

@ -10,11 +10,12 @@ namespace PkmnLib.Dynamic.ScriptHandling;
public class ScriptResolver
{
private IReadOnlyDictionary<(ScriptCategory, StringKey), Func<Script>> _scriptCtors;
private IReadOnlyDictionary<StringKey, Func<ItemScript>> _itemScriptCtors;
private IReadOnlyDictionary<StringKey, Func<IItem, ItemScript>> _itemScriptCtors;
private readonly Dictionary<IItem, ItemScript> _itemBattleScripts = new();
/// <inheritdoc cref="ScriptResolver"/>
public ScriptResolver(IReadOnlyDictionary<(ScriptCategory, StringKey), Func<Script>> scriptCtors,
IReadOnlyDictionary<StringKey, Func<ItemScript>> itemScriptCtors)
IReadOnlyDictionary<StringKey, Func<IItem, ItemScript>> itemScriptCtors)
{
_scriptCtors = scriptCtors;
_itemScriptCtors = itemScriptCtors;
@ -46,6 +47,11 @@ public class ScriptResolver
/// </summary>
public bool TryResolveBattleItemScript(IItem item, [MaybeNullWhen(false)] out ItemScript script)
{
if (_itemBattleScripts.TryGetValue(item, out script))
{
return true;
}
var effect = item.BattleEffect;
if (effect == null)
{
@ -58,8 +64,9 @@ public class ScriptResolver
return false;
}
script = scriptCtor();
script = scriptCtor(item);
script.OnInitialize(effect.Parameters);
_itemBattleScripts[item] = script;
return true;
}
}

View File

@ -36,5 +36,6 @@ public class Gen7Plugin : Dynamic.ScriptHandling.Registry.Plugin
registry.RegisterBattleStatCalculator(new Gen7BattleStatCalculator());
registry.RegisterDamageCalculator(new Gen7DamageCalculator(_configuration.DamageCalculatorHasRandomness));
registry.RegisterMiscLibrary(new Gen7MiscLibrary());
registry.RegisterCaptureLibrary(new Gen7CaptureLibrary());
}
}

View File

@ -0,0 +1,53 @@
using System;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Static;
namespace PkmnLib.Plugin.Gen7.Libraries;
public class Gen7CaptureLibrary : ICaptureLibrary
{
/// <inheritdoc />
public CaptureResult TryCapture(IPokemon target, IItem captureItem, IBattleRandom random)
{
var maxHealth = target.BoostedStats.Hp;
var currentHealth = target.CurrentHealth;
var catchRate = target.Species.CaptureRate;
byte bonusBall = 1;
if (target.Library.ScriptResolver.TryResolveBattleItemScript(captureItem, out var script) &&
script is PokeballScript pokeballScript)
{
bonusBall = pokeballScript.GetCatchRate(target);
}
byte bonusStatus = 1;
target.RunScriptHook(x => x.ChangeCatchRateBonus(target, captureItem, ref bonusStatus));
var modifiedCatchRate =
(((3.0 * maxHealth) - (2.0 * currentHealth)) * catchRate * bonusBall) / (3.0 * maxHealth);
modifiedCatchRate *= bonusStatus;
var shakeProbability = 65536 / Math.Pow((255 / modifiedCatchRate), 0.1875);
byte shakes = 0;
if (modifiedCatchRate >= 255)
{
shakes = 4;
}
else
{
// FIXME: Implement critical capture
for (var i = 0; i < 4; i++)
{
if (random.GetInt(0, 65536) < shakeProbability)
{
shakes++;
}
}
}
var success = shakes >= 3;
return new CaptureResult(success, shakes, false);
}
}

View File

@ -2,6 +2,7 @@ using System.Collections.Generic;
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Static;
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Items;
@ -11,6 +12,11 @@ public class HealingItem : ItemScript
{
private uint _healAmount;
/// <inheritdoc />
public HealingItem(IItem item) : base(item)
{
}
/// <inheritdoc />
public override bool IsItemUsable => true;

View File

@ -0,0 +1,40 @@
using System.Collections.Generic;
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Dynamic.ScriptHandling.Registry;
using PkmnLib.Static;
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Items;
/// <summary>
/// An implementation of a pokeball script that just has a flat catch rate bonus.
/// </summary>
[ItemScript("pokeball")]
public class StaticPokeball : PokeballScript
{
private byte _catchRate;
/// <inheritdoc />
public StaticPokeball(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void OnInitialize(IReadOnlyDictionary<StringKey, object?>? parameters)
{
if (parameters == null || !parameters.TryGetValue("catchRate", out var catchRateObj) ||
catchRateObj is not byte catchRate)
{
catchRate = 1;
}
_catchRate = catchRate;
}
/// <inheritdoc />
public override byte GetCatchRate(IPokemon target)
{
return _catchRate;
}
}