diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java index 556c93d..0cb876c 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java @@ -233,7 +233,8 @@ private int getItemIdFromContainer(ItemContainer container, int slotID) return (item != null) ? item.getId() : -1; } - private int getWeaponId() { + private int getWeaponId() + { int weaponId = getItemIdFromContainer( client.getItemContainer(InventoryID.EQUIPMENT), EquipmentInventorySlot.WEAPON.getSlotIdx() diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomeTileOverlay.java b/src/main/java/com/attacktimer/AttackTimerMetronomeTileOverlay.java index 56a7d78..fd20881 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomeTileOverlay.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomeTileOverlay.java @@ -37,11 +37,8 @@ import net.runelite.client.ui.overlay.OverlayPosition; import net.runelite.client.ui.overlay.OverlayUtil; import javax.inject.Inject; -import java.awt.BasicStroke; -import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; -import java.awt.Polygon; import java.awt.Font; import net.runelite.client.ui.overlay.OverlayLayer; import net.runelite.client.ui.overlay.OverlayPriority; diff --git a/src/main/java/com/attacktimer/VariableSpeed/TormentedDemons.java b/src/main/java/com/attacktimer/VariableSpeed/TormentedDemons.java index 739852b..749092c 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/TormentedDemons.java +++ b/src/main/java/com/attacktimer/VariableSpeed/TormentedDemons.java @@ -1,7 +1,7 @@ package com.attacktimer.VariableSpeed; /* - * Copyright (c) 2024, Lexer747 + * Copyright (c) 2024-2026, Lexer747 * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -36,7 +36,6 @@ import com.attacktimer.ClientUtils.Utils; import net.runelite.api.Client; import net.runelite.api.NPC; -import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.GameTick; /** @@ -59,10 +58,8 @@ public class TormentedDemons implements IVariableSpeed { public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkProcedure, final int baseSpeed, final int curSpeed) { - WorldPoint location = Utils.getLocation(client); - int weaponId = Utils.getWeaponId(client); int targetId = Utils.getTargetId(client); - if (!attackingTormentedDemon(weaponId, location.getRegionID(), location.getX(), location.getY(), targetId)) + if (!isTormentedDemon(targetId)) { return curSpeed; } @@ -120,22 +117,6 @@ public int apply(final Client client, final AnimationData curAnimation, final At private static final int TORMENTED_DEMON_ID = 13600; private static final int TORMENTED_DEMON_2_ID = 13599; - private static final int TORMENTED_DEMON_REGION_ID = 16197; - - private static final int TORMENTED_DEMON_REGION_2_ID = 16452; - private static final int TORMENTED_DEMON_MIN_X = 4010; - - private static final int TORMENTED_DEMON_MAX_X = 4180; - private static final int TORMENTED_DEMON_MIN_Y = 4320; - - private static final int TORMENTED_DEMON_MAX_Y = 4490; - - private static boolean attackingTormentedDemon(int equipped, int regionId, int x, int y, int target) - { - boolean correctCoords = x >= TORMENTED_DEMON_MIN_X && x <= TORMENTED_DEMON_MAX_X && y >= TORMENTED_DEMON_MIN_Y && y <= TORMENTED_DEMON_MAX_Y; - boolean correctRegion = regionId == TORMENTED_DEMON_REGION_ID || regionId == TORMENTED_DEMON_REGION_2_ID; - return correctCoords && correctRegion && isTormentedDemon(target); - } private static boolean isTormentedDemon(int targetId) { @@ -245,25 +226,32 @@ boolean isStale(int tick) return false; } + + // vulnConsumed returns true if the demon was already attack, false if not and the vuln is still usable boolean vulnConsumed(int tick) { if (this.attacked == tick || this.attacked == -1) { - this.attacked = tick; + this.consumeVuln(tick); return false; } else if (this.attacked > tick + VulTicksAfterEnd) { - // already attacked within the window + // already attacked within the window don't consume the vuln again we let update handle this return true; } else { - this.attacked = tick; + this.consumeVuln(tick); return false; } } + void consumeVuln(int tick) + { + this.attacked = tick; + } + @Override public String toString() { diff --git a/src/test/java/com/attacktimer/BasicTests.java b/src/test/java/com/attacktimer/BasicTests.java new file mode 100644 index 0000000..3fbdbf3 --- /dev/null +++ b/src/test/java/com/attacktimer/BasicTests.java @@ -0,0 +1,183 @@ +package com.attacktimer; + +/* + * Copyright (c) 2026, Lexer747 + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import static org.mockito.Mockito.when; + +import java.nio.file.Paths; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.attacktimer.AttackTimerMetronomePlugin.AttackState; + +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; + +import net.runelite.api.ChatMessageType; +import net.runelite.api.Player; +import net.runelite.api.events.ChatMessage; + +public class BasicTests extends IntegrationTests +{ + @Test + public void basicTest() throws Exception + { + ByteArrayDataOutput channel = ByteStreams.newDataOutput(); + underTest.writeState(channel); + // Trivial Pre-conditions: + assertSame(AttackState.NOT_ATTACKING, underTest.attackState); + + // Basic test case: + // 1. Start by setting up the player + // 2. Mock an attack animation + // 3. Check that the plugin has registered the attack + // 4. Check that the plugin counts down correctly + // 5. Check that the plugin is back to a waiting state and it still counts down + + writeTestMessage("1. Start by setting up the player and plugin", channel); + int atkSpeed = 3; // no weapon equipped (4 ticks, plugin starts counting from 3) + int tick = 0; + Player mockedPlayer = pluginMockSetup(); + when(mockedClient.getTickCount()).thenReturn(tick); + + writeTestMessage("2. Mock an attack animation", channel); + + // set the animation to an attack + when(mockedPlayer.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); + // tell the plugin that a tick occurred + onGameTick(channel); + + writeTestMessage("3. Check that the plugin has registered the attack", channel); + assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); + assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); + + // clear the animation + when(mockedPlayer.getAnimation()).thenReturn(noAnimation); + + writeTestMessage("4. Check that the plugin counts down correctly", channel); + while (atkSpeed > 0) + { + tick++; + atkSpeed--; + when(mockedClient.getTickCount()).thenReturn(tick); + onGameTick(channel); + assertSame(AttackState.DELAYED, underTest.attackState); + assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); + } + + writeTestMessage("5. Check that the plugin is back to a waiting state and it still counts down", channel); + tick++; + onGameTick(channel); + assertSame(AttackState.NOT_ATTACKING, underTest.attackState); + while (tick < 30) + { + tick++; + when(mockedClient.getTickCount()).thenReturn(tick); + onGameTick(channel); + assertSame(AttackState.NOT_ATTACKING, underTest.attackState); + assertTrue(underTest.attackDelayHoldoffTicks < 0); // hold off should go negative + } + + performStateVerificationOrUpdate(channel, Paths.get(testdata + "basicTest.txt")); + } + + @Test + public void eatingFoodTest() throws Exception + { + ByteArrayDataOutput channel = ByteStreams.newDataOutput(); + underTest.writeState(channel); + // Trivial Pre-conditions: + assertSame(AttackState.NOT_ATTACKING, underTest.attackState); + writeTestMessage("1. Start by setting up the player and plugin", channel); + int atkSpeed = 3; // no weapon equipped (4 ticks, plugin starts counting from 3) + int tick = 0; + Player mockedPlayer = pluginMockSetup(); + when(mockedClient.getTickCount()).thenReturn(tick); + + writeTestMessage("2. Mock an attack animation", channel); + + // set the animation to an attack + when(mockedPlayer.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); + // tell the plugin that a tick occurred + onGameTick(channel); + + writeTestMessage("3. Check that the plugin has registered the attack", channel); + assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); + assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); + + when(mockedPlayer.getAnimation()).thenReturn(noAnimation); + underTest.writeState(channel); + + writeTestMessage("Perform an eat", channel); + ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You eat the shark.", "", 0); + underTest.onChatMessage(chatMessage); + // the plugin wont apply the delay directly it waits until the next game tick + assertSame(3, underTest.pendingEatDelayTicks); + // Should still be same state + assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); + underTest.writeState(channel); + + // Subsequent values should be offset by the eat delay + writeTestMessage("Next game tick", channel); + atkSpeed += 2; + onGameTick(channel); + assertSame(AttackState.DELAYED, underTest.attackState); + assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); + + // Note that this is essentially a "double" eat test. + writeTestMessage("Perform a fast eat", channel); + chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You eat the halibut.", "", 0); + underTest.onChatMessage(chatMessage); + // the plugin wont apply the delay directly it waits until the next game tick + assertSame(2, underTest.pendingEatDelayTicks); + // Should still be same state + assertSame(AttackState.DELAYED, underTest.attackState); + underTest.writeState(channel); + + // Subsequent values should be offset by the eat delay + writeTestMessage("Next game tick", channel); + atkSpeed += 1; + onGameTick(channel); + assertSame(AttackState.DELAYED, underTest.attackState); + assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); + + writeTestMessage("4. Check that the plugin counts down correctly", channel); + while (atkSpeed > 0) + { + tick++; + atkSpeed--; + when(mockedClient.getTickCount()).thenReturn(tick); + onGameTick(channel); + assertSame(AttackState.DELAYED, underTest.attackState); + assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); + } + + performStateVerificationOrUpdate(channel, Paths.get(testdata + "eatingFoodTest.txt")); + } +} diff --git a/src/test/java/com/attacktimer/IntegrationTests.java b/src/test/java/com/attacktimer/IntegrationTests.java index 49e2d21..9621845 100644 --- a/src/test/java/com/attacktimer/IntegrationTests.java +++ b/src/test/java/com/attacktimer/IntegrationTests.java @@ -33,14 +33,11 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.Collections; import java.util.EnumSet; import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.junit.Before; @@ -49,15 +46,12 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import com.attacktimer.AttackTimerMetronomePlugin.AttackState; import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.testing.fieldbinder.Bind; import com.google.inject.testing.fieldbinder.BoundFieldModule; -import net.runelite.api.ChatMessageType; import net.runelite.api.Client; import net.runelite.api.EnumComposition; import net.runelite.api.EnumID; @@ -69,7 +63,6 @@ import net.runelite.api.WorldView; import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.ChatMessage; import net.runelite.api.events.GameTick; import net.runelite.client.config.ConfigManager; import net.runelite.client.game.ItemManager; @@ -126,6 +119,7 @@ public Player pluginMockSetup() throws Exception when(mockedConfig.enableMetronome()).thenReturn(true); // Create player Player mockedPlayer = mock(Player.class); + when(mockedPlayer.getAnimation()).thenReturn(noAnimation); // Create the enemy NPC mockedTarget = mock(NPC.class); @@ -171,142 +165,6 @@ public Player pluginMockSetup() throws Exception return mockedPlayer; } - @Test - public void basicTest() throws Exception - { - ByteArrayDataOutput channel = ByteStreams.newDataOutput(); - underTest.writeState(channel); - // Trivial Pre-conditions: - assertSame(AttackState.NOT_ATTACKING, underTest.attackState); - - // Basic test case: - // 1. Start by setting up the player - // 2. Mock an attack animation - // 3. Check that the plugin has registered the attack - // 4. Check that the plugin counts down correctly - // 5. Check that the plugin is back to a waiting state and it still counts down - - writeTestMessage("1. Start by setting up the player and plugin", channel); - int atkSpeed = 3; // no weapon equipped (4 ticks, plugin starts counting from 3) - int tick = 0; - Player mockedPlayer = pluginMockSetup(); - when(mockedClient.getTickCount()).thenReturn(tick); - - writeTestMessage("2. Mock an attack animation", channel); - - // set the animation to an attack - when(mockedPlayer.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); - // tell the plugin that a tick occurred - onGameTick(channel); - - writeTestMessage("3. Check that the plugin has registered the attack", channel); - assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); - assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); - - // clear the animation - when(mockedPlayer.getAnimation()).thenReturn(noAnimation); - - writeTestMessage("4. Check that the plugin counts down correctly", channel); - while (atkSpeed > 0) - { - tick++; - atkSpeed--; - when(mockedClient.getTickCount()).thenReturn(tick); - onGameTick(channel); - assertSame(AttackState.DELAYED, underTest.attackState); - assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); - } - - writeTestMessage("5. Check that the plugin is back to a waiting state and it still counts down", channel); - tick++; - onGameTick(channel); - assertSame(AttackState.NOT_ATTACKING, underTest.attackState); - while (tick < 30) - { - tick++; - when(mockedClient.getTickCount()).thenReturn(tick); - onGameTick(channel); - assertSame(AttackState.NOT_ATTACKING, underTest.attackState); - assertTrue(underTest.attackDelayHoldoffTicks < 0); // hold off should go negative - } - - performStateVerificationOrUpdate(channel, Paths.get(testdata + "basicTest.txt")); - } - - @Test - public void eatingFoodTest() throws Exception - { - ByteArrayDataOutput channel = ByteStreams.newDataOutput(); - underTest.writeState(channel); - // Trivial Pre-conditions: - assertSame(AttackState.NOT_ATTACKING, underTest.attackState); - writeTestMessage("1. Start by setting up the player and plugin", channel); - int atkSpeed = 3; // no weapon equipped (4 ticks, plugin starts counting from 3) - int tick = 0; - Player mockedPlayer = pluginMockSetup(); - when(mockedClient.getTickCount()).thenReturn(tick); - - writeTestMessage("2. Mock an attack animation", channel); - - // set the animation to an attack - when(mockedPlayer.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); - // tell the plugin that a tick occurred - onGameTick(channel); - - writeTestMessage("3. Check that the plugin has registered the attack", channel); - assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); - assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); - - when(mockedPlayer.getAnimation()).thenReturn(noAnimation); - underTest.writeState(channel); - - writeTestMessage("Perform an eat", channel); - ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You eat the shark.", "", 0); - underTest.onChatMessage(chatMessage); - // the plugin wont apply the delay directly it waits until the next game tick - assertSame(3, underTest.pendingEatDelayTicks); - // Should still be same state - assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); - underTest.writeState(channel); - - // Subsequent values should be offset by the eat delay - writeTestMessage("Next game tick", channel); - atkSpeed += 2; - onGameTick(channel); - assertSame(AttackState.DELAYED, underTest.attackState); - assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); - - // Note that this is essentially a "double" eat test. - writeTestMessage("Perform a fast eat", channel); - chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You eat the halibut.", "", 0); - underTest.onChatMessage(chatMessage); - // the plugin wont apply the delay directly it waits until the next game tick - assertSame(2, underTest.pendingEatDelayTicks); - // Should still be same state - assertSame(AttackState.DELAYED, underTest.attackState); - underTest.writeState(channel); - - // Subsequent values should be offset by the eat delay - writeTestMessage("Next game tick", channel); - atkSpeed += 1; - onGameTick(channel); - assertSame(AttackState.DELAYED, underTest.attackState); - assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); - - writeTestMessage("4. Check that the plugin counts down correctly", channel); - while (atkSpeed > 0) - { - tick++; - atkSpeed--; - when(mockedClient.getTickCount()).thenReturn(tick); - onGameTick(channel); - assertSame(AttackState.DELAYED, underTest.attackState); - assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); - } - - performStateVerificationOrUpdate(channel, Paths.get(testdata + "eatingFoodTest.txt")); - } - protected void performStateVerificationOrUpdate(ByteArrayDataOutput channel, Path path) throws IOException { var actualBytes = channel.toByteArray(); @@ -343,9 +201,18 @@ protected void writeTestMessage(String message, ByteArrayDataOutput file) file.write(testMessageSuffix); } - private static final int noAnimation = -1; - private static final String testdata = "src/test/java/com/attacktimer/testdata/"; + protected static final int noAnimation = -1; + protected static final String testdata = "src/test/java/com/attacktimer/testdata/"; private static final byte[] testMessagePrefix = "[TEST MESSAGE] ".getBytes(StandardCharsets.UTF_8); private static final byte[] testMessageSuffix = "\n".getBytes(StandardCharsets.UTF_8); + + // This needs at least one public test to keep mockito happy but having real tests in this file would + // result in any future test which extends this test class also having to run and make that test pass. + // + // This does cause test inflation in that the summary will make it look like we have more tests than we + // really do, but I don't have a nicer way to not repeat all the injector boiler plate. + @Test + public void noTest() + {} } \ No newline at end of file diff --git a/src/test/java/com/attacktimer/TormentedDemonsTest.java b/src/test/java/com/attacktimer/TormentedDemonsTest.java new file mode 100644 index 0000000..56a1403 --- /dev/null +++ b/src/test/java/com/attacktimer/TormentedDemonsTest.java @@ -0,0 +1,181 @@ +package com.attacktimer; + +/* + * Copyright (c) 2026, Lexer747 + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.EnumSet; + +import static org.junit.Assert.assertSame; +import org.junit.Test; + +import com.attacktimer.AttackTimerMetronomePlugin.AttackState; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; + +import net.runelite.api.EnumComposition; +import net.runelite.api.EnumID; +import net.runelite.api.IndexedObjectSet; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.Player; +import net.runelite.api.VarPlayer; +import net.runelite.api.Varbits; +import net.runelite.api.WorldType; +import net.runelite.api.WorldView; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.game.ItemEquipmentStats; +import net.runelite.client.game.ItemStats; + +public class TormentedDemonsTest extends IntegrationTests +{ + @Test + public void PunishTest() throws Exception + { + // Equip a weapon which is normally MORE than 4 ticks and correct attack style + runTest("PunishTest", 8, 0, 0, 3); + } + + @Test + public void PunishWastedTest() throws Exception + { + runTest("PunishWastedTest", 4, 0, 0, 3); + } + + @Test + public void PunishWastedWrongStyleTest() throws Exception + { + runTest("PunishWastedWrongStyleTest", 8, 0, 8, 7); + } + + private void runTest(String testName, int aspeed, int EQUIPPED_WEAPON_TYPE, int ATTACK_STYLE, int expected) + throws Exception + { + when(mockedItemManager.getItemStats(-1)) + .thenReturn(new ItemStats(true, 0, 0, ItemEquipmentStats.builder().aspeed(aspeed).build())); + // Use weapon style specified by the test + when(mockedClient.getVarbitValue(Varbits.EQUIPPED_WEAPON_TYPE)).thenReturn(EQUIPPED_WEAPON_TYPE); + when(mockedClient.getVarpValue(VarPlayer.ATTACK_STYLE)).thenReturn(ATTACK_STYLE); + // -- Attack Styles + EnumComposition mockedWeaponEnum = mock(EnumComposition.class); + when(mockedClient.getEnum(EnumID.WEAPON_STYLES)).thenReturn(mockedWeaponEnum); + when(mockedWeaponEnum.getIntValue(0)).thenReturn(-1); + + Player player = pluginMockSetup(); + + ByteArrayDataOutput channel = ByteStreams.newDataOutput(); + underTest.writeState(channel); + + // Trigger the td spot animation so the next punish attack should be 4 ticks + when(td.hasSpotAnim(2852)).thenReturn(true); + // Tell the client a tick has occurred + onGameTick(channel); + assertSame(AttackState.NOT_ATTACKING, underTest.attackState); + // reset the animation (it does last longer than a tick but a tick should be all the client needs) + when(td.hasSpotAnim(2852)).thenReturn(false); + // finally "do an attack" and see if the plugin correctly noticed that this is a punish + when(player.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); + onGameTick(channel); + + assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); + assertSame(expected, underTest.attackDelayHoldoffTicks); + + performStateVerificationOrUpdate(channel, Paths.get(testdata + testName + ".txt")); + } + + @Override + public Player pluginMockSetup() throws Exception + { + // enable the plugin + when(mockedConfig.enableMetronome()).thenReturn(true); + // Create player + Player mockedPlayer = mock(Player.class); + when(mockedPlayer.getAnimation()).thenReturn(-1); + + // Create the tormented demon + td = mock(NPC.class); + NPCComposition mockedCompositions = mock(NPCComposition.class); + when(td.getComposition()).thenReturn(mockedCompositions); + int mockedNpcId = 13600; + when(td.getId()).thenReturn(mockedNpcId); + String[] actions = { + "Attack", "Examine" + }; + when(mockedCompositions.getActions()).thenReturn(actions); + when(mockedNpcManager.getHealth(mockedNpcId)).thenReturn(1); + + // set the player as "attacking" the NPC + when(mockedClient.getLocalPlayer()).thenReturn(mockedPlayer); + when(mockedPlayer.getInteracting()).thenReturn(td); + + // need some extra mocks to stop the plugin running into an exception on the + // client APIs + // -- Mock World + mockedWorldView = mock(WorldView.class); + when(mockedClient.getTopLevelWorldView()).thenReturn(mockedWorldView); + when(mockedClient.getWorldView(0)).thenReturn(mockedWorldView); + int mockedPlane = 0; + when(mockedWorldView.getPlane()).thenReturn(mockedPlane); + WorldPoint worldPoint = new WorldPoint(0, 0, mockedPlane); + LocalPoint localPoint = new LocalPoint(0, 0, mockedPlane); + when(mockedPlayer.getWorldLocation()).thenReturn(worldPoint); + when(mockedPlayer.getLocalLocation()).thenReturn(localPoint); + var memsWorld = EnumSet.of(WorldType.MEMBERS); + when(mockedClient.getWorldType()).thenReturn(memsWorld); + // -- NPCs + worldViewNPCiter(td); + + // Finally turn the plugin "on" + underTest.startUp(); + return mockedPlayer; + } + + protected WorldView mockedWorldView; + protected NPC td; + + private void worldViewNPCiter(NPC mockedTarget) + { + // do a bit of redirection trickery to get a IndexedObjectSet of a non empty list. + var npcs = new ArrayList(1); + npcs.add(mockedTarget); + IndexedObjectSet npcsType = mock(IndexedObjectSet.class); + when(npcsType.iterator()).thenReturn(npcs.iterator()); + when(mockedWorldView.npcs()).thenReturn(npcsType); + } + + @Override + protected void onGameTick(ByteArrayDataOutput file) + { + super.onGameTick(file); + // Since an iterator is stateful and consumed by the plugin we re-mock it each time so it's always + // fresh. + worldViewNPCiter(td); + } +} diff --git a/src/test/java/com/attacktimer/testdata/PunishTest.txt b/src/test/java/com/attacktimer/testdata/PunishTest.txt new file mode 100644 index 0000000..dda34c4 --- /dev/null +++ b/src/test/java/com/attacktimer/testdata/PunishTest.txt @@ -0,0 +1,3 @@ +tickPeriod: 0, uiUnshowDebounceTickCount: 0, attackDelayHoldoffTicks: 0, attackState: NOT_ATTACKING, renderedState: NOT_ATTACKING, pendingEatDelayTicks: 0, currentSpellBook: STANDARD, lastEquippingMonotonicValue: -1, soundEffectTick: -1, soundEffectId: -1 +tickPeriod: 0, uiUnshowDebounceTickCount: -1, attackDelayHoldoffTicks: -1, attackState: NOT_ATTACKING, renderedState: NOT_ATTACKING, pendingEatDelayTicks: 0, currentSpellBook: STANDARD, lastEquippingMonotonicValue: -1, soundEffectTick: -1, soundEffectId: -1 +tickPeriod: 4, uiUnshowDebounceTickCount: 1, attackDelayHoldoffTicks: 3, attackState: DELAYED_FIRST_TICK, renderedState: NOT_ATTACKING, pendingEatDelayTicks: 0, currentSpellBook: STANDARD, lastEquippingMonotonicValue: -1, soundEffectTick: -1, soundEffectId: -1 diff --git a/src/test/java/com/attacktimer/testdata/PunishWastedTest.txt b/src/test/java/com/attacktimer/testdata/PunishWastedTest.txt new file mode 100644 index 0000000..dda34c4 --- /dev/null +++ b/src/test/java/com/attacktimer/testdata/PunishWastedTest.txt @@ -0,0 +1,3 @@ +tickPeriod: 0, uiUnshowDebounceTickCount: 0, attackDelayHoldoffTicks: 0, attackState: NOT_ATTACKING, renderedState: NOT_ATTACKING, pendingEatDelayTicks: 0, currentSpellBook: STANDARD, lastEquippingMonotonicValue: -1, soundEffectTick: -1, soundEffectId: -1 +tickPeriod: 0, uiUnshowDebounceTickCount: -1, attackDelayHoldoffTicks: -1, attackState: NOT_ATTACKING, renderedState: NOT_ATTACKING, pendingEatDelayTicks: 0, currentSpellBook: STANDARD, lastEquippingMonotonicValue: -1, soundEffectTick: -1, soundEffectId: -1 +tickPeriod: 4, uiUnshowDebounceTickCount: 1, attackDelayHoldoffTicks: 3, attackState: DELAYED_FIRST_TICK, renderedState: NOT_ATTACKING, pendingEatDelayTicks: 0, currentSpellBook: STANDARD, lastEquippingMonotonicValue: -1, soundEffectTick: -1, soundEffectId: -1 diff --git a/src/test/java/com/attacktimer/testdata/PunishWastedWrongStyleTest.txt b/src/test/java/com/attacktimer/testdata/PunishWastedWrongStyleTest.txt new file mode 100644 index 0000000..4606678 --- /dev/null +++ b/src/test/java/com/attacktimer/testdata/PunishWastedWrongStyleTest.txt @@ -0,0 +1,3 @@ +tickPeriod: 0, uiUnshowDebounceTickCount: 0, attackDelayHoldoffTicks: 0, attackState: NOT_ATTACKING, renderedState: NOT_ATTACKING, pendingEatDelayTicks: 0, currentSpellBook: STANDARD, lastEquippingMonotonicValue: -1, soundEffectTick: -1, soundEffectId: -1 +tickPeriod: 0, uiUnshowDebounceTickCount: -1, attackDelayHoldoffTicks: -1, attackState: NOT_ATTACKING, renderedState: NOT_ATTACKING, pendingEatDelayTicks: 0, currentSpellBook: STANDARD, lastEquippingMonotonicValue: -1, soundEffectTick: -1, soundEffectId: -1 +tickPeriod: 8, uiUnshowDebounceTickCount: 1, attackDelayHoldoffTicks: 7, attackState: DELAYED_FIRST_TICK, renderedState: NOT_ATTACKING, pendingEatDelayTicks: 0, currentSpellBook: STANDARD, lastEquippingMonotonicValue: -1, soundEffectTick: -1, soundEffectId: -1