Implement most pokeballs
All checks were successful
Build / Build (push) Successful in 1m2s

This commit is contained in:
Deukhoofd 2025-07-20 11:15:45 +02:00
parent db3f7f2403
commit 77d7b86a3c
Signed by: Deukhoofd
GPG Key ID: F63E044490819F6F
19 changed files with 452 additions and 38 deletions

View File

@ -106,7 +106,7 @@ public interface IPokemon : IScriptSource, IDeepCloneable
/// <summary> /// <summary>
/// The happiness of the Pokemon. Also known as friendship. /// The happiness of the Pokemon. Also known as friendship.
/// </summary> /// </summary>
byte Happiness { get; } byte Happiness { get; set; }
/// <summary> /// <summary>
/// The stats of the Pokemon when disregarding any stat boosts. /// The stats of the Pokemon when disregarding any stat boosts.
@ -708,7 +708,7 @@ public class PokemonImpl : ScriptSource, IPokemon
public float HeightInMeters { get; set; } public float HeightInMeters { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public byte Happiness { get; } public byte Happiness { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public StatisticSet<uint> FlatStats { get; } = new(); public StatisticSet<uint> FlatStats { get; } = new();

View File

@ -17,13 +17,23 @@ public abstract class PokeballScript : ItemScript
/// <summary> /// <summary>
/// Returns the catch rate of the Pokéball against the given target Pokémon. /// Returns the catch rate of the Pokéball against the given target Pokémon.
/// </summary> /// </summary>
public abstract byte GetCatchRate(IPokemon target); public abstract void ChangeCatchRate(IPokemon target, ref byte catchRate);
public virtual void OnAfterSuccessfulCapture(IPokemon target)
{
// Default implementation does nothing.
// Override this method in derived classes to add custom behavior after a successful capture.
}
/// <inheritdoc /> /// <inheritdoc />
public override void OnUseWithTarget(IPokemon target, EventHook eventHook) public override void OnUseWithTarget(IPokemon target, EventHook eventHook)
{ {
var battleData = target.BattleData; var battleData = target.BattleData;
battleData?.Battle.AttempCapture(battleData.SideIndex, battleData.Position, Item); var result = battleData?.Battle.AttempCapture(battleData.SideIndex, battleData.Position, Item);
if (result is { IsCaught: true })
{
OnAfterSuccessfulCapture(target);
}
} }
} }

View File

@ -20,6 +20,18 @@ public static class NumericHelpers
return result > byte.MaxValue ? byte.MaxValue : (byte)result; return result > byte.MaxValue ? byte.MaxValue : (byte)result;
} }
public static byte AddOrMax(this byte value, byte addend)
{
var result = value + addend;
return result > byte.MaxValue ? byte.MaxValue : (byte)result;
}
public static byte SubtractOrMin(this byte value, byte subtrahend)
{
var result = value - subtrahend;
return result < 0 ? (byte)0 : (byte)result;
}
/// <summary> /// <summary>
/// Multiplies two values. If this overflows, returns <see cref="byte.MaxValue"/>. /// Multiplies two values. If this overflows, returns <see cref="byte.MaxValue"/>.
/// </summary> /// </summary>

View File

@ -869,6 +869,12 @@
"price": -1, "price": -1,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "pokeball_flat_modifier",
"parameters": {
"catchRate": 1
}
} }
}, },
{ {
@ -1280,6 +1286,7 @@
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
} }
// TODO: implement dive_ball (How do we know when the battle is triggered while surfing, fishing, or diving?)
}, },
{ {
"name": "dna_splicers", "name": "dna_splicers",
@ -1448,6 +1455,7 @@
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
} }
// TODO: implement dusk_ball (How do we know when the battle is triggered at night or in a cave?)
}, },
{ {
"name": "dusk_stone", "name": "dusk_stone",
@ -1744,6 +1752,9 @@
"price": -1, "price": -1,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "fast_ball"
} }
}, },
{ {
@ -1974,6 +1985,9 @@
"price": -1, "price": -1,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "friend_ball"
} }
}, },
{ {
@ -2267,6 +2281,12 @@
"price": 600, "price": 600,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "pokeball_flat_modifier",
"parameters": {
"catchRate": 1.5
}
} }
}, },
{ {
@ -2493,6 +2513,9 @@
"price": -1, "price": -1,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "heavy_ball"
} }
}, },
{ {
@ -3088,6 +3111,9 @@
"price": -1, "price": -1,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "level_ball"
} }
}, },
{ {
@ -3219,6 +3245,9 @@
"price": -1, "price": -1,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "love_ball"
} }
}, },
{ {
@ -3306,6 +3335,7 @@
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
} }
// TODO: implement lure ball effect (how to know if the pokemon was encountered while fishing)
}, },
{ {
"name": "lustrous_orb", "name": "lustrous_orb",
@ -3324,6 +3354,12 @@
"price": 1000, "price": 1000,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "pokeball_flat_modifier",
"parameters": {
"catchRate": 1
}
} }
}, },
{ {
@ -3472,6 +3508,12 @@
"price": -1, "price": -1,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "pokeball_flat_modifier",
"parameters": {
"catchRate": 255
}
} }
}, },
{ {
@ -3819,6 +3861,9 @@
"price": -1, "price": -1,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "love_ball"
} }
}, },
{ {
@ -3914,6 +3959,9 @@
"price": 1000, "price": 1000,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "nest_ball"
} }
}, },
{ {
@ -3924,6 +3972,9 @@
"price": 1000, "price": 1000,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "net_ball"
} }
}, },
{ {
@ -4454,7 +4505,7 @@
"flingPower": 0 "flingPower": 0
}, },
"battleEffect": { "battleEffect": {
"name": "pokeball", "name": "pokeball_flat_modifier",
"parameters": { "parameters": {
"catchRate": 1 "catchRate": 1
} }
@ -4649,6 +4700,12 @@
"price": 20, "price": 20,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "pokeball_flat_modifier",
"parameters": {
"catchRate": 1
}
} }
}, },
{ {
@ -4816,6 +4873,9 @@
"price": 1000, "price": 1000,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "quick_ball"
} }
}, },
{ {
@ -5099,6 +5159,9 @@
"price": 1000, "price": 1000,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "repeat_ball"
} }
}, },
{ {
@ -5418,6 +5481,12 @@
"price": -1, "price": -1,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "pokeball_flat_modifier",
"parameters": {
"catchRate": 1
}
} }
}, },
{ {
@ -5903,6 +5972,12 @@
"price": 300, "price": 300,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "pokeball_flat_modifier",
"parameters": {
"catchRate": 1
}
} }
}, },
{ {
@ -6267,6 +6342,9 @@
"price": 1000, "price": 1000,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "timer_ball"
} }
}, },
{ {
@ -7285,6 +7363,12 @@
"price": 800, "price": 800,
"additionalData": { "additionalData": {
"flingPower": 0 "flingPower": 0
},
"battleEffect": {
"name": "pokeball_flat_modifier",
"parameters": {
"catchRate": 2
}
} }
}, },
{ {

View File

@ -1,4 +1,5 @@
using PkmnLib.Dynamic.Libraries; using PkmnLib.Dynamic.Libraries;
using PkmnLib.Static.Species;
namespace PkmnLib.Plugin.Gen7.Libraries.Battling; namespace PkmnLib.Plugin.Gen7.Libraries.Battling;
@ -11,6 +12,8 @@ public class Gen7CaptureLibrary : ICaptureLibrary
_configuration = configuration; _configuration = configuration;
} }
public bool HasPokemonBeenCaughtBefore(ISpecies species) => _configuration.TimesSpeciesCaught(species) > 0;
/// <inheritdoc /> /// <inheritdoc />
public CaptureResult TryCapture(IPokemon target, IItem captureItem, IBattleRandom random) public CaptureResult TryCapture(IPokemon target, IItem captureItem, IBattleRandom random)
{ {
@ -18,18 +21,17 @@ public class Gen7CaptureLibrary : ICaptureLibrary
var currentHealth = target.CurrentHealth; var currentHealth = target.CurrentHealth;
var catchRate = target.Species.CaptureRate; var catchRate = target.Species.CaptureRate;
byte bonusBall = 1;
if (target.Library.ScriptResolver.TryResolveBattleItemScript(captureItem, out var script) && if (target.Library.ScriptResolver.TryResolveBattleItemScript(captureItem, out var script) &&
script is PokeballScript pokeballScript) script is PokeballScript pokeballScript)
{ {
bonusBall = pokeballScript.GetCatchRate(target); pokeballScript.ChangeCatchRate(target, ref catchRate);
} }
byte bonusStatus = 1; byte bonusStatus = 1;
target.RunScriptHook<IScriptChangeCatchRateBonus>(x => target.RunScriptHook<IScriptChangeCatchRateBonus>(x =>
x.ChangeCatchRateBonus(target, captureItem, ref bonusStatus)); x.ChangeCatchRateBonus(target, captureItem, ref bonusStatus));
var modifiedCatchRate = (3.0f * maxHealth - 2.0f * currentHealth) * catchRate * bonusBall / (3.0f * maxHealth); var modifiedCatchRate = (3.0f * maxHealth - 2.0f * currentHealth) * catchRate / (3.0f * maxHealth);
modifiedCatchRate *= bonusStatus; modifiedCatchRate *= bonusStatus;
var shakeProbability = 65536 / Math.Pow(255 / modifiedCatchRate, 0.1875f); var shakeProbability = 65536 / Math.Pow(255 / modifiedCatchRate, 0.1875f);

View File

@ -0,0 +1,17 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
[ItemScript("fast_ball")]
public class FastBall : PokeballScript
{
/// <inheritdoc />
public FastBall(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
var modifier = target.Form.BaseStats.Speed >= 100 ? 4f : 1f;
catchRate = catchRate.MultiplyOrMax(modifier);
}
}

View File

@ -0,0 +1,21 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
[ItemScript("friend_ball")]
public class FriendBall : PokeballScript
{
/// <inheritdoc />
public FriendBall(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
}
/// <inheritdoc />
public override void OnAfterSuccessfulCapture(IPokemon target)
{
target.Happiness = 200;
}
}

View File

@ -0,0 +1,23 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
[ItemScript("heal_ball")]
public class HealBall : PokeballScript
{
/// <inheritdoc />
public HealBall(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
}
/// <inheritdoc />
public override void OnAfterSuccessfulCapture(IPokemon target)
{
target.Heal(target.MaxHealth, true, forceHeal: true);
target.ClearStatus();
target.RestoreAllPP();
}
}

View File

@ -0,0 +1,38 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
[ItemScript("heavy_ball")]
public class HeavyBall : PokeballScript
{
/// <inheritdoc />
public HeavyBall(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
var weight = target.WeightInKg;
switch (weight)
{
case < 100:
{
catchRate.SubtractOrMin(20);
break;
}
case < 200:
{
break;
}
case < 300:
{
catchRate.AddOrMax(20);
break;
}
default:
{
catchRate.AddOrMax(30);
break;
}
}
}
}

View File

@ -0,0 +1,31 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
[ItemScript("level_ball")]
public class LevelBall : PokeballScript
{
/// <inheritdoc />
public LevelBall(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
if (target.BattleData is null)
return;
var opponentSide = target.BattleData.SideIndex == 0 ? 1 : 0;
var opponent = target.BattleData.Battle.Sides[opponentSide].Pokemon.FirstOrDefault(x => x is not null);
if (opponent is null)
return;
var levelDifferenceModifier = (float)target.Level / opponent.Level;
var catchModifier = levelDifferenceModifier switch
{
>= 1f => 1f,
>= 0.5f => 2f,
>= 0.25f => 4f,
_ => 8f,
};
catchRate = catchRate.MultiplyOrMax(catchModifier);
}
}

View File

@ -0,0 +1,26 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
[ItemScript("love_ball")]
public class LoveBall : PokeballScript
{
/// <inheritdoc />
public LoveBall(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
if (target.BattleData is null)
return;
var opponentSide = target.BattleData.SideIndex == 0 ? 1 : 0;
var opponent = target.BattleData.Battle.Sides[opponentSide].Pokemon.FirstOrDefault(x => x is not null);
if (opponent is null)
return;
if (opponent.Species == target.Species && opponent.Gender != target.Gender)
{
catchRate = catchRate.MultiplyOrMax(8f);
}
}
}

View File

@ -0,0 +1,32 @@
using PkmnLib.Static.Species;
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
[ItemScript("moon_ball")]
public class MoonBall : PokeballScript
{
/// <inheritdoc />
public MoonBall(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
if (target.Species.EvolutionData.Any(x =>
{
switch (x)
{
case ItemUseEvolution itemUseEvolution when itemUseEvolution.Item == "moon_ball":
case ItemGenderEvolution itemGenderEvolution when itemGenderEvolution.Item == "moon_ball":
return true;
default:
return false;
}
}))
{
// If the target can evolve with a Moon Ball, it has a 4x catch rate.
catchRate = catchRate.MultiplyOrMax(4f);
}
}
}

View File

@ -0,0 +1,19 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
[ItemScript("nest_ball")]
public class NestBall : PokeballScript
{
/// <inheritdoc />
public NestBall(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
if (target.Level >= 30)
return;
var modifier = (41 - target.Level) / 10f;
catchRate = catchRate.MultiplyOrMax(modifier);
}
}

View File

@ -0,0 +1,22 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
[ItemScript("net_ball")]
public class NetBall : PokeballScript
{
/// <inheritdoc />
public NetBall(IItem item) : base(item)
{
}
private static readonly StringKey WaterType = "water";
private static readonly StringKey BugType = "bug";
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
if (target.Types.Any(x => x.Name == WaterType || x.Name == BugType))
{
catchRate = catchRate.MultiplyOrMax(3.5f);
}
}
}

View File

@ -0,0 +1,38 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
/// <summary>
/// An implementation of a pokeball script that just has a flat catch rate bonus.
/// </summary>
[ItemScript("pokeball_flat_modifier")]
public class PokeballFlatModifier : PokeballScript
{
private float _catchModifier;
/// <inheritdoc />
public PokeballFlatModifier(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void OnInitialize(IReadOnlyDictionary<StringKey, object?>? parameters)
{
var catchModifier = 1f;
if (parameters != null && parameters.TryGetValue("catchRate", out var catchRateObj))
{
catchModifier = catchRateObj switch
{
float catchModifierFloat => catchModifierFloat,
int catchModifierInt => catchModifierInt,
_ => catchModifier,
};
}
_catchModifier = catchModifier;
}
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
catchRate = catchRate.MultiplyOrMax(_catchModifier);
}
}

View File

@ -0,0 +1,23 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
[ItemScript("quick_ball")]
public class QuickBall : PokeballScript
{
/// <inheritdoc />
public QuickBall(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
var battleData = target.BattleData;
if (battleData is null)
return;
if (battleData.Battle.CurrentTurnNumber == 1)
{
catchRate = catchRate.MultiplyOrMax(5f);
}
}
}

View File

@ -0,0 +1,22 @@
using PkmnLib.Plugin.Gen7.Libraries.Battling;
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
[ItemScript("repeat_ball")]
public class RepeatBall : PokeballScript
{
/// <inheritdoc />
public RepeatBall(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
if (target.Library.CaptureLibrary is Gen7CaptureLibrary captureLibrary &&
captureLibrary.HasPokemonBeenCaughtBefore(target.Species))
{
catchRate = catchRate.MultiplyOrMax(3.5f);
}
}
}

View File

@ -0,0 +1,24 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Items.Pokeballs;
[ItemScript("timer_ball")]
public class TimerBall : PokeballScript
{
/// <inheritdoc />
public TimerBall(IItem item) : base(item)
{
}
/// <inheritdoc />
public override void ChangeCatchRate(IPokemon target, ref byte catchRate)
{
var battleData = target.BattleData;
if (battleData is null)
return;
var turns = battleData.Battle.CurrentTurnNumber;
var modifier = 1 + turns * (1229 / 4096f);
if (modifier > 4f)
modifier = 4f;
catchRate = catchRate.MultiplyOrMax(modifier);
}
}

View File

@ -1,30 +0,0 @@
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) => _catchRate;
}