using PkmnLib.Dynamic.Models; using PkmnLib.Dynamic.Models.Choices; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Plugin.Gen7.Scripts.Moves; using PkmnLib.Plugin.Gen7.Scripts.Status; using PkmnLib.Static; using PkmnLib.Static.Species; namespace PkmnLib.Plugin.Gen7.Tests.Scripts.Moves; /// /// Tests for the move script. /// Gen VII Bulbapedia behavior: "Beat Up inflicts damage on the target from the user and from each conscious /// Pokémon in the user's party that does not have a non-volatile status." Since Generation V, "the base power /// per strike is no longer 10, but instead individually based on the Attack base stats of the party Pokémon: /// BasePower = BaseAttack(PartyMember)/10 + 5". /// public class BeatUpTests { /// /// Creates a mocked party member with the given base Attack stat, usability, and optional non-volatile /// status script in its . /// private static IPokemon CreatePartyMember(ushort baseAttack = 100, bool usable = true, Script? status = null) { var pokemon = Substitute.For(); pokemon.IsUsable.Returns(usable); pokemon.StatusScript.Returns(status == null ? new ScriptContainer() : new ScriptContainer(status)); var form = Substitute.For(); form.BaseStats.Returns(new ImmutableStatisticSet(100, baseAttack, 100, 100, 100, 100)); pokemon.Form.Returns(form); return pokemon; } /// /// Creates a fully mocked test setup where the user and the given other Pokémon form a party in a battle. /// private static (BeatUp script, IMoveChoice choice, IExecutingMove move) CreateTestSetup(IPokemon user, params IPokemon?[] otherPartyMembers) { var script = new BeatUp(); var members = new List { user }; members.AddRange(otherPartyMembers); var party = Substitute.For(); party.GetEnumerator().Returns(_ => members.GetEnumerator()); var battleParty = Substitute.For(); battleParty.Party.Returns(party); var battle = Substitute.For(); battle.Parties.Returns(new[] { battleParty }); var battleData = Substitute.For(); battleData.Battle.Returns(battle); user.BattleData.Returns(battleData); var choice = Substitute.For(); choice.User.Returns(user); var move = Substitute.For(); move.User.Returns(user); return (script, choice, move); } /// /// Bulbapedia: "Beat Up inflicts damage on the target from the user and from each conscious Pokémon in the /// user's party that does not have a non-volatile status." /// With the user and two healthy allies, the move strikes three times. /// [Test] public async Task ChangeNumberOfHits_AllPartyMembersEligible_OneHitPerMember() { // Arrange var user = CreatePartyMember(); var (script, choice, _) = CreateTestSetup(user, CreatePartyMember(), CreatePartyMember()); byte numberOfHits = 1; // Act script.ChangeNumberOfHits(choice, ref numberOfHits); // Assert await Assert.That(numberOfHits).IsEqualTo((byte)3); } /// /// Bulbapedia: "Beat Up inflicts damage on the target from the user and from each conscious Pokémon in the /// user's party". /// A fainted (not conscious) party member does not contribute a strike. /// [Test] public async Task ChangeNumberOfHits_FaintedPartyMember_Excluded() { // Arrange var user = CreatePartyMember(); var (script, choice, _) = CreateTestSetup(user, CreatePartyMember(usable: false), CreatePartyMember()); byte numberOfHits = 1; // Act script.ChangeNumberOfHits(choice, ref numberOfHits); // Assert await Assert.That(numberOfHits).IsEqualTo((byte)2); } /// /// Bulbapedia: "each conscious Pokémon in the user's party that does not have a non-volatile status". /// A party member with a non-volatile status (here ) does not contribute a strike. /// [Test] public async Task ChangeNumberOfHits_StatusedPartyMember_Excluded() { // Arrange var user = CreatePartyMember(); var (script, choice, _) = CreateTestSetup(user, CreatePartyMember(status: new Burned())); byte numberOfHits = 1; // Act script.ChangeNumberOfHits(choice, ref numberOfHits); // Assert await Assert.That(numberOfHits).IsEqualTo((byte)1); } /// /// Technical test: outside of battle (no ) there are no relevant party /// members, and the number of hits falls back to a single strike. /// [Test] public async Task ChangeNumberOfHits_NoBattleData_SingleHit() { // Arrange var script = new BeatUp(); var user = Substitute.For(); user.BattleData.Returns((IPokemonBattleData?)null); var choice = Substitute.For(); choice.User.Returns(user); byte numberOfHits = 3; // Act script.ChangeNumberOfHits(choice, ref numberOfHits); // Assert await Assert.That(numberOfHits).IsEqualTo((byte)1); } /// /// Bulbapedia: "the base power per strike is no longer 10, but instead individually based on the Attack /// base stats of the party Pokémon: BasePower = BaseAttack(PartyMember)/10 + 5". /// Verifies the formula, including integer truncation of the division. /// [Test, Arguments((ushort)10, (ushort)6), Arguments((ushort)55, (ushort)10), Arguments((ushort)59, (ushort)10), Arguments((ushort)100, (ushort)15), Arguments((ushort)255, (ushort)30)] public async Task ChangeBasePower_UsesBaseAttackFormula(ushort baseAttack, ushort expectedBasePower) { // Arrange var user = CreatePartyMember(baseAttack); var (script, _, move) = CreateTestSetup(user); var target = Substitute.For(); ushort basePower = 10; // Act script.ChangeBasePower(move, target, 0, ref basePower); // Assert await Assert.That(basePower).IsEqualTo(expectedBasePower); } /// /// Bulbapedia: "the base power per strike is [...] individually based on the Attack base stats of the party /// Pokémon". The second strike uses the second eligible party member's base Attack, not the user's. /// [Test] public async Task ChangeBasePower_SecondHit_UsesSecondPartyMembersBaseAttack() { // Arrange var user = CreatePartyMember(100); var (script, _, move) = CreateTestSetup(user, CreatePartyMember(250)); var target = Substitute.For(); ushort basePower = 10; // Act script.ChangeBasePower(move, target, 1, ref basePower); // Assert - 250 / 10 + 5 = 30 await Assert.That(basePower).IsEqualTo((ushort)30); } /// /// Bulbapedia: strikes only come from party members "that [do] not have a non-volatile status". /// An ineligible member is skipped entirely, so the strike after the user's uses the next eligible /// member's base Attack. /// [Test] public async Task ChangeBasePower_StatusedMemberSkipped_UsesNextEligibleMember() { // Arrange var user = CreatePartyMember(100); var (script, _, move) = CreateTestSetup(user, CreatePartyMember(250, status: new Burned()), CreatePartyMember(60)); var target = Substitute.For(); ushort basePower = 10; // Act script.ChangeBasePower(move, target, 1, ref basePower); // Assert - the burned member is skipped: 60 / 10 + 5 = 11 await Assert.That(basePower).IsEqualTo((ushort)11); } /// /// Technical test: a hit index beyond the number of eligible party members leaves the base power unchanged /// instead of throwing. /// [Test] public async Task ChangeBasePower_HitIndexBeyondEligibleMembers_BasePowerUnchanged() { // Arrange var user = CreatePartyMember(100); var (script, _, move) = CreateTestSetup(user); var target = Substitute.For(); ushort basePower = 10; // Act script.ChangeBasePower(move, target, 3, ref basePower); // Assert await Assert.That(basePower).IsEqualTo((ushort)10); } }