Even more moves

This commit is contained in:
Deukhoofd 2025-04-19 13:01:10 +02:00
parent c22ad1a793
commit 807acf1947
Signed by: Deukhoofd
GPG Key ID: F63E044490819F6F
19 changed files with 505 additions and 19 deletions

View File

@ -99,8 +99,39 @@ public class BattleChoiceQueue : IDeepCloneable
return true; return true;
} }
/// <summary>
/// This moves the choice of a specific Pokémon to the end of the queue, making it the last choice to be executed.
/// </summary>
/// <returns>
/// Returns true if the Pokémon was found and moved, false otherwise.
/// </returns>
public bool MovePokemonChoiceLast(IPokemon pokemon)
{
var index = Array.FindIndex(_choices, _currentIndex, choice => choice?.User == pokemon);
if (index == -1)
return false;
var choice = _choices[index];
_choices[index] = null;
// Put all choices after the index of the choice forward
for (var i = index; i < _choices.Length - 1; i++)
_choices[i] = _choices[i + 1];
// And insert the choice at the end
_choices[^1] = choice;
return true;
}
internal IReadOnlyList<ITurnChoice?> GetChoices() => _choices; internal IReadOnlyList<ITurnChoice?> GetChoices() => _choices;
public ITurnChoice? FirstOrDefault(Func<ITurnChoice, bool> predicate) => public ITurnChoice? FirstOrDefault(Func<ITurnChoice, bool> predicate) =>
_choices.WhereNotNull().FirstOrDefault(predicate); _choices.Skip(_currentIndex).WhereNotNull().FirstOrDefault(predicate);
public void Remove(ITurnChoice choice)
{
var index = Array.FindIndex(_choices, _currentIndex, x => x == choice);
if (index == -1)
return;
_choices[index] = null;
for (var i = index; i > _currentIndex; i--)
_choices[i] = _choices[i - 1];
}
} }

View File

@ -45,6 +45,8 @@ internal static class MoveTurnExecutor
var targets = var targets =
TargetResolver.ResolveTargets(battle, moveChoice.TargetSide, moveChoice.TargetPosition, targetType); TargetResolver.ResolveTargets(battle, moveChoice.TargetSide, moveChoice.TargetPosition, targetType);
moveChoice.RunScriptHook(x => x.ChangeTargets(moveChoice, ref targets)); moveChoice.RunScriptHook(x => x.ChangeTargets(moveChoice, ref targets));
var targetSide = battle.Sides[moveChoice.TargetSide];
targetSide.RunScriptHook(x => x.ChangeIncomingTargets(moveChoice, ref targets));
byte numberOfHits = 1; byte numberOfHits = 1;
moveChoice.RunScriptHook(x => x.ChangeNumberOfHits(moveChoice, ref numberOfHits)); moveChoice.RunScriptHook(x => x.ChangeNumberOfHits(moveChoice, ref numberOfHits));

View File

@ -140,6 +140,10 @@ public abstract class Script : IDeepCloneable
{ {
} }
public virtual void ChangeIncomingTargets(IMoveChoice moveChoice, ref IReadOnlyList<IPokemon?> targets)
{
}
/// <summary> /// <summary>
/// This function allows you to change a move into a multi-hit move. The number of hits set here /// This function allows you to change a move into a multi-hit move. The number of hits set here
/// gets used as the number of hits. If set to 0, this will behave as if the move missed on its /// gets used as the number of hits. If set to 0, this will behave as if the move missed on its

View File

@ -8533,7 +8533,10 @@
"contact", "contact",
"protect", "protect",
"mirror" "mirror"
] ],
"effect": {
"name": "pursuit"
}
}, },
{ {
"name": "quash", "name": "quash",
@ -8547,7 +8550,10 @@
"flags": [ "flags": [
"protect", "protect",
"mirror" "mirror"
] ],
"effect": {
"name": "quash"
}
}, },
{ {
"name": "quick_attack", "name": "quick_attack",
@ -8563,6 +8569,7 @@
"protect", "protect",
"mirror" "mirror"
] ]
// No secondary effect
}, },
{ {
"name": "quick_guard", "name": "quick_guard",
@ -8575,7 +8582,10 @@
"category": "status", "category": "status",
"flags": [ "flags": [
"snatch" "snatch"
] ],
"effect": {
"name": "quick_guard"
}
}, },
{ {
"name": "quiver_dance", "name": "quiver_dance",
@ -8589,7 +8599,15 @@
"flags": [ "flags": [
"snatch", "snatch",
"dance" "dance"
] ],
"effect": {
"name": "change_multiple_user_stat_boosts",
"parameters": {
"specialAttack": 1,
"specialDefense": 1,
"speed": 1
}
}
}, },
{ {
"name": "rage", "name": "rage",
@ -8604,7 +8622,10 @@
"contact", "contact",
"protect", "protect",
"mirror" "mirror"
] ],
"effect": {
"name": "rage"
}
}, },
{ {
"name": "rage_powder", "name": "rage_powder",
@ -8617,7 +8638,10 @@
"category": "status", "category": "status",
"flags": [ "flags": [
"powder" "powder"
] ],
"effect": {
"name": "rage_powder"
}
}, },
{ {
"name": "rain_dance", "name": "rain_dance",
@ -8628,7 +8652,10 @@
"priority": 0, "priority": 0,
"target": "All", "target": "All",
"category": "status", "category": "status",
"flags": [] "flags": [],
"effect": {
"name": "rain_dance"
}
}, },
{ {
"name": "rapid_spin", "name": "rapid_spin",
@ -8643,7 +8670,10 @@
"contact", "contact",
"protect", "protect",
"mirror" "mirror"
] ],
"effect": {
"name": "rapid_spin"
}
}, },
{ {
"name": "razor_leaf", "name": "razor_leaf",
@ -8657,7 +8687,10 @@
"flags": [ "flags": [
"protect", "protect",
"mirror" "mirror"
] ],
"effect": {
"name": "increased_critical_stage"
}
}, },
{ {
"name": "razor_shell", "name": "razor_shell",
@ -8672,7 +8705,14 @@
"contact", "contact",
"protect", "protect",
"mirror" "mirror"
] ],
"effect": {
"name": "change_target_defense",
"chance": 50,
"parameters": {
"amount": -1
}
}
}, },
{ {
"name": "razor_wind", "name": "razor_wind",
@ -8687,7 +8727,10 @@
"charge", "charge",
"protect", "protect",
"mirror" "mirror"
] ],
"effect": {
"name": "razor_wind"
}
}, },
{ {
"name": "recover", "name": "recover",
@ -8701,7 +8744,13 @@
"flags": [ "flags": [
"snatch", "snatch",
"heal" "heal"
] ],
"effect": {
"name": "heal_percent",
"parameters": {
"healPercent": 0.5
}
}
}, },
{ {
"name": "recycle", "name": "recycle",

View File

@ -0,0 +1,107 @@
using Moq;
using PkmnLib.Dynamic.Models;
using PkmnLib.Dynamic.Models.Choices;
namespace PkmnLib.Tests.Dynamic;
public class ChoiceQueueTests
{
[Test]
public async Task ChoiceQueue_MovePokemonChoiceNext()
{
var pokemon1 = new Mock<IPokemon>();
var pokemon2 = new Mock<IPokemon>();
var pokemon3 = new Mock<IPokemon>();
var pokemon4 = new Mock<IPokemon>();
var choice1 = new Mock<ITurnChoice>();
choice1.Setup(c => c.User).Returns(pokemon1.Object);
var choice2 = new Mock<ITurnChoice>();
choice2.Setup(c => c.User).Returns(pokemon2.Object);
var choice3 = new Mock<ITurnChoice>();
choice3.Setup(c => c.User).Returns(pokemon3.Object);
var choice4 = new Mock<ITurnChoice>();
choice4.Setup(c => c.User).Returns(pokemon4.Object);
var queue = new BattleChoiceQueue([choice1.Object, choice2.Object, choice3.Object, choice4.Object]);
var result = queue.MovePokemonChoiceNext(pokemon3.Object);
await Assert.That(result).IsTrue();
await Assert.That(queue.Dequeue()).IsEqualTo(choice3.Object);
}
[Test]
public async Task ChoiceQueue_MovePokemonChoiceNextFailsIfAlreadyExecuted()
{
var pokemon1 = new Mock<IPokemon>();
var pokemon2 = new Mock<IPokemon>();
var pokemon3 = new Mock<IPokemon>();
var pokemon4 = new Mock<IPokemon>();
var choice1 = new Mock<ITurnChoice>();
choice1.Setup(c => c.User).Returns(pokemon1.Object);
var choice2 = new Mock<ITurnChoice>();
choice2.Setup(c => c.User).Returns(pokemon2.Object);
var choice3 = new Mock<ITurnChoice>();
choice3.Setup(c => c.User).Returns(pokemon3.Object);
var choice4 = new Mock<ITurnChoice>();
choice4.Setup(c => c.User).Returns(pokemon4.Object);
var queue = new BattleChoiceQueue([choice1.Object, choice2.Object, choice3.Object, choice4.Object]);
queue.Dequeue();
var result = queue.MovePokemonChoiceNext(pokemon1.Object);
await Assert.That(result).IsFalse();
await Assert.That(queue.Dequeue()).IsEqualTo(choice2.Object);
}
[Test]
public async Task ChoiceQueue_MovePokemonChoiceLast()
{
var pokemon1 = new Mock<IPokemon>();
var pokemon2 = new Mock<IPokemon>();
var pokemon3 = new Mock<IPokemon>();
var pokemon4 = new Mock<IPokemon>();
var choice1 = new Mock<ITurnChoice>();
choice1.Setup(c => c.User).Returns(pokemon1.Object);
var choice2 = new Mock<ITurnChoice>();
choice2.Setup(c => c.User).Returns(pokemon2.Object);
var choice3 = new Mock<ITurnChoice>();
choice3.Setup(c => c.User).Returns(pokemon3.Object);
var choice4 = new Mock<ITurnChoice>();
choice4.Setup(c => c.User).Returns(pokemon4.Object);
var queue = new BattleChoiceQueue([choice1.Object, choice2.Object, choice3.Object, choice4.Object]);
var result = queue.MovePokemonChoiceLast(pokemon2.Object);
await Assert.That(result).IsTrue();
await Assert.That(queue.Dequeue()).IsEqualTo(choice1.Object);
await Assert.That(queue.Dequeue()).IsEqualTo(choice3.Object);
await Assert.That(queue.Dequeue()).IsEqualTo(choice4.Object);
await Assert.That(queue.Dequeue()).IsEqualTo(choice2.Object);
}
[Test]
public async Task ChoiceQueue_MovePokemonChoiceLastFailsIfAlreadyExecuted()
{
var pokemon1 = new Mock<IPokemon>();
var pokemon2 = new Mock<IPokemon>();
var pokemon3 = new Mock<IPokemon>();
var pokemon4 = new Mock<IPokemon>();
var choice1 = new Mock<ITurnChoice>();
choice1.Setup(c => c.User).Returns(pokemon1.Object);
var choice2 = new Mock<ITurnChoice>();
choice2.Setup(c => c.User).Returns(pokemon2.Object);
var choice3 = new Mock<ITurnChoice>();
choice3.Setup(c => c.User).Returns(pokemon3.Object);
var choice4 = new Mock<ITurnChoice>();
choice4.Setup(c => c.User).Returns(pokemon4.Object);
var queue = new BattleChoiceQueue([choice1.Object, choice2.Object, choice3.Object, choice4.Object]);
queue.Dequeue();
var result = queue.MovePokemonChoiceLast(pokemon1.Object);
await Assert.That(result).IsFalse();
await Assert.That(queue.Dequeue()).IsEqualTo(choice2.Object);
await Assert.That(queue.Dequeue()).IsEqualTo(choice3.Object);
await Assert.That(queue.Dequeue()).IsEqualTo(choice4.Object);
}
}

View File

@ -10,18 +10,19 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CSPath" Version="0.0.4" /> <PackageReference Include="Moq" Version="4.20.70"/>
<PackageReference Include="CSPath" Version="0.0.4"/>
<PackageReference Include="FluentAssertions" Version="6.12.0"/> <PackageReference Include="FluentAssertions" Version="6.12.0"/>
<PackageReference Include="coverlet.collector" Version="6.0.0"/> <PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="System.Linq.Async" Version="6.0.1" /> <PackageReference Include="System.Linq.Async" Version="6.0.1"/>
<PackageReference Include="TUnit" Version="0.5.18" /> <PackageReference Include="TUnit" Version="0.5.18"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PkmnLib.Dataloader\PkmnLib.Dataloader.csproj"/> <ProjectReference Include="..\PkmnLib.Dataloader\PkmnLib.Dataloader.csproj"/>
<ProjectReference Include="..\PkmnLib.Dynamic\PkmnLib.Dynamic.csproj"/> <ProjectReference Include="..\PkmnLib.Dynamic\PkmnLib.Dynamic.csproj"/>
<ProjectReference Include="..\PkmnLib.Static\PkmnLib.Static.csproj"/> <ProjectReference Include="..\PkmnLib.Static\PkmnLib.Static.csproj"/>
<ProjectReference Include="..\Plugins\PkmnLib.Plugin.Gen7\PkmnLib.Plugin.Gen7.csproj" /> <ProjectReference Include="..\Plugins\PkmnLib.Plugin.Gen7\PkmnLib.Plugin.Gen7.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -42,7 +43,7 @@
</Target> </Target>
<Target Name="Husky" BeforeTargets="Restore;CollectPackageReferences" Condition="'$(HUSKY)' != 0"> <Target Name="Husky" BeforeTargets="Restore;CollectPackageReferences" Condition="'$(HUSKY)' != 0">
<Exec Command="dotnet tool restore" StandardOutputImportance="Low" StandardErrorImportance="High" /> <Exec Command="dotnet tool restore" StandardOutputImportance="Low" StandardErrorImportance="High"/>
<Exec Command="dotnet husky install" StandardOutputImportance="Low" StandardErrorImportance="High" WorkingDirectory=".." /> <Exec Command="dotnet husky install" StandardOutputImportance="Low" StandardErrorImportance="High" WorkingDirectory=".."/>
</Target> </Target>
</Project> </Project>

View File

@ -0,0 +1,21 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "pursuit")]
public class Pursuit : Script
{
/// <inheritdoc />
public override void OnBeforeTurnStart(ITurnChoice choice)
{
if (choice is IMoveChoice moveChoice)
choice.User.Volatile.Add(new PursuitEffect(moveChoice));
}
/// <inheritdoc />
public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit) =>
move.User.Volatile.Remove<PursuitEffect>();
/// <inheritdoc />
public override void OnAfterMove(IExecutingMove move) => move.User.Volatile.Remove<PursuitEffect>();
}

View File

@ -0,0 +1,18 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "quash")]
public class Quash : Script
{
/// <inheritdoc />
public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
var battleData = move.User.BattleData;
if (battleData == null)
return;
if (battleData.Battle.ChoiceQueue?.MovePokemonChoiceLast(target) == false)
{
move.GetHitData(target, hit).Fail();
}
}
}

View File

@ -0,0 +1,13 @@
using PkmnLib.Plugin.Gen7.Scripts.Side;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "quick_guard")]
public class QuickGuard : Script
{
/// <inheritdoc />
public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
move.User.BattleData?.BattleSide.VolatileScripts.Add(new QuickGuardEffect());
}
}

View File

@ -0,0 +1,13 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "rage")]
public class Rage : Script
{
/// <inheritdoc />
public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
move.User.Volatile.Add(new RageEffect());
}
}

View File

@ -0,0 +1,18 @@
using PkmnLib.Plugin.Gen7.Scripts.Side;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "rage_powder")]
public class RagePowder : Script
{
/// <inheritdoc />
public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
var battleData = move.User.BattleData;
if (battleData == null)
return;
var effect = battleData.BattleSide.VolatileScripts.Add(new RagePowderEffect(move.User));
((RagePowderEffect)effect.Script!).User = move.User;
}
}

View File

@ -0,0 +1,16 @@
using PkmnLib.Plugin.Gen7.Scripts.Weather;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "rain_dance")]
public class RainDance : Script
{
/// <inheritdoc />
public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
var battleData = move.User.BattleData;
if (battleData == null)
return;
battleData.Battle.SetWeather(ScriptUtils.ResolveName<Rain>(), 5);
}
}

View File

@ -0,0 +1,23 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "rapid_spin")]
public class RapidSpin : Script
{
/// <inheritdoc />
public override void OnSecondaryEffect(IExecutingMove move, IPokemon target, byte hit)
{
move.User.Volatile.Remove<LeechSeedEffect>();
move.User.Volatile.Remove<BindEffect>();
move.User.Volatile.Remove<FireSpinEffect>();
move.User.Volatile.Remove<MagmaStormEffect>();
// TODO: Sand Tomb effect removal
// TODO: Whirlpool effect removal
// TODO: Wrap effect removal
// TODO: Remove Spikes
// TODO: Remove Toxic Spikes
// TODO: Remove Stealth Rock
}
}

View File

@ -0,0 +1,24 @@
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
namespace PkmnLib.Plugin.Gen7.Scripts.Moves;
[Script(ScriptCategory.Move, "razor_wind")]
public class RazorWind : Script
{
/// <inheritdoc />
public override void PreventMove(IExecutingMove move, ref bool prevent)
{
var chargeMoveEffect = move.User.Volatile.Get<ChargeMoveEffect>();
if (chargeMoveEffect != null && chargeMoveEffect.MoveName == move.UseMove.Name)
return;
prevent = true;
move.User.Volatile.Add(new ChargeMoveEffect(move.UseMove.Name, move.User, move.MoveChoice.TargetSide,
move.MoveChoice.TargetPosition));
}
/// <inheritdoc />
public override void ChangeCriticalStage(IExecutingMove move, IPokemon target, byte hit, ref byte stage)
{
stage += 1;
}
}

View File

@ -0,0 +1,27 @@
using PkmnLib.Plugin.Gen7.Scripts.Utils;
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "charge_move_effect")]
public class ChargeMoveEffect : Script
{
public readonly StringKey MoveName;
private readonly IPokemon _user;
private readonly byte _targetSide;
private readonly byte _targetPosition;
public ChargeMoveEffect(StringKey moveName, IPokemon user, byte targetSide, byte targetPosition)
{
MoveName = moveName;
_user = user;
_targetSide = targetSide;
_targetPosition = targetPosition;
}
/// <inheritdoc />
public override void ForceTurnSelection(byte sideIndex, byte position, ref ITurnChoice? choice)
{
choice = TurnChoiceHelper.CreateMoveChoice(_user, MoveName, _targetSide, _targetPosition);
}
}

View File

@ -0,0 +1,52 @@
using PkmnLib.Dynamic.Models.BattleFlow;
using PkmnLib.Static.Utils;
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "pursuit")]
public class PursuitEffect : Script
{
private readonly IMoveChoice _choice;
public PursuitEffect(IMoveChoice choice)
{
_choice = choice;
}
/// <inheritdoc />
public override void OnSwitchOut(IPokemon oldPokemon, byte position)
{
var battleData = oldPokemon.BattleData;
if (battleData == null)
return;
if (battleData.Battle.HasEnded)
return;
if (battleData.Position != _choice.TargetPosition || battleData.SideIndex != _choice.TargetSide)
return;
if (!_choice.User.IsUsable)
return;
if (_choice.User.BattleData?.IsOnBattlefield != true)
return;
var choiceQueue = battleData.Battle.ChoiceQueue;
var choice = choiceQueue?.FirstOrDefault(x => x == _choice);
if (choice == null)
return;
choiceQueue!.Remove(choice);
_choice.Volatile.Add(new PursuitDoublePowerEffect());
RemoveSelf();
TurnRunner.ExecuteChoice(battleData.Battle, _choice);
}
[Script(ScriptCategory.Pokemon, "pursuit_double_power")]
private class PursuitDoublePowerEffect : Script
{
/// <inheritdoc />
public override void ChangeBasePower(IExecutingMove move, IPokemon target, byte hit, ref byte basePower)
{
basePower = basePower.MultiplyOrMax(2);
}
}
}

View File

@ -0,0 +1,19 @@
using PkmnLib.Static;
namespace PkmnLib.Plugin.Gen7.Scripts.Pokemon;
[Script(ScriptCategory.Pokemon, "rage")]
public class RageEffect : Script
{
/// <inheritdoc />
public override void OnIncomingHit(IExecutingMove move, IPokemon target, byte hit)
{
move.User.ChangeStatBoost(Statistic.Attack, 1, true);
}
/// <inheritdoc />
public override void OnEndTurn(IBattle battle)
{
RemoveSelf();
}
}

View File

@ -0,0 +1,18 @@
namespace PkmnLib.Plugin.Gen7.Scripts.Side;
public class QuickGuardEffect : Script
{
/// <inheritdoc />
/// <inheritdoc />
public override void IsInvulnerableToMove(IExecutingMove move, IPokemon target, ref bool invulnerable)
{
if (move.UseMove.Priority > 0)
invulnerable = true;
}
/// <inheritdoc />
public override void OnEndTurn(IBattle battle)
{
RemoveSelf();
}
}

View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace PkmnLib.Plugin.Gen7.Scripts.Side;
[Script(ScriptCategory.Side, "rage_powder")]
public class RagePowderEffect : Script
{
public IPokemon User { get; set; }
public RagePowderEffect(IPokemon user)
{
User = user;
}
/// <inheritdoc />
public override void ChangeIncomingTargets(IMoveChoice moveChoice, ref IReadOnlyList<IPokemon?> targets)
{
// Ignore multi-hit moves
if (targets.Count != 1)
return;
targets = [User];
}
/// <inheritdoc />
public override void OnEndTurn(IBattle battle)
{
RemoveSelf();
}
}